@nx/vite 23.0.0-beta.21 → 23.0.0-beta.23
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/dist/plugins/nx-copy-assets.plugin.d.ts +6 -0
- package/dist/plugins/nx-copy-assets.plugin.js +8 -0
- package/dist/plugins/nx-tsconfig-paths.plugin.d.ts +7 -0
- package/dist/plugins/nx-tsconfig-paths.plugin.js +9 -0
- package/dist/src/generators/configuration/configuration.js +2 -0
- package/dist/src/generators/convert-to-inferred/convert-to-inferred.js +2 -0
- package/dist/src/generators/init/init.js +2 -0
- package/dist/src/generators/init/lib/utils.js +1 -1
- package/dist/src/generators/init/schema.json +1 -1
- package/dist/src/generators/setup-paths-plugin/setup-paths-plugin.js +2 -0
- package/dist/src/migrations/update-23-0-0/ai-instructions-for-vitest-3.md +604 -0
- package/dist/src/migrations/update-23-0-0/ai-instructions-for-vitest-4.md +817 -0
- package/dist/src/migrations/update-23-0-0/lib/ast-edits.d.ts +26 -0
- package/dist/src/migrations/update-23-0-0/lib/ast-edits.js +49 -0
- package/dist/src/migrations/update-23-0-0/lib/ci-files.d.ts +3 -0
- package/dist/src/migrations/update-23-0-0/lib/ci-files.js +30 -0
- package/dist/src/migrations/update-23-0-0/lib/vitest-config-files.d.ts +5 -0
- package/dist/src/migrations/update-23-0-0/lib/vitest-config-files.js +34 -0
- package/dist/src/migrations/update-23-0-0/migrate-to-vitest-3.d.ts +10 -0
- package/dist/src/migrations/update-23-0-0/migrate-to-vitest-3.js +335 -0
- package/dist/src/migrations/update-23-0-0/migrate-to-vitest-4.d.ts +17 -0
- package/dist/src/migrations/update-23-0-0/migrate-to-vitest-4.js +726 -0
- package/dist/src/plugins/plugin.d.ts +3 -3
- package/dist/src/utils/assert-supported-vite-version.d.ts +2 -0
- package/dist/src/utils/assert-supported-vite-version.js +8 -0
- package/dist/src/utils/deprecation.d.ts +4 -0
- package/dist/src/utils/deprecation.js +26 -1
- package/dist/src/utils/ensure-dependencies.js +1 -1
- package/dist/src/utils/generator-utils.js +2 -0
- package/dist/src/utils/versions.d.ts +1 -0
- package/dist/src/utils/versions.js +7 -1
- package/migrations.json +116 -9
- package/package.json +6 -6
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Node } from 'typescript';
|
|
2
|
+
export interface TextEdit {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
replacement: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Apply a list of non-overlapping edits to `contents`. Edits are sorted by
|
|
9
|
+
* `start` descending so positions stay valid as text is spliced.
|
|
10
|
+
*/
|
|
11
|
+
export declare function applyTextEdits(contents: string, edits: TextEdit[]): string;
|
|
12
|
+
/**
|
|
13
|
+
* Edit that removes `node` and a single trailing comma if present. Designed
|
|
14
|
+
* for removing a `PropertyAssignment` from an object literal without leaving
|
|
15
|
+
* a dangling separator.
|
|
16
|
+
*/
|
|
17
|
+
export declare function removeNodeWithTrailingCommaEdit(contents: string, node: Node): TextEdit;
|
|
18
|
+
/**
|
|
19
|
+
* Edit that replaces the textual range of `node` with `replacement`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function replaceNodeTextEdit(node: Node, replacement: string): TextEdit;
|
|
22
|
+
/**
|
|
23
|
+
* Edit that replaces a string literal's content while preserving the user's
|
|
24
|
+
* original quote style. `node` is expected to be a StringLiteral.
|
|
25
|
+
*/
|
|
26
|
+
export declare function replaceStringLiteralValueEdit(contents: string, node: Node, newValue: string): TextEdit;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyTextEdits = applyTextEdits;
|
|
4
|
+
exports.removeNodeWithTrailingCommaEdit = removeNodeWithTrailingCommaEdit;
|
|
5
|
+
exports.replaceNodeTextEdit = replaceNodeTextEdit;
|
|
6
|
+
exports.replaceStringLiteralValueEdit = replaceStringLiteralValueEdit;
|
|
7
|
+
/**
|
|
8
|
+
* Apply a list of non-overlapping edits to `contents`. Edits are sorted by
|
|
9
|
+
* `start` descending so positions stay valid as text is spliced.
|
|
10
|
+
*/
|
|
11
|
+
function applyTextEdits(contents, edits) {
|
|
12
|
+
if (edits.length === 0)
|
|
13
|
+
return contents;
|
|
14
|
+
const sorted = [...edits].sort((a, b) => b.start - a.start);
|
|
15
|
+
let updated = contents;
|
|
16
|
+
for (const edit of sorted) {
|
|
17
|
+
updated =
|
|
18
|
+
updated.slice(0, edit.start) + edit.replacement + updated.slice(edit.end);
|
|
19
|
+
}
|
|
20
|
+
return updated;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Edit that removes `node` and a single trailing comma if present. Designed
|
|
24
|
+
* for removing a `PropertyAssignment` from an object literal without leaving
|
|
25
|
+
* a dangling separator.
|
|
26
|
+
*/
|
|
27
|
+
function removeNodeWithTrailingCommaEdit(contents, node) {
|
|
28
|
+
const start = node.getStart();
|
|
29
|
+
let end = node.getEnd();
|
|
30
|
+
if (contents[end] === ',')
|
|
31
|
+
end++;
|
|
32
|
+
return { start, end, replacement: '' };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Edit that replaces the textual range of `node` with `replacement`.
|
|
36
|
+
*/
|
|
37
|
+
function replaceNodeTextEdit(node, replacement) {
|
|
38
|
+
return { start: node.getStart(), end: node.getEnd(), replacement };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Edit that replaces a string literal's content while preserving the user's
|
|
42
|
+
* original quote style. `node` is expected to be a StringLiteral.
|
|
43
|
+
*/
|
|
44
|
+
function replaceStringLiteralValueEdit(contents, node, newValue) {
|
|
45
|
+
const start = node.getStart();
|
|
46
|
+
const end = node.getEnd();
|
|
47
|
+
const quote = contents[start]; // ' or "
|
|
48
|
+
return { start, end, replacement: `${quote}${newValue}${quote}` };
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isCiFile = isCiFile;
|
|
4
|
+
exports.visitCiFiles = visitCiFiles;
|
|
5
|
+
const devkit_1 = require("@nx/devkit");
|
|
6
|
+
const picomatch = require("picomatch");
|
|
7
|
+
// Common CI provider configs. Mechanical edits inside YAML are risky
|
|
8
|
+
// (comments, anchors, multi-line strings), so the pre-pass only scans these
|
|
9
|
+
// for legacy tokens and forwards file paths as advisory context.
|
|
10
|
+
const CI_GLOBS = [
|
|
11
|
+
'**/.github/workflows/*.{yml,yaml}',
|
|
12
|
+
'**/.gitlab-ci.yml',
|
|
13
|
+
'**/.gitlab-ci.*.yml',
|
|
14
|
+
'**/azure-pipelines.{yml,yaml}',
|
|
15
|
+
'**/azure-pipelines.*.{yml,yaml}',
|
|
16
|
+
'**/.circleci/config.yml',
|
|
17
|
+
'**/bitbucket-pipelines.yml',
|
|
18
|
+
];
|
|
19
|
+
const ciMatchers = CI_GLOBS.map((g) => picomatch(g));
|
|
20
|
+
function isCiFile(filePath) {
|
|
21
|
+
return ciMatchers.some((m) => m(filePath));
|
|
22
|
+
}
|
|
23
|
+
function visitCiFiles(tree, callback) {
|
|
24
|
+
(0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
|
|
25
|
+
if (!isCiFile(filePath))
|
|
26
|
+
return;
|
|
27
|
+
const contents = tree.read(filePath, 'utf-8');
|
|
28
|
+
callback(filePath, contents);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Tree } from '@nx/devkit';
|
|
2
|
+
export declare function isVitestConfigFile(filePath: string): boolean;
|
|
3
|
+
export declare function isVitestWorkspaceFile(filePath: string): boolean;
|
|
4
|
+
export declare function visitVitestConfigFiles(tree: Tree, callback: (filePath: string) => void): void;
|
|
5
|
+
export declare function isJsOrTsFile(filePath: string): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isVitestConfigFile = isVitestConfigFile;
|
|
4
|
+
exports.isVitestWorkspaceFile = isVitestWorkspaceFile;
|
|
5
|
+
exports.visitVitestConfigFiles = visitVitestConfigFiles;
|
|
6
|
+
exports.isJsOrTsFile = isJsOrTsFile;
|
|
7
|
+
const devkit_1 = require("@nx/devkit");
|
|
8
|
+
const picomatch = require("picomatch");
|
|
9
|
+
// Vitest options can live in either `vitest.config.*` (dedicated) or
|
|
10
|
+
// `vite.config.*` (the `test:` block consumed by the inferred plugin).
|
|
11
|
+
const CONFIG_GLOBS = [
|
|
12
|
+
'**/vitest.*config*.{js,ts,mjs,mts,cjs,cts}',
|
|
13
|
+
'**/vite.*config*.{js,ts,mjs,mts,cjs,cts}',
|
|
14
|
+
];
|
|
15
|
+
const WORKSPACE_GLOB = '**/vitest.workspace.{js,ts,mjs,mts,cjs,cts}';
|
|
16
|
+
const configMatchers = CONFIG_GLOBS.map((g) => picomatch(g));
|
|
17
|
+
const workspaceMatcher = picomatch(WORKSPACE_GLOB);
|
|
18
|
+
function isVitestConfigFile(filePath) {
|
|
19
|
+
return configMatchers.some((m) => m(filePath));
|
|
20
|
+
}
|
|
21
|
+
function isVitestWorkspaceFile(filePath) {
|
|
22
|
+
return workspaceMatcher(filePath);
|
|
23
|
+
}
|
|
24
|
+
function visitVitestConfigFiles(tree, callback) {
|
|
25
|
+
(0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
|
|
26
|
+
if (isVitestConfigFile(filePath)) {
|
|
27
|
+
callback(filePath);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const TS_JS_RE = /\.[cm]?[jt]sx?$/;
|
|
32
|
+
function isJsOrTsFile(filePath) {
|
|
33
|
+
return TS_JS_RE.test(filePath);
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Tree } from '@nx/devkit';
|
|
2
|
+
/**
|
|
3
|
+
* Hybrid migration paired with `ai-instructions-for-vitest-3.md`. Applies the
|
|
4
|
+
* deterministic, AST-tractable Vitest 1.x/2.x → 3.x changes mechanically and
|
|
5
|
+
* returns an `agentContext` describing any shape it could not handle, so the
|
|
6
|
+
* paired prompt's agent can finish those by hand.
|
|
7
|
+
*/
|
|
8
|
+
export default function migrateToVitest3(tree: Tree): Promise<{
|
|
9
|
+
agentContext: string[];
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = migrateToVitest3;
|
|
4
|
+
const devkit_1 = require("@nx/devkit");
|
|
5
|
+
const internal_1 = require("@nx/js/internal");
|
|
6
|
+
const tsquery_1 = require("@phenomnomnominal/tsquery");
|
|
7
|
+
const ast_edits_1 = require("./lib/ast-edits");
|
|
8
|
+
const ci_files_1 = require("./lib/ci-files");
|
|
9
|
+
const vitest_config_files_1 = require("./lib/vitest-config-files");
|
|
10
|
+
let ts;
|
|
11
|
+
/**
|
|
12
|
+
* Hybrid migration paired with `ai-instructions-for-vitest-3.md`. Applies the
|
|
13
|
+
* deterministic, AST-tractable Vitest 1.x/2.x → 3.x changes mechanically and
|
|
14
|
+
* returns an `agentContext` describing any shape it could not handle, so the
|
|
15
|
+
* paired prompt's agent can finish those by hand.
|
|
16
|
+
*/
|
|
17
|
+
async function migrateToVitest3(tree) {
|
|
18
|
+
ts ??= (0, internal_1.ensureTypescript)();
|
|
19
|
+
const unhandled = [];
|
|
20
|
+
(0, vitest_config_files_1.visitVitestConfigFiles)(tree, (filePath) => processVitestConfig(tree, filePath, unhandled));
|
|
21
|
+
(0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
|
|
22
|
+
if (!(0, vitest_config_files_1.isJsOrTsFile)(filePath))
|
|
23
|
+
return;
|
|
24
|
+
processSnapshotEnvironmentImport(tree, filePath, unhandled);
|
|
25
|
+
});
|
|
26
|
+
(0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
|
|
27
|
+
if (filePath.endsWith('package.json')) {
|
|
28
|
+
processPackageJson(tree, filePath);
|
|
29
|
+
}
|
|
30
|
+
else if (filePath.endsWith('project.json')) {
|
|
31
|
+
processProjectJson(tree, filePath);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
scanCiFiles(tree, unhandled);
|
|
35
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
36
|
+
// Hybrid migration return shape: `agentContext` (when populated) is
|
|
37
|
+
// forwarded to the paired prompt's agent. When no agent runs, the field is
|
|
38
|
+
// dropped silently per the contract — safe on master and pre-agentic flows.
|
|
39
|
+
if (unhandled.length > 0)
|
|
40
|
+
return { agentContext: unhandled };
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
function processVitestConfig(tree, filePath, unhandled) {
|
|
44
|
+
const contents = tree.read(filePath, 'utf-8');
|
|
45
|
+
// Cheap prechecks: skip parsing if none of the v3 targets appear.
|
|
46
|
+
const hasC8Provider = /['"]c8['"]/.test(contents);
|
|
47
|
+
const hasNoneProvider = /['"]none['"]/.test(contents);
|
|
48
|
+
const hasIndexScripts = contents.includes('indexScripts');
|
|
49
|
+
if (!hasC8Provider && !hasNoneProvider && !hasIndexScripts)
|
|
50
|
+
return;
|
|
51
|
+
const sourceFile = (0, tsquery_1.ast)(contents);
|
|
52
|
+
const edits = [];
|
|
53
|
+
const c8EditCountBefore = edits.length;
|
|
54
|
+
if (hasC8Provider) {
|
|
55
|
+
collectCoverageProviderC8Edits(sourceFile, contents, edits);
|
|
56
|
+
}
|
|
57
|
+
if (hasNoneProvider) {
|
|
58
|
+
collectBrowserProviderNoneEdits(sourceFile, contents, edits);
|
|
59
|
+
}
|
|
60
|
+
const indexEditCountBefore = edits.length;
|
|
61
|
+
if (hasIndexScripts) {
|
|
62
|
+
collectIndexScriptsRenameEdits(sourceFile, edits);
|
|
63
|
+
}
|
|
64
|
+
// Fallback signals: precheck hit a vitest-specific token but the AST didn't
|
|
65
|
+
// surface a shape we know how to rewrite (variable indirection, template
|
|
66
|
+
// literal, spread, ternary, etc.). Forward the file path so the agent can
|
|
67
|
+
// investigate.
|
|
68
|
+
if (hasC8Provider && edits.length === c8EditCountBefore) {
|
|
69
|
+
unhandled.push(`${filePath}: found a \`'c8'\` string but no direct \`coverage.provider: 'c8'\` assignment to rewrite. ` +
|
|
70
|
+
`If this file's coverage provider resolves to c8 via a variable, ternary, or spread, switch it to \`'v8'\`.`);
|
|
71
|
+
}
|
|
72
|
+
if (hasIndexScripts && edits.length === indexEditCountBefore) {
|
|
73
|
+
unhandled.push(`${filePath}: found an \`indexScripts\` reference but no direct \`browser.indexScripts\` assignment to rewrite. ` +
|
|
74
|
+
`If this is the legacy browser option, rename it to \`orchestratorScripts\`.`);
|
|
75
|
+
}
|
|
76
|
+
if (edits.length === 0)
|
|
77
|
+
return;
|
|
78
|
+
tree.write(filePath, (0, ast_edits_1.applyTextEdits)(contents, edits));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* `test.coverage.provider: 'c8'` → `'v8'`. Anchored to the `test.coverage`
|
|
82
|
+
* sub-tree so a coincidental `'c8'` elsewhere isn't rewritten.
|
|
83
|
+
*/
|
|
84
|
+
function collectCoverageProviderC8Edits(sourceFile, contents, edits) {
|
|
85
|
+
const literals = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment:has(Identifier[name=provider]) > StringLiteral[value=c8]');
|
|
86
|
+
for (const lit of literals) {
|
|
87
|
+
if (!isInsideTestSubProperty(lit, 'coverage'))
|
|
88
|
+
continue;
|
|
89
|
+
edits.push((0, ast_edits_1.replaceStringLiteralValueEdit)(contents, lit, 'v8'));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* `test.browser.provider: 'none'` → `'preview'`. Anchored similarly to v3-2b.
|
|
94
|
+
*/
|
|
95
|
+
function collectBrowserProviderNoneEdits(sourceFile, contents, edits) {
|
|
96
|
+
const literals = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment:has(Identifier[name=provider]) > StringLiteral[value=none]');
|
|
97
|
+
for (const lit of literals) {
|
|
98
|
+
if (!isInsideTestSubProperty(lit, 'browser'))
|
|
99
|
+
continue;
|
|
100
|
+
edits.push((0, ast_edits_1.replaceStringLiteralValueEdit)(contents, lit, 'preview'));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* `test.browser.indexScripts` → `orchestratorScripts`.
|
|
105
|
+
*/
|
|
106
|
+
function collectIndexScriptsRenameEdits(sourceFile, edits) {
|
|
107
|
+
const identifiers = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment > Identifier[name=indexScripts]');
|
|
108
|
+
for (const id of identifiers) {
|
|
109
|
+
if (!isInsideTestSubProperty(id, 'browser'))
|
|
110
|
+
continue;
|
|
111
|
+
edits.push((0, ast_edits_1.replaceNodeTextEdit)(id, 'orchestratorScripts'));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* `import { SnapshotEnvironment } from 'vitest'` → `from 'vitest/snapshot'`.
|
|
116
|
+
* Only applied when `SnapshotEnvironment` is the SOLE named binding. Mixed
|
|
117
|
+
* imports (e.g. `{ SnapshotEnvironment, vi }`) are logged for the agent —
|
|
118
|
+
* splitting the import is a semantic decision (does the v3.x `SnapshotEnvironment`
|
|
119
|
+
* still live in `vitest`? — no, it moved in v2).
|
|
120
|
+
*/
|
|
121
|
+
function processSnapshotEnvironmentImport(tree, filePath, unhandled) {
|
|
122
|
+
const contents = tree.read(filePath, 'utf-8');
|
|
123
|
+
if (!contents.includes('SnapshotEnvironment'))
|
|
124
|
+
return;
|
|
125
|
+
const sourceFile = (0, tsquery_1.ast)(contents);
|
|
126
|
+
const importDecls = (0, tsquery_1.query)(sourceFile, 'ImportDeclaration');
|
|
127
|
+
const edits = [];
|
|
128
|
+
for (const decl of importDecls) {
|
|
129
|
+
if (!ts.isStringLiteral(decl.moduleSpecifier))
|
|
130
|
+
continue;
|
|
131
|
+
if (decl.moduleSpecifier.text !== 'vitest')
|
|
132
|
+
continue;
|
|
133
|
+
const named = decl.importClause?.namedBindings;
|
|
134
|
+
if (!named || !ts.isNamedImports(named))
|
|
135
|
+
continue;
|
|
136
|
+
const elements = named.elements;
|
|
137
|
+
const hasSnapshotEnv = elements.some((el) => el.name.text === 'SnapshotEnvironment');
|
|
138
|
+
if (!hasSnapshotEnv)
|
|
139
|
+
continue;
|
|
140
|
+
if (elements.length === 1 && !decl.importClause?.name) {
|
|
141
|
+
// Sole named binding, no default import → flip the module specifier.
|
|
142
|
+
edits.push((0, ast_edits_1.replaceStringLiteralValueEdit)(contents, decl.moduleSpecifier, 'vitest/snapshot'));
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
unhandled.push(`${filePath}: \`SnapshotEnvironment\` is imported from 'vitest' alongside other bindings. ` +
|
|
146
|
+
`Split it into a separate \`import { SnapshotEnvironment } from 'vitest/snapshot'\` statement.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (edits.length > 0) {
|
|
150
|
+
tree.write(filePath, (0, ast_edits_1.applyTextEdits)(contents, edits));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const SEGFAULT_RETRY_RE = /\s*--segfault-retry(?:[= ]\d+)?(?=\s|$)/g;
|
|
154
|
+
// `\b` between `k` and `-` is still a word boundary, so the v3.x flag form
|
|
155
|
+
// `vitest --typecheck-helper` (hypothetical) would otherwise match. Use a
|
|
156
|
+
// lookahead requiring whitespace or end-of-string after the keyword.
|
|
157
|
+
const VITEST_TYPECHECK_RE = /\bvitest\s+typecheck(?=\s|$)/g;
|
|
158
|
+
function processPackageJson(tree, filePath) {
|
|
159
|
+
const contents = tree.read(filePath, 'utf-8');
|
|
160
|
+
let parsed;
|
|
161
|
+
try {
|
|
162
|
+
parsed = JSON.parse(contents);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
let changed = false;
|
|
168
|
+
// V3-1: strip `--segfault-retry` from script values.
|
|
169
|
+
// V3-3: `vitest typecheck` → `vitest --typecheck` in script values.
|
|
170
|
+
if (parsed.scripts && typeof parsed.scripts === 'object') {
|
|
171
|
+
for (const [name, value] of Object.entries(parsed.scripts)) {
|
|
172
|
+
if (typeof value !== 'string')
|
|
173
|
+
continue;
|
|
174
|
+
let updated = value;
|
|
175
|
+
if (updated.includes('--segfault-retry')) {
|
|
176
|
+
updated = updated.replace(SEGFAULT_RETRY_RE, '').trim();
|
|
177
|
+
}
|
|
178
|
+
updated = updated.replace(VITEST_TYPECHECK_RE, 'vitest --typecheck');
|
|
179
|
+
if (updated !== value) {
|
|
180
|
+
parsed.scripts[name] = updated;
|
|
181
|
+
changed = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// V3-2a: `@vitest/coverage-c8` → `@vitest/coverage-v8` in dep buckets,
|
|
186
|
+
// preserving the user's pin verbatim.
|
|
187
|
+
for (const bucket of [
|
|
188
|
+
'dependencies',
|
|
189
|
+
'devDependencies',
|
|
190
|
+
'peerDependencies',
|
|
191
|
+
'optionalDependencies',
|
|
192
|
+
]) {
|
|
193
|
+
const deps = parsed[bucket];
|
|
194
|
+
if (!deps || typeof deps !== 'object')
|
|
195
|
+
continue;
|
|
196
|
+
if (!('@vitest/coverage-c8' in deps))
|
|
197
|
+
continue;
|
|
198
|
+
if ('@vitest/coverage-v8' in deps) {
|
|
199
|
+
// Both keys present; leave the v8 pin alone and just remove the c8 entry.
|
|
200
|
+
delete deps['@vitest/coverage-c8'];
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Rebuild the bucket to preserve key order with c8 swapped for v8.
|
|
204
|
+
const rebuilt = {};
|
|
205
|
+
for (const [k, v] of Object.entries(deps)) {
|
|
206
|
+
if (k === '@vitest/coverage-c8')
|
|
207
|
+
rebuilt['@vitest/coverage-v8'] = v;
|
|
208
|
+
else
|
|
209
|
+
rebuilt[k] = v;
|
|
210
|
+
}
|
|
211
|
+
parsed[bucket] = rebuilt;
|
|
212
|
+
}
|
|
213
|
+
changed = true;
|
|
214
|
+
}
|
|
215
|
+
if (changed) {
|
|
216
|
+
const trailingNewline = contents.endsWith('\n') ? '\n' : '';
|
|
217
|
+
tree.write(filePath, JSON.stringify(parsed, null, 2) + trailingNewline);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Walk all targets in `project.json` and rewrite the same string-level
|
|
222
|
+
* concerns we handle in `package.json` scripts: `--segfault-retry` removal
|
|
223
|
+
* and `vitest typecheck` → `vitest --typecheck`. Applies to `options.args`
|
|
224
|
+
* (string or array), `options.command` (string), and `options.commands`
|
|
225
|
+
* (array of strings).
|
|
226
|
+
*/
|
|
227
|
+
function processProjectJson(tree, filePath) {
|
|
228
|
+
const contents = tree.read(filePath, 'utf-8');
|
|
229
|
+
let parsed;
|
|
230
|
+
try {
|
|
231
|
+
parsed = JSON.parse(contents);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
let changed = false;
|
|
237
|
+
const targets = parsed?.targets;
|
|
238
|
+
if (!targets || typeof targets !== 'object')
|
|
239
|
+
return;
|
|
240
|
+
for (const target of Object.values(targets)) {
|
|
241
|
+
const options = target?.options;
|
|
242
|
+
if (!options || typeof options !== 'object')
|
|
243
|
+
continue;
|
|
244
|
+
if (typeof options.args === 'string') {
|
|
245
|
+
const rewritten = rewriteScriptString(options.args);
|
|
246
|
+
if (rewritten !== options.args) {
|
|
247
|
+
options.args = rewritten;
|
|
248
|
+
changed = true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else if (Array.isArray(options.args)) {
|
|
252
|
+
const filtered = options.args.filter((a) => typeof a !== 'string' || !/^--segfault-retry(?:=\d+)?$/.test(a.trim()));
|
|
253
|
+
if (filtered.length !== options.args.length) {
|
|
254
|
+
options.args = filtered;
|
|
255
|
+
changed = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (typeof options.command === 'string') {
|
|
259
|
+
const rewritten = rewriteScriptString(options.command);
|
|
260
|
+
if (rewritten !== options.command) {
|
|
261
|
+
options.command = rewritten;
|
|
262
|
+
changed = true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (Array.isArray(options.commands)) {
|
|
266
|
+
for (let i = 0; i < options.commands.length; i++) {
|
|
267
|
+
const entry = options.commands[i];
|
|
268
|
+
if (typeof entry === 'string') {
|
|
269
|
+
const rewritten = rewriteScriptString(entry);
|
|
270
|
+
if (rewritten !== entry) {
|
|
271
|
+
options.commands[i] = rewritten;
|
|
272
|
+
changed = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (changed) {
|
|
279
|
+
const trailingNewline = contents.endsWith('\n') ? '\n' : '';
|
|
280
|
+
tree.write(filePath, JSON.stringify(parsed, null, 2) + trailingNewline);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function rewriteScriptString(value) {
|
|
284
|
+
let updated = value;
|
|
285
|
+
if (updated.includes('--segfault-retry')) {
|
|
286
|
+
updated = updated.replace(SEGFAULT_RETRY_RE, '').trim();
|
|
287
|
+
}
|
|
288
|
+
updated = updated.replace(VITEST_TYPECHECK_RE, 'vitest --typecheck');
|
|
289
|
+
return updated;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Scan CI provider configs for the v3 legacy tokens. YAML edits aren't done
|
|
293
|
+
* mechanically (structure/comment/anchor risk); the file path + tokens are
|
|
294
|
+
* forwarded to the agent.
|
|
295
|
+
*/
|
|
296
|
+
function scanCiFiles(tree, unhandled) {
|
|
297
|
+
(0, ci_files_1.visitCiFiles)(tree, (filePath, contents) => {
|
|
298
|
+
const tokens = [];
|
|
299
|
+
if (contents.includes('--segfault-retry')) {
|
|
300
|
+
tokens.push('`--segfault-retry`');
|
|
301
|
+
}
|
|
302
|
+
if (VITEST_TYPECHECK_RE.test(contents)) {
|
|
303
|
+
tokens.push('`vitest typecheck`');
|
|
304
|
+
}
|
|
305
|
+
// Reset lastIndex because of /g.
|
|
306
|
+
VITEST_TYPECHECK_RE.lastIndex = 0;
|
|
307
|
+
if (tokens.length === 0)
|
|
308
|
+
return;
|
|
309
|
+
unhandled.push(`${filePath}: found legacy token(s) in this CI config: ${tokens.join(', ')}. ` +
|
|
310
|
+
`Remove \`--segfault-retry\` (the option was removed in Vitest 2.0) and rewrite \`vitest typecheck\` as \`vitest --typecheck\` (the subcommand was replaced by the flag).`);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Returns true if `node` lives inside a property named `propertyName` whose
|
|
315
|
+
* own parent is a property named `test` — i.e. anchored to the vitest
|
|
316
|
+
* `test.<propertyName>` sub-tree of the config.
|
|
317
|
+
*/
|
|
318
|
+
function isInsideTestSubProperty(node, propertyName) {
|
|
319
|
+
const owningProperty = findEnclosingPropertyAssignment(node, propertyName);
|
|
320
|
+
if (!owningProperty)
|
|
321
|
+
return false;
|
|
322
|
+
return !!findEnclosingPropertyAssignment(owningProperty, 'test');
|
|
323
|
+
}
|
|
324
|
+
function findEnclosingPropertyAssignment(node, name) {
|
|
325
|
+
let current = node.parent;
|
|
326
|
+
while (current) {
|
|
327
|
+
if (ts.isPropertyAssignment(current) &&
|
|
328
|
+
ts.isIdentifier(current.name) &&
|
|
329
|
+
current.name.text === name) {
|
|
330
|
+
return current;
|
|
331
|
+
}
|
|
332
|
+
current = current.parent;
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Tree } from '@nx/devkit';
|
|
2
|
+
/**
|
|
3
|
+
* Hybrid migration paired with `ai-instructions-for-vitest-4.md`. Applies the
|
|
4
|
+
* deterministic, AST-tractable Vitest 3.x → 4.0 changes mechanically and
|
|
5
|
+
* forwards an `agentContext` describing any shape it could not handle so the
|
|
6
|
+
* paired prompt's agent can finish the rest.
|
|
7
|
+
*
|
|
8
|
+
* The "apply" set is intentionally narrow (renames, deletions of dead options,
|
|
9
|
+
* single-property string rewrites). Anything requiring cross-cutting surgery
|
|
10
|
+
* (e.g. pool option flattening, `deps.* → server.deps.*` merge, browser
|
|
11
|
+
* provider function-form rewrite, workspace file inlining) is detected and
|
|
12
|
+
* logged for the agent.
|
|
13
|
+
*/
|
|
14
|
+
export default function migrateToVitest4(tree: Tree): Promise<{
|
|
15
|
+
nextSteps?: string[];
|
|
16
|
+
agentContext?: string[];
|
|
17
|
+
}>;
|