@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/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(
|
|
@@ -150,18 +163,17 @@ function createProgram(cwd: string, onActionExit: (code: number) => void): Comma
|
|
|
150
163
|
`
|
|
151
164
|
Examples:
|
|
152
165
|
$ eslint-config-snapshot init
|
|
153
|
-
Runs interactive
|
|
154
|
-
|
|
155
|
-
preset: 1) minimal, 2) full
|
|
166
|
+
Runs interactive select prompts for target/preset.
|
|
167
|
+
Recommended preset uses checkbox selection for non-default workspaces and group selection.
|
|
156
168
|
|
|
157
|
-
$ eslint-config-snapshot init --yes --target package-json --preset
|
|
158
|
-
Non-interactive
|
|
169
|
+
$ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
|
|
170
|
+
Non-interactive recommended setup in package.json, with effective preview.
|
|
159
171
|
|
|
160
172
|
$ eslint-config-snapshot init --yes --force --target file --preset full
|
|
161
173
|
Overwrite-safe bypass when a config is already detected.
|
|
162
174
|
`
|
|
163
175
|
)
|
|
164
|
-
.action(async (opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean }) => {
|
|
176
|
+
.action(async (opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean; showEffective?: boolean }) => {
|
|
165
177
|
onActionExit(await runInit(cwd, opts))
|
|
166
178
|
})
|
|
167
179
|
|
|
@@ -217,11 +229,11 @@ function parseInitTarget(value: string): InitTarget {
|
|
|
217
229
|
|
|
218
230
|
function parseInitPreset(value: string): InitPreset {
|
|
219
231
|
const normalized = value.trim().toLowerCase()
|
|
220
|
-
if (normalized === 'minimal' || normalized === 'full') {
|
|
232
|
+
if (normalized === 'recommended' || normalized === 'minimal' || normalized === 'full') {
|
|
221
233
|
return normalized
|
|
222
234
|
}
|
|
223
235
|
|
|
224
|
-
throw new InvalidArgumentError('Expected one of: minimal, full')
|
|
236
|
+
throw new InvalidArgumentError('Expected one of: recommended, minimal, full')
|
|
225
237
|
}
|
|
226
238
|
|
|
227
239
|
async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation = false): Promise<number> {
|
|
@@ -349,23 +361,34 @@ async function executePrint(cwd: string, format: PrintFormat): Promise<void> {
|
|
|
349
361
|
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`)
|
|
350
362
|
}
|
|
351
363
|
|
|
352
|
-
async function
|
|
364
|
+
async function executeConfig(cwd: string, format: PrintFormat): Promise<void> {
|
|
365
|
+
const foundConfig = await findConfigPath(cwd)
|
|
353
366
|
const config = await loadConfig(cwd)
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
config.
|
|
358
|
-
|
|
359
|
-
|
|
367
|
+
const resolved = await resolveWorkspaceAssignments(cwd, config)
|
|
368
|
+
const payload = {
|
|
369
|
+
source: foundConfig?.path ?? 'built-in-defaults',
|
|
370
|
+
workspaceInput: config.workspaceInput,
|
|
371
|
+
workspaces: resolved.discovery.workspacesRel,
|
|
372
|
+
grouping: {
|
|
373
|
+
mode: config.grouping.mode,
|
|
374
|
+
allowEmptyGroups: config.grouping.allowEmptyGroups ?? false,
|
|
375
|
+
groups: resolved.assignments.map((entry) => ({ name: entry.name, workspaces: entry.workspaces }))
|
|
376
|
+
},
|
|
377
|
+
sampling: config.sampling
|
|
378
|
+
}
|
|
360
379
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (empty.length > 0) {
|
|
365
|
-
throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(', ')}`)
|
|
366
|
-
}
|
|
380
|
+
if (format === 'short') {
|
|
381
|
+
process.stdout.write(formatShortConfig(payload))
|
|
382
|
+
return
|
|
367
383
|
}
|
|
368
384
|
|
|
385
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSnapshot>> {
|
|
389
|
+
const config = await loadConfig(cwd)
|
|
390
|
+
const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config)
|
|
391
|
+
|
|
369
392
|
const snapshots = new Map<string, BuiltSnapshot>()
|
|
370
393
|
|
|
371
394
|
for (const group of assignments) {
|
|
@@ -411,6 +434,25 @@ async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSn
|
|
|
411
434
|
return snapshots
|
|
412
435
|
}
|
|
413
436
|
|
|
437
|
+
async function resolveWorkspaceAssignments(cwd: string, config: Awaited<ReturnType<typeof loadConfig>>) {
|
|
438
|
+
const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput })
|
|
439
|
+
|
|
440
|
+
const assignments =
|
|
441
|
+
config.grouping.mode === 'standalone'
|
|
442
|
+
? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] }))
|
|
443
|
+
: assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: 'default', match: ['**/*'] }])
|
|
444
|
+
|
|
445
|
+
const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false
|
|
446
|
+
if (!allowEmptyGroups) {
|
|
447
|
+
const empty = assignments.filter((group) => group.workspaces.length === 0)
|
|
448
|
+
if (empty.length > 0) {
|
|
449
|
+
throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(', ')}`)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return { discovery, assignments }
|
|
454
|
+
}
|
|
455
|
+
|
|
414
456
|
async function loadStoredSnapshots(cwd: string): Promise<Map<string, StoredSnapshot>> {
|
|
415
457
|
const dir = path.join(cwd, SNAPSHOT_DIR)
|
|
416
458
|
const files = await fg('**/*.json', { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true })
|
|
@@ -515,9 +557,10 @@ function getDisplayOptionChanges(diff: SnapshotDiff): SnapshotDiff['optionChange
|
|
|
515
557
|
|
|
516
558
|
async function runInit(
|
|
517
559
|
cwd: string,
|
|
518
|
-
opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean } = {}
|
|
560
|
+
opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean; showEffective?: boolean } = {}
|
|
519
561
|
): Promise<number> {
|
|
520
562
|
const force = opts.force ?? false
|
|
563
|
+
const showEffective = opts.showEffective ?? false
|
|
521
564
|
const existing = await findConfigPath(cwd)
|
|
522
565
|
if (existing && !force) {
|
|
523
566
|
process.stderr.write(
|
|
@@ -535,77 +578,50 @@ async function runInit(
|
|
|
535
578
|
}
|
|
536
579
|
|
|
537
580
|
const finalTarget = target ?? 'file'
|
|
538
|
-
const finalPreset = preset ?? '
|
|
581
|
+
const finalPreset = preset ?? 'recommended'
|
|
582
|
+
const configObject = await resolveInitConfigObject(cwd, finalPreset, Boolean(opts.yes))
|
|
539
583
|
|
|
540
|
-
if (
|
|
541
|
-
|
|
584
|
+
if (showEffective) {
|
|
585
|
+
process.stdout.write(`Effective config preview:\n${JSON.stringify(configObject, null, 2)}\n`)
|
|
542
586
|
}
|
|
543
587
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
async function askInitPreferences(): Promise<{ target: InitTarget; preset: InitPreset }> {
|
|
548
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
549
|
-
try {
|
|
550
|
-
const target = await askInitTarget(rl)
|
|
551
|
-
const preset = await askInitPreset(rl)
|
|
552
|
-
return { target, preset }
|
|
553
|
-
} finally {
|
|
554
|
-
rl.close()
|
|
588
|
+
if (finalTarget === 'package-json') {
|
|
589
|
+
return runInitInPackageJson(cwd, configObject, force)
|
|
555
590
|
}
|
|
556
|
-
}
|
|
557
591
|
|
|
558
|
-
|
|
559
|
-
while (true) {
|
|
560
|
-
const answer = await askQuestion(
|
|
561
|
-
rl,
|
|
562
|
-
'Select config target:\n 1) package-json (recommended)\n 2) file\nChoose [1]: '
|
|
563
|
-
)
|
|
564
|
-
const parsed = parseInitTargetChoice(answer)
|
|
565
|
-
if (parsed) {
|
|
566
|
-
return parsed
|
|
567
|
-
}
|
|
568
|
-
process.stdout.write('Please choose 1 (package-json) or 2 (file).\n')
|
|
569
|
-
}
|
|
592
|
+
return runInitInFile(cwd, configObject, force)
|
|
570
593
|
}
|
|
571
594
|
|
|
572
|
-
async function
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
return parsed
|
|
578
|
-
}
|
|
579
|
-
process.stdout.write('Please choose 1 (minimal) or 2 (full).\n')
|
|
580
|
-
}
|
|
595
|
+
async function askInitPreferences(): Promise<{ target: InitTarget; preset: InitPreset }> {
|
|
596
|
+
const { select } = await import('@inquirer/prompts')
|
|
597
|
+
const target = await askInitTarget(select)
|
|
598
|
+
const preset = await askInitPreset(select)
|
|
599
|
+
return { target, preset }
|
|
581
600
|
}
|
|
582
601
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
return undefined
|
|
602
|
+
async function askInitTarget(
|
|
603
|
+
selectPrompt: (options: { message: string; choices: Array<{ name: string; value: InitTarget }> }) => Promise<InitTarget>
|
|
604
|
+
): Promise<InitTarget> {
|
|
605
|
+
return selectPrompt({
|
|
606
|
+
message: 'Select config target',
|
|
607
|
+
choices: [
|
|
608
|
+
{ name: 'package-json (recommended)', value: 'package-json' },
|
|
609
|
+
{ name: 'file', value: 'file' }
|
|
610
|
+
]
|
|
611
|
+
})
|
|
595
612
|
}
|
|
596
613
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
return undefined
|
|
614
|
+
async function askInitPreset(
|
|
615
|
+
selectPrompt: (options: { message: string; choices: Array<{ name: string; value: InitPreset }> }) => Promise<InitPreset>
|
|
616
|
+
): Promise<InitPreset> {
|
|
617
|
+
return selectPrompt({
|
|
618
|
+
message: 'Select preset',
|
|
619
|
+
choices: [
|
|
620
|
+
{ name: 'recommended (default group "*" + static overrides)', value: 'recommended' },
|
|
621
|
+
{ name: 'minimal', value: 'minimal' },
|
|
622
|
+
{ name: 'full', value: 'full' }
|
|
623
|
+
]
|
|
624
|
+
})
|
|
609
625
|
}
|
|
610
626
|
|
|
611
627
|
function askQuestion(rl: ReturnType<typeof createInterface>, prompt: string): Promise<string> {
|
|
@@ -631,7 +647,7 @@ async function askYesNo(prompt: string, defaultYes: boolean): Promise<boolean> {
|
|
|
631
647
|
}
|
|
632
648
|
}
|
|
633
649
|
|
|
634
|
-
async function runInitInFile(cwd: string,
|
|
650
|
+
async function runInitInFile(cwd: string, configObject: Record<string, unknown>, force: boolean): Promise<number> {
|
|
635
651
|
const candidates = [
|
|
636
652
|
'.eslint-config-snapshot.js',
|
|
637
653
|
'.eslint-config-snapshot.cjs',
|
|
@@ -654,12 +670,12 @@ async function runInitInFile(cwd: string, preset: InitPreset, force: boolean): P
|
|
|
654
670
|
}
|
|
655
671
|
|
|
656
672
|
const target = path.join(cwd, 'eslint-config-snapshot.config.mjs')
|
|
657
|
-
await writeFile(target,
|
|
673
|
+
await writeFile(target, toConfigScaffold(configObject), 'utf8')
|
|
658
674
|
process.stdout.write(`Created ${path.basename(target)}\n`)
|
|
659
675
|
return 0
|
|
660
676
|
}
|
|
661
677
|
|
|
662
|
-
async function runInitInPackageJson(cwd: string,
|
|
678
|
+
async function runInitInPackageJson(cwd: string, configObject: Record<string, unknown>, force: boolean): Promise<number> {
|
|
663
679
|
const packageJsonPath = path.join(cwd, 'package.json')
|
|
664
680
|
|
|
665
681
|
let packageJsonRaw: string
|
|
@@ -683,12 +699,142 @@ async function runInitInPackageJson(cwd: string, preset: InitPreset, force: bool
|
|
|
683
699
|
return 1
|
|
684
700
|
}
|
|
685
701
|
|
|
686
|
-
parsed['eslint-config-snapshot'] =
|
|
702
|
+
parsed['eslint-config-snapshot'] = configObject
|
|
687
703
|
await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8')
|
|
688
704
|
process.stdout.write('Created config in package.json under "eslint-config-snapshot"\n')
|
|
689
705
|
return 0
|
|
690
706
|
}
|
|
691
707
|
|
|
708
|
+
async function resolveInitConfigObject(
|
|
709
|
+
cwd: string,
|
|
710
|
+
preset: InitPreset,
|
|
711
|
+
nonInteractive: boolean
|
|
712
|
+
): Promise<Record<string, unknown>> {
|
|
713
|
+
if (preset === 'minimal') {
|
|
714
|
+
return {}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (preset === 'full') {
|
|
718
|
+
return getFullPresetObject()
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return buildRecommendedPresetObject(cwd, nonInteractive)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function buildRecommendedPresetObject(cwd: string, nonInteractive: boolean): Promise<Record<string, unknown>> {
|
|
725
|
+
const workspaces = await discoverInitWorkspaces(cwd)
|
|
726
|
+
const useInteractiveGrouping = !nonInteractive && process.stdin.isTTY && process.stdout.isTTY
|
|
727
|
+
const assignments = useInteractiveGrouping ? await askRecommendedGroupAssignments(workspaces) : new Map<string, number>()
|
|
728
|
+
return buildRecommendedConfigFromAssignments(workspaces, assignments)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function buildRecommendedConfigFromAssignments(
|
|
732
|
+
workspaces: string[],
|
|
733
|
+
assignments: Map<string, number>
|
|
734
|
+
): Record<string, unknown> {
|
|
735
|
+
const groupNumbers = [...new Set(assignments.values())].sort((a, b) => a - b)
|
|
736
|
+
if (groupNumbers.length === 0) {
|
|
737
|
+
return {}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const explicitGroups = groupNumbers.map((number) => ({
|
|
741
|
+
name: `group-${number}`,
|
|
742
|
+
match: workspaces.filter((workspace) => assignments.get(workspace) === number)
|
|
743
|
+
}))
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
grouping: {
|
|
747
|
+
mode: 'match',
|
|
748
|
+
groups: [...explicitGroups, { name: 'default', match: ['**/*'] }]
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function discoverInitWorkspaces(cwd: string): Promise<string[]> {
|
|
754
|
+
const discovered = await discoverWorkspaces({ cwd, workspaceInput: { mode: 'discover' } })
|
|
755
|
+
if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === '.')) {
|
|
756
|
+
return discovered.workspacesRel
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const packageJsonPath = path.join(cwd, 'package.json')
|
|
760
|
+
try {
|
|
761
|
+
const raw = await readFile(packageJsonPath, 'utf8')
|
|
762
|
+
const parsed = JSON.parse(raw) as { workspaces?: string[] | { packages?: string[] } }
|
|
763
|
+
let workspacePatterns: string[] = []
|
|
764
|
+
if (Array.isArray(parsed.workspaces)) {
|
|
765
|
+
workspacePatterns = parsed.workspaces
|
|
766
|
+
} else if (parsed.workspaces && typeof parsed.workspaces === 'object' && Array.isArray(parsed.workspaces.packages)) {
|
|
767
|
+
workspacePatterns = parsed.workspaces.packages
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (workspacePatterns.length === 0) {
|
|
771
|
+
return discovered.workspacesRel
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const workspacePackageFiles = await fg(
|
|
775
|
+
workspacePatterns.map((pattern) => `${trimTrailingSlashes(pattern)}/package.json`),
|
|
776
|
+
{ cwd, onlyFiles: true, dot: true }
|
|
777
|
+
)
|
|
778
|
+
const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath(path.dirname(entry))))].sort((a, b) =>
|
|
779
|
+
a.localeCompare(b)
|
|
780
|
+
)
|
|
781
|
+
if (workspaceDirs.length > 0) {
|
|
782
|
+
return workspaceDirs
|
|
783
|
+
}
|
|
784
|
+
} catch {
|
|
785
|
+
// fallback to discovered output
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return discovered.workspacesRel
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function trimTrailingSlashes(value: string): string {
|
|
792
|
+
let normalized = value
|
|
793
|
+
while (normalized.endsWith('/')) {
|
|
794
|
+
normalized = normalized.slice(0, -1)
|
|
795
|
+
}
|
|
796
|
+
return normalized
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async function askRecommendedGroupAssignments(workspaces: string[]): Promise<Map<string, number>> {
|
|
800
|
+
const { checkbox, select } = await import('@inquirer/prompts')
|
|
801
|
+
process.stdout.write('Recommended setup: select only workspaces that should leave default group "*".\n')
|
|
802
|
+
const overrides = await checkbox<string>({
|
|
803
|
+
message: 'Workspaces outside default group:',
|
|
804
|
+
choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
|
|
805
|
+
pageSize: Math.min(12, Math.max(4, workspaces.length))
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
const assignments = new Map<string, number>()
|
|
809
|
+
let nextGroup = 1
|
|
810
|
+
for (const workspace of overrides) {
|
|
811
|
+
const usedGroups = [...new Set(assignments.values())].sort((a, b) => a - b)
|
|
812
|
+
while (usedGroups.includes(nextGroup)) {
|
|
813
|
+
nextGroup += 1
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const selected = await select<number | 'new'>({
|
|
817
|
+
message: `Select group for ${workspace}`,
|
|
818
|
+
choices: [
|
|
819
|
+
...usedGroups.map((group) => ({ name: `group-${group}`, value: group })),
|
|
820
|
+
{ name: `create new group (group-${nextGroup})`, value: 'new' }
|
|
821
|
+
]
|
|
822
|
+
})
|
|
823
|
+
const groupNumber = selected === 'new' ? nextGroup : selected
|
|
824
|
+
assignments.set(workspace, groupNumber)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return assignments
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function toConfigScaffold(configObject: Record<string, unknown>): string {
|
|
831
|
+
if (Object.keys(configObject).length === 0) {
|
|
832
|
+
return getConfigScaffold('minimal')
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return `export default ${JSON.stringify(configObject, null, 2)}\n`
|
|
836
|
+
}
|
|
837
|
+
|
|
692
838
|
function getFullPresetObject() {
|
|
693
839
|
return {
|
|
694
840
|
workspaceInput: { mode: 'discover' },
|
|
@@ -859,3 +1005,22 @@ function formatShortPrint(
|
|
|
859
1005
|
|
|
860
1006
|
return `${lines.join('\n')}\n`
|
|
861
1007
|
}
|
|
1008
|
+
|
|
1009
|
+
function formatShortConfig(payload: {
|
|
1010
|
+
source: string
|
|
1011
|
+
workspaceInput: unknown
|
|
1012
|
+
workspaces: string[]
|
|
1013
|
+
grouping: { mode: string; allowEmptyGroups: boolean; groups: Array<{ name: string; workspaces: string[] }> }
|
|
1014
|
+
sampling: unknown
|
|
1015
|
+
}): string {
|
|
1016
|
+
const lines: string[] = [
|
|
1017
|
+
`source: ${payload.source}`,
|
|
1018
|
+
`workspaces (${payload.workspaces.length}): ${payload.workspaces.join(', ') || '(none)'}`,
|
|
1019
|
+
`grouping mode: ${payload.grouping.mode} (allow empty: ${payload.grouping.allowEmptyGroups})`
|
|
1020
|
+
]
|
|
1021
|
+
for (const group of payload.grouping.groups) {
|
|
1022
|
+
lines.push(`group ${group.name} (${group.workspaces.length}): ${group.workspaces.join(', ') || '(none)'}`)
|
|
1023
|
+
}
|
|
1024
|
+
lines.push(`workspaceInput: ${JSON.stringify(payload.workspaceInput)}`, `sampling: ${JSON.stringify(payload.sampling)}`)
|
|
1025
|
+
return `${lines.join('\n')}\n`
|
|
1026
|
+
}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
-
import {
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { buildRecommendedConfigFromAssignments, runCli } from '../src/index.js'
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
await rm(path.join(fixtureRoot, '.eslint-config-snapshot'), { recursive: true, force: true })
|
|
12
|
-
})
|
|
8
|
+
const fixtureTemplateRoot = path.resolve('test/fixtures/repo')
|
|
9
|
+
let tmpDir = ''
|
|
10
|
+
let fixtureRoot = ''
|
|
13
11
|
|
|
14
12
|
beforeEach(async () => {
|
|
15
|
-
await
|
|
13
|
+
tmpDir = await mkdtemp(path.join(os.tmpdir(), 'snapshot-cli-integration-'))
|
|
14
|
+
fixtureRoot = path.join(tmpDir, 'repo')
|
|
15
|
+
await cp(fixtureTemplateRoot, fixtureRoot, { recursive: true })
|
|
16
|
+
|
|
16
17
|
await mkdir(path.join(fixtureRoot, 'packages/ws-a/node_modules/eslint/bin'), { recursive: true })
|
|
17
18
|
await mkdir(path.join(fixtureRoot, 'packages/ws-b/node_modules/eslint/bin'), { recursive: true })
|
|
18
19
|
|
|
@@ -35,24 +36,31 @@ beforeEach(async () => {
|
|
|
35
36
|
)
|
|
36
37
|
})
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
if (tmpDir) {
|
|
41
|
+
await rm(tmpDir, { recursive: true, force: true })
|
|
42
|
+
tmpDir = ''
|
|
43
|
+
fixtureRoot = ''
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe.sequential('cli integration', () => {
|
|
48
|
+
it('builds recommended config as dynamic-only when no static overrides are selected', () => {
|
|
49
|
+
const config = buildRecommendedConfigFromAssignments(['packages/ws-a', 'packages/ws-b'], new Map())
|
|
50
|
+
expect(config).toEqual({})
|
|
47
51
|
})
|
|
48
52
|
|
|
49
|
-
it('
|
|
50
|
-
|
|
51
|
-
expect(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
it('builds recommended config with static overrides plus dynamic catch-all', () => {
|
|
54
|
+
const config = buildRecommendedConfigFromAssignments(['packages/ws-a', 'packages/ws-b'], new Map([['packages/ws-b', 2]]))
|
|
55
|
+
expect(config).toEqual({
|
|
56
|
+
grouping: {
|
|
57
|
+
mode: 'match',
|
|
58
|
+
groups: [
|
|
59
|
+
{ name: 'group-2', match: ['packages/ws-b'] },
|
|
60
|
+
{ name: 'default', match: ['**/*'] }
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
})
|
|
56
64
|
})
|
|
57
65
|
|
|
58
66
|
it('snapshot writes deterministic snapshot files', async () => {
|
|
@@ -126,11 +134,22 @@ no-debugger: off
|
|
|
126
134
|
expect(code).toBe(0)
|
|
127
135
|
|
|
128
136
|
const content = await readFile(path.join(tmp, 'eslint-config-snapshot.config.mjs'), 'utf8')
|
|
129
|
-
expect(content).toContain("workspaceInput
|
|
137
|
+
expect(content).toContain('"workspaceInput"')
|
|
138
|
+
expect(content).toContain('"grouping"')
|
|
139
|
+
expect(content).toContain('"sampling"')
|
|
130
140
|
|
|
131
141
|
await rm(tmp, { recursive: true, force: true })
|
|
132
142
|
})
|
|
133
143
|
|
|
144
|
+
it('config prints effective evaluated config and exits 0', async () => {
|
|
145
|
+
const writeSpy = vi.spyOn(process.stdout, 'write')
|
|
146
|
+
const code = await runCli('config', fixtureRoot)
|
|
147
|
+
expect(code).toBe(0)
|
|
148
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"workspaceInput"'))
|
|
149
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"workspaces"'))
|
|
150
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"groups"'))
|
|
151
|
+
})
|
|
152
|
+
|
|
134
153
|
it('init writes minimal config to package.json when target=package-json', async () => {
|
|
135
154
|
const tmp = await mkdtemp(path.join(os.tmpdir(), 'snapshot-init-pkg-'))
|
|
136
155
|
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,44 @@ 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'?: Record<string, unknown>
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
expect(parsed['eslint-config-snapshot']).toEqual({})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('init recommended --show-effective prints preview without explicit sampling block', async () => {
|
|
285
|
+
const initRoot = path.join(tmpDir, 'init-recommended-preview-case')
|
|
286
|
+
await rm(initRoot, { recursive: true, force: true })
|
|
287
|
+
await cp(fixtureRoot, initRoot, { recursive: true })
|
|
288
|
+
repoRoot = initRoot
|
|
289
|
+
|
|
290
|
+
await rm(path.join(repoRoot, 'eslint-config-snapshot.config.mjs'), { force: true })
|
|
291
|
+
|
|
292
|
+
const result = run(['init', '--yes', '--target', 'package-json', '--preset', 'recommended', '--show-effective'])
|
|
293
|
+
expect(result.status).toBe(0)
|
|
294
|
+
expect(result.stdout).toContain('Effective config preview:')
|
|
295
|
+
expect(result.stdout).toContain('{}')
|
|
296
|
+
expect(result.stdout).not.toContain('"workspaceInput"')
|
|
297
|
+
expect(result.stdout).not.toContain('"grouping"')
|
|
298
|
+
expect(result.stdout).not.toContain('"sampling"')
|
|
299
|
+
})
|
|
300
|
+
|
|
253
301
|
it('init fails early on existing config unless --force is provided', async () => {
|
|
254
302
|
const initRoot = path.join(tmpDir, 'init-force-case')
|
|
255
303
|
await rm(initRoot, { recursive: true, force: true })
|
|
@@ -335,14 +383,14 @@ no-debugger: off
|
|
|
335
383
|
expect(result.stderr).toBe('')
|
|
336
384
|
})
|
|
337
385
|
|
|
338
|
-
it('prints init help with
|
|
386
|
+
it('prints init help with select-prompt and force guidance', () => {
|
|
339
387
|
const result = run(['init', '--help'])
|
|
340
388
|
expect(result.status).toBe(0)
|
|
341
389
|
expect(result.stdout).toContain('Initialize config (file or package.json)')
|
|
342
390
|
expect(result.stdout).toContain('-f, --force')
|
|
343
|
-
expect(result.stdout).toContain('Runs interactive
|
|
344
|
-
expect(result.stdout).toContain('
|
|
345
|
-
expect(result.stdout).toContain('
|
|
391
|
+
expect(result.stdout).toContain('Runs interactive select prompts for target/preset.')
|
|
392
|
+
expect(result.stdout).toContain('Recommended preset uses checkbox selection')
|
|
393
|
+
expect(result.stdout).toContain('--show-effective')
|
|
346
394
|
expect(result.stdout).toContain('--yes --force --target file --preset full')
|
|
347
395
|
expect(result.stderr).toBe('')
|
|
348
396
|
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
console.log(JSON.stringify({ rules: { 'no-console': 1, eqeqeq: [2, 'always'] } }))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
console.log(JSON.stringify({ rules: { 'no-console': 2, 'no-debugger': 0 } }))
|