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/fix-vitest-workspace-environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/vite-plugin': patch
---

Avoid overriding environments configured in Vitest workspaces and projects with the `jsdom` default (forward-port of #323 by @carloitaben, fixes #205). A root config that defines `test.projects` (or the pre-Vitest-4 `test.workspace`) runs no tests itself, so it no longer gets `test.environment: 'jsdom'` injected — which made Vitest probe for (and prompt to install) jsdom at startup even when every project runs under node or in browser mode. Each project keeps controlling its own environment.
5 changes: 5 additions & 0 deletions .changeset/jest-dom-resolve-from-project-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/vite-plugin': patch
---

Only inject the `@testing-library/jest-dom` Vitest setup file when the package resolves from the project root (forward-port of #364 by @brenelz, fixes #231). Previously the check ran from the plugin's own location, so with pnpm a transitive jest-dom (for example via Storybook) made Vitest fail with `Failed to load url .../@testing-library/jest-dom/vitest`. The probe now walks `node_modules` up from the Vite root the way Vitest resolves bare `setupFiles` — deliberately ignoring `NODE_PATH`, which pnpm's bin shims (`pnpm vitest`) point at the hoisted virtual store where every transitive dependency is reachable.
89 changes: 88 additions & 1 deletion examples/start-ssr/test/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
// [dev|prod|document|css-filter|entries|endpoint|configure|no-middleware|middleware|preview|render-mode|base|builder-order|builder-prepare|extra-input|babel-hmr|frames|external|detect|vitest|node]
// (default: all)

import { spawn, execSync } from 'node:child_process';
import { spawn, execSync, execFileSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { fileURLToPath, pathToFileURL } from 'node:url';
import path from 'node:path';
Expand Down Expand Up @@ -5204,6 +5204,93 @@ async function runVitestMode() {
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).
let projectsEnv;
let projectsEnvError = '';
try {
projectsEnv = await resolveTestEnvironment({
projects: [{ extends: true, test: { name: 'node', environment: 'node' } }],
});
} catch (e) {
projectsEnvError = String(e);
}
const projectsEnvPass = !projectsEnvError && projectsEnv === undefined;
record(
mode,
'projects-env',
'root config with test.projects gets no injected jsdom environment',
projectsEnvPass,
projectsEnvPass ? undefined : projectsEnvError || `environment: ${projectsEnv}`,
);

// The jest-dom setup file is injected only when the PROJECT can resolve
// `@testing-library/jest-dom` — by walking node_modules up from the Vite
// root, the way vitest itself resolves bare `setupFiles`. Two ways the
// probe used to give a false positive (solidjs/solid-vite-plugin#231,
// forward-port of #364): it ran from the plugin's own location, which under
// pnpm reaches any jest-dom that is a transitive dep somewhere in the tree
// (here: the workspace root's copy, which this repo's plugin can always
// resolve), and `require.resolve` honours NODE_PATH, which pnpm's bin
// shims (`pnpm vitest`) point at the hoisted virtual store. Both are
// simulated at once: an empty project outside the repo, resolved in a
// child process whose NODE_PATH is the repo's node_modules. Control: the
// example root (whose ancestors include the repo's copy) still injects.
const repoRoot = path.resolve(exampleDir, '..', '..');
const emptyRoot = path.join(os.tmpdir(), `solid-vite-plugin-jest-dom-${process.pid}`);
const probe = `
import { resolveConfig } from 'vite';
import solid from '@solidjs/vite-plugin';
const resolved = await resolveConfig(
{ root: process.argv[1], mode: 'test', configFile: false, logLevel: 'silent', plugins: [solid()] },
'serve',
);
console.log(JSON.stringify(resolved.test?.setupFiles ?? null));
`;
const resolveSetupFiles = (root) =>
JSON.parse(
execFileSync(process.execPath, ['--input-type=module', '-e', probe, '--', root], {
cwd: exampleDir,
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 60000,
env: { ...process.env, NODE_PATH: path.join(repoRoot, 'node_modules') },
})
.toString()
.trim(),
);
let emptySetupFiles;
let exampleSetupFiles;
let jestDomError = '';
try {
rmSync(emptyRoot, { recursive: true, force: true });
mkdirSync(emptyRoot, { recursive: true });
writeFileSync(path.join(emptyRoot, 'package.json'), '{ "name": "empty", "private": true }\n');
emptySetupFiles = resolveSetupFiles(emptyRoot);
exampleSetupFiles = resolveSetupFiles(exampleDir);
} catch (e) {
jestDomError = String(e.stderr || e);
} finally {
rmSync(emptyRoot, { recursive: true, force: true });
}
const jestDomPass =
!jestDomError &&
emptySetupFiles === null &&
Array.isArray(exampleSetupFiles) &&
exampleSetupFiles.includes('@testing-library/jest-dom/vitest');
record(
mode,
'jest-dom-root',
'jest-dom setup file is injected only when the project root resolves it (not the plugin, not NODE_PATH)',
jestDomPass,
jestDomPass
? undefined
: jestDomError ||
`empty root: ${JSON.stringify(emptySetupFiles)}, example root: ${JSON.stringify(exampleSetupFiles)}`,
);
}

const ALL_MODES = [
Expand Down
85 changes: 68 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ import {
import { getEnvironmentConsumer, isRunnableEnvironment } from './environment.js';
import { crawlFrameworkPkgs } from 'vitefu';

const require = createRequire(import.meta.url);

/**
* The `lazy()` module-URL placeholder contract, shared with the native
* compiler's `transformLazy` pass: `lazy(() => import("spec"))` calls gain a
Expand Down Expand Up @@ -499,19 +497,55 @@ function containsSolidField(fields: Record<string, any>) {
return false;
}

function getJestDomExport(setupFiles: string[]) {
return setupFiles?.some((path) => /jest-dom/.test(path))
? undefined
: ['@testing-library/jest-dom/vitest', '@testing-library/jest-dom/extend-expect'].find(
(path) => {
try {
require.resolve(path);
return true;
} catch (e) {
return false;
}
},
);
/**
* Locate a bare package the way Vite (and Node without `NODE_PATH`) does: walk
* `<dir>/node_modules/<name>` up from `root`. `require.resolve` can't be used
* for this — it also consults `NODE_PATH`, which pnpm's bin shims (`pnpm
* vitest`, `pnpm test`) point at the hoisted virtual store
* (`node_modules/.pnpm/node_modules`), where every transitive dependency of the
* whole tree is reachable.
*/
function findPackageDir(name: string, root: string): string | undefined {
let dir = root;
while (true) {
const candidate = path.join(dir, 'node_modules', name);
if (existsSync(path.join(candidate, 'package.json'))) return candidate;
const parent = path.dirname(dir);
if (parent === dir) return undefined;
dir = parent;
}
}

function getJestDomExport(setupFiles: string[], root: string) {
if (setupFiles?.some((file) => /jest-dom/.test(file))) return undefined;

// Resolve from the project root, not from this plugin's own location. With pnpm's
// isolated node_modules layout the plugin can reach a jest-dom that only exists as a
// transitive dependency (e.g. of Storybook), while Vitest resolves `setupFiles` from the
// project root, where it isn't installed, and fails to load it.
// https://github.com/solidjs/solid-vite-plugin/issues/231
// The bare specifier (not the resolved path) is injected on purpose: `require.resolve` picks
// jest-dom's CommonJS entry, which Vitest refuses to load, while Vitest itself resolves the
// specifier to the ESM entry.
const packageDir = findPackageDir('@testing-library/jest-dom', root);
if (!packageDir) return undefined;
// Check the subpath against THIS copy: resolving from inside the package
// self-references its `exports` map (v6+), or falls back to its own files
// for versions without one (v5's `extend-expect`). `NODE_PATH` still
// participates in that lookup, so make sure the hit landed in the package
// Vite will resolve rather than in some hoisted copy of another version.
const realPackageDir = realpathSync(packageDir);
const packageRequire = createRequire(path.join(packageDir, 'package.json'));
return ['@testing-library/jest-dom/vitest', '@testing-library/jest-dom/extend-expect'].find(
(specifier) => {
try {
const resolved = realpathSync(packageRequire.resolve(specifier));
return resolved.startsWith(realPackageDir + path.sep);
} catch (e) {
return false;
}
},
);
}

function getSolidOptions(
Expand Down Expand Up @@ -1140,7 +1174,21 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
// 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).
if (!userTest.environment && !userTest.browser?.enabled) {
// 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';
}

Expand All @@ -1165,7 +1213,10 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
// vitest browser mode already has bundled jest-dom assertions
// https://main.vitest.dev/guide/browser/assertion-api.html#assertion-api
if (!userTest.browser?.enabled && !serverTestPosture) {
const jestDomImport = getJestDomExport(userSetupFiles);
const jestDomImport = getJestDomExport(
userSetupFiles,
path.resolve(projectRoot || process.cwd()),
);
if (jestDomImport) {
test.setupFiles = [jestDomImport];
}
Expand Down
Loading