@eslint-config-snapshot/cli 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,10 +3,10 @@
3
3
  // src/index.ts
4
4
  import { Command, CommanderError, InvalidArgumentError } from "commander";
5
5
  import createDebug2 from "debug";
6
- import path4 from "path";
6
+ import path5 from "path";
7
7
 
8
8
  // src/commands/check.ts
9
- import { findConfigPath } from "@eslint-config-snapshot/api";
9
+ import { DEFAULT_CONFIG, findConfigPath } from "@eslint-config-snapshot/api";
10
10
 
11
11
  // src/formatters.ts
12
12
  function formatDiff(groupId, diff) {
@@ -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
- throw new Error(
430
- `Unable to extract ESLint config for workspace ${workspaceRel}. All sampled files were ignored or produced non-JSON output.${context}`
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, group.workspaces, aggregated));
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,21 @@ async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocatio
539
636
  );
540
637
  }
541
638
  let currentSnapshots;
639
+ const skippedWorkspaces = [];
640
+ let discoveredWorkspaces = [];
641
+ const allowWorkspaceExtractionFailure = !foundConfig || isDefaultEquivalentConfig(foundConfig.config);
542
642
  try {
543
- currentSnapshots = await computeCurrentSnapshots(cwd);
643
+ currentSnapshots = await computeCurrentSnapshots(cwd, {
644
+ allowWorkspaceExtractionFailure,
645
+ onWorkspacesDiscovered: (workspacesRel) => {
646
+ discoveredWorkspaces = workspacesRel;
647
+ },
648
+ onWorkspaceSkipped: (skipped) => {
649
+ skippedWorkspaces.push(skipped);
650
+ }
651
+ });
544
652
  } catch (error) {
545
- if (!foundConfig) {
653
+ if (!foundConfig && isWorkspaceDiscoveryDefaultsError(error)) {
546
654
  terminal.write(
547
655
  "Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
548
656
  );
@@ -550,6 +658,10 @@ async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocatio
550
658
  }
551
659
  throw error;
552
660
  }
661
+ if (!foundConfig) {
662
+ writeDiscoveredWorkspacesSummary(terminal, discoveredWorkspaces);
663
+ }
664
+ writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces);
553
665
  if (storedSnapshots.size === 0) {
554
666
  const summary = summarizeSnapshots(currentSnapshots);
555
667
  terminal.write(
@@ -609,12 +721,7 @@ function printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByG
609
721
  if (changes.length === 0) {
610
722
  terminal.write(color.green("\u2705 Great news: no snapshot drift detected.\n"));
611
723
  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
- );
724
+ terminal.write(formatBaselineSummaryLines(currentSummary, workspaceCount));
618
725
  writeEslintVersionSummary(terminal, eslintVersionsByGroup);
619
726
  return 0;
620
727
  }
@@ -649,9 +756,24 @@ function printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByG
649
756
  terminal.subtle(UPDATE_HINT);
650
757
  return 1;
651
758
  }
759
+ function isWorkspaceDiscoveryDefaultsError(error) {
760
+ const message = error instanceof Error ? error.message : String(error);
761
+ return message.includes("Unable to discover workspaces") || message.includes("Unmatched workspaces") || message.includes("zero-config mode");
762
+ }
763
+ function isDefaultEquivalentConfig(config) {
764
+ return JSON.stringify(config) === JSON.stringify(DEFAULT_CONFIG);
765
+ }
766
+ function writeDiscoveredWorkspacesSummary(terminal, workspacesRel) {
767
+ if (workspacesRel.length === 0) {
768
+ terminal.subtle("Auto-discovered workspaces: none\n");
769
+ return;
770
+ }
771
+ terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(", ")}
772
+ `);
773
+ }
652
774
 
653
775
  // src/commands/print.ts
654
- import { findConfigPath as findConfigPath2, loadConfig as loadConfig2 } from "@eslint-config-snapshot/api";
776
+ import { DEFAULT_CONFIG as DEFAULT_CONFIG2, findConfigPath as findConfigPath2, loadConfig as loadConfig2 } from "@eslint-config-snapshot/api";
655
777
  async function executePrint(cwd, terminal, snapshotDir, format) {
656
778
  const foundConfig = await findConfigPath2(cwd);
657
779
  const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir);
@@ -659,7 +781,8 @@ async function executePrint(cwd, terminal, snapshotDir, format) {
659
781
  if (terminal.showProgress) {
660
782
  terminal.subtle("\u{1F50E} Checking current ESLint configuration...\n");
661
783
  }
662
- const currentSnapshots = await computeCurrentSnapshots(cwd);
784
+ const allowWorkspaceExtractionFailure = !foundConfig || isDefaultEquivalentConfig2(foundConfig.config);
785
+ const currentSnapshots = await computeCurrentSnapshots(cwd, { allowWorkspaceExtractionFailure });
663
786
  if (format === "short") {
664
787
  terminal.write(formatShortPrint([...currentSnapshots.values()]));
665
788
  return;
@@ -698,9 +821,12 @@ async function executeConfig(cwd, terminal, snapshotDir, format) {
698
821
  terminal.write(`${JSON.stringify(payload, null, 2)}
