@eslint-config-snapshot/cli 0.1.5 → 0.3.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 +22 -0
- package/README.md +9 -0
- package/dist/index.cjs +202 -84
- package/dist/index.js +202 -82
- package/package.json +3 -2
- package/src/index.ts +253 -88
- package/test/cli.integration.test.ts +44 -25
- package/test/cli.terminal.integration.test.ts +52 -4
- package/test/fixtures/repo/packages/ws-a/node_modules/eslint/bin/eslint.js +0 -1
- package/test/fixtures/repo/packages/ws-a/node_modules/eslint/package.json +0 -4
- package/test/fixtures/repo/packages/ws-b/node_modules/eslint/bin/eslint.js +0 -1
- package/test/fixtures/repo/packages/ws-b/node_modules/eslint/package.json +0 -4
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,21 @@ 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
|
-
Runs interactive
|
|
101
|
-
|
|
102
|
-
preset: 1) minimal, 2) full
|
|
106
|
+
Runs interactive select prompts for target/preset.
|
|
107
|
+
Recommended preset uses checkbox selection for non-default workspaces and group selection.
|
|
103
108
|
|
|
104
|
-
$ eslint-config-snapshot init --yes --target package-json --preset
|
|
105
|
-
Non-interactive
|
|
109
|
+
$ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
|
|
110
|
+
Non-interactive recommended setup in package.json, with effective preview.
|
|
106
111
|
|
|
107
112
|
$ eslint-config-snapshot init --yes --force --target file --preset full
|
|
108
113
|
Overwrite-safe bypass when a config is already detected.
|
|
@@ -145,10 +150,10 @@ function parseInitTarget(value) {
|
|
|
145
150
|
}
|
|
146
151
|
function parseInitPreset(value) {
|
|
147
152
|
const normalized = value.trim().toLowerCase();
|
|
148
|
-
if (normalized === "minimal" || normalized === "full") {
|
|
153
|
+
if (normalized === "recommended" || normalized === "minimal" || normalized === "full") {
|
|
149
154
|
return normalized;
|
|
150
155
|
}
|
|
151
|
-
throw new InvalidArgumentError("Expected one of: minimal, full");
|
|
156
|
+
throw new InvalidArgumentError("Expected one of: recommended, minimal, full");
|
|
152
157
|
}
|
|
153
158
|
async function executeCheck(cwd, format, defaultInvocation = false) {
|
|
154
159
|
const foundConfig = await findConfigPath(cwd);
|
|
@@ -259,17 +264,31 @@ async function executePrint(cwd, format) {
|
|
|
259
264
|
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
260
265
|
`);
|
|
261
266
|
}
|
|
262
|
-
async function
|
|
267
|
+
async function executeConfig(cwd, format) {
|
|
268
|
+
const foundConfig = await findConfigPath(cwd);
|
|
263
269
|
const config = await loadConfig(cwd);
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
const resolved = await resolveWorkspaceAssignments(cwd, config);
|
|
271
|
+
const payload = {
|
|
272
|
+
source: foundConfig?.path ?? "built-in-defaults",
|
|
273
|
+
workspaceInput: config.workspaceInput,
|
|
274
|
+
workspaces: resolved.discovery.workspacesRel,
|
|
275
|
+
grouping: {
|
|
276
|
+
mode: config.grouping.mode,
|
|
277
|
+
allowEmptyGroups: config.grouping.allowEmptyGroups ?? false,
|
|
278
|
+
groups: resolved.assignments.map((entry) => ({ name: entry.name, workspaces: entry.workspaces }))
|
|
279
|
+
},
|
|
280
|
+
sampling: config.sampling
|
|
281
|
+
};
|
|
282
|
+
if (format === "short") {
|
|
283
|
+
process.stdout.write(formatShortConfig(payload));
|
|
284
|
+
return;
|
|
272
285
|
}
|
|
286
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
287
|
+
`);
|
|
288
|
+
}
|
|
289
|
+
async function computeCurrentSnapshots(cwd) {
|
|
290
|
+
const config = await loadConfig(cwd);
|
|
291
|
+
const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
|
|
273
292
|
const snapshots = /* @__PURE__ */ new Map();
|
|
274
293
|
for (const group of assignments) {
|
|
275
294
|
const extractedForGroup = [];
|
|
@@ -304,6 +323,18 @@ async function computeCurrentSnapshots(cwd) {
|
|
|
304
323
|
}
|
|
305
324
|
return snapshots;
|
|
306
325
|
}
|
|
326
|
+
async function resolveWorkspaceAssignments(cwd, config) {
|
|
327
|
+
const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput });
|
|
328
|
+
const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
|
|
329
|
+
const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false;
|
|
330
|
+
if (!allowEmptyGroups) {
|
|
331
|
+
const empty = assignments.filter((group) => group.workspaces.length === 0);
|
|
332
|
+
if (empty.length > 0) {
|
|
333
|
+
throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(", ")}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return { discovery, assignments };
|
|
337
|
+
}
|
|
307
338
|
async function loadStoredSnapshots(cwd) {
|
|
308
339
|
const dir = path.join(cwd, SNAPSHOT_DIR);
|
|
309
340
|
const files = await fg("**/*.json", { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true });
|
|
@@ -385,6 +416,7 @@ function getDisplayOptionChanges(diff) {
|
|
|
385
416
|
}
|
|
386
417
|
async function runInit(cwd, opts = {}) {
|
|
387
418
|
const force = opts.force ?? false;
|
|
419
|
+
const showEffective = opts.showEffective ?? false;
|
|
388
420
|
const existing = await findConfigPath(cwd);
|
|
389
421
|
if (existing && !force) {
|
|
390
422
|
process.stderr.write(
|
|
@@ -401,70 +433,42 @@ async function runInit(cwd, opts = {}) {
|
|
|
401
433
|
preset = interactive.preset;
|
|
402
434
|
}
|
|
403
435
|
const finalTarget = target ?? "file";
|
|
404
|
-
const finalPreset = preset ?? "
|
|
436
|
+
const finalPreset = preset ?? "recommended";
|
|
437
|
+
const configObject = await resolveInitConfigObject(cwd, finalPreset, Boolean(opts.yes));
|
|
438
|
+
if (showEffective) {
|
|
439
|
+
process.stdout.write(`Effective config preview:
|
|
440
|
+
${JSON.stringify(configObject, null, 2)}
|
|
441
|
+
`);
|
|
442
|
+
}
|
|
405
443
|
if (finalTarget === "package-json") {
|
|
406
|
-
return runInitInPackageJson(cwd,
|
|
444
|
+
return runInitInPackageJson(cwd, configObject, force);
|
|
407
445
|
}
|
|
408
|
-
return runInitInFile(cwd,
|
|
446
|
+
return runInitInFile(cwd, configObject, force);
|
|
409
447
|
}
|
|
410
448
|
async function askInitPreferences() {
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
"Select config target:\n 1) package-json (recommended)\n 2) file\nChoose [1]: "
|
|
425
|
-
);
|
|
426
|
-
const parsed = parseInitTargetChoice(answer);
|
|
427
|
-
if (parsed) {
|
|
428
|
-
return parsed;
|
|
429
|
-
}
|
|
430
|
-
process.stdout.write("Please choose 1 (package-json) or 2 (file).\n");
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
async function askInitPreset(rl) {
|
|
434
|
-
while (true) {
|
|
435
|
-
const answer = await askQuestion(rl, "Select preset:\n 1) minimal (recommended)\n 2) full\nChoose [1]: ");
|
|
436
|
-
const parsed = parseInitPresetChoice(answer);
|
|
437
|
-
if (parsed) {
|
|
438
|
-
return parsed;
|
|
439
|
-
}
|
|
440
|
-
process.stdout.write("Please choose 1 (minimal) or 2 (full).\n");
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
function parseInitTargetChoice(value) {
|
|
444
|
-
const normalized = value.trim().toLowerCase();
|
|
445
|
-
if (normalized === "") {
|
|
446
|
-
return "package-json";
|
|
447
|
-
}
|
|
448
|
-
if (normalized === "1" || normalized === "package-json" || normalized === "packagejson" || normalized === "package" || normalized === "pkg") {
|
|
449
|
-
return "package-json";
|
|
450
|
-
}
|
|
451
|
-
if (normalized === "2" || normalized === "file") {
|
|
452
|
-
return "file";
|
|
453
|
-
}
|
|
454
|
-
return void 0;
|
|
449
|
+
const { select } = await import("@inquirer/prompts");
|
|
450
|
+
const target = await askInitTarget(select);
|
|
451
|
+
const preset = await askInitPreset(select);
|
|
452
|
+
return { target, preset };
|
|
453
|
+
}
|
|
454
|
+
async function askInitTarget(selectPrompt) {
|
|
455
|
+
return selectPrompt({
|
|
456
|
+
message: "Select config target",
|
|
457
|
+
choices: [
|
|
458
|
+
{ name: "package-json (recommended)", value: "package-json" },
|
|
459
|
+
{ name: "file", value: "file" }
|
|
460
|
+
]
|
|
461
|
+
});
|
|
455
462
|
}
|
|
456
|
-
function
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return "full";
|
|
466
|
-
}
|
|
467
|
-
return void 0;
|
|
463
|
+
async function askInitPreset(selectPrompt) {
|
|
464
|
+
return selectPrompt({
|
|
465
|
+
message: "Select preset",
|
|
466
|
+
choices: [
|
|
467
|
+
{ name: 'recommended (default group "*" + static overrides)', value: "recommended" },
|
|
468
|
+
{ name: "minimal", value: "minimal" },
|
|
469
|
+
{ name: "full", value: "full" }
|
|
470
|
+
]
|
|
471
|
+
});
|
|
468
472
|
}
|
|
469
473
|
function askQuestion(rl, prompt) {
|
|
470
474
|
return new Promise((resolve) => {
|
|
@@ -486,7 +490,7 @@ async function askYesNo(prompt, defaultYes) {
|
|
|
486
490
|
rl.close();
|
|
487
491
|
}
|
|
488
492
|
}
|
|
489
|
-
async function runInitInFile(cwd,
|
|
493
|
+
async function runInitInFile(cwd, configObject, force) {
|
|
490
494
|
const candidates = [
|
|
491
495
|
".eslint-config-snapshot.js",
|
|
492
496
|
".eslint-config-snapshot.cjs",
|
|
@@ -507,12 +511,12 @@ async function runInitInFile(cwd, preset, force) {
|
|
|
507
511
|
}
|
|
508
512
|
}
|
|
509
513
|
const target = path.join(cwd, "eslint-config-snapshot.config.mjs");
|
|
510
|
-
await writeFile(target,
|
|
514
|
+
await writeFile(target, toConfigScaffold(configObject), "utf8");
|
|
511
515
|
process.stdout.write(`Created ${path.basename(target)}
|
|
512
516
|
`);
|
|
513
517
|
return 0;
|
|
514
518
|
}
|
|
515
|
-
async function runInitInPackageJson(cwd,
|
|
519
|
+
async function runInitInPackageJson(cwd, configObject, force) {
|
|
516
520
|
const packageJsonPath = path.join(cwd, "package.json");
|
|
517
521
|
let packageJsonRaw;
|
|
518
522
|
try {
|
|
@@ -532,12 +536,116 @@ async function runInitInPackageJson(cwd, preset, force) {
|
|
|
532
536
|
process.stderr.write("Config already exists in package.json: eslint-config-snapshot\n");
|
|
533
537
|
return 1;
|
|
534
538
|
}
|
|
535
|
-
parsed["eslint-config-snapshot"] =
|
|
539
|
+
parsed["eslint-config-snapshot"] = configObject;
|
|
536
540
|
await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
|
|
537
541
|
`, "utf8");
|
|
538
542
|
process.stdout.write('Created config in package.json under "eslint-config-snapshot"\n');
|
|
539
543
|
return 0;
|
|
540
544
|
}
|
|
545
|
+
async function resolveInitConfigObject(cwd, preset, nonInteractive) {
|
|
546
|
+
if (preset === "minimal") {
|
|
547
|
+
return {};
|
|
548
|
+
}
|
|
549
|
+
if (preset === "full") {
|
|
550
|
+
return getFullPresetObject();
|
|
551
|
+
}
|
|
552
|
+
return buildRecommendedPresetObject(cwd, nonInteractive);
|
|
553
|
+
}
|
|
554
|
+
async function buildRecommendedPresetObject(cwd, nonInteractive) {
|
|
555
|
+
const workspaces = await discoverInitWorkspaces(cwd);
|
|
556
|
+
const useInteractiveGrouping = !nonInteractive && process.stdin.isTTY && process.stdout.isTTY;
|
|
557
|
+
const assignments = useInteractiveGrouping ? await askRecommendedGroupAssignments(workspaces) : /* @__PURE__ */ new Map();
|
|
558
|
+
return buildRecommendedConfigFromAssignments(workspaces, assignments);
|
|
559
|
+
}
|
|
560
|
+
function buildRecommendedConfigFromAssignments(workspaces, assignments) {
|
|
561
|
+
const groupNumbers = [...new Set(assignments.values())].sort((a, b) => a - b);
|
|
562
|
+
if (groupNumbers.length === 0) {
|
|
563
|
+
return {};
|
|
564
|
+
}
|
|
565
|
+
const explicitGroups = groupNumbers.map((number) => ({
|
|
566
|
+
name: `group-${number}`,
|
|
567
|
+
match: workspaces.filter((workspace) => assignments.get(workspace) === number)
|
|
568
|
+
}));
|
|
569
|
+
return {
|
|
570
|
+
grouping: {
|
|
571
|
+
mode: "match",
|
|
572
|
+
groups: [...explicitGroups, { name: "default", match: ["**/*"] }]
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
async function discoverInitWorkspaces(cwd) {
|
|
577
|
+
const discovered = await discoverWorkspaces({ cwd, workspaceInput: { mode: "discover" } });
|
|
578
|
+
if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === ".")) {
|
|
579
|
+
return discovered.workspacesRel;
|
|
580
|
+
}
|
|
581
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
582
|
+
try {
|
|
583
|
+
const raw = await readFile(packageJsonPath, "utf8");
|
|
584
|
+
const parsed = JSON.parse(raw);
|
|
585
|
+
let workspacePatterns = [];
|
|
586
|
+
if (Array.isArray(parsed.workspaces)) {
|
|
587
|
+
workspacePatterns = parsed.workspaces;
|
|
588
|
+
} else if (parsed.workspaces && typeof parsed.workspaces === "object" && Array.isArray(parsed.workspaces.packages)) {
|
|
589
|
+
workspacePatterns = parsed.workspaces.packages;
|
|
590
|
+
}
|
|
591
|
+
if (workspacePatterns.length === 0) {
|
|
592
|
+
return discovered.workspacesRel;
|
|
593
|
+
}
|
|
594
|
+
const workspacePackageFiles = await fg(
|
|
595
|
+
workspacePatterns.map((pattern) => `${trimTrailingSlashes(pattern)}/package.json`),
|
|
596
|
+
{ cwd, onlyFiles: true, dot: true }
|
|
597
|
+
);
|
|
598
|
+
const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath(path.dirname(entry))))].sort(
|
|
599
|
+
(a, b) => a.localeCompare(b)
|
|
600
|
+
);
|
|
601
|
+
if (workspaceDirs.length > 0) {
|
|
602
|
+
return workspaceDirs;
|
|
603
|
+
}
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
return discovered.workspacesRel;
|
|
607
|
+
}
|
|
608
|
+
function trimTrailingSlashes(value) {
|
|
609
|
+
let normalized = value;
|
|
610
|
+
while (normalized.endsWith("/")) {
|
|
611
|
+
normalized = normalized.slice(0, -1);
|
|
612
|
+
}
|
|
613
|
+
return normalized;
|
|
614
|
+
}
|
|
615
|
+
async function askRecommendedGroupAssignments(workspaces) {
|
|
616
|
+
const { checkbox, select } = await import("@inquirer/prompts");
|
|
617
|
+
process.stdout.write('Recommended setup: select only workspaces that should leave default group "*".\n');
|
|
618
|
+
const overrides = await checkbox({
|
|
619
|
+
message: "Workspaces outside default group:",
|
|
620
|
+
choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
|
|
621
|
+
pageSize: Math.min(12, Math.max(4, workspaces.length))
|
|
622
|
+
});
|
|
623
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
624
|
+
let nextGroup = 1;
|
|
625
|
+
for (const workspace of overrides) {
|
|
626
|
+
const usedGroups = [...new Set(assignments.values())].sort((a, b) => a - b);
|
|
627
|
+
while (usedGroups.includes(nextGroup)) {
|
|
628
|
+
nextGroup += 1;
|
|
629
|
+
}
|
|
630
|
+
const selected = await select({
|
|
631
|
+
message: `Select group for ${workspace}`,
|
|
632
|
+
choices: [
|
|
633
|
+
...usedGroups.map((group) => ({ name: `group-${group}`, value: group })),
|
|
634
|
+
{ name: `create new group (group-${nextGroup})`, value: "new" }
|
|
635
|
+
]
|
|
636
|
+
});
|
|
637
|
+
const groupNumber = selected === "new" ? nextGroup : selected;
|
|
638
|
+
assignments.set(workspace, groupNumber);
|
|
639
|
+
}
|
|
640
|
+
return assignments;
|
|
641
|
+
}
|
|
642
|
+
function toConfigScaffold(configObject) {
|
|
643
|
+
if (Object.keys(configObject).length === 0) {
|
|
644
|
+
return getConfigScaffold("minimal");
|
|
645
|
+
}
|
|
646
|
+
return `export default ${JSON.stringify(configObject, null, 2)}
|
|
647
|
+
`;
|
|
648
|
+
}
|
|
541
649
|
function getFullPresetObject() {
|
|
542
650
|
return {
|
|
543
651
|
workspaceInput: { mode: "discover" },
|
|
@@ -689,9 +797,21 @@ function formatShortPrint(snapshots) {
|
|
|
689
797
|
return `${lines.join("\n")}
|
|
690
798
|
`;
|
|
691
799
|
}
|
|
800
|
+
function formatShortConfig(payload) {
|
|
801
|
+
const lines = [
|
|
802
|
+
`source: ${payload.source}`,
|
|
803
|
+
`workspaces (${payload.workspaces.length}): ${payload.workspaces.join(", ") || "(none)"}`,
|
|
804
|
+
`grouping mode: ${payload.grouping.mode} (allow empty: ${payload.grouping.allowEmptyGroups})`
|
|
805
|
+
];
|
|
806
|
+
for (const group of payload.grouping.groups) {
|
|
807
|
+
lines.push(`group ${group.name} (${group.workspaces.length}): ${group.workspaces.join(", ") || "(none)"}`);
|
|
808
|
+
}
|
|
809
|
+
lines.push(`workspaceInput: ${JSON.stringify(payload.workspaceInput)}`, `sampling: ${JSON.stringify(payload.sampling)}`);
|
|
810
|
+
return `${lines.join("\n")}
|
|
811
|
+
`;
|
|
812
|
+
}
|
|
692
813
|
export {
|
|
814
|
+
buildRecommendedConfigFromAssignments,
|
|
693
815
|
main,
|
|
694
|
-
parseInitPresetChoice,
|
|
695
|
-
parseInitTargetChoice,
|
|
696
816
|
runCli
|
|
697
817
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eslint-config-snapshot/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@inquirer/prompts": "^8.2.0",
|
|
30
31
|
"commander": "^14.0.3",
|
|
31
32
|
"fast-glob": "^3.3.3",
|
|
32
|
-
"@eslint-config-snapshot/api": "0.
|
|
33
|
+
"@eslint-config-snapshot/api": "0.3.0"
|
|
33
34
|
}
|
|
34
35
|
}
|