vite-plus 0.2.1 → 0.2.3

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 (69) hide show
  1. package/bin/oxfmt +6 -3
  2. package/bin/oxlint +6 -3
  3. package/bin/vpr +9 -0
  4. package/binding/index.cjs +53 -53
  5. package/binding/index.d.cts +86 -144
  6. package/dist/agent-D3nZAMGI.js +9456 -0
  7. package/dist/bin.js +10 -7
  8. package/dist/{cli-truncate-CWsmbK3p.js → cli-truncate-BVOlIIPy.js} +1 -1
  9. package/dist/config/bin.js +2 -2
  10. package/dist/{constants-CrfJQIUX.js → constants-CZ7Mnq-C.js} +7 -3
  11. package/dist/create/bin.js +800 -806
  12. package/dist/{define-config-2tfJoXr1.d.ts → define-config-BuMs_LKa.d.ts} +64 -9
  13. package/dist/{define-config-DJUehepE.js → define-config-D2jo1xVA.js} +43 -18
  14. package/dist/{define-config-BGSjF6Xp.cjs → define-config-Dut6XD42.cjs} +61 -22
  15. package/dist/define-config.cjs +2 -1
  16. package/dist/define-config.d.ts +2 -2
  17. package/dist/define-config.js +2 -2
  18. package/dist/{dist-Oxo16Y0q.js → dist-CKz3vcoG.js} +4 -4
  19. package/dist/dist-CtM2JRbM.js +3 -0
  20. package/dist/editor-D1BZiU1h.js +584 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/{json-Dn87fvjk.js → json-DiRs8ceZ.js} +13 -1
  25. package/dist/{log-update-lyIiuflf.js → log-update-DHZRyJ2m.js} +2 -2
  26. package/dist/migration/bin.d.ts +1 -10
  27. package/dist/migration/bin.js +531 -170
  28. package/dist/migration/compat/worker.d.ts +1 -0
  29. package/dist/migration/compat/worker.js +35 -0
  30. package/dist/{oxlint-plugin-config-q8a5PFch.js → oxlint-plugin-config-BSbv66Qn.js} +1 -1
  31. package/dist/oxlint-plugin.js +78 -9
  32. package/dist/pack-bin.js +1 -1
  33. package/dist/{package-BHirM1_v.js → package-Cpo2ei_V.js} +52 -3
  34. package/dist/protocol-D99W10Qi.js +4 -0
  35. package/dist/{report-BHSkWqRR.js → report-BS7ULx7A.js} +2 -0
  36. package/dist/{resolve-vite-config-CmdsfQzS.js → resolve-vite-config-DX2o0SWM.js} +14 -10
  37. package/dist/{rolldown-runtime-DnnnRqeS.js → rolldown-runtime-C7HZzL1F.js} +1 -1
  38. package/dist/staged/bin.js +23 -19
  39. package/dist/{tsconfig-BWQPmGKz.js → tsconfig-D6sTl0dN.js} +439 -208
  40. package/dist/version.js +2 -5
  41. package/dist/versions.js +5 -5
  42. package/dist/{wrap-ansi-k7Dn4VtV.js → wrap-ansi-DNjkuBEp.js} +1 -1
  43. package/dist/wrap-ansi-DUi52C6W.js +2 -0
  44. package/docs/_data/team.ts +9 -0
  45. package/docs/config/check.md +35 -0
  46. package/docs/config/index.md +2 -0
  47. package/docs/config/run.md +61 -12
  48. package/docs/guide/automatic-data-tracking.md +145 -0
  49. package/docs/guide/cache.md +14 -35
  50. package/docs/guide/check.md +16 -0
  51. package/docs/guide/ci.md +4 -0
  52. package/docs/guide/docker.md +175 -0
  53. package/docs/guide/env.md +13 -1
  54. package/docs/guide/github-actions-cache.md +165 -0
  55. package/docs/guide/install.md +12 -2
  56. package/docs/guide/migrate-rules.md +347 -0
  57. package/docs/guide/migrate.md +14 -0
  58. package/docs/guide/pack.md +8 -0
  59. package/docs/guide/run.md +33 -3
  60. package/docs/guide/troubleshooting.md +3 -3
  61. package/docs/guide/upgrade.md +43 -21
  62. package/docs/package.json +3 -2
  63. package/docs/vite.config.ts +17 -0
  64. package/package.json +19 -18
  65. package/dist/agent--cKmgD_n.js +0 -3341
  66. package/dist/compat-Cql3K40m.js +0 -14
  67. package/dist/dist-DRJUd9bL.js +0 -3
  68. package/dist/workspace-D0AVy4fu.js +0 -5830
  69. package/dist/wrap-ansi-CeQuiQ31.js +0 -2