699
822
  `);
700
823
  }
824
+ function isDefaultEquivalentConfig2(config) {
825
+ return JSON.stringify(config) === JSON.stringify(DEFAULT_CONFIG2);
826
+ }
701
827
 
702
828
  // src/commands/update.ts
703
- import { findConfigPath as findConfigPath3 } from "@eslint-config-snapshot/api";
829
+ import { DEFAULT_CONFIG as DEFAULT_CONFIG3, findConfigPath as findConfigPath3 } from "@eslint-config-snapshot/api";
704
830
  async function executeUpdate(cwd, terminal, snapshotDir, printSummary) {
705
831
  const foundConfig = await findConfigPath3(cwd);
706
832
  const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir);
@@ -714,10 +840,21 @@ async function executeUpdate(cwd, terminal, snapshotDir, printSummary) {
714
840
  );
715
841
  }
716
842
  let currentSnapshots;
843
+ const skippedWorkspaces = [];
844
+ let discoveredWorkspaces = [];
845
+ const allowWorkspaceExtractionFailure = !foundConfig || isDefaultEquivalentConfig3(foundConfig.config);
717
846
  try {
718
- currentSnapshots = await computeCurrentSnapshots(cwd);
847
+ currentSnapshots = await computeCurrentSnapshots(cwd, {
848
+ allowWorkspaceExtractionFailure,
849
+ onWorkspacesDiscovered: (workspacesRel) => {
850
+ discoveredWorkspaces = workspacesRel;
851
+ },
852
+ onWorkspaceSkipped: (skipped) => {
853
+ skippedWorkspaces.push(skipped);
854
+ }
855
+ });
719
856
  } catch (error) {
720
- if (!foundConfig) {
857
+ if (!foundConfig && isWorkspaceDiscoveryDefaultsError2(error)) {
721
858
  terminal.write(
722
859
  "Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
723
860
  );
@@ -725,28 +862,45 @@ async function executeUpdate(cwd, terminal, snapshotDir, printSummary) {
725
862
  }
726
863
  throw error;
727
864
  }
865
+ if (!foundConfig) {
866
+ writeDiscoveredWorkspacesSummary2(terminal, discoveredWorkspaces);
867
+ }
868
+ writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces);
728
869
  await writeSnapshots(cwd, snapshotDir, currentSnapshots);
729
870
  if (printSummary) {
730
871
  const summary = summarizeSnapshots(currentSnapshots);
731
872
  const workspaceCount = countUniqueWorkspaces(currentSnapshots);
732
873
  const eslintVersionsByGroup = terminal.showProgress ? await resolveGroupEslintVersions(cwd) : /* @__PURE__ */ new Map();
874
+ const baselineAction = storedSnapshots.size === 0 ? "created" : "updated";
875
+ terminal.success(`\u2705 Great news: baseline was successfully ${baselineAction} for your project.
876
+ `);
733
877
  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
- );
878
+ terminal.write(formatBaselineSummaryLines(summary, workspaceCount));
740
879
  writeEslintVersionSummary(terminal, eslintVersionsByGroup);
741
880
  }
742
881
  return 0;
743
882
  }
883
+ function isWorkspaceDiscoveryDefaultsError2(error) {
884
+ const message = error instanceof Error ? error.message : String(error);
885
+ return message.includes("Unable to discover workspaces") || message.includes("Unmatched workspaces") || message.includes("zero-config mode");
886
+ }
887
+ function isDefaultEquivalentConfig3(config) {
888
+ return JSON.stringify(config) === JSON.stringify(DEFAULT_CONFIG3);
889
+ }
890
+ function writeDiscoveredWorkspacesSummary2(terminal, workspacesRel) {
891
+ if (workspacesRel.length === 0) {
892
+ terminal.subtle("Auto-discovered workspaces: none\n");
893
+ return;
894
+ }
895
+ terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(", ")}
896
+ `);
897
+ }
744
898
 
