@useinsider/eslint-config 3.0.2 → 4.0.0-beta.2

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 (60) hide show
  1. package/README.md +376 -64
  2. package/dist/builders/configFiles.d.ts +13 -0
  3. package/dist/builders/oxlint.d.ts +51 -0
  4. package/dist/builders/oxlintOwnership.d.ts +43 -0
  5. package/dist/builders/scopeBlocks.d.ts +6 -0
  6. package/dist/cli.cjs +1028 -86
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/configs/dependencies.d.ts +3 -0
  9. package/dist/{javascript-VBXqjVi8.cjs → javascript-DH8-N38w.cjs} +5 -5
  10. package/dist/{javascript-VBXqjVi8.cjs.map → javascript-DH8-N38w.cjs.map} +1 -1
  11. package/dist/{javascript-dom-5jRL_yKP.cjs → javascript-dom-CvtQ6_9W.cjs} +4 -4
  12. package/dist/{javascript-dom-5jRL_yKP.cjs.map → javascript-dom-CvtQ6_9W.cjs.map} +1 -1
  13. package/dist/{javascript-node-C1AU_4X2.cjs → javascript-node-BNsy3loY.cjs} +4 -4
  14. package/dist/{javascript-node-C1AU_4X2.cjs.map → javascript-node-BNsy3loY.cjs.map} +1 -1
  15. package/dist/{jest-CHzjzBtf.cjs → jest-4Es9Ggaa.cjs} +3 -3
  16. package/dist/{jest-CHzjzBtf.cjs.map → jest-4Es9Ggaa.cjs.map} +1 -1
  17. package/dist/main.cjs +763 -78
  18. package/dist/main.cjs.map +1 -1
  19. package/dist/main.d.ts +9 -2
  20. package/dist/main.js +746 -61
  21. package/dist/main.js.map +1 -1
  22. package/dist/oxlintOwnership-7hpdB3d1.cjs +199 -0
  23. package/dist/oxlintOwnership-7hpdB3d1.cjs.map +1 -0
  24. package/dist/oxlintOwnership-Cx0hgA_J.js +127 -0
  25. package/dist/oxlintOwnership-Cx0hgA_J.js.map +1 -0
  26. package/dist/removePluginsFromConfigs-D0oSUj2C.cjs.map +1 -1
  27. package/dist/removePluginsFromConfigs-DzqX2Ju2.js.map +1 -1
  28. package/dist/{stylistic-Dio0AM2E.cjs → stylistic-CoP3MT1Y.cjs} +4 -4
  29. package/dist/{stylistic-Dio0AM2E.cjs.map → stylistic-CoP3MT1Y.cjs.map} +1 -1
  30. package/dist/{typescript-C1icmOxY.cjs → typescript-D3VeLnpn.cjs} +7 -7
  31. package/dist/{typescript-C1icmOxY.cjs.map → typescript-D3VeLnpn.cjs.map} +1 -1
  32. package/dist/{typescript-dom-RUJn2WCZ.cjs → typescript-dom-iB_xBTAD.cjs} +4 -4
  33. package/dist/{typescript-dom-RUJn2WCZ.cjs.map → typescript-dom-iB_xBTAD.cjs.map} +1 -1
  34. package/dist/{typescript-node-CBEzcc56.cjs → typescript-node-Cq6qhj53.cjs} +4 -4
  35. package/dist/{typescript-node-CBEzcc56.cjs.map → typescript-node-Cq6qhj53.cjs.map} +1 -1
  36. package/dist/utils/ensureConfigStructure.d.ts +15 -6
  37. package/dist/utils/ensureOxlintConfig.d.ts +10 -0
  38. package/dist/utils/layoutDiagnosis.d.ts +63 -0
  39. package/dist/utils/offerMigration.d.ts +42 -0
  40. package/dist/utils/silenceRules.d.ts +9 -0
  41. package/dist/{vitest-BomI8gtu.cjs → vitest-BlGQswK2.cjs} +3 -3
  42. package/dist/{vitest-BomI8gtu.cjs.map → vitest-BlGQswK2.cjs.map} +1 -1
  43. package/dist/{vue2-azKwRgaJ.cjs → vue2-Da1i_oVn.cjs} +8 -8
  44. package/dist/{vue2-azKwRgaJ.cjs.map → vue2-Da1i_oVn.cjs.map} +1 -1
  45. package/dist/{vue2-options-api-ma3J-bvM.cjs → vue2-options-api-BSdrUm8H.cjs} +8 -8
  46. package/dist/{vue2-options-api-ma3J-bvM.cjs.map → vue2-options-api-BSdrUm8H.cjs.map} +1 -1
  47. package/dist/{vue2-typescript-CZSHzp7I.cjs → vue2-typescript-dXLRSGNP.cjs} +10 -10
  48. package/dist/{vue2-typescript-CZSHzp7I.cjs.map → vue2-typescript-dXLRSGNP.cjs.map} +1 -1
  49. package/dist/{vue2-typescript-options-api-B8OzZlje.cjs → vue2-typescript-options-api-LM7U5dWq.cjs} +10 -10
  50. package/dist/{vue2-typescript-options-api-B8OzZlje.cjs.map → vue2-typescript-options-api-LM7U5dWq.cjs.map} +1 -1
  51. package/dist/{vue3-Df5UQ1Ys.cjs → vue3-621EM2Ii.cjs} +8 -8
  52. package/dist/{vue3-Df5UQ1Ys.cjs.map → vue3-621EM2Ii.cjs.map} +1 -1
  53. package/dist/{vue3-options-api-Bx7yYHza.cjs → vue3-options-api-CC9_fAiq.cjs} +8 -8
  54. package/dist/{vue3-options-api-Bx7yYHza.cjs.map → vue3-options-api-CC9_fAiq.cjs.map} +1 -1
  55. package/dist/{vue3-typescript-C0O1JZlk.cjs → vue3-typescript-DONe-IKz.cjs} +10 -10
  56. package/dist/{vue3-typescript-C0O1JZlk.cjs.map → vue3-typescript-DONe-IKz.cjs.map} +1 -1
  57. package/dist/{vue3-typescript-options-api-cPal4O-K.cjs → vue3-typescript-options-api-D4pqpDFs.cjs} +10 -10
  58. package/dist/{vue3-typescript-options-api-cPal4O-K.cjs.map → vue3-typescript-options-api-D4pqpDFs.cjs.map} +1 -1
  59. package/package.json +8 -5
  60. package/dist/rolldown-runtime-VH7oDXx4.cjs +0 -28
package/dist/cli.cjs CHANGED
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
3
+ const require_oxlintOwnership = require("./oxlintOwnership-7hpdB3d1.cjs");
4
4
  let globals = require("globals");
5
- globals = require_rolldown_runtime.__toESM(globals, 1);
5
+ globals = require_oxlintOwnership.__toESM(globals, 1);
6
6
  let node_fs = require("node:fs");
7
7
  let node_path = require("node:path");
8
8
  let node_url = require("node:url");
9
+ let _typescript_eslint_types = require("@typescript-eslint/types");
9
10
  let node_crypto = require("node:crypto");
10
11
  let node_os = require("node:os");
11
12
  let typescript = require("typescript");
12
- typescript = require_rolldown_runtime.__toESM(typescript, 1);
13
+ typescript = require_oxlintOwnership.__toESM(typescript, 1);
14
+ let _typescript_eslint_parser = require("@typescript-eslint/parser");
13
15
  //#region src/cli/detect.ts
