vite-plus 1.4.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 (68) hide show
  1. package/binding/index.cjs +54 -54
  2. package/binding/index.d.cts +31 -0
  3. package/dist/{agent-D8oxxhVR.js → agent-DkQD3xqe.js} +2 -2
  4. package/dist/bin.js +15 -7
  5. package/dist/config/bin.js +5 -5
  6. package/dist/{constants-DRUnJ_Dz.js → constants-D2P2aVKb.js} +6 -2
  7. package/dist/create/bin.js +114 -64
  8. package/dist/{define-config-rPhYIFSn.d.ts → define-config-BrvHRi-G.d.ts} +30 -2
  9. package/dist/{define-config-Cv2P6NT2.cjs → define-config-CwtQqYC1.cjs} +35 -3
  10. package/dist/{define-config-ChwJcu5e.js → define-config-wR57OZG_.js} +30 -5
  11. package/dist/define-config.cjs +2 -1
  12. package/dist/define-config.d.ts +2 -2
  13. package/dist/define-config.js +1 -1
  14. package/dist/{editor-ClL78D8u.js → editor-aOy1T15H.js} +1266 -884
  15. package/dist/hooks/bin.js +2 -2
  16. package/dist/{hooks-xRcJPc-O.js → hooks-w_e2whkQ.js} +40 -21
  17. package/dist/index.cjs +4 -3
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +2 -2
  20. package/dist/lint-plugins.cjs +1 -1
  21. package/dist/migration/bin.js +1302 -311
  22. package/dist/migration/compat/worker.js +1 -1
  23. package/dist/{oxlint-plugin-config-BVpqYdcC.js → oxlint-plugin-config-CHsI_zKo.js} +1 -1
  24. package/dist/oxlint-plugin.js +29 -4
  25. package/dist/pack-bin.js +2 -2
  26. package/dist/{package-DzsomcUD.js → package-DfFo6-E-.js} +1 -1
  27. package/dist/{prompts-GL1vj0wk.js → prompts-C7DI63WP.js} +3 -3
  28. package/dist/register.d.ts +1 -0
  29. package/dist/register.js +12 -0
  30. package/dist/{report-DLHO30yZ.js → report-BEdpIQvC.js} +2 -0
  31. package/dist/{resolve-vite-config-QO6cC0SP.js → resolve-vite-config-33bzSaeT.js} +1 -1
  32. package/dist/{resolve-vite-config-Dn5896A4.js → resolve-vite-config-Cx3cnVVI.js} +2 -2
  33. package/dist/script-esm-hooks.d.ts +5 -2
  34. package/dist/script-esm-hooks.js +5 -2
  35. package/dist/script-preload.cjs +24 -1
  36. package/dist/script-register.js +7 -2
  37. package/dist/script-watch.js +135 -41
  38. package/dist/staged/bin.js +2 -2
  39. package/dist/sync-versions/bin.js +1 -1
  40. package/dist/toolchain.js +13 -13
  41. package/dist/toolchain.json +13 -13
  42. package/dist/{tsconfig-Do6MAG_f.js → tsconfig-BE3Wj9OX.js} +1 -1
  43. package/dist/{tsconfig-LE_mAwe_.js → tsconfig-shNmmCE_.js} +2 -2
  44. package/dist/version.js +2 -2
  45. package/dist/versions.js +6 -6
  46. package/docs/config/check.md +2 -0
  47. package/docs/config/fmt.md +1 -1
  48. package/docs/config/lint.md +1 -1
  49. package/docs/config/run.md +6 -2
  50. package/docs/config/staged.md +1 -1
  51. package/docs/guide/check.md +5 -1
  52. package/docs/guide/ci.md +2 -2
  53. package/docs/guide/create.md +1 -1
  54. package/docs/guide/env.md +1 -1
  55. package/docs/guide/global-cli.md +12 -8
  56. package/docs/guide/homebrew.md +127 -0
  57. package/docs/guide/ide-integration.md +1 -3
  58. package/docs/guide/implode.md +5 -3
  59. package/docs/guide/local-cli.md +21 -0
  60. package/docs/guide/migrate-rules.md +81 -22
  61. package/docs/guide/migrate.md +9 -9
  62. package/docs/guide/run.md +23 -1
  63. package/docs/guide/test.md +1 -1
  64. package/docs/guide/troubleshooting.md +1 -1
  65. package/docs/guide/upgrade.md +12 -6
  66. package/docs/guide/vitest-v5.md +2 -2
  67. package/docs/guide/vpx.md +44 -4
  68. package/package.json +39 -33
@@ -1,52 +1,36 @@
1
- import { t as documentationUrl$1 } from "../documentation-cv_y0Auv.js";
2
- import { a as muted, c as warnMsg, i as log, o as printHeader, r as formatDuration, t as accent } from "../terminal-BcuPUy-j.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-DRUnJ_Dz.js";
4
- import { a as writeJsonFile, c as modify, i as readJsonFile, l as parse, n as editJsonFile, o as applyEdits, r as isJsonFile, s as findNodeAtLocation, u as parseTree } from "../json-kl84kDyH.js";
5
- import { a as hasVitePlusDependency, i as hasPackageManagerDeclaration, n as detectPackageMetadata, o as readNearestPackageJson } from "../package-DzsomcUD.js";
6
- import { t as vitest_v5_entry_points_default } from "../vitest-v5-entry-points-NyJsFeux.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-GL1vj0wk.js";
8
- import { _ as isGreaterThanOrEqual, p as compare } from "../comparison-CmVirWIW-UlEf2maz.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";
9
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";
10
- import { i as selectAgentTargetPaths, l as displayRelative, n as detectAgentConflicts, r as detectExistingAgentTargetPaths, s as writeAgentInstructions } from "../agent-D8oxxhVR.js";
11
- import { n as addMigrationWarning, r as createMigrationReport } from "../report-DLHO30yZ.js";
12
- import { A as detectVitePlusBootstrapPending, C as removeMigratedOxlintConfigs, Ct as warnMigration, 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 readPackageJsonIfExists, T as injectLintTypeCheckDefaults, U as minimatch, W as findGitRoot, X as confirmTsupMigration, Y as hasFrameworkShim, Z as detectTsupProject, _ as hasExistingViteHooksPolicy, _t as WEBDRIVERIO_PROVIDER, at as warnPackageLevelPrettier, b as preflightGitHooksSetup, bt as getCommandStartIndex, ct as detectIncompatibleEslintIntegration, d as finalizeCoreMigrationForExistingVitePlus, dt as warnIncompatibleEslintIntegration, et as warnMissingTsupConfig, f as detectNodeVersionManagerFile, ft as warnLegacyEslintConfig, g as detectLegacyGitHooksMigrationCandidate, gt as SHELL_STATE_COMMANDS, ht as RETENTION_SCAN_SKIP_DIRS, i as selectEditor, j as ensureVitePlusBootstrap, k as collectToolchainVersionChanges, l as rewriteStandaloneProject, lt as migrateEslintToOxlint, m as migrateSetupVpVersion, mt as LINT_STAGED_JSON_CONFIG_FILES, 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 checkViteVersion, x as shouldSkipStagedMigrationForHooks, xt as isPlainRecord, y as isViteHooksSetupComplete, yt as checkVitestVersion } from "../editor-ClL78D8u.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";
13
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";
14
15
  import { t as require_dist } from "../dist-Bs0SObNC.js";
15
- import { a as hasBaseUrlInTsconfig, i as fixBaseUrlInTsconfig, t as confirmBaseUrlFix } from "../tsconfig-LE_mAwe_.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";
16
18
  import { i as WATCH_OPTIONS_WITH_VALUE, n as SCRIPT_EXTENSIONS, t as NODE_OPTIONS_WITH_VALUE } from "../vpx-script-EXgV5QOf.js";
17
- import { t as unwrapCliParseOutcome } from "../cli-parse-aeUeFDlC.js";
18
- import { t as ROLLDOWN_COMPAT_RESULT_PREFIX } from "../protocol-CkNmY-Mr.js";
19
19
  import path from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
- import { analyzeMigrationSource, isDirectoryGitignored, parseLiteralCommands, parseMigrateArgs } from "../../binding/index.js";
21
+ import { ResolverFactory, analyzeMigrationSource, isDirectoryGitignored, parseLiteralCommands, parseMigrateArgs, parseSync } from "../../binding/index.js";
22
22
  import { styleText } from "node:util";
23
23
  import fs, { existsSync } from "node:fs";
24
24
  import os from "node:os";
25
25
  // src/migration/migrator/tsx.ts
