From bd04c66798b358bd6759f73f58ebe0627fdce3ec Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Tue, 8 Sep 2026 12:12:23 -0500 Subject: [PATCH 1/2] fix: inline packages that consume the Solid runtime in dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to a810d09. Inlining solid-js and @solidjs/web fixes every resolution those two perform, and vitefu inlines packages advertising a `solid` export condition. A package that does neither is still external, so Node resolves its own `import "solid-js"` without `development` and loads the production server build while the inlined graph holds the dev one — the same two-instance split, one layer out. @solidjs/meta is the first-party case: no `solid` export condition, so an app rendering still fails in useContext under solid-js 2.0.0-rc.7 with the core packages already inlined. Classify any package declaring solid-js or @solidjs/web in dependencies or peerDependencies as a semi-framework package: ssr.noExternal without optimizeDeps.exclude, which is right here as they carry no raw Solid components. Gated on replaceDev, so builds are unchanged. --- .changeset/ssr-inline-solid-consumers.md | 5 +++++ src/index.ts | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 .changeset/ssr-inline-solid-consumers.md diff --git a/.changeset/ssr-inline-solid-consumers.md b/.changeset/ssr-inline-solid-consumers.md new file mode 100644 index 0000000..a70a113 --- /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 `<Title>` 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, leaving builds unchanged. diff --git a/src/index.ts b/src/index.ts index 37eae3d..b3903bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -442,6 +442,11 @@ 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']; + function containsSolidField(fields: Record<string, any>) { const keys = Object.keys(fields); for (let i = 0; i < keys.length; i++) { @@ -1034,6 +1039,23 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] { isFrameworkPkgByJson(pkgJson) { return containsSolidField(pkgJson.exports || {}); }, + // 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) { + if (!replaceDev) return false; + return SOLID_RUNTIME_PKGS.some( + (name) => pkgJson.dependencies?.[name] || pkgJson.peerDependencies?.[name], + ); + }, }); // fix for bundling dev in production From 44536147427354acaebd30c5f083878e609516b0 Mon Sep 17 00:00:00 2001 From: Ryan Carniato <ryansolid@gmail.com> Date: Wed, 9 Sep 2026 17:07:29 -0700 Subject: [PATCH 2/2] fix: keep the runtime-consumer inlining narrow Two guards on the semi-framework rule from the previous commit, plus its test-mode gate: - Tooling skip-list (`isFrameworkPkgByName`): `@solidjs/vite-plugin`, `vite`, `vitest`, `eslint-plugin-*`, `vite-plugin-*`, `prettier-plugin-*`, `@types/*`. These declare solid-js as a peer but never run inside the SSR module runner; classifying the plugin itself as semi-framework made vitefu crawl ITS dependencies and deep-include `@solidjs/vite-plugin > @babel/core` / `> @solidjs/babel-plugin` in the CLIENT optimizeDeps (node_modules/.vite/deps 1.5 MB -> 8.3 MB on the fullstack template). Mirrors vite-plugin-svelte's isCommonDepWithoutSvelteField list. - Never re-externalize an inlined core: vitefu pushes the non-framework `dependencies` of every framework package to `ssr.external` in dev, and Vite checks `external` before `noExternal`. @tanstack/solid-router lists `@solidjs/web` under `dependencies`, so a810d09's noExternal for it was defeated and the tanstack template 500'd with "lazy() called but no asset manifest is set". Filter `ssr.external` against the final noExternal list in configEnvironment. - Gate isSemiFrameworkPkgByJson on !isTestMode as well, matching the core inlining in configEnvironment (vitest manages inlining via test.server.deps). Verified on the fullstack (@solidjs/meta) and fullstack-tanstack templates: both 200 under `vite dev` with a single solid-js/@solidjs/web instance, and the client optimizer output back to its next.40 size. Co-authored-by: Cursor <cursoragent@cursor.com> --- .changeset/ssr-inline-solid-consumers.md | 2 +- src/index.ts | 42 ++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/.changeset/ssr-inline-solid-consumers.md b/.changeset/ssr-inline-solid-consumers.md index a70a113..5e3b002 100644 --- a/.changeset/ssr-inline-solid-consumers.md +++ b/.changeset/ssr-inline-solid-consumers.md @@ -2,4 +2,4 @@ '@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 `<Title>` 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, leaving builds unchanged. +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 `<Title>` 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 b3903bf..5e8933f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -447,6 +447,27 @@ function getExtension(filename: string): string { // 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++) { @@ -1039,6 +1060,11 @@ 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 @@ -1051,7 +1077,9 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] { // Semi-framework is the right class: `ssr.noExternal` without // `optimizeDeps.exclude`, since these hold no raw Solid components. isSemiFrameworkPkgByJson(pkgJson) { - if (!replaceDev) return false; + // 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], ); @@ -1242,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)), ]; } }