vite-plus 0.2.7 → 0.2.9

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 (89) hide show
  1. package/AGENTS.md +7 -0
  2. package/LICENSE +5 -1
  3. package/README.md +9 -3
  4. package/bin/vpr +3 -2
  5. package/binding/index.cjs +193 -80
  6. package/binding/index.d.cts +106 -11
  7. package/dist/agent-DQU8uS6P.js +416 -0
  8. package/dist/bin.js +1388 -23
  9. package/dist/{cli-truncate-BVOlIIPy.js → cli-truncate-Bg3RDXpi.js} +0 -2
  10. package/dist/client/bundledDevClient.mjs +1 -0
  11. package/dist/config/bin.js +32 -133
  12. package/dist/{constants-BsuzPowC.js → constants-CG513DRa.js} +10 -2
  13. package/dist/create/bin.js +255 -268
  14. package/dist/{define-config-C2KfLHpC.js → define-config-CV4bQG_8.js} +3 -3
  15. package/dist/{define-config-eLQH9us2.d.ts → define-config-DWoAkNdp.d.ts} +16 -0
  16. package/dist/{define-config-DLh9fE95.cjs → define-config-DyNLA_f0.cjs} +1 -1
  17. package/dist/define-config.cjs +1 -1
  18. package/dist/define-config.d.ts +1 -1
  19. package/dist/define-config.js +1 -1
  20. package/dist/{dist-CKz3vcoG.js → dist-BDYZP12R.js} +196 -157
  21. package/dist/dist-Dqv1-Clg.js +3 -0
  22. package/dist/{agent-CPwWJ4qf.js → editor-g4PosU99.js} +819 -628
  23. package/dist/hooks/bin.d.ts +1 -0
  24. package/dist/hooks/bin.js +130 -0
  25. package/dist/hooks-pNCBWtFT.js +570 -0
  26. package/dist/index.cjs +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/{json-DiRs8ceZ.js → json-qlK6UH0r.js} +40 -38
  30. package/dist/migration/bin.js +65 -58
  31. package/dist/{oxlint-plugin-config-Bxgc4cwF.js → oxlint-plugin-config-BEZ9IUf7.js} +1 -1
  32. package/dist/oxlint-plugin.js +1 -1
  33. package/dist/pack-bin.js +1 -1
  34. package/dist/{package-Ds3uThLb.js → package-B4T8RGMG.js} +2 -2
  35. package/dist/{tsconfig-Jia2vgMG.js → prompts-CHz_98bJ.js} +169 -219
  36. package/dist/{resolve-vite-config-BiuLZnn8.js → resolve-vite-config-Dmeyeyj-.js} +2 -9
  37. package/dist/{rolldown-runtime-C7HZzL1F.js → rolldown-runtime-CMFfr-1z.js} +1 -1
  38. package/dist/staged/bin.js +241 -226
  39. package/dist/terminal-Bz-ps6rJ.js +128 -0
  40. package/dist/test/browser/providers/playwright/context.d.ts +1 -1
  41. package/dist/test/browser/providers/playwright.d.ts +6 -6
  42. package/dist/test/browser/providers/preview/context.d.ts +1 -1
  43. package/dist/test/browser/providers/preview.d.ts +4 -4
  44. package/dist/test/browser/providers/webdriverio/context.d.ts +1 -1
  45. package/dist/test/browser/providers/webdriverio.d.ts +6 -6
  46. package/dist/test/browser-playwright/context.d.ts +1 -1
  47. package/dist/test/browser-playwright.d.ts +6 -6
  48. package/dist/test/browser-preview/context.d.ts +1 -1
  49. package/dist/test/browser-preview.d.ts +4 -4
  50. package/dist/test/browser-webdriverio/context.d.ts +1 -1
  51. package/dist/test/browser-webdriverio.d.ts +6 -6
  52. package/dist/test/context.d.ts +3 -3
  53. package/dist/test/locators.d.ts +1 -1
  54. package/dist/toolchain.d.ts +25 -0
  55. package/dist/toolchain.js +194 -0
  56. package/dist/toolchain.json +193 -0
  57. package/dist/tsconfig-7v_BHagU.js +151 -0
  58. package/dist/{tsgolint-path-B-yOos8p.js → tsgolint-path-CbDReEOx.js} +5 -3
  59. package/dist/tsgolint-path.js +1 -1
  60. package/dist/version.js +6 -6
  61. package/dist/versions.d.ts +3 -3
  62. package/dist/versions.js +6 -6
  63. package/docs/config/index.md +13 -0
  64. package/docs/config/staged.md +2 -1
  65. package/docs/guide/ci.md +37 -4
  66. package/docs/guide/commit-hooks.md +112 -21
  67. package/docs/guide/create.md +7 -4
  68. package/docs/guide/env.md +8 -4
  69. package/docs/guide/fmt.md +2 -2
  70. package/docs/guide/github-actions-cache.md +5 -1
  71. package/docs/guide/ide-integration.md +61 -2
  72. package/docs/guide/index.md +4 -2
  73. package/docs/guide/install.md +27 -0
  74. package/docs/guide/installer-env-vars.md +14 -3
  75. package/docs/guide/lint.md +3 -1
  76. package/docs/guide/migrate.md +49 -4
  77. package/docs/guide/monorepo.md +98 -10
  78. package/docs/guide/troubleshooting.md +12 -3
  79. package/docs/guide/upgrade.md +27 -4
  80. package/docs/package.json +1 -1
  81. package/docs/pnpm-workspace.yaml +7 -0
  82. package/docs/vite.config.ts +3 -0
  83. package/package.json +21 -23
  84. package/rules/vite-tools.yml +0 -7
  85. package/dist/dist-CtM2JRbM.js +0 -3
  86. package/dist/editor-BbT1lTNc.js +0 -675
  87. package/dist/help-YP84FSEz.js +0 -61
  88. package/dist/terminal-uTv0ZaMr.js +0 -33
  89. package/dist/{log-update-DHZRyJ2m.js → log-update-CoW8Z4Dl.js} +1 -1
@@ -1,40 +1,27 @@
1
- import { r as __toESM } from "./rolldown-runtime-C7HZzL1F.js";
2
- import { a as VITEST_AGE_GATE_EXEMPT_PACKAGES, c as VITE_PLUS_OVERRIDE_PACKAGES, d as isForceOverrideMode, l as VITE_PLUS_VERSION, n as BASEURL_TSCONFIG_WARNING, o as VITEST_VERSION, s as VITE_PLUS_NAME } from "./constants-BsuzPowC.js";
3
- import { i as ensureVitePlusImportRuleDefaults, r as createDefaultVitePlusLintConfig } from "./oxlint-plugin-config-Bxgc4cwF.js";
4
- import { B as runCommandSilently, D as require_semver, E as PackageManager, M as multiselect, P as select, R as isCancel, V as require_cross_spawn, a as hasTypesToRewriteInTsconfig, b as scalarString, c as rewriteTypesInTsconfig, i as hasBaseUrlInTsconfig, j as log, k as confirm, l as cancelAndExit, n as findTsconfigFiles, o as hasVitestTypesInTsconfig, s as removeDeprecatedTsconfigFalseOption, v as editYamlFile, w as getSpinner, y as readYamlFile } from "./tsconfig-Jia2vgMG.js";
5
- import { a as writeJsonFile, i as readJsonFile, n as editJsonFile, r as isJsonFile } from "./json-DiRs8ceZ.js";
6
- import { t as require_dist } from "./dist-CKz3vcoG.js";
7
- import { n as detectPackageMetadata } from "./package-Ds3uThLb.js";
1
+ import { r as __toESM } from "./rolldown-runtime-CMFfr-1z.js";
2
+ import { a as VITEST_AGE_GATE_EXEMPT_PACKAGES, c as VITE_PLUS_NAME, f as isForceOverrideMode, l as VITE_PLUS_OVERRIDE_PACKAGES, n as BASEURL_TSCONFIG_WARNING, o as VITEST_VERSION, s as VITE_CONFIG_FILES, u as VITE_PLUS_VERSION } from "./constants-CG513DRa.js";
3
+ import { i as ensureVitePlusImportRuleDefaults, r as createDefaultVitePlusLintConfig } from "./oxlint-plugin-config-BEZ9IUf7.js";
4
+ import { A as isCancel, C as log, E as select, M as runCommandSilently, N as require_cross_spawn, _ as getSpinner, b as require_semver, d as readYamlFile, f as scalarString, g as getSilentSpinner, t as cancelAndExit, u as editYamlFile, w as multiselect, x as confirm, y as PackageManager } from "./prompts-CHz_98bJ.js";
5
+ import { a as writeJsonFile, c as parse, i as readJsonFile, n as editJsonFile, o as applyEdits, r as isJsonFile, s as modify, t as detectFormattingOptions } from "./json-qlK6UH0r.js";
6
+ import { t as require_dist } from "./dist-BDYZP12R.js";
7
+ import { a as hasTypesToRewriteInTsconfig, c as rewriteTypesInTsconfig, i as hasBaseUrlInTsconfig, n as findTsconfigFiles, o as hasVitestTypesInTsconfig, s as removeDeprecatedTsconfigFalseOption } from "./tsconfig-7v_BHagU.js";
8
+ import { n as detectPackageMetadata, r as getScopeFromPackageName } from "./package-B4T8RGMG.js";
9
+ import { l as displayRelative, u as rulesDir } from "./agent-DQU8uS6P.js";
8
10
  import { n as addMigrationWarning, t as addManualStep } from "./report-BS7ULx7A.js";
11
+ import { a as findUnsafeHookInstallPath, n as SUPPORTED_GIT_HOOK_NAMES } from "./hooks-pNCBWtFT.js";
9
12
  import path, { posix, win32 } from "node:path";
10
- import { hasConfigKey, mergeJsonConfig, mergeTsdownConfig, rewriteEslint, rewriteImportsInDirectory, rewriteScripts, wrapLazyPlugins } from "../binding/index.js";
13
+ import { fileURLToPath } from "node:url";
14
+ import { detectWorkspace, hasConfigKey, mergeJsonConfig, mergeTsdownConfig, rewriteEslint, rewriteImportsInDirectory, rewritePrettier, rewriteScripts, wrapLazyPlugins } from "../binding/index.js";
15
+ import { styleText } from "node:util";
11
16
  import * as actualFS from "node:fs";
12
17
  import fs from "node:fs";
13
- import { lstatSync, readdir, readdirSync, readlinkSync, realpathSync as realpathSync$1 } from "fs";
14
- import { styleText } from "node:util";
15
- import { fileURLToPath } from "node:url";
16
18
  import os from "node:os";
19
+ import { lstatSync as lstatSync$1, readdir, readdirSync as readdirSync$1, readlinkSync, realpathSync as realpathSync$1 } from "fs";
17
20
  import "oxlint";
18
21
  import fsPromises, { lstat, readdir as readdir$1, readlink, realpath } from "node:fs/promises";
19
22
  import { EventEmitter } from "node:events";
20
23
  import Stream from "node:stream";
21
24
  import { StringDecoder } from "node:string_decoder";
22
- //#region src/utils/path.ts
23
- function findPkgRoot() {
24
- let dir = import.meta.dirname;
25
- while (dir !== path.dirname(dir)) {
26
- if (fs.existsSync(path.join(dir, "package.json"))) return dir;
27
- dir = path.dirname(dir);
28
- }
29
- return dir;
30
- }
31
- const pkgRoot = findPkgRoot();
32
- const templatesDir = path.join(pkgRoot, "templates");
33
- const rulesDir = path.join(pkgRoot, "rules");
34
- function displayRelative(to, from = process.cwd()) {
35
- return path.relative(from, to).replaceAll("\\", "/");
36
- }
37
- //#endregion
38
25
  //#region src/migration/migrator/shared.ts
39
26
  var import_semver = /* @__PURE__ */ __toESM(require_semver(), 1);
40
27
  const LINT_STAGED_JSON_CONFIG_FILES = [".lintstagedrc.json", ".lintstagedrc"];
@@ -167,14 +154,7 @@ const PRETTIER_CONFIG_FILES = [
167
154
  ];