14
16
  var FLAT_CONFIG_FILENAMES = [
15
17
  "eslint.config.mjs",
@@ -307,20 +309,26 @@ function detectVueApiPattern(input) {
307
309
  //#region src/cli/dispatch.ts
308
310
  var yesFlags = /* @__PURE__ */ new Set(["--yes", "-y"]);
309
311
  var noInstallFlags = /* @__PURE__ */ new Set(["--no-install"]);
312
+ var fromLintFlags = /* @__PURE__ */ new Set(["--from-lint"]);
310
313
  function isYesFlag(arg) {
311
314
  return yesFlags.has(arg);
312
315
  }
313
316
  function isNoInstallFlag(arg) {
314
317
  return noInstallFlags.has(arg);
315
318
  }
319
+ function isFromLintFlag(arg) {
320
+ return fromLintFlags.has(arg);
321
+ }
316
322
  function parseMigrateArgs(rest) {
317
323
  const yes = rest.some(isYesFlag);
318
324
  const noInstall = rest.some(isNoInstallFlag);
319
- const positional = rest.filter((arg) => !isYesFlag(arg) && !isNoInstallFlag(arg));
325
+ const fromLint = rest.some(isFromLintFlag);
326
+ const positional = rest.filter((arg) => !isYesFlag(arg) && !isNoInstallFlag(arg) && !isFromLintFlag(arg));
320
327
  return {
321
328
  target: positional.length > 0 ? positional[positional.length - 1] : void 0,
322
329
  yes,
323
- noInstall
330
+ noInstall,
331
+ fromLint
324
332
  };
325
333
  }
326
334
  function parseScaffoldArgs(argv) {
@@ -329,12 +337,14 @@ function parseScaffoldArgs(argv) {
329
337
  function printUsage(stderr, unknown) {
330
338
  stderr(`[useinsider/eslint-config] Unknown subcommand: ${unknown}`);
331
339
  stderr("Usage:");
332
- stderr(" npx @useinsider/eslint-config [--yes|-y] Scaffold a new eslint.config.mjs");
333
- stderr(" npx @useinsider/eslint-config migrate [path] [--yes|-y] [--no-install]");
334
- stderr(" Migrate an existing v1 useInsider() config to v2");
340
+ stderr(" npx @useinsider/eslint-config [--yes|-y]");
341
+ stderr(" Scaffold insider.lint.*, eslint.config.* and oxlint.config.*");
342
+ stderr(" npx @useinsider/eslint-config migrate [path] [--yes|-y] [--no-install] [--from-lint]");
343
+ stderr(" Split an existing useInsider() or .eslintrc* config into the three-file layout");
344
+ stderr(" --from-lint: started by a failing lint run — confirm first, exit non-zero if declined");
335
345
  }
336
346
  function isScaffoldArgv(argv) {
337
- return argv.length === 0 || argv.every((arg) => isYesFlag(arg));
347
+ return argv.every((arg) => isYesFlag(arg));
338
348
  }
339
349
  async function dispatch(argv, cwd, deps) {
340
350
  if (isScaffoldArgv(argv)) {
@@ -346,18 +356,22 @@ async function dispatch(argv, cwd, deps) {
346
356
  }
347
357
  const [subcommand, ...rest] = argv;
348
358
  if (subcommand === "migrate") {
349
- const { target, yes, noInstall } = parseMigrateArgs(rest);
359
+ const { target, yes, noInstall, fromLint } = parseMigrateArgs(rest);
350
360
  return deps.migrate({
351
361
  cwd,
352
362
  target,
353
363
  yes,
354
- noInstall
364
+ noInstall,
365
+ fromLint
355
366
  });
356
367
  }
357
368
  printUsage(deps.stderr, subcommand);
358
369
  return 1;
359
370
  }
360
371
  //#endregion
372
+ //#region src/configs/dependencies.ts
373
+ var dependencies$14 = { oxlint: true };
374
+ //#endregion
361
375
  //#region src/configs/jest/dependencies.ts
362
376
  var dependencies$13 = { "eslint-plugin-jest": true };
363
377
  //#endregion
@@ -471,6 +485,7 @@ function buildDependencyList(answers, rawManifest = readOwnEslintConfigManifest$
471
485
  deps.add(`@useinsider/eslint-config@${manifest.version}`);
472
486
  deps.add(pinned(manifest, "eslint"));
473
487
  deps.add(pinned(manifest, "globals"));
488
+ applyRecord(deps, manifest, dependencies$14);
474
489
  const wantsTypescript = answers.languages.includes("typescript");
475
490
  if (answers.framework === "vue") applyRecord(deps, manifest, vueDepsFor(answers.vueMajor ?? 3, wantsTypescript));
476
491
  else if (wantsTypescript) applyRecord(deps, manifest, dependencies$12);
@@ -654,39 +669,66 @@ function noteGenericError(clack, summary, context, specs) {
654
669
  async function tryRetryInstall(clack, installer, deps, context) {
655
670
  try {
656
671
  await installer(deps);
672
+ return { installed: true };
657
673
  } catch (retryError) {
658
- noteError(clack, "Retry install failed after wipe: ", retryError instanceof Error ? retryError.message : String(retryError), context, deps);
674
+ const retryReason = retryError instanceof Error ? retryError.message : String(retryError);
675
+ noteError(clack, "Retry install failed after wipe: ", retryReason, context, deps);
676
+ return {
677
+ installed: false,
678
+ reason: retryReason
679
+ };
659
680
  }
660
681
  }
661
682
  async function tryGenericRetryInstall(clack, installer, deps, context) {
662
683
  try {
663
684
  await installer(deps);
685
+ return { installed: true };
664
686
  } catch (retryError) {
665
- noteGenericError(clack, buildGenericSummary(`Clean reinstall failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`), context, deps);
687
+ const summary = buildGenericSummary(`Clean reinstall failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`);
688
+ noteGenericError(clack, summary, context, deps);
689
+ return {
690
+ installed: false,
691
+ reason: summary
692
+ };
666
693
  }
667
694
  }
668
695
  async function handlePeerDepFailure(clack, deps, autoConfirm, installer, reason, context) {
669
696
  if (!await confirmPeerDepWipe(clack, autoConfirm)) {
670
697
  noteError(clack, "Install failed: ", reason, context, deps);
671
- return;
698
+ return {
699
+ installed: false,
700
+ reason
701
+ };
672
702
  }
673
703
  performFullWipe(clack, context);
674
- await tryRetryInstall(clack, installer, deps, context);
704
+ return tryRetryInstall(clack, installer, deps, context);
675
705
  }
676
706
  async function handleGenericFailure(clack, deps, autoConfirm, installer, reason, context) {
677
707
  const summary = buildGenericSummary(reason);
678
708
  if (!await confirmCleanReinstall(clack, autoConfirm, summary)) {
679
709
  if (clack.note) clack.note(`${summary}\nSkipping clean reinstall. Please resolve the issue manually and re-run.`, "Error");
680
- return;
710
+ return {
711
+ installed: false,
712
+ reason: summary
713
+ };
681
714
  }
682
715
  performFullWipe(clack, context);
683
- await tryGenericRetryInstall(clack, installer, deps, context);
684
- }
716
+ return tryGenericRetryInstall(clack, installer, deps, context);
717
+ }
718
+ /**
719
+ * Installs the dependencies, offering one wipe-and-retry when the first attempt fails, and reports
720
+ * whether they ended up installed. Every failure path is already explained to the consumer through
721
+ * `clack`, so the outcome exists for the caller's exit code rather than for a second message.
722
+ * @example
723
+ * const outcome = await installWithRetry(options);
724
+ * if (!outcome.installed) { return 1; }
725
+ */
685
726
  async function installWithRetry(opts) {
686
727
  const installer = opts.installer ?? defaultInstaller;
687
728
  const fs = opts.fsOps ?? defaultWipeFs();
688
729
  try {
689
730
  await installer(opts.deps);
731
+ return { installed: true };
690
732
  } catch (error) {
691
733
  const reason = error instanceof Error ? error.message : String(error);
692
734
  const context = {
@@ -694,12 +736,303 @@ async function installWithRetry(opts) {
694
736
  fs,
695
737
  detected: detectLockfile(opts.cwd, fs.exists)
696
738
  };
697
- if (isPeerDepFailure(reason)) {
698
- await handlePeerDepFailure(opts.clack, opts.deps, opts.autoConfirm, installer, reason, context);
739
+ if (isPeerDepFailure(reason)) return handlePeerDepFailure(opts.clack, opts.deps, opts.autoConfirm, installer, reason, context);
740
+ return handleGenericFailure(opts.clack, opts.deps, opts.autoConfirm, installer, reason, context);
741
+ }
742
+ }
743
+ //#endregion
744
+ //#region src/cli/lintLayout.ts
745
+ var OXLINT_COMMAND = "oxlint --disable-nested-config";
746
+ var OXLINT_LINT_SCRIPT = `${OXLINT_COMMAND} && eslint .`;
747
+ /**
748
+ * Extension for the lint module and the oxlint config. oxlint only discovers `oxlint.config.ts` and
749
+ * `oxlint.config.mts`, and Node warns on a bare `.ts` file inside a package that declares no
750
+ * `"type"`, so such a consumer gets the unambiguous `.mts` form.
751
+ * @example
752
+ * resolveLayoutExtension('/repo'); // 'ts' when /repo/package.json declares "type": "module"
753
+ */
754
+ function resolveLayoutExtension(cwd) {
755
+ try {
756
+ const raw = (0, node_fs.readFileSync)((0, node_path.join)(cwd, "package.json"), "utf8");
757
+ return JSON.parse(raw).type === "module" ? "ts" : "mts";
758
+ } catch {
759
+ return "mts";
760
+ }
761
+ }
762
+ var ESLINT_CONFIG_EXTENSIONS = [
763
+ "ts",
764
+ "mts",
765
+ "cts",
766
+ "js",
767
+ "mjs",
768
+ "cjs"
769
+ ];
770
+ var TYPESCRIPT_CONFIG_EXTENSIONS = /* @__PURE__ */ new Set([
771
+ "cts",
772
+ "mts",
773
+ "ts"
774
+ ]);
775
+ function extensionOf(filename) {
776
+ return filename.slice(filename.lastIndexOf(".") + 1);
777
+ }
778
+ function existingEslintConfigs(cwd) {
779
+ return ESLINT_CONFIG_EXTENSIONS.map((extension) => `eslint.config.${extension}`).filter((filename) => (0, node_fs.existsSync)((0, node_path.join)(cwd, filename)));
780
+ }
781
+ /**
782
+ * The three filenames `useInsider()` requires, resolved against a consumer directory. A JavaScript
783
+ * flat config is renamed to the TypeScript extension — it cannot import the lint module on every
784
+ * supported Node version — and every other `eslint.config.*` in the directory is reported through
785
+ * `replacedEslintConfigs` so the caller deletes it. ESLint resolves `.js`/`.mjs`/`.cjs` before the
786
+ * TypeScript extensions, so a leftover JavaScript config keeps winning over the one just written.
787
+ * @example
788
+ * resolveLintLayout('/repo');
789
+ * // { eslintConfig: 'eslint.config.ts', replacedEslintConfigs: ['eslint.config.js'], ... }
790
+ */
791
+ function resolveLintLayout(cwd) {
792
+ const extension = resolveLayoutExtension(cwd);
793
+ const existing = existingEslintConfigs(cwd);
794
+ const eslintConfig = existing.find((filename) => TYPESCRIPT_CONFIG_EXTENSIONS.has(extensionOf(filename))) ?? `eslint.config.${extension}`;
795
+ return {
796
+ extension,
797
+ lintModule: `insider.lint.${extension}`,
798
+ oxlintConfig: `oxlint.config.${extension}`,
799
+ eslintConfig,
800
+ replacedEslintConfigs: existing.filter((filename) => filename !== eslintConfig)
801
+ };
802
+ }
803
+ /**
804
+ * Body of the generated `eslint.config.*`. The import names the lint module's real extension
805
+ * because neither linter's loader applies TypeScript's `.js` → `.ts` specifier rewriting.
806
+ * @example
807
+ * renderEslintConfigFile('insider.lint.ts'); // "import { useInsiderESLint } from './insider.lint.ts';\n…"
808
+ */
809
+ function renderEslintConfigFile(lintModule) {
810
+ return [
811
+ `import { useInsiderESLint } from './${lintModule}';`,
812
+ "",
813
+ "export default useInsiderESLint();",
814
+ ""
815
+ ].join("\n");
816
+ }
817
+ /**
818
+ * Body of the generated `oxlint.config.*`.
819
+ * @example
820
+ * renderOxlintConfigFile('insider.lint.mts'); // "…export default await useInsiderOXC();\n"
821
+ */
822
+ function renderOxlintConfigFile(lintModule) {
823
+ return [
824
+ `import { useInsiderOXC } from './${lintModule}';`,
825
+ "",
826
+ "export default await useInsiderOXC();",
827
+ ""
828
+ ].join("\n");
829
+ }
830
+ /**
831
+ * The two thin files that sit beside a lint module, ready to write.
832
+ * @example
833
+ * renderLintLayoutFiles(resolveLintLayout('/repo')).map(file => file.filename);
834
+ * // ['eslint.config.mts', 'oxlint.config.mts']
835
+ */
836
+ function renderLintLayoutFiles(layout) {
837
+ return [{
838
+ filename: layout.eslintConfig,
839
+ source: renderEslintConfigFile(layout.lintModule)
840
+ }, {
841
+ filename: layout.oxlintConfig,
842
+ source: renderOxlintConfigFile(layout.lintModule)
843
+ }];
844
+ }
845
+ var DEFAULT_INDENT = 2;
846
+ function detectIndent$1(raw) {
847
+ const firstBreak = raw.indexOf("\n");
848
+ if (firstBreak === -1) return DEFAULT_INDENT;
849
+ const rest = raw.slice(firstBreak + 1);
850
+ const width = rest.length - rest.trimStart().length;
851
+ return width > 0 ? width : DEFAULT_INDENT;
852
+ }
853
+ var SEGMENT_SEPARATOR = /&&|\|\||[;|]/u;
854
+ var ASSIGNMENT = /^[A-Za-z_]\w*=/u;
855
+ var WRAPPER_COMMANDS = /* @__PURE__ */ new Set([
856
+ "bunx",
857
+ "cross-env",
858
+ "dotenv",
859
+ "exec",
860
+ "npx",
861
+ "pnpm",
862
+ "run",
863
+ "yarn"
864
+ ]);
865
+ function baseName(token) {
866
+ return token.slice(token.lastIndexOf("/") + 1).replace(/\.[mc]?js$/u, "");
867
+ }
868
+ function segmentsOf(command) {
869
+ return command.split(SEGMENT_SEPARATOR).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
870
+ }
871
+ function tokensOf(segment) {
872
+ return segment.split(/\s+/u).filter((token) => token.length > 0 && !ASSIGNMENT.test(token));
873
+ }
874
+ function commandWordOf(segment) {
875
+ return tokensOf(segment).map(baseName).find((word) => !word.startsWith("-") && !WRAPPER_COMMANDS.has(word)) ?? null;
876
+ }
877
+ /**
878
+ * Whether a package script really invokes the `eslint` binary, as opposed to delegating to another
879
+ * script that does. `npm run lint:es` is not an ESLint invocation; `NODE_OPTIONS=… eslint .` is.
880
+ * @example
881
+ * invokesBinary('npm run lint:es-ci -- --cache', 'eslint'); // false
882
+ */
883
+ function invokesBinary(command, binary) {
884
+ return segmentsOf(command).some((segment) => commandWordOf(segment) === binary);
885
+ }
886
+ function oxlintPrefixFor(command) {
887
+ return tokensOf(command).includes("--fix") ? `${OXLINT_COMMAND} --fix` : OXLINT_COMMAND;
888
+ }
889
+ var CANONICAL_LINT_SCRIPT = "lint";
890
+ function referencedScripts(command, names) {
891
+ return tokensOf(command).map((token) => token.replace(/^["']|["']$/gu, "").replace(/^npm:/u, "")).filter((token) => names.has(token));
892
+ }
893
+ function collectReachable(scripts, names, script, seen) {
894
+ if (seen.has(script)) return;
895
+ seen.add(script);
896
+ referencedScripts(scripts[script] ?? "", names).forEach((next) => {
897
+ collectReachable(scripts, names, next, seen);
898
+ });
899
+ }
900
+ function reachableFromLint(scripts) {
901
+ const names = new Set(Object.keys(scripts));
902
+ const seen = /* @__PURE__ */ new Set();
903
+ if (names.has(CANONICAL_LINT_SCRIPT)) collectReachable(scripts, names, CANONICAL_LINT_SCRIPT, seen);
904
+ return seen;
905
+ }
906
+ function fixingRank(candidate) {
907
+ return tokensOf(candidate.previous).includes("--fix") ? 1 : 0;
908
+ }
909
+ function compareCandidates(left, right) {
910
+ return fixingRank(left) - fixingRank(right) || left.script.length - right.script.length || left.script.localeCompare(right.script);
911
+ }
912
+ function selectTarget(scripts, candidates) {
913
+ if (candidates.length === 0) return null;
914
+ const reachable = reachableFromLint(scripts);
915
+ const preferred = candidates.filter((candidate) => reachable.has(candidate.script));
916
+ return [...preferred.length > 0 ? preferred : candidates].sort(compareCandidates)[0];
917
+ }
918
+ function planChanges(scripts) {
919
+ const alreadyRunsOxlint = [];
920
+ const candidates = [];
921
+ Object.entries(scripts).forEach(([script, previous]) => {
922
+ if (!invokesBinary(previous, "eslint")) return;
923
+ if (invokesBinary(previous, "oxlint")) {
924
+ alreadyRunsOxlint.push(script);
699
925
  return;
700
926
  }
701
- await handleGenericFailure(opts.clack, opts.deps, opts.autoConfirm, installer, reason, context);
927
+ candidates.push({
928
+ script,
929
+ previous
930
+ });
931
+ });
932
+ const target = selectTarget(scripts, candidates);
933
+ if (!target) return {
934
+ changes: [],
935
+ alreadyRunsOxlint,
936
+ skipped: []
937
+ };
938
+ return {
939
+ changes: [{
940
+ script: target.script,
941
+ previous: target.previous,
942
+ next: `${oxlintPrefixFor(target.previous)} && ${target.previous}`
943
+ }],
944
+ alreadyRunsOxlint,
945
+ skipped: candidates.filter((candidate) => candidate.script !== target.script)
946
+ };
947
+ }
948
+ /**
949
+ * Adds oxlint beside the one script that actually runs ESLint, by prepending to it rather than
950
+ * replacing it — so a composed `lint` such as `run-p lint:type lint:es lint:style` keeps its
951
+ * type-check and stylelint passes and only its `lint:es-ci` leaf is rewritten. Sibling entry points
952
+ * that also run ESLint (`lint:fix`, `lint:ci`) are reported through `skipped` rather than rewritten,
953
+ * so oxlint runs once per lint pass. Never rewrites a script it did not author: when nothing invokes
954
+ * `eslint` and a `lint` script already exists, the manifest is left alone and the conflict reported.
955
+ * @example
956
+ * setLintScript('{"scripts":{"lint":"eslint ."}}').changes;
957
+ * // [{ script: 'lint', previous: 'eslint .', next: 'oxlint --disable-nested-config && eslint .' }]
958
+ */
959
+ function setLintScript(rawManifest) {
960
+ let parsed;
961
+ try {
962
+ parsed = JSON.parse(rawManifest);
963
+ } catch {
964
+ return {
965
+ manifest: rawManifest,
966
+ changed: false,
967
+ changes: [],
968
+ alreadyRunsOxlint: [],
969
+ skipped: []
970
+ };
971
+ }
972
+ const scripts = parsed.scripts ?? {};
973
+ const { changes, alreadyRunsOxlint, skipped } = planChanges(scripts);
974
+ const existingLint = scripts.lint;
975
+ if (changes.length === 0 && alreadyRunsOxlint.length === 0) {
976
+ if (existingLint !== void 0) return {
977
+ manifest: rawManifest,
978
+ changed: false,
979
+ changes: [],
980
+ alreadyRunsOxlint: [],
981
+ skipped: [],
982
+ unresolved: {
983
+ reason: "lint-script-runs-neither",
984
+ script: "lint",
985
+ previous: existingLint
986
+ }
987
+ };
988
+ changes.push({
989
+ script: "lint",
990
+ previous: "",
991
+ next: OXLINT_LINT_SCRIPT
992
+ });
702
993
  }
994
+ if (changes.length === 0) return {
995
+ manifest: rawManifest,
996
+ changed: false,
997
+ changes: [],
998
+ alreadyRunsOxlint,
999
+ skipped
1000
+ };
1001
+ parsed.scripts = { ...scripts };
1002
+ changes.forEach((change) => {
1003
+ parsed.scripts[change.script] = change.next;
1004
+ });
1005
+ return {
1006
+ manifest: `${JSON.stringify(parsed, null, detectIndent$1(rawManifest))}\n`,
1007
+ changed: true,
1008
+ changes,
1009
+ alreadyRunsOxlint,
1010
+ skipped
1011
+ };
1012
+ }
1013
+ /**
1014
+ * The scripts that still run ESLint alone, for the CLI to print. Each line carries the oxlint prefix
1015
+ * that script would need, so a consumer who wants oxlint on a second entry point can paste it.
1016
+ * @example
1017
+ * formatSkippedLintScripts([{ script: 'lint:fix', previous: 'eslint . --fix' }]);
1018
+ * // 'lint:fix\n prefix with: oxlint --disable-nested-config --fix && '
1019
+ */
1020
+ function formatSkippedLintScripts(skipped) {
1021
+ return skipped.map((candidate) => `${candidate.script}\n prefix with: ${oxlintPrefixFor(candidate.previous)} && `).join("\n\n");
1022
+ }
1023
+ /**
1024
+ * One line per rewritten script, for the CLI to print. The previous command survives verbatim inside
1025
+ * the new one, so the report doubles as the recovery instructions.
1026
+ * @example
1027
+ * formatLintScriptChanges([{ script: 'lint:es-ci', previous: 'eslint .', next: 'oxlint && eslint .' }]);
1028
+ * // 'lint:es-ci\n before: eslint .\n after: oxlint && eslint .'
1029
+ */
1030
+ function formatLintScriptChanges(changes) {
1031
+ return changes.map((change) => [
1032
+ change.script,
1033
+ ` before: ${change.previous === "" ? "(no such script)" : change.previous}`,
1034
+ ` after: ${change.next}`
1035
+ ].join("\n")).join("\n\n");
703
1036
  }
704
1037
  //#endregion
705
1038
  //#region src/cli/quote.ts
@@ -995,11 +1328,17 @@ function formatScope$1(scope, entries, baseIndent) {
995
1328
  function renderUseInsiderBody(merged) {
996
1329
  return [...merged.entries()].map(([scope, entries]) => formatScope$1(scope, entries, indent$1)).join("\n");
997
1330
  }
998
- function renderConfigFile(answers) {
1331
+ /**
1332
+ * Body of the generated `insider.lint.*` module — the one file that calls `useInsider()` and the
1333
+ * only one both linter configs import.
1334
+ * @example
1335
+ * renderLintModuleFile(answers); // "…export const { useInsiderESLint, useInsiderOXC } = useInsider({…"
1336
+ */
1337
+ function renderLintModuleFile(answers) {
999
1338
  return [
1000
1339
  "import { useInsider } from '@useinsider/eslint-config';",
1001
1340
  "",
1002
- "export default useInsider({",
1341
+ "export const { useInsiderESLint, useInsiderOXC } = useInsider({",
1003
1342
  renderUseInsiderBody(mergeByScope(buildScopedConfigs(answers))),
1004
1343
  "});",
1005
1344
  ""
@@ -1440,7 +1779,11 @@ var SCOPE_TO_DEPS = {
1440
1779
  vitest: dependencies$11,
1441
1780
  jsdocBasedTs: dependencies$3
1442
1781
  };
1443
- var ALL_PRESET_DEPS = [dependencies$2, ...Object.values(SCOPE_TO_DEPS)];
1782
+ var ALL_PRESET_DEPS = [
1783
+ dependencies$14,
1784
+ dependencies$2,
1785
+ ...Object.values(SCOPE_TO_DEPS)
1786
+ ];
1444
1787
  var PRESET_DEPENDENCY_KEYS = new Set(ALL_PRESET_DEPS.flatMap((record) => Object.keys(record)));
1445
1788
  function isEslintRelatedDependency(name) {
1446
1789
  if (SAFELIST.has(name)) return false;
@@ -1538,6 +1881,9 @@ function buildInstallList(scopes, manifest) {
1538
1881
  const fallbacks = [];
1539
1882
  appendSpec(specs, "@useinsider/eslint-config@latest");
1540
1883
  appendSpec(specs, resolveSpec("eslint", manifest, fallbacks));
1884
+ Object.entries(dependencies$14).forEach(([packageName, shouldInstall]) => {
1885
+ if (shouldInstall) appendSpec(specs, resolveSpec(packageName, manifest, fallbacks));
1886
+ });
1541
1887
  [...scopes].forEach((scope) => {
1542
1888
  const record = SCOPE_TO_DEPS[scope];
1543
1889
  if (!record) return;
@@ -1597,6 +1943,448 @@ function pruneConsumerPackageJson(cwd) {
1597
1943
  };
1598
1944
  }
1599
1945
  //#endregion
1946
+ //#region src/cli/tsconfigFlag.ts
1947
+ var TS_EXTENSIONS_FLAG = "allowImportingTsExtensions";
1948
+ var TS_EXTENSIONS_SNIPPET = [
1949
+ "{",
1950
+ " \"compilerOptions\": {",
1951
+ ` "${TS_EXTENSIONS_FLAG}": true`,
1952
+ " }",
1953
+ "}"
1954
+ ].join("\n");
1955
+ var DEFAULT_INDENT_UNIT = " ";
1956
+ function acceptsTsExtensionsFlag(options) {
1957
+ return options.noEmit === true || options.emitDeclarationOnly === true || options.rewriteRelativeImportExtensions === true;
1958
+ }
1959
+ function parseTsconfig(path, raw) {
1960
+ const { config } = typescript.parseConfigFileTextToJson(path, raw);
1961
+ if (!config) return null;
1962
+ const parsed = typescript.parseJsonConfigFileContent(config, typescript.sys, (0, node_path.dirname)(path), void 0, path);
1963
+ return {
1964
+ enabled: parsed.options.allowImportingTsExtensions === true,
1965
+ acceptsFlag: acceptsTsExtensionsFlag(parsed.options),
1966
+ fileNames: parsed.fileNames,
1967
+ references: (parsed.projectReferences ?? []).map((reference) => reference.path)
1968
+ };
1969
+ }
1970
+ function readTsconfigAt(path, governedFiles) {
1971
+ if (!(0, node_fs.existsSync)(path)) return null;
1972
+ try {
1973
+ const raw = (0, node_fs.readFileSync)(path, "utf8");
1974
+ const parsed = parseTsconfig(path, raw);
1975
+ if (!parsed) return {
1976
+ state: {
1977
+ path,
1978
+ raw,
1979
+ enabled: false,
1980
+ governs: false,
1981
+ acceptsFlag: false
1982
+ },
1983
+ references: []
1984
+ };
1985
+ const owned = new Set(parsed.fileNames.map((name) => typescript.sys.resolvePath(name)));
1986
+ const governs = governedFiles.some((file) => owned.has(typescript.sys.resolvePath(file)));
1987
+ return {
1988
+ state: {
1989
+ path,
1990
+ raw,
1991
+ enabled: parsed.enabled,
1992
+ governs,
1993
+ acceptsFlag: parsed.acceptsFlag
1994
+ },
1995
+ references: parsed.references
1996
+ };
1997
+ } catch {
1998
+ return null;
1999
+ }
2000
+ }
2001
+ function resolveReferencePath(reference) {
2002
+ if (!(0, node_fs.existsSync)(reference)) return reference;
2003
+ return typescript.sys.directoryExists(reference) ? (0, node_path.join)(reference, "tsconfig.json") : reference;
2004
+ }
2005
+ /**
2006
+ * Every tsconfig in the consumer's solution — the root plus each project reference — annotated with
2007
+ * whether it actually type-checks the generated lint files. A solution-style root has an empty file
2008
+ * set, so the project that governs `insider.lint.ts` is usually a referenced `tsconfig.node.json`,
2009
+ * and that is the one `tsc` reports TS5097 from.
2010
+ * @example
2011
+ * collectTsconfigStates('/repo', ['/repo/insider.lint.ts']).filter(state => state.governs);
2012
+ * // [{ path: '/repo/tsconfig.node.json', enabled: true, governs: true, ... }]
2013
+ */
2014
+ function collectTsconfigStates(cwd, governedFiles = []) {
2015
+ const root = readTsconfigAt((0, node_path.join)(cwd, "tsconfig.json"), governedFiles);
2016
+ if (!root) return [];
2017
+ const seen = /* @__PURE__ */ new Set([root.state.path]);
2018
+ const states = [root.state];
2019
+ root.references.forEach((reference) => {
2020
+ const path = resolveReferencePath(reference);
2021
+ if (seen.has(path)) return;
2022
+ seen.add(path);
2023
+ const referenced = readTsconfigAt(path, governedFiles);
2024
+ if (referenced) states.push(referenced.state);
2025
+ });
2026
+ return states;
2027
+ }
2028
+ /**
2029
+ * The tsconfigs that must gain `allowImportingTsExtensions`: the ones that type-check the generated
2030
+ * files and do not already resolve the flag to `true`. Empty when an applicable config already sets
2031
+ * it, and empty when no config type-checks the generated files at all — a tsconfig that never sees
2032
+ * them cannot raise TS5097, so editing it would only add a flag it has no use for.
2033
+ * @example
2034
+ * tsconfigsNeedingFlag('/repo', ['/repo/insider.lint.ts']).map(state => state.path); // []
2035
+ */
2036
+ function tsconfigsNeedingFlag(cwd, governedFiles) {
2037
+ return collectTsconfigStates(cwd, governedFiles).filter((state) => state.governs && !state.enabled);
2038
+ }
2039
+ function rootObject(source) {
2040
+ const [statement] = source.statements;
2041
+ const expression = statement?.expression;
2042
+ return expression && typescript.isObjectLiteralExpression(expression) ? expression : null;
2043
+ }
2044
+ function findProperty$2(object, name) {
2045
+ return object.properties.find((property) => typescript.isPropertyAssignment(property) && typescript.isStringLiteral(property.name) && property.name.text === name);
2046
+ }
2047
+ function indentAt(raw, position) {
2048
+ const lineStart = raw.lastIndexOf("\n", position - 1) + 1;
2049
+ const prefix = raw.slice(lineStart, position);
2050
+ return prefix.trim().length === 0 ? prefix : "";
2051
+ }
2052
+ function detectIndentUnit(raw) {
2053
+ const match = /\n([\t ]+)\S/u.exec(raw);
2054
+ return match ? match[1] : DEFAULT_INDENT_UNIT;
2055
+ }
2056
+ function splice(raw, start, end, text) {
2057
+ return raw.slice(0, start) + text + raw.slice(end);
2058
+ }
2059
+ function setExistingFlag(raw, source, property) {
2060
+ return splice(raw, property.initializer.getStart(source), property.initializer.getEnd(), "true");
2061
+ }
2062
+ function insertIntoCompilerOptions(raw, source, compilerOptions, object) {
2063
+ const openBrace = object.getStart(source) + 1;
2064
+ const [first] = object.properties;
2065
+ if (first) return splice(raw, openBrace, openBrace, `\n${indentAt(raw, first.getStart(source)) || detectIndentUnit(raw).repeat(2)}"${TS_EXTENSIONS_FLAG}": true,`);
2066
+ const outer = indentAt(raw, compilerOptions.getStart(source));
2067
+ const unit = detectIndentUnit(raw);
2068
+ return splice(raw, openBrace, object.getEnd() - 1, `\n${outer}${unit}"${TS_EXTENSIONS_FLAG}": true\n${outer}`);
2069
+ }
2070
+ function insertCompilerOptions(raw, source, root) {
2071
+ const openBrace = root.getStart(source) + 1;
2072
+ const unit = detectIndentUnit(raw);
2073
+ const [first] = root.properties;
2074
+ const indent = first ? indentAt(raw, first.getStart(source)) || unit : unit;
2075
+ const block = `\n${indent}"compilerOptions": {\n${indent}${unit}"${TS_EXTENSIONS_FLAG}": true\n${indent}}`;
2076
+ if (first) return splice(raw, openBrace, openBrace, `${block},`);
2077
+ return splice(raw, openBrace, root.getEnd() - 1, `${block}\n`);
2078
+ }
2079
+ /**
2080
+ * Turns `allowImportingTsExtensions` on in a `tsconfig.json` source without reformatting it. The
2081
+ * file is edited through its JSON syntax tree rather than re-serialized, so comments, key order and
2082
+ * indentation survive.
2083
+ * @example
2084
+ * enableTsExtensionsFlag('{\n "compilerOptions": {\n "strict": true\n }\n}').changed; // true
2085
+ */
2086
+ function enableTsExtensionsFlag(raw) {
2087
+ const source = typescript.parseJsonText("tsconfig.json", raw);
2088
+ const root = rootObject(source);
2089
+ if (!root) return {
2090
+ source: raw,
2091
+ changed: false
2092
+ };
2093
+ const compilerOptions = findProperty$2(root, "compilerOptions");
2094
+ if (!compilerOptions) return {
2095
+ source: insertCompilerOptions(raw, source, root),
2096
+ changed: true
2097
+ };
2098
+ if (!typescript.isObjectLiteralExpression(compilerOptions.initializer)) return {
2099
+ source: raw,
2100
+ changed: false
2101
+ };
2102
+ const existing = findProperty$2(compilerOptions.initializer, TS_EXTENSIONS_FLAG);
2103
+ if (existing) {
2104
+ if (existing.initializer.kind === typescript.SyntaxKind.TrueKeyword) return {
2105
+ source: raw,
2106
+ changed: false
2107
+ };
2108
+ return {
2109
+ source: setExistingFlag(raw, source, existing),
2110
+ changed: true
2111
+ };
2112
+ }
2113
+ return {
2114
+ source: insertIntoCompilerOptions(raw, source, compilerOptions, compilerOptions.initializer),
2115
+ changed: true
2116
+ };
2117
+ }
2118
+ function noteManualStep(clack, lintModule) {
2119
+ if (!clack.note) return;
2120
+ clack.note(`\`eslint.config.*\` imports \`./${lintModule}\` by its real extension, which \`tsc\` rejects with TS5097 unless \`${TS_EXTENSIONS_FLAG}\` is on. Add this to your tsconfig.json:\n\n${TS_EXTENSIONS_SNIPPET}`, "tsconfig.json not updated");
2121
+ }
2122
+ function noteEmitConflict(clack, path, lintModule) {
2123
+ if (!clack.note) return;
2124
+ clack.note(`${path} type-checks \`./${lintModule}\`, so \`tsc\` needs \`${TS_EXTENSIONS_FLAG}\` there — but that project emits, and TypeScript rejects the pair with TS5096. Set \`noEmit\`, \`emitDeclarationOnly\` or \`rewriteRelativeImportExtensions\` on it first, then add:\n\n${TS_EXTENSIONS_SNIPPET}`, "tsconfig.json not updated");
2125
+ }
2126
+ async function confirmFlag(clack, path) {
2127
+ const result = await clack.confirm({
2128
+ message: `Set \`${TS_EXTENSIONS_FLAG}: true\` in ${path}? Without it \`tsc\` fails with TS5097 on the generated config files.`,
2129
+ initialValue: true
2130
+ });
2131
+ return !clack.isCancel(result) && result === true;
2132
+ }
2133
+ function governedFilesOf(input) {
2134
+ return [input.lintModule, input.eslintConfig].filter((name) => Boolean(name)).map((name) => (0, node_path.join)(input.cwd, name));
2135
+ }
2136
+ async function applyToOne(clack, state, input) {
2137
+ if (!state.acceptsFlag) {
2138
+ noteEmitConflict(clack, state.path, input.lintModule);
2139
+ return;
2140
+ }
2141
+ if (!(input.autoConfirm || await confirmFlag(clack, state.path))) {
2142
+ noteManualStep(clack, input.lintModule);
2143
+ return;
2144
+ }
2145
+ const result = enableTsExtensionsFlag(state.raw);
2146
+ if (!result.changed) {
2147
+ noteManualStep(clack, input.lintModule);
2148
+ return;
2149
+ }
2150
+ (0, node_fs.writeFileSync)(state.path, result.source, "utf8");
2151
+ if (clack.note) clack.note(`Set \`${TS_EXTENSIONS_FLAG}: true\` in ${state.path}.`, "Updated tsconfig.json");
2152
+ }
2153
+ /**
2154
+ * Offers to set `allowImportingTsExtensions` on every tsconfig that actually type-checks the
2155
+ * generated lint files, and prints the exact snippet when an offer is declined or when the project
2156
+ * emits — in which case writing the flag would trade TS5097 for TS5096. A no-op when there is no
2157
+ * `tsconfig.json`, when no config governs the generated files, or when an applicable config already
2158
+ * resolves the flag to `true` — through its own `compilerOptions`, through `extends`, or by being a
2159
+ * project reference that already sets it.
2160
+ * @example
2161
+ * await applyTsExtensionsFlag(clack, { cwd: '/repo', lintModule: 'insider.lint.ts' });
2162
+ */
2163
+ async function applyTsExtensionsFlag(clack, input) {
2164
+ await tsconfigsNeedingFlag(input.cwd, governedFilesOf(input)).reduce(async (previous, state) => {
2165
+ await previous;
2166
+ return applyToOne(clack, state, input);
2167
+ }, Promise.resolve());
2168
+ }
2169
+ //#endregion
2170
+ //#region src/cli/migrate/disableDirectives.ts
2171
+ var DIRECTIVE_HEAD = /^\s*eslint-disable(-next-line|-line)?(?![\w-])/u;
2172
+ var SCRIPT_BLOCK = /<script\b[^>]*>([\s\S]*?)<\/script>/giu;
2173
+ var SFC_EXTENSIONS = [
2174
+ ".astro",
2175
+ ".svelte",
2176
+ ".vue"
2177
+ ];
2178
+ var JSX_EXTENSIONS = [
2179
+ ".cjs",
2180
+ ".js",
2181
+ ".jsx",
2182
+ ".mjs",
2183
+ ".tsx"
2184
+ ];
2185
+ var LINTED_EXTENSIONS = [
2186
+ ".astro",
2187
+ ".cjs",
2188
+ ".cts",
2189
+ ".js",
2190
+ ".jsx",
2191
+ ".mjs",
2192
+ ".mts",
2193
+ ".svelte",
2194
+ ".ts",
2195
+ ".tsx",
2196
+ ".vue"
2197
+ ];
2198
+ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
2199
+ ".git",
2200
+ "coverage",
2201
+ "dist",
2202
+ "build",
2203
+ "node_modules",
2204
+ "out",
2205
+ "vendor"
2206
+ ]);
2207
+ function parseDirectiveBody(body) {
2208
+ const [ruleList, ...reasonParts] = body.split("--");
2209
+ return {
2210
+ rules: ruleList.split(",").map((rule) => rule.trim()).filter((rule) => rule.length > 0),
2211
+ reason: reasonParts.join("--").trim()
2212
+ };
2213
+ }
2214
+ function formatDirective(owner, block, kind, rules, reason) {
2215
+ const text = `${owner}-disable${kind} ${reason ? `${rules.join(", ")} -- ${reason}` : rules.join(", ")}`;
2216
+ return block ? `/* ${text} */` : `// ${text}`;
2217
+ }
2218
+ /**
2219
+ * Whether the split hands this rule to oxlint for the given file, so an `eslint-disable` comment
2220
+ * naming it would read as an unused directive to ESLint.
2221
+ * @example
2222
+ * movesToOxlint('no-console', '.ts'); // true
2223
+ * movesToOxlint('@typescript-eslint/no-unused-vars', '.vue'); // false — ESLint still runs it in SFCs
2224
+ */
2225
+ function movesToOxlint(rule, fileExtension) {
2226
+ if (!require_oxlintOwnership.isOxlintOwned(rule)) return false;
2227
+ return !(SFC_EXTENSIONS.includes(fileExtension) && require_oxlintOwnership.isVueUnsafeRule(rule));
2228
+ }
2229
+ function toSpans(comments, offset) {
2230
+ return comments.map((comment) => ({
2231
+ start: comment.range[0] + offset,
2232
+ end: comment.range[1] + offset,
2233
+ block: comment.type === _typescript_eslint_types.AST_TOKEN_TYPES.Block,
2234
+ value: comment.value
2235
+ }));
2236
+ }
2237
+ function parseComments(source, jsx) {
2238
+ try {
2239
+ return (0, _typescript_eslint_parser.parse)(source, {
2240
+ comment: true,
2241
+ jsx,
2242
+ range: true
2243
+ }).comments ?? [];
2244
+ } catch {
2245
+ return null;
2246
+ }
2247
+ }
2248
+ function collectFromScript(source, jsx, offset) {
2249
+ const comments = parseComments(source, jsx) ?? parseComments(source, !jsx);
2250
+ return comments ? toSpans(comments, offset) : [];
2251
+ }
2252
+ var CLOSING_SCRIPT_TAG_LENGTH = 9;
2253
+ function collectSfcComments(source, jsx) {
2254
+ return [...source.matchAll(SCRIPT_BLOCK)].flatMap((match) => {
2255
+ const [whole, body] = match;
2256
+ return collectFromScript(body, jsx, (match.index ?? 0) + whole.length - body.length - CLOSING_SCRIPT_TAG_LENGTH);
2257
+ });
2258
+ }
2259
+ /**
2260
+ * Every real comment node in the file, with ranges relative to the whole source. Only comments are
2261
+ * returned, so a string or template literal that happens to spell out a directive is invisible here.
2262
+ * @example
2263
+ * collectComments("const label = '// eslint-disable-next-line';\n", '.ts'); // []
2264
+ */
2265
+ function collectComments(source, fileExtension) {
2266
+ const jsx = JSX_EXTENSIONS.includes(fileExtension);
2267
+ if (SFC_EXTENSIONS.includes(fileExtension)) return collectSfcComments(source, jsx);
2268
+ return collectFromScript(source, jsx, 0);
2269
+ }
2270
+ function startsItsOwnLine(source, start) {
2271
+ const lineStart = source.lastIndexOf("\n", start - 1) + 1;
2272
+ return source.slice(lineStart, start).trim().length === 0;
2273
+ }
2274
+ function splitLayout(source, span, kind) {
2275
+ if (kind !== "" || !startsItsOwnLine(source, span.start)) return {
2276
+ block: true,
2277
+ separator: " "
2278
+ };
2279
+ const indent = " ".repeat(span.start - (source.lastIndexOf("\n", span.start - 1) + 1));
2280
+ return {
2281
+ block: span.block,
2282
+ separator: `\n${indent}`
2283
+ };
2284
+ }
2285
+ function replacementFor(source, span, fileExtension) {
2286
+ const head = DIRECTIVE_HEAD.exec(span.value);
2287
+ if (!head) return null;
2288
+ const kind = head[1] ?? "";
2289
+ const { rules, reason } = parseDirectiveBody(span.value.slice(head[0].length));
2290
+ if (rules.length === 0) return null;
2291
+ const moved = rules.filter((rule) => movesToOxlint(rule, fileExtension));
2292
+ if (moved.length === 0) return null;
2293
+ const kept = rules.filter((rule) => !moved.includes(rule));
2294
+ if (kept.length === 0) return {
2295
+ start: span.start,
2296
+ end: span.end,
2297
+ text: formatDirective("oxlint", span.block, kind, moved, reason)
2298
+ };
2299
+ const { block, separator } = splitLayout(source, span, kind);
2300
+ const text = formatDirective("oxlint", block, kind, moved, reason) + separator + formatDirective("eslint", block, kind, kept, reason);
2301
+ return {
2302
+ start: span.start,
2303
+ end: span.end,
2304
+ text
2305
+ };
2306
+ }
2307
+ /**
2308
+ * Re-points the rules oxlint now owns onto an `oxlint-disable` comment, leaving the ESLint-owned
2309
+ * ones behind. Only real comment nodes are rewritten, so a string or template literal spelling out
2310
+ * a directive is left alone.
2311
+ * @example
2312
+ * rewriteDirectives('// <eslint>-disable-next-line no-console\nlog();', '.ts').source;
2313
+ * // '// oxlint-disable-next-line no-console\nlog();'
2314
+ */
2315
+ function rewriteDirectives(source, fileExtension) {
2316
+ const replacements = collectComments(source, fileExtension).map((span) => replacementFor(source, span, fileExtension)).filter((replacement) => replacement !== null).sort((left, right) => right.start - left.start);
2317
+ if (replacements.length === 0) return {
2318
+ source,
2319
+ changed: false
2320
+ };
2321
+ return {
2322
+ source: replacements.reduce((accumulator, replacement) => accumulator.slice(0, replacement.start) + replacement.text + accumulator.slice(replacement.end), source),
2323
+ changed: true
2324
+ };
2325
+ }
2326
+ function isLintedFile(filePath) {
2327
+ return LINTED_EXTENSIONS.includes((0, node_path.extname)(filePath));
2328
+ }
2329
+ function readEntries(directory) {
2330
+ try {
2331
+ return (0, node_fs.readdirSync)(directory, { withFileTypes: true });
2332
+ } catch {
2333
+ return [];
2334
+ }
2335
+ }
2336
+ function realPathOf(path) {
2337
+ try {
2338
+ return (0, node_fs.realpathSync)(path);
2339
+ } catch {
2340
+ return null;
2341
+ }
2342
+ }
2343
+ function collectFiles(directory, accumulator, visited) {
2344
+ const real = realPathOf(directory);
2345
+ if (real === null || visited.has(real)) return;
2346
+ visited.add(real);
2347
+ readEntries(directory).forEach((entry) => {
2348
+ if (SKIPPED_DIRECTORIES.has(entry.name)) return;
2349
+ const full = (0, node_path.join)(directory, entry.name);
2350
+ if (entry.isDirectory()) {
2351
+ collectFiles(full, accumulator, visited);
2352
+ return;
2353
+ }
2354
+ if (entry.isFile() && isLintedFile(full)) accumulator.push(full);
2355
+ });
2356
+ }
2357
+ /**
2358
+ * Every file whose `eslint-disable` comments would be re-pointed, and the rewritten source, without
2359
+ * touching disk. The directive rewrite is the largest diff a migration produces, so the count is
2360
+ * reported — and confirmed — before anything is written.
2361
+ * @example
2362
+ * previewDisableDirectives('/repo').length; // 131
2363
+ */
2364
+ function previewDisableDirectives(cwd) {
2365
+ const files = [];
2366
+ collectFiles(cwd, files, /* @__PURE__ */ new Set());
2367
+ return files.reduce((candidates, filePath) => {
2368
+ const result = rewriteDirectives((0, node_fs.readFileSync)(filePath, "utf8"), (0, node_path.extname)(filePath));
2369
+ if (!result.changed) return candidates;
2370
+ return [...candidates, {
2371
+ path: filePath,
2372
+ source: result.source
2373
+ }];
2374
+ }, []);
2375
+ }
2376
+ /**
2377
+ * Applies {@link rewriteDirectives} across a consumer tree, skipping build output and dependencies.
2378
+ * @example
2379
+ * rewriteDisableDirectives('/repo').rewritten; // ['/repo/src/logger.ts']
2380
+ */
2381
+ function rewriteDisableDirectives(cwd, candidates = previewDisableDirectives(cwd)) {
2382
+ candidates.forEach((candidate) => {
2383
+ (0, node_fs.writeFileSync)(candidate.path, candidate.source, "utf8");
2384
+ });
2385
+ return { rewritten: candidates.map((candidate) => candidate.path) };
2386
+ }
2387
+ //#endregion
1600
2388
  //#region src/cli/migrate/stringEntry.ts
1601
2389
  function toEntries(values) {
1602
2390
  return values.map((value) => ({ value }));
@@ -2214,7 +3002,7 @@ function appendScope$1(config, scope, entry) {
2214
3002
  }
2215
3003
  function mergeGlobals(base, extra) {
2216
3004
  if (!base && !extra) return;
2217
- const merged = { ...base ?? {} };
3005
+ const merged = { ...base };
2218
3006
  if (extra) Object.entries(extra).forEach(([key, value]) => {
2219
3007
  if (!(key in merged)) merged[key] = value;
2220
3008
  });
@@ -3539,11 +4327,11 @@ function renderV2(plan) {
3539
4327
  parts.push("");
3540
4328
  if (plan.useInsiderLeadingComment !== void 0) parts.push(plan.useInsiderLeadingComment);
3541
4329
  if (!hasAnyContent(plan)) {
3542
- parts.push("export default useInsider({});");
4330
+ parts.push("export const { useInsiderESLint, useInsiderOXC } = useInsider({});");
3543
4331
  parts.push("");
3544
4332
  return pruneUnusedImports(parts.join("\n"));
3545
4333
  }
3546
- parts.push("export default useInsider({");
4334
+ parts.push("export const { useInsiderESLint, useInsiderOXC } = useInsider({");
3547
4335
  parts.push(renderConfigBody(plan));
3548
4336
  parts.push("});");
3549
4337
  parts.push("");
@@ -3903,15 +4691,56 @@ function formatWarnings(warnings) {
3903
4691
  }
3904
4692
  //#endregion
3905
4693
  //#region src/cli/migrate/writer.ts
4694
+ function writeLayout(directory, lintModuleSource) {
4695
+ const layout = resolveLintLayout(directory);
4696
+ const lintModulePath = (0, node_path.join)(directory, layout.lintModule);
4697
+ (0, node_fs.writeFileSync)(lintModulePath, lintModuleSource, "utf8");
4698
+ const writtenPaths = [lintModulePath];
4699
+ renderLintLayoutFiles(layout).forEach((file) => {
4700
+ const path = (0, node_path.join)(directory, file.filename);
4701
+ (0, node_fs.writeFileSync)(path, file.source, "utf8");
4702
+ writtenPaths.push(path);
4703
+ });
4704
+ return {
4705
+ eslintConfigPath: (0, node_path.join)(directory, layout.eslintConfig),
4706
+ lintModulePath,
4707
+ writtenPaths,
4708
+ replacedEslintConfigPaths: layout.replacedEslintConfigs.map((name) => (0, node_path.join)(directory, name))
4709
+ };
4710
+ }
4711
+ function withoutDuplicates(paths, keep) {
4712
+ return [...new Set(paths.filter((path) => path !== keep))];
4713
+ }
4714
+ /**
4715
+ * Writes the three-file layout beside an existing flat config: the rendered `useInsider()` call
4716
+ * becomes `insider.lint.*`, and `eslint.config.*` / `oxlint.config.*` become thin re-exports. A
4717
+ * JavaScript-extension source config is superseded by the TypeScript one and nominated for deletion.
4718
+ * @example
4719
+ * writeMigration('/repo/eslint.config.js', source).deletedSourceCandidates; // ['/repo/eslint.config.js']
4720
+ */
3906
4721
  function writeMigration(targetPath, newSource) {
3907
- (0, node_fs.writeFileSync)(targetPath, newSource, "utf8");
3908
- return { targetPath };
4722
+ const result = writeLayout((0, node_path.dirname)(targetPath), newSource);
4723
+ return {
4724
+ targetPath: result.eslintConfigPath,
4725
+ lintModulePath: result.lintModulePath,
4726
+ writtenPaths: result.writtenPaths,
4727
+ deletedSourceCandidates: withoutDuplicates([...result.replacedEslintConfigPaths, targetPath], result.eslintConfigPath)
4728
+ };
3909
4729
  }
4730
+ /**
4731
+ * Same as {@link writeMigration}, but for a legacy `.eslintrc*` source that has to be replaced by a
4732
+ * differently-named flat config.
4733
+ * @example
4734
+ * writeMigrationLegacy('/repo/.eslintrc.cjs', '/repo/eslint.config.mts', src).deletedSourceCandidates;
4735
+ * // ['/repo/.eslintrc.cjs']
4736
+ */
3910
4737
  function writeMigrationLegacy(sourcePath, writePath, newSource) {
3911
- (0, node_fs.writeFileSync)(writePath, newSource, "utf8");
4738
+ const result = writeLayout((0, node_path.dirname)(writePath), newSource);
3912
4739
  return {
3913
- targetPath: writePath,
3914
- deletedSourceCandidate: (0, node_path.basename)(sourcePath) === (0, node_path.basename)(writePath) ? void 0 : sourcePath
4740
+ targetPath: result.eslintConfigPath,
4741
+ lintModulePath: result.lintModulePath,
4742
+ writtenPaths: result.writtenPaths,
4743
+ deletedSourceCandidates: withoutDuplicates([...result.replacedEslintConfigPaths, sourcePath], result.eslintConfigPath)
3915
4744
  };
3916
4745
  }
3917
4746
  //#endregion
@@ -3986,26 +4815,14 @@ function summary(plan, format) {
3986
4815
  function planFromSource(source, filePath, format) {
3987
4816
  if (format === "flat") {
3988
4817
  const parseResult = parseV1Config(source, filePath);
3989
- if (!isV1Shape(parseResult)) return null;
3990
- const plan = transformToV2(parseResult);
3991
- return {
3992
- plan,
3993
- rendered: renderV2(plan)
3994
- };
4818
+ return isV1Shape(parseResult) ? transformToV2(parseResult) : null;
3995
4819
  }
3996
4820
  const parseResult = parseEslintrc(source, filePath);
3997
- if (!parseResult.found) return null;
3998
- const plan = transformEslintrcToV2(parseResult);
3999
- return {
4000
- plan,
4001
- rendered: renderV2(plan)
4002
- };
4821
+ return parseResult.found ? transformEslintrcToV2(parseResult) : null;
4003
4822
  }
4004
- function targetWritePath(cwd, sourcePath, format) {
4005
- if (format !== "eslintrc") return sourcePath;
4006
- const base = (0, node_path.basename)(sourcePath);
4007
- const dir = sourcePath.slice(0, sourcePath.length - base.length);
4008
- return (0, node_path.join)(dir.length > 0 ? dir : cwd, "eslint.config.mjs");
4823
+ function targetWritePath(cwd, sourcePath) {
4824
+ const directory = (0, node_path.dirname)(sourcePath) || cwd;
4825
+ return (0, node_path.join)(directory, resolveLintLayout(directory).eslintConfig);
4009
4826
  }
4010
4827
  function parseEslintIgnoreContents(raw) {
4011
4828
  return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
@@ -4039,6 +4856,17 @@ function describeDepChanges(depChanges) {
4039
4856
  lines.push(`- Install (${depChanges.install.specs.length}): ${depChanges.install.specs.join(", ")}`);
4040
4857
  return lines.join("\n");
4041
4858
  }
4859
+ async function confirmGuidedStart(clack, sourcePath) {
4860
+ const result = await clack.confirm({
4861
+ message: `${(0, node_path.basename)(sourcePath)} still uses the v3 single-file layout. Migrate it now?`,
4862
+ initialValue: true
4863
+ });
4864
+ return !clack.isCancel(result) && result === true;
4865
+ }
4866
+ function noteConfigRename(clack, sourcePath, writePath) {
4867
+ if (!clack.note || (0, node_path.basename)(sourcePath) === (0, node_path.basename)(writePath)) return;
4868
+ clack.note(`${(0, node_path.basename)(sourcePath)} -> ${(0, node_path.basename)(writePath)}\n\nESLint loads a JavaScript config through Node, so importing the TypeScript lint module from one needs unflagged type stripping (Node >= 22.18) and fails on Node 20. A TypeScript config goes through jiti and loads on every supported version.`, "Renaming the flat config");
4869
+ }
4042
4870
  async function confirmWrite(clack, writePath, initial = true) {
4043
4871
  const message = `Write migrated config to ${writePath}?`;
4044
4872
  const result = await clack.confirm({
@@ -4080,17 +4908,19 @@ function emitSummary(clack, plan, format) {
4080
4908
  if (plan.warnings.length > 0 && clack.note) clack.note(formatWarnings(plan.warnings), "Warnings");
4081
4909
  }
4082
4910
  function writeAndAnnounce(clack, targetPath, writePath, rendered, format) {
4083
- if (format === "eslintrc") {
4084
- const result = writeMigrationLegacy(targetPath, writePath, rendered);
4085
- clack.outro(`Wrote ${writePath}`);
4086
- return { deletedSourceCandidate: result.deletedSourceCandidate };
4087
- }
4088
- writeMigration(targetPath, rendered);
4089
- clack.outro(`Wrote ${targetPath}`);
4090
- return {};
4091
- }
4092
- async function confirmOverwriteExistingOutput(clack, writePath, sourcePath, initial = false) {
4093
- const message = `An \`${(0, node_path.basename)(writePath)}\` already exists. Overwrite + delete \`${(0, node_path.basename)(sourcePath)}\`?`;
4911
+ const result = format === "eslintrc" ? writeMigrationLegacy(targetPath, writePath, rendered) : writeMigration(targetPath, rendered);
4912
+ clack.outro(`Wrote ${result.writtenPaths.join(", ")}`);
4913
+ return { deletedSourceCandidates: result.deletedSourceCandidates };
4914
+ }
4915
+ function supersededFlatConfigs(sourcePath, writePath) {
4916
+ const directory = (0, node_path.dirname)(writePath);
4917
+ const { replacedEslintConfigs } = resolveLintLayout(directory);
4918
+ const candidates = [...replacedEslintConfigs.map((filename) => (0, node_path.join)(directory, filename)), writePath];
4919
+ return [...new Set(candidates.filter((path) => path !== sourcePath && (0, node_fs.existsSync)(path)))];
4920
+ }
4921
+ async function confirmOverwriteExistingOutput(clack, superseded, sourcePath, initial = false) {
4922
+ const names = superseded.map((path) => (0, node_path.basename)(path));
4923
+ const message = `${names.length === 1 ? `An \`${names[0]}\` already exists` : `\`${names.join("`, `")}\` already exist`}. Overwrite + delete \`${(0, node_path.basename)(sourcePath)}\`?`;
4094
4924
  const result = await clack.confirm({
4095
4925
  message,
4096
4926
  initialValue: initial
@@ -4126,10 +4956,9 @@ function resolveSource(input, clack) {
4126
4956
  };
4127
4957
  }
4128
4958
  function prepareMigration(resolved, input) {
4129
- const planResult = planFromSource(resolved.source, resolved.targetPath, resolved.format);
4130
- if (!planResult) return null;
4131
- const { plan } = planResult;
4132
- const writePath = targetWritePath(input.cwd, resolved.targetPath, resolved.format);
4959
+ const plan = planFromSource(resolved.source, resolved.targetPath, resolved.format);
4960
+ if (!plan) return null;
4961
+ const writePath = targetWritePath(input.cwd, resolved.targetPath);
4133
4962
  const eslintIgnore = findEslintIgnore(resolved.targetPath);
4134
4963
  if (eslintIgnore && eslintIgnore.patterns.length > 0) mergeIgnorePatterns(plan, eslintIgnore.patterns);
4135
4964
  return {
@@ -4140,8 +4969,9 @@ function prepareMigration(resolved, input) {
4140
4969
  };
4141
4970
  }
4142
4971
  async function runConfirmations(clack, input, targetPath, writePath, previous) {
4143
- if (writePath !== targetPath && (0, node_fs.existsSync)(writePath)) {
4144
- if (!await confirmOverwriteExistingOutput(clack, writePath, targetPath, previous?.overwriteExisting ?? false)) {
4972
+ const superseded = supersededFlatConfigs(targetPath, writePath);
4973
+ if (superseded.length > 0) {
4974
+ if (!await confirmOverwriteExistingOutput(clack, superseded, targetPath, previous?.overwriteExisting ?? false)) {
4145
4975
  clack.outro("Aborted: existing output kept.");
4146
4976
  return false;
4147
4977
  }
@@ -4199,7 +5029,7 @@ function resolveInstallCwd(depSummary, input) {
4199
5029
  return input.cwd;
4200
5030
  }
4201
5031
  async function runInstaller(clack, depSummary, input) {
4202
- await installWithRetry({
5032
+ return installWithRetry({
4203
5033
  deps: depSummary.install.specs,
4204
5034
  cwd: resolveInstallCwd(depSummary, input),
4205
5035
  autoConfirm: Boolean(input.yes),
@@ -4211,26 +5041,73 @@ async function runInstaller(clack, depSummary, input) {
4211
5041
  async function applyDepChanges(clack, depSummary, input) {
4212
5042
  if (!depSummary.packageJsonPath) {
4213
5043
  if (clack.note) clack.note("No package.json found alongside the migrated config — skipping dependency updates.", "Notice");
4214
- return;
5044
+ return true;
4215
5045
  }
4216
5046
  persistPrunedManifest(depSummary);
4217
5047
  if (input.noInstall) {
4218
5048
  emitInstallNextSteps(clack, depSummary, input.cwd);
4219
- return;
5049
+ return true;
4220
5050
  }
4221
5051
  if (!input.yes) {
4222
5052
  if (!await confirmInstall(clack, depSummary)) {
4223
5053
  emitInstallNextSteps(clack, depSummary, input.cwd);
4224
- return;
5054
+ return true;
4225
5055
  }
4226
5056
  }
4227
5057
  noteFallbacks(clack, depSummary);
4228
- await runInstaller(clack, depSummary, input);
5058
+ if (!(await runInstaller(clack, depSummary, input)).installed) {
5059
+ emitInstallNextSteps(clack, depSummary, input.cwd);
5060
+ return false;
5061
+ }
5062
+ return true;
4229
5063
  }
4230
5064
  function performWriteAndCleanup(clack, resolved, prepared) {
4231
5065
  const outcome = writeAndAnnounce(clack, resolved.targetPath, prepared.writePath, prepared.rendered, resolved.format);
4232
5066
  if (prepared.eslintIgnore) safeUnlink(prepared.eslintIgnore.path, clack, ".eslintignore");
4233
- if (outcome.deletedSourceCandidate) safeUnlink(outcome.deletedSourceCandidate, clack, "source config");
5067
+ outcome.deletedSourceCandidates.forEach((path) => {
5068
+ safeUnlink(path, clack, "superseded config");
5069
+ });
5070
+ }
5071
+ function noteLintScriptOutcome$1(clack, result) {
5072
+ if (!clack.note) return;
5073
+ if (result.unresolved) {
5074
+ clack.note(`Left \`${result.unresolved.script}\` untouched — it runs neither eslint nor oxlint:\n\n ${result.unresolved.previous}\n\nAdd \`${OXLINT_LINT_SCRIPT}\` yourself, next to whatever invokes ESLint.`, "package.json script not changed");
5075
+ return;
5076
+ }
5077
+ if (result.alreadyRunsOxlint.length > 0 && result.changes.length === 0) {
5078
+ clack.note(`\`${result.alreadyRunsOxlint.join("`, `")}\` already run oxlint — left alone.`, "package.json scripts unchanged");
5079
+ return;
5080
+ }
5081
+ if (result.skipped.length > 0) clack.note(`${formatSkippedLintScripts(result.skipped)}\n\noxlint was added to one entry point only, so it does not run twice over the same tree.`, "package.json scripts left alone");
5082
+ if (result.changes.length === 0) return;
5083
+ clack.note(`${formatLintScriptChanges(result.changes)}\n\noxlint was prepended, never substituted, so every command that was there still runs.`, "Updated package.json scripts");
5084
+ }
5085
+ function applyLintScript$1(clack, directory) {
5086
+ const manifestPath = (0, node_path.join)(directory, "package.json");
5087
+ if (!(0, node_fs.existsSync)(manifestPath)) return;
5088
+ const result = setLintScript((0, node_fs.readFileSync)(manifestPath, "utf8"));
5089
+ if (result.changed) (0, node_fs.writeFileSync)(manifestPath, result.manifest, "utf8");
5090
+ noteLintScriptOutcome$1(clack, result);
5091
+ }
5092
+ async function rewriteDirectivesWithNote(clack, directory, autoConfirm) {
5093
+ const candidates = previewDisableDirectives(directory);
5094
+ if (candidates.length === 0) return;
5095
+ const count = candidates.length;
5096
+ if (clack.note) clack.note(`${count} file(s) carry an \`eslint-disable\` naming a rule oxlint now owns. Left as they are, ESLint reports each one as an unused directive.
5097
+
5098
+ This is a separate, source-wide diff — review it on its own commit.`, "Disable directives");
5099
+ if (!autoConfirm) {
5100
+ const proceed = await clack.confirm({
5101
+ message: `Re-point \`eslint-disable\` onto \`oxlint-disable\` in ${count} file(s) now?`,
5102
+ initialValue: true
5103
+ });
5104
+ if (clack.isCancel(proceed) || proceed !== true) {
5105
+ if (clack.note) clack.note("Skipped. Re-run `npx @useinsider/eslint-config migrate` to apply it later.", "Disable directives not rewritten");
5106
+ return;
5107
+ }
5108
+ }
5109
+ const { rewritten } = rewriteDisableDirectives(directory, candidates);
5110
+ if (clack.note) clack.note(`Re-pointed directives in ${rewritten.length} file(s).`, "Disable directives");
4234
5111
  }
4235
5112
  function loadPreviousMigrate(clack, cwd) {
4236
5113
  const restored = loadSession({ cwd });
@@ -4242,7 +5119,7 @@ function emitNothingToMigrate(clack, format) {
4242
5119
  clack.outro(format === "eslintrc" ? "No legacy `.eslintrc` shape found — nothing to migrate." : "No v1 useInsider patterns found — nothing to migrate.");
4243
5120
  }
4244
5121
  function persistMigrateSession(input, prepared, resolved) {
4245
- const overwriteExisting = prepared.writePath !== resolved.targetPath && (0, node_fs.existsSync)(prepared.writePath);
5122
+ const overwriteExisting = supersededFlatConfigs(resolved.targetPath, prepared.writePath).length > 0;
4246
5123
  saveSession({
4247
5124
  command: "migrate",
4248
5125
  migrate: {
@@ -4254,26 +5131,48 @@ function persistMigrateSession(input, prepared, resolved) {
4254
5131
  }
4255
5132
  }, { cwd: input.cwd });
4256
5133
  }
5134
+ async function finishMigrate(clack, depSummary, input) {
5135
+ if (depSummary && !await applyDepChanges(clack, depSummary, input)) {
5136
+ clack.cancel("Config migrated, but the dependencies did not install. Install them and re-run lint.");
5137
+ return 1;
5138
+ }
5139
+ clearSession({ cwd: input.cwd });
5140
+ return 0;
5141
+ }
4257
5142
  async function runMigrate(input, clackOverride) {
4258
5143
  const clack = clackOverride ?? await loadClack();
4259
5144
  const sourceResult = resolveSource(input, clack);
4260
5145
  if (!sourceResult.ok) return sourceResult.exitCode;
4261
5146
  const { resolved } = sourceResult;
4262
5147
  clack.intro(`Migrating ${resolved.targetPath}`);
5148
+ const declinedExitCode = input.fromLint ? 1 : 0;
5149
+ if (input.fromLint && !input.yes && !await confirmGuidedStart(clack, resolved.targetPath)) {
5150
+ clack.outro("Left the config as it is — nothing was written.");
5151
+ return declinedExitCode;
5152
+ }
4263
5153
  const previousMigrate = loadPreviousMigrate(clack, input.cwd);
4264
5154
  const prepared = prepareMigration(resolved, input);
4265
5155
  if (!prepared) {
4266
5156
  emitNothingToMigrate(clack, resolved.format);
4267
- return 0;
5157
+ return declinedExitCode;
4268
5158
  }
4269
5159
  emitSummary(clack, prepared.plan, resolved.format);
5160
+ noteConfigRename(clack, resolved.targetPath, prepared.writePath);
4270
5161
  const depSummary = computeDepSummary(prepared.plan, prepared.writePath, input.overrides);
4271
- if (!await runConfirmations(clack, input, resolved.targetPath, prepared.writePath, previousMigrate)) return 0;
5162
+ if (!await runConfirmations(clack, input, resolved.targetPath, prepared.writePath, previousMigrate)) return declinedExitCode;
4272
5163
  persistMigrateSession(input, prepared, resolved);
4273
5164
  performWriteAndCleanup(clack, resolved, prepared);
4274
- if (depSummary) await applyDepChanges(clack, depSummary, input);
4275
- clearSession({ cwd: input.cwd });
4276
- return 0;
5165
+ applyLintScript$1(clack, (0, node_path.dirname)(prepared.writePath));
5166
+ await rewriteDirectivesWithNote(clack, (0, node_path.dirname)(prepared.writePath), Boolean(input.yes));
5167
+ const consumerDir = (0, node_path.dirname)(prepared.writePath);
5168
+ const consumerLayout = resolveLintLayout(consumerDir);
5169
+ await applyTsExtensionsFlag(clack, {
5170
+ cwd: consumerDir,
5171
+ lintModule: consumerLayout.lintModule,
5172
+ eslintConfig: consumerLayout.eslintConfig,
5173
+ autoConfirm: input.yes
5174
+ });
5175
+ return finishMigrate(clack, depSummary, input);
4277
5176
  }
4278
5177
  //#endregion
4279
5178
  //#region src/cli/index.ts
@@ -4438,21 +5337,50 @@ async function resolveHybridAnswers(clack, input, answers) {
4438
5337
  };
4439
5338
  return answers;
4440
5339
  }
5340
+ function applyLintScript(cwd) {
5341
+ const manifestPath = (0, node_path.join)(cwd, "package.json");
5342
+ if (!(0, node_fs.existsSync)(manifestPath)) return null;
5343
+ const result = setLintScript((0, node_fs.readFileSync)(manifestPath, "utf8"));
5344
+ if (result.changed) (0, node_fs.writeFileSync)(manifestPath, result.manifest, "utf8");
5345
+ return result;
5346
+ }
5347
+ function noteLintScriptOutcome(clack, result) {
5348
+ if (!clack.note || !result) return;
5349
+ if (result.unresolved) {
5350
+ clack.note(`Left \`${result.unresolved.script}\` untouched — it runs neither eslint nor oxlint:\n\n ${result.unresolved.previous}\n\nAdd \`${OXLINT_LINT_SCRIPT}\` yourself, next to whatever invokes ESLint.`, "package.json script not changed");
5351
+ return;
5352
+ }
5353
+ if (result.skipped.length > 0) clack.note(`${formatSkippedLintScripts(result.skipped)}\n\noxlint was added to one entry point only, so it does not run twice over the same tree.`, "package.json scripts left alone");
5354
+ if (result.changes.length > 0) clack.note(formatLintScriptChanges(result.changes), "Updated package.json scripts");
5355
+ }
5356
+ function writeLintLayout(cwd, lintModuleSource) {
5357
+ const layout = resolveLintLayout(cwd);
5358
+ const lintModulePath = (0, node_path.join)(cwd, layout.lintModule);
5359
+ (0, node_fs.writeFileSync)(lintModulePath, lintModuleSource, "utf8");
5360
+ renderLintLayoutFiles(layout).forEach((file) => {
5361
+ (0, node_fs.writeFileSync)((0, node_path.join)(cwd, file.filename), file.source, "utf8");
5362
+ });
5363
+ layout.replacedEslintConfigs.forEach((filename) => {
5364
+ (0, node_fs.rmSync)((0, node_path.join)(cwd, filename), { force: true });
5365
+ });
5366
+ return {
5367
+ lintModulePath,
5368
+ lintScript: applyLintScript(cwd)
5369
+ };
5370
+ }
4441
5371
  async function installAndWrite(clack, input, answers, options) {
4442
5372
  if (options.pruneBeforeInstall) noteOverridePrune(clack, pruneConsumerPackageJson(input.cwd).removed);
4443
5373
  const deps = buildDependencyList(answers);
4444
5374
  if (clack.note) clack.note(deps.map((dep) => ` - ${dep}`).join("\n"), "Installing dev dependencies");
4445
- await installWithRetry({
5375
+ if (!(await installWithRetry({
4446
5376
  deps,
4447
5377
  cwd: input.cwd,
4448
5378
  autoConfirm: Boolean(input.yes),
4449
5379
  clack,
4450
5380
  installer: input.overrides?.installer ?? installDependencies,
4451
5381
  fsOps: input.overrides?.wipeFs
4452
- });
4453
- const targetPath = (0, node_path.join)(input.cwd, "eslint.config.mjs");
4454
- (0, node_fs.writeFileSync)(targetPath, renderConfigFile(answers), "utf8");
4455
- return targetPath;
5382
+ })).installed) return null;
5383
+ return writeLintLayout(input.cwd, renderLintModuleFile(answers));
4456
5384
  }
4457
5385
  async function runScaffold(input, clackOverride) {
4458
5386
  const clack = clackOverride ?? await loadClack();
@@ -4473,9 +5401,21 @@ async function runScaffold(input, clackOverride) {
4473
5401
  existingConfigChoice: "override"
4474
5402
  }
4475
5403
  }, { cwd: input.cwd });
4476
- const targetPath = await installAndWrite(clack, input, resolvedAnswers, { pruneBeforeInstall: viaOverride });
5404
+ const written = await installAndWrite(clack, input, resolvedAnswers, { pruneBeforeInstall: viaOverride });
5405
+ if (!written) {
5406
+ clack.cancel("Dependencies were not installed — nothing was written. Fix the install and re-run.");
5407
+ return 1;
5408
+ }
5409
+ noteLintScriptOutcome(clack, written.lintScript);
5410
+ const layout = resolveLintLayout(input.cwd);
5411
+ await applyTsExtensionsFlag(clack, {
5412
+ cwd: input.cwd,
5413
+ lintModule: layout.lintModule,
5414
+ eslintConfig: layout.eslintConfig,
5415
+ autoConfirm: input.yes
5416
+ });
4477
5417
  clearSession({ cwd: input.cwd });
4478
- clack.outro(`Wrote ${targetPath}`);
5418
+ clack.outro(`Wrote ${written.lintModulePath} and its sibling linter configs`);
4479
5419
  return 0;
4480
5420
  }
4481
5421
  async function runScaffoldSubcommand(scaffoldInput) {
@@ -4486,7 +5426,8 @@ async function runMigrateSubcommand(input) {
4486
5426
  cwd: input.cwd,
4487
5427
  target: input.target,
4488
5428
  yes: input.yes,
4489
- noInstall: input.noInstall
5429
+ noInstall: input.noInstall,
5430
+ fromLint: input.fromLint
4490
5431
  });
4491
5432
  }
4492
5433
  function writeStderr(line) {
@@ -4514,5 +5455,6 @@ if (!isUnderTest()) main().then(exitCleanly).catch((error) => {
4514
5455
  });
4515
5456
  //#endregion
4516
5457
  exports.runScaffold = runScaffold;
5458
+ exports.writeLintLayout = writeLintLayout;
4517
5459
 
4518
5460
  //# sourceMappingURL=cli.cjs.map