From ff493b95f6fc136e2adc0addc5569280f365cf57 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Mon, 19 Oct 2026 11:26:41 +0800 Subject: [PATCH] fix: dedupe solid packages in production builds - resolve.dedupe was only set while serving, so a dependency with its own nested solid-js copy was bundled twice in production. - Dedupe solid-js, @solidjs/web and @solidjs/signals in every command, for each one the project root can resolve. - Add a build test with a nested solid-js copy. Fixes #112 Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/dedupe-solid-in-builds.md | 5 +++ src/dedupe.test.ts | 66 ++++++++++++++++++++++++++++ src/index.ts | 14 ++++-- 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 .changeset/dedupe-solid-in-builds.md create mode 100644 src/dedupe.test.ts diff --git a/.changeset/dedupe-solid-in-builds.md b/.changeset/dedupe-solid-in-builds.md new file mode 100644 index 0000000..9ef57a2 --- /dev/null +++ b/.changeset/dedupe-solid-in-builds.md @@ -0,0 +1,5 @@ +--- +'@solidjs/vite-plugin': patch +--- + +Production builds now dedupe `solid-js`, `@solidjs/web` and `@solidjs/signals` the same way dev does. A dependency with its own nested copy of Solid no longer adds a second runtime to the bundle, which broke context lookups (#112). diff --git a/src/dedupe.test.ts b/src/dedupe.test.ts new file mode 100644 index 0000000..8b45e87 --- /dev/null +++ b/src/dedupe.test.ts @@ -0,0 +1,66 @@ +import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { build, type Rolldown } from 'vite'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import solidPlugin from './index.js'; + +let root: string; + +async function writePackage(dir: string, pkg: Record, files: Record) { + await mkdir(dir, { recursive: true }); + await writeFile(path.join(dir, 'package.json'), JSON.stringify(pkg)); + for (const [name, code] of Object.entries(files)) { + await writeFile(path.join(dir, name), code); + } +} + +beforeAll(async () => { + root = await mkdtemp(path.join(tmpdir(), 'solid-dedupe-')); + const fakeSolid = (copy: string) => [ + { name: 'solid-js', version: '2.0.0', type: 'module', exports: { '.': './index.js' } }, + { 'index.js': `export const copy = ${JSON.stringify(copy)};\n` }, + ] as const; + await writePackage(path.join(root, 'node_modules/solid-js'), ...fakeSolid('root')); + // A dependency that ships its own nested copy of solid-js. + const lib = path.join(root, 'node_modules/lib'); + await writePackage( + lib, + { name: 'lib', version: '1.0.0', type: 'module', exports: { '.': './index.js' } }, + { 'index.js': "export { copy as libCopy } from 'solid-js';\n" }, + ); + await writePackage(path.join(lib, 'node_modules/solid-js'), ...fakeSolid('nested')); + await writeFile(path.join(root, 'package.json'), JSON.stringify({ name: 'app', type: 'module' })); + await writeFile( + path.join(root, 'entry.js'), + "import { copy } from 'solid-js';\nimport { libCopy } from 'lib';\nexport const copies = [copy, libCopy];\n", + ); +}); + +afterAll(async () => { + await rm(root, { recursive: true, force: true }); +}); + +describe('resolve.dedupe', () => { + // #112: dev deduped solid-js, but production builds bundled the nested copy too. + it('bundles a single solid-js copy in production builds', async () => { + const result = (await build({ + root, + configFile: false, + logLevel: 'silent', + plugins: [solidPlugin()], + build: { + write: false, + minify: false, + rolldownOptions: { input: path.join(root, 'entry.js'), preserveEntrySignatures: 'strict' }, + }, + })) as Rolldown.RolldownOutput | Rolldown.RolldownOutput[]; + const output = Array.isArray(result) ? result[0] : result; + const code = output.output + .filter((chunk): chunk is Rolldown.OutputChunk => chunk.type === 'chunk') + .map((chunk) => chunk.code) + .join('\n'); + expect(code).toContain('"root"'); + expect(code).not.toContain('"nested"'); + }); +}); diff --git a/src/index.ts b/src/index.ts index 7b9ab11..84c85ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1521,10 +1521,16 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { // added to `optimizeDeps.include`: the optimizer already reaches // signals through `solid-js`, and an include entry that doesn't // resolve from the root logs a warning on every start. - const dedupe = - replaceDev && findPackageDir('@solidjs/signals', path.resolve(projectRoot || process.cwd())) - ? [...nestedDeps, '@solidjs/signals'] - : nestedDeps; + // + // Dedupe applies to builds as well as dev. A dependency that ships its + // own nested copy of `solid-js` would otherwise put a second runtime + // in the production bundle, with its own owner tree and contexts. #112 + // Each package is only listed when the root can resolve it, for the + // reason given above for `@solidjs/signals`. + const dedupeRoot = path.resolve(projectRoot || process.cwd()); + const dedupe = ['solid-js', '@solidjs/web', '@solidjs/signals'].filter((name) => + findPackageDir(name, dedupeRoot), + ); const userTest = (userConfig as any).test ?? {}; const test = {} as any;