@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/README.md +122 -17
- package/dist/builders/scopeBlocks.d.ts +6 -0
- package/dist/cli.cjs +949 -223
- package/dist/cli.cjs.map +1 -1
- package/dist/main.cjs +395 -84
- package/dist/main.cjs.map +1 -1
- package/dist/main.js +396 -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,30 @@ 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
|
|
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
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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.
|
|
741
|
-
*
|
|
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');
|
|
804
|
+
* resolveLintLayout('/repo');
|
|
805
|
+
* // { eslintConfig: 'eslint.config.ts', replacedEslintConfigs: ['eslint.config.js'], ... }
|
|
744
806
|
*/
|
|
745
|
-
function resolveLintLayout(cwd
|
|
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
|
|
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.
|
|
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
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
*
|
|
806
|
-
*
|
|
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 ."}}').
|
|
809
|
-
* // '
|
|
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
|
|
822
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
|
1015
|
+
manifest: `${JSON.stringify(parsed, null, detectIndent(rawManifest, DEFAULT_MANIFEST_INDENT))}\n`,
|
|
833
1016
|
changed: true,
|
|
834
|
-
|
|
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
|
-
|
|
1756
|
-
|
|
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
|
|
1759
|
-
|
|
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
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
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)) ||
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
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
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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
|
-
|
|
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
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
3545
|
-
const
|
|
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
|
|
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(
|
|
4387
|
-
const
|
|
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.
|
|
4984
|
+
* writeMigration('/repo/eslint.config.js', source).deletedSourceCandidates; // ['/repo/eslint.config.js']
|
|
4410
4985
|
*/
|
|
4411
4986
|
function writeMigration(targetPath, newSource) {
|
|
4412
|
-
const
|
|
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.
|
|
4997
|
+
* differently-named flat config.
|
|
4422
4998
|
* @example
|
|
4423
|
-
* writeMigrationLegacy('/repo/.eslintrc.cjs', '/repo/eslint.config.
|
|
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
|
|
5003
|
+
const result = writeLayout((0, node_path.dirname)(writePath), newSource);
|
|
4428
5004
|
return {
|
|
4429
|
-
targetPath:
|
|
4430
|
-
lintModulePath,
|
|
4431
|
-
writtenPaths,
|
|
4432
|
-
|
|
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
|
|
4505
|
-
if (format
|
|
4506
|
-
const parseResult =
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
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 =
|
|
4515
|
-
if (
|
|
4516
|
-
|
|
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
|
-
|
|
4519
|
-
|
|
5127
|
+
ok: false,
|
|
5128
|
+
reason: outcome.kind === "refused" ? outcome.reason : null
|
|
4520
5129
|
};
|
|
4521
5130
|
}
|
|
4522
|
-
function
|
|
4523
|
-
if (
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
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,
|
|
4597
|
-
if (clack.note)
|
|
4598
|
-
|
|
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 {
|
|
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,
|
|
4606
|
-
const
|
|
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
|
|
4643
|
-
if (!
|
|
4644
|
-
const
|
|
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
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
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
|
-
|
|
4657
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
4753
|
-
(
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
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
|
-
|
|
4760
|
-
|
|
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 =
|
|
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
|
|
4794
|
-
if (!
|
|
4795
|
-
|
|
4796
|
-
|
|
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
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
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:
|
|
5512
|
+
lintModule: consumerLayout.lintModule,
|
|
5513
|
+
eslintConfig: consumerLayout.eslintConfig,
|
|
4809
5514
|
autoConfirm: input.yes
|
|
4810
5515
|
});
|
|
4811
|
-
|
|
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
|
-
|
|
4992
|
-
|
|
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
|
|
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:
|
|
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 ${
|
|
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) {
|