@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,726 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = migrateToVitest4;
|
|
4
|
+
const devkit_1 = require("@nx/devkit");
|
|
5
|
+
const internal_1 = require("@nx/js/internal");
|
|
6
|
+
const tsquery_1 = require("@phenomnomnominal/tsquery");
|
|
7
|
+
const picomatch = require("picomatch");
|
|
8
|
+
const ast_edits_1 = require("./lib/ast-edits");
|
|
9
|
+
const ci_files_1 = require("./lib/ci-files");
|
|
10
|
+
const vitest_config_files_1 = require("./lib/vitest-config-files");
|
|
11
|
+
let ts;
|
|
12
|
+
/**
|
|
13
|
+
* Hybrid migration paired with `ai-instructions-for-vitest-4.md`. Applies the
|
|
14
|
+
* deterministic, AST-tractable Vitest 3.x → 4.0 changes mechanically and
|
|
15
|
+
* forwards an `agentContext` describing any shape it could not handle so the
|
|
16
|
+
* paired prompt's agent can finish the rest.
|
|
17
|
+
*
|
|
18
|
+
* The "apply" set is intentionally narrow (renames, deletions of dead options,
|
|
19
|
+
* single-property string rewrites). Anything requiring cross-cutting surgery
|
|
20
|
+
* (e.g. pool option flattening, `deps.* → server.deps.*` merge, browser
|
|
21
|
+
* provider function-form rewrite, workspace file inlining) is detected and
|
|
22
|
+
* logged for the agent.
|
|
23
|
+
*/
|
|
24
|
+
async function migrateToVitest4(tree) {
|
|
25
|
+
ts ??= (0, internal_1.ensureTypescript)();
|
|
26
|
+
const unhandled = [];
|
|
27
|
+
(0, vitest_config_files_1.visitVitestConfigFiles)(tree, (filePath) => processVitestConfig(tree, filePath, unhandled));
|
|
28
|
+
// Workspace files are removed entirely in v4 — their presence is always a
|
|
29
|
+
// signal that the agent needs to inline them into the root config.
|
|
30
|
+
(0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
|
|
31
|
+
if (!(0, vitest_config_files_1.isVitestWorkspaceFile)(filePath))
|
|
32
|
+
return;
|
|
33
|
+
unhandled.push(`${filePath}: \`vitest.workspace.*\` files are removed in Vitest 4. ` +
|
|
34
|
+
`Inline its project list into the root \`vitest.config.*\` under \`test.projects\` and delete this file.`);
|
|
35
|
+
});
|
|
36
|
+
(0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
|
|
37
|
+
if (!(0, vitest_config_files_1.isJsOrTsFile)(filePath))
|
|
38
|
+
return;
|
|
39
|
+
processImportsAndCustomCode(tree, filePath, unhandled);
|
|
40
|
+
});
|
|
41
|
+
(0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
|
|
42
|
+
if (filePath.endsWith('package.json')) {
|
|
43
|
+
processPackageJson(tree, filePath, unhandled);
|
|
44
|
+
}
|
|
45
|
+
else if (filePath.endsWith('project.json')) {
|
|
46
|
+
processProjectJson(tree, filePath, unhandled);
|
|
47
|
+
}
|
|
48
|
+
else if (isEnvFile(filePath)) {
|
|
49
|
+
processEnvFile(tree, filePath, unhandled);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
scanCiFiles(tree, unhandled);
|
|
53
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
54
|
+
// Always-shown reminder for state we cannot reach from the workspace tree.
|
|
55
|
+
const nextSteps = [
|
|
56
|
+
`If your CI provider stores Vitest env vars in its dashboard (GitHub Actions repo/org secrets, GitLab CI/CD variables, Vercel/Netlify env vars, etc.), rename \`VITEST_MAX_THREADS\` and \`VITEST_MAX_FORKS\` to \`VITEST_MAX_WORKERS\`, and \`VITE_NODE_DEPS_MODULE_DIRECTORIES\` to \`VITEST_MODULE_DIRECTORIES\`. The pre-pass only handles in-repo files.`,
|
|
57
|
+
];
|
|
58
|
+
const result = {
|
|
59
|
+
nextSteps,
|
|
60
|
+
};
|
|
61
|
+
if (unhandled.length > 0)
|
|
62
|
+
result.agentContext = unhandled;
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
const ENV_FILE_MATCHERS = [picomatch('**/.env'), picomatch('**/.env.*')];
|
|
66
|
+
function isEnvFile(filePath) {
|
|
67
|
+
const base = filePath.split('/').pop() ?? '';
|
|
68
|
+
// Skip `.envrc` (direnv) — different syntax surface.
|
|
69
|
+
if (base === '.envrc')
|
|
70
|
+
return false;
|
|
71
|
+
return ENV_FILE_MATCHERS.some((m) => m(filePath));
|
|
72
|
+
}
|
|
73
|
+
const COVERAGE_DEAD_OPTIONS = new Set([
|
|
74
|
+
'all',
|
|
75
|
+
'extensions',
|
|
76
|
+
'ignoreEmptyLines',
|
|
77
|
+
'experimentalAstAwareRemapping',
|
|
78
|
+
]);
|
|
79
|
+
const POOL_FLATTEN_OPTIONS = new Set(['execArgv', 'isolate']);
|
|
80
|
+
const REMOVED_REPORTER_CALLBACKS = new Set([
|
|
81
|
+
'onCollected',
|
|
82
|
+
'onSpecsCollected',
|
|
83
|
+
'onPathsCollected',
|
|
84
|
+
'onTaskUpdate',
|
|
85
|
+
'onFinished',
|
|
86
|
+
]);
|
|
87
|
+
function processVitestConfig(tree, filePath, unhandled) {
|
|
88
|
+
const contents = tree.read(filePath, 'utf-8');
|
|
89
|
+
// Cheap precheck: skip files that don't mention any v4 target.
|
|
90
|
+
if (!mightContainV4Targets(contents))
|
|
91
|
+
return;
|
|
92
|
+
const sourceFile = (0, tsquery_1.ast)(contents);
|
|
93
|
+
const edits = [];
|
|
94
|
+
collectCoverageRemovalEdits(sourceFile, contents, edits);
|
|
95
|
+
collectWorkspaceRenameEdits(sourceFile, edits);
|
|
96
|
+
collectDepsOptimizerWebEdits(sourceFile, edits);
|
|
97
|
+
collectUseAtomicsRemovalEdits(sourceFile, contents, edits);
|
|
98
|
+
collectMinWorkersRemovalEdits(sourceFile, contents, edits);
|
|
99
|
+
collectReporterRenameEdits(sourceFile, contents, edits);
|
|
100
|
+
collectPoolOptionDetectLogs(sourceFile, filePath, unhandled);
|
|
101
|
+
collectDepsServerMoveLogs(sourceFile, filePath, unhandled);
|
|
102
|
+
collectMatchGlobsLogs(sourceFile, filePath, unhandled);
|
|
103
|
+
collectBrowserStringProviderLogs(sourceFile, filePath, unhandled);
|
|
104
|
+
collectTesterScriptsLogs(sourceFile, filePath, unhandled);
|
|
105
|
+
collectCustomReporterCallbackLogs(sourceFile, filePath, unhandled);
|
|
106
|
+
if (edits.length > 0) {
|
|
107
|
+
tree.write(filePath, (0, ast_edits_1.applyTextEdits)(contents, edits));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function mightContainV4Targets(contents) {
|
|
111
|
+
// Hit-test: keywords we care about. Cheap string scan to avoid parsing
|
|
112
|
+
// every config file in the workspace.
|
|
113
|
+
return /\b(workspace|projects|coverage|useAtomics|minWorkers|reporters|provider|deps|poolOptions|singleFork|singleThread|maxThreads|maxForks|environmentMatchGlobs|poolMatchGlobs|testerScripts|onCollected|onSpecsCollected|onPathsCollected|onTaskUpdate|onFinished|vmThreads)\b/.test(contents);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Coverage option removals (V4-1): `all`, `extensions`, `ignoreEmptyLines`,
|
|
117
|
+
* `experimentalAstAwareRemapping` are all removed in v4. Anchored to
|
|
118
|
+
* `test.coverage`.
|
|
119
|
+
*/
|
|
120
|
+
function collectCoverageRemovalEdits(sourceFile, contents, edits) {
|
|
121
|
+
for (const optionName of COVERAGE_DEAD_OPTIONS) {
|
|
122
|
+
const identifiers = (0, tsquery_1.query)(sourceFile, `PropertyAssignment > Identifier[name=${optionName}]`);
|
|
123
|
+
for (const id of identifiers) {
|
|
124
|
+
if (!isInsideTestSubProperty(id, 'coverage'))
|
|
125
|
+
continue;
|
|
126
|
+
const owningProperty = id.parent;
|
|
127
|
+
edits.push((0, ast_edits_1.removeNodeWithTrailingCommaEdit)(contents, owningProperty));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* `test.workspace` → `test.projects` (V4-2). The external `vitest.workspace.*`
|
|
133
|
+
* file form is detected separately at the top level and always logged.
|
|
134
|
+
*/
|
|
135
|
+
function collectWorkspaceRenameEdits(sourceFile, edits) {
|
|
136
|
+
const identifiers = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment > Identifier[name=workspace]');
|
|
137
|
+
for (const id of identifiers) {
|
|
138
|
+
if (!isImmediateChildOfTest(id))
|
|
139
|
+
continue;
|
|
140
|
+
edits.push((0, ast_edits_1.replaceNodeTextEdit)(id, 'projects'));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* `deps.optimizer.web` → `deps.optimizer.client` (V4-4). Anchored to
|
|
145
|
+
* `test.deps.optimizer` so the `web` rename only applies to that nesting.
|
|
146
|
+
*/
|
|
147
|
+
function collectDepsOptimizerWebEdits(sourceFile, edits) {
|
|
148
|
+
const identifiers = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment > Identifier[name=web]');
|
|
149
|
+
for (const id of identifiers) {
|
|
150
|
+
if (!isInsideChain(id, ['test', 'deps', 'optimizer']))
|
|
151
|
+
continue;
|
|
152
|
+
edits.push((0, ast_edits_1.replaceNodeTextEdit)(id, 'client'));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Remove `poolOptions.threads.useAtomics` (V4-5).
|
|
157
|
+
*/
|
|
158
|
+
function collectUseAtomicsRemovalEdits(sourceFile, contents, edits) {
|
|
159
|
+
const identifiers = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment > Identifier[name=useAtomics]');
|
|
160
|
+
for (const id of identifiers) {
|
|
161
|
+
if (!isInsideChain(id, ['test', 'poolOptions', 'threads']))
|
|
162
|
+
continue;
|
|
163
|
+
const owningProperty = id.parent;
|
|
164
|
+
edits.push((0, ast_edits_1.removeNodeWithTrailingCommaEdit)(contents, owningProperty));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Remove top-level `test.minWorkers` (V4-6). Behaves as `0` in non-watch mode
|
|
169
|
+
* in v4, so dropping the assignment is safe.
|
|
170
|
+
*/
|
|
171
|
+
function collectMinWorkersRemovalEdits(sourceFile, contents, edits) {
|
|
172
|
+
const identifiers = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment > Identifier[name=minWorkers]');
|
|
173
|
+
for (const id of identifiers) {
|
|
174
|
+
if (!isImmediateChildOfTest(id))
|
|
175
|
+
continue;
|
|
176
|
+
const owningProperty = id.parent;
|
|
177
|
+
edits.push((0, ast_edits_1.removeNodeWithTrailingCommaEdit)(contents, owningProperty));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Reporter renames inside `test.reporters` (V4-7).
|
|
182
|
+
* `'verbose'` → `'tree'`
|
|
183
|
+
* `'basic'` → `['default', { summary: false }]`
|
|
184
|
+
*/
|
|
185
|
+
function collectReporterRenameEdits(sourceFile, contents, edits) {
|
|
186
|
+
const reportersProperties = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment:has(Identifier[name=reporters])').filter((p) => ts.isIdentifier(p.name) &&
|
|
187
|
+
p.name.text === 'reporters' &&
|
|
188
|
+
isImmediateChildOfTest(p.name));
|
|
189
|
+
for (const property of reportersProperties) {
|
|
190
|
+
const init = property.initializer;
|
|
191
|
+
if (!ts.isArrayLiteralExpression(init))
|
|
192
|
+
continue;
|
|
193
|
+
for (const element of init.elements) {
|
|
194
|
+
if (!ts.isStringLiteral(element))
|
|
195
|
+
continue;
|
|
196
|
+
if (element.text === 'verbose') {
|
|
197
|
+
edits.push((0, ast_edits_1.replaceStringLiteralValueEdit)(contents, element, 'tree'));
|
|
198
|
+
}
|
|
199
|
+
else if (element.text === 'basic') {
|
|
200
|
+
edits.push((0, ast_edits_1.replaceNodeTextEdit)(element, `['default', { summary: false }]`));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Detect (but do not modify) pool-related options whose v4 semantics require
|
|
207
|
+
* cross-cutting surgery: `singleThread`/`singleFork`, `maxThreads`/`maxForks`,
|
|
208
|
+
* `poolOptions.{forks,threads}.{execArgv,isolate}`,
|
|
209
|
+
* `poolOptions.vmThreads.memoryLimit`.
|
|
210
|
+
*/
|
|
211
|
+
function collectPoolOptionDetectLogs(sourceFile, filePath, unhandled) {
|
|
212
|
+
const singleHits = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment > Identifier[name=/^single(Thread|Fork)$/]');
|
|
213
|
+
for (const id of singleHits) {
|
|
214
|
+
const literalValue = readBooleanLiteralValue(id.parent);
|
|
215
|
+
if (literalValue === true) {
|
|
216
|
+
unhandled.push(`${filePath}: \`${id.text}: true\` is removed in Vitest 4. ` +
|
|
217
|
+
`Replace it with \`maxWorkers: 1, isolate: false\` at the top of the \`test\` block, then remove \`${id.text}\`.`);
|
|
218
|
+
}
|
|
219
|
+
else if (literalValue === false) {
|
|
220
|
+
unhandled.push(`${filePath}: \`${id.text}: false\` is removed in Vitest 4 (it was already the default). ` +
|
|
221
|
+
`Delete the property.`);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
unhandled.push(`${filePath}: \`${id.text}\` is removed in Vitest 4 and the value here is not a boolean literal. ` +
|
|
225
|
+
`If it evaluates to \`true\`, replace with \`maxWorkers: 1, isolate: false\` at the top of \`test\` and remove \`${id.text}\`. ` +
|
|
226
|
+
`If it evaluates to \`false\`, delete it.`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const maxHits = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment > Identifier[name=/^max(Threads|Forks)$/]');
|
|
230
|
+
for (const id of maxHits) {
|
|
231
|
+
if (!isImmediateChildOfTest(id))
|
|
232
|
+
continue;
|
|
233
|
+
const literalValue = readNumericLiteralValue(id.parent);
|
|
234
|
+
const currentValueDescr = literalValue !== undefined ? ` (current value: ${literalValue})` : '';
|
|
235
|
+
unhandled.push(`${filePath}: \`test.${id.text}\`${currentValueDescr} is removed in Vitest 4. ` +
|
|
236
|
+
`Replace with the pool-agnostic \`maxWorkers\` option. ` +
|
|
237
|
+
`If both \`maxThreads\` and \`maxForks\` are set with different values, pick the one matching the pool the project actually uses.`);
|
|
238
|
+
}
|
|
239
|
+
for (const optionName of POOL_FLATTEN_OPTIONS) {
|
|
240
|
+
const ids = (0, tsquery_1.query)(sourceFile, `PropertyAssignment > Identifier[name=${optionName}]`);
|
|
241
|
+
for (const id of ids) {
|
|
242
|
+
const insideForks = isInsideChain(id, ['test', 'poolOptions', 'forks']);
|
|
243
|
+
const insideThreads = isInsideChain(id, [
|
|
244
|
+
'test',
|
|
245
|
+
'poolOptions',
|
|
246
|
+
'threads',
|
|
247
|
+
]);
|
|
248
|
+
if (!insideForks && !insideThreads)
|
|
249
|
+
continue;
|
|
250
|
+
const pool = insideForks ? 'forks' : 'threads';
|
|
251
|
+
unhandled.push(`${filePath}: \`test.poolOptions.${pool}.${optionName}\` was flattened in Vitest 4. ` +
|
|
252
|
+
`Move it to the top-level \`test.${optionName}\` (one value for all pools).`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const memoryLimitIds = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment > Identifier[name=memoryLimit]');
|
|
256
|
+
for (const id of memoryLimitIds) {
|
|
257
|
+
if (!isInsideChain(id, ['test', 'poolOptions', 'vmThreads']))
|
|
258
|
+
continue;
|
|
259
|
+
unhandled.push(`${filePath}: \`test.poolOptions.vmThreads.memoryLimit\` was renamed and lifted in Vitest 4. ` +
|
|
260
|
+
`Move it to top-level \`test.vmMemoryLimit\`.`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function readBooleanLiteralValue(property) {
|
|
264
|
+
const init = property.initializer;
|
|
265
|
+
if (init.kind === ts.SyntaxKind.TrueKeyword)
|
|
266
|
+
return true;
|
|
267
|
+
if (init.kind === ts.SyntaxKind.FalseKeyword)
|
|
268
|
+
return false;
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
function readNumericLiteralValue(property) {
|
|
272
|
+
const init = property.initializer;
|
|
273
|
+
if (ts.isNumericLiteral(init))
|
|
274
|
+
return Number(init.text);
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Detect `test.deps.{external,inline,fallbackCJS}`. Moving them under
|
|
279
|
+
* `test.server.deps` requires conflict-aware merging that we don't attempt
|
|
280
|
+
* mechanically.
|
|
281
|
+
*/
|
|
282
|
+
function collectDepsServerMoveLogs(sourceFile, filePath, unhandled) {
|
|
283
|
+
for (const name of ['external', 'inline', 'fallbackCJS']) {
|
|
284
|
+
const ids = (0, tsquery_1.query)(sourceFile, `PropertyAssignment > Identifier[name=${name}]`);
|
|
285
|
+
for (const id of ids) {
|
|
286
|
+
if (!isInsideChain(id, ['test', 'deps']))
|
|
287
|
+
continue;
|
|
288
|
+
// Don't flag `test.deps.optimizer.<...>` — only direct children of deps.
|
|
289
|
+
if (!isImmediateChildOfProperty(id, 'deps'))
|
|
290
|
+
continue;
|
|
291
|
+
unhandled.push(`${filePath}: \`test.deps.${name}\` moved to \`test.server.deps.${name}\` in Vitest 4. ` +
|
|
292
|
+
`Move the property, merging with any existing \`server.deps\` block.`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* `test.poolMatchGlobs` / `test.environmentMatchGlobs` are removed in v4;
|
|
298
|
+
* their replacement is per-project conditions inside `test.projects`. Always
|
|
299
|
+
* a manual rewrite.
|
|
300
|
+
*/
|
|
301
|
+
function collectMatchGlobsLogs(sourceFile, filePath, unhandled) {
|
|
302
|
+
for (const name of ['poolMatchGlobs', 'environmentMatchGlobs']) {
|
|
303
|
+
const ids = (0, tsquery_1.query)(sourceFile, `PropertyAssignment > Identifier[name=${name}]`);
|
|
304
|
+
for (const id of ids) {
|
|
305
|
+
if (!isImmediateChildOfTest(id))
|
|
306
|
+
continue;
|
|
307
|
+
unhandled.push(`${filePath}: \`test.${name}\` is removed in Vitest 4. ` +
|
|
308
|
+
`Express the same per-glob configuration via \`test.projects\` entries with conditions.`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* `browser.provider` as a string is no longer valid in v4 — it must be the
|
|
314
|
+
* return value of a provider function imported from a per-provider package.
|
|
315
|
+
*/
|
|
316
|
+
function collectBrowserStringProviderLogs(sourceFile, filePath, unhandled) {
|
|
317
|
+
const providers = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment:has(Identifier[name=provider]) > StringLiteral');
|
|
318
|
+
for (const lit of providers) {
|
|
319
|
+
if (!isInsideTestSubProperty(lit, 'browser'))
|
|
320
|
+
continue;
|
|
321
|
+
unhandled.push(`${filePath}: \`browser.provider\` is no longer a string in Vitest 4 (current value: ${lit.getText()}). ` +
|
|
322
|
+
`Install the matching \`@vitest/browser-<provider>\` package and use the function form, ` +
|
|
323
|
+
`e.g. \`provider: playwright(...)\`.`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* `browser.testerScripts` (array of scripts) was replaced by
|
|
328
|
+
* `browser.testerHtmlPath` (single HTML file) — a semantic change.
|
|
329
|
+
*/
|
|
330
|
+
function collectTesterScriptsLogs(sourceFile, filePath, unhandled) {
|
|
331
|
+
const ids = (0, tsquery_1.query)(sourceFile, 'PropertyAssignment > Identifier[name=testerScripts]');
|
|
332
|
+
for (const id of ids) {
|
|
333
|
+
if (!isInsideTestSubProperty(id, 'browser'))
|
|
334
|
+
continue;
|
|
335
|
+
unhandled.push(`${filePath}: \`browser.testerScripts\` was removed in Vitest 4 in favor of \`browser.testerHtmlPath\`. ` +
|
|
336
|
+
`Combine the script contents into a single HTML file and point \`testerHtmlPath\` at it.`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Custom reporter callbacks removed in v4: `onCollected`, `onSpecsCollected`,
|
|
341
|
+
* `onPathsCollected`, `onTaskUpdate`, `onFinished`. They can appear as
|
|
342
|
+
* `PropertyAssignment` (object literal: `onCollected: () => ...`),
|
|
343
|
+
* `MethodDeclaration` (shorthand: `onCollected() {}`), or class method on a
|
|
344
|
+
* reporter class — the search broadens past `PropertyAssignment` to cover
|
|
345
|
+
* all three.
|
|
346
|
+
*/
|
|
347
|
+
function collectCustomReporterCallbackLogs(sourceFile, filePath, unhandled) {
|
|
348
|
+
for (const name of REMOVED_REPORTER_CALLBACKS) {
|
|
349
|
+
const ids = (0, tsquery_1.query)(sourceFile, `Identifier[name=${name}]`);
|
|
350
|
+
const matchedAsName = ids.some(isPropertyOrMethodName);
|
|
351
|
+
if (matchedAsName) {
|
|
352
|
+
unhandled.push(`${filePath}: custom reporter uses the removed-in-v4 \`${name}\` callback. ` +
|
|
353
|
+
`Migrate to the v4 \`onTestModuleCollected\` / \`onTestCaseResult\` / \`onTestRunEnd\` API.`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function isPropertyOrMethodName(id) {
|
|
358
|
+
const parent = id.parent;
|
|
359
|
+
if (!parent)
|
|
360
|
+
return false;
|
|
361
|
+
if (!(ts.isPropertyAssignment(parent) ||
|
|
362
|
+
ts.isShorthandPropertyAssignment(parent) ||
|
|
363
|
+
ts.isMethodDeclaration(parent) ||
|
|
364
|
+
ts.isGetAccessorDeclaration(parent) ||
|
|
365
|
+
ts.isSetAccessorDeclaration(parent) ||
|
|
366
|
+
ts.isMethodSignature(parent) ||
|
|
367
|
+
ts.isPropertySignature(parent))) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
// The identifier must be the NAME of the property/method, not an
|
|
371
|
+
// initializer-side reference.
|
|
372
|
+
return parent.name === id;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Handles import-side concerns:
|
|
376
|
+
* - `@vitest/browser/context` → `vitest/browser` (V4-3a, mechanical when
|
|
377
|
+
* the subpath is exactly `/context`).
|
|
378
|
+
* - `@vitest/browser/utils` (logged — semantic restructure).
|
|
379
|
+
* - `defineWorkspace` from `vitest/config` (logged — file removed in v4).
|
|
380
|
+
* - `jest-snapshot` imports inside custom matcher files (logged).
|
|
381
|
+
*/
|
|
382
|
+
function processImportsAndCustomCode(tree, filePath, unhandled) {
|
|
383
|
+
const contents = tree.read(filePath, 'utf-8');
|
|
384
|
+
const hasBrowserPackage = contents.includes('@vitest/browser');
|
|
385
|
+
const hasDefineWorkspace = contents.includes('defineWorkspace');
|
|
386
|
+
const hasJestSnapshot = contents.includes('jest-snapshot');
|
|
387
|
+
if (!hasBrowserPackage && !hasDefineWorkspace && !hasJestSnapshot)
|
|
388
|
+
return;
|
|
389
|
+
const sourceFile = (0, tsquery_1.ast)(contents);
|
|
390
|
+
const importDecls = (0, tsquery_1.query)(sourceFile, 'ImportDeclaration');
|
|
391
|
+
const edits = [];
|
|
392
|
+
for (const decl of importDecls) {
|
|
393
|
+
if (!ts.isStringLiteral(decl.moduleSpecifier))
|
|
394
|
+
continue;
|
|
395
|
+
const spec = decl.moduleSpecifier.text;
|
|
396
|
+
if (spec === '@vitest/browser/context') {
|
|
397
|
+
edits.push((0, ast_edits_1.replaceStringLiteralValueEdit)(contents, decl.moduleSpecifier, 'vitest/browser'));
|
|
398
|
+
}
|
|
399
|
+
else if (spec === '@vitest/browser/utils') {
|
|
400
|
+
unhandled.push(`${filePath}: imports from '@vitest/browser/utils' moved into 'vitest/browser' under a named \`utils\` export in Vitest 4. ` +
|
|
401
|
+
`Rewrite to \`import { utils } from 'vitest/browser'\` and rebind the used helpers.`);
|
|
402
|
+
}
|
|
403
|
+
else if (spec === '@vitest/browser') {
|
|
404
|
+
unhandled.push(`${filePath}: bare \`@vitest/browser\` imports — the package's public surface moved into \`vitest/browser\` (and the per-provider packages) in Vitest 4. ` +
|
|
405
|
+
`Rewrite the import to \`vitest/browser\` and rebind any helpers that moved to the per-provider package.`);
|
|
406
|
+
}
|
|
407
|
+
else if (spec === 'jest-snapshot') {
|
|
408
|
+
unhandled.push(`${filePath}: custom matcher imports from 'jest-snapshot'. ` +
|
|
409
|
+
`In Vitest 4, import the snapshot helpers from 'vitest' via the \`Snapshots\` namespace.`);
|
|
410
|
+
}
|
|
411
|
+
// `defineWorkspace` is named-imported from vitest/config — flag any usage.
|
|
412
|
+
const named = decl.importClause?.namedBindings;
|
|
413
|
+
if (named && ts.isNamedImports(named)) {
|
|
414
|
+
const usesDefineWorkspace = named.elements.some((el) => el.name.text === 'defineWorkspace');
|
|
415
|
+
if (usesDefineWorkspace) {
|
|
416
|
+
unhandled.push(`${filePath}: \`defineWorkspace\` is removed in Vitest 4 (the external \`vitest.workspace.*\` form is gone). ` +
|
|
417
|
+
`Replace with \`defineConfig\` and inline the project list under \`test.projects\` in the root vitest config.`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (edits.length > 0) {
|
|
422
|
+
tree.write(filePath, (0, ast_edits_1.applyTextEdits)(contents, edits));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const ENV_RENAMES = [
|
|
426
|
+
[/\bVITEST_MAX_THREADS\b/g, 'VITEST_MAX_WORKERS'],
|
|
427
|
+
[/\bVITEST_MAX_FORKS\b/g, 'VITEST_MAX_WORKERS'],
|
|
428
|
+
[/\bVITE_NODE_DEPS_MODULE_DIRECTORIES\b/g, 'VITEST_MODULE_DIRECTORIES'],
|
|
429
|
+
];
|
|
430
|
+
function processPackageJson(tree, filePath, unhandled) {
|
|
431
|
+
const contents = tree.read(filePath, 'utf-8');
|
|
432
|
+
let parsed;
|
|
433
|
+
try {
|
|
434
|
+
parsed = JSON.parse(contents);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
let changed = false;
|
|
440
|
+
if (parsed.scripts && typeof parsed.scripts === 'object') {
|
|
441
|
+
for (const [name, value] of Object.entries(parsed.scripts)) {
|
|
442
|
+
if (typeof value !== 'string')
|
|
443
|
+
continue;
|
|
444
|
+
let updated = value;
|
|
445
|
+
for (const [re, replacement] of ENV_RENAMES) {
|
|
446
|
+
updated = updated.replace(re, replacement);
|
|
447
|
+
}
|
|
448
|
+
if (updated !== value) {
|
|
449
|
+
parsed.scripts[name] = updated;
|
|
450
|
+
changed = true;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// `@vitest/browser` surface moved into the main `vitest` package and the
|
|
455
|
+
// per-provider packages. Removing the dep without installing the new
|
|
456
|
+
// provider package would break browser runs, so log instead of editing.
|
|
457
|
+
for (const bucket of [
|
|
458
|
+
'dependencies',
|
|
459
|
+
'devDependencies',
|
|
460
|
+
'peerDependencies',
|
|
461
|
+
'optionalDependencies',
|
|
462
|
+
]) {
|
|
463
|
+
const deps = parsed[bucket];
|
|
464
|
+
if (!deps || typeof deps !== 'object')
|
|
465
|
+
continue;
|
|
466
|
+
if ('@vitest/browser' in deps) {
|
|
467
|
+
unhandled.push(`${filePath}: \`@vitest/browser\` is no longer needed at the top level in Vitest 4 — ` +
|
|
468
|
+
`install the matching per-provider package (\`@vitest/browser-playwright\`, \`@vitest/browser-webdriverio\`, ` +
|
|
469
|
+
`or \`@vitest/browser-preview\`) and remove the \`@vitest/browser\` entry.`);
|
|
470
|
+
break; // one note per file is enough
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (changed) {
|
|
474
|
+
const trailingNewline = contents.endsWith('\n') ? '\n' : '';
|
|
475
|
+
tree.write(filePath, JSON.stringify(parsed, null, 2) + trailingNewline);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Rewrite `.env` / `.env.*` files: rename the legacy Vitest env vars to the
|
|
480
|
+
* v4-aware names. `VITEST_MAX_THREADS`/`VITEST_MAX_FORKS` only get renamed
|
|
481
|
+
* when exactly one of them is present in the file — both map to the same
|
|
482
|
+
* pool-agnostic `VITEST_MAX_WORKERS`, so a blind rename would produce two
|
|
483
|
+
* conflicting assignments (the latter would silently win per dotenv
|
|
484
|
+
* semantics). When both are present the rename is skipped and the conflict
|
|
485
|
+
* is forwarded to the agent.
|
|
486
|
+
*/
|
|
487
|
+
function processEnvFile(tree, filePath, unhandled) {
|
|
488
|
+
const contents = tree.read(filePath, 'utf-8');
|
|
489
|
+
if (!/(?:VITEST_MAX_THREADS|VITEST_MAX_FORKS|VITE_NODE_DEPS_MODULE_DIRECTORIES)\b/.test(contents)) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const hasThreads = /^(?:\s*(?:export\s+)?)VITEST_MAX_THREADS\b/m.test(contents);
|
|
493
|
+
const hasForks = /^(?:\s*(?:export\s+)?)VITEST_MAX_FORKS\b/m.test(contents);
|
|
494
|
+
let updated = contents;
|
|
495
|
+
if (hasThreads && hasForks) {
|
|
496
|
+
unhandled.push(`${filePath}: both \`VITEST_MAX_THREADS\` and \`VITEST_MAX_FORKS\` are set — both collapse to the single pool-agnostic \`VITEST_MAX_WORKERS\` in Vitest 4. ` +
|
|
497
|
+
`Decide which value should win based on the pool the project uses (\`test.pool\` in the vitest config) and rename only that line.`);
|
|
498
|
+
}
|
|
499
|
+
else if (hasThreads) {
|
|
500
|
+
updated = updated.replace(/^((?:\s*(?:export\s+)?))VITEST_MAX_THREADS\b/gm, '$1VITEST_MAX_WORKERS');
|
|
501
|
+
}
|
|
502
|
+
else if (hasForks) {
|
|
503
|
+
updated = updated.replace(/^((?:\s*(?:export\s+)?))VITEST_MAX_FORKS\b/gm, '$1VITEST_MAX_WORKERS');
|
|
504
|
+
}
|
|
505
|
+
updated = updated.replace(/^((?:\s*(?:export\s+)?))VITE_NODE_DEPS_MODULE_DIRECTORIES\b/gm, '$1VITEST_MODULE_DIRECTORIES');
|
|
506
|
+
if (updated !== contents) {
|
|
507
|
+
tree.write(filePath, updated);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Walk all targets in `project.json` and apply v4 env-var renames inline:
|
|
512
|
+
* - `options.env` keys
|
|
513
|
+
* - `options.{args,command,commands}` string content (VAR=value prefixes)
|
|
514
|
+
* Same conflict guard as `.env` files.
|
|
515
|
+
*/
|
|
516
|
+
function processProjectJson(tree, filePath, unhandled) {
|
|
517
|
+
const contents = tree.read(filePath, 'utf-8');
|
|
518
|
+
let parsed;
|
|
519
|
+
try {
|
|
520
|
+
parsed = JSON.parse(contents);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const targets = parsed?.targets;
|
|
526
|
+
if (!targets || typeof targets !== 'object')
|
|
527
|
+
return;
|
|
528
|
+
let changed = false;
|
|
529
|
+
for (const [targetName, target] of Object.entries(targets)) {
|
|
530
|
+
const options = target?.options;
|
|
531
|
+
if (!options || typeof options !== 'object')
|
|
532
|
+
continue;
|
|
533
|
+
// `options.env`: rename keys, with the same threads/forks conflict guard.
|
|
534
|
+
if (options.env && typeof options.env === 'object') {
|
|
535
|
+
const hasThreads = 'VITEST_MAX_THREADS' in options.env;
|
|
536
|
+
const hasForks = 'VITEST_MAX_FORKS' in options.env;
|
|
537
|
+
if (hasThreads && hasForks) {
|
|
538
|
+
unhandled.push(`${filePath} (target \`${targetName}\`): both \`VITEST_MAX_THREADS\` and \`VITEST_MAX_FORKS\` are set in \`options.env\` — both collapse to \`VITEST_MAX_WORKERS\` in Vitest 4. ` +
|
|
539
|
+
`Decide which value should win and rename only that key.`);
|
|
540
|
+
}
|
|
541
|
+
else if (hasThreads) {
|
|
542
|
+
options.env = renameObjectKey(options.env, 'VITEST_MAX_THREADS', 'VITEST_MAX_WORKERS');
|
|
543
|
+
changed = true;
|
|
544
|
+
}
|
|
545
|
+
else if (hasForks) {
|
|
546
|
+
options.env = renameObjectKey(options.env, 'VITEST_MAX_FORKS', 'VITEST_MAX_WORKERS');
|
|
547
|
+
changed = true;
|
|
548
|
+
}
|
|
549
|
+
if ('VITE_NODE_DEPS_MODULE_DIRECTORIES' in options.env) {
|
|
550
|
+
options.env = renameObjectKey(options.env, 'VITE_NODE_DEPS_MODULE_DIRECTORIES', 'VITEST_MODULE_DIRECTORIES');
|
|
551
|
+
changed = true;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// `options.args` / `options.command` / `options.commands`: in-string renames.
|
|
555
|
+
if (typeof options.args === 'string') {
|
|
556
|
+
const rewritten = rewriteInlineEnvVars(options.args, filePath, targetName, unhandled);
|
|
557
|
+
if (rewritten !== options.args) {
|
|
558
|
+
options.args = rewritten;
|
|
559
|
+
changed = true;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else if (Array.isArray(options.args)) {
|
|
563
|
+
for (let i = 0; i < options.args.length; i++) {
|
|
564
|
+
const entry = options.args[i];
|
|
565
|
+
if (typeof entry !== 'string')
|
|
566
|
+
continue;
|
|
567
|
+
const rewritten = rewriteInlineEnvVars(entry, filePath, targetName, unhandled);
|
|
568
|
+
if (rewritten !== entry) {
|
|
569
|
+
options.args[i] = rewritten;
|
|
570
|
+
changed = true;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (typeof options.command === 'string') {
|
|
575
|
+
const rewritten = rewriteInlineEnvVars(options.command, filePath, targetName, unhandled);
|
|
576
|
+
if (rewritten !== options.command) {
|
|
577
|
+
options.command = rewritten;
|
|
578
|
+
changed = true;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (Array.isArray(options.commands)) {
|
|
582
|
+
for (let i = 0; i < options.commands.length; i++) {
|
|
583
|
+
const entry = options.commands[i];
|
|
584
|
+
if (typeof entry !== 'string')
|
|
585
|
+
continue;
|
|
586
|
+
const rewritten = rewriteInlineEnvVars(entry, filePath, targetName, unhandled);
|
|
587
|
+
if (rewritten !== entry) {
|
|
588
|
+
options.commands[i] = rewritten;
|
|
589
|
+
changed = true;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (changed) {
|
|
595
|
+
const trailingNewline = contents.endsWith('\n') ? '\n' : '';
|
|
596
|
+
tree.write(filePath, JSON.stringify(parsed, null, 2) + trailingNewline);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function renameObjectKey(obj, oldKey, newKey) {
|
|
600
|
+
const rebuilt = {};
|
|
601
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
602
|
+
rebuilt[k === oldKey ? newKey : k] = v;
|
|
603
|
+
}
|
|
604
|
+
return rebuilt;
|
|
605
|
+
}
|
|
606
|
+
const VITEST_MAX_THREADS_INLINE = /\bVITEST_MAX_THREADS\b/g;
|
|
607
|
+
const VITEST_MAX_FORKS_INLINE = /\bVITEST_MAX_FORKS\b/g;
|
|
608
|
+
const VITE_NODE_DEPS_INLINE = /\bVITE_NODE_DEPS_MODULE_DIRECTORIES\b/g;
|
|
609
|
+
function rewriteInlineEnvVars(value, filePath, targetName, unhandled) {
|
|
610
|
+
const hasThreads = /\bVITEST_MAX_THREADS\b/.test(value);
|
|
611
|
+
const hasForks = /\bVITEST_MAX_FORKS\b/.test(value);
|
|
612
|
+
let updated = value;
|
|
613
|
+
if (hasThreads && hasForks) {
|
|
614
|
+
unhandled.push(`${filePath} (target \`${targetName}\`): \`VITEST_MAX_THREADS\` and \`VITEST_MAX_FORKS\` both appear in the same command — both collapse to \`VITEST_MAX_WORKERS\` in Vitest 4. ` +
|
|
615
|
+
`Decide which value should win and rename only that occurrence.`);
|
|
616
|
+
}
|
|
617
|
+
else if (hasThreads) {
|
|
618
|
+
updated = updated.replace(VITEST_MAX_THREADS_INLINE, 'VITEST_MAX_WORKERS');
|
|
619
|
+
}
|
|
620
|
+
else if (hasForks) {
|
|
621
|
+
updated = updated.replace(VITEST_MAX_FORKS_INLINE, 'VITEST_MAX_WORKERS');
|
|
622
|
+
}
|
|
623
|
+
updated = updated.replace(VITE_NODE_DEPS_INLINE, 'VITEST_MODULE_DIRECTORIES');
|
|
624
|
+
return updated;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Scan CI provider configs for v4 legacy env-var tokens and forward the file
|
|
628
|
+
* paths to the agent. YAML structure varies by provider; mechanical edits
|
|
629
|
+
* risk breaking comments, anchors, and multi-line strings.
|
|
630
|
+
*/
|
|
631
|
+
function scanCiFiles(tree, unhandled) {
|
|
632
|
+
(0, ci_files_1.visitCiFiles)(tree, (filePath, contents) => {
|
|
633
|
+
const tokens = [];
|
|
634
|
+
if (/\bVITEST_MAX_THREADS\b/.test(contents)) {
|
|
635
|
+
tokens.push('`VITEST_MAX_THREADS`');
|
|
636
|
+
}
|
|
637
|
+
if (/\bVITEST_MAX_FORKS\b/.test(contents)) {
|
|
638
|
+
tokens.push('`VITEST_MAX_FORKS`');
|
|
639
|
+
}
|
|
640
|
+
if (/\bVITE_NODE_DEPS_MODULE_DIRECTORIES\b/.test(contents)) {
|
|
641
|
+
tokens.push('`VITE_NODE_DEPS_MODULE_DIRECTORIES`');
|
|
642
|
+
}
|
|
643
|
+
if (tokens.length === 0)
|
|
644
|
+
return;
|
|
645
|
+
unhandled.push(`${filePath}: found legacy Vitest env-var token(s) in this CI config: ${tokens.join(', ')}. ` +
|
|
646
|
+
`Rename \`VITEST_MAX_THREADS\` / \`VITEST_MAX_FORKS\` → \`VITEST_MAX_WORKERS\` (pick a single value when both are set), and \`VITE_NODE_DEPS_MODULE_DIRECTORIES\` → \`VITEST_MODULE_DIRECTORIES\`.`);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Returns true if `node` lives inside a property whose name is
|
|
651
|
+
* `propertyName`, whose own parent is a property named `test`.
|
|
652
|
+
*/
|
|
653
|
+
function isInsideTestSubProperty(node, propertyName) {
|
|
654
|
+
const owning = findEnclosingPropertyAssignment(node, propertyName);
|
|
655
|
+
if (!owning)
|
|
656
|
+
return false;
|
|
657
|
+
return !!findEnclosingPropertyAssignment(owning, 'test');
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Returns true if `node`'s identifier sits as a direct property of a
|
|
661
|
+
* `test:` block (i.e. `test.{name}`, not `test.something.{name}`).
|
|
662
|
+
*/
|
|
663
|
+
function isImmediateChildOfTest(node) {
|
|
664
|
+
// The Identifier's parent is a PropertyAssignment; that PA's parent is the
|
|
665
|
+
// ObjectLiteralExpression of `test`; that OLE's parent is the `test:`
|
|
666
|
+
// PropertyAssignment itself.
|
|
667
|
+
const pa = node.parent;
|
|
668
|
+
if (!pa || !ts.isPropertyAssignment(pa))
|
|
669
|
+
return false;
|
|
670
|
+
const ole = pa.parent;
|
|
671
|
+
if (!ole || !ts.isObjectLiteralExpression(ole))
|
|
672
|
+
return false;
|
|
673
|
+
const testPa = ole.parent;
|
|
674
|
+
if (!testPa || !ts.isPropertyAssignment(testPa))
|
|
675
|
+
return false;
|
|
676
|
+
return ts.isIdentifier(testPa.name) && testPa.name.text === 'test';
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Like `isImmediateChildOfTest` but parameterised on the immediate parent's
|
|
680
|
+
* property name.
|
|
681
|
+
*/
|
|
682
|
+
function isImmediateChildOfProperty(node, propertyName) {
|
|
683
|
+
const pa = node.parent;
|
|
684
|
+
if (!pa || !ts.isPropertyAssignment(pa))
|
|
685
|
+
return false;
|
|
686
|
+
const ole = pa.parent;
|
|
687
|
+
if (!ole || !ts.isObjectLiteralExpression(ole))
|
|
688
|
+
return false;
|
|
689
|
+
const ownerPa = ole.parent;
|
|
690
|
+
if (!ownerPa || !ts.isPropertyAssignment(ownerPa))
|
|
691
|
+
return false;
|
|
692
|
+
return ts.isIdentifier(ownerPa.name) && ownerPa.name.text === propertyName;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Returns true if `node`'s ancestors include a chain of property names —
|
|
696
|
+
* outer-most first. So `['test', 'deps', 'optimizer']` matches a node living
|
|
697
|
+
* inside `test.deps.optimizer.<something>` (any sub-property name, e.g.
|
|
698
|
+
* `web`).
|
|
699
|
+
*/
|
|
700
|
+
function isInsideChain(node, chain) {
|
|
701
|
+
// Walk inner → outer through enclosing properties, expecting names to
|
|
702
|
+
// match `chain` from the last (innermost) to the first (outermost).
|
|
703
|
+
let current = node.parent;
|
|
704
|
+
let chainIndex = chain.length - 1;
|
|
705
|
+
while (current && chainIndex >= 0) {
|
|
706
|
+
if (ts.isPropertyAssignment(current) && ts.isIdentifier(current.name)) {
|
|
707
|
+
if (current.name.text === chain[chainIndex]) {
|
|
708
|
+
chainIndex--;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
current = current.parent;
|
|
712
|
+
}
|
|
713
|
+
return chainIndex < 0;
|
|
714
|
+
}
|
|
715
|
+
function findEnclosingPropertyAssignment(node, name) {
|
|
716
|
+
let current = node.parent;
|
|
717
|
+
while (current) {
|
|
718
|
+
if (ts.isPropertyAssignment(current) &&
|
|
719
|
+
ts.isIdentifier(current.name) &&
|
|
720
|
+
current.name.text === name) {
|
|
721
|
+
return current;
|
|
722
|
+
}
|
|
723
|
+
current = current.parent;
|
|
724
|
+
}
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|