@nx/vitest 23.3.0-pr.37068.f95e11e → 23.3.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  [![Semantic Release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](<>)
13
13
  [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
14
14
  [![Join us on the Official Nx Discord Server](https://img.shields.io/discord/1143497901675401286?label=discord)](https://go.nx.dev/community)
15
- [![Nx Sandboxing](https://staging.nx.app/workspaces/62d013ea0852fe0a2df74438/sandbox-badge.svg)](https://nx.dev/docs/features/ci-features/sandboxing)
15
+ [![Hours saved](https://img.shields.io/endpoint?url=https%3A%2F%2Fstaging.nx.app%2Fnx-cloud%2Fbadge%2F62d013ea0852fe0a2df74438%2Fhours-saved.json)](https://nx.dev/nx-cloud)
16
16
 
17
17
  </div>
18
18
 
@@ -1,5 +1,4 @@
1
- import type { RunnerTestFile } from 'vitest/node';
2
- import type { Reporter } from 'vitest/reporters';
1
+ import type { Reporter, RunnerTestFile } from 'vitest/node';
3
2
  export declare class NxReporter implements Reporter {
4
3
  private watch;
5
4
  deferred: {
@@ -66,6 +66,14 @@ async function configurationGeneratorInternal(tree, schema, hasPlugin = false) {
66
66
  const uiFramework = schema.uiFramework ?? (await (0, detect_ui_framework_1.detectUiFramework)(schema.project));
67
67
  const isRootProject = root === '.';
68
68
  tasks.push(await (0, js_1.initGenerator)(tree, { ...schema, skipFormat: true }));
69
+ // Written before init so version selection sees it: analog peers vitest 4,
70
+ // and init is what picks the vitest version.
71
+ if (uiFramework === 'angular' && !schema.skipPackageJson) {
72
+ tasks.push((0, devkit_1.addDependenciesToPackageJson)(tree, {}, {
73
+ '@analogjs/vitest-angular': versions_1.analogVitestAngular,
74
+ '@analogjs/vite-plugin-angular': versions_1.analogVitestAngular,
75
+ }, undefined, true));
76
+ }
69
77
  const initTask = await (0, init_1.default)(tree, {
70
78
  skipFormat: true,
71
79
  addPlugin: schema.addPlugin,
@@ -196,7 +204,8 @@ getTestBed().initTestEnvironment(
196
204
  // workspace files in favor of inlining the projects into a root vitest.config
197
205
  // via `test.projects` (https://vitest.dev/guide/migration.html#workspace-is-replaced-with-projects).
198
206
  // Emit that shape for vitest 4+ and when the installed version can't be
199
- // detected (new installs resolve to v4); vitest 3 keeps the workspace file.
207
+ // detected (a new install resolves to 4 or 5); vitest 3 keeps the workspace
208
+ // file.
200
209
  if (!isRootProject) {
201
210
  const projectGlobs = `'**/vite.config.{mjs,js,ts,mts}', '**/vitest.config.{mjs,js,ts,mts}'`;
202
211
  const vitestMajorVersion = (0, versions_1.getInstalledVitestMajorVersion)(tree);
@@ -5,6 +5,7 @@ exports.updateNxJsonSettings = updateNxJsonSettings;
5
5
  exports.initGenerator = initGenerator;
6
6
  const internal_1 = require("@nx/devkit/internal");
7
7
  const devkit_1 = require("@nx/devkit");
8
+ const semver_1 = require("semver");
8
9
  const versions_1 = require("../../utils/versions");
9
10
  const plugin_1 = require("../../plugins/plugin");
10
11
  const version_utils_1 = require("../../utils/version-utils");
@@ -23,9 +24,19 @@ function updateDependencies(tree, schema) {
23
24
  : installedMajor === 7
24
25
  ? versions_1.viteV7Version
25
26
  : versions_1.viteVersion;
27
+ // Vite below 8 depends on esbuild (8 bundles rolldown instead), whose install
28
+ // script only validates the prebuilt binary that ships as an optional
29
+ // dependency.
30
+ if ((0, semver_1.major)((0, semver_1.coerce)(viteVersionToUse)) < 8) {
31
+ (0, internal_1.acknowledgeBuildScripts)(tree, (0, devkit_1.detectPackageManager)(tree.root), {
32
+ esbuild: false,
33
+ });
34
+ }
26
35
  return (0, devkit_1.addDependenciesToPackageJson)(tree, {}, {
27
36
  '@nx/vitest': versions_1.nxVersion,
28
- vitest: versions_1.vitestVersion,
37
+ // The range about to be installed is passed for the case where the
38
+ // workspace has no vite of its own; it constrains the vitest major.
39
+ vitest: (0, versions_1.versions)(tree, { viteRange: viteVersionToUse }).vitestVersion,
29
40
  vite: viteVersionToUse,
30
41
  }, undefined, schema.keepExistingVersions ?? true);
31
42
  }
@@ -0,0 +1,92 @@
1
+ # Vitest 5 migration
2
+
3
+ The deterministic pre-pass already handled the mechanical parts: removed deep
4
+ entry points were repointed where a plain import or export declaration reached
5
+ them, `.vitest` was added to `.gitignore`, and `vite` was declared in
6
+ `package.json` if the workspace had been relying on Vitest 4 pulling it in.
7
+
8
+ Anything it could not decide for itself is in the context it passed you. Work
9
+ through that list first, then run the tests.
10
+
11
+ Most of what is left is runtime behavior. Do not try to find it by reading every
12
+ test file. Run the test suites, then work through the failures with the list
13
+ below, which covers the changes that produce confusing failures.
14
+
15
+ ## Run the tests first
16
+
17
+ ```bash
18
+ nx run-many -t test
19
+ ```
20
+
21
+ ## What the failures probably mean
22
+
23
+ **Mocks look empty.** `clearMocks` now defaults to `true`, so `mock.calls`,
24
+ `mock.instances`, `mock.contexts` and `mock.results` reset before every test. A
25
+ test that asserts on calls recorded by an earlier test now sees nothing. Fix the
26
+ test, or set `clearMocks: false` in the config to keep the old behavior.
27
+
28
+ **"Cannot call vi.mock inside ...".** `vi.mock`, `vi.unmock` and `vi.hoisted`
29
+ now throw when called outside the module top level. In v4 this only warned. Move
30
+ the call to the top level, or switch to `vi.doMock`.
31
+
32
+ **A test fails that used to pass silently.** An unawaited `resolves`, `rejects`
33
+ or `toMatchFileSnapshot` now fails the test instead of being ignored. Add the
34
+ missing `await`.
35
+
36
+ **`expect.poll` times out.** It now fails when the callback does not settle
37
+ within `timeout`, rather than hanging on the last value.
38
+
39
+ **A `toThrow('')` assertion changed meaning.** An empty string now matches any
40
+ message. Use `/^$/` to assert on an empty message.
41
+
42
+ **Snapshot names moved.** String values interpolated through `$` in
43
+ `test.for`/`test.each` titles are no longer wrapped in quotes, and truncation is
44
+ now controlled by `taskTitleValueFormatTruncate` (default 40).
45
+
46
+ **A CI filter matches nothing.** `-t` and `testNamePattern` now join name
47
+ segments with `' > '` instead of a space.
48
+
49
+ **Per-worker resources collide.** `VITEST_POOL_ID` and `VITEST_WORKER_ID` are
50
+ now 1-based. Any logic deriving a database name, port or directory from them
51
+ shifts by one.
52
+
53
+ **Vitest cannot find a config.** It no longer searches ancestor directories.
54
+ Pass `--config` explicitly, or give the directory its own config.
55
+
56
+ **Projects behave differently.** Inline projects now default to `extends: true`
57
+ and share the root Vite server (`sharedViteServer`). Set either to `false` if a
58
+ project needs its own resolution.
59
+
60
+ ## Check these without running anything
61
+
62
+ - `sequential`, if the pre-pass flagged any. `test.sequential()` and
63
+ `describe.sequential()` are gone. When nothing makes the test concurrent, drop
64
+ the modifier: `test.sequential('a', ...)` becomes `test('a', ...)`. When it was
65
+ opting out of a concurrent suite or a concurrent setting in the config, it
66
+ becomes the `{ concurrent: false }` option. The same applies to a `sequential`
67
+ entry in a test's options object.
68
+ - `vitest/suite` and `vitest/runners` imports, if the pre-pass flagged any. Their
69
+ exports did not move as a group: `getCurrentSuite` and `createTaskCollector` are
70
+ static members of `TestRunner` (exported from `vitest`), `VitestTestRunner` is
71
+ exported from `vitest`, the `VitestRunner` type is in `vitest/runtime`, and
72
+ `NodeBenchmarkRunner`, `getBenchFn` and `getBenchOptions` went with the old
73
+ benchmark API. Rebind the use sites rather than repointing the import.
74
+ - `@vitest/runner` and `@vitest/ws-client` in `package.json`: neither is published
75
+ for v5. Their exports scattered. The hooks and `describe`/`it`/`test` come from
76
+ `vitest`, the collection helpers are now static members of `TestRunner`
77
+ (`TestRunner.getCurrentSuite()`, `TestRunner.getCurrentTest()`), and only the
78
+ `VitestRunner` type and the environment and snapshot helpers are in
79
+ `vitest/runtime`. `collectTests`, `startTests` and `updateTask` have no public
80
+ replacement.
81
+ - Benchmarks: the module-level `bench()` API is gone, along with
82
+ `benchmark.reporters`, `benchmark.outputFile`, `benchmark.compare` and the
83
+ `--compare` CLI flag. `bench` now comes off the test context.
84
+ - `coverage.thresholds` under a glob no longer inherits the top-level `perFile`.
85
+ Set it on each glob that needs it.
86
+ - Browser mode, if the workspace configures it by hand: locators are strict by
87
+ default, `toHaveTextContent` no longer accepts a RegExp (`toMatchTextContent`
88
+ does), browser commands receive a locator object rather than a selector string,
89
+ automocked modules return `undefined`, and `browser.api` moved to top-level
90
+ `api`.
91
+
92
+ The full list is at https://vitest.dev/guide/migration.
@@ -0,0 +1,14 @@
1
+ import { type Tree } from '@nx/devkit';
2
+ /**
3
+ * Hybrid migration paired with `ai-instructions-for-vitest-5.md`. Applies the
4
+ * handful of Vitest 4 -> 5 changes that have one correct answer and forwards
5
+ * everything else to the paired prompt.
6
+ *
7
+ * The apply set is deliberately small: the rest of v5's breaking changes are
8
+ * runtime semantics (mock clearing, unawaited assertions, hoisting) that a
9
+ * test run surfaces far better than a codemod can guess at.
10
+ */
11
+ export default function migrateToVitest5(tree: Tree): Promise<{
12
+ nextSteps?: string[];
13
+ agentContext?: string[];
14
+ }>;
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = migrateToVitest5;
4
+ const devkit_1 = require("@nx/devkit");
5
+ const internal_1 = require("@nx/devkit/internal");
6
+ const ignore_vitest_temp_files_1 = require("../../utils/ignore-vitest-temp-files");
7
+ const semver_1 = require("semver");
8
+ const tsquery_1 = require("@phenomnomnominal/tsquery");
9
+ const TEST_CALLEES = new Set(['test', 'it', 'describe', 'suite', 'bench']);
10
+ /**
11
+ * Hybrid migration paired with `ai-instructions-for-vitest-5.md`. Applies the
12
+ * handful of Vitest 4 -> 5 changes that have one correct answer and forwards
13
+ * everything else to the paired prompt.
14
+ *
15
+ * The apply set is deliberately small: the rest of v5's breaking changes are
16
+ * runtime semantics (mock clearing, unawaited assertions, hoisting) that a
17
+ * test run surfaces far better than a codemod can guess at.
18
+ */
19
+ async function migrateToVitest5(tree) {
20
+ const unhandled = [];
21
+ (0, ignore_vitest_temp_files_1.addVitestTempFilesToGitIgnore)(tree);
22
+ const addedVite = declareVitePeerDependency(tree);
23
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
24
+ if (!isJsOrTsFile(filePath))
25
+ return;
26
+ const contents = tree.read(filePath, 'utf-8');
27
+ if (!contents)
28
+ return;
29
+ const hasEntryPoints = contents.includes('vitest/');
30
+ const hasSequential = contents.includes('sequential');
31
+ if (!hasEntryPoints && !hasSequential)
32
+ return;
33
+ const sourceFile = (0, tsquery_1.ast)(contents);
34
+ if (hasEntryPoints) {
35
+ rewriteRemovedEntryPoints(tree, filePath, contents, sourceFile, unhandled);
36
+ }
37
+ if (hasSequential) {
38
+ detectSequentialUsage(filePath, sourceFile, unhandled);
39
+ }
40
+ });
41
+ await (0, devkit_1.formatFiles)(tree);
42
+ const result = {};
43
+ if (addedVite) {
44
+ result.nextSteps = [
45
+ `Added \`vite\` to the root \`package.json\` devDependencies, matching the version already resolved in the workspace. Vitest 4 depended on Vite directly; Vitest 5 only declares it as a required peer, so a workspace that relied on the transitive copy has none after the upgrade.`,
46
+ ];
47
+ }
48
+ if (unhandled.length > 0)
49
+ result.agentContext = unhandled;
50
+ return result;
51
+ }
52
+ // Fallback only. The workspace's own resolved vite is preferred, since moving
53
+ // a 6.4 or 7.x workspace to 8 is a bundler major riding inside a test upgrade.
54
+ const FALLBACK_VITE_VERSION = '^8.0.0';
55
+ /**
56
+ * Vitest 4 listed `vite` in `dependencies`, so workspaces that only ran tests
57
+ * never had to declare it. Vitest 5 dropped it to a required peer.
58
+ */
59
+ function declareVitePeerDependency(tree) {
60
+ if (!tree.exists('package.json'))
61
+ return false;
62
+ const packageJson = (0, devkit_1.readJson)(tree, 'package.json');
63
+ const hasVitest = packageJson.dependencies?.['vitest'] ??
64
+ packageJson.devDependencies?.['vitest'];
65
+ if (!hasVitest)
66
+ return false;
67
+ if (packageJson.dependencies?.['vite'] ??
68
+ packageJson.devDependencies?.['vite'])
69
+ return false;
70
+ const installedVite = (0, internal_1.getInstalledPackageVersion)('vite');
71
+ (0, devkit_1.updateJson)(tree, 'package.json', (json) => {
72
+ json.devDependencies ??= {};
73
+ json.devDependencies['vite'] = installedVite
74
+ ? `^${(0, semver_1.major)(installedVite)}.0.0`
75
+ : FALLBACK_VITE_VERSION;
76
+ return json;
77
+ });
78
+ return true;
79
+ }
80
+ const TS_JS_RE = /\.[cm]?[jt]sx?$/;
81
+ function isJsOrTsFile(filePath) {
82
+ return TS_JS_RE.test(filePath);
83
+ }
84
+ /**
85
+ * v5 removed the deprecated deep entry points. A specifier is only rewritten
86
+ * when every one of its v4 exports landed in a single v5 module; the rest are
87
+ * described for the agent, because their exports scattered or disappeared.
88
+ */
89
+ const REMOVED_ENTRY_POINTS = {
90
+ 'vitest/reporters': {
91
+ replacement: 'vitest/node',
92
+ note: 'its reporter exports are now in `vitest/node`',
93
+ },
94
+ 'vitest/coverage': {
95
+ replacement: 'vitest/node',
96
+ note: '`BaseCoverageProvider` is now in `vitest/node`',
97
+ },
98
+ 'vitest/snapshot': {
99
+ replacement: 'vitest/runtime',
100
+ note: 'its snapshot exports are now in `vitest/runtime`',
101
+ },
102
+ 'vitest/environments': {
103
+ replacement: 'vitest/runtime',
104
+ note: 'its environment exports are now in `vitest/runtime`',
105
+ },
106
+ 'vitest/suite': {
107
+ note: 'its exports did not survive as module exports. `getCurrentSuite` and `createTaskCollector` are static members of `TestRunner` (exported from `vitest`), and the rest were removed. Rebind the use sites rather than repointing the import',
108
+ },
109
+ 'vitest/runners': {
110
+ note: 'its exports split up. `VitestTestRunner` is exported from `vitest`, `VitestRunner` from `vitest/runtime`, and `NodeBenchmarkRunner` was removed with the old benchmark API',
111
+ },
112
+ };
113
+ /**
114
+ * Exports the v5 benchmark rewrite removed outright. A file importing one of
115
+ * these from `vitest/reporters` cannot simply be repointed.
116
+ */
117
+ const REMOVED_SYMBOLS = new Set([
118
+ 'BenchmarkBuiltinReporters',
119
+ 'BenchmarkReporter',
120
+ 'BenchmarkReportsMap',
121
+ 'VerboseBenchmarkReporter',
122
+ 'NodeBenchmarkRunner',
123
+ ]);
124
+ /**
125
+ * Only import and export declarations are rewritten. A removed specifier
126
+ * reached any other way still has to go, so name it for the agent.
127
+ */
128
+ function reportUnrewritableSpecifiers(sourceFile, moduleSpecifiers, filePath, unhandled) {
129
+ const specifierStarts = new Set(moduleSpecifiers.map((node) => node.getStart(sourceFile)));
130
+ const stragglers = new Set((0, tsquery_1.query)(sourceFile, 'StringLiteral')
131
+ .filter((node) => node.text in REMOVED_ENTRY_POINTS &&
132
+ !specifierStarts.has(node.getStart(sourceFile)))
133
+ .map((node) => node.text));
134
+ for (const specifier of stragglers) {
135
+ unhandled.push(`${filePath} references \`${specifier}\` outside an import or export declaration. Vitest 5 removed it: ${REMOVED_ENTRY_POINTS[specifier].note}.`);
136
+ }
137
+ }
138
+ function rewriteRemovedEntryPoints(tree, filePath, contents, sourceFile, unhandled) {
139
+ const specifiers = (0, tsquery_1.query)(sourceFile, 'ImportDeclaration > StringLiteral, ExportDeclaration > StringLiteral');
140
+ reportUnrewritableSpecifiers(sourceFile, specifiers, filePath, unhandled);
141
+ const opaqueStarts = new Set((0, tsquery_1.query)(sourceFile, OPAQUE_DECLARATIONS).map((node) => node.getStart(sourceFile)));
142
+ let updated = contents;
143
+ let changed = false;
144
+ // Right-to-left so earlier offsets stay valid as the text shifts.
145
+ for (const specifier of [...specifiers].reverse()) {
146
+ const entryPoint = REMOVED_ENTRY_POINTS[specifier.text];
147
+ if (!entryPoint)
148
+ continue;
149
+ const blocker = whyNotRewritable(specifier, entryPoint, opaqueStarts, sourceFile);
150
+ if (blocker) {
151
+ unhandled.push(`${filePath} imports from \`${specifier.text}\`, which Vitest 5 removed: ${blocker}.`);
152
+ continue;
153
+ }
154
+ updated =
155
+ updated.slice(0, specifier.getStart(sourceFile) + 1) +
156
+ entryPoint.replacement +
157
+ updated.slice(specifier.getEnd() - 1);
158
+ changed = true;
159
+ }
160
+ if (changed)
161
+ tree.write(filePath, updated);
162
+ }
163
+ /**
164
+ * Declarations that take a whole module instead of naming what they use, so
165
+ * there is no symbol list to check against the ones Vitest 5 removed.
166
+ */
167
+ const OPAQUE_DECLARATIONS = [
168
+ 'ImportDeclaration:has(NamespaceImport) > StringLiteral',
169
+ 'ExportDeclaration:has(NamespaceExport) > StringLiteral',
170
+ 'ImportDeclaration:has(ImportClause > Identifier) > StringLiteral',
171
+ 'ExportDeclaration:not(:has(ExportSpecifier)) > StringLiteral',
172
+ ].join(', ');
173
+ /** The reason this specifier cannot be repointed, if there is one. */
174
+ function whyNotRewritable(specifier, entryPoint, opaqueStarts, sourceFile) {
175
+ if (!entryPoint.replacement) {
176
+ return entryPoint.note;
177
+ }
178
+ if (opaqueStarts.has(specifier.getStart(sourceFile))) {
179
+ return `${entryPoint.note}, but this declaration takes the whole module rather than naming what it uses, so the symbols cannot be checked here`;
180
+ }
181
+ const removed = boundNamesOf(specifier).filter((name) => REMOVED_SYMBOLS.has(name));
182
+ return removed.length
183
+ ? `\`${removed.join('`, `')}\` no longer exists anywhere, it went with the old benchmark API`
184
+ : undefined;
185
+ }
186
+ function boundNamesOf(specifier) {
187
+ return (0, tsquery_1.query)(specifier.parent, 'ImportSpecifier, ExportSpecifier').map((element) => (element.propertyName ?? element.name).text);
188
+ }
189
+ /**
190
+ * `test.sequential()` / `describe.sequential()` and the `sequential` test
191
+ * option are gone in v5. The replacement depends on whether the test inherits
192
+ * concurrency from a suite or the config, so the agent decides rather than the
193
+ * codemod.
194
+ */
195
+ function detectSequentialUsage(filePath, sourceFile, unhandled) {
196
+ // Both AST forms of a property key, and only in an options object handed to
197
+ // a test or suite call. A loose `{ sequential: true }` is someone else's API.
198
+ const optionKeys = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment > :matches(Identifier[name=sequential], StringLiteral[value=sequential])').filter((key) => {
199
+ // PropertyAssignment < ObjectLiteralExpression < CallExpression.
200
+ const call = key.parent?.parent?.parent;
201
+ return (!!call &&
202
+ 'arguments' in call &&
203
+ TEST_CALLEES.has(call.expression.getText(sourceFile).split('.')[0]));
204
+ });
205
+ const modifiers = (0, tsquery_1.query)(sourceFile, 'PropertyAccessExpression:has(Identifier[name=sequential])').filter((node) => node.name.text === 'sequential' &&
206
+ TEST_CALLEES.has(node.expression.getText(sourceFile)));
207
+ const usages = [...optionKeys, ...modifiers];
208
+ if (usages.length === 0)
209
+ return;
210
+ unhandled.push(`${filePath} uses \`sequential\`, which Vitest 5 removed. Replace \`test.sequential(...)\` / \`describe.sequential(...)\` with a plain \`test(...)\` / \`describe(...)\` when nothing makes them concurrent, and with the \`{ concurrent: false }\` option when they are opting out of a concurrent suite or a concurrent setting in the config.`);
211
+ }
@@ -9,6 +9,7 @@ const node_fs_1 = require("node:fs");
9
9
  const promises_1 = require("node:fs/promises");
10
10
  const node_path_1 = require("node:path");
11
11
  const executor_utils_1 = require("../utils/executor-utils");
12
+ const setup_file_inputs_1 = require("./setup-file-inputs");
12
13
  /**
13
14
  * @deprecated The 'createDependencies' function is now a no-op. This functionality is included in 'createNodesV2'.
14
15
  */
@@ -42,6 +43,7 @@ exports.createNodes = [
42
43
  lockfile,
43
44
  ...(tsconfigChainsByProjectRoot.get(root) ?? []),
44
45
  ]));
46
+ const setupTsconfigJsonCache = new Map();
45
47
  try {
46
48
  return await (0, devkit_1.createNodesFromFiles)(async (configFile, _, context, idx) => {
47
49
  const projectRoot = (0, node_path_1.dirname)(configFile);
@@ -50,7 +52,10 @@ exports.createNodes = [
50
52
  // Adding the config file path to the hash ensures that the final hash value is different
51
53
  // for different config files.
52
54
  const hash = hashes[idx] + configFile;
53
- if (!targetsCache.has(hash)) {
55
+ const cachedTargets = targetsCache.get(hash);
56
+ // Older cache entries predate `setupFileCandidates`.
57
+ if (cachedTargets === undefined ||
58
+ (cachedTargets && !cachedTargets.setupFileCandidates)) {
54
59
  const result = await buildVitestTargets(configFile, projectRoot, normalizedOptions, context, pmc, tsconfigChainsByProjectRoot.get(projectRoot) ?? []);
55
60
  // Cache the result even when it's null (a root orchestrator config)
56
61
  // so the config isn't re-resolved on every project-graph build.
@@ -62,10 +67,12 @@ exports.createNodes = [
62
67
  if (!cached) {
63
68
  return { projects: {} };
64
69
  }
65
- const { metadata, targets } = cached;
70
+ const { metadata, targets, setupFileCandidates } = cached;
66
71
  const project = {
67
72
  root: projectRoot,
68
- targets,
73
+ // Resolved on every run: the cache key does not cover setup files
74
+ // or their tsconfigs.
75
+ targets: withSetupFileInputs(targets, normalizedOptions.testTargetName, setupFileCandidates, projectRoot, context.workspaceRoot, setupTsconfigJsonCache),
69
76
  metadata,
70
77
  };
71
78
  return {
@@ -92,18 +99,6 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
92
99
  catch {
93
100
  // do nothing
94
101
  }
95
- // Workaround for race condition with ESM-only Vite plugins (e.g. @vitejs/plugin-vue@6+)
96
- // If vite.config.ts is compiled as CJS, then when both require('@vitejs/plugin-vue') and import('@vitejs/plugin-vue')
97
- // are pending in the same process, Node will throw an error:
98
- // Error [ERR_INTERNAL_ASSERTION]: Cannot require() ES Module @vitejs/plugin-vue/dist/index.js because it is not yet fully loaded.
99
- // This may be caused by a race condition if the module is simultaneously dynamically import()-ed via Promise.all().
100
- try {
101
- const importVuePlugin = () => new Function('return import("@vitejs/plugin-vue")')();
102
- await importVuePlugin();
103
- }
104
- catch {
105
- // Plugin not installed or not needed, ignore
106
- }
107
102
  // Workaround for race condition with vitest/node on Node 24+
108
103
  // When multiple vitest.config files are processed in parallel, Node can throw:
109
104
  // Error [ERR_INTERNAL_ASSERTION]: Cannot require() ES Module vitest/dist/node.js
@@ -117,10 +112,26 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
117
112
  // vitest/node not available or not needed, ignore
118
113
  }
119
114
  const { resolveConfig } = await (0, executor_utils_1.loadViteDynamicImport)();
120
- const viteBuildConfig = await resolveConfig({
115
+ // Vite fills a missing `root` with `process.cwd()`, which at graph time is
116
+ // the workspace root - not where the task runs. Capture what the config
117
+ // actually authored so a relative path can be resolved the way Vitest will.
118
+ let authoredViteRoot;
119
+ const viteBuildConfig = await (0, internal_1.retryOnRequireEsmRace)(() => resolveConfig({
121
120
  configFile: absoluteConfigFilePath,
122
121
  mode: 'development',
123
- }, 'build');
122
+ plugins: [
123
+ {
124
+ name: 'nx-capture-authored-vitest-root',
125
+ enforce: 'post',
126
+ config: {
127
+ order: 'post',
128
+ handler(config) {
129
+ authoredViteRoot = config.root;
130
+ },
131
+ },
132
+ },
133
+ ],
134
+ }, 'build'));
124
135
  // A root config that aggregates project configs via `test.projects` is just an
125
136
  // orchestrator; the actual tests live in the individual project configs. Skip it
126
137
  // entirely so it does not register a project rooted at the workspace root (which
@@ -134,9 +145,11 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
134
145
  const { testOutputs, hasTest } = getOutputs(viteBuildConfig, projectRoot, context.workspaceRoot);
135
146
  const namedInputs = (0, internal_1.getNamedInputs)(projectRoot, context);
136
147
  const targets = {};
148
+ let setupFileCandidates = [];
137
149
  // if file is vitest.config or vite.config has definition for test, create targets for test and/or atomized tests
138
150
  if (configFilePath.includes('vitest.config') || hasTest) {
139
151
  const isTypecheckEnabled = !!viteBuildConfig.test?.typecheck?.enabled;
152
+ setupFileCandidates = (0, setup_file_inputs_1.resolveSetupFileCandidates)(viteBuildConfig, projectRoot, context.workspaceRoot, authoredViteRoot);
140
153
  targets[options.testTargetName] = await testTarget(namedInputs, testOutputs, projectRoot, options.testMode, pmc, isTypecheckEnabled, tsconfigInputs);
141
154
  if (options.ciTargetName) {
142
155
  const groupName = options.ciGroupName ?? (0, internal_1.deriveGroupNameFromTarget)(options.ciTargetName);
@@ -160,7 +173,7 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
160
173
  // Capture the raw root after user hooks: graph construction and the
161
174
  // atom run resolve it against different cwds.
162
175
  let configuredViteRoot;
163
- const viteServeConfig = await resolveConfig({
176
+ const viteServeConfig = await (0, internal_1.retryOnRequireEsmRace)(() => resolveConfig({
164
177
  configFile: absoluteConfigFilePath,
165
178
  mode: 'test',
166
179
  plugins: [
@@ -186,7 +199,7 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
186
199
  },
187
200
  },
188
201
  ],
189
- }, 'serve');
202
+ }, 'serve'));
190
203
  const projectRootRelativeTestPaths = await getTestPathsRelativeToProjectRoot(projectRoot, context.workspaceRoot,
191
204
  // Only the glob path reads this config; the opt-out path forces the
192
205
  // runtime by receiving no config, and takes `test.dir` separately.
@@ -300,7 +313,28 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
300
313
  }
301
314
  }
302
315
  }
303
- return { targets, metadata };
316
+ return { targets, metadata, setupFileCandidates };
317
+ }
318
+ function withSetupFileInputs(targets, testTargetName, setupFileCandidates, projectRoot, workspaceRoot, jsonCache) {
319
+ if (setupFileCandidates.length === 0)
320
+ return targets;
321
+ const { files, tsconfigs } = (0, setup_file_inputs_1.collectSetupFileInputs)(setupFileCandidates, projectRoot, workspaceRoot, jsonCache);
322
+ if (files.length === 0 && tsconfigs.length === 0)
323
+ return targets;
324
+ const setupInputs = [
325
+ ...tsconfigs.map((f) => ({
326
+ json: `{workspaceRoot}/${f}`,
327
+ fields: ['compilerOptions'],
328
+ })),
329
+ // Whole-file, not just `compilerOptions`: these are sources Vitest
330
+ // executes, so any change to them changes the run.
331
+ ...files.map((f) => `{workspaceRoot}/${f}`),
332
+ ];
333
+ const inputs = [...targets[testTargetName].inputs, ...setupInputs];
334
+ return Object.fromEntries(Object.entries(targets).map(([name, target]) => [
335
+ name,
336
+ { ...target, inputs },
337
+ ]));
304
338
  }
305
339
  async function testTarget(namedInputs, outputs, projectRoot, testMode = 'watch', pmc, isTypecheckEnabled, tsconfigInputs) {
306
340
  const command = testMode === 'run' ? 'vitest run' : 'vitest';
@@ -408,7 +442,7 @@ function normalizeOutputPath(outputPath, projectRoot, workspaceRoot, path) {
408
442
  }
409
443
  else {
410
444
  if ((0, node_path_1.isAbsolute)(outputPath)) {
411
- return `{workspaceRoot}/${(0, node_path_1.relative)(workspaceRoot, outputPath)}`;
445
+ return `{workspaceRoot}/${(0, devkit_1.normalizePath)((0, node_path_1.relative)(workspaceRoot, outputPath))}`;
412
446
  }
413
447
  else {
414
448
  if (outputPath.startsWith('..')) {
@@ -0,0 +1,24 @@
1
+ import type { RawTsconfigJsonCache } from '@nx/js/internal';
2
+ import type { ResolvedConfig } from 'vite';
3
+ /**
4
+ * Resolves the files Vitest loads from outside the project root - `setupFiles`
5
+ * and `globalSetup`. `collectSetupFileInputs` declares the ones that exist,
6
+ * together with the tsconfigs Vite reads to transform them.
7
+ *
8
+ * Reads the build-resolved config, as the inferred target's inputs are built
9
+ * from it. Setup files contributed by an `apply: 'serve'` plugin are therefore
10
+ * invisible here - the same build/serve divergence the atomized path documents.
11
+ *
12
+ * Solution-style tsconfigs are deliberately not followed. When the nearest
13
+ * tsconfig of a setup file is a solution file, Vite walks its `references` and
14
+ * reads every project's tsconfig; declaring those would attach one input per
15
+ * project in the workspace. Keep the setup file inside a project (or beside a
16
+ * leaf tsconfig) instead.
17
+ */
18
+ export declare function resolveSetupFileCandidates(viteConfig: ResolvedConfig, projectRoot: string, workspaceRoot: string,
19
+ /** `root` as the config authored it, before vite defaults it to `cwd`. */
20
+ authoredViteRoot?: string): string[];
21
+ export declare function collectSetupFileInputs(setupFileCandidates: string[], projectRoot: string, workspaceRoot: string, jsonCache: RawTsconfigJsonCache): {
22
+ files: string[];
23
+ tsconfigs: string[];
24
+ };
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveSetupFileCandidates = resolveSetupFileCandidates;
4
+ exports.collectSetupFileInputs = collectSetupFileInputs;
5
+ const internal_1 = require("@nx/js/internal");
6
+ const js_1 = require("@nx/js");
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ /**
10
+ * Resolves the files Vitest loads from outside the project root - `setupFiles`
11
+ * and `globalSetup`. `collectSetupFileInputs` declares the ones that exist,
12
+ * together with the tsconfigs Vite reads to transform them.
13
+ *
14
+ * Reads the build-resolved config, as the inferred target's inputs are built
15
+ * from it. Setup files contributed by an `apply: 'serve'` plugin are therefore
16
+ * invisible here - the same build/serve divergence the atomized path documents.
17
+ *
18
+ * Solution-style tsconfigs are deliberately not followed. When the nearest
19
+ * tsconfig of a setup file is a solution file, Vite walks its `references` and
20
+ * reads every project's tsconfig; declaring those would attach one input per
21
+ * project in the workspace. Keep the setup file inside a project (or beside a
22
+ * leaf tsconfig) instead.
23
+ */
24
+ function resolveSetupFileCandidates(viteConfig, projectRoot, workspaceRoot,
25
+ /** `root` as the config authored it, before vite defaults it to `cwd`. */
26
+ authoredViteRoot) {
27
+ // At the workspace root everything is already covered by `default`.
28
+ if (projectRoot === '.')
29
+ return [];
30
+ const entries = [
31
+ viteConfig.test?.setupFiles,
32
+ viteConfig.test?.globalSetup,
33
+ ].flatMap((value) => typeof value === 'string' ? [value] : Array.isArray(value) ? value : []);
34
+ if (entries.length === 0)
35
+ return [];
36
+ // Vitest resolves both options against its root, and the inferred target
37
+ // runs with `cwd` set to the project root - so a config that authors no
38
+ // root resolves them against the project, not `viteConfig.root` (which vite
39
+ // has already defaulted to the graph process's cwd). Mirrors
40
+ // `effectiveVitestRoot` in plugin.ts.
41
+ const fullProjectRoot = (0, node_path_1.resolve)(workspaceRoot, projectRoot);
42
+ const authoredRoot = viteConfig.test?.root ?? authoredViteRoot;
43
+ const configRoot = !authoredRoot
44
+ ? fullProjectRoot
45
+ : (0, node_path_1.isAbsolute)(authoredRoot)
46
+ ? authoredRoot
47
+ : (0, node_path_1.resolve)(fullProjectRoot, authoredRoot);
48
+ const candidates = new Set();
49
+ for (const entry of entries) {
50
+ const absolutePath = (0, node_path_1.isAbsolute)(entry) ? entry : (0, node_path_1.resolve)(configRoot, entry);
51
+ const wsRelative = declarable(absolutePath, projectRoot, workspaceRoot);
52
+ if (wsRelative)
53
+ candidates.add(wsRelative);
54
+ }
55
+ return [...candidates];
56
+ }
57
+ function collectSetupFileInputs(setupFileCandidates, projectRoot, workspaceRoot, jsonCache) {
58
+ const rootTsConfigName = (0, js_1.getRootTsConfigFileName)();
59
+ const files = [];
60
+ const tsconfigs = new Set();
61
+ for (const file of setupFileCandidates) {
62
+ const absolutePath = (0, node_path_1.join)(workspaceRoot, file);
63
+ if (!(0, node_fs_1.existsSync)(absolutePath))
64
+ continue;
65
+ files.push(file);
66
+ // The tsconfig Vite resolves for the file: the nearest one walking up,
67
+ // plus its extends chain.
68
+ let dir = (0, node_path_1.dirname)(absolutePath);
69
+ while (dir.startsWith(workspaceRoot)) {
70
+ const candidate = (0, node_path_1.join)(dir, 'tsconfig.json');
71
+ if ((0, node_fs_1.existsSync)(candidate)) {
72
+ (0, internal_1.walkTsconfigExtendsChain)(candidate, (absPath) => {
73
+ const relativePath = declarable(absPath, projectRoot, workspaceRoot);
74
+ if (relativePath && relativePath !== rootTsConfigName) {
75
+ tsconfigs.add(relativePath);
76
+ }
77
+ return 'continue';
78
+ }, { jsonCache });
79
+ break;
80
+ }
81
+ const parent = (0, node_path_1.dirname)(dir);
82
+ if (parent === dir)
83
+ break;
84
+ dir = parent;
85
+ }
86
+ }
87
+ return { files, tsconfigs: [...tsconfigs] };
88
+ }
89
+ /** Workspace-relative path, or null when it is not ours to declare. */
90
+ function declarable(absolutePath, projectRoot, workspaceRoot) {
91
+ const wsRelative = (0, node_path_1.relative)(workspaceRoot, absolutePath).split(node_path_1.sep).join('/');
92
+ // Outside the workspace → cannot be expressed as an input.
93
+ if (wsRelative.startsWith('../') || wsRelative === '..')
94
+ return null;
95
+ // Inside node_modules → invalidated via the lockfile.
96
+ if (wsRelative.startsWith('node_modules/') ||
97
+ wsRelative.includes('/node_modules/'))
98
+ return null;
99
+ // Inside the project → covered by `default`.
100
+ if (wsRelative === projectRoot || wsRelative.startsWith(`${projectRoot}/`))
101
+ return null;
102
+ return wsRelative;
103
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ensureDependencies = ensureDependencies;
4
4
  const devkit_1 = require("@nx/devkit");
5
+ const internal_1 = require("@nx/js/internal");
5
6
  const semver_1 = require("semver");
6
7
  const versions_1 = require("./versions");
7
8
  async function ensureDependencies(tree, schema) {
@@ -21,12 +22,10 @@ async function ensureDependencies(tree, schema) {
21
22
  else if (schema.testEnvironment !== 'node' && schema.testEnvironment) {
22
23
  devkit_1.logger.info(`A custom environment was provided: ${schema.testEnvironment}. You need to install it manually.`);
23
24
  }
24
- if (schema.uiFramework === 'angular') {
25
- devDependencies['@analogjs/vitest-angular'] = versions_1.analogVitestAngular;
26
- devDependencies['@analogjs/vite-plugin-angular'] = versions_1.analogVitestAngular;
27
- }
28
25
  if (schema.uiFramework === 'react') {
29
26
  if (schema.compiler === 'swc') {
27
+ // @vitejs/plugin-react-swc depends on @swc/core.
28
+ (0, internal_1.acknowledgeSwcBuildScripts)(tree);
30
29
  devDependencies['@vitejs/plugin-react-swc'] = versions_1.vitePluginReactSwcVersion;
31
30
  }
32
31
  else {
@@ -10,14 +10,19 @@ async function ignoreVitestTempFiles(tree, projectRoot) {
10
10
  await ignoreVitestTempFilesInEslintConfig(tree, projectRoot);
11
11
  }
12
12
  function addVitestTempFilesToGitIgnore(tree) {
13
- let gitIgnoreContents = tree.exists('.gitignore')
13
+ const contents = tree.exists('.gitignore')
14
14
  ? tree.read('.gitignore', 'utf-8')
15
15
  : '';
16
- if (!/^vitest\.config\.\*\.timestamp\*$/m.test(gitIgnoreContents)) {
17
- gitIgnoreContents = (0, devkit_1.stripIndents) `${gitIgnoreContents}
18
- vitest.config.*.timestamp*`;
16
+ // Vitest 5 writes attachments, blob reports, failure screenshots and the
17
+ // json/junit reporter output to `.vitest`.
18
+ const additions = ['vitest.config.*.timestamp*', '.vitest'].filter((entry) => !new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm').test(contents));
19
+ if (!additions.length) {
20
+ return;
19
21
  }
20
- tree.write('.gitignore', gitIgnoreContents);
22
+ // Plain concatenation rather than a template: `stripIndents` would rewrite
23
+ // every existing line of the user's file.
24
+ const separator = !contents || contents.endsWith('\n') ? '' : '\n';
25
+ tree.write('.gitignore', `${contents}${separator}${additions.join('\n')}\n`);
21
26
  }
22
27
  async function ignoreVitestTempFilesInEslintConfig(tree, projectRoot) {
23
28
  // Checked before `ensurePackage` so an Oxlint workspace does not install
@@ -1,3 +1,3 @@
1
1
  import { type Tree } from '@nx/devkit';
2
- export declare function getInstalledViteVersion(tree: Tree): string;
2
+ export declare function getInstalledViteVersion(tree: Tree): string | undefined;
3
3
  export declare function getInstalledViteMajorVersion(tree: Tree): 5 | 6 | 7 | 8 | undefined;
@@ -4,15 +4,14 @@ exports.getInstalledViteVersion = getInstalledViteVersion;
4
4
  exports.getInstalledViteMajorVersion = getInstalledViteMajorVersion;
5
5
  const devkit_1 = require("@nx/devkit");
6
6
  const semver_1 = require("semver");
7
- const versions_1 = require("./versions");
8
7
  function getInstalledViteVersion(tree) {
9
8
  const installedViteVersion = (0, devkit_1.getDependencyVersionFromPackageJson)(tree, 'vite');
10
9
  if (!installedViteVersion ||
11
10
  installedViteVersion === 'latest' ||
12
11
  installedViteVersion === 'beta') {
13
- return (0, semver_1.clean)(versions_1.vitestVersion) ?? (0, semver_1.coerce)(versions_1.vitestVersion).version;
12
+ return undefined;
14
13
  }
15
- return (0, semver_1.clean)(installedViteVersion) ?? (0, semver_1.coerce)(installedViteVersion).version;
14
+ return (0, semver_1.clean)(installedViteVersion) ?? (0, semver_1.coerce)(installedViteVersion)?.version;
16
15
  }
17
16
  function getInstalledViteMajorVersion(tree) {
18
17
  const installedViteVersion = getInstalledViteVersion(tree);
@@ -1,9 +1,9 @@
1
1
  import { type Tree } from '@nx/devkit';
2
2
  export declare const nxVersion: any;
3
3
  export declare const minSupportedVitestVersion = "3.0.0";
4
- export declare const vitestVersion = "~4.1.0";
5
- export declare const vitestCoverageV8Version = "~4.1.0";
6
- export declare const vitestCoverageIstanbulVersion = "~4.1.0";
4
+ export declare const vitestVersion = "~5.0.1";
5
+ export declare const vitestCoverageV8Version = "~5.0.1";
6
+ export declare const vitestCoverageIstanbulVersion = "~5.0.1";
7
7
  export declare const viteVersion = "^8.0.0";
8
8
  export declare const viteV7Version = "^7.0.0";
9
9
  export declare const viteV6Version = "^6.0.0";
@@ -18,12 +18,16 @@ export declare const happyDomVersion = "^20.10.4";
18
18
  export declare const edgeRuntimeVmVersion = "~3.0.2";
19
19
  export declare const jitiVersion = "2.4.2";
20
20
  export declare const analogVitestAngular = "~2.6.0";
21
+ export type VersionSelectionOptions = {
22
+ /** Vite range about to be installed, when the workspace declares none yet. */
23
+ viteRange?: string;
24
+ };
21
25
  type VitestVersions = {
22
26
  vitestVersion: string;
23
27
  vitestCoverageV8Version: string;
24
28
  vitestCoverageIstanbulVersion: string;
25
29
  };
26
- export declare function versions(tree: Tree): VitestVersions;
30
+ export declare function versions(tree: Tree, options?: VersionSelectionOptions): VitestVersions;
27
31
  export declare function getInstalledVitestVersion(tree?: Tree): string | null;
28
32
  export declare function getInstalledVitestMajorVersion(tree?: Tree): number | null;
29
33
  export {};
@@ -10,9 +10,9 @@ const path_1 = require("path");
10
10
  const semver_1 = require("semver");
11
11
  exports.nxVersion = require((0, path_1.join)('@nx/vitest', 'package.json')).version;
12
12
  exports.minSupportedVitestVersion = '3.0.0';
13
- exports.vitestVersion = '~4.1.0';
14
- exports.vitestCoverageV8Version = '~4.1.0';
15
- exports.vitestCoverageIstanbulVersion = '~4.1.0';
13
+ exports.vitestVersion = '~5.0.1';
14
+ exports.vitestCoverageV8Version = '~5.0.1';
15
+ exports.vitestCoverageIstanbulVersion = '~5.0.1';
16
16
  exports.viteVersion = '^8.0.0';
17
17
  exports.viteV7Version = '^7.0.0';
18
18
  exports.viteV6Version = '^6.0.0';
@@ -27,25 +27,63 @@ exports.happyDomVersion = '^20.10.4';
27
27
  exports.edgeRuntimeVmVersion = '~3.0.2';
28
28
  exports.jitiVersion = '2.4.2';
29
29
  exports.analogVitestAngular = '~2.6.0';
30
- const latestVersions = {
31
- vitestVersion: exports.vitestVersion,
32
- vitestCoverageV8Version: exports.vitestCoverageV8Version,
33
- vitestCoverageIstanbulVersion: exports.vitestCoverageIstanbulVersion,
34
- };
35
- const versionMap = {
36
- 3: {
37
- vitestVersion: '^3.0.0',
38
- vitestCoverageV8Version: '^3.0.5',
39
- vitestCoverageIstanbulVersion: '^3.0.5',
30
+ // Highest major first. Vitest 3 declares no vite peer at all, so it accepts
31
+ // anything and terminates the search.
32
+ const VITEST_COMPAT = [
33
+ {
34
+ major: 5,
35
+ minVite: '6.4.0',
36
+ versions: {
37
+ vitestVersion: exports.vitestVersion,
38
+ vitestCoverageV8Version: exports.vitestCoverageV8Version,
39
+ vitestCoverageIstanbulVersion: exports.vitestCoverageIstanbulVersion,
40
+ },
40
41
  },
41
- };
42
- function versions(tree) {
42
+ {
43
+ major: 4,
44
+ minVite: '6.0.0',
45
+ versions: {
46
+ vitestVersion: '^4.0.0',
47
+ vitestCoverageV8Version: '^4.0.0',
48
+ vitestCoverageIstanbulVersion: '^4.0.0',
49
+ },
50
+ },
51
+ {
52
+ major: 3,
53
+ minVite: '0.0.0',
54
+ versions: {
55
+ vitestVersion: '^3.0.0',
56
+ vitestCoverageV8Version: '^3.0.5',
57
+ vitestCoverageIstanbulVersion: '^3.0.5',
58
+ },
59
+ },
60
+ ];
61
+ function versions(tree, options) {
62
+ const cap = highestMajorTheWorkspacePeersAllow(tree);
43
63
  const installedVitestVersion = getInstalledVitestVersion(tree);
44
- if (!installedVitestVersion) {
45
- return latestVersions;
64
+ if (installedVitestVersion) {
65
+ const installedMajor = (0, semver_1.major)(installedVitestVersion);
66
+ if (installedMajor > cap) {
67
+ throw new Error(`The installed vitest version "${installedVitestVersion}" is not compatible with this workspace, which has packages that peer vitest ${cap} and below. Pin vitest to ^${cap}.0.0 first.`);
68
+ }
69
+ return (VITEST_COMPAT.find((compat) => compat.major === installedMajor)
70
+ ?.versions ?? VITEST_COMPAT[0].versions);
46
71
  }
47
- const vitestMajorVersion = (0, semver_1.major)(installedVitestVersion);
48
- return versionMap[vitestMajorVersion] ?? latestVersions;
72
+ // The manifest wins over the caller's range: it carries the minor a
73
+ // major-derived range would drop, and it is what `keepExistingVersions` keeps.
74
+ const viteRange = (0, devkit_1.getDependencyVersionFromPackageJson)(tree, 'vite') ?? options?.viteRange;
75
+ const viteFloor = viteRange ? (0, semver_1.coerce)(viteRange)?.version : undefined;
76
+ const supported = VITEST_COMPAT.find((compat) => compat.major <= cap && (!viteFloor || (0, semver_1.gte)(viteFloor, compat.minVite)));
77
+ return (supported ?? VITEST_COMPAT[0]).versions;
78
+ }
79
+ /**
80
+ * `@analogjs/vitest-angular` at the version Nx installs, and `@angular/build`,
81
+ * both peer vitest 4. A workspace carrying either cannot take vitest 5.
82
+ */
83
+ function highestMajorTheWorkspacePeersAllow(tree) {
84
+ return (0, devkit_1.getDependencyVersionFromPackageJson)(tree, '@analogjs/vitest-angular') || (0, devkit_1.getDependencyVersionFromPackageJson)(tree, '@angular/build')
85
+ ? 4
86
+ : 5;
49
87
  }
50
88
  function getInstalledVitestVersion(tree) {
51
89
  if (!tree) {
package/migrations.json CHANGED
@@ -34,6 +34,15 @@
34
34
  "description": "Replace `__dirname` with `import.meta.dirname` in `vite.config.mts`/`vitest.config.mts` files so they work with Vite's `configLoader: 'native'`.",
35
35
  "implementation": "./dist/src/migrations/update-23-2-0/use-import-meta-dirname",
36
36
  "documentation": "./dist/src/migrations/update-23-2-0/use-import-meta-dirname.md"
37
+ },
38
+ "migrate-to-vitest-5": {
39
+ "version": "23.3.0-beta.8",
40
+ "requires": {
41
+ "vitest": ">=5.0.0"
42
+ },
43
+ "description": "Migrate workspaces past breaking changes for Vitest 5.",
44
+ "implementation": "./dist/src/migrations/update-23-3-0/migrate-to-vitest-5",
45
+ "prompt": "./dist/src/migrations/update-23-3-0/ai-instructions-for-vitest-5.md"
37
46
  }
38
47
  },
39
48
  "packageJsonUpdates": {
@@ -140,6 +149,86 @@
140
149
  "alwaysAddToPackageJson": false
141
150
  }
142
151
  }
152
+ },
153
+ "23.3.0-vitest-v5": {
154
+ "version": "23.3.0-beta.8",
155
+ "requires": {
156
+ "vitest": ">=4.0.0 <5.0.0"
157
+ },
158
+ "incompatibleWith": {
159
+ "vite": "<6.4.0",
160
+ "@analogjs/vitest-angular": "*",
161
+ "@angular/build": "*",
162
+ "@vitest/browser-playwright": "*",
163
+ "@vitest/browser-preview": "*",
164
+ "@vitest/browser-webdriverio": "*"
165
+ },
166
+ "packages": {
167
+ "vitest": {
168
+ "version": "^5.0.0",
169
+ "alwaysAddToPackageJson": false
170
+ }
171
+ }
172
+ },
173
+ "23.3.0-vitest-v5-coverage-v8": {
174
+ "version": "23.3.0-beta.8",
175
+ "requires": {
176
+ "@vitest/coverage-v8": ">=4.0.0 <5.0.0"
177
+ },
178
+ "incompatibleWith": {
179
+ "vite": "<6.4.0",
180
+ "@analogjs/vitest-angular": "*",
181
+ "@angular/build": "*",
182
+ "@vitest/browser-playwright": "*",
183
+ "@vitest/browser-preview": "*",
184
+ "@vitest/browser-webdriverio": "*"
185
+ },
186
+ "packages": {
187
+ "@vitest/coverage-v8": {
188
+ "version": "^5.0.0",
189
+ "alwaysAddToPackageJson": false
190
+ }
191
+ }
192
+ },
193
+ "23.3.0-vitest-v5-coverage-istanbul": {
194
+ "version": "23.3.0-beta.8",
195
+ "requires": {
196
+ "@vitest/coverage-istanbul": ">=4.0.0 <5.0.0"
197
+ },
198
+ "incompatibleWith": {
199
+ "vite": "<6.4.0",
200
+ "@analogjs/vitest-angular": "*",
201
+ "@angular/build": "*",
202
+ "@vitest/browser-playwright": "*",
203
+ "@vitest/browser-preview": "*",
204
+ "@vitest/browser-webdriverio": "*"
205
+ },
206
+ "packages": {
207
+ "@vitest/coverage-istanbul": {
208
+ "version": "^5.0.0",
209
+ "alwaysAddToPackageJson": false
210
+ }
211
+ }
212
+ },
213
+ "23.3.0-vitest-v5-ui": {
214
+ "version": "23.3.0-beta.8",
215
+ "requires": {
216
+ "@vitest/ui": ">=4.0.0 <5.0.0"
217
+ },
218
+ "incompatibleWith": {
219
+ "vite": "<6.4.0",
220
+ "@analogjs/vitest-angular": "*",
221
+ "@angular/build": "*",
222
+ "@vitest/browser-playwright": "*",
223
+ "@vitest/browser-preview": "*",
224
+ "@vitest/browser-webdriverio": "*"
225
+ },
226
+ "packages": {
227
+ "@vitest/ui": {
228
+ "version": "^5.0.0",
229
+ "alwaysAddToPackageJson": false
230
+ }
231
+ }
143
232
  }
144
233
  }
145
234
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nx/vitest",
3
3
  "description": "The Nx Plugin for Vitest to enable fast unit testing with Vitest.",
4
- "version": "23.3.0-pr.37068.f95e11e",
4
+ "version": "23.3.0-rc.0",
5
5
  "type": "commonjs",
6
6
  "files": [
7
7
  "dist",
@@ -78,16 +78,16 @@
78
78
  "executors": "./executors.json",
79
79
  "generators": "./generators.json",
80
80
  "dependencies": {
81
+ "@nx/devkit": "23.3.0-rc.0",
82
+ "@nx/js": "23.3.0-rc.0",
81
83
  "tslib": "^2.3.0",
82
84
  "semver": "^7.6.3",
83
- "@phenomnomnominal/tsquery": "~6.2.0",
84
- "@nx/devkit": "23.3.0-pr.37068.f95e11e",
85
- "@nx/js": "23.3.0-pr.37068.f95e11e"
85
+ "@phenomnomnominal/tsquery": "~6.2.0"
86
86
  },
87
87
  "peerDependencies": {
88
- "vitest": "^3.0.0 || ^4.0.0",
89
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
90
- "@nx/eslint": "23.3.0-pr.37068.f95e11e"
88
+ "@nx/eslint": "23.3.0-rc.0",
89
+ "vitest": "^3.0.0 || ^4.0.0 || ^5.0.0",
90
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
91
91
  },
92
92
  "peerDependenciesMeta": {
93
93
  "@nx/eslint": {
@@ -101,6 +101,6 @@
101
101
  }
102
102
  },
103
103
  "devDependencies": {
104
- "nx": "23.3.0-pr.37068.f95e11e"
104
+ "nx": "23.3.0-rc.0"
105
105
  }
106
106
  }
@@ -0,0 +1,18 @@
1
+ <p style="text-align: center;">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-dark.svg">
4
+ <img alt="Nx - Smart Monorepos · Fast Builds" src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-light.svg" width="100%">
5
+ </picture>
6
+ </p>
7
+
8
+ {{links}}
9
+
10
+ <hr>
11
+
12
+ # Nx: Smart Monorepos · Fast Builds
13
+
14
+ Get to green PRs in half the time. Nx optimizes your builds, scales your CI, and fixes failed PRs. Built for developers and AI agents.
15
+
16
+ This package is a [Vitest plugin for Nx](https://nx.dev/nx-api/vitest).
17
+
18
+ {{content}}