@useinsider/eslint-config 4.0.0-beta.1 → 4.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -309,20 +309,26 @@ function detectVueApiPattern(input) {
309
309
  //#region src/cli/dispatch.ts
310
310
  var yesFlags = /* @__PURE__ */ new Set(["--yes", "-y"]);
311
311
  var noInstallFlags = /* @__PURE__ */ new Set(["--no-install"]);
312
+ var fromLintFlags = /* @__PURE__ */ new Set(["--from-lint"]);
312
313
  function isYesFlag(arg) {
313
314
  return yesFlags.has(arg);
314
315
  }
315
316
  function isNoInstallFlag(arg) {
316
317
  return noInstallFlags.has(arg);
317
318
  }
319
+ function isFromLintFlag(arg) {
320
+ return fromLintFlags.has(arg);
321
+ }
318
322
  function parseMigrateArgs(rest) {
319
323
  const yes = rest.some(isYesFlag);
320
324
  const noInstall = rest.some(isNoInstallFlag);
321
- 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));
322
327
  return {
323
328
  target: positional.length > 0 ? positional[positional.length - 1] : void 0,
324
329
  yes,
325
- noInstall
330
+ noInstall,
331
+ fromLint
326
332
  };
327
333
  }
328
334
  function parseScaffoldArgs(argv) {
@@ -333,8 +339,9 @@ function printUsage(stderr, unknown) {
333
339
  stderr("Usage:");
334
340
  stderr(" npx @useinsider/eslint-config [--yes|-y]");
335
341
  stderr(" Scaffold insider.lint.*, eslint.config.* and oxlint.config.*");
336
- stderr(" npx @useinsider/eslint-config migrate [path] [--yes|-y] [--no-install]");
342
+ stderr(" npx @useinsider/eslint-config migrate [path] [--yes|-y] [--no-install] [--from-lint]");
337
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");
338
345
  }
339
346
  function isScaffoldArgv(argv) {
340
347
  return argv.every((arg) => isYesFlag(arg));
@@ -349,12 +356,13 @@ async function dispatch(argv, cwd, deps) {
349
356
  }
350
357
  const [subcommand, ...rest] = argv;
351
358
  if (subcommand === "migrate") {
352
- const { target, yes, noInstall } = parseMigrateArgs(rest);
359
+ const { target, yes, noInstall, fromLint } = parseMigrateArgs(rest);
353
360
  return deps.migrate({
354
361
  cwd,
355
362
  target,
356
363
  yes,
357
- noInstall
364
+ noInstall,
365
+ fromLint
358
366
  });
359
367
  }
360
368
  printUsage(deps.stderr, subcommand);
@@ -661,39 +669,66 @@ function noteGenericError(clack, summary, context, specs) {
661
669
  async function tryRetryInstall(clack, installer, deps, context) {
662
670
  try {
663
671
  await installer(deps);
672
+ return { installed: true };
664
673
  } catch (retryError) {
665
- 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
+ };
666
680
  }
667
681
  }
668
682
  async function tryGenericRetryInstall(clack, installer, deps, context) {
669
683
  try {
670
684
  await installer(deps);
685
+ return { installed: true };
671
686
  } catch (retryError) {
672
- 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
+ };
673
693
  }
674
694
  }
675
695
  async function handlePeerDepFailure(clack, deps, autoConfirm, installer, reason, context) {
676
696
  if (!await confirmPeerDepWipe(clack, autoConfirm)) {
677
697
  noteError(clack, "Install failed: ", reason, context, deps);
678
- return;
698
+ return {
699
+ installed: false,
700
+ reason
701
+ };
679
702
  }
680
703
  performFullWipe(clack, context);
681
- await tryRetryInstall(clack, installer, deps, context);
704
+ return tryRetryInstall(clack, installer, deps, context);
682
705
  }
683
706
  async function handleGenericFailure(clack, deps, autoConfirm, installer, reason, context) {
684
707
  const summary = buildGenericSummary(reason);
685
708
  if (!await confirmCleanReinstall(clack, autoConfirm, summary)) {
686
709
  if (clack.note) clack.note(`${summary}\nSkipping clean reinstall. Please resolve the issue manually and re-run.`, "Error");
687
- return;
710
+ return {
711
+ installed: false,
712
+ reason: summary
713
+ };
688
714
  }
689
715
  performFullWipe(clack, context);
690
- await tryGenericRetryInstall(clack, installer, deps, context);
716
+ return tryGenericRetryInstall(clack, installer, deps, context);
691
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
+ */
692
726
  async function installWithRetry(opts) {
693
727
  const installer = opts.installer ?? defaultInstaller;
694
728
  const fs = opts.fsOps ?? defaultWipeFs();
695
729
  try {
696
730
  await installer(opts.deps);
731
+ return { installed: true };
697
732
  } catch (error) {
698
733
  const reason = error instanceof Error ? error.message : String(error);
699
734
  const context = {
@@ -701,16 +736,30 @@ async function installWithRetry(opts) {
701
736
  fs,
702
737
  detected: detectLockfile(opts.cwd, fs.exists)
703
738
  };
704
- if (isPeerDepFailure(reason)) {
705
- await handlePeerDepFailure(opts.clack, opts.deps, opts.autoConfirm, installer, reason, context);
706
- return;
707
- }
708
- await handleGenericFailure(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);
709
741
  }
710
742
  }
711
743
  //#endregion
744
+ //#region src/cli/detectIndent.ts
745
+ var DEFAULT_INDENT = " ";
746
+ /**
747
+ * The indentation a JSON source already uses, as the string `JSON.stringify` takes for its third
748
+ * argument. A width would be wrong: `JSON.stringify` emits that many *spaces*, so a tab-indented
749
+ * manifest comes back re-indented with spaces and every line of it shows up in the diff.
750
+ * @example
751
+ * detectIndent('{\n\t"name": "app"\n}'); // '\t'
752
+ * @example
753
+ * detectIndent('{"name":"app"}', ' '); // ' '
754
+ */
755
+ function detectIndent(raw, fallback = DEFAULT_INDENT) {
756
+ const match = /\n([\t ]+)\S/u.exec(raw);
757
+ return match ? match[1] : fallback;
758
+ }
759
+ //#endregion
712
760
  //#region src/cli/lintLayout.ts
713
- var OXLINT_LINT_SCRIPT = "oxlint --disable-nested-config && eslint .";
761
+ var OXLINT_COMMAND = "oxlint --disable-nested-config";
762
+ var OXLINT_LINT_SCRIPT = `${OXLINT_COMMAND} && eslint .`;
714
763
  /**
715
764
  * Extension for the lint module and the oxlint config. oxlint only discovers `oxlint.config.ts` and
716
765
  * `oxlint.config.mts`, and Node warns on a bare `.ts` file inside a package that declares no
@@ -726,29 +775,45 @@ function resolveLayoutExtension(cwd) {
726
775
  return "mts";
727
776
  }
728
777
  }
729
- function findExistingEslintConfig(cwd, fallback) {
730
- return [
731
- "ts",
732
- "mts",
733
- "cts",
734
- "js",
735
- "mjs",
736
- "cjs"
737
- ].map((ext) => `eslint.config.${ext}`).find((filename) => (0, node_fs.existsSync)((0, node_path.join)(cwd, filename))) ?? fallback;
778
+ var ESLINT_CONFIG_EXTENSIONS = [
779
+ "ts",
780
+ "mts",
781
+ "cts",
782
+ "js",
783
+ "mjs",
784
+ "cjs"
785
+ ];
786
+ var TYPESCRIPT_CONFIG_EXTENSIONS = /* @__PURE__ */ new Set([
787
+ "cts",
788
+ "mts",
789
+ "ts"
790
+ ]);
791
+ function extensionOf(filename) {
792
+ return filename.slice(filename.lastIndexOf(".") + 1);
793
+ }
794
+ function existingEslintConfigs(cwd) {
795
+ return ESLINT_CONFIG_EXTENSIONS.map((extension) => `eslint.config.${extension}`).filter((filename) => (0, node_fs.existsSync)((0, node_path.join)(cwd, filename)));
738
796
  }
739
797
  /**
740
- * The three filenames `useInsider()` requires, resolved against a consumer directory. An existing
741
- * `eslint.config.*` keeps its name so a migration does not leave two flat configs behind.
798
+ * The three filenames `useInsider()` requires, resolved against a consumer directory. A JavaScript
799
+ * flat config is renamed to the TypeScript extension — it cannot import the lint module on every
800
+ * supported Node version — and every other `eslint.config.*` in the directory is reported through
801
+ * `replacedEslintConfigs` so the caller deletes it. ESLint resolves `.js`/`.mjs`/`.cjs` before the
802
+ * TypeScript extensions, so a leftover JavaScript config keeps winning over the one just written.
742
803
  * @example
743
- * resolveLintLayout('/repo'); // { lintModule: 'insider.lint.mts', eslintConfig: 'eslint.config.mjs', ... }
804
+ * resolveLintLayout('/repo');
805
+ * // { eslintConfig: 'eslint.config.ts', replacedEslintConfigs: ['eslint.config.js'], ... }
744
806
  */
745
- function resolveLintLayout(cwd, eslintConfigFallback = "eslint.config.mjs") {
807
+ function resolveLintLayout(cwd) {
746
808
  const extension = resolveLayoutExtension(cwd);
809
+ const existing = existingEslintConfigs(cwd);
810
+ const eslintConfig = existing.find((filename) => TYPESCRIPT_CONFIG_EXTENSIONS.has(extensionOf(filename))) ?? `eslint.config.${extension}`;
747
811
  return {
748
812
  extension,
749
813
  lintModule: `insider.lint.${extension}`,
750
814
  oxlintConfig: `oxlint.config.${extension}`,
751
- eslintConfig: findExistingEslintConfig(cwd, eslintConfigFallback)
815
+ eslintConfig,
816
+ replacedEslintConfigs: existing.filter((filename) => filename !== eslintConfig)
752
817
  };
753
818
  }
754
819
  /**
@@ -782,7 +847,7 @@ function renderOxlintConfigFile(lintModule) {
782
847
  * The two thin files that sit beside a lint module, ready to write.
783
848
  * @example
784
849
  * renderLintLayoutFiles(resolveLintLayout('/repo')).map(file => file.filename);
785
- * // ['eslint.config.mjs', 'oxlint.config.mts']
850
+ * // ['eslint.config.mts', 'oxlint.config.mts']
786
851
  */
787
852
  function renderLintLayoutFiles(layout) {
788
853
  return [{
@@ -793,20 +858,112 @@ function renderLintLayoutFiles(layout) {
793
858
  source: renderOxlintConfigFile(layout.lintModule)
794
859
  }];
795
860
  }
796
- var DEFAULT_INDENT = 2;
797
- function detectIndent$1(raw) {
798
- const firstBreak = raw.indexOf("\n");
799
- if (firstBreak === -1) return DEFAULT_INDENT;
800
- const rest = raw.slice(firstBreak + 1);
801
- const width = rest.length - rest.trimStart().length;
802
- return width > 0 ? width : DEFAULT_INDENT;
861
+ var DEFAULT_MANIFEST_INDENT = " ";
862
+ var SEGMENT_SEPARATOR = /&&|\|\||[;|]/u;
863
+ var ASSIGNMENT = /^[A-Za-z_]\w*=/u;
864
+ var WRAPPER_COMMANDS = /* @__PURE__ */ new Set([
865
+ "bunx",
866
+ "cross-env",
867
+ "dotenv",
868
+ "exec",
869
+ "npx",
870
+ "pnpm",
871
+ "run",
872
+ "yarn"
873
+ ]);
874
+ function baseName(token) {
875
+ return token.slice(token.lastIndexOf("/") + 1).replace(/\.[mc]?js$/u, "");
876
+ }
877
+ function segmentsOf(command) {
878
+ return command.split(SEGMENT_SEPARATOR).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
879
+ }
880
+ function tokensOf(segment) {
881
+ return segment.split(/\s+/u).filter((token) => token.length > 0 && !ASSIGNMENT.test(token));
882
+ }
883
+ function commandWordOf(segment) {
884
+ return tokensOf(segment).map(baseName).find((word) => !word.startsWith("-") && !WRAPPER_COMMANDS.has(word)) ?? null;
885
+ }
886
+ /**
887
+ * Whether a package script really invokes the `eslint` binary, as opposed to delegating to another
888
+ * script that does. `npm run lint:es` is not an ESLint invocation; `NODE_OPTIONS=… eslint .` is.
889
+ * @example
890
+ * invokesBinary('npm run lint:es-ci -- --cache', 'eslint'); // false
891
+ */
892
+ function invokesBinary(command, binary) {
893
+ return segmentsOf(command).some((segment) => commandWordOf(segment) === binary);
894
+ }
895
+ function oxlintPrefixFor(command) {
896
+ return tokensOf(command).includes("--fix") ? `${OXLINT_COMMAND} --fix` : OXLINT_COMMAND;
897
+ }
898
+ var CANONICAL_LINT_SCRIPT = "lint";
899
+ function referencedScripts(command, names) {
900
+ return tokensOf(command).map((token) => token.replace(/^["']|["']$/gu, "").replace(/^npm:/u, "")).filter((token) => names.has(token));
901
+ }
902
+ function collectReachable(scripts, names, script, seen) {
903
+ if (seen.has(script)) return;
904
+ seen.add(script);
905
+ referencedScripts(scripts[script] ?? "", names).forEach((next) => {
906
+ collectReachable(scripts, names, next, seen);
907
+ });
908
+ }
909
+ function reachableFromLint(scripts) {
910
+ const names = new Set(Object.keys(scripts));
911
+ const seen = /* @__PURE__ */ new Set();
912
+ if (names.has(CANONICAL_LINT_SCRIPT)) collectReachable(scripts, names, CANONICAL_LINT_SCRIPT, seen);
913
+ return seen;
914
+ }
915
+ function fixingRank(candidate) {
916
+ return tokensOf(candidate.previous).includes("--fix") ? 1 : 0;
917
+ }
918
+ function compareCandidates(left, right) {
919
+ return fixingRank(left) - fixingRank(right) || left.script.length - right.script.length || left.script.localeCompare(right.script);
920
+ }
921
+ function selectTarget(scripts, candidates) {
922
+ if (candidates.length === 0) return null;
923
+ const reachable = reachableFromLint(scripts);
924
+ const preferred = candidates.filter((candidate) => reachable.has(candidate.script));
925
+ return [...preferred.length > 0 ? preferred : candidates].sort(compareCandidates)[0];
926
+ }
927
+ function planChanges(scripts) {
928
+ const alreadyRunsOxlint = [];
929
+ const candidates = [];
930
+ Object.entries(scripts).forEach(([script, previous]) => {
931
+ if (!invokesBinary(previous, "eslint")) return;
932
+ if (invokesBinary(previous, "oxlint")) {
933
+ alreadyRunsOxlint.push(script);
934
+ return;
935
+ }
936
+ candidates.push({
937
+ script,
938
+ previous
939
+ });
940
+ });
941
+ const target = selectTarget(scripts, candidates);
942
+ if (!target) return {
943
+ changes: [],
944
+ alreadyRunsOxlint,
945
+ skipped: []
946
+ };
947
+ return {
948
+ changes: [{
949
+ script: target.script,
950
+ previous: target.previous,
951
+ next: `${oxlintPrefixFor(target.previous)} && ${target.previous}`
952
+ }],
953
+ alreadyRunsOxlint,
954
+ skipped: candidates.filter((candidate) => candidate.script !== target.script)
955
+ };
803
956
  }
804
957
  /**
805
- * Rewrites the consumer's `lint` script so both linters run. Returns the manifest untouched when it
806
- * already runs oxlint, so re-running the CLI is idempotent.
958
+ * Adds oxlint beside the one script that actually runs ESLint, by prepending to it rather than
959
+ * replacing it — so a composed `lint` such as `run-p lint:type lint:es lint:style` keeps its
960
+ * type-check and stylelint passes and only its `lint:es-ci` leaf is rewritten. Sibling entry points
961
+ * that also run ESLint (`lint:fix`, `lint:ci`) are reported through `skipped` rather than rewritten,
962
+ * so oxlint runs once per lint pass. Never rewrites a script it did not author: when nothing invokes
963
+ * `eslint` and a `lint` script already exists, the manifest is left alone and the conflict reported.
807
964
  * @example
808
- * setLintScript('{"scripts":{"lint":"eslint ."}}').manifest;
809
- * // '…"lint": "oxlint --disable-nested-config && eslint ."…'
965
+ * setLintScript('{"scripts":{"lint":"eslint ."}}').changes;
966
+ * // [{ script: 'lint', previous: 'eslint .', next: 'oxlint --disable-nested-config && eslint .' }]
810
967
  */
811
968
  function setLintScript(rawManifest) {
812
969
  let parsed;
@@ -815,25 +972,77 @@ function setLintScript(rawManifest) {
815
972
  } catch {
816
973
  return {
817
974
  manifest: rawManifest,
818
- changed: false
975
+ changed: false,
976
+ changes: [],
977
+ alreadyRunsOxlint: [],
978
+ skipped: []
819
979
  };
820
980
  }
821
- const previous = parsed.scripts?.lint;
822
- if (previous?.includes("oxlint")) return {
981
+ const scripts = parsed.scripts ?? {};
982
+ const { changes, alreadyRunsOxlint, skipped } = planChanges(scripts);
983
+ const existingLint = scripts.lint;
984
+ if (changes.length === 0 && alreadyRunsOxlint.length === 0) {
985
+ if (existingLint !== void 0) return {
986
+ manifest: rawManifest,
987
+ changed: false,
988
+ changes: [],
989
+ alreadyRunsOxlint: [],
990
+ skipped: [],
991
+ unresolved: {
992
+ reason: "lint-script-runs-neither",
993
+ script: "lint",
994
+ previous: existingLint
995
+ }
996
+ };
997
+ changes.push({
998
+ script: "lint",
999
+ previous: "",
1000
+ next: OXLINT_LINT_SCRIPT
1001
+ });
1002
+ }
1003
+ if (changes.length === 0) return {
823
1004
  manifest: rawManifest,
824
1005
  changed: false,
825
- previous
826
- };
827
- parsed.scripts = {
828
- ...parsed.scripts,
829
- lint: OXLINT_LINT_SCRIPT
1006
+ changes: [],
1007
+ alreadyRunsOxlint,
1008
+ skipped
830
1009
  };
1010
+ parsed.scripts = { ...scripts };
1011
+ changes.forEach((change) => {
1012
+ parsed.scripts[change.script] = change.next;
1013
+ });
831
1014
  return {
832
- manifest: `${JSON.stringify(parsed, null, detectIndent$1(rawManifest))}\n`,
1015
+ manifest: `${JSON.stringify(parsed, null, detectIndent(rawManifest, DEFAULT_MANIFEST_INDENT))}\n`,
833
1016
  changed: true,
834
- previous
1017
+ changes,
1018
+ alreadyRunsOxlint,
1019
+ skipped
835
1020
  };
836
1021
  }
1022
+ /**
1023
+ * The scripts that still run ESLint alone, for the CLI to print. Each line carries the oxlint prefix
1024
+ * that script would need, so a consumer who wants oxlint on a second entry point can paste it.
1025
+ * @example
1026
+ * formatSkippedLintScripts([{ script: 'lint:fix', previous: 'eslint . --fix' }]);
1027
+ * // 'lint:fix\n prefix with: oxlint --disable-nested-config --fix && '
1028
+ */
1029
+ function formatSkippedLintScripts(skipped) {
1030
+ return skipped.map((candidate) => `${candidate.script}\n prefix with: ${oxlintPrefixFor(candidate.previous)} && `).join("\n\n");
1031
+ }
1032
+ /**
1033
+ * One line per rewritten script, for the CLI to print. The previous command survives verbatim inside
1034
+ * the new one, so the report doubles as the recovery instructions.
1035
+ * @example
1036
+ * formatLintScriptChanges([{ script: 'lint:es-ci', previous: 'eslint .', next: 'oxlint && eslint .' }]);
1037
+ * // 'lint:es-ci\n before: eslint .\n after: oxlint && eslint .'
1038
+ */
1039
+ function formatLintScriptChanges(changes) {
1040
+ return changes.map((change) => [
1041
+ change.script,
1042
+ ` before: ${change.previous === "" ? "(no such script)" : change.previous}`,
1043
+ ` after: ${change.next}`
1044
+ ].join("\n")).join("\n\n");
1045
+ }
837
1046
  //#endregion
838
1047
  //#region src/cli/quote.ts
839
1048
  function quote(value) {
@@ -1585,6 +1794,18 @@ var ALL_PRESET_DEPS = [
1585
1794
  ...Object.values(SCOPE_TO_DEPS)
1586
1795
  ];
1587
1796
  var PRESET_DEPENDENCY_KEYS = new Set(ALL_PRESET_DEPS.flatMap((record) => Object.keys(record)));
1797
+ var MIGRATABLE_SCOPES = new Set(Object.keys(SCOPE_TO_DEPS));
1798
+ /**
1799
+ * Whether a `useInsider()` option key is a scope this migration can derive dependencies for. The
1800
+ * four `*OptionsApi` scopes are deliberately absent from the registry, so a config using one is
1801
+ * carried over untouched while its dependencies are reported as unresolved.
1802
+ * @example
1803
+ * isMigratableScope('vue3Typescript'); // true
1804
+ * isMigratableScope('vue3OptionsApi'); // false
1805
+ */
1806
+ function isMigratableScope(key) {
1807
+ return MIGRATABLE_SCOPES.has(key);
1808
+ }
1588
1809
  function isEslintRelatedDependency(name) {
1589
1810
  if (SAFELIST.has(name)) return false;
1590
1811
  if (EXACT_PATTERNS.has(name)) return true;
@@ -1699,11 +1920,6 @@ function buildInstallList(scopes, manifest) {
1699
1920
  var readOwnEslintConfigManifest = readOwnEslintConfigManifest$1;
1700
1921
  //#endregion
1701
1922
  //#region src/cli/prunePackageJson.ts
1702
- function detectIndent(raw) {
1703
- const match = /\n([ \t]+)"/u.exec(raw);
1704
- if (!match) return " ";
1705
- return match[1];
1706
- }
1707
1923
  function serializeManifest(manifest, indent) {
1708
1924
  return `${JSON.stringify(manifest, null, indent)}\n`;
1709
1925
  }
@@ -1752,32 +1968,89 @@ var TS_EXTENSIONS_SNIPPET = [
1752
1968
  " }",
1753
1969
  "}"
1754
1970
  ].join("\n");
1755
- var DEFAULT_INDENT_UNIT = " ";
1756
- function readEffectiveFlag(path, raw) {
1971
+ function acceptsTsExtensionsFlag(options) {
1972
+ return options.noEmit === true || options.emitDeclarationOnly === true || options.rewriteRelativeImportExtensions === true;
1973
+ }
1974
+ function parseTsconfig(path, raw) {
1757
1975
  const { config } = typescript.parseConfigFileTextToJson(path, raw);
1758
- if (!config) return false;
1759
- return typescript.parseJsonConfigFileContent(config, typescript.sys, (0, node_path.dirname)(path), void 0, path).options.allowImportingTsExtensions === true;
1976
+ if (!config) return null;
1977
+ const parsed = typescript.parseJsonConfigFileContent(config, typescript.sys, (0, node_path.dirname)(path), void 0, path);
1978
+ return {
1979
+ enabled: parsed.options.allowImportingTsExtensions === true,
1980
+ acceptsFlag: acceptsTsExtensionsFlag(parsed.options),
1981
+ fileNames: parsed.fileNames,
1982
+ references: (parsed.projectReferences ?? []).map((reference) => reference.path)
1983
+ };
1760
1984
  }
1761
- /**
1762
- * The consumer's root `tsconfig.json` plus whether `allowImportingTsExtensions` already resolves to
1763
- * `true` — including when it arrives through `extends`, which is followed the same way `tsc` does.
1764
- * @example
1765
- * readTsconfigState('/repo')?.enabled; // true when /repo/tsconfig.json extends a base that sets it
1766
- */
1767
- function readTsconfigState(cwd) {
1768
- const path = (0, node_path.join)(cwd, "tsconfig.json");
1985
+ function readTsconfigAt(path, governedFiles) {
1769
1986
  if (!(0, node_fs.existsSync)(path)) return null;
1770
1987
  try {
1771
1988
  const raw = (0, node_fs.readFileSync)(path, "utf8");
1989
+ const parsed = parseTsconfig(path, raw);
1990
+ if (!parsed) return {
1991
+ state: {
1992
+ path,
1993
+ raw,
1994
+ enabled: false,
1995
+ governs: false,
1996
+ acceptsFlag: false
1997
+ },
1998
+ references: []
1999
+ };
2000
+ const owned = new Set(parsed.fileNames.map((name) => typescript.sys.resolvePath(name)));
2001
+ const governs = governedFiles.some((file) => owned.has(typescript.sys.resolvePath(file)));
1772
2002
  return {
1773
- path,
1774
- raw,
1775
- enabled: readEffectiveFlag(path, raw)
2003
+ state: {
2004
+ path,
2005
+ raw,
2006
+ enabled: parsed.enabled,
2007
+ governs,
2008
+ acceptsFlag: parsed.acceptsFlag
2009
+ },
2010
+ references: parsed.references
1776
2011
  };
1777
2012
  } catch {
1778
2013
  return null;
1779
2014
  }
1780
2015
  }
2016
+ function resolveReferencePath(reference) {
2017
+ if (!(0, node_fs.existsSync)(reference)) return reference;
2018
+ return typescript.sys.directoryExists(reference) ? (0, node_path.join)(reference, "tsconfig.json") : reference;
2019
+ }
2020
+ /**
2021
+ * Every tsconfig in the consumer's solution — the root plus each project reference — annotated with
2022
+ * whether it actually type-checks the generated lint files. A solution-style root has an empty file
2023
+ * set, so the project that governs `insider.lint.ts` is usually a referenced `tsconfig.node.json`,
2024
+ * and that is the one `tsc` reports TS5097 from.
2025
+ * @example
2026
+ * collectTsconfigStates('/repo', ['/repo/insider.lint.ts']).filter(state => state.governs);
2027
+ * // [{ path: '/repo/tsconfig.node.json', enabled: true, governs: true, ... }]
2028
+ */
2029
+ function collectTsconfigStates(cwd, governedFiles = []) {
2030
+ const root = readTsconfigAt((0, node_path.join)(cwd, "tsconfig.json"), governedFiles);
2031
+ if (!root) return [];
2032
+ const seen = /* @__PURE__ */ new Set([root.state.path]);
2033
+ const states = [root.state];
2034
+ root.references.forEach((reference) => {
2035
+ const path = resolveReferencePath(reference);
2036
+ if (seen.has(path)) return;
2037
+ seen.add(path);
2038
+ const referenced = readTsconfigAt(path, governedFiles);
2039
+ if (referenced) states.push(referenced.state);
2040
+ });
2041
+ return states;
2042
+ }
2043
+ /**
2044
+ * The tsconfigs that must gain `allowImportingTsExtensions`: the ones that type-check the generated
2045
+ * files and do not already resolve the flag to `true`. Empty when an applicable config already sets
2046
+ * it, and empty when no config type-checks the generated files at all — a tsconfig that never sees
2047
+ * them cannot raise TS5097, so editing it would only add a flag it has no use for.
2048
+ * @example
2049
+ * tsconfigsNeedingFlag('/repo', ['/repo/insider.lint.ts']).map(state => state.path); // []
2050
+ */
2051
+ function tsconfigsNeedingFlag(cwd, governedFiles) {
2052
+ return collectTsconfigStates(cwd, governedFiles).filter((state) => state.governs && !state.enabled);
2053
+ }
1781
2054
  function rootObject(source) {
1782
2055
  const [statement] = source.statements;
1783
2056
  const expression = statement?.expression;
@@ -1791,10 +2064,6 @@ function indentAt(raw, position) {
1791
2064
  const prefix = raw.slice(lineStart, position);
1792
2065
  return prefix.trim().length === 0 ? prefix : "";
1793
2066
  }
1794
- function detectIndentUnit(raw) {
1795
- const match = /\n([\t ]+)\S/u.exec(raw);
1796
- return match ? match[1] : DEFAULT_INDENT_UNIT;
1797
- }
1798
2067
  function splice(raw, start, end, text) {
1799
2068
  return raw.slice(0, start) + text + raw.slice(end);
1800
2069
  }
@@ -1804,14 +2073,14 @@ function setExistingFlag(raw, source, property) {
1804
2073
  function insertIntoCompilerOptions(raw, source, compilerOptions, object) {
1805
2074
  const openBrace = object.getStart(source) + 1;
1806
2075
  const [first] = object.properties;
1807
- if (first) return splice(raw, openBrace, openBrace, `\n${indentAt(raw, first.getStart(source)) || detectIndentUnit(raw).repeat(2)}"${TS_EXTENSIONS_FLAG}": true,`);
2076
+ if (first) return splice(raw, openBrace, openBrace, `\n${indentAt(raw, first.getStart(source)) || detectIndent(raw).repeat(2)}"${TS_EXTENSIONS_FLAG}": true,`);
1808
2077
  const outer = indentAt(raw, compilerOptions.getStart(source));
1809
- const unit = detectIndentUnit(raw);
2078
+ const unit = detectIndent(raw);
1810
2079
  return splice(raw, openBrace, object.getEnd() - 1, `\n${outer}${unit}"${TS_EXTENSIONS_FLAG}": true\n${outer}`);
1811
2080
  }
1812
2081
  function insertCompilerOptions(raw, source, root) {
1813
2082
  const openBrace = root.getStart(source) + 1;
1814
- const unit = detectIndentUnit(raw);
2083
+ const unit = detectIndent(raw);
1815
2084
  const [first] = root.properties;
1816
2085
  const indent = first ? indentAt(raw, first.getStart(source)) || unit : unit;
1817
2086
  const block = `\n${indent}"compilerOptions": {\n${indent}${unit}"${TS_EXTENSIONS_FLAG}": true\n${indent}}`;
@@ -1861,6 +2130,10 @@ function noteManualStep(clack, lintModule) {
1861
2130
  if (!clack.note) return;
1862
2131
  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");
1863
2132
  }
2133
+ function noteEmitConflict(clack, path, lintModule) {
2134
+ if (!clack.note) return;
2135
+ 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");
2136
+ }
1864
2137
  async function confirmFlag(clack, path) {
1865
2138
  const result = await clack.confirm({
1866
2139
  message: `Set \`${TS_EXTENSIONS_FLAG}: true\` in ${path}? Without it \`tsc\` fails with TS5097 on the generated config files.`,
@@ -1868,16 +2141,14 @@ async function confirmFlag(clack, path) {
1868
2141
  });
1869
2142
  return !clack.isCancel(result) && result === true;
1870
2143
  }
1871
- /**
1872
- * Offers to set `allowImportingTsExtensions` on the consumer's `tsconfig.json`, and prints the exact
1873
- * snippet when the offer is declined. A no-op when there is no `tsconfig.json` or the flag already
1874
- * resolves to `true` through the `extends` chain.
1875
- * @example
1876
- * await applyTsExtensionsFlag(clack, { cwd: '/repo', lintModule: 'insider.lint.ts' });
1877
- */
1878
- async function applyTsExtensionsFlag(clack, input) {
1879
- const state = readTsconfigState(input.cwd);
1880
- if (!state || state.enabled) return;
2144
+ function governedFilesOf(input) {
2145
+ return [input.lintModule, input.eslintConfig].filter((name) => Boolean(name)).map((name) => (0, node_path.join)(input.cwd, name));
2146
+ }
2147
+ async function applyToOne(clack, state, input) {
2148
+ if (!state.acceptsFlag) {
2149
+ noteEmitConflict(clack, state.path, input.lintModule);
2150
+ return;
2151
+ }
1881
2152
  if (!(input.autoConfirm || await confirmFlag(clack, state.path))) {
1882
2153
  noteManualStep(clack, input.lintModule);
1883
2154
  return;
@@ -1890,6 +2161,22 @@ async function applyTsExtensionsFlag(clack, input) {
1890
2161
  (0, node_fs.writeFileSync)(state.path, result.source, "utf8");
1891
2162
  if (clack.note) clack.note(`Set \`${TS_EXTENSIONS_FLAG}: true\` in ${state.path}.`, "Updated tsconfig.json");
1892
2163
  }
2164
+ /**
2165
+ * Offers to set `allowImportingTsExtensions` on every tsconfig that actually type-checks the
2166
+ * generated lint files, and prints the exact snippet when an offer is declined or when the project
2167
+ * emits — in which case writing the flag would trade TS5097 for TS5096. A no-op when there is no
2168
+ * `tsconfig.json`, when no config governs the generated files, or when an applicable config already
2169
+ * resolves the flag to `true` — through its own `compilerOptions`, through `extends`, or by being a
2170
+ * project reference that already sets it.
2171
+ * @example
2172
+ * await applyTsExtensionsFlag(clack, { cwd: '/repo', lintModule: 'insider.lint.ts' });
2173
+ */
2174
+ async function applyTsExtensionsFlag(clack, input) {
2175
+ await tsconfigsNeedingFlag(input.cwd, governedFilesOf(input)).reduce(async (previous, state) => {
2176
+ await previous;
2177
+ return applyToOne(clack, state, input);
2178
+ }, Promise.resolve());
2179
+ }
1893
2180
  //#endregion
1894
2181
  //#region src/cli/migrate/disableDirectives.ts
1895
2182
  var DIRECTIVE_HEAD = /^\s*eslint-disable(-next-line|-line)?(?![\w-])/u;
@@ -2050,31 +2337,103 @@ function rewriteDirectives(source, fileExtension) {
2050
2337
  function isLintedFile(filePath) {
2051
2338
  return LINTED_EXTENSIONS.includes((0, node_path.extname)(filePath));
2052
2339
  }
2053
- function collectFiles(directory, accumulator) {
2054
- (0, node_fs.readdirSync)(directory).forEach((entry) => {
2055
- if (SKIPPED_DIRECTORIES.has(entry)) return;
2056
- const full = (0, node_path.join)(directory, entry);
2057
- if ((0, node_fs.statSync)(full).isDirectory()) {
2058
- collectFiles(full, accumulator);
2340
+ function readEntries(directory) {
2341
+ try {
2342
+ return (0, node_fs.readdirSync)(directory, { withFileTypes: true });
2343
+ } catch {
2344
+ return [];
2345
+ }
2346
+ }
2347
+ function realPathOf(path) {
2348
+ try {
2349
+ return (0, node_fs.realpathSync)(path);
2350
+ } catch {
2351
+ return null;
2352
+ }
2353
+ }
2354
+ function collectFiles(directory, accumulator, visited) {
2355
+ const real = realPathOf(directory);
2356
+ if (real === null || visited.has(real)) return;
2357
+ visited.add(real);
2358
+ readEntries(directory).forEach((entry) => {
2359
+ if (SKIPPED_DIRECTORIES.has(entry.name)) return;
2360
+ const full = (0, node_path.join)(directory, entry.name);
2361
+ if (entry.isDirectory()) {
2362
+ collectFiles(full, accumulator, visited);
2363
+ return;
2364
+ }
2365
+ if (entry.isFile() && isLintedFile(full)) accumulator.push(full);
2366
+ });
2367
+ }
2368
+ function readSource(filePath) {
2369
+ try {
2370
+ return (0, node_fs.readFileSync)(filePath, "utf8");
2371
+ } catch {
2372
+ return null;
2373
+ }
2374
+ }
2375
+ /**
2376
+ * Every file whose `eslint-disable` comments would be re-pointed, and the rewritten source, without
2377
+ * touching disk. The directive rewrite is the largest diff a migration produces, so the count is
2378
+ * reported — and confirmed — before anything is written. Files that could not be read come back in
2379
+ * `skipped` instead of aborting the walk.
2380
+ * @example
2381
+ * previewDisableDirectives('/repo').candidates.length; // 131
2382
+ */
2383
+ function previewDisableDirectives(cwd) {
2384
+ const files = [];
2385
+ const candidates = [];
2386
+ const skipped = [];
2387
+ collectFiles(cwd, files, /* @__PURE__ */ new Set());
2388
+ files.forEach((filePath) => {
2389
+ const source = readSource(filePath);
2390
+ if (source === null) {
2391
+ skipped.push(filePath);
2059
2392
  return;
2060
2393
  }
2061
- if (isLintedFile(full)) accumulator.push(full);
2394
+ const result = rewriteDirectives(source, (0, node_path.extname)(filePath));
2395
+ if (result.changed) candidates.push({
2396
+ path: filePath,
2397
+ source: result.source
2398
+ });
2062
2399
  });
2400
+ return {
2401
+ candidates,
2402
+ skipped
2403
+ };
2404
+ }
2405
+ /**
2406
+ * What the consumer is told about the files the rewrite could not touch: the paths, then why it
2407
+ * matters and how to finish the job. Nothing else in the migration is affected, and saying so is the
2408
+ * point — the layout was already written by the time the walk ran.
2409
+ * @example
2410
+ * formatUntouchedDirectiveFiles(['/repo/src/vendor.ts']); // '/repo/src/vendor.ts\n\nThese could not…'
2411
+ */
2412
+ function formatUntouchedDirectiveFiles(skipped) {
2413
+ return `${skipped.join("\n")}\n\nThese could not be read or written, so their \`eslint-disable\` comments still name rules oxlint now owns and ESLint will report each one as an unused directive. Everything else was migrated. Fix the access and re-run \`npx @useinsider/eslint-config migrate\` to pick them up.`;
2063
2414
  }
2064
2415
  /**
2065
2416
  * Applies {@link rewriteDirectives} across a consumer tree, skipping build output and dependencies.
2417
+ * A candidate whose write fails comes back in `skipped`, so one failed write costs its own
2418
+ * directives rather than every later file's.
2066
2419
  * @example
2067
2420
  * rewriteDisableDirectives('/repo').rewritten; // ['/repo/src/logger.ts']
2068
2421
  */
2069
- function rewriteDisableDirectives(cwd) {
2070
- const files = [];
2071
- collectFiles(cwd, files);
2072
- return { rewritten: files.filter((filePath) => {
2073
- const result = rewriteDirectives((0, node_fs.readFileSync)(filePath, "utf8"), (0, node_path.extname)(filePath));
2074
- if (!result.changed) return false;
2075
- (0, node_fs.writeFileSync)(filePath, result.source, "utf8");
2076
- return true;
2077
- }) };
2422
+ function rewriteDisableDirectives(cwd, candidates = previewDisableDirectives(cwd).candidates) {
2423
+ const rewritten = [];
2424
+ const skipped = [];
2425
+ candidates.forEach((candidate) => {
2426
+ try {
2427
+ (0, node_fs.writeFileSync)(candidate.path, candidate.source, "utf8");
2428
+ rewritten.push(candidate.path);
2429
+ } catch {
2430
+ skipped.push(candidate.path);
2431
+ }
2432
+ });
2433
+ return {
2434
+ rewritten,
2435
+ skipped
2436
+ };
2078
2437
  }
2079
2438
  //#endregion
2080
2439
  //#region src/cli/migrate/stringEntry.ts
@@ -2103,7 +2462,7 @@ function leadingCommentTextFor(sourceFile, node) {
2103
2462
  }
2104
2463
  //#endregion
2105
2464
  //#region src/cli/migrate/eslintrc/parse.ts
2106
- function lineOf$1(sourceFile, node) {
2465
+ function lineOf$2(sourceFile, node) {
2107
2466
  const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
2108
2467
  return line + 1;
2109
2468
  }
@@ -2397,7 +2756,7 @@ function readSharedBlockFields(input) {
2397
2756
  };
2398
2757
  }
2399
2758
  function readOverride(object, context, elementNode) {
2400
- const line = lineOf$1(context.sourceFile, object);
2759
+ const line = lineOf$2(context.sourceFile, object);
2401
2760
  const filesProperty = findProperty$1(object, "files");
2402
2761
  const files = filesProperty ? readStringOrEntries(filesProperty.initializer, context.sourceFile) : [];
2403
2762
  if (files.length === 0) {
@@ -2454,7 +2813,7 @@ function findExportObject(sourceFile) {
2454
2813
  return result;
2455
2814
  }
2456
2815
  function buildRoot(object, context) {
2457
- const line = lineOf$1(context.sourceFile, object);
2816
+ const line = lineOf$2(context.sourceFile, object);
2458
2817
  const shared = readSharedBlockFields({
2459
2818
  object,
2460
2819
  line,
@@ -3084,7 +3443,7 @@ var knownPresets = /* @__PURE__ */ new Set([
3084
3443
  "dom",
3085
3444
  "node"
3086
3445
  ]);
3087
- function lineOf(sourceFile, node) {
3446
+ function lineOf$1(sourceFile, node) {
3088
3447
  const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
3089
3448
  return line + 1;
3090
3449
  }
@@ -3387,7 +3746,7 @@ function classifyPresets(presetList, line, warnings) {
3387
3746
  };
3388
3747
  }
3389
3748
  function parseUseInsiderCall(callExpression, sourceFile, warnings) {
3390
- const line = lineOf(sourceFile, callExpression);
3749
+ const line = lineOf$1(sourceFile, callExpression);
3391
3750
  const [firstArgument] = callExpression.arguments;
3392
3751
  if (!firstArgument || !typescript.isObjectLiteralExpression(firstArgument)) {
3393
3752
  warnings.push({
@@ -3487,7 +3846,7 @@ function tryClassifyStandaloneGlobals(objectLiteral, keys, line, sourceFile) {
3487
3846
  };
3488
3847
  }
3489
3848
  function classifyObjectElement(objectLiteral, sourceFile, warnings) {
3490
- const line = lineOf(sourceFile, objectLiteral);
3849
+ const line = lineOf$1(sourceFile, objectLiteral);
3491
3850
  const keys = objectLiteral.properties.map(propertyKeyName).filter((name) => name !== null);
3492
3851
  const ignoresBlock = tryClassifyIgnoresBlock(objectLiteral, keys, line, sourceFile);
3493
3852
  if (ignoresBlock) return ignoresBlock;
@@ -3524,7 +3883,7 @@ function classifyArrayElement(element, sourceFile, warnings) {
3524
3883
  if (leadingComment !== void 0) parsed.leadingComment = leadingComment;
3525
3884
  return parsed;
3526
3885
  }
3527
- const line = lineOf(sourceFile, element);
3886
+ const line = lineOf$1(sourceFile, element);
3528
3887
  warnings.push({
3529
3888
  kind: "unrecognized-block",
3530
3889
  line,
@@ -3541,18 +3900,19 @@ function classifyArrayElement(element, sourceFile, warnings) {
3541
3900
  function isDefineConfigCall(expression) {
3542
3901
  return typescript.isCallExpression(expression) && typescript.isIdentifier(expression.expression) && expression.expression.text === "defineConfig";
3543
3902
  }
3544
- function findDefaultExportArray(sourceFile) {
3545
- const exportExpressions = sourceFile.statements.filter(typescript.isExportAssignment).map((statement) => unwrapTypeCasts(statement.expression));
3546
- const directArray = exportExpressions.find(typescript.isArrayLiteralExpression);
3547
- if (directArray) return directArray;
3548
- const defineConfigCall = exportExpressions.find(isDefineConfigCall);
3549
- if (!defineConfigCall) return null;
3550
- const [first] = defineConfigCall.arguments;
3903
+ function defineConfigArray(exportExpressions) {
3904
+ const [first] = exportExpressions.find(isDefineConfigCall)?.arguments ?? [];
3551
3905
  if (!first) return null;
3552
3906
  const unwrappedFirst = unwrapTypeCasts(first);
3553
3907
  if (typescript.isArrayLiteralExpression(unwrappedFirst)) return unwrappedFirst;
3554
3908
  return null;
3555
3909
  }
3910
+ function findDefaultExportElements(sourceFile) {
3911
+ const exportExpressions = sourceFile.statements.filter(typescript.isExportAssignment).map((statement) => unwrapTypeCasts(statement.expression));
3912
+ const array = exportExpressions.find(typescript.isArrayLiteralExpression) ?? defineConfigArray(exportExpressions);
3913
+ if (array) return [...array.elements];
3914
+ return exportExpressions;
3915
+ }
3556
3916
  function checkDefineConfigWrap(sourceFile) {
3557
3917
  return sourceFile.statements.some((statement) => {
3558
3918
  if (!typescript.isExportAssignment(statement)) return false;
@@ -3591,7 +3951,7 @@ function classifyInsiderImport(statement, sourceFile, flags) {
3591
3951
  return;
3592
3952
  }
3593
3953
  flags.hasUseInsider = true;
3594
- const line = lineOf(sourceFile, statement);
3954
+ const line = lineOf$1(sourceFile, statement);
3595
3955
  flags.preserved.push({
3596
3956
  raw: statement.getText(sourceFile),
3597
3957
  moduleSpecifier: "@useinsider/eslint-config"
@@ -3636,7 +3996,7 @@ function collectSilenceRulesCalls(sourceFile) {
3636
3996
  if (!rules) return null;
3637
3997
  const leadingComment = leadingCommentTextFor(sourceFile, statement);
3638
3998
  const call = {
3639
- line: lineOf(sourceFile, statement),
3999
+ line: lineOf$1(sourceFile, statement),
3640
4000
  rules
3641
4001
  };
3642
4002
  if (leadingComment !== void 0) call.leadingComment = leadingComment;
@@ -3649,8 +4009,7 @@ function parseV1Config(source, filePath) {
3649
4009
  const imports = inspectImports(sourceFile);
3650
4010
  warnings.push(...imports.warnings);
3651
4011
  const silenceRulesCalls = collectSilenceRulesCalls(sourceFile);
3652
- const arrayLiteral = findDefaultExportArray(sourceFile);
3653
- const arrayElements = arrayLiteral ? arrayLiteral.elements.map((element) => classifyArrayElement(element, sourceFile, warnings)).filter((entry) => entry !== null) : [];
4012
+ const arrayElements = findDefaultExportElements(sourceFile).map((element) => classifyArrayElement(element, sourceFile, warnings)).filter((entry) => entry !== null);
3654
4013
  return {
3655
4014
  hasUseInsiderImport: imports.hasUseInsider,
3656
4015
  hasSilenceRulesImport: imports.hasSilenceRules,
@@ -3662,6 +4021,220 @@ function parseV1Config(source, filePath) {
3662
4021
  };
3663
4022
  }
3664
4023
  //#endregion
4024
+ //#region src/cli/migrate/rehouseV3.ts
4025
+ var INSIDER_MODULE = "@useinsider/eslint-config";
4026
+ var COMPOSER_NAME = "useInsider";
4027
+ var DEFINE_CONFIG_NAME = "defineConfig";
4028
+ var SILENCE_RULES_NAME = "silenceRules";
4029
+ var RENDERER_EXPORT = "export const { useInsiderESLint, useInsiderOXC } = ";
4030
+ var RESERVED_KEYS = /* @__PURE__ */ new Set([
4031
+ "ignores",
4032
+ "jquery",
4033
+ "strict",
4034
+ "spellChecker",
4035
+ "silenceRules",
4036
+ "configFiles"
4037
+ ]);
4038
+ function isInsiderImport(statement) {
4039
+ const { moduleSpecifier } = statement;
4040
+ return typescript.isStringLiteral(moduleSpecifier) && moduleSpecifier.text === INSIDER_MODULE;
4041
+ }
4042
+ function composerAliases(sourceFile) {
4043
+ const aliases = /* @__PURE__ */ new Set();
4044
+ sourceFile.statements.forEach((statement) => {
4045
+ if (!typescript.isImportDeclaration(statement) || !isInsiderImport(statement)) return;
4046
+ const bindings = statement.importClause?.namedBindings;
4047
+ if (!bindings || !typescript.isNamedImports(bindings)) return;
4048
+ bindings.elements.forEach((element) => {
4049
+ if ((element.propertyName?.text ?? element.name.text) === COMPOSER_NAME) aliases.add(element.name.text);
4050
+ });
4051
+ });
4052
+ return aliases;
4053
+ }
4054
+ function isCallOf(node, names) {
4055
+ return typescript.isCallExpression(node) && typescript.isIdentifier(node.expression) && names.has(node.expression.text);
4056
+ }
4057
+ function collectCalls(sourceFile, names) {
4058
+ const found = [];
4059
+ const visit = (node) => {
4060
+ if (isCallOf(node, names)) found.push(node);
4061
+ typescript.forEachChild(node, visit);
4062
+ };
4063
+ visit(sourceFile);
4064
+ return found;
4065
+ }
4066
+ function peel(expression, state) {
4067
+ if (typescript.isParenthesizedExpression(expression) || typescript.isSpreadElement(expression)) return peel(expression.expression, state);
4068
+ if (typescript.isAsExpression(expression) || typescript.isSatisfiesExpression(expression)) {
4069
+ state.droppedAnnotation = true;
4070
+ return peel(expression.expression, state);
4071
+ }
4072
+ if (typescript.isAwaitExpression(expression)) {
4073
+ state.awaited = true;
4074
+ return peel(expression.expression, state);
4075
+ }
4076
+ return expression;
4077
+ }
4078
+ function peelArray(expression, state) {
4079
+ if (!typescript.isArrayLiteralExpression(expression) || expression.elements.length === 0) return expression;
4080
+ state.arrayWrapped = true;
4081
+ state.siblingElements = expression.elements.length - 1;
4082
+ return peel(expression.elements[0], state);
4083
+ }
4084
+ function unwrapDefaultExport(expression, state) {
4085
+ const peeled = peel(expression, state);
4086
+ if (isCallOf(peeled, /* @__PURE__ */ new Set([DEFINE_CONFIG_NAME]))) {
4087
+ state.definedConfig = true;
4088
+ return peeled;
4089
+ }
4090
+ return peelArray(peeled, state);
4091
+ }
4092
+ function argumentObject(call) {
4093
+ const [first] = call.arguments;
4094
+ if (!first || !typescript.isObjectLiteralExpression(first)) return null;
4095
+ return first;
4096
+ }
4097
+ function propertyKey(property) {
4098
+ const { name } = property;
4099
+ if (name === void 0) return null;
4100
+ if (typescript.isIdentifier(name) || typescript.isStringLiteral(name)) return name.text;
4101
+ return null;
4102
+ }
4103
+ function unreadableReason(argument, keysRead) {
4104
+ if (!argument) return "is not an object literal";
4105
+ if (argument.properties.some(typescript.isSpreadAssignment)) return "spreads another object";
4106
+ return keysRead < argument.properties.length ? "has a property whose key is not a literal name" : null;
4107
+ }
4108
+ function readArgument(call) {
4109
+ const argument = argumentObject(call);
4110
+ const optionKeys = (argument?.properties ?? []).map(propertyKey).filter((key) => key !== null);
4111
+ const candidates = optionKeys.filter((key) => !RESERVED_KEYS.has(key));
4112
+ return {
4113
+ optionKeys,
4114
+ scopes: candidates.filter(isMigratableScope),
4115
+ unregisteredKeys: candidates.filter((key) => !isMigratableScope(key)),
4116
+ unreadable: unreadableReason(argument, optionKeys.length)
4117
+ };
4118
+ }
4119
+ function refuse(reason) {
4120
+ return {
4121
+ kind: "refused",
4122
+ reason
4123
+ };
4124
+ }
4125
+ function multipleCallsReason(name) {
4126
+ return `${name} calls ${COMPOSER_NAME}() more than once. v4 has exactly one call — in the lint module — and it returns the two renderers instead of a config array, so the results cannot be concatenated. Merge them into a single call, one entry per scope key, then re-run the migration.`;
4127
+ }
4128
+ function mixedArrayReason(name, siblings) {
4129
+ return `${name}'s default export combines ${COMPOSER_NAME}() with ${siblings} other flat-config ${siblings === 1 ? "block" : "blocks"}, which have nowhere to go in a lint module. Fold them into the call first — directory ignores into \`ignores\`, per-file rule overrides into \`configFiles\` — then re-run the migration.`;
4130
+ }
4131
+ function indirectCallReason(name) {
4132
+ return `${name} calls ${COMPOSER_NAME}(), but its default export is not that call. The migration re-houses the call itself, so make it the default export (\`export default ${COMPOSER_NAME}({ ... })\`) and re-run the migration.`;
4133
+ }
4134
+ function defineConfigReason(name) {
4135
+ return `${name} wraps ${COMPOSER_NAME}() in ${DEFINE_CONFIG_NAME}(). v4's lint module exports the two renderers rather than a flat-config array, so there is nothing left for ${DEFINE_CONFIG_NAME}() to validate — drop the wrapper and re-run the migration.`;
4136
+ }
4137
+ function presetReason(name) {
4138
+ return `${name} still passes the v1 \`preset\` option, and this release could not read it — the preset list has to be an array of string literals. Restore the literal list and re-run the migration, or convert the call to the scope-keyed form by hand.`;
4139
+ }
4140
+ function annotationWarning(line) {
4141
+ return {
4142
+ kind: "dropped-type-annotation",
4143
+ line,
4144
+ message: `The type annotation on the default export was dropped — ${COMPOSER_NAME}() now returns \`{ useInsiderESLint, useInsiderOXC }\` rather than a config array. Delete the type import if nothing else uses it.`
4145
+ };
4146
+ }
4147
+ function unregisteredScopeWarning(key, line) {
4148
+ return {
4149
+ kind: "unregistered-scope",
4150
+ line,
4151
+ message: `\`${key}\` has no dependency manifest in the migration, so nothing was added to the install list for it. The key itself was carried over untouched — check its plugins are installed.`
4152
+ };
4153
+ }
4154
+ function unreadableArgumentWarning(line, reason) {
4155
+ return {
4156
+ kind: "unrecognized-block",
4157
+ line,
4158
+ message: `The ${COMPOSER_NAME}() argument ${reason}, so its scope list could not be read in full. The call was carried over untouched; the dependency list may be incomplete.`
4159
+ };
4160
+ }
4161
+ function lineOf(sourceFile, node) {
4162
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
4163
+ }
4164
+ function collectWarnings(line, reading, state) {
4165
+ const warnings = [];
4166
+ if (state.droppedAnnotation) warnings.push(annotationWarning(line));
4167
+ if (reading.unreadable) warnings.push(unreadableArgumentWarning(line, reading.unreadable));
4168
+ reading.unregisteredKeys.forEach((key) => {
4169
+ warnings.push(unregisteredScopeWarning(key, line));
4170
+ });
4171
+ return warnings;
4172
+ }
4173
+ function spliceExport(input) {
4174
+ const start = input.statement.getStart(input.sourceFile);
4175
+ const prefix = input.source.slice(0, start);
4176
+ const suffix = input.source.slice(input.statement.getEnd());
4177
+ const rehoused = `${prefix}${RENDERER_EXPORT}${input.callText};${suffix}`;
4178
+ return rehoused.endsWith("\n") ? rehoused : `${rehoused}\n`;
4179
+ }
4180
+ function defaultExports(sourceFile) {
4181
+ return sourceFile.statements.filter(typescript.isExportAssignment).filter((statement) => statement.isExportEquals !== true);
4182
+ }
4183
+ function emptyState() {
4184
+ return {
4185
+ awaited: false,
4186
+ arrayWrapped: false,
4187
+ droppedAnnotation: false,
4188
+ siblingElements: 0,
4189
+ definedConfig: false
4190
+ };
4191
+ }
4192
+ /**
4193
+ * Re-houses a v3 single-file config as a v4 lint module. Nothing about the configuration itself
4194
+ * changes between the two majors — only where the call lives — so the source is spliced rather than
4195
+ * re-rendered: every import, `silenceRules(...)` call, comment and blank line outside the export
4196
+ * statement survives byte-for-byte.
4197
+ * @example
4198
+ * rehouseV3Config("import { useInsider } from '@useinsider/eslint-config';\nexport default useInsider({});", 'eslint.config.js');
4199
+ * // { kind: 'rehoused', rehoused: { lintModule: '…export const { useInsiderESLint, useInsiderOXC } = useInsider({});\n', … } }
4200
+ */
4201
+ function rehouseV3Config(source, filePath) {
4202
+ const name = (0, node_path.basename)(filePath);
4203
+ const sourceFile = typescript.createSourceFile(filePath, source, typescript.ScriptTarget.Latest, true);
4204
+ const aliases = composerAliases(sourceFile);
4205
+ const calls = collectCalls(sourceFile, aliases);
4206
+ const exportStatements = defaultExports(sourceFile);
4207
+ if (calls.length === 0) return { kind: "not-v3" };
4208
+ if (exportStatements.length !== 1) return refuse(indirectCallReason(name));
4209
+ if (calls.length > 1) return refuse(multipleCallsReason(name));
4210
+ const [statement] = exportStatements;
4211
+ const state = emptyState();
4212
+ const unwrapped = unwrapDefaultExport(statement.expression, state);
4213
+ if (state.definedConfig) return refuse(defineConfigReason(name));
4214
+ if (state.siblingElements > 0) return refuse(mixedArrayReason(name, state.siblingElements));
4215
+ if (!isCallOf(unwrapped, aliases)) return refuse(indirectCallReason(name));
4216
+ const reading = readArgument(unwrapped);
4217
+ if (reading.optionKeys.includes("preset")) return refuse(presetReason(name));
4218
+ return {
4219
+ kind: "rehoused",
4220
+ rehoused: {
4221
+ lintModule: spliceExport({
4222
+ source,
4223
+ sourceFile,
4224
+ statement,
4225
+ callText: unwrapped.getText(sourceFile)
4226
+ }),
4227
+ scopes: reading.scopes,
4228
+ optionKeys: reading.optionKeys,
4229
+ unregisteredKeys: reading.unregisteredKeys,
4230
+ silenceRulesCallCount: collectCalls(sourceFile, /* @__PURE__ */ new Set([SILENCE_RULES_NAME])).length,
4231
+ droppedAwait: state.awaited,
4232
+ droppedArrayWrapper: state.arrayWrapped,
4233
+ warnings: collectWarnings(lineOf(sourceFile, unwrapped), reading, state)
4234
+ }
4235
+ };
4236
+ }
4237
+ //#endregion
3665
4238
  //#region src/cli/migrate/pruneUnusedImports.ts
3666
4239
  var INSIDER_MODULE_SPECIFIER = "@useinsider/eslint-config";
3667
4240
  function readModuleSpecifier(statement) {
@@ -4383,53 +4956,56 @@ function formatWarnings(warnings) {
4383
4956
  }
4384
4957
  //#endregion
4385
4958
  //#region src/cli/migrate/writer.ts
4386
- function writeLayout(eslintConfigPath, lintModuleSource) {
4387
- const directory = (0, node_path.dirname)(eslintConfigPath);
4388
- const layout = resolveLintLayout(directory, (0, node_path.basename)(eslintConfigPath));
4959
+ function writeLayout(directory, lintModuleSource) {
4960
+ const layout = resolveLintLayout(directory);
4389
4961
  const lintModulePath = (0, node_path.join)(directory, layout.lintModule);
4390
4962
  (0, node_fs.writeFileSync)(lintModulePath, lintModuleSource, "utf8");
4391
4963
  const writtenPaths = [lintModulePath];
4392
- renderLintLayoutFiles({
4393
- ...layout,
4394
- eslintConfig: (0, node_path.basename)(eslintConfigPath)
4395
- }).forEach((file) => {
4964
+ renderLintLayoutFiles(layout).forEach((file) => {
4396
4965
  const path = (0, node_path.join)(directory, file.filename);
4397
4966
  (0, node_fs.writeFileSync)(path, file.source, "utf8");
4398
4967
  writtenPaths.push(path);
4399
4968
  });
4400
4969
  return {
4970
+ eslintConfigPath: (0, node_path.join)(directory, layout.eslintConfig),
4401
4971
  lintModulePath,
4402
- writtenPaths
4972
+ writtenPaths,
4973
+ replacedEslintConfigPaths: layout.replacedEslintConfigs.map((name) => (0, node_path.join)(directory, name))
4403
4974
  };
4404
4975
  }
4976
+ function withoutDuplicates(paths, keep) {
4977
+ return [...new Set(paths.filter((path) => path !== keep))];
4978
+ }
4405
4979
  /**
4406
4980
  * Writes the three-file layout beside an existing flat config: the rendered `useInsider()` call
4407
- * becomes `insider.lint.*`, and `eslint.config.*` / `oxlint.config.*` become thin re-exports.
4981
+ * becomes `insider.lint.*`, and `eslint.config.*` / `oxlint.config.*` become thin re-exports. A
4982
+ * JavaScript-extension source config is superseded by the TypeScript one and nominated for deletion.
4408
4983
  * @example
4409
- * writeMigration('/repo/eslint.config.mjs', source).writtenPaths.length; // 3
4984
+ * writeMigration('/repo/eslint.config.js', source).deletedSourceCandidates; // ['/repo/eslint.config.js']
4410
4985
  */
4411
4986
  function writeMigration(targetPath, newSource) {
4412
- const { lintModulePath, writtenPaths } = writeLayout(targetPath, newSource);
4987
+ const result = writeLayout((0, node_path.dirname)(targetPath), newSource);
4413
4988
  return {
4414
- targetPath,
4415
- lintModulePath,
4416
- writtenPaths
4989
+ targetPath: result.eslintConfigPath,
4990
+ lintModulePath: result.lintModulePath,
4991
+ writtenPaths: result.writtenPaths,
4992
+ deletedSourceCandidates: withoutDuplicates([...result.replacedEslintConfigPaths, targetPath], result.eslintConfigPath)
4417
4993
  };
4418
4994
  }
4419
4995
  /**
4420
4996
  * Same as {@link writeMigration}, but for a legacy `.eslintrc*` source that has to be replaced by a
4421
- * differently-named flat config. Nominates the old file for deletion when the name changed.
4997
+ * differently-named flat config.
4422
4998
  * @example
4423
- * writeMigrationLegacy('/repo/.eslintrc.cjs', '/repo/eslint.config.mjs', source).deletedSourceCandidate;
4424
- * // '/repo/.eslintrc.cjs'
4999
+ * writeMigrationLegacy('/repo/.eslintrc.cjs', '/repo/eslint.config.mts', src).deletedSourceCandidates;
5000
+ * // ['/repo/.eslintrc.cjs']
4425
5001
  */
4426
5002
  function writeMigrationLegacy(sourcePath, writePath, newSource) {
4427
- const { lintModulePath, writtenPaths } = writeLayout(writePath, newSource);
5003
+ const result = writeLayout((0, node_path.dirname)(writePath), newSource);
4428
5004
  return {
4429
- targetPath: writePath,
4430
- lintModulePath,
4431
- writtenPaths,
4432
- deletedSourceCandidate: (0, node_path.basename)(sourcePath) === (0, node_path.basename)(writePath) ? void 0 : sourcePath
5005
+ targetPath: result.eslintConfigPath,
5006
+ lintModulePath: result.lintModulePath,
5007
+ writtenPaths: result.writtenPaths,
5008
+ deletedSourceCandidates: withoutDuplicates([...result.replacedEslintConfigPaths, sourcePath], result.eslintConfigPath)
4433
5009
  };
4434
5010
  }
4435
5011
  //#endregion
@@ -4489,6 +5065,22 @@ function describeStrict(plan) {
4489
5065
  if (plan.config.strictGlobs.length > 0) return `Strict scopes: 1 (${plan.config.strictGlobs.length} globs)`;
4490
5066
  return "Strict scopes: 0";
4491
5067
  }
5068
+ function describeRehousedKeys(label, keys) {
5069
+ return keys.length === 0 ? `${label}: (none)` : `${label}: ${keys.join(", ")}`;
5070
+ }
5071
+ function rehouseSummary(rehoused) {
5072
+ const lines = [
5073
+ "Source: v3 single-file eslint.config.*",
5074
+ "Re-housed as the lint module: only the export statement changed, every other line is byte-for-byte.",
5075
+ describeRehousedKeys("Options carried over", rehoused.optionKeys),
5076
+ describeRehousedKeys("Scopes with dependencies", rehoused.scopes),
5077
+ `silenceRules() calls kept: ${rehoused.silenceRulesCallCount}`
5078
+ ];
5079
+ if (rehoused.droppedAwait) lines.push("Dropped the `await` on the call — useInsider() is synchronous in v4.");
5080
+ if (rehoused.droppedArrayWrapper) lines.push("Dropped the array wrapper around the call — the lint module exports the renderers.");
5081
+ lines.push(`Warnings: ${rehoused.warnings.length}`);
5082
+ return lines.join("\n");
5083
+ }
4492
5084
  function summary(plan, format) {
4493
5085
  const lines = [];
4494
5086
  if (format === "eslintrc") lines.push("Source: legacy .eslintrc");
@@ -4501,29 +5093,51 @@ function summary(plan, format) {
4501
5093
  lines.push(`Warnings: ${plan.warnings.length}`);
4502
5094
  return lines.join("\n");
4503
5095
  }
4504
- function planFromSource(source, filePath, format) {
4505
- if (format === "flat") {
4506
- const parseResult = parseV1Config(source, filePath);
4507
- if (!isV1Shape(parseResult)) return null;
4508
- const plan = transformToV2(parseResult);
4509
- return {
4510
- plan,
4511
- rendered: renderV2(plan)
5096
+ function sourceModelFrom(resolved) {
5097
+ if (resolved.format !== "flat") {
5098
+ const parseResult = parseEslintrc(resolved.source, resolved.targetPath);
5099
+ return parseResult.found ? {
5100
+ ok: true,
5101
+ model: {
5102
+ kind: "plan",
5103
+ plan: transformEslintrcToV2(parseResult)
5104
+ }
5105
+ } : {
5106
+ ok: false,
5107
+ reason: null
4512
5108
  };
4513
5109
  }
4514
- const parseResult = parseEslintrc(source, filePath);
4515
- if (!parseResult.found) return null;
4516
- const plan = transformEslintrcToV2(parseResult);
5110
+ const parseResult = parseV1Config(resolved.source, resolved.targetPath);
5111
+ if (isV1Shape(parseResult)) return {
5112
+ ok: true,
5113
+ model: {
5114
+ kind: "plan",
5115
+ plan: transformToV2(parseResult)
5116
+ }
5117
+ };
5118
+ const outcome = rehouseV3Config(resolved.source, resolved.targetPath);
5119
+ if (outcome.kind === "rehoused") return {
5120
+ ok: true,
5121
+ model: {
5122
+ kind: "rehouse",
5123
+ rehoused: outcome.rehoused
5124
+ }
5125
+ };
4517
5126
  return {
4518
- plan,
4519
- rendered: renderV2(plan)
5127
+ ok: false,
5128
+ reason: outcome.kind === "refused" ? outcome.reason : null
4520
5129
  };
4521
5130
  }
4522
- function targetWritePath(cwd, sourcePath, format) {
4523
- if (format !== "eslintrc") return sourcePath;
4524
- const base = (0, node_path.basename)(sourcePath);
4525
- const dir = sourcePath.slice(0, sourcePath.length - base.length);
4526
- return (0, node_path.join)(dir.length > 0 ? dir : cwd, "eslint.config.mjs");
5131
+ function scopesOf(model) {
5132
+ if (model.kind === "plan") return new Set(model.plan.config.scopes.keys());
5133
+ return new Set(model.rehoused.scopes);
5134
+ }
5135
+ function warningsOf(model) {
5136
+ return model.kind === "plan" ? model.plan.warnings : model.rehoused.warnings;
5137
+ }
5138
+ function targetWritePath(cwd, sourcePath) {
5139
+ const directory = (0, node_path.dirname)(sourcePath) || cwd;
5140
+ return (0, node_path.join)(directory, resolveLintLayout(directory).eslintConfig);
4527
5141
  }
4528
5142
  function parseEslintIgnoreContents(raw) {
4529
5143
  return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
@@ -4557,6 +5171,17 @@ function describeDepChanges(depChanges) {
4557
5171
  lines.push(`- Install (${depChanges.install.specs.length}): ${depChanges.install.specs.join(", ")}`);
4558
5172
  return lines.join("\n");
4559
5173
  }
5174
+ async function confirmGuidedStart(clack, sourcePath) {
5175
+ const result = await clack.confirm({
5176
+ message: `${(0, node_path.basename)(sourcePath)} still uses the v3 single-file layout. Migrate it now?`,
5177
+ initialValue: true
5178
+ });
5179
+ return !clack.isCancel(result) && result === true;
5180
+ }
5181
+ function noteConfigRename(clack, sourcePath, writePath) {
5182
+ if (!clack.note || (0, node_path.basename)(sourcePath) === (0, node_path.basename)(writePath)) return;
5183
+ 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");
5184
+ }
4560
5185
  async function confirmWrite(clack, writePath, initial = true) {
4561
5186
  const message = `Write migrated config to ${writePath}?`;
4562
5187
  const result = await clack.confirm({
@@ -4593,17 +5218,26 @@ function emitInstallNextSteps(clack, depSummary, cwd) {
4593
5218
  if (message === null) return;
4594
5219
  clack.note(message, "Next steps");
4595
5220
  }
4596
- function emitSummary(clack, plan, format) {
4597
- if (clack.note) clack.note(summary(plan, format), "Migration summary");
4598
- if (plan.warnings.length > 0 && clack.note) clack.note(formatWarnings(plan.warnings), "Warnings");
5221
+ function emitSummary(clack, model, format) {
5222
+ if (!clack.note) return;
5223
+ clack.note(model.kind === "plan" ? summary(model.plan, format) : rehouseSummary(model.rehoused), "Migration summary");
5224
+ const warnings = warningsOf(model);
5225
+ if (warnings.length > 0) clack.note(formatWarnings(warnings), "Warnings");
4599
5226
  }
4600
5227
  function writeAndAnnounce(clack, targetPath, writePath, rendered, format) {
4601
5228
  const result = format === "eslintrc" ? writeMigrationLegacy(targetPath, writePath, rendered) : writeMigration(targetPath, rendered);
4602
5229
  clack.outro(`Wrote ${result.writtenPaths.join(", ")}`);
4603
- return { deletedSourceCandidate: result.deletedSourceCandidate };
5230
+ return { deletedSourceCandidates: result.deletedSourceCandidates };
5231
+ }
5232
+ function supersededFlatConfigs(sourcePath, writePath) {
5233
+ const directory = (0, node_path.dirname)(writePath);
5234
+ const { replacedEslintConfigs } = resolveLintLayout(directory);
5235
+ const candidates = [...replacedEslintConfigs.map((filename) => (0, node_path.join)(directory, filename)), writePath];
5236
+ return [...new Set(candidates.filter((path) => path !== sourcePath && (0, node_fs.existsSync)(path)))];
4604
5237
  }
4605
- async function confirmOverwriteExistingOutput(clack, writePath, sourcePath, initial = false) {
4606
- const message = `An \`${(0, node_path.basename)(writePath)}\` already exists. Overwrite + delete \`${(0, node_path.basename)(sourcePath)}\`?`;
5238
+ async function confirmOverwriteExistingOutput(clack, superseded, sourcePath, initial = false) {
5239
+ const names = superseded.map((path) => (0, node_path.basename)(path));
5240
+ const message = `${names.length === 1 ? `An \`${names[0]}\` already exists` : `\`${names.join("`, `")}\` already exist`}. Overwrite + delete \`${(0, node_path.basename)(sourcePath)}\`?`;
4607
5241
  const result = await clack.confirm({
4608
5242
  message,
4609
5243
  initialValue: initial
@@ -4638,23 +5272,30 @@ function resolveSource(input, clack) {
4638
5272
  }
4639
5273
  };
4640
5274
  }
5275
+ function renderModel(model, eslintIgnore) {
5276
+ if (model.kind === "rehouse") return model.rehoused.lintModule;
5277
+ if (eslintIgnore && eslintIgnore.patterns.length > 0) mergeIgnorePatterns(model.plan, eslintIgnore.patterns);
5278
+ return renderV2(model.plan);
5279
+ }
4641
5280
  function prepareMigration(resolved, input) {
4642
- const planResult = planFromSource(resolved.source, resolved.targetPath, resolved.format);
4643
- if (!planResult) return null;
4644
- const { plan } = planResult;
4645
- const writePath = targetWritePath(input.cwd, resolved.targetPath, resolved.format);
5281
+ const outcome = sourceModelFrom(resolved);
5282
+ if (!outcome.ok) return outcome;
5283
+ const writePath = targetWritePath(input.cwd, resolved.targetPath);
4646
5284
  const eslintIgnore = findEslintIgnore(resolved.targetPath);
4647
- if (eslintIgnore && eslintIgnore.patterns.length > 0) mergeIgnorePatterns(plan, eslintIgnore.patterns);
4648
5285
  return {
4649
- plan,
4650
- rendered: renderV2(plan),
4651
- writePath,
4652
- eslintIgnore
5286
+ ok: true,
5287
+ prepared: {
5288
+ model: outcome.model,
5289
+ rendered: renderModel(outcome.model, eslintIgnore),
5290
+ writePath,
5291
+ eslintIgnore
5292
+ }
4653
5293
  };
4654
5294
  }
4655
5295
  async function runConfirmations(clack, input, targetPath, writePath, previous) {
4656
- if (writePath !== targetPath && (0, node_fs.existsSync)(writePath)) {
4657
- if (!await confirmOverwriteExistingOutput(clack, writePath, targetPath, previous?.overwriteExisting ?? false)) {
5296
+ const superseded = supersededFlatConfigs(targetPath, writePath);
5297
+ if (superseded.length > 0) {
5298
+ if (!await confirmOverwriteExistingOutput(clack, superseded, targetPath, previous?.overwriteExisting ?? false)) {
4658
5299
  clack.outro("Aborted: existing output kept.");
4659
5300
  return false;
4660
5301
  }
@@ -4678,7 +5319,7 @@ function tryReadConsumerManifest(path) {
4678
5319
  return null;
4679
5320
  }
4680
5321
  }
4681
- function computeDepSummary(plan, writePath, overrides) {
5322
+ function computeDepSummary(model, writePath, overrides) {
4682
5323
  const pkgPath = consumerPackageJsonPath(writePath);
4683
5324
  const manifest = tryReadConsumerManifest(pkgPath);
4684
5325
  if (!manifest) return {
@@ -4691,7 +5332,7 @@ function computeDepSummary(plan, writePath, overrides) {
4691
5332
  };
4692
5333
  const pruneOutcome = pruneEslintDependencies(manifest);
4693
5334
  const ownManifest = (overrides?.readEslintConfigManifest ?? readOwnEslintConfigManifest)();
4694
- const install = buildInstallList(new Set(plan.config.scopes.keys()), ownManifest);
5335
+ const install = buildInstallList(scopesOf(model), ownManifest);
4695
5336
  return {
4696
5337
  removed: pruneOutcome.removed,
4697
5338
  install,
@@ -4712,7 +5353,7 @@ function resolveInstallCwd(depSummary, input) {
4712
5353
  return input.cwd;
4713
5354
  }
4714
5355
  async function runInstaller(clack, depSummary, input) {
4715
- await installWithRetry({
5356
+ return installWithRetry({
4716
5357
  deps: depSummary.install.specs,
4717
5358
  cwd: resolveInstallCwd(depSummary, input),
4718
5359
  autoConfirm: Boolean(input.yes),
@@ -4724,42 +5365,79 @@ async function runInstaller(clack, depSummary, input) {
4724
5365
  async function applyDepChanges(clack, depSummary, input) {
4725
5366
  if (!depSummary.packageJsonPath) {
4726
5367
  if (clack.note) clack.note("No package.json found alongside the migrated config — skipping dependency updates.", "Notice");
4727
- return;
5368
+ return true;
4728
5369
  }
4729
5370
  persistPrunedManifest(depSummary);
4730
5371
  if (input.noInstall) {
4731
5372
  emitInstallNextSteps(clack, depSummary, input.cwd);
4732
- return;
5373
+ return true;
4733
5374
  }
4734
5375
  if (!input.yes) {
4735
5376
  if (!await confirmInstall(clack, depSummary)) {
4736
5377
  emitInstallNextSteps(clack, depSummary, input.cwd);
4737
- return;
5378
+ return true;
4738
5379
  }
4739
5380
  }
4740
5381
  noteFallbacks(clack, depSummary);
4741
- await runInstaller(clack, depSummary, input);
5382
+ if (!(await runInstaller(clack, depSummary, input)).installed) {
5383
+ emitInstallNextSteps(clack, depSummary, input.cwd);
5384
+ return false;
5385
+ }
5386
+ return true;
4742
5387
  }
4743
5388
  function performWriteAndCleanup(clack, resolved, prepared) {
4744
5389
  const outcome = writeAndAnnounce(clack, resolved.targetPath, prepared.writePath, prepared.rendered, resolved.format);
4745
- if (prepared.eslintIgnore) safeUnlink(prepared.eslintIgnore.path, clack, ".eslintignore");
4746
- if (outcome.deletedSourceCandidate) safeUnlink(outcome.deletedSourceCandidate, clack, "source config");
5390
+ if (prepared.eslintIgnore && prepared.model.kind === "plan") safeUnlink(prepared.eslintIgnore.path, clack, ".eslintignore");
5391
+ outcome.deletedSourceCandidates.forEach((path) => {
5392
+ safeUnlink(path, clack, "superseded config");
5393
+ });
5394
+ }
5395
+ function noteLintScriptOutcome$1(clack, result) {
5396
+ if (!clack.note) return;
5397
+ if (result.unresolved) {
5398
+ 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");
5399
+ return;
5400
+ }
5401
+ if (result.alreadyRunsOxlint.length > 0 && result.changes.length === 0) {
5402
+ clack.note(`\`${result.alreadyRunsOxlint.join("`, `")}\` already run oxlint — left alone.`, "package.json scripts unchanged");
5403
+ return;
5404
+ }
5405
+ 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");
5406
+ if (result.changes.length === 0) return;
5407
+ clack.note(`${formatLintScriptChanges(result.changes)}\n\noxlint was prepended, never substituted, so every command that was there still runs.`, "Updated package.json scripts");
4747
5408
  }
4748
5409
  function applyLintScript$1(clack, directory) {
4749
5410
  const manifestPath = (0, node_path.join)(directory, "package.json");
4750
5411
  if (!(0, node_fs.existsSync)(manifestPath)) return;
4751
5412
  const result = setLintScript((0, node_fs.readFileSync)(manifestPath, "utf8"));
4752
- if (!result.changed) return;
4753
- (0, node_fs.writeFileSync)(manifestPath, result.manifest, "utf8");
4754
- if (clack.note) {
4755
- const previous = result.previous ?? "(none)";
4756
- clack.note(`lint: ${previous} -> oxlint --disable-nested-config && eslint .`, "Updated package.json script");
5413
+ if (result.changed) (0, node_fs.writeFileSync)(manifestPath, result.manifest, "utf8");
5414
+ noteLintScriptOutcome$1(clack, result);
5415
+ }
5416
+ function noteUntouchedDirectiveFiles(clack, skipped) {
5417
+ if (skipped.length === 0 || !clack.note) return;
5418
+ clack.note(formatUntouchedDirectiveFiles(skipped), `${skipped.length} file(s) left untouched`);
5419
+ }
5420
+ async function rewriteDirectivesWithNote(clack, directory, autoConfirm) {
5421
+ const { candidates, skipped } = previewDisableDirectives(directory);
5422
+ noteUntouchedDirectiveFiles(clack, skipped);
5423
+ if (candidates.length === 0) return;
5424
+ const count = candidates.length;
5425
+ 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.
5426
+
5427
+ This is a separate, source-wide diff — review it on its own commit.`, "Disable directives");
5428
+ if (!autoConfirm) {
5429
+ const proceed = await clack.confirm({
5430
+ message: `Re-point \`eslint-disable\` onto \`oxlint-disable\` in ${count} file(s) now?`,
5431
+ initialValue: true
5432
+ });
5433
+ if (clack.isCancel(proceed) || proceed !== true) {
5434
+ if (clack.note) clack.note("Skipped. Re-run `npx @useinsider/eslint-config migrate` to apply it later.", "Disable directives not rewritten");
5435
+ return;
5436
+ }
4757
5437
  }
4758
- }
4759
- function rewriteDirectivesWithNote(clack, directory) {
4760
- const { rewritten } = rewriteDisableDirectives(directory);
4761
- if (rewritten.length === 0 || !clack.note) return;
4762
- clack.note(`Re-pointed eslint-disable comments onto oxlint-disable in ${rewritten.length} file(s).`, "Disable directives");
5438
+ const { rewritten, skipped: unwritten } = rewriteDisableDirectives(directory, candidates);
5439
+ if (clack.note) clack.note(`Re-pointed directives in ${rewritten.length} file(s).`, "Disable directives");
5440
+ noteUntouchedDirectiveFiles(clack, unwritten);
4763
5441
  }
4764
5442
  function loadPreviousMigrate(clack, cwd) {
4765
5443
  const restored = loadSession({ cwd });
@@ -4768,10 +5446,15 @@ function loadPreviousMigrate(clack, cwd) {
4768
5446
  return previousMigrate;
4769
5447
  }
4770
5448
  function emitNothingToMigrate(clack, format) {
4771
- clack.outro(format === "eslintrc" ? "No legacy `.eslintrc` shape found — nothing to migrate." : "No v1 useInsider patterns found — nothing to migrate.");
5449
+ clack.outro(format === "eslintrc" ? "No legacy `.eslintrc` shape found — nothing to migrate." : "No v1 or v3 useInsider patterns found — nothing to migrate.");
5450
+ }
5451
+ function noteUnfoldedEslintIgnore(clack, prepared) {
5452
+ const patterns = prepared.eslintIgnore?.patterns ?? [];
5453
+ if (!clack.note || prepared.model.kind !== "rehouse" || patterns.length === 0) return;
5454
+ clack.note(`${prepared.eslintIgnore?.path} still holds ${patterns.length} pattern(s). Flat config does not read it, and the v3 config was carried over verbatim, so they were not folded into \`ignores\`. Move the ones you still need into the \`ignores\` array and delete the file.`, ".eslintignore left alone");
4772
5455
  }
4773
5456
  function persistMigrateSession(input, prepared, resolved) {
4774
- const overwriteExisting = prepared.writePath !== resolved.targetPath && (0, node_fs.existsSync)(prepared.writePath);
5457
+ const overwriteExisting = supersededFlatConfigs(resolved.targetPath, prepared.writePath).length > 0;
4775
5458
  saveSession({
4776
5459
  command: "migrate",
4777
5460
  migrate: {
@@ -4783,34 +5466,54 @@ function persistMigrateSession(input, prepared, resolved) {
4783
5466
  }
4784
5467
  }, { cwd: input.cwd });
4785
5468
  }
5469
+ async function finishMigrate(clack, depSummary, input) {
5470
+ if (depSummary && !await applyDepChanges(clack, depSummary, input)) {
5471
+ clack.cancel("Config migrated, but the dependencies did not install. Install them and re-run lint.");
5472
+ return 1;
5473
+ }
5474
+ clearSession({ cwd: input.cwd });
5475
+ return 0;
5476
+ }
4786
5477
  async function runMigrate(input, clackOverride) {
4787
5478
  const clack = clackOverride ?? await loadClack();
4788
5479
  const sourceResult = resolveSource(input, clack);
4789
5480
  if (!sourceResult.ok) return sourceResult.exitCode;
4790
5481
  const { resolved } = sourceResult;
4791
5482
  clack.intro(`Migrating ${resolved.targetPath}`);
5483
+ const declinedExitCode = input.fromLint ? 1 : 0;
5484
+ if (input.fromLint && !input.yes && !await confirmGuidedStart(clack, resolved.targetPath)) {
5485
+ clack.outro("Left the config as it is — nothing was written.");
5486
+ return declinedExitCode;
5487
+ }
4792
5488
  const previousMigrate = loadPreviousMigrate(clack, input.cwd);
4793
- const prepared = prepareMigration(resolved, input);
4794
- if (!prepared) {
4795
- emitNothingToMigrate(clack, resolved.format);
4796
- return 0;
5489
+ const outcome = prepareMigration(resolved, input);
5490
+ if (!outcome.ok) {
5491
+ if (outcome.reason === null) {
5492
+ emitNothingToMigrate(clack, resolved.format);
5493
+ return declinedExitCode;
5494
+ }
5495
+ clack.cancel(outcome.reason);
5496
+ return 1;
4797
5497
  }
4798
- emitSummary(clack, prepared.plan, resolved.format);
4799
- const depSummary = computeDepSummary(prepared.plan, prepared.writePath, input.overrides);
4800
- if (!await runConfirmations(clack, input, resolved.targetPath, prepared.writePath, previousMigrate)) return 0;
5498
+ const { prepared } = outcome;
5499
+ emitSummary(clack, prepared.model, resolved.format);
5500
+ noteUnfoldedEslintIgnore(clack, prepared);
5501
+ noteConfigRename(clack, resolved.targetPath, prepared.writePath);
5502
+ const depSummary = computeDepSummary(prepared.model, prepared.writePath, input.overrides);
5503
+ if (!await runConfirmations(clack, input, resolved.targetPath, prepared.writePath, previousMigrate)) return declinedExitCode;
4801
5504
  persistMigrateSession(input, prepared, resolved);
4802
5505
  performWriteAndCleanup(clack, resolved, prepared);
4803
5506
  applyLintScript$1(clack, (0, node_path.dirname)(prepared.writePath));
4804
- rewriteDirectivesWithNote(clack, (0, node_path.dirname)(prepared.writePath));
5507
+ await rewriteDirectivesWithNote(clack, (0, node_path.dirname)(prepared.writePath), Boolean(input.yes));
4805
5508
  const consumerDir = (0, node_path.dirname)(prepared.writePath);
5509
+ const consumerLayout = resolveLintLayout(consumerDir);
4806
5510
  await applyTsExtensionsFlag(clack, {
4807
5511
  cwd: consumerDir,
4808
- lintModule: resolveLintLayout(consumerDir).lintModule,
5512
+ lintModule: consumerLayout.lintModule,
5513
+ eslintConfig: consumerLayout.eslintConfig,
4809
5514
  autoConfirm: input.yes
4810
5515
  });
4811
- if (depSummary) await applyDepChanges(clack, depSummary, input);
4812
- clearSession({ cwd: input.cwd });
4813
- return 0;
5516
+ return finishMigrate(clack, depSummary, input);
4814
5517
  }
4815
5518
  //#endregion
4816
5519
  //#region src/cli/index.ts
@@ -4977,9 +5680,19 @@ async function resolveHybridAnswers(clack, input, answers) {
4977
5680
  }
4978
5681
  function applyLintScript(cwd) {
4979
5682
  const manifestPath = (0, node_path.join)(cwd, "package.json");
4980
- if (!(0, node_fs.existsSync)(manifestPath)) return;
5683
+ if (!(0, node_fs.existsSync)(manifestPath)) return null;
4981
5684
  const result = setLintScript((0, node_fs.readFileSync)(manifestPath, "utf8"));
4982
5685
  if (result.changed) (0, node_fs.writeFileSync)(manifestPath, result.manifest, "utf8");
5686
+ return result;
5687
+ }
5688
+ function noteLintScriptOutcome(clack, result) {
5689
+ if (!clack.note || !result) return;
5690
+ if (result.unresolved) {
5691
+ 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");
5692
+ return;
5693
+ }
5694
+ 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");
5695
+ if (result.changes.length > 0) clack.note(formatLintScriptChanges(result.changes), "Updated package.json scripts");
4983
5696
  }
4984
5697
  function writeLintLayout(cwd, lintModuleSource) {
4985
5698
  const layout = resolveLintLayout(cwd);
@@ -4988,21 +5701,26 @@ function writeLintLayout(cwd, lintModuleSource) {
4988
5701
  renderLintLayoutFiles(layout).forEach((file) => {
4989
5702
  (0, node_fs.writeFileSync)((0, node_path.join)(cwd, file.filename), file.source, "utf8");
4990
5703
  });
4991
- applyLintScript(cwd);
4992
- return lintModulePath;
5704
+ layout.replacedEslintConfigs.forEach((filename) => {
5705
+ (0, node_fs.rmSync)((0, node_path.join)(cwd, filename), { force: true });
5706
+ });
5707
+ return {
5708
+ lintModulePath,
5709
+ lintScript: applyLintScript(cwd)
5710
+ };
4993
5711
  }
4994
5712
  async function installAndWrite(clack, input, answers, options) {
4995
5713
  if (options.pruneBeforeInstall) noteOverridePrune(clack, pruneConsumerPackageJson(input.cwd).removed);
4996
5714
  const deps = buildDependencyList(answers);
4997
5715
  if (clack.note) clack.note(deps.map((dep) => ` - ${dep}`).join("\n"), "Installing dev dependencies");
4998
- await installWithRetry({
5716
+ if (!(await installWithRetry({
4999
5717
  deps,
5000
5718
  cwd: input.cwd,
5001
5719
  autoConfirm: Boolean(input.yes),
5002
5720
  clack,
5003
5721
  installer: input.overrides?.installer ?? installDependencies,
5004
5722
  fsOps: input.overrides?.wipeFs
5005
- });
5723
+ })).installed) return null;
5006
5724
  return writeLintLayout(input.cwd, renderLintModuleFile(answers));
5007
5725
  }
5008
5726
  async function runScaffold(input, clackOverride) {
@@ -5024,14 +5742,21 @@ async function runScaffold(input, clackOverride) {
5024
5742
  existingConfigChoice: "override"
5025
5743
  }
5026
5744
  }, { cwd: input.cwd });
5027
- const targetPath = await installAndWrite(clack, input, resolvedAnswers, { pruneBeforeInstall: viaOverride });
5745
+ const written = await installAndWrite(clack, input, resolvedAnswers, { pruneBeforeInstall: viaOverride });
5746
+ if (!written) {
5747
+ clack.cancel("Dependencies were not installed — nothing was written. Fix the install and re-run.");
5748
+ return 1;
5749
+ }
5750
+ noteLintScriptOutcome(clack, written.lintScript);
5751
+ const layout = resolveLintLayout(input.cwd);
5028
5752
  await applyTsExtensionsFlag(clack, {
5029
5753
  cwd: input.cwd,
5030
- lintModule: resolveLintLayout(input.cwd).lintModule,
5754
+ lintModule: layout.lintModule,
5755
+ eslintConfig: layout.eslintConfig,
5031
5756
  autoConfirm: input.yes
5032
5757
  });
5033
5758
  clearSession({ cwd: input.cwd });
5034
- clack.outro(`Wrote ${targetPath} and its sibling linter configs`);
5759
+ clack.outro(`Wrote ${written.lintModulePath} and its sibling linter configs`);
5035
5760
  return 0;
5036
5761
  }
5037
5762
  async function runScaffoldSubcommand(scaffoldInput) {
@@ -5042,7 +5767,8 @@ async function runMigrateSubcommand(input) {
5042
5767
  cwd: input.cwd,
5043
5768
  target: input.target,
5044
5769
  yes: input.yes,
5045
- noInstall: input.noInstall
5770
+ noInstall: input.noInstall,
5771
+ fromLint: input.fromLint
5046
5772
  });
5047
5773
  }
5048
5774
  function writeStderr(line) {