vite-plus 0.3.3 → 1.0.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/LICENSE +35 -146
- package/README.md +3 -3
- package/binding/index.cjs +57 -54
- package/binding/index.d.cts +9 -0
- package/dist/{agent-Cu6-tXIP.js → agent-Dinpyd-k.js} +6 -5
- package/dist/bin.js +48 -18
- package/dist/config/bin.js +2 -2
- package/dist/constants-CiKwrA0v.js +356 -0
- package/dist/create/bin.js +18 -12
- package/dist/{define-config-DPNEJxPz.d.ts → define-config-B9usMnEa.d.ts} +51 -10
- package/dist/{define-config-D0mevyb3.cjs → define-config-CBccpane.cjs} +173 -44
- package/dist/{define-config-IgzmnvvV.js → define-config-jhuYJ7Tb.js} +158 -42
- package/dist/define-config.cjs +1 -1
- package/dist/define-config.d.ts +1 -1
- package/dist/define-config.js +1 -1
- package/dist/documentation-DqcZSthV.js +11 -0
- package/dist/{editor-4sjlHmUh.js → editor-DMBczTdj.js} +205 -152
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{json-cULBl7Pi.js → json-JP24qHUF.js} +14 -6
- package/dist/lint-plugins.cjs +1 -1
- package/dist/migration/bin.js +3122 -15
- package/dist/{oxlint-plugin-config-BVqEe9KY.js → oxlint-plugin-config-BgsDA6ua.js} +1 -1
- package/dist/oxlint-plugin.js +9 -7
- package/dist/pack-bin.js +1 -1
- package/dist/{package-CHH8jWan.js → package-B85H2Nw0.js} +6 -2
- package/dist/{vitest-ecosystem-BiRqcaRI.js → package-overrides-Dpn4scvN.js} +1 -7
- package/dist/{prompts-k14KgthC.js → prompts-CQobX7uP.js} +2 -2
- package/dist/{resolve-vite-config-BqvS2P98.js → resolve-vite-config-BUFUmi8h.js} +2 -2
- package/dist/{resolve-vite-config-CTOTuqTi.js → resolve-vite-config-I8fmpR7I.js} +1 -1
- package/dist/staged/bin.js +1205 -3317
- package/dist/sync-versions/bin.js +3 -2
- package/dist/sync-versions/bin.mjs +17 -2
- package/dist/test/browser/context.js +1 -1
- package/dist/test/browser/providers/playwright/context.js +1 -1
- package/dist/test/browser/providers/preview/context.js +1 -1
- package/dist/test/browser-playwright/context.js +1 -1
- package/dist/test/browser-preview/context.js +1 -1
- package/dist/test/context.d.ts +2 -942
- package/dist/test/context.js +1 -1
- package/dist/test/internal/traces.js +1 -0
- package/dist/test/locators.d.ts +2 -294
- package/dist/test/matchers.d.ts +2 -29
- package/dist/test/mocker.d.ts +2 -0
- package/dist/test/mocker.js +1 -0
- package/dist/test/{browser-webdriverio/context.js → plugins/browser-context.d.ts} +1 -0
- package/dist/test/plugins/browser-context.js +1 -1
- package/dist/test/plugins/browser-locators.d.ts +2 -0
- package/dist/test/plugins/browser-playwright.d.ts +2 -0
- package/dist/test/plugins/browser-preview.d.ts +2 -0
- package/dist/test/plugins/browser.d.ts +2 -0
- package/dist/test/plugins/mocker-automock.d.ts +2 -0
- package/dist/test/plugins/mocker-browser.d.ts +2 -0
- package/dist/test/plugins/mocker-node.d.ts +2 -0
- package/dist/test/plugins/mocker-redirect.d.ts +2 -0
- package/dist/test/plugins/mocker-register.d.ts +2 -0
- package/dist/test/plugins/mocker-transforms.d.ts +2 -0
- package/dist/test/plugins/mocker.d.ts +2 -0
- package/dist/test/plugins/pretty-format.d.ts +2 -0
- package/dist/test/plugins/snapshot-environment.d.ts +2 -0
- package/dist/test/plugins/snapshot-manager.d.ts +2 -0
- package/dist/test/plugins/snapshot.d.ts +2 -0
- package/dist/test/plugins/spy.d.ts +2 -0
- package/dist/test/plugins/utils-constants.d.ts +2 -0
- package/dist/test/plugins/utils-diff.d.ts +2 -0
- package/dist/test/plugins/utils-display.d.ts +2 -0
- package/dist/test/plugins/utils-error.d.ts +2 -0
- package/dist/test/plugins/utils-helpers.d.ts +2 -0
- package/dist/test/plugins/utils-offset.d.ts +2 -0
- package/dist/test/plugins/utils-resolver.d.ts +2 -0
- package/dist/test/plugins/utils-serialize.d.ts +2 -0
- package/dist/test/plugins/utils-source-map-node.d.ts +2 -0
- package/dist/test/plugins/utils-source-map.d.ts +2 -0
- package/dist/test/plugins/utils-timers.d.ts +2 -0
- package/dist/test/plugins/utils.d.ts +2 -0
- package/dist/toolchain.js +8 -8
- package/dist/toolchain.json +8 -8
- package/dist/{tsconfig-C3h9CVTA.js → tsconfig-Buo0RSzz.js} +8 -4
- package/dist/{tsconfig-euB-GmIE.js → tsconfig-C1VeTG9k.js} +1 -1
- package/dist/version.js +5 -3
- package/dist/versions.js +4 -4
- package/dist/vitest-ecosystem-BU0AqZq-.js +23 -0
- package/dist/vitest-v5-entry-points-DthU1e_O.js +25 -0
- package/docs/config/fmt.md +2 -2
- package/docs/config/lint.md +2 -2
- package/docs/config/run.md +1 -1
- package/docs/guide/check.md +2 -0
- package/docs/guide/commit-hooks.md +8 -1
- package/docs/guide/env.md +18 -13
- package/docs/guide/fmt.md +4 -2
- package/docs/guide/index.md +1 -1
- package/docs/guide/install.md +1 -1
- package/docs/guide/lint.md +3 -1
- package/docs/guide/local-cli.md +3 -3
- package/docs/guide/migrate-rules.md +16 -12
- package/docs/guide/migrate.md +86 -20
- package/docs/guide/monorepo.md +5 -1
- package/docs/guide/test.md +2 -0
- package/docs/guide/troubleshooting.md +3 -3
- package/docs/guide/vitest-v5.md +221 -0
- package/package.json +64 -87
- package/bin/oxfmt +0 -33
- package/bin/oxlint +0 -38
- package/dist/cli-truncate-Bg3RDXpi.js +0 -865
- package/dist/constants-C2dTOTe-.js +0 -80
- package/dist/is-fullwidth-code-point-BUNlIICg.js +0 -8
- package/dist/log-update-CoW8Z4Dl.js +0 -491
- package/dist/strip-ansi-C3wrWz9t.js +0 -853
- package/dist/test/browser/providers/webdriverio/context.d.ts +0 -1
- package/dist/test/browser/providers/webdriverio/context.js +0 -1
- package/dist/test/browser/providers/webdriverio.d.ts +0 -77
- package/dist/test/browser/providers/webdriverio.js +0 -1
- package/dist/test/browser-webdriverio/context.d.ts +0 -1
- package/dist/test/browser-webdriverio.d.ts +0 -77
- package/dist/test/browser-webdriverio.js +0 -1
- package/dist/test/coverage.d.ts +0 -2
- package/dist/test/coverage.js +0 -1
- package/dist/test/environments.d.ts +0 -2
- package/dist/test/environments.js +0 -1
- package/dist/test/plugins/browser-webdriverio.js +0 -1
- package/dist/test/plugins/expect.js +0 -1
- package/dist/test/plugins/runner-types.js +0 -1
- package/dist/test/plugins/runner-utils.js +0 -1
- package/dist/test/plugins/runner.js +0 -1
- package/dist/test/reporters.d.ts +0 -2
- package/dist/test/reporters.js +0 -1
- package/dist/test/runners.d.ts +0 -2
- package/dist/test/runners.js +0 -1
- package/dist/test/snapshot.d.ts +0 -2
- package/dist/test/snapshot.js +0 -1
- package/dist/test/suite.d.ts +0 -2
- package/dist/test/suite.js +0 -1
- package/dist/tsgolint-path-CbDReEOx.js +0 -34
- package/dist/tsgolint-path.d.ts +0 -7
- package/dist/tsgolint-path.js +0 -2
- package/dist/wrap-ansi-DNjkuBEp.js +0 -157
- package/dist/wrap-ansi-DUi52C6W.js +0 -2
package/dist/migration/bin.js
CHANGED
|
@@ -1,24 +1,3088 @@
|
|
|
1
1
|
import { r as __toESM } from "../rolldown-runtime-CMFfr-1z.js";
|
|
2
|
+
import { t as documentationUrl$1 } from "../documentation-DqcZSthV.js";
|
|
2
3
|
import { a as muted, c as warnMsg, i as log, o as printHeader, r as formatDuration, t as accent } from "../terminal-MKGAuy-p.js";
|
|
3
|
-
import { a as SETUP_VP_VERSION, f as VITE_PLUS_VERSION, m as isForceOverrideMode } from "../constants-
|
|
4
|
-
import { a as writeJsonFile, i as readJsonFile } from "../json-
|
|
5
|
-
import { a as readNearestPackageJson, i as hasVitePlusDependency } from "../package-
|
|
6
|
-
import {
|
|
4
|
+
import { _ as engines, a as SETUP_VP_VERSION, d as VITE_PLUS_OVERRIDE_PACKAGES, f as VITE_PLUS_VERSION, m as isForceOverrideMode } from "../constants-CiKwrA0v.js";
|
|
5
|
+
import { a as writeJsonFile, c as modify, i as readJsonFile, l as parse, o as applyEdits, s as findNodeAtLocation, u as parseTree } from "../json-JP24qHUF.js";
|
|
6
|
+
import { a as readNearestPackageJson, i as hasVitePlusDependency, n as detectPackageMetadata } from "../package-B85H2Nw0.js";
|
|
7
|
+
import { t as vitest_v5_entry_points_default } from "../vitest-v5-entry-points-DthU1e_O.js";
|
|
8
|
+
import { E as spinner, S as log$1, T as select, b as confirm, c as selectPackageManager, i as promptGitHooks, k as isCancel, l as upgradeYarn, n as defaultInteractive, o as runViteFmt, r as downloadPackageManager$1, s as runViteInstall, t as cancelAndExit, w as outro, y as PackageManager } from "../prompts-CQobX7uP.js";
|
|
7
9
|
import { t as require_semver } from "../semver-CaDAb8vV.js";
|
|
8
|
-
import { i as selectAgentTargetPaths, l as displayRelative, n as detectAgentConflicts, r as detectExistingAgentTargetPaths, s as writeAgentInstructions } from "../agent-
|
|
10
|
+
import { i as selectAgentTargetPaths, l as displayRelative, n as detectAgentConflicts, r as detectExistingAgentTargetPaths, s as writeAgentInstructions } from "../agent-Dinpyd-k.js";
|
|
9
11
|
import { n as addMigrationWarning, r as createMigrationReport } from "../report-CK5TnTsr.js";
|
|
10
|
-
import { $ as
|
|
12
|
+
import { $ as confirmEslintMigration, A as createCatalogDependencyResolver, B as addFrameworkShim, C as mergeViteConfigFiles, D as usesWebdriverioProvider, E as ensureVitePlusBootstrap, G as migrateTsupToTsdown, H as hasFrameworkShim, I as minimatch, J as confirmPrettierMigration, L as findGitRoot, M as detectWorkspace$1, O as configureYarnNodeModulesMode, Q as warnPackageLevelPrettier, S as mergeTsdownConfigFile, T as detectVitePlusBootstrapPending, U as confirmTsupMigration, V as detectFramework, W as detectTsupProject, X as migratePrettierToOxfmt, Y as detectPrettierProject, _ as installGitHooks, at as warnLegacyEslintConfig, ct as checkViteVersion, d as finalizeCoreMigrationForExistingVitePlus, et as detectEslintProject, f as detectNodeVersionManagerFile, g as detectLegacyGitHooksMigrationCandidate, i as selectEditor, it as warnIncompatibleEslintIntegration, k as detectYarnPnpMode, l as rewriteStandaloneProject, lt as checkVitestVersion, m as migrateSetupVpVersion, nt as migrateEslintToOxlint, o as writeEditorConfigs, ot as warnPackageLevelEslint, p as migrateNodeVersionManagerFile, q as warnMissingTsupConfig, r as isJsonLikeFile, s as rewriteMonorepo, st as WEBDRIVERIO_PROVIDER, t as detectEditorConflicts, tt as detectIncompatibleEslintIntegration, u as detectPendingCoreMigration, v as preflightGitHooksSetup, w as collectToolchainVersionChanges, x as injectLintTypeCheckDefaults, y as shouldSkipStagedMigrationForHooks, z as extractOverrideParentSegments } from "../editor-DMBczTdj.js";
|
|
11
13
|
import { n as runCommandSilently } from "../command-CguLh2KL.js";
|
|
12
|
-
import {
|
|
14
|
+
import { t as require_dist } from "../dist-BDYZP12R.js";
|
|
15
|
+
import { a as hasBaseUrlInTsconfig, i as fixBaseUrlInTsconfig, t as confirmBaseUrlFix } from "../tsconfig-Buo0RSzz.js";
|
|
16
|
+
import { t as extractOverrideTargetName } from "../package-overrides-Dpn4scvN.js";
|
|
13
17
|
import { t as unwrapCliParseOutcome } from "../cli-parse-DHE8NrBU.js";
|
|
14
18
|
import { t as ROLLDOWN_COMPAT_RESULT_PREFIX } from "../protocol-D99W10Qi.js";
|
|
15
19
|
import path from "node:path";
|
|
16
20
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { parseMigrateArgs } from "../../binding/index.js";
|
|
21
|
+
import { analyzeMigrationSource, isDirectoryGitignored, parseMigrateArgs } from "../../binding/index.js";
|
|
18
22
|
import { styleText } from "node:util";
|
|
19
23
|
import fs, { existsSync } from "node:fs";
|
|
20
|
-
//#region src/migration/
|
|
24
|
+
//#region src/migration/vitest-v5/commands.ts
|
|
25
|
+
/** Parse only a single literal argv command. Shell expansion, pipelines, and
|
|
26
|
+
* compound commands need review; treating them as argv could change behavior. */
|
|
27
|
+
function literalArgv(command) {
|
|
28
|
+
const tokens = [];
|
|
29
|
+
let value = "";
|
|
30
|
+
let start = 0;
|
|
31
|
+
let quote = "";
|
|
32
|
+
let active = false;
|
|
33
|
+
for (let i = 0; i < command.length; i++) {
|
|
34
|
+
const character = command[i];
|
|
35
|
+
if (quote) {
|
|
36
|
+
if (character === quote) quote = "";
|
|
37
|
+
else if (character === "\\" || quote === "\"" && (character === "$" || character === "`")) return;
|
|
38
|
+
else value += character;
|
|
39
|
+
} else if (character === "'" || character === "\"") {
|
|
40
|
+
if (!active) start = i;
|
|
41
|
+
quote = character;
|
|
42
|
+
active = true;
|
|
43
|
+
} else if (character === "\n" || character === "\r") return;
|
|
44
|
+
else if (/\s/.test(character)) {
|
|
45
|
+
if (active) tokens.push({
|
|
46
|
+
value,
|
|
47
|
+
start,
|
|
48
|
+
end: i
|
|
49
|
+
});
|
|
50
|
+
active = false;
|
|
51
|
+
value = "";
|
|
52
|
+
} else if (/[|&;<>($`\\)]/.test(character)) return;
|
|
53
|
+
else {
|
|
54
|
+
if (!active) start = i;
|
|
55
|
+
value += character;
|
|
56
|
+
active = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (quote) return;
|
|
60
|
+
if (active) tokens.push({
|
|
61
|
+
value,
|
|
62
|
+
start,
|
|
63
|
+
end: command.length
|
|
64
|
+
});
|
|
65
|
+
return tokens;
|
|
66
|
+
}
|
|
67
|
+
/** Read command lists without executing a shell. Directory-changing builtins,
|
|
68
|
+
* expansions, pipelines, and shell programs remain unresolved. */
|
|
69
|
+
function literalTestCommands(command) {
|
|
70
|
+
const commands = [];
|
|
71
|
+
let quote = "";
|
|
72
|
+
let start = 0;
|
|
73
|
+
const append = (end) => {
|
|
74
|
+
const tokens = literalArgv(command.slice(start, end));
|
|
75
|
+
if (!tokens?.length) return false;
|
|
76
|
+
const values = tokens.map(({ value }) => value);
|
|
77
|
+
while (/^[A-Za-z_]\w*=/.test(values[0] ?? "")) values.shift();
|
|
78
|
+
if (!values.length || [
|
|
79
|
+
"cd",
|
|
80
|
+
"pushd",
|
|
81
|
+
"popd",
|
|
82
|
+
".",
|
|
83
|
+
"source",
|
|
84
|
+
"eval",
|
|
85
|
+
"export",
|
|
86
|
+
"set",
|
|
87
|
+
"unset"
|
|
88
|
+
].includes(values[0])) return false;
|
|
89
|
+
commands.push(values);
|
|
90
|
+
return true;
|
|
91
|
+
};
|
|
92
|
+
for (let index = 0; index < command.length; index++) {
|
|
93
|
+
const char = command[index];
|
|
94
|
+
if (char === "\\") return;
|
|
95
|
+
if (quote) {
|
|
96
|
+
if (char === quote) quote = "";
|
|
97
|
+
} else if (char === "\"" || char === "'") quote = char;
|
|
98
|
+
else if (char === "#") return;
|
|
99
|
+
else if (char === ";" || command.slice(index, index + 2) === "&&" || command.slice(index, index + 2) === "||") {
|
|
100
|
+
if (!append(index)) return;
|
|
101
|
+
if (char !== ";") index++;
|
|
102
|
+
start = index + 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return !quote && append(command.length) ? commands : void 0;
|
|
106
|
+
}
|
|
107
|
+
/** Locate the runner's arguments after a supported package-manager wrapper. */
|
|
108
|
+
function vitestCommandArgsStart(values) {
|
|
109
|
+
let start = 0;
|
|
110
|
+
if ([
|
|
111
|
+
"pnpm",
|
|
112
|
+
"npm",
|
|
113
|
+
"yarn",
|
|
114
|
+
"bun",
|
|
115
|
+
"npx",
|
|
116
|
+
"bunx"
|
|
117
|
+
].includes(values[0])) {
|
|
118
|
+
start = 1;
|
|
119
|
+
if (values[start] === "exec" || values[start] === "x") start++;
|
|
120
|
+
if (values[start] === "--") start++;
|
|
121
|
+
}
|
|
122
|
+
if (values[start] === "vitest") return start + 1;
|
|
123
|
+
if (values[start] === "vp" && values[start + 1] === "test") return start + 2;
|
|
124
|
+
return -1;
|
|
125
|
+
}
|
|
126
|
+
function migrateBenchmarkOutput(command) {
|
|
127
|
+
const argv = literalArgv(command);
|
|
128
|
+
if (!argv) return command;
|
|
129
|
+
const values = argv.map(({ value }) => value);
|
|
130
|
+
const start = vitestCommandArgsStart(values);
|
|
131
|
+
if (start < 0) return command;
|
|
132
|
+
const end = values.indexOf("--", start);
|
|
133
|
+
const args = argv.slice(start, end < 0 ? void 0 : end);
|
|
134
|
+
for (let index = 0; index < args.length; index++) {
|
|
135
|
+
const { value } = args[index];
|
|
136
|
+
if (/^--(?:outputJson|outputFile|reporters?)$/.test(value)) {
|
|
137
|
+
const next = args[++index]?.value;
|
|
138
|
+
if (!next || next.startsWith("-")) return command;
|
|
139
|
+
} else if (value.startsWith("-") && !/^--(?:run|watch)$/.test(value) && !/^--(?:outputJson|outputFile|reporters?)=/.test(value)) return command;
|
|
140
|
+
}
|
|
141
|
+
const legacy = args.filter(({ value }) => /^--outputJson(?:=|$)/.test(value));
|
|
142
|
+
if (legacy.length !== 1) return command;
|
|
143
|
+
const flag = legacy[0];
|
|
144
|
+
const value = flag.value.startsWith("--outputJson=") ? flag.value.slice(13) : args[args.indexOf(flag) + 1]?.value;
|
|
145
|
+
if (!value || value.startsWith("-")) return command;
|
|
146
|
+
const last = flag.value.includes("=") ? flag : args[args.indexOf(flag) + 1];
|
|
147
|
+
const raw = flag === last ? command.slice(flag.start + 13, flag.end) : command.slice(last.start, last.end);
|
|
148
|
+
if (!/^[\w./:@+-]+$/.test(raw) && !/^(['"]).*\1$/.test(raw)) return command;
|
|
149
|
+
const output = args.filter(({ value }) => /^--outputFile(?:[.=]|$)/.test(value));
|
|
150
|
+
if (output.length > 1) return command;
|
|
151
|
+
if (output.length) {
|
|
152
|
+
const token = output[0];
|
|
153
|
+
if (!/^--outputFile(?:=|$)/.test(token.value)) return command;
|
|
154
|
+
if ((token.value.includes("=") ? token.value.slice(13) : args[args.indexOf(token) + 1]?.value) !== value) return command;
|
|
155
|
+
}
|
|
156
|
+
const reporters = [];
|
|
157
|
+
for (const [index, { value }] of args.entries()) if (/^--reporters?=/.test(value)) reporters.push(value.slice(value.indexOf("=") + 1));
|
|
158
|
+
else if (/^--reporters?$/.test(value)) reporters.push(args[index + 1]?.value ?? "");
|
|
159
|
+
if (reporters.some((name) => ![
|
|
160
|
+
"default",
|
|
161
|
+
"verbose",
|
|
162
|
+
"json"
|
|
163
|
+
].includes(name))) return command;
|
|
164
|
+
const replacement = [
|
|
165
|
+
...!reporters.length ? ["--reporter=default"] : [],
|
|
166
|
+
...!reporters.includes("json") ? ["--reporter=json"] : [],
|
|
167
|
+
...!output.length ? [`--outputFile=${raw}`] : []
|
|
168
|
+
].join(" ");
|
|
169
|
+
return command.slice(0, flag.start) + replacement + command.slice(last.end);
|
|
170
|
+
}
|
|
171
|
+
function migrateVitestV5Command(file, command, preserveV4, line = 1, reviewV4 = preserveV4) {
|
|
172
|
+
const findings = [];
|
|
173
|
+
const report = (code, message, severity = "review") => findings.push({
|
|
174
|
+
file,
|
|
175
|
+
line,
|
|
176
|
+
column: 1,
|
|
177
|
+
code,
|
|
178
|
+
message,
|
|
179
|
+
severity
|
|
180
|
+
});
|
|
181
|
+
if (!/\bvitest\b|\bvp\s+test\b/.test(command)) return {
|
|
182
|
+
content: command,
|
|
183
|
+
findings
|
|
184
|
+
};
|
|
185
|
+
command = migrateBenchmarkOutput(command);
|
|
186
|
+
const argv = literalArgv(command);
|
|
187
|
+
const values = argv?.map(({ value }) => value);
|
|
188
|
+
const runner = values ? vitestCommandArgsStart(values) : -1;
|
|
189
|
+
let removedFlag = /(?:^|\s)(?:--compare|--outputJson)(?:\s|=|$)/.test(command);
|
|
190
|
+
let reviewNamePattern = /(?:^|\s)(?:-t|--testNamePattern|--test-name-pattern)(?:\s|=)/.test(command);
|
|
191
|
+
if (values && runner >= 0) {
|
|
192
|
+
const terminator = values.indexOf("--", runner);
|
|
193
|
+
const args = values.slice(runner, terminator === -1 ? void 0 : terminator);
|
|
194
|
+
removedFlag = args.some((value) => /^(?:--compare|--outputJson)(?:=|$)/.test(value));
|
|
195
|
+
reviewNamePattern = args.some((value, index) => {
|
|
196
|
+
if (!/^(?:-t|--testNamePattern|--test-name-pattern)(?:=|$)/.test(value)) return false;
|
|
197
|
+
const pattern = value.includes("=") ? value.slice(value.indexOf("=") + 1) : args[index + 1];
|
|
198
|
+
return !pattern || !/^[\w-]+$/.test(pattern);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (reviewV4 && reviewNamePattern) report("test-name-pattern", "Review test-name patterns across suite boundaries; full names now use > separators.");
|
|
202
|
+
if (removedFlag) report("benchmark-api", "Replace removed --compare/--outputJson flags with bench context comparisons or regular JSON reporter output. The bench subcommand is still supported.", "block");
|
|
203
|
+
if (!reviewV4 || !/\blist\b/.test(command)) return {
|
|
204
|
+
content: command,
|
|
205
|
+
findings
|
|
206
|
+
};
|
|
207
|
+
if (!argv || !values) {
|
|
208
|
+
report("static-list", "Review this wrapped or compound list command and explicitly choose --no-static-parse to retain runtime collection.");
|
|
209
|
+
return {
|
|
210
|
+
content: command,
|
|
211
|
+
findings
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const list = runner;
|
|
215
|
+
if (list < 0 || values[list] !== "list") {
|
|
216
|
+
report("static-list", "Resolve this list command wrapper and explicitly choose a static-parse setting.");
|
|
217
|
+
return {
|
|
218
|
+
content: command,
|
|
219
|
+
findings
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const optionsEnd = values.indexOf("--", list + 1);
|
|
223
|
+
const args = values.slice(list + 1, optionsEnd < 0 ? void 0 : optionsEnd);
|
|
224
|
+
if (!preserveV4 || args.some((arg) => /^--(?:no-)?static-parse(?:=|$)/.test(arg))) return {
|
|
225
|
+
content: command,
|
|
226
|
+
findings
|
|
227
|
+
};
|
|
228
|
+
const offset = argv[list].end;
|
|
229
|
+
return {
|
|
230
|
+
content: `${command.slice(0, offset)} --no-static-parse${command.slice(offset)}`,
|
|
231
|
+
findings
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/migration/vitest-v5/ast.ts
|
|
236
|
+
function isNode(value) {
|
|
237
|
+
return typeof value === "object" && value !== null && "type" in value && "start" in value;
|
|
238
|
+
}
|
|
239
|
+
function isString(node) {
|
|
240
|
+
return node?.type === "Literal" && typeof node.value === "string";
|
|
241
|
+
}
|
|
242
|
+
function isBoolean(node) {
|
|
243
|
+
return node?.type === "Literal" && typeof node.value === "boolean";
|
|
244
|
+
}
|
|
245
|
+
function isRegExp(node) {
|
|
246
|
+
return node?.type === "Literal" && "regex" in node;
|
|
247
|
+
}
|
|
248
|
+
function propertyName(node) {
|
|
249
|
+
if (node?.type === "Identifier") return node.name;
|
|
250
|
+
if (isString(node)) return node.value;
|
|
251
|
+
}
|
|
252
|
+
function memberName(node) {
|
|
253
|
+
if (node?.type !== "MemberExpression") return;
|
|
254
|
+
if (node.computed && !isString(node.property)) return;
|
|
255
|
+
return propertyName(node.property);
|
|
256
|
+
}
|
|
257
|
+
function isModuleExports(editor, node) {
|
|
258
|
+
return node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier" && node.object.name === "module" && propertyName(node.property) === "exports" && !editor.binding(node.object);
|
|
259
|
+
}
|
|
260
|
+
function objectProperty(object, key) {
|
|
261
|
+
return object.properties.find((prop) => prop.type === "Property" && prop.kind === "init" && !prop.computed && propertyName(prop.key) === key);
|
|
262
|
+
}
|
|
263
|
+
function staticObject(node, allowMethods = false) {
|
|
264
|
+
if (node?.type !== "ObjectExpression") return false;
|
|
265
|
+
const names = /* @__PURE__ */ new Set();
|
|
266
|
+
return node.properties.every((prop) => {
|
|
267
|
+
if (prop.type !== "Property" || prop.method && !allowMethods || prop.kind !== "init" || prop.computed) return false;
|
|
268
|
+
const name = propertyName(prop.key);
|
|
269
|
+
if (name === void 0 || names.has(name)) return false;
|
|
270
|
+
names.add(name);
|
|
271
|
+
return true;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** Consider both possible project lists without evaluating environment-dependent
|
|
275
|
+
* config code. Unknown spreads stay in the list for a localized review. */
|
|
276
|
+
function projectElements(node) {
|
|
277
|
+
const expand = (value) => {
|
|
278
|
+
if (value.type === "ArrayExpression") return projectElements(value);
|
|
279
|
+
if (value.type === "ConditionalExpression") {
|
|
280
|
+
const consequent = expand(value.consequent);
|
|
281
|
+
const alternate = expand(value.alternate);
|
|
282
|
+
if (consequent && alternate && ![...consequent, ...alternate].some((entry) => isString(entry) && entry.value.startsWith("!"))) return [...consequent, ...alternate];
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
return node.elements.flatMap((entry) => entry?.type === "SpreadElement" ? expand(entry.argument) ?? [entry] : [entry]);
|
|
286
|
+
}
|
|
287
|
+
/** Compare data literals without evaluating config code or invoking getters.
|
|
288
|
+
* Property order and spelling do not change the value of a plain config object. */
|
|
289
|
+
function sameStaticValue(left, right) {
|
|
290
|
+
if (left.type === "Literal" && right.type === "Literal") return !("regex" in left || "regex" in right || "bigint" in left || "bigint" in right) && Object.is(left.value, right.value);
|
|
291
|
+
if (left.type === "UnaryExpression" && right.type === "UnaryExpression") return ["+", "-"].includes(left.operator) && left.operator === right.operator && left.argument.type === "Literal" && typeof left.argument.value === "number" && sameStaticValue(left.argument, right.argument);
|
|
292
|
+
if (left.type === "ArrayExpression" && right.type === "ArrayExpression") return left.elements.length === right.elements.length && left.elements.every((element, index) => {
|
|
293
|
+
const other = right.elements[index];
|
|
294
|
+
return !!element && !!other && sameStaticValue(element, other);
|
|
295
|
+
});
|
|
296
|
+
if (staticObject(left) && staticObject(right)) return left.properties.length === right.properties.length && left.properties.every((property) => {
|
|
297
|
+
const prop = property;
|
|
298
|
+
const other = objectProperty(right, propertyName(prop.key));
|
|
299
|
+
return !!other && sameStaticValue(prop.value, other.value);
|
|
300
|
+
});
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
function importedName(editor, node, sources) {
|
|
304
|
+
if (node?.type === "MemberExpression" && !node.optional && !node.computed && node.object.type === "Identifier") {
|
|
305
|
+
const declaration = editor.binding(node.object)?.declaration;
|
|
306
|
+
const parent = declaration && editor.parent(declaration);
|
|
307
|
+
if (declaration?.type === "ImportNamespaceSpecifier" && parent?.type === "ImportDeclaration" && parent.importKind !== "type" && sources.has(parent.source.value)) return propertyName(node.property);
|
|
308
|
+
}
|
|
309
|
+
if (node?.type !== "Identifier") return;
|
|
310
|
+
const declaration = editor.binding(node)?.declaration;
|
|
311
|
+
const parent = declaration && editor.parent(declaration);
|
|
312
|
+
if (declaration?.type !== "ImportSpecifier" || parent?.type !== "ImportDeclaration") return;
|
|
313
|
+
if (declaration.importKind === "type" || parent.importKind === "type") return;
|
|
314
|
+
if (sources.has(parent.source.value)) return propertyName(declaration.imported);
|
|
315
|
+
}
|
|
316
|
+
const ROOT_TEST_SOURCES = /* @__PURE__ */ new Set([
|
|
317
|
+
"vitest",
|
|
318
|
+
"vite-plus/test",
|
|
319
|
+
"@vitest/runner"
|
|
320
|
+
]);
|
|
321
|
+
const CONFIG_SOURCES = /* @__PURE__ */ new Set([
|
|
322
|
+
"vitest/config",
|
|
323
|
+
"vite-plus",
|
|
324
|
+
"vite-plus/test/config",
|
|
325
|
+
"vite"
|
|
326
|
+
]);
|
|
327
|
+
const NODE_SOURCES = /* @__PURE__ */ new Set(["vitest/node", "vite-plus/test/node"]);
|
|
328
|
+
function parseSource(file, source) {
|
|
329
|
+
const { ast, bindings, comments } = JSON.parse(analyzeMigrationSource(file, source));
|
|
330
|
+
return {
|
|
331
|
+
program: ast.node,
|
|
332
|
+
bindings,
|
|
333
|
+
comments
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function testApiName(editor, node, globals = false) {
|
|
337
|
+
const imported = importedName(editor, node, ROOT_TEST_SOURCES);
|
|
338
|
+
if (imported) return imported;
|
|
339
|
+
if (node?.type === "MemberExpression" && !node.optional && isImportMetaVitest(node.object)) return memberName(node);
|
|
340
|
+
const binding = node && editor.binding(node);
|
|
341
|
+
const property = binding?.declaration;
|
|
342
|
+
const pattern = property && editor.parent(property);
|
|
343
|
+
const declaration = pattern && editor.parent(pattern);
|
|
344
|
+
if (binding?.constant && property?.type === "Property" && !property.computed && property.value.type === "Identifier" && pattern?.type === "ObjectPattern" && declaration?.type === "VariableDeclarator" && isImportMetaVitest(declaration.init)) return propertyName(property.key);
|
|
345
|
+
if (globals && node?.type === "Identifier" && !editor.binding(node)) return node.name;
|
|
346
|
+
}
|
|
347
|
+
function isImportMetaVitest(node) {
|
|
348
|
+
return node?.type === "MemberExpression" && !node.optional && memberName(node) === "vitest" && node.object.type === "MetaProperty" && node.object.meta.name === "import" && node.object.property.name === "meta";
|
|
349
|
+
}
|
|
350
|
+
function printAddedProperty(property, indent, newline) {
|
|
351
|
+
const comment = property.comment ? `${property.comment.replaceAll("\n", `${newline}${indent}`)}${newline}${indent}` : "";
|
|
352
|
+
const value = typeof property.value === "string" ? property.value : printAddedObject(property.value, indent, newline);
|
|
353
|
+
return `${comment}${property.key}: ${value}`;
|
|
354
|
+
}
|
|
355
|
+
/** Format generated objects without reindenting copied expressions or string literals. */
|
|
356
|
+
function printAddedObject(properties, indent = "", newline = "\n") {
|
|
357
|
+
const childIndent = `${indent} `;
|
|
358
|
+
return `{${newline}${properties.map((property) => `${childIndent}${printAddedProperty(property, childIndent, newline)}`).join(`,${newline}`)}${newline}${indent}}`;
|
|
359
|
+
}
|
|
360
|
+
/** Offset edits retain comments and formatting outside the precise changed span. */
|
|
361
|
+
var SourceEditor = class {
|
|
362
|
+
file;
|
|
363
|
+
source;
|
|
364
|
+
ast;
|
|
365
|
+
comments;
|
|
366
|
+
findings = [];
|
|
367
|
+
nodes = [];
|
|
368
|
+
parents = /* @__PURE__ */ new Map();
|
|
369
|
+
bindings = /* @__PURE__ */ new Map();
|
|
370
|
+
names = /* @__PURE__ */ new Set();
|
|
371
|
+
edits = [];
|
|
372
|
+
additions = /* @__PURE__ */ new Map();
|
|
373
|
+
constructor(file, source) {
|
|
374
|
+
this.file = file;
|
|
375
|
+
this.source = source;
|
|
376
|
+
const parsed = parseSource(file, source);
|
|
377
|
+
this.ast = parsed.program;
|
|
378
|
+
this.comments = parsed.comments;
|
|
379
|
+
const identifiers = /* @__PURE__ */ new Map();
|
|
380
|
+
const index = (node, parent) => {
|
|
381
|
+
this.nodes.push(node);
|
|
382
|
+
if (parent) this.parents.set(node, parent);
|
|
383
|
+
if (node.type === "Identifier") {
|
|
384
|
+
identifiers.set(node.start, node);
|
|
385
|
+
this.names.add(node.name);
|
|
386
|
+
}
|
|
387
|
+
for (const value of Object.values(node)) for (const child of Array.isArray(value) ? value : [value]) if (isNode(child)) index(child, node);
|
|
388
|
+
};
|
|
389
|
+
index(this.ast);
|
|
390
|
+
for (const binding of parsed.bindings) {
|
|
391
|
+
const identifier = identifiers.get(binding.start);
|
|
392
|
+
const declaration = identifier && this.parent(identifier);
|
|
393
|
+
if (!declaration) continue;
|
|
394
|
+
const resolved = {
|
|
395
|
+
declaration,
|
|
396
|
+
references: binding.references.map((start) => identifiers.get(start)).filter(isNode),
|
|
397
|
+
constant: binding.constant
|
|
398
|
+
};
|
|
399
|
+
for (const start of [binding.start, ...binding.references]) this.bindings.set(start, resolved);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
visit(visitors) {
|
|
403
|
+
for (const node of this.nodes) {
|
|
404
|
+
const visitor = visitors[node.type];
|
|
405
|
+
visitor?.(node);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
parent(node) {
|
|
409
|
+
return this.parents.get(node);
|
|
410
|
+
}
|
|
411
|
+
functionParent(node) {
|
|
412
|
+
let parent = this.parent(node);
|
|
413
|
+
while (parent) {
|
|
414
|
+
if (parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression") return parent;
|
|
415
|
+
parent = this.parent(parent);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
binding(node) {
|
|
419
|
+
return node.type === "Identifier" ? this.bindings.get(node.start) : void 0;
|
|
420
|
+
}
|
|
421
|
+
uniqueName(base, preferBase = false) {
|
|
422
|
+
let name = preferBase && !this.names.has(base) ? base : `_${base}`;
|
|
423
|
+
let suffix = 2;
|
|
424
|
+
while (this.names.has(name)) name = `_${base}${suffix++}`;
|
|
425
|
+
this.names.add(name);
|
|
426
|
+
return name;
|
|
427
|
+
}
|
|
428
|
+
text(node) {
|
|
429
|
+
return this.source.slice(node.start, node.end);
|
|
430
|
+
}
|
|
431
|
+
replace(node, text) {
|
|
432
|
+
this.edit(node.start, node.end, text);
|
|
433
|
+
}
|
|
434
|
+
edit(start, end, text) {
|
|
435
|
+
const same = this.edits.find((edit) => edit.start === start && edit.end === end);
|
|
436
|
+
if (same?.text === text) return;
|
|
437
|
+
if (same && start === end) {
|
|
438
|
+
same.text += text;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
this.edits.push({
|
|
442
|
+
start,
|
|
443
|
+
end,
|
|
444
|
+
text
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
add(object, key, value, comment) {
|
|
448
|
+
if (objectProperty(object, key)) return;
|
|
449
|
+
let additions = this.additions.get(object);
|
|
450
|
+
if (!additions) {
|
|
451
|
+
additions = /* @__PURE__ */ new Map();
|
|
452
|
+
this.additions.set(object, additions);
|
|
453
|
+
}
|
|
454
|
+
additions.set(key, {
|
|
455
|
+
key,
|
|
456
|
+
value,
|
|
457
|
+
comment
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
remove(object, prop) {
|
|
461
|
+
const index = object.properties.indexOf(prop);
|
|
462
|
+
const nextStart = object.properties[index + 1]?.start ?? object.end - 1;
|
|
463
|
+
const previousEnd = object.properties[index - 1]?.end ?? object.start + 1;
|
|
464
|
+
const commaBetween = (start, end) => {
|
|
465
|
+
for (let index = start; index < end; index++) {
|
|
466
|
+
const comment = this.comments.find((span) => span.start <= index && index < span.end);
|
|
467
|
+
if (comment) index = comment.end - 1;
|
|
468
|
+
else if (this.source[index] === ",") return index;
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
const followingComma = commaBetween(prop.end, nextStart);
|
|
472
|
+
const precedingComma = commaBetween(previousEnd, prop.start);
|
|
473
|
+
this.replace(prop, "");
|
|
474
|
+
const comma = followingComma ?? precedingComma;
|
|
475
|
+
if (comma !== void 0) this.edit(comma, comma + 1, "");
|
|
476
|
+
}
|
|
477
|
+
report(node, code, message, severity = "review") {
|
|
478
|
+
const lines = this.source.slice(0, node?.start ?? 0).split(/\r\n|[\r\n\u2028\u2029]/);
|
|
479
|
+
const line = lines.length;
|
|
480
|
+
const column = lines.at(-1).length + 1;
|
|
481
|
+
if (!this.findings.some((item) => item.line === line && item.column === column && item.code === code && item.message === message)) this.findings.push({
|
|
482
|
+
file: this.file,
|
|
483
|
+
line,
|
|
484
|
+
column,
|
|
485
|
+
code,
|
|
486
|
+
severity,
|
|
487
|
+
message
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
finish() {
|
|
491
|
+
for (const [object, additions] of this.additions) {
|
|
492
|
+
const start = object.start + 1;
|
|
493
|
+
const first = object.properties[0]?.start ?? object.end - 1;
|
|
494
|
+
const prefix = this.source.slice(start, first);
|
|
495
|
+
const newline = this.source.includes("\r\n") ? "\r\n" : "\n";
|
|
496
|
+
const multilinePrefix = prefix.includes("\n");
|
|
497
|
+
const multiline = multilinePrefix || [...additions.values()].some(({ comment, value }) => comment || Array.isArray(value));
|
|
498
|
+
const indent = this.source.slice(this.source.lastIndexOf("\n", first) + 1, first).match(/^[\t ]*/)?.[0] ?? "";
|
|
499
|
+
let objectIndent = indent;
|
|
500
|
+
if (!multilinePrefix) {
|
|
501
|
+
const lineStart = this.source.lastIndexOf("\n", object.start) + 1;
|
|
502
|
+
let parent = this.parent(object);
|
|
503
|
+
while (parent && parent.start >= lineStart) {
|
|
504
|
+
if (parent.type === "ObjectExpression") objectIndent += " ";
|
|
505
|
+
parent = this.parent(parent);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const propertyIndent = multilinePrefix && object.properties.length ? indent : `${objectIndent} `;
|
|
509
|
+
const entries = [...additions.values()].map((property) => printAddedProperty(property, propertyIndent, newline));
|
|
510
|
+
const suffix = multiline && !multilinePrefix ? `${newline}${object.properties.length ? propertyIndent : objectIndent}` : "";
|
|
511
|
+
this.edit(start, multiline && !multilinePrefix && /^\s*$/.test(prefix) ? first : start, multiline ? `${newline}${propertyIndent}${entries.join(`,${newline}${propertyIndent}`)}${object.properties.length ? "," : ""}${suffix}` : ` ${entries.join(", ")}${object.properties.length ? "," : ""} `);
|
|
512
|
+
}
|
|
513
|
+
const edits = this.edits.toSorted((a, b) => b.start - a.start || b.end - a.end);
|
|
514
|
+
let result = this.source;
|
|
515
|
+
let previousStart = this.source.length + 1;
|
|
516
|
+
for (const edit of edits) {
|
|
517
|
+
if (edit.end > previousStart) {
|
|
518
|
+
this.report(void 0, "overlapping-edits", "Review the overlapping Vitest transformations; this file was not changed.");
|
|
519
|
+
return {
|
|
520
|
+
content: this.source,
|
|
521
|
+
findings: this.findings
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
525
|
+
previousStart = edit.start;
|
|
526
|
+
}
|
|
527
|
+
if (result !== this.source) try {
|
|
528
|
+
parseSource(this.file, result);
|
|
529
|
+
} catch {
|
|
530
|
+
this.report(void 0, "unsafe-syntax", "The proposed transform did not parse. Review this file manually; it was not changed.");
|
|
531
|
+
return {
|
|
532
|
+
content: this.source,
|
|
533
|
+
findings: this.findings
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
content: result,
|
|
538
|
+
findings: this.findings
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/migration/vitest-v5/benchmark-config.ts
|
|
544
|
+
const REPORTERS = /* @__PURE__ */ new Set([
|
|
545
|
+
"default",
|
|
546
|
+
"verbose",
|
|
547
|
+
"dot",
|
|
548
|
+
"json",
|
|
549
|
+
"junit",
|
|
550
|
+
"tap",
|
|
551
|
+
"tap-flat",
|
|
552
|
+
"hanging-process",
|
|
553
|
+
"github-actions"
|
|
554
|
+
]);
|
|
555
|
+
function reporterNames(node) {
|
|
556
|
+
const entries = node.type === "ArrayExpression" ? node.elements : [node];
|
|
557
|
+
return entries.every(isString) ? entries.map((entry) => entry.value) : void 0;
|
|
558
|
+
}
|
|
559
|
+
/** Move only resolved built-in output settings. The normal config pass reports
|
|
560
|
+
* any retained legacy options as blockers; no partial output plan is applied. */
|
|
561
|
+
function migrateBenchmarkConfig(editor, test) {
|
|
562
|
+
const benchmark = objectProperty(test, "benchmark")?.value;
|
|
563
|
+
if (!staticObject(benchmark)) return;
|
|
564
|
+
const oldReporters = objectProperty(benchmark, "reporters");
|
|
565
|
+
const oldOutput = objectProperty(benchmark, "outputFile");
|
|
566
|
+
const oldJson = objectProperty(benchmark, "outputJson");
|
|
567
|
+
if (!oldReporters && !oldOutput && !oldJson) return;
|
|
568
|
+
const benchmarkNames = oldReporters ? reporterNames(oldReporters.value) : ["default"];
|
|
569
|
+
if (!benchmarkNames?.length || benchmarkNames.some((name) => !["default", "verbose"].includes(name))) return;
|
|
570
|
+
let destination;
|
|
571
|
+
for (const prop of [oldOutput, oldJson]) {
|
|
572
|
+
if (!prop) continue;
|
|
573
|
+
if (!isString(prop.value) || !prop.value.value || destination !== void 0 && destination !== prop.value.value) return;
|
|
574
|
+
destination = prop.value.value;
|
|
575
|
+
}
|
|
576
|
+
const reporters = objectProperty(test, "reporters");
|
|
577
|
+
const names = reporters ? reporterNames(reporters.value) : benchmarkNames;
|
|
578
|
+
if (!names || names.some((name) => !REPORTERS.has(name))) return;
|
|
579
|
+
const terminal = names.filter((name) => [
|
|
580
|
+
"default",
|
|
581
|
+
"verbose",
|
|
582
|
+
"dot"
|
|
583
|
+
].includes(name));
|
|
584
|
+
if (reporters && oldReporters && terminal.some((name) => !benchmarkNames.includes(name))) return;
|
|
585
|
+
const output = objectProperty(test, "outputFile");
|
|
586
|
+
const jsonOutput = output && (staticObject(output.value) ? objectProperty(output.value, "json")?.value : output.value);
|
|
587
|
+
if (destination) {
|
|
588
|
+
if (jsonOutput && (!isString(jsonOutput) || jsonOutput.value !== destination) || output && !isString(output.value) && !staticObject(output.value)) return;
|
|
589
|
+
}
|
|
590
|
+
const combined = [.../* @__PURE__ */ new Set([
|
|
591
|
+
...names,
|
|
592
|
+
...terminal.length && !oldReporters ? [] : benchmarkNames,
|
|
593
|
+
...destination ? ["json"] : []
|
|
594
|
+
])];
|
|
595
|
+
if (reporters) {
|
|
596
|
+
if (combined.join("\0") !== names.join("\0")) {
|
|
597
|
+
if (editor.comments.some(({ start, end }) => start >= reporters.value.start && end <= reporters.value.end)) return;
|
|
598
|
+
editor.replace(reporters.value, JSON.stringify(combined));
|
|
599
|
+
}
|
|
600
|
+
} else editor.add(test, "reporters", JSON.stringify(combined));
|
|
601
|
+
if (destination && !jsonOutput) {
|
|
602
|
+
if (output && staticObject(output.value)) editor.add(output.value, "json", JSON.stringify(destination));
|
|
603
|
+
else editor.add(test, "outputFile", `{ json: ${JSON.stringify(destination)} }`);
|
|
604
|
+
}
|
|
605
|
+
for (const prop of [
|
|
606
|
+
oldReporters,
|
|
607
|
+
oldOutput,
|
|
608
|
+
oldJson
|
|
609
|
+
]) if (prop) editor.remove(benchmark, prop);
|
|
610
|
+
}
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/migration/vitest-v5/compatibility.ts
|
|
613
|
+
const settings = {
|
|
614
|
+
clearMocks: {
|
|
615
|
+
value: "false",
|
|
616
|
+
reason: "preserve mock call history.",
|
|
617
|
+
adoption: "Remove after tests no longer rely on calls from setup or earlier tests.",
|
|
618
|
+
section: "clearmocks-is-enabled-by-default"
|
|
619
|
+
},
|
|
620
|
+
sharedViteServer: {
|
|
621
|
+
value: "false",
|
|
622
|
+
reason: "keep separate Vite servers for inline projects.",
|
|
623
|
+
adoption: "Remove when plugins and config hooks can run once for shared projects.",
|
|
624
|
+
section: "inline-projects-share-the-vite-server-by-default"
|
|
625
|
+
},
|
|
626
|
+
extends: {
|
|
627
|
+
value: "false",
|
|
628
|
+
reason: "keep this inline project independent of the root config.",
|
|
629
|
+
adoption: "Remove to inherit root options, including plugins and setup files.",
|
|
630
|
+
section: "inline-projects-inherit-the-root-config-by-default"
|
|
631
|
+
},
|
|
632
|
+
exact: {
|
|
633
|
+
value: "false",
|
|
634
|
+
reason: "keep partial, case-insensitive locator matching.",
|
|
635
|
+
adoption: "Remove after updating locators for full, case-sensitive matches.",
|
|
636
|
+
section: "locators-are-strict-by-default"
|
|
637
|
+
},
|
|
638
|
+
perFile: {
|
|
639
|
+
value: "true",
|
|
640
|
+
reason: "enforce this coverage threshold per file.",
|
|
641
|
+
adoption: "Keep for per-file enforcement; remove to check matching files as a group.",
|
|
642
|
+
section: "glob-coverage-thresholds-no-longer-inherit-perfile"
|
|
643
|
+
},
|
|
644
|
+
toNotFake: {
|
|
645
|
+
value: "['Temporal']",
|
|
646
|
+
reason: "exclude the global Temporal polyfill from fake timers.",
|
|
647
|
+
adoption: "Remove only the Temporal entry when it should follow mocked time.",
|
|
648
|
+
section: "fake-timers-and-setsystemtime-now-mock-temporal"
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
/** Only attach this guidance to newly inserted v4 compatibility properties. */
|
|
652
|
+
function compatibilityProperty(key) {
|
|
653
|
+
const { value, reason, adoption, section } = settings[key];
|
|
654
|
+
return {
|
|
655
|
+
key,
|
|
656
|
+
value,
|
|
657
|
+
comment: `// Vitest v4 compatibility: ${reason}\n// ${adoption}\n// ${documentationUrl$1("/guide/vitest-v5#remove-unneeded-compatibility-settings")}\n// https://vitest.dev/guide/migration/#${section}`
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
//#endregion
|
|
661
|
+
//#region src/migration/vitest-v5/config-object.ts
|
|
662
|
+
const CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
663
|
+
"root",
|
|
664
|
+
"test",
|
|
665
|
+
"extends"
|
|
666
|
+
]);
|
|
667
|
+
function unrelatedSpread(node) {
|
|
668
|
+
if (node.type === "ConditionalExpression") return unrelatedSpread(node.consequent) && unrelatedSpread(node.alternate);
|
|
669
|
+
return node.type === "ObjectExpression" && node.properties.every((property) => property.type === "SpreadElement" ? unrelatedSpread(property.argument) : !property.computed && !CONFIG_KEYS.has(propertyName(property.key) ?? "test"));
|
|
670
|
+
}
|
|
671
|
+
/** Read a config without evaluating user code. Only a single unconditional
|
|
672
|
+
* return is supported; nested callback returns and conditional exits are not
|
|
673
|
+
* alternative configs. Return the original object so edits keep its spans. */
|
|
674
|
+
function resolveConfigObject(editor, node) {
|
|
675
|
+
if (node?.type === "Identifier") {
|
|
676
|
+
const binding = editor.binding(node);
|
|
677
|
+
if (binding?.references.some((reference) => {
|
|
678
|
+
const parent = editor.parent(reference);
|
|
679
|
+
return !(parent?.type === "ExportDefaultDeclaration" || parent?.type === "AssignmentExpression" && parent.right === reference && isModuleExports(editor, parent.left) || parent?.type === "CallExpression" && parent.arguments[0] === reference && ["defineConfig", "defineProject"].includes(importedName(editor, parent.callee, CONFIG_SOURCES) ?? ""));
|
|
680
|
+
})) return;
|
|
681
|
+
node = binding?.constant && binding.declaration.type === "VariableDeclarator" ? binding.declaration.init : void 0;
|
|
682
|
+
}
|
|
683
|
+
if (node?.type === "CallExpression") node = ["defineConfig", "defineProject"].includes(importedName(editor, node.callee, CONFIG_SOURCES) ?? "") ? node.arguments[0] : void 0;
|
|
684
|
+
if (node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression" || node?.type === "FunctionDeclaration") {
|
|
685
|
+
if (node.generator) return;
|
|
686
|
+
const body = node.body;
|
|
687
|
+
if (!body) return;
|
|
688
|
+
if (body.type === "BlockStatement") {
|
|
689
|
+
const last = body.body.at(-1);
|
|
690
|
+
if (last?.type !== "ReturnStatement" || !body.body.slice(0, -1).every((statement) => statement.type === "VariableDeclaration")) return;
|
|
691
|
+
node = last.argument;
|
|
692
|
+
} else node = body;
|
|
693
|
+
}
|
|
694
|
+
if (node?.type !== "ObjectExpression") return;
|
|
695
|
+
const names = /* @__PURE__ */ new Set();
|
|
696
|
+
for (const property of node.properties) {
|
|
697
|
+
if (property.type === "SpreadElement") {
|
|
698
|
+
if (!unrelatedSpread(property.argument)) return;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
const name = propertyName(property.key);
|
|
702
|
+
if (property.computed || property.kind !== "init" || name === void 0 || names.has(name)) return;
|
|
703
|
+
names.add(name);
|
|
704
|
+
}
|
|
705
|
+
return node;
|
|
706
|
+
}
|
|
707
|
+
//#endregion
|
|
708
|
+
//#region src/migration/vitest-v5/config.ts
|
|
709
|
+
function isTrue(node) {
|
|
710
|
+
return isBoolean(node) && node.value;
|
|
711
|
+
}
|
|
712
|
+
const CONFIG_FILENAME = /(?:^|[/\\])(?:vite|vitest)(?:\.[\w-]+)?\.config\.[cm]?[jt]s$/;
|
|
713
|
+
const CONFIG_HELPER_IMPORT = /['"](?:vite|vitest\/config|vite-plus(?:\/test\/config)?)['"]/;
|
|
714
|
+
function hasTestConfigHelper(file, source) {
|
|
715
|
+
if (!CONFIG_HELPER_IMPORT.test(source)) return false;
|
|
716
|
+
try {
|
|
717
|
+
const editor = new SourceEditor(file, source);
|
|
718
|
+
if (editor.ast.body.some((node) => node.type === "ImportDeclaration" && node.importKind !== "type" && ["vitest/config", "vite-plus/test/config"].includes(node.source.value) && node.specifiers.some((specifier) => specifier.type === "ImportNamespaceSpecifier" || specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && [
|
|
719
|
+
"defineConfig",
|
|
720
|
+
"defineProject",
|
|
721
|
+
"mergeConfig"
|
|
722
|
+
].includes(propertyName(specifier.imported) ?? "")))) return true;
|
|
723
|
+
let hasTestOptions = false;
|
|
724
|
+
editor.visit({ CallExpression(node) {
|
|
725
|
+
if ([
|
|
726
|
+
"defineConfig",
|
|
727
|
+
"defineProject",
|
|
728
|
+
"mergeConfig"
|
|
729
|
+
].includes(importedName(editor, node.callee, CONFIG_SOURCES) ?? "") && node.arguments.some((argument) => argument.type === "ObjectExpression" && !!objectProperty(argument, "test"))) hasTestOptions = true;
|
|
730
|
+
} });
|
|
731
|
+
return hasTestOptions;
|
|
732
|
+
} catch {
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/** Include literal referenced configs even when they use a custom filename and
|
|
737
|
+
* export a raw object. Candidate files come from the owning package's scan. */
|
|
738
|
+
function findVitestV5ConfigFiles(sources, entries = []) {
|
|
739
|
+
const files = new Set([...sources].filter(([file, source]) => /\.[cm]?[jt]sx?$/.test(file) && (CONFIG_FILENAME.test(file) || hasTestConfigHelper(file, source))).map(([file]) => file));
|
|
740
|
+
for (const file of entries) if (sources.has(file)) files.add(file);
|
|
741
|
+
for (const file of files) try {
|
|
742
|
+
const editor = new SourceEditor(file, sources.get(file));
|
|
743
|
+
function addReference(reference) {
|
|
744
|
+
if (reference.startsWith("!")) return;
|
|
745
|
+
const directory = path.dirname(file);
|
|
746
|
+
const target = path.resolve(directory, reference);
|
|
747
|
+
if (sources.has(target) && /\.[cm]?[jt]sx?$/.test(target)) {
|
|
748
|
+
files.add(target);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const pattern = reference.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
752
|
+
for (const candidate of sources.keys()) {
|
|
753
|
+
const relative = path.relative(directory, candidate).replaceAll("\\", "/");
|
|
754
|
+
if (/\.[cm]?[jt]sx?$/.test(candidate) && (minimatch(relative, pattern, { dot: true }) || CONFIG_FILENAME.test(candidate) && minimatch(path.posix.dirname(relative), pattern, { dot: true }))) files.add(candidate);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const collectReferences = (config) => {
|
|
758
|
+
const object = resolveConfigObject(editor, config);
|
|
759
|
+
if (!object) return;
|
|
760
|
+
config = object;
|
|
761
|
+
const base = objectProperty(config, "extends")?.value;
|
|
762
|
+
if (isString(base)) addReference(base.value);
|
|
763
|
+
const test = objectProperty(config, "test")?.value;
|
|
764
|
+
const projects = test?.type === "ObjectExpression" ? objectProperty(test, "projects")?.value : void 0;
|
|
765
|
+
if (projects?.type !== "ArrayExpression") return;
|
|
766
|
+
for (const project of projectElements(projects)) if (isString(project)) addReference(project.value);
|
|
767
|
+
else collectReferences(project ?? void 0);
|
|
768
|
+
};
|
|
769
|
+
editor.visit({
|
|
770
|
+
ExportDefaultDeclaration(node) {
|
|
771
|
+
const declaration = node.declaration;
|
|
772
|
+
if (declaration.type === "Identifier") {
|
|
773
|
+
const binding = editor.binding(declaration);
|
|
774
|
+
if (binding?.constant && binding.declaration.type === "VariableDeclarator") collectReferences(binding.declaration.init ?? void 0);
|
|
775
|
+
} else collectReferences(declaration);
|
|
776
|
+
},
|
|
777
|
+
CallExpression(node) {
|
|
778
|
+
if ([
|
|
779
|
+
"defineConfig",
|
|
780
|
+
"defineProject",
|
|
781
|
+
"mergeConfig"
|
|
782
|
+
].includes(importedName(editor, node.callee, CONFIG_SOURCES) ?? "")) for (const argument of node.arguments) collectReferences(argument);
|
|
783
|
+
},
|
|
784
|
+
AssignmentExpression(node) {
|
|
785
|
+
if (isModuleExports(editor, node.left)) collectReferences(node.right);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
} catch {}
|
|
789
|
+
return files;
|
|
790
|
+
}
|
|
791
|
+
function hasConfigMerge(editor) {
|
|
792
|
+
let found = false;
|
|
793
|
+
editor.visit({ CallExpression(node) {
|
|
794
|
+
if (importedName(editor, node.callee, CONFIG_SOURCES) === "mergeConfig") found = true;
|
|
795
|
+
} });
|
|
796
|
+
return found;
|
|
797
|
+
}
|
|
798
|
+
/** Defaults on one merge fragment can override explicit settings in another.
|
|
799
|
+
* Include local imported configs so the preflight and finalization agree. */
|
|
800
|
+
function findVitestV5MergedConfigFiles(sources, configFiles) {
|
|
801
|
+
const merged = /* @__PURE__ */ new Set();
|
|
802
|
+
const imports = /* @__PURE__ */ new Map();
|
|
803
|
+
const extensions = [
|
|
804
|
+
".ts",
|
|
805
|
+
".mts",
|
|
806
|
+
".cts",
|
|
807
|
+
".js",
|
|
808
|
+
".mjs",
|
|
809
|
+
".cjs",
|
|
810
|
+
".tsx",
|
|
811
|
+
".jsx"
|
|
812
|
+
];
|
|
813
|
+
for (const file of configFiles) try {
|
|
814
|
+
const editor = new SourceEditor(file, sources.get(file));
|
|
815
|
+
if (hasConfigMerge(editor)) merged.add(file);
|
|
816
|
+
const dependencies = [];
|
|
817
|
+
for (const node of editor.ast.body) {
|
|
818
|
+
if (node.type !== "ImportDeclaration" && node.type !== "ExportNamedDeclaration" && node.type !== "ExportAllDeclaration") continue;
|
|
819
|
+
const reference = node.source?.value;
|
|
820
|
+
if (!reference?.startsWith(".")) continue;
|
|
821
|
+
const target = path.resolve(path.dirname(file), reference);
|
|
822
|
+
const dependency = [
|
|
823
|
+
target,
|
|
824
|
+
target.replace(/\.([cm]?)js$/, ".$1ts"),
|
|
825
|
+
...extensions.map((extension) => `${target}${extension}`),
|
|
826
|
+
...extensions.map((extension) => path.join(target, `index${extension}`))
|
|
827
|
+
].find((candidate) => configFiles.has(candidate));
|
|
828
|
+
if (dependency) dependencies.push(dependency);
|
|
829
|
+
}
|
|
830
|
+
imports.set(file, dependencies);
|
|
831
|
+
} catch {}
|
|
832
|
+
for (const file of merged) for (const dependency of imports.get(file) ?? []) merged.add(dependency);
|
|
833
|
+
return merged;
|
|
834
|
+
}
|
|
835
|
+
function migrateVitestV5Config(file, source, options, mergedConfig = false) {
|
|
836
|
+
const output = migrateConfig(file, source, options, mergedConfig, true);
|
|
837
|
+
const result = migrateConfig(file, output.content, options, mergedConfig);
|
|
838
|
+
return {
|
|
839
|
+
content: result.content,
|
|
840
|
+
findings: [...output.findings.filter(({ code }) => ["overlapping-edits", "unsafe-syntax"].includes(code)), ...result.findings]
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
function migrateConfig(file, source, options, mergedConfig, benchmarkOnly = false) {
|
|
844
|
+
const editor = new SourceEditor(file, source);
|
|
845
|
+
const visited = /* @__PURE__ */ new Set();
|
|
846
|
+
const merged = mergedConfig || hasConfigMerge(editor);
|
|
847
|
+
const preserveDefaults = options.preserveV4 && !merged && !benchmarkOnly;
|
|
848
|
+
const reviewV4 = options.preserveV4 || options.reviewV4;
|
|
849
|
+
if (merged && reviewV4) editor.report(void 0, "merged-config-defaults", "Review the effective merged config before adding v4 defaults for clearMocks, browser locators, fake timers, and projects. Defaults were not added to config fragments because they can override explicit settings in another fragment.");
|
|
850
|
+
function nested(object, key, additions, apply) {
|
|
851
|
+
const prop = objectProperty(object, key);
|
|
852
|
+
if (!prop) editor.add(object, key, typeof additions === "string" ? `{ ${additions} }` : additions);
|
|
853
|
+
else if (staticObject(prop.value)) apply(prop.value);
|
|
854
|
+
else editor.report(prop, "dynamic-config", `Resolve ${key} before applying Vitest v5 compatibility options.`);
|
|
855
|
+
}
|
|
856
|
+
function addCompatibility(object, key) {
|
|
857
|
+
const property = compatibilityProperty(key);
|
|
858
|
+
editor.add(object, key, property.value, property.comment);
|
|
859
|
+
}
|
|
860
|
+
function testOptions(test, inherits, parentTest) {
|
|
861
|
+
if (benchmarkOnly) {
|
|
862
|
+
if (!parentTest && !merged) migrateBenchmarkConfig(editor, test);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (preserveDefaults && !inherits) addCompatibility(test, "clearMocks");
|
|
866
|
+
const namePattern = objectProperty(test, "testNamePattern");
|
|
867
|
+
if (reviewV4 && namePattern && !(isString(namePattern.value) && /^[\w-]+$/.test(namePattern.value.value))) editor.report(namePattern, "test-name-pattern", "Review test-name patterns across suite boundaries; full names now use > separators.");
|
|
868
|
+
const browser = objectProperty(test, "browser");
|
|
869
|
+
if (browser && staticObject(browser.value)) {
|
|
870
|
+
const value = browser.value;
|
|
871
|
+
const inheritsBrowser = inherits && parentTest && objectProperty(parentTest, "browser");
|
|
872
|
+
if (preserveDefaults && !inheritsBrowser) nested(value, "locators", [compatibilityProperty("exact")], (locators) => addCompatibility(locators, "exact"));
|
|
873
|
+
const api = objectProperty(value, "api");
|
|
874
|
+
if (api) {
|
|
875
|
+
const target = objectProperty(test, "api");
|
|
876
|
+
if (target && !sameStaticValue(target.value, api.value)) editor.report(api, "api-conflict", "Resolve conflicting test.api and test.browser.api values before upgrading.", "block");
|
|
877
|
+
else {
|
|
878
|
+
editor.add(test, "api", editor.text(api.value));
|
|
879
|
+
editor.remove(value, api);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const screenshot = objectProperty(value, "screenshotDirectory");
|
|
883
|
+
if (options.preserveV4 && screenshot) {
|
|
884
|
+
const setting = `screenshotDirectory: ${editor.text(screenshot.value)}`;
|
|
885
|
+
nested(value, "expect", `toMatchScreenshot: { ${setting} }`, (expect) => {
|
|
886
|
+
nested(expect, "toMatchScreenshot", setting, (match) => editor.add(match, "screenshotDirectory", editor.text(screenshot.value)));
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
if (objectProperty(value, "commands")) editor.report(browser, "locator-commands", "Review browser command parameters: locators are SerializedLocator objects, not selector strings.");
|
|
890
|
+
} else if (browser && reviewV4) editor.report(browser, "dynamic-project", "Review dynamic browser options and preserve locators.exact: false when migrating v4.");
|
|
891
|
+
if (preserveDefaults && options.temporalPolyfill) nested(test, "fakeTimers", [compatibilityProperty("toNotFake")], (timers) => addCompatibility(timers, "toNotFake"));
|
|
892
|
+
const coverage = objectProperty(test, "coverage");
|
|
893
|
+
if (coverage && staticObject(coverage.value)) {
|
|
894
|
+
for (const key of ["include", "exclude"]) {
|
|
895
|
+
const pattern = objectProperty(coverage.value, key);
|
|
896
|
+
if (pattern && reviewV4) editor.report(pattern, "coverage-patterns", `Compare v4/v5 resolved coverage.${key} file sets; v5 matches relative paths more precisely.`);
|
|
897
|
+
}
|
|
898
|
+
const thresholds = objectProperty(coverage.value, "thresholds");
|
|
899
|
+
if (preserveDefaults && thresholds && staticObject(thresholds.value) && isTrue(objectProperty(thresholds.value, "perFile")?.value)) for (const threshold of thresholds.value.properties) {
|
|
900
|
+
if (threshold.type !== "Property" || [
|
|
901
|
+
"perFile",
|
|
902
|
+
"lines",
|
|
903
|
+
"branches",
|
|
904
|
+
"functions",
|
|
905
|
+
"statements",
|
|
906
|
+
"autoUpdate",
|
|
907
|
+
"100"
|
|
908
|
+
].includes(propertyName(threshold.key) ?? "")) continue;
|
|
909
|
+
if (staticObject(threshold.value)) addCompatibility(threshold.value, "perFile");
|
|
910
|
+
else editor.report(threshold, "coverage-thresholds", "Set perFile: true on this dynamic glob threshold to retain v4 enforcement.");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
const benchmark = objectProperty(test, "benchmark");
|
|
914
|
+
if (benchmark) {
|
|
915
|
+
if (staticObject(benchmark.value)) for (const key of [
|
|
916
|
+
"reporters",
|
|
917
|
+
"outputFile",
|
|
918
|
+
"compare",
|
|
919
|
+
"outputJson"
|
|
920
|
+
]) {
|
|
921
|
+
const removed = objectProperty(benchmark.value, key);
|
|
922
|
+
if (removed) editor.report(removed, "benchmark-api", `Replace removed benchmark.${key} with regular test reporters and the bench test-context fixture.`, "block");
|
|
923
|
+
}
|
|
924
|
+
else editor.report(benchmark, "benchmark-api", "Review dynamic benchmark options for removed reporters, outputFile, compare, and outputJson settings.");
|
|
925
|
+
}
|
|
926
|
+
const projects = objectProperty(test, "projects");
|
|
927
|
+
if (!projects) return;
|
|
928
|
+
if (projects.value.type !== "ArrayExpression") {
|
|
929
|
+
if (reviewV4) editor.report(projects, "dynamic-project", "Resolve dynamic projects and review inheritance and sharedViteServer before upgrading.");
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (projects.value.elements.some((item) => item && !isString(item)) && preserveDefaults) addCompatibility(test, "sharedViteServer");
|
|
933
|
+
for (const project of projectElements(projects.value)) {
|
|
934
|
+
if (!project || isString(project)) continue;
|
|
935
|
+
if (!staticObject(project)) {
|
|
936
|
+
if (reviewV4) editor.report(project, "dynamic-project", "Review this function, promise, or dynamic inline project. Set explicit inheritance and v4 compatibility options.");
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
const extendsValue = objectProperty(project, "extends");
|
|
940
|
+
if (preserveDefaults) addCompatibility(project, "extends");
|
|
941
|
+
config(project, isTrue(extendsValue?.value), test);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function config(object, inherits = false, parentTest) {
|
|
945
|
+
if (visited.has(object)) return;
|
|
946
|
+
visited.add(object);
|
|
947
|
+
const extendsValue = objectProperty(object, "extends");
|
|
948
|
+
if (extendsValue && !isBoolean(extendsValue.value) && reviewV4) {
|
|
949
|
+
editor.report(extendsValue, "project-inheritance", "Review this external or dynamic project base before adding v4 compatibility defaults. Inherited project settings were left unchanged.");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const test = objectProperty(object, "test");
|
|
953
|
+
if (!test) {
|
|
954
|
+
if (preserveDefaults && !inherits) {
|
|
955
|
+
const properties = [compatibilityProperty("clearMocks")];
|
|
956
|
+
if (options.temporalPolyfill) properties.push({
|
|
957
|
+
key: "fakeTimers",
|
|
958
|
+
value: [compatibilityProperty("toNotFake")]
|
|
959
|
+
});
|
|
960
|
+
editor.add(object, "test", properties);
|
|
961
|
+
}
|
|
962
|
+
} else if (staticObject(test.value, true)) testOptions(test.value, inherits, parentTest);
|
|
963
|
+
else if (reviewV4) editor.report(test, "dynamic-config", "Review dynamic test options; v4 defaults cannot be preserved automatically.");
|
|
964
|
+
}
|
|
965
|
+
editor.visit({
|
|
966
|
+
AssignmentExpression(node) {
|
|
967
|
+
if (isModuleExports(editor, node.left)) {
|
|
968
|
+
const object = resolveConfigObject(editor, node.right);
|
|
969
|
+
if (object && node.right.type !== "CallExpression") config(object);
|
|
970
|
+
else if (node.right.type !== "CallExpression" && reviewV4) editor.report(node, "dynamic-config", "Review the effective CommonJS test config and its v4 defaults.");
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
ExportDefaultDeclaration(node) {
|
|
974
|
+
const object = resolveConfigObject(editor, node.declaration);
|
|
975
|
+
if (object && node.declaration.type !== "CallExpression") config(object);
|
|
976
|
+
else if (node.declaration.type !== "CallExpression" && reviewV4) editor.report(node, "dynamic-config", "Review the effective function or promise config and its v4 defaults.");
|
|
977
|
+
},
|
|
978
|
+
CallExpression(node) {
|
|
979
|
+
const name = importedName(editor, node.callee, CONFIG_SOURCES);
|
|
980
|
+
if (name === "defineConfig" || name === "defineProject") {
|
|
981
|
+
if (benchmarkOnly && name === "defineProject") return;
|
|
982
|
+
const parent = editor.parent(node);
|
|
983
|
+
const binding = parent?.type === "VariableDeclarator" && parent.id.type === "Identifier" ? editor.binding(parent.id) : void 0;
|
|
984
|
+
if (!(parent?.type === "ExportDefaultDeclaration" || parent?.type === "AssignmentExpression" && parent.right === node && isModuleExports(editor, parent.left) || binding?.constant && binding.references.length > 0 && binding.references.every((reference) => editor.parent(reference)?.type === "ExportDefaultDeclaration")) && !merged) {
|
|
985
|
+
if (reviewV4) editor.report(node, "dynamic-config", "Review this config fragment in its wrapper or inline project before adding v4 compatibility settings.");
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const object = resolveConfigObject(editor, node.arguments[0]);
|
|
989
|
+
if (object) config(object);
|
|
990
|
+
else if (reviewV4) editor.report(node.arguments[0], "dynamic-config", "Review this dynamic config; explicit v4 compatibility settings need manual insertion.");
|
|
991
|
+
}
|
|
992
|
+
if (name === "mergeConfig" && reviewV4) editor.report(node, "nested-project-merge", "Check merged root configs for test.projects; v5 permits nested projects and can recurse or duplicate them.");
|
|
993
|
+
},
|
|
994
|
+
Property(node) {
|
|
995
|
+
if (reviewV4 && [
|
|
996
|
+
"config",
|
|
997
|
+
"configResolved",
|
|
998
|
+
"configureServer",
|
|
999
|
+
"configureVitest"
|
|
1000
|
+
].includes(propertyName(node.key) ?? "")) editor.report(node, "project-server-lifecycle", "Review this plugin hook for per-project config execution or Vite server state.");
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
return editor.finish();
|
|
1004
|
+
}
|
|
1005
|
+
//#endregion
|
|
1006
|
+
//#region src/migration/vitest-v5/documentation.ts
|
|
1007
|
+
const guide = documentationUrl$1("/guide/vitest-v5");
|
|
1008
|
+
const upstream = "https://vitest.dev/guide/migration/";
|
|
1009
|
+
const sections = {
|
|
1010
|
+
"api-conflict": "browser-api-is-replaced-by-the-top-level-api",
|
|
1011
|
+
"assertion-types": "assertion-types-expose-return-and-received-types",
|
|
1012
|
+
"async-render": "render-is-async-in-vitest-browser-vue-and-vitest-browser-svelte",
|
|
1013
|
+
"benchmark-api": "benchmarking-api-rewrite",
|
|
1014
|
+
"browser-automock": "automocked-modules-stay-automocked-in-the-browser",
|
|
1015
|
+
"browser-session": "browser-orchestrator-url-requires-a-session",
|
|
1016
|
+
"class-mock": "class-mocks-keep-prototype-methods",
|
|
1017
|
+
"coverage-patterns": "coverage-include-and-exclude-match-more-precisely",
|
|
1018
|
+
"coverage-thresholds": "glob-coverage-thresholds-no-longer-inherit-perfile",
|
|
1019
|
+
"dom-global": "dom-environment-global-assignments-now-update-the-underlying-window",
|
|
1020
|
+
"global-descriptors": "populateglobal-returns-descriptors-in-originals",
|
|
1021
|
+
"locator-commands": "locators-in-commands-are-serialized-as-objects",
|
|
1022
|
+
"nested-hoisted-mock": "hoisted-mocking-calls-must-be-at-the-top-level",
|
|
1023
|
+
"nested-project-merge": "referenced-config-files-can-define-their-own-projects",
|
|
1024
|
+
"poll-timeout": "expect-poll-fails-when-it-times-out",
|
|
1025
|
+
"project-inheritance": "inline-projects-inherit-the-root-config-by-default",
|
|
1026
|
+
"project-server-lifecycle": "inline-projects-share-the-vite-server-by-default",
|
|
1027
|
+
"removed-api": "removed-deprecated-entrypoints",
|
|
1028
|
+
"resolve-config": "resolveconfig-returns-the-resolved-vite-config",
|
|
1029
|
+
"sequential-api": "removed-test-sequential-describe-sequential-and-sequential-options",
|
|
1030
|
+
"temporal-system-time": "fake-timers-and-setsystemtime-now-mock-temporal",
|
|
1031
|
+
"test-name-pattern": "testnamepattern-matches-the-joined-full-name",
|
|
1032
|
+
"text-content": "tohavetextcontent-now-performs-strict-equality",
|
|
1033
|
+
"text-content-project": "tohavetextcontent-now-performs-strict-equality",
|
|
1034
|
+
"ui-token": "vitest-ui-requires-an-authenticated-url",
|
|
1035
|
+
"unawaited-assertion": "unawaited-asynchronous-assertions-fail-the-test",
|
|
1036
|
+
"worker-id": "worker-and-concurrency-ids-are-1-based"
|
|
1037
|
+
};
|
|
1038
|
+
const localSections = {
|
|
1039
|
+
"browser-provider": "community-webdriverio-provider",
|
|
1040
|
+
"dynamic-config": "resolve-migration-findings",
|
|
1041
|
+
"dynamic-project": "resolve-migration-findings",
|
|
1042
|
+
"global-api-ownership": "resolve-migration-findings",
|
|
1043
|
+
"jest-dom-types": "review-checklist",
|
|
1044
|
+
"merged-config-defaults": "preserve-existing-behavior",
|
|
1045
|
+
"node-runtime": "node-runtime",
|
|
1046
|
+
"overlapping-edits": "resolve-migration-findings",
|
|
1047
|
+
"source-parse": "resolve-migration-findings",
|
|
1048
|
+
"source-version": "before-you-migrate",
|
|
1049
|
+
"static-collect": "source-changes",
|
|
1050
|
+
"static-list": "source-changes",
|
|
1051
|
+
"unsafe-syntax": "resolve-migration-findings"
|
|
1052
|
+
};
|
|
1053
|
+
/** Give future/unknown findings a useful fallback rather than omitting help. */
|
|
1054
|
+
function vitestV5Documentation(code) {
|
|
1055
|
+
if (sections[code]) return `${upstream}#${sections[code]}`;
|
|
1056
|
+
return `${guide}#${localSections[code] ?? "resolve-migration-findings"}`;
|
|
1057
|
+
}
|
|
1058
|
+
//#endregion
|
|
1059
|
+
//#region src/migration/vitest-v5/lockfile.ts
|
|
21
1060
|
var import_semver = /* @__PURE__ */ __toESM(require_semver(), 1);
|
|
1061
|
+
var import_dist = require_dist();
|
|
1062
|
+
function record$1(value) {
|
|
1063
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1064
|
+
}
|
|
1065
|
+
function declaredVitest(pkg) {
|
|
1066
|
+
const manifest = record$1(pkg);
|
|
1067
|
+
for (const field of [
|
|
1068
|
+
"dependencies",
|
|
1069
|
+
"devDependencies",
|
|
1070
|
+
"optionalDependencies",
|
|
1071
|
+
"peerDependencies"
|
|
1072
|
+
]) {
|
|
1073
|
+
const spec = record$1(manifest?.[field])?.vitest;
|
|
1074
|
+
if (typeof spec === "string") return spec;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
/** Read a direct upstream Vitest edge without installing or changing a lockfile.
|
|
1078
|
+
* Unsupported protocols, ambiguous layouts and stale entries remain unknown.
|
|
1079
|
+
* In particular, never infer a runner from an unrelated transitive package. */
|
|
1080
|
+
function lockedVitestVersion(root, directory, manager, spec) {
|
|
1081
|
+
const range = spec.replace(/^npm:vitest@/, "");
|
|
1082
|
+
const validRange = import_semver.default.validRange(range);
|
|
1083
|
+
if (!validRange && !/^[\w.-]+$/.test(range)) return;
|
|
1084
|
+
function valid(version) {
|
|
1085
|
+
if (typeof version !== "string" || !import_semver.default.valid(version)) return;
|
|
1086
|
+
return !validRange || import_semver.default.satisfies(version, range, { includePrerelease: true }) ? version : void 0;
|
|
1087
|
+
}
|
|
1088
|
+
const relative = path.relative(root, directory).replaceAll("\\", "/");
|
|
1089
|
+
const read = (name) => fs.readFileSync(path.join(root, name), "utf8").replace(/^\uFEFF/, "");
|
|
1090
|
+
try {
|
|
1091
|
+
if (manager === PackageManager.npm) {
|
|
1092
|
+
const packages = record$1(JSON.parse(read(fs.existsSync(path.join(root, "npm-shrinkwrap.json")) ? "npm-shrinkwrap.json" : "package-lock.json")).packages);
|
|
1093
|
+
if (!packages) return;
|
|
1094
|
+
if (declaredVitest(packages[relative]) !== spec) return;
|
|
1095
|
+
let owner = relative;
|
|
1096
|
+
while (true) {
|
|
1097
|
+
const entry = record$1(packages[path.posix.join(owner, "node_modules/vitest")]);
|
|
1098
|
+
if (entry) return !entry.link && (!entry.name || entry.name === "vitest") ? valid(entry.version) : void 0;
|
|
1099
|
+
if (!owner) return;
|
|
1100
|
+
const parent = path.posix.dirname(owner);
|
|
1101
|
+
owner = parent === "." ? "" : parent;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (manager === PackageManager.yarn) {
|
|
1105
|
+
const text = read("yarn.lock");
|
|
1106
|
+
if (/^__metadata:/m.test(text)) {
|
|
1107
|
+
const document = (0, import_dist.parseDocument)(text);
|
|
1108
|
+
if (document.errors.length) return;
|
|
1109
|
+
const descriptors = /* @__PURE__ */ new Set([`vitest@${spec}`, `vitest@npm:${range}`]);
|
|
1110
|
+
const entries = Object.entries(record$1(document.toJS()) ?? {}).filter(([keys]) => keys.split(/,\s+/).some((key) => descriptors.has(key)));
|
|
1111
|
+
if (entries.length !== 1) return;
|
|
1112
|
+
const entry = record$1(entries[0][1]);
|
|
1113
|
+
return typeof entry?.resolution === "string" && entry.resolution.startsWith("vitest@npm:") ? valid(entry.resolution.slice(11)) : void 0;
|
|
1114
|
+
}
|
|
1115
|
+
const matches = [];
|
|
1116
|
+
for (const block of text.split(/\r?\n(?=[^\s#])/)) {
|
|
1117
|
+
const [header] = block.split(/\r?\n/);
|
|
1118
|
+
if (!header.endsWith(":")) continue;
|
|
1119
|
+
const document = (0, import_dist.parseDocument)(`[${header.slice(0, -1)}]`);
|
|
1120
|
+
const keys = document.errors.length ? void 0 : document.toJS();
|
|
1121
|
+
if (Array.isArray(keys) && keys.includes(`vitest@${spec}`)) {
|
|
1122
|
+
const version = /^ version "([^"]+)"\r?$/m.exec(block)?.[1];
|
|
1123
|
+
if (!version) return;
|
|
1124
|
+
matches.push(version);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return matches.length === 1 ? valid(matches[0]) : void 0;
|
|
1128
|
+
}
|
|
1129
|
+
if (manager === PackageManager.bun) {
|
|
1130
|
+
const errors = [];
|
|
1131
|
+
const lock = record$1(parse(read("bun.lock"), errors, { allowTrailingComma: true }));
|
|
1132
|
+
if (errors.length || declaredVitest(record$1(lock?.workspaces)?.[relative]) !== spec) return;
|
|
1133
|
+
const packages = record$1(lock?.packages);
|
|
1134
|
+
const direct = packages?.vitest;
|
|
1135
|
+
if (!Array.isArray(direct) || typeof direct[0] !== "string" || !direct[0].startsWith("vitest@")) return;
|
|
1136
|
+
const directVersion = valid(direct[0].slice(7));
|
|
1137
|
+
if (!directVersion) return;
|
|
1138
|
+
const versions = /* @__PURE__ */ new Set();
|
|
1139
|
+
for (const entry of Object.values(packages ?? {})) {
|
|
1140
|
+
if (!Array.isArray(entry) || typeof entry[0] !== "string" || !entry[0].startsWith("vitest@")) continue;
|
|
1141
|
+
const version = valid(entry[0].slice(7));
|
|
1142
|
+
if (version) versions.add(version);
|
|
1143
|
+
}
|
|
1144
|
+
return versions.size === 1 ? directVersion : void 0;
|
|
1145
|
+
}
|
|
1146
|
+
} catch {}
|
|
1147
|
+
}
|
|
1148
|
+
//#endregion
|
|
1149
|
+
//#region src/migration/vitest-v5/storybook.ts
|
|
1150
|
+
const STORYBOOK_PLUGIN = /* @__PURE__ */ new Set(["@storybook/addon-vitest/vitest-plugin"]);
|
|
1151
|
+
const NODE_PATH = /* @__PURE__ */ new Set(["node:path", "path"]);
|
|
1152
|
+
const NODE_URL = /* @__PURE__ */ new Set(["node:url", "url"]);
|
|
1153
|
+
function importMeta(node) {
|
|
1154
|
+
return node.type === "MetaProperty" && node.meta.name === "import" && node.property.name === "meta";
|
|
1155
|
+
}
|
|
1156
|
+
function pathFunction(editor, node) {
|
|
1157
|
+
const name = importedName(editor, node, NODE_PATH);
|
|
1158
|
+
if (name) return name;
|
|
1159
|
+
if (node.type !== "MemberExpression" || node.object.type !== "Identifier") return;
|
|
1160
|
+
const declaration = editor.binding(node.object)?.declaration;
|
|
1161
|
+
const parent = declaration && editor.parent(declaration);
|
|
1162
|
+
if (declaration?.type === "ImportDefaultSpecifier" && parent?.type === "ImportDeclaration" && NODE_PATH.has(parent.source.value)) return memberName(node);
|
|
1163
|
+
}
|
|
1164
|
+
/** Recognize common configDir expressions, without evaluating config code. */
|
|
1165
|
+
function configPath(editor, node, file, cwd, seen = /* @__PURE__ */ new Set()) {
|
|
1166
|
+
if (seen.has(node)) return;
|
|
1167
|
+
seen.add(node);
|
|
1168
|
+
if (isString(node)) return node.value;
|
|
1169
|
+
if (node.type === "Identifier") {
|
|
1170
|
+
const binding = editor.binding(node);
|
|
1171
|
+
if (!binding && node.name === "__dirname") return path.dirname(file);
|
|
1172
|
+
if (!binding && node.name === "__filename") return file;
|
|
1173
|
+
if (binding?.constant && binding.declaration.type === "VariableDeclarator" && binding.declaration.init) return configPath(editor, binding.declaration.init, file, cwd, seen);
|
|
1174
|
+
}
|
|
1175
|
+
if (node.type === "MemberExpression" && importMeta(node.object)) {
|
|
1176
|
+
if (memberName(node) === "dirname") return path.dirname(file);
|
|
1177
|
+
if (memberName(node) === "filename") return file;
|
|
1178
|
+
}
|
|
1179
|
+
if (node.type !== "CallExpression" || node.optional) return;
|
|
1180
|
+
const first = node.arguments[0];
|
|
1181
|
+
if (importedName(editor, node.callee, NODE_URL) === "fileURLToPath" && node.arguments.length === 1 && first?.type === "MemberExpression" && importMeta(first.object) && memberName(first) === "url") return file;
|
|
1182
|
+
const method = pathFunction(editor, node.callee);
|
|
1183
|
+
if (![
|
|
1184
|
+
"join",
|
|
1185
|
+
"resolve",
|
|
1186
|
+
"dirname"
|
|
1187
|
+
].includes(method ?? "")) return;
|
|
1188
|
+
const args = node.arguments.map((arg) => configPath(editor, arg, file, cwd, new Set(seen)));
|
|
1189
|
+
if (args.some((arg) => arg === void 0)) return;
|
|
1190
|
+
const values = args;
|
|
1191
|
+
if (method === "dirname") return values.length === 1 ? path.dirname(values[0]) : void 0;
|
|
1192
|
+
return method === "resolve" ? path.resolve(cwd, ...values) : path.join(...values);
|
|
1193
|
+
}
|
|
1194
|
+
/** The addon replaces test.include with Storybook stories, not Vitest's default
|
|
1195
|
+
* test/spec glob. Keep this adapter static; never load addons or run presets.
|
|
1196
|
+
* https://storybook.js.org/docs/writing-tests/integrations/vitest-addon */
|
|
1197
|
+
function resolveStorybookScope(editor, object, sources, file, cwd) {
|
|
1198
|
+
const plugins = objectProperty(object, "plugins")?.value;
|
|
1199
|
+
if (!plugins) return;
|
|
1200
|
+
const calls = [];
|
|
1201
|
+
editor.visit({ CallExpression(node) {
|
|
1202
|
+
if (node.start >= plugins.start && node.end <= plugins.end && importedName(editor, node.callee, STORYBOOK_PLUGIN) === "storybookTest") calls.push(node);
|
|
1203
|
+
} });
|
|
1204
|
+
if (!calls.length) return;
|
|
1205
|
+
const unknown = (detail) => ({ reason: `Storybook test ownership is unresolved (${path.basename(file)}): ${detail}.` });
|
|
1206
|
+
if (calls.length !== 1) return unknown("multiple storybookTest calls");
|
|
1207
|
+
const call = calls[0];
|
|
1208
|
+
let container = call;
|
|
1209
|
+
while (container !== plugins) {
|
|
1210
|
+
const parent = editor.parent(container);
|
|
1211
|
+
if (parent?.type !== "ArrayExpression" && parent?.type !== "AwaitExpression") return unknown("conditional or wrapped storybookTest call");
|
|
1212
|
+
container = parent;
|
|
1213
|
+
}
|
|
1214
|
+
const options = call.arguments[0];
|
|
1215
|
+
if (call.optional || call.arguments.length > 1 || options && !staticObject(options)) return unknown("dynamic storybookTest options");
|
|
1216
|
+
const configDir = options && objectProperty(options, "configDir")?.value;
|
|
1217
|
+
const directory = configDir ? configPath(editor, configDir, file, cwd) : ".storybook";
|
|
1218
|
+
if (directory === void 0) return unknown("dynamic configDir");
|
|
1219
|
+
const resolvedDir = path.resolve(cwd, directory);
|
|
1220
|
+
const mains = [
|
|
1221
|
+
"js",
|
|
1222
|
+
"jsx",
|
|
1223
|
+
"ts",
|
|
1224
|
+
"tsx",
|
|
1225
|
+
"mjs",
|
|
1226
|
+
"mts",
|
|
1227
|
+
"cjs",
|
|
1228
|
+
"cts"
|
|
1229
|
+
].map((extension) => path.join(resolvedDir, `main.${extension}`)).filter((candidate) => sources.has(candidate));
|
|
1230
|
+
const label = path.relative(cwd, resolvedDir).replaceAll("\\", "/");
|
|
1231
|
+
if (mains.length !== 1) return unknown(`expected one readable main config in ${label}`);
|
|
1232
|
+
if ([
|
|
1233
|
+
"js",
|
|
1234
|
+
"ts",
|
|
1235
|
+
"mjs",
|
|
1236
|
+
"cjs"
|
|
1237
|
+
].some((extension) => sources.has(path.join(resolvedDir, `presets.${extension}`)))) return unknown(`custom presets in ${label}`);
|
|
1238
|
+
const mainFile = mains[0];
|
|
1239
|
+
try {
|
|
1240
|
+
const mainEditor = new SourceEditor(mainFile, sources.get(mainFile));
|
|
1241
|
+
const main = resolveConfigObject(mainEditor, mainEditor.ast.body.find((node) => node.type === "ExportDefaultDeclaration")?.declaration);
|
|
1242
|
+
if (!main || main.properties.some((property) => property.type === "SpreadElement")) return unknown(`dynamic main config in ${label}`);
|
|
1243
|
+
if ([
|
|
1244
|
+
"presets",
|
|
1245
|
+
"viteFinal",
|
|
1246
|
+
"experimental_indexers"
|
|
1247
|
+
].some((key) => objectProperty(main, key))) return unknown(`custom discovery hooks in ${label}/main`);
|
|
1248
|
+
const stories = objectProperty(main, "stories")?.value;
|
|
1249
|
+
if (stories?.type !== "ArrayExpression") return unknown(`dynamic stories in ${label}/main`);
|
|
1250
|
+
const include = [];
|
|
1251
|
+
for (const story of stories.elements) {
|
|
1252
|
+
let pattern;
|
|
1253
|
+
if (isString(story)) pattern = story.value;
|
|
1254
|
+
else if (staticObject(story)) {
|
|
1255
|
+
const directory = objectProperty(story, "directory")?.value;
|
|
1256
|
+
const files = objectProperty(story, "files")?.value;
|
|
1257
|
+
if (!isString(directory) || !isString(files)) return unknown(`dynamic story directory/files in ${label}/main`);
|
|
1258
|
+
pattern = path.join(directory.value, files.value);
|
|
1259
|
+
} else return unknown(`dynamic stories in ${label}/main`);
|
|
1260
|
+
if (pattern.startsWith("!")) return unknown(`negated story pattern in ${label}/main`);
|
|
1261
|
+
const target = path.resolve(resolvedDir, pattern);
|
|
1262
|
+
if (!/[?*{[(]/.test(pattern) && !sources.has(target)) return unknown(`unresolved story file or directory in ${label}/main`);
|
|
1263
|
+
include.push(target.replaceAll("\\", "/"));
|
|
1264
|
+
}
|
|
1265
|
+
return {
|
|
1266
|
+
root: path.resolve(resolvedDir, ".."),
|
|
1267
|
+
include
|
|
1268
|
+
};
|
|
1269
|
+
} catch {
|
|
1270
|
+
return unknown(`cannot parse ${label}/main`);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
//#endregion
|
|
1274
|
+
//#region src/migration/vitest-v5/scopes.ts
|
|
1275
|
+
const DEFAULT_TEST_INCLUDE = ["**/*.{test,spec}.?(c|m)[jt]s?(x)"];
|
|
1276
|
+
const DEFAULT_TEST_EXCLUDE = ["**/node_modules/**", "**/.git/**"];
|
|
1277
|
+
const DEFAULT_BENCHMARK_INCLUDE = ["**/*.{bench,benchmark}.?(c|m)[jt]s?(x)"];
|
|
1278
|
+
const DEFAULT_BENCHMARK_EXCLUDE = ["**/node_modules/**", "**/.git/**"];
|
|
1279
|
+
const TEST_CONFIG_SOURCES = /* @__PURE__ */ new Set([
|
|
1280
|
+
"vitest/config",
|
|
1281
|
+
"vite-plus/test/config",
|
|
1282
|
+
"vite-plus"
|
|
1283
|
+
]);
|
|
1284
|
+
const SCOPE_KEYS = [
|
|
1285
|
+
"root",
|
|
1286
|
+
"dir",
|
|
1287
|
+
"include",
|
|
1288
|
+
"exclude",
|
|
1289
|
+
"includeSource",
|
|
1290
|
+
"setupFiles",
|
|
1291
|
+
"globals",
|
|
1292
|
+
"browser",
|
|
1293
|
+
"benchmark",
|
|
1294
|
+
"projects",
|
|
1295
|
+
"name"
|
|
1296
|
+
];
|
|
1297
|
+
const CONFIG_NAMES = ["vitest.config", "vite.config"].flatMap((name) => [
|
|
1298
|
+
".ts",
|
|
1299
|
+
".mts",
|
|
1300
|
+
".cts",
|
|
1301
|
+
".js",
|
|
1302
|
+
".mjs",
|
|
1303
|
+
".cjs"
|
|
1304
|
+
].map((extension) => name + extension));
|
|
1305
|
+
function defaultConfig(sources, root) {
|
|
1306
|
+
return CONFIG_NAMES.map((name) => path.join(root, name)).find((file) => sources.has(file));
|
|
1307
|
+
}
|
|
1308
|
+
/** Candidate config files are not all active projects. Select each package's
|
|
1309
|
+
* default config or literal script selections before following project edges. */
|
|
1310
|
+
function findVitestV5ConfigEntries(sources, directories) {
|
|
1311
|
+
const entries = [];
|
|
1312
|
+
for (const root of directories) {
|
|
1313
|
+
const manifest = JSON.parse((sources.get(path.join(root, "package.json")) ?? "{}").replace(/^\uFEFF/, ""));
|
|
1314
|
+
const commands = Object.values(manifest.scripts ?? {}).filter((command) => typeof command === "string" && /\bvitest\b|\bvp\s+test\b/.test(command));
|
|
1315
|
+
if (!commands.length) {
|
|
1316
|
+
entries.push({
|
|
1317
|
+
root,
|
|
1318
|
+
file: defaultConfig(sources, root),
|
|
1319
|
+
implicit: true
|
|
1320
|
+
});
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
for (const command of commands) {
|
|
1324
|
+
const invocations = literalTestCommands(command)?.filter((argv) => vitestCommandArgsStart(argv) >= 0);
|
|
1325
|
+
if (!invocations?.length) {
|
|
1326
|
+
entries.push({
|
|
1327
|
+
root,
|
|
1328
|
+
uncertain: true
|
|
1329
|
+
});
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
for (const argv of invocations) {
|
|
1333
|
+
const start = vitestCommandArgsStart(argv);
|
|
1334
|
+
let config;
|
|
1335
|
+
let rootOverride;
|
|
1336
|
+
let dirOverride;
|
|
1337
|
+
let uncertain = false;
|
|
1338
|
+
let browserOverride = false;
|
|
1339
|
+
const projects = [];
|
|
1340
|
+
for (let index = start; index < argv.length && argv[index] !== "--"; index++) {
|
|
1341
|
+
const arg = argv[index];
|
|
1342
|
+
const option = arg.split("=")[0];
|
|
1343
|
+
browserOverride ||= /^--(?:no-)?browser(?:[.=]|$)/.test(arg);
|
|
1344
|
+
if ([
|
|
1345
|
+
"--config",
|
|
1346
|
+
"-c",
|
|
1347
|
+
"--root",
|
|
1348
|
+
"-r",
|
|
1349
|
+
"--dir",
|
|
1350
|
+
"--project",
|
|
1351
|
+
"-p"
|
|
1352
|
+
].includes(option)) {
|
|
1353
|
+
const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : argv[++index];
|
|
1354
|
+
if (!value || value.startsWith("-")) uncertain = true;
|
|
1355
|
+
else if (option === "--project" || option === "-p") projects.push(value);
|
|
1356
|
+
else if (option === "--config" || option === "-c") config = value;
|
|
1357
|
+
else if (option === "--dir") dirOverride = path.resolve(root, value);
|
|
1358
|
+
else rootOverride = path.resolve(root, value);
|
|
1359
|
+
} else if (/^--(?:no-)?(?:globals|include|includeSource|exclude|workspace|setupFiles|benchmark\.(?:include|exclude|includeSource))(?:[.=]|$)/.test(arg)) uncertain = true;
|
|
1360
|
+
}
|
|
1361
|
+
const configRoot = rootOverride ?? root;
|
|
1362
|
+
const file = config ? path.resolve(configRoot, config) : defaultConfig(sources, configRoot);
|
|
1363
|
+
entries.push({
|
|
1364
|
+
root,
|
|
1365
|
+
file,
|
|
1366
|
+
rootOverride,
|
|
1367
|
+
dirOverride,
|
|
1368
|
+
projects,
|
|
1369
|
+
browserOverride,
|
|
1370
|
+
uncertain: uncertain || !!config && !sources.has(file)
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return entries;
|
|
1376
|
+
}
|
|
1377
|
+
function staticPatterns(node, editor) {
|
|
1378
|
+
if (node?.type === "MemberExpression" && importedName(editor, node.object, TEST_CONFIG_SOURCES) === "configDefaults") {
|
|
1379
|
+
if (memberName(node) === "include") return DEFAULT_TEST_INCLUDE;
|
|
1380
|
+
if (memberName(node) === "exclude") return DEFAULT_TEST_EXCLUDE;
|
|
1381
|
+
}
|
|
1382
|
+
if (node?.type !== "ArrayExpression") return;
|
|
1383
|
+
const patterns = [];
|
|
1384
|
+
for (const entry of node.elements) if (isString(entry)) patterns.push(entry.value);
|
|
1385
|
+
else if (entry?.type === "SpreadElement") {
|
|
1386
|
+
const values = staticPatterns(entry.argument, editor);
|
|
1387
|
+
if (!values) return;
|
|
1388
|
+
patterns.push(...values);
|
|
1389
|
+
} else return;
|
|
1390
|
+
return patterns.some((pattern) => pattern.startsWith("!")) ? void 0 : patterns;
|
|
1391
|
+
}
|
|
1392
|
+
function booleanOption(node, fallback) {
|
|
1393
|
+
if (!node) return fallback;
|
|
1394
|
+
return isBoolean(node) ? node.value : void 0;
|
|
1395
|
+
}
|
|
1396
|
+
function benchmarkPatterns(node, inherited, editor) {
|
|
1397
|
+
if (!inherited || node && !staticObject(node)) return;
|
|
1398
|
+
const patterns = { ...inherited };
|
|
1399
|
+
for (const key of [
|
|
1400
|
+
"include",
|
|
1401
|
+
"exclude",
|
|
1402
|
+
"includeSource"
|
|
1403
|
+
]) {
|
|
1404
|
+
const value = node && objectProperty(node, key)?.value;
|
|
1405
|
+
if (value) {
|
|
1406
|
+
const resolved = staticPatterns(value, editor);
|
|
1407
|
+
if (!resolved) return;
|
|
1408
|
+
patterns[key] = [...inherited[key] ?? [], ...resolved];
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return patterns;
|
|
1412
|
+
}
|
|
1413
|
+
function matches(file, root, pattern) {
|
|
1414
|
+
return minimatch(path.relative(root, file).replaceAll("\\", "/"), pattern.replace(/^\.\//, ""), { dot: true });
|
|
1415
|
+
}
|
|
1416
|
+
function insideDirectory(file, directory) {
|
|
1417
|
+
const relative = path.relative(directory, file);
|
|
1418
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
1419
|
+
}
|
|
1420
|
+
function matchesProject(name, pattern) {
|
|
1421
|
+
const expression = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
1422
|
+
return new RegExp(`^${expression}$`, "i").test(name);
|
|
1423
|
+
}
|
|
1424
|
+
function projectSelected(test, filters, inheritedBrowser) {
|
|
1425
|
+
if (!filters.length) return true;
|
|
1426
|
+
const name = test && objectProperty(test, "name")?.value;
|
|
1427
|
+
if (!isString(name)) return;
|
|
1428
|
+
const positives = filters.filter((pattern) => !pattern.startsWith("!"));
|
|
1429
|
+
const excluded = (name) => filters.some((pattern) => pattern.startsWith("!") && matchesProject(name, pattern.slice(1)));
|
|
1430
|
+
if (excluded(name.value)) return false;
|
|
1431
|
+
if (!positives.length || positives.some((pattern) => matchesProject(name.value, pattern))) return true;
|
|
1432
|
+
const browser = test && objectProperty(test, "browser")?.value;
|
|
1433
|
+
if (!browser) return inheritedBrowser === false ? false : void 0;
|
|
1434
|
+
if (!staticObject(browser)) return;
|
|
1435
|
+
const enabled = objectProperty(browser, "enabled")?.value;
|
|
1436
|
+
if (isBoolean(enabled) && !enabled.value) return false;
|
|
1437
|
+
const instances = objectProperty(browser, "instances")?.value;
|
|
1438
|
+
if (instances?.type !== "ArrayExpression") return;
|
|
1439
|
+
for (const instance of instances.elements) {
|
|
1440
|
+
if (!staticObject(instance)) return;
|
|
1441
|
+
const explicitName = objectProperty(instance, "name")?.value;
|
|
1442
|
+
const browserName = objectProperty(instance, "browser")?.value;
|
|
1443
|
+
let instanceName;
|
|
1444
|
+
if (isString(explicitName)) instanceName = explicitName.value;
|
|
1445
|
+
else if (!explicitName && isString(browserName)) instanceName = name.value ? `${name.value} (${browserName.value})` : browserName.value;
|
|
1446
|
+
if (!instanceName || !excluded(instanceName) && positives.some((pattern) => matchesProject(instanceName, pattern))) return;
|
|
1447
|
+
}
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
/** Vitest resolves local setup entries through local-pkg/mlly with import
|
|
1451
|
+
* conditions, then falls back to the literal path. Do not guess TypeScript
|
|
1452
|
+
* extensions or package exports that cannot be resolved from scanned files. */
|
|
1453
|
+
function resolveSetupFile(sources, root, reference) {
|
|
1454
|
+
const target = path.resolve(root, reference);
|
|
1455
|
+
if (!/^\.\.?(?:[/\\]|$)/.test(reference) && !path.isAbsolute(reference)) {
|
|
1456
|
+
const packageName = reference.split("/").slice(0, reference.startsWith("@") ? 2 : 1).join("/");
|
|
1457
|
+
for (const parent of sources.keys()) {
|
|
1458
|
+
if (path.basename(parent) !== "package.json" || !insideDirectory(root, path.dirname(parent))) continue;
|
|
1459
|
+
const manifest = JSON.parse(sources.get(parent).replace(/^\uFEFF/, ""));
|
|
1460
|
+
if ([
|
|
1461
|
+
"dependencies",
|
|
1462
|
+
"devDependencies",
|
|
1463
|
+
"peerDependencies",
|
|
1464
|
+
"optionalDependencies"
|
|
1465
|
+
].some((key) => manifest[key]?.[packageName])) return;
|
|
1466
|
+
}
|
|
1467
|
+
return sources.has(target) ? target : void 0;
|
|
1468
|
+
}
|
|
1469
|
+
const extensions = [
|
|
1470
|
+
".mjs",
|
|
1471
|
+
".cjs",
|
|
1472
|
+
".js",
|
|
1473
|
+
".json"
|
|
1474
|
+
];
|
|
1475
|
+
return [
|
|
1476
|
+
target,
|
|
1477
|
+
...extensions.map((extension) => /[/\\]$/.test(reference) ? path.join(target, extension) : target + extension),
|
|
1478
|
+
...extensions.map((extension) => path.join(target, "index" + extension))
|
|
1479
|
+
].find((candidate) => sources.has(candidate));
|
|
1480
|
+
}
|
|
1481
|
+
/** Resolve only selected configs and their project references. Unknown config
|
|
1482
|
+
* or conflicting project globals requires review, not silent suppression. */
|
|
1483
|
+
function resolveVitestV5TestModes(sources, configFiles, preserveV4, entries) {
|
|
1484
|
+
const scopes = [];
|
|
1485
|
+
const findings = [];
|
|
1486
|
+
const queue = [...entries];
|
|
1487
|
+
const packageRoots = [...new Set(entries.map((entry) => entry.root))];
|
|
1488
|
+
const visitedEntries = /* @__PURE__ */ new Set();
|
|
1489
|
+
for (const entry of queue) {
|
|
1490
|
+
const key = JSON.stringify(entry);
|
|
1491
|
+
if (visitedEntries.has(key)) continue;
|
|
1492
|
+
visitedEntries.add(key);
|
|
1493
|
+
const unknown = () => scopes.push({ root: entry.root });
|
|
1494
|
+
if (entry.uncertain) {
|
|
1495
|
+
unknown();
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
if (!entry.file) continue;
|
|
1499
|
+
let found = false;
|
|
1500
|
+
let editor;
|
|
1501
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1502
|
+
const walk = (object, directory = entry.root, base, inline = false) => {
|
|
1503
|
+
found = true;
|
|
1504
|
+
const resolved = resolveConfigObject(editor, object);
|
|
1505
|
+
if (!resolved) {
|
|
1506
|
+
unknown();
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
object = resolved;
|
|
1510
|
+
if (visited.has(object)) return;
|
|
1511
|
+
visited.add(object);
|
|
1512
|
+
const test = objectProperty(object, "test")?.value;
|
|
1513
|
+
if (test && !staticObject(test, true)) {
|
|
1514
|
+
unknown();
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const root = (test && objectProperty(test, "root")?.value) ?? objectProperty(object, "root")?.value;
|
|
1518
|
+
if (root && !isString(root)) {
|
|
1519
|
+
unknown();
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const resolvedRoot = !inline && entry.rootOverride || path.resolve(directory, isString(root) ? root.value : ".");
|
|
1523
|
+
const projects = test && objectProperty(test, "projects")?.value;
|
|
1524
|
+
const selected = projects ? true : projectSelected(test, entry.projects ?? [], base ? base.browser : false);
|
|
1525
|
+
if (selected === false) return;
|
|
1526
|
+
const storybook = resolveStorybookScope(editor, object, sources, entry.file, entry.cwd ?? entry.root) ?? base?.storybook;
|
|
1527
|
+
const scopeRoot = test && objectProperty(test, "root") ? resolvedRoot : storybook?.root ?? resolvedRoot;
|
|
1528
|
+
const dirValue = test && objectProperty(test, "dir")?.value;
|
|
1529
|
+
let dir = base ? base.dir : "";
|
|
1530
|
+
if (dirValue) dir = isString(dirValue) ? dirValue.value : void 0;
|
|
1531
|
+
const effectiveDir = (!inline ? entry.dirOverride : void 0) ?? dir;
|
|
1532
|
+
const include = test && objectProperty(test, "include")?.value;
|
|
1533
|
+
const includeSource = test && objectProperty(test, "includeSource")?.value;
|
|
1534
|
+
const exclude = test && objectProperty(test, "exclude")?.value;
|
|
1535
|
+
const includePatterns = include ? staticPatterns(include, editor) : void 0;
|
|
1536
|
+
const sourcePatterns = includeSource ? staticPatterns(includeSource, editor) : [];
|
|
1537
|
+
const inheritedSourcePatterns = base ? base.includeSource : [];
|
|
1538
|
+
const excludePatterns = exclude ? staticPatterns(exclude, editor) : void 0;
|
|
1539
|
+
if (include && !includePatterns || exclude && !excludePatterns) {
|
|
1540
|
+
unknown();
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const browser = test && objectProperty(test, "browser")?.value;
|
|
1544
|
+
const enabled = staticObject(browser) ? objectProperty(browser, "enabled")?.value : void 0;
|
|
1545
|
+
let browserMode = base ? base.browser : false;
|
|
1546
|
+
if (browser) browserMode = staticObject(browser) ? booleanOption(enabled, browserMode) : void 0;
|
|
1547
|
+
const globals = test && objectProperty(test, "globals")?.value;
|
|
1548
|
+
const benchmark = test && objectProperty(test, "benchmark")?.value;
|
|
1549
|
+
const setup = test && objectProperty(test, "setupFiles")?.value;
|
|
1550
|
+
let setupFiles = [];
|
|
1551
|
+
if (setup) setupFiles = isString(setup) ? [setup.value] : staticPatterns(setup, editor);
|
|
1552
|
+
let discoveryRoot;
|
|
1553
|
+
if (effectiveDir !== void 0) discoveryRoot = effectiveDir ? path.resolve(entry.cwd ?? entry.root, effectiveDir) : scopeRoot;
|
|
1554
|
+
const projectName = test && objectProperty(test, "name")?.value;
|
|
1555
|
+
const scope = {
|
|
1556
|
+
storybook,
|
|
1557
|
+
name: isString(projectName) ? projectName.value : path.basename(entry.file),
|
|
1558
|
+
excludedRoots: entry.implicit && !inline && !root && path.basename(entry.file).startsWith("vite.config.") && (!test || test.properties.every((property) => property.type === "Property" && ["clearMocks", "fakeTimers"].includes(propertyName(property.key) ?? ""))) ? packageRoots.filter((root) => root !== entry.root && insideDirectory(root, entry.root)) : void 0,
|
|
1559
|
+
root: scopeRoot,
|
|
1560
|
+
dir,
|
|
1561
|
+
discoveryRoot,
|
|
1562
|
+
include: includePatterns ? [...base?.include ?? [], ...includePatterns] : base?.include,
|
|
1563
|
+
includeSource: sourcePatterns && inheritedSourcePatterns ? [...inheritedSourcePatterns, ...sourcePatterns] : void 0,
|
|
1564
|
+
exclude: excludePatterns ? [...base?.exclude ?? [], ...excludePatterns] : base?.exclude,
|
|
1565
|
+
benchmark: benchmarkPatterns(benchmark, base ? base.benchmark : {}, editor),
|
|
1566
|
+
setupFiles: setupFiles && (!base || base.setupFiles) ? [...base?.setupFiles ?? [], ...setupFiles] : void 0,
|
|
1567
|
+
browser: entry.browserOverride ? void 0 : browserMode,
|
|
1568
|
+
globals: selected === void 0 ? void 0 : booleanOption(globals, base ? base.globals : false)
|
|
1569
|
+
};
|
|
1570
|
+
const addScope = () => {
|
|
1571
|
+
if (!scope.includeSource) editor.report(includeSource ?? test ?? object, "global-api-ownership", "Resolve test.includeSource patterns before migrating in-source global APIs. In-source test ownership is not statically known.");
|
|
1572
|
+
if (scope.storybook?.reason) editor.report(objectProperty(object, "plugins")?.value ?? object, "global-api-ownership", scope.storybook.reason);
|
|
1573
|
+
const resolvedSetup = scope.setupFiles?.map((reference) => resolveSetupFile(sources, scope.root, reference));
|
|
1574
|
+
const unresolvedSetup = !resolvedSetup || resolvedSetup.some((file) => !file);
|
|
1575
|
+
if (!scope.discoveryRoot) editor.report(dirValue ?? object, "global-api-ownership", "Resolve test.dir before migrating global APIs. The test discovery directory is not statically known.");
|
|
1576
|
+
if (unresolvedSetup) editor.report(setup ?? object, "global-api-ownership", "Resolve setupFiles before migrating global APIs. A setup entry cannot be resolved safely from the scanned files.");
|
|
1577
|
+
if (!scope.benchmark) editor.report(benchmark ?? object, "global-api-ownership", "Resolve test.benchmark file patterns before migrating global APIs. Benchmark file ownership is not statically known.");
|
|
1578
|
+
scopes.push({
|
|
1579
|
+
...scope,
|
|
1580
|
+
setupFiles: resolvedSetup?.filter((file) => !!file),
|
|
1581
|
+
unresolvedSetup
|
|
1582
|
+
});
|
|
1583
|
+
};
|
|
1584
|
+
if (!projects) {
|
|
1585
|
+
addScope();
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
if (projects.type !== "ArrayExpression") {
|
|
1589
|
+
unknown();
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const elements = projectElements(projects);
|
|
1593
|
+
const excluded = elements.filter(isString).map((node) => node.value).filter((value) => value.startsWith("!")).map((value) => value.slice(1));
|
|
1594
|
+
for (const project of elements) {
|
|
1595
|
+
if (isString(project)) {
|
|
1596
|
+
if (project.value.startsWith("!")) continue;
|
|
1597
|
+
const reference = project.value.replaceAll("\\", "/").replace("<rootDir>", scope.root);
|
|
1598
|
+
const target = path.resolve(scope.root, reference);
|
|
1599
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1600
|
+
if (sources.has(target)) candidates.add(target);
|
|
1601
|
+
const directoryConfig = defaultConfig(sources, target);
|
|
1602
|
+
if (directoryConfig) candidates.add(directoryConfig);
|
|
1603
|
+
for (const file of configFiles) if (matches(file, scope.root, reference) || matches(path.dirname(file), scope.root, reference) && defaultConfig(sources, path.dirname(file)) === file) candidates.add(file);
|
|
1604
|
+
if (!candidates.size) {
|
|
1605
|
+
unknown();
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
for (const file of candidates) {
|
|
1609
|
+
if (excluded.some((pattern) => matches(file, scope.root, pattern) || matches(path.dirname(file), scope.root, pattern))) continue;
|
|
1610
|
+
if (file === entry.file) addScope();
|
|
1611
|
+
else queue.push({
|
|
1612
|
+
file,
|
|
1613
|
+
root: path.dirname(file),
|
|
1614
|
+
cwd: entry.cwd ?? entry.root,
|
|
1615
|
+
projects: entry.projects,
|
|
1616
|
+
browserOverride: entry.browserOverride
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
if (!staticObject(project)) {
|
|
1622
|
+
unknown();
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
const extendsValue = objectProperty(project, "extends")?.value;
|
|
1626
|
+
if (extendsValue && !isBoolean(extendsValue)) {
|
|
1627
|
+
const baseFile = isString(extendsValue) && path.resolve(scope.root, extendsValue.value);
|
|
1628
|
+
const baseSource = baseFile && sources.get(baseFile);
|
|
1629
|
+
if (baseSource) {
|
|
1630
|
+
const baseEditor = new SourceEditor(baseFile, baseSource);
|
|
1631
|
+
const baseObject = resolveConfigObject(baseEditor, baseEditor.ast.body.find((node) => node.type === "ExportDefaultDeclaration")?.declaration);
|
|
1632
|
+
if (baseObject) {
|
|
1633
|
+
const baseTest = objectProperty(baseObject, "test")?.value;
|
|
1634
|
+
if (!baseTest || staticObject(baseTest, true) && !SCOPE_KEYS.some((key) => objectProperty(baseTest, key))) {
|
|
1635
|
+
walk(project, scope.root, void 0, true);
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
unknown();
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
const inherits = isBoolean(extendsValue) ? extendsValue.value : !preserveV4;
|
|
1644
|
+
walk(project, scope.root, inherits ? scope : void 0, true);
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
try {
|
|
1648
|
+
editor = new SourceEditor(entry.file, sources.get(entry.file));
|
|
1649
|
+
editor.visit({
|
|
1650
|
+
ExportDefaultDeclaration(node) {
|
|
1651
|
+
walk(node.declaration);
|
|
1652
|
+
},
|
|
1653
|
+
AssignmentExpression(node) {
|
|
1654
|
+
if (isModuleExports(editor, node.left)) walk(node.right);
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
findings.push(...editor.findings);
|
|
1658
|
+
} catch {
|
|
1659
|
+
unknown();
|
|
1660
|
+
}
|
|
1661
|
+
if (!found) unknown();
|
|
1662
|
+
}
|
|
1663
|
+
return {
|
|
1664
|
+
modes: new Map([...sources.keys()].map((file) => {
|
|
1665
|
+
const matching = [];
|
|
1666
|
+
let benchmarkFile = false;
|
|
1667
|
+
const hasInSourceTests = sources.get(file)?.includes("import.meta.vitest");
|
|
1668
|
+
for (const scope of scopes) {
|
|
1669
|
+
if (scope.excludedRoots?.some((root) => insideDirectory(file, root))) continue;
|
|
1670
|
+
const setup = scope.setupFiles?.includes(file);
|
|
1671
|
+
const inside = insideDirectory(file, scope.root);
|
|
1672
|
+
const discoveryRoot = scope.discoveryRoot;
|
|
1673
|
+
const sourceCandidate = hasInSourceTests && discoveryRoot && insideDirectory(file, discoveryRoot) && !(scope.exclude ?? DEFAULT_TEST_EXCLUDE).some((pattern) => matches(file, discoveryRoot, pattern));
|
|
1674
|
+
const inSource = sourceCandidate && scope.includeSource?.some((pattern) => matches(file, discoveryRoot, pattern));
|
|
1675
|
+
const test = scope.storybook ? discoveryRoot && scope.storybook.include?.some((pattern) => minimatch(file.replaceAll("\\", "/"), pattern, { dot: true })) && !(scope.exclude ?? DEFAULT_TEST_EXCLUDE).some((pattern) => matches(file, discoveryRoot, pattern)) : discoveryRoot && insideDirectory(file, discoveryRoot) && (scope.include ?? DEFAULT_TEST_INCLUDE).some((pattern) => matches(file, discoveryRoot, pattern)) && !(scope.exclude ?? DEFAULT_TEST_EXCLUDE).some((pattern) => matches(file, discoveryRoot, pattern));
|
|
1676
|
+
const benchmark = discoveryRoot && insideDirectory(file, discoveryRoot) && scope.benchmark && !(scope.benchmark.exclude ?? DEFAULT_BENCHMARK_EXCLUDE).some((pattern) => matches(file, discoveryRoot, pattern)) && ((scope.benchmark.include ?? DEFAULT_BENCHMARK_INCLUDE).some((pattern) => matches(file, discoveryRoot, pattern)) || scope.benchmark.includeSource?.some((pattern) => matches(file, discoveryRoot, pattern)) && hasInSourceTests);
|
|
1677
|
+
benchmarkFile ||= !!benchmark;
|
|
1678
|
+
if (setup || test || inSource || benchmark) matching.push({
|
|
1679
|
+
scope,
|
|
1680
|
+
certain: true
|
|
1681
|
+
});
|
|
1682
|
+
else if ((!discoveryRoot || !scope.setupFiles || scope.unresolvedSetup || scope.storybook?.reason) && inside || !scope.benchmark && discoveryRoot && insideDirectory(file, discoveryRoot) || sourceCandidate && !scope.includeSource) matching.push({
|
|
1683
|
+
scope,
|
|
1684
|
+
certain: false
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
const browserModes = new Set(matching.map(({ scope, certain }) => certain ? scope.browser : void 0));
|
|
1688
|
+
const globals = matching.length > 0 && matching.every(({ scope, certain }) => certain && scope.globals === true);
|
|
1689
|
+
const ownershipReason = matching.some(({ scope }) => scope.storybook) ? matching.find(({ scope }) => scope.storybook?.reason)?.scope.storybook?.reason ?? (browserModes.size > 1 ? `Conflicting Node/browser projects: ${[...new Set(matching.map(({ scope }) => scope.name))].join(", ")}.` : void 0) : void 0;
|
|
1690
|
+
return [file, {
|
|
1691
|
+
benchmark: benchmarkFile,
|
|
1692
|
+
browser: browserModes.size === 1 ? browserModes.values().next().value : void 0,
|
|
1693
|
+
...ownershipReason ? { ownershipReason } : {},
|
|
1694
|
+
globals,
|
|
1695
|
+
reviewGlobals: !globals && matching.some(({ scope, certain }) => !certain || scope.globals !== false)
|
|
1696
|
+
}];
|
|
1697
|
+
})),
|
|
1698
|
+
findings
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
//#endregion
|
|
1702
|
+
//#region src/migration/vitest-v5/benchmarks.ts
|
|
1703
|
+
/** Keep the workload as a separate callback: making it the test callback
|
|
1704
|
+
* would measure nothing, and inlining it into a new closure can change scope. */
|
|
1705
|
+
function migrateBenchmarks(editor, globals = false) {
|
|
1706
|
+
const imports = /* @__PURE__ */ new Set();
|
|
1707
|
+
const bindings = /* @__PURE__ */ new Set();
|
|
1708
|
+
const calls = /* @__PURE__ */ new Set();
|
|
1709
|
+
const benchIdentifiers = [];
|
|
1710
|
+
let fixtureAlias;
|
|
1711
|
+
let shadowedGlobalThis = false;
|
|
1712
|
+
editor.visit({ Identifier(node) {
|
|
1713
|
+
if (node.name === "bench") benchIdentifiers.push(node);
|
|
1714
|
+
if (node.name === "globalThis" && editor.binding(node)) shadowedGlobalThis = true;
|
|
1715
|
+
} });
|
|
1716
|
+
function workload(node, seen = /* @__PURE__ */ new Set()) {
|
|
1717
|
+
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration") return !!node.body && node.params.length === 0 && !node.generator;
|
|
1718
|
+
if (node.type !== "Identifier" || seen.has(node)) return false;
|
|
1719
|
+
seen.add(node);
|
|
1720
|
+
const binding = editor.binding(node);
|
|
1721
|
+
if (!binding?.constant) return false;
|
|
1722
|
+
const declaration = binding.declaration;
|
|
1723
|
+
if (declaration.type === "FunctionDeclaration") return workload(declaration, seen);
|
|
1724
|
+
return declaration.type === "VariableDeclarator" && !!declaration.init && workload(declaration.init, seen);
|
|
1725
|
+
}
|
|
1726
|
+
function directCall(reference) {
|
|
1727
|
+
let callee = reference;
|
|
1728
|
+
let modifier;
|
|
1729
|
+
const member = editor.parent(reference);
|
|
1730
|
+
if (member?.type === "MemberExpression" && member.object === reference) {
|
|
1731
|
+
modifier = memberName(member);
|
|
1732
|
+
if (member.optional || !modifier || ![
|
|
1733
|
+
"skip",
|
|
1734
|
+
"only",
|
|
1735
|
+
"todo"
|
|
1736
|
+
].includes(modifier)) return;
|
|
1737
|
+
callee = member;
|
|
1738
|
+
}
|
|
1739
|
+
const call = editor.parent(callee);
|
|
1740
|
+
if (call?.type !== "CallExpression" || call.callee !== callee || call.optional || call.typeArguments || editor.parent(call)?.type !== "ExpressionStatement" || !call.arguments[0] || call.arguments[0].type === "SpreadElement") return;
|
|
1741
|
+
const callback = call.arguments[1];
|
|
1742
|
+
if (!(modifier === "todo" && call.arguments.length === 1)) {
|
|
1743
|
+
if (call.arguments.length !== 2 || !callback || !workload(callback)) return;
|
|
1744
|
+
}
|
|
1745
|
+
let fn = editor.functionParent(call);
|
|
1746
|
+
while (fn) {
|
|
1747
|
+
const registration = editor.parent(fn);
|
|
1748
|
+
if (fn.async || fn.generator || registration?.type !== "CallExpression" || registration.optional || !registration.arguments.includes(fn)) return;
|
|
1749
|
+
let suite = registration.callee;
|
|
1750
|
+
if (!["describe", "suite"].includes(testApiName(editor, suite, globals) ?? "")) {
|
|
1751
|
+
if (suite.type !== "MemberExpression" || suite.optional || !["skip", "only"].includes(memberName(suite) ?? "")) return;
|
|
1752
|
+
suite = suite.object;
|
|
1753
|
+
}
|
|
1754
|
+
if (!["describe", "suite"].includes(testApiName(editor, suite, globals) ?? "")) return;
|
|
1755
|
+
fn = editor.functionParent(registration);
|
|
1756
|
+
}
|
|
1757
|
+
return {
|
|
1758
|
+
call,
|
|
1759
|
+
modifier
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
function rewrite({ call, modifier }, test) {
|
|
1763
|
+
calls.add(call);
|
|
1764
|
+
editor.replace(call.callee, `${test}${modifier ? `.${modifier}` : ""}`);
|
|
1765
|
+
const callback = call.arguments[1];
|
|
1766
|
+
if (!callback) return;
|
|
1767
|
+
let name = editor.text(call.arguments[0]);
|
|
1768
|
+
const captures = [];
|
|
1769
|
+
if (!isString(call.arguments[0])) {
|
|
1770
|
+
const captured = editor.uniqueName("benchName");
|
|
1771
|
+
captures.push(`const ${captured} = (${name});`);
|
|
1772
|
+
editor.replace(call.arguments[0], captured);
|
|
1773
|
+
name = captured;
|
|
1774
|
+
}
|
|
1775
|
+
if (callback.type === "Identifier") {
|
|
1776
|
+
const captured = editor.uniqueName("benchFn");
|
|
1777
|
+
captures.push(`const ${captured} = ${editor.text(callback)};`);
|
|
1778
|
+
editor.replace(callback, captured);
|
|
1779
|
+
}
|
|
1780
|
+
if (captures.length) {
|
|
1781
|
+
const statement = editor.parent(call);
|
|
1782
|
+
editor.edit(statement.start, statement.start, `{ ${captures.join(" ")} `);
|
|
1783
|
+
editor.edit(statement.end, statement.end, " }");
|
|
1784
|
+
}
|
|
1785
|
+
const shadowsBench = callback.type !== "Identifier" && benchIdentifiers.some((node) => node.start >= callback.start && node.end <= callback.end);
|
|
1786
|
+
let fixtureName = "bench";
|
|
1787
|
+
if (shadowsBench) fixtureName = fixtureAlias ??= editor.uniqueName("bench");
|
|
1788
|
+
const fixture = fixtureName === "bench" ? "bench" : `bench: ${fixtureName}`;
|
|
1789
|
+
editor.edit(callback.start, callback.start, `async ({ ${fixture} }) => { await ${fixtureName}(${name}, `);
|
|
1790
|
+
editor.edit(callback.end, callback.end, ").run(); }");
|
|
1791
|
+
}
|
|
1792
|
+
function unsupportedInSource(node) {
|
|
1793
|
+
editor.report(node, "benchmark-api", "Migrate this import.meta.vitest bench reference manually: automatic migration requires direct calls with locally resolved zero-argument callbacks and no benchmark options or escaped references. Keep the import.meta.vitest guard.", "block");
|
|
1794
|
+
}
|
|
1795
|
+
editor.visit({
|
|
1796
|
+
VariableDeclarator(node) {
|
|
1797
|
+
if (node.id.type !== "ObjectPattern" || !isImportMetaVitest(node.init)) return;
|
|
1798
|
+
for (const property of node.id.properties) {
|
|
1799
|
+
if (property.type !== "Property" || propertyName(property.key) !== "bench") continue;
|
|
1800
|
+
const binding = editor.binding(property.value);
|
|
1801
|
+
const registrations = binding?.references.map(directCall);
|
|
1802
|
+
if (property.computed || node.id.typeAnnotation || !binding?.constant || node.id.properties.some((prop) => prop.type === "RestElement") || !registrations || registrations.some((registration) => !registration)) {
|
|
1803
|
+
unsupportedInSource(property);
|
|
1804
|
+
continue;
|
|
1805
|
+
}
|
|
1806
|
+
const test = editor.uniqueName("test", true);
|
|
1807
|
+
editor.replace(property, test === "test" ? "test" : `test: ${test}`);
|
|
1808
|
+
bindings.add(property);
|
|
1809
|
+
for (const registration of registrations) rewrite(registration, test);
|
|
1810
|
+
}
|
|
1811
|
+
},
|
|
1812
|
+
ImportDeclaration(node) {
|
|
1813
|
+
if (!["vitest", "vite-plus/test"].includes(node.source.value) || node.importKind === "type") return;
|
|
1814
|
+
for (const specifier of node.specifiers) {
|
|
1815
|
+
if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type" || propertyName(specifier.imported) !== "bench") continue;
|
|
1816
|
+
const binding = editor.binding(specifier.local);
|
|
1817
|
+
if (!binding) continue;
|
|
1818
|
+
const registrations = binding.references.map(directCall);
|
|
1819
|
+
if (registrations.some((registration) => !registration)) continue;
|
|
1820
|
+
const test = editor.uniqueName("test", true);
|
|
1821
|
+
editor.replace(specifier, test === "test" ? "test" : `test as ${test}`);
|
|
1822
|
+
imports.add(specifier);
|
|
1823
|
+
for (const registration of registrations) rewrite(registration, test);
|
|
1824
|
+
}
|
|
1825
|
+
},
|
|
1826
|
+
Identifier(node) {
|
|
1827
|
+
if (!globals || node.name !== "bench" || editor.binding(node) || shadowedGlobalThis) return;
|
|
1828
|
+
const registration = directCall(node);
|
|
1829
|
+
if (registration) rewrite(registration, "globalThis.test");
|
|
1830
|
+
},
|
|
1831
|
+
MemberExpression(node) {
|
|
1832
|
+
const inSource = isImportMetaVitest(node.object) && memberName(node) === "bench";
|
|
1833
|
+
if (!inSource && testApiName(editor, node, globals) !== "bench") return;
|
|
1834
|
+
const registration = !node.optional && directCall(node);
|
|
1835
|
+
if (registration && (inSource || node.object.type === "Identifier")) rewrite(registration, `${editor.text(node.object)}.test`);
|
|
1836
|
+
else if (inSource) unsupportedInSource(node);
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
return {
|
|
1840
|
+
imports,
|
|
1841
|
+
bindings,
|
|
1842
|
+
calls
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
//#endregion
|
|
1846
|
+
//#region src/migration/vitest-v5/source.ts
|
|
1847
|
+
const RUNNER_SYMBOLS = {
|
|
1848
|
+
test: "test",
|
|
1849
|
+
it: "it",
|
|
1850
|
+
describe: "describe",
|
|
1851
|
+
suite: "suite",
|
|
1852
|
+
recordArtifact: "recordArtifact",
|
|
1853
|
+
TestAPI: "TestAPI",
|
|
1854
|
+
SuiteAPI: "SuiteAPI",
|
|
1855
|
+
SuiteCollector: "SuiteCollector",
|
|
1856
|
+
TestArtifact: "TestArtifact",
|
|
1857
|
+
File: "RunnerTestFile",
|
|
1858
|
+
Suite: "RunnerTestSuite",
|
|
1859
|
+
Test: "RunnerTestCase",
|
|
1860
|
+
Task: "RunnerTask",
|
|
1861
|
+
VitestRunner: "VitestTestRunner",
|
|
1862
|
+
VitestRunnerConfig: "TestRunnerConfig"
|
|
1863
|
+
};
|
|
1864
|
+
const RUNNER_METHODS = {
|
|
1865
|
+
getCurrentSuite: "getCurrentSuite",
|
|
1866
|
+
getCurrentTest: "getCurrentTest",
|
|
1867
|
+
createTaskCollector: "createTaskCollector",
|
|
1868
|
+
getFn: "getTestFn",
|
|
1869
|
+
getHooks: "getSuiteHooks",
|
|
1870
|
+
setFn: "setTestFn",
|
|
1871
|
+
setHooks: "setSuiteHooks"
|
|
1872
|
+
};
|
|
1873
|
+
const EXPECT_SYMBOLS = /* @__PURE__ */ new Set([
|
|
1874
|
+
"expect",
|
|
1875
|
+
"assert",
|
|
1876
|
+
"should",
|
|
1877
|
+
"createExpect",
|
|
1878
|
+
"Assertion",
|
|
1879
|
+
"AsymmetricMatchersContaining",
|
|
1880
|
+
"ExpectStatic",
|
|
1881
|
+
"JestAssertion",
|
|
1882
|
+
"Matcher",
|
|
1883
|
+
"Matchers",
|
|
1884
|
+
"MatchersObject",
|
|
1885
|
+
"MatcherState"
|
|
1886
|
+
]);
|
|
1887
|
+
const RUNNER_SOURCES = /* @__PURE__ */ new Set([
|
|
1888
|
+
"@vitest/runner",
|
|
1889
|
+
"vitest/suite",
|
|
1890
|
+
"vite-plus/test/suite",
|
|
1891
|
+
"vite-plus/test/plugins/runner"
|
|
1892
|
+
]);
|
|
1893
|
+
const EXPECT_SOURCES = /* @__PURE__ */ new Set(["@vitest/expect", "vite-plus/test/plugins/expect"]);
|
|
1894
|
+
const RUNNERS_SOURCES = /* @__PURE__ */ new Set(["vitest/runners", "vite-plus/test/runners"]);
|
|
1895
|
+
const REMOVED_SOURCES = /* @__PURE__ */ new Set([
|
|
1896
|
+
"vitest/internal/module-runner",
|
|
1897
|
+
"vite-plus/test/internal/module-runner",
|
|
1898
|
+
"@vitest/runner/utils",
|
|
1899
|
+
"@vitest/runner/types",
|
|
1900
|
+
"vite-plus/test/plugins/runner-utils",
|
|
1901
|
+
"vite-plus/test/plugins/runner-types"
|
|
1902
|
+
]);
|
|
1903
|
+
const LEGACY_API_SOURCES = /* @__PURE__ */ new Set([
|
|
1904
|
+
...RUNNER_SOURCES,
|
|
1905
|
+
...RUNNERS_SOURCES,
|
|
1906
|
+
...EXPECT_SOURCES,
|
|
1907
|
+
...REMOVED_SOURCES
|
|
1908
|
+
]);
|
|
1909
|
+
const RENDER_SOURCES = /* @__PURE__ */ new Set(["vitest-browser-vue", "vitest-browser-svelte"]);
|
|
1910
|
+
const REGISTRATIONS = /* @__PURE__ */ new Set([
|
|
1911
|
+
"test",
|
|
1912
|
+
"it",
|
|
1913
|
+
"describe",
|
|
1914
|
+
"suite"
|
|
1915
|
+
]);
|
|
1916
|
+
const ASYNC_CALLBACKS = /* @__PURE__ */ new Set([
|
|
1917
|
+
"test",
|
|
1918
|
+
"it",
|
|
1919
|
+
"beforeEach",
|
|
1920
|
+
"afterEach",
|
|
1921
|
+
"beforeAll",
|
|
1922
|
+
"afterAll"
|
|
1923
|
+
]);
|
|
1924
|
+
const DOM_GLOBALS = /* @__PURE__ */ new Set([
|
|
1925
|
+
"window",
|
|
1926
|
+
"document",
|
|
1927
|
+
"navigator",
|
|
1928
|
+
"location",
|
|
1929
|
+
"matchMedia",
|
|
1930
|
+
"innerWidth",
|
|
1931
|
+
"innerHeight",
|
|
1932
|
+
"screen",
|
|
1933
|
+
"Element",
|
|
1934
|
+
"HTMLElement",
|
|
1935
|
+
"Node",
|
|
1936
|
+
"Event",
|
|
1937
|
+
"MutationObserver",
|
|
1938
|
+
"ResizeObserver",
|
|
1939
|
+
"requestAnimationFrame",
|
|
1940
|
+
"cancelAnimationFrame",
|
|
1941
|
+
"getComputedStyle"
|
|
1942
|
+
]);
|
|
1943
|
+
const CHILD_PROCESS_SOURCES = /* @__PURE__ */ new Set(["node:child_process", "child_process"]);
|
|
1944
|
+
const ENVIRONMENT_SOURCES = /* @__PURE__ */ new Set([
|
|
1945
|
+
"vitest/environments",
|
|
1946
|
+
"vite-plus/test/environments",
|
|
1947
|
+
"vitest/runtime",
|
|
1948
|
+
"vite-plus/test/runtime"
|
|
1949
|
+
]);
|
|
1950
|
+
const ASSERTION_SOURCES = /* @__PURE__ */ new Set([
|
|
1951
|
+
"vitest",
|
|
1952
|
+
"vite-plus/test",
|
|
1953
|
+
...EXPECT_SOURCES
|
|
1954
|
+
]);
|
|
1955
|
+
/** Follow immutable bindings, including Map iteration variables, back to
|
|
1956
|
+
* populateGlobal. A map merely named `originals` is not a Vitest API. */
|
|
1957
|
+
function globalRestorationOrigin(editor, node, seen = /* @__PURE__ */ new Set()) {
|
|
1958
|
+
if (seen.has(node)) return;
|
|
1959
|
+
seen.add(node);
|
|
1960
|
+
const origin = (value) => globalRestorationOrigin(editor, value, seen);
|
|
1961
|
+
if (node.type === "CallExpression") {
|
|
1962
|
+
if (importedName(editor, node.callee, ENVIRONMENT_SOURCES) === "populateGlobal") return "result";
|
|
1963
|
+
if (memberName(node.callee) === "get" && node.callee.type === "MemberExpression" && origin(node.callee.object) === "originals") return "value";
|
|
1964
|
+
}
|
|
1965
|
+
if (node.type === "MemberExpression" && memberName(node) === "originals") return origin(node.object) === "result" ? "originals" : void 0;
|
|
1966
|
+
const binding = editor.binding(node);
|
|
1967
|
+
if (!binding?.constant) return;
|
|
1968
|
+
const declaration = binding.declaration;
|
|
1969
|
+
const parent = editor.parent(declaration);
|
|
1970
|
+
if (declaration.type === "VariableDeclarator" && declaration.init) return origin(declaration.init);
|
|
1971
|
+
if (declaration.type === "Property" && !declaration.computed && propertyName(declaration.key) === "originals" && parent?.type === "ObjectPattern") {
|
|
1972
|
+
const variable = editor.parent(parent);
|
|
1973
|
+
if (variable?.type === "VariableDeclarator" && variable.init) return origin(variable.init) === "result" ? "originals" : void 0;
|
|
1974
|
+
}
|
|
1975
|
+
if ((declaration.type === "ArrowFunctionExpression" || declaration.type === "FunctionExpression") && declaration.params[0]?.type === "Identifier" && editor.binding(declaration.params[0]) === binding && parent?.type === "CallExpression" && parent.arguments[0] === declaration && parent.callee.type === "MemberExpression" && memberName(parent.callee) === "forEach") return origin(parent.callee.object) === "originals" ? "value" : void 0;
|
|
1976
|
+
if (declaration.type === "ArrayPattern" && declaration.elements[1]?.type === "Identifier" && editor.binding(declaration.elements[1]) === binding && parent?.type === "VariableDeclarator") {
|
|
1977
|
+
const variables = editor.parent(parent);
|
|
1978
|
+
const loop = variables && editor.parent(variables);
|
|
1979
|
+
if (loop?.type === "ForOfStatement" && loop.left === variables) {
|
|
1980
|
+
const iterable = loop.right;
|
|
1981
|
+
return origin(iterable.type === "CallExpression" && iterable.callee.type === "MemberExpression" && memberName(iterable.callee) === "entries" ? iterable.callee.object : iterable) === "originals" ? "value" : void 0;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
function assertionTypeName(editor, node) {
|
|
1986
|
+
if (node.type === "TSQualifiedName") {
|
|
1987
|
+
if (node.left.type === "Identifier" && node.left.name === "jest" && node.right.name === "Matchers") return "jest.Matchers";
|
|
1988
|
+
const declaration = editor.binding(node.left)?.declaration;
|
|
1989
|
+
const imported = declaration && editor.parent(declaration);
|
|
1990
|
+
return declaration?.type === "ImportNamespaceSpecifier" && imported?.type === "ImportDeclaration" && ASSERTION_SOURCES.has(imported.source.value) ? node.right.name : void 0;
|
|
1991
|
+
}
|
|
1992
|
+
if (node.type !== "Identifier") return;
|
|
1993
|
+
const declaration = editor.binding(node)?.declaration;
|
|
1994
|
+
const imported = declaration && editor.parent(declaration);
|
|
1995
|
+
if (declaration?.type === "ImportSpecifier" && imported?.type === "ImportDeclaration") return ASSERTION_SOURCES.has(imported.source.value) ? propertyName(declaration.imported) : void 0;
|
|
1996
|
+
return node.name;
|
|
1997
|
+
}
|
|
1998
|
+
function isJestMatchers(editor, node) {
|
|
1999
|
+
let block = editor.parent(node);
|
|
2000
|
+
if (block?.type === "ExportNamedDeclaration") block = editor.parent(block);
|
|
2001
|
+
const namespace = block && editor.parent(block);
|
|
2002
|
+
return node.id.name === "Matchers" && namespace?.type === "TSModuleDeclaration" && namespace.id.type === "Identifier" && namespace.id.name === "jest";
|
|
2003
|
+
}
|
|
2004
|
+
function replacementImport(source, name) {
|
|
2005
|
+
if (RUNNER_SOURCES.has(source)) return RUNNER_SYMBOLS[name];
|
|
2006
|
+
if (RUNNERS_SOURCES.has(source) && name === "VitestTestRunner") return "TestRunner";
|
|
2007
|
+
if (EXPECT_SOURCES.has(source) && EXPECT_SYMBOLS.has(name)) return name;
|
|
2008
|
+
}
|
|
2009
|
+
function processApiName(editor, node) {
|
|
2010
|
+
const imported = importedName(editor, node, CHILD_PROCESS_SOURCES);
|
|
2011
|
+
if (imported) return imported;
|
|
2012
|
+
let name = memberName(node);
|
|
2013
|
+
let receiver = node.type === "MemberExpression" ? node.object : node;
|
|
2014
|
+
let declaration = editor.binding(receiver)?.declaration;
|
|
2015
|
+
const parent = declaration && editor.parent(declaration);
|
|
2016
|
+
if (declaration?.type === "ImportDefaultSpecifier" && parent?.type === "ImportDeclaration" && CHILD_PROCESS_SOURCES.has(parent.source.value)) return name;
|
|
2017
|
+
if (declaration?.type === "Property" && parent?.type === "ObjectPattern") {
|
|
2018
|
+
name = propertyName(declaration.key);
|
|
2019
|
+
declaration = editor.parent(parent);
|
|
2020
|
+
}
|
|
2021
|
+
if (declaration?.type === "VariableDeclarator" && declaration.init) receiver = declaration.init;
|
|
2022
|
+
return receiver.type === "CallExpression" && receiver.callee.type === "Identifier" && receiver.callee.name === "require" && !editor.binding(receiver.callee) && isString(receiver.arguments[0]) && CHILD_PROCESS_SOURCES.has(receiver.arguments[0].value) ? name : void 0;
|
|
2023
|
+
}
|
|
2024
|
+
/** A package-name string in plugin code or documentation is not a runner
|
|
2025
|
+
* dependency. Module references, in-source APIs, and bound process calls establish usage. */
|
|
2026
|
+
function hasVitestV5SourceUsage(file, source) {
|
|
2027
|
+
if (!/vitest|vite-plus|\bvp\b/.test(source)) return false;
|
|
2028
|
+
let editor;
|
|
2029
|
+
try {
|
|
2030
|
+
editor = new SourceEditor(file, source);
|
|
2031
|
+
} catch {
|
|
2032
|
+
return true;
|
|
2033
|
+
}
|
|
2034
|
+
let used = editor.comments.some(({ start, end }) => /^\/\/\/\s*<reference\s+types\s*=\s*['"](?:vitest(?:\/[^'"]*)?|@vitest\/[^'"]+|vite-plus\/test(?:\/[^'"]*)?)['"]/.test(source.slice(start, end)));
|
|
2035
|
+
const moduleReference = (node) => {
|
|
2036
|
+
if (isString(node) && /^(?:vitest(?:\/|$)|@vitest\/|vite-plus\/test(?:\/|$))/.test(node.value)) used = true;
|
|
2037
|
+
};
|
|
2038
|
+
editor.visit({
|
|
2039
|
+
MemberExpression(node) {
|
|
2040
|
+
if (isImportMetaVitest(node)) used = true;
|
|
2041
|
+
},
|
|
2042
|
+
ImportDeclaration: (node) => moduleReference(node.source),
|
|
2043
|
+
ExportNamedDeclaration: (node) => moduleReference(node.source),
|
|
2044
|
+
ExportAllDeclaration: (node) => moduleReference(node.source),
|
|
2045
|
+
ImportExpression: (node) => moduleReference(node.source),
|
|
2046
|
+
TSImportType: (node) => moduleReference(node.source),
|
|
2047
|
+
TSExternalModuleReference: (node) => moduleReference(node.expression),
|
|
2048
|
+
TSModuleDeclaration: (node) => moduleReference(node.id),
|
|
2049
|
+
CallExpression(node) {
|
|
2050
|
+
if (node.callee.type === "Identifier" && node.callee.name === "require" && !editor.binding(node.callee)) moduleReference(node.arguments[0]);
|
|
2051
|
+
const command = node.arguments[0];
|
|
2052
|
+
if ([
|
|
2053
|
+
"exec",
|
|
2054
|
+
"execSync",
|
|
2055
|
+
"execFile",
|
|
2056
|
+
"execFileSync",
|
|
2057
|
+
"spawn",
|
|
2058
|
+
"spawnSync"
|
|
2059
|
+
].includes(processApiName(editor, node.callee) ?? "") && isString(command)) {
|
|
2060
|
+
const args = node.arguments[1];
|
|
2061
|
+
const argv = args?.type === "ArrayExpression" && args.elements.every(isString) ? args.elements.map((arg) => arg.value).join(" ") : "";
|
|
2062
|
+
if (/\b(?:vitest|vp\s+test)(?:\s|$)/.test(`${command.value} ${argv}`)) used = true;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
return used;
|
|
2067
|
+
}
|
|
2068
|
+
function looksLikeConstructorMock(editor, node) {
|
|
2069
|
+
if (node.arguments.some((argument) => argument.type === "ClassExpression" || argument.type === "FunctionExpression")) return true;
|
|
2070
|
+
if (memberName(node.callee) === "spyOn" && isString(node.arguments[1]) && /^[A-Z]/.test(node.arguments[1].value)) return true;
|
|
2071
|
+
const parent = editor.parent(node);
|
|
2072
|
+
if (parent?.type === "VariableDeclarator" && parent.id.type === "Identifier") {
|
|
2073
|
+
const name = parent.id.name;
|
|
2074
|
+
const binding = editor.binding(parent.id);
|
|
2075
|
+
return /^[A-Z]/.test(name) || !!binding?.references.some((ref) => editor.parent(ref)?.type === "NewExpression");
|
|
2076
|
+
}
|
|
2077
|
+
return false;
|
|
2078
|
+
}
|
|
2079
|
+
function chain(node, isApi) {
|
|
2080
|
+
const members = [];
|
|
2081
|
+
const calls = [];
|
|
2082
|
+
while (node.type === "MemberExpression" || node.type === "CallExpression") {
|
|
2083
|
+
if (node.optional) break;
|
|
2084
|
+
if (isApi(node)) break;
|
|
2085
|
+
if (node.type === "MemberExpression") {
|
|
2086
|
+
const name = memberName(node);
|
|
2087
|
+
if (name === void 0) break;
|
|
2088
|
+
members.push(name);
|
|
2089
|
+
node = node.object;
|
|
2090
|
+
} else {
|
|
2091
|
+
calls.push(node);
|
|
2092
|
+
node = node.callee;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
return {
|
|
2096
|
+
root: node,
|
|
2097
|
+
members,
|
|
2098
|
+
calls
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
function migrateVitestV5Source(file, source, options) {
|
|
2102
|
+
const result = rewriteSource(file, source, options);
|
|
2103
|
+
if (options.reviewGlobals && !options.globals) {
|
|
2104
|
+
const withGlobals = rewriteSource(file, source, {
|
|
2105
|
+
...options,
|
|
2106
|
+
globals: true
|
|
2107
|
+
});
|
|
2108
|
+
if (result.content !== withGlobals.content || withGlobals.findings.some((finding) => !result.findings.some((existing) => existing.code === finding.code && existing.line === finding.line && existing.column === finding.column && existing.message === finding.message))) result.findings.push({
|
|
2109
|
+
file,
|
|
2110
|
+
line: 1,
|
|
2111
|
+
column: 1,
|
|
2112
|
+
severity: "review",
|
|
2113
|
+
code: "global-api-ownership",
|
|
2114
|
+
message: "Resolve the Vitest project ownership of this file before migrating its affected global APIs. Global API edits were not applied because config selection, file scope, or globals settings are unresolved or conflicting."
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
return result;
|
|
2118
|
+
}
|
|
2119
|
+
function rewriteSource(file, source, options) {
|
|
2120
|
+
const editor = new SourceEditor(file, source);
|
|
2121
|
+
const reviewV4 = options.preserveV4 || options.reviewV4;
|
|
2122
|
+
const benchmarks = migrateBenchmarks(editor, options.globals);
|
|
2123
|
+
const runnerAliases = [];
|
|
2124
|
+
const asyncFunctions = /* @__PURE__ */ new Set();
|
|
2125
|
+
function apiName(node) {
|
|
2126
|
+
return testApiName(editor, node, options.globals) ?? (importedName(editor, node, EXPECT_SOURCES) === "expect" ? "expect" : void 0);
|
|
2127
|
+
}
|
|
2128
|
+
function canAwait(node) {
|
|
2129
|
+
const fn = editor.functionParent(node);
|
|
2130
|
+
if (!fn) return false;
|
|
2131
|
+
if (fn.async) return true;
|
|
2132
|
+
if (fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression" || fn.generator || fn.returnType) return false;
|
|
2133
|
+
const parent = editor.parent(fn);
|
|
2134
|
+
if (parent?.type !== "CallExpression" || !parent.arguments.includes(fn)) return false;
|
|
2135
|
+
const { root, members } = chain(parent.callee, (node) => !!apiName(node));
|
|
2136
|
+
if (!ASYNC_CALLBACKS.has(apiName(root) ?? "") || members.includes("extend")) return false;
|
|
2137
|
+
if (!asyncFunctions.has(fn)) {
|
|
2138
|
+
editor.edit(fn.start, fn.start, "async ");
|
|
2139
|
+
asyncFunctions.add(fn);
|
|
2140
|
+
}
|
|
2141
|
+
return true;
|
|
2142
|
+
}
|
|
2143
|
+
function unsupported(node, source, symbol, typeOnly) {
|
|
2144
|
+
editor.report(node, "removed-api", `Migrate ${symbol} from ${source} manually; no reviewed root v5 replacement exists.`, typeOnly ? "review" : "block");
|
|
2145
|
+
}
|
|
2146
|
+
function dynamicImport(node, source) {
|
|
2147
|
+
if (LEGACY_API_SOURCES.has(source)) unsupported(node, source, "dynamic/CommonJS import", false);
|
|
2148
|
+
}
|
|
2149
|
+
editor.visit({
|
|
2150
|
+
ImportDeclaration(node) {
|
|
2151
|
+
const source = node.source.value;
|
|
2152
|
+
if (ROOT_TEST_SOURCES.has(source)) {
|
|
2153
|
+
for (const specifier of node.specifiers) if (specifier.type === "ImportSpecifier" && propertyName(specifier.imported) === "bench" && node.importKind !== "type" && specifier.importKind !== "type" && !benchmarks.imports.has(specifier)) editor.report(specifier, "benchmark-api", "Migrate these bench references manually: automatic migration requires direct calls with inline or locally resolved zero-argument callbacks and no benchmark options. Review wrappers, comparison groups, and escaped references.", "block");
|
|
2154
|
+
}
|
|
2155
|
+
if (source in vitest_v5_entry_points_default) {
|
|
2156
|
+
editor.replace(node.source, JSON.stringify(vitest_v5_entry_points_default[source]));
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
if (!LEGACY_API_SOURCES.has(source)) return;
|
|
2160
|
+
const imports = [];
|
|
2161
|
+
const constants = [];
|
|
2162
|
+
const remaining = [];
|
|
2163
|
+
let runnerName;
|
|
2164
|
+
for (const specifier of node.specifiers) {
|
|
2165
|
+
const typeOnly = node.importKind === "type" || specifier.type === "ImportSpecifier" && specifier.importKind === "type";
|
|
2166
|
+
const name = specifier.type === "ImportSpecifier" ? propertyName(specifier.imported) : "*";
|
|
2167
|
+
const target = replacementImport(source, name);
|
|
2168
|
+
if (target) imports.push(`${typeOnly ? "type " : ""}${target}${specifier.local.name === target ? "" : ` as ${specifier.local.name}`}`);
|
|
2169
|
+
else if (RUNNER_SOURCES.has(source) && RUNNER_METHODS[name] && !typeOnly) {
|
|
2170
|
+
if (!runnerName) {
|
|
2171
|
+
runnerName = editor.uniqueName("VitestTestRunner");
|
|
2172
|
+
imports.push(`TestRunner as ${runnerName}`);
|
|
2173
|
+
}
|
|
2174
|
+
constants.push(`const ${specifier.local.name} = ${runnerName}.${RUNNER_METHODS[name]};`);
|
|
2175
|
+
} else {
|
|
2176
|
+
unsupported(specifier, source, name, typeOnly);
|
|
2177
|
+
remaining.push(editor.text(specifier));
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (!node.specifiers.length) unsupported(node, source, "side-effect import", false);
|
|
2181
|
+
if (imports.length) {
|
|
2182
|
+
if (node.specifiers.some((specifier) => specifier.type !== "ImportSpecifier")) return;
|
|
2183
|
+
const kept = remaining.length ? `import ${node.importKind === "type" ? "type " : ""}{ ${remaining.join(", ")} } from ${JSON.stringify(source)};\n` : "";
|
|
2184
|
+
editor.replace(node, `${kept}import { ${imports.join(", ")} } from 'vite-plus/test';`);
|
|
2185
|
+
runnerAliases.push(...constants);
|
|
2186
|
+
}
|
|
2187
|
+
},
|
|
2188
|
+
ExportNamedDeclaration(node) {
|
|
2189
|
+
const source = node.source?.value;
|
|
2190
|
+
if (!source) return;
|
|
2191
|
+
if (source in vitest_v5_entry_points_default) editor.replace(node.source, JSON.stringify(vitest_v5_entry_points_default[source]));
|
|
2192
|
+
else if (LEGACY_API_SOURCES.has(source)) for (const specifier of node.specifiers) unsupported(specifier, source, "re-export", node.exportKind === "type" || specifier.type === "ExportSpecifier" && specifier.exportKind === "type");
|
|
2193
|
+
},
|
|
2194
|
+
ExportAllDeclaration(node) {
|
|
2195
|
+
const source = node.source.value;
|
|
2196
|
+
if (LEGACY_API_SOURCES.has(source)) unsupported(node, source, "export *", node.exportKind === "type");
|
|
2197
|
+
},
|
|
2198
|
+
CallExpression(node) {
|
|
2199
|
+
if (node.optional) return;
|
|
2200
|
+
const parent = editor.parent(node);
|
|
2201
|
+
const { root, members } = chain(node.callee, (node) => !!apiName(node));
|
|
2202
|
+
const name = apiName(root);
|
|
2203
|
+
if (name === "bench" && (root.type !== "Identifier" || !editor.binding(root)) && !benchmarks.calls.has(node)) editor.report(node, "benchmark-api", "Migrate this bench call manually: automatic migration requires a direct call with an inline or locally resolved zero-argument callback and no benchmark options.", "block");
|
|
2204
|
+
if (REGISTRATIONS.has(name ?? "")) {
|
|
2205
|
+
const seqMember = node.callee.type === "MemberExpression" && memberName(node.callee) === "sequential" ? node.callee : void 0;
|
|
2206
|
+
const opts = node.arguments[1];
|
|
2207
|
+
const sequentialOption = staticObject(opts) ? objectProperty(opts, "sequential") : void 0;
|
|
2208
|
+
let unresolvedSequential = members.includes("sequential");
|
|
2209
|
+
if (seqMember && node.arguments.length >= 2) {
|
|
2210
|
+
if (staticObject(opts) && !objectProperty(opts, "concurrent") && (!sequentialOption || isBoolean(sequentialOption.value) && sequentialOption.value.value)) {
|
|
2211
|
+
editor.edit(seqMember.object.end, seqMember.end, "");
|
|
2212
|
+
if (!sequentialOption) editor.add(opts, "concurrent", "false");
|
|
2213
|
+
unresolvedSequential = false;
|
|
2214
|
+
} else if ((opts?.type === "ArrowFunctionExpression" || opts?.type === "FunctionExpression") && (node.arguments.length === 2 || node.arguments.length === 3 && node.arguments[2].type === "Literal" && typeof node.arguments[2].value === "number" && /^\s*,\s*$/.test(editor.source.slice(opts.end, node.arguments[2].start)))) {
|
|
2215
|
+
const timeout = node.arguments[2];
|
|
2216
|
+
editor.edit(seqMember.object.end, seqMember.end, "");
|
|
2217
|
+
editor.edit(opts.start, opts.start, `{ concurrent: false${timeout ? `, timeout: ${editor.text(timeout)}` : ""} }, `);
|
|
2218
|
+
if (timeout) editor.edit(opts.end, timeout.end, "");
|
|
2219
|
+
unresolvedSequential = false;
|
|
2220
|
+
} else editor.report(node, "sequential-api", "Replace sequential with concurrent: false after resolving the options and callback.");
|
|
2221
|
+
} else if (members.includes("sequential")) editor.report(node, "sequential-api", "Review the sequential modifier chain and replace it with concurrent: false.");
|
|
2222
|
+
for (const argument of node.arguments.slice(1)) {
|
|
2223
|
+
if (unresolvedSequential || !staticObject(argument)) continue;
|
|
2224
|
+
const sequential = objectProperty(argument, "sequential");
|
|
2225
|
+
if (!sequential) continue;
|
|
2226
|
+
if (isBoolean(sequential.value) && sequential.value.value && !objectProperty(argument, "concurrent")) {
|
|
2227
|
+
editor.replace(sequential.key, "concurrent");
|
|
2228
|
+
editor.replace(sequential.value, "false");
|
|
2229
|
+
} else editor.report(sequential, "sequential-api", "Resolve sequential/concurrent options explicitly before removing sequential.");
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
if (name === "vi" || name === "vitest") {
|
|
2233
|
+
const method = memberName(node.callee);
|
|
2234
|
+
if ([
|
|
2235
|
+
"mock",
|
|
2236
|
+
"unmock",
|
|
2237
|
+
"hoisted"
|
|
2238
|
+
].includes(method ?? "")) {
|
|
2239
|
+
let statement = parent;
|
|
2240
|
+
while (statement && !statement.type.endsWith("Statement") && statement.type !== "VariableDeclaration" && statement.type !== "Program") statement = editor.parent(statement);
|
|
2241
|
+
if (editor.functionParent(node) || !statement || editor.parent(statement)?.type !== "Program") editor.report(node, "nested-hoisted-mock", "Move this hoisted mock to the top level after reviewing captured scope.");
|
|
2242
|
+
}
|
|
2243
|
+
if (reviewV4 && (options.browser || options.browser === void 0 && options.browserPossible) && method === "mock" && node.arguments.length === 1) editor.report(node, "browser-automock", "Browser automocks now keep mock defaults; choose { spy: true } if real implementations are required.");
|
|
2244
|
+
if (reviewV4 && (method === "fn" || method === "spyOn") && looksLikeConstructorMock(editor, node)) editor.report(node, "class-mock", "If this mock replaces a constructor, review its prototype, methods, and instanceof behavior.");
|
|
2245
|
+
if (reviewV4 && method === "setSystemTime" && (options.temporalPolyfill || /\bTemporal\b/.test(source))) editor.report(node, "temporal-system-time", "vi.setSystemTime now changes Temporal even without fake timers; toNotFake does not preserve this behavior.");
|
|
2246
|
+
}
|
|
2247
|
+
if (reviewV4 && memberName(node.callee) === "mockImplementation" && looksLikeConstructorMock(editor, node)) editor.report(node, "class-mock", "Review constructor mock implementations and their inherited prototypes.");
|
|
2248
|
+
if (name === "expect") {
|
|
2249
|
+
const matcher = memberName(node.callee);
|
|
2250
|
+
const first = node.arguments[0];
|
|
2251
|
+
if (options.preserveV4 && ["toThrow", "toThrowError"].includes(matcher ?? "") && isString(first) && first.value === "") editor.replace(first, "/^$/");
|
|
2252
|
+
if (reviewV4 && matcher === "toHaveTextContent") {
|
|
2253
|
+
const browserAssertion = members.includes("element") || options.browser === true;
|
|
2254
|
+
const literal = isString(first) || isRegExp(first);
|
|
2255
|
+
if (options.preserveV4 && browserAssertion && literal) {
|
|
2256
|
+
const callee = node.callee;
|
|
2257
|
+
editor.replace(callee.property, callee.computed ? JSON.stringify("toMatchTextContent") : "toMatchTextContent");
|
|
2258
|
+
} else if (browserAssertion && !literal) editor.report(node, "text-content", "Choose toMatchTextContent for v4 partial/regex matching after resolving the expected value.");
|
|
2259
|
+
else if (options.browser === void 0 && options.browserPossible) editor.report(node, "text-content-project", options.ownershipReason ? `${options.ownershipReason} Browser assertions need toMatchTextContent for v4 partial matching; keep Node jest-dom assertions unchanged.` : "Resolve this assertion's test project: browser assertions need toMatchTextContent for v4 partial matching; keep Node jest-dom assertions unchanged.");
|
|
2260
|
+
}
|
|
2261
|
+
if (reviewV4 && members.includes("poll") && matcher !== "poll") editor.report(node, "poll-timeout", "Review the configured expect.poll timeout; v5 rejects assertions that finish after it.");
|
|
2262
|
+
const asynchronous = members.some((member) => [
|
|
2263
|
+
"resolves",
|
|
2264
|
+
"rejects",
|
|
2265
|
+
"poll",
|
|
2266
|
+
"element",
|
|
2267
|
+
"toMatchFileSnapshot",
|
|
2268
|
+
"toMatchScreenshot"
|
|
2269
|
+
].includes(member));
|
|
2270
|
+
if (asynchronous && parent?.type === "ExpressionStatement") {
|
|
2271
|
+
if (canAwait(node)) editor.edit(node.start, node.start, "await ");
|
|
2272
|
+
else editor.report(node, "unawaited-assertion", "Await or return this asynchronous assertion in an async-compatible function.");
|
|
2273
|
+
} else if (asynchronous && parent?.type !== "MemberExpression" && parent?.type !== "AwaitExpression" && parent?.type !== "ReturnStatement") editor.report(node, "unawaited-assertion", "Check that the result of this asynchronous assertion is awaited or returned.");
|
|
2274
|
+
}
|
|
2275
|
+
if (importedName(editor, node.callee, RENDER_SOURCES) === "render" && parent?.type !== "AwaitExpression" && parent?.type !== "ReturnStatement") {
|
|
2276
|
+
if (canAwait(node)) {
|
|
2277
|
+
editor.edit(node.start, node.start, "(await ");
|
|
2278
|
+
editor.edit(node.end, node.end, ")");
|
|
2279
|
+
} else editor.report(node, "async-render", "Await render from vitest-browser-vue/svelte after making the enclosing contract async-compatible.");
|
|
2280
|
+
}
|
|
2281
|
+
if (importedName(editor, node.callee, NODE_SOURCES) === "resolveConfig" && options.preserveV4) {
|
|
2282
|
+
const declaration = parent && editor.parent(parent);
|
|
2283
|
+
if (parent?.type === "AwaitExpression" && declaration?.type === "VariableDeclarator" && declaration.id.type === "ObjectPattern") {
|
|
2284
|
+
const props = declaration.id.properties;
|
|
2285
|
+
if (props.every((prop) => prop.type === "Property" && !prop.computed && prop.value.type === "Identifier" && ["viteConfig", "vitestConfig"].includes(propertyName(prop.key) ?? ""))) {
|
|
2286
|
+
const vite = props.find((prop) => prop.type === "Property" && propertyName(prop.key) === "viteConfig");
|
|
2287
|
+
const vitest = props.find((prop) => prop.type === "Property" && propertyName(prop.key) === "vitestConfig");
|
|
2288
|
+
const local = vite ? editor.text(vite.value) : editor.uniqueName("viteConfig");
|
|
2289
|
+
editor.replace(declaration.id, local);
|
|
2290
|
+
if (vitest) editor.edit(declaration.end, declaration.end, `, ${editor.text(vitest.value)} = ${local}.test`);
|
|
2291
|
+
} else editor.report(declaration, "resolve-config", "Replace resolveConfig pair destructuring with the Vite config return value and its .test property.");
|
|
2292
|
+
} else editor.report(node, "resolve-config", "Review resolveConfig consumers; the v5 return value is the Vite config with Vitest options under .test.");
|
|
2293
|
+
}
|
|
2294
|
+
if (memberName(node.callee) === "collect") {
|
|
2295
|
+
const object = node.callee.object;
|
|
2296
|
+
const binding = editor.binding(object);
|
|
2297
|
+
let initializer = binding?.declaration.type === "VariableDeclarator" ? binding.declaration.init : void 0;
|
|
2298
|
+
if (initializer?.type === "AwaitExpression") initializer = initializer.argument;
|
|
2299
|
+
const vitestRunner = initializer?.type === "CallExpression" && ["createVitest", "startVitest"].includes(importedName(editor, initializer.callee, NODE_SOURCES) ?? "");
|
|
2300
|
+
const known = vitestRunner && binding?.constant;
|
|
2301
|
+
const opts = node.arguments[1];
|
|
2302
|
+
if (known && options.preserveV4 && node.arguments.length <= 2 && !node.arguments.some((argument) => argument.type === "SpreadElement") && (!opts || staticObject(opts))) {
|
|
2303
|
+
if (staticObject(opts)) editor.add(opts, "staticParse", "false");
|
|
2304
|
+
else editor.edit(node.end - 1, node.end - 1, `${node.arguments.length ? ", " : "undefined, "}{ staticParse: false }`);
|
|
2305
|
+
} else if (reviewV4 && vitestRunner && (!known || node.arguments.some((argument) => argument.type === "SpreadElement") || opts && !staticObject(opts))) editor.report(node, "static-collect", "If this is Vitest.collect(), review staticParse and set it to false to retain runtime collection.");
|
|
2306
|
+
}
|
|
2307
|
+
if (node.callee.type === "Identifier" && node.callee.name === "require" && !editor.binding(node.callee) && isString(node.arguments[0])) dynamicImport(node, node.arguments[0].value);
|
|
2308
|
+
},
|
|
2309
|
+
ImportExpression(node) {
|
|
2310
|
+
if (isString(node.source)) dynamicImport(node, node.source.value);
|
|
2311
|
+
},
|
|
2312
|
+
MemberExpression(node) {
|
|
2313
|
+
if (reviewV4 && ["VITEST_POOL_ID", "VITEST_WORKER_ID"].includes(memberName(node) ?? "")) editor.report(node, "worker-id", "Review worker/pool ID arithmetic and indexing: IDs now start at 1, not 0.");
|
|
2314
|
+
},
|
|
2315
|
+
VariableDeclarator(node) {
|
|
2316
|
+
if (reviewV4 && node.id.type === "ObjectPattern") {
|
|
2317
|
+
for (const prop of node.id.properties) if (prop.type === "Property" && ["VITEST_POOL_ID", "VITEST_WORKER_ID"].includes(propertyName(prop.key) ?? "")) editor.report(prop, "worker-id", "Review worker/pool ID arithmetic and indexing: IDs now start at 1, not 0.");
|
|
2318
|
+
}
|
|
2319
|
+
},
|
|
2320
|
+
AssignmentExpression(node) {
|
|
2321
|
+
if (reviewV4 && globalRestorationOrigin(editor, node.right) === "value") editor.report(node, "global-descriptors", "populateGlobal().originals stores descriptors; restore them with Object.defineProperty, not assignment.");
|
|
2322
|
+
const left = node.left;
|
|
2323
|
+
if (reviewV4 && (left.type === "MemberExpression" && left.object.type === "Identifier" && !editor.binding(left.object) && [
|
|
2324
|
+
"window",
|
|
2325
|
+
"globalThis",
|
|
2326
|
+
"global"
|
|
2327
|
+
].includes(editor.text(left.object)) && DOM_GLOBALS.has(memberName(left) ?? "") || left.type === "Identifier" && DOM_GLOBALS.has(left.name) && !editor.binding(left))) editor.report(node, "dom-global", "In jsdom/happy-dom, global assignment also updates the window; review this DOM override.");
|
|
2328
|
+
},
|
|
2329
|
+
TSInterfaceDeclaration(node) {
|
|
2330
|
+
if (reviewV4 && (isJestMatchers(editor, node) || ["Assertion", "Matchers"].includes(node.id.name) && (node.typeParameters?.params.length ?? 0) < 2)) editor.report(node, "assertion-types", "Update custom matcher declarations for the v5 return and received type parameters; review jest.Matchers augmentations.");
|
|
2331
|
+
},
|
|
2332
|
+
TSTypeReference(node) {
|
|
2333
|
+
const name = assertionTypeName(editor, node.typeName);
|
|
2334
|
+
if (reviewV4 && (name === "jest.Matchers" || ["Assertion", "Matchers"].includes(name ?? "") && (node.typeArguments?.params.length ?? 0) < 2)) editor.report(node, "assertion-types", "Review old assertion generics; v5 includes return and received types.");
|
|
2335
|
+
},
|
|
2336
|
+
TSImportType(node) {
|
|
2337
|
+
const source = node.source.value;
|
|
2338
|
+
if (LEGACY_API_SOURCES.has(source)) unsupported(node, source, "type import", true);
|
|
2339
|
+
},
|
|
2340
|
+
Literal(node) {
|
|
2341
|
+
if (!isString(node)) return;
|
|
2342
|
+
const value = node.value;
|
|
2343
|
+
if (/\/__vitest__\//.test(value) && !/[?&]token=/.test(value)) editor.report(node, "ui-token", "Use the authenticated UI URL printed by Vitest, including its token.");
|
|
2344
|
+
if (/\/__vitest_test__\//.test(value) && !/[?&]sessionId=/.test(value)) editor.report(node, "browser-session", "Use the browser orchestrator URL opened by Vitest, including its sessionId.");
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
if (runnerAliases.length) {
|
|
2348
|
+
const firstStatement = editor.ast.body.find((node) => node.type !== "ImportDeclaration" && !(node.type === "ExpressionStatement" && node.directive));
|
|
2349
|
+
const offset = firstStatement?.start ?? source.length;
|
|
2350
|
+
editor.edit(offset, offset, `${firstStatement ? "" : "\n"}${runnerAliases.join("\n")}\n`);
|
|
2351
|
+
}
|
|
2352
|
+
const result = editor.finish();
|
|
2353
|
+
const benchmark = benchmarks.imports.values().next().value ?? benchmarks.bindings.values().next().value ?? benchmarks.calls.values().next().value;
|
|
2354
|
+
if (benchmark && result.content === source) editor.report(benchmark, "benchmark-api", "The benchmark rewrite was discarded. Migrate the remaining legacy bench API manually before upgrading to Vitest v5.", "block");
|
|
2355
|
+
return result;
|
|
2356
|
+
}
|
|
2357
|
+
//#endregion
|
|
2358
|
+
//#region src/migration/vitest-v5/webdriverio.ts
|
|
2359
|
+
const MIN_VERSION = "5.0.0";
|
|
2360
|
+
const DEFAULT_SPEC = "^5.0.0";
|
|
2361
|
+
const REGISTRY_ALIAS = `npm:${WEBDRIVERIO_PROVIDER}@`;
|
|
2362
|
+
const INSTALL_FIELDS = [
|
|
2363
|
+
"dependencies",
|
|
2364
|
+
"devDependencies",
|
|
2365
|
+
"optionalDependencies"
|
|
2366
|
+
];
|
|
2367
|
+
/** Narrow mixed ranges without discarding the user's newer allowed versions. */
|
|
2368
|
+
function webdriverioMigrationSpec(spec) {
|
|
2369
|
+
if (spec.startsWith(REGISTRY_ALIAS)) {
|
|
2370
|
+
const migrated = webdriverioMigrationSpec(spec.slice(REGISTRY_ALIAS.length));
|
|
2371
|
+
return migrated === void 0 ? void 0 : `${REGISTRY_ALIAS}${migrated}`;
|
|
2372
|
+
}
|
|
2373
|
+
if (!import_semver.default.validRange(spec)) return;
|
|
2374
|
+
const branches = new import_semver.default.Range(spec).set.map((set) => set.map((item) => item.value).join(" "));
|
|
2375
|
+
if (branches.every((branch) => {
|
|
2376
|
+
const min = import_semver.default.minVersion(branch);
|
|
2377
|
+
return min && import_semver.default.gte(min, MIN_VERSION);
|
|
2378
|
+
})) return spec;
|
|
2379
|
+
const narrowed = branches.map((branch) => `${branch} >=${MIN_VERSION}`.trim()).filter((branch) => import_semver.default.minVersion(branch) !== null);
|
|
2380
|
+
return narrowed.length ? narrowed.join(" || ") : DEFAULT_SPEC;
|
|
2381
|
+
}
|
|
2382
|
+
function object(value) {
|
|
2383
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2384
|
+
}
|
|
2385
|
+
/** Compose dependency edits into the versioned preflight's in-memory sources.
|
|
2386
|
+
* No writes or network resolution: unresolved custom specs block before install. */
|
|
2387
|
+
function migrateWebdriverioDependencies(workspace, sources, inputs) {
|
|
2388
|
+
const findings = [];
|
|
2389
|
+
const rootManifest = path.join(workspace.rootDir, "package.json");
|
|
2390
|
+
const manifests = [rootManifest, ...(workspace.packages ?? []).map((pkg) => path.resolve(workspace.rootDir, pkg.path, "package.json"))];
|
|
2391
|
+
const read = (file) => {
|
|
2392
|
+
const source = sources.get(file);
|
|
2393
|
+
if (source === void 0) return {};
|
|
2394
|
+
return object(file.endsWith(".json") ? parse(source) : (0, import_dist.parseDocument)(source).toJS());
|
|
2395
|
+
};
|
|
2396
|
+
const get = (file, keys) => keys.reduce((value, key) => object(value)[key], read(file));
|
|
2397
|
+
const set = (file, keys, value) => {
|
|
2398
|
+
if (get(file, keys) === value) return;
|
|
2399
|
+
const source = sources.get(file);
|
|
2400
|
+
if (file.endsWith(".json")) sources.set(file, applyEdits(source, modify(source, keys, value, { formattingOptions: {
|
|
2401
|
+
insertSpaces: true,
|
|
2402
|
+
tabSize: 2
|
|
2403
|
+
} })));
|
|
2404
|
+
else {
|
|
2405
|
+
const doc = (0, import_dist.parseDocument)(source);
|
|
2406
|
+
if (value === void 0) doc.deleteIn(keys);
|
|
2407
|
+
else doc.setIn(keys, value);
|
|
2408
|
+
sources.set(file, doc.toString());
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
const block = (file, label, spec) => {
|
|
2412
|
+
findings.push({
|
|
2413
|
+
file,
|
|
2414
|
+
line: 1,
|
|
2415
|
+
column: 1,
|
|
2416
|
+
code: "browser-provider",
|
|
2417
|
+
severity: "block",
|
|
2418
|
+
message: `Cannot ensure ${label} (${spec}) uses ${WEBDRIVERIO_PROVIDER} >=${MIN_VERSION}. Select a compatible provider version, then re-run migration.`
|
|
2419
|
+
});
|
|
2420
|
+
};
|
|
2421
|
+
const catalogEntry = (spec, name) => {
|
|
2422
|
+
const catalog = spec.slice(8);
|
|
2423
|
+
const keys = catalog && catalog !== "default" ? [
|
|
2424
|
+
"catalogs",
|
|
2425
|
+
catalog,
|
|
2426
|
+
name
|
|
2427
|
+
] : ["catalog", name];
|
|
2428
|
+
if (workspace.packageManager === PackageManager.bun) {
|
|
2429
|
+
for (const prefix of [["workspaces"], []]) if (typeof get(rootManifest, [...prefix, ...keys]) === "string") return {
|
|
2430
|
+
file: rootManifest,
|
|
2431
|
+
keys: [...prefix, ...keys]
|
|
2432
|
+
};
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
const file = path.join(workspace.rootDir, workspace.packageManager === PackageManager.yarn ? ".yarnrc.yml" : "pnpm-workspace.yaml");
|
|
2436
|
+
return typeof get(file, keys) === "string" ? {
|
|
2437
|
+
file,
|
|
2438
|
+
keys
|
|
2439
|
+
} : void 0;
|
|
2440
|
+
};
|
|
2441
|
+
const update = (file, keys, directory, seen = /* @__PURE__ */ new Set()) => {
|
|
2442
|
+
const spec = get(file, keys);
|
|
2443
|
+
if (typeof spec !== "string") return;
|
|
2444
|
+
const location = `${file}:${keys.join(".")}`;
|
|
2445
|
+
if (seen.has(location)) {
|
|
2446
|
+
block(file, keys.join("."), spec);
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
seen.add(location);
|
|
2450
|
+
if (spec.startsWith("catalog:")) {
|
|
2451
|
+
const entry = catalogEntry(spec, WEBDRIVERIO_PROVIDER);
|
|
2452
|
+
if (entry) update(entry.file, entry.keys, directory, seen);
|
|
2453
|
+
else block(file, keys.join("."), spec);
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
if (spec.startsWith("$")) {
|
|
2457
|
+
const name = spec.slice(1);
|
|
2458
|
+
const field = INSTALL_FIELDS.find((field) => typeof get(rootManifest, [field, name]) === "string");
|
|
2459
|
+
if (field && name === "@vitest/browser-webdriverio") update(rootManifest, [field, name], workspace.rootDir, seen);
|
|
2460
|
+
else block(file, keys.join("."), spec);
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
const migrated = webdriverioMigrationSpec(spec);
|
|
2464
|
+
if (migrated !== void 0) {
|
|
2465
|
+
set(file, keys, migrated);
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
if (/^(?:file:|link:|workspace:)/.test(spec)) {
|
|
2469
|
+
const local = spec.startsWith("workspace:") ? manifests.find((manifest) => read(manifest).name === WEBDRIVERIO_PROVIDER) : path.resolve(directory, spec.slice(spec.indexOf(":") + 1), "package.json");
|
|
2470
|
+
if (local && fs.existsSync(local)) {
|
|
2471
|
+
const text = fs.readFileSync(local, "utf8");
|
|
2472
|
+
inputs.set(local, text);
|
|
2473
|
+
const pkg = object(parse(text));
|
|
2474
|
+
if (pkg.name === "@vitest/browser-webdriverio" && typeof pkg.version === "string" && import_semver.default.valid(pkg.version) && import_semver.default.gte(pkg.version, MIN_VERSION)) return;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
block(file, keys.join("."), spec);
|
|
2478
|
+
};
|
|
2479
|
+
const selected = manifests.filter((file) => {
|
|
2480
|
+
const pkg = read(file);
|
|
2481
|
+
return [...INSTALL_FIELDS, "peerDependencies"].some((field) => typeof object(pkg[field])["@vitest/browser-webdriverio"] === "string") || usesWebdriverioProvider(path.dirname(file));
|
|
2482
|
+
});
|
|
2483
|
+
if (!selected.length) return findings;
|
|
2484
|
+
for (const file of selected) {
|
|
2485
|
+
const directory = path.dirname(file);
|
|
2486
|
+
let installed = false;
|
|
2487
|
+
for (const field of INSTALL_FIELDS) if (typeof get(file, [field, "@vitest/browser-webdriverio"]) === "string") {
|
|
2488
|
+
update(file, [field, WEBDRIVERIO_PROVIDER], directory);
|
|
2489
|
+
installed = true;
|
|
2490
|
+
}
|
|
2491
|
+
if (!installed) {
|
|
2492
|
+
const peer = get(file, ["peerDependencies", WEBDRIVERIO_PROVIDER]);
|
|
2493
|
+
const inherited = file !== rootManifest ? INSTALL_FIELDS.map((field) => get(rootManifest, [field, WEBDRIVERIO_PROVIDER])).find((spec) => typeof spec === "string") : void 0;
|
|
2494
|
+
let spec = typeof peer === "string" ? peer : inherited;
|
|
2495
|
+
if (spec?.startsWith("catalog:") && typeof peer === "string") {
|
|
2496
|
+
const entry = catalogEntry(spec, WEBDRIVERIO_PROVIDER);
|
|
2497
|
+
const resolved = entry && get(entry.file, entry.keys);
|
|
2498
|
+
spec = typeof resolved === "string" ? resolved : spec;
|
|
2499
|
+
}
|
|
2500
|
+
set(file, ["devDependencies", WEBDRIVERIO_PROVIDER], spec ?? DEFAULT_SPEC);
|
|
2501
|
+
update(file, ["devDependencies", WEBDRIVERIO_PROVIDER], directory);
|
|
2502
|
+
}
|
|
2503
|
+
if (!INSTALL_FIELDS.some((field) => typeof get(file, [field, "webdriverio"]) === "string")) {
|
|
2504
|
+
let peer = "*";
|
|
2505
|
+
const pkg = read(file);
|
|
2506
|
+
for (const name of [
|
|
2507
|
+
"webdriverio",
|
|
2508
|
+
"@wdio/cli",
|
|
2509
|
+
"@wdio/globals"
|
|
2510
|
+
]) {
|
|
2511
|
+
const spec = [...INSTALL_FIELDS, "peerDependencies"].map((field) => object(pkg[field])[name]).find((spec) => typeof spec === "string");
|
|
2512
|
+
if (!spec) continue;
|
|
2513
|
+
const entry = spec.startsWith("catalog:") ? catalogEntry(spec, name) : void 0;
|
|
2514
|
+
const resolved = entry ? get(entry.file, entry.keys) : spec;
|
|
2515
|
+
if (typeof resolved === "string" && import_semver.default.validRange(resolved)) {
|
|
2516
|
+
peer = name === "webdriverio" ? spec : resolved;
|
|
2517
|
+
break;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
const metadata = peer === "*" ? detectPackageMetadata(directory, "webdriverio") : void 0;
|
|
2521
|
+
if (metadata && import_semver.default.valid(metadata.version)) {
|
|
2522
|
+
const metadataFile = path.join(metadata.path, "package.json");
|
|
2523
|
+
inputs.set(metadataFile, fs.readFileSync(metadataFile, "utf8"));
|
|
2524
|
+
peer = `^${metadata.version}`;
|
|
2525
|
+
}
|
|
2526
|
+
set(file, ["devDependencies", "webdriverio"], peer);
|
|
2527
|
+
}
|
|
2528
|
+
if (workspace.packageManager === PackageManager.npm && VITE_PLUS_OVERRIDE_PACKAGES.vite && !INSTALL_FIELDS.some((field) => get(file, [field, "vite"]) !== void 0)) set(file, ["devDependencies", "vite"], VITE_PLUS_OVERRIDE_PACKAGES.vite);
|
|
2529
|
+
}
|
|
2530
|
+
const projectNames = selected.map((file) => read(file).name).filter((name) => typeof name === "string");
|
|
2531
|
+
function matchesProject(parents) {
|
|
2532
|
+
const concrete = parents.filter((parent) => parent !== "**");
|
|
2533
|
+
if (concrete.length === 0) return true;
|
|
2534
|
+
if (concrete.length !== 1) return false;
|
|
2535
|
+
const pattern = concrete[0].split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
2536
|
+
const expression = new RegExp(`^${pattern}$`);
|
|
2537
|
+
return projectNames.some((name) => expression.test(name));
|
|
2538
|
+
}
|
|
2539
|
+
const overrideMap = (file, keys, parents = []) => {
|
|
2540
|
+
for (const [key, value] of Object.entries(object(get(file, keys)))) {
|
|
2541
|
+
const target = extractOverrideTargetName(key);
|
|
2542
|
+
const ancestors = [...parents, ...extractOverrideParentSegments(key) ?? []];
|
|
2543
|
+
const entry = [...keys, key];
|
|
2544
|
+
if (target === "@vitest/browser-webdriverio" && matchesProject(ancestors)) {
|
|
2545
|
+
const versionPath = typeof value === "string" ? entry : [...entry, "."];
|
|
2546
|
+
const before = get(file, versionPath);
|
|
2547
|
+
const catalog = typeof before === "string" && before.startsWith("catalog:") ? catalogEntry(before, WEBDRIVERIO_PROVIDER) : void 0;
|
|
2548
|
+
const pin = catalog ? get(catalog.file, catalog.keys) : before;
|
|
2549
|
+
const pinnedRange = typeof pin === "string" ? import_semver.default.validRange(pin.startsWith(REGISTRY_ALIAS) ? pin.slice(REGISTRY_ALIAS.length) : pin) : null;
|
|
2550
|
+
if (pinnedRange && !import_semver.default.intersects(pinnedRange, `>=${MIN_VERSION}`)) {
|
|
2551
|
+
set(file, versionPath, void 0);
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
update(file, versionPath, workspace.rootDir);
|
|
2555
|
+
if (workspace.packageManager === PackageManager.npm && keys[0] === "overrides" && ancestors.length === 0 && selected.includes(rootManifest) && typeof before === "string") {
|
|
2556
|
+
const after = get(file, versionPath);
|
|
2557
|
+
const field = INSTALL_FIELDS.find((field) => get(rootManifest, [field, WEBDRIVERIO_PROVIDER]) !== void 0);
|
|
2558
|
+
if (typeof after === "string" && !after.startsWith("$") && webdriverioMigrationSpec(after) === after) {
|
|
2559
|
+
const direct = get(rootManifest, [field, WEBDRIVERIO_PROVIDER]);
|
|
2560
|
+
if (typeof direct === "string" && webdriverioMigrationSpec(before) !== before && webdriverioMigrationSpec(direct) === direct) set(file, versionPath, `$${WEBDRIVERIO_PROVIDER}`);
|
|
2561
|
+
else if (direct === after) continue;
|
|
2562
|
+
else if (typeof direct === "string" && import_semver.default.validRange(direct) && import_semver.default.validRange(after) && import_semver.default.intersects(direct, after)) {
|
|
2563
|
+
set(rootManifest, [field, WEBDRIVERIO_PROVIDER], after);
|
|
2564
|
+
set(file, versionPath, `$${WEBDRIVERIO_PROVIDER}`);
|
|
2565
|
+
} else block(file, versionPath.join("."), after);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
if (value !== null && typeof value === "object") overrideMap(file, entry, [...ancestors, target]);
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
overrideMap(rootManifest, ["overrides"]);
|
|
2573
|
+
overrideMap(rootManifest, ["resolutions"]);
|
|
2574
|
+
overrideMap(rootManifest, ["pnpm", "overrides"]);
|
|
2575
|
+
overrideMap(path.join(workspace.rootDir, "pnpm-workspace.yaml"), ["overrides"]);
|
|
2576
|
+
return findings;
|
|
2577
|
+
}
|
|
2578
|
+
//#endregion
|
|
2579
|
+
//#region src/migration/migrator/vitest-v5.ts
|
|
2580
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2581
|
+
"node_modules",
|
|
2582
|
+
".git",
|
|
2583
|
+
".vite",
|
|
2584
|
+
".vitest",
|
|
2585
|
+
".vite-plus",
|
|
2586
|
+
".cache",
|
|
2587
|
+
".yarn",
|
|
2588
|
+
"dist",
|
|
2589
|
+
"build",
|
|
2590
|
+
"out",
|
|
2591
|
+
"coverage",
|
|
2592
|
+
".next",
|
|
2593
|
+
".nuxt",
|
|
2594
|
+
".svelte-kit"
|
|
2595
|
+
]);
|
|
2596
|
+
const CODE_FILE = /\.[cm]?[jt]sx?$/;
|
|
2597
|
+
const VITEST_SIGNAL = /(?:['"](?:vitest(?:\/[^'"]*)?|@vitest\/[^'"]+|vite-plus\/test[^'"]*)['"]|\bvp\s+test\b|\bvitest\s+(?:run|list|bench)|\bimport\s*\.\s*meta\s*\.\s*vitest\b)/;
|
|
2598
|
+
const VITEST_COMMAND = /\b(?:vp\s+test|vitest)(?:\s|$)/;
|
|
2599
|
+
const BROWSER_SIGNAL = /@vitest\/browser|(?:vitest|vite-plus\/test)\/browser|vitest-browser-|browser\s*:\s*\{|--browser(?:[.=\s'"]|$)/;
|
|
2600
|
+
const LEGACY_RUNNER = "@voidzero-dev/vite-plus-test";
|
|
2601
|
+
const DEFERRED_SOURCE_REVIEWS = /* @__PURE__ */ new Set([
|
|
2602
|
+
"resolve-config",
|
|
2603
|
+
"overlapping-edits",
|
|
2604
|
+
"unsafe-syntax"
|
|
2605
|
+
]);
|
|
2606
|
+
function workspaceFromPlan(plan) {
|
|
2607
|
+
return {
|
|
2608
|
+
rootDir: plan.rootDir,
|
|
2609
|
+
packageManager: plan.packageManager,
|
|
2610
|
+
packages: plan.projects.slice(1).map((project) => ({
|
|
2611
|
+
name: "",
|
|
2612
|
+
path: path.relative(plan.rootDir, project.directory)
|
|
2613
|
+
}))
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
function readJson(file) {
|
|
2617
|
+
return parseJson(fs.readFileSync(file, "utf8"));
|
|
2618
|
+
}
|
|
2619
|
+
function parseJson(source) {
|
|
2620
|
+
return JSON.parse(source.replace(/^\uFEFF/, ""));
|
|
2621
|
+
}
|
|
2622
|
+
function record(value) {
|
|
2623
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2624
|
+
}
|
|
2625
|
+
function dependency(pkg, name) {
|
|
2626
|
+
for (const field of [
|
|
2627
|
+
"dependencies",
|
|
2628
|
+
"devDependencies",
|
|
2629
|
+
"optionalDependencies",
|
|
2630
|
+
"peerDependencies"
|
|
2631
|
+
]) {
|
|
2632
|
+
const spec = record(pkg[field])?.[name];
|
|
2633
|
+
if (typeof spec === "string") return spec;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
function filesInProject(directory) {
|
|
2637
|
+
const files = [];
|
|
2638
|
+
const walk = (current) => {
|
|
2639
|
+
if (current !== directory && fs.existsSync(path.join(current, "package.json"))) return;
|
|
2640
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
2641
|
+
const file = path.join(current, entry.name);
|
|
2642
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) walk(file);
|
|
2643
|
+
else if (entry.isFile() && ![".pnp.cjs", ".pnp.loader.mjs"].includes(entry.name) && (CODE_FILE.test(file) || /\.(?:json|ya?ml|sh)$/.test(file) || /^Dockerfile(?:\.|$)|^Containerfile(?:\.|$)/.test(entry.name) || [
|
|
2644
|
+
".node-version",
|
|
2645
|
+
".nvmrc",
|
|
2646
|
+
".gitignore",
|
|
2647
|
+
"yarn.lock",
|
|
2648
|
+
"bun.lock"
|
|
2649
|
+
].includes(entry.name))) files.push(file);
|
|
2650
|
+
}
|
|
2651
|
+
};
|
|
2652
|
+
walk(directory);
|
|
2653
|
+
return files.toSorted();
|
|
2654
|
+
}
|
|
2655
|
+
function finding(file, code, message, severity = "review", source = "", offset = 0) {
|
|
2656
|
+
const prefix = source.slice(0, offset);
|
|
2657
|
+
return {
|
|
2658
|
+
file,
|
|
2659
|
+
line: prefix.split("\n").length,
|
|
2660
|
+
column: offset - prefix.lastIndexOf("\n"),
|
|
2661
|
+
code,
|
|
2662
|
+
severity,
|
|
2663
|
+
message
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
/** Follow this project's dependency edges, never an unrelated transitive runner. */
|
|
2667
|
+
function lockedSourceVersion(directory, root, pkg, manager, expectedRunnerPackage) {
|
|
2668
|
+
if (manager !== void 0 && manager !== PackageManager.pnpm) {
|
|
2669
|
+
const spec = dependency(pkg, "vitest");
|
|
2670
|
+
return spec && !expectedRunnerPackage ? lockedVitestVersion(root, directory, manager, spec) : void 0;
|
|
2671
|
+
}
|
|
2672
|
+
const lockfile = path.join(root, "pnpm-lock.yaml");
|
|
2673
|
+
if (!fs.existsSync(lockfile)) return;
|
|
2674
|
+
try {
|
|
2675
|
+
const document = (0, import_dist.parseDocument)(fs.readFileSync(lockfile, "utf8"));
|
|
2676
|
+
if (document.errors.length) return;
|
|
2677
|
+
const lock = record(document.toJS());
|
|
2678
|
+
const importer = record(record(lock?.importers)?.[path.relative(root, directory).replaceAll("\\", "/") || "."]);
|
|
2679
|
+
if (!importer) return;
|
|
2680
|
+
const name = dependency(pkg, "vitest") ? "vitest" : "vite-plus";
|
|
2681
|
+
const spec = dependency(pkg, name);
|
|
2682
|
+
if (!spec) return;
|
|
2683
|
+
const entry = [
|
|
2684
|
+
"dependencies",
|
|
2685
|
+
"devDependencies",
|
|
2686
|
+
"optionalDependencies"
|
|
2687
|
+
].map((field) => record(importer[field])?.[name]).find((value) => value !== void 0);
|
|
2688
|
+
const metadata = record(entry);
|
|
2689
|
+
if (typeof metadata?.specifier === "string" && metadata.specifier !== spec) return;
|
|
2690
|
+
const follow = (name, value, depth = 0) => {
|
|
2691
|
+
const reference = typeof value === "string" ? value : record(value)?.version;
|
|
2692
|
+
if (typeof reference !== "string" || depth > 3) return;
|
|
2693
|
+
if (name === "vitest" && expectedRunnerPackage && !reference.startsWith(`${expectedRunnerPackage}@`)) return;
|
|
2694
|
+
if (reference.startsWith(`${LEGACY_RUNNER}@`)) return follow(LEGACY_RUNNER, reference.slice(29), depth + 1);
|
|
2695
|
+
const version = reference.split("(")[0];
|
|
2696
|
+
if (!import_semver.default.valid(version)) return;
|
|
2697
|
+
if (name === "vitest") return version;
|
|
2698
|
+
if (name === LEGACY_RUNNER) {
|
|
2699
|
+
const runnerVersion = record(record(record(lock?.packages)?.[`${name}@${version}`])?.peerDependencies)?.["@vitest/ui"];
|
|
2700
|
+
return typeof runnerVersion === "string" ? import_semver.default.valid(runnerVersion) ?? void 0 : void 0;
|
|
2701
|
+
}
|
|
2702
|
+
const dependencies = record(record(record(lock?.snapshots)?.[`${name}@${reference}`])?.dependencies);
|
|
2703
|
+
return follow("vitest", dependencies?.vitest, depth + 1) ?? follow(LEGACY_RUNNER, dependencies?.[LEGACY_RUNNER], depth + 1);
|
|
2704
|
+
};
|
|
2705
|
+
return follow(name, entry);
|
|
2706
|
+
} catch {
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
function unambiguousRunnerVersion(range) {
|
|
2711
|
+
if (!import_semver.default.validRange(range) || !import_semver.default.subset(range, "<5.0.0-0", { includePrerelease: true }) && !import_semver.default.subset(range, ">=5.0.0-0", { includePrerelease: true })) return;
|
|
2712
|
+
return import_semver.default.minVersion(range)?.version;
|
|
2713
|
+
}
|
|
2714
|
+
function installedSourceVersion(directory, name, expectedPackage, expectedRange) {
|
|
2715
|
+
const installed = detectPackageMetadata(directory, name);
|
|
2716
|
+
if (!installed || expectedPackage && installed.name !== expectedPackage) return;
|
|
2717
|
+
if (expectedRange && import_semver.default.validRange(expectedRange) && !import_semver.default.satisfies(installed.version, expectedRange, { includePrerelease: true })) return;
|
|
2718
|
+
if (installed.name === "vitest") return import_semver.default.valid(installed.version) ?? void 0;
|
|
2719
|
+
const pkg = readJson(path.join(installed.path, "package.json"));
|
|
2720
|
+
if (installed.name === LEGACY_RUNNER) {
|
|
2721
|
+
const version = record(pkg.peerDependencies)?.["@vitest/ui"];
|
|
2722
|
+
return typeof version === "string" ? import_semver.default.valid(version) ?? void 0 : void 0;
|
|
2723
|
+
}
|
|
2724
|
+
if (installed.name !== "vite-plus") return;
|
|
2725
|
+
const bundled = dependency(pkg, "vitest");
|
|
2726
|
+
if (bundled?.startsWith("catalog:")) return installedSourceVersion(installed.path, "vitest");
|
|
2727
|
+
if (bundled && import_semver.default.validRange(bundled)) {
|
|
2728
|
+
const version = installedSourceVersion(installed.path, "vitest");
|
|
2729
|
+
return version && import_semver.default.satisfies(version, bundled, { includePrerelease: true }) ? version : unambiguousRunnerVersion(bundled);
|
|
2730
|
+
}
|
|
2731
|
+
return dependency(pkg, LEGACY_RUNNER) ? installedSourceVersion(installed.path, LEGACY_RUNNER) : void 0;
|
|
2732
|
+
}
|
|
2733
|
+
function sourceVersion(directory, root, pkg, manager) {
|
|
2734
|
+
const catalog = createCatalogDependencyResolver(root, manager ?? PackageManager.pnpm);
|
|
2735
|
+
const vitePlus = dependency(pkg, "vite-plus");
|
|
2736
|
+
const vitePlusRange = vitePlus?.startsWith("catalog:") ? catalog?.(vitePlus, "vite-plus") : vitePlus;
|
|
2737
|
+
let spec = dependency(pkg, "vitest");
|
|
2738
|
+
if (spec?.startsWith("catalog:")) spec = catalog?.(spec, "vitest");
|
|
2739
|
+
const legacyAlias = spec?.startsWith(`npm:${LEGACY_RUNNER}@`);
|
|
2740
|
+
if (spec?.startsWith("npm:vitest@")) spec = spec.slice(11);
|
|
2741
|
+
if (![
|
|
2742
|
+
"dependencies",
|
|
2743
|
+
"devDependencies",
|
|
2744
|
+
"optionalDependencies"
|
|
2745
|
+
].some((field) => typeof record(pkg[field])?.vitest === "string") && vitePlus) {
|
|
2746
|
+
const bundled = installedSourceVersion(directory, "vite-plus", "vite-plus", vitePlusRange);
|
|
2747
|
+
if (bundled) return bundled;
|
|
2748
|
+
}
|
|
2749
|
+
const expectedRunnerPackage = legacyAlias ? LEGACY_RUNNER : void 0;
|
|
2750
|
+
const locked = lockedSourceVersion(directory, root, pkg, manager, expectedRunnerPackage);
|
|
2751
|
+
if (locked && (!spec || legacyAlias || !import_semver.default.validRange(spec) || import_semver.default.satisfies(locked, spec, { includePrerelease: true }))) return locked;
|
|
2752
|
+
const installed = installedSourceVersion(directory, "vitest", expectedRunnerPackage);
|
|
2753
|
+
if (installed && (!spec || legacyAlias || import_semver.default.satisfies(installed, spec, { includePrerelease: true }))) return installed;
|
|
2754
|
+
if (legacyAlias) return "4.0.0";
|
|
2755
|
+
if (spec && import_semver.default.validRange(spec)) return unambiguousRunnerVersion(spec);
|
|
2756
|
+
if (vitePlus) {
|
|
2757
|
+
const bundled = installedSourceVersion(directory, "vite-plus", "vite-plus", vitePlusRange);
|
|
2758
|
+
if (bundled) return bundled;
|
|
2759
|
+
}
|
|
2760
|
+
if (directory !== root) return sourceVersion(root, root, readJson(path.join(root, "package.json")), manager);
|
|
2761
|
+
}
|
|
2762
|
+
function checkNodeRange(file, value, label, findings, publicContract = false) {
|
|
2763
|
+
if (!publicContract && [
|
|
2764
|
+
"lts/*",
|
|
2765
|
+
"lts",
|
|
2766
|
+
"latest",
|
|
2767
|
+
"current",
|
|
2768
|
+
"node",
|
|
2769
|
+
"stable"
|
|
2770
|
+
].includes(value)) return;
|
|
2771
|
+
const range = import_semver.default.validRange(value);
|
|
2772
|
+
if (publicContract && range) {
|
|
2773
|
+
const minimum = import_semver.default.minVersion(range);
|
|
2774
|
+
if (minimum) {
|
|
2775
|
+
const majorRange = `${minimum.major}.x`;
|
|
2776
|
+
if (import_semver.default.satisfies(minimum, engines.node) || import_semver.default.subset(majorRange, range) && import_semver.default.intersects(majorRange, engines.node)) return;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
let message = `Resolve ${label} (${value}) and select Node ${engines.node}.`;
|
|
2780
|
+
if (range && !publicContract && !import_semver.default.intersects(range, engines.node)) {
|
|
2781
|
+
const current = import_semver.default.minVersion(range);
|
|
2782
|
+
const upgrade = current && new import_semver.default.Range(engines.node).set.map((comparators) => import_semver.default.minVersion(comparators.map(({ value }) => value).join(" "))).filter((version) => version !== null).toSorted(import_semver.default.compare).find((version) => import_semver.default.gte(version, current));
|
|
2783
|
+
if (upgrade) return upgrade.version;
|
|
2784
|
+
} else if (range) {
|
|
2785
|
+
if (import_semver.default.subset(range, engines.node) || !publicContract && !/[<>|*]/.test(value)) return;
|
|
2786
|
+
message = `${label} (${value}) includes unsupported test runtimes. Pin the test/CI runtime to Node ${engines.node}; keep the library's public engine contract separate.`;
|
|
2787
|
+
}
|
|
2788
|
+
findings.push(finding(file, "node-runtime", message));
|
|
2789
|
+
}
|
|
2790
|
+
function migrateNode(file, source, findings) {
|
|
2791
|
+
const base = path.basename(file);
|
|
2792
|
+
if (base === ".node-version" || base === ".nvmrc") {
|
|
2793
|
+
const value = source.trim();
|
|
2794
|
+
const upgrade = checkNodeRange(file, value.replace(/^v/, ""), base, findings);
|
|
2795
|
+
if (upgrade) {
|
|
2796
|
+
const start = source.indexOf(value);
|
|
2797
|
+
return source.slice(0, start) + (value.startsWith("v") ? "v" : "") + upgrade + source.slice(start + value.length);
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
if (base === "package.json") {
|
|
2801
|
+
const pkg = parseJson(source);
|
|
2802
|
+
const tree = parseTree(source.replace(/^\uFEFF/, " "));
|
|
2803
|
+
const edits = [];
|
|
2804
|
+
const checkRuntime = (value, label, keys) => {
|
|
2805
|
+
const upgrade = checkNodeRange(file, value, label, findings);
|
|
2806
|
+
const node = tree && findNodeAtLocation(tree, keys);
|
|
2807
|
+
if (upgrade && node) edits.push({
|
|
2808
|
+
offset: node.offset,
|
|
2809
|
+
length: node.length,
|
|
2810
|
+
content: JSON.stringify(upgrade)
|
|
2811
|
+
});
|
|
2812
|
+
};
|
|
2813
|
+
if (typeof pkg.engines?.node === "string") checkNodeRange(file, pkg.engines.node, "engines.node", findings, true);
|
|
2814
|
+
const runtime = pkg.devEngines?.runtime;
|
|
2815
|
+
for (const [index, entry] of (Array.isArray(runtime) ? runtime : [runtime]).entries()) if (entry?.name === "node" && typeof entry.version === "string") checkRuntime(entry.version, "devEngines.runtime", [
|
|
2816
|
+
"devEngines",
|
|
2817
|
+
"runtime",
|
|
2818
|
+
...Array.isArray(runtime) ? [index] : [],
|
|
2819
|
+
"version"
|
|
2820
|
+
]);
|
|
2821
|
+
if (typeof pkg.volta?.node === "string" && ![".node-version", ".nvmrc"].some((name) => fs.existsSync(path.join(path.dirname(file), name)))) checkRuntime(pkg.volta.node, "volta.node", ["volta", "node"]);
|
|
2822
|
+
return applyEdits(source, edits);
|
|
2823
|
+
}
|
|
2824
|
+
return source;
|
|
2825
|
+
}
|
|
2826
|
+
function scanAndRewriteFile(file, source, project, testMode, mergedConfig) {
|
|
2827
|
+
const findings = [];
|
|
2828
|
+
if (!project.active) return {
|
|
2829
|
+
content: migrateNode(file, source, findings),
|
|
2830
|
+
findings
|
|
2831
|
+
};
|
|
2832
|
+
let content = source;
|
|
2833
|
+
if (CODE_FILE.test(file) && (testMode.benchmark || testMode.globals || testMode.reviewGlobals || VITEST_SIGNAL.test(source) || /\.(?:test|spec)\./.test(file) || project.configFiles.has(file))) try {
|
|
2834
|
+
const sourceResult = migrateVitestV5Source(file, content, {
|
|
2835
|
+
...project.options,
|
|
2836
|
+
...testMode
|
|
2837
|
+
});
|
|
2838
|
+
content = sourceResult.content;
|
|
2839
|
+
findings.push(...sourceResult.findings);
|
|
2840
|
+
const pending = project.pendingSourceReviews?.[path.relative(project.directory, file).replaceAll("\\", "/")];
|
|
2841
|
+
if (!project.options.preserveV4 && pending?.length) {
|
|
2842
|
+
const review = migrateVitestV5Source(file, content, {
|
|
2843
|
+
...project.options,
|
|
2844
|
+
...testMode,
|
|
2845
|
+
preserveV4: true
|
|
2846
|
+
});
|
|
2847
|
+
findings.push(...review.findings.filter(({ code }) => pending.includes(code)));
|
|
2848
|
+
}
|
|
2849
|
+
if (project.configFiles.has(file)) {
|
|
2850
|
+
const configResult = migrateVitestV5Config(file, content, project.options, mergedConfig);
|
|
2851
|
+
content = configResult.content;
|
|
2852
|
+
findings.push(...configResult.findings);
|
|
2853
|
+
}
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
content = source;
|
|
2856
|
+
findings.push(finding(file, "source-parse", `Parse and review this file before migration: ${error instanceof Error ? error.message : String(error)}`));
|
|
2857
|
+
if (/['"](?:@vitest\/(?:runner|expect)|(?:vitest|vite-plus\/test)\/(?:runners|suite|internal\/module-runner))/.test(source)) findings.push(finding(file, "removed-api", "This unparsed file references removed APIs. Resolve it before changing dependencies.", "block"));
|
|
2858
|
+
}
|
|
2859
|
+
else if (path.basename(file) === "package.json") {
|
|
2860
|
+
const pkg = parseJson(source);
|
|
2861
|
+
const tree = parseTree(source.replace(/^\uFEFF/, " "));
|
|
2862
|
+
const edits = [];
|
|
2863
|
+
for (const [name, command] of Object.entries(record(pkg.scripts) ?? {})) {
|
|
2864
|
+
if (typeof command !== "string") continue;
|
|
2865
|
+
const node = tree && findNodeAtLocation(tree, ["scripts", name]);
|
|
2866
|
+
if (!node) continue;
|
|
2867
|
+
const result = migrateVitestV5Command(file, command, project.options.preserveV4, source.slice(0, node.offset).split("\n").length, project.options.preserveV4 || project.options.reviewV4);
|
|
2868
|
+
findings.push(...result.findings);
|
|
2869
|
+
if (result.content !== command) edits.push({
|
|
2870
|
+
offset: node.offset,
|
|
2871
|
+
length: node.length,
|
|
2872
|
+
content: JSON.stringify(result.content)
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
content = applyEdits(source, edits);
|
|
2876
|
+
} else if (/^tsconfig(?:\..+)?\.json$/.test(path.basename(file))) {
|
|
2877
|
+
const types = record(record(parse(source))?.compilerOptions)?.types;
|
|
2878
|
+
if (Array.isArray(types) && types.includes("@testing-library/jest-dom") && !types.includes("@testing-library/jest-dom/vitest")) findings.push(finding(file, "jest-dom-types", "If this config checks Vitest tests, load @testing-library/jest-dom/vitest in compilerOptions.types or an included TypeScript setup file. The root jest-dom type entry augments Jest, not Vitest v5."));
|
|
2879
|
+
} else if (/\.ya?ml$/.test(file)) {
|
|
2880
|
+
const document = (0, import_dist.parseDocument)(source);
|
|
2881
|
+
let changed = false;
|
|
2882
|
+
(0, import_dist.visit)(document, { Pair(_key, pair) {
|
|
2883
|
+
if (!(0, import_dist.isScalar)(pair.key) || ![
|
|
2884
|
+
"run",
|
|
2885
|
+
"script",
|
|
2886
|
+
"command"
|
|
2887
|
+
].includes(String(pair.key.value)) || !(0, import_dist.isScalar)(pair.value) || typeof pair.value.value !== "string") return;
|
|
2888
|
+
const value = pair.value.value;
|
|
2889
|
+
const result = migrateVitestV5Command(file, value.includes("\n") ? `(${value})` : value, project.options.preserveV4, source.slice(0, pair.key.range?.[0] ?? 0).split("\n").length, project.options.preserveV4 || project.options.reviewV4);
|
|
2890
|
+
findings.push(...result.findings);
|
|
2891
|
+
if (!value.includes("\n") && result.content !== value) {
|
|
2892
|
+
pair.value.value = result.content;
|
|
2893
|
+
changed = true;
|
|
2894
|
+
}
|
|
2895
|
+
} });
|
|
2896
|
+
if (changed) content = document.toString();
|
|
2897
|
+
} else if (file.endsWith(".sh")) findings.push(...migrateVitestV5Command(file, `(${source})`, project.options.preserveV4, 1, project.options.preserveV4 || project.options.reviewV4).findings);
|
|
2898
|
+
return {
|
|
2899
|
+
content: migrateNode(file, content, findings),
|
|
2900
|
+
findings
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
/** Read-only preflight. Call before package-manager conversion, installs,
|
|
2904
|
+
* catalog updates, or generic import rewrites can erase the source version. */
|
|
2905
|
+
function planVitestV5Migration(workspace, originalProjects) {
|
|
2906
|
+
const findings = [];
|
|
2907
|
+
const directories = [workspace.rootDir, ...(workspace.packages ?? []).map((pkg) => path.resolve(workspace.rootDir, pkg.path))];
|
|
2908
|
+
const projects = [];
|
|
2909
|
+
const projectSources = new Map([...new Set(directories)].map((directory) => [directory, new Map(filesInProject(directory).map((file) => [file, fs.readFileSync(file, "utf8")]))]));
|
|
2910
|
+
const allSources = /* @__PURE__ */ new Map();
|
|
2911
|
+
for (const sources of projectSources.values()) for (const [file, source] of sources) allSources.set(file, source);
|
|
2912
|
+
const rewrittenSources = new Map(allSources);
|
|
2913
|
+
const configEntries = findVitestV5ConfigEntries(allSources, projectSources.keys());
|
|
2914
|
+
const allConfigs = findVitestV5ConfigFiles(allSources, configEntries.flatMap((entry) => entry.file ? [entry.file] : []));
|
|
2915
|
+
const mergedConfigs = findVitestV5MergedConfigFiles(allSources, allConfigs);
|
|
2916
|
+
const browserPossible = [...allSources.values()].some((source) => BROWSER_SIGNAL.test(source));
|
|
2917
|
+
const browserCliOverride = [...allSources].some(([file, source]) => /\.(?:ya?ml|sh)$/.test(file) && /\b(?:vitest|vp\s+test)\b[^\n]*--(?:no-)?browser(?:[.=\s'"]|$)/.test(source));
|
|
2918
|
+
const testModesByVersion = /* @__PURE__ */ new Map();
|
|
2919
|
+
const inputs = new Map(allSources);
|
|
2920
|
+
for (const [directory, sources] of projectSources) {
|
|
2921
|
+
const pkg = readJson(path.join(directory, "package.json"));
|
|
2922
|
+
const configFiles = new Set([...allConfigs].filter((file) => sources.has(file)));
|
|
2923
|
+
const original = originalProjects?.get(directory);
|
|
2924
|
+
const version = original ? original.sourceVersion : sourceVersion(directory, workspace.rootDir, pkg, workspace.packageManager);
|
|
2925
|
+
const active = original?.active ?? (!!dependency(pkg, "vitest") || Object.values(record(pkg.scripts) ?? {}).some((command) => typeof command === "string" && VITEST_COMMAND.test(command)) || [...sources].some(([file, source]) => CODE_FILE.test(file) && hasVitestV5SourceUsage(file, source) || configFiles.has(file) && /\btest\s*:/.test(source)));
|
|
2926
|
+
const options = {
|
|
2927
|
+
preserveV4: original?.options.preserveV4 ?? (!!version && import_semver.default.major(version) < 5),
|
|
2928
|
+
reviewV4: !!version && import_semver.default.major(version) < 5,
|
|
2929
|
+
browser: [...sources.values()].some((source) => BROWSER_SIGNAL.test(source)),
|
|
2930
|
+
browserPossible,
|
|
2931
|
+
temporalPolyfill: [...sources.values()].some((source) => /(?:['"]temporal-polyfill\/global['"]|(?:globalThis|global)\.Temporal\s*=|Object\.(?:assign|defineProperty)\(globalThis,\s*(?:\{\s*Temporal|['"]Temporal['"]))/.test(source))
|
|
2932
|
+
};
|
|
2933
|
+
if (active && !version) findings.push(finding(path.join(directory, "package.json"), "source-version", "Cannot determine the original Vitest version. Install the original lockfile, then re-run migration so v4 defaults are not applied to a v5 project.", "block"));
|
|
2934
|
+
if (active && version && import_semver.default.major(version) < 4) findings.push(finding(path.join(directory, "package.json"), "source-version", "Upgrade the original project to Vitest 4 before running this migration.", "block"));
|
|
2935
|
+
const project = {
|
|
2936
|
+
directory,
|
|
2937
|
+
sourceVersion: version,
|
|
2938
|
+
active,
|
|
2939
|
+
options,
|
|
2940
|
+
configFiles,
|
|
2941
|
+
pendingSourceReviews: original?.pendingSourceReviews
|
|
2942
|
+
};
|
|
2943
|
+
projects.push(project);
|
|
2944
|
+
let testModes = testModesByVersion.get(options.preserveV4);
|
|
2945
|
+
if (!testModes) {
|
|
2946
|
+
testModes = resolveVitestV5TestModes(allSources, allConfigs, options.preserveV4, configEntries);
|
|
2947
|
+
testModesByVersion.set(options.preserveV4, testModes);
|
|
2948
|
+
}
|
|
2949
|
+
for (const [file, source] of sources) {
|
|
2950
|
+
if (/[/\\](?:pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lock)$/.test(file)) continue;
|
|
2951
|
+
const result = scanAndRewriteFile(file, source, project, {
|
|
2952
|
+
...testModes.modes.get(file),
|
|
2953
|
+
...browserCliOverride ? { browser: void 0 } : {}
|
|
2954
|
+
}, mergedConfigs.has(file));
|
|
2955
|
+
findings.push(...testModes.findings.filter((item) => item.file === file));
|
|
2956
|
+
findings.push(...result.findings);
|
|
2957
|
+
if (source !== result.content) rewrittenSources.set(file, result.content);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
findings.push(...migrateWebdriverioDependencies(workspace, rewrittenSources, inputs));
|
|
2961
|
+
return {
|
|
2962
|
+
rootDir: workspace.rootDir,
|
|
2963
|
+
packageManager: workspace.packageManager,
|
|
2964
|
+
projects,
|
|
2965
|
+
findings,
|
|
2966
|
+
changes: [...rewrittenSources].flatMap(([file, after]) => {
|
|
2967
|
+
const before = allSources.get(file);
|
|
2968
|
+
return before !== after ? [{
|
|
2969
|
+
file,
|
|
2970
|
+
before,
|
|
2971
|
+
after
|
|
2972
|
+
}] : [];
|
|
2973
|
+
}),
|
|
2974
|
+
inputs
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
function formatVitestV5Findings(plan) {
|
|
2978
|
+
const unique = [...new Map(plan.findings.map((item) => [`${item.file}:${item.line}:${item.column}:${item.code}:${item.message}`, item])).values()];
|
|
2979
|
+
if (!unique.length) return "";
|
|
2980
|
+
const blocks = unique.filter((item) => item.severity === "block").length;
|
|
2981
|
+
const lines = [`Vitest v5: ${unique.length} review item${unique.length === 1 ? "" : "s"}${blocks ? ` (${blocks} block dependency updates)` : ""}`];
|
|
2982
|
+
let previous = "";
|
|
2983
|
+
for (const item of unique.toSorted((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column)) {
|
|
2984
|
+
if (item.file !== previous) {
|
|
2985
|
+
lines.push(`\n${path.relative(plan.rootDir, item.file) || "."}`);
|
|
2986
|
+
previous = item.file;
|
|
2987
|
+
}
|
|
2988
|
+
lines.push(` ${item.line}:${item.column} ${item.severity === "block" ? "BLOCK" : "REVIEW"} [${item.code}] ${item.message}`, ` Docs: ${vitestV5Documentation(item.code)}`);
|
|
2989
|
+
}
|
|
2990
|
+
return lines.join("\n");
|
|
2991
|
+
}
|
|
2992
|
+
/** Apply a fully checked compatibility plan. Refuse stale input so an editor
|
|
2993
|
+
* change cannot be overwritten between phases. */
|
|
2994
|
+
function applyVitestV5Migration(plan) {
|
|
2995
|
+
if (plan.findings.some((item) => item.severity === "block")) throw new Error("Vitest v5 preflight has blocking findings. No migration edits were applied.");
|
|
2996
|
+
for (const [file, before] of plan.inputs) if ((fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null) !== before) throw new Error(`Migration input changed: ${file}. Re-run vp migrate.`);
|
|
2997
|
+
for (const change of plan.changes) fs.writeFileSync(change.file, change.after);
|
|
2998
|
+
return plan.changes.length;
|
|
2999
|
+
}
|
|
3000
|
+
/** Upgrade runtime declarations before installers or tool migrators start.
|
|
3001
|
+
* Keep source/config edits deferred until the tool version gates pass. */
|
|
3002
|
+
function applyVitestV5NodeMigration(plan) {
|
|
3003
|
+
const changes = [];
|
|
3004
|
+
for (const [file, before] of plan.inputs) {
|
|
3005
|
+
if (before === null || ![
|
|
3006
|
+
".node-version",
|
|
3007
|
+
".nvmrc",
|
|
3008
|
+
"package.json"
|
|
3009
|
+
].includes(path.basename(file))) continue;
|
|
3010
|
+
const after = migrateNode(file, before, []);
|
|
3011
|
+
if (after !== before) changes.push({
|
|
3012
|
+
file,
|
|
3013
|
+
before,
|
|
3014
|
+
after
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
return applyVitestV5Migration({
|
|
3018
|
+
...plan,
|
|
3019
|
+
changes
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
/** Earlier setup/install steps can change manifests and configs. Re-plan from
|
|
3023
|
+
* their current text without losing the runner version captured before install.
|
|
3024
|
+
* Apply immediately afterward, so the normal stale-input guard still protects
|
|
3025
|
+
* every write. Run this after the earlier tool migration gates have passed. */
|
|
3026
|
+
function refreshVitestV5Migration(plan) {
|
|
3027
|
+
return planVitestV5Migration(workspaceFromPlan(plan), new Map(plan.projects.map((project) => [project.directory, project])));
|
|
3028
|
+
}
|
|
3029
|
+
function vitestV5NeedsMigration(plan) {
|
|
3030
|
+
return plan.changes.length > 0 || plan.projects.some((project) => project.active && project.options.preserveV4);
|
|
3031
|
+
}
|
|
3032
|
+
function currentProjectConfigs(plan) {
|
|
3033
|
+
const filesByProject = new Map(plan.projects.map((project) => [project.directory, filesInProject(project.directory)]));
|
|
3034
|
+
const sources = new Map([...filesByProject.values()].flat().map((file) => [file, fs.readFileSync(file, "utf8")]));
|
|
3035
|
+
const configs = findVitestV5ConfigFiles(sources, findVitestV5ConfigEntries(sources, filesByProject.keys()).flatMap((entry) => entry.file ? [entry.file] : []));
|
|
3036
|
+
return new Map([...filesByProject].map(([directory, files]) => [directory, files.filter((file) => configs.has(file))]));
|
|
3037
|
+
}
|
|
3038
|
+
/** Run after other migration steps create or merge configs. Keep the source
|
|
3039
|
+
* version and deferred reviews in memory until the final report; no state file. */
|
|
3040
|
+
function finishVitestV5Migration(plan) {
|
|
3041
|
+
const findings = [];
|
|
3042
|
+
const completedProjects = /* @__PURE__ */ new Map();
|
|
3043
|
+
const projectConfigs = currentProjectConfigs(plan);
|
|
3044
|
+
const configSources = new Map([...projectConfigs.values()].flat().map((file) => [file, fs.readFileSync(file, "utf8")]));
|
|
3045
|
+
const mergedConfigs = findVitestV5MergedConfigFiles(configSources, new Set(configSources.keys()));
|
|
3046
|
+
for (const project of plan.projects) {
|
|
3047
|
+
if (!project.active || !project.sourceVersion) continue;
|
|
3048
|
+
const configs = projectConfigs.get(project.directory);
|
|
3049
|
+
for (const file of configs) {
|
|
3050
|
+
const before = fs.readFileSync(file, "utf8");
|
|
3051
|
+
try {
|
|
3052
|
+
const result = migrateVitestV5Config(file, before, project.options, mergedConfigs.has(file));
|
|
3053
|
+
findings.push(...result.findings);
|
|
3054
|
+
if (!result.findings.some((finding) => finding.severity === "block") && result.content !== before) fs.writeFileSync(file, result.content);
|
|
3055
|
+
} catch (error) {
|
|
3056
|
+
findings.push(finding(file, "source-parse", `Review this config: ${String(error)}`));
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
if (!isDirectoryGitignored(findGitRoot(project.directory) ?? plan.rootDir, path.join(project.directory, ".vitest"))) {
|
|
3060
|
+
const ignore = path.join(project.directory, ".gitignore");
|
|
3061
|
+
const ignored = fs.existsSync(ignore) ? fs.readFileSync(ignore, "utf8") : "";
|
|
3062
|
+
fs.writeFileSync(ignore, `${ignored}${ignored && !ignored.endsWith("\n") ? "\n" : ""}.vitest/\n`);
|
|
3063
|
+
}
|
|
3064
|
+
const pendingSourceReviews = {};
|
|
3065
|
+
for (const item of plan.findings) {
|
|
3066
|
+
if (!DEFERRED_SOURCE_REVIEWS.has(item.code) || !item.file.startsWith(`${project.directory}${path.sep}`)) continue;
|
|
3067
|
+
if (plan.projects.some((other) => other !== project && other.directory.startsWith(`${project.directory}${path.sep}`) && item.file.startsWith(`${other.directory}${path.sep}`))) continue;
|
|
3068
|
+
const relative = path.relative(project.directory, item.file).replaceAll("\\", "/");
|
|
3069
|
+
pendingSourceReviews[relative] ??= [];
|
|
3070
|
+
if (!pendingSourceReviews[relative].includes(item.code)) pendingSourceReviews[relative].push(item.code);
|
|
3071
|
+
}
|
|
3072
|
+
completedProjects.set(project.directory, {
|
|
3073
|
+
...project,
|
|
3074
|
+
options: {
|
|
3075
|
+
...project.options,
|
|
3076
|
+
preserveV4: false
|
|
3077
|
+
},
|
|
3078
|
+
pendingSourceReviews
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
3081
|
+
const after = planVitestV5Migration(workspaceFromPlan(plan), new Map(plan.projects.map((project) => [project.directory, completedProjects.get(project.directory) ?? project])));
|
|
3082
|
+
return [...findings, ...after.findings];
|
|
3083
|
+
}
|
|
3084
|
+
//#endregion
|
|
3085
|
+
//#region src/migration/compat/runner.ts
|
|
22
3086
|
/**
|
|
23
3087
|
* Resolve the isolated compat worker emitted at `migration/compat/worker.js`.
|
|
24
3088
|
*
|
|
@@ -677,7 +3741,25 @@ async function downloadSupportedPackageManager(options) {
|
|
|
677
3741
|
}
|
|
678
3742
|
return downloadResult;
|
|
679
3743
|
}
|
|
680
|
-
|
|
3744
|
+
function completeVitestV5Migration(plan, report) {
|
|
3745
|
+
const findings = finishVitestV5Migration(plan);
|
|
3746
|
+
report.warnings = report.warnings.filter((warning) => !warning.startsWith("Vitest v5:"));
|
|
3747
|
+
const summary = formatVitestV5Findings({
|
|
3748
|
+
rootDir: plan.rootDir,
|
|
3749
|
+
findings
|
|
3750
|
+
});
|
|
3751
|
+
if (summary) addMigrationWarning(report, summary);
|
|
3752
|
+
}
|
|
3753
|
+
function applyRefreshedVitestV5Migration(plan) {
|
|
3754
|
+
const refreshed = refreshVitestV5Migration(plan);
|
|
3755
|
+
if (refreshed.findings.some((finding) => finding.severity === "block")) {
|
|
3756
|
+
log$1.warn(formatVitestV5Findings(refreshed));
|
|
3757
|
+
cancelAndExit("Resolve the blocking Vitest v5 findings, then re-run `vp migrate`. Earlier setup steps may have changed project files; Vitest v5 edits were not applied.", 1);
|
|
3758
|
+
}
|
|
3759
|
+
applyVitestV5Migration(refreshed);
|
|
3760
|
+
return refreshed;
|
|
3761
|
+
}
|
|
3762
|
+
async function executeMigrationPlan(workspaceInfoOptional, plan, interactive, vitestV5Plan, preExistingChangedPaths) {
|
|
681
3763
|
const report = createMigrationReport();
|
|
682
3764
|
const migrationProgress = interactive ? spinner({ indicator: "timer" }) : void 0;
|
|
683
3765
|
let migrationProgressStarted = false;
|
|
@@ -702,6 +3784,7 @@ async function executeMigrationPlan(workspaceInfoOptional, plan, interactive, pr
|
|
|
702
3784
|
migrationProgressStarted = false;
|
|
703
3785
|
}
|
|
704
3786
|
};
|
|
3787
|
+
applyVitestV5NodeMigration(vitestV5Plan);
|
|
705
3788
|
const downloadResult = await downloadSupportedPackageManager({
|
|
706
3789
|
rootDir: workspaceInfoOptional.rootDir,
|
|
707
3790
|
packageManager: plan.packageManager,
|
|
@@ -722,7 +3805,8 @@ async function executeMigrationPlan(workspaceInfoOptional, plan, interactive, pr
|
|
|
722
3805
|
}
|
|
723
3806
|
if (plan.migrateNodeVersionFile && plan.nodeVersionDetection) {
|
|
724
3807
|
updateMigrationProgress("Migrating node version file");
|
|
725
|
-
|
|
3808
|
+
const detection = detectNodeVersionManagerFile(workspaceInfo.rootDir);
|
|
3809
|
+
if (detection) migrateNodeVersionManagerFile(workspaceInfo.rootDir, detection, report);
|
|
726
3810
|
}
|
|
727
3811
|
updateMigrationProgress("Updating setup-vp workflows");
|
|
728
3812
|
migrateSetupVpVersion(workspaceInfo.rootDir, report);
|
|
@@ -771,10 +3855,13 @@ async function executeMigrationPlan(workspaceInfoOptional, plan, interactive, pr
|
|
|
771
3855
|
cancelAndExit("Complete the tsup migration manually, then re-run `vp migrate`.", 1);
|
|
772
3856
|
}
|
|
773
3857
|
}
|
|
3858
|
+
vitestV5Plan = applyRefreshedVitestV5Migration(vitestV5Plan);
|
|
774
3859
|
const skipStagedMigration = shouldSkipStagedMigrationForHooks(workspaceInfo.rootDir, plan.shouldSetupHooks, plan.packageManager, workspaceInfo.packages);
|
|
775
3860
|
updateMigrationProgress("Rewriting configs");
|
|
776
3861
|
if (workspaceInfo.isMonorepo) rewriteMonorepo(workspaceInfo, skipStagedMigration, true, report);
|
|
777
3862
|
else rewriteStandaloneProject(workspaceInfo.rootDir, workspaceInfo, skipStagedMigration, true, report);
|
|
3863
|
+
clearMigrationProgress();
|
|
3864
|
+
completeVitestV5Migration(vitestV5Plan, report);
|
|
778
3865
|
if (plan.shouldSetupHooks) {
|
|
779
3866
|
updateMigrationProgress("Configuring git hooks");
|
|
780
3867
|
installGitHooks(workspaceInfo.rootDir, true, report, plan.packageManager, workspaceInfo.packages);
|
|
@@ -826,10 +3913,17 @@ async function main() {
|
|
|
826
3913
|
const initialChangedPaths = await collectChangedFormatPaths(workspaceInfoOptional.rootDir);
|
|
827
3914
|
const preExistingChangedPaths = initialChangedPaths ? new Set(initialChangedPaths) : void 0;
|
|
828
3915
|
const resolvedPackageManager = workspaceInfoOptional.packageManager ?? "unknown";
|
|
3916
|
+
let vitestV5Plan = planVitestV5Migration(workspaceInfoOptional);
|
|
3917
|
+
const vitestV5Preflight = formatVitestV5Findings(vitestV5Plan);
|
|
3918
|
+
if (vitestV5Plan.findings.some((finding) => finding.severity === "block")) {
|
|
3919
|
+
log$1.warn(vitestV5Preflight);
|
|
3920
|
+
cancelAndExit("Resolve the blocking Vitest v5 findings, then re-run `vp migrate`. No project files were changed.", 1);
|
|
3921
|
+
}
|
|
829
3922
|
const rootPkg = readNearestPackageJson(workspaceInfoOptional.rootDir);
|
|
830
3923
|
if (hasVitePlusDependency(rootPkg) && !isForceOverrideMode()) {
|
|
831
3924
|
let convertYarnPnp = await confirmYarnNodeModulesMode(workspaceInfoOptional.rootDir, workspaceInfoOptional.packageManager, workspaceInfoOptional.packageManagerVersion, options.interactive);
|
|
832
|
-
let didMigrate =
|
|
3925
|
+
let didMigrate = vitestV5NeedsMigration(vitestV5Plan);
|
|
3926
|
+
const nodeUpgrades = applyVitestV5NodeMigration(vitestV5Plan);
|
|
833
3927
|
let installDurationMs = 0;
|
|
834
3928
|
let finalInstallOk = true;
|
|
835
3929
|
let canFormatMigratedProject = !process.env.VP_SKIP_INSTALL;
|
|
@@ -887,11 +3981,12 @@ async function main() {
|
|
|
887
3981
|
report.dependencyUpgrades = await collectToolchainVersionChanges(workspaceInfoOptional.rootDir);
|
|
888
3982
|
updateMigrationProgress("Rewriting toolchain imports across the workspace");
|
|
889
3983
|
const coreMigrationResult = finalizeCoreMigrationForExistingVitePlus(workspaceInfoOptional, true, report, pendingCoreMigration);
|
|
890
|
-
if (coreMigrationResult.scripts || coreMigrationResult.tsconfigTypes || coreMigrationResult.imports || coreMigrationResult.tsdownConfig) didMigrate = true;
|
|
3984
|
+
if (coreMigrationResult.dependencies || coreMigrationResult.scripts || coreMigrationResult.tsconfigTypes || coreMigrationResult.imports || coreMigrationResult.tsdownConfig) didMigrate = true;
|
|
891
3985
|
clearMigrationProgress();
|
|
892
3986
|
const fullSetup = options.full === true;
|
|
893
3987
|
const skippedSetupCandidates = !fullSetup && !hasExplicitExistingVitePlusSetupRequest(options) && hasExistingVitePlusMigrationCandidates(workspaceInfoOptional, options);
|
|
894
3988
|
if (!didMigrate && !convertYarnPnp && report.warnings.length === 0 && !vitePlusBootstrapPending && !(fullSetup ? hasExistingVitePlusMigrationCandidates(workspaceInfoOptional, options) : hasExplicitExistingVitePlusSetupRequest(options))) {
|
|
3989
|
+
if (vitestV5Preflight) log$1.warn(vitestV5Preflight);
|
|
895
3990
|
if (skippedSetupCandidates) log(FULL_MIGRATION_HINT);
|
|
896
3991
|
outro(`This project is already using Vite+! ${accent("Happy coding!")}`);
|
|
897
3992
|
return;
|
|
@@ -899,7 +3994,7 @@ async function main() {
|
|
|
899
3994
|
const setupOptions = getExistingVitePlusSetupOptions(options, fullSetup);
|
|
900
3995
|
const plan = await collectMigrationSetupPlan(workspaceInfoOptional.rootDir, packageManager, setupOptions, workspaceInfoOptional.packages, fullSetup);
|
|
901
3996
|
const frameworkShimFrameworks = fullSetup ? await collectFrameworkShimFrameworks(workspaceInfoOptional.rootDir, options, workspaceInfoOptional.packages) : void 0;
|
|
902
|
-
let needsInstall =
|
|
3997
|
+
let needsInstall = coreMigrationResult.dependencies || nodeUpgrades > 0;
|
|
903
3998
|
if (vitePlusBootstrapPending) {
|
|
904
3999
|
const downloadResult = await ensureExistingPackageManager();
|
|
905
4000
|
if (downloadResult && packageManager) {
|
|
@@ -991,6 +4086,12 @@ async function main() {
|
|
|
991
4086
|
}
|
|
992
4087
|
}
|
|
993
4088
|
if (addFrameworkShimsForWorkspace(workspaceInfoOptional.rootDir, frameworkShimFrameworks, workspaceInfoOptional.packages, report, updateMigrationProgress)) didMigrate = true;
|
|
4089
|
+
vitestV5Plan = applyRefreshedVitestV5Migration(vitestV5Plan);
|
|
4090
|
+
needsInstall ||= vitestV5Plan.changes.some(({ file }) => [
|
|
4091
|
+
"package.json",
|
|
4092
|
+
"pnpm-workspace.yaml",
|
|
4093
|
+
".yarnrc.yml"
|
|
4094
|
+
].includes(path.basename(file)));
|
|
994
4095
|
if (eslintMigrated || prettierMigrated || tsupMigrated) {
|
|
995
4096
|
updateMigrationProgress("Rewriting configs");
|
|
996
4097
|
mergeViteConfigFiles(workspaceInfoOptional.rootDir, true, report, workspaceInfoOptional.packages);
|
|
@@ -1013,6 +4114,8 @@ async function main() {
|
|
|
1013
4114
|
}
|
|
1014
4115
|
}
|
|
1015
4116
|
if (needsInstall) {
|
|
4117
|
+
clearMigrationProgress();
|
|
4118
|
+
completeVitestV5Migration(vitestV5Plan, report);
|
|
1016
4119
|
const resolved = await ensureExistingPackageManager();
|
|
1017
4120
|
updateMigrationProgress("Installing dependencies");
|
|
1018
4121
|
const resolvedVersion = resolved?.version ?? packageManagerVersion;
|
|
@@ -1053,6 +4156,10 @@ async function main() {
|
|
|
1053
4156
|
});
|
|
1054
4157
|
didMigrate = true;
|
|
1055
4158
|
}
|
|
4159
|
+
if (!needsInstall) {
|
|
4160
|
+
clearMigrationProgress();
|
|
4161
|
+
completeVitestV5Migration(vitestV5Plan, report);
|
|
4162
|
+
}
|
|
1056
4163
|
await checkWorkspaceRolldownCompatibility(workspaceInfoOptional, report, updateMigrationProgress);
|
|
1057
4164
|
if (didMigrate && finalInstallOk && canFormatMigratedProject && canFormatWithOxfmt(prettierProject.hasDependency, prettierMigrated)) {
|
|
1058
4165
|
clearMigrationProgress();
|
|
@@ -1078,7 +4185,7 @@ async function main() {
|
|
|
1078
4185
|
return;
|
|
1079
4186
|
}
|
|
1080
4187
|
const plan = await collectMigrationPlan(workspaceInfoOptional.rootDir, workspaceInfoOptional.packageManager, workspaceInfoOptional.packageManagerVersion, options, workspaceInfoOptional.packages);
|
|
1081
|
-
const result = await executeMigrationPlan(workspaceInfoOptional, plan, options.interactive, preExistingChangedPaths);
|
|
4188
|
+
const result = await executeMigrationPlan(workspaceInfoOptional, plan, options.interactive, vitestV5Plan, preExistingChangedPaths);
|
|
1082
4189
|
showMigrationSummary({
|
|
1083
4190
|
projectRoot: workspaceInfoOptional.rootDir,
|
|
1084
4191
|
packageManager: plan.packageManager,
|