@eslint-config-snapshot/cli 1.0.0 → 1.1.0
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/CHANGELOG.md +11 -0
- package/dist/index.cjs +173 -31
- package/dist/index.js +173 -31
- package/package.json +2 -2
- package/src/commands/check.ts +45 -6
- package/src/commands/skipped-workspaces.ts +66 -0
- package/src/commands/update.ts +40 -7
- package/src/formatters.ts +7 -0
- package/src/runtime.ts +61 -6
- package/test/cli.integration.test.ts +38 -1
- package/test/cli.terminal.integration.test.ts +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @eslint-config-snapshot/cli
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Minor release: improve skipped workspace messaging and OSS compatibility documentation.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- @eslint-config-snapshot/api@1.1.0
|
|
13
|
+
|
|
3
14
|
## 1.0.0
|
|
4
15
|
|
|
5
16
|
### Major Changes
|
package/dist/index.cjs
CHANGED
|
@@ -38,7 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
module.exports = __toCommonJS(index_exports);
|
|
39
39
|
var import_commander = require("commander");
|
|
40
40
|
var import_debug2 = __toESM(require("debug"), 1);
|
|
41
|
-
var
|
|
41
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
42
42
|
|
|
43
43
|
// src/commands/check.ts
|
|
44
44
|
var import_api3 = require("@eslint-config-snapshot/api");
|
|
@@ -216,6 +216,12 @@ function formatStoredSnapshotSummary(storedSnapshots) {
|
|
|
216
216
|
const summary = summarizeSnapshots(storedSnapshots);
|
|
217
217
|
return `${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off)`;
|
|
218
218
|
}
|
|
219
|
+
function formatBaselineSummaryLines(summary, workspaceCount) {
|
|
220
|
+
return `- \u{1F4E6} baseline: ${summary.groups} groups, ${summary.rules} rules
|
|
221
|
+
- \u{1F5C2}\uFE0F workspaces scanned: ${workspaceCount}
|
|
222
|
+
- \u{1F39A}\uFE0F severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
219
225
|
function countRuleSeverities(ruleObjects) {
|
|
220
226
|
let rules = 0;
|
|
221
227
|
let error = 0;
|
|
@@ -395,19 +401,24 @@ var import_node_path2 = __toESM(require("path"), 1);
|
|
|
395
401
|
var debugWorkspace = (0, import_debug.default)("eslint-config-snapshot:workspace");
|
|
396
402
|
var debugDiff = (0, import_debug.default)("eslint-config-snapshot:diff");
|
|
397
403
|
var debugTiming = (0, import_debug.default)("eslint-config-snapshot:timing");
|
|
398
|
-
async function computeCurrentSnapshots(cwd) {
|
|
404
|
+
async function computeCurrentSnapshots(cwd, options) {
|
|
405
|
+
const allowWorkspaceExtractionFailure = options?.allowWorkspaceExtractionFailure ?? false;
|
|
406
|
+
const onWorkspacesDiscovered = options?.onWorkspacesDiscovered;
|
|
407
|
+
const onWorkspaceSkipped = options?.onWorkspaceSkipped;
|
|
399
408
|
const computeStartedAt = Date.now();
|
|
400
409
|
const configStartedAt = Date.now();
|
|
401
410
|
const config = await (0, import_api2.loadConfig)(cwd);
|
|
402
411
|
debugTiming("phase=loadConfig elapsedMs=%d", Date.now() - configStartedAt);
|
|
403
412
|
const assignmentStartedAt = Date.now();
|
|
404
413
|
const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
|
|
414
|
+
onWorkspacesDiscovered?.(discovery.workspacesRel);
|
|
405
415
|
debugTiming("phase=resolveWorkspaceAssignments elapsedMs=%d", Date.now() - assignmentStartedAt);
|
|
406
416
|
debugWorkspace("root=%s groups=%d workspaces=%d", discovery.rootAbs, assignments.length, discovery.workspacesRel.length);
|
|
407
417
|
const snapshots = /* @__PURE__ */ new Map();
|
|
408
418
|
for (const group of assignments) {
|
|
409
419
|
const groupStartedAt = Date.now();
|
|
410
420
|
const extractedForGroup = [];
|
|
421
|
+
const extractedWorkspaces = [];
|
|
411
422
|
debugWorkspace("group=%s workspaces=%o", group.name, group.workspaces);
|
|
412
423
|
for (const workspaceRel of group.workspaces) {
|
|
413
424
|
const workspaceAbs = import_node_path2.default.resolve(discovery.rootAbs, workspaceRel);
|
|
@@ -440,7 +451,7 @@ async function computeCurrentSnapshots(cwd) {
|
|
|
440
451
|
continue;
|
|
441
452
|
}
|
|
442
453
|
const message = result.error instanceof Error ? result.error.message : String(result.error);
|
|
443
|
-
if (isRecoverableExtractionError(message)) {
|
|
454
|
+
if (isRecoverableExtractionError(message) || allowWorkspaceExtractionFailure) {
|
|
444
455
|
lastExtractionError = message;
|
|
445
456
|
continue;
|
|
446
457
|
}
|
|
@@ -448,10 +459,23 @@ async function computeCurrentSnapshots(cwd) {
|
|
|
448
459
|
}
|
|
449
460
|
if (extractedCount === 0) {
|
|
450
461
|
const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : "";
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
462
|
+
if (allowWorkspaceExtractionFailure && isSkippableWorkspaceExtractionFailure(lastExtractionError)) {
|
|
463
|
+
onWorkspaceSkipped?.({
|
|
464
|
+
groupId: group.name,
|
|
465
|
+
workspaceRel,
|
|
466
|
+
reason: lastExtractionError ?? "unknown extraction failure"
|
|
467
|
+
});
|
|
468
|
+
debugWorkspace(
|
|
469
|
+
"group=%s workspace=%s skipped=true reason=%s",
|
|
470
|
+
group.name,
|
|
471
|
+
workspaceRel,
|
|
472
|
+
lastExtractionError ?? "unknown extraction failure"
|
|
473
|
+
);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
throw new Error(`Unable to extract ESLint config for workspace ${workspaceRel}.${context}`);
|
|
454
477
|
}
|
|
478
|
+
extractedWorkspaces.push(workspaceRel);
|
|
455
479
|
debugWorkspace(
|
|
456
480
|
"group=%s workspace=%s extracted=%d failed=%d",
|
|
457
481
|
group.name,
|
|
@@ -460,8 +484,15 @@ async function computeCurrentSnapshots(cwd) {
|
|
|
460
484
|
results.length - extractedCount
|
|
461
485
|
);
|
|
462
486
|
}
|
|
487
|
+
if (extractedForGroup.length === 0) {
|
|
488
|
+
if (allowWorkspaceExtractionFailure) {
|
|
489
|
+
debugWorkspace("group=%s skipped=true reason=no-extracted-workspaces", group.name);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
throw new Error(`Unable to extract ESLint config for group ${group.name}: no workspace produced a valid config`);
|
|
493
|
+
}
|
|
463
494
|
const aggregated = (0, import_api2.aggregateRules)(extractedForGroup);
|
|
464
|
-
snapshots.set(group.name, (0, import_api2.buildSnapshot)(group.name,
|
|
495
|
+
snapshots.set(group.name, (0, import_api2.buildSnapshot)(group.name, extractedWorkspaces, aggregated));
|
|
465
496
|
debugWorkspace(
|
|
466
497
|
"group=%s aggregatedRules=%d groupElapsedMs=%d",
|
|
467
498
|
group.name,
|
|
@@ -470,11 +501,20 @@ async function computeCurrentSnapshots(cwd) {
|
|
|
470
501
|
);
|
|
471
502
|
}
|
|
472
503
|
debugTiming("phase=computeCurrentSnapshots elapsedMs=%d", Date.now() - computeStartedAt);
|
|
504
|
+
if (snapshots.size === 0) {
|
|
505
|
+
throw new Error("Unable to extract ESLint config from discovered workspaces in zero-config mode");
|
|
506
|
+
}
|
|
473
507
|
return snapshots;
|
|
474
508
|
}
|
|
475
509
|
function isRecoverableExtractionError(message) {
|
|
476
510
|
return message.startsWith("Invalid JSON from eslint --print-config") || message.startsWith("Empty ESLint print-config output") || message.includes("File ignored because of a matching ignore pattern") || message.includes("File ignored by default");
|
|
477
511
|
}
|
|
512
|
+
function isSkippableWorkspaceExtractionFailure(message) {
|
|
513
|
+
if (!message) {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
return isRecoverableExtractionError(message) || message.startsWith("Failed to load config") || message.startsWith("Failed to run eslint --print-config") || message.startsWith("Unable to resolve eslint from workspace");
|
|
517
|
+
}
|
|
478
518
|
async function resolveWorkspaceAssignments(cwd, config) {
|
|
479
519
|
const discovery = await (0, import_api2.discoverWorkspaces)({ cwd, workspaceInput: config.workspaceInput });
|
|
480
520
|
const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : (0, import_api2.assignGroupsByMatch)(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
|
|
@@ -544,6 +584,63 @@ async function resolveGroupEslintVersions(cwd) {
|
|
|
544
584
|
return result;
|
|
545
585
|
}
|
|
546
586
|
|
|
587
|
+
// src/commands/skipped-workspaces.ts
|
|
588
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
589
|
+
function writeSkippedWorkspaceSummary(terminal, cwd, configPath, skippedWorkspaces) {
|
|
590
|
+
if (skippedWorkspaces.length === 0) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const workspacePaths = collectSkippedWorkspacePaths(skippedWorkspaces);
|
|
594
|
+
terminal.warning(
|
|
595
|
+
`Heads up: ${workspacePaths.length} workspace(s) were skipped because ESLint auto-discovery could not extract an effective config for them.
|
|
596
|
+
`
|
|
597
|
+
);
|
|
598
|
+
terminal.warning(`Skipped workspaces: ${workspacePaths.join(", ")}
|
|
599
|
+
`);
|
|
600
|
+
terminal.subtle(formatScopedConfigHint(cwd, configPath, workspacePaths));
|
|
601
|
+
}
|
|
602
|
+
function collectSkippedWorkspacePaths(skippedWorkspaces) {
|
|
603
|
+
const unique = /* @__PURE__ */ new Set();
|
|
604
|
+
for (const skipped of skippedWorkspaces) {
|
|
605
|
+
unique.add(skipped.workspaceRel);
|
|
606
|
+
}
|
|
607
|
+
return [...unique].sort();
|
|
608
|
+
}
|
|
609
|
+
function toExcludeGlobs(workspacePaths) {
|
|
610
|
+
return workspacePaths.map((workspacePath) => `${workspacePath}/**`);
|
|
611
|
+
}
|
|
612
|
+
function formatScopedConfigHint(cwd, configPath, workspacePaths) {
|
|
613
|
+
const excludeGlobs = toExcludeGlobs(workspacePaths);
|
|
614
|
+
if (configPath && import_node_path3.default.basename(configPath) === "package.json") {
|
|
615
|
+
return `Tip: if these workspaces are intentionally out of scope, add this under "eslint-config-snapshot" in package.json:
|
|
616
|
+
${JSON.stringify(
|
|
617
|
+
{
|
|
618
|
+
sampling: {
|
|
619
|
+
excludeGlobs
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
null,
|
|
623
|
+
2
|
|
624
|
+
)}
|
|
625
|
+
`;
|
|
626
|
+
}
|
|
627
|
+
const objectLiteral = `{
|
|
628
|
+
sampling: {
|
|
629
|
+
excludeGlobs: [
|
|
630
|
+
${excludeGlobs.map((value) => ` '${value}'`).join(",\n")}
|
|
631
|
+
]
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
`;
|
|
635
|
+
if (configPath) {
|
|
636
|
+
const relConfigPath = import_node_path3.default.relative(cwd, configPath) || import_node_path3.default.basename(configPath);
|
|
637
|
+
return `Tip: if these workspaces are intentionally out of scope, add this in ${relConfigPath}:
|
|
638
|
+
${objectLiteral}`;
|
|
639
|
+
}
|
|
640
|
+
return `Tip: if these workspaces are intentionally out of scope, run \`eslint-config-snapshot init\` and add this config:
|
|
641
|
+
${objectLiteral}`;
|
|
642
|
+
}
|
|
643
|
+
|
|
547
644
|
// src/commands/check.ts
|
|
548
645
|
var UPDATE_HINT = "Tip: when you intentionally accept changes, run `eslint-config-snapshot --update` to refresh the baseline.\n";
|
|
549
646
|
async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocation = false) {
|
|
@@ -561,10 +658,20 @@ async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocatio
|
|
|
561
658
|
);
|
|
562
659
|
}
|
|
563
660
|
let currentSnapshots;
|
|
661
|
+
const skippedWorkspaces = [];
|
|
662
|
+
let discoveredWorkspaces = [];
|
|
564
663
|
try {
|
|
565
|
-
currentSnapshots = await computeCurrentSnapshots(cwd
|
|
664
|
+
currentSnapshots = await computeCurrentSnapshots(cwd, {
|
|
665
|
+
allowWorkspaceExtractionFailure: !foundConfig,
|
|
666
|
+
onWorkspacesDiscovered: (workspacesRel) => {
|
|
667
|
+
discoveredWorkspaces = workspacesRel;
|
|
668
|
+
},
|
|
669
|
+
onWorkspaceSkipped: (skipped) => {
|
|
670
|
+
skippedWorkspaces.push(skipped);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
566
673
|
} catch (error) {
|
|
567
|
-
if (!foundConfig) {
|
|
674
|
+
if (!foundConfig && isWorkspaceDiscoveryDefaultsError(error)) {
|
|
568
675
|
terminal.write(
|
|
569
676
|
"Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
|
|
570
677
|
);
|
|
@@ -572,6 +679,10 @@ async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocatio
|
|
|
572
679
|
}
|
|
573
680
|
throw error;
|
|
574
681
|
}
|
|
682
|
+
if (!foundConfig) {
|
|
683
|
+
writeDiscoveredWorkspacesSummary(terminal, discoveredWorkspaces);
|
|
684
|
+
}
|
|
685
|
+
writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces);
|
|
575
686
|
if (storedSnapshots.size === 0) {
|
|
576
687
|
const summary = summarizeSnapshots(currentSnapshots);
|
|
577
688
|
terminal.write(
|
|
@@ -631,12 +742,7 @@ function printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByG
|
|
|
631
742
|
if (changes.length === 0) {
|
|
632
743
|
terminal.write(color.green("\u2705 Great news: no snapshot drift detected.\n"));
|
|
633
744
|
terminal.section("\u{1F4CA} Summary");
|
|
634
|
-
terminal.write(
|
|
635
|
-
`- \u{1F4E6} baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules
|
|
636
|
-
- \u{1F5C2}\uFE0F workspaces scanned: ${workspaceCount}
|
|
637
|
-
- \u{1F39A}\uFE0F severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off
|
|
638
|
-
`
|
|
639
|
-
);
|
|
745
|
+
terminal.write(formatBaselineSummaryLines(currentSummary, workspaceCount));
|
|
640
746
|
writeEslintVersionSummary(terminal, eslintVersionsByGroup);
|
|
641
747
|
return 0;
|
|
642
748
|
}
|
|
@@ -671,6 +777,18 @@ function printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByG
|
|
|
671
777
|
terminal.subtle(UPDATE_HINT);
|
|
672
778
|
return 1;
|
|
673
779
|
}
|
|
780
|
+
function isWorkspaceDiscoveryDefaultsError(error) {
|
|
781
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
782
|
+
return message.includes("Unable to discover workspaces") || message.includes("Unmatched workspaces") || message.includes("zero-config mode");
|
|
783
|
+
}
|
|
784
|
+
function writeDiscoveredWorkspacesSummary(terminal, workspacesRel) {
|
|
785
|
+
if (workspacesRel.length === 0) {
|
|
786
|
+
terminal.subtle("Auto-discovered workspaces: none\n");
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(", ")}
|
|
790
|
+
`);
|
|
791
|
+
}
|
|
674
792
|
|
|
675
793
|
// src/commands/print.ts
|
|
676
794
|
var import_api4 = require("@eslint-config-snapshot/api");
|
|
@@ -736,10 +854,20 @@ async function executeUpdate(cwd, terminal, snapshotDir, printSummary) {
|
|
|
736
854
|
);
|
|
737
855
|
}
|
|
738
856
|
let currentSnapshots;
|
|
857
|
+
const skippedWorkspaces = [];
|
|
858
|
+
let discoveredWorkspaces = [];
|
|
739
859
|
try {
|
|
740
|
-
currentSnapshots = await computeCurrentSnapshots(cwd
|
|
860
|
+
currentSnapshots = await computeCurrentSnapshots(cwd, {
|
|
861
|
+
allowWorkspaceExtractionFailure: !foundConfig,
|
|
862
|
+
onWorkspacesDiscovered: (workspacesRel) => {
|
|
863
|
+
discoveredWorkspaces = workspacesRel;
|
|
864
|
+
},
|
|
865
|
+
onWorkspaceSkipped: (skipped) => {
|
|
866
|
+
skippedWorkspaces.push(skipped);
|
|
867
|
+
}
|
|
868
|
+
});
|
|
741
869
|
} catch (error) {
|
|
742
|
-
if (!foundConfig) {
|
|
870
|
+
if (!foundConfig && isWorkspaceDiscoveryDefaultsError2(error)) {
|
|
743
871
|
terminal.write(
|
|
744
872
|
"Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
|
|
745
873
|
);
|
|
@@ -747,28 +875,42 @@ async function executeUpdate(cwd, terminal, snapshotDir, printSummary) {
|
|
|
747
875
|
}
|
|
748
876
|
throw error;
|
|
749
877
|
}
|
|
878
|
+
if (!foundConfig) {
|
|
879
|
+
writeDiscoveredWorkspacesSummary2(terminal, discoveredWorkspaces);
|
|
880
|
+
}
|
|
881
|
+
writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces);
|
|
750
882
|
await writeSnapshots(cwd, snapshotDir, currentSnapshots);
|
|
751
883
|
if (printSummary) {
|
|
752
884
|
const summary = summarizeSnapshots(currentSnapshots);
|
|
753
885
|
const workspaceCount = countUniqueWorkspaces(currentSnapshots);
|
|
754
886
|
const eslintVersionsByGroup = terminal.showProgress ? await resolveGroupEslintVersions(cwd) : /* @__PURE__ */ new Map();
|
|
887
|
+
const baselineAction = storedSnapshots.size === 0 ? "created" : "updated";
|
|
888
|
+
terminal.success(`\u2705 Great news: baseline was successfully ${baselineAction} for your project.
|
|
889
|
+
`);
|
|
755
890
|
terminal.section("\u{1F4CA} Summary");
|
|
756
|
-
terminal.write(
|
|
757
|
-
`Baseline updated: ${summary.groups} groups, ${summary.rules} rules.
|
|
758
|
-
Workspaces scanned: ${workspaceCount}.
|
|
759
|
-
Severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.
|
|
760
|
-
`
|
|
761
|
-
);
|
|
891
|
+
terminal.write(formatBaselineSummaryLines(summary, workspaceCount));
|
|
762
892
|
writeEslintVersionSummary(terminal, eslintVersionsByGroup);
|
|
763
893
|
}
|
|
764
894
|
return 0;
|
|
765
895
|
}
|
|
896
|
+
function isWorkspaceDiscoveryDefaultsError2(error) {
|
|
897
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
898
|
+
return message.includes("Unable to discover workspaces") || message.includes("Unmatched workspaces") || message.includes("zero-config mode");
|
|
899
|
+
}
|
|
900
|
+
function writeDiscoveredWorkspacesSummary2(terminal, workspacesRel) {
|
|
901
|
+
if (workspacesRel.length === 0) {
|
|
902
|
+
terminal.subtle("Auto-discovered workspaces: none\n");
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(", ")}
|
|
906
|
+
`);
|
|
907
|
+
}
|
|
766
908
|
|
|
767
909
|
// src/init.ts
|
|
768
910
|
var import_api6 = require("@eslint-config-snapshot/api");
|
|
769
911
|
var import_fast_glob2 = __toESM(require("fast-glob"), 1);
|
|
770
912
|
var import_promises2 = require("fs/promises");
|
|
771
|
-
var
|
|
913
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
772
914
|
async function runInit(cwd, opts, runtime) {
|
|
773
915
|
const force = opts.force ?? false;
|
|
774
916
|
const showEffective = opts.showEffective ?? false;
|
|
@@ -836,7 +978,7 @@ async function runInitInFile(cwd, configObject, force, runtime) {
|
|
|
836
978
|
];
|
|
837
979
|
for (const candidate of candidates) {
|
|
838
980
|
try {
|
|
839
|
-
await (0, import_promises2.access)(
|
|
981
|
+
await (0, import_promises2.access)(import_node_path4.default.join(cwd, candidate));
|
|
840
982
|
if (!force) {
|
|
841
983
|
runtime.writeStderr(`Config already exists: ${candidate}
|
|
842
984
|
`);
|
|
@@ -845,14 +987,14 @@ async function runInitInFile(cwd, configObject, force, runtime) {
|
|
|
845
987
|
} catch {
|
|
846
988
|
}
|
|
847
989
|
}
|
|
848
|
-
const target =
|
|
990
|
+
const target = import_node_path4.default.join(cwd, "eslint-config-snapshot.config.mjs");
|
|
849
991
|
await (0, import_promises2.writeFile)(target, toConfigScaffold(configObject), "utf8");
|
|
850
|
-
runtime.writeStdout(`Created ${
|
|
992
|
+
runtime.writeStdout(`Created ${import_node_path4.default.basename(target)}
|
|
851
993
|
`);
|
|
852
994
|
return 0;
|
|
853
995
|
}
|
|
854
996
|
async function runInitInPackageJson(cwd, configObject, force, runtime) {
|
|
855
|
-
const packageJsonPath =
|
|
997
|
+
const packageJsonPath = import_node_path4.default.join(cwd, "package.json");
|
|
856
998
|
let packageJsonRaw;
|
|
857
999
|
try {
|
|
858
1000
|
packageJsonRaw = await (0, import_promises2.readFile)(packageJsonPath, "utf8");
|
|
@@ -913,7 +1055,7 @@ async function discoverInitWorkspaces(cwd) {
|
|
|
913
1055
|
if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === ".")) {
|
|
914
1056
|
return discovered.workspacesRel;
|
|
915
1057
|
}
|
|
916
|
-
const packageJsonPath =
|
|
1058
|
+
const packageJsonPath = import_node_path4.default.join(cwd, "package.json");
|
|
917
1059
|
try {
|
|
918
1060
|
const raw = await (0, import_promises2.readFile)(packageJsonPath, "utf8");
|
|
919
1061
|
const parsed = JSON.parse(raw);
|
|
@@ -931,7 +1073,7 @@ async function discoverInitWorkspaces(cwd) {
|
|
|
931
1073
|
onlyFiles: true,
|
|
932
1074
|
dot: true
|
|
933
1075
|
});
|
|
934
|
-
const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => (0, import_api6.normalizePath)(
|
|
1076
|
+
const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => (0, import_api6.normalizePath)(import_node_path4.default.dirname(entry))))].sort(
|
|
935
1077
|
(a, b) => a.localeCompare(b)
|
|
936
1078
|
);
|
|
937
1079
|
if (workspaceDirs.length > 0) {
|
|
@@ -1342,7 +1484,7 @@ function isDirectCliExecution() {
|
|
|
1342
1484
|
if (!entry) {
|
|
1343
1485
|
return false;
|
|
1344
1486
|
}
|
|
1345
|
-
const normalized =
|
|
1487
|
+
const normalized = import_node_path5.default.basename(entry).toLowerCase();
|
|
1346
1488
|
return normalized === "index.js" || normalized === "index.cjs" || normalized === "index.ts" || normalized === "eslint-config-snapshot";
|
|
1347
1489
|
}
|
|
1348
1490
|
if (isDirectCliExecution()) {
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
5
5
|
import createDebug2 from "debug";
|
|
6
|
-
import
|
|
6
|
+
import path5 from "path";
|
|
7
7
|
|
|
8
8
|
// src/commands/check.ts
|
|
9
9
|
import { findConfigPath } from "@eslint-config-snapshot/api";
|
|
@@ -181,6 +181,12 @@ function formatStoredSnapshotSummary(storedSnapshots) {
|
|
|
181
181
|
const summary = summarizeSnapshots(storedSnapshots);
|
|
182
182
|
return `${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off)`;
|
|
183
183
|
}
|
|
184
|
+
function formatBaselineSummaryLines(summary, workspaceCount) {
|
|
185
|
+
return `- \u{1F4E6} baseline: ${summary.groups} groups, ${summary.rules} rules
|
|
186
|
+
- \u{1F5C2}\uFE0F workspaces scanned: ${workspaceCount}
|
|
187
|
+
- \u{1F39A}\uFE0F severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
184
190
|
function countRuleSeverities(ruleObjects) {
|
|
185
191
|
let rules = 0;
|
|
186
192
|
let error = 0;
|
|
@@ -373,19 +379,24 @@ import path2 from "path";
|
|
|
373
379
|
var debugWorkspace = createDebug("eslint-config-snapshot:workspace");
|
|
374
380
|
var debugDiff = createDebug("eslint-config-snapshot:diff");
|
|
375
381
|
var debugTiming = createDebug("eslint-config-snapshot:timing");
|
|
376
|
-
async function computeCurrentSnapshots(cwd) {
|
|
382
|
+
async function computeCurrentSnapshots(cwd, options) {
|
|
383
|
+
const allowWorkspaceExtractionFailure = options?.allowWorkspaceExtractionFailure ?? false;
|
|
384
|
+
const onWorkspacesDiscovered = options?.onWorkspacesDiscovered;
|
|
385
|
+
const onWorkspaceSkipped = options?.onWorkspaceSkipped;
|
|
377
386
|
const computeStartedAt = Date.now();
|
|
378
387
|
const configStartedAt = Date.now();
|
|
379
388
|
const config = await loadConfig(cwd);
|
|
380
389
|
debugTiming("phase=loadConfig elapsedMs=%d", Date.now() - configStartedAt);
|
|
381
390
|
const assignmentStartedAt = Date.now();
|
|
382
391
|
const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
|
|
392
|
+
onWorkspacesDiscovered?.(discovery.workspacesRel);
|
|
383
393
|
debugTiming("phase=resolveWorkspaceAssignments elapsedMs=%d", Date.now() - assignmentStartedAt);
|
|
384
394
|
debugWorkspace("root=%s groups=%d workspaces=%d", discovery.rootAbs, assignments.length, discovery.workspacesRel.length);
|
|
385
395
|
const snapshots = /* @__PURE__ */ new Map();
|
|
386
396
|
for (const group of assignments) {
|
|
387
397
|
const groupStartedAt = Date.now();
|
|
388
398
|
const extractedForGroup = [];
|
|
399
|
+
const extractedWorkspaces = [];
|
|
389
400
|
debugWorkspace("group=%s workspaces=%o", group.name, group.workspaces);
|
|
390
401
|
for (const workspaceRel of group.workspaces) {
|
|
391
402
|
const workspaceAbs = path2.resolve(discovery.rootAbs, workspaceRel);
|
|
@@ -418,7 +429,7 @@ async function computeCurrentSnapshots(cwd) {
|
|
|
418
429
|
continue;
|
|
419
430
|
}
|
|
420
431
|
const message = result.error instanceof Error ? result.error.message : String(result.error);
|
|
421
|
-
if (isRecoverableExtractionError(message)) {
|
|
432
|
+
if (isRecoverableExtractionError(message) || allowWorkspaceExtractionFailure) {
|
|
422
433
|
lastExtractionError = message;
|
|
423
434
|
continue;
|
|
424
435
|
}
|
|
@@ -426,10 +437,23 @@ async function computeCurrentSnapshots(cwd) {
|
|
|
426
437
|
}
|
|
427
438
|
if (extractedCount === 0) {
|
|
428
439
|
const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : "";
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
440
|
+
if (allowWorkspaceExtractionFailure && isSkippableWorkspaceExtractionFailure(lastExtractionError)) {
|
|
441
|
+
onWorkspaceSkipped?.({
|
|
442
|
+
groupId: group.name,
|
|
443
|
+
workspaceRel,
|
|
444
|
+
reason: lastExtractionError ?? "unknown extraction failure"
|
|
445
|
+
});
|
|
446
|
+
debugWorkspace(
|
|
447
|
+
"group=%s workspace=%s skipped=true reason=%s",
|
|
448
|
+
group.name,
|
|
449
|
+
workspaceRel,
|
|
450
|
+
lastExtractionError ?? "unknown extraction failure"
|
|
451
|
+
);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
throw new Error(`Unable to extract ESLint config for workspace ${workspaceRel}.${context}`);
|
|
432
455
|
}
|
|
456
|
+
extractedWorkspaces.push(workspaceRel);
|
|
433
457
|
debugWorkspace(
|
|
434
458
|
"group=%s workspace=%s extracted=%d failed=%d",
|
|
435
459
|
group.name,
|
|
@@ -438,8 +462,15 @@ async function computeCurrentSnapshots(cwd) {
|
|
|
438
462
|
results.length - extractedCount
|
|
439
463
|
);
|
|
440
464
|
}
|
|
465
|
+
if (extractedForGroup.length === 0) {
|
|
466
|
+
if (allowWorkspaceExtractionFailure) {
|
|
467
|
+
debugWorkspace("group=%s skipped=true reason=no-extracted-workspaces", group.name);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
throw new Error(`Unable to extract ESLint config for group ${group.name}: no workspace produced a valid config`);
|
|
471
|
+
}
|
|
441
472
|
const aggregated = aggregateRules(extractedForGroup);
|
|
442
|
-
snapshots.set(group.name, buildSnapshot(group.name,
|
|
473
|
+
snapshots.set(group.name, buildSnapshot(group.name, extractedWorkspaces, aggregated));
|
|
443
474
|
debugWorkspace(
|
|
444
475
|
"group=%s aggregatedRules=%d groupElapsedMs=%d",
|
|
445
476
|
group.name,
|
|
@@ -448,11 +479,20 @@ async function computeCurrentSnapshots(cwd) {
|
|
|
448
479
|
);
|
|
449
480
|
}
|
|
450
481
|
debugTiming("phase=computeCurrentSnapshots elapsedMs=%d", Date.now() - computeStartedAt);
|
|
482
|
+
if (snapshots.size === 0) {
|
|
483
|
+
throw new Error("Unable to extract ESLint config from discovered workspaces in zero-config mode");
|
|
484
|
+
}
|
|
451
485
|
return snapshots;
|
|
452
486
|
}
|
|
453
487
|
function isRecoverableExtractionError(message) {
|
|
454
488
|
return message.startsWith("Invalid JSON from eslint --print-config") || message.startsWith("Empty ESLint print-config output") || message.includes("File ignored because of a matching ignore pattern") || message.includes("File ignored by default");
|
|
455
489
|
}
|
|
490
|
+
function isSkippableWorkspaceExtractionFailure(message) {
|
|
491
|
+
if (!message) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
return isRecoverableExtractionError(message) || message.startsWith("Failed to load config") || message.startsWith("Failed to run eslint --print-config") || message.startsWith("Unable to resolve eslint from workspace");
|
|
495
|
+
}
|
|
456
496
|
async function resolveWorkspaceAssignments(cwd, config) {
|
|
457
497
|
const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput });
|
|
458
498
|
const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
|
|
@@ -522,6 +562,63 @@ async function resolveGroupEslintVersions(cwd) {
|
|
|
522
562
|
return result;
|
|
523
563
|
}
|
|
524
564
|
|
|
565
|
+
// src/commands/skipped-workspaces.ts
|
|
566
|
+
import path3 from "path";
|
|
567
|
+
function writeSkippedWorkspaceSummary(terminal, cwd, configPath, skippedWorkspaces) {
|
|
568
|
+
if (skippedWorkspaces.length === 0) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const workspacePaths = collectSkippedWorkspacePaths(skippedWorkspaces);
|
|
572
|
+
terminal.warning(
|
|
573
|
+
`Heads up: ${workspacePaths.length} workspace(s) were skipped because ESLint auto-discovery could not extract an effective config for them.
|
|
574
|
+
`
|
|
575
|
+
);
|
|
576
|
+
terminal.warning(`Skipped workspaces: ${workspacePaths.join(", ")}
|
|
577
|
+
`);
|
|
578
|
+
terminal.subtle(formatScopedConfigHint(cwd, configPath, workspacePaths));
|
|
579
|
+
}
|
|
580
|
+
function collectSkippedWorkspacePaths(skippedWorkspaces) {
|
|
581
|
+
const unique = /* @__PURE__ */ new Set();
|
|
582
|
+
for (const skipped of skippedWorkspaces) {
|
|
583
|
+
unique.add(skipped.workspaceRel);
|
|
584
|
+
}
|
|
585
|
+
return [...unique].sort();
|
|
586
|
+
}
|
|
587
|
+
function toExcludeGlobs(workspacePaths) {
|
|
588
|
+
return workspacePaths.map((workspacePath) => `${workspacePath}/**`);
|
|
589
|
+
}
|
|
590
|
+
function formatScopedConfigHint(cwd, configPath, workspacePaths) {
|
|
591
|
+
const excludeGlobs = toExcludeGlobs(workspacePaths);
|
|
592
|
+
if (configPath && path3.basename(configPath) === "package.json") {
|
|
593
|
+
return `Tip: if these workspaces are intentionally out of scope, add this under "eslint-config-snapshot" in package.json:
|
|
594
|
+
${JSON.stringify(
|
|
595
|
+
{
|
|
596
|
+
sampling: {
|
|
597
|
+
excludeGlobs
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
null,
|
|
601
|
+
2
|
|
602
|
+
)}
|
|
603
|
+
`;
|
|
604
|
+
}
|
|
605
|
+
const objectLiteral = `{
|
|
606
|
+
sampling: {
|
|
607
|
+
excludeGlobs: [
|
|
608
|
+
${excludeGlobs.map((value) => ` '${value}'`).join(",\n")}
|
|
609
|
+
]
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
`;
|
|
613
|
+
if (configPath) {
|
|
614
|
+
const relConfigPath = path3.relative(cwd, configPath) || path3.basename(configPath);
|
|
615
|
+
return `Tip: if these workspaces are intentionally out of scope, add this in ${relConfigPath}:
|
|
616
|
+
${objectLiteral}`;
|
|
617
|
+
}
|
|
618
|
+
return `Tip: if these workspaces are intentionally out of scope, run \`eslint-config-snapshot init\` and add this config:
|
|
619
|
+
${objectLiteral}`;
|
|
620
|
+
}
|
|
621
|
+
|
|
525
622
|
// src/commands/check.ts
|
|
526
623
|
var UPDATE_HINT = "Tip: when you intentionally accept changes, run `eslint-config-snapshot --update` to refresh the baseline.\n";
|
|
527
624
|
async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocation = false) {
|
|
@@ -539,10 +636,20 @@ async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocatio
|
|
|
539
636
|
);
|
|
540
637
|
}
|
|
541
638
|
let currentSnapshots;
|
|
639
|
+
const skippedWorkspaces = [];
|
|
640
|
+
let discoveredWorkspaces = [];
|
|
542
641
|
try {
|
|
543
|
-
currentSnapshots = await computeCurrentSnapshots(cwd
|
|
642
|
+
currentSnapshots = await computeCurrentSnapshots(cwd, {
|
|
643
|
+
allowWorkspaceExtractionFailure: !foundConfig,
|
|
644
|
+
onWorkspacesDiscovered: (workspacesRel) => {
|
|
645
|
+
discoveredWorkspaces = workspacesRel;
|
|
646
|
+
},
|
|
647
|
+
onWorkspaceSkipped: (skipped) => {
|
|
648
|
+
skippedWorkspaces.push(skipped);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
544
651
|
} catch (error) {
|
|
545
|
-
if (!foundConfig) {
|
|
652
|
+
if (!foundConfig && isWorkspaceDiscoveryDefaultsError(error)) {
|
|
546
653
|
terminal.write(
|
|
547
654
|
"Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
|
|
548
655
|
);
|
|
@@ -550,6 +657,10 @@ async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocatio
|
|
|
550
657
|
}
|
|
551
658
|
throw error;
|
|
552
659
|
}
|
|
660
|
+
if (!foundConfig) {
|
|
661
|
+
writeDiscoveredWorkspacesSummary(terminal, discoveredWorkspaces);
|
|
662
|
+
}
|
|
663
|
+
writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces);
|
|
553
664
|
if (storedSnapshots.size === 0) {
|
|
554
665
|
const summary = summarizeSnapshots(currentSnapshots);
|
|
555
666
|
terminal.write(
|
|
@@ -609,12 +720,7 @@ function printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByG
|
|
|
609
720
|
if (changes.length === 0) {
|
|
610
721
|
terminal.write(color.green("\u2705 Great news: no snapshot drift detected.\n"));
|
|
611
722
|
terminal.section("\u{1F4CA} Summary");
|
|
612
|
-
terminal.write(
|
|
613
|
-
`- \u{1F4E6} baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules
|
|
614
|
-
- \u{1F5C2}\uFE0F workspaces scanned: ${workspaceCount}
|
|
615
|
-
- \u{1F39A}\uFE0F severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off
|
|
616
|
-
`
|
|
617
|
-
);
|
|
723
|
+
terminal.write(formatBaselineSummaryLines(currentSummary, workspaceCount));
|
|
618
724
|
writeEslintVersionSummary(terminal, eslintVersionsByGroup);
|
|
619
725
|
return 0;
|
|
620
726
|
}
|
|
@@ -649,6 +755,18 @@ function printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByG
|
|
|
649
755
|
terminal.subtle(UPDATE_HINT);
|
|
650
756
|
return 1;
|
|
651
757
|
}
|
|
758
|
+
function isWorkspaceDiscoveryDefaultsError(error) {
|
|
759
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
760
|
+
return message.includes("Unable to discover workspaces") || message.includes("Unmatched workspaces") || message.includes("zero-config mode");
|
|
761
|
+
}
|
|
762
|
+
function writeDiscoveredWorkspacesSummary(terminal, workspacesRel) {
|
|
763
|
+
if (workspacesRel.length === 0) {
|
|
764
|
+
terminal.subtle("Auto-discovered workspaces: none\n");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(", ")}
|
|
768
|
+
`);
|
|
769
|
+
}
|
|
652
770
|
|
|
653
771
|
// src/commands/print.ts
|
|
654
772
|
import { findConfigPath as findConfigPath2, loadConfig as loadConfig2 } from "@eslint-config-snapshot/api";
|
|
@@ -714,10 +832,20 @@ async function executeUpdate(cwd, terminal, snapshotDir, printSummary) {
|
|
|
714
832
|
);
|
|
715
833
|
}
|
|
716
834
|
let currentSnapshots;
|
|
835
|
+
const skippedWorkspaces = [];
|
|
836
|
+
let discoveredWorkspaces = [];
|
|
717
837
|
try {
|
|
718
|
-
currentSnapshots = await computeCurrentSnapshots(cwd
|
|
838
|
+
currentSnapshots = await computeCurrentSnapshots(cwd, {
|
|
839
|
+
allowWorkspaceExtractionFailure: !foundConfig,
|
|
840
|
+
onWorkspacesDiscovered: (workspacesRel) => {
|
|
841
|
+
discoveredWorkspaces = workspacesRel;
|
|
842
|
+
},
|
|
843
|
+
onWorkspaceSkipped: (skipped) => {
|
|
844
|
+
skippedWorkspaces.push(skipped);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
719
847
|
} catch (error) {
|
|
720
|
-
if (!foundConfig) {
|
|
848
|
+
if (!foundConfig && isWorkspaceDiscoveryDefaultsError2(error)) {
|
|
721
849
|
terminal.write(
|
|
722
850
|
"Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
|
|
723
851
|
);
|
|
@@ -725,28 +853,42 @@ async function executeUpdate(cwd, terminal, snapshotDir, printSummary) {
|
|
|
725
853
|
}
|
|
726
854
|
throw error;
|
|
727
855
|
}
|
|
856
|
+
if (!foundConfig) {
|
|
857
|
+
writeDiscoveredWorkspacesSummary2(terminal, discoveredWorkspaces);
|
|
858
|
+
}
|
|
859
|
+
writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces);
|
|
728
860
|
await writeSnapshots(cwd, snapshotDir, currentSnapshots);
|
|
729
861
|
if (printSummary) {
|
|
730
862
|
const summary = summarizeSnapshots(currentSnapshots);
|
|
731
863
|
const workspaceCount = countUniqueWorkspaces(currentSnapshots);
|
|
732
864
|
const eslintVersionsByGroup = terminal.showProgress ? await resolveGroupEslintVersions(cwd) : /* @__PURE__ */ new Map();
|
|
865
|
+
const baselineAction = storedSnapshots.size === 0 ? "created" : "updated";
|
|
866
|
+
terminal.success(`\u2705 Great news: baseline was successfully ${baselineAction} for your project.
|
|
867
|
+
`);
|
|
733
868
|
terminal.section("\u{1F4CA} Summary");
|
|
734
|
-
terminal.write(
|
|
735
|
-
`Baseline updated: ${summary.groups} groups, ${summary.rules} rules.
|
|
736
|
-
Workspaces scanned: ${workspaceCount}.
|
|
737
|
-
Severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.
|
|
738
|
-
`
|
|
739
|
-
);
|
|
869
|
+
terminal.write(formatBaselineSummaryLines(summary, workspaceCount));
|
|
740
870
|
writeEslintVersionSummary(terminal, eslintVersionsByGroup);
|
|
741
871
|
}
|
|
742
872
|
return 0;
|
|
743
873
|
}
|
|
874
|
+
function isWorkspaceDiscoveryDefaultsError2(error) {
|
|
875
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
876
|
+
return message.includes("Unable to discover workspaces") || message.includes("Unmatched workspaces") || message.includes("zero-config mode");
|
|
877
|
+
}
|
|
878
|
+
function writeDiscoveredWorkspacesSummary2(terminal, workspacesRel) {
|
|
879
|
+
if (workspacesRel.length === 0) {
|
|
880
|
+
terminal.subtle("Auto-discovered workspaces: none\n");
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(", ")}
|
|
884
|
+
`);
|
|
885
|
+
}
|
|
744
886
|
|
|
745
887
|
// src/init.ts
|
|
746
888
|
import { discoverWorkspaces as discoverWorkspaces2, findConfigPath as findConfigPath4, getConfigScaffold, normalizePath as normalizePath2 } from "@eslint-config-snapshot/api";
|
|
747
889
|
import fg2 from "fast-glob";
|
|
748
890
|
import { access, readFile, writeFile } from "fs/promises";
|
|
749
|
-
import
|
|
891
|
+
import path4 from "path";
|
|
750
892
|
async function runInit(cwd, opts, runtime) {
|
|
751
893
|
const force = opts.force ?? false;
|
|
752
894
|
const showEffective = opts.showEffective ?? false;
|
|
@@ -814,7 +956,7 @@ async function runInitInFile(cwd, configObject, force, runtime) {
|
|
|
814
956
|
];
|
|
815
957
|
for (const candidate of candidates) {
|
|
816
958
|
try {
|
|
817
|
-
await access(
|
|
959
|
+
await access(path4.join(cwd, candidate));
|
|
818
960
|
if (!force) {
|
|
819
961
|
runtime.writeStderr(`Config already exists: ${candidate}
|
|
820
962
|
`);
|
|
@@ -823,14 +965,14 @@ async function runInitInFile(cwd, configObject, force, runtime) {
|
|
|
823
965
|
} catch {
|
|
824
966
|
}
|
|
825
967
|
}
|
|
826
|
-
const target =
|
|
968
|
+
const target = path4.join(cwd, "eslint-config-snapshot.config.mjs");
|
|
827
969
|
await writeFile(target, toConfigScaffold(configObject), "utf8");
|
|
828
|
-
runtime.writeStdout(`Created ${
|
|
970
|
+
runtime.writeStdout(`Created ${path4.basename(target)}
|
|
829
971
|
`);
|
|
830
972
|
return 0;
|
|
831
973
|
}
|
|
832
974
|
async function runInitInPackageJson(cwd, configObject, force, runtime) {
|
|
833
|
-
const packageJsonPath =
|
|
975
|
+
const packageJsonPath = path4.join(cwd, "package.json");
|
|
834
976
|
let packageJsonRaw;
|
|
835
977
|
try {
|
|
836
978
|
packageJsonRaw = await readFile(packageJsonPath, "utf8");
|
|
@@ -891,7 +1033,7 @@ async function discoverInitWorkspaces(cwd) {
|
|
|
891
1033
|
if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === ".")) {
|
|
892
1034
|
return discovered.workspacesRel;
|
|
893
1035
|
}
|
|
894
|
-
const packageJsonPath =
|
|
1036
|
+
const packageJsonPath = path4.join(cwd, "package.json");
|
|
895
1037
|
try {
|
|
896
1038
|
const raw = await readFile(packageJsonPath, "utf8");
|
|
897
1039
|
const parsed = JSON.parse(raw);
|
|
@@ -909,7 +1051,7 @@ async function discoverInitWorkspaces(cwd) {
|
|
|
909
1051
|
onlyFiles: true,
|
|
910
1052
|
dot: true
|
|
911
1053
|
});
|
|
912
|
-
const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath2(
|
|
1054
|
+
const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath2(path4.dirname(entry))))].sort(
|
|
913
1055
|
(a, b) => a.localeCompare(b)
|
|
914
1056
|
);
|
|
915
1057
|
if (workspaceDirs.length > 0) {
|
|
@@ -1320,7 +1462,7 @@ function isDirectCliExecution() {
|
|
|
1320
1462
|
if (!entry) {
|
|
1321
1463
|
return false;
|
|
1322
1464
|
}
|
|
1323
|
-
const normalized =
|
|
1465
|
+
const normalized = path5.basename(entry).toLowerCase();
|
|
1324
1466
|
return normalized === "index.js" || normalized === "index.cjs" || normalized === "index.ts" || normalized === "eslint-config-snapshot";
|
|
1325
1467
|
}
|
|
1326
1468
|
if (isDirectCliExecution()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eslint-config-snapshot/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,6 +31,6 @@
|
|
|
31
31
|
"commander": "^14.0.3",
|
|
32
32
|
"debug": "^4.4.3",
|
|
33
33
|
"fast-glob": "^3.3.3",
|
|
34
|
-
"@eslint-config-snapshot/api": "1.
|
|
34
|
+
"@eslint-config-snapshot/api": "1.1.0"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/commands/check.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { findConfigPath } from '@eslint-config-snapshot/api'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
countUniqueWorkspaces,
|
|
5
|
+
decorateDiffLine,
|
|
6
|
+
formatBaselineSummaryLines,
|
|
7
|
+
formatDiff,
|
|
8
|
+
summarizeChanges,
|
|
9
|
+
summarizeSnapshots
|
|
10
|
+
} from '../formatters.js'
|
|
4
11
|
import { writeEslintVersionSummary, writeRunContextHeader } from '../run-context.js'
|
|
5
12
|
import {
|
|
6
13
|
type BuiltSnapshot,
|
|
@@ -9,10 +16,12 @@ import {
|
|
|
9
16
|
type GroupEslintVersions,
|
|
10
17
|
loadStoredSnapshots,
|
|
11
18
|
resolveGroupEslintVersions,
|
|
19
|
+
type SkippedWorkspace,
|
|
12
20
|
type SnapshotDiff,
|
|
13
21
|
writeSnapshots
|
|
14
22
|
} from '../runtime.js'
|
|
15
23
|
import { type TerminalIO } from '../terminal.js'
|
|
24
|
+
import { writeSkippedWorkspaceSummary } from './skipped-workspaces.js'
|
|
16
25
|
|
|
17
26
|
export type CheckFormat = 'summary' | 'status' | 'diff'
|
|
18
27
|
|
|
@@ -42,10 +51,20 @@ export async function executeCheck(
|
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
let currentSnapshots: Map<string, BuiltSnapshot>
|
|
54
|
+
const skippedWorkspaces: SkippedWorkspace[] = []
|
|
55
|
+
let discoveredWorkspaces: string[] = []
|
|
45
56
|
try {
|
|
46
|
-
currentSnapshots = await computeCurrentSnapshots(cwd
|
|
57
|
+
currentSnapshots = await computeCurrentSnapshots(cwd, {
|
|
58
|
+
allowWorkspaceExtractionFailure: !foundConfig,
|
|
59
|
+
onWorkspacesDiscovered: (workspacesRel) => {
|
|
60
|
+
discoveredWorkspaces = workspacesRel
|
|
61
|
+
},
|
|
62
|
+
onWorkspaceSkipped: (skipped) => {
|
|
63
|
+
skippedWorkspaces.push(skipped)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
47
66
|
} catch (error: unknown) {
|
|
48
|
-
if (!foundConfig) {
|
|
67
|
+
if (!foundConfig && isWorkspaceDiscoveryDefaultsError(error)) {
|
|
49
68
|
terminal.write(
|
|
50
69
|
'Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n'
|
|
51
70
|
)
|
|
@@ -54,6 +73,10 @@ export async function executeCheck(
|
|
|
54
73
|
|
|
55
74
|
throw error
|
|
56
75
|
}
|
|
76
|
+
if (!foundConfig) {
|
|
77
|
+
writeDiscoveredWorkspacesSummary(terminal, discoveredWorkspaces)
|
|
78
|
+
}
|
|
79
|
+
writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces)
|
|
57
80
|
if (storedSnapshots.size === 0) {
|
|
58
81
|
const summary = summarizeSnapshots(currentSnapshots)
|
|
59
82
|
terminal.write(
|
|
@@ -126,9 +149,7 @@ function printWhatChanged(
|
|
|
126
149
|
if (changes.length === 0) {
|
|
127
150
|
terminal.write(color.green('✅ Great news: no snapshot drift detected.\n'))
|
|
128
151
|
terminal.section('📊 Summary')
|
|
129
|
-
terminal.write(
|
|
130
|
-
`- 📦 baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules\n- 🗂️ workspaces scanned: ${workspaceCount}\n- 🎚️ severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off\n`
|
|
131
|
-
)
|
|
152
|
+
terminal.write(formatBaselineSummaryLines(currentSummary, workspaceCount))
|
|
132
153
|
writeEslintVersionSummary(terminal, eslintVersionsByGroup)
|
|
133
154
|
return 0
|
|
134
155
|
}
|
|
@@ -155,3 +176,21 @@ function printWhatChanged(
|
|
|
155
176
|
|
|
156
177
|
return 1
|
|
157
178
|
}
|
|
179
|
+
|
|
180
|
+
function isWorkspaceDiscoveryDefaultsError(error: unknown): boolean {
|
|
181
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
182
|
+
return (
|
|
183
|
+
message.includes('Unable to discover workspaces') ||
|
|
184
|
+
message.includes('Unmatched workspaces') ||
|
|
185
|
+
message.includes('zero-config mode')
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function writeDiscoveredWorkspacesSummary(terminal: TerminalIO, workspacesRel: string[]): void {
|
|
190
|
+
if (workspacesRel.length === 0) {
|
|
191
|
+
terminal.subtle('Auto-discovered workspaces: none\n')
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(', ')}\n`)
|
|
196
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { type SkippedWorkspace } from '../runtime.js'
|
|
4
|
+
import { type TerminalIO } from '../terminal.js'
|
|
5
|
+
|
|
6
|
+
export function writeSkippedWorkspaceSummary(
|
|
7
|
+
terminal: TerminalIO,
|
|
8
|
+
cwd: string,
|
|
9
|
+
configPath: string | undefined,
|
|
10
|
+
skippedWorkspaces: SkippedWorkspace[]
|
|
11
|
+
): void {
|
|
12
|
+
if (skippedWorkspaces.length === 0) {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const workspacePaths = collectSkippedWorkspacePaths(skippedWorkspaces)
|
|
17
|
+
terminal.warning(
|
|
18
|
+
`Heads up: ${workspacePaths.length} workspace(s) were skipped because ESLint auto-discovery could not extract an effective config for them.\n`
|
|
19
|
+
)
|
|
20
|
+
terminal.warning(`Skipped workspaces: ${workspacePaths.join(', ')}\n`)
|
|
21
|
+
terminal.subtle(formatScopedConfigHint(cwd, configPath, workspacePaths))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function collectSkippedWorkspacePaths(skippedWorkspaces: SkippedWorkspace[]): string[] {
|
|
25
|
+
const unique = new Set<string>()
|
|
26
|
+
for (const skipped of skippedWorkspaces) {
|
|
27
|
+
unique.add(skipped.workspaceRel)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return [...unique].sort()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toExcludeGlobs(workspacePaths: string[]): string[] {
|
|
34
|
+
return workspacePaths.map((workspacePath) => `${workspacePath}/**`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatScopedConfigHint(cwd: string, configPath: string | undefined, workspacePaths: string[]): string {
|
|
38
|
+
const excludeGlobs = toExcludeGlobs(workspacePaths)
|
|
39
|
+
|
|
40
|
+
if (configPath && path.basename(configPath) === 'package.json') {
|
|
41
|
+
return `Tip: if these workspaces are intentionally out of scope, add this under "eslint-config-snapshot" in package.json:\n${JSON.stringify(
|
|
42
|
+
{
|
|
43
|
+
sampling: {
|
|
44
|
+
excludeGlobs
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
null,
|
|
48
|
+
2
|
|
49
|
+
)}\n`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const objectLiteral = `{
|
|
53
|
+
sampling: {
|
|
54
|
+
excludeGlobs: [
|
|
55
|
+
${excludeGlobs.map((value) => ` '${value}'`).join(',\n')}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}\n`
|
|
59
|
+
|
|
60
|
+
if (configPath) {
|
|
61
|
+
const relConfigPath = path.relative(cwd, configPath) || path.basename(configPath)
|
|
62
|
+
return `Tip: if these workspaces are intentionally out of scope, add this in ${relConfigPath}:\n${objectLiteral}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return `Tip: if these workspaces are intentionally out of scope, run \`eslint-config-snapshot init\` and add this config:\n${objectLiteral}`
|
|
66
|
+
}
|
package/src/commands/update.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { findConfigPath } from '@eslint-config-snapshot/api'
|
|
2
2
|
|
|
3
|
-
import { countUniqueWorkspaces, summarizeSnapshots } from '../formatters.js'
|
|
3
|
+
import { countUniqueWorkspaces, formatBaselineSummaryLines, summarizeSnapshots } from '../formatters.js'
|
|
4
4
|
import { writeEslintVersionSummary, writeRunContextHeader } from '../run-context.js'
|
|
5
|
-
import { computeCurrentSnapshots, loadStoredSnapshots, resolveGroupEslintVersions, writeSnapshots } from '../runtime.js'
|
|
5
|
+
import { computeCurrentSnapshots, loadStoredSnapshots, resolveGroupEslintVersions, type SkippedWorkspace, writeSnapshots } from '../runtime.js'
|
|
6
6
|
import { type TerminalIO } from '../terminal.js'
|
|
7
|
+
import { writeSkippedWorkspaceSummary } from './skipped-workspaces.js'
|
|
7
8
|
|
|
8
9
|
export async function executeUpdate(cwd: string, terminal: TerminalIO, snapshotDir: string, printSummary: boolean): Promise<number> {
|
|
9
10
|
const foundConfig = await findConfigPath(cwd)
|
|
@@ -20,10 +21,20 @@ export async function executeUpdate(cwd: string, terminal: TerminalIO, snapshotD
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
let currentSnapshots
|
|
24
|
+
const skippedWorkspaces: SkippedWorkspace[] = []
|
|
25
|
+
let discoveredWorkspaces: string[] = []
|
|
23
26
|
try {
|
|
24
|
-
currentSnapshots = await computeCurrentSnapshots(cwd
|
|
27
|
+
currentSnapshots = await computeCurrentSnapshots(cwd, {
|
|
28
|
+
allowWorkspaceExtractionFailure: !foundConfig,
|
|
29
|
+
onWorkspacesDiscovered: (workspacesRel) => {
|
|
30
|
+
discoveredWorkspaces = workspacesRel
|
|
31
|
+
},
|
|
32
|
+
onWorkspaceSkipped: (skipped) => {
|
|
33
|
+
skippedWorkspaces.push(skipped)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
25
36
|
} catch (error: unknown) {
|
|
26
|
-
if (!foundConfig) {
|
|
37
|
+
if (!foundConfig && isWorkspaceDiscoveryDefaultsError(error)) {
|
|
27
38
|
terminal.write(
|
|
28
39
|
'Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n'
|
|
29
40
|
)
|
|
@@ -32,18 +43,40 @@ export async function executeUpdate(cwd: string, terminal: TerminalIO, snapshotD
|
|
|
32
43
|
|
|
33
44
|
throw error
|
|
34
45
|
}
|
|
46
|
+
if (!foundConfig) {
|
|
47
|
+
writeDiscoveredWorkspacesSummary(terminal, discoveredWorkspaces)
|
|
48
|
+
}
|
|
49
|
+
writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces)
|
|
35
50
|
await writeSnapshots(cwd, snapshotDir, currentSnapshots)
|
|
36
51
|
|
|
37
52
|
if (printSummary) {
|
|
38
53
|
const summary = summarizeSnapshots(currentSnapshots)
|
|
39
54
|
const workspaceCount = countUniqueWorkspaces(currentSnapshots)
|
|
40
55
|
const eslintVersionsByGroup = terminal.showProgress ? await resolveGroupEslintVersions(cwd) : new Map<string, string[]>()
|
|
56
|
+
const baselineAction = storedSnapshots.size === 0 ? 'created' : 'updated'
|
|
57
|
+
terminal.success(`✅ Great news: baseline was successfully ${baselineAction} for your project.\n`)
|
|
41
58
|
terminal.section('📊 Summary')
|
|
42
|
-
terminal.write(
|
|
43
|
-
`Baseline updated: ${summary.groups} groups, ${summary.rules} rules.\nWorkspaces scanned: ${workspaceCount}.\nSeverity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.\n`
|
|
44
|
-
)
|
|
59
|
+
terminal.write(formatBaselineSummaryLines(summary, workspaceCount))
|
|
45
60
|
writeEslintVersionSummary(terminal, eslintVersionsByGroup)
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
return 0
|
|
49
64
|
}
|
|
65
|
+
|
|
66
|
+
function isWorkspaceDiscoveryDefaultsError(error: unknown): boolean {
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
68
|
+
return (
|
|
69
|
+
message.includes('Unable to discover workspaces') ||
|
|
70
|
+
message.includes('Unmatched workspaces') ||
|
|
71
|
+
message.includes('zero-config mode')
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function writeDiscoveredWorkspacesSummary(terminal: TerminalIO, workspacesRel: string[]): void {
|
|
76
|
+
if (workspacesRel.length === 0) {
|
|
77
|
+
terminal.subtle('Auto-discovered workspaces: none\n')
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(', ')}\n`)
|
|
82
|
+
}
|
package/src/formatters.ts
CHANGED
|
@@ -213,6 +213,13 @@ export function formatStoredSnapshotSummary(storedSnapshots: Map<string, Snapsho
|
|
|
213
213
|
return `${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off)`
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
export function formatBaselineSummaryLines(
|
|
217
|
+
summary: { groups: number; rules: number; error: number; warn: number; off: number },
|
|
218
|
+
workspaceCount: number
|
|
219
|
+
): string {
|
|
220
|
+
return `- 📦 baseline: ${summary.groups} groups, ${summary.rules} rules\n- 🗂️ workspaces scanned: ${workspaceCount}\n- 🎚️ severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off\n`
|
|
221
|
+
}
|
|
222
|
+
|
|
216
223
|
export function countRuleSeverities(ruleObjects: RuleObject[]) {
|
|
217
224
|
let rules = 0
|
|
218
225
|
let error = 0
|
package/src/runtime.ts
CHANGED
|
@@ -32,8 +32,23 @@ export type WorkspaceAssignments = {
|
|
|
32
32
|
discovery: WorkspaceDiscovery
|
|
33
33
|
assignments: GroupAssignment[]
|
|
34
34
|
}
|
|
35
|
+
export type SkippedWorkspace = {
|
|
36
|
+
groupId: string
|
|
37
|
+
workspaceRel: string
|
|
38
|
+
reason: string
|
|
39
|
+
}
|
|
35
40
|
|
|
36
|
-
export async function computeCurrentSnapshots(
|
|
41
|
+
export async function computeCurrentSnapshots(
|
|
42
|
+
cwd: string,
|
|
43
|
+
options?: {
|
|
44
|
+
allowWorkspaceExtractionFailure?: boolean
|
|
45
|
+
onWorkspacesDiscovered?: (workspacesRel: string[]) => void
|
|
46
|
+
onWorkspaceSkipped?: (skipped: SkippedWorkspace) => void
|
|
47
|
+
}
|
|
48
|
+
): Promise<Map<string, BuiltSnapshot>> {
|
|
49
|
+
const allowWorkspaceExtractionFailure = options?.allowWorkspaceExtractionFailure ?? false
|
|
50
|
+
const onWorkspacesDiscovered = options?.onWorkspacesDiscovered
|
|
51
|
+
const onWorkspaceSkipped = options?.onWorkspaceSkipped
|
|
37
52
|
const computeStartedAt = Date.now()
|
|
38
53
|
const configStartedAt = Date.now()
|
|
39
54
|
const config = await loadConfig(cwd)
|
|
@@ -41,6 +56,7 @@ export async function computeCurrentSnapshots(cwd: string): Promise<Map<string,
|
|
|
41
56
|
|
|
42
57
|
const assignmentStartedAt = Date.now()
|
|
43
58
|
const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config)
|
|
59
|
+
onWorkspacesDiscovered?.(discovery.workspacesRel)
|
|
44
60
|
debugTiming('phase=resolveWorkspaceAssignments elapsedMs=%d', Date.now() - assignmentStartedAt)
|
|
45
61
|
debugWorkspace('root=%s groups=%d workspaces=%d', discovery.rootAbs, assignments.length, discovery.workspacesRel.length)
|
|
46
62
|
|
|
@@ -49,6 +65,7 @@ export async function computeCurrentSnapshots(cwd: string): Promise<Map<string,
|
|
|
49
65
|
for (const group of assignments) {
|
|
50
66
|
const groupStartedAt = Date.now()
|
|
51
67
|
const extractedForGroup = []
|
|
68
|
+
const extractedWorkspaces: string[] = []
|
|
52
69
|
debugWorkspace('group=%s workspaces=%o', group.name, group.workspaces)
|
|
53
70
|
|
|
54
71
|
for (const workspaceRel of group.workspaces) {
|
|
@@ -85,7 +102,7 @@ export async function computeCurrentSnapshots(cwd: string): Promise<Map<string,
|
|
|
85
102
|
}
|
|
86
103
|
|
|
87
104
|
const message = result.error instanceof Error ? result.error.message : String(result.error)
|
|
88
|
-
if (isRecoverableExtractionError(message)) {
|
|
105
|
+
if (isRecoverableExtractionError(message) || allowWorkspaceExtractionFailure) {
|
|
89
106
|
lastExtractionError = message
|
|
90
107
|
continue
|
|
91
108
|
}
|
|
@@ -95,10 +112,24 @@ export async function computeCurrentSnapshots(cwd: string): Promise<Map<string,
|
|
|
95
112
|
|
|
96
113
|
if (extractedCount === 0) {
|
|
97
114
|
const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : ''
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
115
|
+
if (allowWorkspaceExtractionFailure && isSkippableWorkspaceExtractionFailure(lastExtractionError)) {
|
|
116
|
+
onWorkspaceSkipped?.({
|
|
117
|
+
groupId: group.name,
|
|
118
|
+
workspaceRel,
|
|
119
|
+
reason: lastExtractionError ?? 'unknown extraction failure'
|
|
120
|
+
})
|
|
121
|
+
debugWorkspace(
|
|
122
|
+
'group=%s workspace=%s skipped=true reason=%s',
|
|
123
|
+
group.name,
|
|
124
|
+
workspaceRel,
|
|
125
|
+
lastExtractionError ?? 'unknown extraction failure'
|
|
126
|
+
)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
throw new Error(`Unable to extract ESLint config for workspace ${workspaceRel}.${context}`)
|
|
101
131
|
}
|
|
132
|
+
extractedWorkspaces.push(workspaceRel)
|
|
102
133
|
|
|
103
134
|
debugWorkspace(
|
|
104
135
|
'group=%s workspace=%s extracted=%d failed=%d',
|
|
@@ -109,8 +140,16 @@ export async function computeCurrentSnapshots(cwd: string): Promise<Map<string,
|
|
|
109
140
|
)
|
|
110
141
|
}
|
|
111
142
|
|
|
143
|
+
if (extractedForGroup.length === 0) {
|
|
144
|
+
if (allowWorkspaceExtractionFailure) {
|
|
145
|
+
debugWorkspace('group=%s skipped=true reason=no-extracted-workspaces', group.name)
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`Unable to extract ESLint config for group ${group.name}: no workspace produced a valid config`)
|
|
149
|
+
}
|
|
150
|
+
|
|
112
151
|
const aggregated = aggregateRules(extractedForGroup)
|
|
113
|
-
snapshots.set(group.name, buildSnapshot(group.name,
|
|
152
|
+
snapshots.set(group.name, buildSnapshot(group.name, extractedWorkspaces, aggregated))
|
|
114
153
|
debugWorkspace(
|
|
115
154
|
'group=%s aggregatedRules=%d groupElapsedMs=%d',
|
|
116
155
|
group.name,
|
|
@@ -120,6 +159,9 @@ export async function computeCurrentSnapshots(cwd: string): Promise<Map<string,
|
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
debugTiming('phase=computeCurrentSnapshots elapsedMs=%d', Date.now() - computeStartedAt)
|
|
162
|
+
if (snapshots.size === 0) {
|
|
163
|
+
throw new Error('Unable to extract ESLint config from discovered workspaces in zero-config mode')
|
|
164
|
+
}
|
|
123
165
|
return snapshots
|
|
124
166
|
}
|
|
125
167
|
|
|
@@ -132,6 +174,19 @@ function isRecoverableExtractionError(message: string): boolean {
|
|
|
132
174
|
)
|
|
133
175
|
}
|
|
134
176
|
|
|
177
|
+
function isSkippableWorkspaceExtractionFailure(message: string | undefined): boolean {
|
|
178
|
+
if (!message) {
|
|
179
|
+
return true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
isRecoverableExtractionError(message) ||
|
|
184
|
+
message.startsWith('Failed to load config') ||
|
|
185
|
+
message.startsWith('Failed to run eslint --print-config') ||
|
|
186
|
+
message.startsWith('Unable to resolve eslint from workspace')
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
135
190
|
export async function resolveWorkspaceAssignments(cwd: string, config: SnapshotConfig): Promise<WorkspaceAssignments> {
|
|
136
191
|
const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput })
|
|
137
192
|
|
|
@@ -105,6 +105,43 @@ describe.sequential('cli integration', () => {
|
|
|
105
105
|
expect(code).toBe(1)
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
+
it('update in zero-config mode skips workspaces with unrecoverable eslint extraction failures', async () => {
|
|
109
|
+
await rm(path.join(fixtureRoot, 'eslint-config-snapshot.config.mjs'), { force: true })
|
|
110
|
+
const packageJsonPath = path.join(fixtureRoot, 'package.json')
|
|
111
|
+
const packageJsonRaw = await readFile(packageJsonPath, 'utf8')
|
|
112
|
+
const packageJson = JSON.parse(packageJsonRaw) as Record<string, unknown>
|
|
113
|
+
delete packageJson['eslint-config-snapshot']
|
|
114
|
+
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`)
|
|
115
|
+
|
|
116
|
+
await writeFile(
|
|
117
|
+
path.join(fixtureRoot, 'packages/ws-b/node_modules/eslint/bin/eslint.js'),
|
|
118
|
+
"console.error('Failed to load config \"next/core-web-vitals\" to extend from.'); process.exit(1)\n"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const writeSpy = vi.spyOn(process.stdout, 'write')
|
|
122
|
+
const code = await runCli('update', fixtureRoot)
|
|
123
|
+
expect(code).toBe(0)
|
|
124
|
+
expect(writeSpy).toHaveBeenCalledWith(
|
|
125
|
+
expect.stringContaining('workspace(s) were skipped because ESLint auto-discovery could not extract an effective config')
|
|
126
|
+
)
|
|
127
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('Skipped workspaces: packages/ws-b'))
|
|
128
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('excludeGlobs'))
|
|
129
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('packages/ws-b/**'))
|
|
130
|
+
|
|
131
|
+
const snapshotRaw = await readFile(path.join(fixtureRoot, '.eslint-config-snapshot/default.json'), 'utf8')
|
|
132
|
+
const snapshot = JSON.parse(snapshotRaw)
|
|
133
|
+
|
|
134
|
+
expect(snapshot).toEqual({
|
|
135
|
+
formatVersion: 1,
|
|
136
|
+
groupId: 'default',
|
|
137
|
+
workspaces: ['packages/ws-a'],
|
|
138
|
+
rules: {
|
|
139
|
+
eqeqeq: ['error', 'always'],
|
|
140
|
+
'no-console': ['warn']
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
108
145
|
it('status is minimal and exits 0 when clean', async () => {
|
|
109
146
|
await runCli('snapshot', fixtureRoot)
|
|
110
147
|
|
|
@@ -185,7 +222,7 @@ no-debugger: off
|
|
|
185
222
|
const writeSpy = vi.spyOn(process.stdout, 'write')
|
|
186
223
|
const code = await runCli(undefined, fixtureRoot, ['--update'])
|
|
187
224
|
expect(code).toBe(0)
|
|
188
|
-
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('
|
|
225
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('baseline was successfully created'))
|
|
189
226
|
})
|
|
190
227
|
|
|
191
228
|
it('supports canonical check and update commands', async () => {
|
|
@@ -81,7 +81,7 @@ describe('cli terminal invocation', () => {
|
|
|
81
81
|
it('snapshot succeeds and compare returns clean result', () => {
|
|
82
82
|
const snapshot = run(['snapshot'])
|
|
83
83
|
expect(snapshot.status).toBe(0)
|
|
84
|
-
expect(snapshot.stdout).toContain('
|
|
84
|
+
expect(snapshot.stdout).toContain('baseline was successfully created')
|
|
85
85
|
expect(snapshot.stderr).toBe('')
|
|
86
86
|
|
|
87
87
|
const compare = run(['compare'])
|
|
@@ -384,10 +384,21 @@ no-debugger: off
|
|
|
384
384
|
it('updates snapshots with --update without command', () => {
|
|
385
385
|
const result = run(['--update'])
|
|
386
386
|
expect(result.status).toBe(0)
|
|
387
|
-
expect(result.stdout).toContain('
|
|
387
|
+
expect(result.stdout).toContain('baseline was successfully created')
|
|
388
388
|
expect(result.stderr).toBe('')
|
|
389
389
|
})
|
|
390
390
|
|
|
391
|
+
it('prints updated message when baseline already exists', () => {
|
|
392
|
+
const first = run(['--update'])
|
|
393
|
+
expect(first.status).toBe(0)
|
|
394
|
+
expect(first.stdout).toContain('baseline was successfully created')
|
|
395
|
+
|
|
396
|
+
const second = run(['--update'])
|
|
397
|
+
expect(second.status).toBe(0)
|
|
398
|
+
expect(second.stdout).toContain('baseline was successfully updated')
|
|
399
|
+
expect(second.stderr).toBe('')
|
|
400
|
+
})
|
|
401
|
+
|
|
391
402
|
it('prints init help with select-prompt and force guidance', () => {
|
|
392
403
|
const result = run(['init', '--help'])
|
|
393
404
|
expect(result.status).toBe(0)
|
|
@@ -403,7 +414,7 @@ no-debugger: off
|
|
|
403
414
|
it('supports canonical check and update commands', () => {
|
|
404
415
|
const update = run(['update'])
|
|
405
416
|
expect(update.status).toBe(0)
|
|
406
|
-
expect(update.stdout).toContain('
|
|
417
|
+
expect(update.stdout).toContain('baseline was successfully created')
|
|
407
418
|
|
|
408
419
|
const check = run(['check'])
|
|
409
420
|
expect(check.status).toBe(0)
|