@nx/vitest 23.1.0-rc.3 → 23.2.0-beta.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.
|
@@ -7,6 +7,7 @@ const devkit_1 = require("@nx/devkit");
|
|
|
7
7
|
const options_utils_1 = require("../../../utils/options-utils");
|
|
8
8
|
const path_1 = require("path");
|
|
9
9
|
const executor_utils_1 = require("../../../utils/executor-utils");
|
|
10
|
+
const devkit_internals_1 = require("nx/src/devkit-internals");
|
|
10
11
|
async function getOptions(options, context, projectRoot) {
|
|
11
12
|
// Allows ESM to be required in CJS modules. Vite will be published as ESM in the future.
|
|
12
13
|
const { loadConfigFromFile } = await (0, executor_utils_1.loadViteDynamicImport)();
|
|
@@ -49,10 +50,18 @@ async function getOptions(options, context, projectRoot) {
|
|
|
49
50
|
'--': _dashdash, color: _color, w: _w,
|
|
50
51
|
// Pass through any additional Vitest options
|
|
51
52
|
...passThroughOptions } = normalizedExtraArgs;
|
|
53
|
+
// Vitest keeps the UI/browser server alive only in watch mode and has no
|
|
54
|
+
// --ui -> watch link, so a run-once `nx test --ui` tears the UI down right
|
|
55
|
+
// after the run. When --ui is requested from an interactive, non-CI terminal,
|
|
56
|
+
// default watch on so the UI stays open; bare runs and CI stay run-once so
|
|
57
|
+
// `nx run-many`/`affected` don't hang. An explicit CLI --watch/--no-watch or a
|
|
58
|
+
// config `test.watch` still takes precedence.
|
|
59
|
+
const uiRequested = passThroughOptions.ui === true;
|
|
60
|
+
const watchForUi = uiRequested && !!process.stdin.isTTY && !(0, devkit_internals_1.isCI)();
|
|
52
61
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
watch: watch ??
|
|
63
|
+
resolved?.config?.['test']?.watch ??
|
|
64
|
+
watchForUi,
|
|
56
65
|
// Pass through any additional Vitest options
|
|
57
66
|
...passThroughOptions,
|
|
58
67
|
// This should not be needed as it's going to be set in vite.config.ts
|
|
@@ -16,6 +16,7 @@ const version_utils_1 = require("../../utils/version-utils");
|
|
|
16
16
|
const versions_1 = require("../../utils/versions");
|
|
17
17
|
const assert_supported_vitest_version_1 = require("../../utils/assert-supported-vitest-version");
|
|
18
18
|
const semver_1 = require("semver");
|
|
19
|
+
let ts;
|
|
19
20
|
/**
|
|
20
21
|
* Determines whether to use vitest.config.mts instead of vite.config.mts.
|
|
21
22
|
* Returns true for new non-framework projects that don't already have a vite.config.
|
|
@@ -176,15 +177,86 @@ getTestBed().initTestEnvironment(
|
|
|
176
177
|
const installDependenciesTask = (0, devkit_1.addDependenciesToPackageJson)(tree, {}, devDependencies, undefined, true);
|
|
177
178
|
tasks.push(installDependenciesTask);
|
|
178
179
|
}
|
|
179
|
-
// Setup
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
180
|
+
// Setup the root config aggregating the project configs. Vitest 4 removed
|
|
181
|
+
// workspace files in favor of inlining the projects into a root vitest.config
|
|
182
|
+
// via `test.projects` (https://vitest.dev/guide/migration.html#workspace-is-replaced-with-projects).
|
|
183
|
+
// Emit that shape for vitest 4+ and when the installed version can't be
|
|
184
|
+
// detected (new installs resolve to v4); vitest 3 keeps the workspace file.
|
|
185
|
+
if (!isRootProject) {
|
|
186
|
+
const projectGlobs = `'**/vite.config.{mjs,js,ts,mts}', '**/vitest.config.{mjs,js,ts,mts}'`;
|
|
187
|
+
const vitestMajorVersion = (0, versions_1.getInstalledVitestMajorVersion)(tree);
|
|
188
|
+
if (vitestMajorVersion === null || vitestMajorVersion >= 4) {
|
|
189
|
+
const hasWorkspaceFile = ['ts', 'js', 'json'].some((ext) => tree.exists(`vitest.workspace.${ext}`) ||
|
|
190
|
+
tree.exists(`vitest.projects.${ext}`));
|
|
191
|
+
const rootVitestConfig = findRootConfig(tree, 'vitest.config');
|
|
192
|
+
const rootViteConfig = findRootConfig(tree, 'vite.config');
|
|
193
|
+
if (hasWorkspaceFile) {
|
|
194
|
+
// A workspace/projects file already defines the project set and the
|
|
195
|
+
// vitest 4 migration converts it; leave it untouched.
|
|
196
|
+
}
|
|
197
|
+
else if (rootVitestConfig) {
|
|
198
|
+
// A root vitest.config.* wins vitest's config resolution, so we can
|
|
199
|
+
// neither add a competing aggregator nor safely rewrite it. Warn when it
|
|
200
|
+
// doesn't aggregate, or when its shape can't be read statically, since
|
|
201
|
+
// the new project would otherwise be silently absent from
|
|
202
|
+
// workspace-level vitest runs.
|
|
203
|
+
const declaresProjects = rootConfigDeclaresProjects(tree, rootVitestConfig);
|
|
204
|
+
if (declaresProjects === 'missing') {
|
|
205
|
+
devkit_1.logger.warn(`Found a root "${rootVitestConfig}" without a \`test.projects\` entry. ` +
|
|
206
|
+
`The "${schema.project}" project won't be part of workspace-level ` +
|
|
207
|
+
`vitest runs until you add its config file to \`test.projects\` there.`);
|
|
208
|
+
}
|
|
209
|
+
else if (declaresProjects === 'unknown') {
|
|
210
|
+
devkit_1.logger.warn(`Found a root "${rootVitestConfig}" whose test setup couldn't be ` +
|
|
211
|
+
`analyzed. If the "${schema.project}" project isn't picked up by ` +
|
|
212
|
+
`workspace-level vitest runs, add its config file to ` +
|
|
213
|
+
`\`test.projects\` there.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (rootViteConfig) {
|
|
217
|
+
// A root vite.config.* is vitest's config today. Writing a
|
|
218
|
+
// vitest.config.* would win resolution and shadow it, dropping the vite
|
|
219
|
+
// settings (aliases, plugins) from vitest runs, and projects don't
|
|
220
|
+
// inherit those from a root aggregator either. Leave it in place.
|
|
221
|
+
const declaresProjects = rootConfigDeclaresProjects(tree, rootViteConfig);
|
|
222
|
+
if (declaresProjects === 'missing') {
|
|
223
|
+
devkit_1.logger.warn(`Found a root "${rootViteConfig}" without a \`test.projects\` entry. ` +
|
|
224
|
+
`The "${schema.project}" project runs through that root config, so ` +
|
|
225
|
+
`its own vitest configuration (e.g. \`environment\`, \`setupFiles\`) ` +
|
|
226
|
+
`won't apply. Add its config file to a \`test.projects\` entry there ` +
|
|
227
|
+
`to run it with its own configuration.`);
|
|
228
|
+
}
|
|
229
|
+
else if (declaresProjects === 'unknown') {
|
|
230
|
+
devkit_1.logger.warn(`Found a root "${rootViteConfig}" whose test setup couldn't be ` +
|
|
231
|
+
`analyzed. If the "${schema.project}" project isn't picked up by ` +
|
|
232
|
+
`workspace-level vitest runs, add its config file to ` +
|
|
233
|
+
`\`test.projects\` there.`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// No root config exists, so emit the aggregator. Its projects glob
|
|
238
|
+
// matches every vite/vitest config, including this file itself and any
|
|
239
|
+
// root vite.config added later; exclude both so neither is resolved as
|
|
240
|
+
// an extra project that, carrying no `include`, re-runs every spec via
|
|
241
|
+
// the default glob.
|
|
242
|
+
tree.write('vitest.config.ts', `import { defineConfig } from 'vitest/config';
|
|
243
|
+
|
|
244
|
+
export default defineConfig({
|
|
245
|
+
test: {
|
|
246
|
+
projects: [${projectGlobs}, '!vitest.config.{mjs,js,ts,mts}', '!vite.config.{mjs,js,ts,mts}'],
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else if (!tree.exists(`vitest.workspace.ts`) &&
|
|
253
|
+
!tree.exists(`vitest.workspace.js`) &&
|
|
254
|
+
!tree.exists(`vitest.workspace.json`) &&
|
|
255
|
+
!tree.exists(`vitest.projects.ts`) &&
|
|
256
|
+
!tree.exists(`vitest.projects.js`) &&
|
|
257
|
+
!tree.exists(`vitest.projects.json`)) {
|
|
258
|
+
tree.write('vitest.workspace.ts', `export default [${projectGlobs}];`);
|
|
259
|
+
}
|
|
188
260
|
}
|
|
189
261
|
if (!schema.skipFormat) {
|
|
190
262
|
await (0, devkit_1.formatFiles)(tree);
|
|
@@ -369,4 +441,84 @@ function findTestDefault(td, target) {
|
|
|
369
441
|
}
|
|
370
442
|
return value;
|
|
371
443
|
}
|
|
444
|
+
function findRootConfig(tree, name) {
|
|
445
|
+
for (const ext of ['ts', 'mts', 'cts', 'js', 'mjs', 'cjs']) {
|
|
446
|
+
const candidate = `${name}.${ext}`;
|
|
447
|
+
if (tree.exists(candidate)) {
|
|
448
|
+
return candidate;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Classifies a root config's default export by whether it already aggregates
|
|
455
|
+
* projects via `test.projects` (or the vitest 3 `test.workspace`):
|
|
456
|
+
* - `'declares'`: it aggregates projects.
|
|
457
|
+
* - `'missing'`: it's a readable object config with no such aggregation.
|
|
458
|
+
* - `'unknown'`: the shape can't be read statically (dynamic/function configs,
|
|
459
|
+
* spreads), where the safe move is to leave the config untouched rather than
|
|
460
|
+
* shadow it.
|
|
461
|
+
*/
|
|
462
|
+
function rootConfigDeclaresProjects(tree, configPath) {
|
|
463
|
+
ts ??= (0, internal_2.ensureTypescript)();
|
|
464
|
+
const { tsquery } = require('@phenomnomnominal/tsquery');
|
|
465
|
+
let sourceFile;
|
|
466
|
+
try {
|
|
467
|
+
sourceFile = tsquery.ast(tree.read(configPath, 'utf-8'));
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return 'unknown';
|
|
471
|
+
}
|
|
472
|
+
const exportAssignment = sourceFile.statements.find((s) => ts.isExportAssignment(s) && !s.isExportEquals);
|
|
473
|
+
if (!exportAssignment) {
|
|
474
|
+
return 'unknown';
|
|
475
|
+
}
|
|
476
|
+
let expression = unwrapExpression(exportAssignment.expression);
|
|
477
|
+
// Unwrap a single-argument config wrapper such as `defineConfig(...)`.
|
|
478
|
+
if (ts.isCallExpression(expression) &&
|
|
479
|
+
ts.isIdentifier(expression.expression) &&
|
|
480
|
+
expression.arguments.length === 1) {
|
|
481
|
+
expression = unwrapExpression(expression.arguments[0]);
|
|
482
|
+
}
|
|
483
|
+
if (!ts.isObjectLiteralExpression(expression)) {
|
|
484
|
+
return 'unknown';
|
|
485
|
+
}
|
|
486
|
+
// A spread could hide a `test` block we can't see.
|
|
487
|
+
if (expression.properties.some((p) => ts.isSpreadAssignment(p))) {
|
|
488
|
+
return 'unknown';
|
|
489
|
+
}
|
|
490
|
+
const testProperty = findObjectProperty(expression, 'test');
|
|
491
|
+
if (!testProperty) {
|
|
492
|
+
return 'missing';
|
|
493
|
+
}
|
|
494
|
+
if (!ts.isObjectLiteralExpression(testProperty.initializer)) {
|
|
495
|
+
return 'unknown';
|
|
496
|
+
}
|
|
497
|
+
const testObject = testProperty.initializer;
|
|
498
|
+
if (testObject.properties.some((p) => ts.isSpreadAssignment(p))) {
|
|
499
|
+
return 'unknown';
|
|
500
|
+
}
|
|
501
|
+
return findObjectProperty(testObject, 'projects') ||
|
|
502
|
+
findObjectProperty(testObject, 'workspace')
|
|
503
|
+
? 'declares'
|
|
504
|
+
: 'missing';
|
|
505
|
+
}
|
|
506
|
+
function unwrapExpression(expression) {
|
|
507
|
+
while (ts.isAsExpression(expression) ||
|
|
508
|
+
ts.isSatisfiesExpression(expression) ||
|
|
509
|
+
ts.isParenthesizedExpression(expression)) {
|
|
510
|
+
expression = expression.expression;
|
|
511
|
+
}
|
|
512
|
+
return expression;
|
|
513
|
+
}
|
|
514
|
+
function findObjectProperty(objectLiteral, name) {
|
|
515
|
+
for (const property of objectLiteral.properties) {
|
|
516
|
+
if (ts.isPropertyAssignment(property) &&
|
|
517
|
+
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&
|
|
518
|
+
property.name.text === name) {
|
|
519
|
+
return property;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
372
524
|
exports.default = configurationGenerator;
|
|
@@ -53,9 +53,18 @@ exports.createNodes = [
|
|
|
53
53
|
// for different config files.
|
|
54
54
|
const hash = hashes[idx] + configFile;
|
|
55
55
|
if (!targetsCache.has(hash)) {
|
|
56
|
-
|
|
56
|
+
const result = await buildVitestTargets(configFile, projectRoot, normalizedOptions, context, pmc, tsconfigChainsByProjectRoot.get(projectRoot) ?? []);
|
|
57
|
+
// Cache the result even when it's null (a root orchestrator config)
|
|
58
|
+
// so the config isn't re-resolved on every project-graph build.
|
|
59
|
+
targetsCache.set(hash, result);
|
|
57
60
|
}
|
|
58
|
-
const
|
|
61
|
+
const cached = targetsCache.get(hash);
|
|
62
|
+
// `buildVitestTargets` returns null for a root orchestrator config
|
|
63
|
+
// that must not become a project; register no node for it.
|
|
64
|
+
if (!cached) {
|
|
65
|
+
return { projects: {} };
|
|
66
|
+
}
|
|
67
|
+
const { projectType, metadata, targets } = cached;
|
|
59
68
|
const project = {
|
|
60
69
|
root: projectRoot,
|
|
61
70
|
targets,
|
|
@@ -115,14 +124,16 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
|
|
|
115
124
|
configFile: absoluteConfigFilePath,
|
|
116
125
|
mode: 'development',
|
|
117
126
|
}, 'build');
|
|
118
|
-
//
|
|
119
|
-
//
|
|
127
|
+
// A root config that aggregates project configs via `test.projects` is just an
|
|
128
|
+
// orchestrator; the actual tests live in the individual project configs. Skip it
|
|
129
|
+
// entirely so it does not register a project rooted at the workspace root (which
|
|
130
|
+
// would, for example, make `nx format` treat the whole workspace as one project).
|
|
120
131
|
const isWorkspaceRoot = projectRoot === '.';
|
|
121
132
|
// TODO(jack): Remove this cast when @nx/vitest switches to moduleResolution:
|
|
122
133
|
// "nodenext". Vite 8's rolldown types break vitest's test augmentation.
|
|
123
134
|
const hasProjectsProperty = Array.isArray(viteBuildConfig?.test?.projects);
|
|
124
135
|
if (isWorkspaceRoot && hasProjectsProperty) {
|
|
125
|
-
return
|
|
136
|
+
return null;
|
|
126
137
|
}
|
|
127
138
|
let metadata = {};
|
|
128
139
|
const { testOutputs, hasTest } = getOutputs(viteBuildConfig, projectRoot, context.workspaceRoot);
|
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.
|
|
4
|
+
"version": "23.2.0-beta.0",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -73,13 +73,13 @@
|
|
|
73
73
|
"tslib": "^2.3.0",
|
|
74
74
|
"semver": "^7.6.3",
|
|
75
75
|
"@phenomnomnominal/tsquery": "~6.2.0",
|
|
76
|
-
"@nx/devkit": "23.
|
|
77
|
-
"@nx/js": "23.
|
|
76
|
+
"@nx/devkit": "23.2.0-beta.0",
|
|
77
|
+
"@nx/js": "23.2.0-beta.0"
|
|
78
78
|
},
|
|
79
79
|
"peerDependencies": {
|
|
80
80
|
"vitest": "^3.0.0 || ^4.0.0",
|
|
81
81
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
|
82
|
-
"@nx/eslint": "23.
|
|
82
|
+
"@nx/eslint": "23.2.0-beta.0"
|
|
83
83
|
},
|
|
84
84
|
"peerDependenciesMeta": {
|
|
85
85
|
"@nx/eslint": {
|
|
@@ -93,6 +93,6 @@
|
|
|
93
93
|
}
|
|
94
94
|
},
|
|
95
95
|
"devDependencies": {
|
|
96
|
-
"nx": "23.
|
|
96
|
+
"nx": "23.2.0-beta.0"
|
|
97
97
|
}
|
|
98
98
|
}
|