@nx/vitest 23.3.0-beta.6 → 23.3.0-beta.8

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);
@@ -34,7 +34,9 @@ function updateDependencies(tree, schema) {
34
34
  }
35
35
  return (0, devkit_1.addDependenciesToPackageJson)(tree, {}, {
36
36
  '@nx/vitest': versions_1.nxVersion,
37
- 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,
38
40
  vite: viteVersionToUse,
39
41
  }, undefined, schema.keepExistingVersions ?? true);
40
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
+ }
@@ -442,7 +442,7 @@ function normalizeOutputPath(outputPath, projectRoot, workspaceRoot, path) {
442
442
  }
443
443
  else {
444
444
  if ((0, node_path_1.isAbsolute)(outputPath)) {
445
- return `{workspaceRoot}/${(0, node_path_1.relative)(workspaceRoot, outputPath)}`;
445
+ return `{workspaceRoot}/${(0, devkit_1.normalizePath)((0, node_path_1.relative)(workspaceRoot, outputPath))}`;
446
446
  }
447
447
  else {
448
448
  if (outputPath.startsWith('..')) {
@@ -22,10 +22,6 @@ async function ensureDependencies(tree, schema) {
22
22
  else if (schema.testEnvironment !== 'node' && schema.testEnvironment) {
23
23
  devkit_1.logger.info(`A custom environment was provided: ${schema.testEnvironment}. You need to install it manually.`);
24
24
  }
25
- if (schema.uiFramework === 'angular') {
26
- devDependencies['@analogjs/vitest-angular'] = versions_1.analogVitestAngular;
27
- devDependencies['@analogjs/vite-plugin-angular'] = versions_1.analogVitestAngular;
28
- }
29
25
  if (schema.uiFramework === 'react') {
30
26
  if (schema.compiler === 'swc') {
31
27
  // @vitejs/plugin-react-swc depends on @swc/core.
@@ -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-beta.6",
4
+ "version": "23.3.0-beta.8",
5
5
  "type": "commonjs",
6
6
  "files": [
7
7
  "dist",
@@ -78,14 +78,14 @@
78
78
  "executors": "./executors.json",
79
79
  "generators": "./generators.json",
80
80
  "dependencies": {
81
- "@nx/devkit": "23.3.0-beta.6",
82
- "@nx/js": "23.3.0-beta.6",
81
+ "@nx/devkit": "23.3.0-beta.8",
82
+ "@nx/js": "23.3.0-beta.8",
83
83
  "tslib": "^2.3.0",
84
84
  "semver": "^7.6.3",
85
85
  "@phenomnomnominal/tsquery": "~6.2.0"
86
86
  },
87
87
  "peerDependencies": {
88
- "@nx/eslint": "23.3.0-beta.6",
88
+ "@nx/eslint": "23.3.0-beta.8",
89
89
  "vitest": "^3.0.0 || ^4.0.0 || ^5.0.0",
90
90
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
91
91
  },
@@ -101,6 +101,6 @@
101
101
  }
102
102
  },
103
103
  "devDependencies": {
104
- "nx": "23.3.0-beta.6"
104
+ "nx": "23.3.0-beta.8"
105
105
  }
106
106
  }