168
155
  function detectConfigs(projectPath) {
169
156
  const configs = {};
170
- for (const config of [
171
- "vite.config.ts",
172
- "vite.config.mts",
173
- "vite.config.cts",
174
- "vite.config.js",
175
- "vite.config.mjs",
176
- "vite.config.cjs"
177
- ]) if (fs.existsSync(path.join(projectPath, config))) {
157
+ for (const config of VITE_CONFIG_FILES) if (fs.existsSync(path.join(projectPath, config))) {
178
158
  configs.viteConfig = config;
179
159
  break;
180
160
  }
@@ -322,17 +302,7 @@ async function runOxlintMigrateStep(vpBin, cwd, migratePackage, args, spinner, f
322
302
  }
323
303
  async function migrateEslintToOxlint(projectPath, interactive, eslintConfigFile, packages, options) {
324
304
  const vpBin = process.env.VP_CLI_BIN ?? "vp";
325
- const spinner = options?.silent ? {
326
- start: () => {},
327
- stop: () => {},
328
- pause: () => {},
329
- resume: () => {},
330
- cancel: () => {},
331
- error: () => {},
332
- clear: () => {},
333
- message: () => {},
334
- isCancelled: false
335
- } : getSpinner(interactive);
305
+ const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive);
336
306
  if (eslintConfigFile) {
337
307
  const { versions } = await import("./versions.js");
338
308
  const migratePackage = `@oxlint/migrate@${versions.oxlint}`;
@@ -743,6 +713,174 @@ async function promptEslintMigration(projectPath, interactive, packages) {
743
713
  return true;
744
714
  }
745
715
  //#endregion
716
+ //#region src/migration/migrator/prettier.ts
717
+ function detectPrettierProject(projectPath, packages) {
718
+ const packageJsonPath = path.join(projectPath, "package.json");
719
+ if (!fs.existsSync(packageJsonPath)) return { hasDependency: false };
720
+ const pkg = readJsonFile(packageJsonPath);
721
+ let hasDependency = !!(pkg.devDependencies?.prettier || pkg.dependencies?.prettier);
722
+ const configFile = detectConfigs(projectPath).prettierConfig;
723
+ if (!hasDependency && packages) for (const wp of packages) {
724
+ const pkgJsonPath = path.join(projectPath, wp.path, "package.json");
725
+ if (!fs.existsSync(pkgJsonPath)) continue;
726
+ const wpPkg = readJsonFile(pkgJsonPath);
727
+ if (wpPkg.devDependencies?.prettier || wpPkg.dependencies?.prettier) {
728
+ hasDependency = true;
729
+ break;
730
+ }
731
+ }
732
+ return {
733
+ hasDependency,
734
+ configFile
735
+ };
736
+ }
737
+ /**
738
+ * Run `vp fmt --migrate=prettier` step with graceful error handling.
739
+ * Returns true on success, false on failure.
740
+ */
741
+ async function runPrettierMigrateStep(vpBin, cwd, spinner, failMessage, manualHint) {
742
+ try {
743
+ const result = await runCommandSilently({
744
+ command: vpBin,
745
+ args: ["fmt", "--migrate=prettier"],
746
+ cwd,
747
+ envs: process.env
748
+ });
749
+ if (result.exitCode !== 0) {
750
+ spinner.stop(failMessage);
751
+ const stderr = result.stderr.toString().trim();
752
+ if (stderr) log.warn(`⚠ ${stderr}`);
753
+ log.info(manualHint);
754
+ return false;
755
+ }
756
+ return true;
757
+ } catch {
758
+ spinner.stop(failMessage);
759
+ log.info(manualHint);
760
+ return false;
761
+ }
762
+ }
763
+ async function migratePrettierToOxfmt(projectPath, interactive, prettierConfigFile, packages, options) {
764
+ const vpBin = process.env.VP_CLI_BIN ?? "vp";
765
+ const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive);
766
+ if (prettierConfigFile) {
767
+ let tempPrettierConfig;
768
+ if (prettierConfigFile === "package.json#prettier") {
769
+ const packageJsonPath = path.join(projectPath, "package.json");
770
+ const pkg = readJsonFile(packageJsonPath);
771
+ if (pkg.prettier) {
772
+ tempPrettierConfig = path.join(projectPath, ".prettierrc.json");
773
+ fs.writeFileSync(tempPrettierConfig, JSON.stringify(pkg.prettier, null, 2));
774
+ } else return true;
775
+ }
776
+ try {
777
+ spinner.start("Migrating Prettier config to Oxfmt...");
778
+ if (!await runPrettierMigrateStep(vpBin, projectPath, spinner, "Prettier migration failed", "You can run `vp fmt --migrate=prettier` manually later")) return false;
779
+ spinner.stop("Prettier config migrated to .oxfmtrc.json");
780
+ } finally {
781
+ if (tempPrettierConfig) try {
782
+ fs.unlinkSync(tempPrettierConfig);
783
+ } catch {}
784
+ }
785
+ }
786
+ if (options?.report) options.report.prettierMigrated = true;
787
+ deletePrettierConfigFiles(projectPath, options?.report, options?.silent);
788
+ rewritePrettierPackageJson(path.join(projectPath, "package.json"));
789
+ if (packages) for (const pkg of packages) rewritePrettierPackageJson(path.join(projectPath, pkg.path, "package.json"));
790
+ rewritePrettierLintStagedConfigFiles(projectPath, options?.report);
791
+ const prettierIgnorePath = path.join(projectPath, ".prettierignore");
792
+ if (fs.existsSync(prettierIgnorePath)) warnMigration(`${displayRelative(prettierIgnorePath)} found — Oxfmt supports .prettierignore, but using the \`ignorePatterns\` option is recommended.`, options?.report);
793
+ return true;
794
+ }
795
+ function deletePrettierConfigFiles(basePath, report, silent = false) {
796
+ const configs = detectConfigs(basePath);
797
+ if (configs.prettierConfig && configs.prettierConfig !== "package.json#prettier") {
798
+ const configPath = path.join(basePath, configs.prettierConfig);
799
+ if (fs.existsSync(configPath)) {
800
+ fs.unlinkSync(configPath);
801
+ if (report) report.removedConfigCount++;
802
+ if (!silent) log.success(`✔ Removed ${displayRelative(configPath)}`);
803
+ }
804
+ }
805
+ for (const file of PRETTIER_CONFIG_FILES) {
806
+ if (file === configs.prettierConfig) continue;
807
+ const configPath = path.join(basePath, file);
808
+ if (fs.existsSync(configPath)) {
809
+ fs.unlinkSync(configPath);
810
+ if (report) report.removedConfigCount++;
811
+ if (!silent) log.success(`✔ Removed ${displayRelative(configPath)}`);
812
+ }
813
+ }
814
+ editJsonFile(path.join(basePath, "package.json"), (pkg) => {
815
+ if (pkg.prettier) {
816
+ delete pkg.prettier;
817
+ return pkg;
818
+ }
819
+ });
820
+ }
821
+ function rewritePrettierPackageJson(packageJsonPath) {
822
+ if (!fs.existsSync(packageJsonPath)) return;
823
+ editJsonFile(packageJsonPath, (pkg) => {
824
+ let changed = false;
825
+ if (pkg.devDependencies) {
826
+ for (const dep of Object.keys(pkg.devDependencies)) if (dep === "prettier" || dep.startsWith("prettier-plugin-")) {
827
+ delete pkg.devDependencies[dep];
828
+ changed = true;
829
+ }
830
+ }
831
+ if (pkg.dependencies) {
832
+ for (const dep of Object.keys(pkg.dependencies)) if (dep === "prettier" || dep.startsWith("prettier-plugin-")) {
833
+ delete pkg.dependencies[dep];
834
+ changed = true;
835
+ }
836
+ }
837
+ if (pkg.scripts) {
838
+ const updated = rewritePrettier(JSON.stringify(pkg.scripts));
839
+ if (updated) {
840
+ pkg.scripts = JSON.parse(updated);
841
+ changed = true;
842
+ }
843
+ }
844
+ if (pkg["lint-staged"]) {
845
+ const updated = rewritePrettier(JSON.stringify(pkg["lint-staged"]));
846
+ if (updated) {
847
+ pkg["lint-staged"] = JSON.parse(updated);
848
+ changed = true;
849
+ }
850
+ }
851
+ return changed ? pkg : void 0;
852
+ });
853
+ }
854
+ function rewritePrettierLintStagedConfigFiles(projectPath, report) {
855
+ rewriteToolLintStagedConfigFiles(projectPath, rewritePrettier, "prettier", report);
856
+ }
857
+ function warnPackageLevelPrettier() {
858
+ log.warn("Prettier detected in workspace packages but no root config found. Package-level Prettier must be migrated manually.");
859
+ }
860
+ async function confirmPrettierMigration(interactive) {
861
+ if (interactive) {
862
+ const confirmed = await confirm({
863
+ 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."),
864
+ initialValue: true
865
+ });
866
+ if (isCancel(confirmed)) cancelAndExit();
867
+ return confirmed;
868
+ }
869
+ log.info("Prettier configuration detected. Auto-migrating to Oxfmt...");
870
+ return true;
871
+ }
872
+ async function promptPrettierMigration(projectPath, interactive, packages) {
873
+ const prettierProject = detectPrettierProject(projectPath, packages);
874
+ if (!prettierProject.hasDependency) return false;
875
+ if (!prettierProject.configFile) {
876
+ warnPackageLevelPrettier();
877
+ return false;
878
+ }
879
+ if (!await confirmPrettierMigration(interactive)) return false;
880
+ if (!await migratePrettierToOxfmt(projectPath, interactive, prettierProject.configFile, packages)) cancelAndExit("Prettier migration failed.", 1);
881
+ return true;
882
+ }
883
+ //#endregion
746
884
  //#region src/migration/migrator/tsconfig.ts
747
885
  function cleanupDeprecatedTsconfigOptions(projectPath, silent = false, report) {
748
886
  const deprecatedOptions = ["esModuleInterop", "allowSyntheticDefaultImports"];
@@ -767,6 +905,66 @@ function hasTsconfigTypesToRewrite(projectPath) {
767
905
  return findTsconfigFiles(projectPath).some((filePath) => hasTypesToRewriteInTsconfig(filePath));
768
906
  }
769
907
  //#endregion
908
+ //#region src/migration/migrator/framework-shim.ts
909
+ const FRAMEWORK_SHIMS = {
910
+ vue: [
911
+ "declare module '*.vue' {",
912
+ " import type { DefineComponent } from 'vue';",
913
+ " const component: DefineComponent<{}, {}, unknown>;",
914
+ " export default component;",
915
+ "}"
916
+ ].join("\n"),
917
+ astro: "/// <reference types=\"astro/client\" />"
918
+ };
919
+ function detectFramework(projectPath) {
920
+ const packageJsonPath = path.join(projectPath, "package.json");
921
+ if (!fs.existsSync(packageJsonPath)) return [];
922
+ const pkg = readJsonFile(packageJsonPath);
923
+ const allDeps = {
924
+ ...pkg.dependencies,
925
+ ...pkg.devDependencies
926
+ };
927
+ return ["vue", "astro"].filter((framework) => !!allDeps[framework]);
928
+ }
929
+ function getEnvDtsPath(projectPath) {
930
+ const srcEnvDts = path.join(projectPath, "src", "env.d.ts");
931
+ const rootEnvDts = path.join(projectPath, "env.d.ts");
932
+ for (const candidate of [srcEnvDts, rootEnvDts]) if (fs.existsSync(candidate)) return candidate;
933
+ return fs.existsSync(path.join(projectPath, "src")) ? srcEnvDts : rootEnvDts;
934
+ }
935
+ function hasFrameworkShim(projectPath, framework) {
936
+ const dirsToScan = [projectPath, path.join(projectPath, "src")];
937
+ for (const dir of dirsToScan) {
938
+ if (!fs.existsSync(dir)) continue;
939
+ let entries;
940
+ try {
941
+ entries = fs.readdirSync(dir);
942
+ } catch {
943
+ continue;
944
+ }
945
+ for (const entry of entries) {
946
+ if (!entry.endsWith(".d.ts")) continue;
947
+ const content = fs.readFileSync(path.join(dir, entry), "utf-8");
948
+ if (framework === "astro") {
949
+ if (content.includes("astro/client")) return true;
950
+ } else if (content.includes(`'*.${framework}'`) || content.includes(`"*.${framework}"`)) return true;
951
+ }
952
+ }
953
+ return false;
954
+ }
955
+ function addFrameworkShim(projectPath, framework, report) {
956
+ const envDtsPath = getEnvDtsPath(projectPath);
957
+ const shim = FRAMEWORK_SHIMS[framework];
958
+ if (fs.existsSync(envDtsPath)) {
959
+ const existing = fs.readFileSync(envDtsPath, "utf-8");
960
+ fs.writeFileSync(envDtsPath, `${existing.trimEnd()}\n\n${shim}\n`, "utf-8");
961
+ } else {
962
+ fs.mkdirSync(path.dirname(envDtsPath), { recursive: true });
963
+ fs.writeFileSync(envDtsPath, `${shim}\n`, "utf-8");
964
+ }
965
+ if (report) report.frameworkShimAdded = true;
966
+ }
967
+ //#endregion
770
968
  //#region src/migration/migrator/vitest-ecosystem.ts
771
969
  var import_dist = require_dist();
772
970
  const VITEST_ALIGN_EXCLUDED = /* @__PURE__ */ new Set(["@vitest/eslint-plugin", "@vitest/coverage-c8"]);
@@ -1251,7 +1449,7 @@ function expand_(str, max, isTop) {
1251
1449
  }
1252
1450
  //#endregion
1253
1451
  //#region ../../node_modules/.pnpm/minimatch@10.2.4/node_modules/minimatch/dist/esm/assert-valid-pattern.js
1254
- const MAX_PATTERN_LENGTH = 1024 * 64;
1452
+ const MAX_PATTERN_LENGTH = 65536;
1255
1453
  const assertValidPattern = (pattern) => {
1256
1454
  if (typeof pattern !== "string") throw new TypeError("invalid pattern");
1257
1455
  if (pattern.length > MAX_PATTERN_LENGTH) throw new TypeError("pattern is too long");
@@ -2487,9 +2685,10 @@ var Minimatch = class {
2487
2685
  const next = pp[i + 1];
2488
2686
  const prev = pp[i - 1];
2489
2687
  if (p !== GLOBSTAR || prev === GLOBSTAR) return;
2490
- if (prev === void 0) if (next !== void 0 && next !== GLOBSTAR) pp[i + 1] = "(?:\\/|" + twoStar + "\\/)?" + next;
2491
- else pp[i] = twoStar;
2492
- else if (next === void 0) pp[i - 1] = prev + "(?:\\/|\\/" + twoStar + ")?";
2688
+ if (prev === void 0) {
2689
+ if (next !== void 0 && next !== GLOBSTAR) pp[i + 1] = "(?:\\/|" + twoStar + "\\/)?" + next;
2690
+ else pp[i] = twoStar;
2691
+ } else if (next === void 0) pp[i - 1] = prev + "(?:\\/|\\/" + twoStar + ")?";
2493
2692
  else if (next !== GLOBSTAR) {
2494
2693
  pp[i - 1] = prev + "(?:\\/|\\/" + twoStar + "\\/)" + next;
2495
2694
  pp[i + 1] = GLOBSTAR;
@@ -2556,7 +2755,6 @@ minimatch.escape = escape;
2556
2755
  minimatch.unescape = unescape;
2557
2756
  //#endregion
2558
2757
  //#region ../../node_modules/.pnpm/lru-cache@11.2.7/node_modules/lru-cache/dist/esm/index.min.js
2559
- var import_cross_spawn = /* @__PURE__ */ __toESM(require_cross_spawn(), 1);
2560
2758
  var x = typeof performance == "object" && performance && typeof performance.now == "function" ? performance : Date;
2561
2759
  var I = /* @__PURE__ */ new Set();
2562
2760
  var R = typeof process == "object" && process ? process : {};
@@ -2748,7 +2946,7 @@ var L = class c {
2748
2946
  }
2749
2947
  }
2750
2948
  getRemainingTTL(t) {
2751
- return this.#s.has(t) ? Infinity : 0;
2949
+ return this.#s.has(t) ? 1 / 0 : 0;
2752
2950
  }
2753
2951
  #j() {
2754
2952
  let t = new z(this.#o), e = new z(this.#o);
@@ -2788,7 +2986,7 @@ var L = class c {
2788
2986
  let r = this.#s.get(h);
2789
2987
  if (r === void 0) return 0;
2790
2988
  let a = t[r], w = e[r];
2791
- if (!a || !w) return Infinity;
2989
+ if (!a || !w) return 1 / 0;
2792
2990
  return a - ((n || o()) - w);
2793
2991
  }, this.#p = (h) => {
2794
2992
  let r = e[h], a = t[h];
@@ -3982,12 +4180,15 @@ while (this[FLUSHCHUNK](this[BUFFERSHIFT]()) && this[BUFFER].length);
3982
4180
  return isStream;
3983
4181
  }
3984
4182
  };
4183
+ //#endregion
4184
+ //#region ../../node_modules/.pnpm/path-scurry@2.0.1/node_modules/path-scurry/dist/esm/index.js
4185
+ const realpathSync$2 = realpathSync$1.native;
3985
4186
  const defaultFS = {
3986
- lstatSync,
4187
+ lstatSync: lstatSync$1,
3987
4188
  readdir,
3988
- readdirSync,
4189
+ readdirSync: readdirSync$1,
3989
4190
  readlinkSync,
3990
- realpathSync: realpathSync$1.native,
4191
+ realpathSync: realpathSync$2,
3991
4192
  promises: {
3992
4193
  lstat,
3993
4194
  readdir: readdir$1,
@@ -4025,7 +4226,7 @@ const ENOREALPATH = 512;
4025
4226
  const ENOCHILD = 704;
4026
4227
  const TYPEMASK = 1023;
4027
4228
  const entToType = (s) => s.isFile() ? IFREG : s.isDirectory() ? IFDIR : s.isSymbolicLink() ? IFLNK : s.isCharacterDevice() ? IFCHR : s.isBlockDevice() ? IFBLK : s.isSocket() ? IFSOCK : s.isFIFO() ? IFIFO : UNKNOWN;
4028
- const normalizeCache = new L({ max: 2 ** 12 });
4229
+ const normalizeCache = new L({ max: 4096 });
4029
4230
  const normalize$1 = (s) => {
4030
4231
  const c = normalizeCache.get(s);
4031
4232
  if (c) return c;
@@ -4033,7 +4234,7 @@ const normalize$1 = (s) => {
4033
4234
  normalizeCache.set(s, n);
4034
4235
  return n;
4035
4236
  };
4036
- const normalizeNocaseCache = new L({ max: 2 ** 12 });
4237
+ const normalizeNocaseCache = new L({ max: 4096 });
4037
4238
  const normalizeNocase = (s) => {
4038
4239
  const c = normalizeNocaseCache.get(s);
4039
4240
  if (c) return c;
@@ -4055,7 +4256,7 @@ var ResolveCache = class extends L {
4055
4256
  * @internal
4056
4257
  */
4057
4258
  var ChildrenCache = class extends L {
4058
- constructor(maxSize = 16 * 1024) {
4259
+ constructor(maxSize = 16384) {
4059
4260
  super({
4060
4261
  maxSize,
4061
4262
  sizeCalculation: (a) => a.length + 1
@@ -4388,7 +4589,7 @@ var PathBase = class {
4388
4589
  return this[`is${type}`]();
4389
4590
  }
4390
4591
  getType() {
4391
- return this.isUnknown() ? "Unknown" : this.isDirectory() ? "Directory" : this.isFile() ? "File" : this.isSymbolicLink() ? "SymbolicLink" : this.isFIFO() ? "FIFO" : this.isCharacterDevice() ? "CharacterDevice" : this.isBlockDevice() ? "BlockDevice" : /* c8 ignore start */ this.isSocket() ? "Socket" : "Unknown";
4592
+ return this.isUnknown() ? "Unknown" : this.isDirectory() ? "Directory" : this.isFile() ? "File" : this.isSymbolicLink() ? "SymbolicLink" : this.isFIFO() ? "FIFO" : this.isCharacterDevice() ? "CharacterDevice" : this.isBlockDevice() ? "BlockDevice" : /* c8 ignore start */ this.isSocket() ? "Socket" : "Unknown";
4392
4593
  /* c8 ignore stop */
4393
4594
  }
4394
4595
  /**
@@ -5013,7 +5214,7 @@ var PathScurryBase = class {
5013
5214
  *
5014
5215
  * @internal
5015
5216
  */
5016
- constructor(cwd = process.cwd(), pathImpl, sep, { nocase, childrenCacheSize = 16 * 1024, fs = defaultFS } = {}) {
5217
+ constructor(cwd = process.cwd(), pathImpl, sep, { nocase, childrenCacheSize = 16384, fs = defaultFS } = {}) {
5017
5218
  this.#fs = fsFromOption(fs);
5018
5219
  if (cwd instanceof URL || cwd.startsWith("file://")) cwd = fileURLToPath(cwd);
5019
5220
  const cwdPath = pathImpl.resolve(cwd);
@@ -5753,8 +5954,10 @@ var Ignore = class {
5753
5954
  const absolute = p.isAbsolute();
5754
5955
  if (absolute) this.absolute.push(m);
5755
5956
  else this.relative.push(m);
5756
- if (children) if (absolute) this.absoluteChildren.push(m);
5757
- else this.relativeChildren.push(m);
5957
+ if (children) {
5958
+ if (absolute) this.absoluteChildren.push(m);
5959
+ else this.relativeChildren.push(m);
5960
+ }
5758
5961
  }
5759
5962
  }
5760
5963
  ignored(p) {
@@ -6504,6 +6707,96 @@ const glob = Object.assign(glob_, {
6504
6707
  });
6505
6708
  glob.glob = glob;
6506
6709
  //#endregion
6710
+ //#region src/utils/workspace.ts
6711
+ async function detectWorkspace$1(rootDir) {
6712
+ const bindingResult = await detectWorkspace(rootDir);
6713
+ const result = {
6714
+ rootDir,
6715
+ packageManager: void 0,
6716
+ packageManagerVersion: "latest",
6717
+ isMonorepo: false,
6718
+ monorepoScope: "",
6719
+ workspacePatterns: [],
6720
+ parentDirs: [],
6721
+ packages: []
6722
+ };
6723
+ if (bindingResult.packageManagerName) result.packageManager = bindingResult.packageManagerName;
6724
+ if (bindingResult.packageManagerVersion) result.packageManagerVersion = bindingResult.packageManagerVersion;
6725
+ if (bindingResult.isMonorepo) result.isMonorepo = bindingResult.isMonorepo;
6726
+ if (bindingResult.root) result.rootDir = bindingResult.root;
6727
+ if (result.isMonorepo) {
6728
+ const pnpmWorkspaceFile = path.join(result.rootDir, "pnpm-workspace.yaml");
6729
+ const packageJsonFile = path.join(result.rootDir, "package.json");
6730
+ if (fs.existsSync(pnpmWorkspaceFile)) {
6731
+ const workspaceConfig = readYamlFile(pnpmWorkspaceFile);
6732
+ if (Array.isArray(workspaceConfig.packages)) result.workspacePatterns = workspaceConfig.packages;
6733
+ } else if (fs.existsSync(packageJsonFile)) {
6734
+ const pkg = readJsonFile(packageJsonFile);
6735
+ if (Array.isArray(pkg.workspaces)) result.workspacePatterns = pkg.workspaces;
6736
+ else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) result.workspacePatterns = pkg.workspaces.packages;
6737
+ }
6738
+ const dirs = /* @__PURE__ */ new Set();
6739
+ for (const pattern of result.workspacePatterns) {
6740
+ if (!pattern.endsWith("*")) continue;
6741
+ const dir = pattern.replace(/\/\*{1,2}$/, "");
6742
+ if (dir) dirs.add(dir);
6743
+ }
6744
+ result.parentDirs = Array.from(dirs).sort();
6745
+ const pkg = readJsonFile(packageJsonFile);
6746
+ if (pkg.name) result.monorepoScope = getScopeFromPackageName(pkg.name);
6747
+ result.packages = discoverWorkspacePackages(result.workspacePatterns, result.rootDir);
6748
+ }
6749
+ return result;
6750
+ }
6751
+ function isBingoTemplate(pkg) {
6752
+ return !!pkg.dependencies?.bingo;
6753
+ }
6754
+ function discoverWorkspacePackages(workspacePatterns, rootDir) {
6755
+ const packages = [];
6756
+ if (workspacePatterns.length === 0) return packages;
6757
+ const packageJsonRelativePaths = globSync(workspacePatterns.map((pattern) => `${pattern}/package.json`), {
6758
+ absolute: false,
6759
+ cwd: rootDir,
6760
+ ignore: ["**/node_modules/**"]
6761
+ });
6762
+ for (const packageJsonRelativePath of packageJsonRelativePaths) {
6763
+ const packageJsonPath = path.join(rootDir, packageJsonRelativePath);
6764
+ const pkg = readJsonFile(packageJsonPath);
6765
+ if (!pkg.name) continue;
6766
+ packages.push({
6767
+ name: pkg.name,
6768
+ path: path.dirname(packageJsonRelativePath).split(path.sep).join("/"),
6769
+ description: pkg.description,
6770
+ version: pkg.version
6771
+ });
6772
+ }
6773
+ return packages;
6774
+ }
6775
+ function updatePackageJsonWithDeps(rootDir, projectDir, dependencies, dependencyType) {
6776
+ const packageJsonPath = path.join(rootDir, projectDir, "package.json");
6777
+ editJsonFile(packageJsonPath, (pkg) => {
6778
+ if (!pkg[dependencyType]) pkg[dependencyType] = {};
6779
+ for (const dep of dependencies) pkg[dependencyType][dep] = "workspace:*";
6780
+ return pkg;
6781
+ });
6782
+ }
6783
+ function updateWorkspaceConfig(projectPath, workspaceInfo) {
6784
+ for (const pattern of workspaceInfo.workspacePatterns) if (minimatch(projectPath, pattern)) return;
6785
+ const parentDir = path.dirname(projectPath);
6786
+ const pattern = parentDir === "." ? projectPath : `${parentDir}/*`;
6787
+ if (workspaceInfo.packageManager === PackageManager.pnpm) editYamlFile(path.join(workspaceInfo.rootDir, "pnpm-workspace.yaml"), (doc) => {
6788
+ let packages = doc.getIn(["packages"]);
6789
+ if (!packages) packages = new import_dist.YAMLSeq();
6790
+ packages.add(new import_dist.Scalar(pattern));
6791
+ doc.setIn(["packages"], packages);
6792
+ });
6793
+ else editJsonFile(path.join(workspaceInfo.rootDir, "package.json"), (pkg) => {
6794
+ if (pkg.workspaces && !Array.isArray(pkg.workspaces)) pkg.workspaces.packages = [...pkg.workspaces.packages || [], pattern];
6795
+ else pkg.workspaces = [...pkg.workspaces || [], pattern];
6796
+ return pkg;
6797
+ });
6798
+ }
6799
+ //#endregion
6507
6800
  //#region src/migration/migrator/catalog.ts
6508
6801
  const BROWSER_PROVIDER_POSTINSTALL_PACKAGES = ["edgedriver", "geckodriver"];
6509
6802
  const PUBLIC_PEER_DEPENDENCY_FALLBACKS = {
@@ -6684,7 +6977,8 @@ function rewritePnpmWorkspaceYaml(projectPath, pnpmMajorVersion, shouldAllowBrow
6684
6977
  * so all overrides must live in pnpm-workspace.yaml.
6685
6978
  */
6686
6979
  function migratePnpmOverridesToWorkspaceYaml(projectPath, overrides) {
6687
- editYamlFile(path.join(projectPath, "pnpm-workspace.yaml"), (doc) => {
6980
+ const pnpmWorkspaceYamlPath = path.join(projectPath, "pnpm-workspace.yaml");
6981
+ editYamlFile(pnpmWorkspaceYamlPath, (doc) => {
6688
6982
  for (const [key, value] of Object.entries(overrides)) doc.setIn(["overrides", scalarString(key)], scalarString(value));
6689
6983
  });
6690
6984
  }
@@ -7801,20 +8095,21 @@ function ensureVitePlusBootstrap(workspaceInfo, report) {
7801
8095
  });
7802
8096
  result.packageJson = result.packageJson || childChanged;
7803
8097
  }
7804
- if (workspaceInfo.packageManager === PackageManager.pnpm) if (usePnpmWorkspaceYaml) {
7805
- const before = readTextFileIfExists(pnpmWorkspaceYamlPath);
7806
- migratePnpmSettingsToWorkspaceYaml(projectPath, movedPnpmSettings);
7807
- const catalogDependencyResolver = readPnpmWorkspaceCatalogDependencyResolver(projectPath);
7808
- if (movedPnpmSettings !== void 0 || result.packageJson || ecosystemCatalogReferencesPending || !pnpmWorkspaceExoticSubdepsSettingSatisfied(projectPath) || pnpmWorkspaceMinimumReleaseAgeExemptionsPending(projectPath) || workspaceCatalogVitePlusDependencyPending(projectPath, workspaceInfo.packages, catalogDependencyResolver) || !overridesSatisfyVitePlus(readPnpmWorkspaceOverrides(projectPath), usesVitest, catalogDependencyResolver) || !pnpmPeerDependencyRulesSatisfyVitePlus(readPnpmWorkspacePeerDependencyRules(projectPath), usesVitest)) rewritePnpmWorkspaceYaml(projectPath, pnpmMajorVersion, shouldAllowBrowserBuilds, usesVitest, vitestEcosystemPackages, true, providerCatalogAdditions);
7809
- if (fs.existsSync(pnpmWorkspaceYamlPath)) ensurePnpmWorkspacePackages(projectPath, workspaceInfo.workspacePatterns);
7810
- result.packageManagerConfig = before !== readTextFileIfExists(pnpmWorkspaceYamlPath);
7811
- } else {
7812
- const exoticChanged = ensurePnpmWorkspaceExoticSubdepsSetting(projectPath);
7813
- if (fs.existsSync(pnpmWorkspaceYamlPath)) ensurePnpmWorkspacePackages(projectPath, workspaceInfo.workspacePatterns);
7814
- const after = readTextFileIfExists(pnpmWorkspaceYamlPath);
7815
- result.packageManagerConfig = exoticChanged || pnpmWorkspaceCatalogBefore !== after;
7816
- }
7817
- else if (workspaceInfo.packageManager === PackageManager.yarn) {
8098
+ if (workspaceInfo.packageManager === PackageManager.pnpm) {
8099
+ if (usePnpmWorkspaceYaml) {
8100
+ const before = readTextFileIfExists(pnpmWorkspaceYamlPath);
8101
+ migratePnpmSettingsToWorkspaceYaml(projectPath, movedPnpmSettings);
8102
+ const catalogDependencyResolver = readPnpmWorkspaceCatalogDependencyResolver(projectPath);
8103
+ if (movedPnpmSettings !== void 0 || result.packageJson || ecosystemCatalogReferencesPending || !pnpmWorkspaceExoticSubdepsSettingSatisfied(projectPath) || pnpmWorkspaceMinimumReleaseAgeExemptionsPending(projectPath) || workspaceCatalogVitePlusDependencyPending(projectPath, workspaceInfo.packages, catalogDependencyResolver) || !overridesSatisfyVitePlus(readPnpmWorkspaceOverrides(projectPath), usesVitest, catalogDependencyResolver) || !pnpmPeerDependencyRulesSatisfyVitePlus(readPnpmWorkspacePeerDependencyRules(projectPath), usesVitest)) rewritePnpmWorkspaceYaml(projectPath, pnpmMajorVersion, shouldAllowBrowserBuilds, usesVitest, vitestEcosystemPackages, true, providerCatalogAdditions);
8104
+ if (fs.existsSync(pnpmWorkspaceYamlPath)) ensurePnpmWorkspacePackages(projectPath, workspaceInfo.workspacePatterns);
8105
+ result.packageManagerConfig = before !== readTextFileIfExists(pnpmWorkspaceYamlPath);
8106
+ } else {
8107
+ const exoticChanged = ensurePnpmWorkspaceExoticSubdepsSetting(projectPath);
8108
+ if (fs.existsSync(pnpmWorkspaceYamlPath)) ensurePnpmWorkspacePackages(projectPath, workspaceInfo.workspacePatterns);
8109
+ const after = readTextFileIfExists(pnpmWorkspaceYamlPath);
8110
+ result.packageManagerConfig = exoticChanged || pnpmWorkspaceCatalogBefore !== after;
8111
+ }
8112
+ } else if (workspaceInfo.packageManager === PackageManager.yarn) {
7818
8113
  const yarnrcYmlPath = path.join(projectPath, ".yarnrc.yml");
7819
8114
  const before = readTextFileIfExists(yarnrcYmlPath);
7820
8115
  rewriteYarnrcYml(projectPath, usesVitest, vitestEcosystemPackages, providerCatalogAdditions, supportCatalog);
@@ -8143,7 +8438,8 @@ function mergeTsdownConfigFile(projectPath, silent = false, report) {
8143
8438
  mergeAndRemoveJsonConfig(projectPath, viteConfig, configs.tsdownConfig, "pack", silent, report);
8144
8439
  return;
8145
8440
  }
8146
- const result = mergeTsdownConfig(fullViteConfigPath, `./${configs.tsdownConfig}`);
8441
+ const tsdownRelativePath = `./${configs.tsdownConfig}`;
8442
+ const result = mergeTsdownConfig(fullViteConfigPath, tsdownRelativePath);
8147
8443
  if (result.updated) {
8148
8444
  fs.writeFileSync(fullViteConfigPath, result.content);
8149
8445
  if (report) report.tsdownImportCount++;
@@ -8168,7 +8464,8 @@ function mergeViteConfigFiles(projectPath, silent = false, report, packages, wor
8168
8464
  } else warnMigration(BASEURL_TSCONFIG_WARNING, report);
8169
8465
  sanitizeMigratedOxlintConfig(oxlintJson, collectInstalledPackageNames(workspaceRoot ?? projectPath, packages), report);
8170
8466
  ensureSvelteRuneGlobals(oxlintJson);
8171
- writeJsonFile(fullOxlintPath, ensureVitePlusImportRuleDefaults(oxlintJson));
8467
+ const normalizedOxlintConfig = ensureVitePlusImportRuleDefaults(oxlintJson);
8468
+ writeJsonFile(fullOxlintPath, normalizedOxlintConfig);
8172
8469
  mergeAndRemoveJsonConfig(projectPath, viteConfig, configs.oxlintConfig, "lint", silent, report);
8173
8470
  }
8174
8471
  if (configs.oxfmtConfig) mergeAndRemoveJsonConfig(projectPath, viteConfig, configs.oxfmtConfig, "fmt", silent, report);
@@ -8306,107 +8603,114 @@ function rewriteAllImports(projectPath, silent = false, report, preserveNuxtVite
8306
8603
  log.success(`Rewrote imports in ${modified === 1 ? "one file" : `${modified} files`}`);
8307
8604
  log.info(result.modifiedFiles.map((file) => ` ${displayRelative(file)}`).join("\n"));
8308
8605
  }
8309
- if (errors > 0) if (report) warnMigration(`${errors === 1 ? "one file had an error" : `${errors} files had errors`} while rewriting imports`, report);
8310
- else {
8311
- log.warn(`⚠ ${errors === 1 ? "one file had an error" : `${errors} files had errors`}:`);
8312
- for (const error of result.errors) log.error(` ${displayRelative(error.path)}: ${error.message}`);
8606
+ if (errors > 0) {
8607
+ if (report) warnMigration(`${errors === 1 ? "one file had an error" : `${errors} files had errors`} while rewriting imports`, report);
8608
+ else {
8609
+ log.warn(`⚠ ${errors === 1 ? "one file had an error" : `${errors} files had errors`}:`);
8610
+ for (const error of result.errors) log.error(` ${displayRelative(error.path)}: ${error.message}`);
8611
+ }
8313
8612
  }
8314
8613
  return modified > 0;
8315
8614
  }
8316
8615
  //#endregion
8317
- //#region src/migration/migrator/git-hooks.ts
8616
+ //#region src/utils/git.ts
8617
+ var import_cross_spawn = /* @__PURE__ */ __toESM(require_cross_spawn(), 1);
8318
8618
  /**
8319
- * Check if the project has an unsupported husky version (<9.0.0).
8320
- * Uses `semver.coerce` to handle ranges like `^8.0.0` `8.0.0`.
8321
- * When the specifier is a catalog reference (e.g. `"catalog:"`), resolves
8322
- * it from the active package manager's catalog first — a `catalog:` spec is
8323
- * only meaningful to the manager that owns the workspace, so we never read a
8324
- * leftover/foreign catalog file. When it is still not coercible (e.g.
8325
- * `"latest"`), falls back to the installed version in node_modules via
8326
- * `detectPackageMetadata`.
8327
- * Returns a reason string if hooks migration should be skipped, or null
8328
- * if husky is absent or compatible.
8619
+ * Walk up from `startPath` looking for `.git` (directory or file — submodules
8620
+ * use a `.git` file). Returns the directory that contains `.git`, or `null`.
8329
8621
  */
8330
- function checkUnsupportedHuskyVersion(projectPath, deps, prodDeps, packageManager) {
8331
- const huskyVersion = deps?.husky ?? prodDeps?.husky;
8332
- if (!huskyVersion) return null;
8333
- let coerced = import_semver.default.coerce(huskyVersion);
8334
- if (coerced == null && packageManager != null && huskyVersion.startsWith("catalog:")) {
8335
- const resolved = createCatalogDependencyResolver(projectPath, packageManager)?.(huskyVersion, "husky");
8336
- if (resolved) coerced = import_semver.default.coerce(resolved);
8337
- }
8338
- if (coerced == null) {
8339
- const installed = detectPackageMetadata(projectPath, "husky");
8340
- if (installed) coerced = import_semver.default.coerce(installed.version);
8341
- 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.`;
8342
- }
8343
- 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.";
8344
- return null;
8622
+ function findGitRoot(startPath) {
8623
+ let dir = startPath;
8624
+ while (true) {
8625
+ if (fs.existsSync(path.join(dir, ".git"))) return dir;
8626
+ const parent = path.dirname(dir);
8627
+ if (parent === dir) return null;
8628
+ dir = parent;
8629
+ }
8630
+ }
8631
+ async function initGitRepository(cwd) {
8632
+ return (await runCommandSilently({
8633
+ command: "git",
8634
+ args: ["init"],
8635
+ cwd,
8636
+ envs: process.env
8637
+ })).exitCode === 0;
8345
8638
  }
8639
+ //#endregion
8640
+ //#region src/migration/migrator/git-hooks.ts
8346
8641
  const OTHER_HOOK_TOOLS = [
8347
8642
  "simple-git-hooks",
8348
8643
  "lefthook",
8349
8644
  "yorkie"
8350
8645
  ];
8351
- const REPLACED_HOOK_PACKAGES = ["husky", "lint-staged"];
8352
- function removeReplacedHookPackages(packageJsonPath) {
8646
+ const SUPPORTED_GIT_HOOK_NAME_SET = new Set(SUPPORTED_GIT_HOOK_NAMES);
8647
+ function removeReplacedStagedPackage(packageJsonPath) {
8353
8648
  editJsonFile(packageJsonPath, (pkg) => {
8354
- for (const name of REPLACED_HOOK_PACKAGES) {
8355
- if (pkg.devDependencies?.[name]) delete pkg.devDependencies[name];
8356
- if (pkg.dependencies?.[name]) delete pkg.dependencies[name];
8357
- }
8649
+ if (pkg.devDependencies?.["lint-staged"]) delete pkg.devDependencies["lint-staged"];
8650
+ if (pkg.dependencies?.["lint-staged"]) delete pkg.dependencies["lint-staged"];
8358
8651
  return pkg;
8359
8652
  });
8360
8653
  }
8361
8654
  function detectLegacyGitHooksMigrationCandidate(projectPath) {
8362
8655
  const packageJsonPath = path.join(projectPath, "package.json");
8363
8656
  if (!fs.existsSync(packageJsonPath)) return false;
8364
- const pkg = readJsonFile(packageJsonPath);
8365
- return getOldHooksDir(projectPath) !== void 0 || pkg["lint-staged"] !== void 0;
8657
+ return readJsonFile(packageJsonPath)["lint-staged"] !== void 0;
8366
8658
  }
8367
- /**
8368
- * Walk up from `startPath` looking for `.git` (directory or file submodules
8369
- * use a `.git` file). Returns the directory that contains `.git`, or `null`.
8370
- */
8371
- function findGitRoot(startPath) {
8372
- let dir = startPath;
8373
- while (true) {
8374
- if (fs.existsSync(path.join(dir, ".git"))) return dir;
8375
- const parent = path.dirname(dir);
8376
- if (parent === dir) return null;
8377
- dir = parent;
8378
- }
8659
+ function hasHuskySetup(projectPath, pkg) {
8660
+ return pkg.devDependencies?.husky !== void 0 || pkg.dependencies?.husky !== void 0 || pkg.optionalDependencies?.husky !== void 0 || pkg.peerDependencies?.husky !== void 0 || pkg.husky !== void 0 || (pkg.scripts?.prepare ? /\bhusky\b/.test(pkg.scripts.prepare) : false) || fs.existsSync(path.join(projectPath, ".husky"));
8379
8661
  }
8380
- /**
8381
- * Normalize "husky install [dir]""husky [dir]" so downstream regex
8382
- * and ast-grep rules can match a single pattern.
8383
- */
8384
- function collapseHuskyInstall(script) {
8385
- return script.replace("husky install ", "husky ").replace("husky install", "husky");
8662
+ function hasExistingViteHooksPolicy(projectPath) {
8663
+ const hooksDir = path.join(projectPath, ".vite-hooks");
8664
+ if (!fs.lstatSync(hooksDir, { throwIfNoEntry: false })?.isDirectory()) return false;
8665
+ return fs.readdirSync(hooksDir, { withFileTypes: true }).some((entry) => entry.isFile() && SUPPORTED_GIT_HOOK_NAME_SET.has(entry.name));
8386
8666
  }
8387
- /**
8388
- * High-level helper: detect old hooks dir, set up git hooks, and rewrite
8389
- * the prepare script. Returns true if hooks were successfully installed.
8390
- */
8391
- function installGitHooks(projectPath, silent = false, report, packageManager) {
8392
- if (setupGitHooks(projectPath, getOldHooksDir(projectPath), silent, report, packageManager)) {
8393
- rewritePrepareScript(projectPath);
8394
- return true;
8667
+ function findNonRegularViteHookPath(projectPath) {
8668
+ for (const hookName of SUPPORTED_GIT_HOOK_NAMES) {
8669
+ const hookPath = path.join(projectPath, ".vite-hooks", hookName);
8670
+ const stats = fs.lstatSync(hookPath, { throwIfNoEntry: false });
8671
+ if (stats && !stats.isFile()) return path.join(".vite-hooks", hookName);
8395
8672
  }
8396
- return false;
8673
+ return null;
8674
+ }
8675
+ function findWorkspacePackageHookPolicy(workspaceRoot, packages) {
8676
+ for (const pkg of packages) {
8677
+ const packagePath = path.join(workspaceRoot, pkg.path);
8678
+ if (hasExistingViteHooksPolicy(packagePath)) return `Detected project-owned Vite+ hooks in workspace package "${pkg.path}" — leaving the existing hook setup unchanged.`;
8679
+ const packageJsonPath = path.join(packagePath, "package.json");
8680
+ const pkgContent = fs.existsSync(packageJsonPath) ? readJsonFile(packageJsonPath) : {};
8681
+ if (hasHuskySetup(packagePath, pkgContent)) return `Detected Husky in workspace package "${pkg.path}" — leaving its hooks, configuration, and dependencies unchanged.`;
8682
+ const deps = pkgContent.devDependencies;
8683
+ const prodDeps = pkgContent.dependencies;
8684
+ for (const tool of OTHER_HOOK_TOOLS) if (deps?.[tool] || prodDeps?.[tool] || pkgContent[tool]) return `Detected ${tool} in workspace package "${pkg.path}" — leaving the existing hook setup unchanged.`;
8685
+ const gitRoot = findGitRoot(packagePath);
8686
+ if (gitRoot && path.resolve(packagePath) === path.resolve(gitRoot)) {
8687
+ const configuredHooksPath = getConfiguredHooksPath(packagePath);
8688
+ const normalizedHooksPath = normalizeGitHooksPath(configuredHooksPath);
8689
+ if (configuredHooksPath && normalizedHooksPath !== ".vite-hooks/_") return `core.hooksPath is already set to "${configuredHooksPath}" in workspace package "${pkg.path}" — leaving the existing hook setup unchanged.`;
8690
+ }
8691
+ }
8692
+ return null;
8693
+ }
8694
+ function getConfiguredHooksPath(projectPath) {
8695
+ const result = import_cross_spawn.default.sync("git", [
8696
+ "config",
8697
+ "--get",
8698
+ "core.hooksPath"
8699
+ ], {
8700
+ cwd: projectPath,
8701
+ stdio: "pipe"
8702
+ });
8703
+ return result.status === 0 ? result.stdout?.toString().trim() ?? "" : "";
8704
+ }
8705
+ function normalizeGitHooksPath(hooksPath) {
8706
+ return path.posix.normalize(hooksPath.replaceAll("\\", "/")).replace(/\/$/, "");
8397
8707
  }
8398
8708
  /**
8399
- * Read-only probe: extract the old husky hooks directory from `scripts.prepare`
8400
- * without modifying package.json. Returns undefined when no husky reference is found.
8709
+ * High-level helper that sets up Vite+ hooks when no other hook tool owns the
8710
+ * project. Husky setups are deliberately preserved for a dedicated migration.
8401
8711
  */
8402
- function getOldHooksDir(rootDir) {
8403
- const packageJsonPath = path.join(rootDir, "package.json");
8404
- if (!fs.existsSync(packageJsonPath)) return;
8405
- const pkg = readJsonFile(packageJsonPath);
8406
- if (!pkg.scripts?.prepare) return;
8407
- const match = collapseHuskyInstall(pkg.scripts.prepare).match(/\bhusky(?:\s+([\w./-]+))?/);
8408
- if (!match) return;
8409
- return match[1] ?? ".husky";
8712
+ function installGitHooks(projectPath, silent = false, report, packageManager, packages = []) {
8713
+ return setupGitHooks(projectPath, silent, report, packageManager, packages);
8410
8714
  }
8411
8715
  /**
8412
8716
  * Pre-flight check: verify that git hooks can be set up for this project.
@@ -8416,10 +8720,9 @@ function getOldHooksDir(rootDir) {
8416
8720
  * These checks are deterministic and read-only — they do not modify
8417
8721
  * the project in any way, making them safe to call before migration.
8418
8722
  *
8419
- * `packageManager` is the project's detected manager; it scopes `catalog:`
8420
- * resolution to that manager's catalog so a foreign catalog file is ignored.
8723
+ * The package manager argument remains for API compatibility with callers.
8421
8724
  */
8422
- function preflightGitHooksSetup(projectPath, packageManager) {
8725
+ function preflightGitHooksSetup(projectPath, _packageManager, packages = []) {
8423
8726
  const gitRoot = findGitRoot(projectPath);
8424
8727
  if (gitRoot && path.resolve(projectPath) !== path.resolve(gitRoot)) return "Subdirectory project detected — skipping git hooks setup. Configure hooks at the repository root.";
8425
8728
  const packageJsonPath = path.join(projectPath, "package.json");
@@ -8427,19 +8730,41 @@ function preflightGitHooksSetup(projectPath, packageManager) {
8427
8730
  const pkgContent = readJsonFile(packageJsonPath);
8428
8731
  const deps = pkgContent.devDependencies;
8429
8732
  const prodDeps = pkgContent.dependencies;
8733
+ const configuredHooksPath = gitRoot ? getConfiguredHooksPath(projectPath) : "";
8734
+ const normalizedHooksPath = normalizeGitHooksPath(configuredHooksPath);
8735
+ if (hasHuskySetup(projectPath, pkgContent) || normalizedHooksPath === ".husky" || normalizedHooksPath.startsWith(".husky/")) return "Detected Husky — leaving its hooks, configuration, and dependencies unchanged. Migrate Husky manually before enabling Vite+ hooks.";
8430
8736
  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, see https://viteplus.dev/guide/migrate#git-hook-tools`;
8431
- const huskyReason = checkUnsupportedHuskyVersion(projectPath, deps, prodDeps, packageManager);
8432
- if (huskyReason) return huskyReason;
8737
+ const workspacePackageReason = findWorkspacePackageHookPolicy(projectPath, packages);
8738
+ if (workspacePackageReason) return workspacePackageReason;
8739
+ const disabledHooksEnvironment = [
8740
+ "HUSKY",
8741
+ "VP_GIT_HOOKS",
8742
+ "VITE_GIT_HOOKS"
8743
+ ].find((name) => process.env[name] === "0");
8744
+ if (disabledHooksEnvironment) return `Git hooks are disabled through ${disabledHooksEnvironment}=0 — skipping git hooks setup.`;
8745
+ if (configuredHooksPath && normalizedHooksPath !== ".vite-hooks/_") return `core.hooksPath is already set to "${configuredHooksPath}" — leaving the existing hook setup unchanged.`;
8746
+ const unsafeInstallPath = findUnsafeHookInstallPath(projectPath, ".vite-hooks");
8747
+ if (unsafeInstallPath) return `Git hook dispatcher path "${unsafeInstallPath.relativePath}" is unsafe — leaving the existing hook setup unchanged.`;
8748
+ const nonRegularHookPath = findNonRegularViteHookPath(projectPath);
8749
+ if (nonRegularHookPath) return `Git hook path "${nonRegularHookPath}" is not a regular file — leaving the existing hook setup unchanged.`;
8433
8750
  if (hasUnsupportedLintStagedConfig(projectPath)) return "Unsupported lint-staged config format — skipping git hooks setup. Please configure git hooks manually.";
8434
8751
  return null;
8435
8752
  }
8436
8753
  /**
8754
+ * Decide whether config rewriting must leave lint-staged configuration in
8755
+ * place. Call this after scaffolding but before rewriting the project so an
8756
+ * existing hook owner never observes a partially migrated configuration.
8757
+ */
8758
+ function shouldSkipStagedMigrationForHooks(projectPath, shouldSetupHooks, packageManager, packages = []) {
8759
+ return !shouldSetupHooks || hasExistingViteHooksPolicy(projectPath) || preflightGitHooksSetup(projectPath, packageManager, packages) !== null;
8760
+ }
8761
+ /**
8437
8762
  * Set up git hooks with husky + lint-staged via vp commands.
8438
8763
  * Skips if another hook tool is detected (warns user).
8439
8764
  * Returns true if hooks were successfully set up, false if skipped.
8440
8765
  */
8441
- function setupGitHooks(projectPath, oldHooksDir, silent = false, report, packageManager) {
8442
- const reason = preflightGitHooksSetup(projectPath, packageManager);
8766
+ function setupGitHooks(projectPath, silent = false, report, packageManager, packages = []) {
8767
+ const reason = preflightGitHooksSetup(projectPath, packageManager, packages);
8443
8768
  if (reason) {
8444
8769
  warnMigration(reason, report);
8445
8770
  return false;
@@ -8447,73 +8772,29 @@ function setupGitHooks(projectPath, oldHooksDir, silent = false, report, package
8447
8772
  const packageJsonPath = path.join(projectPath, "package.json");
8448
8773
  if (!fs.existsSync(packageJsonPath)) return false;
8449
8774
  const gitRoot = findGitRoot(projectPath);
8450
- const isCustomDir = oldHooksDir != null && oldHooksDir !== ".husky";
8451
- const hooksDir = isCustomDir ? oldHooksDir : ".vite-hooks";
8775
+ const hooksDir = ".vite-hooks";
8776
+ const hasExistingHookPolicy = hasExistingViteHooksPolicy(projectPath);
8452
8777
  editJsonFile(packageJsonPath, (pkg) => {
8453
8778
  if (!pkg.scripts) pkg.scripts = {};
8454
8779
  if (!pkg.scripts.prepare) pkg.scripts.prepare = "vp config";
8455
- else if (!pkg.scripts.prepare.includes("vp config") && !/\bhusky\b/.test(pkg.scripts.prepare)) pkg.scripts.prepare = `vp config && ${pkg.scripts.prepare}`;
8780
+ else if (!pkg.scripts.prepare.includes("vp config")) pkg.scripts.prepare = `vp config && ${pkg.scripts.prepare}`;
8456
8781
  return pkg;
8457
8782
  });
8458
8783
  let stagedMerged = hasStagedConfigInViteConfig(projectPath);
8459
8784
  const hasStandaloneConfig = hasStandaloneLintStagedConfig(projectPath);
8460
- if (!stagedMerged && !hasStandaloneConfig) {
8785
+ if (!hasExistingHookPolicy && !stagedMerged && !hasStandaloneConfig) {
8461
8786
  const stagedConfig = readJsonFile(packageJsonPath)?.["lint-staged"] ?? DEFAULT_STAGED_CONFIG;
8462
8787
  const updated = rewriteScripts(JSON.stringify(stagedConfig), readRulesYaml());
8463
8788
  stagedMerged = mergeStagedConfigToViteConfig(projectPath, updated ? JSON.parse(updated) : stagedConfig, silent, report);
8464
8789
  }
8465
- if (stagedMerged) removeLintStagedFromPackageJson(packageJsonPath);
8466
- if (oldHooksDir && !isCustomDir) {
8467
- const oldDir = path.join(projectPath, oldHooksDir);
8468
- if (fs.existsSync(oldDir)) {
8469
- const targetDir = path.join(projectPath, hooksDir);
8470
- fs.mkdirSync(targetDir, { recursive: true });
8471
- for (const entry of fs.readdirSync(oldDir, { withFileTypes: true })) {
8472
- if (entry.isDirectory() || entry.name.startsWith(".")) continue;
8473
- const src = path.join(oldDir, entry.name);
8474
- const dest = path.join(targetDir, entry.name);
8475
- fs.copyFileSync(src, dest);
8476
- fs.chmodSync(dest, 493);
8477
- }
8478
- fs.rmSync(oldDir, {
8479
- recursive: true,
8480
- force: true
8481
- });
8482
- }
8483
- }
8484
- if (stagedMerged) createPreCommitHook(projectPath, hooksDir);
8790
+ if (!hasExistingHookPolicy && stagedMerged) removeLintStagedFromPackageJson(packageJsonPath);
8791
+ if (!hasExistingHookPolicy && stagedMerged) createPreCommitHook(projectPath, hooksDir);
8485
8792
  if (!gitRoot) {
8486
- removeReplacedHookPackages(packageJsonPath);
8793
+ if (!hasExistingHookPolicy && stagedMerged) removeReplacedStagedPackage(packageJsonPath);
8487
8794
  return true;
8488
8795
  }
8489
- if (oldHooksDir) {
8490
- const checkResult = import_cross_spawn.default.sync("git", [
8491
- "config",
8492
- "--local",
8493
- "core.hooksPath"
8494
- ], {
8495
- cwd: projectPath,
8496
- stdio: "pipe"
8497
- });
8498
- const existingPath = checkResult.status === 0 ? checkResult.stdout?.toString().trim() : "";
8499
- if (existingPath === `${oldHooksDir}/_` || existingPath === oldHooksDir) import_cross_spawn.default.sync("git", [
8500
- "config",
8501
- "--local",
8502
- "--unset",
8503
- "core.hooksPath"
8504
- ], {
8505
- cwd: projectPath,
8506
- stdio: "pipe"
8507
- });
8508
- }
8509
8796
  const vpBin = process.env.VP_CLI_BIN ?? "vp";
8510
- const configArgs = isCustomDir ? [
8511
- "config",
8512
- "--no-agent",
8513
- "--hooks-dir",
8514
- hooksDir
8515
- ] : ["config", "--no-agent"];
8516
- const configResult = import_cross_spawn.default.sync(vpBin, configArgs, {
8797
+ const configResult = import_cross_spawn.default.sync(vpBin, ["config", "--no-agent"], {
8517
8798
  cwd: projectPath,
8518
8799
  stdio: "pipe"
8519
8800
  });
@@ -8523,7 +8804,7 @@ function setupGitHooks(projectPath, oldHooksDir, silent = false, report, package
8523
8804
  warnMigration(`Git hooks not configured — ${stdout}`, report);
8524
8805
  return false;
8525
8806
  }
8526
- removeReplacedHookPackages(packageJsonPath);
8807
+ if (!hasExistingHookPolicy && stagedMerged) removeReplacedStagedPackage(packageJsonPath);
8527
8808
  if (report) report.gitHooksConfigured = true;
8528
8809
  if (!silent) log.success("✔ Git hooks configured");
8529
8810
  return true;
@@ -8548,85 +8829,14 @@ function hasUnsupportedLintStagedConfig(projectPath) {
8548
8829
  if (fs.existsSync(lintstagedrcPath) && !isJsonFile(lintstagedrcPath)) return true;
8549
8830
  return false;
8550
8831
  }
8551
- /**
8552
- * Create pre-commit hook file in the hooks directory.
8553
- */
8554
- 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/];
8555
8832
  const DEFAULT_STAGED_CONFIG = { "*": "vp check --fix" };
8556
- /**
8557
- * Ensure the pre-commit hook exists with `vp staged`, and that
8558
- * vite.config.ts contains a `staged` block (using the default config
8559
- * if none is present). Called by `vp config` after hook installation.
8560
- */
8561
- function ensurePreCommitHook(projectPath, dir = ".vite-hooks") {
8562
- if (!hasStagedConfigInViteConfig(projectPath)) mergeStagedConfigToViteConfig(projectPath, DEFAULT_STAGED_CONFIG, true);
8563
- createPreCommitHook(projectPath, dir);
8564
- }
8565
8833
  function createPreCommitHook(projectPath, dir = ".vite-hooks") {
8566
8834
  const huskyDir = path.join(projectPath, dir);
8567
8835
  fs.mkdirSync(huskyDir, { recursive: true });
8568
8836
  const hookPath = path.join(huskyDir, "pre-commit");
8569
- if (fs.existsSync(hookPath)) {
8570
- const existing = fs.readFileSync(hookPath, "utf8");
8571
- if (existing.includes("vp staged")) return;
8572
- const lines = existing.split("\n");
8573
- let replaced = false;
8574
- const result = [];
8575
- for (const line of lines) {
8576
- const trimmed = line.trim();
8577
- if (!replaced) {
8578
- let matched = false;
8579
- for (const pattern of STALE_LINT_STAGED_PATTERNS) {
8580
- const match = pattern.exec(trimmed);
8581
- if (match) {
8582
- const parts = [
8583
- match[1]?.trim() ?? "",
8584
- "vp staged",
8585
- trimmed.slice(match[0].length).trim()
8586
- ].filter(Boolean);
8587
- result.push(parts.join(" "));
8588
- replaced = true;
8589
- matched = true;
8590
- break;
8591
- }
8592
- }
8593
- if (matched) continue;
8594
- }
8595
- result.push(line);
8596
- }
8597
- if (!replaced) fs.writeFileSync(hookPath, `${result.join("\n").trimEnd()}\nvp staged\n`);
8598
- else fs.writeFileSync(hookPath, result.join("\n"));
8599
- } else {
8600
- fs.writeFileSync(hookPath, "vp staged\n");
8601
- fs.chmodSync(hookPath, 493);
8602
- }
8603
- }
8604
- /**
8605
- * Rewrite only `scripts.prepare` in the root package.json using vite-prepare.yml rules.
8606
- * Collapses "husky install" → "husky" before applying ast-grep so that the
8607
- * replace-husky rule produces "vp config" with any directory argument preserved.
8608
- * Returns the old husky hooks dir (if any) for migration to .vite-hooks.
8609
- * Called only when hooks are being set up (not with --no-hooks).
8610
- */
8611
- function rewritePrepareScript(rootDir) {
8612
- const packageJsonPath = path.join(rootDir, "package.json");
8613
- if (!fs.existsSync(packageJsonPath)) return;
8614
- let oldDir;
8615
- editJsonFile(packageJsonPath, (pkg) => {
8616
- if (!pkg.scripts?.prepare) return pkg;
8617
- const prepare = collapseHuskyInstall(pkg.scripts.prepare);
8618
- const updated = rewriteScripts(JSON.stringify({ prepare }), readPrepareRulesYaml());
8619
- if (updated) {
8620
- let newPrepare = JSON.parse(updated).prepare;
8621
- newPrepare = newPrepare.replace(/\bvp config(?:\s+(?!-)([\w./-]+))?/, (_match, dir) => {
8622
- oldDir = dir ?? ".husky";
8623
- return dir ? `vp config --hooks-dir ${dir}` : "vp config";
8624
- });
8625
- pkg.scripts.prepare = newPrepare;
8626
- } else if (prepare !== pkg.scripts.prepare) pkg.scripts.prepare = prepare;
8627
- return pkg;
8628
- });
8629
- return oldDir;
8837
+ if (fs.existsSync(hookPath)) return;
8838
+ fs.writeFileSync(hookPath, "vp staged\n");
8839
+ fs.chmodSync(hookPath, 493);
8630
8840
  }
8631
8841
  //#endregion
8632
8842
  //#region src/migration/migrator/setup.ts
@@ -8769,24 +8979,35 @@ function migrateNodeVersionManagerFile(projectPath, detection, report) {
8769
8979
  //#endregion
8770
8980
  //#region src/migration/migrator/core-finalization.ts
8771
8981
  const RULES_YAML_PATH = path.join(rulesDir, "vite-tools.yml");
8772
- const PREPARE_RULES_YAML_PATH = path.join(rulesDir, "vite-prepare.yml");
8982
+ path.join(rulesDir, "vite-prepare.yml");
8773
8983
  let cachedRulesYaml;
8774
8984
  let cachedRulesYamlNoLintStaged;
8775
- let cachedPrepareRulesYaml;
8776
8985
  function readRulesYaml() {
8777
8986
  cachedRulesYaml ??= fs.readFileSync(RULES_YAML_PATH, "utf8");
8778
8987
  return cachedRulesYaml;
8779
8988
  }
8989
+ function splitRuleDocuments(yaml) {
8990
+ const lines = yaml.split("\n");
8991
+ const documents = [];
8992
+ let start = 0;
8993
+ for (const [index, line] of lines.entries()) {
8994
+ if (line.trimEnd() !== "---") continue;
8995
+ let documentStart = index;
8996
+ while (documentStart > start && lines[documentStart - 1].trimStart().startsWith("#")) documentStart--;
8997
+ if (documentStart > start) {
8998
+ documents.push(lines.slice(start, documentStart).join("\n"));
8999
+ start = documentStart;
9000
+ }
9001
+ }
9002
+ documents.push(lines.slice(start).join("\n"));
9003
+ return documents;
9004
+ }
8780
9005
  function getScriptRulesYaml(skipStagedMigration) {
8781
9006
  const yaml = readRulesYaml();
8782
9007
  if (!skipStagedMigration) return yaml;
8783
- cachedRulesYamlNoLintStaged ??= yaml.split("\n\n\n").filter((block) => !block.includes("id: replace-lint-staged")).join("\n\n\n");
9008
+ cachedRulesYamlNoLintStaged ??= splitRuleDocuments(yaml).filter((document) => !document.includes("id: replace-lint-staged")).join("\n");
8784
9009
  return cachedRulesYamlNoLintStaged;
8785
9010
  }
8786
- function readPrepareRulesYaml() {
8787
- cachedPrepareRulesYaml ??= fs.readFileSync(PREPARE_RULES_YAML_PATH, "utf8");
8788
- return cachedPrepareRulesYaml;
8789
- }
8790
9011
  function getCoreMigrationProjectPaths(workspaceInfo) {
8791
9012
  return [workspaceInfo.rootDir, ...(workspaceInfo.packages ?? []).map((pkg) => path.join(workspaceInfo.rootDir, pkg.path))];
8792
9013
  }
@@ -9014,300 +9235,235 @@ function rewriteMonorepoProject(projectPath, packageManager, skipStagedMigration
9014
9235
  if (!deferLazyPluginWrapping) wrapLazyPluginsInViteConfig(projectPath, silent, report);
9015
9236
  }
9016
9237
  //#endregion
9017
- //#region src/utils/agent.ts
9018
- const AGENTS = [
9019
- {
9020
- id: "agents",
9021
- label: "AGENTS.md",
9022
- targetPath: "AGENTS.md",
9023
- hint: "Codex, Amp, OpenCode, and similar agents",
9024
- aliases: [
9025
- "agents.md",
9026
- "chatgpt",
9027
- "chatgpt-codex",
9028
- "codex",
9029
- "amp",
9030
- "kilo",
9031
- "kilo-code",
9032
- "kiro",
9033
- "kiro-cli",
9034
- "opencode",
9035
- "other"
9036
- ]
9037
- },
9038
- {
9039
- id: "claude",
9040
- label: "CLAUDE.md",
9041
- targetPath: "CLAUDE.md",
9042
- hint: "Claude Code",
9043
- aliases: ["claude.md", "claude-code"]
9044
- },
9045
- {
9046
- id: "gemini",
9047
- label: "GEMINI.md",
9048
- targetPath: "GEMINI.md",
9049
- hint: "Gemini CLI",
9050
- aliases: ["gemini.md", "gemini-cli"]
9238
+ //#region src/utils/editor.ts
9239
+ const VSCODE_SETTINGS = {
9240
+ "editor.defaultFormatter": "oxc.oxc-vscode",
9241
+ "[javascript]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
9242
+ "[javascriptreact]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
9243
+ "[typescript]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
9244
+ "[typescriptreact]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
9245
+ "oxc.disableNestedConfig": true,
9246
+ "oxc.fmt.disableNestedConfig": true,
9247
+ "editor.formatOnSave": true,
9248
+ "editor.formatOnSaveMode": "file",
9249
+ "editor.codeActionsOnSave": { "source.fixAll.oxc": "explicit" }
9250
+ };
9251
+ const VSCODE_EXTENSIONS = { recommendations: ["VoidZero.vite-plus-extension-pack"] };
9252
+ const ZED_SETTINGS = {
9253
+ lsp: {
9254
+ oxlint: { initialization_options: { settings: {
9255
+ run: "onType",
9256
+ fixKind: "safe_fix",
9257
+ typeAware: true,
9258
+ unusedDisableDirectives: "deny"
9259
+ } } },
9260
+ oxfmt: { initialization_options: { settings: {
9261
+ "fmt.configPath": "./vite.config.ts",
9262
+ run: "onSave"
9263
+ } } }
9051
9264
  },
9265
+ languages: Object.fromEntries([
9266
+ "CSS",
9267
+ "GraphQL",
9268
+ "Handlebars",
9269
+ "HTML",
9270
+ "JavaScript",
9271
+ "JSX",
9272
+ "JSON",
9273
+ "JSON5",
9274
+ "JSONC",
9275
+ "Less",
9276
+ "Markdown",
9277
+ "MDX",
9278
+ "SCSS",
9279
+ "TypeScript",
9280
+ "TSX",
9281
+ "Vue.js",
9282
+ "YAML"
9283
+ ].map((language) => [language, {
9284
+ format_on_save: "on",
9285
+ prettier: { allowed: false },
9286
+ formatter: [{ language_server: { name: "oxfmt" } }],
9287
+ ...language === "JavaScript" ? { code_action: "source.fixAll.oxc" } : {}
9288
+ }]))
9289
+ };
9290
+ const JETBRAINS_EXTERNAL_DEPENDENCIES = `<?xml version="1.0" encoding="UTF-8"?>
9291
+ <project version="4">
9292
+ <component name="ExternalDependencies">
9293
+ <plugin id="com.github.oxc.project.oxcintellijplugin" />
9294
+ </component>
9295
+ </project>
9296
+ `;
9297
+ function jetbrainsWorkspaceConfig(packageManager) {
9298
+ return `<?xml version="1.0" encoding="UTF-8"?>
9299
+ <project version="4">
9300
+ <component name="PropertiesComponent">
9301
+ <![CDATA[${JSON.stringify({ keyToString: {
9302
+ "javascript.preferred.runtime.type.id": "node",
9303
+ nodejs_interpreter_path: "$USER_HOME$/.vite-plus/bin/node.exe",
9304
+ nodejs_package_manager_path: packageManager
9305
+ } }, null, 2)}]]>
9306
+ </component>
9307
+ </project>
9308
+ `;
9309
+ }
9310
+ const EDITORS = [
9052
9311
  {
9053
- id: "copilot",
9054
- label: ".github/copilot-instructions.md",
9055
- targetPath: ".github/copilot-instructions.md",
9056
- hint: "GitHub Copilot",
9057
- aliases: ["github-copilot", "copilot-instructions.md"]
9312
+ id: "vscode",
9313
+ label: "VSCode",
9314
+ targetDir: ".vscode",
9315
+ files: {
9316
+ "settings.json": VSCODE_SETTINGS,
9317
+ "extensions.json": VSCODE_EXTENSIONS
9318
+ }
9058
9319
  },
9059
9320
  {
9060
- id: "cursor",
9061
- label: ".cursor/rules/viteplus.mdc",
9062
- targetPath: ".cursor/rules/viteplus.mdc",
9063
- hint: "Cursor",
9064
- aliases: ["viteplus.mdc"]
9321
+ id: "zed",
9322
+ label: "Zed",
9323
+ targetDir: ".zed",
9324
+ files: { "settings.json": ZED_SETTINGS }
9065
9325
  },
9066
9326
  {
9067
9327
  id: "jetbrains",
9068
- label: ".aiassistant/rules/viteplus.md",
9069
- targetPath: ".aiassistant/rules/viteplus.md",
9070
- hint: "JetBrains AI Assistant",
9071
- aliases: [
9072
- "jetbrains",
9073
- "jetbrains-ai-assistant",
9074
- "aiassistant",
9075
- "viteplus.md"
9076
- ]
9328
+ label: "JetBrains (IntelliJ, WebStorm, etc)",
9329
+ targetDir: ".idea",
9330
+ files: {
9331
+ "externalDependencies.xml": JETBRAINS_EXTERNAL_DEPENDENCIES,
9332
+ "workspace.xml": jetbrainsWorkspaceConfig(PackageManager.pnpm),
9333
+ "OxfmtSettings.xml": `<?xml version="1.0" encoding="UTF-8"?>
9334
+ <project version="4">
9335
+ <component name="OxfmtSettings">
9336
+ <option name="preferOxfmtCodeStyleSettings" value="true" />
9337
+ </component>
9338
+ </project>
9339
+ `
9340
+ }
9077
9341
  }
9078
9342
  ];
9079
- const AGENT_DEFAULT_ID = "agents";
9080
- const AGENT_STANDARD_PATH = "AGENTS.md";
9081
- const COPILOT_AGENT_ID = "copilot";
9082
- const COPILOT_SETUP_WORKFLOW_PATH = ".github/workflows/copilot-setup-steps.yml";
9083
- const AGENT_INSTRUCTIONS_START_MARKER = "<!--VITE PLUS START-->";
9084
- const AGENT_INSTRUCTIONS_END_MARKER = "<!--VITE PLUS END-->";
9085
- const AGENT_ALIASES = Object.fromEntries(AGENTS.flatMap((option) => (option.aliases ?? []).map((alias) => [normalizeAgentName(alias), option.id])));
9086
- const COPILOT_SETUP_WORKFLOW_CONTENT = `name: "Copilot Setup Steps"
9087
-
9088
- on:
9089
- workflow_dispatch:
9090
- push:
9091
- paths:
9092
- - .github/workflows/copilot-setup-steps.yml
9093
- pull_request:
9094
- paths:
9095
- - .github/workflows/copilot-setup-steps.yml
9096
-
9097
- jobs:
9098
- copilot-setup-steps:
9099
- runs-on: ubuntu-latest
9100
- permissions:
9101
- contents: read
9102
- steps:
9103
- - name: Checkout code
9104
- uses: actions/checkout@v6
9105
- with:
9106
- persist-credentials: false
9107
- - name: Set up Vite+
9108
- uses: voidzero-dev/setup-vp@v1
9109
- with:
9110
- cache: true
9111
- run-install: true
9112
- - name: Verify Vite+
9113
- run: vp --version
9114
- `;
9115
- async function selectAgentTargets({ interactive, agent, onCancel }) {
9116
- if (agent === false) return {
9117
- targetPaths: void 0,
9118
- selectedAgents: []
9119
- };
9120
- if (interactive && !agent) {
9121
- const selectedAgentIds = await multiselect({
9122
- message: "Which coding agent instruction files should Vite+ create?",
9123
- options: AGENTS.map((option) => ({
9343
+ const EDITOR_ALIASES = [{
9344
+ id: "intellij",
9345
+ alias: "jetbrains"
9346
+ }, {
9347
+ id: "webstorm",
9348
+ alias: "jetbrains"
9349
+ }];
9350
+ async function selectEditor({ interactive, editor, onCancel }) {
9351
+ if (editor === false) return;
9352
+ if (interactive && !editor) {
9353
+ const editorOptions = EDITORS.map((option) => ({
9354
+ label: option.label,
9355
+ value: option.id,
9356
+ hint: option.targetDir
9357
+ }));
9358
+ const otherOption = {
9359
+ label: "Other",
9360
+ value: null,
9361
+ hint: "Skip writing editor configs"
9362
+ };
9363
+ const selectedEditor = await select({
9364
+ message: "Which editor are you using?\n " + styleText("gray", "Writes editor config files to enable recommended extensions and Oxlint/Oxfmt integrations."),
9365
+ options: [...editorOptions, otherOption],
9366
+ initialValue: "vscode"
9367
+ });
9368
+ if (isCancel(selectedEditor)) {
9369
+ onCancel();
9370
+ return;
9371
+ }
9372
+ if (selectedEditor === null) return;
9373
+ return resolveEditorId(selectedEditor);
9374
+ }
9375
+ if (editor) return resolveEditorId(editor);
9376
+ }
9377
+ async function selectEditors({ interactive, editor, onCancel }) {
9378
+ if (editor === false) return;
9379
+ if (interactive && !editor) {
9380
+ const selectedEditors = await multiselect({
9381
+ message: "Which editors are you using?\n " + styleText("gray", "Writes editor config files to enable recommended extensions and Oxlint/Oxfmt integrations."),
9382
+ options: EDITORS.map((option) => ({
9124
9383
  label: option.label,
9125
9384
  value: option.id,
9126
- hint: option.hint
9385
+ hint: option.targetDir
9127
9386
  })),
9128
- initialValues: [AGENT_DEFAULT_ID],
9387
+ initialValues: ["vscode"],
9129
9388
  required: false
9130
9389
  });
9131
- if (isCancel(selectedAgentIds)) {
9390
+ if (isCancel(selectedEditors)) {
9132
9391
  onCancel();
9133
- return {
9134
- targetPaths: void 0,
9135
- selectedAgents: []
9136
- };
9392
+ return;
9137
9393
  }
9138
- if (selectedAgentIds.length === 0) return {
9139
- targetPaths: void 0,
9140
- selectedAgents: []
9141
- };
9142
- const selectedAgents = resolveAgentOptions(selectedAgentIds);
9143
- return {
9144
- targetPaths: getAgentTargetPaths(selectedAgents),
9145
- selectedAgents
9146
- };
9394
+ return selectedEditors.length === 0 ? void 0 : resolveEditorIds(selectedEditors);
9147
9395
  }
9148
- const selectedAgents = resolveAgentOptions(agent ?? AGENT_DEFAULT_ID);
9149
- return {
9150
- targetPaths: getAgentTargetPaths(selectedAgents),
9151
- selectedAgents
9152
- };
9153
- }
9154
- async function selectAgentTargetPaths({ interactive, agent, onCancel }) {
9155
- return (await selectAgentTargets({
9156
- interactive,
9157
- agent,
9158
- onCancel
9159
- })).targetPaths;
9160
- }
9161
- function detectExistingAgentTargetPaths(projectRoot) {
9162
- const detectedPaths = [];
9163
- const seenTargetPaths = /* @__PURE__ */ new Set();
9164
- for (const option of AGENTS) {
9165
- if (seenTargetPaths.has(option.targetPath)) continue;
9166
- seenTargetPaths.add(option.targetPath);
9167
- const targetPath = path.join(projectRoot, option.targetPath);
9168
- if (fs.existsSync(targetPath) && !fs.lstatSync(targetPath).isSymbolicLink()) detectedPaths.push(option.targetPath);
9396
+ if (editor) {
9397
+ const editorId = resolveEditorId(editor);
9398
+ return editorId ? [editorId] : void 0;
9169
9399
  }
9170
- return detectedPaths.length > 0 ? detectedPaths : void 0;
9171
- }
9172
- /**
9173
- * Silently update agent instruction files that contain Vite+ markers.
9174
- * - No agent files → no writes
9175
- * - No Vite+ markers → no writes
9176
- * - Markers present, content up to date → no writes
9177
- * - Markers present, content outdated → update marked section
9178
- */
9179
- function updateExistingAgentInstructions(projectRoot) {
9180
- const targetPaths = detectExistingAgentTargetPaths(projectRoot);
9181
- if (!targetPaths) return;
9182
- const templatePath = path.join(pkgRoot, "AGENTS.md");
9183
- if (!fs.existsSync(templatePath)) return;
9184
- const templateContent = fs.readFileSync(templatePath, "utf-8");
9185
- for (const targetPath of targetPaths) try {
9186
- const fullPath = path.join(projectRoot, targetPath);
9187
- const existing = fs.readFileSync(fullPath, "utf-8");
9188
- const updated = replaceMarkedAgentInstructionsSection(existing, templateContent);
9189
- if (updated !== void 0 && updated !== existing) fs.writeFileSync(fullPath, updated);
9190
- } catch {}
9191
9400
  }
9192
- function resolveAgentOptions(agent) {
9193
- const agentNames = parseAgentNames(agent);
9194
- const resolvedAgentNames = agentNames.length > 0 ? agentNames : [AGENT_DEFAULT_ID];
9195
- const dedupedAgents = [];
9196
- const seenAgentIds = /* @__PURE__ */ new Set();
9197
- for (const name of resolvedAgentNames) {
9198
- const option = resolveSingleAgentOption(name);
9199
- if (seenAgentIds.has(option.id)) continue;
9200
- seenAgentIds.add(option.id);
9201
- dedupedAgents.push(option);
9202
- }
9203
- return dedupedAgents;
9204
- }
9205
- function getAgentTargetPaths(agents) {
9206
- const dedupedTargetPaths = [];
9207
- const seenTargetPaths = /* @__PURE__ */ new Set();
9208
- for (const agent of agents) {
9209
- if (seenTargetPaths.has(agent.targetPath)) continue;
9210
- seenTargetPaths.add(agent.targetPath);
9211
- dedupedTargetPaths.push(agent.targetPath);
9212
- }
9213
- return dedupedTargetPaths;
9214
- }
9215
- function parseAgentNames(agent) {
9216
- if (!agent) return [];
9217
- return (Array.isArray(agent) ? agent : [agent]).filter((value) => typeof value === "string").flatMap((value) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
9218
- }
9219
- function resolveSingleAgentOption(agent) {
9220
- const normalized = normalizeAgentName(agent);
9221
- const alias = AGENT_ALIASES[normalized];
9222
- const resolved = alias ? normalizeAgentName(alias) : normalized;
9223
- 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);
9401
+ function detectExistingEditors(projectRoot) {
9402
+ const editors = [];
9403
+ for (const option of EDITORS) for (const fileName of Object.keys(option.files)) {
9404
+ const filePath = path.join(projectRoot, option.targetDir, fileName);
9405
+ if (fs.existsSync(filePath)) {
9406
+ editors.push(option.id);
9407
+ break;
9408
+ }
9409
+ }
9410
+ return editors.length === 0 ? void 0 : editors;
9224
9411
  }
9225
9412
  /**
9226
- * Detect agent instruction files that would conflict (exist without markers).
9227
- * Returns only files that need a user decision (append or skip).
9413
+ * Detect editor config files that would conflict (already exist).
9228
9414
  * Read-only — does not write or modify any files.
9229
9415
  */
9230
- async function detectAgentConflicts({ projectRoot, targetPaths }) {
9231
- if (!targetPaths || targetPaths.length === 0) return [];
9232
- const sourcePath = path.join(pkgRoot, "AGENTS.md");
9233
- if (!fs.existsSync(sourcePath)) return [];
9234
- const incomingContent = await fsPromises.readFile(sourcePath, "utf-8");
9235
- const shouldLinkToAgents = targetPaths.includes(AGENT_STANDARD_PATH);
9236
- const orderedPaths = shouldLinkToAgents ? [AGENT_STANDARD_PATH, ...targetPaths.filter((p) => p !== AGENT_STANDARD_PATH)] : targetPaths;
9416
+ function detectEditorConflicts({ projectRoot, editorId }) {
9417
+ if (!editorId) return [];
9418
+ const editorConfig = EDITORS.find((e) => e.id === editorId);
9419
+ if (!editorConfig) return [];
9237
9420
  const conflicts = [];
9238
- const seenDestinationPaths = /* @__PURE__ */ new Set();
9239
- const seenRealPaths = /* @__PURE__ */ new Set();
9240
- for (const targetPathToCheck of orderedPaths) {
9241
- const destinationPath = path.join(projectRoot, targetPathToCheck);
9242
- const destinationKey = path.resolve(destinationPath);
9243
- if (seenDestinationPaths.has(destinationKey)) continue;
9244
- seenDestinationPaths.add(destinationKey);
9245
- if (shouldLinkToAgents && targetPathToCheck !== AGENT_STANDARD_PATH) {
9246
- if (await getExistingPathKind(destinationPath) !== "file") continue;
9247
- }
9248
- if (fs.existsSync(destinationPath)) {
9249
- if (fs.lstatSync(destinationPath).isSymbolicLink()) continue;
9250
- const destinationRealPath = await fsPromises.realpath(destinationPath);
9251
- if (seenRealPaths.has(destinationRealPath)) continue;
9252
- if (replaceMarkedAgentInstructionsSection(await fsPromises.readFile(destinationPath, "utf-8"), incomingContent) !== void 0) {
9253
- seenRealPaths.add(destinationRealPath);
9254
- continue;
9255
- }
9256
- conflicts.push({ targetPath: targetPathToCheck });
9257
- seenRealPaths.add(destinationRealPath);
9258
- }
9421
+ for (const fileName of Object.keys(editorConfig.files)) {
9422
+ const filePath = path.join(projectRoot, editorConfig.targetDir, fileName);
9423
+ if (fs.existsSync(filePath)) conflicts.push({
9424
+ fileName,
9425
+ displayPath: `${editorConfig.targetDir}/${fileName}`
9426
+ });
9259
9427
  }
9260
9428
  return conflicts;
9261
9429
  }
9262
- async function writeAgentInstructions({ projectRoot, targetPath, targetPaths, interactive, conflictDecisions, silent = false }) {
9263
- const paths = [...targetPaths ?? [], ...targetPath ? [targetPath] : []];
9264
- if (paths.length === 0) return;
9265
- const sourcePath = path.join(pkgRoot, "AGENTS.md");
9266
- if (!fs.existsSync(sourcePath)) {
9267
- if (!silent) log.warn("Agent instructions template not found; skipping.");
9268
- return;
9269
- }
9270
- const seenDestinationPaths = /* @__PURE__ */ new Set();
9271
- const seenRealPaths = /* @__PURE__ */ new Set();
9272
- const incomingContent = await fsPromises.readFile(sourcePath, "utf-8");
9273
- const shouldLinkToAgents = paths.includes(AGENT_STANDARD_PATH);
9274
- const orderedPaths = shouldLinkToAgents ? [AGENT_STANDARD_PATH, ...paths.filter((p) => p !== AGENT_STANDARD_PATH)] : paths;
9275
- for (const targetPathToWrite of orderedPaths) {
9276
- const destinationPath = path.join(projectRoot, targetPathToWrite);
9277
- const destinationKey = path.resolve(destinationPath);
9278
- if (seenDestinationPaths.has(destinationKey)) continue;
9279
- seenDestinationPaths.add(destinationKey);
9280
- await fsPromises.mkdir(path.dirname(destinationPath), { recursive: true });
9281
- if (shouldLinkToAgents && targetPathToWrite !== AGENT_STANDARD_PATH) {
9282
- if (await tryLinkTargetToAgents(projectRoot, targetPathToWrite, silent)) continue;
9283
- }
9284
- if (fs.existsSync(destinationPath)) {
9285
- if (fs.lstatSync(destinationPath).isSymbolicLink()) {
9286
- if (!silent) log.info(`Skipped writing ${targetPathToWrite} (symlink)`);
9287
- continue;
9288
- }
9289
- const destinationRealPath = await fsPromises.realpath(destinationPath);
9290
- if (seenRealPaths.has(destinationRealPath)) {
9291
- if (!silent) log.info(`Skipped writing ${targetPathToWrite} (duplicate target)`);
9292
- continue;
9293
- }
9294
- const existingContent = await fsPromises.readFile(destinationPath, "utf-8");
9295
- const updatedContent = replaceMarkedAgentInstructionsSection(existingContent, incomingContent);
9296
- if (updatedContent !== void 0) {
9297
- if (updatedContent !== existingContent) await fsPromises.writeFile(destinationPath, updatedContent);
9298
- seenRealPaths.add(destinationRealPath);
9299
- continue;
9300
- }
9430
+ async function writeEditorConfigs({ projectRoot, editorId, interactive, conflictDecisions, silent = false, extraVsCodeSettings, packageManager = PackageManager.pnpm }) {
9431
+ const editorIds = normalizeEditorSelection(editorId);
9432
+ if (editorIds.length === 0) return;
9433
+ for (const currentEditorId of editorIds) await writeEditorConfig({
9434
+ projectRoot,
9435
+ editorId: currentEditorId,
9436
+ interactive,
9437
+ conflictDecisions,
9438
+ silent,
9439
+ extraVsCodeSettings,
9440
+ packageManager
9441
+ });
9442
+ }
9443
+ async function writeEditorConfig({ projectRoot, editorId, interactive, conflictDecisions, silent, extraVsCodeSettings, packageManager }) {
9444
+ const editorConfig = EDITORS.find((e) => e.id === editorId);
9445
+ if (!editorConfig) return;
9446
+ const targetDir = path.join(projectRoot, editorConfig.targetDir);
9447
+ await fsPromises.mkdir(targetDir, { recursive: true });
9448
+ for (const [fileName, baseIncoming] of Object.entries(editorConfig.files)) {
9449
+ const incoming = editorId === "vscode" && fileName === "settings.json" && extraVsCodeSettings ? {
9450
+ ...extraVsCodeSettings,
9451
+ ...baseIncoming
9452
+ } : editorId === "jetbrains" && fileName === "workspace.xml" ? jetbrainsWorkspaceConfig(packageManager) : baseIncoming;
9453
+ const filePath = path.join(targetDir, fileName);
9454
+ const jsonFormat = isJsonLikeFile(fileName);
9455
+ if (fs.existsSync(filePath)) {
9456
+ const displayPath = `${editorConfig.targetDir}/${fileName}`;
9301
9457
  let conflictAction;
9302
- const preResolved = conflictDecisions?.get(targetPathToWrite);
9458
+ const preResolved = conflictDecisions?.get(displayPath) ?? conflictDecisions?.get(fileName);
9303
9459
  if (preResolved) conflictAction = preResolved;
9304
9460
  else if (interactive) {
9305
9461
  const action = await select({
9306
- message: `Agent instructions already exist at ${targetPathToWrite}.\n ` + styleText("gray", "The Vite+ template includes guidance on `vp` commands, the build pipeline, and project conventions."),
9462
+ message: `${displayPath} already exists.\n ` + styleText("gray", jsonFormat ? `Vite+ adds ${editorConfig.label} settings for the built-in linter and formatter. Merge adds new keys without overwriting existing ones.` : `Vite+ adds ${editorConfig.label} settings for the built-in linter and formatter. Overwrite replaces the existing file with the generated config.`),
9307
9463
  options: [{
9308
- label: "Append",
9309
- value: "append",
9310
- hint: "Add template content to the end"
9464
+ label: jsonFormat ? "Merge" : "Overwrite",
9465
+ value: "merge",
9466
+ hint: jsonFormat ? "Merge new settings into existing file" : "Replace existing file with generated config"
9311
9467
  }, {
9312
9468
  label: "Skip",
9313
9469
  value: "skip",
@@ -9315,86 +9471,121 @@ async function writeAgentInstructions({ projectRoot, targetPath, targetPaths, in
9315
9471
  }],
9316
9472
  initialValue: "skip"
9317
9473
  });
9318
- conflictAction = isCancel(action) || action === "skip" ? "skip" : "append";
9319
- } else conflictAction = "skip";
9320
- if (conflictAction === "append") await appendAgentContent(destinationPath, targetPathToWrite, existingContent, incomingContent, silent);
9321
- else {
9322
- const suffix = !preResolved && !interactive ? " (already exists)" : "";
9323
- if (!silent) log.info(`Skipped writing ${targetPathToWrite}${suffix}`);
9324
- }
9325
- seenRealPaths.add(destinationRealPath);
9474
+ conflictAction = isCancel(action) || action === "skip" ? "skip" : "merge";
9475
+ } else conflictAction = jsonFormat ? "merge" : "skip";
9476
+ if (conflictAction === "merge") {
9477
+ if (jsonFormat) {
9478
+ if (!isPlainObject(incoming)) throw new Error(`Cannot merge editor config: ${displayPath} incoming value is not JSON`);
9479
+ mergeAndWriteEditorConfig(filePath, incoming, fileName, displayPath, silent);
9480
+ } else writeTextEditorConfig(filePath, incoming, displayPath, silent);
9481
+ } else if (!silent) log.info(`Skipped writing ${displayPath}`);
9326
9482
  continue;
9327
9483
  }
9328
- await fsPromises.writeFile(destinationPath, incomingContent);
9329
- if (!silent) log.success(`Wrote agent instructions to ${targetPathToWrite}`);
9330
- seenRealPaths.add(await fsPromises.realpath(destinationPath));
9484
+ if (jsonFormat) {
9485
+ if (!isPlainObject(incoming)) throw new Error(`Cannot write editor config: ${editorConfig.targetDir}/${fileName} must be JSON`);
9486
+ writeJsonFile(filePath, incoming);
9487
+ } else writeTextEditorConfig(filePath, incoming, `${editorConfig.targetDir}/${fileName}`, silent);
9488
+ if (!silent) log.success(`Wrote editor config to ${editorConfig.targetDir}/${fileName}`);
9331
9489
  }
9332
9490
  }
9333
- async function writeCopilotSetupWorkflow({ projectRoot, silent = false }) {
9334
- const destinationPath = path.join(projectRoot, COPILOT_SETUP_WORKFLOW_PATH);
9335
- await fsPromises.mkdir(path.dirname(destinationPath), { recursive: true });
9336
- if (fs.existsSync(destinationPath)) {
9337
- if (!silent) log.info(`Skipped writing ${COPILOT_SETUP_WORKFLOW_PATH} (already exists)`);
9491
+ function isJsonLikeFile(fileName) {
9492
+ const ext = path.extname(fileName).toLowerCase();
9493
+ return ext === ".json" || ext === ".jsonc";
9494
+ }
9495
+ function writeTextEditorConfig(filePath, incoming, displayPath, silent = false) {
9496
+ if (typeof incoming !== "string") throw new Error(`Cannot write editor config: ${displayPath} must be text content`);
9497
+ if ((fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : void 0) === incoming) {
9498
+ if (!silent) log.info(`No changes needed for ${displayPath}`);
9338
9499
  return;
9339
9500
  }
9340
- await fsPromises.writeFile(destinationPath, COPILOT_SETUP_WORKFLOW_CONTENT);
9341
- if (!silent) log.success(`Wrote Copilot setup workflow to ${COPILOT_SETUP_WORKFLOW_PATH}`);
9342
- }
9343
- async function appendAgentContent(destinationPath, targetPath, existingContent, incomingContent, silent = false) {
9344
- const separator = existingContent.endsWith("\n") ? "" : "\n";
9345
- await fsPromises.appendFile(destinationPath, `${separator}\n${incomingContent}`);
9346
- if (!silent) log.success(`Appended agent instructions to ${targetPath}`);
9347
- }
9348
- function normalizeAgentName(value) {
9349
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
9350
- }
9351
- function replaceMarkedAgentInstructionsSection(existing, incoming) {
9352
- const existingRange = getMarkedRange(existing, AGENT_INSTRUCTIONS_START_MARKER, AGENT_INSTRUCTIONS_END_MARKER);
9353
- if (!existingRange) return;
9354
- const incomingRange = getMarkedRange(incoming, AGENT_INSTRUCTIONS_START_MARKER, AGENT_INSTRUCTIONS_END_MARKER);
9355
- if (!incomingRange) return;
9356
- return `${existing.slice(0, existingRange.start)}${incoming.slice(incomingRange.start, incomingRange.end)}${existing.slice(existingRange.end)}`;
9357
- }
9358
- async function tryLinkTargetToAgents(projectRoot, targetPath, silent = false) {
9359
- const destinationPath = path.join(projectRoot, targetPath);
9360
- const agentsPath = path.join(projectRoot, AGENT_STANDARD_PATH);
9361
- const symlinkTarget = path.relative(path.dirname(destinationPath), agentsPath);
9362
- const existing = await getExistingPathKind(destinationPath);
9363
- if (existing === "file") return false;
9364
- if (existing === "symlink") {
9365
- const currentLink = await fsPromises.readlink(destinationPath);
9366
- if (path.resolve(path.dirname(destinationPath), currentLink) === agentsPath) {
9367
- if (!silent) log.info(`Skipped linking ${targetPath} (already linked to ${AGENT_STANDARD_PATH})`);
9368
- return true;
9369
- }
9370
- await fsPromises.unlink(destinationPath);
9501
+ fs.writeFileSync(filePath, incoming, "utf-8");
9502
+ }
9503
+ function normalizeEditorSelection(editorId) {
9504
+ if (!editorId) return [];
9505
+ return [...new Set(Array.isArray(editorId) ? editorId : [editorId])];
9506
+ }
9507
+ /**
9508
+ * Merge incoming settings into an existing editor JSON/JSONC file by patching the
9509
+ * original text with `jsonc-parser` instead of re-serializing a merged object.
9510
+ * This preserves comments, key order, trailing commas, and untouched formatting.
9511
+ * Existing values always win; only missing keys/branches are inserted.
9512
+ */
9513
+ function mergeAndWriteEditorConfig(filePath, incoming, fileName, displayPath, silent = false) {
9514
+ const originalText = fs.readFileSync(filePath, "utf-8");
9515
+ const existing = parse(originalText);
9516
+ if (!isPlainObject(existing)) throw new Error(`Cannot merge editor config: ${displayPath} is not a JSON object`);
9517
+ const formattingOptions = detectFormattingOptions(originalText);
9518
+ const newText = fileName === "extensions.json" ? mergeExtensionsText(originalText, existing, incoming, formattingOptions) : mergeSettingsText(originalText, existing, incoming, formattingOptions);
9519
+ if (newText === originalText) {
9520
+ if (!silent) log.info(`No changes needed for ${displayPath}`);
9521
+ return;
9371
9522
  }
9372
- try {
9373
- await fsPromises.symlink(symlinkTarget, destinationPath);
9374
- } catch (err) {
9375
- if (err.code === "EPERM") {
9376
- await fsPromises.copyFile(agentsPath, destinationPath);
9377
- if (!silent) log.success(`Copied ${AGENT_STANDARD_PATH} to ${targetPath}`);
9378
- return true;
9523
+ fs.writeFileSync(filePath, newText, "utf-8");
9524
+ if (!silent) log.success(`Merged editor config into ${displayPath}`);
9525
+ }
9526
+ function isPlainObject(value) {
9527
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9528
+ }
9529
+ /** Apply a single `jsonc-parser` modification to `text` and return the patched text. */
9530
+ function applyJsoncEdit(text, path, value, options) {
9531
+ return applyEdits(text, modify(text, path, value, options));
9532
+ }
9533
+ /**
9534
+ * Deep-merge missing keys from `incoming` into the existing text. Inserts a whole
9535
+ * branch when it is absent, and recurses only when both sides are non-array objects
9536
+ * so comments inside existing branches are preserved.
9537
+ */
9538
+ function mergeSettingsText(text, existing, incoming, formattingOptions) {
9539
+ let currentText = text;
9540
+ const insertMissing = (existingNode, incomingNode, basePath) => {
9541
+ for (const [key, value] of Object.entries(incomingNode)) {
9542
+ const fullPath = [...basePath, key];
9543
+ if (!(key in existingNode)) currentText = applyJsoncEdit(currentText, fullPath, value, { formattingOptions });
9544
+ else if (isPlainObject(existingNode[key]) && isPlainObject(value)) insertMissing(existingNode[key], value, fullPath);
9379
9545
  }
9380
- throw err;
9546
+ };
9547
+ insertMissing(existing, incoming, []);
9548
+ return currentText;
9549
+ }
9550
+ /**
9551
+ * For `extensions.json`, append missing recommendations without rebuilding the array,
9552
+ * so comments inside the array survive. Existing entries always win.
9553
+ */
9554
+ function mergeExtensionsText(text, existing, incoming, formattingOptions) {
9555
+ const incomingRecs = Array.isArray(incoming["recommendations"]) ? incoming["recommendations"] : [];
9556
+ const existingValue = existing["recommendations"];
9557
+ if (!("recommendations" in existing)) return applyJsoncEdit(text, ["recommendations"], incomingRecs, { formattingOptions });
9558
+ if (!Array.isArray(existingValue)) return text;
9559
+ const existingRecs = new Set(existingValue);
9560
+ let currentText = text;
9561
+ let nextIndex = existingValue.length;
9562
+ for (const rec of incomingRecs) {
9563
+ if (existingRecs.has(rec)) continue;
9564
+ currentText = applyJsoncEdit(currentText, ["recommendations", nextIndex], rec, {
9565
+ formattingOptions,
9566
+ isArrayInsertion: true
9567
+ });
9568
+ nextIndex++;
9381
9569
  }
9382
- if (!silent) log.success(`Linked ${targetPath} to ${AGENT_STANDARD_PATH}`);
9383
- return true;
9570
+ return currentText;
9384
9571
  }
9385
- async function getExistingPathKind(filePath) {
9386
- if (!fs.existsSync(filePath)) return "missing";
9387
- return (await fsPromises.lstat(filePath)).isSymbolicLink() ? "symlink" : "file";
9572
+ function resolveEditorId(editor) {
9573
+ const normalized = editor.trim().toLowerCase();
9574
+ const match = EDITORS.find((option) => option.id === normalized || option.label.toLowerCase() === normalized);
9575
+ if (match) return match.id;
9576
+ const aliasMatch = EDITOR_ALIASES.find((option) => option.id === normalized);
9577
+ if (aliasMatch) {
9578
+ log.warn(`--editor ${aliasMatch.id} was passed; use --editor ${aliasMatch.alias} instead, as it's the canonical ID for that editor.`);
9579
+ return aliasMatch.alias;
9580
+ }
9388
9581
  }
9389
- function getMarkedRange(content, startMarker, endMarker) {
9390
- const start = content.indexOf(startMarker);
9391
- if (start === -1) return;
9392
- const endMarkerIndex = content.indexOf(endMarker, start + startMarker.length);
9393
- if (endMarkerIndex === -1) return;
9394
- return {
9395
- start,
9396
- end: endMarkerIndex + endMarker.length
9397
- };
9582
+ function resolveEditorIds(editors) {
9583
+ const editorIds = editors.flatMap((editor) => {
9584
+ const editorId = resolveEditorId(editor);
9585
+ return editorId ? [editorId] : [];
9586
+ });
9587
+ const uniqueEditorIds = [...new Set(editorIds)];
9588
+ return uniqueEditorIds.length === 0 ? void 0 : uniqueEditorIds;
9398
9589
  }
9399
9590
  //#endregion
9400
- export { globSync as A, warnPackageLevelEslint as B, injectLintTypeCheckDefaults as C, ensureVitePlusBootstrap as D, detectVitePlusBootstrapPending as E, migrateEslintToOxlint as F, checkVitestVersion as G, PRETTIER_PACKAGE_JSON_CONFIG as H, promptEslintMigration as I, templatesDir as J, warnMigration as K, rewriteToolLintStagedConfigFiles as L, confirmEslintMigration as M, detectEslintProject as N, configureYarnNodeModulesMode as O, detectIncompatibleEslintIntegration as P, warnIncompatibleEslintIntegration as R, injectCreateDefaultTemplate as S, collectToolchainVersionChanges as T, detectConfigs as U, PRETTIER_CONFIG_FILES as V, checkViteVersion as W, detectLegacyGitHooksMigrationCandidate as _, selectAgentTargets as a, preflightGitHooksSetup as b, writeCopilotSetupWorkflow as c, rewriteStandaloneProject as d, detectPendingCoreMigration as f, setPackageManager as g, migrateNodeVersionManagerFile as h, selectAgentTargetPaths as i, minimatch as j, detectYarnPnpMode as k, rewriteMonorepo as l, detectNodeVersionManagerFile as m, detectAgentConflicts as n, updateExistingAgentInstructions as o, finalizeCoreMigrationForExistingVitePlus as p, displayRelative as q, detectExistingAgentTargetPaths as r, writeAgentInstructions as s, COPILOT_AGENT_ID as t, rewriteMonorepoProject as u, ensurePreCommitHook as v, mergeViteConfigFiles as w, hasStagedConfigInViteConfig as x, installGitHooks as y, warnLegacyEslintConfig as z };
9591
+ export { isBingoTemplate as A, warnPackageLevelPrettier as B, mergeViteConfigFiles as C, configureYarnNodeModulesMode as D, ensureVitePlusBootstrap as E, hasFrameworkShim as F, promptEslintMigration as G, detectEslintProject as H, confirmPrettierMigration as I, warnPackageLevelEslint as J, warnIncompatibleEslintIntegration as K, detectPrettierProject as L, updateWorkspaceConfig as M, addFrameworkShim as N, detectYarnPnpMode as O, detectFramework as P, migratePrettierToOxfmt as R, injectLintTypeCheckDefaults as S, detectVitePlusBootstrapPending as T, detectIncompatibleEslintIntegration as U, confirmEslintMigration as V, migrateEslintToOxlint as W, checkVitestVersion as X, checkViteVersion as Y, preflightGitHooksSetup as _, selectEditors as a, initGitRepository as b, rewriteMonorepoProject as c, finalizeCoreMigrationForExistingVitePlus as d, detectNodeVersionManagerFile as f, installGitHooks as g, detectLegacyGitHooksMigrationCandidate as h, selectEditor as i, updatePackageJsonWithDeps as j, detectWorkspace$1 as k, rewriteStandaloneProject as l, setPackageManager as m, detectExistingEditors as n, writeEditorConfigs as o, migrateNodeVersionManagerFile as p, warnLegacyEslintConfig as q, isJsonLikeFile as r, rewriteMonorepo as s, detectEditorConflicts as t, detectPendingCoreMigration as u, shouldSkipStagedMigrationForHooks as v, collectToolchainVersionChanges as w, injectCreateDefaultTemplate as x, findGitRoot as y, promptPrettierMigration as z };