@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 +1 -1
- package/dist/src/executors/test/lib/nx-reporter.d.ts +1 -2
- package/dist/src/generators/configuration/configuration.js +10 -1
- package/dist/src/generators/init/init.js +12 -1
- package/dist/src/migrations/update-23-3-0/ai-instructions-for-vitest-5.md +92 -0
- package/dist/src/migrations/update-23-3-0/migrate-to-vitest-5.d.ts +14 -0
- package/dist/src/migrations/update-23-3-0/migrate-to-vitest-5.js +211 -0
- package/dist/src/plugins/plugin.js +55 -21
- package/dist/src/plugins/setup-file-inputs.d.ts +24 -0
- package/dist/src/plugins/setup-file-inputs.js +103 -0
- package/dist/src/utils/ensure-dependencies.js +3 -4
- package/dist/src/utils/ignore-vitest-temp-files.js +10 -5
- package/dist/src/utils/version-utils.d.ts +1 -1
- package/dist/src/utils/version-utils.js +2 -3
- package/dist/src/utils/versions.d.ts +8 -4
- package/dist/src/utils/versions.js +57 -19
- package/migrations.json +89 -0
- package/package.json +8 -8
- package/readme-template.md +18 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](<>)
|
|
13
13
|
[](http://commitizen.github.io/cz-cli/)
|
|
14
14
|
[](https://go.nx.dev/community)
|
|
15
|
-
[](https://nx.dev/nx-cloud)
|
|
16
16
|
|
|
17
17
|
</div>
|
|
18
18
|
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
const contents = tree.exists('.gitignore')
|
|
14
14
|
? tree.read('.gitignore', 'utf-8')
|
|
15
15
|
: '';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
12
|
+
return undefined;
|
|
14
13
|
}
|
|
15
|
-
return (0, semver_1.clean)(installedViteVersion) ?? (0, semver_1.coerce)(installedViteVersion)
|
|
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 = "~
|
|
5
|
-
export declare const vitestCoverageV8Version = "~
|
|
6
|
-
export declare const vitestCoverageIstanbulVersion = "~
|
|
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 = '~
|
|
14
|
-
exports.vitestCoverageV8Version = '~
|
|
15
|
-
exports.vitestCoverageIstanbulVersion = '~
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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 (
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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-
|
|
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
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
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-
|
|
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}}
|