diff --git a/.changeset/no-jsdom-default.md b/.changeset/no-jsdom-default.md new file mode 100644 index 0000000..1657e38 --- /dev/null +++ b/.changeset/no-jsdom-default.md @@ -0,0 +1,11 @@ +--- +'@solidjs/vite-plugin': patch +--- + +The plugin no longer sets `test.environment: 'jsdom'` for Vitest. Set the +environment yourself (`jsdom` or `happy-dom` for DOM component tests); Vitest's +own default is `node`. The default also applied to Node-only suites that loaded +the plugin through a shared config, which then ran under jsdom and failed in +ways that were hard to trace. The client test posture (browser conditions, DOM +codegen) is unchanged, and `environment: 'node'` still selects the server +posture. diff --git a/.changeset/vitest-ssr-no-browser-condition.md b/.changeset/vitest-ssr-no-browser-condition.md new file mode 100644 index 0000000..4eb6a3b --- /dev/null +++ b/.changeset/vitest-ssr-no-browser-condition.md @@ -0,0 +1,5 @@ +--- +'@solidjs/vite-plugin': patch +--- + +In jsdom and happy-dom tests, Node no longer gets the `browser` condition for packages that Vitest leaves external. Node-only packages such as `msw/node` now load (#125). diff --git a/README.md b/README.md index 537b1a0..97713a0 100644 --- a/README.md +++ b/README.md @@ -1152,7 +1152,7 @@ For other breaking changes, check [the migration guide of vite](https://vitejs.d If you are using [vitest](https://vitest.dev/), this plugin already injects the necessary configuration for you. It even automatically detects if you have `@testing-library/jest-dom` installed in your project and automatically adds it to the `setupFiles`. All you need to add (if you want) is `globals`, `coverage`, and other testing configuration of your choice. If you can live without those, enjoy using vitest without the need to configure it yourself. -Tests default to the client posture, regardless of the app's `ssr` flag: DOM codegen, non-hydratable (nothing hydrates in a test), browser export conditions, and a `jsdom` default `test.environment`. `solid-js`, `@solidjs/web` and the packages that import them (such as `@solidjs/testing-library`) are inlined, so their imports resolve the browser build through Vite on any host runtime, Deno included. A server-rendered app needs no `ssr: mode !== 'test'` workaround — DOM component tests just work. +Tests default to the client posture, regardless of the app's `ssr` flag: DOM codegen, non-hydratable (nothing hydrates in a test), and browser export conditions. The plugin does not set `test.environment`: set `environment: 'jsdom'` (or `'happy-dom'`) for DOM component tests, as Vitest's own default is `node`. `solid-js`, `@solidjs/web` and the packages that import them (such as `@solidjs/testing-library`) are inlined, so their imports resolve the browser build through Vite on any host runtime, Deno included. A server-rendered app needs no `ssr: mode !== 'test'` workaround — DOM component tests just work. Server-runtime unit tests (server functions, sessions, `renderToString` — anything that needs `isServer` to be `true` and the real server build of the framework) opt out per [vitest project](https://vitest.dev/guide/projects) by setting `test.environment: 'node'` (or `'edge-runtime'`) explicitly. That project gets the server posture end to end: server export conditions, ssr codegen, and the framework inlined so the whole graph — request-event storage included — resolves into one server-build instance. No inline/alias configuration needed. Both postures coexist in one workspace: diff --git a/examples/start-ssr/src/posture.test.tsx b/examples/start-ssr/src/posture.test.tsx index 8a0277a..d0b9385 100644 --- a/examples/start-ssr/src/posture.test.tsx +++ b/examples/start-ssr/src/posture.test.tsx @@ -1,8 +1,8 @@ // Vitest posture regression test (vitest mode in test/run.mjs): this app is // `ssr: true`, but tests must compile and resolve with the CLIENT posture — -// dom codegen, non-hydratable, browser export conditions, jsdom by default -// (the plugin injects all of that in test mode; this example's config has no -// `test` block and no `ssr: mode !== 'test'` workaround). Before the fix this +// dom codegen, non-hydratable, browser export conditions (the plugin injects +// all of that in test mode; this example's config only sets `environment: +// 'jsdom'` and has no `ssr: mode !== 'test'` workaround). Before the fix this // file would compile hydratable (or with server semantics under a node // environment) and DOM component tests broke. import { expect, it } from 'vitest'; diff --git a/examples/start-ssr/test/run.mjs b/examples/start-ssr/test/run.mjs index 73e659d..e0fdf58 100644 --- a/examples/start-ssr/test/run.mjs +++ b/examples/start-ssr/test/run.mjs @@ -6318,7 +6318,7 @@ async function runNodeMode() { // Vitest posture: even though this app is `ssr: true`, tests compile and // resolve with the client posture — dom codegen, non-hydratable, browser -// conditions, jsdom default — with no `test` block and no +// conditions — with only `environment: 'jsdom'` and no // `ssr: mode !== 'test'` workaround in the config. src/posture.test.tsx // carries the actual assertions (isServer false, non-hydratable codegen, a // DOM component rendering and updating under jsdom); this mode just runs it. @@ -6424,17 +6424,15 @@ async function runVitestMode() { projectsPass ? undefined : projectsOut.slice(-2000), ); - // Browser-mode projects must NOT get the jsdom default: vitest 4 probes - // for the environment's package at startup (getEnvPackageName → - // ensureInstalled) and sets a failing exit code when jsdom isn't - // installed — even though the suite itself runs (and passes) in the real - // browser. The gate mirrors the plugin's jest-dom one (`browser.enabled` - // skips the injection); everything else keeps the jsdom default. Asserted - // at the config-resolution level: jsdom IS installed in this example, so - // a full browser run couldn't reproduce the missing-package probe. + // The plugin injects no `test.environment`, in any project shape: a + // default applied to Node-only suites that loaded the plugin too, and in + // browser mode or a projects root vitest probes for the environment's + // package at startup and fails the run when it isn't installed (#205). + // Resolved without the example's config file, which sets jsdom itself. + const { default: solid } = await import('@solidjs/vite-plugin'); const resolveTestEnvironment = async (testBlock) => { const resolved = await resolveConfig( - { root: exampleDir, mode: 'test', test: testBlock }, + { root: exampleDir, mode: 'test', configFile: false, plugins: [solid()], test: testBlock }, 'serve', ); return resolved.test?.environment; @@ -6450,20 +6448,17 @@ async function runVitestMode() { } catch (e) { envError = String(e); } - const browserEnvPass = !envError && browserEnv === undefined && defaultEnv === 'jsdom'; + const browserEnvPass = !envError && browserEnv === undefined && defaultEnv === undefined; record( mode, 'browser-env', - 'browser-mode project gets no injected jsdom environment (non-browser keeps the jsdom default)', + 'no injected test environment (browser mode or a plain test block)', browserEnvPass, browserEnvPass ? undefined : envError || `browser: ${browserEnv}, default: ${defaultEnv}`, ); - // A root config that defines `test.projects` runs no tests itself — each - // project controls its own environment — so it must NOT get the jsdom - // default either: vitest probes for the root environment's package at - // startup and prompts for jsdom even when every project runs under node - // or in the browser (solidjs/solid-vite-plugin#205, forward-port of #323). + // A root config that defines `test.projects` runs no tests itself; it + // gets no environment either (solidjs/solid-vite-plugin#205). let projectsEnv; let projectsEnvError = ''; try { @@ -6592,6 +6587,56 @@ async function runVitestMode() { : jestDomError || `empty root: ${JSON.stringify(emptySetupFiles)}, example root: ${JSON.stringify(exampleSetupFiles)}`, ); + + // Vitest passes the `ssr` environment's conditions to Node as + // `--conditions` for the packages it leaves external. A jsdom run used to + // put `browser` there, so Node-only packages failed to load: `msw/node` + // imports `@mswjs/interceptors/ClientRequest`, whose `browser` export is + // `null` (#125). jsdom: `client` keeps `browser` and `ssr` does not. + // node: neither gets it (server posture, unchanged). + const conditionsProbe = ` + import { resolveConfig } from 'vite'; + import solid from '@solidjs/vite-plugin'; + const out = {}; + for (const environment of ['jsdom', 'node']) { + const resolved = await resolveConfig( + { mode: 'test', configFile: false, logLevel: 'silent', plugins: [solid()], test: { environment } }, + 'serve', + ); + out[environment] = { + client: resolved.environments.client.resolve.conditions, + ssr: resolved.environments.ssr.resolve.conditions, + }; + } + console.log(JSON.stringify(out)); + `; + let testConditions; + let conditionsError = ''; + try { + testConditions = JSON.parse( + execFileSync(process.execPath, ['--input-type=module', '-e', conditionsProbe], { + cwd: exampleDir, + stdio: ['ignore', 'pipe', 'pipe'], + timeout: 60000, + }) + .toString() + .trim(), + ); + } catch (e) { + conditionsError = String(e.stderr || e); + } + const conditionsPass = + !conditionsError && + testConditions.jsdom.client.includes('browser') && + !testConditions.jsdom.ssr.includes('browser') && + !testConditions.node.ssr.includes('browser'); + record( + mode, + 'ssr-conditions', + 'jsdom tests keep the browser condition out of the ssr environment (Node --conditions)', + conditionsPass, + conditionsPass ? undefined : conditionsError || JSON.stringify(testConditions), + ); } // The CSP nonce on the request event (SSR_NONCE=1 wires src/middleware.ts, diff --git a/examples/start-ssr/vite.config.ts b/examples/start-ssr/vite.config.ts index 789d671..811e2af 100644 --- a/examples/start-ssr/vite.config.ts +++ b/examples/start-ssr/vite.config.ts @@ -111,9 +111,8 @@ export default defineConfig({ // DOM component tests under jsdom get the client posture (browser // conditions, dom codegen), while the node project gets the server // posture end to end (server conditions, isServer true, ssr codegen) - // just by writing `environment: 'node'`. Without the knob the config has - // no `test` block at all: the zero-config client-posture default stays - // under test too. + // just by writing `environment: 'node'`. Without the knob the config only + // picks jsdom: the client posture needs no other `test` options. ...(process.env.VITEST_PROJECTS ? { test: { @@ -137,7 +136,7 @@ export default defineConfig({ ], }, } - : {}), + : { test: { environment: 'jsdom' } }), ...(process.env.EXTRA_CLIENT_INPUT ? { environments: { diff --git a/src/index.ts b/src/index.ts index 02db8ea..c4df8b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1216,6 +1216,10 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { let projectRoot = process.cwd(); let isTestMode = false; let serverTestPosture = false; + // True when a jsdom or happy-dom test run inlines the Solid runtime. The + // `ssr` environment then does not get the `browser` condition (see + // `configEnvironment`). + let domTestRuntimeInlined = false; // Lets the dependency scanner crawl `.tsrx` imports. Registered on every // environment's `optimizeDeps` (top-level for client, configEnvironment @@ -1428,16 +1432,17 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { observe = options.observe === true; projectRoot = userConfig.root || projectRoot; isTestMode = userConfig.mode === 'test'; + domTestRuntimeInlined = false; forceProduction = options.dev === false && command === 'serve' && !isTestMode; // Per-vitest-project posture: the client posture (browser conditions, - // dom codegen, jsdom default) is right for DOM component tests but + // dom codegen) is right for DOM component tests but // wrong for server-runtime unit tests. A project that explicitly opts // into a server runtime — `test: { environment: 'node' }` (or // 'edge-runtime') — gets the server posture end to end: no browser // condition injection, so the framework resolves its real server // build (isServer true) with no inline/alias workarounds. DOM - // environments (the jsdom default, happy-dom, browser mode) keep the - // client posture. Each vitest project resolves its own config, so the + // environments (jsdom, happy-dom, browser mode) and an unset + // environment keep the client posture. Each vitest project resolves its own config, so the // hooks below see the posture of the project they serve. serverTestPosture = isTestMode && @@ -1549,30 +1554,10 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { ? [userTest.setupFiles] : userTest.setupFiles || []; - // Regardless of the app's `ssr` flag: tests run with the client - // posture (DOM component tests are the norm), so the default test - // environment is a DOM. Node-environment tests opt in explicitly. - // Browser-mode projects get the real browser DOM, so don't default - // them to jsdom — vitest probes for the environment's package at - // startup and fails the run if jsdom isn't installed. They fall - // back to vitest's own node default (no package probe). - // A root config that defines `test.projects` (or the pre-vitest-4 - // `test.workspace`) doesn't run tests itself: each project controls - // its own environment, so the root gets no jsdom default either — - // otherwise vitest probes for jsdom at the root on startup even when - // every project runs under node or in the browser. Note that an - // inline project with `extends: true` inherits the root file's - // `test.projects` and opts out too: projects declare their - // environment explicitly, as in vitest's own projects guide. - // https://github.com/solidjs/solid-vite-plugin/issues/205 - if ( - !userTest.environment && - !userTest.browser?.enabled && - !userTest.workspace && - !userTest.projects - ) { - test.environment = 'jsdom'; - } + // No `test.environment` is injected: the project picks its own + // (`jsdom` or `happy-dom` for DOM component tests). A default here + // also applied to every Node-only test suite that happened to load + // the plugin, which then ran under a DOM it never asked for. // Vitest 5 defaults `test.sharedViteServer` to true: inline projects // then reuse the root Vite server and take their `test` options from @@ -1626,6 +1611,14 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { }, }; } + const environment = userTest.environment ?? test.environment; + domTestRuntimeInlined = + !serverTestPosture && + !userTest.browser?.enabled && + (environment === 'jsdom' || environment === 'happy-dom') && + !userTest.server?.deps?.external?.find((item: string | RegExp) => + /solid-js/.test(item.toString()), + ); // jest-dom's DOM matchers have no place in a server-posture project; // vitest browser mode already has bundled jest-dom assertions // https://main.vitest.dev/guide/browser/assertion-api.html#assertion-api @@ -1749,7 +1742,20 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { // a server runtime (`test.environment: 'node'` / 'edge-runtime') // keep the default server conditions instead, so the framework's // real server build resolves (isServer true). - ...(isTestMode && !serverTestPosture && !opts.isSsrTargetWebworker ? ['browser'] : []), + // + // 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). jsdom and happy-dom tests run in the + // `client` environment with the runtime inlined, so `ssr` does not + // get `browser` in that case. + ...(isTestMode && + !serverTestPosture && + !opts.isSsrTargetWebworker && + !(domTestRuntimeInlined && name === 'ssr') + ? ['browser'] + : []), // Vite's `development|production` resolves to `development` in any // non-production mode, which would undo `dev: false` under `vite dev`. ...(forceProduction