@useinsider/eslint-config 4.0.0-beta.1 → 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.
- package/README.md +100 -9
- package/dist/builders/scopeBlocks.d.ts +6 -0
- package/dist/cli.cjs +551 -166
- package/dist/cli.cjs.map +1 -1
- package/dist/main.cjs +390 -84
- package/dist/main.cjs.map +1 -1
- package/dist/main.js +391 -85
- package/dist/main.js.map +1 -1
- package/dist/utils/ensureConfigStructure.d.ts +8 -4
- package/dist/utils/ensureOxlintConfig.d.ts +2 -1
- package/dist/utils/layoutDiagnosis.d.ts +63 -0
- package/dist/utils/offerMigration.d.ts +42 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,14 @@ async function installWithRetry(opts) {
|
|
|
701
736
|
fs,
|
|
702
737
|
detected: detectLockfile(opts.cwd, fs.exists)
|
|
703
738
|
};
|
|
704
|
-
if (isPeerDepFailure(reason))
|
|
705
|
-
|
|
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
|
|
712
744
|
//#region src/cli/lintLayout.ts
|
|
713
|
-
var
|
|
745
|
+
var OXLINT_COMMAND = "oxlint --disable-nested-config";
|
|
746
|
+
var OXLINT_LINT_SCRIPT = `${OXLINT_COMMAND} && eslint .`;
|
|
714
747
|
/**
|
|
715
748
|
* Extension for the lint module and the oxlint config. oxlint only discovers `oxlint.config.ts` and
|
|
716
749
|
* `oxlint.config.mts`, and Node warns on a bare `.ts` file inside a package that declares no
|
|
@@ -726,29 +759,45 @@ function resolveLayoutExtension(cwd) {
|
|
|
726
759
|
return "mts";
|
|
727
760
|
}
|
|
728
761
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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)));
|
|
738
780
|
}
|
|
739
781
|
/**
|
|
740
|
-
* The three filenames `useInsider()` requires, resolved against a consumer directory.
|
|
741
|
-
*
|
|
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.
|
|
742
787
|
* @example
|
|
743
|
-
* resolveLintLayout('/repo');
|
|
788
|
+
* resolveLintLayout('/repo');
|
|
789
|
+
* // { eslintConfig: 'eslint.config.ts', replacedEslintConfigs: ['eslint.config.js'], ... }
|
|
744
790
|
*/
|
|
745
|
-
function resolveLintLayout(cwd
|
|
791
|
+
function resolveLintLayout(cwd) {
|
|
746
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}`;
|
|
747
795
|
return {
|
|
748
796
|
extension,
|
|
749
797
|
lintModule: `insider.lint.${extension}`,
|
|
750
798
|
oxlintConfig: `oxlint.config.${extension}`,
|
|
751
|
-
eslintConfig
|
|
799
|
+
eslintConfig,
|
|
800
|
+
replacedEslintConfigs: existing.filter((filename) => filename !== eslintConfig)
|
|
752
801
|
};
|
|
753
802
|
}
|
|
754
803
|
/**
|
|
@@ -782,7 +831,7 @@ function renderOxlintConfigFile(lintModule) {
|
|
|
782
831
|
* The two thin files that sit beside a lint module, ready to write.
|
|
783
832
|
* @example
|
|
784
833
|
* renderLintLayoutFiles(resolveLintLayout('/repo')).map(file => file.filename);
|
|
785
|
-
* // ['eslint.config.
|
|
834
|
+
* // ['eslint.config.mts', 'oxlint.config.mts']
|
|
786
835
|
*/
|
|
787
836
|
function renderLintLayoutFiles(layout) {
|
|
788
837
|
return [{
|
|
@@ -801,12 +850,111 @@ function detectIndent$1(raw) {
|
|
|
801
850
|
const width = rest.length - rest.trimStart().length;
|
|
802
851
|
return width > 0 ? width : DEFAULT_INDENT;
|
|
803
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);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
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
|
+
}
|
|
804
948
|
/**
|
|
805
|
-
*
|
|
806
|
-
*
|
|
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.
|
|
807
955
|
* @example
|
|
808
|
-
* setLintScript('{"scripts":{"lint":"eslint ."}}').
|
|
809
|
-
* // '
|
|
956
|
+
* setLintScript('{"scripts":{"lint":"eslint ."}}').changes;
|
|
957
|
+
* // [{ script: 'lint', previous: 'eslint .', next: 'oxlint --disable-nested-config && eslint .' }]
|
|
810
958
|
*/
|
|
811
959
|
function setLintScript(rawManifest) {
|
|
812
960
|
let parsed;
|
|
@@ -815,25 +963,77 @@ function setLintScript(rawManifest) {
|
|
|
815
963
|
} catch {
|
|
816
964
|
return {
|
|
817
965
|
manifest: rawManifest,
|
|
818
|
-
changed: false
|
|
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
|
+
}
|
|
819
987
|
};
|
|
988
|
+
changes.push({
|
|
989
|
+
script: "lint",
|
|
990
|
+
previous: "",
|
|
991
|
+
next: OXLINT_LINT_SCRIPT
|
|
992
|
+
});
|
|
820
993
|
}
|
|
821
|
-
|
|
822
|
-
if (previous?.includes("oxlint")) return {
|
|
994
|
+
if (changes.length === 0) return {
|
|
823
995
|
manifest: rawManifest,
|
|
824
996
|
changed: false,
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
...parsed.scripts,
|
|
829
|
-
lint: OXLINT_LINT_SCRIPT
|
|
997
|
+
changes: [],
|
|
998
|
+
alreadyRunsOxlint,
|
|
999
|
+
skipped
|
|
830
1000
|
};
|
|
1001
|
+
parsed.scripts = { ...scripts };
|
|
1002
|
+
changes.forEach((change) => {
|
|
1003
|
+
parsed.scripts[change.script] = change.next;
|
|
1004
|
+
});
|
|
831
1005
|
return {
|
|
832
1006
|
manifest: `${JSON.stringify(parsed, null, detectIndent$1(rawManifest))}\n`,
|
|
833
1007
|
changed: true,
|
|
834
|
-
|
|
1008
|
+
changes,
|
|
1009
|
+
alreadyRunsOxlint,
|
|
1010
|
+
skipped
|
|
835
1011
|
};
|
|
836
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");
|
|
1036
|
+
}
|
|
837
1037
|
//#endregion
|
|
838
1038
|
//#region src/cli/quote.ts
|
|
839
1039
|
function quote(value) {
|
|
@@ -1753,31 +1953,89 @@ var TS_EXTENSIONS_SNIPPET = [
|
|
|
1753
1953
|
"}"
|
|
1754
1954
|
].join("\n");
|
|
1755
1955
|
var DEFAULT_INDENT_UNIT = " ";
|
|
1756
|
-
function
|
|
1956
|
+
function acceptsTsExtensionsFlag(options) {
|
|
1957
|
+
return options.noEmit === true || options.emitDeclarationOnly === true || options.rewriteRelativeImportExtensions === true;
|
|
1958
|
+
}
|
|
1959
|
+
function parseTsconfig(path, raw) {
|
|
1757
1960
|
const { config } = typescript.parseConfigFileTextToJson(path, raw);
|
|
1758
|
-
if (!config) return
|
|
1759
|
-
|
|
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
|
+
};
|
|
1760
1969
|
}
|
|
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");
|
|
1970
|
+
function readTsconfigAt(path, governedFiles) {
|
|
1769
1971
|
if (!(0, node_fs.existsSync)(path)) return null;
|
|
1770
1972
|
try {
|
|
1771
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)));
|
|
1772
1987
|
return {
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1988
|
+
state: {
|
|
1989
|
+
path,
|
|
1990
|
+
raw,
|
|
1991
|
+
enabled: parsed.enabled,
|
|
1992
|
+
governs,
|
|
1993
|
+
acceptsFlag: parsed.acceptsFlag
|
|
1994
|
+
},
|
|
1995
|
+
references: parsed.references
|
|
1776
1996
|
};
|
|
1777
1997
|
} catch {
|
|
1778
1998
|
return null;
|
|
1779
1999
|
}
|
|
1780
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
|
+
}
|
|
1781
2039
|
function rootObject(source) {
|
|
1782
2040
|
const [statement] = source.statements;
|
|
1783
2041
|
const expression = statement?.expression;
|
|
@@ -1861,6 +2119,10 @@ function noteManualStep(clack, lintModule) {
|
|
|
1861
2119
|
if (!clack.note) return;
|
|
1862
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");
|
|
1863
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
|
+
}
|
|
1864
2126
|
async function confirmFlag(clack, path) {
|
|
1865
2127
|
const result = await clack.confirm({
|
|
1866
2128
|
message: `Set \`${TS_EXTENSIONS_FLAG}: true\` in ${path}? Without it \`tsc\` fails with TS5097 on the generated config files.`,
|
|
@@ -1868,16 +2130,14 @@ async function confirmFlag(clack, path) {
|
|
|
1868
2130
|
});
|
|
1869
2131
|
return !clack.isCancel(result) && result === true;
|
|
1870
2132
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
const state = readTsconfigState(input.cwd);
|
|
1880
|
-
if (!state || state.enabled) return;
|
|
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
|
+
}
|
|
1881
2141
|
if (!(input.autoConfirm || await confirmFlag(clack, state.path))) {
|
|
1882
2142
|
noteManualStep(clack, input.lintModule);
|
|
1883
2143
|
return;
|
|
@@ -1890,6 +2150,22 @@ async function applyTsExtensionsFlag(clack, input) {
|
|
|
1890
2150
|
(0, node_fs.writeFileSync)(state.path, result.source, "utf8");
|
|
1891
2151
|
if (clack.note) clack.note(`Set \`${TS_EXTENSIONS_FLAG}: true\` in ${state.path}.`, "Updated tsconfig.json");
|
|
1892
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
|
+
}
|
|
1893
2169
|
//#endregion
|
|
1894
2170
|
//#region src/cli/migrate/disableDirectives.ts
|
|
1895
2171
|
var DIRECTIVE_HEAD = /^\s*eslint-disable(-next-line|-line)?(?![\w-])/u;
|
|
@@ -2050,31 +2326,63 @@ function rewriteDirectives(source, fileExtension) {
|
|
|
2050
2326
|
function isLintedFile(filePath) {
|
|
2051
2327
|
return LINTED_EXTENSIONS.includes((0, node_path.extname)(filePath));
|
|
2052
2328
|
}
|
|
2053
|
-
function
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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);
|
|
2059
2352
|
return;
|
|
2060
2353
|
}
|
|
2061
|
-
if (isLintedFile(full)) accumulator.push(full);
|
|
2354
|
+
if (entry.isFile() && isLintedFile(full)) accumulator.push(full);
|
|
2062
2355
|
});
|
|
2063
2356
|
}
|
|
2064
2357
|
/**
|
|
2065
|
-
*
|
|
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.
|
|
2066
2361
|
* @example
|
|
2067
|
-
*
|
|
2362
|
+
* previewDisableDirectives('/repo').length; // 131
|
|
2068
2363
|
*/
|
|
2069
|
-
function
|
|
2364
|
+
function previewDisableDirectives(cwd) {
|
|
2070
2365
|
const files = [];
|
|
2071
|
-
collectFiles(cwd, files);
|
|
2072
|
-
return
|
|
2366
|
+
collectFiles(cwd, files, /* @__PURE__ */ new Set());
|
|
2367
|
+
return files.reduce((candidates, filePath) => {
|
|
2073
2368
|
const result = rewriteDirectives((0, node_fs.readFileSync)(filePath, "utf8"), (0, node_path.extname)(filePath));
|
|
2074
|
-
if (!result.changed) return
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
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) };
|
|
2078
2386
|
}
|
|
2079
2387
|
//#endregion
|
|
2080
2388
|
//#region src/cli/migrate/stringEntry.ts
|
|
@@ -4383,53 +4691,56 @@ function formatWarnings(warnings) {
|
|
|
4383
4691
|
}
|
|
4384
4692
|
//#endregion
|
|
4385
4693
|
//#region src/cli/migrate/writer.ts
|
|
4386
|
-
function writeLayout(
|
|
4387
|
-
const
|
|
4388
|
-
const layout = resolveLintLayout(directory, (0, node_path.basename)(eslintConfigPath));
|
|
4694
|
+
function writeLayout(directory, lintModuleSource) {
|
|
4695
|
+
const layout = resolveLintLayout(directory);
|
|
4389
4696
|
const lintModulePath = (0, node_path.join)(directory, layout.lintModule);
|
|
4390
4697
|
(0, node_fs.writeFileSync)(lintModulePath, lintModuleSource, "utf8");
|
|
4391
4698
|
const writtenPaths = [lintModulePath];
|
|
4392
|
-
renderLintLayoutFiles({
|
|
4393
|
-
...layout,
|
|
4394
|
-
eslintConfig: (0, node_path.basename)(eslintConfigPath)
|
|
4395
|
-
}).forEach((file) => {
|
|
4699
|
+
renderLintLayoutFiles(layout).forEach((file) => {
|
|
4396
4700
|
const path = (0, node_path.join)(directory, file.filename);
|
|
4397
4701
|
(0, node_fs.writeFileSync)(path, file.source, "utf8");
|
|
4398
4702
|
writtenPaths.push(path);
|
|
4399
4703
|
});
|
|
4400
4704
|
return {
|
|
4705
|
+
eslintConfigPath: (0, node_path.join)(directory, layout.eslintConfig),
|
|
4401
4706
|
lintModulePath,
|
|
4402
|
-
writtenPaths
|
|
4707
|
+
writtenPaths,
|
|
4708
|
+
replacedEslintConfigPaths: layout.replacedEslintConfigs.map((name) => (0, node_path.join)(directory, name))
|
|
4403
4709
|
};
|
|
4404
4710
|
}
|
|
4711
|
+
function withoutDuplicates(paths, keep) {
|
|
4712
|
+
return [...new Set(paths.filter((path) => path !== keep))];
|
|
4713
|
+
}
|
|
4405
4714
|
/**
|
|
4406
4715
|
* 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.
|
|
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.
|
|
4408
4718
|
* @example
|
|
4409
|
-
* writeMigration('/repo/eslint.config.
|
|
4719
|
+
* writeMigration('/repo/eslint.config.js', source).deletedSourceCandidates; // ['/repo/eslint.config.js']
|
|
4410
4720
|
*/
|
|
4411
4721
|
function writeMigration(targetPath, newSource) {
|
|
4412
|
-
const
|
|
4722
|
+
const result = writeLayout((0, node_path.dirname)(targetPath), newSource);
|
|
4413
4723
|
return {
|
|
4414
|
-
targetPath,
|
|
4415
|
-
lintModulePath,
|
|
4416
|
-
writtenPaths
|
|
4724
|
+
targetPath: result.eslintConfigPath,
|
|
4725
|
+
lintModulePath: result.lintModulePath,
|
|
4726
|
+
writtenPaths: result.writtenPaths,
|
|
4727
|
+
deletedSourceCandidates: withoutDuplicates([...result.replacedEslintConfigPaths, targetPath], result.eslintConfigPath)
|
|
4417
4728
|
};
|
|
4418
4729
|
}
|
|
4419
4730
|
/**
|
|
4420
4731
|
* Same as {@link writeMigration}, but for a legacy `.eslintrc*` source that has to be replaced by a
|
|
4421
|
-
* differently-named flat config.
|
|
4732
|
+
* differently-named flat config.
|
|
4422
4733
|
* @example
|
|
4423
|
-
* writeMigrationLegacy('/repo/.eslintrc.cjs', '/repo/eslint.config.
|
|
4424
|
-
* // '/repo/.eslintrc.cjs'
|
|
4734
|
+
* writeMigrationLegacy('/repo/.eslintrc.cjs', '/repo/eslint.config.mts', src).deletedSourceCandidates;
|
|
4735
|
+
* // ['/repo/.eslintrc.cjs']
|
|
4425
4736
|
*/
|
|
4426
4737
|
function writeMigrationLegacy(sourcePath, writePath, newSource) {
|
|
4427
|
-
const
|
|
4738
|
+
const result = writeLayout((0, node_path.dirname)(writePath), newSource);
|
|
4428
4739
|
return {
|
|
4429
|
-
targetPath:
|
|
4430
|
-
lintModulePath,
|
|
4431
|
-
writtenPaths,
|
|
4432
|
-
|
|
4740
|
+
targetPath: result.eslintConfigPath,
|
|
4741
|
+
lintModulePath: result.lintModulePath,
|
|
4742
|
+
writtenPaths: result.writtenPaths,
|
|
4743
|
+
deletedSourceCandidates: withoutDuplicates([...result.replacedEslintConfigPaths, sourcePath], result.eslintConfigPath)
|
|
4433
4744
|
};
|
|
4434
4745
|
}
|
|
4435
4746
|
//#endregion
|
|
@@ -4504,26 +4815,14 @@ function summary(plan, format) {
|
|
|
4504
4815
|
function planFromSource(source, filePath, format) {
|
|
4505
4816
|
if (format === "flat") {
|
|
4506
4817
|
const parseResult = parseV1Config(source, filePath);
|
|
4507
|
-
|
|
4508
|
-
const plan = transformToV2(parseResult);
|
|
4509
|
-
return {
|
|
4510
|
-
plan,
|
|
4511
|
-
rendered: renderV2(plan)
|
|
4512
|
-
};
|
|
4818
|
+
return isV1Shape(parseResult) ? transformToV2(parseResult) : null;
|
|
4513
4819
|
}
|
|
4514
4820
|
const parseResult = parseEslintrc(source, filePath);
|
|
4515
|
-
|
|
4516
|
-
const plan = transformEslintrcToV2(parseResult);
|
|
4517
|
-
return {
|
|
4518
|
-
plan,
|
|
4519
|
-
rendered: renderV2(plan)
|
|
4520
|
-
};
|
|
4821
|
+
return parseResult.found ? transformEslintrcToV2(parseResult) : null;
|
|
4521
4822
|
}
|
|
4522
|
-
function targetWritePath(cwd, sourcePath
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
const dir = sourcePath.slice(0, sourcePath.length - base.length);
|
|
4526
|
-
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);
|
|
4527
4826
|
}
|
|
4528
4827
|
function parseEslintIgnoreContents(raw) {
|
|
4529
4828
|
return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
@@ -4557,6 +4856,17 @@ function describeDepChanges(depChanges) {
|
|
|
4557
4856
|
lines.push(`- Install (${depChanges.install.specs.length}): ${depChanges.install.specs.join(", ")}`);
|
|
4558
4857
|
return lines.join("\n");
|
|
4559
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
|
+
}
|
|
4560
4870
|
async function confirmWrite(clack, writePath, initial = true) {
|
|
4561
4871
|
const message = `Write migrated config to ${writePath}?`;
|
|
4562
4872
|
const result = await clack.confirm({
|
|
@@ -4600,10 +4910,17 @@ function emitSummary(clack, plan, format) {
|
|
|
4600
4910
|
function writeAndAnnounce(clack, targetPath, writePath, rendered, format) {
|
|
4601
4911
|
const result = format === "eslintrc" ? writeMigrationLegacy(targetPath, writePath, rendered) : writeMigration(targetPath, rendered);
|
|
4602
4912
|
clack.outro(`Wrote ${result.writtenPaths.join(", ")}`);
|
|
4603
|
-
return {
|
|
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)))];
|
|
4604
4920
|
}
|
|
4605
|
-
async function confirmOverwriteExistingOutput(clack,
|
|
4606
|
-
const
|
|
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)}\`?`;
|
|
4607
4924
|
const result = await clack.confirm({
|
|
4608
4925
|
message,
|
|
4609
4926
|
initialValue: initial
|
|
@@ -4639,10 +4956,9 @@ function resolveSource(input, clack) {
|
|
|
4639
4956
|
};
|
|
4640
4957
|
}
|
|
4641
4958
|
function prepareMigration(resolved, input) {
|
|
4642
|
-
const
|
|
4643
|
-
if (!
|
|
4644
|
-
const
|
|
4645
|
-
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);
|
|
4646
4962
|
const eslintIgnore = findEslintIgnore(resolved.targetPath);
|
|
4647
4963
|
if (eslintIgnore && eslintIgnore.patterns.length > 0) mergeIgnorePatterns(plan, eslintIgnore.patterns);
|
|
4648
4964
|
return {
|
|
@@ -4653,8 +4969,9 @@ function prepareMigration(resolved, input) {
|
|
|
4653
4969
|
};
|
|
4654
4970
|
}
|
|
4655
4971
|
async function runConfirmations(clack, input, targetPath, writePath, previous) {
|
|
4656
|
-
|
|
4657
|
-
|
|
4972
|
+
const superseded = supersededFlatConfigs(targetPath, writePath);
|
|
4973
|
+
if (superseded.length > 0) {
|
|
4974
|
+
if (!await confirmOverwriteExistingOutput(clack, superseded, targetPath, previous?.overwriteExisting ?? false)) {
|
|
4658
4975
|
clack.outro("Aborted: existing output kept.");
|
|
4659
4976
|
return false;
|
|
4660
4977
|
}
|
|
@@ -4712,7 +5029,7 @@ function resolveInstallCwd(depSummary, input) {
|
|
|
4712
5029
|
return input.cwd;
|
|
4713
5030
|
}
|
|
4714
5031
|
async function runInstaller(clack, depSummary, input) {
|
|
4715
|
-
|
|
5032
|
+
return installWithRetry({
|
|
4716
5033
|
deps: depSummary.install.specs,
|
|
4717
5034
|
cwd: resolveInstallCwd(depSummary, input),
|
|
4718
5035
|
autoConfirm: Boolean(input.yes),
|
|
@@ -4724,42 +5041,73 @@ async function runInstaller(clack, depSummary, input) {
|
|
|
4724
5041
|
async function applyDepChanges(clack, depSummary, input) {
|
|
4725
5042
|
if (!depSummary.packageJsonPath) {
|
|
4726
5043
|
if (clack.note) clack.note("No package.json found alongside the migrated config — skipping dependency updates.", "Notice");
|
|
4727
|
-
return;
|
|
5044
|
+
return true;
|
|
4728
5045
|
}
|
|
4729
5046
|
persistPrunedManifest(depSummary);
|
|
4730
5047
|
if (input.noInstall) {
|
|
4731
5048
|
emitInstallNextSteps(clack, depSummary, input.cwd);
|
|
4732
|
-
return;
|
|
5049
|
+
return true;
|
|
4733
5050
|
}
|
|
4734
5051
|
if (!input.yes) {
|
|
4735
5052
|
if (!await confirmInstall(clack, depSummary)) {
|
|
4736
5053
|
emitInstallNextSteps(clack, depSummary, input.cwd);
|
|
4737
|
-
return;
|
|
5054
|
+
return true;
|
|
4738
5055
|
}
|
|
4739
5056
|
}
|
|
4740
5057
|
noteFallbacks(clack, depSummary);
|
|
4741
|
-
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;
|
|
4742
5063
|
}
|
|
4743
5064
|
function performWriteAndCleanup(clack, resolved, prepared) {
|
|
4744
5065
|
const outcome = writeAndAnnounce(clack, resolved.targetPath, prepared.writePath, prepared.rendered, resolved.format);
|
|
4745
5066
|
if (prepared.eslintIgnore) safeUnlink(prepared.eslintIgnore.path, clack, ".eslintignore");
|
|
4746
|
-
|
|
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");
|
|
4747
5084
|
}
|
|
4748
5085
|
function applyLintScript$1(clack, directory) {
|
|
4749
5086
|
const manifestPath = (0, node_path.join)(directory, "package.json");
|
|
4750
5087
|
if (!(0, node_fs.existsSync)(manifestPath)) return;
|
|
4751
5088
|
const result = setLintScript((0, node_fs.readFileSync)(manifestPath, "utf8"));
|
|
4752
|
-
if (
|
|
4753
|
-
(
|
|
4754
|
-
if (clack.note) {
|
|
4755
|
-
const previous = result.previous ?? "(none)";
|
|
4756
|
-
clack.note(`lint: ${previous} -> oxlint --disable-nested-config && eslint .`, "Updated package.json script");
|
|
4757
|
-
}
|
|
5089
|
+
if (result.changed) (0, node_fs.writeFileSync)(manifestPath, result.manifest, "utf8");
|
|
5090
|
+
noteLintScriptOutcome$1(clack, result);
|
|
4758
5091
|
}
|
|
4759
|
-
function rewriteDirectivesWithNote(clack, directory) {
|
|
4760
|
-
const
|
|
4761
|
-
if (
|
|
4762
|
-
|
|
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");
|
|
4763
5111
|
}
|
|
4764
5112
|
function loadPreviousMigrate(clack, cwd) {
|
|
4765
5113
|
const restored = loadSession({ cwd });
|
|
@@ -4771,7 +5119,7 @@ function emitNothingToMigrate(clack, format) {
|
|
|
4771
5119
|
clack.outro(format === "eslintrc" ? "No legacy `.eslintrc` shape found — nothing to migrate." : "No v1 useInsider patterns found — nothing to migrate.");
|
|
4772
5120
|
}
|
|
4773
5121
|
function persistMigrateSession(input, prepared, resolved) {
|
|
4774
|
-
const overwriteExisting =
|
|
5122
|
+
const overwriteExisting = supersededFlatConfigs(resolved.targetPath, prepared.writePath).length > 0;
|
|
4775
5123
|
saveSession({
|
|
4776
5124
|
command: "migrate",
|
|
4777
5125
|
migrate: {
|
|
@@ -4783,34 +5131,48 @@ function persistMigrateSession(input, prepared, resolved) {
|
|
|
4783
5131
|
}
|
|
4784
5132
|
}, { cwd: input.cwd });
|
|
4785
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
|
+
}
|
|
4786
5142
|
async function runMigrate(input, clackOverride) {
|
|
4787
5143
|
const clack = clackOverride ?? await loadClack();
|
|
4788
5144
|
const sourceResult = resolveSource(input, clack);
|
|
4789
5145
|
if (!sourceResult.ok) return sourceResult.exitCode;
|
|
4790
5146
|
const { resolved } = sourceResult;
|
|
4791
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
|
+
}
|
|
4792
5153
|
const previousMigrate = loadPreviousMigrate(clack, input.cwd);
|
|
4793
5154
|
const prepared = prepareMigration(resolved, input);
|
|
4794
5155
|
if (!prepared) {
|
|
4795
5156
|
emitNothingToMigrate(clack, resolved.format);
|
|
4796
|
-
return
|
|
5157
|
+
return declinedExitCode;
|
|
4797
5158
|
}
|
|
4798
5159
|
emitSummary(clack, prepared.plan, resolved.format);
|
|
5160
|
+
noteConfigRename(clack, resolved.targetPath, prepared.writePath);
|
|
4799
5161
|
const depSummary = computeDepSummary(prepared.plan, prepared.writePath, input.overrides);
|
|
4800
|
-
if (!await runConfirmations(clack, input, resolved.targetPath, prepared.writePath, previousMigrate)) return
|
|
5162
|
+
if (!await runConfirmations(clack, input, resolved.targetPath, prepared.writePath, previousMigrate)) return declinedExitCode;
|
|
4801
5163
|
persistMigrateSession(input, prepared, resolved);
|
|
4802
5164
|
performWriteAndCleanup(clack, resolved, prepared);
|
|
4803
5165
|
applyLintScript$1(clack, (0, node_path.dirname)(prepared.writePath));
|
|
4804
|
-
rewriteDirectivesWithNote(clack, (0, node_path.dirname)(prepared.writePath));
|
|
5166
|
+
await rewriteDirectivesWithNote(clack, (0, node_path.dirname)(prepared.writePath), Boolean(input.yes));
|
|
4805
5167
|
const consumerDir = (0, node_path.dirname)(prepared.writePath);
|
|
5168
|
+
const consumerLayout = resolveLintLayout(consumerDir);
|
|
4806
5169
|
await applyTsExtensionsFlag(clack, {
|
|
4807
5170
|
cwd: consumerDir,
|
|
4808
|
-
lintModule:
|
|
5171
|
+
lintModule: consumerLayout.lintModule,
|
|
5172
|
+
eslintConfig: consumerLayout.eslintConfig,
|
|
4809
5173
|
autoConfirm: input.yes
|
|
4810
5174
|
});
|
|
4811
|
-
|
|
4812
|
-
clearSession({ cwd: input.cwd });
|
|
4813
|
-
return 0;
|
|
5175
|
+
return finishMigrate(clack, depSummary, input);
|
|
4814
5176
|
}
|
|
4815
5177
|
//#endregion
|
|
4816
5178
|
//#region src/cli/index.ts
|
|
@@ -4977,9 +5339,19 @@ async function resolveHybridAnswers(clack, input, answers) {
|
|
|
4977
5339
|
}
|
|
4978
5340
|
function applyLintScript(cwd) {
|
|
4979
5341
|
const manifestPath = (0, node_path.join)(cwd, "package.json");
|
|
4980
|
-
if (!(0, node_fs.existsSync)(manifestPath)) return;
|
|
5342
|
+
if (!(0, node_fs.existsSync)(manifestPath)) return null;
|
|
4981
5343
|
const result = setLintScript((0, node_fs.readFileSync)(manifestPath, "utf8"));
|
|
4982
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");
|
|
4983
5355
|
}
|
|
4984
5356
|
function writeLintLayout(cwd, lintModuleSource) {
|
|
4985
5357
|
const layout = resolveLintLayout(cwd);
|
|
@@ -4988,21 +5360,26 @@ function writeLintLayout(cwd, lintModuleSource) {
|
|
|
4988
5360
|
renderLintLayoutFiles(layout).forEach((file) => {
|
|
4989
5361
|
(0, node_fs.writeFileSync)((0, node_path.join)(cwd, file.filename), file.source, "utf8");
|
|
4990
5362
|
});
|
|
4991
|
-
|
|
4992
|
-
|
|
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
|
+
};
|
|
4993
5370
|
}
|
|
4994
5371
|
async function installAndWrite(clack, input, answers, options) {
|
|
4995
5372
|
if (options.pruneBeforeInstall) noteOverridePrune(clack, pruneConsumerPackageJson(input.cwd).removed);
|
|
4996
5373
|
const deps = buildDependencyList(answers);
|
|
4997
5374
|
if (clack.note) clack.note(deps.map((dep) => ` - ${dep}`).join("\n"), "Installing dev dependencies");
|
|
4998
|
-
await installWithRetry({
|
|
5375
|
+
if (!(await installWithRetry({
|
|
4999
5376
|
deps,
|
|
5000
5377
|
cwd: input.cwd,
|
|
5001
5378
|
autoConfirm: Boolean(input.yes),
|
|
5002
5379
|
clack,
|
|
5003
5380
|
installer: input.overrides?.installer ?? installDependencies,
|
|
5004
5381
|
fsOps: input.overrides?.wipeFs
|
|
5005
|
-
});
|
|
5382
|
+
})).installed) return null;
|
|
5006
5383
|
return writeLintLayout(input.cwd, renderLintModuleFile(answers));
|
|
5007
5384
|
}
|
|
5008
5385
|
async function runScaffold(input, clackOverride) {
|
|
@@ -5024,14 +5401,21 @@ async function runScaffold(input, clackOverride) {
|
|
|
5024
5401
|
existingConfigChoice: "override"
|
|
5025
5402
|
}
|
|
5026
5403
|
}, { cwd: input.cwd });
|
|
5027
|
-
const
|
|
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);
|
|
5028
5411
|
await applyTsExtensionsFlag(clack, {
|
|
5029
5412
|
cwd: input.cwd,
|
|
5030
|
-
lintModule:
|
|
5413
|
+
lintModule: layout.lintModule,
|
|
5414
|
+
eslintConfig: layout.eslintConfig,
|
|
5031
5415
|
autoConfirm: input.yes
|
|
5032
5416
|
});
|
|
5033
5417
|
clearSession({ cwd: input.cwd });
|
|
5034
|
-
clack.outro(`Wrote ${
|
|
5418
|
+
clack.outro(`Wrote ${written.lintModulePath} and its sibling linter configs`);
|
|
5035
5419
|
return 0;
|
|
5036
5420
|
}
|
|
5037
5421
|
async function runScaffoldSubcommand(scaffoldInput) {
|
|
@@ -5042,7 +5426,8 @@ async function runMigrateSubcommand(input) {
|
|
|
5042
5426
|
cwd: input.cwd,
|
|
5043
5427
|
target: input.target,
|
|
5044
5428
|
yes: input.yes,
|
|
5045
|
-
noInstall: input.noInstall
|
|
5429
|
+
noInstall: input.noInstall,
|
|
5430
|
+
fromLint: input.fromLint
|
|
5046
5431
|
});
|
|
5047
5432
|
}
|
|
5048
5433
|
function writeStderr(line) {
|