vite-plus 0.3.2 → 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.
Files changed (149) hide show
  1. package/LICENSE +35 -146
  2. package/README.md +3 -3
  3. package/binding/index.cjs +140 -55
  4. package/binding/index.d.cts +49 -0
  5. package/dist/{agent-C59HKB4Y.js → agent-Dinpyd-k.js} +6 -5
  6. package/dist/bin.js +48 -18
  7. package/dist/config/bin.js +2 -2
  8. package/dist/constants-CiKwrA0v.js +356 -0
  9. package/dist/create/bin.js +96 -71
  10. package/dist/{define-config-DPNEJxPz.d.ts → define-config-B9usMnEa.d.ts} +51 -10
  11. package/dist/{define-config-DpAPETDf.cjs → define-config-CBccpane.cjs} +173 -44
  12. package/dist/{define-config-DAaCa0U_.js → define-config-jhuYJ7Tb.js} +158 -42
  13. package/dist/define-config.cjs +1 -1
  14. package/dist/define-config.d.ts +1 -1
  15. package/dist/define-config.js +1 -1
  16. package/dist/documentation-DqcZSthV.js +11 -0
  17. package/dist/{editor-QOIGtmH4.js → editor-DMBczTdj.js} +216 -152
  18. package/dist/index.cjs +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/{json-cULBl7Pi.js → json-JP24qHUF.js} +14 -6
  22. package/dist/lint-plugins.cjs +1 -1
  23. package/dist/migration/bin.js +3122 -15
  24. package/dist/{oxlint-plugin-config-BU42tr8y.js → oxlint-plugin-config-BgsDA6ua.js} +1 -1
  25. package/dist/oxlint-plugin.js +9 -7
  26. package/dist/pack-bin.js +1 -1
  27. package/dist/{package-BB-Jh_z3.js → package-B85H2Nw0.js} +6 -2
  28. package/dist/{vitest-ecosystem-BiRqcaRI.js → package-overrides-Dpn4scvN.js} +1 -7
  29. package/dist/{prompts-CujFOJXu.js → prompts-CQobX7uP.js} +2 -2
  30. package/dist/{resolve-vite-config-CHHjsjKJ.js → resolve-vite-config-BUFUmi8h.js} +2 -2
  31. package/dist/{resolve-vite-config-6Nox70uP.js → resolve-vite-config-I8fmpR7I.js} +1 -1
  32. package/dist/staged/bin.js +1205 -3317
  33. package/dist/sync-versions/bin.js +3 -2
  34. package/dist/sync-versions/bin.mjs +17 -2
  35. package/dist/test/browser/context.js +1 -1
  36. package/dist/test/browser/providers/playwright/context.js +1 -1
  37. package/dist/test/browser/providers/preview/context.js +1 -1
  38. package/dist/test/browser-playwright/context.js +1 -1
  39. package/dist/test/browser-preview/context.js +1 -1
  40. package/dist/test/context.d.ts +2 -942
  41. package/dist/test/context.js +1 -1
  42. package/dist/test/internal/traces.js +1 -0
  43. package/dist/test/locators.d.ts +2 -294
  44. package/dist/test/matchers.d.ts +2 -29
  45. package/dist/test/mocker.d.ts +2 -0
  46. package/dist/test/mocker.js +1 -0
  47. package/dist/test/{browser-webdriverio/context.js → plugins/browser-context.d.ts} +1 -0
  48. package/dist/test/plugins/browser-context.js +1 -1
  49. package/dist/test/plugins/browser-locators.d.ts +2 -0
  50. package/dist/test/plugins/browser-playwright.d.ts +2 -0
  51. package/dist/test/plugins/browser-preview.d.ts +2 -0
  52. package/dist/test/plugins/browser.d.ts +2 -0
  53. package/dist/test/plugins/mocker-automock.d.ts +2 -0
  54. package/dist/test/plugins/mocker-browser.d.ts +2 -0
  55. package/dist/test/plugins/mocker-node.d.ts +2 -0
  56. package/dist/test/plugins/mocker-redirect.d.ts +2 -0
  57. package/dist/test/plugins/mocker-register.d.ts +2 -0
  58. package/dist/test/plugins/mocker-transforms.d.ts +2 -0
  59. package/dist/test/plugins/mocker.d.ts +2 -0
  60. package/dist/test/plugins/pretty-format.d.ts +2 -0
  61. package/dist/test/plugins/snapshot-environment.d.ts +2 -0
  62. package/dist/test/plugins/snapshot-manager.d.ts +2 -0
  63. package/dist/test/plugins/snapshot.d.ts +2 -0
  64. package/dist/test/plugins/spy.d.ts +2 -0
  65. package/dist/test/plugins/utils-constants.d.ts +2 -0
  66. package/dist/test/plugins/utils-diff.d.ts +2 -0
  67. package/dist/test/plugins/utils-display.d.ts +2 -0
  68. package/dist/test/plugins/utils-error.d.ts +2 -0
  69. package/dist/test/plugins/utils-helpers.d.ts +2 -0
  70. package/dist/test/plugins/utils-offset.d.ts +2 -0
  71. package/dist/test/plugins/utils-resolver.d.ts +2 -0
  72. package/dist/test/plugins/utils-serialize.d.ts +2 -0
  73. package/dist/test/plugins/utils-source-map-node.d.ts +2 -0
  74. package/dist/test/plugins/utils-source-map.d.ts +2 -0
  75. package/dist/test/plugins/utils-timers.d.ts +2 -0
  76. package/dist/test/plugins/utils.d.ts +2 -0
  77. package/dist/toolchain.js +10 -10
  78. package/dist/toolchain.json +10 -10
  79. package/dist/{tsconfig-DCtWfQPA.js → tsconfig-Buo0RSzz.js} +8 -4
  80. package/dist/{tsconfig-MXktI3SX.js → tsconfig-C1VeTG9k.js} +1 -1
  81. package/dist/version.js +5 -3
  82. package/dist/versions.js +5 -5
  83. package/dist/vitest-ecosystem-BU0AqZq-.js +23 -0
  84. package/dist/vitest-v5-entry-points-DthU1e_O.js +25 -0
  85. package/docs/config/fmt.md +2 -2
  86. package/docs/config/lint.md +2 -2
  87. package/docs/config/run.md +1 -1
  88. package/docs/config/staged.md +1 -2
  89. package/docs/guide/check.md +2 -0
  90. package/docs/guide/ci.md +28 -31
  91. package/docs/guide/commit-hooks.md +18 -34
  92. package/docs/guide/create.md +1 -2
  93. package/docs/guide/docker.md +20 -68
  94. package/docs/guide/env.md +47 -32
  95. package/docs/guide/fmt.md +4 -2
  96. package/docs/guide/global-cli.md +426 -0
  97. package/docs/guide/ide-integration.md +1 -2
  98. package/docs/guide/implode.md +14 -1
  99. package/docs/guide/index.md +38 -35
  100. package/docs/guide/install.md +18 -20
  101. package/docs/guide/lint.md +6 -10
  102. package/docs/guide/local-cli.md +123 -0
  103. package/docs/guide/migrate-rules.md +104 -297
  104. package/docs/guide/migrate.md +82 -59
  105. package/docs/guide/monorepo.md +5 -1
  106. package/docs/guide/run.md +1 -1
  107. package/docs/guide/test.md +2 -0
  108. package/docs/guide/troubleshooting.md +6 -11
  109. package/docs/guide/upgrade-project.md +61 -0
  110. package/docs/guide/upgrade.md +17 -62
  111. package/docs/guide/vitest-v5.md +221 -0
  112. package/package.json +67 -90
  113. package/bin/oxfmt +0 -33
  114. package/bin/oxlint +0 -38
  115. package/dist/cli-truncate-Bg3RDXpi.js +0 -865
  116. package/dist/constants-IhOditRA.js +0 -80
  117. package/dist/is-fullwidth-code-point-BUNlIICg.js +0 -8
  118. package/dist/log-update-CoW8Z4Dl.js +0 -491
  119. package/dist/strip-ansi-C3wrWz9t.js +0 -853
  120. package/dist/test/browser/providers/webdriverio/context.d.ts +0 -1
  121. package/dist/test/browser/providers/webdriverio/context.js +0 -1
  122. package/dist/test/browser/providers/webdriverio.d.ts +0 -77
  123. package/dist/test/browser/providers/webdriverio.js +0 -1
  124. package/dist/test/browser-webdriverio/context.d.ts +0 -1
  125. package/dist/test/browser-webdriverio.d.ts +0 -77
  126. package/dist/test/browser-webdriverio.js +0 -1
  127. package/dist/test/coverage.d.ts +0 -2
  128. package/dist/test/coverage.js +0 -1
  129. package/dist/test/environments.d.ts +0 -2
  130. package/dist/test/environments.js +0 -1
  131. package/dist/test/plugins/browser-webdriverio.js +0 -1
  132. package/dist/test/plugins/expect.js +0 -1
  133. package/dist/test/plugins/runner-types.js +0 -1
  134. package/dist/test/plugins/runner-utils.js +0 -1
  135. package/dist/test/plugins/runner.js +0 -1
  136. package/dist/test/reporters.d.ts +0 -2
  137. package/dist/test/reporters.js +0 -1
  138. package/dist/test/runners.d.ts +0 -2
  139. package/dist/test/runners.js +0 -1
  140. package/dist/test/snapshot.d.ts +0 -2
  141. package/dist/test/snapshot.js +0 -1
  142. package/dist/test/suite.d.ts +0 -2
  143. package/dist/test/suite.js +0 -1
  144. package/dist/tsgolint-path-CbDReEOx.js +0 -34
  145. package/dist/tsgolint-path.d.ts +0 -7
  146. package/dist/tsgolint-path.js +0 -2
  147. package/dist/wrap-ansi-DNjkuBEp.js +0 -157
  148. package/dist/wrap-ansi-DUi52C6W.js +0 -2
  149. package/docs/guide/installer-env-vars.md +0 -286