26
26
  const TSX_COMMAND = /(?:^|[\s;&|("'=/\\`])tsx(?:\.(?:cmd|exe|ps1))?(?:\/(?:esm|cjs))?(?=$|[\s;&|)"'`])/i;
27
- const LOADER_OPTIONS = /* @__PURE__ */ new Set([
28
- "--import",
29
- "--loader",
30
- "--experimental-loader",
31
- "--require",
32
- "-r"
33
- ]);
34
27
  const TSX_LOADER = /^tsx(?:\/(?:esm|cjs))?$/;
35
- const LOADER_SETTINGS = /* @__PURE__ */ new Set([
36
- "import",
37
- "loader",
38
- "require"
39
- ]);
40
28
  const KEEP_EVAL = "`vpx` does not run TypeScript passed to --eval or --print";
41
29
  const KEEP_CHECK = "`vpx` does not transform TypeScript for -c or --check";
42
- const KEEP_REPL = "`vpx` does not start a TypeScript REPL";
30
+ const KEEP_REPL$1 = "`vpx` does not start a TypeScript REPL";
43
31
  const KEEP_TEST_GLOBS = "`--test` without files finds `.ts` tests with tsx globs; list the test files for `vpx --test`";
44
32
  const KEEP_WATCH_PATH = "`--watch-path` has no `vpx` equivalent; use `--include`";
45
- const KEEP_SHELL = "the command uses shell syntax that vp migrate does not rewrite, such as pipes, variables, or subshells";
46
- const KEEP_OTHER = "vp migrate does not rewrite this use of tsx";
47
- const KEEP_TSX_TSCONFIG = "`TSX_TSCONFIG_PATH` selects the tsconfig for tsx only; pass it to `vpx` as `--tsconfig`";
48
33
  const KEEP_NODE_OPTIONS = "`NODE_OPTIONS` loads tsx, whose hooks would compete with those of `vpx`; remove it first";
49
- const KEEP_WATCH_TEST = "`vpx --watch` does not run `--test`";
50
34
  const KEEP_OPTIONS = /* @__PURE__ */ new Map([
51
35
  ["-e", KEEP_EVAL],
52
36
  ["--eval", KEEP_EVAL],
@@ -55,12 +39,11 @@ const KEEP_OPTIONS = /* @__PURE__ */ new Map([
55
39
  ["-pe", KEEP_EVAL],
56
40
  ["-c", KEEP_CHECK],
57
41
  ["--check", KEEP_CHECK],
58
- ["-i", KEEP_REPL],
59
- ["--interactive", KEEP_REPL],
42
+ ["-i", KEEP_REPL$1],
43
+ ["--interactive", KEEP_REPL$1],
60
44
  ["--watch-path", KEEP_WATCH_PATH]
61
45
  ]);
62
46
  const TSX_ENV_SHEBANG = /^(#!\s*\S*\benv\s+(?:-S\s+)?)(tsx(?=[ \t\r\n]|$)[^\r\n]*)/;
63
- const SHEBANG_ENTRY = "./shebang.ts";
64
47
  const TSX_SHEBANG = /^#![^\n]*[\s/]tsx(?=\s|$)/;
65
48
  const TSX_LOADER_FLAG = /(?:(?:--import|--loader|--experimental-loader|--require|-r)[= \t]+|(?<![\w-])(?:import|loader|experimental-loader|require)=)['"]?tsx(?:\/(?:esm|cjs))?(?![\w./-])/;
66
49
  const TSX_CODE_REFERENCE = new RegExp([
@@ -70,12 +53,209 @@ const TSX_CODE_REFERENCE = new RegExp([
70
53
  TSX_LOADER_FLAG.source,
71
54
  /['"`](?:--import|--loader|--experimental-loader|--require|-r)['"`]\s*,\s*['"`]tsx(?:\/(?:esm|cjs))?['"`]/.source
72
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/;
73
61
  const TSX_TEXT_REFERENCE = new RegExp([/(?:^|[\s;&|("'=/\\`])tsx(?:\.(?:cmd|exe|ps1))?(?:\/(?:esm|cjs))?['"]?(?:[ \t]+\S|[ \t]*$)/.source, TSX_LOADER_FLAG.source].join("|"), "m");
74
62
  const TSX_CONFIG_VALUE = new RegExp([
75
63
  /(?:^|[\s;&|("'=/\\`])tsx(?:\.(?:cmd|exe|ps1))?[ \t]+\S/.source,
76
64
  TSX_LOADER_FLAG.source,
77
65
  /^tsx\//.source
78
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;
135
+ }
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`;
145
+ }
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`";
79
259
  const IGNORED_FIELDS = /* @__PURE__ */ new Set([
80
260
  "bundleDependencies",
81
261
  "bundledDependencies",
@@ -129,22 +309,6 @@ const USE_SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
129
309
  ".vite",
130
310
  "coverage"
131
311
  ]);
132
- const ESM_SYNTAX = /(?:^|;)\s*(?:import(?![\w$])\s*(?![\w$]+\s*=)[\w*{'"]|export(?![\w$])\s*[\w*{])|\bimport\.meta\b/m;
133
- const COMMONJS_GLOBALS = [
134
- ["__dirname", /(?<![.\w$])__dirname\b/],
135
- ["__filename", /(?<![.\w$])__filename\b/],
136
- ["require", /(?<![.\w$])require\s*(?:\(|\.\s*[\w$])/],
137
- ["exports", /(?<![.\w$'"`])exports(?![\w$'"`]|\s*:)/],
138
- ["module", /(?<![.\w$])module\s*(?:\.\s*[\w$]|\[)/]
139
- ];
140
- const EXPORT_ASSIGNMENT = /(?:^|;)\s*export\s*=/m;
141
- const RELATIVE_REQUIRE = /(?<![.\w$])require\s*\(\s*['"`](\.{1,2}\/[^'"`]+?)\.(js|cjs|mjs|jsx)['"`]/g;
142
- const TYPESCRIPT_EXTENSIONS = {
143
- js: ["ts", "tsx"],
144
- cjs: ["cts"],
145
- mjs: ["mts"],
146
- jsx: ["tsx"]
147
- };
148
312
  const shellCommand = (previous) => /^-[a-z]*c$/.test(previous);
149
313
  const COMMAND_LAUNCHERS = /* @__PURE__ */ new Map([
150
314
  ["bash", shellCommand],
@@ -154,15 +318,11 @@ const COMMAND_LAUNCHERS = /* @__PURE__ */ new Map([
154
318
  ["zsh", shellCommand]
155
319
  ]);
156
320
  /**
157
- * Rewrite the tsx invocations in a command to `vpx`: `tsx a.ts` becomes `vpx a.ts`,
158
- * `tsx watch a.ts` becomes `vpx --watch a.ts` (`vpx watch` runs the npm package `watch`),
159
- * and `node --import tsx a.ts` becomes `vpx a.ts`. Package-manager wrappers such as
160
- * `npx tsx` go away with tsx, since `vpx` is a bin of vite-plus. A form `vpx` cannot run
161
- * stays as it is, with the reason in `kept`. With `cwd`, the directory the command runs
162
- * in, the scripts it runs are checked for syntax `vpx` runs differently.
321
+ * Rewrite supported runner and Node.js loader invocations while preserving shell
322
+ * syntax and arguments. Unsupported forms remain, with their reasons in `kept`.
163
323
  */
164
- function rewriteTsxCommand(command, cwd) {
165
- if (!TSX_COMMAND.test(command)) return {
324
+ function rewriteScriptRunnerCommand(runner, command, cwd, environment = [], checkingScripts = /* @__PURE__ */ new Set()) {
325
+ if (!runner.commandPattern.test(command) && !runner.envKeepReason(environment)) return {
166
326
  command,
167
327
  kept: []
168
328
  };
@@ -174,8 +334,10 @@ function rewriteTsxCommand(command, cwd) {
174
334
  const rewrite = {
175
335
  command,
176
336
  cwd,
337
+ environment,
177
338
  edits: [],
178
- kept: []
339
+ kept: [],
340
+ checkingScripts
179
341
  };
180
342
  for (const tokens of segments) {
181
343
  const name = tokens[0].value;
@@ -183,17 +345,16 @@ function rewriteTsxCommand(command, cwd) {
183
345
  rewrite.cwd = void 0;
184
346
  continue;
185
347
  }
186
- rewriteSegment(rewrite, tokens);
348
+ rewriteSegment(runner, rewrite, tokens);
187
349
  }
188
- let rewritten = command;
189
- for (const edit of rewrite.edits.toSorted((a, b) => b.start - a.start)) rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
190
- if (!rewrite.kept.length && TSX_COMMAND.test(rewritten)) rewrite.kept.push(KEEP_OTHER);
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}`);
191
352
  return {
192
353
  command: rewritten,
193
354
  kept: rewrite.kept
194
355
  };
195
356
  }
196
- function rewriteSegment(rewrite, tokens) {
357
+ function rewriteSegment(runner, rewrite, tokens) {
197
358
  const values = tokens.map(({ value }) => value);
198
359
  let first = 0;
199
360
  const skipAssignments = () => {
@@ -205,24 +366,160 @@ function rewriteSegment(rewrite, tokens) {
205
366
  first++;
206
367
  skipAssignments();
207
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
+ }
208
379
  if (first >= values.length) return;
209
- const envReason = envKeepReason(values.slice(0, first));
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;
210
388
  const start = first + getCommandStartIndex(values.slice(first));
389
+ if ([
390
+ "env",
391
+ "cross-env",
392
+ "cross-env-shell"
393
+ ].includes(values[start])) environment.keepReason ??= inheritedOptionsKeepReason(runner);
211
394
  const launcher = COMMAND_LAUNCHERS.get(values[start]);
212
- if (values[start] === "tsx") rewriteTsx(rewrite, tokens, first, start, envReason);
213
- else if (values[start] === "node") rewriteNodeLoader(rewrite, tokens, first, start, envReason);
214
- else if (shell) for (const token of tokens.slice(start)) rewriteQuotedCommand(rewrite, token, envReason);
215
- else if (launcher) {
216
- for (let index = start + 1; index < tokens.length; index++) if (launcher(values[index - 1])) rewriteQuotedCommand(rewrite, tokens[index], envReason);
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",
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;
217
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 (![
449
+ "npm",
450
+ "pnpm",
451
+ "yarn",
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
+ };
218
492
  }
219
- /**
220
- * Why environment assignments keep tsx for the command they apply to, or undefined.
221
- * Names match in any case, as on Windows.
222
- */
223
- function envKeepReason(assignments) {
224
- if (assignments.some((value) => /^TSX_TSCONFIG_PATH=/i.test(value))) return KEEP_TSX_TSCONFIG;
225
- if (assignments.some((value) => /^NODE_OPTIONS=/i.test(value) && TSX_COMMAND.test(value.slice(13)))) return KEEP_NODE_OPTIONS;
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;
226
523
  }
227
524
  /**
228
525
  * An option's name without `=value`. Node.js reads `_` as `-` in long option names, as
@@ -235,75 +532,42 @@ function optionName(arg) {
235
532
  /** Remove `tokens[index]` with the space before it. */
236
533
  function removal(tokens, index) {
237
534
  return {
238
- start: tokens[index - 1].end,
535
+ start: tokens[index - 1]?.end ?? tokens[index].start,
239
536
  end: tokens[index].end,
240
537
  text: ""
241
538
  };
242
539
  }
243
540
  /**
244
- * Why `vpx` would fail on a script that tsx runs, judged from its source. tsx resolves
245
- * `require('./a.js')` to `a.ts`, and compiles ES module syntax to CommonJS in a `.cts`
246
- * file and in a package without `"type": "module"`. `vpx` resolves `require()` as
247
- * Node.js does, rejects ES module syntax in a `.cts` file, and runs the other files as ES
248
- * modules, so `__dirname`, `__filename`, and `require` are not defined.
249
- */
250
- function scriptKeepReason(cwd, script) {
251
- if (cwd === void 0 || /[*?[{]/.test(script) || !SCRIPT_EXTENSIONS.some((extension) => script.endsWith(extension))) return;
252
- const file = script.startsWith("~/") ? path.join(os.homedir(), script.slice(2)) : path.resolve(cwd, script);
253
- let source;
254
- try {
255
- source = fs.readFileSync(file, "utf8");
256
- } catch (error) {
257
- return error.code === "ENOENT" ? `\`${script}\` does not exist yet, so vp migrate cannot check it` : void 0;
258
- }
259
- for (const [, base, extension] of source.matchAll(RELATIVE_REQUIRE)) {
260
- const resolve = (ext) => path.resolve(path.dirname(file), `${base}.${ext}`);
261
- const typescript = fs.existsSync(resolve(extension)) ? void 0 : TYPESCRIPT_EXTENSIONS[extension].find((ext) => fs.existsSync(resolve(ext)));
262
- if (typescript) return `\`${script}\` requires \`${base}.${extension}\`, which only tsx resolves to \`${base}.${typescript}\`; write the TypeScript extension`;
263
- }
264
- const code = stripComments(source);
265
- if (!/\.(?:c?ts|tsx|jsx?)$/.test(script) || !(ESM_SYNTAX.test(source) || ESM_SYNTAX.test(code))) return;
266
- if (script.endsWith(".cts")) return `\`${script}\` uses import or export, which \`vpx\` does not compile to CommonJS; rename it to .mts`;
267
- 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`;
268
- }
269
- /** `source` without comments; a string that only looks like one, as in `'http://x'`, stays. */
270
- function stripComments(source) {
271
- return source.replaceAll(/("(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|`(?:\\.|[^`\\])*`)|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (_, string) => string ?? "");
272
- }
273
- /**
274
- * Whether `code`, without comments, uses a CommonJS global it does not declare or import
275
- * at module level, as in `const require = createRequire(import.meta.url)` or `import
276
- * module from 'node:module'`. A declaration inside a function, which an unindented line
277
- * does not start, covers only that function.
278
- */
279
- function usesCommonJsGlobals(code) {
280
- 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));
281
- }
282
- /**
283
- * Why `vpx` cannot run what tsx or `node --import tsx` runs: `positionals` are the entry
541
+ * Why `vpx` cannot run an invocation: `positionals` are the entry
284
542
  * and its arguments, or with `--test`, the test files; `preloads` are local `--require`
285
543
  * and `--import` modules, which load first.
286
544
  */
287
- function runKeepReason(cwd, test, positionals, preloads) {
545
+ function runKeepReason(runner, cwd, test, positionals, preloads) {
288
546
  const [entry] = positionals;
289
- if (entry === void 0) return test ? KEEP_TEST_GLOBS : KEEP_REPL;
547
+ if (entry === void 0) return test ? runner.testWithoutFilesReason : KEEP_REPL;
290
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`;
291
549
  const scripts = test ? positionals.filter((value) => !value.startsWith("-")) : [entry];
292
550
  for (const script of [...preloads, ...scripts]) {
293
- const reason = scriptKeepReason(cwd, script);
551
+ const reason = runner.scriptKeepReason?.(cwd, script);
294
552
  if (reason) return reason;
295
553
  }
296
554
  }
297
- function rewriteTsx(rewrite, tokens, first, start, keepReason) {
555
+ function rewriteRunner(runner, rewrite, tokens, first, start, keepReason, environment) {
298
556
  if (keepReason) {
299
557
  rewrite.kept.push(keepReason);
300
558
  return;
301
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;
302
566
  let index = start + 1;
303
- const watch = tokens[index]?.value === "watch";
567
+ const watch = runner.watchSubcommand !== false && tokens[index]?.value === "watch";
304
568
  if (watch) index++;
305
- const scanned = scanOptions(tokens, index, true);
306
- const reason = runReason(rewrite.cwd, tokens, scanned, watch);
569
+ const scanned = scanOptions(runner, tokens, index, true);
570
+ const reason = runReason(runner, rewrite.cwd, tokens, scanned, watch);
307
571
  if (reason) {
308
572
  rewrite.kept.push(reason);
309
573
  return;
@@ -312,15 +576,39 @@ function rewriteTsx(rewrite, tokens, first, start, keepReason) {
312
576
  start: tokens[first].start,
313
577
  end: tokens[index - 1].end,
314
578
  text: watch ? "vpx --watch" : "vpx"
315
- }, ...scanned.removals);
316
- }
317
- function rewriteNodeLoader(rewrite, tokens, first, start, keepReason) {
318
- const scanned = scanOptions(tokens, start + 1, false);
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);
319
607
  if (!scanned.loaders) {
320
- if (keepReason === KEEP_NODE_OPTIONS) rewrite.kept.push(keepReason);
608
+ if (keepReason) rewrite.kept.push(keepReason);
321
609
  return;
322
610
  }
323
- const reason = keepReason ?? runReason(rewrite.cwd, tokens, scanned, false);
611
+ const reason = keepReason ?? runReason(runner, rewrite.cwd, tokens, scanned, false);
324
612
  if (reason) {
325
613
  rewrite.kept.push(reason);
326
614
  return;
@@ -329,14 +617,85 @@ function rewriteNodeLoader(rewrite, tokens, first, start, keepReason) {
329
617
  start: tokens[first].start,
330
618
  end: tokens[start].end,
331
619
  text: "vpx"
332
- }, ...scanned.removals);
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
+ };
333
677
  }
334
- /** Read the options of `tsx` (`tsx` true) or `node` from `tokens[index]` up to the entry. */
335
- function scanOptions(tokens, index, tsx) {
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) {
336
692
  const scanned = {
337
693
  entryIndex: tokens.length,
338
694
  removals: [],
339
695
  loaders: 0,
696
+ runnerLoader: false,
697
+ imports: [],
698
+ inlineCode: false,
340
699
  preloads: [],
341
700
  test: false,
342
701
  watch: false
@@ -349,52 +708,82 @@ function scanOptions(tokens, index, tsx) {
349
708
  }
350
709
  const name = optionName(value);
351
710
  const inline = value.includes("=");
352
- if (tsx && name === "--no-cache") {
711
+ const argument = inline ? value.slice(value.indexOf("=") + 1) : tokens[index + 1]?.value;
712
+ if (directRunner && runner.dropOptions?.has(name)) {
353
713
  scanned.removals.push(removal(tokens, index));
354
714
  continue;
355
715
  }
356
716
  if (LOADER_OPTIONS.has(name)) {
357
- const loader = inline ? value.slice(value.indexOf("=") + 1) : tokens[index + 1]?.value;
358
- if (loader !== void 0 && TSX_LOADER.test(loader)) {
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
+ }
359
726
  scanned.removals.push({
360
727
  ...removal(tokens, index),
361
728
  end: tokens[inline ? index : index + 1].end
362
729
  });
363
730
  scanned.loaders++;
364
- } else if (loader !== void 0 && (loader.startsWith(".") || path.isAbsolute(loader))) scanned.preloads.push(loader);
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
+ }
365
742
  if (!inline) index++;
366
743
  continue;
367
744
  }
368
- scanned.keepReason ??= KEEP_OPTIONS.get(name);
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
+ }
369
757
  scanned.test ||= name === "--test";
370
758
  scanned.watch ||= name === "--watch";
371
- if (!inline && (NODE_OPTIONS_WITH_VALUE.has(name) || tsx && (WATCH_OPTIONS_WITH_VALUE.has(name) || name === "--tsconfig"))) index++;
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++;
372
769
  }
373
770
  return scanned;
374
771
  }
375
772
  /** Why `vpx` cannot run what the scanned options and the scripts after them run. */
376
- function runReason(cwd, tokens, scanned, watch) {
773
+ function runReason(runner, cwd, tokens, scanned, watch) {
377
774
  if (scanned.keepReason) return scanned.keepReason;
378
775
  if ((watch || scanned.watch) && scanned.test) return KEEP_WATCH_TEST;
379
- return runKeepReason(cwd, scanned.test, tokens.slice(scanned.entryIndex).map(({ value }) => value), scanned.preloads);
776
+ return runKeepReason(runner, cwd, scanned.test, tokens.slice(scanned.entryIndex).map(({ value }) => value), scanned.preloads);
380
777
  }
381
- /**
382
- * Rewrite a command that a launcher runs, given as one quoted argument. `inherited` keeps
383
- * tsx for it, as when the launcher passes on `TSX_TSCONFIG_PATH`.
384
- */
385
- function rewriteQuotedCommand(rewrite, token, inherited) {
778
+ /** Rewrite a quoted launcher command with the environment it inherits. */
779
+ function rewriteQuotedCommand(runner, rewrite, token, environment) {
386
780
  const raw = rewrite.command.slice(token.start, token.end);
387
781
  const quote = raw[0];
388
- if (quote !== "\"" && quote !== "'" || raw.length < 2 || !raw.endsWith(quote)) return;
389
- const inner = rewriteTsxCommand(token.value, rewrite.cwd);
782
+ if (quote !== "\"" && quote !== "'" || raw.length < 2 || !raw.endsWith(quote)) return false;
783
+ const inner = rewriteScriptRunnerCommand(runner, token.value, rewrite.cwd, environment, rewrite.checkingScripts);
390
784
  rewrite.kept.push(...inner.kept);
391
- if (inner.command === token.value) return;
392
- if (inherited) rewrite.kept.push(inherited);
393
- else if (!inner.command.includes(quote)) rewrite.edits.push({
394
- start: token.start,
395
- end: token.end,
396
- text: `${quote}${inner.command}${quote}`
397
- });
785
+ if (inner.command !== token.value) rewrite.edits.push(literalEdit(rewrite.command, token, inner.command));
786
+ return token.value.trim() !== "";
398
787
  }
399
788
  /** The commands in a scripts or lint-staged record. */
400
789
  function recordCommands(record) {
@@ -409,38 +798,37 @@ function lintStagedJsonFiles(dir) {
409
798
  return LINT_STAGED_JSON_CONFIG_FILES.map((name) => path.join(dir, name)).filter((file) => fs.existsSync(file) && isJsonFile(file));
410
799
  }
411
800
  /**
412
- * A package that lists tsx outside devDependencies keeps it: its published code or
413
- * production scripts can need tsx, and `vpx` comes from the vite-plus devDependency.
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.
414
803
  */
415
- function ownsTsxAtRuntime(pkg) {
416
- return pkg.dependencies?.tsx !== void 0 || pkg.optionalDependencies?.tsx !== void 0 || pkg.peerDependencies?.tsx !== void 0;
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);
417
806
  }
418
807
  function packageDirs(rootDir, packages) {
419
808
  return [rootDir, ...(packages ?? []).map((pkg) => path.join(rootDir, pkg.path))];
420
809
  }
421
810
  /**
422
- * Whether a script, lint-staged task, or `#!/usr/bin/env tsx` shebang runs tsx in a form
423
- * `vpx` replaces.
811
+ * Detect supported invocations, or retained uses when the profile requests warnings.
424
812
  */
425
- function detectTsxProject(rootDir, packages) {
813
+ function detectScriptRunnerProject(runner, rootDir, packages) {
426
814
  return packageDirs(rootDir, packages).some((dir) => {
427
815
  const pkg = readPackageJsonIfExists(path.join(dir, "package.json"));
428
- if (!pkg || ownsTsxAtRuntime(pkg)) return false;
429
- return [...packageCommands(pkg), ...lintStagedJsonFiles(dir).flatMap((file) => recordCommands(readJsonFile(file)))].some((command) => rewriteTsxCommand(command, dir).command !== command) || sourceTreeMatches(dir, (content, file) => {
430
- const result = rewriteTsxShebang(content, file);
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);
431
819
  return result !== void 0 && "content" in result;
432
- });
820
+ }, { includeExtensionless: runner.includeExtensionlessShebangs });
433
821
  });
434
822
  }
435
823
  /** Rewrite each command of a scripts or lint-staged record; returns whether one changed. */
436
- function rewriteCommandRecord(record, label, cwd, report) {
824
+ function rewriteCommandRecord(runner, record, label, cwd, report) {
437
825
  let edited = false;
438
826
  for (const [key, value] of Object.entries(record)) {
439
827
  const commands = Array.isArray(value) ? value : [value];
440
828
  const rewritten = commands.map((command) => {
441
829
  if (typeof command !== "string") return command;
442
- const result = rewriteTsxCommand(command, cwd);
443
- for (const reason of result.kept) warnMigration(`Kept tsx in ${label} "${key}": ${reason}.`, report);
830
+ const result = rewriteScriptRunnerCommand(runner, command, cwd);
831
+ for (const reason of result.kept) warnMigration(`Kept ${runner.name} in ${label} "${key}": ${reason}.`, report);
444
832
  return result.command;
445
833
  });
446
834
  if (rewritten.some((command, index) => command !== commands[index])) {
@@ -451,98 +839,144 @@ function rewriteCommandRecord(record, label, cwd, report) {
451
839
  return edited;
452
840
  }
453
841
  /**
454
- * The `vpx` version of a script file's tsx shebang, or why it keeps tsx; undefined when
455
- * the file has no `#!/usr/bin/env tsx` shebang.
842
+ * The migrated runner or Node.js preload shebang, or its retention reason.
456
843
  */
457
- function rewriteTsxShebang(content, file) {
458
- const match = TSX_ENV_SHEBANG.exec(content);
844
+ function rewriteRunnerShebang(runner, content, file, packageDir) {
845
+ const match = runner.envShebang.exec(content);
459
846
  if (!match) return;
460
847
  const [line, prefix, command] = match;
461
- const result = rewriteTsxCommand(`${command.trimEnd()} ${SHEBANG_ENTRY}`);
462
- const reason = result.kept[0] ?? scriptKeepReason(path.dirname(file), `./${path.basename(file)}`);
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)}`);
463
867
  if (reason) return { reason };
464
- return { content: `${prefix}${result.command.slice(0, -` ${SHEBANG_ENTRY}`.length)}${content.slice(line.length)}` };
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)}` };
465
871
  }
466
- /** Point `#!/usr/bin/env tsx` shebangs of a package's script files at `vpx`. */
467
- function rewriteTsxShebangs(dir, report) {
872
+ /** Rewrite matching runner and Node.js preload shebangs in a package's scripts. */
873
+ function rewriteRunnerShebangs(runner, dir, report) {
468
874
  let changed = false;
469
875
  sourceTreeMatches(dir, (content, file) => {
470
- const result = rewriteTsxShebang(content, file);
471
- if (result && "reason" in result) warnMigration(`Kept tsx in the shebang of ${displayRelative(file)}: ${result.reason}.`, report);
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);
472
878
  else if (result) {
473
879
  fs.writeFileSync(file, result.content);
474
880
  changed = true;
475
881
  }
476
882
  return false;
477
- });
883
+ }, { includeExtensionless: runner.includeExtensionlessShebangs });
478
884
  return changed;
479
885
  }
480
886
  /**
481
- * What still runs or imports tsx under `dir`, or undefined. The scan crosses into nested
482
- * packages, which can resolve this package's tsx, and reads build output, which can
483
- * still load it.
887
+ * Find a remaining use, including nested packages and build output that can resolve
888
+ * the package's runner dependencies.
484
889
  */
485
- function findTsxUse(dir) {
890
+ function findRunnerUse(runner, dir, retainUnverified = true) {
486
891
  let found;
487
892
  sourceTreeMatches(dir, (content, file) => {
488
893
  if (LOCKFILES.has(path.basename(file))) return false;
489
- const uses = fileUsesTsx(content, file);
490
- if (uses) found = `${displayRelative(file)} still uses tsx`;
491
- return uses;
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;
492
897
  }, {
493
898
  crossPackageBoundaries: true,
494
899
  extensions: REFERENCE_EXTENSIONS,
900
+ fileNamePattern: runner.referenceFileNamePattern,
495
901
  includeExtensionless: true,
496
902
  includePackageReferences: true,
497
903
  matchesPackage: (packageDir, pkg) => {
498
- const field = Object.entries(pkg).find(([key, value]) => !IGNORED_FIELDS.has(key) && jsonUsesTsx(value, [
904
+ const field = Object.entries(pkg).find(([key, value]) => !IGNORED_FIELDS.has(key) && jsonUsesRunner(runner, value, [
499
905
  "imports",
500
906
  "lint-staged",
501
907
  "scripts"
502
908
  ].includes(key)))?.[0];
503
- if (field !== void 0) found = `\`${field}\` in ${displayRelative(path.join(packageDir, "package.json"))} still uses tsx`;
504
- return field !== void 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;
505
915
  },
506
916
  skipDirs: USE_SCAN_SKIP_DIRS
507
917
  });
508
918
  return found;
509
919
  }
510
- /** Whether `file` runs, imports, or loads tsx. */
511
- function fileUsesTsx(content, file) {
512
- if (TSX_SHEBANG.test(content)) return true;
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;
513
956
  const name = path.basename(file);
514
- if (LINT_STAGED_JSON_CONFIG_FILES.some((config) => config === name) && jsonUsesTsx(parse(content), true)) return true;
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;
515
959
  const extension = path.extname(file);
516
- if (extension === ".json" || extension === ".jsonc") return jsonUsesTsx(parse(content), false);
517
- if (CODE_EXTENSIONS.has(extension)) return TSX_CODE_REFERENCE.test(content);
518
- return TSX_TEXT_REFERENCE.test(content) || extension === "" && TSX_CODE_REFERENCE.test(content);
519
- }
520
- /**
521
- * Whether a JSON value holds a string that runs or loads tsx: a tsx command with an
522
- * argument, a loader flag such as `--import=tsx` or `["--import", "tsx"]`, or a subpath
523
- * such as `tsx/cjs`. A bare `tsx` counts only in `commands` (scripts, lint-staged tasks,
524
- * and `imports` aliases) and as a loader setting such as Mocha's `"import": "tsx"`;
525
- * elsewhere it is often a file extension, as in `moduleFileExtensions`.
526
- */
527
- function jsonUsesTsx(value, commands) {
528
- if (typeof value === "string") return commands && TSX_COMMAND.test(value) || TSX_CONFIG_VALUE.test(value);
529
- if (Array.isArray(value)) return value.some((item, index) => jsonUsesTsx(item, commands) || typeof item === "string" && LOADER_OPTIONS.has(item) && TSX_LOADER.test(String(value[index + 1])));
530
- return isPlainRecord(value) && Object.entries(value).some(([key, item]) => jsonUsesTsx(item, commands) || LOADER_SETTINGS.has(key) && [item].flat().some((loader) => TSX_LOADER.test(String(loader))));
531
- }
532
- /**
533
- * Move tsx scripts to `vpx`: rewrite package.json scripts, lint-staged tasks, and
534
- * `#!/usr/bin/env tsx` shebangs, then remove the tsx devDependency from each package
535
- * where nothing uses tsx anymore.
536
- */
537
- function migrateTsxToVpx(rootDir, packages, report) {
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) {
538
972
  let changed = false;
539
973
  const migrated = [];
540
974
  for (const dir of packageDirs(rootDir, packages)) {
541
975
  const packageJsonPath = path.join(dir, "package.json");
542
976
  const pkg = readPackageJsonIfExists(packageJsonPath);
543
977
  if (!pkg) continue;
544
- if (ownsTsxAtRuntime(pkg)) {
545
- if (packageCommands(pkg).some((c) => TSX_COMMAND.test(c))) warnMigration(`Kept tsx in ${displayRelative(packageJsonPath)}: it is listed in dependencies, optionalDependencies, or peerDependencies there, and \`vpx\` comes from the vite-plus devDependency.`, report);
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);
546
980
  continue;
547
981
  }
548
982
  migrated.push(dir);
@@ -550,51 +984,538 @@ function migrateTsxToVpx(rootDir, packages, report) {
550
984
  let edited = false;
551
985
  for (const field of ["scripts", "lint-staged"]) {
552
986
  const record = json[field];
553
- if (isPlainRecord(record) && rewriteCommandRecord(record, `${displayRelative(packageJsonPath)} ${field}`, dir, report)) edited = true;
987
+ if (isPlainRecord(record) && rewriteCommandRecord(runner, record, `${displayRelative(packageJsonPath)} ${field}`, dir, report)) edited = true;
554
988
  }
555
989
  changed ||= edited;
556
990
  return edited ? json : void 0;
557
991
  });
558
992
  for (const file of lintStagedJsonFiles(dir)) editJsonFile(file, (config) => {
559
- const edited = rewriteCommandRecord(config, displayRelative(file), dir, report);
993
+ const edited = rewriteCommandRecord(runner, config, displayRelative(file), dir, report);
560
994
  changed ||= edited;
561
995
  return edited ? config : void 0;
562
996
  });
563
- if (rewriteTsxShebangs(dir, report)) changed = true;
997
+ if (rewriteRunnerShebangs(runner, dir, report)) changed = true;
564
998
  }
565
999
  let dependencyRemoved = false;
566
1000
  for (const dir of migrated) {
567
1001
  const packageJsonPath = path.join(dir, "package.json");
568
- if (readPackageJsonIfExists(packageJsonPath)?.devDependencies?.tsx === void 0) continue;
569
- const use = findTsxUse(dir);
1002
+ const devDependencies = readPackageJsonIfExists(packageJsonPath)?.devDependencies;
1003
+ if (!runner.dependencies.some((dependency) => devDependencies?.[dependency] !== void 0)) continue;
1004
+ const use = findRunnerUse(runner, dir);
570
1005
  if (use) {
571
- warnMigration(`Kept the tsx devDependency in ${displayRelative(packageJsonPath)}: ${use}.`, report);
1006
+ warnMigration(`Kept the ${runner.dependencies.join(" / ")} devDependency in ${displayRelative(packageJsonPath)}: ${use}.`, report);
572
1007
  continue;
573
1008
  }
574
1009
  editJsonFile(packageJsonPath, (json) => {
575
- delete json.devDependencies.tsx;
1010
+ for (const dependency of runner.dependencies) delete json.devDependencies[dependency];
576
1011
  return json;
577
1012
  });
578
1013
  dependencyRemoved = true;
579
1014
  }
580
- if (report && changed) report.tsxMigrated = true;
581
1015
  return {
582
1016
  changed: changed || dependencyRemoved,
583
- dependencyRemoved
1017
+ dependencyRemoved,
1018
+ scriptsChanged: changed
584
1019
  };
585
1020
  }
586
- async function confirmTsxMigration(interactive) {
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;
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) {
587
1261
  if (interactive) {
588
1262
  const confirmed = await confirm({
589
- message: "Migrate tsx scripts to vpx?\n " + styleText("gray", "vpx is Vite+'s built-in TypeScript script runner: `tsx <file>` becomes `vpx <file>`, `tsx watch` becomes `vpx --watch`, and the tsx dependency is removed once nothing uses it."),
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."),
590
1264
  initialValue: true
591
1265
  });
592
1266
  if (isCancel(confirmed)) cancelAndExit();
593
1267
  return confirmed;
594
1268
  }
595
- log$1.info("tsx scripts detected. Auto-migrating to vpx...");
1269
+ log.info("ts-node scripts detected. Auto-migrating supported commands to Vite+...");
596
1270
  return true;
597
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
+ }
598
1519
  // src/migration/vitest-v5/commands.ts
599
1520
  /** Read test commands without evaluating shell state or directory changes. */
600
1521
  function literalTestCommands(command) {
@@ -1547,6 +2468,44 @@ var import_dist = require_dist();
1547
2468
  function record$1(value) {
1548
2469
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1549
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
+ }
1550
2509
  function declaredVitest(pkg) {
1551
2510
  const manifest = record$1(pkg);
1552
2511
  for (const field of [
@@ -1562,7 +2521,7 @@ function declaredVitest(pkg) {
1562
2521
  /** Read a direct upstream Vitest edge without installing or changing a lockfile.
1563
2522
  * Unsupported protocols, ambiguous layouts and stale entries remain unknown.
1564
2523
  * In particular, never infer a runner from an unrelated transitive package. */
1565
- function lockedVitestVersion(root, directory, manager, spec) {
2524
+ function lockedVitestVersion(root, directory, manager, spec, readLockfile) {
1566
2525
  const range = spec.replace(/^npm:vitest@/, "");
1567
2526
  const validRange = normalizeRange(range);
1568
2527
  if (!validRange && !/^[\w.-]+$/.test(range)) return;
@@ -1571,10 +2530,9 @@ function lockedVitestVersion(root, directory, manager, spec) {
1571
2530
  return !validRange || satisfies(version, range, { includePrerelease: true }) ? version : void 0;
1572
2531
  }
1573
2532
  const relative = path.relative(root, directory).replaceAll("\\", "/");
1574
- const read = (name) => fs.readFileSync(path.join(root, name), "utf8").replace(/^\uFEFF/, "");
1575
2533
  try {
1576
2534
  if (manager === PackageManager.npm) {
1577
- 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);
1578
2536
  if (!packages) return;
1579
2537
  if (declaredVitest(packages[relative]) !== spec) return;
1580
2538
  let owner = relative;
@@ -1587,34 +2545,29 @@ function lockedVitestVersion(root, directory, manager, spec) {
1587
2545
  }
1588
2546
  }
1589
2547
  if (manager === PackageManager.yarn) {
1590
- const text = read("yarn.lock");
1591
- if (/^__metadata:/m.test(text)) {
1592
- const document = (0, import_dist.parseDocument)(text);
1593
- if (document.errors.length) return;
2548
+ const lock = readLockfile(path.join(root, "yarn.lock"), parseYarnLockfile);
2549
+ if (!lock) return;
2550
+ if (lock.modern) {
1594
2551
  const descriptors = /* @__PURE__ */ new Set([`vitest@${spec}`, `vitest@npm:${range}`]);
1595
- 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)));
1596
2553
  if (entries.length !== 1) return;
1597
2554
  const entry = record$1(entries[0][1]);
1598
2555
  return typeof entry?.resolution === "string" && entry.resolution.startsWith("vitest@npm:") ? valid(entry.resolution.slice(11)) : void 0;
1599
2556
  }
1600
2557
  const matches = [];
1601
- for (const block of text.split(/\r?\n(?=[^\s#])/)) {
1602
- const [header] = block.split(/\r?\n/);
1603
- if (!header.endsWith(":")) continue;
1604
- const document = (0, import_dist.parseDocument)(`[${header.slice(0, -1)}]`);
1605
- const keys = document.errors.length ? void 0 : document.toJS();
1606
- if (Array.isArray(keys) && keys.includes(`vitest@${spec}`)) {
1607
- const version = /^ version "([^"]+)"\r?$/m.exec(block)?.[1];
1608
- if (!version) return;
1609
- matches.push(version);
1610
- }
2558
+ for (const { keys, version } of lock.entries) if (keys.includes(`vitest@${spec}`)) {
2559
+ if (!version) return;
2560
+ matches.push(version);
1611
2561
  }
1612
2562
  return matches.length === 1 ? valid(matches[0]) : void 0;
1613
2563
  }
1614
2564
  if (manager === PackageManager.bun) {
1615
- const errors = [];
1616
- const lock = record$1(parse(read("bun.lock"), errors, { allowTrailingComma: true }));
1617
- 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;
1618
2571
  const packages = record$1(lock?.packages);
1619
2572
  const direct = packages?.vitest;
1620
2573
  if (!Array.isArray(direct) || typeof direct[0] !== "string" || !direct[0].startsWith("vitest@")) return;
@@ -2916,7 +3869,7 @@ function migrateWebdriverioDependencies(workspace, sources, inputs) {
2916
3869
  const set = (file, keys, value) => {
2917
3870
  if (get(file, keys) === value) return;
2918
3871
  const source = sources.get(file);
2919
- 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: {
2920
3873
  insertSpaces: true,
2921
3874
  tabSize: 2
2922
3875
  } })));
@@ -3182,17 +4135,16 @@ function finding(file, code, message, severity = "review", source = "", offset =
3182
4135
  };
3183
4136
  }
3184
4137
  /** Follow this project's dependency edges, never an unrelated transitive runner. */
3185
- function lockedSourceVersion(directory, root, pkg, manager, expectedRunnerPackage) {
4138
+ function lockedSourceVersion(directory, root, pkg, readLockfile, manager, expectedRunnerPackage) {
3186
4139
  if (manager !== void 0 && manager !== PackageManager.pnpm) {
3187
4140
  const spec = dependency(pkg, "vitest");
3188
- return spec && !expectedRunnerPackage ? lockedVitestVersion(root, directory, manager, spec) : void 0;
4141
+ return spec && !expectedRunnerPackage ? lockedVitestVersion(root, directory, manager, spec, readLockfile) : void 0;
3189
4142
  }
3190
- const lockfile = path.join(root, "pnpm-lock.yaml");
3191
- if (!fs.existsSync(lockfile)) return;
3192
4143
  try {
3193
- const document = (0, import_dist.parseDocument)(fs.readFileSync(lockfile, "utf8"));
3194
- if (document.errors.length) return;
3195
- 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
+ });
3196
4148
  const importer = record(record(lock?.importers)?.[path.relative(root, directory).replaceAll("\\", "/") || "."]);
3197
4149
  if (!importer) return;
3198
4150
  const name = dependency(pkg, "vitest") ? "vitest" : "vite-plus";
@@ -3249,7 +4201,7 @@ function installedSourceVersion(directory, name, expectedPackage, expectedRange)
3249
4201
  }
3250
4202
  return dependency(pkg, LEGACY_RUNNER) ? installedSourceVersion(installed.path, LEGACY_RUNNER) : void 0;
3251
4203
  }
3252
- function sourceVersion(directory, root, pkg, manager) {
4204
+ function sourceVersion(directory, root, pkg, readLockfile, manager) {
3253
4205
  const catalog = createCatalogDependencyResolver(root, manager ?? PackageManager.pnpm);
3254
4206
  const vitePlus = dependency(pkg, "vite-plus");
3255
4207
  const vitePlusRange = vitePlus?.startsWith("catalog:") ? catalog?.(vitePlus, "vite-plus") : vitePlus;
@@ -3266,7 +4218,7 @@ function sourceVersion(directory, root, pkg, manager) {
3266
4218
  if (bundled) return bundled;
3267
4219
  }
3268
4220
  const expectedRunnerPackage = legacyAlias ? LEGACY_RUNNER : void 0;
3269
- const locked = lockedSourceVersion(directory, root, pkg, manager, expectedRunnerPackage);
4221
+ const locked = lockedSourceVersion(directory, root, pkg, readLockfile, manager, expectedRunnerPackage);
3270
4222
  if (locked && (!spec || legacyAlias || !normalizeRange(spec) || satisfies(locked, spec, { includePrerelease: true }))) return locked;
3271
4223
  const installed = installedSourceVersion(directory, "vitest", expectedRunnerPackage);
3272
4224
  if (installed && (!spec || legacyAlias || satisfies(installed, spec, { includePrerelease: true }))) return installed;
@@ -3276,7 +4228,7 @@ function sourceVersion(directory, root, pkg, manager) {
3276
4228
  const bundled = installedSourceVersion(directory, "vite-plus", "vite-plus", vitePlusRange);
3277
4229
  if (bundled) return bundled;
3278
4230
  }
3279
- 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);
3280
4232
  }
3281
4233
  function checkNodeRange(file, value, label, findings, publicContract = false) {
3282
4234
  if (!publicContract && [
@@ -3338,7 +4290,7 @@ function migrateNode(file, source, findings) {
3338
4290
  "version"
3339
4291
  ]);
3340
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"]);
3341
- return applyEdits(source, edits);
4293
+ return applyEdits$1(source, edits);
3342
4294
  }
3343
4295
  return source;
3344
4296
  }
@@ -3391,7 +4343,7 @@ function scanAndRewriteFile(file, source, project, testMode, mergedConfig) {
3391
4343
  content: JSON.stringify(result.content)
3392
4344
  });
3393
4345
  }
3394
- content = applyEdits(source, edits);
4346
+ content = applyEdits$1(source, edits);
3395
4347
  } else if (/^tsconfig(?:\..+)?\.json$/.test(path.basename(file))) {
3396
4348
  const types = record(record(parse(source))?.compilerOptions)?.types;
3397
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."));
@@ -3422,6 +4374,7 @@ function scanAndRewriteFile(file, source, project, testMode, mergedConfig) {
3422
4374
  /** Read-only preflight. Call before package-manager conversion, installs,
3423
4375
  * catalog updates, or generic import rewrites can erase the source version. */
3424
4376
  function planVitestV5Migration(workspace, originalProjects) {
4377
+ const readLockfile = createLockfileReader();
3425
4378
  const findings = [];
3426
4379
  const directories = [workspace.rootDir, ...(workspace.packages ?? []).map((pkg) => path.resolve(workspace.rootDir, pkg.path))];
3427
4380
  const projects = [];
@@ -3440,7 +4393,7 @@ function planVitestV5Migration(workspace, originalProjects) {
3440
4393
  const pkg = readJson(path.join(directory, "package.json"));
3441
4394
  const configFiles = new Set([...allConfigs].filter((file) => sources.has(file)));
3442
4395
  const original = originalProjects?.get(directory);
3443
- 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);
3444
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)));
3445
4398
  const options = {
3446
4399
  preserveV4: original?.options.preserveV4 ?? (!!version && getMajor(version) < 5),
@@ -3897,13 +4850,13 @@ function prepareNpmViteAliasReinstall(rootDir, projectPaths = [rootDir]) {
3897
4850
  async function collectGitHooksDecision(rootDir, packageManager, options, packages = []) {
3898
4851
  const hasExistingHooks = options.hooks === void 0 && hasExistingViteHooksPolicy(rootDir);
3899
4852
  if (hasExistingHooks && isViteHooksSetupComplete(rootDir)) {
3900
- log(`${muted("•")} Git hooks already configured, skipping`);
4853
+ log$1(`${muted("•")} Git hooks already configured, skipping`);
3901
4854
  return false;
3902
4855
  }
3903
4856
  if (!hasExistingHooks && !await promptGitHooks(options)) return false;
3904
4857
  const reason = preflightGitHooksSetup(rootDir, packageManager, packages);
3905
4858
  if (reason) {
3906
- log$1.warn(`⚠ ${reason}`);
4859
+ log.warn(`⚠ ${reason}`);
3907
4860
  return false;
3908
4861
  }
3909
4862
  return true;
@@ -4042,7 +4995,7 @@ async function confirmYarnNodeModulesMode(rootDir, packageManager, packageManage
4042
4995
  if (packageManager !== PackageManager.yarn) return false;
4043
4996
  const pnp = detectYarnPnpMode(rootDir, packageManagerVersion);
4044
4997
  if (!pnp) return false;
4045
- 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).`);
4046
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);
4047
5000
  if (interactive) {
4048
5001
  const confirmed = await confirm({
@@ -4146,7 +5099,7 @@ function hasExistingVitePlusMigrationCandidates(workspaceInfo, options) {
4146
5099
  const eslintProject = detectEslintProject(workspaceInfo.rootDir, workspaceInfo.packages);
4147
5100
  const prettierProject = detectPrettierProject(workspaceInfo.rootDir, workspaceInfo.packages);
4148
5101
  const tsupProject = detectTsupProject(workspaceInfo.rootDir, workspaceInfo.packages);
4149
- return hasExplicitExistingVitePlusSetupRequest(options) || detectLegacyGitHooksMigrationCandidate(workspaceInfo.rootDir) || hasBaseUrlInWorkspace(workspaceInfo) || eslintProject.hasDependency || prettierProject.hasDependency || tsupProject.hasDependency || detectTsxProject(workspaceInfo.rootDir, workspaceInfo.packages) || 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;
4150
5103
  }
4151
5104
  function getExistingVitePlusSetupOptions(options, useFullMigrationDefaults = false) {
4152
5105
  if (useFullMigrationDefaults) return options;
@@ -4167,6 +5120,8 @@ async function collectMigrationPlan(rootDir, detectedPackageManager, detectedPac
4167
5120
  else if (prettierProject.hasDependency) warnPackageLevelPrettier();
4168
5121
  const { migrateTsup, tsupConfigFile } = await collectTsupMigrationDecision(rootDir, options, packages);
4169
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);
4170
5125
  const fixBaseUrl = hasBaseUrlInWorkspace({
4171
5126
  rootDir,
4172
5127
  packages
@@ -4185,6 +5140,8 @@ async function collectMigrationPlan(rootDir, detectedPackageManager, detectedPac
4185
5140
  migrateTsup,
4186
5141
  tsupConfigFile,
4187
5142
  migrateTsx,
5143
+ migrateOxnode,
5144
+ migrateTsNode,
4188
5145
  fixBaseUrl,
4189
5146
  migrateNodeVersionFile,
4190
5147
  nodeVersionDetection,
@@ -4196,52 +5153,56 @@ function showMigrationSummary(options) {
4196
5153
  const { projectRoot, packageManager, packageManagerVersion, installDurationMs, finalInstallOk, report, updatedExistingVitePlus, suggestFullMigration } = options;
4197
5154
  const projectLabel = displayRelative(projectRoot) || ".";
4198
5155
  const configUpdates = report.createdViteConfigCount + report.mergedConfigCount + report.mergedStagedConfigCount + report.inlinedLintStagedConfigCount + report.removedConfigCount + report.tsdownImportCount + report.wrappedPluginConfigCount + report.migratedTaskCacheConfigCount;
4199
- log(`${styleText("magenta", "◇")} ${updatedExistingVitePlus ? "Updated" : "Migrated"} ${accent(projectLabel)} to Vite+ ${VITE_PLUS_VERSION}`);
4200
- 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}`);
4201
5158
  if (report.dependencyUpgrades.length > 0) {
4202
5159
  const nameWidth = Math.max(...report.dependencyUpgrades.map((change) => change.name.length));
4203
5160
  const fromWidth = Math.max(...report.dependencyUpgrades.map((change) => (change.from ?? "").length));
4204
- log(`${styleText("gray", "•")} Dependencies:`);
5161
+ log$1(`${styleText("gray", "•")} Dependencies:`);
4205
5162
  for (const change of report.dependencyUpgrades) {
4206
5163
  const name = change.name.padEnd(nameWidth);
4207
5164
  const from = muted((change.from ?? "").padEnd(fromWidth));
4208
5165
  const to = styleText("green", change.to);
4209
- log(` ${name} ${from} → ${to}`);
5166
+ log$1(` ${name} ${from} → ${to}`);
4210
5167
  }
4211
5168
  }
4212
- 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)}`);
4213
5170
  if (configUpdates > 0 || report.rewrittenImportFileCount > 0) {
4214
5171
  const parts = [];
4215
5172
  if (configUpdates > 0) parts.push(`${configUpdates} ${configUpdates === 1 ? "config update" : "config updates"} applied`);
4216
5173
  if (report.rewrittenImportFileCount > 0) parts.push(`${report.rewrittenImportFileCount} ${report.rewrittenImportFileCount === 1 ? "file had" : "files had"} imports rewritten`);
4217
- log(`${styleText("gray", "•")} ${parts.join(", ")}`);
4218
- }
4219
- if (report.preservedUpstreamVitestImportFileCount > 0) log(`${styleText("gray", "•")} Kept upstream \`vitest\` imports in ${report.preservedUpstreamVitestImportFileCount} ${report.preservedUpstreamVitestImportFileCount === 1 ? "file" : "files"} for @nuxt/test-utils compatibility`);
4220
- if (report.eslintMigrated) log(`${styleText("gray", "•")} ESLint rules migrated to Oxlint`);
4221
- if (report.prettierMigrated) log(`${styleText("gray", "•")} Prettier migrated to Oxfmt`);
4222
- if (report.tsupMigrated) log(`${styleText("gray", "•")} tsup config migrated to tsdown (\`vp pack\`)`);
4223
- if (report.tsxMigrated) log(`${styleText("gray", "•")} tsx scripts migrated to \`vpx\``);
4224
- if (report.nodeVersionFileMigrated) log(`${styleText("gray", "•")} Node version manager file migrated to .node-version`);
5174
+ log$1(`${styleText("gray", "•")} ${parts.join(", ")}`);
5175
+ }
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`);
4225
5184
  const setupVpFileCount = report.setupVpVersionUpdatedFileCount;
4226
5185
  if (setupVpFileCount > 0) {
4227
5186
  const fileLabel = setupVpFileCount === 1 ? "file" : "files";
4228
- log(`${styleText("gray", "•")} setup-vp updated to ${SETUP_VP_VERSION} in ${setupVpFileCount} GitHub Actions ${fileLabel}`);
4229
- }
4230
- if (report.wrappedPluginConfigCount > 0) log(`${styleText("gray", "•")} Inline Vite plugins wrapped with lazyPlugins for check/lint/fmt`);
4231
- if (report.migratedTaskCacheConfigCount > 0) log(`${styleText("gray", "•")} Task cache settings moved under \`cache\``);
4232
- if (report.gitHooksConfigured) log(`${styleText("gray", "•")} Git hooks configured`);
4233
- if (report.frameworkShimAdded) log(`${styleText("gray", "•")} TypeScript shim added for framework component files`);
4234
- if (report.packageManagerBootstrapConfigured) log(`${styleText("gray", "•")} Package manager settings configured`);
4235
- if (suggestFullMigration) log(FULL_MIGRATION_HINT);
4236
- if (report.warnings.length > 0) {
4237
- log(`${styleText("yellow", "!")} Warnings:`);
4238
- for (const warning of report.warnings) log(` - ${warning}`);
4239
- }
5187
+ log$1(`${styleText("gray", "•")} setup-vp updated to ${SETUP_VP_VERSION} in ${setupVpFileCount} GitHub Actions ${fileLabel}`);
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);
4240
5196
  if (report.manualSteps.length > 0) {
4241
- log(`${styleText("blue", "→")} Manual follow-up:`);
4242
- 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}`);
4243
5199
  }
4244
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
+ }
4245
5206
  async function downloadSupportedPackageManager(options) {
4246
5207
  const { rootDir, packageManager, packageManagerVersion, interactive, updateMigrationProgress, failMigrationProgress } = options;
4247
5208
  updateMigrationProgress("Preparing migration");
@@ -4251,11 +5212,11 @@ async function downloadSupportedPackageManager(options) {
4251
5212
  await upgradeYarn(rootDir, interactive, true);
4252
5213
  } else if (packageManager === PackageManager.pnpm && satisfies(downloadResult.version, "< 9.5.0")) {
4253
5214
  failMigrationProgress("Migration failed");
4254
- 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`);
4255
5216
  cancelAndExit("Vite+ cannot automatically migrate this project yet.", 1);
4256
5217
  } else if (packageManager === PackageManager.npm && satisfies(downloadResult.version, "< 8.3.0")) {
4257
5218
  failMigrationProgress("Migration failed");
4258
- 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`);
4259
5220
  cancelAndExit("Vite+ cannot automatically migrate this project yet.", 1);
4260
5221
  }
4261
5222
  return downloadResult;
@@ -4272,7 +5233,7 @@ function completeVitestV5Migration(plan, report) {
4272
5233
  function applyRefreshedVitestV5Migration(plan) {
4273
5234
  const refreshed = refreshVitestV5Migration(plan);
4274
5235
  if (refreshed.findings.some((finding) => finding.severity === "block")) {
4275
- log$1.warn(formatVitestV5Findings(refreshed));
5236
+ log.warn(formatVitestV5Findings(refreshed));
4276
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);
4277
5238
  }
4278
5239
  applyVitestV5Migration(refreshed);
@@ -4280,6 +5241,7 @@ function applyRefreshedVitestV5Migration(plan) {
4280
5241
  }
4281
5242
  async function executeMigrationPlan(workspaceInfoOptional, plan, interactive, vitestV5Plan, preExistingChangedPaths) {
4282
5243
  const report = createMigrationReport();
5244
+ report.warnings.push(...detectMigrationWarnings(workspaceInfoOptional.rootDir, workspaceInfoOptional.packages));
4283
5245
  const migrationProgress = interactive ? spinner({ indicator: "timer" }) : void 0;
4284
5246
  let migrationProgressStarted = false;
4285
5247
  const updateMigrationProgress = (message) => {
@@ -4320,7 +5282,7 @@ async function executeMigrationPlan(workspaceInfoOptional, plan, interactive, vi
4320
5282
  if (plan.convertYarnPnp) {
4321
5283
  updateMigrationProgress("Configuring Yarn node-modules mode");
4322
5284
  report.packageManagerBootstrapConfigured = configureYarnNodeModulesMode(workspaceInfo.rootDir);
4323
- if (report.packageManagerBootstrapConfigured) log$1.success("✔ Switched Yarn to node-modules mode");
5285
+ if (report.packageManagerBootstrapConfigured) log.success("✔ Switched Yarn to node-modules mode");
4324
5286
  }
4325
5287
  if (plan.migrateNodeVersionFile && plan.nodeVersionDetection) {
4326
5288
  updateMigrationProgress("Migrating node version file");
@@ -4378,6 +5340,14 @@ async function executeMigrationPlan(workspaceInfoOptional, plan, interactive, vi
4378
5340
  updateMigrationProgress("Migrating tsx scripts");
4379
5341
  migrateTsxToVpx(workspaceInfo.rootDir, workspaceInfo.packages, report);
4380
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
+ }
4381
5351
  vitestV5Plan = applyRefreshedVitestV5Migration(vitestV5Plan);
4382
5352
  const skipStagedMigration = shouldSkipStagedMigrationForHooks(workspaceInfo.rootDir, plan.shouldSetupHooks, plan.packageManager, workspaceInfo.packages);
4383
5353
  updateMigrationProgress("Rewriting configs");
@@ -4443,11 +5413,11 @@ async function main() {
4443
5413
  let vitestV5Plan = planVitestV5Migration(workspaceInfoOptional);
4444
5414
  const vitestV5Preflight = formatVitestV5Findings(vitestV5Plan);
4445
5415
  if (vitestV5Plan.findings.some((finding) => finding.severity === "block")) {
4446
- log$1.warn(vitestV5Preflight);
5416
+ log.warn(vitestV5Preflight);
4447
5417
  cancelAndExit("Resolve the blocking Vitest v5 findings, then re-run `vp migrate`. No project files were changed.", 1);
4448
5418
  }
4449
5419
  const rootPkg = readNearestPackageJson(workspaceInfoOptional.rootDir);
4450
- 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.`);
4451
5421
  if (hasVitePlusDependency(rootPkg) && !isForceOverrideMode()) {
4452
5422
  let convertYarnPnp = await confirmYarnNodeModulesMode(workspaceInfoOptional.rootDir, workspaceInfoOptional.packageManager, workspaceInfoOptional.packageManagerVersion, options.interactive);
4453
5423
  let didMigrate = vitestV5NeedsMigration(vitestV5Plan);
@@ -4456,6 +5426,7 @@ async function main() {
4456
5426
  let finalInstallOk = true;
4457
5427
  let canFormatMigratedProject = !process.env.VP_SKIP_INSTALL;
4458
5428
  const report = createMigrationReport();
5429
+ const migrationWarnings = options.full ? detectMigrationWarnings(workspaceInfoOptional.rootDir, workspaceInfoOptional.packages) : [];
4459
5430
  const migrationProgress = options.interactive ? spinner({ indicator: "timer" }) : void 0;
4460
5431
  let migrationProgressStarted = false;
4461
5432
  const updateMigrationProgress = (message) => {
@@ -4515,12 +5486,14 @@ async function main() {
4515
5486
  const fullSetup = options.full === true;
4516
5487
  const skippedSetupCandidates = !fullSetup && !hasExplicitExistingVitePlusSetupRequest(options) && hasExistingVitePlusMigrationCandidates(workspaceInfoOptional, options);
4517
5488
  if (!didMigrate && !convertYarnPnp && report.warnings.length === 0 && !vitePlusBootstrapPending && !(fullSetup ? hasExistingVitePlusMigrationCandidates(workspaceInfoOptional, options) : hasExplicitExistingVitePlusSetupRequest(options))) {
4518
- if (vitestV5Preflight) log$1.warn(vitestV5Preflight);
4519
- for (const warning of coreMigrationResult.taskCacheWarnings) log$1.warn(warning);
4520
- 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);
4521
5493
  outro(`This project is already using Vite+! ${accent("Happy coding!")}`);
4522
5494
  return;
4523
5495
  }
5496
+ report.warnings.push(...migrationWarnings);
4524
5497
  for (const warning of coreMigrationResult.taskCacheWarnings) addMigrationWarning(report, warning);
4525
5498
  const setupOptions = getExistingVitePlusSetupOptions(options, fullSetup);
4526
5499
  const plan = await collectMigrationSetupPlan(workspaceInfoOptional.rootDir, packageManager, setupOptions, workspaceInfoOptional.packages, fullSetup);
@@ -4610,6 +5583,24 @@ async function main() {
4610
5583
  needsInstall ||= tsxResult.dependencyRemoved;
4611
5584
  }
4612
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
+ }
4613
5604
  if (fullSetup) {
4614
5605
  const nodeVersionDetection = detectNodeVersionManagerFile(workspaceInfoOptional.rootDir);
4615
5606
  if (nodeVersionDetection) {
@@ -4620,7 +5611,7 @@ async function main() {
4620
5611
  if (convertYarnPnp) {
4621
5612
  updateMigrationProgress("Configuring Yarn node-modules mode");
4622
5613
  if (configureYarnNodeModulesMode(workspaceInfoOptional.rootDir)) {
4623
- log$1.success("✔ Switched Yarn to node-modules mode");
5614
+ log.success("✔ Switched Yarn to node-modules mode");
4624
5615
  report.packageManagerBootstrapConfigured = true;
4625
5616
  didMigrate = true;
4626
5617
  needsInstall = true;
@@ -4723,7 +5714,7 @@ async function main() {
4723
5714
  });
4724
5715
  } else {
4725
5716
  clearMigrationProgress();
4726
- if (skippedSetupCandidates) log(FULL_MIGRATION_HINT);
5717
+ if (skippedSetupCandidates) log$1(FULL_MIGRATION_HINT);
4727
5718
  outro(`This project is already using Vite+! ${accent("Happy coding!")}`);
4728
5719
  }
4729
5720
  return;
@@ -4740,7 +5731,7 @@ async function main() {
4740
5731
  });
4741
5732
  }
4742
5733
  main().catch((err) => {
4743
- log$1.error(err.message);
5734
+ log.error(err.message);
4744
5735
  console.error(err);
4745
5736
  process.exit(1);
4746
5737
  });