diff --git a/.changeset/ssr-inline-solid-consumers.md b/.changeset/ssr-inline-solid-consumers.md new file mode 100644 index 0000000..5e3b002 --- /dev/null +++ b/.changeset/ssr-inline-solid-consumers.md @@ -0,0 +1,5 @@ +--- +'@solidjs/vite-plugin': patch +--- + +Packages that consume the Solid runtime without declaring a `solid` export condition are now inlined in dev server environments too, closing the remaining half of the two-instance split. Inlining `solid-js` and `@solidjs/web` fixes every resolution those two perform, and vitefu inlines packages that advertise a `solid` export condition — but a package that does neither is still externalized, and Node resolves its own `import "solid-js"` without the `development` condition, so it loads the production server build while the inlined graph holds the dev one. `@solidjs/meta` is the first-party example: it has no `solid` condition, so under solid-js 2.0.0-rc.7 an app rendering a `` still died in `useContext` on a second `sharedConfig` even with the core packages inlined. The crawl now also classifies any package declaring `solid-js` or `@solidjs/web` in its `dependencies` or `peerDependencies` as a semi-framework package — `ssr.noExternal` without `optimizeDeps.exclude`, since these hold no raw Solid components — so third-party component libraries and metadata helpers reach the same copy as everything else. Gated on the dev-condition swap (and off under vitest, which manages inlining itself), leaving builds unchanged. Two guards keep the rule narrow: tooling that declares `solid-js` as a peer but never runs inside the SSR module runner — `@solidjs/vite-plugin` itself, `vite`, `vitest`, `eslint-plugin-*`, `vite-plugin-*`, `prettier-plugin-*`, `@types/*` — is skipped entirely (classifying the plugin would also crawl its dependencies and pre-bundle `@babel/core` and `@solidjs/babel-plugin` into the browser's `optimizeDeps`, several megabytes of dead weight per cold start); and the `ssr.external` list vitefu derives from framework packages' non-framework `dependencies` is filtered against the final `noExternal` list, because Vite gives `external` precedence — a framework package listing `@solidjs/web` under `dependencies` (e.g. `@tanstack/solid-router`) would otherwise re-externalize a core the plugin just inlined and split the runtime again. diff --git a/src/index.ts b/src/index.ts index 37eae3d..5e8933f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -442,6 +442,32 @@ function getExtension(filename: string): string { const index = filename.lastIndexOf('.'); return index < 0 ? '' : filename.substring(index).replace(/\?.+$/, ''); } +// The packages whose dev/production server builds are selected by the +// `development` export condition. A dependency on either means the package +// consumes the runtime and must resolve it through Vite in dev. +const SOLID_RUNTIME_PKGS = ['solid-js', '@solidjs/web']; + +// Tooling that declares solid-js as a peer but never runs inside the SSR +// module runner. Kept out of the crawl entirely: classifying them as +// semi-framework would also crawl THEIR dependencies, which vitefu deep- +// includes in the client optimizer (`@solidjs/vite-plugin > @babel/core` +// pre-bundled for the browser — ~2.6 MB of dead weight per cold start). +// Mirrors vite-plugin-svelte's isCommonDepWithoutSvelteField list. +const NON_RUNTIME_SOLID_PKGS = ['@solidjs/vite-plugin', 'vite', 'vitest', 'eslint-plugin-solid']; +const NON_RUNTIME_SOLID_PREFIXES = [ + 'vite-plugin-', + 'eslint-plugin-', + 'prettier-plugin-', + '@types/', +]; +function isNonRuntimeSolidPkg(name: string): boolean { + const bare = name.slice(name.lastIndexOf('/') + 1); + return ( + NON_RUNTIME_SOLID_PKGS.includes(name) || + NON_RUNTIME_SOLID_PREFIXES.some((p) => (p.startsWith('@') ? name : bare).startsWith(p)) + ); +} + function containsSolidField(fields: Record<string, any>) { const keys = Object.keys(fields); for (let i = 0; i < keys.length; i++) { @@ -1034,6 +1060,30 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] { isFrameworkPkgByJson(pkgJson) { return containsSolidField(pkgJson.exports || {}); }, + // `false` = neither framework nor semi-framework, and don't crawl + // its deps; `undefined` = unknown, fall through to the json checks. + isFrameworkPkgByName(name) { + return isNonRuntimeSolidPkg(name) ? false : undefined; + }, + // Under `vite dev` the runtime must not be split in two. Inlined + // modules resolve `solid-js` through Vite with `development` (its dev + // server build); an externalized package's own imports are resolved by + // Node, which has no `development` condition, so it loads the + // production build instead. Both then run, each with its own + // `sharedConfig` — the manifest `renderToStream` sets lands on one and + // `lazy()` reads the other. `resolve.externalConditions` below only + // fixes the external's own entry, not what it imports, so every + // package that consumes the runtime has to go through Vite as well. + // Semi-framework is the right class: `ssr.noExternal` without + // `optimizeDeps.exclude`, since these hold no raw Solid components. + isSemiFrameworkPkgByJson(pkgJson) { + // Same gate as the core inlining in configEnvironment: dev serve + // only, never vitest (it manages inlining via test.server.deps). + if (!replaceDev || isTestMode) return false; + return SOLID_RUNTIME_PKGS.some( + (name) => pkgJson.dependencies?.[name] || pkgJson.peerDependencies?.[name], + ); + }, }); // fix for bundling dev in production @@ -1220,13 +1270,21 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] { // Only set resolve.external if noExternal is not true (to avoid conflicts with plugins like Cloudflare) if (name === 'ssr' && solidPkgsConfig) { if (config.resolve.noExternal !== true) { - config.resolve.noExternal = [ + const noExternal = [ ...(Array.isArray(config.resolve.noExternal) ? config.resolve.noExternal : []), ...solidPkgsConfig.ssr.noExternal, ]; + config.resolve.noExternal = noExternal; + // vitefu externalizes the non-framework deps of every framework + // package in dev, and Vite gives `external` precedence over + // `noExternal`. A framework package that lists solid-js or + // @solidjs/web under `dependencies` (not peer — e.g. + // @tanstack/solid-router 2.0.0-rc.7 → @solidjs/web) would therefore + // re-externalize a core inlined above and split the runtime again. + // Nothing inlined may appear in `external`. config.resolve.external = [ ...(Array.isArray(config.resolve.external) ? config.resolve.external : []), - ...solidPkgsConfig.ssr.external, + ...solidPkgsConfig.ssr.external.filter((dep) => !noExternal.includes(dep)), ]; } }