@@ -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-IhOditRA.js";
4
- import { a as writeJsonFile, i as readJsonFile } from "../json-cULBl7Pi.js";
5
- import { a as readNearestPackageJson, i as hasVitePlusDependency } from "../package-BB-Jh_z3.js";
6
- 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-CujFOJXu.js";
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-C59HKB4Y.js";
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 warnIncompatibleEslintIntegration, A as detectYarnPnpMode, B as migrateTsupToTsdown, C as injectLintTypeCheckDefaults, D as detectVitePlusBootstrapPending, E as collectToolchainVersionChanges, F as addFrameworkShim, G as migratePrettierToOxfmt, H as warnMissingTsupConfig, I as detectFramework, J as confirmEslintMigration, L as hasFrameworkShim, O as ensureVitePlusBootstrap, R as confirmTsupMigration, T as mergeViteConfigFiles, U as confirmPrettierMigration, W as detectPrettierProject, X as detectIncompatibleEslintIntegration, Y as detectEslintProject, Z as migrateEslintToOxlint, _ as installGitHooks, d as finalizeCoreMigrationForExistingVitePlus, et as warnLegacyEslintConfig, f as detectNodeVersionManagerFile, g as detectLegacyGitHooksMigrationCandidate, i as selectEditor, j as detectWorkspace$1, k as configureYarnNodeModulesMode, l as rewriteStandaloneProject, m as migrateSetupVpVersion, nt as checkViteVersion, o as writeEditorConfigs, p as migrateNodeVersionManagerFile, q as warnPackageLevelPrettier, r as isJsonLikeFile, rt as checkVitestVersion, s as rewriteMonorepo, t as detectEditorConflicts, tt as warnPackageLevelEslint, u as detectPendingCoreMigration, v as preflightGitHooksSetup, w as mergeTsdownConfigFile, y as shouldSkipStagedMigrationForHooks, z as detectTsupProject } from "../editor-QOIGtmH4.js";
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 { a as hasBaseUrlInTsconfig, i as fixBaseUrlInTsconfig, t as confirmBaseUrlFix } from "../tsconfig-DCtWfQPA.js";
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/compat/runner.ts
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
- async function executeMigrationPlan(workspaceInfoOptional, plan, interactive, preExistingChangedPaths) {
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
- migrateNodeVersionManagerFile(workspaceInfo.rootDir, plan.nodeVersionDetection, report);
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 = false;
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 = false;
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,