Skip to content

feat(solidjs-vite-plugin): read lazily loaded plugin and ternary paths - #2147

Closed
everton-dgn wants to merge 10 commits into
webpro-nl:mainfrom
everton-dgn:feat/solid-vite-plugin-lazy-paths
Closed

everton-dgn wants to merge 10 commits into
webpro-nl:mainfrom
everton-dgn:feat/solid-vite-plugin-lazy-paths

Conversation

@everton-dgn

@everton-dgn everton-dgn commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #2144, which only reads calls to a statically imported @solidjs/vite-plugin. Configs that load their Vite plugins with import() get no entries yet:

// vite.config.ts
const loadPlugins = async () => ({ solid: (await import('@solidjs/vite-plugin')).default });

export default async ({ mode }) => {
  const modules = await loadPlugins();
  return {
    plugins: [
      modules.solid({
        ssr: true,
        start: { middleware: mode === 'e2e' ? './src/e2e-middleware.ts' : './src/middleware.ts' },
      }),
    ],
  };
};

What changes:

  • when the config contains import('@solidjs/vite-plugin'), the first argument of every call in the config is read, since the plugin binding can't be traced through the lazy module
  • a ternary with string literals adds both paths, and a branch without a path (undefined, true) keeps the convention; for env, only false drops it

Without that dynamic import, the calls that get read stay the same: only direct calls to the statically imported binding. A plugin imported through a local module is not followed, and a timing({ start }) call next to it is not read; the -spa fixture covers that and fails without the gate. The ternary handling applies there too. In a lazy config, another call with a start or serverFunctions object would be read too, and options passed as a variable (solid(options)) are not read.

I ran this on a Solid 2 app that loads its Vite plugins like the config above and listed six of these files as entries by hand. Without those entries, Knip 6.41.0 reported 5 unused files and 1 unused export on top of the baseline. With this change the report matches the baseline.

Mark the files @solidjs/vite-plugin loads in start mode as production
entries: src/entry-server.*, src/entry-client.*, src/App.*,
src/Document.*, env.{ts,js} and the module paths in the start options.

Resolve start entries from the Vite root, accounting for SSR and client
mode. Also include serverFunctions.configure with or without start.
Keep start mode conventions, literal start.* paths, serverFunctions.configure
and the Vite root for configs that import the plugin statically.

Lazily loaded plugins, ternary paths and the SSR and client mode rules move
to follow-up pull requests. The tests are now one table over smaller
fixtures without node_modules stubs.
@pkg-pr-new

pkg-pr-new Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/knip@2147
npm i https://pkg.pr.new/@knip/language-server@2147
npm i https://pkg.pr.new/@knip/mcp@2147

commit: 342acf8

…y-paths

# Conflicts:
#	packages/knip/fixtures/plugins/solidjs-vite-plugin-entries/vite.config.ts
#	packages/knip/src/plugins/solidjs-vite-plugin/resolveFromAST.ts
#	packages/knip/test/plugins/solidjs-vite-plugin.test.ts

@webpro webpro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for splitting this out. The failed Node 20 workflow is a runner crash: shard 3/3 passed 460/460 tests and the later knip --production --strict step exited with -1073741502, after which fail-fast cancelled the other jobs. #2148 is green on the same workflow.

return false;
};

const getPluginCalls = (program: Program) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading every call in the config when the plugin is not statically imported means any call whose first argument has a start property is read as the Solid plugin call, as the description notes. If we keep it, could it only kick in when the config contains import('@solidjs/vite-plugin')? That covers the lazy pattern from the description and keeps other configs on the static path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: the fallback now only applies when the config contains import('@solidjs/vite-plugin'), detected with an ImportExpression visitor. Without it, findImportedCalls stays the only path. The type-only import check is gone, since a type import never produces a callable binding.

…gin-lazy-paths

# Conflicts:
#	packages/knip/fixtures/plugins/solidjs-vite-plugin-spa/vite.config.ts
#	packages/knip/fixtures/plugins/solidjs-vite-plugin/vite.config.ts
#	packages/knip/src/plugins/solidjs-vite-plugin/resolveFromAST.ts
#	packages/knip/test/plugins/solidjs-vite-plugin.test.ts
@everton-dgn
everton-dgn marked this pull request as ready for review October 14, 2026 22:53
@webpro

webpro commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Thanks for all the work. Really appreciate all your contributions.

Upon closer look, are you sure this is an actual use case? Dynamic imports are not that common? In general, this is the downside of resolveFromAST: we shouldn't be chasing too many dynamic code patterns. Such as dynamic imports and spreads (like ...clientOnly in #2148 handles).

So resolveFromAST is fast, but once it gets complicated we should switch to resolveConfig to import the config object and read values. And resolveFromAST is still available for the remaining missing values in that config object file.

That's why I see two paths forward, please pick one:

  1. Close this PR and keep a simple version of feat(solidjs-vite-plugin): follow Solid's SSR and client mode entry rules #2148 with resolveFromAST (and accept that dynamic imports are not supported the way you intended here).
  2. Rework the plugin to use resolveConfig for as much as possible; and potentially add resolveFromAST for missing/unreachable values.

Let me know if you think one of the options is feasible. And sometimes the additional complexity just isn't worth it to get rid of one false positive.

@everton-dgn

Copy link
Copy Markdown
Contributor Author

Thanks! It came from a real app, but I agree it isn't common enough to chase in resolveFromAST. Option 2 doesn't look feasible either: solid() returns plain Vite plugin objects and keeps its options in closures, so the loaded config doesn't expose start or serverFunctions to resolveConfig.

So going with option 1 and closing this one. #2148 is updated on top of main without the lazy import, ternary and spread handling. Configs like the one above can list their entries in the Knip config.

@webpro

webpro commented Oct 15, 2026

Copy link
Copy Markdown
Member

Thank you for thinking along with me, excellent stuff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants