vite-plus 1.3.0 → 1.5.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 (106) hide show
  1. package/AGENTS.md +4 -0
  2. package/LICENSE +1 -878
  3. package/README.md +1 -1
  4. package/bin/vpx +9 -0
  5. package/binding/index.cjs +59 -54
  6. package/binding/index.d.cts +159 -2
  7. package/dist/{agent-97KmGCyL.js → agent-DkQD3xqe.js} +5 -7
  8. package/dist/bin.js +35 -34
  9. package/dist/{cli-parse-DHE8NrBU.js → cli-parse-aeUeFDlC.js} +2 -3
  10. package/dist/command-TjS_6y9O.js +1 -0
  11. package/dist/{command-CdMBoZFD.js → command-rY79K5NI.js} +28 -30
  12. package/dist/{comparison-CmVirWIW-DUGaiYqF.js → comparison-CmVirWIW-UlEf2maz.js} +1 -2
  13. package/dist/config/bin.js +8 -10
  14. package/dist/{constants-DR55DpRm.js → constants-D2P2aVKb.js} +8 -6
  15. package/dist/create/bin.d.ts +4 -8
  16. package/dist/create/bin.js +151 -120
  17. package/dist/{define-config-Bexdd6RZ.d.ts → define-config-BrvHRi-G.d.ts} +31 -13
  18. package/dist/{define-config-BxDc_9BK.cjs → define-config-CwtQqYC1.cjs} +41 -14
  19. package/dist/{define-config-CvE5pHqB.js → define-config-wR57OZG_.js} +34 -12
  20. package/dist/define-config.cjs +2 -1
  21. package/dist/define-config.d.ts +2 -2
  22. package/dist/define-config.js +1 -1
  23. package/dist/{dist-DIpm7mot.js → dist-Bs0SObNC.js} +73 -145
  24. package/dist/{dist-Cqz8OtiO.js → dist-CdkMvIiQ.js} +11 -5
  25. package/dist/dist-zDZV5eEc.js +3 -0
  26. package/dist/{documentation-DqcZSthV.js → documentation-cv_y0Auv.js} +1 -2
  27. package/dist/{editor-CtJGQP7C.js → editor-aOy1T15H.js} +2735 -5072
  28. package/dist/{help-BmKpeOP9.js → help-B03t_iQ4.js} +1 -2
  29. package/dist/hooks/bin.js +4 -5
  30. package/dist/{hooks-DhwsWb46.js → hooks-w_e2whkQ.js} +45 -25
  31. package/dist/index.cjs +5 -5
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +2 -2
  34. package/dist/{json-JP24qHUF.js → json-kl84kDyH.js} +9 -18
  35. package/dist/lint-plugins.cjs +1 -1
  36. package/dist/migration/bin.js +1782 -256
  37. package/dist/migration/compat/worker.js +4 -6
  38. package/dist/{oxlint-plugin-config-BIzrO2y2.js → oxlint-plugin-config-CHsI_zKo.js} +3 -4
  39. package/dist/oxlint-plugin.d.ts +0 -2
  40. package/dist/oxlint-plugin.js +33 -9
  41. package/dist/{pack-BK38MLVZ.d.ts → pack-ARPFMefW.d.ts} +0 -2
  42. package/dist/pack-bin.d.ts +1 -3
  43. package/dist/pack-bin.js +4 -5
  44. package/dist/pack.d.ts +1 -1
  45. package/dist/{package-CQDXTm6j.js → package-DfFo6-E-.js} +10 -95
  46. package/dist/{package-overrides-D1RcXSFl.js → package-overrides-Ci8GXouY.js} +5 -8
  47. package/dist/picomatch-DEUoj81q.js +1916 -0
  48. package/dist/{prompts-1xNz_ne0.js → prompts-C7DI63WP.js} +30 -50
  49. package/dist/{protocol-D99W10Qi.js → protocol-CkNmY-Mr.js} +1 -2
  50. package/dist/register.d.ts +1 -0
  51. package/dist/register.js +12 -0
  52. package/dist/{report-NhQk8VHb.js → report-BEdpIQvC.js} +4 -2
  53. package/dist/resolve-vite-config-33bzSaeT.js +1 -0
  54. package/dist/{resolve-vite-config-BVr1SUNg.js → resolve-vite-config-Cx3cnVVI.js} +9 -25
  55. package/dist/{rolldown-runtime-kJ_i4yPo.js → rolldown-runtime-DegdyBIC.js} +1 -2
  56. package/dist/script-esm-hooks.d.ts +11 -0
  57. package/dist/script-esm-hooks.js +27 -0
  58. package/dist/script-hooks-9Fof5UMY.js +176 -0
  59. package/dist/script-preload.cjs +491 -0
  60. package/dist/script-register.d.ts +1 -0
  61. package/dist/script-register.js +27 -0
  62. package/dist/script-watch.d.ts +1 -0
  63. package/dist/script-watch.js +2002 -0
  64. package/dist/staged/bin.js +53 -2011
  65. package/dist/sync-versions/bin.js +26 -48
  66. package/dist/sync-versions/bin.mjs +109 -217
  67. package/dist/{terminal-MKGAuy-p.js → terminal-BcuPUy-j.js} +1 -2
  68. package/dist/toolchain.js +38 -12
  69. package/dist/toolchain.json +38 -12
  70. package/dist/{tsconfig-zWgHiQ3X.js → tsconfig-BE3Wj9OX.js} +1 -1
  71. package/dist/{tsconfig-BZu7kl-x.js → tsconfig-shNmmCE_.js} +8 -10
  72. package/dist/version.d.ts +1 -3
  73. package/dist/version.js +5 -6
  74. package/dist/versions.js +7 -7
  75. package/dist/{vitest-ecosystem-BU0AqZq-.js → vitest-ecosystem-BeEYz-qF.js} +1 -2
  76. package/dist/{vitest-v5-entry-points-DthU1e_O.js → vitest-v5-entry-points-NyJsFeux.js} +1 -2
  77. package/dist/vpx-bin.d.ts +1 -0
  78. package/dist/vpx-bin.js +232 -0
  79. package/dist/vpx-script-EXgV5QOf.js +434 -0
  80. package/docs/config/check.md +2 -0
  81. package/docs/config/fmt.md +1 -1
  82. package/docs/config/lint.md +1 -1
  83. package/docs/config/run.md +10 -2
  84. package/docs/config/staged.md +1 -1
  85. package/docs/guide/check.md +6 -1
  86. package/docs/guide/ci.md +13 -3
  87. package/docs/guide/create.md +1 -1
  88. package/docs/guide/env.md +11 -4
  89. package/docs/guide/global-cli.md +59 -3
  90. package/docs/guide/homebrew.md +127 -0
  91. package/docs/guide/ide-integration.md +1 -3
  92. package/docs/guide/implode.md +5 -3
  93. package/docs/guide/local-cli.md +21 -0
  94. package/docs/guide/migrate-rules.md +120 -18
  95. package/docs/guide/migrate.md +9 -9
  96. package/docs/guide/package-management.md +1 -1
  97. package/docs/guide/run.md +23 -1
  98. package/docs/guide/test.md +1 -1
  99. package/docs/guide/troubleshooting.md +1 -1
  100. package/docs/guide/upgrade.md +12 -6
  101. package/docs/guide/vitest-v5.md +2 -2
  102. package/docs/guide/vpx.md +210 -9
  103. package/package.json +43 -34
  104. package/dist/command-DVTILtS-.js +0 -1
  105. package/dist/dist-Dhu07wmN.js +0 -3
  106. package/dist/resolve-vite-config-7WDtXO4G.js +0 -1
@@ -1,129 +1,1544 @@
1
- import { t as documentationUrl$1 } from "../documentation-DqcZSthV.js";
2
- 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 { _ as engines, a as SETUP_VP_VERSION, d as VITE_PLUS_OVERRIDE_PACKAGES, f as VITE_PLUS_VERSION, m as isForceOverrideMode } from "../constants-DR55DpRm.js";
4
- 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";
5
- import { a as hasVitePlusDependency, i as hasPackageManagerDeclaration, n as detectPackageMetadata, o as readNearestPackageJson } from "../package-CQDXTm6j.js";
6
- import { t as vitest_v5_entry_points_default } from "../vitest-v5-entry-points-DthU1e_O.js";
7
- import { C as confirm, D as outro, M as isCancel, O as select, S as normalize$1, T as log$1, c as selectPackageManager, i as promptGitHooks, k as spinner, l as upgradeYarn, n as defaultInteractive, o as runViteFmt, r as downloadPackageManager$1, s as runViteInstall, t as cancelAndExit, x as getMajor, y as PackageManager } from "../prompts-1xNz_ne0.js";
8
- import { _ as isGreaterThanOrEqual, p as compare } from "../comparison-CmVirWIW-DUGaiYqF.js";
9
- import { a as rangeToComparators, i as normalizeRange, n as findMinimumForRange, o as rangesIntersect, r as isRangeSubset, s as satisfies, t as extractOverrideTargetName } from "../package-overrides-D1RcXSFl.js";
10
- import { i as selectAgentTargetPaths, l as displayRelative, n as detectAgentConflicts, r as detectExistingAgentTargetPaths, s as writeAgentInstructions } from "../agent-97KmGCyL.js";
11
- import { n as addMigrationWarning, r as createMigrationReport } from "../report-NhQk8VHb.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-CtJGQP7C.js";
13
- import { n as runCommandSilently } from "../command-CdMBoZFD.js";
14
- import { t as require_dist } from "../dist-DIpm7mot.js";
15
- import { a as hasBaseUrlInTsconfig, i as fixBaseUrlInTsconfig, t as confirmBaseUrlFix } from "../tsconfig-BZu7kl-x.js";
16
- import { t as unwrapCliParseOutcome } from "../cli-parse-DHE8NrBU.js";
17
- import { t as ROLLDOWN_COMPAT_RESULT_PREFIX } from "../protocol-D99W10Qi.js";
1
+ import { C as confirm, D as outro, M as isCancel, O as select, S as normalize$1, T as log, c as selectPackageManager, i as promptGitHooks, k as spinner, l as upgradeYarn, n as defaultInteractive, o as runViteFmt, r as downloadPackageManager$1, s as runViteInstall, t as cancelAndExit, x as getMajor, y as PackageManager } from "../prompts-C7DI63WP.js";
2
+ import { a as rangeToComparators, i as normalizeRange, n as findMinimumForRange, o as rangesIntersect, r as isRangeSubset, s as satisfies, t as extractOverrideTargetName } from "../package-overrides-Ci8GXouY.js";
3
+ import { i as selectAgentTargetPaths, l as displayRelative, n as detectAgentConflicts, r as detectExistingAgentTargetPaths, s as writeAgentInstructions } from "../agent-DkQD3xqe.js";
4
+ import { t as unwrapCliParseOutcome } from "../cli-parse-aeUeFDlC.js";
5
+ import { _ as engines, a as SETUP_VP_VERSION, d as VITE_PLUS_OVERRIDE_PACKAGES, f as VITE_PLUS_VERSION, m as isForceOverrideMode } from "../constants-D2P2aVKb.js";
6
+ import { A as detectVitePlusBootstrapPending, C as removeMigratedOxlintConfigs, Ct as readPackageJsonIfExists, D as mergeViteConfigFiles, E as mergeTsdownConfigFile, F as detectYarnPnpMode, I as createCatalogDependencyResolver, J as detectFramework, K as extractOverrideParentSegments, M as sourceTreeMatches, N as usesWebdriverioProvider, O as retryPreservedOxlintConfigs, P as configureYarnNodeModulesMode, Q as migrateTsupToTsdown, R as detectWorkspace$1, S as prepareOxlintConfigs, St as isPlainRecord, T as injectLintTypeCheckDefaults, U as minimatch, W as findGitRoot, X as confirmTsupMigration, Y as hasFrameworkShim, Z as detectTsupProject, _ as hasExistingViteHooksPolicy, _t as SHELL_STATE_COMMANDS, at as warnPackageLevelPrettier, b as preflightGitHooksSetup, bt as checkVitestVersion, ct as detectIncompatibleEslintIntegration, d as finalizeCoreMigrationForExistingVitePlus, dt as warnIncompatibleEslintIntegration, et as warnMissingTsupConfig, f as detectNodeVersionManagerFile, ft as warnLegacyEslintConfig, g as detectLegacyGitHooksMigrationCandidate, gt as RETENTION_SCAN_SKIP_DIRS, ht as LINT_STAGED_JSON_CONFIG_FILES, i as selectEditor, j as ensureVitePlusBootstrap, k as collectToolchainVersionChanges, l as rewriteStandaloneProject, lt as migrateEslintToOxlint, m as migrateSetupVpVersion, mt as detectMigrationWarnings, nt as detectPrettierProject, o as writeEditorConfigs, ot as confirmEslintMigration, p as migrateNodeVersionManagerFile, pt as warnPackageLevelEslint, q as addFrameworkShim, r as isJsonLikeFile, rt as migratePrettierToOxfmt, s as rewriteMonorepo, st as detectEslintProject, t as detectEditorConflicts, tt as confirmPrettierMigration, u as detectPendingCoreMigration, v as installGitHooks, vt as WEBDRIVERIO_PROVIDER, wt as warnMigration, x as shouldSkipStagedMigrationForHooks, xt as getCommandStartIndex, y as isViteHooksSetupComplete, yt as checkViteVersion } from "../editor-aOy1T15H.js";
7
+ import { a as hasVitePlusDependency, i as hasPackageManagerDeclaration, n as detectPackageMetadata, o as readNearestPackageJson } from "../package-DfFo6-E-.js";
8
+ import { a as muted, c as warnMsg, i as log$1, o as printHeader, r as formatDuration, t as accent } from "../terminal-BcuPUy-j.js";
9
+ import { a as hasBaseUrlInTsconfig, i as fixBaseUrlInTsconfig, t as confirmBaseUrlFix } from "../tsconfig-shNmmCE_.js";
10
+ import { i as runCommandSilently } from "../command-rY79K5NI.js";
11
+ import { n as addMigrationWarning, r as createMigrationReport } from "../report-BEdpIQvC.js";
12
+ import { t as ROLLDOWN_COMPAT_RESULT_PREFIX } from "../protocol-CkNmY-Mr.js";
13
+ import { a as writeJsonFile, c as modify, i as readJsonFile, l as parse, n as editJsonFile, o as applyEdits$1, r as isJsonFile, s as findNodeAtLocation, u as parseTree } from "../json-kl84kDyH.js";
14
+ import { _ as isGreaterThanOrEqual, p as compare } from "../comparison-CmVirWIW-UlEf2maz.js";
15
+ import { t as require_dist } from "../dist-Bs0SObNC.js";
16
+ import { t as documentationUrl$1 } from "../documentation-cv_y0Auv.js";
17
+ import { t as vitest_v5_entry_points_default } from "../vitest-v5-entry-points-NyJsFeux.js";
18
+ import { i as WATCH_OPTIONS_WITH_VALUE, n as SCRIPT_EXTENSIONS, t as NODE_OPTIONS_WITH_VALUE } from "../vpx-script-EXgV5QOf.js";
18
19
  import path from "node:path";
19
20
  import { fileURLToPath } from "node:url";
20
- import { analyzeMigrationSource, isDirectoryGitignored, parseMigrateArgs } from "../../binding/index.js";
21
+ import { ResolverFactory, analyzeMigrationSource, isDirectoryGitignored, parseLiteralCommands, parseMigrateArgs, parseSync } from "../../binding/index.js";
21
22
  import { styleText } from "node:util";
22
23
  import fs, { existsSync } from "node:fs";
