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/node-22-floor-signals-dedupe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/vite-plugin': patch
---

Node floor and a signals dedupe. `engines.node` is now `>=22.12.0` (was `^20.19.0 || >=22.12.0`), matching the `solid-js`, `@solidjs/web` and `@solidjs/signals` 2.0 runtime packages, which have been ESM-only with that floor since rc.8; the README says the same. Under `serve`, `@solidjs/signals` joins `solid-js` and `@solidjs/web` in `resolve.dedupe` when the app root can reach a copy of it (`node_modules/@solidjs/signals`, walking up — an npm-style hoist or a direct dependency): `@solidjs/diagnostics/browser` and `solid-js/attribution` import the signals core directly, so a nested/duplicated install yielded a second engine beside the one `solid-js` loads. The entry is gated on the root copy because `resolve.dedupe` resolves the listed package from the root and Vite's Node-side resolver (`fetchModule` for externalized SSR imports, and the externalize decision) has no importer fallback when that misses — under pnpm's isolated layout, where signals exists only as `solid-js`'s transitive dependency, an unconditional entry turned `import "@solidjs/signals"` from the inlined `solid-js` into `ERR_MODULE_NOT_FOUND` once anything externalized it (vitefu does, when a semi-framework package such as `@solidjs/diagnostics` lists it under `dependencies`). With no root copy there is nothing to dedupe to, so the gate loses nothing. Signals does not join `optimizeDeps.include`: the optimizer already reaches it through `solid-js`, and an include entry that doesn't resolve from the root warns on every start.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ Join [solid discord](https://discord.com/invite/solidjs) and check the [troubles

## Requirements

This module is 100% ESM compatible and requires Node.js `^20.19.0 || >=22.12.0`.
This module is 100% ESM compatible and requires Node.js `>=22.12.0` (the same
floor as the `solid-js` 2.0 runtime packages).

You can check your current Node.js version by running `node -v`. Use a version
manager such as [Volta](https://volta.sh/) or [nvm](https://github.com/nvm-sh/nvm)
Expand Down
2 changes: 1 addition & 1 deletion examples/start-ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "vite",
"build": "vite build",
"serve": "NODE_ENV=production node server.js",
"test": "node test/run.mjs && node test/http-bridge.mjs && node test/components-warning.mjs && node test/webworker-warning.mjs"
"test": "node test/run.mjs && node test/http-bridge.mjs && node test/components-warning.mjs && node test/webworker-warning.mjs && node test/dedupe.mjs"
},
"devDependencies": {
"jsdom": "^26.1.0",
Expand Down
125 changes: 125 additions & 0 deletions examples/start-ssr/test/dedupe.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Config-level test for the dev `resolve.dedupe` list:
// - `solid-js` and `@solidjs/web` are always deduped under `serve`,
// - `@solidjs/signals` joins them only when the app root can reach a copy
// (`<root>/node_modules/@solidjs/signals`, walking up). Dedupe resolves
// the listed package from the root and Vite's Node-side resolver
// (`fetchModule`, the externalize decision) has no importer fallback,
// so under pnpm's isolated layout — signals present only as `solid-js`'s
// transitive dependency, which is this example's own layout — the entry
// would turn `import "@solidjs/signals"` from the inlined `solid-js` into
// ERR_MODULE_NOT_FOUND once anything externalizes signals,
// - `@solidjs/signals` never rides into `optimizeDeps.include` (an include
// entry that doesn't resolve from the root warns on every start),
// - under `build` the list is empty (dev-only fix).
//
// Pure resolveConfig — no dev server, no browser. Requires the plugin built
// (pnpm build at the repo root). Usage: node test/dedupe.mjs

import { fileURLToPath } from 'node:url';
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import { resolveConfig } from 'vite';
import solidPlugin from '@solidjs/vite-plugin';

const exampleDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));