745
899
  // src/init.ts
746
900
  import { discoverWorkspaces as discoverWorkspaces2, findConfigPath as findConfigPath4, getConfigScaffold, normalizePath as normalizePath2 } from "@eslint-config-snapshot/api";
747
901
  import fg2 from "fast-glob";
748
902
  import { access, readFile, writeFile } from "fs/promises";
749
- import path3 from "path";
903
+ import path4 from "path";
750
904
  async function runInit(cwd, opts, runtime) {
751
905
  const force = opts.force ?? false;
752
906
  const showEffective = opts.showEffective ?? false;
@@ -814,7 +968,7 @@ async function runInitInFile(cwd, configObject, force, runtime) {
814
968
  ];
815
969
  for (const candidate of candidates) {
816
970
  try {
817
- await access(path3.join(cwd, candidate));
971
+ await access(path4.join(cwd, candidate));
818
972
  if (!force) {
819
973
  runtime.writeStderr(`Config already exists: ${candidate}
820
974
  `);
@@ -823,14 +977,14 @@ async function runInitInFile(cwd, configObject, force, runtime) {
823
977
  } catch {
824
978
  }
825
979
  }
826
- const target = path3.join(cwd, "eslint-config-snapshot.config.mjs");
980
+ const target = path4.join(cwd, "eslint-config-snapshot.config.mjs");
827
981
  await writeFile(target, toConfigScaffold(configObject), "utf8");
828
- runtime.writeStdout(`Created ${path3.basename(target)}
982
+ runtime.writeStdout(`Created ${path4.basename(target)}
829
983
  `);
830
984
  return 0;
831
985
  }
832
986
  async function runInitInPackageJson(cwd, configObject, force, runtime) {
833
- const packageJsonPath = path3.join(cwd, "package.json");
987
+ const packageJsonPath = path4.join(cwd, "package.json");
834
988
  let packageJsonRaw;
835
989
  try {
836
990
  packageJsonRaw = await readFile(packageJsonPath, "utf8");
@@ -891,7 +1045,7 @@ async function discoverInitWorkspaces(cwd) {
891
1045
  if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === ".")) {
892
1046
  return discovered.workspacesRel;
893
1047
  }
894
- const packageJsonPath = path3.join(cwd, "package.json");
1048
+ const packageJsonPath = path4.join(cwd, "package.json");
895
1049
  try {
896
1050
  const raw = await readFile(packageJsonPath, "utf8");
897
1051
  const parsed = JSON.parse(raw);
@@ -909,7 +1063,7 @@ async function discoverInitWorkspaces(cwd) {
909
1063
  onlyFiles: true,
910
1064
  dot: true
911
1065
  });
912
- const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath2(path3.dirname(entry))))].sort(
1066
+ const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath2(path4.dirname(entry))))].sort(
913
1067
  (a, b) => a.localeCompare(b)
914
1068
  );
915
1069
  if (workspaceDirs.length > 0) {
@@ -1320,7 +1474,7 @@ function isDirectCliExecution() {
1320
1474
  if (!entry) {
1321
1475
  return false;
1322
1476
  }
1323
- const normalized = path4.basename(entry).toLowerCase();
1477
+ const normalized = path5.basename(entry).toLowerCase();
1324
1478
  return normalized === "index.js" || normalized === "index.cjs" || normalized === "index.ts" || normalized === "eslint-config-snapshot";
1325
1479
  }
1326
1480
  if (isDirectCliExecution()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint-config-snapshot/cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
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.0.0"
34
+ "@eslint-config-snapshot/api": "1.1.1"
35
35
  }
36
36
  }
@@ -1,6 +1,13 @@
1
- import { findConfigPath } from '@eslint-config-snapshot/api'
1
+ import { DEFAULT_CONFIG, findConfigPath, type SnapshotConfig } from '@eslint-config-snapshot/api'
2
2
 
3
- import { countUniqueWorkspaces, decorateDiffLine, formatDiff, summarizeChanges, summarizeSnapshots } from '../formatters.js'
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,21 @@ export async function executeCheck(
42
51
  }
43
52
 
44
53
  let currentSnapshots: Map<string, BuiltSnapshot>
54
+ const skippedWorkspaces: SkippedWorkspace[] = []
55
+ let discoveredWorkspaces: string[] = []
56
+ const allowWorkspaceExtractionFailure = !foundConfig || isDefaultEquivalentConfig(foundConfig.config)
45
57
  try {
46
- currentSnapshots = await computeCurrentSnapshots(cwd)
58
+ currentSnapshots = await computeCurrentSnapshots(cwd, {
59
+ allowWorkspaceExtractionFailure,
60
+ onWorkspacesDiscovered: (workspacesRel) => {
61
+ discoveredWorkspaces = workspacesRel
62
+ },
63
+ onWorkspaceSkipped: (skipped) => {
64
+ skippedWorkspaces.push(skipped)
65
+ }
66
+ })
47
67
  } catch (error: unknown) {
48
- if (!foundConfig) {
68
+ if (!foundConfig && isWorkspaceDiscoveryDefaultsError(error)) {
49
69
  terminal.write(
50
70
  'Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n'
51
71
  )
@@ -54,6 +74,10 @@ export async function executeCheck(
54
74
 
55
75
  throw error
56
76
  }
77
+ if (!foundConfig) {
78
+ writeDiscoveredWorkspacesSummary(terminal, discoveredWorkspaces)
79
+ }
80
+ writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces)
57
81
  if (storedSnapshots.size === 0) {
58
82
  const summary = summarizeSnapshots(currentSnapshots)
59
83
  terminal.write(
@@ -126,9 +150,7 @@ function printWhatChanged(
126
150
  if (changes.length === 0) {
127
151
  terminal.write(color.green('✅ Great news: no snapshot drift detected.\n'))
128
152
  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
- )
153
+ terminal.write(formatBaselineSummaryLines(currentSummary, workspaceCount))
132
154
  writeEslintVersionSummary(terminal, eslintVersionsByGroup)
133
155
  return 0
134
156
  }
@@ -155,3 +177,25 @@ function printWhatChanged(
155
177
 
156
178
  return 1
157
179
  }
180
+
181
+ function isWorkspaceDiscoveryDefaultsError(error: unknown): boolean {
182
+ const message = error instanceof Error ? error.message : String(error)
183
+ return (
184
+ message.includes('Unable to discover workspaces') ||
185
+ message.includes('Unmatched workspaces') ||
186
+ message.includes('zero-config mode')
187
+ )
188
+ }
189
+
190
+ function isDefaultEquivalentConfig(config: SnapshotConfig): boolean {
191
+ return JSON.stringify(config) === JSON.stringify(DEFAULT_CONFIG)
192
+ }
193
+
194
+ function writeDiscoveredWorkspacesSummary(terminal: TerminalIO, workspacesRel: string[]): void {
195
+ if (workspacesRel.length === 0) {
196
+ terminal.subtle('Auto-discovered workspaces: none\n')
197
+ return
198
+ }
199
+
200
+ terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(', ')}\n`)
201
+ }
@@ -1,4 +1,4 @@
1
- import { findConfigPath, loadConfig } from '@eslint-config-snapshot/api'
1
+ import { DEFAULT_CONFIG, findConfigPath, loadConfig, type SnapshotConfig } from '@eslint-config-snapshot/api'
2
2
 
3
3
  import { formatShortConfig, formatShortPrint } from '../formatters.js'
4
4
  import { writeRunContextHeader } from '../run-context.js'
@@ -14,7 +14,8 @@ export async function executePrint(cwd: string, terminal: TerminalIO, snapshotDi
14
14
  if (terminal.showProgress) {
15
15
  terminal.subtle('🔎 Checking current ESLint configuration...\n')
16
16
  }
17
- const currentSnapshots = await computeCurrentSnapshots(cwd)
17
+ const allowWorkspaceExtractionFailure = !foundConfig || isDefaultEquivalentConfig(foundConfig.config)
18
+ const currentSnapshots = await computeCurrentSnapshots(cwd, { allowWorkspaceExtractionFailure })
18
19
 
19
20
  if (format === 'short') {
20
21
  terminal.write(formatShortPrint([...currentSnapshots.values()]))
@@ -56,3 +57,7 @@ export async function executeConfig(cwd: string, terminal: TerminalIO, snapshotD
56
57
 
57
58
  terminal.write(`${JSON.stringify(payload, null, 2)}\n`)
58
59
  }
60
+
61
+ function isDefaultEquivalentConfig(config: SnapshotConfig): boolean {
62
+ return JSON.stringify(config) === JSON.stringify(DEFAULT_CONFIG)
63
+ }
@@ -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
+ }