@@ -1,3341 +0,0 @@
1
- import { r as __toESM } from "./rolldown-runtime-DnnnRqeS.js";
2
- import { a as VITEST_VERSION, c as VITE_PLUS_VERSION, i as VITEST_AGE_GATE_EXEMPT_PACKAGES, n as BASEURL_TSCONFIG_WARNING, o as VITE_PLUS_NAME, s as VITE_PLUS_OVERRIDE_PACKAGES, u as isForceOverrideMode } from "./constants-CrfJQIUX.js";
3
- import { i as ensureVitePlusImportRuleDefaults, r as createDefaultVitePlusLintConfig } from "./oxlint-plugin-config-q8a5PFch.js";
4
- import { A as select, C as require_semver, D as log, I as runCommandSilently, L as require_cross_spawn, O as multiselect, P as R, S as PackageManager, T as confirm, a as hasTypesToRewriteInTsconfig, b as getSpinner, c as cancelAndExit, i as hasBaseUrlInTsconfig, n as findTsconfigFiles, o as removeDeprecatedTsconfigFalseOption, s as rewriteTypesInTsconfig } from "./tsconfig-BWQPmGKz.js";
5
- import { n as isJsonFile, r as readJsonFile, t as editJsonFile } from "./json-Dn87fvjk.js";
6
- import { t as require_dist } from "./dist-Oxo16Y0q.js";
7
- import { n as detectPackageMetadata } from "./package-BHirM1_v.js";
8
- import { n as addMigrationWarning, t as addManualStep } from "./report-BHSkWqRR.js";
9
- import path from "node:path";
10
- import { hasConfigKey, mergeJsonConfig, mergeTsdownConfig, rewriteEslint, rewriteImportsInDirectory, rewritePrettier, rewriteScripts, wrapLazyPlugins } from "../binding/index.js";
11
- import fs from "node:fs";
12
- import { styleText } from "node:util";
13
- import os from "node:os";
14
- import fsPromises from "node:fs/promises";
15
- //#region src/utils/path.ts
16
- var import_cross_spawn = /* @__PURE__ */ __toESM(require_cross_spawn(), 1);
17
- var import_semver = /* @__PURE__ */ __toESM(require_semver(), 1);
18
- var import_dist = require_dist();
19
- function findPkgRoot() {
20
- let dir = import.meta.dirname;
21
- while (dir !== path.dirname(dir)) {
22
- if (fs.existsSync(path.join(dir, "package.json"))) return dir;
23
- dir = path.dirname(dir);
24
- }
25
- return dir;
26
- }
27
- const pkgRoot = findPkgRoot();
28
- const templatesDir = path.join(pkgRoot, "templates");
29
- const rulesDir = path.join(pkgRoot, "rules");
30
- function displayRelative(to, from = process.cwd()) {
31
- return path.relative(from, to).replaceAll("\\", "/");
32
- }
33
- //#endregion
34
- //#region src/utils/yaml.ts
35
- function readYamlFile(file) {
36
- return (0, import_dist.parse)(fs.readFileSync(file, "utf-8"));
37
- }
38
- function editYamlFile(file, callback) {
39
- const doc = (0, import_dist.parseDocument)(fs.readFileSync(file, "utf-8"));
40
- callback(doc);
41
- fs.writeFileSync(file, doc.toString({ singleQuote: true }), "utf-8");
42
- }
43
- function scalarString(value) {
44
- return new import_dist.Scalar(value);
45
- }
46
- //#endregion
47
- //#region src/migration/detector.ts
48
- const PRETTIER_PACKAGE_JSON_CONFIG = "package.json#prettier";
49
- const PRETTIER_CONFIG_FILES = [
50
- ".prettierrc",
51
- ".prettierrc.json",
52
- ".prettierrc.jsonc",
53
- ".prettierrc.yaml",
54
- ".prettierrc.yml",
55
- ".prettierrc.toml",
56
- ".prettierrc.js",
57
- ".prettierrc.cjs",
58
- ".prettierrc.mjs",
59
- ".prettierrc.ts",
60
- ".prettierrc.cts",
61
- ".prettierrc.mts",
62
- "prettier.config.js",
63
- "prettier.config.cjs",
64
- "prettier.config.mjs",
65
- "prettier.config.ts",
66
- "prettier.config.cts",
67
- "prettier.config.mts"
68
- ];
69
- function detectConfigs(projectPath) {
70
- const configs = {};
71
- for (const config of [
72
- "vite.config.ts",
73
- "vite.config.mts",
74
- "vite.config.cts",
75
- "vite.config.js",
76
- "vite.config.mjs",
77
- "vite.config.cjs"
78
- ]) if (fs.existsSync(path.join(projectPath, config))) {
79
- configs.viteConfig = config;
80
- break;
81
- }
82
- for (const config of [
83
- "vitest.config.ts",
84
- "vitest.config.mts",
85
- "vitest.config.cts",
86
- "vitest.config.js",
87
- "vitest.config.mjs",
88
- "vitest.config.cjs"
89
- ]) if (fs.existsSync(path.join(projectPath, config))) {
90
- configs.vitestConfig = config;
91
- break;
92
- }
93
- for (const config of [
94
- "tsdown.config.ts",
95
- "tsdown.config.mts",
96
- "tsdown.config.cts",
97
- "tsdown.config.js",
98
- "tsdown.config.mjs",
99
- "tsdown.config.cjs",
100
- "tsdown.config.json",
101
- "tsdown.config"
102
- ]) if (fs.existsSync(path.join(projectPath, config))) {
103
- configs.tsdownConfig = config;
104
- break;
105
- }
106
- for (const config of [".oxlintrc.json", ".oxlintrc.jsonc"]) if (fs.existsSync(path.join(projectPath, config))) {
107
- configs.oxlintConfig = config;
108
- break;
109
- }
110
- for (const config of [".oxfmtrc.json", ".oxfmtrc.jsonc"]) if (fs.existsSync(path.join(projectPath, config))) {
111
- configs.oxfmtConfig = config;
112
- break;
113
- }
114
- for (const config of [
115
- "eslint.config.js",
116
- "eslint.config.mjs",
117
- "eslint.config.cjs",
118
- "eslint.config.ts",
119
- "eslint.config.mts",
120
- "eslint.config.cts"
121
- ]) if (fs.existsSync(path.join(projectPath, config))) {
122
- configs.eslintConfig = config;
123
- break;
124
- }
125
- for (const config of [
126
- ".eslintrc",
127
- ".eslintrc.json",
128
- ".eslintrc.js",
129
- ".eslintrc.cjs",
130
- ".eslintrc.yaml",
131
- ".eslintrc.yml"
132
- ]) if (fs.existsSync(path.join(projectPath, config))) {
133
- configs.eslintLegacyConfig = config;
134
- break;
135
- }
136
- for (const config of PRETTIER_CONFIG_FILES) if (fs.existsSync(path.join(projectPath, config))) {
137
- configs.prettierConfig = config;
138
- break;
139
- }
140
- if (fs.existsSync(path.join(projectPath, ".prettierignore"))) configs.prettierIgnore = true;
141
- if (fs.existsSync(path.join(projectPath, ".nvmrc"))) configs.nvmrcFile = true;
142
- const packageJsonPath = path.join(projectPath, "package.json");
143
- if (fs.existsSync(packageJsonPath)) try {
144
- const content = fs.readFileSync(packageJsonPath, "utf8");
145
- const pkg = JSON.parse(content);
146
- if (!configs.prettierConfig && pkg.prettier) configs.prettierConfig = PRETTIER_PACKAGE_JSON_CONFIG;
147
- const voltaNode = pkg.volta?.node;
148
- if (typeof voltaNode === "string") configs.voltaNode = voltaNode;
149
- } catch {}
150
- return configs;
151
- }
152
- //#endregion
153
- //#region src/migration/migrator.ts
154
- const LINT_STAGED_JSON_CONFIG_FILES = [".lintstagedrc.json", ".lintstagedrc"];
155
- const LINT_STAGED_OTHER_CONFIG_FILES = [
156
- ".lintstagedrc.yaml",
157
- ".lintstagedrc.yml",
158
- ".lintstagedrc.mjs",
159
- "lint-staged.config.mjs",
160
- ".lintstagedrc.cjs",
161
- "lint-staged.config.cjs",
162
- ".lintstagedrc.js",
163
- "lint-staged.config.js",
164
- ".lintstagedrc.ts",
165
- "lint-staged.config.ts",
166
- ".lintstagedrc.mts",
167
- "lint-staged.config.mts",
168
- ".lintstagedrc.cts",
169
- "lint-staged.config.cts"
170
- ];
171
- const LINT_STAGED_ALL_CONFIG_FILES = [...LINT_STAGED_JSON_CONFIG_FILES, ...LINT_STAGED_OTHER_CONFIG_FILES];
172
- const REMOVE_PACKAGES = [
173
- "oxlint",
174
- "oxlint-tsgolint",
175
- "oxfmt",
176
- "tsdown",
177
- "@vitest/browser",
178
- "@vitest/browser-preview"
179
- ];
180
- const WEBDRIVERIO_PROVIDER = "@vitest/browser-webdriverio";
181
- const PLAYWRIGHT_PROVIDER = "@vitest/browser-playwright";
182
- const OPT_IN_BROWSER_PROVIDERS = [WEBDRIVERIO_PROVIDER, PLAYWRIGHT_PROVIDER];
183
- const PROVIDER_OVERRIDE_DROP_NAMES = [...REMOVE_PACKAGES, ...OPT_IN_BROWSER_PROVIDERS];
184
- function extractOverrideTargetName(key) {
185
- let target = key.trim();
186
- for (let delim = target.search(/[^ |@]>/); delim !== -1; delim = target.search(/[^ |@]>/)) target = target.slice(delim + 2).trim();
187
- if (!target) return target;
188
- if (target.includes("/")) {
189
- const segments = target.split("/");
190
- const last = segments[segments.length - 1];
191
- const scope = segments[segments.length - 2];
192
- target = scope?.startsWith("@") ? `${scope}/${last}` : last;
193
- }
194
- const nameStart = target.startsWith("@") ? target.indexOf("/") + 1 : 0;
195
- const versionAt = target.indexOf("@", nameStart);
196
- if (versionAt > 0) target = target.slice(0, versionAt);
197
- return target;
198
- }
199
- function isRemovePackageOverrideKey(key) {
200
- return PROVIDER_OVERRIDE_DROP_NAMES.includes(extractOverrideTargetName(key));
201
- }
202
- function stripSegmentVersion(segment) {
203
- const nameStart = segment.startsWith("@") ? segment.indexOf("/") + 1 : 0;
204
- const versionAt = segment.indexOf("@", nameStart);
205
- return versionAt > 0 ? segment.slice(0, versionAt) : segment;
206
- }
207
- function parentGlobMatchesName(glob, name) {
208
- const pattern = glob.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
209
- return new RegExp(`^${pattern}$`).test(name);
210
- }
211
- function ancestorSegmentMatches(segment, name) {
212
- return segment.includes("*") ? parentGlobMatchesName(segment, name) : segment === name;
213
- }
214
- const OWNED_PROVIDER_ANCESTOR_NAMES = REMOVE_PACKAGES.filter((name) => name.startsWith("@vitest/"));
215
- function parentChainReachesVitePlus(segments) {
216
- const concrete = segments.filter((segment) => segment !== "**");
217
- let index = 0;
218
- if (concrete.length > 0 && ancestorSegmentMatches(concrete[0], "vite-plus")) index = 1;
219
- for (; index < concrete.length; index += 1) {
220
- const segment = concrete[index];
221
- if (!OWNED_PROVIDER_ANCESTOR_NAMES.some((name) => ancestorSegmentMatches(segment, name))) return false;
222
- }
223
- return true;
224
- }
225
- function extractOverrideParentSegments(key) {
226
- let rest = key.trim();
227
- const pnpmParents = [];
228
- for (let delim = rest.search(/[^ |@]>/); delim !== -1; delim = rest.search(/[^ |@]>/)) {
229
- pnpmParents.push(stripSegmentVersion(rest.slice(0, delim + 1).trim()));
230
- rest = rest.slice(delim + 2).trim();
231
- }
232
- if (pnpmParents.length > 0) return pnpmParents;
233
- if (!rest.includes("/")) return null;
234
- const segments = rest.split("/");
235
- const descriptorSegmentCount = segments[segments.length - 2]?.startsWith("@") ?? false ? 2 : 1;
236
- const rawParents = segments.slice(0, segments.length - descriptorSegmentCount);
237
- if (rawParents.length === 0) return null;
238
- const parents = [];
239
- for (let i = 0; i < rawParents.length; i += 1) {
240
- const segment = rawParents[i];
241
- if (segment.startsWith("@") && i + 1 < rawParents.length) {
242
- parents.push(stripSegmentVersion(`${segment}/${rawParents[i + 1]}`));
243
- i += 1;
244
- } else parents.push(stripSegmentVersion(segment));
245
- }
246
- return parents;
247
- }
248
- function providerKeyReachesVitePlus(key, ancestorChain) {
249
- if (!isRemovePackageOverrideKey(key)) return false;
250
- const keyParents = extractOverrideParentSegments(key) ?? [];
251
- return parentChainReachesVitePlus([...ancestorChain, ...keyParents]);
252
- }
253
- function shouldDropProviderOverrideKey(key) {
254
- return providerKeyReachesVitePlus(key, []);
255
- }
256
- function childChainContribution(key) {
257
- return [...extractOverrideParentSegments(key) ?? [], extractOverrideTargetName(key)];
258
- }
259
- // @vitest/provider`. Covers bare, versioned, global-glob and `vite-plus`-parent
260
- function dropRemovePackageOverrideKeys(overrides, ancestorChain = []) {
261
- if (!overrides) return false;
262
- let removed = false;
263
- for (const key of Object.keys(overrides)) {
264
- const value = overrides[key];
265
- const child = value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
266
- if (providerKeyReachesVitePlus(key, ancestorChain)) {
267
- if (child) {
268
- let changed = false;
269
- if ("." in child) {
270
- delete child["."];
271
- changed = true;
272
- }
273
- if (dropRemovePackageOverrideKeys(child, [...ancestorChain, ...childChainContribution(key)])) changed = true;
274
- if (Object.keys(child).length === 0) {
275
- delete overrides[key];
276
- changed = true;
277
- }
278
- if (changed) removed = true;
279
- } else {
280
- delete overrides[key];
281
- removed = true;
282
- }
283
- continue;
284
- }
285
- if (child) {
286
- if (dropRemovePackageOverrideKeys(child, [...ancestorChain, ...childChainContribution(key)])) {
287
- removed = true;
288
- if (Object.keys(child).length === 0) delete overrides[key];
289
- }
290
- }
291
- }
292
- return removed;
293
- }
294
- const BROWSER_PROVIDER_PEER_DEPS = {
295
- "@vitest/browser-playwright": "playwright",
296
- "@vitest/browser-webdriverio": "webdriverio"
297
- };
298
- const BROWSER_PROVIDER_POSTINSTALL_PACKAGES = ["edgedriver", "geckodriver"];
299
- const WEBDRIVERIO_ALLOW_SIGNAL_DEPS = ["webdriverio", WEBDRIVERIO_PROVIDER];
300
- const VITEST_BROWSER_DEP_NAMES = [
301
- "@vitest/browser",
302
- "@vitest/browser-preview",
303
- "@vitest/browser-playwright",
304
- "@vitest/browser-webdriverio"
305
- ];
306
- const PUBLIC_PEER_DEPENDENCY_FALLBACKS = {
307
- vite: "*",
308
- vitest: "*"
309
- };
310
- const OXLINT_NATIVE_PLUGINS = new Set([
311
- "eslint",
312
- "react",
313
- "unicorn",
314
- "typescript",
315
- "oxc",
316
- "import",
317
- "jsdoc",
318
- "jest",
319
- "vitest",
320
- "jsx-a11y",
321
- "nextjs",
322
- "react-perf",
323
- "promise",
324
- "node",
325
- "vue"
326
- ]);
327
- const LEGACY_WRAPPER_PACKAGE_NAMES = ["@voidzero-dev/vite-plus-test"];
328
- const LEGACY_WRAPPER_FALLBACK_VERSIONS = { vitest: VITEST_VERSION };
329
- function isLegacyWrapperSpec(value) {
330
- if (typeof value !== "string" || !value) return false;
331
- for (const name of LEGACY_WRAPPER_PACKAGE_NAMES) if (value === `npm:${name}` || value.startsWith(`npm:${name}@`)) return true;
332
- return false;
333
- }
334
- /**
335
- * Rewrite or remove keys whose value points at a deleted vite-plus wrapper.
336
- * When a fallback exists for the key (e.g. `vitest`), the value is replaced
337
- * so existing `catalog:` references continue to resolve. Otherwise the key
338
- * is dropped entirely. Returns true iff any entry was changed.
339
- *
340
- * npm/bun `overrides` may nest an object of scoped overrides under a parent
341
- * key (e.g. `{ "some-parent": { "vitest": "npm:@voidzero-dev/vite-plus-test@latest" } }`),
342
- * so object values are recursed into; a parent emptied by pruning is dropped so
343
- * no `{}` is left behind. Flat maps (pnpm `overrides`, yarn `resolutions`,
344
- * catalogs) hold only string values, where the recursion is inert.
345
- */
346
- function pruneLegacyWrapperAliases(record) {
347
- if (!record) return false;
348
- let mutated = false;
349
- for (const key of Object.keys(record)) {
350
- const value = record[key];
351
- if (value !== null && typeof value === "object" && !Array.isArray(value)) {
352
- if (pruneLegacyWrapperAliases(value)) {
353
- mutated = true;
354
- if (Object.keys(value).length === 0) delete record[key];
355
- }
356
- continue;
357
- }
358
- if (isLegacyWrapperSpec(value)) {
359
- const fallback = LEGACY_WRAPPER_FALLBACK_VERSIONS[key];
360
- if (fallback !== void 0) record[key] = fallback;
361
- else delete record[key];
362
- mutated = true;
363
- }
364
- }
365
- return mutated;
366
- }
367
- function warnMigration(message, report) {
368
- addMigrationWarning(report, message);
369
- if (!report) log.warn(message);
370
- }
371
- function infoMigration(message, report) {
372
- addManualStep(report, message);
373
- if (!report) log.info(message);
374
- }
375
- function checkViteVersion(projectPath) {
376
- return checkPackageVersion(projectPath, "vite", "7.0.0");
377
- }
378
- function checkVitestVersion(projectPath) {
379
- return checkPackageVersion(projectPath, "vitest", "4.0.0");
380
- }
381
- /**
382
- * Check the package version is supported by auto migration
383
- * @param projectPath - The path to the project
384
- * @param name - The name of the package
385
- * @param minVersion - The minimum version of the package
386
- * @returns true if the package version is supported by auto migration
387
- */
388
- function checkPackageVersion(projectPath, name, minVersion) {
389
- const metadata = detectPackageMetadata(projectPath, name);
390
- if (!metadata || metadata.name !== name) return true;
391
- if (import_semver.default.satisfies(metadata.version, `<${minVersion}`)) {
392
- const packageJsonFilePath = path.join(projectPath, "package.json");
393
- log.error(`✘ ${name}@${metadata.version} in ${displayRelative(packageJsonFilePath)} is not supported by auto migration`);
394
- log.info(`Please upgrade ${name} to version >=${minVersion} first`);
395
- return false;
396
- }
397
- return true;
398
- }
399
- function detectEslintProject(projectPath, packages) {
400
- const packageJsonPath = path.join(projectPath, "package.json");
401
- if (!fs.existsSync(packageJsonPath)) return { hasDependency: false };
402
- const pkg = readJsonFile(packageJsonPath);
403
- let hasDependency = !!(pkg.devDependencies?.eslint || pkg.dependencies?.eslint);
404
- const configs = detectConfigs(projectPath);
405
- let configFile = configs.eslintConfig;
406
- const legacyConfigFile = configs.eslintLegacyConfig;
407
- if (!hasDependency && packages) for (const wp of packages) {
408
- const pkgJsonPath = path.join(projectPath, wp.path, "package.json");
409
- if (!fs.existsSync(pkgJsonPath)) continue;
410
- const wpPkg = readJsonFile(pkgJsonPath);
411
- if (wpPkg.devDependencies?.eslint || wpPkg.dependencies?.eslint) {
412
- hasDependency = true;
413
- break;
414
- }
415
- }
416
- return {
417
- hasDependency,
418
- configFile,
419
- legacyConfigFile
420
- };
421
- }
422
- /**
423
- * Run a `vp dlx @oxlint/migrate` step with graceful error handling.
424
- * Returns true on success, false on failure (spawn error or non-zero exit).
425
- */
426
- async function runOxlintMigrateStep(vpBin, cwd, migratePackage, args, spinner, failMessage, manualHint) {
427
- try {
428
- const result = await runCommandSilently({
429
- command: vpBin,
430
- args: [
431
- "dlx",
432
- migratePackage,
433
- ...args
434
- ],
435
- cwd,
436
- envs: process.env
437
- });
438
- if (result.exitCode !== 0) {
439
- spinner.stop(failMessage);
440
- const stderr = result.stderr.toString().trim();
441
- if (stderr) log.warn(`⚠ ${stderr}`);
442
- log.info(manualHint);
443
- return false;
444
- }
445
- return true;
446
- } catch {
447
- spinner.stop(failMessage);
448
- log.info(manualHint);
449
- return false;
450
- }
451
- }
452
- async function migrateEslintToOxlint(projectPath, interactive, eslintConfigFile, packages, options) {
453
- const vpBin = process.env.VP_CLI_BIN ?? "vp";
454
- const spinner = options?.silent ? {
455
- start: () => {},
456
- stop: () => {},
457
- pause: () => {},
458
- resume: () => {},
459
- cancel: () => {},
460
- error: () => {},
461
- clear: () => {},
462
- message: () => {},
463
- isCancelled: false
464
- } : getSpinner(interactive);
465
- if (eslintConfigFile) {
466
- const { versions } = await import("./versions.js");
467
- const migratePackage = `@oxlint/migrate@${versions.oxlint}`;
468
- const migrateArgs = [
469
- "--merge",
470
- ...!hasBaseUrlInTsconfig(projectPath) ? ["--type-aware"] : [],
471
- "--with-nursery",
472
- "--details"
473
- ];
474
- spinner.start("Migrating ESLint config to Oxlint...");
475
- if (!await runOxlintMigrateStep(vpBin, projectPath, migratePackage, migrateArgs, spinner, "ESLint migration failed", `You can run \`vp dlx ${migratePackage} ${migrateArgs.join(" ")}\` manually later`)) return false;
476
- spinner.stop("ESLint config migrated to .oxlintrc.json");
477
- spinner.start("Replacing ESLint comments with Oxlint equivalents...");
478
- if (await runOxlintMigrateStep(vpBin, projectPath, migratePackage, ["--replace-eslint-comments"], spinner, "ESLint comment replacement failed", `You can run \`vp dlx ${migratePackage} --replace-eslint-comments\` manually later`)) spinner.stop("ESLint comments replaced");
479
- }
480
- if (options?.report) options.report.eslintMigrated = true;
481
- const preserveJsPlugins = collectJsPluginPackageNames(projectPath);
482
- const cleanupTargets = [projectPath, ...(packages ?? []).map((p) => path.join(projectPath, p.path))];
483
- for (const target of cleanupTargets) {
484
- if (!fs.existsSync(path.join(target, "package.json"))) continue;
485
- deleteEslintConfigFiles(target, options?.report, options?.silent);
486
- rewriteEslintPackageJson(path.join(target, "package.json"), preserveJsPlugins);
487
- rewriteEslintLintStagedConfigFiles(target, options?.report);
488
- }
489
- return true;
490
- }
491
- /**
492
- * Read `<projectPath>/.oxlintrc.json` (if any) and collect the package
493
- * names referenced via `lint.jsPlugins[]` string entries. Object-form
494
- * entries (`{ name, specifier }`) and local-path specifiers (`./X`,
495
- * `../X`, `/X`) are excluded — neither maps to a `package.json` entry
496
- * we'd accidentally strip.
497
- */
498
- function collectJsPluginPackageNames(projectPath) {
499
- const out = /* @__PURE__ */ new Set();
500
- const oxlintConfigPath = path.join(projectPath, ".oxlintrc.json");
501
- if (!fs.existsSync(oxlintConfigPath)) return out;
502
- let config;
503
- try {
504
- config = readJsonFile(oxlintConfigPath, true);
505
- } catch {
506
- return out;
507
- }
508
- const collectFrom = (jsPlugins) => {
509
- for (const entry of jsPlugins ?? []) {
510
- if (typeof entry !== "string") continue;
511
- if (entry.startsWith("./") || entry.startsWith("../") || entry.startsWith("/")) continue;
512
- out.add(entry);
513
- }
514
- };
515
- collectFrom(config.jsPlugins);
516
- if (Array.isArray(config.overrides)) for (const override of config.overrides) collectFrom(override.jsPlugins);
517
- return out;
518
- }
519
- function deleteEslintConfigFiles(basePath, report, silent = false) {
520
- const configs = detectConfigs(basePath);
521
- for (const file of [configs.eslintConfig, configs.eslintLegacyConfig]) if (file) {
522
- const configPath = path.join(basePath, file);
523
- if (fs.existsSync(configPath)) {
524
- fs.unlinkSync(configPath);
525
- if (report) report.removedConfigCount++;
526
- if (!silent) log.success(`✔ Removed ${displayRelative(configPath)}`);
527
- }
528
- }
529
- }
530
- const ESLINT_ECOSYSTEM_NAMES = new Set([
531
- "eslint",
532
- "typescript-eslint",
533
- "eslintrc",
534
- "eslint-utils",
535
- "eslint-visitor-keys",
536
- "eslint-scope",
537
- "eslint-define-config",
538
- "eslint-doc-generator",
539
- "@typescript-eslint/eslint-plugin",
540
- "@typescript-eslint/parser",
541
- "@typescript-eslint/rule-tester"
542
- ]);
543
- const ESLINT_ECOSYSTEM_PREFIXES = [
544
- "eslint-plugin-",
545
- "eslint-config-",
546
- "eslint-formatter-"
547
- ];
548
- const ESLINT_ECOSYSTEM_SCOPES = [
549
- "@eslint/",
550
- "@eslint-community/",
551
- "@angular-eslint/"
552
- ];
553
- /**
554
- * Decide whether a dependency entry should be removed alongside `eslint`
555
- * itself. The set is intentionally broad: anything whose only purpose is
556
- * to extend, configure, format, or wire ESLint becomes dead weight after
557
- * migration. `@types/<X>` packages are checked symmetrically with `<X>`
558
- * so type-only counterparts of removed runtime packages also go.
559
- */
560
- function isEslintEcosystemDep(name) {
561
- const stripped = name.startsWith("@types/") ? name.slice(7) : name;
562
- if (ESLINT_ECOSYSTEM_NAMES.has(stripped)) return true;
563
- if (ESLINT_ECOSYSTEM_PREFIXES.some((p) => stripped.startsWith(p))) return true;
564
- if (ESLINT_ECOSYSTEM_SCOPES.some((s) => stripped.startsWith(s))) return true;
565
- // @vitest/eslint-plugin
566
- if (/^@[^/]+\/eslint-(plugin|config|formatter)(-.+)?$/.test(stripped)) return true;
567
- return false;
568
- }
569
- /**
570
- * Rewrite a project's `package.json` after ESLint has been migrated to
571
- * Oxlint: drop every ESLint-ecosystem dependency (see
572
- * `isEslintEcosystemDep`), strip empty containers, and rewrite eslint
573
- * tokens in scripts / lint-staged. Applied uniformly to the root and to
574
- * every workspace package — the migration treats the whole workspace as
575
- * in scope for adoption, so a half-cleanup at the workspace level would
576
- * be inconsistent with the rest of the flow (which already replaces
577
- * vite-related overrides and adds vite-plus across all packages).
578
- *
579
- * `preserveJsPlugins` names packages that `@oxlint/migrate` referenced
580
- * via `lint.jsPlugins` and that Oxlint will need to `import()` at lint
581
- * time. They override `isEslintEcosystemDep` so the generated config
582
- * isn't immediately invalidated by the cleanup step.
583
- */
584
- function rewriteEslintPackageJson(packageJsonPath, preserveJsPlugins = /* @__PURE__ */ new Set()) {
585
- editJsonFile(packageJsonPath, (pkg) => {
586
- let changed = false;
587
- for (const field of [
588
- "devDependencies",
589
- "dependencies",
590
- "peerDependencies",
591
- "optionalDependencies"
592
- ]) {
593
- const deps = pkg[field];
594
- if (!deps) continue;
595
- let removedAny = false;
596
- for (const name of Object.keys(deps)) {
597
- if (preserveJsPlugins.has(name)) continue;
598
- if (isEslintEcosystemDep(name)) {
599
- delete deps[name];
600
- changed = true;
601
- removedAny = true;
602
- }
603
- }
604
- if (removedAny && Object.keys(deps).length === 0) delete pkg[field];
605
- }
606
- if (pkg.scripts) {
607
- const updated = rewriteEslint(JSON.stringify(pkg.scripts));
608
- if (updated) {
609
- pkg.scripts = JSON.parse(updated);
610
- changed = true;
611
- }
612
- }
613
- if (pkg["lint-staged"]) {
614
- const updated = rewriteEslint(JSON.stringify(pkg["lint-staged"]));
615
- if (updated) {
616
- pkg["lint-staged"] = JSON.parse(updated);
617
- changed = true;
618
- }
619
- }
620
- return changed ? pkg : void 0;
621
- });
622
- }
623
- /**
624
- * Rewrite tool references in lint-staged config files (JSON ones are rewritten,
625
- * non-JSON ones get a warning).
626
- */
627
- function rewriteToolLintStagedConfigFiles(projectPath, rewriteFn, toolName, report) {
628
- for (const filename of LINT_STAGED_JSON_CONFIG_FILES) {
629
- const configPath = path.join(projectPath, filename);
630
- if (!fs.existsSync(configPath)) continue;
631
- if (filename === ".lintstagedrc" && !isJsonFile(configPath)) {
632
- warnMigration(`${displayRelative(configPath)} is not JSON — please update ${toolName} references manually`, report);
633
- continue;
634
- }
635
- editJsonFile(configPath, (config) => {
636
- const updated = rewriteFn(JSON.stringify(config));
637
- if (updated) return JSON.parse(updated);
638
- });
639
- }
640
- for (const filename of LINT_STAGED_OTHER_CONFIG_FILES) {
641
- const configPath = path.join(projectPath, filename);
642
- if (!fs.existsSync(configPath)) continue;
643
- warnMigration(`${displayRelative(configPath)} — please update ${toolName} references manually`, report);
644
- }
645
- }
646
- function rewriteEslintLintStagedConfigFiles(projectPath, report) {
647
- rewriteToolLintStagedConfigFiles(projectPath, rewriteEslint, "eslint", report);
648
- }
649
- function detectPrettierProject(projectPath, packages) {
650
- const packageJsonPath = path.join(projectPath, "package.json");
651
- if (!fs.existsSync(packageJsonPath)) return { hasDependency: false };
652
- const pkg = readJsonFile(packageJsonPath);
653
- let hasDependency = !!(pkg.devDependencies?.prettier || pkg.dependencies?.prettier);
654
- const configFile = detectConfigs(projectPath).prettierConfig;
655
- if (!hasDependency && packages) for (const wp of packages) {
656
- const pkgJsonPath = path.join(projectPath, wp.path, "package.json");
657
- if (!fs.existsSync(pkgJsonPath)) continue;
658
- const wpPkg = readJsonFile(pkgJsonPath);
659
- if (wpPkg.devDependencies?.prettier || wpPkg.dependencies?.prettier) {
660
- hasDependency = true;
661
- break;
662
- }
663
- }
664
- return {
665
- hasDependency,
666
- configFile
667
- };
668
- }
669
- /**
670
- * Run `vp fmt --migrate=prettier` step with graceful error handling.
671
- * Returns true on success, false on failure.
672
- */
673
- async function runPrettierMigrateStep(vpBin, cwd, spinner, failMessage, manualHint) {
674
- try {
675
- const result = await runCommandSilently({
676
- command: vpBin,
677
- args: ["fmt", "--migrate=prettier"],
678
- cwd,
679
- envs: process.env
680
- });
681
- if (result.exitCode !== 0) {
682
- spinner.stop(failMessage);
683
- const stderr = result.stderr.toString().trim();
684
- if (stderr) log.warn(`⚠ ${stderr}`);
685
- log.info(manualHint);
686
- return false;
687
- }
688
- return true;
689
- } catch {
690
- spinner.stop(failMessage);
691
- log.info(manualHint);
692
- return false;
693
- }
694
- }
695
- async function migratePrettierToOxfmt(projectPath, interactive, prettierConfigFile, packages, options) {
696
- const vpBin = process.env.VP_CLI_BIN ?? "vp";
697
- const spinner = options?.silent ? {
698
- start: () => {},
699
- stop: () => {},
700
- pause: () => {},
701
- resume: () => {},
702
- cancel: () => {},
703
- error: () => {},
704
- clear: () => {},
705
- message: () => {},
706
- isCancelled: false
707
- } : getSpinner(interactive);
708
- if (prettierConfigFile) {
709
- let tempPrettierConfig;
710
- if (prettierConfigFile === "package.json#prettier") {
711
- const pkg = readJsonFile(path.join(projectPath, "package.json"));
712
- if (pkg.prettier) {
713
- tempPrettierConfig = path.join(projectPath, ".prettierrc.json");
714
- fs.writeFileSync(tempPrettierConfig, JSON.stringify(pkg.prettier, null, 2));
715
- } else return true;
716
- }
717
- try {
718
- spinner.start("Migrating Prettier config to Oxfmt...");
719
- if (!await runPrettierMigrateStep(vpBin, projectPath, spinner, "Prettier migration failed", "You can run `vp fmt --migrate=prettier` manually later")) return false;
720
- spinner.stop("Prettier config migrated to .oxfmtrc.json");
721
- } finally {
722
- if (tempPrettierConfig) try {
723
- fs.unlinkSync(tempPrettierConfig);
724
- } catch {}
725
- }
726
- }
727
- if (options?.report) options.report.prettierMigrated = true;
728
- deletePrettierConfigFiles(projectPath, options?.report, options?.silent);
729
- rewritePrettierPackageJson(path.join(projectPath, "package.json"));
730
- if (packages) for (const pkg of packages) rewritePrettierPackageJson(path.join(projectPath, pkg.path, "package.json"));
731
- rewritePrettierLintStagedConfigFiles(projectPath, options?.report);
732
- const prettierIgnorePath = path.join(projectPath, ".prettierignore");
733
- if (fs.existsSync(prettierIgnorePath)) warnMigration(`${displayRelative(prettierIgnorePath)} found — Oxfmt supports .prettierignore, but using the \`ignorePatterns\` option is recommended.`, options?.report);
734
- return true;
735
- }
736
- function deletePrettierConfigFiles(basePath, report, silent = false) {
737
- const configs = detectConfigs(basePath);
738
- if (configs.prettierConfig && configs.prettierConfig !== "package.json#prettier") {
739
- const configPath = path.join(basePath, configs.prettierConfig);
740
- if (fs.existsSync(configPath)) {
741
- fs.unlinkSync(configPath);
742
- if (report) report.removedConfigCount++;
743
- if (!silent) log.success(`✔ Removed ${displayRelative(configPath)}`);
744
- }
745
- }
746
- for (const file of PRETTIER_CONFIG_FILES) {
747
- if (file === configs.prettierConfig) continue;
748
- const configPath = path.join(basePath, file);
749
- if (fs.existsSync(configPath)) {
750
- fs.unlinkSync(configPath);
751
- if (report) report.removedConfigCount++;
752
- if (!silent) log.success(`✔ Removed ${displayRelative(configPath)}`);
753
- }
754
- }
755
- editJsonFile(path.join(basePath, "package.json"), (pkg) => {
756
- if (pkg.prettier) {
757
- delete pkg.prettier;
758
- return pkg;
759
- }
760
- });
761
- }
762
- function rewritePrettierPackageJson(packageJsonPath) {
763
- if (!fs.existsSync(packageJsonPath)) return;
764
- editJsonFile(packageJsonPath, (pkg) => {
765
- let changed = false;
766
- if (pkg.devDependencies) {
767
- for (const dep of Object.keys(pkg.devDependencies)) if (dep === "prettier" || dep.startsWith("prettier-plugin-")) {
768
- delete pkg.devDependencies[dep];
769
- changed = true;
770
- }
771
- }
772
- if (pkg.dependencies) {
773
- for (const dep of Object.keys(pkg.dependencies)) if (dep === "prettier" || dep.startsWith("prettier-plugin-")) {
774
- delete pkg.dependencies[dep];
775
- changed = true;
776
- }
777
- }
778
- if (pkg.scripts) {
779
- const updated = rewritePrettier(JSON.stringify(pkg.scripts));
780
- if (updated) {
781
- pkg.scripts = JSON.parse(updated);
782
- changed = true;
783
- }
784
- }
785
- if (pkg["lint-staged"]) {
786
- const updated = rewritePrettier(JSON.stringify(pkg["lint-staged"]));
787
- if (updated) {
788
- pkg["lint-staged"] = JSON.parse(updated);
789
- changed = true;
790
- }
791
- }
792
- return changed ? pkg : void 0;
793
- });
794
- }
795
- function rewritePrettierLintStagedConfigFiles(projectPath, report) {
796
- rewriteToolLintStagedConfigFiles(projectPath, rewritePrettier, "prettier", report);
797
- }
798
- function cleanupDeprecatedTsconfigOptions(projectPath, silent = false, report) {
799
- const deprecatedOptions = ["esModuleInterop", "allowSyntheticDefaultImports"];
800
- const files = findTsconfigFiles(projectPath);
801
- for (const filePath of files) for (const name of deprecatedOptions) if (removeDeprecatedTsconfigFalseOption(filePath, name)) {
802
- if (report) report.removedConfigCount++;
803
- if (!silent) log.success(`✔ Removed ${name}: false from ${displayRelative(filePath)}`);
804
- warnMigration(`Removed \`"${name}": false\` from ${displayRelative(filePath)} — this option has been deprecated. See https://github.com/oxc-project/tsgolint/issues/351, https://github.com/microsoft/TypeScript/issues/62529`, report);
805
- }
806
- }
807
- function rewriteTsconfigTypes(projectPath, silent = false, report) {
808
- let changed = false;
809
- const files = findTsconfigFiles(projectPath);
810
- for (const filePath of files) if (rewriteTypesInTsconfig(filePath)) {
811
- changed = true;
812
- if (report) report.removedConfigCount++;
813
- if (!silent) log.success(`✔ Rewrote types in ${displayRelative(filePath)}`);
814
- }
815
- return changed;
816
- }
817
- function hasTsconfigTypesToRewrite(projectPath) {
818
- return findTsconfigFiles(projectPath).some((filePath) => hasTypesToRewriteInTsconfig(filePath));
819
- }
820
- const FRAMEWORK_SHIMS = {
821
- vue: [
822
- "declare module '*.vue' {",
823
- " import type { DefineComponent } from 'vue';",
824
- " const component: DefineComponent<{}, {}, unknown>;",
825
- " export default component;",
826
- "}"
827
- ].join("\n"),
828
- astro: "/// <reference types=\"astro/client\" />"
829
- };
830
- function detectFramework(projectPath) {
831
- const packageJsonPath = path.join(projectPath, "package.json");
832
- if (!fs.existsSync(packageJsonPath)) return [];
833
- const pkg = readJsonFile(packageJsonPath);
834
- const allDeps = {
835
- ...pkg.dependencies,
836
- ...pkg.devDependencies
837
- };
838
- return ["vue", "astro"].filter((framework) => !!allDeps[framework]);
839
- }
840
- function getEnvDtsPath(projectPath) {
841
- const srcEnvDts = path.join(projectPath, "src", "env.d.ts");
842
- const rootEnvDts = path.join(projectPath, "env.d.ts");
843
- for (const candidate of [srcEnvDts, rootEnvDts]) if (fs.existsSync(candidate)) return candidate;
844
- return fs.existsSync(path.join(projectPath, "src")) ? srcEnvDts : rootEnvDts;
845
- }
846
- function hasFrameworkShim(projectPath, framework) {
847
- const dirsToScan = [projectPath, path.join(projectPath, "src")];
848
- for (const dir of dirsToScan) {
849
- if (!fs.existsSync(dir)) continue;
850
- let entries;
851
- try {
852
- entries = fs.readdirSync(dir);
853
- } catch {
854
- continue;
855
- }
856
- for (const entry of entries) {
857
- if (!entry.endsWith(".d.ts")) continue;
858
- const content = fs.readFileSync(path.join(dir, entry), "utf-8");
859
- if (framework === "astro") {
860
- if (content.includes("astro/client")) return true;
861
- } else if (content.includes(`'*.${framework}'`) || content.includes(`"*.${framework}"`)) return true;
862
- }
863
- }
864
- return false;
865
- }
866
- function addFrameworkShim(projectPath, framework, report) {
867
- const envDtsPath = getEnvDtsPath(projectPath);
868
- const shim = FRAMEWORK_SHIMS[framework];
869
- if (fs.existsSync(envDtsPath)) {
870
- const existing = fs.readFileSync(envDtsPath, "utf-8");
871
- fs.writeFileSync(envDtsPath, `${existing.trimEnd()}\n\n${shim}\n`, "utf-8");
872
- } else {
873
- fs.mkdirSync(path.dirname(envDtsPath), { recursive: true });
874
- fs.writeFileSync(envDtsPath, `${shim}\n`, "utf-8");
875
- }
876
- if (report) report.frameworkShimAdded = true;
877
- }
878
- /**
879
- * Rewrite standalone project to add vite-plus dependencies
880
- * @param projectPath - The path to the project
881
- */
882
- function rewriteStandaloneProject(projectPath, workspaceInfo, skipStagedMigration, silent = false, report) {
883
- const packageJsonPath = path.join(projectPath, "package.json");
884
- if (!fs.existsSync(packageJsonPath)) return;
885
- const packageManager = workspaceInfo.packageManager;
886
- const catalogDependencyResolver = createCatalogDependencyResolver(projectPath, packageManager);
887
- const pnpmMajorVersion = pnpmMajor(workspaceInfo.downloadPackageManager.version);
888
- let extractedStagedConfig = null;
889
- let remainingPnpmOverrides;
890
- let shouldRewritePnpmWorkspaceYaml = false;
891
- let shouldAddPnpmWorkspaceVitePlusOverride = false;
892
- let shouldAllowBrowserProviderBuilds = false;
893
- let usePnpmWorkspaceYaml = false;
894
- editJsonFile(packageJsonPath, (pkg) => {
895
- shouldAllowBrowserProviderBuilds = hasOwnWebdriverioDependency(pkg) || usesWebdriverioProvider(projectPath);
896
- pruneLegacyWrapperAliases(pkg.resolutions);
897
- pruneLegacyWrapperAliases(pkg.overrides);
898
- pruneLegacyWrapperAliases(pkg.pnpm?.overrides);
899
- dropRemovePackageOverrideKeys(pkg.resolutions);
900
- dropRemovePackageOverrideKeys(pkg.overrides);
901
- if (packageManager === PackageManager.yarn) pkg.resolutions = {
902
- ...pkg.resolutions,
903
- ...VITE_PLUS_OVERRIDE_PACKAGES
904
- };
905
- else if (packageManager === PackageManager.npm || packageManager === PackageManager.bun) {
906
- pkg.overrides = {
907
- ...pkg.overrides,
908
- ...VITE_PLUS_OVERRIDE_PACKAGES
909
- };
910
- if (packageManager === PackageManager.bun) pkg.devDependencies = {
911
- ...pkg.devDependencies,
912
- vite: VITE_PLUS_OVERRIDE_PACKAGES.vite
913
- };
914
- } else if (packageManager === PackageManager.pnpm) {
915
- usePnpmWorkspaceYaml = !pkg.pnpm;
916
- if (usePnpmWorkspaceYaml) {
917
- shouldRewritePnpmWorkspaceYaml = true;
918
- shouldAddPnpmWorkspaceVitePlusOverride = isForceOverrideMode();
919
- }
920
- const overrideKeys = Object.keys(VITE_PLUS_OVERRIDE_PACKAGES);
921
- if (!usePnpmWorkspaceYaml) {
922
- dropRemovePackageOverrideKeys(pkg.pnpm?.overrides);
923
- pkg.pnpm = {
924
- ...pkg.pnpm,
925
- overrides: {
926
- ...pkg.pnpm?.overrides,
927
- ...VITE_PLUS_OVERRIDE_PACKAGES,
928
- ...isForceOverrideMode() ? { [VITE_PLUS_NAME]: VITE_PLUS_VERSION } : {}
929
- },
930
- peerDependencyRules: {
931
- ...pkg.pnpm?.peerDependencyRules,
932
- allowAny: [...new Set([...pkg.pnpm?.peerDependencyRules?.allowAny ?? [], ...overrideKeys])],
933
- allowedVersions: {
934
- ...pkg.pnpm?.peerDependencyRules?.allowedVersions,
935
- ...Object.fromEntries(overrideKeys.map((key) => [key, "*"]))
936
- }
937
- }
938
- };
939
- } else remainingPnpmOverrides = cleanupPnpmOverridesForWorkspaceYaml(pkg, overrideKeys);
940
- for (const key in pkg.pnpm?.overrides) if (key.includes(">")) {
941
- const splits = key.split(">");
942
- if (splits[splits.length - 1].trim() === "vite") delete pkg.pnpm.overrides[key];
943
- }
944
- for (const key of [...overrideKeys, ...PROVIDER_OVERRIDE_DROP_NAMES]) if (pkg.resolutions?.[key]) delete pkg.resolutions[key];
945
- if (!usePnpmWorkspaceYaml && pnpmMajorVersion !== void 0 && pkg.pnpm) applyBuildAllowanceToPackageJsonPnpm(pkg.pnpm, pnpmMajorVersion, shouldAllowBrowserProviderBuilds);
946
- }
947
- extractedStagedConfig = rewritePackageJson(pkg, packageManager, usePnpmWorkspaceYaml, skipStagedMigration, catalogDependencyResolver, usesVitestBrowserMode(projectPath), collectProviderSourceModes(projectPath));
948
- if (!pkg.devDependencies?.["vite-plus"] || isForceOverrideMode()) {
949
- const version = usePnpmWorkspaceYaml && !VITE_PLUS_VERSION.startsWith("file:") ? "catalog:" : VITE_PLUS_VERSION;
950
- pkg.devDependencies = {
951
- ...pkg.devDependencies,
952
- [VITE_PLUS_NAME]: version
953
- };
954
- }
955
- return pkg;
956
- });
957
- if (shouldRewritePnpmWorkspaceYaml) rewritePnpmWorkspaceYaml(projectPath, pnpmMajorVersion, shouldAllowBrowserProviderBuilds);
958
- if (remainingPnpmOverrides) migratePnpmOverridesToWorkspaceYaml(projectPath, remainingPnpmOverrides);
959
- if (shouldAddPnpmWorkspaceVitePlusOverride) migratePnpmOverridesToWorkspaceYaml(projectPath, { [VITE_PLUS_NAME]: VITE_PLUS_VERSION });
960
- if (packageManager === PackageManager.yarn) rewriteYarnrcYml(projectPath);
961
- else if (packageManager === PackageManager.bun) ensureBunfigPeerSuppression(projectPath);
962
- if (extractedStagedConfig) {
963
- if (mergeStagedConfigToViteConfig(projectPath, extractedStagedConfig, silent, report)) removeLintStagedFromPackageJson(packageJsonPath);
964
- }
965
- if (!skipStagedMigration) rewriteLintStagedConfigFile(projectPath, report);
966
- cleanupDeprecatedTsconfigOptions(projectPath, silent, report);
967
- rewriteTsconfigTypes(projectPath, silent, report);
968
- mergeViteConfigFiles(projectPath, silent, report, workspaceInfo.packages);
969
- injectLintTypeCheckDefaults(projectPath, silent, report);
970
- injectFmtDefaults(projectPath, silent, report);
971
- mergeTsdownConfigFile(projectPath, silent, report);
972
- rewriteAllImports(projectPath, silent, report);
973
- wrapLazyPluginsInViteConfig(projectPath, silent, report);
974
- setPackageManager(projectPath, workspaceInfo.downloadPackageManager);
975
- }
976
- /**
977
- * Rewrite monorepo to add vite-plus dependencies
978
- * @param workspaceInfo - The workspace info
979
- */
980
- function rewriteMonorepo(workspaceInfo, skipStagedMigration, silent = false, report) {
981
- const catalogDependencyResolver = createCatalogDependencyResolver(workspaceInfo.rootDir, workspaceInfo.packageManager);
982
- const pnpmMajorVersion = pnpmMajor(workspaceInfo.downloadPackageManager.version);
983
- const workspaceShouldAllowBrowserBuilds = workspaceUsesWebdriverio(workspaceInfo.rootDir, workspaceInfo.packages);
984
- if (workspaceInfo.packageManager === PackageManager.pnpm) rewritePnpmWorkspaceYaml(workspaceInfo.rootDir, pnpmMajorVersion, workspaceShouldAllowBrowserBuilds);
985
- else if (workspaceInfo.packageManager === PackageManager.yarn) rewriteYarnrcYml(workspaceInfo.rootDir);
986
- else if (workspaceInfo.packageManager === PackageManager.bun) rewriteBunCatalog(workspaceInfo.rootDir);
987
- rewriteRootWorkspacePackageJson(workspaceInfo.rootDir, workspaceInfo.packageManager, skipStagedMigration, catalogDependencyResolver, workspaceInfo.packages, pnpmMajorVersion, workspaceShouldAllowBrowserBuilds);
988
- const workspaceContext = {
989
- rootDir: workspaceInfo.rootDir,
990
- packages: workspaceInfo.packages
991
- };
992
- for (const pkg of workspaceInfo.packages) rewriteMonorepoProject(path.join(workspaceInfo.rootDir, pkg.path), workspaceInfo.packageManager, skipStagedMigration, silent, report, catalogDependencyResolver, workspaceContext, true);
993
- if (!skipStagedMigration) rewriteLintStagedConfigFile(workspaceInfo.rootDir, report);
994
- cleanupDeprecatedTsconfigOptions(workspaceInfo.rootDir, silent, report);
995
- rewriteTsconfigTypes(workspaceInfo.rootDir, silent, report);
996
- mergeViteConfigFiles(workspaceInfo.rootDir, silent, report, workspaceInfo.packages);
997
- injectLintTypeCheckDefaults(workspaceInfo.rootDir, silent, report);
998
- injectFmtDefaults(workspaceInfo.rootDir, silent, report);
999
- mergeTsdownConfigFile(workspaceInfo.rootDir, silent, report);
1000
- rewriteAllImports(workspaceInfo.rootDir, silent, report);
1001
- wrapLazyPluginsInViteConfig(workspaceInfo.rootDir, silent, report);
1002
- for (const pkg of workspaceInfo.packages) wrapLazyPluginsInViteConfig(path.join(workspaceInfo.rootDir, pkg.path), silent, report);
1003
- setPackageManager(workspaceInfo.rootDir, workspaceInfo.downloadPackageManager);
1004
- }
1005
- /**
1006
- * Rewrite monorepo project to add vite-plus dependencies
1007
- * @param projectPath - The path to the project
1008
- * @param workspaceContext - Full workspace info, used so the lint-config
1009
- * sanitizer can see hoisted deps living elsewhere in the workspace,
1010
- * not just this sub-package's own `package.json`. `rootDir` is the
1011
- * workspace root (paths in `packages` are relative to it); `packages`
1012
- * is the workspace package list.
1013
- */
1014
- function rewriteMonorepoProject(projectPath, packageManager, skipStagedMigration, silent = false, report, catalogDependencyResolver, workspaceContext, deferLazyPluginWrapping = false) {
1015
- cleanupDeprecatedTsconfigOptions(projectPath, silent, report);
1016
- rewriteTsconfigTypes(projectPath, silent, report);
1017
- mergeViteConfigFiles(projectPath, silent, report, workspaceContext?.packages, workspaceContext?.rootDir);
1018
- mergeTsdownConfigFile(projectPath, silent, report);
1019
- const packageJsonPath = path.join(projectPath, "package.json");
1020
- if (!fs.existsSync(packageJsonPath)) return;
1021
- const yarnHoisting = packageManager === PackageManager.yarn ? findYarnWorkspaceHoisting(workspaceContext?.rootDir ?? projectPath) : void 0;
1022
- let extractedStagedConfig = null;
1023
- editJsonFile(packageJsonPath, (pkg) => {
1024
- extractedStagedConfig = rewritePackageJson(pkg, packageManager, true, skipStagedMigration, catalogDependencyResolver, usesVitestBrowserMode(projectPath), collectProviderSourceModes(projectPath));
1025
- if (yarnHoisting && path.resolve(projectPath) !== yarnHoisting.rootDir && pkg.devDependencies?.["vite-plus"]) applyYarnWorkspaceHoistingFix(pkg, yarnHoisting.limit, yarnHoisting.nodeLinker, path.relative(yarnHoisting.rootDir, projectPath) || projectPath, report);
1026
- return pkg;
1027
- });
1028
- if (extractedStagedConfig) {
1029
- if (mergeStagedConfigToViteConfig(projectPath, extractedStagedConfig, silent, report)) removeLintStagedFromPackageJson(packageJsonPath);
1030
- }
1031
- if (!deferLazyPluginWrapping) wrapLazyPluginsInViteConfig(projectPath, silent, report);
1032
- }
1033
- /**
1034
- * Rewrite pnpm-workspace.yaml to add vite-plus dependencies
1035
- * @param projectPath - The path to the project
1036
- */
1037
- function rewritePnpmWorkspaceYaml(projectPath, pnpmMajorVersion, shouldAllowBrowserBuilds) {
1038
- const pnpmWorkspaceYamlPath = path.join(projectPath, "pnpm-workspace.yaml");
1039
- if (!fs.existsSync(pnpmWorkspaceYamlPath)) fs.writeFileSync(pnpmWorkspaceYamlPath, "");
1040
- editYamlFile(pnpmWorkspaceYamlPath, (doc) => {
1041
- rewriteCatalog(doc);
1042
- if (pnpmMajorVersion !== void 0) applyBuildAllowanceToWorkspaceYaml(doc, pnpmMajorVersion, shouldAllowBrowserBuilds);
1043
- const overrides = doc.getIn(["overrides"]);
1044
- pruneYamlMapLegacyWrapperAliases(overrides);
1045
- if (overrides instanceof import_dist.YAMLMap) {
1046
- const keysSnapshot = overrides.items.map((item) => item.key);
1047
- for (const keyNode of keysSnapshot) if (shouldDropProviderOverrideKey(keyNode instanceof import_dist.Scalar ? String(keyNode.value ?? "") : String(keyNode ?? ""))) overrides.delete(keyNode);
1048
- }
1049
- for (const key of Object.keys(VITE_PLUS_OVERRIDE_PACKAGES)) {
1050
- const version = getCatalogDependencySpec(getYamlMapScalarStringValue(overrides, key), VITE_PLUS_OVERRIDE_PACKAGES[key], true);
1051
- doc.setIn(["overrides", scalarString(key)], scalarString(version));
1052
- }
1053
- const updatedOverrides = doc.getIn(["overrides"]);
1054
- for (const item of updatedOverrides.items) if (item.key.value.includes(">")) {
1055
- const splits = item.key.value.split(">");
1056
- if (splits[splits.length - 1].trim() === "vite") updatedOverrides.delete(item.key);
1057
- }
1058
- let allowAny = doc.getIn(["peerDependencyRules", "allowAny"]);
1059
- if (!allowAny) allowAny = new import_dist.YAMLSeq();
1060
- const existing = new Set(allowAny.items.map((n) => n.value));
1061
- for (const key of Object.keys(VITE_PLUS_OVERRIDE_PACKAGES)) if (!existing.has(key)) allowAny.add(scalarString(key));
1062
- doc.setIn(["peerDependencyRules", "allowAny"], allowAny);
1063
- let allowedVersions = doc.getIn(["peerDependencyRules", "allowedVersions"]);
1064
- if (!allowedVersions) allowedVersions = new import_dist.YAMLMap();
1065
- for (const key of Object.keys(VITE_PLUS_OVERRIDE_PACKAGES)) allowedVersions.set(scalarString(key), scalarString("*"));
1066
- doc.setIn(["peerDependencyRules", "allowedVersions"], allowedVersions);
1067
- if (doc.has("minimumReleaseAge")) {
1068
- const excludes = [
1069
- "vite-plus",
1070
- "@voidzero-dev/*",
1071
- "oxlint",
1072
- "@oxlint/*",
1073
- "oxlint-tsgolint",
1074
- "@oxlint-tsgolint/*",
1075
- "oxfmt",
1076
- "@oxfmt/*",
1077
- ...VITEST_AGE_GATE_EXEMPT_PACKAGES
1078
- ];
1079
- let minimumReleaseAgeExclude = doc.getIn(["minimumReleaseAgeExclude"]);
1080
- if (!minimumReleaseAgeExclude) minimumReleaseAgeExclude = new import_dist.YAMLSeq();
1081
- const existing = new Set(minimumReleaseAgeExclude.items.map((n) => n.value));
1082
- for (const exclude of excludes) if (!existing.has(exclude)) minimumReleaseAgeExclude.add(scalarString(exclude));
1083
- doc.setIn(["minimumReleaseAgeExclude"], minimumReleaseAgeExclude);
1084
- }
1085
- });
1086
- }
1087
- /**
1088
- * Clean up pnpm.overrides and peerDependencyRules from package.json when migrating
1089
- * to pnpm-workspace.yaml. Returns any remaining non-Vite overrides that need to be
1090
- * moved to pnpm-workspace.yaml.
1091
- */
1092
- function cleanupPnpmOverridesForWorkspaceYaml(pkg, overrideKeys) {
1093
- dropRemovePackageOverrideKeys(pkg.pnpm?.overrides);
1094
- const catalogOverrides = {};
1095
- const overrides = pkg.pnpm?.overrides;
1096
- for (const key of [...overrideKeys, ...PROVIDER_OVERRIDE_DROP_NAMES]) {
1097
- const value = overrides?.[key];
1098
- if (value) {
1099
- if (overrideKeys.includes(key) && value.startsWith("catalog:")) catalogOverrides[key] = value;
1100
- delete overrides[key];
1101
- }
1102
- }
1103
- for (const key in pkg.pnpm?.overrides) if (key.includes(">")) {
1104
- const splits = key.split(">");
1105
- if (splits[splits.length - 1].trim() === "vite") delete pkg.pnpm.overrides[key];
1106
- }
1107
- let remaining;
1108
- if (Object.keys(catalogOverrides).length > 0) remaining = { ...catalogOverrides };
1109
- if (pkg.pnpm?.overrides && Object.keys(pkg.pnpm.overrides).length > 0) remaining = {
1110
- ...remaining,
1111
- ...pkg.pnpm.overrides
1112
- };
1113
- delete pkg.pnpm?.overrides;
1114
- cleanupPeerDependencyRules(pkg.pnpm?.peerDependencyRules, overrideKeys);
1115
- if (pkg.pnpm?.peerDependencyRules && Object.keys(pkg.pnpm.peerDependencyRules).length === 0) delete pkg.pnpm.peerDependencyRules;
1116
- if (pkg.pnpm && Object.keys(pkg.pnpm).length === 0) delete pkg.pnpm;
1117
- return remaining;
1118
- }
1119
- /**
1120
- * Move remaining non-Vite pnpm.overrides from package.json to pnpm-workspace.yaml.
1121
- * pnpm ignores workspace-level overrides when pnpm.overrides exists in package.json,
1122
- * so all overrides must live in pnpm-workspace.yaml.
1123
- */
1124
- function migratePnpmOverridesToWorkspaceYaml(projectPath, overrides) {
1125
- editYamlFile(path.join(projectPath, "pnpm-workspace.yaml"), (doc) => {
1126
- for (const [key, value] of Object.entries(overrides)) doc.setIn(["overrides", scalarString(key)], scalarString(value));
1127
- });
1128
- }
1129
- function hasOwnWebdriverioDependency(pkg) {
1130
- for (const name of WEBDRIVERIO_ALLOW_SIGNAL_DEPS) if (pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? pkg.optionalDependencies?.[name] ?? pkg.peerDependencies?.[name]) return true;
1131
- return false;
1132
- }
1133
- function workspaceUsesWebdriverio(rootDir, packages) {
1134
- const rootPkg = readPackageJsonIfExists(path.join(rootDir, "package.json"));
1135
- if (rootPkg && hasOwnWebdriverioDependency(rootPkg)) return true;
1136
- if (usesWebdriverioProvider(rootDir)) return true;
1137
- if (!packages) return false;
1138
- for (const pkg of packages) {
1139
- const packageDir = path.join(rootDir, pkg.path);
1140
- const subPkg = readPackageJsonIfExists(path.join(packageDir, "package.json"));
1141
- if (subPkg && hasOwnWebdriverioDependency(subPkg)) return true;
1142
- if (usesWebdriverioProvider(packageDir)) return true;
1143
- }
1144
- return false;
1145
- }
1146
- function readPackageJsonIfExists(packageJsonPath) {
1147
- if (!fs.existsSync(packageJsonPath)) return;
1148
- try {
1149
- return readJsonFile(packageJsonPath);
1150
- } catch {
1151
- return;
1152
- }
1153
- }
1154
- function pnpmMajor(version) {
1155
- const coerced = version ? import_semver.default.coerce(version)?.version : void 0;
1156
- return coerced ? import_semver.default.major(coerced) : void 0;
1157
- }
1158
- function applyBuildAllowanceToPackageJsonPnpm(pnpm, major, shouldAllow) {
1159
- if (major >= 10) {
1160
- if (shouldAllow) for (const name of BROWSER_PROVIDER_POSTINSTALL_PACKAGES) (pnpm.allowBuilds ??= {})[name] = true;
1161
- } else if (shouldAllow) {
1162
- const list = pnpm.onlyBuiltDependencies ?? [];
1163
- const existing = new Set(list);
1164
- for (const name of BROWSER_PROVIDER_POSTINSTALL_PACKAGES) if (!existing.has(name)) {
1165
- list.push(name);
1166
- existing.add(name);
1167
- }
1168
- pnpm.onlyBuiltDependencies = list;
1169
- }
1170
- }
1171
- function applyBuildAllowanceToWorkspaceYaml(doc, major, shouldAllow) {
1172
- if (major >= 10) {
1173
- if (shouldAllow) {
1174
- const existing = doc.getIn(["allowBuilds"]);
1175
- const isNew = !(existing instanceof import_dist.YAMLMap);
1176
- const allowBuilds = isNew ? new import_dist.YAMLMap() : existing;
1177
- for (const name of BROWSER_PROVIDER_POSTINSTALL_PACKAGES) allowBuilds.set(scalarString(name), new import_dist.Scalar(true));
1178
- if (isNew) doc.setIn(["allowBuilds"], allowBuilds);
1179
- }
1180
- } else if (shouldAllow) {
1181
- let onlyBuiltDependencies = doc.getIn(["onlyBuiltDependencies"]);
1182
- if (!(onlyBuiltDependencies instanceof import_dist.YAMLSeq)) onlyBuiltDependencies = new import_dist.YAMLSeq();
1183
- const existing = new Set(onlyBuiltDependencies.items.map((n) => n.value));
1184
- for (const name of BROWSER_PROVIDER_POSTINSTALL_PACKAGES) if (!existing.has(name)) onlyBuiltDependencies.add(scalarString(name));
1185
- doc.setIn(["onlyBuiltDependencies"], onlyBuiltDependencies);
1186
- }
1187
- }
1188
- /**
1189
- * Remove only Vite-managed entries from peerDependencyRules, preserving custom ones.
1190
- */
1191
- function cleanupPeerDependencyRules(peerDependencyRules, overrideKeys) {
1192
- if (!peerDependencyRules) return;
1193
- if (Array.isArray(peerDependencyRules.allowAny)) {
1194
- peerDependencyRules.allowAny = peerDependencyRules.allowAny.filter((key) => !overrideKeys.includes(key));
1195
- if (peerDependencyRules.allowAny.length === 0) delete peerDependencyRules.allowAny;
1196
- }
1197
- if (peerDependencyRules.allowedVersions) {
1198
- for (const key of overrideKeys) delete peerDependencyRules.allowedVersions[key];
1199
- if (Object.keys(peerDependencyRules.allowedVersions).length === 0) delete peerDependencyRules.allowedVersions;
1200
- }
1201
- }
1202
- /**
1203
- * Rewrite .yarnrc.yml to add vite-plus dependencies
1204
- * @param projectPath - The path to the project
1205
- */
1206
- function readYarnrcValue(dir, key) {
1207
- const yarnrcYmlPath = path.join(dir, ".yarnrc.yml");
1208
- if (!fs.existsSync(yarnrcYmlPath)) return;
1209
- try {
1210
- const value = readYamlFile(yarnrcYmlPath)?.[key];
1211
- return typeof value === "string" ? value : void 0;
1212
- } catch {
1213
- return;
1214
- }
1215
- }
1216
- function resolveEffectiveYarnConfigValue(workspaceRootDir, key, envVar) {
1217
- const fromEnv = process.env[envVar]?.trim();
1218
- if (fromEnv) return fromEnv;
1219
- let dir = path.resolve(workspaceRootDir);
1220
- for (;;) {
1221
- const value = readYarnrcValue(dir, key);
1222
- if (value !== void 0) return value;
1223
- const parent = path.dirname(dir);
1224
- if (parent === dir) break;
1225
- dir = parent;
1226
- }
1227
- const home = os.homedir();
1228
- return home ? readYarnrcValue(home, key) : void 0;
1229
- }
1230
- function dirIsWorkspaceRoot(dir) {
1231
- const pkgJsonPath = path.join(dir, "package.json");
1232
- if (!fs.existsSync(pkgJsonPath)) return false;
1233
- try {
1234
- return readJsonFile(pkgJsonPath).workspaces != null;
1235
- } catch {
1236
- return false;
1237
- }
1238
- }
1239
- function findYarnWorkspaceHoisting(startDir) {
1240
- let dir = path.resolve(startDir);
1241
- for (;;) {
1242
- if (dirIsWorkspaceRoot(dir)) return {
1243
- rootDir: dir,
1244
- limit: resolveEffectiveYarnConfigValue(dir, "nmHoistingLimits", "YARN_NM_HOISTING_LIMITS"),
1245
- nodeLinker: resolveEffectiveYarnConfigValue(dir, "nodeLinker", "YARN_NODE_LINKER")
1246
- };
1247
- const parent = path.dirname(dir);
1248
- if (parent === dir) return;
1249
- dir = parent;
1250
- }
1251
- }
1252
- function setYarnWorkspaceHoistingOptOut(pkg) {
1253
- if (pkg.installConfig?.hoistingLimits !== void 0) return;
1254
- pkg.installConfig = {
1255
- ...pkg.installConfig,
1256
- hoistingLimits: "none"
1257
- };
1258
- }
1259
- function applyYarnWorkspaceHoistingFix(pkg, rootLimit, nodeLinker, workspaceLabel, report) {
1260
- if (nodeLinker !== "node-modules") return;
1261
- if (rootLimit === "workspaces" && pkg.installConfig?.hoistingLimits === void 0) {
1262
- setYarnWorkspaceHoistingOptOut(pkg);
1263
- return;
1264
- }
1265
- const explicit = pkg.installConfig?.hoistingLimits;
1266
- if (rootLimit === "dependencies" || explicit === "workspaces" || explicit === "dependencies") warnMigration(`Yarn workspace "${workspaceLabel}" isolates dependency hoisting (hoistingLimits: ${explicit ?? rootLimit}), so it keeps its own \`vitest\`/\`vite-plus\` copy and \`vp test\` may crash with a split \`@vitest/runner\`. Dedupe them to a single copy — relax this workspace's hoisting isolation or pin one \`vitest\` for the workspace.`, report);
1267
- }
1268
- function rewriteYarnrcYml(projectPath) {
1269
- const yarnrcYmlPath = path.join(projectPath, ".yarnrc.yml");
1270
- if (!fs.existsSync(yarnrcYmlPath)) fs.writeFileSync(yarnrcYmlPath, "");
1271
- editYamlFile(yarnrcYmlPath, (doc) => {
1272
- if (!doc.has("nodeLinker")) doc.set("nodeLinker", "node-modules");
1273
- let npmPreapprovedPackages = doc.getIn(["npmPreapprovedPackages"]);
1274
- if (!npmPreapprovedPackages) npmPreapprovedPackages = new import_dist.YAMLSeq();
1275
- const existingPreapproved = new Set(npmPreapprovedPackages.items.map((n) => n.value));
1276
- for (const pkg of VITEST_AGE_GATE_EXEMPT_PACKAGES) if (!existingPreapproved.has(pkg)) npmPreapprovedPackages.add(scalarString(pkg));
1277
- doc.setIn(["npmPreapprovedPackages"], npmPreapprovedPackages);
1278
- rewriteCatalog(doc);
1279
- });
1280
- }
1281
- /**
1282
- * Rewrite catalog in pnpm-workspace.yaml or .yarnrc.yml
1283
- * @param doc - The document to rewrite
1284
- */
1285
- function getCatalogDependencySpec(currentValue, version, supportCatalog, options) {
1286
- if (options?.dependencyField === "peerDependencies") {
1287
- if (currentValue?.startsWith("catalog:") && options.dependencyName) {
1288
- const resolved = options.catalogDependencyResolver?.(currentValue, options.dependencyName);
1289
- if (resolved && !isVitePlusOverrideSpec(resolved)) return resolved;
1290
- return PUBLIC_PEER_DEPENDENCY_FALLBACKS[options.dependencyName] ?? currentValue;
1291
- }
1292
- return currentValue ?? version;
1293
- }
1294
- if (options?.dependencyField === "optionalDependencies" && options?.packageManager === PackageManager.yarn) return version;
1295
- if (!supportCatalog || version.startsWith("file:")) return version;
1296
- return currentValue?.startsWith("catalog:") ? currentValue : "catalog:";
1297
- }
1298
- function isVitePlusOverrideSpec(value) {
1299
- return Object.values(VITE_PLUS_OVERRIDE_PACKAGES).includes(value) || value.startsWith("npm:@voidzero-dev/vite-plus-");
1300
- }
1301
- function createCatalogDependencyResolver(projectPath, packageManager) {
1302
- if (packageManager === PackageManager.pnpm) {
1303
- const pnpmWorkspaceYamlPath = path.join(projectPath, "pnpm-workspace.yaml");
1304
- if (!fs.existsSync(pnpmWorkspaceYamlPath)) return;
1305
- const doc = readYamlFile(pnpmWorkspaceYamlPath);
1306
- return createCatalogDependencyResolverFromCatalogs(doc?.catalog, doc?.catalogs);
1307
- }
1308
- if (packageManager === PackageManager.yarn) {
1309
- const yarnrcYmlPath = path.join(projectPath, ".yarnrc.yml");
1310
- if (!fs.existsSync(yarnrcYmlPath)) return;
1311
- const doc = readYamlFile(yarnrcYmlPath);
1312
- return createCatalogDependencyResolverFromCatalogs(doc?.catalog, doc?.catalogs);
1313
- }
1314
- if (packageManager === PackageManager.bun) {
1315
- const packageJsonPath = path.join(projectPath, "package.json");
1316
- if (!fs.existsSync(packageJsonPath)) return;
1317
- const pkg = readJsonFile(packageJsonPath);
1318
- const workspacesObj = pkg.workspaces && !Array.isArray(pkg.workspaces) ? pkg.workspaces : void 0;
1319
- const fromWorkspaces = createCatalogDependencyResolverFromCatalogs(workspacesObj?.catalog, workspacesObj?.catalogs);
1320
- const fromPkg = createCatalogDependencyResolverFromCatalogs(pkg.catalog, pkg.catalogs);
1321
- return (catalogSpec, dependencyName) => fromWorkspaces(catalogSpec, dependencyName) ?? fromPkg(catalogSpec, dependencyName);
1322
- }
1323
- }
1324
- function createCatalogDependencyResolverFromCatalogs(catalog, catalogs) {
1325
- return (catalogSpec, dependencyName) => {
1326
- const catalogName = catalogSpec.slice(8);
1327
- if (catalogName && catalogName !== "default") return catalogs?.[catalogName]?.[dependencyName];
1328
- return catalog?.[dependencyName];
1329
- };
1330
- }
1331
- function getYamlMapScalarStringValue(map, key) {
1332
- if (!(map instanceof import_dist.YAMLMap)) return;
1333
- for (const item of map.items) if (item.key instanceof import_dist.Scalar && item.key.value === key && item.value instanceof import_dist.Scalar && typeof item.value.value === "string") return item.value.value;
1334
- }
1335
- function pruneYamlMapLegacyWrapperAliases(map) {
1336
- if (!(map instanceof import_dist.YAMLMap)) return;
1337
- const stale = [];
1338
- for (const item of map.items) {
1339
- const value = item.value instanceof import_dist.Scalar ? item.value.value : void 0;
1340
- if (typeof value === "string" && isLegacyWrapperSpec(value) && item.key instanceof import_dist.Scalar) stale.push({
1341
- key: item.key,
1342
- fallback: LEGACY_WRAPPER_FALLBACK_VERSIONS[item.key.value]
1343
- });
1344
- }
1345
- for (const { key, fallback } of stale) if (fallback !== void 0) map.set(key, scalarString(fallback));
1346
- else map.delete(key);
1347
- }
1348
- function rewriteCatalog(doc) {
1349
- for (const [key, value] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) {
1350
- if (value.startsWith("file:")) continue;
1351
- doc.setIn(["catalog", key], scalarString(value));
1352
- }
1353
- if (!VITE_PLUS_VERSION.startsWith("file:")) doc.setIn(["catalog", VITE_PLUS_NAME], scalarString(VITE_PLUS_VERSION));
1354
- for (const name of REMOVE_PACKAGES) {
1355
- const path = ["catalog", name];
1356
- if (doc.hasIn(path)) doc.deleteIn(path);
1357
- }
1358
- pruneYamlMapLegacyWrapperAliases(doc.getIn(["catalog"]));
1359
- const catalogs = doc.getIn(["catalogs"]);
1360
- if (!(catalogs instanceof import_dist.YAMLMap)) return;
1361
- for (const item of catalogs.items) {
1362
- const catalogName = item.key instanceof import_dist.Scalar ? item.key.value : void 0;
1363
- if (typeof catalogName !== "string" || !(item.value instanceof import_dist.YAMLMap)) continue;
1364
- for (const [key, value] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) {
1365
- const catalogPath = [
1366
- "catalogs",
1367
- catalogName,
1368
- key
1369
- ];
1370
- if (!value.startsWith("file:") && doc.hasIn(catalogPath)) doc.setIn(catalogPath, scalarString(value));
1371
- }
1372
- const vitePlusPath = [
1373
- "catalogs",
1374
- catalogName,
1375
- VITE_PLUS_NAME
1376
- ];
1377
- if (!VITE_PLUS_VERSION.startsWith("file:") && doc.hasIn(vitePlusPath)) doc.setIn(vitePlusPath, scalarString(VITE_PLUS_VERSION));
1378
- for (const name of REMOVE_PACKAGES) {
1379
- const catalogPath = [
1380
- "catalogs",
1381
- catalogName,
1382
- name
1383
- ];
1384
- if (doc.hasIn(catalogPath)) doc.deleteIn(catalogPath);
1385
- }
1386
- pruneYamlMapLegacyWrapperAliases(item.value);
1387
- }
1388
- }
1389
- function rewriteCatalogObject(catalog, addMissing) {
1390
- for (const [key, value] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) {
1391
- if (value.startsWith("file:") || !addMissing && !(key in catalog)) continue;
1392
- catalog[key] = value;
1393
- }
1394
- if (!VITE_PLUS_VERSION.startsWith("file:") && (addMissing || "vite-plus" in catalog)) catalog[VITE_PLUS_NAME] = VITE_PLUS_VERSION;
1395
- for (const name of REMOVE_PACKAGES) delete catalog[name];
1396
- }
1397
- function rewriteCatalogsObject(catalogs) {
1398
- for (const catalog of Object.values(catalogs)) rewriteCatalogObject(catalog, false);
1399
- }
1400
- /**
1401
- * Bun rejects vitest@4.1.9's `vite^6/^7/^8` peer-dep when the user's project
1402
- * overrides `vite` to `@voidzero-dev/vite-plus-core` (whose package.json version
1403
- * does not match those ranges). pnpm/yarn/npm all tolerate this redirect; bun
1404
- * does not, and there is no `peerDependencyRules`-style escape hatch — only the
1405
- * `[install] peer = false` setting in `bunfig.toml`.
1406
- *
1407
- * `vite-plus`/`@voidzero-dev/vite-plus-core` already provide the vite surface
1408
- * the user needs, so disabling bun's auto-install of *missing* peers is safe in
1409
- * this configuration: any vitest peer that's not already pulled in transitively
1410
- * (jsdom, happy-dom, etc.) is marked optional upstream anyway.
1411
- *
1412
- * Writes/merges `bunfig.toml` at `projectPath` so the suppression applies on
1413
- * the migration's reinstall AND every subsequent `bun install` the user runs.
1414
- */
1415
- function ensureBunfigPeerSuppression(projectPath) {
1416
- const bunfigPath = path.join(projectPath, "bunfig.toml");
1417
- const block = "[install]\npeer = false\n";
1418
- if (!fs.existsSync(bunfigPath)) {
1419
- fs.writeFileSync(bunfigPath, block);
1420
- return;
1421
- }
1422
- const existing = fs.readFileSync(bunfigPath, "utf8");
1423
- if (/^\s*peer\s*=\s*(true|false)\s*$/m.test(existing)) return;
1424
- const installSectionRe = /^\[install\][^[]*/m;
1425
- const next = installSectionRe.test(existing) ? existing.replace(installSectionRe, (section) => `${section.trimEnd()}\npeer = false\n`) : `${existing.trimEnd()}\n\n${block}`;
1426
- fs.writeFileSync(bunfigPath, next);
1427
- }
1428
- /**
1429
- * Write catalog entries to root package.json for bun.
1430
- * Bun stores catalogs in package.json under the `catalog` key,
1431
- * unlike pnpm which uses pnpm-workspace.yaml.
1432
- * @see https://bun.sh/docs/pm/catalogs
1433
- */
1434
- function rewriteBunCatalog(projectPath) {
1435
- const packageJsonPath = path.join(projectPath, "package.json");
1436
- if (!fs.existsSync(packageJsonPath)) return;
1437
- editJsonFile(packageJsonPath, (pkg) => {
1438
- const workspacesObj = pkg.workspaces && !Array.isArray(pkg.workspaces) ? pkg.workspaces : void 0;
1439
- const useWorkspacesCatalog = workspacesObj?.catalog != null || pkg.catalog == null && workspacesObj?.catalogs != null;
1440
- const catalog = { ...useWorkspacesCatalog ? workspacesObj?.catalog : pkg.catalog };
1441
- rewriteCatalogObject(catalog, true);
1442
- pruneLegacyWrapperAliases(catalog);
1443
- if (useWorkspacesCatalog) {
1444
- workspacesObj.catalog = catalog;
1445
- if (pkg.catalog) {
1446
- rewriteCatalogObject(pkg.catalog, false);
1447
- pruneLegacyWrapperAliases(pkg.catalog);
1448
- }
1449
- } else {
1450
- pkg.catalog = catalog;
1451
- if (workspacesObj?.catalog) {
1452
- rewriteCatalogObject(workspacesObj.catalog, false);
1453
- pruneLegacyWrapperAliases(workspacesObj.catalog);
1454
- }
1455
- }
1456
- if (workspacesObj?.catalogs) {
1457
- rewriteCatalogsObject(workspacesObj.catalogs);
1458
- for (const named of Object.values(workspacesObj.catalogs)) pruneLegacyWrapperAliases(named);
1459
- }
1460
- if (pkg.catalogs) {
1461
- rewriteCatalogsObject(pkg.catalogs);
1462
- for (const named of Object.values(pkg.catalogs)) pruneLegacyWrapperAliases(named);
1463
- }
1464
- const overrides = { ...pkg.overrides };
1465
- pruneLegacyWrapperAliases(overrides);
1466
- for (const [key, value] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) {
1467
- const current = overrides[key];
1468
- if (current !== void 0 && typeof current !== "string") continue;
1469
- overrides[key] = getCatalogDependencySpec(current, value, true);
1470
- }
1471
- pkg.overrides = overrides;
1472
- return pkg;
1473
- });
1474
- ensureBunfigPeerSuppression(projectPath);
1475
- }
1476
- /**
1477
- * Rewrite root workspace package.json to add vite-plus dependencies
1478
- * @param projectPath - The path to the project
1479
- */
1480
- function rewriteRootWorkspacePackageJson(projectPath, packageManager, skipStagedMigration, catalogDependencyResolver, packages, pnpmMajorVersion, shouldAllowBrowserBuilds = false) {
1481
- const packageJsonPath = path.join(projectPath, "package.json");
1482
- if (!fs.existsSync(packageJsonPath)) return;
1483
- let remainingPnpmOverrides;
1484
- editJsonFile(packageJsonPath, (pkg) => {
1485
- pruneLegacyWrapperAliases(pkg.resolutions);
1486
- pruneLegacyWrapperAliases(pkg.overrides);
1487
- pruneLegacyWrapperAliases(pkg.pnpm?.overrides);
1488
- dropRemovePackageOverrideKeys(pkg.resolutions);
1489
- dropRemovePackageOverrideKeys(pkg.overrides);
1490
- if (packageManager === PackageManager.yarn) pkg.resolutions = {
1491
- ...pkg.resolutions,
1492
- ...VITE_PLUS_OVERRIDE_PACKAGES
1493
- };
1494
- else if (packageManager === PackageManager.npm) pkg.overrides = {
1495
- ...pkg.overrides,
1496
- ...VITE_PLUS_OVERRIDE_PACKAGES
1497
- };
1498
- else if (packageManager === PackageManager.bun) pkg.devDependencies = {
1499
- ...pkg.devDependencies,
1500
- vite: getCatalogDependencySpec(pkg.devDependencies?.vite, VITE_PLUS_OVERRIDE_PACKAGES.vite, true)
1501
- };
1502
- else if (packageManager === PackageManager.pnpm) {
1503
- const overrideKeys = Object.keys(VITE_PLUS_OVERRIDE_PACKAGES);
1504
- if (isForceOverrideMode()) {
1505
- dropRemovePackageOverrideKeys(pkg.pnpm?.overrides);
1506
- pkg.pnpm = {
1507
- ...pkg.pnpm,
1508
- overrides: {
1509
- ...pkg.pnpm?.overrides,
1510
- ...VITE_PLUS_OVERRIDE_PACKAGES,
1511
- [VITE_PLUS_NAME]: VITE_PLUS_VERSION
1512
- }
1513
- };
1514
- } else {
1515
- for (const key of [...overrideKeys, ...PROVIDER_OVERRIDE_DROP_NAMES]) if (pkg.resolutions?.[key]) delete pkg.resolutions[key];
1516
- remainingPnpmOverrides = cleanupPnpmOverridesForWorkspaceYaml(pkg, overrideKeys);
1517
- }
1518
- for (const key in pkg.pnpm?.overrides) if (key.includes(">")) {
1519
- const splits = key.split(">");
1520
- if (splits[splits.length - 1].trim() === "vite") delete pkg.pnpm.overrides[key];
1521
- }
1522
- if (pnpmMajorVersion !== void 0 && pkg.pnpm) applyBuildAllowanceToPackageJsonPnpm(pkg.pnpm, pnpmMajorVersion, shouldAllowBrowserBuilds);
1523
- }
1524
- if (!pkg.devDependencies?.["vite-plus"]) pkg.devDependencies = {
1525
- ...pkg.devDependencies,
1526
- [VITE_PLUS_NAME]: packageManager === PackageManager.npm || VITE_PLUS_VERSION.startsWith("file:") ? VITE_PLUS_VERSION : "catalog:"
1527
- };
1528
- return pkg;
1529
- });
1530
- if (remainingPnpmOverrides) migratePnpmOverridesToWorkspaceYaml(projectPath, remainingPnpmOverrides);
1531
- rewriteMonorepoProject(projectPath, packageManager, skipStagedMigration, void 0, void 0, catalogDependencyResolver, packages ? {
1532
- rootDir: projectPath,
1533
- packages
1534
- } : void 0, true);
1535
- }
1536
- const RULES_YAML_PATH = path.join(rulesDir, "vite-tools.yml");
1537
- const PREPARE_RULES_YAML_PATH = path.join(rulesDir, "vite-prepare.yml");
1538
- let cachedRulesYaml;
1539
- let cachedRulesYamlNoLintStaged;
1540
- let cachedPrepareRulesYaml;
1541
- function readRulesYaml() {
1542
- cachedRulesYaml ??= fs.readFileSync(RULES_YAML_PATH, "utf8");
1543
- return cachedRulesYaml;
1544
- }
1545
- function getScriptRulesYaml(skipStagedMigration) {
1546
- const yaml = readRulesYaml();
1547
- if (!skipStagedMigration) return yaml;
1548
- cachedRulesYamlNoLintStaged ??= yaml.split("\n\n\n").filter((block) => !block.includes("id: replace-lint-staged")).join("\n\n\n");
1549
- return cachedRulesYamlNoLintStaged;
1550
- }
1551
- function readPrepareRulesYaml() {
1552
- cachedPrepareRulesYaml ??= fs.readFileSync(PREPARE_RULES_YAML_PATH, "utf8");
1553
- return cachedPrepareRulesYaml;
1554
- }
1555
- function getCoreMigrationProjectPaths(workspaceInfo) {
1556
- return [workspaceInfo.rootDir, ...(workspaceInfo.packages ?? []).map((pkg) => path.join(workspaceInfo.rootDir, pkg.path))];
1557
- }
1558
- function hasCorePackageScriptRewrites(projectPath) {
1559
- const packageJsonPath = path.join(projectPath, "package.json");
1560
- if (!fs.existsSync(packageJsonPath)) return false;
1561
- const pkg = readJsonFile(packageJsonPath);
1562
- if (!pkg.scripts) return false;
1563
- return !!rewriteScripts(JSON.stringify(pkg.scripts), getScriptRulesYaml(true));
1564
- }
1565
- function rewriteCorePackageScripts(projectPath) {
1566
- const packageJsonPath = path.join(projectPath, "package.json");
1567
- if (!fs.existsSync(packageJsonPath)) return false;
1568
- let changed = false;
1569
- editJsonFile(packageJsonPath, (pkg) => {
1570
- if (!pkg.scripts) return;
1571
- const updated = rewriteScripts(JSON.stringify(pkg.scripts), getScriptRulesYaml(true));
1572
- if (!updated) return;
1573
- pkg.scripts = JSON.parse(updated);
1574
- changed = true;
1575
- return pkg;
1576
- });
1577
- return changed;
1578
- }
1579
- function detectPendingCoreMigration(workspaceInfo) {
1580
- const projectPaths = getCoreMigrationProjectPaths(workspaceInfo);
1581
- return {
1582
- scripts: projectPaths.some((projectPath) => hasCorePackageScriptRewrites(projectPath)),
1583
- tsconfigTypes: projectPaths.some((projectPath) => hasTsconfigTypesToRewrite(projectPath))
1584
- };
1585
- }
1586
- function finalizeCoreMigrationForExistingVitePlus(workspaceInfo, silent = false, report, pending = detectPendingCoreMigration(workspaceInfo)) {
1587
- const projectPaths = getCoreMigrationProjectPaths(workspaceInfo);
1588
- const result = {
1589
- scripts: false,
1590
- tsconfigTypes: false,
1591
- imports: false
1592
- };
1593
- if (pending.scripts) for (const projectPath of projectPaths) result.scripts = rewriteCorePackageScripts(projectPath) || result.scripts;
1594
- if (pending.tsconfigTypes) for (const projectPath of projectPaths) result.tsconfigTypes = rewriteTsconfigTypes(projectPath, silent, report) || result.tsconfigTypes;
1595
- result.imports = rewriteAllImports(workspaceInfo.rootDir, silent, report);
1596
- return result;
1597
- }
1598
- function getVitePlusOverridePackageName(dependencyName) {
1599
- if (dependencyName === "vite") return "@voidzero-dev/vite-plus-core";
1600
- if (dependencyName === "vitest") return "@voidzero-dev/vite-plus-test";
1601
- }
1602
- function isSemanticVitePlusOverrideSpec(dependencyName, spec) {
1603
- if (!spec) return false;
1604
- if (isLegacyWrapperSpec(spec)) return false;
1605
- if (spec === VITE_PLUS_OVERRIDE_PACKAGES[dependencyName]) return true;
1606
- const packageName = getVitePlusOverridePackageName(dependencyName);
1607
- return packageName !== void 0 && spec.includes(packageName);
1608
- }
1609
- function overrideSpecSatisfiesVitePlus(dependencyName, spec, catalogDependencyResolver) {
1610
- if (!spec) return false;
1611
- if (isSemanticVitePlusOverrideSpec(dependencyName, spec)) return true;
1612
- if (!spec.startsWith("catalog:")) return false;
1613
- return isSemanticVitePlusOverrideSpec(dependencyName, catalogDependencyResolver?.(spec, dependencyName));
1614
- }
1615
- function overridesSatisfyVitePlus(overrides, catalogDependencyResolver) {
1616
- return Object.keys(VITE_PLUS_OVERRIDE_PACKAGES).every((dependencyName) => overrideSpecSatisfiesVitePlus(dependencyName, overrides?.[dependencyName], catalogDependencyResolver));
1617
- }
1618
- function hasPackageManagerPin(pkg) {
1619
- return Boolean(pkg.packageManager || pkg.devEngines?.packageManager);
1620
- }
1621
- function vitePlusDependencyNeedsConcreteVersion(pkg) {
1622
- return [
1623
- pkg.devDependencies,
1624
- pkg.dependencies,
1625
- pkg.optionalDependencies
1626
- ].some((dependencies) => dependencies?.["vite-plus"]?.startsWith("catalog:") ?? false);
1627
- }
1628
- function defaultCatalogVitePlusDependencyPending(pkg, catalogDependencyResolver) {
1629
- return [
1630
- pkg.devDependencies,
1631
- pkg.dependencies,
1632
- pkg.optionalDependencies
1633
- ].some((dependencies) => {
1634
- const spec = dependencies?.[VITE_PLUS_NAME];
1635
- if (spec !== "catalog:" && spec !== "catalog:default") return false;
1636
- return catalogDependencyResolver?.(spec, VITE_PLUS_NAME) !== VITE_PLUS_VERSION;
1637
- });
1638
- }
1639
- function pnpmPeerDependencyRulesSatisfyVitePlus(peerDependencyRules) {
1640
- const overrideKeys = Object.keys(VITE_PLUS_OVERRIDE_PACKAGES);
1641
- const allowAny = new Set(peerDependencyRules?.allowAny ?? []);
1642
- const allowedVersions = peerDependencyRules?.allowedVersions ?? {};
1643
- return overrideKeys.every((key) => allowAny.has(key) && allowedVersions[key] === "*");
1644
- }
1645
- function npmVitePlusManagedDependenciesPending(pkg) {
1646
- const dependencyGroups = [
1647
- pkg.devDependencies,
1648
- pkg.dependencies,
1649
- pkg.optionalDependencies
1650
- ];
1651
- return Object.keys(VITE_PLUS_OVERRIDE_PACKAGES).some((dependencyName) => dependencyGroups.some((dependencies) => dependencies?.[dependencyName] !== void 0 && !overrideSpecSatisfiesVitePlus(dependencyName, dependencies[dependencyName])));
1652
- }
1653
- function readPnpmWorkspaceCatalogDependencyResolver(projectPath) {
1654
- const pnpmWorkspaceYamlPath = path.join(projectPath, "pnpm-workspace.yaml");
1655
- if (!fs.existsSync(pnpmWorkspaceYamlPath)) return;
1656
- const doc = readYamlFile(pnpmWorkspaceYamlPath);
1657
- return createCatalogDependencyResolverFromCatalogs(doc?.catalog, doc?.catalogs);
1658
- }
1659
- function readPnpmWorkspaceOverrides(projectPath) {
1660
- const pnpmWorkspaceYamlPath = path.join(projectPath, "pnpm-workspace.yaml");
1661
- if (!fs.existsSync(pnpmWorkspaceYamlPath)) return;
1662
- return readYamlFile(pnpmWorkspaceYamlPath)?.overrides;
1663
- }
1664
- function readPnpmWorkspacePeerDependencyRules(projectPath) {
1665
- const pnpmWorkspaceYamlPath = path.join(projectPath, "pnpm-workspace.yaml");
1666
- if (!fs.existsSync(pnpmWorkspaceYamlPath)) return;
1667
- return readYamlFile(pnpmWorkspaceYamlPath)?.peerDependencyRules;
1668
- }
1669
- function yarnrcSatisfiesVitePlus(projectPath) {
1670
- const yarnrcYmlPath = path.join(projectPath, ".yarnrc.yml");
1671
- if (!fs.existsSync(yarnrcYmlPath)) return false;
1672
- const doc = readYamlFile(yarnrcYmlPath);
1673
- return !!doc && Object.hasOwn(doc, "nodeLinker") && overridesSatisfyVitePlus(doc.catalog) && (VITE_PLUS_VERSION.startsWith("file:") || doc.catalog?.["vite-plus"] === VITE_PLUS_VERSION);
1674
- }
1675
- function ensurePnpmWorkspacePackages(projectPath, workspacePatterns) {
1676
- if (workspacePatterns.length === 0) return false;
1677
- const pnpmWorkspaceYamlPath = path.join(projectPath, "pnpm-workspace.yaml");
1678
- let changed = false;
1679
- editYamlFile(pnpmWorkspaceYamlPath, (doc) => {
1680
- if (doc.has("packages")) return;
1681
- const packages = new import_dist.YAMLSeq();
1682
- for (const pattern of workspacePatterns) packages.add(scalarString(pattern));
1683
- doc.set("packages", packages);
1684
- changed = true;
1685
- });
1686
- return changed;
1687
- }
1688
- function readBunCatalogDependencyResolver(pkg) {
1689
- const workspacesObj = pkg.workspaces && !Array.isArray(pkg.workspaces) ? pkg.workspaces : {};
1690
- const fromWorkspaces = createCatalogDependencyResolverFromCatalogs(workspacesObj.catalog, workspacesObj.catalogs);
1691
- const fromPkg = createCatalogDependencyResolverFromCatalogs(pkg.catalog, pkg.catalogs);
1692
- return (catalogSpec, dependencyName) => fromWorkspaces(catalogSpec, dependencyName) ?? fromPkg(catalogSpec, dependencyName);
1693
- }
1694
- function detectVitePlusBootstrapPending(projectPath, packageManager) {
1695
- const packageJsonPath = path.join(projectPath, "package.json");
1696
- if (!fs.existsSync(packageJsonPath)) return false;
1697
- const pkg = readJsonFile(packageJsonPath);
1698
- if (!pkg.devDependencies?.["vite-plus"] || !hasPackageManagerPin(pkg)) return true;
1699
- if (packageManager === void 0) return true;
1700
- if (packageManager === PackageManager.yarn) return !overridesSatisfyVitePlus(pkg.resolutions) || !yarnrcSatisfiesVitePlus(projectPath);
1701
- if (packageManager === PackageManager.npm) return vitePlusDependencyNeedsConcreteVersion(pkg) || !overridesSatisfyVitePlus(pkg.overrides) || npmVitePlusManagedDependenciesPending(pkg);
1702
- if (packageManager === PackageManager.bun) return !overridesSatisfyVitePlus(pkg.overrides, readBunCatalogDependencyResolver(pkg));
1703
- if (packageManager === PackageManager.pnpm) {
1704
- if (pkg.pnpm) return vitePlusDependencyNeedsConcreteVersion(pkg) || !overridesSatisfyVitePlus(pkg.pnpm.overrides) || !pnpmPeerDependencyRulesSatisfyVitePlus(pkg.pnpm.peerDependencyRules);
1705
- const resolver = readPnpmWorkspaceCatalogDependencyResolver(projectPath);
1706
- return defaultCatalogVitePlusDependencyPending(pkg, resolver) || !overridesSatisfyVitePlus(readPnpmWorkspaceOverrides(projectPath), resolver) || !pnpmPeerDependencyRulesSatisfyVitePlus(readPnpmWorkspacePeerDependencyRules(projectPath));
1707
- }
1708
- return false;
1709
- }
1710
- function ensureVitePlusDependencySpecs(pkg, version) {
1711
- let changed = false;
1712
- if (version !== "catalog:") {
1713
- const dependencyGroups = [
1714
- pkg.devDependencies,
1715
- pkg.dependencies,
1716
- pkg.optionalDependencies
1717
- ];
1718
- for (const dependencies of dependencyGroups) if (dependencies?.["vite-plus"]?.startsWith("catalog:")) {
1719
- dependencies[VITE_PLUS_NAME] = version;
1720
- changed = true;
1721
- }
1722
- }
1723
- if (pkg.devDependencies?.["vite-plus"]) return changed;
1724
- pkg.devDependencies = {
1725
- ...pkg.devDependencies,
1726
- [VITE_PLUS_NAME]: version
1727
- };
1728
- return true;
1729
- }
1730
- function ensureOverrideEntries(overrides, catalogDependencyResolver) {
1731
- const next = { ...overrides };
1732
- let changed = false;
1733
- for (const [dependencyName, overrideSpec] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) if (!overrideSpecSatisfiesVitePlus(dependencyName, next[dependencyName], catalogDependencyResolver)) {
1734
- next[dependencyName] = overrideSpec;
1735
- changed = true;
1736
- }
1737
- return {
1738
- overrides: next,
1739
- changed
1740
- };
1741
- }
1742
- function ensureNpmVitePlusManagedDependencies(pkg) {
1743
- let changed = false;
1744
- const dependencyGroups = [
1745
- pkg.devDependencies,
1746
- pkg.dependencies,
1747
- pkg.optionalDependencies
1748
- ];
1749
- for (const [dependencyName, version] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) for (const dependencies of dependencyGroups) if (dependencies?.[dependencyName] !== void 0 && !overrideSpecSatisfiesVitePlus(dependencyName, dependencies[dependencyName])) {
1750
- dependencies[dependencyName] = version;
1751
- changed = true;
1752
- }
1753
- return changed;
1754
- }
1755
- function ensurePnpmPeerDependencyRules(pkg) {
1756
- const overrideKeys = Object.keys(VITE_PLUS_OVERRIDE_PACKAGES);
1757
- pkg.pnpm ??= {};
1758
- const peerDependencyRules = {
1759
- ...pkg.pnpm.peerDependencyRules,
1760
- allowAny: [...new Set([...pkg.pnpm.peerDependencyRules?.allowAny ?? [], ...overrideKeys])],
1761
- allowedVersions: {
1762
- ...pkg.pnpm.peerDependencyRules?.allowedVersions,
1763
- ...Object.fromEntries(overrideKeys.map((key) => [key, "*"]))
1764
- }
1765
- };
1766
- const changed = JSON.stringify(pkg.pnpm.peerDependencyRules ?? {}) !== JSON.stringify(peerDependencyRules);
1767
- pkg.pnpm.peerDependencyRules = peerDependencyRules;
1768
- return changed;
1769
- }
1770
- function ensureVitePlusBootstrap(workspaceInfo, report) {
1771
- const projectPath = workspaceInfo.rootDir;
1772
- const packageJsonPath = path.join(projectPath, "package.json");
1773
- const result = {
1774
- changed: false,
1775
- packageJson: false,
1776
- packageManagerConfig: false,
1777
- packageManagerField: false
1778
- };
1779
- if (!fs.existsSync(packageJsonPath)) return result;
1780
- editJsonFile(packageJsonPath, (pkg) => {
1781
- const usePnpmWorkspaceYaml = workspaceInfo.packageManager === PackageManager.pnpm && !pkg.pnpm;
1782
- let packageJsonChanged = ensureVitePlusDependencySpecs(pkg, !VITE_PLUS_VERSION.startsWith("file:") && (usePnpmWorkspaceYaml || workspaceInfo.packageManager === PackageManager.bun) ? "catalog:" : VITE_PLUS_VERSION);
1783
- if (workspaceInfo.packageManager === PackageManager.npm) packageJsonChanged = ensureNpmVitePlusManagedDependencies(pkg) || packageJsonChanged;
1784
- if (workspaceInfo.packageManager === PackageManager.yarn) {
1785
- const ensured = ensureOverrideEntries(pkg.resolutions);
1786
- if (ensured.changed) {
1787
- pkg.resolutions = ensured.overrides;
1788
- packageJsonChanged = true;
1789
- }
1790
- } else if (workspaceInfo.packageManager === PackageManager.npm) {
1791
- const ensured = ensureOverrideEntries(pkg.overrides);
1792
- if (ensured.changed) {
1793
- pkg.overrides = ensured.overrides;
1794
- packageJsonChanged = true;
1795
- }
1796
- } else if (workspaceInfo.packageManager === PackageManager.bun) {
1797
- const ensured = ensureOverrideEntries(pkg.overrides, readBunCatalogDependencyResolver(pkg));
1798
- if (ensured.changed) {
1799
- pkg.overrides = ensured.overrides;
1800
- packageJsonChanged = true;
1801
- }
1802
- } else if (workspaceInfo.packageManager === PackageManager.pnpm && pkg.pnpm) {
1803
- const ensured = ensureOverrideEntries(pkg.pnpm.overrides);
1804
- if (ensured.changed) {
1805
- pkg.pnpm.overrides = ensured.overrides;
1806
- packageJsonChanged = true;
1807
- }
1808
- packageJsonChanged = ensurePnpmPeerDependencyRules(pkg) || packageJsonChanged;
1809
- }
1810
- result.packageJson = packageJsonChanged;
1811
- return pkg;
1812
- });
1813
- if (workspaceInfo.packageManager === PackageManager.pnpm) {
1814
- const pkg = readJsonFile(packageJsonPath);
1815
- if (!pkg.pnpm) {
1816
- const pnpmWorkspaceYamlPath = path.join(projectPath, "pnpm-workspace.yaml");
1817
- const before = fs.existsSync(pnpmWorkspaceYamlPath) ? fs.readFileSync(pnpmWorkspaceYamlPath, "utf-8") : void 0;
1818
- const catalogDependencyResolver = readPnpmWorkspaceCatalogDependencyResolver(projectPath);
1819
- if (defaultCatalogVitePlusDependencyPending(pkg, catalogDependencyResolver) || !overridesSatisfyVitePlus(readPnpmWorkspaceOverrides(projectPath), catalogDependencyResolver) || !pnpmPeerDependencyRulesSatisfyVitePlus(readPnpmWorkspacePeerDependencyRules(projectPath))) rewritePnpmWorkspaceYaml(projectPath, void 0, false);
1820
- if (fs.existsSync(pnpmWorkspaceYamlPath)) ensurePnpmWorkspacePackages(projectPath, workspaceInfo.workspacePatterns);
1821
- result.packageManagerConfig = before !== (fs.existsSync(pnpmWorkspaceYamlPath) ? fs.readFileSync(pnpmWorkspaceYamlPath, "utf-8") : void 0);
1822
- }
1823
- } else if (workspaceInfo.packageManager === PackageManager.yarn) {
1824
- const yarnrcYmlPath = path.join(projectPath, ".yarnrc.yml");
1825
- const before = fs.existsSync(yarnrcYmlPath) ? fs.readFileSync(yarnrcYmlPath, "utf-8") : void 0;
1826
- rewriteYarnrcYml(projectPath);
1827
- result.packageManagerConfig = before !== fs.readFileSync(yarnrcYmlPath, "utf-8");
1828
- } else if (workspaceInfo.packageManager === PackageManager.bun) {
1829
- const before = fs.readFileSync(packageJsonPath, "utf-8");
1830
- rewriteBunCatalog(projectPath);
1831
- const after = fs.readFileSync(packageJsonPath, "utf-8");
1832
- result.packageJson = result.packageJson || before !== after;
1833
- }
1834
- const beforePackageManager = fs.readFileSync(packageJsonPath, "utf-8");
1835
- setPackageManager(projectPath, workspaceInfo.downloadPackageManager);
1836
- result.packageManagerField = beforePackageManager !== fs.readFileSync(packageJsonPath, "utf-8");
1837
- result.changed = result.packageJson || result.packageManagerConfig || result.packageManagerField;
1838
- if (result.changed && report) report.packageManagerBootstrapConfigured = true;
1839
- return result;
1840
- }
1841
- const VITEST_BROWSER_SPECIFIER_HINTS = [
1842
- "@vitest/browser",
1843
- "vite-plus/test/browser",
1844
- "vite-plus/test/plugins/browser",
1845
- "vite-plus/test/internal/browser",
1846
- "vite-plus/test/client",
1847
- "vite-plus/test/context",
1848
- "vite-plus/test/locators",
1849
- "vite-plus/test/matchers",
1850
- "vite-plus/test/utils"
1851
- ];
1852
- const WEBDRIVERIO_PROVIDER_SPECIFIER_HINTS = [
1853
- "@vitest/browser-webdriverio",
1854
- "vite-plus/test/browser-webdriverio",
1855
- "vite-plus/test/browser/providers/webdriverio",
1856
- "vite-plus/test/plugins/browser-webdriverio"
1857
- ];
1858
- const PLAYWRIGHT_PROVIDER_SPECIFIER_HINTS = [
1859
- "@vitest/browser-playwright",
1860
- "vite-plus/test/browser-playwright",
1861
- "vite-plus/test/browser/providers/playwright",
1862
- "vite-plus/test/plugins/browser-playwright"
1863
- ];
1864
- const BROWSER_PROVIDER_SPECIFIER_HINTS = {
1865
- [WEBDRIVERIO_PROVIDER]: WEBDRIVERIO_PROVIDER_SPECIFIER_HINTS,
1866
- [PLAYWRIGHT_PROVIDER]: PLAYWRIGHT_PROVIDER_SPECIFIER_HINTS
1867
- };
1868
- const VITEST_SCAN_EXTENSIONS = new Set([
1869
- ".ts",
1870
- ".mts",
1871
- ".cts",
1872
- ".tsx",
1873
- ".js",
1874
- ".mjs",
1875
- ".cjs",
1876
- ".jsx"
1877
- ]);
1878
- const VITEST_SCAN_SKIP_DIRS = new Set([
1879
- "node_modules",
1880
- "dist",
1881
- "build",
1882
- "out",
1883
- "coverage",
1884
- ".git",
1885
- ".next",
1886
- ".nuxt",
1887
- ".svelte-kit",
1888
- ".vite",
1889
- ".cache"
1890
- ]);
1891
- /**
1892
- * Detect whether a package uses vitest's browser mode.
1893
- *
1894
- * Upstream `@vitest/browser` injects `optimizeDeps.include` entries of the form
1895
- * `vitest > expect-type` (and `vitest > @vitest/snapshot > magic-string`,
1896
- * `vitest > @vitest/expect > chai`). Vite resolves the leading `vitest` segment
1897
- * from the Vite config root, so `vitest` MUST be resolvable as a package from
1898
- * the consuming package's directory. In a pnpm strict (non-hoisted) layout,
1899
- * `vitest` pulled in only transitively via `vite-plus` is NOT reachable from the
1900
- * package root — the optimizer then fails with `Failed to resolve dependency`
1901
- * and the browser test page hangs forever.
1902
- *
1903
- * When this returns true the migration adds `vitest` as a direct
1904
- * devDependency so it is hoisted next to the package and the optimizer chain
1905
- * resolves. The signal is any of the package's TS/JS files (config, workspace
1906
- * config under any name, or test file) referencing `@vitest/browser*` or
1907
- * `vite-plus/test/browser*`. The scan recurses through the package directory
1908
- * (skipping `node_modules`, build output, VCS metadata) so browser config in a
1909
- * non-standard filename or browser imports in test files are all caught.
1910
- *
1911
- * Recursion stops at nested `package.json` boundaries: a workspace sub-package
1912
- * is a separate package that the migration scans on its own pass, so the root
1913
- * package must not inherit a browser-mode signal from a sub-package.
1914
- */
1915
- function sourceTreeReferencesAny(projectPath, hints) {
1916
- const matchesHint = (content) => hints.some((hint) => content.includes(hint));
1917
- const scanDir = (dir, isRoot) => {
1918
- let entries;
1919
- try {
1920
- entries = fs.readdirSync(dir, { withFileTypes: true });
1921
- } catch {
1922
- return false;
1923
- }
1924
- if (!isRoot && entries.some((e) => e.isFile() && e.name === "package.json")) return false;
1925
- for (const entry of entries) {
1926
- const entryPath = path.join(dir, entry.name);
1927
- if (entry.isDirectory()) {
1928
- if (VITEST_SCAN_SKIP_DIRS.has(entry.name)) continue;
1929
- if (scanDir(entryPath, false)) return true;
1930
- } else if (entry.isFile() && VITEST_SCAN_EXTENSIONS.has(path.extname(entry.name))) try {
1931
- if (matchesHint(fs.readFileSync(entryPath, "utf8"))) return true;
1932
- } catch {}
1933
- }
1934
- return false;
1935
- };
1936
- return scanDir(projectPath, true);
1937
- }
1938
- function usesVitestBrowserMode(projectPath) {
1939
- return sourceTreeReferencesAny(projectPath, VITEST_BROWSER_SPECIFIER_HINTS);
1940
- }
1941
- function usesWebdriverioProvider(projectPath) {
1942
- return sourceTreeReferencesAny(projectPath, WEBDRIVERIO_PROVIDER_SPECIFIER_HINTS);
1943
- }
1944
- function collectProviderSourceModes(projectPath) {
1945
- const modes = {};
1946
- for (const provider of OPT_IN_BROWSER_PROVIDERS) modes[provider] = sourceTreeReferencesAny(projectPath, BROWSER_PROVIDER_SPECIFIER_HINTS[provider]);
1947
- return modes;
1948
- }
1949
- function rewritePackageJson(pkg, packageManager, isMonorepo, skipStagedMigration, catalogDependencyResolver, vitestBrowserMode, providerSourceModes) {
1950
- if (pkg.scripts) {
1951
- const updated = rewriteScripts(JSON.stringify(pkg.scripts), getScriptRulesYaml(skipStagedMigration));
1952
- if (updated) pkg.scripts = JSON.parse(updated);
1953
- }
1954
- let extractedStagedConfig = null;
1955
- if (!skipStagedMigration && pkg["lint-staged"]) {
1956
- const config = pkg["lint-staged"];
1957
- const updated = rewriteScripts(JSON.stringify(config), readRulesYaml());
1958
- extractedStagedConfig = updated ? JSON.parse(updated) : config;
1959
- }
1960
- const supportCatalog = !!isMonorepo && packageManager !== PackageManager.npm;
1961
- let needVitePlus = false;
1962
- const dependencyGroups = [
1963
- {
1964
- dependencyField: "devDependencies",
1965
- dependencies: pkg.devDependencies
1966
- },
1967
- {
1968
- dependencyField: "dependencies",
1969
- dependencies: pkg.dependencies
1970
- },
1971
- {
1972
- dependencyField: "peerDependencies",
1973
- dependencies: pkg.peerDependencies
1974
- },
1975
- {
1976
- dependencyField: "optionalDependencies",
1977
- dependencies: pkg.optionalDependencies
1978
- }
1979
- ];
1980
- for (const { dependencies } of dependencyGroups) if (pruneLegacyWrapperAliases(dependencies)) needVitePlus = true;
1981
- for (const [key, version] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) for (const { dependencyField, dependencies } of dependencyGroups) if (dependencies?.[key]) {
1982
- dependencies[key] = getCatalogDependencySpec(dependencies[key], version, supportCatalog, {
1983
- dependencyField,
1984
- dependencyName: key,
1985
- packageManager,
1986
- catalogDependencyResolver
1987
- });
1988
- needVitePlus = true;
1989
- }
1990
- if (isForceOverrideMode()) {
1991
- for (const { dependencies } of dependencyGroups) if (dependencies?.["vite-plus"]) {
1992
- dependencies[VITE_PLUS_NAME] = VITE_PLUS_VERSION;
1993
- needVitePlus = true;
1994
- }
1995
- }
1996
- const hasBrowserDepSignal = VITEST_BROWSER_DEP_NAMES.some((name) => dependencyGroups.some(({ dependencies }) => dependencies?.[name] !== void 0));
1997
- for (const name of REMOVE_PACKAGES) {
1998
- let wasRemoved = false;
1999
- for (const { dependencies } of dependencyGroups) if (dependencies?.[name]) {
2000
- delete dependencies[name];
2001
- wasRemoved = true;
2002
- }
2003
- if (wasRemoved) needVitePlus = true;
2004
- const peerDep = BROWSER_PROVIDER_PEER_DEPS[name];
2005
- if (wasRemoved && peerDep && !pkg.devDependencies?.[peerDep] && !pkg.dependencies?.[peerDep] && !pkg.peerDependencies?.[peerDep] && !pkg.optionalDependencies?.[peerDep]) {
2006
- pkg.devDependencies ??= {};
2007
- pkg.devDependencies[peerDep] = "*";
2008
- }
2009
- }
2010
- let usesAnyOptInProvider = false;
2011
- for (const provider of OPT_IN_BROWSER_PROVIDERS) {
2012
- if (!(providerSourceModes?.[provider] || dependencyGroups.some(({ dependencies }) => dependencies?.[provider] !== void 0))) continue;
2013
- usesAnyOptInProvider = true;
2014
- const installGroup = [
2015
- pkg.dependencies,
2016
- pkg.devDependencies,
2017
- pkg.optionalDependencies
2018
- ].find((deps) => deps?.[provider] !== void 0);
2019
- if (installGroup) installGroup[provider] = VITEST_VERSION;
2020
- else {
2021
- pkg.devDependencies ??= {};
2022
- pkg.devDependencies[provider] = VITEST_VERSION;
2023
- }
2024
- const peer = BROWSER_PROVIDER_PEER_DEPS[provider];
2025
- const peerPresent = pkg.dependencies?.[peer] ?? pkg.devDependencies?.[peer] ?? pkg.peerDependencies?.[peer] ?? pkg.optionalDependencies?.[peer];
2026
- if (peer && !peerPresent) {
2027
- pkg.devDependencies ??= {};
2028
- pkg.devDependencies[peer] = "*";
2029
- }
2030
- needVitePlus = true;
2031
- }
2032
- if (usesAnyOptInProvider && packageManager === PackageManager.npm) {
2033
- const viteOverride = VITE_PLUS_OVERRIDE_PACKAGES.vite;
2034
- const viteAlreadyDirect = pkg.dependencies?.vite ?? pkg.devDependencies?.vite ?? pkg.optionalDependencies?.vite;
2035
- if (viteOverride && !viteAlreadyDirect) {
2036
- pkg.devDependencies ??= {};
2037
- pkg.devDependencies.vite = viteOverride;
2038
- needVitePlus = true;
2039
- }
2040
- }
2041
- const effectiveBrowserMode = vitestBrowserMode || hasBrowserDepSignal;
2042
- const installableNames = [
2043
- ...Object.keys(pkg.dependencies ?? {}),
2044
- ...Object.keys(pkg.devDependencies ?? {}),
2045
- ...Object.keys(pkg.optionalDependencies ?? {})
2046
- ];
2047
- const adjacentSignals = [...installableNames, ...Object.keys(pkg.peerDependencies ?? {})];
2048
- const isVitestAdjacent = !installableNames.includes("vitest") && adjacentSignals.some((name) => name !== "vitest" && name.includes("vitest"));
2049
- const canonicalVitePlusSpec = supportCatalog && !VITE_PLUS_VERSION.startsWith("file:") ? "catalog:" : VITE_PLUS_VERSION;
2050
- const existingVitePlus = pkg.devDependencies?.[VITE_PLUS_NAME];
2051
- const shouldNormalizeExistingVitePlus = !!existingVitePlus && supportCatalog && existingVitePlus !== canonicalVitePlusSpec && !isProtocolPinnedSpec(existingVitePlus);
2052
- if (!existingVitePlus && (isVitestAdjacent || effectiveBrowserMode)) needVitePlus = true;
2053
- const needDirectVitest = needVitePlus || effectiveBrowserMode || isVitestAdjacent;
2054
- if (needVitePlus || shouldNormalizeExistingVitePlus) pkg.devDependencies = {
2055
- ...pkg.devDependencies,
2056
- [VITE_PLUS_NAME]: canonicalVitePlusSpec
2057
- };
2058
- if (needDirectVitest) {
2059
- const installableDeps = {
2060
- ...pkg.dependencies,
2061
- ...pkg.devDependencies,
2062
- ...pkg.optionalDependencies
2063
- };
2064
- if (!installableDeps.vitest && (effectiveBrowserMode || Object.keys(installableDeps).some((name) => name.includes("vitest")))) {
2065
- pkg.devDependencies ??= {};
2066
- pkg.devDependencies.vitest = getCatalogDependencySpec(void 0, VITEST_VERSION, supportCatalog);
2067
- }
2068
- }
2069
- return extractedStagedConfig;
2070
- }
2071
- function isProtocolPinnedSpec(spec) {
2072
- return /^(catalog:|workspace:|link:|file:|npm:|github:|git[+:]|https?:\/\/)/.test(spec);
2073
- }
2074
- function removeLintStagedFromPackageJson(packageJsonPath) {
2075
- editJsonFile(packageJsonPath, (pkg) => {
2076
- if (pkg["lint-staged"]) {
2077
- delete pkg["lint-staged"];
2078
- return pkg;
2079
- }
2080
- });
2081
- }
2082
- function rewriteLintStagedConfigFile(projectPath, report) {
2083
- let hasUnsupported = false;
2084
- for (const filename of LINT_STAGED_JSON_CONFIG_FILES) {
2085
- const configPath = path.join(projectPath, filename);
2086
- if (!fs.existsSync(configPath)) continue;
2087
- if (filename === ".lintstagedrc" && !isJsonFile(configPath)) {
2088
- warnMigration(`${displayRelative(configPath)} is not JSON format — please migrate to "staged" in vite.config.ts manually`, report);
2089
- hasUnsupported = true;
2090
- continue;
2091
- }
2092
- if (!hasStagedConfigInViteConfig(projectPath)) {
2093
- const config = readJsonFile(configPath);
2094
- const updated = rewriteScripts(JSON.stringify(config), readRulesYaml());
2095
- if (!mergeStagedConfigToViteConfig(projectPath, updated ? JSON.parse(updated) : config, true, report)) continue;
2096
- fs.unlinkSync(configPath);
2097
- if (report) report.inlinedLintStagedConfigCount++;
2098
- } else warnMigration(`${displayRelative(configPath)} found but "staged" already exists in vite.config.ts — please merge manually`, report);
2099
- }
2100
- for (const filename of LINT_STAGED_OTHER_CONFIG_FILES) {
2101
- const configPath = path.join(projectPath, filename);
2102
- if (!fs.existsSync(configPath)) continue;
2103
- warnMigration(`${displayRelative(configPath)} — please migrate to "staged" in vite.config.ts manually`, report);
2104
- hasUnsupported = true;
2105
- }
2106
- if (hasUnsupported) infoMigration("Only \"staged\" in vite.config.ts is supported. See https://viteplus.dev/guide/migrate#lint-staged", report);
2107
- }
2108
- /**
2109
- * Ensure vite.config.ts exists, create it if not
2110
- * @returns The vite config filename
2111
- */
2112
- function ensureViteConfig(projectPath, configs, silent = false, report) {
2113
- if (!configs.viteConfig) {
2114
- configs.viteConfig = "vite.config.ts";
2115
- const viteConfigPath = path.join(projectPath, "vite.config.ts");
2116
- fs.writeFileSync(viteConfigPath, `import { defineConfig } from '${VITE_PLUS_NAME}';
2117
-
2118
- export default defineConfig({});
2119
- `);
2120
- if (report) report.createdViteConfigCount++;
2121
- if (!silent) log.success(`✔ Created vite.config.ts in ${displayRelative(viteConfigPath)}`);
2122
- }
2123
- return configs.viteConfig;
2124
- }
2125
- /**
2126
- * Merge tsdown.config.* into vite.config.ts
2127
- * - For JSON files: merge content directly into `pack` field and delete the JSON file
2128
- * - For TS/JS files: import the config file
2129
- */
2130
- function mergeTsdownConfigFile(projectPath, silent = false, report) {
2131
- const configs = detectConfigs(projectPath);
2132
- if (!configs.tsdownConfig) return;
2133
- const viteConfig = ensureViteConfig(projectPath, configs, silent, report);
2134
- const fullViteConfigPath = path.join(projectPath, viteConfig);
2135
- const fullTsdownConfigPath = path.join(projectPath, configs.tsdownConfig);
2136
- if (configs.tsdownConfig.endsWith(".json")) {
2137
- mergeAndRemoveJsonConfig(projectPath, viteConfig, configs.tsdownConfig, "pack", silent, report);
2138
- return;
2139
- }
2140
- const result = mergeTsdownConfig(fullViteConfigPath, `./${configs.tsdownConfig}`);
2141
- if (result.updated) {
2142
- fs.writeFileSync(fullViteConfigPath, result.content);
2143
- if (report) report.tsdownImportCount++;
2144
- if (!silent) log.success(`✔ Added import for ${displayRelative(fullTsdownConfigPath)} in ${displayRelative(fullViteConfigPath)}`);
2145
- }
2146
- infoMigration(`Please manually merge ${displayRelative(fullTsdownConfigPath)} into ${displayRelative(fullViteConfigPath)}, see https://viteplus.dev/guide/migrate#tsdown`, report);
2147
- }
2148
- /**
2149
- * Best-effort: derive the Oxlint rule-namespace a JS plugin package
2150
- * contributes. Mirrors the conventions @oxlint/migrate uses when
2151
- * translating ESLint configs, and the conventions Oxlint-native plugin
2152
- * authors use (`oxlint-plugin-<name>` — see posva/pinia-colada in the
2153
- * wild):
2154
- * `eslint-plugin-unocss` → `unocss` (rules: `unocss/order`)
2155
- * `oxlint-plugin-posva` → `posva` (rules: `posva/foo`)
2156
- * `@stylistic/eslint-plugin` → `@stylistic` (rules: `@stylistic/indent`)
2157
- * `@stylistic/eslint-plugin-ts` → `@stylistic/ts` (rules: `@stylistic/ts/indent`)
2158
- * `@scope/oxlint-plugin-x` → `@scope/x`
2159
- * anything else → the package name verbatim
2160
- */
2161
- function deriveJsPluginNamespace(packageName) {
2162
- for (const prefix of ["eslint-plugin-", "oxlint-plugin-"]) if (packageName.startsWith(prefix)) return packageName.slice(prefix.length) || packageName;
2163
- const scoped = packageName.match(/^(@[^/]+)\/(?:eslint|oxlint)-plugin(?:-(.+))?$/);
2164
- if (scoped) return scoped[2] ? `${scoped[1]}/${scoped[2]}` : scoped[1];
2165
- return packageName;
2166
- }
2167
- /**
2168
- * Collect every dependency name declared across the root + workspace
2169
- * `package.json` files after the ESLint cleanup has run. Used to verify
2170
- * that JS plugins referenced by the generated `.oxlintrc.json` are
2171
- * actually installable.
2172
- */
2173
- function collectInstalledPackageNames(projectPath, packages) {
2174
- const names = /* @__PURE__ */ new Set();
2175
- const paths = [projectPath, ...(packages ?? []).map((p) => path.join(projectPath, p.path))];
2176
- for (const dir of paths) {
2177
- const pkgJsonPath = path.join(dir, "package.json");
2178
- if (!fs.existsSync(pkgJsonPath)) continue;
2179
- let pkg;
2180
- try {
2181
- pkg = readJsonFile(pkgJsonPath);
2182
- } catch {
2183
- continue;
2184
- }
2185
- for (const field of [
2186
- "devDependencies",
2187
- "dependencies",
2188
- "peerDependencies",
2189
- "optionalDependencies"
2190
- ]) {
2191
- const deps = pkg[field];
2192
- if (deps) for (const name of Object.keys(deps)) names.add(name);
2193
- }
2194
- }
2195
- return names;
2196
- }
2197
- /**
2198
- * Test whether a rule key (e.g. `@stylistic/ts/indent`) belongs to any
2199
- * namespace in `namespaces`. We can't just split on the first `/` —
2200
- * `@stylistic/eslint-plugin-ts` contributes the multi-segment namespace
2201
- * `@stylistic/ts`, so the lookup has to try progressively longer
2202
- * prefixes until one matches or we run out of slashes.
2203
- */
2204
- function ruleKeyMatchesNamespace(key, namespaces) {
2205
- if (!key.includes("/")) return true;
2206
- let idx = key.indexOf("/");
2207
- while (idx !== -1) {
2208
- if (namespaces.has(key.slice(0, idx))) return true;
2209
- idx = key.indexOf("/", idx + 1);
2210
- }
2211
- return false;
2212
- }
2213
- /** Filter a rules object to only entries whose namespace is recognized. */
2214
- function filterRulesAgainstNamespaces(rules, namespaces) {
2215
- const out = {};
2216
- for (const [key, value] of Object.entries(rules)) if (ruleKeyMatchesNamespace(key, namespaces)) out[key] = value;
2217
- return out;
2218
- }
2219
- /**
2220
- * Sort a jsPlugins array into installed entries (kept) and string
2221
- * entries for packages that aren't present in the workspace. Object-form
2222
- * entries (`{ name, specifier }`) and string entries that look like
2223
- * local paths (`./X`, `/X`, `../X`) are passed through — Oxlint resolves
2224
- * them itself.
2225
- */
2226
- function partitionJsPlugins(entries, availablePackages) {
2227
- const kept = [];
2228
- const dropped = [];
2229
- for (const entry of entries) {
2230
- if (typeof entry !== "string") {
2231
- kept.push(entry);
2232
- continue;
2233
- }
2234
- if (entry.startsWith("./") || entry.startsWith("../") || entry.startsWith("/")) {
2235
- kept.push(entry);
2236
- continue;
2237
- }
2238
- if (availablePackages.has(entry)) kept.push(entry);
2239
- else dropped.push(entry);
2240
- }
2241
- return {
2242
- kept,
2243
- dropped
2244
- };
2245
- }
2246
- /** Build the set of rule-key namespaces backed by a given jsPlugins set. */
2247
- function jsPluginsToNamespaces(entries) {
2248
- const ns = /* @__PURE__ */ new Set();
2249
- for (const entry of entries) if (typeof entry === "string") ns.add(deriveJsPluginNamespace(entry));
2250
- else if (entry && typeof entry === "object" && "name" in entry && entry.name) ns.add(entry.name);
2251
- ns.delete("");
2252
- return ns;
2253
- }
2254
- /**
2255
- * Sanitize the `.oxlintrc.json` produced by `@oxlint/migrate` (in-place)
2256
- * before it gets merged into `vite.config.ts`. Drop references that
2257
- * won't resolve at lint time and warn the user.
2258
- *
2259
- * Why: `@oxlint/migrate` can emit `jsPlugins[]` / `plugins[]` / `rules`
2260
- * entries referring to packages the user never installed (e.g.
2261
- * translating `@unocss/eslint-config` into `eslint-plugin-unocss`),
2262
- * to plugins outside Oxlint's native set, or under namespaces no
2263
- * surviving plugin contributes. Without sanitization, `vp lint` aborts
2264
- * with "Failed to load JS plugin" / "Plugin not found" before running
2265
- * any rule. This produces a degraded-but-functional config instead.
2266
- *
2267
- * Per-override entries (`overrides[].jsPlugins`, `.plugins`, `.rules`)
2268
- * are sanitized independently — an override can introduce its own
2269
- * jsPlugin, so namespace availability is computed per-override (base
2270
- * namespaces ∪ the override's own surviving jsPlugins' namespaces).
2271
- */
2272
- function sanitizeMigratedOxlintConfig(config, availablePackages, report) {
2273
- const allDroppedJsPlugins = /* @__PURE__ */ new Set();
2274
- const allDroppedPlugins = /* @__PURE__ */ new Set();
2275
- const baseSplit = partitionJsPlugins(config.jsPlugins ?? [], availablePackages);
2276
- for (const n of baseSplit.dropped) allDroppedJsPlugins.add(n);
2277
- if (config.jsPlugins && baseSplit.dropped.length > 0) config.jsPlugins = baseSplit.kept;
2278
- const baseNamespaces = new Set(OXLINT_NATIVE_PLUGINS);
2279
- for (const ns of jsPluginsToNamespaces(baseSplit.kept)) baseNamespaces.add(ns);
2280
- if (config.plugins) {
2281
- const keptPlugins = [];
2282
- for (const p of config.plugins) if (baseNamespaces.has(p)) keptPlugins.push(p);
2283
- else allDroppedPlugins.add(p);
2284
- if (keptPlugins.length !== config.plugins.length) config.plugins = keptPlugins;
2285
- }
2286
- if (config.rules) {
2287
- const filtered = filterRulesAgainstNamespaces(config.rules, baseNamespaces);
2288
- if (Object.keys(filtered).length !== Object.keys(config.rules).length) config.rules = filtered;
2289
- }
2290
- if (Array.isArray(config.overrides)) for (const override of config.overrides) {
2291
- let overrideSurvivors = [];
2292
- if (override.jsPlugins) {
2293
- const split = partitionJsPlugins(override.jsPlugins, availablePackages);
2294
- for (const n of split.dropped) allDroppedJsPlugins.add(n);
2295
- if (split.dropped.length > 0) override.jsPlugins = split.kept;
2296
- overrideSurvivors = split.kept;
2297
- }
2298
- const overrideNamespaces = new Set(baseNamespaces);
2299
- for (const ns of jsPluginsToNamespaces(overrideSurvivors)) overrideNamespaces.add(ns);
2300
- if (override.plugins) {
2301
- const keptOverridePlugins = [];
2302
- for (const p of override.plugins) if (overrideNamespaces.has(p)) keptOverridePlugins.push(p);
2303
- else allDroppedPlugins.add(p);
2304
- if (keptOverridePlugins.length !== override.plugins.length) override.plugins = keptOverridePlugins;
2305
- }
2306
- if (override.rules) {
2307
- const filtered = filterRulesAgainstNamespaces(override.rules, overrideNamespaces);
2308
- if (Object.keys(filtered).length !== Object.keys(override.rules).length) override.rules = filtered;
2309
- }
2310
- }
2311
- if (allDroppedJsPlugins.size > 0) warnMigration(`Stripped JS plugin reference(s) from the generated lint config: ${[...allDroppedJsPlugins].join(", ")}. No matching package is present in this workspace, so loading them at lint time would fail. If you want their Oxlint coverage back, install each package (e.g. \`vp install <name>\`) and add its name back to \`lint.jsPlugins\` in vite.config.ts.`, report);
2312
- if (allDroppedPlugins.size > 0) warnMigration(`Stripped unknown plugin reference(s) from the generated lint config: ${[...allDroppedPlugins].join(", ")}. These aren't native Oxlint plugins and no surviving JS plugin contributes them.`, report);
2313
- }
2314
- /**
2315
- * Merge oxlint and oxfmt config into vite.config.ts
2316
- */
2317
- function mergeViteConfigFiles(projectPath, silent = false, report, packages, workspaceRoot) {
2318
- const configs = detectConfigs(projectPath);
2319
- if (!configs.oxfmtConfig && !configs.oxlintConfig) return;
2320
- const viteConfig = ensureViteConfig(projectPath, configs, silent, report);
2321
- if (configs.oxlintConfig) {
2322
- const fullOxlintPath = path.join(projectPath, configs.oxlintConfig);
2323
- const oxlintJson = readJsonFile(fullOxlintPath, true);
2324
- if (!oxlintJson.options) oxlintJson.options = {};
2325
- if (!hasBaseUrlInTsconfig(projectPath)) {
2326
- if (oxlintJson.options.typeAware === void 0) oxlintJson.options.typeAware = true;
2327
- if (oxlintJson.options.typeCheck === void 0) oxlintJson.options.typeCheck = true;
2328
- } else warnMigration(BASEURL_TSCONFIG_WARNING, report);
2329
- sanitizeMigratedOxlintConfig(oxlintJson, collectInstalledPackageNames(workspaceRoot ?? projectPath, packages), report);
2330
- const normalizedOxlintConfig = ensureVitePlusImportRuleDefaults(oxlintJson);
2331
- fs.writeFileSync(fullOxlintPath, JSON.stringify(normalizedOxlintConfig, null, 2));
2332
- mergeAndRemoveJsonConfig(projectPath, viteConfig, configs.oxlintConfig, "lint", silent, report);
2333
- }
2334
- if (configs.oxfmtConfig) mergeAndRemoveJsonConfig(projectPath, viteConfig, configs.oxfmtConfig, "fmt", silent, report);
2335
- }
2336
- /**
2337
- * Inject typeAware and typeCheck defaults into vite.config.ts lint config.
2338
- * Called after mergeViteConfigFiles() to handle the case where no .oxlintrc.json exists
2339
- * (e.g., newly created projects from create-vite templates).
2340
- */
2341
- function injectLintTypeCheckDefaults(projectPath, silent = false, report) {
2342
- if (hasBaseUrlInTsconfig(projectPath)) {
2343
- warnMigration(BASEURL_TSCONFIG_WARNING, report);
2344
- return;
2345
- }
2346
- injectConfigDefaults(projectPath, "lint", ".vite-plus-lint-init.oxlintrc.json", JSON.stringify(createDefaultVitePlusLintConfig({ includeTypeAwareDefaults: true })), silent, report);
2347
- }
2348
- function injectFmtDefaults(projectPath, silent = false, report) {
2349
- injectConfigDefaults(projectPath, "fmt", ".vite-plus-fmt-init.oxfmtrc.json", JSON.stringify({}), silent, report);
2350
- }
2351
- /**
2352
- * Wire `create.defaultTemplate: '<scope>'` into the new monorepo's
2353
- * `vite.config.ts`. The caller is `bin.ts`, only when scaffolding a
2354
- * monorepo from a bundled `@org` manifest entry — that's the case where
2355
- * the user just picked a template from a specific org and naturally
2356
- * wants subsequent `vp create` invocations from the workspace to default
2357
- * to that same org's picker.
2358
- */
2359
- function injectCreateDefaultTemplate(projectPath, scope, silent = false, report) {
2360
- if (!scope) return;
2361
- injectConfigDefaults(projectPath, "create", ".vite-plus-create-init.json", JSON.stringify({ defaultTemplate: scope }), silent, report);
2362
- }
2363
- function injectConfigDefaults(projectPath, configKey, tempFileName, tempFileContent, silent, report) {
2364
- const configs = detectConfigs(projectPath);
2365
- if (configs.viteConfig && hasConfigKey(path.join(projectPath, configs.viteConfig), configKey)) return;
2366
- const viteConfig = ensureViteConfig(projectPath, configs, silent, report);
2367
- const tempConfigPath = path.join(projectPath, tempFileName);
2368
- fs.writeFileSync(tempConfigPath, tempFileContent);
2369
- const fullViteConfigPath = path.join(projectPath, viteConfig);
2370
- let result;
2371
- try {
2372
- result = mergeJsonConfig(fullViteConfigPath, tempConfigPath, configKey);
2373
- } finally {
2374
- fs.rmSync(tempConfigPath, { force: true });
2375
- }
2376
- if (result.updated) fs.writeFileSync(fullViteConfigPath, result.content);
2377
- }
2378
- function mergeAndRemoveJsonConfig(projectPath, viteConfigPath, jsonConfigPath, configKey, silent = false, report) {
2379
- const fullViteConfigPath = path.join(projectPath, viteConfigPath);
2380
- const fullJsonConfigPath = path.join(projectPath, jsonConfigPath);
2381
- if (hasConfigKey(fullViteConfigPath, configKey)) {
2382
- fs.unlinkSync(fullJsonConfigPath);
2383
- if (!silent) log.info(`${configKey} config already present in ${displayRelative(fullViteConfigPath)} — removed redundant ${displayRelative(fullJsonConfigPath)}`);
2384
- return;
2385
- }
2386
- const result = mergeJsonConfig(fullViteConfigPath, fullJsonConfigPath, configKey);
2387
- if (result.updated) {
2388
- fs.writeFileSync(fullViteConfigPath, result.content);
2389
- fs.unlinkSync(fullJsonConfigPath);
2390
- if (report) report.mergedConfigCount++;
2391
- if (!silent) log.success(`✔ Merged ${displayRelative(fullJsonConfigPath)} into ${displayRelative(fullViteConfigPath)}`);
2392
- } else {
2393
- warnMigration(`Failed to merge ${displayRelative(fullJsonConfigPath)} into ${displayRelative(fullViteConfigPath)}`, report);
2394
- infoMigration("Please complete the merge manually and follow the instructions in the documentation: https://viteplus.dev/config/", report);
2395
- }
2396
- }
2397
- /**
2398
- * Merge a staged config object into vite.config.ts as `staged: { ... }`.
2399
- * Writes the config to a temp JSON file, calls mergeJsonConfig NAPI, then cleans up.
2400
- */
2401
- function mergeStagedConfigToViteConfig(projectPath, stagedConfig, silent = false, report) {
2402
- const viteConfig = ensureViteConfig(projectPath, detectConfigs(projectPath), silent, report);
2403
- const fullViteConfigPath = path.join(projectPath, viteConfig);
2404
- const tempJsonPath = path.join(projectPath, ".staged-config-temp.json");
2405
- fs.writeFileSync(tempJsonPath, JSON.stringify(stagedConfig, null, 2));
2406
- let result;
2407
- try {
2408
- result = mergeJsonConfig(fullViteConfigPath, tempJsonPath, "staged");
2409
- } finally {
2410
- fs.unlinkSync(tempJsonPath);
2411
- }
2412
- if (result.updated) {
2413
- fs.writeFileSync(fullViteConfigPath, result.content);
2414
- if (report) report.mergedStagedConfigCount++;
2415
- if (!silent) log.success(`✔ Merged staged config into ${displayRelative(fullViteConfigPath)}`);
2416
- return true;
2417
- } else {
2418
- warnMigration(`Failed to merge staged config into ${displayRelative(fullViteConfigPath)}`, report);
2419
- infoMigration(`Please add staged config to ${displayRelative(fullViteConfigPath)} manually, see https://viteplus.dev/guide/migrate#lint-staged`, report);
2420
- return false;
2421
- }
2422
- }
2423
- /**
2424
- * Check if vite.config.ts already has a `staged` config key.
2425
- */
2426
- function hasStagedConfigInViteConfig(projectPath) {
2427
- const configs = detectConfigs(projectPath);
2428
- if (!configs.viteConfig) return false;
2429
- const viteConfigPath = path.join(projectPath, configs.viteConfig);
2430
- const content = fs.readFileSync(viteConfigPath, "utf8");
2431
- return /\bstaged\s*:/.test(content);
2432
- }
2433
- /**
2434
- * Wrap safe inline Vite plugin arrays with lazyPlugins so check/lint/fmt do not
2435
- * eagerly execute plugin factories while loading vite.config.ts.
2436
- */
2437
- function wrapLazyPluginsInViteConfig(projectPath, silent = false, report) {
2438
- const configs = detectConfigs(projectPath);
2439
- if (!configs.viteConfig) return;
2440
- const viteConfigPath = path.join(projectPath, configs.viteConfig);
2441
- const result = wrapLazyPlugins(viteConfigPath);
2442
- if (!result.updated) return;
2443
- fs.writeFileSync(viteConfigPath, result.content);
2444
- if (report) report.wrappedPluginConfigCount++;
2445
- if (!silent) log.success(`✔ Wrapped inline Vite plugins with lazyPlugins in ${displayRelative(viteConfigPath)}`);
2446
- }
2447
- /**
2448
- * Rewrite imports in all TypeScript/JavaScript files under a directory
2449
- * This rewrites vite/vitest imports to @voidzero-dev/vite-plus
2450
- * @param projectPath - The root directory to search for files
2451
- */
2452
- function rewriteAllImports(projectPath, silent = false, report) {
2453
- const result = rewriteImportsInDirectory(projectPath);
2454
- const modified = result.modifiedFiles.length;
2455
- const errors = result.errors.length;
2456
- if (report) {
2457
- report.rewrittenImportFileCount += modified;
2458
- report.rewrittenImportErrors.push(...result.errors.map((error) => ({
2459
- path: displayRelative(error.path),
2460
- message: error.message
2461
- })));
2462
- }
2463
- if (!silent && modified > 0) {
2464
- log.success(`Rewrote imports in ${modified === 1 ? "one file" : `${modified} files`}`);
2465
- log.info(result.modifiedFiles.map((file) => ` ${displayRelative(file)}`).join("\n"));
2466
- }
2467
- if (errors > 0) if (report) warnMigration(`${errors === 1 ? "one file had an error" : `${errors} files had errors`} while rewriting imports`, report);
2468
- else {
2469
- log.warn(`⚠ ${errors === 1 ? "one file had an error" : `${errors} files had errors`}:`);
2470
- for (const error of result.errors) log.error(` ${displayRelative(error.path)}: ${error.message}`);
2471
- }
2472
- return modified > 0;
2473
- }
2474
- /**
2475
- * Check if the project has an unsupported husky version (<9.0.0).
2476
- * Uses `semver.coerce` to handle ranges like `^8.0.0` → `8.0.0`.
2477
- * When the specifier is a catalog reference (e.g. `"catalog:"`), resolves
2478
- * it from the active package manager's catalog first — a `catalog:` spec is
2479
- * only meaningful to the manager that owns the workspace, so we never read a
2480
- * leftover/foreign catalog file. When it is still not coercible (e.g.
2481
- * `"latest"`), falls back to the installed version in node_modules via
2482
- * `detectPackageMetadata`.
2483
- * Returns a reason string if hooks migration should be skipped, or null
2484
- * if husky is absent or compatible.
2485
- */
2486
- function checkUnsupportedHuskyVersion(projectPath, deps, prodDeps, packageManager) {
2487
- const huskyVersion = deps?.husky ?? prodDeps?.husky;
2488
- if (!huskyVersion) return null;
2489
- let coerced = import_semver.default.coerce(huskyVersion);
2490
- if (coerced == null && packageManager != null && huskyVersion.startsWith("catalog:")) {
2491
- const resolved = createCatalogDependencyResolver(projectPath, packageManager)?.(huskyVersion, "husky");
2492
- if (resolved) coerced = import_semver.default.coerce(resolved);
2493
- }
2494
- if (coerced == null) {
2495
- const installed = detectPackageMetadata(projectPath, "husky");
2496
- if (installed) coerced = import_semver.default.coerce(installed.version);
2497
- if (coerced == null) return `Could not determine husky version from "${huskyVersion}" — please specify a semver-compatible version (e.g., "^9.0.0") and re-run migration.`;
2498
- }
2499
- if (import_semver.default.satisfies(coerced, "<9.0.0")) return "Detected husky <9.0.0 — please upgrade to husky v9+ first, then re-run migration.";
2500
- return null;
2501
- }
2502
- const OTHER_HOOK_TOOLS = [
2503
- "simple-git-hooks",
2504
- "lefthook",
2505
- "yorkie"
2506
- ];
2507
- const REPLACED_HOOK_PACKAGES = ["husky", "lint-staged"];
2508
- function removeReplacedHookPackages(packageJsonPath) {
2509
- editJsonFile(packageJsonPath, (pkg) => {
2510
- for (const name of REPLACED_HOOK_PACKAGES) {
2511
- if (pkg.devDependencies?.[name]) delete pkg.devDependencies[name];
2512
- if (pkg.dependencies?.[name]) delete pkg.dependencies[name];
2513
- }
2514
- return pkg;
2515
- });
2516
- }
2517
- function detectLegacyGitHooksMigrationCandidate(projectPath) {
2518
- const packageJsonPath = path.join(projectPath, "package.json");
2519
- if (!fs.existsSync(packageJsonPath)) return false;
2520
- const pkg = readJsonFile(packageJsonPath);
2521
- return getOldHooksDir(projectPath) !== void 0 || pkg["lint-staged"] !== void 0;
2522
- }
2523
- /**
2524
- * Walk up from `startPath` looking for `.git` (directory or file — submodules
2525
- * use a `.git` file). Returns the directory that contains `.git`, or `null`.
2526
- */
2527
- function findGitRoot(startPath) {
2528
- let dir = startPath;
2529
- while (true) {
2530
- if (fs.existsSync(path.join(dir, ".git"))) return dir;
2531
- const parent = path.dirname(dir);
2532
- if (parent === dir) return null;
2533
- dir = parent;
2534
- }
2535
- }
2536
- /**
2537
- * Normalize "husky install [dir]" → "husky [dir]" so downstream regex
2538
- * and ast-grep rules can match a single pattern.
2539
- */
2540
- function collapseHuskyInstall(script) {
2541
- return script.replace("husky install ", "husky ").replace("husky install", "husky");
2542
- }
2543
- /**
2544
- * High-level helper: detect old hooks dir, set up git hooks, and rewrite
2545
- * the prepare script. Returns true if hooks were successfully installed.
2546
- */
2547
- function installGitHooks(projectPath, silent = false, report, packageManager) {
2548
- if (setupGitHooks(projectPath, getOldHooksDir(projectPath), silent, report, packageManager)) {
2549
- rewritePrepareScript(projectPath);
2550
- return true;
2551
- }
2552
- return false;
2553
- }
2554
- /**
2555
- * Read-only probe: extract the old husky hooks directory from `scripts.prepare`
2556
- * without modifying package.json. Returns undefined when no husky reference is found.
2557
- */
2558
- function getOldHooksDir(rootDir) {
2559
- const packageJsonPath = path.join(rootDir, "package.json");
2560
- if (!fs.existsSync(packageJsonPath)) return;
2561
- const pkg = readJsonFile(packageJsonPath);
2562
- if (!pkg.scripts?.prepare) return;
2563
- const match = collapseHuskyInstall(pkg.scripts.prepare).match(/\bhusky(?:\s+([\w./-]+))?/);
2564
- if (!match) return;
2565
- return match[1] ?? ".husky";
2566
- }
2567
- /**
2568
- * Pre-flight check: verify that git hooks can be set up for this project.
2569
- * Returns `null` if hooks setup can proceed, or a warning reason string
2570
- * explaining why hooks setup should be skipped.
2571
- *
2572
- * These checks are deterministic and read-only — they do not modify
2573
- * the project in any way, making them safe to call before migration.
2574
- *
2575
- * `packageManager` is the project's detected manager; it scopes `catalog:`
2576
- * resolution to that manager's catalog so a foreign catalog file is ignored.
2577
- */
2578
- function preflightGitHooksSetup(projectPath, packageManager) {
2579
- const gitRoot = findGitRoot(projectPath);
2580
- if (gitRoot && path.resolve(projectPath) !== path.resolve(gitRoot)) return "Subdirectory project detected — skipping git hooks setup. Configure hooks at the repository root.";
2581
- const packageJsonPath = path.join(projectPath, "package.json");
2582
- if (!fs.existsSync(packageJsonPath)) return null;
2583
- const pkgContent = readJsonFile(packageJsonPath);
2584
- const deps = pkgContent.devDependencies;
2585
- const prodDeps = pkgContent.dependencies;
2586
- for (const tool of OTHER_HOOK_TOOLS) if (deps?.[tool] || prodDeps?.[tool] || pkgContent[tool]) return `Detected ${tool} — skipping git hooks setup. Please configure git hooks manually.`;
2587
- const huskyReason = checkUnsupportedHuskyVersion(projectPath, deps, prodDeps, packageManager);
2588
- if (huskyReason) return huskyReason;
2589
- if (hasUnsupportedLintStagedConfig(projectPath)) return "Unsupported lint-staged config format — skipping git hooks setup. Please configure git hooks manually.";
2590
- return null;
2591
- }
2592
- /**
2593
- * Set up git hooks with husky + lint-staged via vp commands.
2594
- * Skips if another hook tool is detected (warns user).
2595
- * Returns true if hooks were successfully set up, false if skipped.
2596
- */
2597
- function setupGitHooks(projectPath, oldHooksDir, silent = false, report, packageManager) {
2598
- const reason = preflightGitHooksSetup(projectPath, packageManager);
2599
- if (reason) {
2600
- warnMigration(reason, report);
2601
- return false;
2602
- }
2603
- const packageJsonPath = path.join(projectPath, "package.json");
2604
- if (!fs.existsSync(packageJsonPath)) return false;
2605
- const gitRoot = findGitRoot(projectPath);
2606
- const isCustomDir = oldHooksDir != null && oldHooksDir !== ".husky";
2607
- const hooksDir = isCustomDir ? oldHooksDir : ".vite-hooks";
2608
- editJsonFile(packageJsonPath, (pkg) => {
2609
- if (!pkg.scripts) pkg.scripts = {};
2610
- if (!pkg.scripts.prepare) pkg.scripts.prepare = "vp config";
2611
- else if (!pkg.scripts.prepare.includes("vp config") && !/\bhusky\b/.test(pkg.scripts.prepare)) pkg.scripts.prepare = `vp config && ${pkg.scripts.prepare}`;
2612
- return pkg;
2613
- });
2614
- let stagedMerged = hasStagedConfigInViteConfig(projectPath);
2615
- const hasStandaloneConfig = hasStandaloneLintStagedConfig(projectPath);
2616
- if (!stagedMerged && !hasStandaloneConfig) {
2617
- const stagedConfig = readJsonFile(packageJsonPath)?.["lint-staged"] ?? DEFAULT_STAGED_CONFIG;
2618
- const updated = rewriteScripts(JSON.stringify(stagedConfig), readRulesYaml());
2619
- stagedMerged = mergeStagedConfigToViteConfig(projectPath, updated ? JSON.parse(updated) : stagedConfig, silent, report);
2620
- }
2621
- if (stagedMerged) removeLintStagedFromPackageJson(packageJsonPath);
2622
- if (oldHooksDir && !isCustomDir) {
2623
- const oldDir = path.join(projectPath, oldHooksDir);
2624
- if (fs.existsSync(oldDir)) {
2625
- const targetDir = path.join(projectPath, hooksDir);
2626
- fs.mkdirSync(targetDir, { recursive: true });
2627
- for (const entry of fs.readdirSync(oldDir, { withFileTypes: true })) {
2628
- if (entry.isDirectory() || entry.name.startsWith(".")) continue;
2629
- const src = path.join(oldDir, entry.name);
2630
- const dest = path.join(targetDir, entry.name);
2631
- fs.copyFileSync(src, dest);
2632
- fs.chmodSync(dest, 493);
2633
- }
2634
- fs.rmSync(oldDir, {
2635
- recursive: true,
2636
- force: true
2637
- });
2638
- }
2639
- }
2640
- if (stagedMerged) createPreCommitHook(projectPath, hooksDir);
2641
- if (!gitRoot) {
2642
- removeReplacedHookPackages(packageJsonPath);
2643
- return true;
2644
- }
2645
- if (oldHooksDir) {
2646
- const checkResult = import_cross_spawn.default.sync("git", [
2647
- "config",
2648
- "--local",
2649
- "core.hooksPath"
2650
- ], {
2651
- cwd: projectPath,
2652
- stdio: "pipe"
2653
- });
2654
- const existingPath = checkResult.status === 0 ? checkResult.stdout?.toString().trim() : "";
2655
- if (existingPath === `${oldHooksDir}/_` || existingPath === oldHooksDir) import_cross_spawn.default.sync("git", [
2656
- "config",
2657
- "--local",
2658
- "--unset",
2659
- "core.hooksPath"
2660
- ], {
2661
- cwd: projectPath,
2662
- stdio: "pipe"
2663
- });
2664
- }
2665
- const vpBin = process.env.VP_CLI_BIN ?? "vp";
2666
- const configArgs = isCustomDir ? [
2667
- "config",
2668
- "--no-agent",
2669
- "--hooks-dir",
2670
- hooksDir
2671
- ] : ["config", "--no-agent"];
2672
- const configResult = import_cross_spawn.default.sync(vpBin, configArgs, {
2673
- cwd: projectPath,
2674
- stdio: "pipe"
2675
- });
2676
- if (configResult.status === 0) {
2677
- const stdout = configResult.stdout?.toString().trim() ?? "";
2678
- if (stdout) {
2679
- warnMigration(`Git hooks not configured — ${stdout}`, report);
2680
- return false;
2681
- }
2682
- removeReplacedHookPackages(packageJsonPath);
2683
- if (report) report.gitHooksConfigured = true;
2684
- if (!silent) log.success("✔ Git hooks configured");
2685
- return true;
2686
- }
2687
- warnMigration("Failed to install git hooks", report);
2688
- return false;
2689
- }
2690
- /**
2691
- * Check if a standalone lint-staged config file exists
2692
- */
2693
- function hasStandaloneLintStagedConfig(projectPath) {
2694
- return LINT_STAGED_ALL_CONFIG_FILES.some((file) => fs.existsSync(path.join(projectPath, file)));
2695
- }
2696
- /**
2697
- * Check if a standalone lint-staged config exists in a format that can't be
2698
- * auto-migrated to "staged" in vite.config.ts (non-JSON files like .yaml,
2699
- * .mjs, .cjs, .js, or a non-JSON .lintstagedrc).
2700
- */
2701
- function hasUnsupportedLintStagedConfig(projectPath) {
2702
- for (const filename of LINT_STAGED_OTHER_CONFIG_FILES) if (fs.existsSync(path.join(projectPath, filename))) return true;
2703
- const lintstagedrcPath = path.join(projectPath, ".lintstagedrc");
2704
- if (fs.existsSync(lintstagedrcPath) && !isJsonFile(lintstagedrcPath)) return true;
2705
- return false;
2706
- }
2707
- /**
2708
- * Create pre-commit hook file in the hooks directory.
2709
- */
2710
- const STALE_LINT_STAGED_PATTERNS = [/^((?:[A-Z_][A-Z0-9_]*(?:=\S*)?\s+)*)(pnpm|pnpm exec|npx|yarn|yarn run|npm exec|npm run|bunx|bun run|bun x)\s+lint-staged\b/, /^((?:[A-Z_][A-Z0-9_]*(?:=\S*)?\s+)*)lint-staged\b/];
2711
- const DEFAULT_STAGED_CONFIG = { "*": "vp check --fix" };
2712
- /**
2713
- * Ensure the pre-commit hook exists with `vp staged`, and that
2714
- * vite.config.ts contains a `staged` block (using the default config
2715
- * if none is present). Called by `vp config` after hook installation.
2716
- */
2717
- function ensurePreCommitHook(projectPath, dir = ".vite-hooks") {
2718
- if (!hasStagedConfigInViteConfig(projectPath)) mergeStagedConfigToViteConfig(projectPath, DEFAULT_STAGED_CONFIG, true);
2719
- createPreCommitHook(projectPath, dir);
2720
- }
2721
- function createPreCommitHook(projectPath, dir = ".vite-hooks") {
2722
- const huskyDir = path.join(projectPath, dir);
2723
- fs.mkdirSync(huskyDir, { recursive: true });
2724
- const hookPath = path.join(huskyDir, "pre-commit");
2725
- if (fs.existsSync(hookPath)) {
2726
- const existing = fs.readFileSync(hookPath, "utf8");
2727
- if (existing.includes("vp staged")) return;
2728
- const lines = existing.split("\n");
2729
- let replaced = false;
2730
- const result = [];
2731
- for (const line of lines) {
2732
- const trimmed = line.trim();
2733
- if (!replaced) {
2734
- let matched = false;
2735
- for (const pattern of STALE_LINT_STAGED_PATTERNS) {
2736
- const match = pattern.exec(trimmed);
2737
- if (match) {
2738
- const parts = [
2739
- match[1]?.trim() ?? "",
2740
- "vp staged",
2741
- trimmed.slice(match[0].length).trim()
2742
- ].filter(Boolean);
2743
- result.push(parts.join(" "));
2744
- replaced = true;
2745
- matched = true;
2746
- break;
2747
- }
2748
- }
2749
- if (matched) continue;
2750
- }
2751
- result.push(line);
2752
- }
2753
- if (!replaced) fs.writeFileSync(hookPath, `${result.join("\n").trimEnd()}\nvp staged\n`);
2754
- else fs.writeFileSync(hookPath, result.join("\n"));
2755
- } else {
2756
- fs.writeFileSync(hookPath, "vp staged\n");
2757
- fs.chmodSync(hookPath, 493);
2758
- }
2759
- }
2760
- /**
2761
- * Rewrite only `scripts.prepare` in the root package.json using vite-prepare.yml rules.
2762
- * Collapses "husky install" → "husky" before applying ast-grep so that the
2763
- * replace-husky rule produces "vp config" with any directory argument preserved.
2764
- * Returns the old husky hooks dir (if any) for migration to .vite-hooks.
2765
- * Called only when hooks are being set up (not with --no-hooks).
2766
- */
2767
- function rewritePrepareScript(rootDir) {
2768
- const packageJsonPath = path.join(rootDir, "package.json");
2769
- if (!fs.existsSync(packageJsonPath)) return;
2770
- let oldDir;
2771
- editJsonFile(packageJsonPath, (pkg) => {
2772
- if (!pkg.scripts?.prepare) return pkg;
2773
- const prepare = collapseHuskyInstall(pkg.scripts.prepare);
2774
- const updated = rewriteScripts(JSON.stringify({ prepare }), readPrepareRulesYaml());
2775
- if (updated) {
2776
- let newPrepare = JSON.parse(updated).prepare;
2777
- newPrepare = newPrepare.replace(/\bvp config(?:\s+(?!-)([\w./-]+))?/, (_match, dir) => {
2778
- oldDir = dir ?? ".husky";
2779
- return dir ? `vp config --hooks-dir ${dir}` : "vp config";
2780
- });
2781
- pkg.scripts.prepare = newPrepare;
2782
- } else if (prepare !== pkg.scripts.prepare) pkg.scripts.prepare = prepare;
2783
- return pkg;
2784
- });
2785
- return oldDir;
2786
- }
2787
- function setPackageManager(projectDir, downloadPackageManager) {
2788
- editJsonFile(path.join(projectDir, "package.json"), (pkg) => {
2789
- if (!pkg.packageManager && !pkg.devEngines?.packageManager) pkg.devEngines = {
2790
- ...typeof pkg.devEngines === "object" && pkg.devEngines !== null && !Array.isArray(pkg.devEngines) ? pkg.devEngines : void 0,
2791
- packageManager: {
2792
- name: downloadPackageManager.name,
2793
- version: downloadPackageManager.version,
2794
- onFail: "download"
2795
- }
2796
- };
2797
- return pkg;
2798
- });
2799
- }
2800
- /**
2801
- * Detect a .nvmrc file in the project directory.
2802
- * If not found, check for a Volta node version in package.json.
2803
- * If either is found, return the relevant info for migration.
2804
- * Returns undefined if not found or .node-version already exists.
2805
- */
2806
- function detectNodeVersionManagerFile(projectPath) {
2807
- if (fs.existsSync(path.join(projectPath, ".node-version"))) return;
2808
- const configs = detectConfigs(projectPath);
2809
- if (configs.nvmrcFile) return configs.voltaNode ? {
2810
- file: ".nvmrc",
2811
- voltaPresent: true
2812
- } : { file: ".nvmrc" };
2813
- if (configs.voltaNode) return {
2814
- file: "package.json",
2815
- voltaNodeVersion: configs.voltaNode
2816
- };
2817
- }
2818
- /**
2819
- * Parse a version alias from a .nvmrc file into a .node-version compatible string.
2820
- * Accepts the first line of .nvmrc (pre-trimmed).
2821
- * Returns null for unsupported aliases like "system", "default", "iojs".
2822
- */
2823
- function parseNvmrcVersion(alias) {
2824
- const version = alias.trim();
2825
- if (!version) return null;
2826
- if (version === "node" || version === "stable") return "lts/*";
2827
- if (version === "iojs" || version === "system" || version === "default") return null;
2828
- if (version.startsWith("lts/")) return version;
2829
- const normalized = version.startsWith("v") ? version.slice(1) : version;
2830
- if (!normalized || !import_semver.default.validRange(normalized)) return null;
2831
- return normalized;
2832
- }
2833
- /**
2834
- * Migrate .nvmrc or Volta node version from package.json to .node-version.
2835
- * - For .nvmrc: the source file is removed after migration.
2836
- * - For package.json (Volta): the volta field is left as-is; removal is left to the user's discretion.
2837
- * Returns true on success, false if migration was skipped or failed.
2838
- */
2839
- function migrateNodeVersionManagerFile(projectPath, detection, report) {
2840
- const nodeVersionPath = path.join(projectPath, ".node-version");
2841
- if (detection.file === "package.json") {
2842
- const { voltaNodeVersion } = detection;
2843
- const resolvedVersion = voltaNodeVersion === "lts" ? "lts/*" : voltaNodeVersion;
2844
- if (!import_semver.default.valid(resolvedVersion) && resolvedVersion !== "lts/*") {
2845
- warnMigration(`package.json volta.node "${voltaNodeVersion}" is not an exact version. Pin an exact version (e.g. ${voltaNodeVersion}.0 or run \`volta pin node@${voltaNodeVersion}\`) then re-run migration.`, report);
2846
- return false;
2847
- }
2848
- fs.writeFileSync(nodeVersionPath, `${resolvedVersion}\n`);
2849
- if (report) {
2850
- report.manualSteps.push("Remove the \"volta\" field from package.json");
2851
- report.nodeVersionFileMigrated = true;
2852
- } else log.info("You can now remove the \"volta\" field from package.json manually.");
2853
- return true;
2854
- }
2855
- const sourcePath = path.join(projectPath, ".nvmrc");
2856
- const originalAlias = fs.readFileSync(sourcePath, "utf8").split("\n")[0]?.trim() ?? "";
2857
- const version = parseNvmrcVersion(originalAlias);
2858
- if (!version) {
2859
- warnMigration(".nvmrc contains an unsupported version alias. Create .node-version manually with your desired Node.js version.", report);
2860
- return false;
2861
- }
2862
- if (version === "lts/*" && (originalAlias === "node" || originalAlias === "stable")) log.info(`"${originalAlias}" in .nvmrc is not a specific version; automatically mapping to "lts/*"`);
2863
- fs.writeFileSync(nodeVersionPath, `${version}\n`);
2864
- fs.unlinkSync(sourcePath);
2865
- if (report) {
2866
- report.nodeVersionFileMigrated = true;
2867
- if (detection.voltaPresent) report.manualSteps.push("Remove the \"volta\" field from package.json");
2868
- } else if (detection.voltaPresent) log.info("You can now remove the \"volta\" field from package.json manually.");
2869
- return true;
2870
- }
2871
- function warnPackageLevelEslint() {
2872
- log.warn("ESLint detected in workspace packages but no root config found. Package-level ESLint must be migrated manually.");
2873
- }
2874
- const INCOMPATIBLE_ESLINT_INTEGRATIONS = ["@nuxt/eslint"];
2875
- /**
2876
- * Detect framework-ESLint integration packages whose ESLint migration is
2877
- * known to be incompatible. Returns the offending package name, or
2878
- * `undefined` if none is present.
2879
- */
2880
- function detectIncompatibleEslintIntegration(projectPath, packages) {
2881
- const candidates = [projectPath, ...(packages ?? []).map((p) => path.join(projectPath, p.path))];
2882
- for (const candidate of candidates) {
2883
- const pkgJsonPath = path.join(candidate, "package.json");
2884
- if (!fs.existsSync(pkgJsonPath)) continue;
2885
- let pkg;
2886
- try {
2887
- pkg = readJsonFile(pkgJsonPath);
2888
- } catch {
2889
- continue;
2890
- }
2891
- for (const name of INCOMPATIBLE_ESLINT_INTEGRATIONS) if (pkg.devDependencies?.[name] || pkg.dependencies?.[name]) return name;
2892
- }
2893
- }
2894
- function warnIncompatibleEslintIntegration(name) {
2895
- log.warn(`${name} detected — automatic ESLint migration is skipped. ${name} wires ESLint into a framework-specific flow that Vite+ cannot migrate cleanly yet. Your ESLint setup is preserved. To migrate manually, remove ${name} from package.json and re-run \`vp migrate\`.`);
2896
- }
2897
- function warnLegacyEslintConfig(legacyConfigFile) {
2898
- log.warn(`Legacy ESLint configuration detected (${legacyConfigFile}). Automatic migration to Oxlint requires ESLint v9+ with flat config format (eslint.config.*). Please upgrade to ESLint v9 first: https://eslint.org/docs/latest/use/migrate-to-9.0.0`);
2899
- }
2900
- async function confirmEslintMigration(interactive) {
2901
- if (interactive) {
2902
- const confirmed = await confirm({
2903
- message: "Migrate ESLint rules to Oxlint using @oxlint/migrate?\n " + styleText("gray", "Oxlint is Vite+'s built-in linter — significantly faster than ESLint with compatible rule support. @oxlint/migrate converts your existing rules automatically."),
2904
- initialValue: true
2905
- });
2906
- if (R(confirmed)) cancelAndExit();
2907
- return confirmed;
2908
- }
2909
- return true;
2910
- }
2911
- async function promptEslintMigration(projectPath, interactive, packages) {
2912
- const incompatible = detectIncompatibleEslintIntegration(projectPath, packages);
2913
- if (incompatible) {
2914
- warnIncompatibleEslintIntegration(incompatible);
2915
- return false;
2916
- }
2917
- const eslintProject = detectEslintProject(projectPath, packages);
2918
- if (eslintProject.hasDependency && !eslintProject.configFile && eslintProject.legacyConfigFile) {
2919
- warnLegacyEslintConfig(eslintProject.legacyConfigFile);
2920
- return false;
2921
- }
2922
- if (!eslintProject.hasDependency) return false;
2923
- if (!eslintProject.configFile) {
2924
- warnPackageLevelEslint();
2925
- return false;
2926
- }
2927
- if (!await confirmEslintMigration(interactive)) return false;
2928
- if (!await migrateEslintToOxlint(projectPath, interactive, eslintProject.configFile, packages)) cancelAndExit("ESLint migration failed.", 1);
2929
- return true;
2930
- }
2931
- function warnPackageLevelPrettier() {
2932
- log.warn("Prettier detected in workspace packages but no root config found. Package-level Prettier must be migrated manually.");
2933
- }
2934
- async function confirmPrettierMigration(interactive) {
2935
- if (interactive) {
2936
- const confirmed = await confirm({
2937
- message: "Migrate Prettier to Oxfmt?\n " + styleText("gray", "Oxfmt is Vite+'s built-in formatter that replaces Prettier with faster performance. Your configuration will be converted automatically."),
2938
- initialValue: true
2939
- });
2940
- if (R(confirmed)) cancelAndExit();
2941
- return confirmed;
2942
- }
2943
- log.info("Prettier configuration detected. Auto-migrating to Oxfmt...");
2944
- return true;
2945
- }
2946
- async function promptPrettierMigration(projectPath, interactive, packages) {
2947
- const prettierProject = detectPrettierProject(projectPath, packages);
2948
- if (!prettierProject.hasDependency) return false;
2949
- if (!prettierProject.configFile) {
2950
- warnPackageLevelPrettier();
2951
- return false;
2952
- }
2953
- if (!await confirmPrettierMigration(interactive)) return false;
2954
- if (!await migratePrettierToOxfmt(projectPath, interactive, prettierProject.configFile, packages)) cancelAndExit("Prettier migration failed.", 1);
2955
- return true;
2956
- }
2957
- //#endregion
2958
- //#region src/utils/agent.ts
2959
- const AGENTS = [
2960
- {
2961
- id: "agents",
2962
- label: "AGENTS.md",
2963
- targetPath: "AGENTS.md",
2964
- hint: "Codex, Amp, OpenCode, and similar agents",
2965
- aliases: [
2966
- "agents.md",
2967
- "chatgpt",
2968
- "chatgpt-codex",
2969
- "codex",
2970
- "amp",
2971
- "kilo",
2972
- "kilo-code",
2973
- "kiro",
2974
- "kiro-cli",
2975
- "opencode",
2976
- "other"
2977
- ]
2978
- },
2979
- {
2980
- id: "claude",
2981
- label: "CLAUDE.md",
2982
- targetPath: "CLAUDE.md",
2983
- hint: "Claude Code",
2984
- aliases: ["claude.md", "claude-code"]
2985
- },
2986
- {
2987
- id: "gemini",
2988
- label: "GEMINI.md",
2989
- targetPath: "GEMINI.md",
2990
- hint: "Gemini CLI",
2991
- aliases: ["gemini.md", "gemini-cli"]
2992
- },
2993
- {
2994
- id: "copilot",
2995
- label: ".github/copilot-instructions.md",
2996
- targetPath: ".github/copilot-instructions.md",
2997
- hint: "GitHub Copilot",
2998
- aliases: ["github-copilot", "copilot-instructions.md"]
2999
- },
3000
- {
3001
- id: "cursor",
3002
- label: ".cursor/rules/viteplus.mdc",
3003
- targetPath: ".cursor/rules/viteplus.mdc",
3004
- hint: "Cursor",
3005
- aliases: ["viteplus.mdc"]
3006
- },
3007
- {
3008
- id: "jetbrains",
3009
- label: ".aiassistant/rules/viteplus.md",
3010
- targetPath: ".aiassistant/rules/viteplus.md",
3011
- hint: "JetBrains AI Assistant",
3012
- aliases: [
3013
- "jetbrains",
3014
- "jetbrains-ai-assistant",
3015
- "aiassistant",
3016
- "viteplus.md"
3017
- ]
3018
- }
3019
- ];
3020
- const AGENT_DEFAULT_ID = "agents";
3021
- const AGENT_STANDARD_PATH = "AGENTS.md";
3022
- const COPILOT_AGENT_ID = "copilot";
3023
- const COPILOT_SETUP_WORKFLOW_PATH = ".github/workflows/copilot-setup-steps.yml";
3024
- const AGENT_INSTRUCTIONS_START_MARKER = "<!--VITE PLUS START-->";
3025
- const AGENT_INSTRUCTIONS_END_MARKER = "<!--VITE PLUS END-->";
3026
- const AGENT_ALIASES = Object.fromEntries(AGENTS.flatMap((option) => (option.aliases ?? []).map((alias) => [normalizeAgentName(alias), option.id])));
3027
- const COPILOT_SETUP_WORKFLOW_CONTENT = `name: "Copilot Setup Steps"
3028
-
3029
- on:
3030
- workflow_dispatch:
3031
- push:
3032
- paths:
3033
- - .github/workflows/copilot-setup-steps.yml
3034
- pull_request:
3035
- paths:
3036
- - .github/workflows/copilot-setup-steps.yml
3037
-
3038
- jobs:
3039
- copilot-setup-steps:
3040
- runs-on: ubuntu-latest
3041
- permissions:
3042
- contents: read
3043
- steps:
3044
- - name: Checkout code
3045
- uses: actions/checkout@v6
3046
- with:
3047
- persist-credentials: false
3048
- - name: Set up Vite+
3049
- uses: voidzero-dev/setup-vp@v1
3050
- with:
3051
- cache: true
3052
- run-install: true
3053
- - name: Verify Vite+
3054
- run: vp --version
3055
- `;
3056
- async function selectAgentTargets({ interactive, agent, onCancel }) {
3057
- if (agent === false) return {
3058
- targetPaths: void 0,
3059
- selectedAgents: []
3060
- };
3061
- if (interactive && !agent) {
3062
- const selectedAgentIds = await multiselect({
3063
- message: "Which coding agent instruction files should Vite+ create?",
3064
- options: AGENTS.map((option) => ({
3065
- label: option.label,
3066
- value: option.id,
3067
- hint: option.hint
3068
- })),
3069
- initialValues: [AGENT_DEFAULT_ID],
3070
- required: false
3071
- });
3072
- if (R(selectedAgentIds)) {
3073
- onCancel();
3074
- return {
3075
- targetPaths: void 0,
3076
- selectedAgents: []
3077
- };
3078
- }
3079
- if (selectedAgentIds.length === 0) return {
3080
- targetPaths: void 0,
3081
- selectedAgents: []
3082
- };
3083
- const selectedAgents = resolveAgentOptions(selectedAgentIds);
3084
- return {
3085
- targetPaths: getAgentTargetPaths(selectedAgents),
3086
- selectedAgents
3087
- };
3088
- }
3089
- const selectedAgents = resolveAgentOptions(agent ?? AGENT_DEFAULT_ID);
3090
- return {
3091
- targetPaths: getAgentTargetPaths(selectedAgents),
3092
- selectedAgents
3093
- };
3094
- }
3095
- async function selectAgentTargetPaths({ interactive, agent, onCancel }) {
3096
- return (await selectAgentTargets({
3097
- interactive,
3098
- agent,
3099
- onCancel
3100
- })).targetPaths;
3101
- }
3102
- function detectExistingAgentTargetPaths(projectRoot) {
3103
- const detectedPaths = [];
3104
- const seenTargetPaths = /* @__PURE__ */ new Set();
3105
- for (const option of AGENTS) {
3106
- if (seenTargetPaths.has(option.targetPath)) continue;
3107
- seenTargetPaths.add(option.targetPath);
3108
- const targetPath = path.join(projectRoot, option.targetPath);
3109
- if (fs.existsSync(targetPath) && !fs.lstatSync(targetPath).isSymbolicLink()) detectedPaths.push(option.targetPath);
3110
- }
3111
- return detectedPaths.length > 0 ? detectedPaths : void 0;
3112
- }
3113
- /**
3114
- * Silently update agent instruction files that contain Vite+ markers.
3115
- * - No agent files → no writes
3116
- * - No Vite+ markers → no writes
3117
- * - Markers present, content up to date → no writes
3118
- * - Markers present, content outdated → update marked section
3119
- */
3120
- function updateExistingAgentInstructions(projectRoot) {
3121
- const targetPaths = detectExistingAgentTargetPaths(projectRoot);
3122
- if (!targetPaths) return;
3123
- const templatePath = path.join(pkgRoot, "AGENTS.md");
3124
- if (!fs.existsSync(templatePath)) return;
3125
- const templateContent = fs.readFileSync(templatePath, "utf-8");
3126
- for (const targetPath of targetPaths) try {
3127
- const fullPath = path.join(projectRoot, targetPath);
3128
- const existing = fs.readFileSync(fullPath, "utf-8");
3129
- const updated = replaceMarkedAgentInstructionsSection(existing, templateContent);
3130
- if (updated !== void 0 && updated !== existing) fs.writeFileSync(fullPath, updated);
3131
- } catch {}
3132
- }
3133
- function resolveAgentOptions(agent) {
3134
- const agentNames = parseAgentNames(agent);
3135
- const resolvedAgentNames = agentNames.length > 0 ? agentNames : [AGENT_DEFAULT_ID];
3136
- const dedupedAgents = [];
3137
- const seenAgentIds = /* @__PURE__ */ new Set();
3138
- for (const name of resolvedAgentNames) {
3139
- const option = resolveSingleAgentOption(name);
3140
- if (seenAgentIds.has(option.id)) continue;
3141
- seenAgentIds.add(option.id);
3142
- dedupedAgents.push(option);
3143
- }
3144
- return dedupedAgents;
3145
- }
3146
- function getAgentTargetPaths(agents) {
3147
- const dedupedTargetPaths = [];
3148
- const seenTargetPaths = /* @__PURE__ */ new Set();
3149
- for (const agent of agents) {
3150
- if (seenTargetPaths.has(agent.targetPath)) continue;
3151
- seenTargetPaths.add(agent.targetPath);
3152
- dedupedTargetPaths.push(agent.targetPath);
3153
- }
3154
- return dedupedTargetPaths;
3155
- }
3156
- function parseAgentNames(agent) {
3157
- if (!agent) return [];
3158
- return (Array.isArray(agent) ? agent : [agent]).filter((value) => typeof value === "string").flatMap((value) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
3159
- }
3160
- function resolveSingleAgentOption(agent) {
3161
- const normalized = normalizeAgentName(agent);
3162
- const alias = AGENT_ALIASES[normalized];
3163
- const resolved = alias ? normalizeAgentName(alias) : normalized;
3164
- return AGENTS.find((option) => normalizeAgentName(option.id) === resolved || normalizeAgentName(option.label) === resolved || normalizeAgentName(option.targetPath) === resolved || option.aliases?.some((candidate) => normalizeAgentName(candidate) === resolved)) ?? AGENTS.find((option) => option.id === AGENT_DEFAULT_ID);
3165
- }
3166
- /**
3167
- * Detect agent instruction files that would conflict (exist without markers).
3168
- * Returns only files that need a user decision (append or skip).
3169
- * Read-only — does not write or modify any files.
3170
- */
3171
- async function detectAgentConflicts({ projectRoot, targetPaths }) {
3172
- if (!targetPaths || targetPaths.length === 0) return [];
3173
- const sourcePath = path.join(pkgRoot, "AGENTS.md");
3174
- if (!fs.existsSync(sourcePath)) return [];
3175
- const incomingContent = await fsPromises.readFile(sourcePath, "utf-8");
3176
- const shouldLinkToAgents = targetPaths.includes(AGENT_STANDARD_PATH);
3177
- const orderedPaths = shouldLinkToAgents ? [AGENT_STANDARD_PATH, ...targetPaths.filter((p) => p !== AGENT_STANDARD_PATH)] : targetPaths;
3178
- const conflicts = [];
3179
- const seenDestinationPaths = /* @__PURE__ */ new Set();
3180
- const seenRealPaths = /* @__PURE__ */ new Set();
3181
- for (const targetPathToCheck of orderedPaths) {
3182
- const destinationPath = path.join(projectRoot, targetPathToCheck);
3183
- const destinationKey = path.resolve(destinationPath);
3184
- if (seenDestinationPaths.has(destinationKey)) continue;
3185
- seenDestinationPaths.add(destinationKey);
3186
- if (shouldLinkToAgents && targetPathToCheck !== AGENT_STANDARD_PATH) {
3187
- if (await getExistingPathKind(destinationPath) !== "file") continue;
3188
- }
3189
- if (fs.existsSync(destinationPath)) {
3190
- if (fs.lstatSync(destinationPath).isSymbolicLink()) continue;
3191
- const destinationRealPath = await fsPromises.realpath(destinationPath);
3192
- if (seenRealPaths.has(destinationRealPath)) continue;
3193
- if (replaceMarkedAgentInstructionsSection(await fsPromises.readFile(destinationPath, "utf-8"), incomingContent) !== void 0) {
3194
- seenRealPaths.add(destinationRealPath);
3195
- continue;
3196
- }
3197
- conflicts.push({ targetPath: targetPathToCheck });
3198
- seenRealPaths.add(destinationRealPath);
3199
- }
3200
- }
3201
- return conflicts;
3202
- }
3203
- async function writeAgentInstructions({ projectRoot, targetPath, targetPaths, interactive, conflictDecisions, silent = false }) {
3204
- const paths = [...targetPaths ?? [], ...targetPath ? [targetPath] : []];
3205
- if (paths.length === 0) return;
3206
- const sourcePath = path.join(pkgRoot, "AGENTS.md");
3207
- if (!fs.existsSync(sourcePath)) {
3208
- if (!silent) log.warn("Agent instructions template not found; skipping.");
3209
- return;
3210
- }
3211
- const seenDestinationPaths = /* @__PURE__ */ new Set();
3212
- const seenRealPaths = /* @__PURE__ */ new Set();
3213
- const incomingContent = await fsPromises.readFile(sourcePath, "utf-8");
3214
- const shouldLinkToAgents = paths.includes(AGENT_STANDARD_PATH);
3215
- const orderedPaths = shouldLinkToAgents ? [AGENT_STANDARD_PATH, ...paths.filter((p) => p !== AGENT_STANDARD_PATH)] : paths;
3216
- for (const targetPathToWrite of orderedPaths) {
3217
- const destinationPath = path.join(projectRoot, targetPathToWrite);
3218
- const destinationKey = path.resolve(destinationPath);
3219
- if (seenDestinationPaths.has(destinationKey)) continue;
3220
- seenDestinationPaths.add(destinationKey);
3221
- await fsPromises.mkdir(path.dirname(destinationPath), { recursive: true });
3222
- if (shouldLinkToAgents && targetPathToWrite !== AGENT_STANDARD_PATH) {
3223
- if (await tryLinkTargetToAgents(projectRoot, targetPathToWrite, silent)) continue;
3224
- }
3225
- if (fs.existsSync(destinationPath)) {
3226
- if (fs.lstatSync(destinationPath).isSymbolicLink()) {
3227
- if (!silent) log.info(`Skipped writing ${targetPathToWrite} (symlink)`);
3228
- continue;
3229
- }
3230
- const destinationRealPath = await fsPromises.realpath(destinationPath);
3231
- if (seenRealPaths.has(destinationRealPath)) {
3232
- if (!silent) log.info(`Skipped writing ${targetPathToWrite} (duplicate target)`);
3233
- continue;
3234
- }
3235
- const existingContent = await fsPromises.readFile(destinationPath, "utf-8");
3236
- const updatedContent = replaceMarkedAgentInstructionsSection(existingContent, incomingContent);
3237
- if (updatedContent !== void 0) {
3238
- if (updatedContent !== existingContent) await fsPromises.writeFile(destinationPath, updatedContent);
3239
- seenRealPaths.add(destinationRealPath);
3240
- continue;
3241
- }
3242
- let conflictAction;
3243
- const preResolved = conflictDecisions?.get(targetPathToWrite);
3244
- if (preResolved) conflictAction = preResolved;
3245
- else if (interactive) {
3246
- const action = await select({
3247
- message: `Agent instructions already exist at ${targetPathToWrite}.\n ` + styleText("gray", "The Vite+ template includes guidance on `vp` commands, the build pipeline, and project conventions."),
3248
- options: [{
3249
- label: "Append",
3250
- value: "append",
3251
- hint: "Add template content to the end"
3252
- }, {
3253
- label: "Skip",
3254
- value: "skip",
3255
- hint: "Leave existing file unchanged"
3256
- }],
3257
- initialValue: "skip"
3258
- });
3259
- conflictAction = R(action) || action === "skip" ? "skip" : "append";
3260
- } else conflictAction = "skip";
3261
- if (conflictAction === "append") await appendAgentContent(destinationPath, targetPathToWrite, existingContent, incomingContent, silent);
3262
- else {
3263
- const suffix = !preResolved && !interactive ? " (already exists)" : "";
3264
- if (!silent) log.info(`Skipped writing ${targetPathToWrite}${suffix}`);
3265
- }
3266
- seenRealPaths.add(destinationRealPath);
3267
- continue;
3268
- }
3269
- await fsPromises.writeFile(destinationPath, incomingContent);
3270
- if (!silent) log.success(`Wrote agent instructions to ${targetPathToWrite}`);
3271
- seenRealPaths.add(await fsPromises.realpath(destinationPath));
3272
- }
3273
- }
3274
- async function writeCopilotSetupWorkflow({ projectRoot, silent = false }) {
3275
- const destinationPath = path.join(projectRoot, COPILOT_SETUP_WORKFLOW_PATH);
3276
- await fsPromises.mkdir(path.dirname(destinationPath), { recursive: true });
3277
- if (fs.existsSync(destinationPath)) {
3278
- if (!silent) log.info(`Skipped writing ${COPILOT_SETUP_WORKFLOW_PATH} (already exists)`);
3279
- return;
3280
- }
3281
- await fsPromises.writeFile(destinationPath, COPILOT_SETUP_WORKFLOW_CONTENT);
3282
- if (!silent) log.success(`Wrote Copilot setup workflow to ${COPILOT_SETUP_WORKFLOW_PATH}`);
3283
- }
3284
- async function appendAgentContent(destinationPath, targetPath, existingContent, incomingContent, silent = false) {
3285
- const separator = existingContent.endsWith("\n") ? "" : "\n";
3286
- await fsPromises.appendFile(destinationPath, `${separator}\n${incomingContent}`);
3287
- if (!silent) log.success(`Appended agent instructions to ${targetPath}`);
3288
- }
3289
- function normalizeAgentName(value) {
3290
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
3291
- }
3292
- function replaceMarkedAgentInstructionsSection(existing, incoming) {
3293
- const existingRange = getMarkedRange(existing, AGENT_INSTRUCTIONS_START_MARKER, AGENT_INSTRUCTIONS_END_MARKER);
3294
- if (!existingRange) return;
3295
- const incomingRange = getMarkedRange(incoming, AGENT_INSTRUCTIONS_START_MARKER, AGENT_INSTRUCTIONS_END_MARKER);
3296
- if (!incomingRange) return;
3297
- return `${existing.slice(0, existingRange.start)}${incoming.slice(incomingRange.start, incomingRange.end)}${existing.slice(existingRange.end)}`;
3298
- }
3299
- async function tryLinkTargetToAgents(projectRoot, targetPath, silent = false) {
3300
- const destinationPath = path.join(projectRoot, targetPath);
3301
- const agentsPath = path.join(projectRoot, AGENT_STANDARD_PATH);
3302
- const symlinkTarget = path.relative(path.dirname(destinationPath), agentsPath);
3303
- const existing = await getExistingPathKind(destinationPath);
3304
- if (existing === "file") return false;
3305
- if (existing === "symlink") {
3306
- const currentLink = await fsPromises.readlink(destinationPath);
3307
- if (path.resolve(path.dirname(destinationPath), currentLink) === agentsPath) {
3308
- if (!silent) log.info(`Skipped linking ${targetPath} (already linked to ${AGENT_STANDARD_PATH})`);
3309
- return true;
3310
- }
3311
- await fsPromises.unlink(destinationPath);
3312
- }
3313
- try {
3314
- await fsPromises.symlink(symlinkTarget, destinationPath);
3315
- } catch (err) {
3316
- if (err.code === "EPERM") {
3317
- await fsPromises.copyFile(agentsPath, destinationPath);
3318
- if (!silent) log.success(`Copied ${AGENT_STANDARD_PATH} to ${targetPath}`);
3319
- return true;
3320
- }
3321
- throw err;
3322
- }
3323
- if (!silent) log.success(`Linked ${targetPath} to ${AGENT_STANDARD_PATH}`);
3324
- return true;
3325
- }
3326
- async function getExistingPathKind(filePath) {
3327
- if (!fs.existsSync(filePath)) return "missing";
3328
- return (await fsPromises.lstat(filePath)).isSymbolicLink() ? "symlink" : "file";
3329
- }
3330
- function getMarkedRange(content, startMarker, endMarker) {
3331
- const start = content.indexOf(startMarker);
3332
- if (start === -1) return;
3333
- const endMarkerIndex = content.indexOf(endMarker, start + startMarker.length);
3334
- if (endMarkerIndex === -1) return;
3335
- return {
3336
- start,
3337
- end: endMarkerIndex + endMarker.length
3338
- };
3339
- }
3340
- //#endregion
3341
- export { mergeViteConfigFiles as A, setPackageManager as B, ensureVitePlusBootstrap as C, injectCreateDefaultTemplate as D, hasStagedConfigInViteConfig as E, promptEslintMigration as F, editYamlFile as G, warnLegacyEslintConfig as H, promptPrettierMigration as I, templatesDir as J, readYamlFile as K, rewriteMonorepo as L, migrateNodeVersionManagerFile as M, migratePrettierToOxfmt as N, injectLintTypeCheckDefaults as O, preflightGitHooksSetup as P, rewriteMonorepoProject as R, ensurePreCommitHook as S, hasFrameworkShim as T, warnPackageLevelEslint as U, warnIncompatibleEslintIntegration as V, warnPackageLevelPrettier as W, detectLegacyGitHooksMigrationCandidate as _, selectAgentTargets as a, detectPrettierProject as b, writeCopilotSetupWorkflow as c, checkVitestVersion as d, confirmEslintMigration as f, detectIncompatibleEslintIntegration as g, detectFramework as h, selectAgentTargetPaths as i, migrateEslintToOxlint as j, installGitHooks as k, addFrameworkShim as l, detectEslintProject as m, detectAgentConflicts as n, updateExistingAgentInstructions as o, confirmPrettierMigration as p, displayRelative as q, detectExistingAgentTargetPaths as r, writeAgentInstructions as s, COPILOT_AGENT_ID as t, checkViteVersion as u, detectNodeVersionManagerFile as v, finalizeCoreMigrationForExistingVitePlus as w, detectVitePlusBootstrapPending as x, detectPendingCoreMigration as y, rewriteStandaloneProject as z };