From d373e4f4a2ca6c10ae83b517ca88efdef4fb5b3c Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Mon, 9 Nov 2026 11:18:40 -0600 Subject: [PATCH] fix(vitest): load client solid-js in node-env files of DOM test runs Fixes #534 --- .../vitest-node-env-browser-condition.md | 5 ++ .../tests/jsdom/node-environment.test.tsx | 22 ++++++++ examples/vite-6/tests/node-conditions.mjs | 51 ++++++++++++------- src/index.ts | 28 ++++++---- src/test-config.test.ts | 41 +++++++++++++++ 5 files changed, 117 insertions(+), 30 deletions(-) create mode 100644 .changeset/vitest-node-env-browser-condition.md create mode 100644 examples/vite-6/tests/jsdom/node-environment.test.tsx diff --git a/.changeset/vitest-node-env-browser-condition.md b/.changeset/vitest-node-env-browser-condition.md new file mode 100644 index 00000000..34d5822e --- /dev/null +++ b/.changeset/vitest-node-env-browser-condition.md @@ -0,0 +1,5 @@ +--- +'vite-plugin-solid': patch +--- + +Load the client build of solid-js in test files that set `// @vitest-environment node` when `test.environment` is `jsdom` or `happy-dom`. The `ssr` environment keeps the `browser` condition again, and only the conditions Vitest passes to Node leave it out, so `msw/node` still loads. diff --git a/examples/vite-6/tests/jsdom/node-environment.test.tsx b/examples/vite-6/tests/jsdom/node-environment.test.tsx new file mode 100644 index 00000000..a766d182 --- /dev/null +++ b/examples/vite-6/tests/jsdom/node-environment.test.tsx @@ -0,0 +1,22 @@ +// @vitest-environment node +// Regression test for solidjs/solid-vite-plugin#534. This file runs in the +// `ssr` environment while the suite defaults to jsdom, and must still load +// the client build of solid-js. +import { createEffect, createRoot } from 'solid-js'; +import { isServer } from 'solid-js/web'; +import { expect, test } from 'vitest'; + +test('loads the client build of solid-js', () => { + expect(isServer).toBe(false); +}); + +test('runs effects', async () => { + let ran = false; + createRoot(() => + createEffect(() => { + ran = true; + }), + ); + await Promise.resolve(); + expect(ran).toBe(true); +}); diff --git a/examples/vite-6/tests/node-conditions.mjs b/examples/vite-6/tests/node-conditions.mjs index d55debc3..4ec5a1c2 100644 --- a/examples/vite-6/tests/node-conditions.mjs +++ b/examples/vite-6/tests/node-conditions.mjs @@ -1,41 +1,54 @@ -// Vitest passes the `ssr` environment's conditions to Node as `--conditions` -// for the packages it leaves external. In a jsdom test run the plugin used to -// add `browser` there, so Node-only packages failed to load. For example, -// `msw/node` imports `@mswjs/interceptors/ClientRequest`, whose `browser` -// export is `null` (#125). +// Vitest passes the resolved `ssr.resolve.conditions` to Node as +// `--conditions` for the packages it leaves external. In a jsdom test run the +// plugin used to add `browser` there, so Node-only packages failed to load. +// For example, `msw/node` imports `@mswjs/interceptors/ClientRequest`, whose +// `browser` export is `null` (#125). Files with `// @vitest-environment node` +// run in the `ssr` environment, so it keeps `browser` for Vite's own +// resolution of the inlined solid-js (#534). // // Checks: -// - jsdom: `client` keeps `browser`, `ssr` does not. -// - node environment: `ssr` keeps `browser`, since its tests run there. +// - jsdom: `client` and `ssr` keep `browser`, Node's conditions do not. +// - jsdom with `browser` set by the user on `ssr`: Node keeps it. +// - node environment: `ssr` and Node keep `browser`. import { resolveConfig } from 'vite'; import solid from 'vite-plugin-solid'; -async function conditions(test) { +async function conditions(test, extra = {}) { const resolved = await resolveConfig( - { mode: 'test', configFile: false, logLevel: 'silent', plugins: [solid()], test }, + { mode: 'test', configFile: false, logLevel: 'silent', plugins: [solid()], test, ...extra }, 'serve', ); return { client: resolved.environments.client.resolve.conditions, ssr: resolved.environments.ssr.resolve.conditions, + node: resolved.ssr.resolve.conditions, }; } const failures = []; -const jsdom = await conditions({ environment: 'jsdom' }); -if (!jsdom.client.includes('browser')) { - failures.push(`jsdom: client conditions lack browser: ${JSON.stringify(jsdom.client)}`); -} -if (jsdom.ssr.includes('browser')) { - failures.push(`jsdom: ssr conditions include browser: ${JSON.stringify(jsdom.ssr)}`); +function expectBrowser(label, list, expected) { + if (list.includes('browser') !== expected) { + failures.push(`${label} ${expected ? 'lacks' : 'includes'} browser: ${JSON.stringify(list)}`); + } } + +const jsdom = await conditions({ environment: 'jsdom' }); +expectBrowser('jsdom: client conditions', jsdom.client, true); +expectBrowser('jsdom: ssr environment conditions', jsdom.ssr, true); +expectBrowser('jsdom: Node conditions', jsdom.node, false); + +const userSsr = await conditions( + { environment: 'jsdom' }, + { environments: { ssr: { resolve: { conditions: ['browser', 'node', 'import'] } } } }, +); +expectBrowser('jsdom + user ssr browser: Node conditions', userSsr.node, true); + const node = await conditions({ environment: 'node' }); -if (!node.ssr.includes('browser')) { - failures.push(`node: ssr conditions lack browser: ${JSON.stringify(node.ssr)}`); -} +expectBrowser('node: ssr environment conditions', node.ssr, true); +expectBrowser('node: Node conditions', node.node, true); if (failures.length) { for (const failure of failures) console.error(`FAIL: ${failure}`); process.exit(1); } -console.log('PASS: jsdom tests keep browser out of the ssr conditions'); +console.log('PASS: jsdom tests keep browser out of the Node conditions only'); diff --git a/src/index.ts b/src/index.ts index 3c89d58d..e5cfe8ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -270,10 +270,10 @@ export default function solidPlugin(options: Partial = {}): Plugin { // by the crawl in test mode. They are inlined with solid-js (see // `test.server.deps` below). const testRuntimeConsumers = new Set(); - // True when a DOM test run inlines the Solid runtime. The `ssr` - // environment then does not get the `browser` condition (see - // `configEnvironment` below). + // True when a DOM test run inlines the Solid runtime. Node then does not + // get the `browser` condition (see `configResolved` below). let domTestRuntimeInlined = false; + let ssrBrowserAdded = false; return { name: 'solid', @@ -285,6 +285,7 @@ export default function solidPlugin(options: Partial = {}): Plugin { projectRoot = userConfig.root; isTestMode = userConfig.mode === 'test'; domTestRuntimeInlined = false; + ssrBrowserAdded = false; // Leave the user's `resolve.alias` as it is. Turning an object into an // array changes how Vite merges it with aliases from later plugins: @@ -488,18 +489,12 @@ export default function solidPlugin(options: Partial = {}): Plugin { config.resolve.conditions = [...defaultServerConditions]; } } - // Vitest passes the `ssr` environment's conditions to Node as - // `--conditions` for the packages it leaves external. With `browser` - // there, Node-only packages fail to load. For example, `msw/node` - // imports `@mswjs/interceptors/ClientRequest`, whose `browser` - // export is `null` (#125). DOM tests run in the `client` environment, - // and the Solid runtime is inlined in that case, so the `ssr` - // environment does not need `browser`. const addBrowser = isTestMode && !opts.isSsrTargetWebworker && !options.ssr && - !(domTestRuntimeInlined && name === 'ssr'); + !config.resolve.conditions.includes('browser'); + if (name === 'ssr') ssrBrowserAdded = addBrowser; config.resolve.conditions = [ 'solid', // A server build with `ssr.target: 'webworker'` starts from Vite's @@ -533,6 +528,17 @@ export default function solidPlugin(options: Partial = {}): Plugin { config.command === 'serve' && config.mode !== 'production' && options.hot !== false; + + // Vitest sends only this copy to Node; `browser` there breaks `msw/node` (#125, #534). + const ssrResolve = config.ssr?.resolve; + if ( + domTestRuntimeInlined && + ssrBrowserAdded && + ssrResolve?.conditions && + ssrResolve !== config.environments?.ssr?.resolve + ) { + ssrResolve.conditions = ssrResolve.conditions.filter((c) => c !== 'browser'); + } }, resolveId(id) { diff --git a/src/test-config.test.ts b/src/test-config.test.ts index ae927798..10007a9d 100644 --- a/src/test-config.test.ts +++ b/src/test-config.test.ts @@ -24,3 +24,44 @@ describe('test config', () => { expect(test?.server).toBeUndefined(); }); }); + +describe('test conditions', () => { + async function conditions(test: Record, extra: Record = {}) { + const { resolveConfig } = await import('vite'); + const resolved = await resolveConfig( + { + mode: 'test', + configFile: false, + logLevel: 'silent', + plugins: [solidPlugin()], + test, + ...extra, + } as any, + 'serve', + ); + return { + ssr: resolved.environments.ssr.resolve.conditions, + node: resolved.ssr.resolve!.conditions!, + }; + } + + it('keeps browser on the ssr environment but not in the Node conditions for DOM tests', async () => { + const { ssr, node } = await conditions({ environment: 'happy-dom' }); + expect(ssr).toContain('browser'); + expect(node).not.toContain('browser'); + }); + + it('keeps a browser condition the user set on the ssr environment', async () => { + const { node } = await conditions( + { environment: 'jsdom' }, + { environments: { ssr: { resolve: { conditions: ['browser', 'node', 'import'] } } } }, + ); + expect(node).toContain('browser'); + }); + + it('keeps browser in the Node conditions for the node environment', async () => { + const { ssr, node } = await conditions({ environment: 'node' }); + expect(ssr).toContain('browser'); + expect(node).toContain('browser'); + }); +});