23
- //#region src/migration/vitest-v5/commands.ts
24
- /** Parse only a single literal argv command. Shell expansion, pipelines, and
25
- * compound commands need review; treating them as argv could change behavior. */
26
- function literalArgv(command) {
27
- const tokens = [];
28
- let value = "";
29
- let start = 0;
30
- let quote = "";
31
- let active = false;
32
- for (let i = 0; i < command.length; i++) {
33
- const character = command[i];
34
- if (quote) {
35
- if (character === quote) quote = "";
36
- else if (character === "\\" || quote === "\"" && (character === "$" || character === "`")) return;
37
- else value += character;
38
- } else if (character === "'" || character === "\"") {
39
- if (!active) start = i;
40
- quote = character;
41
- active = true;
42
- } else if (character === "\n" || character === "\r") return;
43
- else if (/\s/.test(character)) {
44
- if (active) tokens.push({
45
- value,
46
- start,
47
- end: i
48
- });
49
- active = false;
50
- value = "";
51
- } else if (/[|&;<>($`\\)]/.test(character)) return;
52
- else {
53
- if (!active) start = i;
54
- value += character;
55
- active = true;
56
- }
24
+ import os from "node:os";
25
+ // src/migration/migrator/tsx.ts
26
+ const TSX_COMMAND = /(?:^|[\s;&|("'=/\\`])tsx(?:\.(?:cmd|exe|ps1))?(?:\/(?:esm|cjs))?(?=$|[\s;&|)"'`])/i;
27
+ const TSX_LOADER = /^tsx(?:\/(?:esm|cjs))?$/;
28
+ const KEEP_EVAL = "`vpx` does not run TypeScript passed to --eval or --print";
29
+ const KEEP_CHECK = "`vpx` does not transform TypeScript for -c or --check";
30
+ const KEEP_REPL$1 = "`vpx` does not start a TypeScript REPL";
31
+ const KEEP_TEST_GLOBS = "`--test` without files finds `.ts` tests with tsx globs; list the test files for `vpx --test`";
32
+ const KEEP_WATCH_PATH = "`--watch-path` has no `vpx` equivalent; use `--include`";
33
+ const KEEP_NODE_OPTIONS = "`NODE_OPTIONS` loads tsx, whose hooks would compete with those of `vpx`; remove it first";
34
+ const KEEP_OPTIONS = /* @__PURE__ */ new Map([
35
+ ["-e", KEEP_EVAL],
36
+ ["--eval", KEEP_EVAL],
37
+ ["-p", KEEP_EVAL],
38
+ ["--print", KEEP_EVAL],
39
+ ["-pe", KEEP_EVAL],
40
+ ["-c", KEEP_CHECK],
41
+ ["--check", KEEP_CHECK],
42
+ ["-i", KEEP_REPL$1],
43
+ ["--interactive", KEEP_REPL$1],
44
+ ["--watch-path", KEEP_WATCH_PATH]
45
+ ]);
46
+ const TSX_ENV_SHEBANG = /^(#!\s*\S*\benv\s+(?:-S\s+)?)(tsx(?=[ \t\r\n]|$)[^\r\n]*)/;
47
+ const TSX_SHEBANG = /^#![^\n]*[\s/]tsx(?=\s|$)/;
48
+ const TSX_LOADER_FLAG = /(?:(?:--import|--loader|--experimental-loader|--require|-r)[= \t]+|(?<![\w-])(?:import|loader|experimental-loader|require)=)['"]?tsx(?:\/(?:esm|cjs))?(?![\w./-])/;
49
+ const TSX_CODE_REFERENCE = new RegExp([
50
+ /(?:\bfrom|\bimport|\bimport\.meta\.resolve|\brequire(?:\.resolve)?)\s*\(?\s*(?:\/\*[\s\S]*?\*\/\s*)*['"`]tsx(?:\/[^'"`]*)?['"`]/.source,
51
+ /['"`](?:npx |pnpm (?:exec )?|yarn |bunx )?tsx\s/.source,
52
+ /\(\s*\[?\s*['"`]tsx['"`]\s*[,)\]]/.source,
53
+ TSX_LOADER_FLAG.source,
54
+ /['"`](?:--import|--loader|--experimental-loader|--require|-r)['"`]\s*,\s*['"`]tsx(?:\/(?:esm|cjs))?['"`]/.source
55
+ ].join("|"));
56
+ const JSDOC_TYPE_TAG = /(?:^|\s)@(arg|argument|augments|enum|extends|implements|import|param|prop|property|return|returns|satisfies|template|this|type|typedef)\b\s*/g;
57
+ const JSDOC_PARAMETER_NAME = /^[^\s{[=]+(?:\[\])?(?:\.[^\s{[=]+)*/;
58
+ const JSDOC_TEMPLATE_PARAMETER_NAME = /^(?:(?:in|out|const)\s+)*[$\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*/u;
59
+ const TSX_IMPORT_TYPE = /\bimport\s*\(\s*(['"])tsx(?:\/[^'"]*)?\1\s*(?:,|\))/;
60
+ const TSX_JSDOC_IMPORT = /^(?:[^\s{},]+\s*,\s*)?(?:\{[^}]*\}|\*\s+as\s+[^\s{},]+|[^\s{},]+)\s+from\s*(['"])tsx(?:\/[^'"]*)?\1/;
61
+ const TSX_TEXT_REFERENCE = new RegExp([/(?:^|[\s;&|("'=/\\`])tsx(?:\.(?:cmd|exe|ps1))?(?:\/(?:esm|cjs))?['"]?(?:[ \t]+\S|[ \t]*$)/.source, TSX_LOADER_FLAG.source].join("|"), "m");
62
+ const TSX_CONFIG_VALUE = new RegExp([
63
+ /(?:^|[\s;&|("'=/\\`])tsx(?:\.(?:cmd|exe|ps1))?[ \t]+\S/.source,
64
+ TSX_LOADER_FLAG.source,
65
+ /^tsx\//.source
66
+ ].join("|"), "i");
67
+ const ESM_SYNTAX = /(?:^|;)\s*(?:import(?![\w$])\s*(?![\w$]+\s*=)[\w*{'"]|export(?![\w$])\s*[\w*{])|\bimport\.meta\b/m;
68
+ const COMMONJS_GLOBALS = [
69
+ ["__dirname", /(?<![.\w$])__dirname\b/],
70
+ ["__filename", /(?<![.\w$])__filename\b/],
71
+ ["require", /(?<![.\w$])require\s*(?:\(|\.\s*[\w$])/],
72
+ ["exports", /(?<![.\w$'"`])exports(?![\w$'"`]|\s*:)/],
73
+ ["module", /(?<![.\w$])module\s*(?:\.\s*[\w$]|\[)/]
74
+ ];
75
+ const EXPORT_ASSIGNMENT = /(?:^|;)\s*export\s*=/m;
76
+ const RELATIVE_REQUIRE = /(?<![.\w$])require\s*\(\s*['"`](\.{1,2}\/[^'"`]+?)\.(js|cjs|mjs|jsx)['"`]/g;
77
+ const TYPESCRIPT_EXTENSIONS = {
78
+ js: ["ts", "tsx"],
79
+ cjs: ["cts"],
80
+ mjs: ["mts"],
81
+ jsx: ["tsx"]
82
+ };
83
+ const TSX_RUNNER = {
84
+ name: "tsx",
85
+ dependencies: ["tsx"],
86
+ commandPattern: TSX_COMMAND,
87
+ loaderPattern: TSX_LOADER,
88
+ isRegister: (option, value) => option === "--import" && value === "tsx",
89
+ envShebang: TSX_ENV_SHEBANG,
90
+ shebangPattern: TSX_SHEBANG,
91
+ codeReference: TSX_CODE_REFERENCE,
92
+ codeUsesRunner: codeUsesTsx,
93
+ textReference: TSX_TEXT_REFERENCE,
94
+ configValue: TSX_CONFIG_VALUE,
95
+ keepOptions: KEEP_OPTIONS,
96
+ dropOptions: /* @__PURE__ */ new Set(["--no-cache"]),
97
+ optionsWithValue: /* @__PURE__ */ new Set([...WATCH_OPTIONS_WITH_VALUE, "--tsconfig"]),
98
+ testWithoutFilesReason: KEEP_TEST_GLOBS,
99
+ envKeepReason,
100
+ scriptKeepReason: (cwd, script) => typescriptScriptKeepReason(cwd, script, "tsx")
101
+ };
102
+ function detectTsxProject(rootDir, packages) {
103
+ return detectScriptRunnerProject(TSX_RUNNER, rootDir, packages);
104
+ }
105
+ function migrateTsxToVpx(rootDir, packages, report) {
106
+ const result = migrateScriptRunnerToVpx(TSX_RUNNER, rootDir, packages, report);
107
+ if (report && result.scriptsChanged) report.tsxMigrated = true;
108
+ return {
109
+ changed: result.changed,
110
+ dependencyRemoved: result.dependencyRemoved
111
+ };
112
+ }
113
+ /**
114
+ * Why environment assignments keep tsx for the command they apply to, or undefined.
115
+ * Names match in any case, as on Windows.
116
+ */
117
+ function envKeepReason(assignments) {
118
+ if (assignments.some((value) => /^NODE_OPTIONS=/i.test(value) && TSX_COMMAND.test(value.slice(13)))) return KEEP_NODE_OPTIONS;
119
+ }
120
+ /**
121
+ * Why `vpx` would fail on a script that tsx runs, judged from its source. tsx resolves
122
+ * `require('./a.js')` to `a.ts`, and compiles ES module syntax to CommonJS in a `.cts`
123
+ * file and in a package without `"type": "module"`. `vpx` resolves `require()` as
124
+ * Node.js does, rejects ES module syntax in a `.cts` file, and runs the other files as ES
125
+ * modules, so `__dirname`, `__filename`, and `require` are not defined.
126
+ */
127
+ function typescriptScriptKeepReason(cwd, script, runner) {
128
+ if (cwd === void 0 || /[*?[{]/.test(script) || !SCRIPT_EXTENSIONS.some((extension) => script.endsWith(extension))) return;
129
+ const file = script.startsWith("~/") ? path.join(os.homedir(), script.slice(2)) : path.resolve(cwd, script);
130
+ let source;
131
+ try {
132
+ source = fs.readFileSync(file, "utf8");
133
+ } catch (error) {
134
+ return error.code === "ENOENT" ? `\`${script}\` does not exist yet, so vp migrate cannot check it` : void 0;
57
135
  }
58
- if (quote) return;
59
- if (active) tokens.push({
60
- value,
61
- start,
62
- end: command.length
63
- });
64
- return tokens;
136
+ if (runner === "tsx") for (const [, base, extension] of source.matchAll(RELATIVE_REQUIRE)) {
137
+ const resolve = (ext) => path.resolve(path.dirname(file), `${base}.${ext}`);
138
+ const typescript = fs.existsSync(resolve(extension)) ? void 0 : TYPESCRIPT_EXTENSIONS[extension].find((ext) => fs.existsSync(resolve(ext)));
139
+ if (typescript) return `\`${script}\` requires \`${base}.${extension}\`, which only ${runner} resolves to \`${base}.${typescript}\`; write the TypeScript extension`;
140
+ }
141
+ const code = stripComments(source);
142
+ if (!/\.(?:c?ts|tsx|jsx?)$/.test(script) || !(ESM_SYNTAX.test(source) || ESM_SYNTAX.test(code))) return;
143
+ if (script.endsWith(".cts")) return `\`${script}\` uses import or export, which \`vpx\` does not compile to CommonJS; rename it to .mts`;
144
+ if ((usesCommonJsGlobals(code) || EXPORT_ASSIGNMENT.test(code)) && readNearestPackageJson(path.dirname(file))?.type !== "module") return `\`${script}\` mixes import or export with CommonJS globals such as __dirname, require, or module.exports, and \`vpx\` runs it as an ES module; use import.meta.dirname, import.meta.filename, createRequire(), or export`;
65
145
  }
66
- /** Read command lists without executing a shell. Directory-changing builtins,
67
- * expansions, pipelines, and shell programs remain unresolved. */
68
- function literalTestCommands(command) {
69
- const commands = [];
70
- let quote = "";
71
- let start = 0;
72
- const append = (end) => {
73
- const tokens = literalArgv(command.slice(start, end));
74
- if (!tokens?.length) return false;
75
- const values = tokens.map(({ value }) => value);
76
- while (/^[A-Za-z_]\w*=/.test(values[0] ?? "")) values.shift();
77
- if (!values.length || [
78
- "cd",
79
- "pushd",
80
- "popd",
81
- ".",
82
- "source",
83
- "eval",
84
- "export",
85
- "set",
86
- "unset"
87
- ].includes(values[0])) return false;
88
- commands.push(values);
89
- return true;
146
+ /** `source` without comments; a string that only looks like one, as in `'http://x'`, stays. */
147
+ function stripComments(source) {
148
+ return source.replaceAll(/("(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|`(?:\\.|[^`\\])*`)|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (_, string) => string ?? "");
149
+ }
150
+ /**
151
+ * Whether `code`, without comments, uses a CommonJS global it does not declare or import
152
+ * at module level, as in `const require = createRequire(import.meta.url)` or `import
153
+ * module from 'node:module'`. A declaration inside a function, which an unindented line
154
+ * does not start, covers only that function.
155
+ */
156
+ function usesCommonJsGlobals(code) {
157
+ return COMMONJS_GLOBALS.some(([name, use]) => use.test(code) && !new RegExp(`^(?:(?:export\\s+)?(?:const|let|var|function|class)\\s+${name}\\b|import\\s+(?:\\*\\s+as\\s+)?${name}\\b)`, "m").test(code));
158
+ }
159
+ /** Whether a JSDoc type annotation or import tag needs tsx's declarations. */
160
+ function jsdocUsesTsx(value) {
161
+ if (!value.startsWith("*")) return false;
162
+ const text = value.replaceAll(/^\s*\* ?/gm, "").replaceAll(/```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)/g, "");
163
+ for (const match of text.matchAll(JSDOC_TYPE_TAG)) {
164
+ const [tag, name] = match;
165
+ if (jsdocTagUsesTsx(name, text.slice(match.index + tag.length))) return true;
166
+ }
167
+ return false;
168
+ }
169
+ function jsdocTagUsesTsx(name, body) {
170
+ if (name === "import") return TSX_JSDOC_IMPORT.test(body);
171
+ if (/^(?:arg|argument|param|prop|property)$/.test(name) && !body.startsWith("{")) {
172
+ const end = body.startsWith("[") ? jsdocDelimitedEnd(body) : JSDOC_PARAMETER_NAME.exec(body)?.[0].length;
173
+ if (end === void 0) return false;
174
+ body = body.slice(end).trimStart();
175
+ }
176
+ if (body.startsWith("{")) {
177
+ const end = jsdocDelimitedEnd(body);
178
+ if (end === void 0) return false;
179
+ if (TSX_IMPORT_TYPE.test(body.slice(0, end))) return true;
180
+ body = body.slice(end).trimStart();
181
+ } else if (/^(?:type|this|enum|augments|extends|implements)$/.test(name)) return jsdocUnbracedTypeUsesTsx(body);
182
+ if (name !== "template") return false;
183
+ while (body) {
184
+ const hasDefault = body.startsWith("[");
185
+ const end = hasDefault ? jsdocDelimitedEnd(body) : JSDOC_TEMPLATE_PARAMETER_NAME.exec(body)?.[0].length;
186
+ if (end === void 0) return false;
187
+ if (hasDefault && TSX_IMPORT_TYPE.test(body.slice(0, end))) return true;
188
+ body = body.slice(end).trimStart();
189
+ if (!body.startsWith(",")) return false;
190
+ body = body.slice(1).trimStart();
191
+ }
192
+ return false;
193
+ }
194
+ /** End of a braced type or bracketed name/default, excluding trailing prose. */
195
+ function jsdocDelimitedEnd(body) {
196
+ const open = body[0];
197
+ const close = open === "{" ? "}" : "]";
198
+ let depth = 0;
199
+ for (const token of body.matchAll(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|[{}[\]]/g)) if (token[0] === open) depth++;
200
+ else if (token[0] === close && --depth === 0) return token.index + 1;
201
+ }
202
+ /** Find the complete unbraced type before a tag's description or the next tag. */
203
+ function jsdocUnbracedTypeUsesTsx(body) {
204
+ if (!TSX_IMPORT_TYPE.test(body)) return false;
205
+ const boundaries = Array.from(body.matchAll(/\s+|$/g)).toReversed();
206
+ for (const boundary of boundaries) {
207
+ const type = body.slice(0, boundary.index);
208
+ if (parseSync("jsdoc.ts", `type T = (${type});`).errors.length > 0) continue;
209
+ if (TSX_IMPORT_TYPE.test(type)) return true;
210
+ const rest = body.slice(boundary.index).trimStart();
211
+ return /^(?:[|&<.([?!:]|extends\b|is\b)/.test(rest) && TSX_IMPORT_TYPE.test(rest);
212
+ }
213
+ return true;
214
+ }
215
+ /** Whether source code or JSDoc types still need tsx. */
216
+ function codeUsesTsx(content, file) {
217
+ if (!content.includes("tsx")) return false;
218
+ if (!SCRIPT_EXTENSIONS.includes(path.extname(file))) return TSX_CODE_REFERENCE.test(content);
219
+ const parsed = parseSync(file, content);
220
+ if (parsed.errors.length > 0) return TSX_CODE_REFERENCE.test(content);
221
+ const isJavaScript = /\.(?:[cm]?js|jsx)$/.test(file);
222
+ let codeWithoutComments = "";
223
+ let offset = 0;
224
+ for (const comment of parsed.comments) {
225
+ if (isJavaScript && comment.type === "Block" && jsdocUsesTsx(comment.value)) return true;
226
+ codeWithoutComments += `${content.slice(offset, comment.start)} `;
227
+ offset = comment.end;
228
+ }
229
+ return TSX_CODE_REFERENCE.test(codeWithoutComments + content.slice(offset));
230
+ }
231
+ async function confirmTsxMigration(interactive) {
232
+ if (interactive) {
233
+ const confirmed = await confirm({
234
+ message: "Migrate tsx scripts to Vite+?\n " + styleText("gray", "`tsx <file>` becomes `vpx <file>`, `tsx watch` becomes `vpx --watch`, and `node --import tsx` uses `vite-plus/register`. The tsx dependency is removed once nothing uses it."),
235
+ initialValue: true
236
+ });
237
+ if (isCancel(confirmed)) cancelAndExit();
238
+ return confirmed;
239
+ }
240
+ log.info("tsx scripts detected. Auto-migrating to Vite+...");
241
+ return true;
242
+ }
243
+ // src/migration/migrator/script-runner.ts
244
+ const LOADER_OPTIONS = /* @__PURE__ */ new Set([
245
+ "--import",
246
+ "--loader",
247
+ "--experimental-loader",
248
+ "--require",
249
+ "-r"
250
+ ]);
251
+ const LOADER_SETTINGS = /* @__PURE__ */ new Set([
252
+ "import",
253
+ "loader",
254
+ "require"
255
+ ]);
256
+ const KEEP_REPL = "`vpx` does not start a TypeScript REPL";
257
+ const KEEP_SHELL = "the command uses shell syntax that vp migrate does not rewrite, such as pipes, variables, or subshells";
258
+ const KEEP_WATCH_TEST = "`vpx --watch` does not run `--test`";
259
+ const IGNORED_FIELDS = /* @__PURE__ */ new Set([
260
+ "bundleDependencies",
261
+ "bundledDependencies",
262
+ "dependencies",
263
+ "description",
264
+ "devDependencies",
265
+ "keywords",
266
+ "name",
267
+ "optionalDependencies",
268
+ "overrides",
269
+ "peerDependencies",
270
+ "peerDependenciesMeta",
271
+ "pnpm",
272
+ "resolutions"
273
+ ]);
274
+ const CODE_EXTENSIONS = /* @__PURE__ */ new Set([
275
+ ...SCRIPT_EXTENSIONS,
276
+ ".astro",
277
+ ".svelte",
278
+ ".vue"
279
+ ]);
280
+ const REFERENCE_EXTENSIONS = /* @__PURE__ */ new Set([
281
+ ...CODE_EXTENSIONS,
282
+ ".json",
283
+ ".jsonc",
284
+ ".toml",
285
+ ".yml",
286
+ ".yaml",
287
+ ".sh",
288
+ ".bash",
289
+ ".zsh",
290
+ ".fish",
291
+ ".ps1",
292
+ ".cmd",
293
+ ".bat"
294
+ ]);
295
+ const LOCKFILES = /* @__PURE__ */ new Set([
296
+ "bun.lock",
297
+ "npm-shrinkwrap.json",
298
+ "package-lock.json",
299
+ "pnpm-lock.yaml",
300
+ "yarn.lock"
301
+ ]);
302
+ const USE_SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
303
+ ...RETENTION_SCAN_SKIP_DIRS,
304
+ ".cache",
305
+ ".next",
306
+ ".nuxt",
307
+ ".svelte-kit",
308
+ ".turbo",
309
+ ".vite",
310
+ "coverage"
311
+ ]);
312
+ const shellCommand = (previous) => /^-[a-z]*c$/.test(previous);
313
+ const COMMAND_LAUNCHERS = /* @__PURE__ */ new Map([
314
+ ["bash", shellCommand],
315
+ ["concurrently", () => true],
316
+ ["nodemon", (previous) => previous === "--exec" || previous === "-x"],
317
+ ["sh", shellCommand],
318
+ ["zsh", shellCommand]
319
+ ]);
320
+ /**
321
+ * Rewrite supported runner and Node.js loader invocations while preserving shell
322
+ * syntax and arguments. Unsupported forms remain, with their reasons in `kept`.
323
+ */
324
+ function rewriteScriptRunnerCommand(runner, command, cwd, environment = [], checkingScripts = /* @__PURE__ */ new Set()) {
325
+ if (!runner.commandPattern.test(command) && !runner.envKeepReason(environment)) return {
326
+ command,
327
+ kept: []
328
+ };
329
+ const segments = parseLiteralCommands(command, true);
330
+ if (!segments || /%\w+%/.test(command)) return {
331
+ command,
332
+ kept: [KEEP_SHELL]
90
333
  };
91
- for (let index = 0; index < command.length; index++) {
92
- const char = command[index];
93
- if (char === "\\") return;
94
- if (quote) {
95
- if (char === quote) quote = "";
96
- } else if (char === "\"" || char === "'") quote = char;
97
- else if (char === "#") return;
98
- else if (char === ";" || command.slice(index, index + 2) === "&&" || command.slice(index, index + 2) === "||") {
99
- if (!append(index)) return;
100
- if (char !== ";") index++;
101
- start = index + 1;
334
+ const rewrite = {
335
+ command,
336
+ cwd,
337
+ environment,
338
+ edits: [],
339
+ kept: [],
340
+ checkingScripts
341
+ };
342
+ for (const tokens of segments) {
343
+ const name = tokens[0].value;
344
+ if (SHELL_STATE_COMMANDS.includes(name) || name === "chdir") {
345
+ rewrite.cwd = void 0;
346
+ continue;
102
347
  }
348
+ rewriteSegment(runner, rewrite, tokens);
103
349
  }
104
- return !quote && append(command.length) ? commands : void 0;
350
+ const rewritten = applyEdits(command, rewrite.edits);
351
+ if (!rewrite.kept.length && runner.commandPattern.test(rewritten)) rewrite.kept.push(`vp migrate does not rewrite this use of ${runner.name}`);
352
+ return {
353
+ command: rewritten,
354
+ kept: rewrite.kept
355
+ };
105
356
  }
106
- /** Locate the runner's arguments after a supported package-manager wrapper. */
107
- function vitestCommandArgsStart(values) {
108
- let start = 0;
357
+ function rewriteSegment(runner, rewrite, tokens) {
358
+ const values = tokens.map(({ value }) => value);
359
+ let first = 0;
360
+ const skipAssignments = () => {
361
+ while (/^[A-Za-z_]\w*=/.test(values[first] ?? "")) first++;
362
+ };
363
+ skipAssignments();
364
+ const shell = values[first] === "cross-env-shell";
365
+ if (values[first] === "cross-env" || shell) {
366
+ first++;
367
+ skipAssignments();
368
+ }
369
+ let unknownEnvironment = false;
370
+ while (values[first] === "env") {
371
+ first++;
372
+ if (values[first] === "--") first++;
373
+ if (values[first]?.startsWith("-")) {
374
+ unknownEnvironment = true;
375
+ break;
376
+ }
377
+ skipAssignments();
378
+ }
379
+ if (first >= values.length) return;
380
+ const assignments = [...rewrite.environment, ...values.slice(0, first)];
381
+ const envReason = runner.envKeepReason(assignments);
382
+ const environment = scanEnvironmentOptions(runner, assignments);
383
+ if (unknownEnvironment) environment.keepReason ??= inheritedOptionsKeepReason(runner);
384
+ environment.keepReason ??= runner.registerEnvKeepReason?.(assignments);
385
+ environment.keepReason ??= environment.preloads.map((script) => runner.scriptKeepReason?.(rewrite.cwd, script)).find((reason) => reason !== void 0);
386
+ const editStart = rewrite.edits.length;
387
+ const keptStart = rewrite.kept.length;
388
+ const start = first + getCommandStartIndex(values.slice(first));
109
389
  if ([
390
+ "env",
391
+ "cross-env",
392
+ "cross-env-shell"
393
+ ].includes(values[start])) environment.keepReason ??= inheritedOptionsKeepReason(runner);
394
+ const launcher = COMMAND_LAUNCHERS.get(values[start]);
395
+ if (values[start] === runner.name || runner.aliases?.includes(values[start])) rewriteRunner(runner, rewrite, tokens, first, start, envReason ?? environment.keepReason, assignments);
396
+ else if (values[start] === "node") rewriteNodeLoader(runner, rewrite, tokens, first, start, envReason, assignments, environment);
397
+ else if (shell || launcher) {
398
+ const commands = shell ? tokens.slice(start) : tokens.slice(start + 1).filter((_, index) => launcher(values[start + index]));
399
+ let checked = commands.length > 0;
400
+ for (const token of commands) if (!rewriteQuotedCommand(runner, rewrite, token, assignments)) checked = false;
401
+ if (!checked && environment.replacements.length && runner.registerEnvRequiresNode) rewrite.kept.push(`vp migrate only replaces ${runner.name} in NODE_OPTIONS when every launcher target is a quoted literal node command`);
402
+ } else if (values.slice(start).some((value) => runner.commandPattern.test(value))) rewrite.kept.push(`vp migrate does not rewrite this use of ${runner.name}`);
403
+ else if (environment.replacements.length && runner.registerEnvRequiresNode) rewrite.kept.push(`vp migrate only replaces ${runner.name} in NODE_OPTIONS for a literal node command`);
404
+ if (!environment.replacements.length) return;
405
+ const taskStart = [
406
+ "npm",
110
407
  "pnpm",
408
+ "yarn",
409
+ "bun"
410
+ ].includes(values[start]) ? start : first;
411
+ const reason = environment.keepReason ?? packageTaskKeepReason(runner, rewrite, values.slice(taskStart), assignments);
412
+ if (reason && rewrite.kept.length === keptStart) rewrite.kept.push(reason);
413
+ if (rewrite.kept.length !== keptStart) {
414
+ rewrite.edits.length = editStart;
415
+ return;
416
+ }
417
+ for (const replacement of environment.replacements) {
418
+ const index = replacement.index - rewrite.environment.length;
419
+ if (index >= 0) rewrite.edits.push(literalEdit(rewrite.command, tokens[index], replacement.value));
420
+ }
421
+ }
422
+ function inheritedOptionsKeepReason(runner) {
423
+ return `\`NODE_OPTIONS\` loads ${runner.name} into a child command that vp migrate cannot check`;
424
+ }
425
+ /** Check package scripts that inherit a preload without changing their definitions here. */
426
+ function taskKeepReason(runner, rewrite, name, inherited, lifecycle) {
427
+ if (!name || !rewrite.cwd) return inheritedOptionsKeepReason(runner);
428
+ const scripts = readNearestPackageJson(rewrite.cwd)?.scripts;
429
+ if (!isPlainRecord(scripts) || typeof scripts[name] !== "string") return inheritedOptionsKeepReason(runner);
430
+ const names = lifecycle ? [
431
+ `pre${name}`,
432
+ name,
433
+ `post${name}`
434
+ ] : [name];
435
+ for (const task of names) {
436
+ const command = scripts[task];
437
+ if (typeof command !== "string") continue;
438
+ const key = `${rewrite.cwd}\0${task}`;
439
+ if (rewrite.checkingScripts.has(key)) return inheritedOptionsKeepReason(runner);
440
+ const checking = /* @__PURE__ */ new Set([...rewrite.checkingScripts, key]);
441
+ const migrated = rewriteScriptRunnerCommand(runner, command, rewrite.cwd, [], checking);
442
+ const reason = migrated.kept[0] ?? rewriteScriptRunnerCommand(runner, migrated.command, rewrite.cwd, inherited, checking).kept[0];
443
+ if (reason) return `script \`${task}\` needs ${runner.name}: ${reason}`;
444
+ }
445
+ }
446
+ function packageTaskKeepReason(runner, rewrite, values, inherited) {
447
+ const [manager, command] = values;
448
+ if (![
111
449
  "npm",
450
+ "pnpm",
112
451
  "yarn",
113
- "bun",
114
- "npx",
115
- "bunx"
116
- ].includes(values[0])) {
117
- start = 1;
118
- if (values[start] === "exec" || values[start] === "x") start++;
119
- if (values[start] === "--") start++;
452
+ "bun"
453
+ ].includes(manager)) return;
454
+ if ([
455
+ "exec",
456
+ "x",
457
+ "dlx"
458
+ ].includes(command)) return;
459
+ const explicitRun = command === "run" || command === "run-script";
460
+ const name = explicitRun ? values[2] : command;
461
+ const expectedLength = explicitRun ? 3 : 2;
462
+ if (values.length !== expectedLength || name?.startsWith("-")) return inheritedOptionsKeepReason(runner);
463
+ return taskKeepReason(runner, rewrite, name, inherited, true);
464
+ }
465
+ function applyEdits(command, pending) {
466
+ let rewritten = command;
467
+ const edits = [];
468
+ for (const edit of pending.toSorted((a, b) => b.start - a.start || b.end - a.end)) {
469
+ const previous = edits.at(-1);
470
+ if (previous?.text === "" && edit.text === "" && edit.end >= previous.start) {
471
+ previous.start = Math.min(previous.start, edit.start);
472
+ previous.end = Math.max(previous.end, edit.end);
473
+ } else edits.push({ ...edit });
474
+ }
475
+ for (const edit of edits) rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
476
+ return rewritten;
477
+ }
478
+ /** Preserve the surrounding quote style when replacing one complete shell token. */
479
+ function literalEdit(command, token, value) {
480
+ const raw = command.slice(token.start, token.end);
481
+ const prefix = /^([A-Za-z_]\w*=)/.exec(raw)?.[1] ?? "";
482
+ const quote = raw[prefix.length];
483
+ const preserveQuote = prefix !== "" || raw.endsWith(quote);
484
+ let content = value.slice(prefix.length);
485
+ if (quote === "'" && preserveQuote) content = `'${content.replaceAll("'", "'\\''")}'`;
486
+ else if (quote === "\"" && preserveQuote || !/^[\w./=:-]+$/.test(content)) content = `"${content.replaceAll(/[\\"$`]/g, "\\$&")}"`;
487
+ return {
488
+ start: token.start,
489
+ end: token.end,
490
+ text: `${prefix}${content}`
491
+ };
492
+ }
493
+ /** NODE_OPTIONS uses double quotes and spaces, not shell single quotes or tab separators. */
494
+ function scanEnvironmentOptions(runner, assignments) {
495
+ const result = {
496
+ replacements: [],
497
+ preloads: [],
498
+ args: []
499
+ };
500
+ for (const [index, assignment] of assignments.entries()) {
501
+ if (!/^NODE_OPTIONS=/i.test(assignment)) continue;
502
+ const value = assignment.slice(13);
503
+ const segments = value.includes("'") || value.replaceAll(/"[^"]*"/g, "").includes(" ") ? void 0 : parseLiteralCommands(value, false);
504
+ if (!segments || segments.length !== 1) {
505
+ result.args = void 0;
506
+ if (runner.commandPattern.test(value)) result.keepReason = runner.envKeepReason([assignment]) ?? `NODE_OPTIONS loads ${runner.name} hooks that vp migrate cannot replace`;
507
+ continue;
508
+ }
509
+ result.args?.push(...segments[0].map(({ value }) => value));
510
+ const scanned = scanOptions(runner, segments[0], 0, false);
511
+ result.preloads.push(...scanned.preloads);
512
+ result.keepReason ??= scanned.registerKeepReason;
513
+ if (scanned.loaders !== scanned.imports.length || scanned.entryIndex !== segments[0].length) {
514
+ if (runner.commandPattern.test(value)) result.keepReason = runner.envKeepReason([assignment]) ?? `NODE_OPTIONS loads ${runner.name} hooks that vp migrate cannot replace`;
515
+ continue;
516
+ }
517
+ if (scanned.imports.length) result.replacements.push({
518
+ index,
519
+ value: `${assignment.slice(0, 13)}${applyEdits(value, importEdits(value, scanned))}`
520
+ });
521
+ }
522
+ return result;
523
+ }
524
+ /**
525
+ * An option's name without `=value`. Node.js reads `_` as `-` in long option names, as
526
+ * in `--watch_path`, and so does `vpx`.
527
+ */
528
+ function optionName(arg) {
529
+ const name = arg.split("=", 1)[0];
530
+ return name.startsWith("--") ? `--${name.slice(2).replaceAll("_", "-")}` : name;
531
+ }
532
+ /** Remove `tokens[index]` with the space before it. */
533
+ function removal(tokens, index) {
534
+ return {
535
+ start: tokens[index - 1]?.end ?? tokens[index].start,
536
+ end: tokens[index].end,
537
+ text: ""
538
+ };
539
+ }
540
+ /**
541
+ * Why `vpx` cannot run an invocation: `positionals` are the entry
542
+ * and its arguments, or with `--test`, the test files; `preloads` are local `--require`
543
+ * and `--import` modules, which load first.
544
+ */
545
+ function runKeepReason(runner, cwd, test, positionals, preloads) {
546
+ const [entry] = positionals;
547
+ if (entry === void 0) return test ? runner.testWithoutFilesReason : KEEP_REPL;
548
+ if (!test && !SCRIPT_EXTENSIONS.some((extension) => entry.endsWith(extension))) return `\`${entry}\` has no file extension, which \`vpx\` needs to run it as a script`;
549
+ const scripts = test ? positionals.filter((value) => !value.startsWith("-")) : [entry];
550
+ for (const script of [...preloads, ...scripts]) {
551
+ const reason = runner.scriptKeepReason?.(cwd, script);
552
+ if (reason) return reason;
553
+ }
554
+ }
555
+ function rewriteRunner(runner, rewrite, tokens, first, start, keepReason, environment) {
556
+ if (keepReason) {
557
+ rewrite.kept.push(keepReason);
558
+ return;
559
+ }
560
+ const prepared = prepareRun(runner, rewrite, tokens, start, environment, false);
561
+ if (prepared.keepReason) {
562
+ rewrite.kept.push(prepared.keepReason);
563
+ return;
564
+ }
565
+ tokens = prepared.tokens;
566
+ let index = start + 1;
567
+ const watch = runner.watchSubcommand !== false && tokens[index]?.value === "watch";
568
+ if (watch) index++;
569
+ const scanned = scanOptions(runner, tokens, index, true);
570
+ const reason = runReason(runner, rewrite.cwd, tokens, scanned, watch);
571
+ if (reason) {
572
+ rewrite.kept.push(reason);
573
+ return;
574
+ }
575
+ rewrite.edits.push({
576
+ start: tokens[first].start,
577
+ end: tokens[index - 1].end,
578
+ text: watch ? "vpx --watch" : "vpx"
579
+ }, ...prepared.edits, ...scanned.removals);
580
+ }
581
+ function rewriteNodeLoader(runner, rewrite, tokens, first, start, keepReason, environment, environmentOptions) {
582
+ const original = scanOptions(runner, tokens, start + 1, false);
583
+ if (runner.isRegister && original.loaders === original.imports.length && (original.imports.length || environmentOptions.replacements.length)) {
584
+ const prepared = prepareRun(runner, rewrite, tokens, start, environment, true, environmentOptions);
585
+ const scanned = scanOptions(runner, prepared.tokens, start + 1, false);
586
+ const scripts = scanned.inlineCode ? [] : prepared.tokens.slice(scanned.entryIndex).map(({ value }) => value);
587
+ let reason = environmentOptions.keepReason ?? prepared.keepReason ?? scanned.registerKeepReason;
588
+ if (reason === void 0 && environmentOptions.replacements.length && scanned.runScript !== void 0) reason = taskKeepReason(runner, rewrite, scanned.entryIndex < prepared.tokens.length ? void 0 : scanned.runScript, environment, false);
589
+ reason ??= [...scanned.preloads, ...scanned.test ? scripts : scripts.slice(0, 1)].map((script) => runner.scriptKeepReason?.(rewrite.cwd, script)).find((reason) => reason !== void 0);
590
+ if (reason) rewrite.kept.push(reason);
591
+ else rewrite.edits.push(...prepared.edits, ...importEdits(rewrite.command, scanned));
592
+ return;
593
+ }
594
+ if (!original.loaders) {
595
+ const environmentLoader = environment.some((assignment) => /^NODE_OPTIONS=/i.test(assignment) && runner.commandPattern.test(assignment.slice(13)));
596
+ const reason = original.runnerLoader ? original.keepReason : environmentLoader ? keepReason : void 0;
597
+ if (reason) rewrite.kept.push(reason);
598
+ return;
599
+ }
600
+ const prepared = prepareRun(runner, rewrite, tokens, start, environment, true);
601
+ if (prepared.keepReason) {
602
+ rewrite.kept.push(prepared.keepReason);
603
+ return;
604
+ }
605
+ tokens = prepared.tokens;
606
+ const scanned = scanOptions(runner, tokens, start + 1, false);
607
+ if (!scanned.loaders) {
608
+ if (keepReason) rewrite.kept.push(keepReason);
609
+ return;
610
+ }
611
+ const reason = keepReason ?? runReason(runner, rewrite.cwd, tokens, scanned, false);
612
+ if (reason) {
613
+ rewrite.kept.push(reason);
614
+ return;
615
+ }
616
+ rewrite.edits.push({
617
+ start: tokens[first].start,
618
+ end: tokens[start].end,
619
+ text: "vpx"
620
+ }, ...prepared.edits, ...scanned.removals);
621
+ }
622
+ /** Preserve original source ranges while normalizing runner-specific options. */
623
+ function prepareRun(runner, rewrite, tokens, start, environment, nodeLoader, publicRegister) {
624
+ const prepared = runner.prepareRun?.(tokens.slice(start + 1).map(({ value }) => value), {
625
+ cwd: rewrite.cwd,
626
+ environment,
627
+ nodeLoader,
628
+ executable: tokens[start].value,
629
+ publicRegister: publicRegister !== void 0,
630
+ nodeOptionsArgs: publicRegister?.args
631
+ });
632
+ const edits = [];
633
+ const added = prepared?.prependArgs?.map(quoteArgument) ?? [];
634
+ if (added.some((value) => value === void 0) || prepared?.edits?.some((edit) => edit.value !== void 0 && quoteArgument(edit.value) === void 0)) return {
635
+ tokens,
636
+ edits,
637
+ keepReason: "a rewritten argument needs shell escaping that vp migrate does not support"
638
+ };
639
+ const changes = new Map(prepared?.edits?.map((edit) => [start + 1 + edit.index, edit]));
640
+ const normalized = tokens.flatMap((token, index) => {
641
+ const change = changes.get(index);
642
+ if (!change) return [token];
643
+ if (change.value === void 0) {
644
+ edits.push(removal(tokens, index));
645
+ return [];
646
+ }
647
+ const text = quoteArgument(change.value);
648
+ edits.push({
649
+ start: token.start,
650
+ end: token.end,
651
+ text
652
+ });
653
+ return [{
654
+ ...token,
655
+ value: change.value
656
+ }];
657
+ });
658
+ if (prepared?.prependArgs?.length) {
659
+ const position = tokens[start].end;
660
+ edits.push({
661
+ start: position,
662
+ end: position,
663
+ text: ` ${added.join(" ")}`
664
+ });
665
+ normalized.splice(start + 1, 0, ...prepared.prependArgs.map((value) => ({
666
+ ...tokens[start],
667
+ value,
668
+ start: position,
669
+ end: position
670
+ })));
671
+ }
672
+ return {
673
+ tokens: normalized,
674
+ edits,
675
+ keepReason: prepared?.keepReason
676
+ };
677
+ }
678
+ function quoteArgument(value) {
679
+ if (/^[\w./:=@+-]+$/.test(value)) return value;
680
+ return /^[\w./:=@+ -]+$/.test(value) ? `"${value}"` : void 0;
681
+ }
682
+ function importEdits(command, scanned) {
683
+ const edits = [];
684
+ for (const { token, value, option } of scanned.imports) {
685
+ if (option) edits.push(literalEdit(command, option, "--import"));
686
+ edits.push(literalEdit(command, token, value));
687
+ }
688
+ return edits;
689
+ }
690
+ /** Read options from the executable to the entry file. */
691
+ function scanOptions(runner, tokens, index, directRunner) {
692
+ const scanned = {
693
+ entryIndex: tokens.length,
694
+ removals: [],
695
+ loaders: 0,
696
+ runnerLoader: false,
697
+ imports: [],
698
+ inlineCode: false,
699
+ preloads: [],
700
+ test: false,
701
+ watch: false
702
+ };
703
+ for (; index < tokens.length; index++) {
704
+ const { value } = tokens[index];
705
+ if (value === "--" || !value.startsWith("-")) {
706
+ scanned.entryIndex = value === "--" ? index + 1 : index;
707
+ break;
708
+ }
709
+ const name = optionName(value);
710
+ const inline = value.includes("=");
711
+ const argument = inline ? value.slice(value.indexOf("=") + 1) : tokens[index + 1]?.value;
712
+ if (directRunner && runner.dropOptions?.has(name)) {
713
+ scanned.removals.push(removal(tokens, index));
714
+ continue;
715
+ }
716
+ if (LOADER_OPTIONS.has(name)) {
717
+ const loader = argument;
718
+ if (loader !== void 0 && runner.loaderPattern.test(loader)) {
719
+ scanned.runnerLoader = true;
720
+ if (runner.isLoader && !runner.isLoader(name, loader)) {
721
+ scanned.keepReason ??= `vp migrate does not rewrite ${name} ${loader}`;
722
+ scanned.registerKeepReason ??= scanned.keepReason;
723
+ if (!inline) index++;
724
+ continue;
725
+ }
726
+ scanned.removals.push({
727
+ ...removal(tokens, index),
728
+ end: tokens[inline ? index : index + 1].end
729
+ });
730
+ scanned.loaders++;
731
+ if (runner.isRegister?.(name, loader)) scanned.imports.push({
732
+ token: tokens[inline ? index : index + 1],
733
+ value: inline ? "--import=vite-plus/register" : "vite-plus/register",
734
+ option: !inline && name !== "--import" ? tokens[index] : void 0
735
+ });
736
+ } else if (loader !== void 0) {
737
+ const reason = runner.registerPreloadKeepReason?.(name, loader);
738
+ scanned.registerKeepReason ??= reason;
739
+ if (directRunner) scanned.keepReason ??= reason;
740
+ if (loader.startsWith(".") || path.isAbsolute(loader)) scanned.preloads.push(loader);
741
+ }
742
+ if (!inline) index++;
743
+ continue;
744
+ }
745
+ scanned.keepReason ??= runner.keepOptions.get(name);
746
+ scanned.registerKeepReason ??= runner.registerKeepOptions?.get(name);
747
+ if (runner.registerInlineCodeKeepReason) {
748
+ const code = [
749
+ "-e",
750
+ "-p",
751
+ "-pe",
752
+ "--eval",
753
+ "--print"
754
+ ].includes(name) ? argument : /^-(?:pe|ep|e|p)(.+)$/.exec(value)?.[1];
755
+ if (code !== void 0) scanned.registerKeepReason ??= runner.registerInlineCodeKeepReason(code);
756
+ }
757
+ scanned.test ||= name === "--test";
758
+ scanned.watch ||= name === "--watch";
759
+ scanned.inlineCode ||= [
760
+ "-e",
761
+ "-p",
762
+ "-pe",
763
+ "--eval",
764
+ "--print",
765
+ "--run"
766
+ ].includes(name);
767
+ if (name === "--run") scanned.runScript = argument ?? "";
768
+ if (!inline && (NODE_OPTIONS_WITH_VALUE.has(name) || directRunner && runner.optionsWithValue?.has(name))) index++;
769
+ }
770
+ return scanned;
771
+ }
772
+ /** Why `vpx` cannot run what the scanned options and the scripts after them run. */
773
+ function runReason(runner, cwd, tokens, scanned, watch) {
774
+ if (scanned.keepReason) return scanned.keepReason;
775
+ if ((watch || scanned.watch) && scanned.test) return KEEP_WATCH_TEST;
776
+ return runKeepReason(runner, cwd, scanned.test, tokens.slice(scanned.entryIndex).map(({ value }) => value), scanned.preloads);
777
+ }
778
+ /** Rewrite a quoted launcher command with the environment it inherits. */
779
+ function rewriteQuotedCommand(runner, rewrite, token, environment) {
780
+ const raw = rewrite.command.slice(token.start, token.end);
781
+ const quote = raw[0];
782
+ if (quote !== "\"" && quote !== "'" || raw.length < 2 || !raw.endsWith(quote)) return false;
783
+ const inner = rewriteScriptRunnerCommand(runner, token.value, rewrite.cwd, environment, rewrite.checkingScripts);
784
+ rewrite.kept.push(...inner.kept);
785
+ if (inner.command !== token.value) rewrite.edits.push(literalEdit(rewrite.command, token, inner.command));
786
+ return token.value.trim() !== "";
787
+ }
788
+ /** The commands in a scripts or lint-staged record. */
789
+ function recordCommands(record) {
790
+ if (!isPlainRecord(record)) return [];
791
+ return Object.values(record).flatMap((value) => Array.isArray(value) ? value : [value]).filter((command) => typeof command === "string");
792
+ }
793
+ /** The commands a package.json runs: its scripts and lint-staged tasks. */
794
+ function packageCommands(pkg) {
795
+ return [...recordCommands(pkg.scripts), ...recordCommands(pkg["lint-staged"])];
796
+ }
797
+ function lintStagedJsonFiles(dir) {
798
+ return LINT_STAGED_JSON_CONFIG_FILES.map((name) => path.join(dir, name)).filter((file) => fs.existsSync(file) && isJsonFile(file));
799
+ }
800
+ /**
801
+ * A package that lists a runner outside devDependencies keeps it: published code or
802
+ * production scripts can need it, and `vpx` comes from the vite-plus devDependency.
803
+ */
804
+ function ownsRunnerAtRuntime(runner, pkg) {
805
+ return runner.dependencies.some((dependency) => pkg.dependencies?.[dependency] !== void 0 || pkg.optionalDependencies?.[dependency] !== void 0 || pkg.peerDependencies?.[dependency] !== void 0);
806
+ }
807
+ function packageDirs(rootDir, packages) {
808
+ return [rootDir, ...(packages ?? []).map((pkg) => path.join(rootDir, pkg.path))];
809
+ }
810
+ /**
811
+ * Detect supported invocations, or retained uses when the profile requests warnings.
812
+ */
813
+ function detectScriptRunnerProject(runner, rootDir, packages) {
814
+ return packageDirs(rootDir, packages).some((dir) => {
815
+ const pkg = readPackageJsonIfExists(path.join(dir, "package.json"));
816
+ if (!pkg || !runner.detectKeptUses && ownsRunnerAtRuntime(runner, pkg)) return false;
817
+ return [...packageCommands(pkg), ...lintStagedJsonFiles(dir).flatMap((file) => recordCommands(readJsonFile(file)))].some((command) => runner.detectKeptUses ? runner.commandPattern.test(command) : rewriteScriptRunnerCommand(runner, command, dir).command !== command) || runner.detectKeptUses && findRunnerUse(runner, dir, ownsRunnerAtRuntime(runner, pkg) || runner.dependencies.some((dependency) => pkg.devDependencies?.[dependency] !== void 0)) !== void 0 || sourceTreeMatches(dir, (content, file) => {
818
+ const result = rewriteRunnerShebang(runner, content, file, dir);
819
+ return result !== void 0 && "content" in result;
820
+ }, { includeExtensionless: runner.includeExtensionlessShebangs });
821
+ });
822
+ }
823
+ /** Rewrite each command of a scripts or lint-staged record; returns whether one changed. */
824
+ function rewriteCommandRecord(runner, record, label, cwd, report) {
825
+ let edited = false;
826
+ for (const [key, value] of Object.entries(record)) {
827
+ const commands = Array.isArray(value) ? value : [value];
828
+ const rewritten = commands.map((command) => {
829
+ if (typeof command !== "string") return command;
830
+ const result = rewriteScriptRunnerCommand(runner, command, cwd);
831
+ for (const reason of result.kept) warnMigration(`Kept ${runner.name} in ${label} "${key}": ${reason}.`, report);
832
+ return result.command;
833
+ });
834
+ if (rewritten.some((command, index) => command !== commands[index])) {
835
+ record[key] = Array.isArray(value) ? rewritten : rewritten[0];
836
+ edited = true;
837
+ }
838
+ }
839
+ return edited;
840
+ }
841
+ /**
842
+ * The migrated runner or Node.js preload shebang, or its retention reason.
843
+ */
844
+ function rewriteRunnerShebang(runner, content, file, packageDir) {
845
+ const match = runner.envShebang.exec(content);
846
+ if (!match) return;
847
+ const [line, prefix, command] = match;
848
+ const entry = quoteArgument(`./${path.basename(file)}`);
849
+ if (entry === void 0) return { reason: "the script path needs shell escaping that vp migrate does not support" };
850
+ const shebangRunner = runner.prepareRun ? {
851
+ ...runner,
852
+ prepareRun: (args, context) => runner.prepareRun(args, {
853
+ ...context,
854
+ cwd: path.dirname(file),
855
+ shebang: true
856
+ })
857
+ } : runner;
858
+ const original = `${command} ${entry}`;
859
+ const result = rewriteScriptRunnerCommand(shebangRunner, original);
860
+ if (result.command === original && !result.kept.length) return;
861
+ const tokens = parseLiteralCommands(command, true)?.[0];
862
+ const executable = tokens?.[0]?.value;
863
+ const directRunner = executable === runner.name || executable !== void 0 && runner.aliases?.includes(executable) === true;
864
+ const optionsStart = directRunner && runner.watchSubcommand !== false && tokens?.[1]?.value === "watch" ? 2 : 1;
865
+ const preloads = tokens && (executable === "node" || directRunner) ? scanOptions(runner, tokens, optionsStart, directRunner).preloads : [];
866
+ const reason = result.kept[0] ?? preloads.map((script) => runner.scriptKeepReason?.(packageDir, script)).find((reason) => reason !== void 0) ?? runner.scriptKeepReason?.(path.dirname(file), `./${path.basename(file)}`);
867
+ if (reason) return { reason };
868
+ const rewritten = result.command.slice(0, -` ${entry}`.length);
869
+ if (!path.extname(file) && /^vpx(?:\s|$)/.test(rewritten)) return;
870
+ return { content: `${prefix}${rewritten}${content.slice(line.length)}` };
871
+ }
872
+ /** Rewrite matching runner and Node.js preload shebangs in a package's scripts. */
873
+ function rewriteRunnerShebangs(runner, dir, report) {
874
+ let changed = false;
875
+ sourceTreeMatches(dir, (content, file) => {
876
+ const result = rewriteRunnerShebang(runner, content, file, dir);
877
+ if (result && "reason" in result) warnMigration(`Kept ${runner.name} in the shebang of ${displayRelative(file)}: ${result.reason}.`, report);
878
+ else if (result) {
879
+ fs.writeFileSync(file, result.content);
880
+ changed = true;
881
+ }
882
+ return false;
883
+ }, { includeExtensionless: runner.includeExtensionlessShebangs });
884
+ return changed;
885
+ }
886
+ /**
887
+ * Find a remaining use, including nested packages and build output that can resolve
888
+ * the package's runner dependencies.
889
+ */
890
+ function findRunnerUse(runner, dir, retainUnverified = true) {
891
+ let found;
892
+ sourceTreeMatches(dir, (content, file) => {
893
+ if (LOCKFILES.has(path.basename(file))) return false;
894
+ if (fileUsesRunner(runner, content, file)) found = `${displayRelative(file)} still uses ${runner.name}`;
895
+ else if (path.basename(file) !== "package.json") found = environmentFileUse(runner, content, void 0, retainUnverified);
896
+ return found !== void 0;
897
+ }, {
898
+ crossPackageBoundaries: true,
899
+ extensions: REFERENCE_EXTENSIONS,
900
+ fileNamePattern: runner.referenceFileNamePattern,
901
+ includeExtensionless: true,
902
+ includePackageReferences: true,
903
+ matchesPackage: (packageDir, pkg) => {
904
+ const field = Object.entries(pkg).find(([key, value]) => !IGNORED_FIELDS.has(key) && jsonUsesRunner(runner, value, [
905
+ "imports",
906
+ "lint-staged",
907
+ "scripts"
908
+ ].includes(key)))?.[0];
909
+ if (field !== void 0) found = `\`${field}\` in ${displayRelative(path.join(packageDir, "package.json"))} still uses ${runner.name}`;
910
+ else {
911
+ found = packageCommands(pkg).map((command) => environmentFileUse(runner, command, packageDir, retainUnverified)).find((reason) => reason !== void 0);
912
+ found ??= environmentFileUse(runner, JSON.stringify(Object.fromEntries(Object.entries(pkg).filter(([key]) => !IGNORED_FIELDS.has(key) && key !== "scripts" && key !== "lint-staged"))), void 0, retainUnverified);
913
+ }
914
+ return found !== void 0;
915
+ },
916
+ skipDirs: USE_SCAN_SKIP_DIRS
917
+ });
918
+ return found;
919
+ }
920
+ const ENV_FILE_OPTION = /--env[-_]file(?:[-_]if[-_]exists)?(?=$|[=\s'"])/;
921
+ /** Explicit Node.js environment inputs need checking regardless of their extension. */
922
+ function environmentFileUse(runner, command, cwd, retainUnverified = true) {
923
+ if (!ENV_FILE_OPTION.test(command)) return;
924
+ const unchecked = retainUnverified ? `an explicit Node.js env file could load ${runner.name}, but vp migrate cannot check it` : void 0;
925
+ const segments = parseLiteralCommands(command, true);
926
+ if (!cwd || !segments || /%\w+%/.test(command)) return unchecked;
927
+ for (const tokens of segments) {
928
+ const values = tokens.map(({ value }) => value);
929
+ if (SHELL_STATE_COMMANDS.includes(values[0]) || values[0] === "chdir") cwd = void 0;
930
+ if (!values.some((value) => ENV_FILE_OPTION.test(value))) continue;
931
+ const start = getCommandStartIndex(values);
932
+ if (!cwd || values[start] !== "node") return unchecked;
933
+ for (let index = start + 1; index < values.length; index++) {
934
+ const value = values[index];
935
+ if (value === "--" || !value.startsWith("-")) break;
936
+ const name = optionName(value);
937
+ const inline = value.includes("=");
938
+ const takesArgument = NODE_OPTIONS_WITH_VALUE.has(name);
939
+ const argument = inline ? value.slice(value.indexOf("=") + 1) : values[index + 1];
940
+ if (name === "--env-file" || name === "--env-file-if-exists") {
941
+ if (!argument) return unchecked;
942
+ const file = path.resolve(cwd, argument);
943
+ try {
944
+ if (runner.textReference.test(fs.readFileSync(file, "utf8"))) return `${displayRelative(file)} still uses ${runner.name}`;
945
+ } catch {
946
+ if (retainUnverified) return `the env file ${displayRelative(file)} could load ${runner.name}, but vp migrate cannot read it`;
947
+ }
948
+ } else if (ENV_FILE_OPTION.test(value) || takesArgument && argument && ENV_FILE_OPTION.test(argument)) return unchecked;
949
+ if (!inline && takesArgument) index++;
950
+ }
951
+ }
952
+ }
953
+ /** Whether a file still runs, imports, or loads the runner. */
954
+ function fileUsesRunner(runner, content, file) {
955
+ if (runner.shebangPattern.test(content)) return true;
956
+ const name = path.basename(file);
957
+ if (runner.referenceFileNamePattern?.test(name)) return runner.textReference.test(content);
958
+ if (LINT_STAGED_JSON_CONFIG_FILES.some((config) => config === name) && jsonUsesRunner(runner, parse(content), true)) return true;
959
+ const extension = path.extname(file);
960
+ if (extension === ".json" || extension === ".jsonc") return jsonUsesRunner(runner, parse(content), false);
961
+ if (CODE_EXTENSIONS.has(extension)) return runner.codeUsesRunner?.(content, file) ?? runner.codeReference.test(content);
962
+ return runner.textReference.test(content) || extension === "" && runner.codeReference.test(content);
963
+ }
964
+ /** Recognize command strings and loader references in structured configuration. */
965
+ function jsonUsesRunner(runner, value, commands) {
966
+ if (typeof value === "string") return commands && runner.commandPattern.test(value) || runner.configValue.test(value);
967
+ if (Array.isArray(value)) return value.some((item, index) => jsonUsesRunner(runner, item, commands) || typeof item === "string" && LOADER_OPTIONS.has(item) && runner.loaderPattern.test(String(value[index + 1])));
968
+ return isPlainRecord(value) && Object.entries(value).some(([key, item]) => jsonUsesRunner(runner, item, commands) || LOADER_SETTINGS.has(key) && [item].flat().some((loader) => runner.loaderPattern.test(String(loader))));
969
+ }
970
+ /** Rewrite known command surfaces, then remove dependencies with no remaining uses. */
971
+ function migrateScriptRunnerToVpx(runner, rootDir, packages, report) {
972
+ let changed = false;
973
+ const migrated = [];
974
+ for (const dir of packageDirs(rootDir, packages)) {
975
+ const packageJsonPath = path.join(dir, "package.json");
976
+ const pkg = readPackageJsonIfExists(packageJsonPath);
977
+ if (!pkg) continue;
978
+ if (ownsRunnerAtRuntime(runner, pkg)) {
979
+ if (packageCommands(pkg).some((c) => runner.commandPattern.test(c))) warnMigration(`Kept ${runner.name} in ${displayRelative(packageJsonPath)}: it is listed in dependencies, optionalDependencies, or peerDependencies there, and \`vpx\` comes from the vite-plus devDependency.`, report);
980
+ continue;
981
+ }
982
+ migrated.push(dir);
983
+ editJsonFile(packageJsonPath, (json) => {
984
+ let edited = false;
985
+ for (const field of ["scripts", "lint-staged"]) {
986
+ const record = json[field];
987
+ if (isPlainRecord(record) && rewriteCommandRecord(runner, record, `${displayRelative(packageJsonPath)} ${field}`, dir, report)) edited = true;
988
+ }
989
+ changed ||= edited;
990
+ return edited ? json : void 0;
991
+ });
992
+ for (const file of lintStagedJsonFiles(dir)) editJsonFile(file, (config) => {
993
+ const edited = rewriteCommandRecord(runner, config, displayRelative(file), dir, report);
994
+ changed ||= edited;
995
+ return edited ? config : void 0;
996
+ });
997
+ if (rewriteRunnerShebangs(runner, dir, report)) changed = true;
998
+ }
999
+ let dependencyRemoved = false;
1000
+ for (const dir of migrated) {
1001
+ const packageJsonPath = path.join(dir, "package.json");
1002
+ const devDependencies = readPackageJsonIfExists(packageJsonPath)?.devDependencies;
1003
+ if (!runner.dependencies.some((dependency) => devDependencies?.[dependency] !== void 0)) continue;
1004
+ const use = findRunnerUse(runner, dir);
1005
+ if (use) {
1006
+ warnMigration(`Kept the ${runner.dependencies.join(" / ")} devDependency in ${displayRelative(packageJsonPath)}: ${use}.`, report);
1007
+ continue;
1008
+ }
1009
+ editJsonFile(packageJsonPath, (json) => {
1010
+ for (const dependency of runner.dependencies) delete json.devDependencies[dependency];
1011
+ return json;
1012
+ });
1013
+ dependencyRemoved = true;
1014
+ }
1015
+ return {
1016
+ changed: changed || dependencyRemoved,
1017
+ dependencyRemoved,
1018
+ scriptsChanged: changed
1019
+ };
1020
+ }
1021
+ // src/migration/migrator/oxnode.ts
1022
+ const OXNODE_COMMAND = /(?:^|[\s;&|("'=/\\`])oxnode(?:\.(?:cmd|exe|ps1))?(?=$|[@\s;&|)"'`])|@oxc-node\/core(?:\/[^\s'"`]+)?/i;
1023
+ const OXNODE_LOADER = /^@oxc-node\/core(?:\/.*)?$/;
1024
+ const OXNODE_PACKAGE_REFERENCE = /@oxc-node\/(?:cli|core)(?=$|[\s/'"`])/;
1025
+ const OXNODE_CODE_REFERENCE = new RegExp([OXNODE_COMMAND.source, OXNODE_PACKAGE_REFERENCE.source].join("|"), "i");
1026
+ const OXNODE_TEXT_REFERENCE = new RegExp([OXNODE_COMMAND.source, OXNODE_PACKAGE_REFERENCE.source].join("|"), "im");
1027
+ const KEEP_NODE_WATCH = "oxnode --watch starts the Node.js watcher, while `vpx --watch` uses its own watcher; use oxnode watch to migrate it";
1028
+ const KEEP_TRANSFORM_DEPS = "`OXC_TRANSFORM_ALL` changes dependency transforms, while the Vite+ loader always transforms only TypeScript dependencies";
1029
+ const REGISTER_KEEP_OPTIONS = /* @__PURE__ */ new Map([
1030
+ ["--env-file", "an env file can configure oxc-node hooks or dependency transforms; migrate it manually"],
1031
+ ["--env-file-if-exists", "an env file can configure oxc-node hooks or dependency transforms; migrate it manually"],
1032
+ ["--config-file", "a Node.js config file can load additional hooks; migrate it manually"],
1033
+ ["--experimental-config-file", "a Node.js config file can load additional hooks; migrate it manually"],
1034
+ ["--experimental-default-config-file", "a Node.js config file can load additional hooks; migrate it manually"]
1035
+ ]);
1036
+ function isOxnodeRegister(option, value) {
1037
+ return option === "--import" && /^@oxc-node\/core\/register(?:\.mjs)?$/.test(value);
1038
+ }
1039
+ function oxnodeEnvironmentReason(assignments) {
1040
+ for (const assignment of assignments) {
1041
+ const separator = assignment.indexOf("=");
1042
+ const name = assignment.slice(0, separator).toUpperCase();
1043
+ const value = assignment.slice(separator + 1);
1044
+ if (name === "OXC_TRANSFORM_ALL" && value !== "typescript") return KEEP_TRANSFORM_DEPS;
1045
+ if ((name === "VP_SCRIPT_TSCONFIG" || name === "TSX_TSCONFIG_PATH") && value) return `\`${name}\` would select a different configuration for the Vite+ loader`;
1046
+ }
1047
+ }
1048
+ const OXNODE_RUNNER = {
1049
+ name: "oxnode",
1050
+ dependencies: ["@oxc-node/cli", "@oxc-node/core"],
1051
+ commandPattern: OXNODE_COMMAND,
1052
+ loaderPattern: OXNODE_LOADER,
1053
+ isLoader: isOxnodeRegister,
1054
+ isRegister: isOxnodeRegister,
1055
+ registerEnvKeepReason: oxnodeEnvironmentReason,
1056
+ registerKeepOptions: REGISTER_KEEP_OPTIONS,
1057
+ registerPreloadKeepReason(option, value) {
1058
+ if (option === "--loader" || option === "--experimental-loader" || option === "--import" && value === "vite-plus/register") return;
1059
+ return "an additional startup preload can configure oxc-node hooks or dependency transforms; migrate it manually";
1060
+ },
1061
+ registerInlineCodeKeepReason(code) {
1062
+ if (OXNODE_PACKAGE_REFERENCE.test(code)) return "inline code uses a standalone oxc-node API or loader that does not configure the bundled Vite+ loader";
1063
+ },
1064
+ envShebang: /^(#!\s*\S*\benv\s+(?:-S\s+)?)((?:oxnode|node)(?=[ \t\r\n]|$)[^\r\n]*)/,
1065
+ includeExtensionlessShebangs: true,
1066
+ shebangPattern: /^#![^\n]*[\s/]oxnode(?=\s|$)/,
1067
+ codeReference: OXNODE_CODE_REFERENCE,
1068
+ textReference: OXNODE_TEXT_REFERENCE,
1069
+ configValue: OXNODE_TEXT_REFERENCE,
1070
+ referenceFileNamePattern: /^\.env(?:\.|$)/,
1071
+ keepOptions: new Map([
1072
+ ...REGISTER_KEEP_OPTIONS,
1073
+ ["-e", "`vpx` does not run TypeScript passed to --eval or --print"],
1074
+ ["--eval", "`vpx` does not run TypeScript passed to --eval or --print"],
1075
+ ["-p", "`vpx` does not run TypeScript passed to --eval or --print"],
1076
+ ["-pe", "`vpx` does not run TypeScript passed to --eval or --print"],
1077
+ ["--print", "`vpx` does not run TypeScript passed to --eval or --print"],
1078
+ ["-c", "`vpx -c` selects package shell mode instead of Node.js syntax checking"],
1079
+ ["--check", "`vpx` does not transform TypeScript for --check"],
1080
+ ["-i", "`vpx` does not start a TypeScript REPL"],
1081
+ ["--interactive", "`vpx` does not start a TypeScript REPL"],
1082
+ ["--watch", KEEP_NODE_WATCH],
1083
+ ["--watch-path", KEEP_NODE_WATCH],
1084
+ ["--node-help", "`vpx` does not support oxnode --node-help"],
1085
+ ["-h", "oxnode and `vpx` have different help output"],
1086
+ ["--help", "oxnode and `vpx` have different help output"],
1087
+ ["-v", "oxnode and `vpx` have different versions"],
1088
+ ["--version", "oxnode and `vpx` have different versions"]
1089
+ ]),
1090
+ optionsWithValue: /* @__PURE__ */ new Set([...WATCH_OPTIONS_WITH_VALUE, "--tsconfig"]),
1091
+ testWithoutFilesReason: "list the test files explicitly before migrating to `vpx --test`",
1092
+ envKeepReason(assignments) {
1093
+ const reason = oxnodeEnvironmentReason(assignments);
1094
+ if (reason) return reason;
1095
+ for (const assignment of assignments) {
1096
+ const separator = assignment.indexOf("=");
1097
+ const name = assignment.slice(0, separator).toUpperCase();
1098
+ const value = assignment.slice(separator + 1);
1099
+ if (name === "NODE_OPTIONS" && /@oxc-node\/core/.test(value)) return "`NODE_OPTIONS` loads oxc-node hooks that would compete with those of `vpx`; remove it first";
1100
+ }
1101
+ },
1102
+ scriptKeepReason(cwd, script) {
1103
+ if (/[*?[{]/.test(script)) return;
1104
+ let file;
1105
+ if (script.startsWith("~/")) file = path.join(os.homedir(), script.slice(2));
1106
+ else if (path.isAbsolute(script)) file = script;
1107
+ else if (cwd !== void 0) file = path.resolve(cwd, script);
1108
+ else return;
1109
+ try {
1110
+ const source = fs.readFileSync(file, "utf8");
1111
+ if (OXNODE_PACKAGE_REFERENCE.test(source.replace(/^#![^\r\n]*/, ""))) return `\`${script}\` uses a standalone oxc-node API or loader that does not configure the bundled \`vpx\` loader`;
1112
+ } catch {}
1113
+ },
1114
+ detectKeptUses: true
1115
+ };
1116
+ function detectOxnodeProject(rootDir, packages) {
1117
+ return detectScriptRunnerProject(OXNODE_RUNNER, rootDir, packages);
1118
+ }
1119
+ function migrateOxnodeToVpx(rootDir, packages, report) {
1120
+ const result = migrateScriptRunnerToVpx(OXNODE_RUNNER, rootDir, packages, report);
1121
+ if (report && result.scriptsChanged) report.oxnodeMigrated = true;
1122
+ return {
1123
+ changed: result.changed,
1124
+ dependencyRemoved: result.dependencyRemoved
1125
+ };
1126
+ }
1127
+ async function confirmOxnodeMigration(interactive) {
1128
+ if (interactive) {
1129
+ const confirmed = await confirm({
1130
+ message: "Migrate oxnode scripts to Vite+?\n " + styleText("gray", "`oxnode <file>` becomes `vpx <file>`, and `oxnode watch` becomes `vpx --watch`. Node.js imports use `vite-plus/register`. Standalone oxc-node dependencies are removed once nothing uses them."),
1131
+ initialValue: true
1132
+ });
1133
+ if (isCancel(confirmed)) cancelAndExit();
1134
+ return confirmed;
120
1135
  }
1136
+ log.info("oxnode scripts detected. Auto-migrating to Vite+...");
1137
+ return true;
1138
+ }
1139
+ // src/migration/migrator/ts-node.ts
1140
+ const TS_NODE_REFERENCE = /(?:^|[\s;&|("'=/\\`])ts-node(?:-(?:cwd|esm|script|transpile-only))?(?:\.(?:cmd|exe|ps1))?(?=$|[@/\s;&|)"'`])/i;
1141
+ const TS_NODE_LOADERS = /^ts-node(?:\/.*)?$/;
1142
+ /** Check module metadata only; migration must not execute a project's dependencies. */
1143
+ function incompatibleModuleSource(file, source) {
1144
+ file = fs.realpathSync(file);
1145
+ const { ast } = JSON.parse(analyzeMigrationSource(file, source));
1146
+ let requireResolver;
1147
+ let importResolver;
1148
+ for (const node of ast.node.body) {
1149
+ let specifier;
1150
+ let namedOnly = false;
1151
+ if (node.type === "ImportDeclaration" && node.importKind !== "type") {
1152
+ const bindings = node.specifiers.filter((binding) => binding.type !== "ImportSpecifier" || binding.importKind !== "type");
1153
+ if (!bindings.length) continue;
1154
+ specifier = node.source.value;
1155
+ namedOnly = bindings.every((binding) => binding.type === "ImportSpecifier" && (binding.imported.type === "Identifier" ? binding.imported.name : binding.imported.value) !== "default");
1156
+ } else if (node.type === "ExportNamedDeclaration" && node.source && node.exportKind !== "type") {
1157
+ const bindings = node.specifiers.filter((binding) => binding.exportKind !== "type");
1158
+ if (!bindings.length) continue;
1159
+ specifier = node.source.value;
1160
+ namedOnly = bindings.every((binding) => (binding.local.type === "Identifier" ? binding.local.name : binding.local.value) !== "default");
1161
+ } else if (node.type === "ExportAllDeclaration" && node.exportKind !== "type") specifier = node.source.value;
1162
+ else continue;
1163
+ requireResolver ??= new ResolverFactory({
1164
+ conditionNames: [
1165
+ "node",
1166
+ "require",
1167
+ "module-sync",
1168
+ "node-addons"
1169
+ ],
1170
+ builtinModules: true,
1171
+ moduleType: true,
1172
+ nodePath: false
1173
+ });
1174
+ importResolver ??= requireResolver.cloneWithOptions({
1175
+ conditionNames: [
1176
+ "node",
1177
+ "import",
1178
+ "module-sync",
1179
+ "node-addons"
1180
+ ],
1181
+ builtinModules: true,
1182
+ moduleType: true,
1183
+ nodePath: false
1184
+ });
1185
+ const required = requireResolver.sync(path.dirname(file), specifier);
1186
+ const imported = importResolver.sync(path.dirname(file), specifier);
1187
+ if (namedOnly && required.builtin && required.builtin.resolved === imported.builtin?.resolved) continue;
1188
+ if (required.path && required.path === imported.path && String(required.moduleType) === "module" && String(imported.moduleType) === "module") continue;
1189
+ return specifier;
1190
+ }
1191
+ }
1192
+ function isTsNodeLoader(option, value) {
1193
+ return (option === "-r" || option === "--require" || option === "--import") && /^ts-node\/register(?:\/(?:transpile-only|type-check|files))?$/.test(value) || (option === "--loader" || option === "--experimental-loader") && /^ts-node\/esm(?:\/transpile-only)?(?:\.mjs)?$/.test(value);
1194
+ }
1195
+ const TS_NODE_RUNNER = {
1196
+ name: "ts-node",
1197
+ aliases: ["ts-node-esm", "ts-node-transpile-only"],
1198
+ watchSubcommand: false,
1199
+ dependencies: ["ts-node"],
1200
+ commandPattern: TS_NODE_REFERENCE,
1201
+ loaderPattern: TS_NODE_LOADERS,
1202
+ isLoader: isTsNodeLoader,
1203
+ isRegister: isTsNodeLoader,
1204
+ registerEnvRequiresNode: true,
1205
+ envShebang: /^(#!\s*\S*\benv\s+(?:-S\s+)?)(ts-node(?:-(?:esm|transpile-only))?(?=[ \t\r\n]|$)[^\r\n]*)/,
1206
+ shebangPattern: /^#![^\n]*[\s/]ts-node(?:-(?:cwd|esm|script|transpile-only))?(?=\s|$)/,
1207
+ codeReference: TS_NODE_REFERENCE,
1208
+ textReference: TS_NODE_REFERENCE,
1209
+ configValue: TS_NODE_REFERENCE,
1210
+ referenceFileNamePattern: /^\.env(?:\.|$)/,
1211
+ keepOptions: /* @__PURE__ */ new Map([
1212
+ ["-e", "`vpx` does not run TypeScript passed to --eval or --print"],
1213
+ ["--eval", "`vpx` does not run TypeScript passed to --eval or --print"],
1214
+ ["-p", "`vpx` does not run TypeScript passed to --eval or --print"],
1215
+ ["--print", "`vpx` does not run TypeScript passed to --eval or --print"],
1216
+ ["-c", "`vpx -c` selects package shell mode instead of Node.js syntax checking"],
1217
+ ["--check", "`vpx` does not transform TypeScript for --check"],
1218
+ ["-i", "`vpx` does not start a TypeScript REPL"],
1219
+ ["--interactive", "`vpx` does not start a TypeScript REPL"],
1220
+ ["--watch", "Node.js watch mode and `vpx --watch` use different watchers"],
1221
+ ["--watch-path", "Node.js watch mode and `vpx --watch` use different watchers"],
1222
+ ["-h", "ts-node and `vpx` have different help output"],
1223
+ ["--help", "ts-node and `vpx` have different help output"],
1224
+ ["-v", "ts-node and `vpx` have different versions"],
1225
+ ["--version", "ts-node and `vpx` have different versions"]
1226
+ ]),
1227
+ optionsWithValue: /* @__PURE__ */ new Set(["--tsconfig"]),
1228
+ testWithoutFilesReason: "list the test files explicitly before migrating to `vpx --test`",
1229
+ envKeepReason(assignments) {
1230
+ if (assignments.some((assignment) => /^NODE_OPTIONS=/i.test(assignment) && TS_NODE_REFERENCE.test(assignment.slice(13)))) return "`NODE_OPTIONS` loads ts-node hooks that would compete with those of `vpx`; remove it first";
1231
+ if (assignments.some((assignment) => /^NODE_OPTIONS=.+/i.test(assignment))) return "`NODE_OPTIONS` can change ts-node startup or load configuration preloads; review it before migrating";
1232
+ },
1233
+ prepareRun: (args, context) => prepareTsNodeRun(args, context),
1234
+ scriptKeepReason(cwd, script) {
1235
+ const reason = typescriptScriptKeepReason(cwd, script, "ts-node");
1236
+ if (reason) return reason;
1237
+ if (cwd !== void 0 && !/[*?[{]/.test(script)) try {
1238
+ const file = script.startsWith("~/") ? path.join(os.homedir(), script.slice(2)) : path.resolve(cwd, script);
1239
+ const source = fs.readFileSync(file, "utf8").replace(/^#![^\n]*\n?/, "");
1240
+ if (TS_NODE_REFERENCE.test(source)) return `\`${script}\` uses a standalone ts-node API or loader that does not configure the bundled \`vpx\` loader`;
1241
+ const incompatible = incompatibleModuleSource(file, source);
1242
+ if (incompatible !== void 0) return `\`${script}\` has runtime bindings from \`${incompatible}\` whose CommonJS interop or import/require resolution may change with \`vpx\`; migrate the bindings manually`;
1243
+ } catch {
1244
+ return `\`${script}\` cannot be read or parsed, so vp migrate cannot check its CommonJS interop`;
1245
+ }
1246
+ },
1247
+ detectKeptUses: true
1248
+ };
1249
+ function detectTsNodeProject(rootDir, packages) {
1250
+ return detectScriptRunnerProject(TS_NODE_RUNNER, rootDir, packages);
1251
+ }
1252
+ function migrateTsNodeToVpx(rootDir, packages, report) {
1253
+ const result = migrateScriptRunnerToVpx(TS_NODE_RUNNER, rootDir, packages, report);
1254
+ if (report && result.scriptsChanged) report.tsNodeMigrated = true;
1255
+ return {
1256
+ changed: result.changed,
1257
+ dependencyRemoved: result.dependencyRemoved
1258
+ };
1259
+ }
1260
+ async function confirmTsNodeMigration(interactive) {
1261
+ if (interactive) {
1262
+ const confirmed = await confirm({
1263
+ message: "Migrate ts-node scripts to Vite+?\n " + styleText("gray", "Commands with type-checking disabled become `vpx`. Supported Node.js preloads become `vite-plus/register`. Commands that type-check or use custom ts-node settings stay unchanged. The ts-node devDependency is removed once nothing uses it."),
1264
+ initialValue: true
1265
+ });
1266
+ if (isCancel(confirmed)) cancelAndExit();
1267
+ return confirmed;
1268
+ }
1269
+ log.info("ts-node scripts detected. Auto-migrating supported commands to Vite+...");
1270
+ return true;
1271
+ }
1272
+ // src/migration/migrator/ts-node-config.ts
1273
+ const KEEP_TYPE_CHECK = "this ts-node command type-checks before execution; `vpx` does not type-check. Keep a separate type-check command before changing this script";
1274
+ const KEEP_CWD = "the working directory is unknown, so the ts-node configuration cannot be checked";
1275
+ const TS_NODE_LOADER = /^ts-node\/(?:register(?:\/(?:transpile-only|type-check|files))?|esm(?:\/transpile-only)?(?:\.mjs)?)$/;
1276
+ const PRELOAD_OPTIONS = /* @__PURE__ */ new Set([
1277
+ "-r",
1278
+ "--require",
1279
+ "--import",
1280
+ "--loader",
1281
+ "--experimental-loader"
1282
+ ]);
1283
+ const BOOLEAN_OPTIONS = /* @__PURE__ */ new Map([
1284
+ ["-T", "transpileOnly"],
1285
+ ["--transpileOnly", "transpileOnly"],
1286
+ ["--transpile-only", "transpileOnly"],
1287
+ ["--typeCheck", "typeCheck"],
1288
+ ["--type-check", "typeCheck"],
1289
+ ["--esm", "esm"],
1290
+ ["--files", "files"]
1291
+ ]);
1292
+ const CONFIG_OPTIONS = /* @__PURE__ */ new Set([
1293
+ "transpileOnly",
1294
+ "typeCheck",
1295
+ "esm",
1296
+ "files"
1297
+ ]);
1298
+ function booleanEnvironment(value) {
1299
+ if (/^(?:y|yes|true|1)$/i.test(value)) return true;
1300
+ if (/^(?:n|no|false|0)$/i.test(value)) return false;
1301
+ }
1302
+ function nearestTsconfig(dir) {
1303
+ for (;;) {
1304
+ const file = path.join(dir, "tsconfig.json");
1305
+ if (fs.existsSync(file)) return file;
1306
+ const parent = path.dirname(dir);
1307
+ if (parent === dir) return;
1308
+ dir = parent;
1309
+ }
1310
+ }
1311
+ function readTsNodeEnvironment(context) {
1312
+ const options = {};
1313
+ let project;
1314
+ for (const assignment of context.environment) {
1315
+ const separator = assignment.indexOf("=");
1316
+ if (separator < 0) continue;
1317
+ const originalName = assignment.slice(0, separator);
1318
+ const name = originalName.toUpperCase();
1319
+ const value = assignment.slice(separator + 1);
1320
+ if (originalName !== name && (name.startsWith("TS_NODE_") || context.publicRegister && name === "NODE_OPTIONS")) return { keepReason: `\`${originalName}\` uses platform-dependent environment variable casing` };
1321
+ if (name === "TS_NODE_PROJECT") project = value || void 0;
1322
+ else if (name === "TS_NODE_TRANSPILE_ONLY" || name === "TS_NODE_TYPE_CHECK") {
1323
+ const parsed = booleanEnvironment(value);
1324
+ if (parsed === void 0) return { keepReason: `\`${name}\` does not have a literal boolean value` };
1325
+ options[name === "TS_NODE_TRANSPILE_ONLY" ? "transpileOnly" : "typeCheck"] = parsed;
1326
+ } else if (name.startsWith("TS_NODE_")) return { keepReason: `\`${name}\` configures ts-node behavior that vp migrate does not translate` };
1327
+ else if ([
1328
+ "OXC_TSCONFIG_PATH",
1329
+ "TSX_TSCONFIG_PATH",
1330
+ "VP_SCRIPT_TSCONFIG"
1331
+ ].includes(name) && value) return { keepReason: `\`${name}\` would select a different configuration for the Vite+ loader` };
1332
+ }
1333
+ return {
1334
+ options,
1335
+ project
1336
+ };
1337
+ }
1338
+ /** Read only data; migration must not load ts-node or a project's compiler. */
1339
+ function readTsNodeOptions(file, ancestors = /* @__PURE__ */ new Set()) {
1340
+ const keepConfig = (reason) => ({ keepReason: `the ts-node configuration in \`${file}\` ${reason}` });
1341
+ if (ancestors.has(file)) return keepConfig("has circular inheritance");
1342
+ const chain = new Set(ancestors).add(file);
1343
+ let config;
1344
+ try {
1345
+ const errors = [];
1346
+ config = parse(fs.readFileSync(file, "utf8"), errors, { allowTrailingComma: true });
1347
+ if (errors.length || !isPlainRecord(config)) return keepConfig("cannot be parsed");
1348
+ } catch {
1349
+ return keepConfig("cannot be read");
1350
+ }
1351
+ let inherited = {};
1352
+ let compilerOptions = {};
1353
+ if (config.extends !== void 0) {
1354
+ if (typeof config.extends !== "string" || !config.extends.startsWith(".") && !path.isAbsolute(config.extends)) return keepConfig("uses inheritance that vp migrate cannot resolve");
1355
+ let base = path.resolve(path.dirname(file), config.extends);
1356
+ if (!fs.existsSync(base) && !base.endsWith(".json")) base += ".json";
1357
+ const result = readTsNodeOptions(base, chain);
1358
+ if (result.keepReason) return result;
1359
+ inherited = result.options ?? {};
1360
+ compilerOptions = result.compilerOptions ?? {};
1361
+ }
1362
+ if (config.compilerOptions !== void 0) {
1363
+ if (!isPlainRecord(config.compilerOptions)) return keepConfig("has an invalid `compilerOptions` object");
1364
+ compilerOptions = {
1365
+ ...compilerOptions,
1366
+ ...config.compilerOptions
1367
+ };
1368
+ }
1369
+ const settings = config["ts-node"];
1370
+ if (settings === void 0) return {
1371
+ options: inherited,
1372
+ compilerOptions
1373
+ };
1374
+ if (!isPlainRecord(settings)) return keepConfig("has an invalid `ts-node` object");
1375
+ for (const [key, value] of Object.entries(settings)) {
1376
+ if (!CONFIG_OPTIONS.has(key)) return keepConfig(`uses \`ts-node.${key}\`, which vp migrate does not translate`);
1377
+ if (typeof value !== "boolean") return keepConfig(`has a non-boolean \`ts-node.${key}\` value`);
1378
+ }
1379
+ return {
1380
+ options: {
1381
+ ...inherited,
1382
+ ...settings
1383
+ },
1384
+ compilerOptions
1385
+ };
1386
+ }
1387
+ /**
1388
+ * Translate the supported ts-node options before the shared runner scans Node options.
1389
+ * ts-node merges environment < inherited config < CLI/API, then typeCheck overrides
1390
+ * transpileOnly. Keep all other compiler behavior for manual migration.
1391
+ */
1392
+ function prepareTsNodeRun(args, context) {
1393
+ const { cwd, nodeLoader, executable } = context;
1394
+ if (!cwd) return { keepReason: KEEP_CWD };
1395
+ if (context.publicRegister) {
1396
+ if (context.nodeOptionsArgs === void 0 && context.environment.some((assignment) => /^NODE_OPTIONS=.+/i.test(assignment))) return { keepReason: "the literal NODE_OPTIONS arguments cannot be checked for ts-node settings" };
1397
+ args = [...context.nodeOptionsArgs ?? [], ...args];
1398
+ }
1399
+ const environment = readTsNodeEnvironment(context);
1400
+ if ("keepReason" in environment) return environment;
1401
+ const explicit = {};
1402
+ if (executable === "ts-node-transpile-only") explicit.transpileOnly = true;
1403
+ else if (executable !== "ts-node" && executable !== "ts-node-esm" && !nodeLoader) return { keepReason: `vp migrate does not translate the \`${executable}\` execution mode` };
1404
+ const edits = [];
1405
+ let project;
1406
+ let entry;
1407
+ let loaders = 0;
1408
+ for (let index = 0; index < args.length; index++) {
1409
+ const value = args[index];
1410
+ if (value === "--") {
1411
+ entry = args[index + 1];
1412
+ break;
1413
+ }
1414
+ if (!value.startsWith("-")) {
1415
+ entry = value;
1416
+ break;
1417
+ }
1418
+ const equals = value.indexOf("=");
1419
+ const originalOption = equals < 0 ? value : value.slice(0, equals);
1420
+ const name = nodeLoader && originalOption.startsWith("--") ? originalOption.replaceAll("_", "-") : originalOption;
1421
+ const inline = equals < 0 ? void 0 : value.slice(equals + 1);
1422
+ if (context.publicRegister && name === "--run") return { keepReason: "vp migrate cannot check the ts-node settings of a package script run by Node.js" };
1423
+ if (context.publicRegister && (name === "--eval" || name === "--print" || /^-[ep]/.test(name) || name === "-i" || name === "--interactive")) return { keepReason: "vp migrate does not translate ts-node inline code or REPL execution" };
1424
+ if (nodeLoader && (name === "--env-file" || name === "--env-file-if-exists")) return { keepReason: `\`${name}\` can configure ts-node through environment variables; vp migrate does not translate those settings` };
1425
+ if (nodeLoader && [
1426
+ "--config-file",
1427
+ "--experimental-config-file",
1428
+ "--experimental-default-config-file"
1429
+ ].includes(name)) return { keepReason: `\`${name}\` can load preloads that configure ts-node; migrate it manually` };
1430
+ const booleanOption = BOOLEAN_OPTIONS.get(name);
1431
+ if (!nodeLoader && booleanOption) {
1432
+ if (inline !== void 0) return { keepReason: `ts-node treats \`${name}=value\` as an enabled flag; use an explicit configuration value before migrating` };
1433
+ explicit[booleanOption] = true;
1434
+ edits.push({ index });
1435
+ continue;
1436
+ }
1437
+ if (!nodeLoader && (name === "-P" || name === "--project")) {
1438
+ if (project !== void 0) return { keepReason: "the command selects a ts-node project more than once" };
1439
+ const projectPath = inline ?? args[index + 1];
1440
+ if (!projectPath || projectPath.startsWith("-")) return { keepReason: `\`${name}\` does not name a tsconfig` };
1441
+ project = {
1442
+ path: projectPath,
1443
+ index,
1444
+ inline: inline !== void 0
1445
+ };
1446
+ if (!project.inline) index++;
1447
+ continue;
1448
+ }
1449
+ if (PRELOAD_OPTIONS.has(name)) {
1450
+ if (!nodeLoader && name !== "-r" && name !== "--require") return { keepReason: `\`${name}\` uses ts-node behavior that vp migrate does not translate` };
1451
+ const preload = inline ?? args[index + 1];
1452
+ if (!preload || preload.startsWith("-")) return { keepReason: `\`${name}\` does not name a preload` };
1453
+ if (TS_NODE_LOADER.test(preload)) {
1454
+ if (!nodeLoader || ++loaders > 1) return { keepReason: "the command registers ts-node more than once" };
1455
+ if (/\/transpile-only(?:\.mjs)?$/.test(preload)) explicit.transpileOnly = true;
1456
+ else if (preload.endsWith("/type-check")) explicit.typeCheck = true;
1457
+ } else return { keepReason: `the additional preload \`${preload}\` can configure ts-node before execution; migrate it manually` };
1458
+ if (inline === void 0) index++;
1459
+ continue;
1460
+ }
1461
+ if (!nodeLoader) return { keepReason: `\`${name}\` uses ts-node behavior that vp migrate does not translate` };
1462
+ if (inline === void 0 && NODE_OPTIONS_WITH_VALUE.has(name)) index++;
1463
+ }
1464
+ if (!entry || entry === "-") return { keepReason: "vp migrate only replaces ts-node commands that name a script file" };
1465
+ const selectedProject = project?.path ?? environment.project;
1466
+ if (context.shebang && selectedProject !== void 0) return { keepReason: "the ts-node shebang selects a tsconfig relative to the caller; migrate it manually" };
1467
+ let configFile;
1468
+ if (selectedProject) {
1469
+ configFile = path.resolve(cwd, selectedProject);
1470
+ if (fs.statSync(configFile, { throwIfNoEntry: false })?.isDirectory()) {
1471
+ if (project === void 0) return { keepReason: "`TS_NODE_PROJECT` names a directory; `vpx` requires the tsconfig file path" };
1472
+ configFile = path.join(configFile, "tsconfig.json");
1473
+ project.path = path.join(project.path, "tsconfig.json");
1474
+ }
1475
+ } else {
1476
+ let searchDir = cwd;
1477
+ if (!nodeLoader) try {
1478
+ searchDir = path.dirname(fs.realpathSync(path.resolve(cwd, entry)));
1479
+ } catch {
1480
+ return { keepReason: `\`${entry}\` cannot be resolved, so its ts-node configuration cannot be checked` };
1481
+ }
1482
+ configFile = nearestTsconfig(searchDir);
1483
+ if (context.publicRegister && !configFile) {
1484
+ let entryConfig;
1485
+ try {
1486
+ entryConfig = nearestTsconfig(path.dirname(fs.realpathSync(path.resolve(cwd, entry))));
1487
+ } catch {
1488
+ return { keepReason: `\`${entry}\` cannot be resolved, so its ts-node configuration cannot be checked` };
1489
+ }
1490
+ if (entryConfig) return { keepReason: "the Node.js preload would discover an entrypoint tsconfig that ts-node does not use; select the intended config with TS_NODE_PROJECT before migrating" };
1491
+ }
1492
+ }
1493
+ const config = configFile ? readTsNodeOptions(configFile) : { options: {} };
1494
+ if (config.keepReason) return { keepReason: config.keepReason };
1495
+ if (context.shebang && configFile) return { keepReason: "the ts-node shebang needs a pinned tsconfig, whose path depends on the caller; migrate it manually" };
1496
+ const options = {
1497
+ ...environment.options,
1498
+ ...config.options,
1499
+ ...explicit
1500
+ };
1501
+ if (options.transpileOnly !== true || options.typeCheck === true) return { keepReason: KEEP_TYPE_CHECK };
1502
+ if (configFile && config.compilerOptions?.target == null && typeof config.compilerOptions?.useDefineForClassFields !== "boolean") return { keepReason: "the ts-node config defaults to assignment class fields, while the Vite+ loader defaults to define semantics; set compilerOptions.useDefineForClassFields explicitly before migrating" };
1503
+ if (project !== void 0) {
1504
+ edits.push({
1505
+ index: project.index,
1506
+ value: `--tsconfig=${project.path.split(path.sep).join("/")}`
1507
+ });
1508
+ if (!project.inline) edits.push({ index: project.index + 1 });
1509
+ }
1510
+ if (configFile && selectedProject === void 0) {
1511
+ if (context.publicRegister) return { keepReason: "the Node.js preload needs the ts-node config pinned explicitly; set TS_NODE_PROJECT to its file path before migrating" };
1512
+ return {
1513
+ edits,
1514
+ prependArgs: [`--tsconfig=${path.relative(fs.realpathSync(cwd), fs.realpathSync(configFile)).split(path.sep).join("/")}`]
1515
+ };
1516
+ }
1517
+ return { edits };
1518
+ }
1519
+ // src/migration/vitest-v5/commands.ts
1520
+ /** Read test commands without evaluating shell state or directory changes. */
1521
+ function literalTestCommands(command) {
1522
+ const commands = parseLiteralCommands(command, true);
1523
+ if (!commands) return;
1524
+ const result = [];
1525
+ for (const tokens of commands) {
1526
+ const values = tokens.map(({ value }) => value);
1527
+ while (/^[A-Za-z_]\w*=/.test(values[0] ?? "")) values.shift();
1528
+ if (!values.length || SHELL_STATE_COMMANDS.includes(values[0])) return;
1529
+ result.push(values);
1530
+ }
1531
+ return result;
1532
+ }
1533
+ /** Locate the runner's arguments after a supported package-manager wrapper. */
1534
+ function vitestCommandArgsStart(values) {
1535
+ const start = getCommandStartIndex(values);
121
1536
  if (values[start] === "vitest") return start + 1;
122
1537
  if (values[start] === "vp" && values[start + 1] === "test") return start + 2;
123
1538
  return -1;
124
1539
  }
125
1540
  function migrateBenchmarkOutput(command) {
126
- const argv = literalArgv(command);
1541
+ const argv = parseLiteralCommands(command, false)?.[0];
127
1542
  if (!argv) return command;
128
1543
  const values = argv.map(({ value }) => value);
129
1544
  const start = vitestCommandArgsStart(values);
@@ -182,7 +1597,7 @@ function migrateVitestV5Command(file, command, preserveV4, line = 1, reviewV4 =
182
1597
  findings
183
1598
  };
184
1599
  command = migrateBenchmarkOutput(command);
185
- const argv = literalArgv(command);
1600
+ const argv = parseLiteralCommands(command, false)?.[0];
186
1601
  const values = argv?.map(({ value }) => value);
187
1602
  const runner = values ? vitestCommandArgsStart(values) : -1;
188
1603
  let removedFlag = /(?:^|\s)(?:--compare|--outputJson)(?:\s|=|$)/.test(command);
@@ -230,8 +1645,7 @@ function migrateVitestV5Command(file, command, preserveV4, line = 1, reviewV4 =
230
1645
  findings
231
1646
  };
232
1647
  }
233
- //#endregion
234
- //#region src/migration/vitest-v5/ast.ts
1648
+ // src/migration/vitest-v5/ast.ts
235
1649
  function isNode(value) {
236
1650
  return typeof value === "object" && value !== null && "type" in value && "start" in value;
237
1651
  }
@@ -505,7 +1919,7 @@ var SourceEditor = class {
505
1919
  }
506
1920
  }
507
1921
  const propertyIndent = multilinePrefix && object.properties.length ? indent : `${objectIndent} `;
508
- const entries = [...additions.values()].map((property) => printAddedProperty(property, propertyIndent, newline));
1922
+ const entries = Array.from(additions.values(), (property) => printAddedProperty(property, propertyIndent, newline));
509
1923
  const suffix = multiline && !multilinePrefix ? `${newline}${object.properties.length ? propertyIndent : objectIndent}` : "";
510
1924
  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 ? "," : ""} `);
511
1925
  }
@@ -538,8 +1952,7 @@ var SourceEditor = class {
538
1952
  };
539
1953
  }
540
1954
  };
541
- //#endregion
542
- //#region src/migration/vitest-v5/benchmark-config.ts
1955
+ // src/migration/vitest-v5/benchmark-config.ts
543
1956
  const REPORTERS = /* @__PURE__ */ new Set([
544
1957
  "default",
545
1958
  "verbose",
@@ -607,8 +2020,7 @@ function migrateBenchmarkConfig(editor, test) {
607
2020
  oldJson
608
2021
  ]) if (prop) editor.remove(benchmark, prop);
609
2022
  }
610
- //#endregion
611
- //#region src/migration/vitest-v5/compatibility.ts
2023
+ // src/migration/vitest-v5/compatibility.ts
612
2024
  const settings = {
613
2025
  clearMocks: {
614
2026
  value: "false",
@@ -656,8 +2068,7 @@ function compatibilityProperty(key) {
656
2068
  comment: `// Vitest v4 compatibility: ${reason}\n// ${adoption}\n// ${documentationUrl$1("/guide/vitest-v5#remove-unneeded-compatibility-settings")}\n// https://vitest.dev/guide/migration/#${section}`
657
2069
  };
658
2070
  }
659
- //#endregion
660
- //#region src/migration/vitest-v5/config-object.ts
2071
+ // src/migration/vitest-v5/config-object.ts
661
2072
  const CONFIG_KEYS = /* @__PURE__ */ new Set([
662
2073
  "root",
663
2074
  "test",
@@ -703,8 +2114,7 @@ function resolveConfigObject(editor, node) {
703
2114
  }
704
2115
  return node;
705
2116
  }
706
- //#endregion
707
- //#region src/migration/vitest-v5/config.ts
2117
+ // src/migration/vitest-v5/config.ts
708
2118
  function isTrue(node) {
709
2119
  return isBoolean(node) && node.value;
710
2120
  }
@@ -1001,8 +2411,7 @@ function migrateConfig(file, source, options, mergedConfig, benchmarkOnly = fals
1001
2411
  });
1002
2412
  return editor.finish();
1003
2413
  }
1004
- //#endregion
1005
- //#region src/migration/vitest-v5/documentation.ts
2414
+ // src/migration/vitest-v5/documentation.ts
1006
2415
  const guide = documentationUrl$1("/guide/vitest-v5");
1007
2416
  const upstream = "https://vitest.dev/guide/migration/";
1008
2417
  const sections = {
@@ -1054,12 +2463,49 @@ function vitestV5Documentation(code) {
1054
2463
  if (sections[code]) return `${upstream}#${sections[code]}`;
1055
2464
  return `${guide}#${localSections[code] ?? "resolve-migration-findings"}`;
1056
2465
  }
1057
- //#endregion
1058
- //#region src/migration/vitest-v5/lockfile.ts
2466
+ // src/migration/vitest-v5/lockfile.ts
1059
2467
  var import_dist = require_dist();
1060
2468
  function record$1(value) {
1061
2469
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1062
2470
  }
2471
+ /** Create one reader per preflight, with a single parser for each lockfile path. */
2472
+ function createLockfileReader() {
2473
+ const locks = /* @__PURE__ */ new Map();
2474
+ return (file, parse) => {
2475
+ if (!locks.has(file)) {
2476
+ locks.set(file, void 0);
2477
+ try {
2478
+ locks.set(file, parse(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")));
2479
+ } catch {}
2480
+ }
2481
+ return locks.get(file);
2482
+ };
2483
+ }
2484
+ function parseYarnLockfile(text) {
2485
+ if (/^__metadata:/m.test(text)) {
2486
+ const document = (0, import_dist.parseDocument)(text);
2487
+ if (document.errors.length) return;
2488
+ return {
2489
+ modern: true,
2490
+ entries: record$1(document.toJS()) ?? {}
2491
+ };
2492
+ }
2493
+ const entries = [];
2494
+ for (const block of text.split(/\r?\n(?=[^\s#])/)) {
2495
+ const [header] = block.split(/\r?\n/);
2496
+ if (!header.endsWith(":")) continue;
2497
+ const document = (0, import_dist.parseDocument)(`[${header.slice(0, -1)}]`);
2498
+ const keys = document.errors.length ? void 0 : document.toJS();
2499
+ if (Array.isArray(keys)) entries.push({
2500
+ keys,
2501
+ version: /^ version "([^"]+)"\r?$/m.exec(block)?.[1]
2502
+ });
2503
+ }
2504
+ return {
2505
+ modern: false,
2506
+ entries
2507
+ };
2508
+ }
1063
2509
  function declaredVitest(pkg) {
1064
2510
  const manifest = record$1(pkg);
1065
2511
  for (const field of [
@@ -1075,7 +2521,7 @@ function declaredVitest(pkg) {
1075
2521
  /** Read a direct upstream Vitest edge without installing or changing a lockfile.
1076
2522
  * Unsupported protocols, ambiguous layouts and stale entries remain unknown.
1077
2523
  * In particular, never infer a runner from an unrelated transitive package. */
1078
- function lockedVitestVersion(root, directory, manager, spec) {
2524
+ function lockedVitestVersion(root, directory, manager, spec, readLockfile) {
1079
2525
  const range = spec.replace(/^npm:vitest@/, "");
1080
2526
  const validRange = normalizeRange(range);
1081
2527
  if (!validRange && !/^[\w.-]+$/.test(range)) return;
@@ -1084,10 +2530,9 @@ function lockedVitestVersion(root, directory, manager, spec) {
1084
2530
  return !validRange || satisfies(version, range, { includePrerelease: true }) ? version : void 0;
1085
2531
  }
1086
2532
  const relative = path.relative(root, directory).replaceAll("\\", "/");
1087
- const read = (name) => fs.readFileSync(path.join(root, name), "utf8").replace(/^\uFEFF/, "");
1088
2533
  try {
1089
2534
  if (manager === PackageManager.npm) {
1090
- const packages = record$1(JSON.parse(read(fs.existsSync(path.join(root, "npm-shrinkwrap.json")) ? "npm-shrinkwrap.json" : "package-lock.json")).packages);
2535
+ const packages = record$1(readLockfile(path.join(root, fs.existsSync(path.join(root, "npm-shrinkwrap.json")) ? "npm-shrinkwrap.json" : "package-lock.json"), (source) => record$1(JSON.parse(source)))?.packages);
1091
2536
  if (!packages) return;
1092
2537
  if (declaredVitest(packages[relative]) !== spec) return;
1093
2538
  let owner = relative;
@@ -1100,34 +2545,29 @@ function lockedVitestVersion(root, directory, manager, spec) {
1100
2545
  }
1101
2546
  }
1102
2547
  if (manager === PackageManager.yarn) {
1103
- const text = read("yarn.lock");
1104
- if (/^__metadata:/m.test(text)) {
1105
- const document = (0, import_dist.parseDocument)(text);
1106
- if (document.errors.length) return;
2548
+ const lock = readLockfile(path.join(root, "yarn.lock"), parseYarnLockfile);
2549
+ if (!lock) return;
2550
+ if (lock.modern) {
1107
2551
  const descriptors = /* @__PURE__ */ new Set([`vitest@${spec}`, `vitest@npm:${range}`]);
1108
- const entries = Object.entries(record$1(document.toJS()) ?? {}).filter(([keys]) => keys.split(/,\s+/).some((key) => descriptors.has(key)));
2552
+ const entries = Object.entries(lock.entries).filter(([keys]) => keys.split(/,\s+/).some((key) => descriptors.has(key)));
1109
2553
  if (entries.length !== 1) return;
1110
2554
  const entry = record$1(entries[0][1]);
1111
2555
  return typeof entry?.resolution === "string" && entry.resolution.startsWith("vitest@npm:") ? valid(entry.resolution.slice(11)) : void 0;
1112
2556
  }
1113
2557
  const matches = [];
1114
- for (const block of text.split(/\r?\n(?=[^\s#])/)) {
1115
- const [header] = block.split(/\r?\n/);
1116
- if (!header.endsWith(":")) continue;
1117
- const document = (0, import_dist.parseDocument)(`[${header.slice(0, -1)}]`);
1118
- const keys = document.errors.length ? void 0 : document.toJS();
1119
- if (Array.isArray(keys) && keys.includes(`vitest@${spec}`)) {
1120
- const version = /^ version "([^"]+)"\r?$/m.exec(block)?.[1];
1121
- if (!version) return;
1122
- matches.push(version);
1123
- }
2558
+ for (const { keys, version } of lock.entries) if (keys.includes(`vitest@${spec}`)) {
2559
+ if (!version) return;
2560
+ matches.push(version);
1124
2561
  }
1125
2562
  return matches.length === 1 ? valid(matches[0]) : void 0;
1126
2563
  }
1127
2564
  if (manager === PackageManager.bun) {
1128
- const errors = [];
1129
- const lock = record$1(parse(read("bun.lock"), errors, { allowTrailingComma: true }));
1130
- if (errors.length || declaredVitest(record$1(lock?.workspaces)?.[relative]) !== spec) return;
2565
+ const lock = readLockfile(path.join(root, "bun.lock"), (source) => {
2566
+ const errors = [];
2567
+ const parsed = record$1(parse(source, errors, { allowTrailingComma: true }));
2568
+ return errors.length ? void 0 : parsed;
2569
+ });
2570
+ if (declaredVitest(record$1(lock?.workspaces)?.[relative]) !== spec) return;
1131
2571
  const packages = record$1(lock?.packages);
1132
2572
  const direct = packages?.vitest;
1133
2573
  if (!Array.isArray(direct) || typeof direct[0] !== "string" || !direct[0].startsWith("vitest@")) return;
@@ -1143,8 +2583,7 @@ function lockedVitestVersion(root, directory, manager, spec) {
1143
2583
  }
1144
2584
  } catch {}
1145
2585
  }
1146
- //#endregion
1147
- //#region src/migration/vitest-v5/storybook.ts
2586
+ // src/migration/vitest-v5/storybook.ts
1148
2587
  const STORYBOOK_PLUGIN = /* @__PURE__ */ new Set(["@storybook/addon-vitest/vitest-plugin"]);
1149
2588
  const NODE_PATH = /* @__PURE__ */ new Set(["node:path", "path"]);
1150
2589
  const NODE_URL = /* @__PURE__ */ new Set(["node:url", "url"]);
@@ -1268,8 +2707,7 @@ function resolveStorybookScope(editor, object, sources, file, cwd) {
1268
2707
  return unknown(`cannot parse ${label}/main`);
1269
2708
  }
1270
2709
  }
1271
- //#endregion
1272
- //#region src/migration/vitest-v5/scopes.ts
2710
+ // src/migration/vitest-v5/scopes.ts
1273
2711
  const DEFAULT_TEST_INCLUDE = ["**/*.{test,spec}.?(c|m)[jt]s?(x)"];
1274
2712
  const DEFAULT_TEST_EXCLUDE = ["**/node_modules/**", "**/.git/**"];
1275
2713
  const DEFAULT_BENCHMARK_INCLUDE = ["**/*.{bench,benchmark}.?(c|m)[jt]s?(x)"];
@@ -1659,7 +3097,7 @@ function resolveVitestV5TestModes(sources, configFiles, preserveV4, entries) {
1659
3097
  if (!found) unknown();
1660
3098
  }
1661
3099
  return {
1662
- modes: new Map([...sources.keys()].map((file) => {
3100
+ modes: new Map(Array.from(sources.keys(), (file) => {
1663
3101
  const matching = [];
1664
3102
  let benchmarkFile = false;
1665
3103
  const hasInSourceTests = sources.get(file)?.includes("import.meta.vitest");
@@ -1696,8 +3134,7 @@ function resolveVitestV5TestModes(sources, configFiles, preserveV4, entries) {
1696
3134
  findings
1697
3135
  };
1698
3136
  }
1699
- //#endregion
1700
- //#region src/migration/vitest-v5/benchmarks.ts
3137
+ // src/migration/vitest-v5/benchmarks.ts
1701
3138
  /** Keep the workload as a separate callback: making it the test callback
1702
3139
  * would measure nothing, and inlining it into a new closure can change scope. */
1703
3140
  function migrateBenchmarks(editor, globals = false) {
@@ -1840,8 +3277,7 @@ function migrateBenchmarks(editor, globals = false) {
1840
3277
  calls
1841
3278
  };
1842
3279
  }
1843
- //#endregion
1844
- //#region src/migration/vitest-v5/source.ts
3280
+ // src/migration/vitest-v5/source.ts
1845
3281
  const RUNNER_SYMBOLS = {
1846
3282
  test: "test",
1847
3283
  it: "it",
@@ -2074,6 +3510,45 @@ function looksLikeConstructorMock(editor, node) {
2074
3510
  }
2075
3511
  return false;
2076
3512
  }
3513
+ function unwrapTypeExpression(node) {
3514
+ while (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion") node = node.expression;
3515
+ return node;
3516
+ }
3517
+ /** Recognize a local promise consumed by a later statement without crossing
3518
+ * control flow or function boundaries. Ambiguous paths still need review. */
3519
+ function isAssertionBindingHandled(editor, node) {
3520
+ let initializer = node;
3521
+ let declarator = editor.parent(initializer);
3522
+ while (declarator && unwrapTypeExpression(declarator) === node) {
3523
+ initializer = declarator;
3524
+ declarator = editor.parent(initializer);
3525
+ }
3526
+ if (declarator?.type !== "VariableDeclarator" || declarator.init !== initializer || declarator.id.type !== "Identifier") return false;
3527
+ const binding = editor.binding(declarator.id);
3528
+ const declaration = editor.parent(declarator);
3529
+ const block = declaration && editor.parent(declaration);
3530
+ const fn = editor.functionParent(node);
3531
+ if (!binding?.constant || declaration?.type !== "VariableDeclaration" || declaration.kind !== "const" || block?.type !== "BlockStatement" || !fn || fn.generator) return false;
3532
+ for (let ancestor = editor.parent(block); ancestor && ancestor !== fn; ancestor = editor.parent(ancestor)) if (ancestor.type === "TryStatement") return false;
3533
+ for (const statement of block.body.slice(block.body.indexOf(declaration) + 1)) switch (statement.type) {
3534
+ case "ReturnStatement": {
3535
+ if (!statement.argument) return false;
3536
+ let expression = unwrapTypeExpression(statement.argument);
3537
+ if (expression.type === "AwaitExpression") expression = unwrapTypeExpression(expression.argument);
3538
+ return editor.binding(expression) === binding;
3539
+ }
3540
+ case "ExpressionStatement": {
3541
+ const expression = unwrapTypeExpression(statement.expression);
3542
+ if (expression.type === "AwaitExpression" && editor.binding(unwrapTypeExpression(expression.argument)) === binding) return true;
3543
+ break;
3544
+ }
3545
+ case "VariableDeclaration":
3546
+ case "FunctionDeclaration":
3547
+ case "EmptyStatement": break;
3548
+ default: return false;
3549
+ }
3550
+ return false;
3551
+ }
2077
3552
  function chain(node, isApi) {
2078
3553
  const members = [];
2079
3554
  const calls = [];
@@ -2268,7 +3743,7 @@ function rewriteSource(file, source, options) {
2268
3743
  if (asynchronous && parent?.type === "ExpressionStatement") {
2269
3744
  if (canAwait(node)) editor.edit(node.start, node.start, "await ");
2270
3745
  else editor.report(node, "unawaited-assertion", "Await or return this asynchronous assertion in an async-compatible function.");
2271
- } 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.");
3746
+ } else if (asynchronous && parent?.type !== "MemberExpression" && parent?.type !== "AwaitExpression" && parent?.type !== "ReturnStatement" && !isAssertionBindingHandled(editor, node)) editor.report(node, "unawaited-assertion", "Check that the result of this asynchronous assertion is awaited or returned.");
2272
3747
  }
2273
3748
  if (importedName(editor, node.callee, RENDER_SOURCES) === "render" && parent?.type !== "AwaitExpression" && parent?.type !== "ReturnStatement") {
2274
3749
  if (canAwait(node)) {
@@ -2352,8 +3827,7 @@ function rewriteSource(file, source, options) {
2352
3827
  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");
2353
3828
  return result;
2354
3829
  }
2355
- //#endregion
2356
- //#region src/migration/vitest-v5/webdriverio.ts
3830
+ // src/migration/vitest-v5/webdriverio.ts
2357
3831
  const MIN_VERSION = "5.0.0";
2358
3832
  const DEFAULT_SPEC = "^5.0.0";
2359
3833
  const REGISTRY_ALIAS = `npm:${WEBDRIVERIO_PROVIDER}@`;
@@ -2395,7 +3869,7 @@ function migrateWebdriverioDependencies(workspace, sources, inputs) {
2395
3869
  const set = (file, keys, value) => {
2396
3870
  if (get(file, keys) === value) return;
2397
3871
  const source = sources.get(file);
2398
- if (file.endsWith(".json")) sources.set(file, applyEdits(source, modify(source, keys, value, { formattingOptions: {
3872
+ if (file.endsWith(".json")) sources.set(file, applyEdits$1(source, modify(source, keys, value, { formattingOptions: {
2399
3873
  insertSpaces: true,
2400
3874
  tabSize: 2
2401
3875
  } })));
@@ -2573,8 +4047,7 @@ function migrateWebdriverioDependencies(workspace, sources, inputs) {
2573
4047
  overrideMap(path.join(workspace.rootDir, "pnpm-workspace.yaml"), ["overrides"]);
2574
4048
  return findings;
2575
4049
  }
2576
- //#endregion
2577
- //#region src/migration/migrator/vitest-v5.ts
4050
+ // src/migration/migrator/vitest-v5.ts
2578
4051
  const SKIP_DIRS = /* @__PURE__ */ new Set([
2579
4052
  "node_modules",
2580
4053
  ".git",
@@ -2662,17 +4135,16 @@ function finding(file, code, message, severity = "review", source = "", offset =
2662
4135
  };
2663
4136
  }
2664
4137
  /** Follow this project's dependency edges, never an unrelated transitive runner. */
2665
- function lockedSourceVersion(directory, root, pkg, manager, expectedRunnerPackage) {
4138
+ function lockedSourceVersion(directory, root, pkg, readLockfile, manager, expectedRunnerPackage) {
2666
4139
  if (manager !== void 0 && manager !== PackageManager.pnpm) {
2667
4140
  const spec = dependency(pkg, "vitest");
2668
- return spec && !expectedRunnerPackage ? lockedVitestVersion(root, directory, manager, spec) : void 0;
4141
+ return spec && !expectedRunnerPackage ? lockedVitestVersion(root, directory, manager, spec, readLockfile) : void 0;
2669
4142
  }
2670
- const lockfile = path.join(root, "pnpm-lock.yaml");
2671
- if (!fs.existsSync(lockfile)) return;
2672
4143
  try {
2673
- const document = (0, import_dist.parseDocument)(fs.readFileSync(lockfile, "utf8"));
2674
- if (document.errors.length) return;
2675
- const lock = record(document.toJS());
4144
+ const lock = readLockfile(path.join(root, "pnpm-lock.yaml"), (source) => {
4145
+ const document = (0, import_dist.parseDocument)(source);
4146
+ return document.errors.length ? void 0 : record(document.toJS());
4147
+ });
2676
4148
  const importer = record(record(lock?.importers)?.[path.relative(root, directory).replaceAll("\\", "/") || "."]);
2677
4149
  if (!importer) return;
2678
4150
  const name = dependency(pkg, "vitest") ? "vitest" : "vite-plus";
@@ -2729,7 +4201,7 @@ function installedSourceVersion(directory, name, expectedPackage, expectedRange)
2729
4201
  }
2730
4202
  return dependency(pkg, LEGACY_RUNNER) ? installedSourceVersion(installed.path, LEGACY_RUNNER) : void 0;
2731
4203
  }
2732
- function sourceVersion(directory, root, pkg, manager) {
4204
+ function sourceVersion(directory, root, pkg, readLockfile, manager) {
2733
4205
  const catalog = createCatalogDependencyResolver(root, manager ?? PackageManager.pnpm);
2734
4206
  const vitePlus = dependency(pkg, "vite-plus");
2735
4207
  const vitePlusRange = vitePlus?.startsWith("catalog:") ? catalog?.(vitePlus, "vite-plus") : vitePlus;
@@ -2746,7 +4218,7 @@ function sourceVersion(directory, root, pkg, manager) {
2746
4218
  if (bundled) return bundled;
2747
4219
  }
2748
4220
  const expectedRunnerPackage = legacyAlias ? LEGACY_RUNNER : void 0;
2749
- const locked = lockedSourceVersion(directory, root, pkg, manager, expectedRunnerPackage);
4221
+ const locked = lockedSourceVersion(directory, root, pkg, readLockfile, manager, expectedRunnerPackage);
2750
4222
  if (locked && (!spec || legacyAlias || !normalizeRange(spec) || satisfies(locked, spec, { includePrerelease: true }))) return locked;
2751
4223
  const installed = installedSourceVersion(directory, "vitest", expectedRunnerPackage);
2752
4224
  if (installed && (!spec || legacyAlias || satisfies(installed, spec, { includePrerelease: true }))) return installed;
@@ -2756,7 +4228,7 @@ function sourceVersion(directory, root, pkg, manager) {
2756
4228
  const bundled = installedSourceVersion(directory, "vite-plus", "vite-plus", vitePlusRange);
2757
4229
  if (bundled) return bundled;
2758
4230
  }
2759
- if (directory !== root) return sourceVersion(root, root, readJson(path.join(root, "package.json")), manager);
4231
+ if (directory !== root) return sourceVersion(root, root, readJson(path.join(root, "package.json")), readLockfile, manager);
2760
4232
  }
2761
4233
  function checkNodeRange(file, value, label, findings, publicContract = false) {
2762
4234
  if (!publicContract && [
@@ -2818,7 +4290,7 @@ function migrateNode(file, source, findings) {
2818
4290
  "version"
2819
4291
  ]);
2820
4292
  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"]);
2821
- return applyEdits(source, edits);
4293
+ return applyEdits$1(source, edits);
2822
4294
  }
2823
4295
  return source;
2824
4296
  }
@@ -2871,7 +4343,7 @@ function scanAndRewriteFile(file, source, project, testMode, mergedConfig) {
2871
4343
  content: JSON.stringify(result.content)
2872
4344
  });
2873
4345
  }
2874
- content = applyEdits(source, edits);
4346
+ content = applyEdits$1(source, edits);
2875
4347
  } else if (/^tsconfig(?:\..+)?\.json$/.test(path.basename(file))) {
2876
4348
  const types = record(record(parse(source))?.compilerOptions)?.types;
2877
4349
  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."));
@@ -2902,10 +4374,11 @@ function scanAndRewriteFile(file, source, project, testMode, mergedConfig) {
2902
4374
  /** Read-only preflight. Call before package-manager conversion, installs,
2903
4375
  * catalog updates, or generic import rewrites can erase the source version. */
2904
4376
  function planVitestV5Migration(workspace, originalProjects) {
4377
+ const readLockfile = createLockfileReader();
2905
4378
  const findings = [];
2906
4379
  const directories = [workspace.rootDir, ...(workspace.packages ?? []).map((pkg) => path.resolve(workspace.rootDir, pkg.path))];
2907
4380
  const projects = [];
2908
- const projectSources = new Map([...new Set(directories)].map((directory) => [directory, new Map(filesInProject(directory).map((file) => [file, fs.readFileSync(file, "utf8")]))]));
4381
+ const projectSources = new Map(Array.from(new Set(directories), (directory) => [directory, new Map(filesInProject(directory).map((file) => [file, fs.readFileSync(file, "utf8")]))]));
2909
4382
  const allSources = /* @__PURE__ */ new Map();
2910
4383
  for (const sources of projectSources.values()) for (const [file, source] of sources) allSources.set(file, source);
2911
4384
  const rewrittenSources = new Map(allSources);
@@ -2920,7 +4393,7 @@ function planVitestV5Migration(workspace, originalProjects) {
2920
4393
  const pkg = readJson(path.join(directory, "package.json"));
2921
4394
  const configFiles = new Set([...allConfigs].filter((file) => sources.has(file)));
2922
4395
  const original = originalProjects?.get(directory);
2923
- const version = original ? original.sourceVersion : sourceVersion(directory, workspace.rootDir, pkg, workspace.packageManager);
4396
+ const version = original ? original.sourceVersion : sourceVersion(directory, workspace.rootDir, pkg, readLockfile, workspace.packageManager);
2924
4397
  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)));
2925
4398
  const options = {
2926
4399
  preserveV4: original?.options.preserveV4 ?? (!!version && getMajor(version) < 5),
@@ -3032,7 +4505,7 @@ function currentProjectConfigs(plan) {
3032
4505
  const filesByProject = new Map(plan.projects.map((project) => [project.directory, filesInProject(project.directory)]));
3033
4506
  const sources = new Map([...filesByProject.values()].flat().map((file) => [file, fs.readFileSync(file, "utf8")]));
3034
4507
  const configs = findVitestV5ConfigFiles(sources, findVitestV5ConfigEntries(sources, filesByProject.keys()).flatMap((entry) => entry.file ? [entry.file] : []));
3035
- return new Map([...filesByProject].map(([directory, files]) => [directory, files.filter((file) => configs.has(file))]));
4508
+ return new Map(Array.from(filesByProject, ([directory, files]) => [directory, files.filter((file) => configs.has(file))]));
3036
4509
  }
3037
4510
  /** Run after other migration steps create or merge configs. Keep the source
3038
4511
  * version and deferred reviews in memory until the final report; no state file. */
@@ -3080,8 +4553,7 @@ function finishVitestV5Migration(plan) {
3080
4553
  const after = planVitestV5Migration(workspaceFromPlan(plan), new Map(plan.projects.map((project) => [project.directory, completedProjects.get(project.directory) ?? project])));
3081
4554
  return [...findings, ...after.findings];
3082
4555
  }
3083
- //#endregion
3084
- //#region src/migration/compat/runner.ts
4556
+ // src/migration/compat/runner.ts
3085
4557
  /**
3086
4558
  * Resolve the isolated compat worker emitted at `migration/compat/worker.js`.
3087
4559
  *
@@ -3137,8 +4609,7 @@ async function checkRolldownCompatibility(rootDir, report) {
3137
4609
  for (const warning of compatibilityResult?.warnings ?? []) addMigrationWarning(report, warning);
3138
4610
  } catch {}
3139
4611
  }
3140
- //#endregion
3141
- //#region src/migration/format.ts
4612
+ // src/migration/format.ts
3142
4613
  const FORMAT_FAILURE_MESSAGE = "Automatic formatting failed. Run `vp fmt` manually after migration.";
3143
4614
  const MAX_FORMAT_ARG_BYTES = process.platform === "win32" ? 24e3 : 1e5;
3144
4615
  function chunkPathsByArgLength(paths) {
@@ -3264,8 +4735,29 @@ async function formatMigratedProject(projectRoot, interactive, report, options =
3264
4735
  addMigrationWarning(report, FORMAT_FAILURE_MESSAGE);
3265
4736
  return false;
3266
4737
  }
3267
- //#endregion
3268
- //#region src/migration/npm-reinstall.ts
4738
+ // src/migration/install-result.ts
4739
+ /**
4740
+ * Reconcile a CommandRunSummary from `runViteInstall` with the migration's
4741
+ * duration counter and exit-code state. `runViteInstall` returns
4742
+ * `{ status: 'failed', exitCode }` without throwing; treating that as a success
4743
+ * (incrementing duration unconditionally) would let the migration claim
4744
+ * "Dependencies installed" while node_modules is desynced from the just-mutated
4745
+ * package.json. This helper centralizes the right handling: credit duration on
4746
+ * success, warn + flip exitCode on failure, stay silent on skip.
4747
+ */
4748
+ function handleInstallResult(installSummary, rootDir, report, options) {
4749
+ if (installSummary.status === "installed") return installSummary.durationMs;
4750
+ if (installSummary.status === "failed") {
4751
+ const exitCode = installSummary.exitCode ?? 1;
4752
+ const message = `Dependency installation failed (exit code ${exitCode}). Run \`vp install\` manually in ${rootDir} to resync node_modules.`;
4753
+ warnMsg(message);
4754
+ report.warnings.push(message);
4755
+ if (options?.propagateExitCode !== false) process.exitCode = exitCode;
4756
+ return 0;
4757
+ }
4758
+ return 0;
4759
+ }
4760
+ // src/migration/npm-reinstall.ts
3269
4761
  const VITE_PLUS_CORE_PACKAGE = "@voidzero-dev/vite-plus-core";
3270
4762
  function isViteInstallPath(packagePath) {
3271
4763
  return packagePath === "node_modules/vite" || packagePath.endsWith("/node_modules/vite");
@@ -3354,18 +4846,20 @@ function prepareNpmViteAliasReinstall(rootDir, projectPaths = [rootDir]) {
3354
4846
  }
3355
4847
  };
3356
4848
  }
3357
- //#endregion
3358
- //#region src/migration/setup-plan.ts
4849
+ // src/migration/setup-plan.ts
3359
4850
  async function collectGitHooksDecision(rootDir, packageManager, options, packages = []) {
3360
- let shouldSetupHooks = await promptGitHooks(options);
3361
- if (shouldSetupHooks) {
3362
- const reason = preflightGitHooksSetup(rootDir, packageManager, packages);
3363
- if (reason) {
3364
- log$1.warn(`⚠ ${reason}`);
3365
- shouldSetupHooks = false;
3366
- }
4851
+ const hasExistingHooks = options.hooks === void 0 && hasExistingViteHooksPolicy(rootDir);
4852
+ if (hasExistingHooks && isViteHooksSetupComplete(rootDir)) {
4853
+ log$1(`${muted("•")} Git hooks already configured, skipping`);
4854
+ return false;
3367
4855
  }
3368
- return shouldSetupHooks;
4856
+ if (!hasExistingHooks && !await promptGitHooks(options)) return false;
4857
+ const reason = preflightGitHooksSetup(rootDir, packageManager, packages);
4858
+ if (reason) {
4859
+ log.warn(`⚠ ${reason}`);
4860
+ return false;
4861
+ }
4862
+ return true;
3369
4863
  }
3370
4864
  async function collectAgentInstructionPlan(rootDir, options) {
3371
4865
  const existingAgentTargetPaths = options.agent !== void 0 || !options.interactive ? void 0 : detectExistingAgentTargetPaths(rootDir);
@@ -3469,13 +4963,9 @@ async function collectMigrationSetupPlan(rootDir, packageManager, options, packa
3469
4963
  ...eslintPlan
3470
4964
  };
3471
4965
  }
3472
- //#endregion
3473
- //#region src/migration/bin.ts
4966
+ // src/migration/bin.ts
3474
4967
  async function confirmNodeVersionFileMigration(interactive, detection) {
3475
- const message = {
3476
- "package.json": "Migrate Volta node version (package.json) to .node-version?",
3477
- ".nvmrc": "Migrate .nvmrc to .node-version?"
3478
- }[detection.file];
4968
+ const message = { "package.json": "Migrate Volta node version (package.json) to .node-version?" }[detection.file];
3479
4969
  if (interactive) {
3480
4970
  const confirmed = await confirm({
3481
4971
  message,
@@ -3505,7 +4995,7 @@ async function confirmYarnNodeModulesMode(rootDir, packageManager, packageManage
3505
4995
  if (packageManager !== PackageManager.yarn) return false;
3506
4996
  const pnp = detectYarnPnpMode(rootDir, packageManagerVersion);
3507
4997
  if (!pnp) return false;
3508
- log$1.warn(`⚠ Vite+ does not currently support Yarn Plug'n'Play (PnP).`);
4998
+ log.warn(`⚠ Vite+ does not currently support Yarn Plug'n'Play (PnP).`);
3509
4999
  if (pnp.source === "environment") cancelAndExit("YARN_NODE_LINKER=pnp overrides project configuration. Set it to node-modules or unset it, then re-run `vp migrate`.", 1);
3510
5000
  if (interactive) {
3511
5001
  const confirmed = await confirm({
@@ -3609,7 +5099,7 @@ function hasExistingVitePlusMigrationCandidates(workspaceInfo, options) {
3609
5099
  const eslintProject = detectEslintProject(workspaceInfo.rootDir, workspaceInfo.packages);
3610
5100
  const prettierProject = detectPrettierProject(workspaceInfo.rootDir, workspaceInfo.packages);
3611
5101
  const tsupProject = detectTsupProject(workspaceInfo.rootDir, workspaceInfo.packages);
3612
- return hasExplicitExistingVitePlusSetupRequest(options) || detectLegacyGitHooksMigrationCandidate(workspaceInfo.rootDir) || hasBaseUrlInWorkspace(workspaceInfo) || eslintProject.hasDependency || prettierProject.hasDependency || tsupProject.hasDependency || detectNodeVersionManagerFile(workspaceInfo.rootDir) !== void 0 || getFrameworkShimCandidates(workspaceInfo.rootDir, workspaceInfo.packages).length > 0;
5102
+ return hasExplicitExistingVitePlusSetupRequest(options) || detectLegacyGitHooksMigrationCandidate(workspaceInfo.rootDir) || hasBaseUrlInWorkspace(workspaceInfo) || eslintProject.hasDependency || prettierProject.hasDependency || tsupProject.hasDependency || detectTsxProject(workspaceInfo.rootDir, workspaceInfo.packages) || detectOxnodeProject(workspaceInfo.rootDir, workspaceInfo.packages) || detectTsNodeProject(workspaceInfo.rootDir, workspaceInfo.packages) || detectNodeVersionManagerFile(workspaceInfo.rootDir) !== void 0 || getFrameworkShimCandidates(workspaceInfo.rootDir, workspaceInfo.packages).length > 0;
3613
5103
  }
3614
5104
  function getExistingVitePlusSetupOptions(options, useFullMigrationDefaults = false) {
3615
5105
  if (useFullMigrationDefaults) return options;
@@ -3629,6 +5119,9 @@ async function collectMigrationPlan(rootDir, detectedPackageManager, detectedPac
3629
5119
  if (prettierProject.hasDependency && prettierProject.configFile) migratePrettier = await confirmPrettierMigration(options.interactive);
3630
5120
  else if (prettierProject.hasDependency) warnPackageLevelPrettier();
3631
5121
  const { migrateTsup, tsupConfigFile } = await collectTsupMigrationDecision(rootDir, options, packages);
5122
+ const migrateTsx = detectTsxProject(rootDir, packages) && await confirmTsxMigration(options.interactive);
5123
+ const migrateOxnode = detectOxnodeProject(rootDir, packages) && await confirmOxnodeMigration(options.interactive);
5124
+ const migrateTsNode = detectTsNodeProject(rootDir, packages) && await confirmTsNodeMigration(options.interactive);
3632
5125
  const fixBaseUrl = hasBaseUrlInWorkspace({
3633
5126
  rootDir,
3634
5127
  packages
@@ -3646,83 +5139,70 @@ async function collectMigrationPlan(rootDir, detectedPackageManager, detectedPac
3646
5139
  prettierConfigFile: prettierProject.configFile,
3647
5140
  migrateTsup,
3648
5141
  tsupConfigFile,
5142
+ migrateTsx,
5143
+ migrateOxnode,
5144
+ migrateTsNode,
3649
5145
  fixBaseUrl,
3650
5146
  migrateNodeVersionFile,
3651
5147
  nodeVersionDetection,
3652
5148
  frameworkShimFrameworks
3653
5149
  };
3654
5150
  }
3655
- /**
3656
- * Reconcile a CommandRunSummary from `runViteInstall` with the migration's
3657
- * duration counter and exit-code state. `runViteInstall` returns
3658
- * `{ status: 'failed', exitCode }` without throwing; treating that as a success
3659
- * (incrementing duration unconditionally) would let the migration claim
3660
- * "Dependencies installed" while node_modules is desynced from the just-mutated
3661
- * package.json. This helper centralizes the right handling: credit duration on
3662
- * success, warn + flip exitCode on failure, stay silent on skip.
3663
- */
3664
- function handleInstallResult(installSummary, rootDir, report, options) {
3665
- if (installSummary.status === "installed") return installSummary.durationMs;
3666
- if (installSummary.status === "failed") {
3667
- const exitCode = installSummary.exitCode ?? 1;
3668
- const message = `Dependency installation failed (exit code ${exitCode}). Run \`vp install\` manually in ${rootDir} to resync node_modules.`;
3669
- warnMsg(message);
3670
- report.warnings.push(message);
3671
- if (options?.propagateExitCode !== false) process.exitCode = exitCode;
3672
- return 0;
3673
- }
3674
- return 0;
3675
- }
3676
5151
  const FULL_MIGRATION_HINT = `${styleText("gray", "•")} Skipped editor, hooks, and lint setup. Run \`vp migrate --full\` to apply them.`;
3677
5152
  function showMigrationSummary(options) {
3678
5153
  const { projectRoot, packageManager, packageManagerVersion, installDurationMs, finalInstallOk, report, updatedExistingVitePlus, suggestFullMigration } = options;
3679
5154
  const projectLabel = displayRelative(projectRoot) || ".";
3680
5155
  const configUpdates = report.createdViteConfigCount + report.mergedConfigCount + report.mergedStagedConfigCount + report.inlinedLintStagedConfigCount + report.removedConfigCount + report.tsdownImportCount + report.wrappedPluginConfigCount + report.migratedTaskCacheConfigCount;
3681
- log(`${styleText("magenta", "◇")} ${updatedExistingVitePlus ? "Updated" : "Migrated"} ${accent(projectLabel)} to Vite+ ${VITE_PLUS_VERSION}`);
3682
- log(`${styleText("gray", "•")} Node ${process.versions.node} ${packageManager} ${packageManagerVersion}`);
5156
+ log$1(`${styleText("magenta", "◇")} ${updatedExistingVitePlus ? "Updated" : "Migrated"} ${accent(projectLabel)} to Vite+ ${VITE_PLUS_VERSION}`);
5157
+ log$1(`${styleText("gray", "•")} Node ${process.versions.node} ${packageManager} ${packageManagerVersion}`);
3683
5158
  if (report.dependencyUpgrades.length > 0) {
3684
5159
  const nameWidth = Math.max(...report.dependencyUpgrades.map((change) => change.name.length));
3685
5160
  const fromWidth = Math.max(...report.dependencyUpgrades.map((change) => (change.from ?? "").length));
3686
- log(`${styleText("gray", "•")} Dependencies:`);
5161
+ log$1(`${styleText("gray", "•")} Dependencies:`);
3687
5162
  for (const change of report.dependencyUpgrades) {
3688
5163
  const name = change.name.padEnd(nameWidth);
3689
5164
  const from = muted((change.from ?? "").padEnd(fromWidth));
3690
5165
  const to = styleText("green", change.to);
3691
- log(` ${name} ${from} → ${to}`);
5166
+ log$1(` ${name} ${from} → ${to}`);
3692
5167
  }
3693
5168
  }
3694
- if (finalInstallOk && installDurationMs > 0) log(`${styleText("green", "✓")} Dependencies installed in ${formatDuration(installDurationMs)}`);
5169
+ if (finalInstallOk && installDurationMs > 0) log$1(`${styleText("green", "✓")} Dependencies installed in ${formatDuration(installDurationMs)}`);
3695
5170
  if (configUpdates > 0 || report.rewrittenImportFileCount > 0) {
3696
5171
  const parts = [];
3697
5172
  if (configUpdates > 0) parts.push(`${configUpdates} ${configUpdates === 1 ? "config update" : "config updates"} applied`);
3698
5173
  if (report.rewrittenImportFileCount > 0) parts.push(`${report.rewrittenImportFileCount} ${report.rewrittenImportFileCount === 1 ? "file had" : "files had"} imports rewritten`);
3699
- log(`${styleText("gray", "•")} ${parts.join(", ")}`);
5174
+ log$1(`${styleText("gray", "•")} ${parts.join(", ")}`);
3700
5175
  }
3701
- if (report.preservedUpstreamVitestImportFileCount > 0) log(`${styleText("gray", "•")} Kept upstream \`vitest\` imports in ${report.preservedUpstreamVitestImportFileCount} ${report.preservedUpstreamVitestImportFileCount === 1 ? "file" : "files"} for @nuxt/test-utils compatibility`);
3702
- if (report.eslintMigrated) log(`${styleText("gray", "•")} ESLint rules migrated to Oxlint`);
3703
- if (report.prettierMigrated) log(`${styleText("gray", "•")} Prettier migrated to Oxfmt`);
3704
- if (report.tsupMigrated) log(`${styleText("gray", "•")} tsup config migrated to tsdown (\`vp pack\`)`);
3705
- if (report.nodeVersionFileMigrated) log(`${styleText("gray", "•")} Node version manager file migrated to .node-version`);
5176
+ if (report.preservedUpstreamVitestImportFileCount > 0) log$1(`${styleText("gray", "•")} Kept upstream \`vitest\` imports in ${report.preservedUpstreamVitestImportFileCount} ${report.preservedUpstreamVitestImportFileCount === 1 ? "file" : "files"} for @nuxt/test-utils compatibility`);
5177
+ if (report.eslintMigrated) log$1(`${styleText("gray", "•")} ESLint rules migrated to Oxlint`);
5178
+ if (report.prettierMigrated) log$1(`${styleText("gray", "•")} Prettier migrated to Oxfmt`);
5179
+ if (report.tsupMigrated) log$1(`${styleText("gray", "•")} tsup config migrated to tsdown (\`vp pack\`)`);
5180
+ if (report.tsxMigrated) log$1(`${styleText("gray", "•")} tsx scripts migrated to Vite+`);
5181
+ if (report.oxnodeMigrated) log$1(`${styleText("gray", "•")} oxnode scripts migrated to Vite+`);
5182
+ if (report.tsNodeMigrated) log$1(`${styleText("gray", "•")} ts-node scripts migrated to Vite+`);
5183
+ if (report.nodeVersionFileMigrated) log$1(`${styleText("gray", "•")} Node version manager file migrated to .node-version`);
3706
5184
  const setupVpFileCount = report.setupVpVersionUpdatedFileCount;
3707
5185
  if (setupVpFileCount > 0) {
3708
5186
  const fileLabel = setupVpFileCount === 1 ? "file" : "files";
3709
- log(`${styleText("gray", "•")} setup-vp updated to ${SETUP_VP_VERSION} in ${setupVpFileCount} GitHub Actions ${fileLabel}`);
3710
- }
3711
- if (report.wrappedPluginConfigCount > 0) log(`${styleText("gray", "•")} Inline Vite plugins wrapped with lazyPlugins for check/lint/fmt`);
3712
- if (report.migratedTaskCacheConfigCount > 0) log(`${styleText("gray", "•")} Task cache settings moved under \`cache\``);
3713
- if (report.gitHooksConfigured) log(`${styleText("gray", "•")} Git hooks configured`);
3714
- if (report.frameworkShimAdded) log(`${styleText("gray", "•")} TypeScript shim added for framework component files`);
3715
- if (report.packageManagerBootstrapConfigured) log(`${styleText("gray", "•")} Package manager settings configured`);
3716
- if (suggestFullMigration) log(FULL_MIGRATION_HINT);
3717
- if (report.warnings.length > 0) {
3718
- log(`${styleText("yellow", "!")} Warnings:`);
3719
- for (const warning of report.warnings) log(` - ${warning}`);
5187
+ log$1(`${styleText("gray", "•")} setup-vp updated to ${SETUP_VP_VERSION} in ${setupVpFileCount} GitHub Actions ${fileLabel}`);
3720
5188
  }
5189
+ if (report.wrappedPluginConfigCount > 0) log$1(`${styleText("gray", "•")} Inline Vite plugins wrapped with lazyPlugins for check/lint/fmt`);
5190
+ if (report.migratedTaskCacheConfigCount > 0) log$1(`${styleText("gray", "•")} Task cache settings moved under \`cache\``);
5191
+ if (report.gitHooksConfigured) log$1(`${styleText("gray", "•")} Git hooks configured`);
5192
+ if (report.frameworkShimAdded) log$1(`${styleText("gray", "•")} TypeScript shim added for framework component files`);
5193
+ if (report.packageManagerBootstrapConfigured) log$1(`${styleText("gray", "•")} Package manager settings configured`);
5194
+ if (suggestFullMigration) log$1(FULL_MIGRATION_HINT);
5195
+ showMigrationWarnings(report.warnings);
3721
5196
  if (report.manualSteps.length > 0) {
3722
- log(`${styleText("blue", "→")} Manual follow-up:`);
3723
- for (const step of report.manualSteps) log(` - ${step}`);
5197
+ log$1(`${styleText("blue", "→")} Manual follow-up:`);
5198
+ for (const step of report.manualSteps) log$1(` - ${step}`);
3724
5199
  }
3725
5200
  }
5201
+ function showMigrationWarnings(warnings) {
5202
+ if (warnings.length === 0) return;
5203
+ log$1(`${styleText("yellow", "!")} Warnings:`);
5204
+ for (const warning of warnings) log$1(` - ${warning}`);
5205
+ }
3726
5206
  async function downloadSupportedPackageManager(options) {
3727
5207
  const { rootDir, packageManager, packageManagerVersion, interactive, updateMigrationProgress, failMigrationProgress } = options;
3728
5208
  updateMigrationProgress("Preparing migration");
@@ -3732,11 +5212,11 @@ async function downloadSupportedPackageManager(options) {
3732
5212
  await upgradeYarn(rootDir, interactive, true);
3733
5213
  } else if (packageManager === PackageManager.pnpm && satisfies(downloadResult.version, "< 9.5.0")) {
3734
5214
  failMigrationProgress("Migration failed");
3735
- log$1.error(`✘ pnpm@${downloadResult.version} is not supported by auto migration, please upgrade pnpm to >=9.5.0 first`);
5215
+ log.error(`✘ pnpm@${downloadResult.version} is not supported by auto migration, please upgrade pnpm to >=9.5.0 first`);
3736
5216
  cancelAndExit("Vite+ cannot automatically migrate this project yet.", 1);
3737
5217
  } else if (packageManager === PackageManager.npm && satisfies(downloadResult.version, "< 8.3.0")) {
3738
5218
  failMigrationProgress("Migration failed");
3739
- log$1.error(`✘ npm@${downloadResult.version} is not supported by auto migration, please upgrade npm to >=8.3.0 first`);
5219
+ log.error(`✘ npm@${downloadResult.version} is not supported by auto migration, please upgrade npm to >=8.3.0 first`);
3740
5220
  cancelAndExit("Vite+ cannot automatically migrate this project yet.", 1);
3741
5221
  }
3742
5222
  return downloadResult;
@@ -3753,7 +5233,7 @@ function completeVitestV5Migration(plan, report) {
3753
5233
  function applyRefreshedVitestV5Migration(plan) {
3754
5234
  const refreshed = refreshVitestV5Migration(plan);
3755
5235
  if (refreshed.findings.some((finding) => finding.severity === "block")) {
3756
- log$1.warn(formatVitestV5Findings(refreshed));
5236
+ log.warn(formatVitestV5Findings(refreshed));
3757
5237
  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
5238
  }
3759
5239
  applyVitestV5Migration(refreshed);
@@ -3761,6 +5241,7 @@ function applyRefreshedVitestV5Migration(plan) {
3761
5241
  }
3762
5242
  async function executeMigrationPlan(workspaceInfoOptional, plan, interactive, vitestV5Plan, preExistingChangedPaths) {
3763
5243
  const report = createMigrationReport();
5244
+ report.warnings.push(...detectMigrationWarnings(workspaceInfoOptional.rootDir, workspaceInfoOptional.packages));
3764
5245
  const migrationProgress = interactive ? spinner({ indicator: "timer" }) : void 0;
3765
5246
  let migrationProgressStarted = false;
3766
5247
  const updateMigrationProgress = (message) => {
@@ -3801,7 +5282,7 @@ async function executeMigrationPlan(workspaceInfoOptional, plan, interactive, vi
3801
5282
  if (plan.convertYarnPnp) {
3802
5283
  updateMigrationProgress("Configuring Yarn node-modules mode");
3803
5284
  report.packageManagerBootstrapConfigured = configureYarnNodeModulesMode(workspaceInfo.rootDir);
3804
- if (report.packageManagerBootstrapConfigured) log$1.success("✔ Switched Yarn to node-modules mode");
5285
+ if (report.packageManagerBootstrapConfigured) log.success("✔ Switched Yarn to node-modules mode");
3805
5286
  }
3806
5287
  if (plan.migrateNodeVersionFile && plan.nodeVersionDetection) {
3807
5288
  updateMigrationProgress("Migrating node version file");
@@ -3855,6 +5336,18 @@ async function executeMigrationPlan(workspaceInfoOptional, plan, interactive, vi
3855
5336
  cancelAndExit("Complete the tsup migration manually, then re-run `vp migrate`.", 1);
3856
5337
  }
3857
5338
  }
5339
+ if (plan.migrateTsx) {
5340
+ updateMigrationProgress("Migrating tsx scripts");
5341
+ migrateTsxToVpx(workspaceInfo.rootDir, workspaceInfo.packages, report);
5342
+ }
5343
+ if (plan.migrateOxnode) {
5344
+ updateMigrationProgress("Migrating oxnode scripts");
5345
+ migrateOxnodeToVpx(workspaceInfo.rootDir, workspaceInfo.packages, report);
5346
+ }
5347
+ if (plan.migrateTsNode) {
5348
+ updateMigrationProgress("Migrating ts-node scripts");
5349
+ migrateTsNodeToVpx(workspaceInfo.rootDir, workspaceInfo.packages, report);
5350
+ }
3858
5351
  vitestV5Plan = applyRefreshedVitestV5Migration(vitestV5Plan);
3859
5352
  const skipStagedMigration = shouldSkipStagedMigrationForHooks(workspaceInfo.rootDir, plan.shouldSetupHooks, plan.packageManager, workspaceInfo.packages);
3860
5353
  updateMigrationProgress("Rewriting configs");
@@ -3920,11 +5413,11 @@ async function main() {
3920
5413
  let vitestV5Plan = planVitestV5Migration(workspaceInfoOptional);
3921
5414
  const vitestV5Preflight = formatVitestV5Findings(vitestV5Plan);
3922
5415
  if (vitestV5Plan.findings.some((finding) => finding.severity === "block")) {
3923
- log$1.warn(vitestV5Preflight);
5416
+ log.warn(vitestV5Preflight);
3924
5417
  cancelAndExit("Resolve the blocking Vitest v5 findings, then re-run `vp migrate`. No project files were changed.", 1);
3925
5418
  }
3926
5419
  const rootPkg = readNearestPackageJson(workspaceInfoOptional.rootDir);
3927
- if (workspaceInfoOptional.packageManager && !hasPackageManagerDeclaration(rootPkg)) log$1.warn(`No package manager is declared in package.json; using ${workspaceInfoOptional.packageManager} for this migration without adding a pin. Run \`vp env pin\` to declare it explicitly.`);
5420
+ if (workspaceInfoOptional.packageManager && !hasPackageManagerDeclaration(rootPkg)) log.warn(`No package manager is declared in package.json; using ${workspaceInfoOptional.packageManager} for this migration without adding a pin. Run \`vp env pin\` to declare it explicitly.`);
3928
5421
  if (hasVitePlusDependency(rootPkg) && !isForceOverrideMode()) {
3929
5422
  let convertYarnPnp = await confirmYarnNodeModulesMode(workspaceInfoOptional.rootDir, workspaceInfoOptional.packageManager, workspaceInfoOptional.packageManagerVersion, options.interactive);
3930
5423
  let didMigrate = vitestV5NeedsMigration(vitestV5Plan);
@@ -3933,6 +5426,7 @@ async function main() {
3933
5426
  let finalInstallOk = true;
3934
5427
  let canFormatMigratedProject = !process.env.VP_SKIP_INSTALL;
3935
5428
  const report = createMigrationReport();
5429
+ const migrationWarnings = options.full ? detectMigrationWarnings(workspaceInfoOptional.rootDir, workspaceInfoOptional.packages) : [];
3936
5430
  const migrationProgress = options.interactive ? spinner({ indicator: "timer" }) : void 0;
3937
5431
  let migrationProgressStarted = false;
3938
5432
  const updateMigrationProgress = (message) => {
@@ -3987,16 +5481,19 @@ async function main() {
3987
5481
  updateMigrationProgress("Rewriting toolchain imports across the workspace");
3988
5482
  const coreMigrationResult = finalizeCoreMigrationForExistingVitePlus(workspaceInfoOptional, true, report, pendingCoreMigration);
3989
5483
  if (coreMigrationResult.dependencies || coreMigrationResult.scripts || coreMigrationResult.tsconfigTypes || coreMigrationResult.imports || coreMigrationResult.tsdownConfig || coreMigrationResult.taskCacheConfig) didMigrate = true;
5484
+ if (retryPreservedOxlintConfigs(workspaceInfoOptional.rootDir, true, report, workspaceInfoOptional.packages)) didMigrate = true;
3990
5485
  clearMigrationProgress();
3991
5486
  const fullSetup = options.full === true;
3992
5487
  const skippedSetupCandidates = !fullSetup && !hasExplicitExistingVitePlusSetupRequest(options) && hasExistingVitePlusMigrationCandidates(workspaceInfoOptional, options);
3993
5488
  if (!didMigrate && !convertYarnPnp && report.warnings.length === 0 && !vitePlusBootstrapPending && !(fullSetup ? hasExistingVitePlusMigrationCandidates(workspaceInfoOptional, options) : hasExplicitExistingVitePlusSetupRequest(options))) {
3994
- if (vitestV5Preflight) log$1.warn(vitestV5Preflight);
3995
- for (const warning of coreMigrationResult.taskCacheWarnings) log$1.warn(warning);
3996
- if (skippedSetupCandidates) log(FULL_MIGRATION_HINT);
5489
+ showMigrationWarnings(migrationWarnings);
5490
+ if (vitestV5Preflight) log.warn(vitestV5Preflight);
5491
+ for (const warning of coreMigrationResult.taskCacheWarnings) log.warn(warning);
5492
+ if (skippedSetupCandidates) log$1(FULL_MIGRATION_HINT);
3997
5493
  outro(`This project is already using Vite+! ${accent("Happy coding!")}`);
3998
5494
  return;
3999
5495
  }
5496
+ report.warnings.push(...migrationWarnings);
4000
5497
  for (const warning of coreMigrationResult.taskCacheWarnings) addMigrationWarning(report, warning);
4001
5498
  const setupOptions = getExistingVitePlusSetupOptions(options, fullSetup);
4002
5499
  const plan = await collectMigrationSetupPlan(workspaceInfoOptional.rootDir, packageManager, setupOptions, workspaceInfoOptional.packages, fullSetup);
@@ -4077,6 +5574,33 @@ async function main() {
4077
5574
  tsupMigrated = true;
4078
5575
  }
4079
5576
  }
5577
+ if (fullSetup && detectTsxProject(workspaceInfoOptional.rootDir, workspaceInfoOptional.packages)) {
5578
+ if (options.interactive) clearMigrationProgress();
5579
+ if (await confirmTsxMigration(options.interactive)) {
5580
+ updateMigrationProgress("Migrating tsx scripts");
5581
+ const tsxResult = migrateTsxToVpx(workspaceInfoOptional.rootDir, workspaceInfoOptional.packages, report);
5582
+ didMigrate ||= tsxResult.changed;
5583
+ needsInstall ||= tsxResult.dependencyRemoved;
5584
+ }
5585
+ }
5586
+ if (fullSetup && detectOxnodeProject(workspaceInfoOptional.rootDir, workspaceInfoOptional.packages)) {
5587
+ if (options.interactive) clearMigrationProgress();
5588
+ if (await confirmOxnodeMigration(options.interactive)) {
5589
+ updateMigrationProgress("Migrating oxnode scripts");
5590
+ const oxnodeResult = migrateOxnodeToVpx(workspaceInfoOptional.rootDir, workspaceInfoOptional.packages, report);
5591
+ didMigrate ||= oxnodeResult.changed;
5592
+ needsInstall ||= oxnodeResult.dependencyRemoved;
5593
+ }
5594
+ }
5595
+ if (fullSetup && detectTsNodeProject(workspaceInfoOptional.rootDir, workspaceInfoOptional.packages)) {
5596
+ if (options.interactive) clearMigrationProgress();
5597
+ if (await confirmTsNodeMigration(options.interactive)) {
5598
+ updateMigrationProgress("Migrating ts-node scripts");
5599
+ const tsNodeResult = migrateTsNodeToVpx(workspaceInfoOptional.rootDir, workspaceInfoOptional.packages, report);
5600
+ didMigrate ||= tsNodeResult.changed;
5601
+ needsInstall ||= tsNodeResult.dependencyRemoved;
5602
+ }
5603
+ }
4080
5604
  if (fullSetup) {
4081
5605
  const nodeVersionDetection = detectNodeVersionManagerFile(workspaceInfoOptional.rootDir);
4082
5606
  if (nodeVersionDetection) {
@@ -4087,7 +5611,7 @@ async function main() {
4087
5611
  if (convertYarnPnp) {
4088
5612
  updateMigrationProgress("Configuring Yarn node-modules mode");
4089
5613
  if (configureYarnNodeModulesMode(workspaceInfoOptional.rootDir)) {
4090
- log$1.success("✔ Switched Yarn to node-modules mode");
5614
+ log.success("✔ Switched Yarn to node-modules mode");
4091
5615
  report.packageManagerBootstrapConfigured = true;
4092
5616
  didMigrate = true;
4093
5617
  needsInstall = true;
@@ -4102,7 +5626,10 @@ async function main() {
4102
5626
  ].includes(path.basename(file)));
4103
5627
  if (eslintMigrated || prettierMigrated || tsupMigrated) {
4104
5628
  updateMigrationProgress("Rewriting configs");
4105
- mergeViteConfigFiles(workspaceInfoOptional.rootDir, true, report, workspaceInfoOptional.packages);
5629
+ const rootDir = workspaceInfoOptional.rootDir;
5630
+ const oxlintConfigs = workspaceInfoOptional.isMonorepo ? prepareOxlintConfigs([rootDir, ...workspaceInfoOptional.packages.map((pkg) => path.join(rootDir, pkg.path))]) : void 0;
5631
+ mergeViteConfigFiles(rootDir, true, report, workspaceInfoOptional.packages, rootDir, oxlintConfigs);
5632
+ if (oxlintConfigs) removeMigratedOxlintConfigs(oxlintConfigs);
4106
5633
  if (tsupMigrated) {
4107
5634
  mergeTsdownConfigFile(workspaceInfoOptional.rootDir, true, report);
4108
5635
  for (const pkg of workspaceInfoOptional.packages ?? []) mergeTsdownConfigFile(path.join(workspaceInfoOptional.rootDir, pkg.path), true, report);
@@ -4187,7 +5714,7 @@ async function main() {
4187
5714
  });
4188
5715
  } else {
4189
5716
  clearMigrationProgress();
4190
- if (skippedSetupCandidates) log(FULL_MIGRATION_HINT);
5717
+ if (skippedSetupCandidates) log$1(FULL_MIGRATION_HINT);
4191
5718
  outro(`This project is already using Vite+! ${accent("Happy coding!")}`);
4192
5719
  }
4193
5720
  return;
@@ -4204,9 +5731,8 @@ async function main() {
4204
5731
  });
4205
5732
  }
4206
5733
  main().catch((err) => {
4207
- log$1.error(err.message);
5734
+ log.error(err.message);
4208
5735
  console.error(err);
4209
5736
  process.exit(1);
4210
5737
  });
4211
- //#endregion
4212
5738
  export {};