@eslint-config-snapshot/cli 0.1.4 → 0.2.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 +19 -0
- package/README.md +60 -0
- package/dist/index.cjs +190 -27
- package/dist/index.js +191 -27
- package/package.json +2 -2
- package/src/index.ts +236 -34
- package/test/cli.integration.test.ts +17 -4
- package/test/cli.terminal.integration.test.ts +43 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @eslint-config-snapshot/cli
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add effective config inspection command and recommended grouped init workflow with numeric workspace assignments.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- @eslint-config-snapshot/api@0.2.0
|
|
13
|
+
|
|
14
|
+
## 0.1.5
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Add package-level README files for npm pages with cross-links between CLI and API docs.
|
|
19
|
+
- Updated dependencies
|
|
20
|
+
- @eslint-config-snapshot/api@0.1.5
|
|
21
|
+
|
|
3
22
|
## 0.1.4
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @eslint-config-snapshot/cli
|
|
2
|
+
|
|
3
|
+
Deterministic ESLint config drift checker for workspaces.
|
|
4
|
+
|
|
5
|
+
`@eslint-config-snapshot/cli` snapshots effective ESLint rule state and reports drift after dependency or config changes.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -D @eslint-config-snapshot/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or run without install:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @eslint-config-snapshot/cli@latest --update
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
Create baseline:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
eslint-config-snapshot --update
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Check drift:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
eslint-config-snapshot
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
- `check`
|
|
36
|
+
- `update`
|
|
37
|
+
- `print`
|
|
38
|
+
- `config`
|
|
39
|
+
- `init`
|
|
40
|
+
|
|
41
|
+
Compatibility aliases:
|
|
42
|
+
|
|
43
|
+
- `snapshot` => `update`
|
|
44
|
+
- `compare` => `check --format diff`
|
|
45
|
+
- `status` => `check --format status`
|
|
46
|
+
- `what-changed` => `check --format summary`
|
|
47
|
+
|
|
48
|
+
## Notes
|
|
49
|
+
|
|
50
|
+
- Node.js `>=20` required.
|
|
51
|
+
- If no config is found, built-in defaults are used.
|
|
52
|
+
- Snapshots are stored under `.eslint-config-snapshot/`.
|
|
53
|
+
|
|
54
|
+
## Related Packages
|
|
55
|
+
|
|
56
|
+
- API engine: [`@eslint-config-snapshot/api`](https://www.npmjs.com/package/@eslint-config-snapshot/api)
|
|
57
|
+
|
|
58
|
+
## More Docs
|
|
59
|
+
|
|
60
|
+
- Project overview and full guides: [root README](https://github.com/gabrielmoreira/eslint-config-snapshot#readme)
|
package/dist/index.cjs
CHANGED
|
@@ -114,17 +114,23 @@ function createProgram(cwd, onActionExit) {
|
|
|
114
114
|
await executePrint(cwd, format);
|
|
115
115
|
onActionExit(0);
|
|
116
116
|
});
|
|
117
|
-
program.command("
|
|
117
|
+
program.command("config").description("Print effective evaluated config").option("--format <format>", "Output format: json|short", parsePrintFormat, "json").option("--short", "Alias for --format short").action(async (opts) => {
|
|
118
|
+
const format = opts.short ? "short" : opts.format;
|
|
119
|
+
await executeConfig(cwd, format);
|
|
120
|
+
onActionExit(0);
|
|
121
|
+
});
|
|
122
|
+
program.command("init").description("Initialize config (file or package.json)").option("--target <target>", "Config target: file|package-json", parseInitTarget).option("--preset <preset>", "Config preset: recommended|minimal|full", parseInitPreset).option("--show-effective", "Print the evaluated config that will be written").option("-f, --force", "Allow init even when an existing config is detected").option("-y, --yes", "Skip prompts and use defaults/options").addHelpText(
|
|
118
123
|
"after",
|
|
119
124
|
`
|
|
120
125
|
Examples:
|
|
121
126
|
$ eslint-config-snapshot init
|
|
122
127
|
Runs interactive numbered prompts:
|
|
123
128
|
target: 1) package-json, 2) file
|
|
124
|
-
preset: 1)
|
|
129
|
+
preset: 1) recommended, 2) minimal, 3) full
|
|
130
|
+
recommended preset supports per-workspace group number assignment.
|
|
125
131
|
|
|
126
|
-
$ eslint-config-snapshot init --yes --target package-json --preset
|
|
127
|
-
Non-interactive
|
|
132
|
+
$ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
|
|
133
|
+
Non-interactive recommended setup in package.json, with effective preview.
|
|
128
134
|
|
|
129
135
|
$ eslint-config-snapshot init --yes --force --target file --preset full
|
|
130
136
|
Overwrite-safe bypass when a config is already detected.
|
|
@@ -167,10 +173,10 @@ function parseInitTarget(value) {
|
|
|
167
173
|
}
|
|
168
174
|
function parseInitPreset(value) {
|
|
169
175
|
const normalized = value.trim().toLowerCase();
|
|
170
|
-
if (normalized === "minimal" || normalized === "full") {
|
|
176
|
+
if (normalized === "recommended" || normalized === "minimal" || normalized === "full") {
|
|
171
177
|
return normalized;
|
|
172
178
|
}
|
|
173
|
-
throw new import_commander.InvalidArgumentError("Expected one of: minimal, full");
|
|
179
|
+
throw new import_commander.InvalidArgumentError("Expected one of: recommended, minimal, full");
|
|
174
180
|
}
|
|
175
181
|
async function executeCheck(cwd, format, defaultInvocation = false) {
|
|
176
182
|
const foundConfig = await (0, import_api.findConfigPath)(cwd);
|
|
@@ -281,17 +287,31 @@ async function executePrint(cwd, format) {
|
|
|
281
287
|
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
282
288
|
`);
|
|
283
289
|
}
|
|
284
|
-
async function
|
|
290
|
+
async function executeConfig(cwd, format) {
|
|
291
|
+
const foundConfig = await (0, import_api.findConfigPath)(cwd);
|
|
285
292
|
const config = await (0, import_api.loadConfig)(cwd);
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
293
|
+
const resolved = await resolveWorkspaceAssignments(cwd, config);
|
|
294
|
+
const payload = {
|
|
295
|
+
source: foundConfig?.path ?? "built-in-defaults",
|
|
296
|
+
workspaceInput: config.workspaceInput,
|
|
297
|
+
workspaces: resolved.discovery.workspacesRel,
|
|
298
|
+
grouping: {
|
|
299
|
+
mode: config.grouping.mode,
|
|
300
|
+
allowEmptyGroups: config.grouping.allowEmptyGroups ?? false,
|
|
301
|
+
groups: resolved.assignments.map((entry) => ({ name: entry.name, workspaces: entry.workspaces }))
|
|
302
|
+
},
|
|
303
|
+
sampling: config.sampling
|
|
304
|
+
};
|
|
305
|
+
if (format === "short") {
|
|
306
|
+
process.stdout.write(formatShortConfig(payload));
|
|
307
|
+
return;
|
|
294
308
|
}
|
|
309
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
310
|
+
`);
|
|
311
|
+
}
|
|
312
|
+
async function computeCurrentSnapshots(cwd) {
|
|
313
|
+
const config = await (0, import_api.loadConfig)(cwd);
|
|
314
|
+
const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
|
|
295
315
|
const snapshots = /* @__PURE__ */ new Map();
|
|
296
316
|
for (const group of assignments) {
|
|
297
317
|
const extractedForGroup = [];
|
|
@@ -326,6 +346,18 @@ async function computeCurrentSnapshots(cwd) {
|
|
|
326
346
|
}
|
|
327
347
|
return snapshots;
|
|
328
348
|
}
|
|
349
|
+
async function resolveWorkspaceAssignments(cwd, config) {
|
|
350
|
+
const discovery = await (0, import_api.discoverWorkspaces)({ cwd, workspaceInput: config.workspaceInput });
|
|
351
|
+
const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : (0, import_api.assignGroupsByMatch)(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
|
|
352
|
+
const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false;
|
|
353
|
+
if (!allowEmptyGroups) {
|
|
354
|
+
const empty = assignments.filter((group) => group.workspaces.length === 0);
|
|
355
|
+
if (empty.length > 0) {
|
|
356
|
+
throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(", ")}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return { discovery, assignments };
|
|
360
|
+
}
|
|
329
361
|
async function loadStoredSnapshots(cwd) {
|
|
330
362
|
const dir = import_node_path.default.join(cwd, SNAPSHOT_DIR);
|
|
331
363
|
const files = await (0, import_fast_glob.default)("**/*.json", { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true });
|
|
@@ -407,6 +439,7 @@ function getDisplayOptionChanges(diff) {
|
|
|
407
439
|
}
|
|
408
440
|
async function runInit(cwd, opts = {}) {
|
|
409
441
|
const force = opts.force ?? false;
|
|
442
|
+
const showEffective = opts.showEffective ?? false;
|
|
410
443
|
const existing = await (0, import_api.findConfigPath)(cwd);
|
|
411
444
|
if (existing && !force) {
|
|
412
445
|
process.stderr.write(
|
|
@@ -423,11 +456,17 @@ async function runInit(cwd, opts = {}) {
|
|
|
423
456
|
preset = interactive.preset;
|
|
424
457
|
}
|
|
425
458
|
const finalTarget = target ?? "file";
|
|
426
|
-
const finalPreset = preset ?? "
|
|
459
|
+
const finalPreset = preset ?? "recommended";
|
|
460
|
+
const configObject = await resolveInitConfigObject(cwd, finalPreset, Boolean(opts.yes));
|
|
461
|
+
if (showEffective) {
|
|
462
|
+
process.stdout.write(`Effective config preview:
|
|
463
|
+
${JSON.stringify(configObject, null, 2)}
|
|
464
|
+
`);
|
|
465
|
+
}
|
|
427
466
|
if (finalTarget === "package-json") {
|
|
428
|
-
return runInitInPackageJson(cwd,
|
|
467
|
+
return runInitInPackageJson(cwd, configObject, force);
|
|
429
468
|
}
|
|
430
|
-
return runInitInFile(cwd,
|
|
469
|
+
return runInitInFile(cwd, configObject, force);
|
|
431
470
|
}
|
|
432
471
|
async function askInitPreferences() {
|
|
433
472
|
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
@@ -454,12 +493,15 @@ async function askInitTarget(rl) {
|
|
|
454
493
|
}
|
|
455
494
|
async function askInitPreset(rl) {
|
|
456
495
|
while (true) {
|
|
457
|
-
const answer = await askQuestion(
|
|
496
|
+
const answer = await askQuestion(
|
|
497
|
+
rl,
|
|
498
|
+
"Select preset:\n 1) recommended (group by workspace numbers)\n 2) minimal\n 3) full\nChoose [1]: "
|
|
499
|
+
);
|
|
458
500
|
const parsed = parseInitPresetChoice(answer);
|
|
459
501
|
if (parsed) {
|
|
460
502
|
return parsed;
|
|
461
503
|
}
|
|
462
|
-
process.stdout.write("Please choose 1 (minimal) or
|
|
504
|
+
process.stdout.write("Please choose 1 (recommended), 2 (minimal), or 3 (full).\n");
|
|
463
505
|
}
|
|
464
506
|
}
|
|
465
507
|
function parseInitTargetChoice(value) {
|
|
@@ -478,12 +520,15 @@ function parseInitTargetChoice(value) {
|
|
|
478
520
|
function parseInitPresetChoice(value) {
|
|
479
521
|
const normalized = value.trim().toLowerCase();
|
|
480
522
|
if (normalized === "") {
|
|
481
|
-
return "
|
|
523
|
+
return "recommended";
|
|
524
|
+
}
|
|
525
|
+
if (normalized === "1" || normalized === "recommended" || normalized === "rec" || normalized === "grouped") {
|
|
526
|
+
return "recommended";
|
|
482
527
|
}
|
|
483
|
-
if (normalized === "
|
|
528
|
+
if (normalized === "2" || normalized === "minimal" || normalized === "min") {
|
|
484
529
|
return "minimal";
|
|
485
530
|
}
|
|
486
|
-
if (normalized === "
|
|
531
|
+
if (normalized === "3" || normalized === "full") {
|
|
487
532
|
return "full";
|
|
488
533
|
}
|
|
489
534
|
return void 0;
|
|
@@ -508,7 +553,7 @@ async function askYesNo(prompt, defaultYes) {
|
|
|
508
553
|
rl.close();
|
|
509
554
|
}
|
|
510
555
|
}
|
|
511
|
-
async function runInitInFile(cwd,
|
|
556
|
+
async function runInitInFile(cwd, configObject, force) {
|
|
512
557
|
const candidates = [
|
|
513
558
|
".eslint-config-snapshot.js",
|
|
514
559
|
".eslint-config-snapshot.cjs",
|
|
@@ -529,12 +574,12 @@ async function runInitInFile(cwd, preset, force) {
|
|
|
529
574
|
}
|
|
530
575
|
}
|
|
531
576
|
const target = import_node_path.default.join(cwd, "eslint-config-snapshot.config.mjs");
|
|
532
|
-
await (0, import_promises.writeFile)(target, (
|
|
577
|
+
await (0, import_promises.writeFile)(target, toConfigScaffold(configObject), "utf8");
|
|
533
578
|
process.stdout.write(`Created ${import_node_path.default.basename(target)}
|
|
534
579
|
`);
|
|
535
580
|
return 0;
|
|
536
581
|
}
|
|
537
|
-
async function runInitInPackageJson(cwd,
|
|
582
|
+
async function runInitInPackageJson(cwd, configObject, force) {
|
|
538
583
|
const packageJsonPath = import_node_path.default.join(cwd, "package.json");
|
|
539
584
|
let packageJsonRaw;
|
|
540
585
|
try {
|
|
@@ -554,12 +599,117 @@ async function runInitInPackageJson(cwd, preset, force) {
|
|
|
554
599
|
process.stderr.write("Config already exists in package.json: eslint-config-snapshot\n");
|
|
555
600
|
return 1;
|
|
556
601
|
}
|
|
557
|
-
parsed["eslint-config-snapshot"] =
|
|
602
|
+
parsed["eslint-config-snapshot"] = configObject;
|
|
558
603
|
await (0, import_promises.writeFile)(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
|
|
559
604
|
`, "utf8");
|
|
560
605
|
process.stdout.write('Created config in package.json under "eslint-config-snapshot"\n');
|
|
561
606
|
return 0;
|
|
562
607
|
}
|
|
608
|
+
async function resolveInitConfigObject(cwd, preset, nonInteractive) {
|
|
609
|
+
if (preset === "minimal") {
|
|
610
|
+
return {};
|
|
611
|
+
}
|
|
612
|
+
if (preset === "full") {
|
|
613
|
+
return getFullPresetObject();
|
|
614
|
+
}
|
|
615
|
+
return buildRecommendedPresetObject(cwd, nonInteractive);
|
|
616
|
+
}
|
|
617
|
+
async function buildRecommendedPresetObject(cwd, nonInteractive) {
|
|
618
|
+
const workspaces = await discoverInitWorkspaces(cwd);
|
|
619
|
+
const assignments = new Map(workspaces.map((workspace) => [workspace, 1]));
|
|
620
|
+
if (!nonInteractive && process.stdin.isTTY && process.stdout.isTTY) {
|
|
621
|
+
process.stdout.write("Recommended setup: assign a group number for each workspace (default: 1).\n");
|
|
622
|
+
for (const workspace of workspaces) {
|
|
623
|
+
const answer = await askGroupNumber(workspace);
|
|
624
|
+
assignments.set(workspace, answer);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const groupNumbers = [...new Set(assignments.values())].sort((a, b) => a - b);
|
|
628
|
+
const groups = groupNumbers.map((number) => ({
|
|
629
|
+
name: `group-${number}`,
|
|
630
|
+
match: workspaces.filter((workspace) => assignments.get(workspace) === number)
|
|
631
|
+
}));
|
|
632
|
+
return {
|
|
633
|
+
workspaceInput: { mode: "manual", workspaces },
|
|
634
|
+
grouping: {
|
|
635
|
+
mode: "match",
|
|
636
|
+
groups
|
|
637
|
+
},
|
|
638
|
+
sampling: {
|
|
639
|
+
maxFilesPerWorkspace: 8,
|
|
640
|
+
includeGlobs: ["**/*.{js,jsx,ts,tsx,cjs,mjs}"],
|
|
641
|
+
excludeGlobs: ["**/node_modules/**", "**/dist/**"],
|
|
642
|
+
hintGlobs: []
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
async function discoverInitWorkspaces(cwd) {
|
|
647
|
+
const discovered = await (0, import_api.discoverWorkspaces)({ cwd, workspaceInput: { mode: "discover" } });
|
|
648
|
+
if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === ".")) {
|
|
649
|
+
return discovered.workspacesRel;
|
|
650
|
+
}
|
|
651
|
+
const packageJsonPath = import_node_path.default.join(cwd, "package.json");
|
|
652
|
+
try {
|
|
653
|
+
const raw = await (0, import_promises.readFile)(packageJsonPath, "utf8");
|
|
654
|
+
const parsed = JSON.parse(raw);
|
|
655
|
+
let workspacePatterns = [];
|
|
656
|
+
if (Array.isArray(parsed.workspaces)) {
|
|
657
|
+
workspacePatterns = parsed.workspaces;
|
|
658
|
+
} else if (parsed.workspaces && typeof parsed.workspaces === "object" && Array.isArray(parsed.workspaces.packages)) {
|
|
659
|
+
workspacePatterns = parsed.workspaces.packages;
|
|
660
|
+
}
|
|
661
|
+
if (workspacePatterns.length === 0) {
|
|
662
|
+
return discovered.workspacesRel;
|
|
663
|
+
}
|
|
664
|
+
const workspacePackageFiles = await (0, import_fast_glob.default)(
|
|
665
|
+
workspacePatterns.map((pattern) => `${trimTrailingSlashes(pattern)}/package.json`),
|
|
666
|
+
{ cwd, onlyFiles: true, dot: true }
|
|
667
|
+
);
|
|
668
|
+
const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => (0, import_api.normalizePath)(import_node_path.default.dirname(entry))))].sort(
|
|
669
|
+
(a, b) => a.localeCompare(b)
|
|
670
|
+
);
|
|
671
|
+
if (workspaceDirs.length > 0) {
|
|
672
|
+
return workspaceDirs;
|
|
673
|
+
}
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
return discovered.workspacesRel;
|
|
677
|
+
}
|
|
678
|
+
function trimTrailingSlashes(value) {
|
|
679
|
+
let normalized = value;
|
|
680
|
+
while (normalized.endsWith("/")) {
|
|
681
|
+
normalized = normalized.slice(0, -1);
|
|
682
|
+
}
|
|
683
|
+
return normalized;
|
|
684
|
+
}
|
|
685
|
+
async function askGroupNumber(workspace) {
|
|
686
|
+
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
687
|
+
try {
|
|
688
|
+
while (true) {
|
|
689
|
+
const answer = await askQuestion(rl, `Group number for ${workspace} [1]: `);
|
|
690
|
+
const normalized = answer.trim();
|
|
691
|
+
if (normalized === "") {
|
|
692
|
+
return 1;
|
|
693
|
+
}
|
|
694
|
+
if (/^\d+$/.test(normalized)) {
|
|
695
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
696
|
+
if (parsed >= 1) {
|
|
697
|
+
return parsed;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
process.stdout.write("Please provide a positive integer (1, 2, 3, ...).\n");
|
|
701
|
+
}
|
|
702
|
+
} finally {
|
|
703
|
+
rl.close();
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function toConfigScaffold(configObject) {
|
|
707
|
+
if (Object.keys(configObject).length === 0) {
|
|
708
|
+
return (0, import_api.getConfigScaffold)("minimal");
|
|
709
|
+
}
|
|
710
|
+
return `export default ${JSON.stringify(configObject, null, 2)}
|
|
711
|
+
`;
|
|
712
|
+
}
|
|
563
713
|
function getFullPresetObject() {
|
|
564
714
|
return {
|
|
565
715
|
workspaceInput: { mode: "discover" },
|
|
@@ -711,6 +861,19 @@ function formatShortPrint(snapshots) {
|
|
|
711
861
|
return `${lines.join("\n")}
|
|
712
862
|
`;
|
|
713
863
|
}
|
|
864
|
+
function formatShortConfig(payload) {
|
|
865
|
+
const lines = [
|
|
866
|
+
`source: ${payload.source}`,
|
|
867
|
+
`workspaces (${payload.workspaces.length}): ${payload.workspaces.join(", ") || "(none)"}`,
|
|
868
|
+
`grouping mode: ${payload.grouping.mode} (allow empty: ${payload.grouping.allowEmptyGroups})`
|
|
869
|
+
];
|
|
870
|
+
for (const group of payload.grouping.groups) {
|
|
871
|
+
lines.push(`group ${group.name} (${group.workspaces.length}): ${group.workspaces.join(", ") || "(none)"}`);
|
|
872
|
+
}
|
|
873
|
+
lines.push(`workspaceInput: ${JSON.stringify(payload.workspaceInput)}`, `sampling: ${JSON.stringify(payload.sampling)}`);
|
|
874
|
+
return `${lines.join("\n")}
|
|
875
|
+
`;
|
|
876
|
+
}
|
|
714
877
|
// Annotate the CommonJS export names for ESM import in node:
|
|
715
878
|
0 && (module.exports = {
|
|
716
879
|
main,
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getConfigScaffold,
|
|
13
13
|
hasDiff,
|
|
14
14
|
loadConfig,
|
|
15
|
+
normalizePath,
|
|
15
16
|
readSnapshotFile,
|
|
16
17
|
sampleWorkspaceFiles,
|
|
17
18
|
writeSnapshotFile
|
|
@@ -92,17 +93,23 @@ function createProgram(cwd, onActionExit) {
|
|
|
92
93
|
await executePrint(cwd, format);
|
|
93
94
|
onActionExit(0);
|
|
94
95
|
});
|
|
95
|
-
program.command("
|
|
96
|
+
program.command("config").description("Print effective evaluated config").option("--format <format>", "Output format: json|short", parsePrintFormat, "json").option("--short", "Alias for --format short").action(async (opts) => {
|
|
97
|
+
const format = opts.short ? "short" : opts.format;
|
|
98
|
+
await executeConfig(cwd, format);
|
|
99
|
+
onActionExit(0);
|
|
100
|
+
});
|
|
101
|
+
program.command("init").description("Initialize config (file or package.json)").option("--target <target>", "Config target: file|package-json", parseInitTarget).option("--preset <preset>", "Config preset: recommended|minimal|full", parseInitPreset).option("--show-effective", "Print the evaluated config that will be written").option("-f, --force", "Allow init even when an existing config is detected").option("-y, --yes", "Skip prompts and use defaults/options").addHelpText(
|
|
96
102
|
"after",
|
|
97
103
|
`
|
|
98
104
|
Examples:
|
|
99
105
|
$ eslint-config-snapshot init
|
|
100
106
|
Runs interactive numbered prompts:
|
|
101
107
|
target: 1) package-json, 2) file
|
|
102
|
-
preset: 1)
|
|
108
|
+
preset: 1) recommended, 2) minimal, 3) full
|
|
109
|
+
recommended preset supports per-workspace group number assignment.
|
|
103
110
|
|
|
104
|
-
$ eslint-config-snapshot init --yes --target package-json --preset
|
|
105
|
-
Non-interactive
|
|
111
|
+
$ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
|
|
112
|
+
Non-interactive recommended setup in package.json, with effective preview.
|
|
106
113
|
|
|
107
114
|
$ eslint-config-snapshot init --yes --force --target file --preset full
|
|
108
115
|
Overwrite-safe bypass when a config is already detected.
|
|
@@ -145,10 +152,10 @@ function parseInitTarget(value) {
|
|
|
145
152
|
}
|
|
146
153
|
function parseInitPreset(value) {
|
|
147
154
|
const normalized = value.trim().toLowerCase();
|
|
148
|
-
if (normalized === "minimal" || normalized === "full") {
|
|
155
|
+
if (normalized === "recommended" || normalized === "minimal" || normalized === "full") {
|
|
149
156
|
return normalized;
|
|
150
157
|
}
|
|
151
|
-
throw new InvalidArgumentError("Expected one of: minimal, full");
|
|
158
|
+
throw new InvalidArgumentError("Expected one of: recommended, minimal, full");
|
|
152
159
|
}
|
|
153
160
|
async function executeCheck(cwd, format, defaultInvocation = false) {
|
|
154
161
|
const foundConfig = await findConfigPath(cwd);
|
|
@@ -259,17 +266,31 @@ async function executePrint(cwd, format) {
|
|
|
259
266
|
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
260
267
|
`);
|
|
261
268
|
}
|
|
262
|
-
async function
|
|
269
|
+
async function executeConfig(cwd, format) {
|
|
270
|
+
const foundConfig = await findConfigPath(cwd);
|
|
263
271
|
const config = await loadConfig(cwd);
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
+
const resolved = await resolveWorkspaceAssignments(cwd, config);
|
|
273
|
+
const payload = {
|
|
274
|
+
source: foundConfig?.path ?? "built-in-defaults",
|
|
275
|
+
workspaceInput: config.workspaceInput,
|
|
276
|
+
workspaces: resolved.discovery.workspacesRel,
|
|
277
|
+
grouping: {
|
|
278
|
+
mode: config.grouping.mode,
|
|
279
|
+
allowEmptyGroups: config.grouping.allowEmptyGroups ?? false,
|
|
280
|
+
groups: resolved.assignments.map((entry) => ({ name: entry.name, workspaces: entry.workspaces }))
|
|
281
|
+
},
|
|
282
|
+
sampling: config.sampling
|
|
283
|
+
};
|
|
284
|
+
if (format === "short") {
|
|
285
|
+
process.stdout.write(formatShortConfig(payload));
|
|
286
|
+
return;
|
|
272
287
|
}
|
|
288
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
289
|
+
`);
|
|
290
|
+
}
|
|
291
|
+
async function computeCurrentSnapshots(cwd) {
|
|
292
|
+
const config = await loadConfig(cwd);
|
|
293
|
+
const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
|
|
273
294
|
const snapshots = /* @__PURE__ */ new Map();
|
|
274
295
|
for (const group of assignments) {
|
|
275
296
|
const extractedForGroup = [];
|
|
@@ -304,6 +325,18 @@ async function computeCurrentSnapshots(cwd) {
|
|
|
304
325
|
}
|
|
305
326
|
return snapshots;
|
|
306
327
|
}
|
|
328
|
+
async function resolveWorkspaceAssignments(cwd, config) {
|
|
329
|
+
const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput });
|
|
330
|
+
const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
|
|
331
|
+
const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false;
|
|
332
|
+
if (!allowEmptyGroups) {
|
|
333
|
+
const empty = assignments.filter((group) => group.workspaces.length === 0);
|
|
334
|
+
if (empty.length > 0) {
|
|
335
|
+
throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(", ")}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { discovery, assignments };
|
|
339
|
+
}
|
|
307
340
|
async function loadStoredSnapshots(cwd) {
|
|
308
341
|
const dir = path.join(cwd, SNAPSHOT_DIR);
|
|
309
342
|
const files = await fg("**/*.json", { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true });
|
|
@@ -385,6 +418,7 @@ function getDisplayOptionChanges(diff) {
|
|
|
385
418
|
}
|
|
386
419
|
async function runInit(cwd, opts = {}) {
|
|
387
420
|
const force = opts.force ?? false;
|
|
421
|
+
const showEffective = opts.showEffective ?? false;
|
|
388
422
|
const existing = await findConfigPath(cwd);
|
|
389
423
|
if (existing && !force) {
|
|
390
424
|
process.stderr.write(
|
|
@@ -401,11 +435,17 @@ async function runInit(cwd, opts = {}) {
|
|
|
401
435
|
preset = interactive.preset;
|
|
402
436
|
}
|
|
403
437
|
const finalTarget = target ?? "file";
|
|
404
|
-
const finalPreset = preset ?? "
|
|
438
|
+
const finalPreset = preset ?? "recommended";
|
|
439
|
+
const configObject = await resolveInitConfigObject(cwd, finalPreset, Boolean(opts.yes));
|
|
440
|
+
if (showEffective) {
|
|
441
|
+
process.stdout.write(`Effective config preview:
|
|
442
|
+
${JSON.stringify(configObject, null, 2)}
|
|
443
|
+
`);
|
|
444
|
+
}
|
|
405
445
|
if (finalTarget === "package-json") {
|
|
406
|
-
return runInitInPackageJson(cwd,
|
|
446
|
+
return runInitInPackageJson(cwd, configObject, force);
|
|
407
447
|
}
|
|
408
|
-
return runInitInFile(cwd,
|
|
448
|
+
return runInitInFile(cwd, configObject, force);
|
|
409
449
|
}
|
|
410
450
|
async function askInitPreferences() {
|
|
411
451
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -432,12 +472,15 @@ async function askInitTarget(rl) {
|
|
|
432
472
|
}
|
|
433
473
|
async function askInitPreset(rl) {
|
|
434
474
|
while (true) {
|
|
435
|
-
const answer = await askQuestion(
|
|
475
|
+
const answer = await askQuestion(
|
|
476
|
+
rl,
|
|
477
|
+
"Select preset:\n 1) recommended (group by workspace numbers)\n 2) minimal\n 3) full\nChoose [1]: "
|
|
478
|
+
);
|
|
436
479
|
const parsed = parseInitPresetChoice(answer);
|
|
437
480
|
if (parsed) {
|
|
438
481
|
return parsed;
|
|
439
482
|
}
|
|
440
|
-
process.stdout.write("Please choose 1 (minimal) or
|
|
483
|
+
process.stdout.write("Please choose 1 (recommended), 2 (minimal), or 3 (full).\n");
|
|
441
484
|
}
|
|
442
485
|
}
|
|
443
486
|
function parseInitTargetChoice(value) {
|
|
@@ -456,12 +499,15 @@ function parseInitTargetChoice(value) {
|
|
|
456
499
|
function parseInitPresetChoice(value) {
|
|
457
500
|
const normalized = value.trim().toLowerCase();
|
|
458
501
|
if (normalized === "") {
|
|
459
|
-
return "
|
|
502
|
+
return "recommended";
|
|
503
|
+
}
|
|
504
|
+
if (normalized === "1" || normalized === "recommended" || normalized === "rec" || normalized === "grouped") {
|
|
505
|
+
return "recommended";
|
|
460
506
|
}
|
|
461
|
-
if (normalized === "
|
|
507
|
+
if (normalized === "2" || normalized === "minimal" || normalized === "min") {
|
|
462
508
|
return "minimal";
|
|
463
509
|
}
|
|
464
|
-
if (normalized === "
|
|
510
|
+
if (normalized === "3" || normalized === "full") {
|
|
465
511
|
return "full";
|
|
466
512
|
}
|
|
467
513
|
return void 0;
|
|
@@ -486,7 +532,7 @@ async function askYesNo(prompt, defaultYes) {
|
|
|
486
532
|
rl.close();
|
|
487
533
|
}
|
|
488
534
|
}
|
|
489
|
-
async function runInitInFile(cwd,
|
|
535
|
+
async function runInitInFile(cwd, configObject, force) {
|
|
490
536
|
const candidates = [
|
|
491
537
|
".eslint-config-snapshot.js",
|
|
492
538
|
".eslint-config-snapshot.cjs",
|
|
@@ -507,12 +553,12 @@ async function runInitInFile(cwd, preset, force) {
|
|
|
507
553
|
}
|
|
508
554
|
}
|
|
509
555
|
const target = path.join(cwd, "eslint-config-snapshot.config.mjs");
|
|
510
|
-
await writeFile(target,
|
|
556
|
+
await writeFile(target, toConfigScaffold(configObject), "utf8");
|
|
511
557
|
process.stdout.write(`Created ${path.basename(target)}
|
|
512
558
|
`);
|
|
513
559
|
return 0;
|
|
514
560
|
}
|
|
515
|
-
async function runInitInPackageJson(cwd,
|
|
561
|
+
async function runInitInPackageJson(cwd, configObject, force) {
|
|
516
562
|
const packageJsonPath = path.join(cwd, "package.json");
|
|
517
563
|
let packageJsonRaw;
|
|
518
564
|
try {
|
|
@@ -532,12 +578,117 @@ async function runInitInPackageJson(cwd, preset, force) {
|
|
|
532
578
|
process.stderr.write("Config already exists in package.json: eslint-config-snapshot\n");
|
|
533
579
|
return 1;
|
|
534
580
|
}
|
|
535
|
-
parsed["eslint-config-snapshot"] =
|
|
581
|
+
parsed["eslint-config-snapshot"] = configObject;
|
|
536
582
|
await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
|
|
537
583
|
`, "utf8");
|
|
538
584
|
process.stdout.write('Created config in package.json under "eslint-config-snapshot"\n');
|
|
539
585
|
return 0;
|
|
540
586
|
}
|
|
587
|
+
async function resolveInitConfigObject(cwd, preset, nonInteractive) {
|
|
588
|
+
if (preset === "minimal") {
|
|
589
|
+
return {};
|
|
590
|
+
}
|
|
591
|
+
if (preset === "full") {
|
|
592
|
+
return getFullPresetObject();
|
|
593
|
+
}
|
|
594
|
+
return buildRecommendedPresetObject(cwd, nonInteractive);
|
|
595
|
+
}
|
|
596
|
+
async function buildRecommendedPresetObject(cwd, nonInteractive) {
|
|
597
|
+
const workspaces = await discoverInitWorkspaces(cwd);
|
|
598
|
+
const assignments = new Map(workspaces.map((workspace) => [workspace, 1]));
|
|
599
|
+
if (!nonInteractive && process.stdin.isTTY && process.stdout.isTTY) {
|
|
600
|
+
process.stdout.write("Recommended setup: assign a group number for each workspace (default: 1).\n");
|
|
601
|
+
for (const workspace of workspaces) {
|
|
602
|
+
const answer = await askGroupNumber(workspace);
|
|
603
|
+
assignments.set(workspace, answer);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const groupNumbers = [...new Set(assignments.values())].sort((a, b) => a - b);
|
|
607
|
+
const groups = groupNumbers.map((number) => ({
|
|
608
|
+
name: `group-${number}`,
|
|
609
|
+
match: workspaces.filter((workspace) => assignments.get(workspace) === number)
|
|
610
|
+
}));
|
|
611
|
+
return {
|
|
612
|
+
workspaceInput: { mode: "manual", workspaces },
|
|
613
|
+
grouping: {
|
|
614
|
+
mode: "match",
|
|
615
|
+
groups
|
|
616
|
+
},
|
|
617
|
+
sampling: {
|
|
618
|
+
maxFilesPerWorkspace: 8,
|
|
619
|
+
includeGlobs: ["**/*.{js,jsx,ts,tsx,cjs,mjs}"],
|
|
620
|
+
excludeGlobs: ["**/node_modules/**", "**/dist/**"],
|
|
621
|
+
hintGlobs: []
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
async function discoverInitWorkspaces(cwd) {
|
|
626
|
+
const discovered = await discoverWorkspaces({ cwd, workspaceInput: { mode: "discover" } });
|
|
627
|
+
if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === ".")) {
|
|
628
|
+
return discovered.workspacesRel;
|
|
629
|
+
}
|
|
630
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
631
|
+
try {
|
|
632
|
+
const raw = await readFile(packageJsonPath, "utf8");
|
|
633
|
+
const parsed = JSON.parse(raw);
|
|
634
|
+
let workspacePatterns = [];
|
|
635
|
+
if (Array.isArray(parsed.workspaces)) {
|
|
636
|
+
workspacePatterns = parsed.workspaces;
|
|
637
|
+
} else if (parsed.workspaces && typeof parsed.workspaces === "object" && Array.isArray(parsed.workspaces.packages)) {
|
|
638
|
+
workspacePatterns = parsed.workspaces.packages;
|
|
639
|
+
}
|
|
640
|
+
if (workspacePatterns.length === 0) {
|
|
641
|
+
return discovered.workspacesRel;
|
|
642
|
+
}
|
|
643
|
+
const workspacePackageFiles = await fg(
|
|
644
|
+
workspacePatterns.map((pattern) => `${trimTrailingSlashes(pattern)}/package.json`),
|
|
645
|
+
{ cwd, onlyFiles: true, dot: true }
|
|
646
|
+
);
|
|
647
|
+
const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath(path.dirname(entry))))].sort(
|
|
648
|
+
(a, b) => a.localeCompare(b)
|
|
649
|
+
);
|
|
650
|
+
if (workspaceDirs.length > 0) {
|
|
651
|
+
return workspaceDirs;
|
|
652
|
+
}
|
|
653
|
+
} catch {
|
|
654
|
+
}
|
|
655
|
+
return discovered.workspacesRel;
|
|
656
|
+
}
|
|
657
|
+
function trimTrailingSlashes(value) {
|
|
658
|
+
let normalized = value;
|
|
659
|
+
while (normalized.endsWith("/")) {
|
|
660
|
+
normalized = normalized.slice(0, -1);
|
|
661
|
+
}
|
|
662
|
+
return normalized;
|
|
663
|
+
}
|
|
664
|
+
async function askGroupNumber(workspace) {
|
|
665
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
666
|
+
try {
|
|
667
|
+
while (true) {
|
|
668
|
+
const answer = await askQuestion(rl, `Group number for ${workspace} [1]: `);
|
|
669
|
+
const normalized = answer.trim();
|
|
670
|
+
if (normalized === "") {
|
|
671
|
+
return 1;
|
|
672
|
+
}
|
|
673
|
+
if (/^\d+$/.test(normalized)) {
|
|
674
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
675
|
+
if (parsed >= 1) {
|
|
676
|
+
return parsed;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
process.stdout.write("Please provide a positive integer (1, 2, 3, ...).\n");
|
|
680
|
+
}
|
|
681
|
+
} finally {
|
|
682
|
+
rl.close();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function toConfigScaffold(configObject) {
|
|
686
|
+
if (Object.keys(configObject).length === 0) {
|
|
687
|
+
return getConfigScaffold("minimal");
|
|
688
|
+
}
|
|
689
|
+
return `export default ${JSON.stringify(configObject, null, 2)}
|
|
690
|
+
`;
|
|
691
|
+
}
|
|
541
692
|
function getFullPresetObject() {
|
|
542
693
|
return {
|
|
543
694
|
workspaceInput: { mode: "discover" },
|
|
@@ -689,6 +840,19 @@ function formatShortPrint(snapshots) {
|
|
|
689
840
|
return `${lines.join("\n")}
|
|
690
841
|
`;
|
|
691
842
|
}
|
|
843
|
+
function formatShortConfig(payload) {
|
|
844
|
+
const lines = [
|
|
845
|
+
`source: ${payload.source}`,
|
|
846
|
+
`workspaces (${payload.workspaces.length}): ${payload.workspaces.join(", ") || "(none)"}`,
|
|
847
|
+
`grouping mode: ${payload.grouping.mode} (allow empty: ${payload.grouping.allowEmptyGroups})`
|
|
848
|
+
];
|
|
849
|
+
for (const group of payload.grouping.groups) {
|
|
850
|
+
lines.push(`group ${group.name} (${group.workspaces.length}): ${group.workspaces.join(", ") || "(none)"}`);
|
|
851
|
+
}
|
|
852
|
+
lines.push(`workspaceInput: ${JSON.stringify(payload.workspaceInput)}`, `sampling: ${JSON.stringify(payload.sampling)}`);
|
|
853
|
+
return `${lines.join("\n")}
|
|
854
|
+
`;
|
|
855
|
+
}
|
|
692
856
|
export {
|
|
693
857
|
main,
|
|
694
858
|
parseInitPresetChoice,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eslint-config-snapshot/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,6 +29,6 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"commander": "^14.0.3",
|
|
31
31
|
"fast-glob": "^3.3.3",
|
|
32
|
-
"@eslint-config-snapshot/api": "0.
|
|
32
|
+
"@eslint-config-snapshot/api": "0.2.0"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getConfigScaffold,
|
|
11
11
|
hasDiff,
|
|
12
12
|
loadConfig,
|
|
13
|
+
normalizePath,
|
|
13
14
|
readSnapshotFile,
|
|
14
15
|
sampleWorkspaceFiles,
|
|
15
16
|
writeSnapshotFile
|
|
@@ -30,7 +31,7 @@ type SnapshotDiff = ReturnType<typeof diffSnapshots>
|
|
|
30
31
|
type CheckFormat = 'summary' | 'status' | 'diff'
|
|
31
32
|
type PrintFormat = 'json' | 'short'
|
|
32
33
|
type InitTarget = 'file' | 'package-json'
|
|
33
|
-
type InitPreset = 'minimal' | 'full'
|
|
34
|
+
type InitPreset = 'recommended' | 'minimal' | 'full'
|
|
34
35
|
|
|
35
36
|
type RootOptions = {
|
|
36
37
|
update?: boolean
|
|
@@ -138,11 +139,23 @@ function createProgram(cwd: string, onActionExit: (code: number) => void): Comma
|
|
|
138
139
|
onActionExit(0)
|
|
139
140
|
})
|
|
140
141
|
|
|
142
|
+
program
|
|
143
|
+
.command('config')
|
|
144
|
+
.description('Print effective evaluated config')
|
|
145
|
+
.option('--format <format>', 'Output format: json|short', parsePrintFormat, 'json')
|
|
146
|
+
.option('--short', 'Alias for --format short')
|
|
147
|
+
.action(async (opts: { format: PrintFormat; short?: boolean }) => {
|
|
148
|
+
const format: PrintFormat = opts.short ? 'short' : opts.format
|
|
149
|
+
await executeConfig(cwd, format)
|
|
150
|
+
onActionExit(0)
|
|
151
|
+
})
|
|
152
|
+
|
|
141
153
|
program
|
|
142
154
|
.command('init')
|
|
143
155
|
.description('Initialize config (file or package.json)')
|
|
144
156
|
.option('--target <target>', 'Config target: file|package-json', parseInitTarget)
|
|
145
|
-
.option('--preset <preset>', 'Config preset: minimal|full', parseInitPreset)
|
|
157
|
+
.option('--preset <preset>', 'Config preset: recommended|minimal|full', parseInitPreset)
|
|
158
|
+
.option('--show-effective', 'Print the evaluated config that will be written')
|
|
146
159
|
.option('-f, --force', 'Allow init even when an existing config is detected')
|
|
147
160
|
.option('-y, --yes', 'Skip prompts and use defaults/options')
|
|
148
161
|
.addHelpText(
|
|
@@ -152,16 +165,17 @@ Examples:
|
|
|
152
165
|
$ eslint-config-snapshot init
|
|
153
166
|
Runs interactive numbered prompts:
|
|
154
167
|
target: 1) package-json, 2) file
|
|
155
|
-
preset: 1)
|
|
168
|
+
preset: 1) recommended, 2) minimal, 3) full
|
|
169
|
+
recommended preset supports per-workspace group number assignment.
|
|
156
170
|
|
|
157
|
-
$ eslint-config-snapshot init --yes --target package-json --preset
|
|
158
|
-
Non-interactive
|
|
171
|
+
$ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
|
|
172
|
+
Non-interactive recommended setup in package.json, with effective preview.
|
|
159
173
|
|
|
160
174
|
$ eslint-config-snapshot init --yes --force --target file --preset full
|
|
161
175
|
Overwrite-safe bypass when a config is already detected.
|
|
162
176
|
`
|
|
163
177
|
)
|
|
164
|
-
.action(async (opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean }) => {
|
|
178
|
+
.action(async (opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean; showEffective?: boolean }) => {
|
|
165
179
|
onActionExit(await runInit(cwd, opts))
|
|
166
180
|
})
|
|
167
181
|
|
|
@@ -217,11 +231,11 @@ function parseInitTarget(value: string): InitTarget {
|
|
|
217
231
|
|
|
218
232
|
function parseInitPreset(value: string): InitPreset {
|
|
219
233
|
const normalized = value.trim().toLowerCase()
|
|
220
|
-
if (normalized === 'minimal' || normalized === 'full') {
|
|
234
|
+
if (normalized === 'recommended' || normalized === 'minimal' || normalized === 'full') {
|
|
221
235
|
return normalized
|
|
222
236
|
}
|
|
223
237
|
|
|
224
|
-
throw new InvalidArgumentError('Expected one of: minimal, full')
|
|
238
|
+
throw new InvalidArgumentError('Expected one of: recommended, minimal, full')
|
|
225
239
|
}
|
|
226
240
|
|
|
227
241
|
async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation = false): Promise<number> {
|
|
@@ -349,23 +363,34 @@ async function executePrint(cwd: string, format: PrintFormat): Promise<void> {
|
|
|
349
363
|
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`)
|
|
350
364
|
}
|
|
351
365
|
|
|
352
|
-
async function
|
|
366
|
+
async function executeConfig(cwd: string, format: PrintFormat): Promise<void> {
|
|
367
|
+
const foundConfig = await findConfigPath(cwd)
|
|
353
368
|
const config = await loadConfig(cwd)
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
config.
|
|
358
|
-
|
|
359
|
-
|
|
369
|
+
const resolved = await resolveWorkspaceAssignments(cwd, config)
|
|
370
|
+
const payload = {
|
|
371
|
+
source: foundConfig?.path ?? 'built-in-defaults',
|
|
372
|
+
workspaceInput: config.workspaceInput,
|
|
373
|
+
workspaces: resolved.discovery.workspacesRel,
|
|
374
|
+
grouping: {
|
|
375
|
+
mode: config.grouping.mode,
|
|
376
|
+
allowEmptyGroups: config.grouping.allowEmptyGroups ?? false,
|
|
377
|
+
groups: resolved.assignments.map((entry) => ({ name: entry.name, workspaces: entry.workspaces }))
|
|
378
|
+
},
|
|
379
|
+
sampling: config.sampling
|
|
380
|
+
}
|
|
360
381
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (empty.length > 0) {
|
|
365
|
-
throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(', ')}`)
|
|
366
|
-
}
|
|
382
|
+
if (format === 'short') {
|
|
383
|
+
process.stdout.write(formatShortConfig(payload))
|
|
384
|
+
return
|
|
367
385
|
}
|
|
368
386
|
|
|
387
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSnapshot>> {
|
|
391
|
+
const config = await loadConfig(cwd)
|
|
392
|
+
const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config)
|
|
393
|
+
|
|
369
394
|
const snapshots = new Map<string, BuiltSnapshot>()
|
|
370
395
|
|
|
371
396
|
for (const group of assignments) {
|
|
@@ -411,6 +436,25 @@ async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSn
|
|
|
411
436
|
return snapshots
|
|
412
437
|
}
|
|
413
438
|
|
|
439
|
+
async function resolveWorkspaceAssignments(cwd: string, config: Awaited<ReturnType<typeof loadConfig>>) {
|
|
440
|
+
const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput })
|
|
441
|
+
|
|
442
|
+
const assignments =
|
|
443
|
+
config.grouping.mode === 'standalone'
|
|
444
|
+
? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] }))
|
|
445
|
+
: assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: 'default', match: ['**/*'] }])
|
|
446
|
+
|
|
447
|
+
const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false
|
|
448
|
+
if (!allowEmptyGroups) {
|
|
449
|
+
const empty = assignments.filter((group) => group.workspaces.length === 0)
|
|
450
|
+
if (empty.length > 0) {
|
|
451
|
+
throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(', ')}`)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return { discovery, assignments }
|
|
456
|
+
}
|
|
457
|
+
|
|
414
458
|
async function loadStoredSnapshots(cwd: string): Promise<Map<string, StoredSnapshot>> {
|
|
415
459
|
const dir = path.join(cwd, SNAPSHOT_DIR)
|
|
416
460
|
const files = await fg('**/*.json', { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true })
|
|
@@ -515,9 +559,10 @@ function getDisplayOptionChanges(diff: SnapshotDiff): SnapshotDiff['optionChange
|
|
|
515
559
|
|
|
516
560
|
async function runInit(
|
|
517
561
|
cwd: string,
|
|
518
|
-
opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean } = {}
|
|
562
|
+
opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean; showEffective?: boolean } = {}
|
|
519
563
|
): Promise<number> {
|
|
520
564
|
const force = opts.force ?? false
|
|
565
|
+
const showEffective = opts.showEffective ?? false
|
|
521
566
|
const existing = await findConfigPath(cwd)
|
|
522
567
|
if (existing && !force) {
|
|
523
568
|
process.stderr.write(
|
|
@@ -535,13 +580,18 @@ async function runInit(
|
|
|
535
580
|
}
|
|
536
581
|
|
|
537
582
|
const finalTarget = target ?? 'file'
|
|
538
|
-
const finalPreset = preset ?? '
|
|
583
|
+
const finalPreset = preset ?? 'recommended'
|
|
584
|
+
const configObject = await resolveInitConfigObject(cwd, finalPreset, Boolean(opts.yes))
|
|
585
|
+
|
|
586
|
+
if (showEffective) {
|
|
587
|
+
process.stdout.write(`Effective config preview:\n${JSON.stringify(configObject, null, 2)}\n`)
|
|
588
|
+
}
|
|
539
589
|
|
|
540
590
|
if (finalTarget === 'package-json') {
|
|
541
|
-
return runInitInPackageJson(cwd,
|
|
591
|
+
return runInitInPackageJson(cwd, configObject, force)
|
|
542
592
|
}
|
|
543
593
|
|
|
544
|
-
return runInitInFile(cwd,
|
|
594
|
+
return runInitInFile(cwd, configObject, force)
|
|
545
595
|
}
|
|
546
596
|
|
|
547
597
|
async function askInitPreferences(): Promise<{ target: InitTarget; preset: InitPreset }> {
|
|
@@ -571,12 +621,15 @@ async function askInitTarget(rl: ReturnType<typeof createInterface>): Promise<In
|
|
|
571
621
|
|
|
572
622
|
async function askInitPreset(rl: ReturnType<typeof createInterface>): Promise<InitPreset> {
|
|
573
623
|
while (true) {
|
|
574
|
-
const answer = await askQuestion(
|
|
624
|
+
const answer = await askQuestion(
|
|
625
|
+
rl,
|
|
626
|
+
'Select preset:\n 1) recommended (group by workspace numbers)\n 2) minimal\n 3) full\nChoose [1]: '
|
|
627
|
+
)
|
|
575
628
|
const parsed = parseInitPresetChoice(answer)
|
|
576
629
|
if (parsed) {
|
|
577
630
|
return parsed
|
|
578
631
|
}
|
|
579
|
-
process.stdout.write('Please choose 1 (minimal) or
|
|
632
|
+
process.stdout.write('Please choose 1 (recommended), 2 (minimal), or 3 (full).\n')
|
|
580
633
|
}
|
|
581
634
|
}
|
|
582
635
|
|
|
@@ -597,12 +650,15 @@ export function parseInitTargetChoice(value: string): InitTarget | undefined {
|
|
|
597
650
|
export function parseInitPresetChoice(value: string): InitPreset | undefined {
|
|
598
651
|
const normalized = value.trim().toLowerCase()
|
|
599
652
|
if (normalized === '') {
|
|
600
|
-
return '
|
|
653
|
+
return 'recommended'
|
|
601
654
|
}
|
|
602
|
-
if (normalized === '1' || normalized === '
|
|
655
|
+
if (normalized === '1' || normalized === 'recommended' || normalized === 'rec' || normalized === 'grouped') {
|
|
656
|
+
return 'recommended'
|
|
657
|
+
}
|
|
658
|
+
if (normalized === '2' || normalized === 'minimal' || normalized === 'min') {
|
|
603
659
|
return 'minimal'
|
|
604
660
|
}
|
|
605
|
-
if (normalized === '
|
|
661
|
+
if (normalized === '3' || normalized === 'full') {
|
|
606
662
|
return 'full'
|
|
607
663
|
}
|
|
608
664
|
return undefined
|
|
@@ -631,7 +687,7 @@ async function askYesNo(prompt: string, defaultYes: boolean): Promise<boolean> {
|
|
|
631
687
|
}
|
|
632
688
|
}
|
|
633
689
|
|
|
634
|
-
async function runInitInFile(cwd: string,
|
|
690
|
+
async function runInitInFile(cwd: string, configObject: Record<string, unknown>, force: boolean): Promise<number> {
|
|
635
691
|
const candidates = [
|
|
636
692
|
'.eslint-config-snapshot.js',
|
|
637
693
|
'.eslint-config-snapshot.cjs',
|
|
@@ -654,12 +710,12 @@ async function runInitInFile(cwd: string, preset: InitPreset, force: boolean): P
|
|
|
654
710
|
}
|
|
655
711
|
|
|
656
712
|
const target = path.join(cwd, 'eslint-config-snapshot.config.mjs')
|
|
657
|
-
await writeFile(target,
|
|
713
|
+
await writeFile(target, toConfigScaffold(configObject), 'utf8')
|
|
658
714
|
process.stdout.write(`Created ${path.basename(target)}\n`)
|
|
659
715
|
return 0
|
|
660
716
|
}
|
|
661
717
|
|
|
662
|
-
async function runInitInPackageJson(cwd: string,
|
|
718
|
+
async function runInitInPackageJson(cwd: string, configObject: Record<string, unknown>, force: boolean): Promise<number> {
|
|
663
719
|
const packageJsonPath = path.join(cwd, 'package.json')
|
|
664
720
|
|
|
665
721
|
let packageJsonRaw: string
|
|
@@ -683,12 +739,139 @@ async function runInitInPackageJson(cwd: string, preset: InitPreset, force: bool
|
|
|
683
739
|
return 1
|
|
684
740
|
}
|
|
685
741
|
|
|
686
|
-
parsed['eslint-config-snapshot'] =
|
|
742
|
+
parsed['eslint-config-snapshot'] = configObject
|
|
687
743
|
await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8')
|
|
688
744
|
process.stdout.write('Created config in package.json under "eslint-config-snapshot"\n')
|
|
689
745
|
return 0
|
|
690
746
|
}
|
|
691
747
|
|
|
748
|
+
async function resolveInitConfigObject(
|
|
749
|
+
cwd: string,
|
|
750
|
+
preset: InitPreset,
|
|
751
|
+
nonInteractive: boolean
|
|
752
|
+
): Promise<Record<string, unknown>> {
|
|
753
|
+
if (preset === 'minimal') {
|
|
754
|
+
return {}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (preset === 'full') {
|
|
758
|
+
return getFullPresetObject()
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return buildRecommendedPresetObject(cwd, nonInteractive)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async function buildRecommendedPresetObject(cwd: string, nonInteractive: boolean): Promise<Record<string, unknown>> {
|
|
765
|
+
const workspaces = await discoverInitWorkspaces(cwd)
|
|
766
|
+
const assignments = new Map<string, number>(workspaces.map((workspace) => [workspace, 1]))
|
|
767
|
+
|
|
768
|
+
if (!nonInteractive && process.stdin.isTTY && process.stdout.isTTY) {
|
|
769
|
+
process.stdout.write('Recommended setup: assign a group number for each workspace (default: 1).\n')
|
|
770
|
+
for (const workspace of workspaces) {
|
|
771
|
+
const answer = await askGroupNumber(workspace)
|
|
772
|
+
assignments.set(workspace, answer)
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const groupNumbers = [...new Set(assignments.values())].sort((a, b) => a - b)
|
|
777
|
+
const groups = groupNumbers.map((number) => ({
|
|
778
|
+
name: `group-${number}`,
|
|
779
|
+
match: workspaces.filter((workspace) => assignments.get(workspace) === number)
|
|
780
|
+
}))
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
workspaceInput: { mode: 'manual', workspaces },
|
|
784
|
+
grouping: {
|
|
785
|
+
mode: 'match',
|
|
786
|
+
groups
|
|
787
|
+
},
|
|
788
|
+
sampling: {
|
|
789
|
+
maxFilesPerWorkspace: 8,
|
|
790
|
+
includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'],
|
|
791
|
+
excludeGlobs: ['**/node_modules/**', '**/dist/**'],
|
|
792
|
+
hintGlobs: []
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
async function discoverInitWorkspaces(cwd: string): Promise<string[]> {
|
|
798
|
+
const discovered = await discoverWorkspaces({ cwd, workspaceInput: { mode: 'discover' } })
|
|
799
|
+
if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === '.')) {
|
|
800
|
+
return discovered.workspacesRel
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const packageJsonPath = path.join(cwd, 'package.json')
|
|
804
|
+
try {
|
|
805
|
+
const raw = await readFile(packageJsonPath, 'utf8')
|
|
806
|
+
const parsed = JSON.parse(raw) as { workspaces?: string[] | { packages?: string[] } }
|
|
807
|
+
let workspacePatterns: string[] = []
|
|
808
|
+
if (Array.isArray(parsed.workspaces)) {
|
|
809
|
+
workspacePatterns = parsed.workspaces
|
|
810
|
+
} else if (parsed.workspaces && typeof parsed.workspaces === 'object' && Array.isArray(parsed.workspaces.packages)) {
|
|
811
|
+
workspacePatterns = parsed.workspaces.packages
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (workspacePatterns.length === 0) {
|
|
815
|
+
return discovered.workspacesRel
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const workspacePackageFiles = await fg(
|
|
819
|
+
workspacePatterns.map((pattern) => `${trimTrailingSlashes(pattern)}/package.json`),
|
|
820
|
+
{ cwd, onlyFiles: true, dot: true }
|
|
821
|
+
)
|
|
822
|
+
const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath(path.dirname(entry))))].sort((a, b) =>
|
|
823
|
+
a.localeCompare(b)
|
|
824
|
+
)
|
|
825
|
+
if (workspaceDirs.length > 0) {
|
|
826
|
+
return workspaceDirs
|
|
827
|
+
}
|
|
828
|
+
} catch {
|
|
829
|
+
// fallback to discovered output
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return discovered.workspacesRel
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function trimTrailingSlashes(value: string): string {
|
|
836
|
+
let normalized = value
|
|
837
|
+
while (normalized.endsWith('/')) {
|
|
838
|
+
normalized = normalized.slice(0, -1)
|
|
839
|
+
}
|
|
840
|
+
return normalized
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function askGroupNumber(workspace: string): Promise<number> {
|
|
844
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
845
|
+
try {
|
|
846
|
+
while (true) {
|
|
847
|
+
const answer = await askQuestion(rl, `Group number for ${workspace} [1]: `)
|
|
848
|
+
const normalized = answer.trim()
|
|
849
|
+
if (normalized === '') {
|
|
850
|
+
return 1
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (/^\d+$/.test(normalized)) {
|
|
854
|
+
const parsed = Number.parseInt(normalized, 10)
|
|
855
|
+
if (parsed >= 1) {
|
|
856
|
+
return parsed
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
process.stdout.write('Please provide a positive integer (1, 2, 3, ...).\n')
|
|
861
|
+
}
|
|
862
|
+
} finally {
|
|
863
|
+
rl.close()
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function toConfigScaffold(configObject: Record<string, unknown>): string {
|
|
868
|
+
if (Object.keys(configObject).length === 0) {
|
|
869
|
+
return getConfigScaffold('minimal')
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return `export default ${JSON.stringify(configObject, null, 2)}\n`
|
|
873
|
+
}
|
|
874
|
+
|
|
692
875
|
function getFullPresetObject() {
|
|
693
876
|
return {
|
|
694
877
|
workspaceInput: { mode: 'discover' },
|
|
@@ -859,3 +1042,22 @@ function formatShortPrint(
|
|
|
859
1042
|
|
|
860
1043
|
return `${lines.join('\n')}\n`
|
|
861
1044
|
}
|
|
1045
|
+
|
|
1046
|
+
function formatShortConfig(payload: {
|
|
1047
|
+
source: string
|
|
1048
|
+
workspaceInput: unknown
|
|
1049
|
+
workspaces: string[]
|
|
1050
|
+
grouping: { mode: string; allowEmptyGroups: boolean; groups: Array<{ name: string; workspaces: string[] }> }
|
|
1051
|
+
sampling: unknown
|
|
1052
|
+
}): string {
|
|
1053
|
+
const lines: string[] = [
|
|
1054
|
+
`source: ${payload.source}`,
|
|
1055
|
+
`workspaces (${payload.workspaces.length}): ${payload.workspaces.join(', ') || '(none)'}`,
|
|
1056
|
+
`grouping mode: ${payload.grouping.mode} (allow empty: ${payload.grouping.allowEmptyGroups})`
|
|
1057
|
+
]
|
|
1058
|
+
for (const group of payload.grouping.groups) {
|
|
1059
|
+
lines.push(`group ${group.name} (${group.workspaces.length}): ${group.workspaces.join(', ') || '(none)'}`)
|
|
1060
|
+
}
|
|
1061
|
+
lines.push(`workspaceInput: ${JSON.stringify(payload.workspaceInput)}`, `sampling: ${JSON.stringify(payload.sampling)}`)
|
|
1062
|
+
return `${lines.join('\n')}\n`
|
|
1063
|
+
}
|
|
@@ -47,10 +47,12 @@ describe('cli integration', () => {
|
|
|
47
47
|
})
|
|
48
48
|
|
|
49
49
|
it('parses init interactive preset choices from numeric and aliases', () => {
|
|
50
|
-
expect(parseInitPresetChoice('')).toBe('
|
|
51
|
-
expect(parseInitPresetChoice('1')).toBe('
|
|
50
|
+
expect(parseInitPresetChoice('')).toBe('recommended')
|
|
51
|
+
expect(parseInitPresetChoice('1')).toBe('recommended')
|
|
52
|
+
expect(parseInitPresetChoice('rec')).toBe('recommended')
|
|
53
|
+
expect(parseInitPresetChoice('2')).toBe('minimal')
|
|
52
54
|
expect(parseInitPresetChoice('min')).toBe('minimal')
|
|
53
|
-
expect(parseInitPresetChoice('
|
|
55
|
+
expect(parseInitPresetChoice('3')).toBe('full')
|
|
54
56
|
expect(parseInitPresetChoice('full')).toBe('full')
|
|
55
57
|
expect(parseInitPresetChoice('invalid')).toBeUndefined()
|
|
56
58
|
})
|
|
@@ -126,11 +128,22 @@ no-debugger: off
|
|
|
126
128
|
expect(code).toBe(0)
|
|
127
129
|
|
|
128
130
|
const content = await readFile(path.join(tmp, 'eslint-config-snapshot.config.mjs'), 'utf8')
|
|
129
|
-
expect(content).toContain("workspaceInput
|
|
131
|
+
expect(content).toContain('"workspaceInput"')
|
|
132
|
+
expect(content).toContain('"grouping"')
|
|
133
|
+
expect(content).toContain('"sampling"')
|
|
130
134
|
|
|
131
135
|
await rm(tmp, { recursive: true, force: true })
|
|
132
136
|
})
|
|
133
137
|
|
|
138
|
+
it('config prints effective evaluated config and exits 0', async () => {
|
|
139
|
+
const writeSpy = vi.spyOn(process.stdout, 'write')
|
|
140
|
+
const code = await runCli('config', fixtureRoot)
|
|
141
|
+
expect(code).toBe(0)
|
|
142
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"workspaceInput"'))
|
|
143
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"workspaces"'))
|
|
144
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"groups"'))
|
|
145
|
+
})
|
|
146
|
+
|
|
134
147
|
it('init writes minimal config to package.json when target=package-json', async () => {
|
|
135
148
|
const tmp = await mkdtemp(path.join(os.tmpdir(), 'snapshot-init-pkg-'))
|
|
136
149
|
await writeFile(path.join(tmp, 'package.json'), JSON.stringify({ name: 'fixture', private: true }, null, 2))
|
|
@@ -66,6 +66,7 @@ describe('cli terminal invocation', () => {
|
|
|
66
66
|
expect(result.stdout).toContain('check [options]')
|
|
67
67
|
expect(result.stdout).toContain('update|snapshot')
|
|
68
68
|
expect(result.stdout).toContain('print [options]')
|
|
69
|
+
expect(result.stdout).toContain('config [options]')
|
|
69
70
|
expect(result.stdout).toContain('init')
|
|
70
71
|
expect(result.stderr).toBe('')
|
|
71
72
|
})
|
|
@@ -232,6 +233,15 @@ no-debugger: off
|
|
|
232
233
|
expect(existing.stderr).toContain('rerun with --force')
|
|
233
234
|
})
|
|
234
235
|
|
|
236
|
+
it('config prints effective evaluated config output', () => {
|
|
237
|
+
const result = run(['config'])
|
|
238
|
+
expect(result.status).toBe(0)
|
|
239
|
+
expect(result.stdout).toContain('"workspaceInput"')
|
|
240
|
+
expect(result.stdout).toContain('"workspaces"')
|
|
241
|
+
expect(result.stdout).toContain('"groups"')
|
|
242
|
+
expect(result.stderr).toBe('')
|
|
243
|
+
})
|
|
244
|
+
|
|
235
245
|
it('init can write config to package.json', async () => {
|
|
236
246
|
const initRoot = path.join(tmpDir, 'init-package-json-case')
|
|
237
247
|
await rm(initRoot, { recursive: true, force: true })
|
|
@@ -250,6 +260,37 @@ no-debugger: off
|
|
|
250
260
|
expect(parsed['eslint-config-snapshot']).toEqual({})
|
|
251
261
|
})
|
|
252
262
|
|
|
263
|
+
it('init recommended writes grouped workspace config in package.json', async () => {
|
|
264
|
+
const initRoot = path.join(tmpDir, 'init-recommended-package-json-case')
|
|
265
|
+
await rm(initRoot, { recursive: true, force: true })
|
|
266
|
+
await cp(fixtureRoot, initRoot, { recursive: true })
|
|
267
|
+
repoRoot = initRoot
|
|
268
|
+
|
|
269
|
+
await rm(path.join(repoRoot, 'eslint-config-snapshot.config.mjs'), { force: true })
|
|
270
|
+
|
|
271
|
+
const created = run(['init', '--yes', '--target', 'package-json', '--preset', 'recommended'])
|
|
272
|
+
expect(created.status).toBe(0)
|
|
273
|
+
expect(created.stdout).toBe('Created config in package.json under "eslint-config-snapshot"\n')
|
|
274
|
+
expect(created.stderr).toBe('')
|
|
275
|
+
|
|
276
|
+
const packageJsonRaw = await readFile(path.join(repoRoot, 'package.json'), 'utf8')
|
|
277
|
+
const parsed = JSON.parse(packageJsonRaw) as {
|
|
278
|
+
'eslint-config-snapshot'?: {
|
|
279
|
+
workspaceInput?: { mode?: string; workspaces?: string[] }
|
|
280
|
+
grouping?: { mode?: string; groups?: Array<{ name: string; match: string[] }> }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
expect(parsed['eslint-config-snapshot']?.workspaceInput).toEqual({
|
|
285
|
+
mode: 'manual',
|
|
286
|
+
workspaces: ['packages/ws-a', 'packages/ws-b']
|
|
287
|
+
})
|
|
288
|
+
expect(parsed['eslint-config-snapshot']?.grouping).toEqual({
|
|
289
|
+
mode: 'match',
|
|
290
|
+
groups: [{ name: 'group-1', match: ['packages/ws-a', 'packages/ws-b'] }]
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
253
294
|
it('init fails early on existing config unless --force is provided', async () => {
|
|
254
295
|
const initRoot = path.join(tmpDir, 'init-force-case')
|
|
255
296
|
await rm(initRoot, { recursive: true, force: true })
|
|
@@ -342,7 +383,8 @@ no-debugger: off
|
|
|
342
383
|
expect(result.stdout).toContain('-f, --force')
|
|
343
384
|
expect(result.stdout).toContain('Runs interactive numbered prompts:')
|
|
344
385
|
expect(result.stdout).toContain('target: 1) package-json, 2) file')
|
|
345
|
-
expect(result.stdout).toContain('preset: 1)
|
|
386
|
+
expect(result.stdout).toContain('preset: 1) recommended, 2) minimal, 3) full')
|
|
387
|
+
expect(result.stdout).toContain('--show-effective')
|
|
346
388
|
expect(result.stdout).toContain('--yes --force --target file --preset full')
|
|
347
389
|
expect(result.stderr).toBe('')
|
|
348
390
|
})
|