const results = [];
function record(name, ok, detail = '') {
results.push({ name, ok, detail });
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail && !ok ? ` — ${detail}` : ''}`);
}

function findUp(root, name) {
let dir = root;
while (true) {
if (fs.existsSync(path.join(dir, 'node_modules', name, 'package.json'))) return dir;
const parent = path.dirname(dir);
if (parent === dir) return undefined;
dir = parent;
}
}

async function resolveWith(root, command) {
return resolveConfig(
{ root, configFile: false, logLevel: 'error', plugins: [solidPlugin({ ssr: true })] },
command,
);
}

// ---- this example: pnpm isolated layout, signals is transitive only -------
{
const reachable = findUp(exampleDir, '@solidjs/signals');
record(
'precondition: @solidjs/signals is not reachable from the example root',
reachable === undefined,
`found under ${reachable}`,
);
const config = await resolveWith(exampleDir, 'serve');
record(
'serve: solid-js and @solidjs/web deduped',
config.resolve.dedupe.includes('solid-js') && config.resolve.dedupe.includes('@solidjs/web'),
config.resolve.dedupe.join(', '),
);
record(
'serve, no root copy: @solidjs/signals NOT in resolve.dedupe',
!config.resolve.dedupe.includes('@solidjs/signals'),
config.resolve.dedupe.join(', '),
);
record(
'serve: @solidjs/signals not in optimizeDeps.include',
!config.optimizeDeps.include.includes('@solidjs/signals'),
config.optimizeDeps.include.join(', '),
);
}

// ---- a root that CAN reach a copy (npm-style hoist / direct dependency) ----
{
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'solid-dedupe-'));
try {
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify({ name: 'dedupe-probe', private: true, type: 'module' }),
);
const stub = path.join(root, 'node_modules', '@solidjs', 'signals');
fs.mkdirSync(stub, { recursive: true });
fs.writeFileSync(
path.join(stub, 'package.json'),
JSON.stringify({ name: '@solidjs/signals', version: '0.0.0-stub' }),
);

const serve = await resolveWith(root, 'serve');
record(
'serve, root copy present: @solidjs/signals in resolve.dedupe',
serve.resolve.dedupe.includes('@solidjs/signals'),
serve.resolve.dedupe.join(', '),
);
record(
'serve, root copy present: solid-js and @solidjs/web still deduped',
serve.resolve.dedupe.includes('solid-js') && serve.resolve.dedupe.includes('@solidjs/web'),
serve.resolve.dedupe.join(', '),
);
record(
'serve, root copy present: @solidjs/signals still not in optimizeDeps.include',
!serve.optimizeDeps.include.includes('@solidjs/signals'),
serve.optimizeDeps.include.join(', '),
);

const build = await resolveWith(root, 'build');
record(
'build: resolve.dedupe is empty (dev-only fix)',
build.resolve.dedupe.length === 0,
build.resolve.dedupe.join(', '),
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}

const failed = results.filter((r) => !r.ok);
console.log(`\n${results.length - failed.length}/${results.length} assertions passed`);
if (failed.length) {
console.log('\nFailures:');
for (const f of failed) console.log(` ${f.name} — ${f.detail}`);
process.exit(1);
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "solid-js integration plugin for Vite",
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
"node": ">=22.12.0"
},
"files": [
"dist",
Expand Down
26 changes: 25 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1287,6 +1287,30 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {

// fix for bundling dev in production
const nestedDeps = replaceDev ? ['solid-js', '@solidjs/web'] : [];
// `@solidjs/signals` is the reactive core `solid-js` depends on.
// `@solidjs/diagnostics/browser` and `solid-js/attribution` import it
// directly, so a nested/duplicated install (npm nesting a second copy
// under a package that lists it as a dependency) yields a second
// engine beside the one `solid-js` loads — the same two-engines
// symptom the `optimizeDeps.include` entries below guard against on
// the pre-bundle path. Dedupe it to the app's copy — but only when the
// app root can reach one. `resolve.dedupe` resolves the listed package
// from the root, and Vite's Node-side resolver (`fetchModule` for
// externalized SSR imports, and the externalize decision itself) has
// no importer fallback when that misses. Under pnpm's isolated layout
// signals exists only as `solid-js`'s transitive dependency, so with
// the entry always on, `import "@solidjs/signals"` from the inlined
// `solid-js` fails with ERR_MODULE_NOT_FOUND as soon as anything
// externalizes it (vitefu does, once a semi-framework package such as
// `@solidjs/diagnostics` lists it under `dependencies`). With no root
// copy there is nothing to dedupe TO, so the gate loses nothing. Not
// 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;

const userTest = (userConfig as any).test ?? {};
const test = {} as any;
Expand Down Expand Up @@ -1361,7 +1385,7 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
// esbuild: { include: /\.ts$/ },
// resolve.conditions is handled per-environment in configEnvironment.
resolve: {
dedupe: nestedDeps,
dedupe,
},
optimizeDeps: {
extensions: ['.tsrx'],
Expand Down
Loading