Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ssr-inline-solid-consumers.md
Original file line number Diff line number Diff line change
@@ -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 (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.
62 changes: 60 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)),
];
}
}
Expand Down
Loading