@eslint-config-snapshot/cli 0.3.2 → 0.5.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/dist/index.cjs +318 -71
- package/dist/index.js +320 -72
- package/package.json +2 -2
- package/src/index.ts +370 -77
- package/test/cli.integration.test.ts +8 -0
- package/test/cli.npm-isolated.integration.test.ts +27 -6
- package/test/cli.pnpm-isolated.integration.test.ts +27 -10
- package/test/cli.terminal.integration.test.ts +1 -1
package/src/index.ts
CHANGED
|
@@ -5,18 +5,20 @@ import {
|
|
|
5
5
|
buildSnapshot,
|
|
6
6
|
diffSnapshots,
|
|
7
7
|
discoverWorkspaces,
|
|
8
|
-
|
|
8
|
+
extractRulesForWorkspaceSamples,
|
|
9
9
|
findConfigPath,
|
|
10
10
|
getConfigScaffold,
|
|
11
11
|
hasDiff,
|
|
12
12
|
loadConfig,
|
|
13
13
|
normalizePath,
|
|
14
14
|
readSnapshotFile,
|
|
15
|
+
resolveEslintVersionForWorkspace,
|
|
15
16
|
sampleWorkspaceFiles,
|
|
16
17
|
writeSnapshotFile
|
|
17
18
|
} from '@eslint-config-snapshot/api'
|
|
18
19
|
import { Command, CommanderError, InvalidArgumentError } from 'commander'
|
|
19
20
|
import fg from 'fast-glob'
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
20
22
|
import { access, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
21
23
|
import path from 'node:path'
|
|
22
24
|
import { createInterface } from 'node:readline'
|
|
@@ -32,44 +34,69 @@ type CheckFormat = 'summary' | 'status' | 'diff'
|
|
|
32
34
|
type PrintFormat = 'json' | 'short'
|
|
33
35
|
type InitTarget = 'file' | 'package-json'
|
|
34
36
|
type InitPreset = 'recommended' | 'minimal' | 'full'
|
|
37
|
+
type RuleEntry = [severity: 'off' | 'warn' | 'error'] | [severity: 'off' | 'warn' | 'error', options: unknown]
|
|
38
|
+
type RuleObject = Record<string, RuleEntry>
|
|
39
|
+
type GroupEslintVersions = Map<string, string[]>
|
|
35
40
|
|
|
36
41
|
type RootOptions = {
|
|
37
42
|
update?: boolean
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
type RunTimer = {
|
|
46
|
+
label: string
|
|
47
|
+
startedAtMs: number
|
|
48
|
+
pausedMs: number
|
|
49
|
+
pauseStartedAtMs: number | undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let activeRunTimer: RunTimer | undefined
|
|
53
|
+
let cachedCliVersion: string | undefined
|
|
54
|
+
|
|
40
55
|
export async function runCli(command: string | undefined, cwd: string, flags: string[] = []): Promise<number> {
|
|
41
56
|
const argv = command ? [command, ...flags] : [...flags]
|
|
42
57
|
return runArgv(argv, cwd)
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
async function runArgv(argv: string[], cwd: string): Promise<number> {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
const invocationLabel = resolveInvocationLabel(argv)
|
|
62
|
+
beginRunTimer(invocationLabel)
|
|
63
|
+
let exitCode = 1
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const hasCommandToken = argv.some((token) => !token.startsWith('-'))
|
|
67
|
+
if (!hasCommandToken) {
|
|
68
|
+
exitCode = await runDefaultInvocation(argv, cwd)
|
|
69
|
+
return exitCode
|
|
70
|
+
}
|
|
50
71
|
|
|
51
|
-
|
|
72
|
+
let actionCode: number | undefined
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
const program = createProgram(cwd, (code) => {
|
|
75
|
+
actionCode = code
|
|
76
|
+
})
|
|
56
77
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
try {
|
|
79
|
+
await program.parseAsync(argv, { from: 'user' })
|
|
80
|
+
} catch (error: unknown) {
|
|
81
|
+
if (error instanceof CommanderError) {
|
|
82
|
+
if (error.code === 'commander.helpDisplayed') {
|
|
83
|
+
exitCode = 0
|
|
84
|
+
return exitCode
|
|
85
|
+
}
|
|
86
|
+
exitCode = error.exitCode
|
|
87
|
+
return exitCode
|
|
63
88
|
}
|
|
64
|
-
|
|
89
|
+
|
|
90
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
91
|
+
process.stderr.write(`${message}\n`)
|
|
92
|
+
return 1
|
|
65
93
|
}
|
|
66
94
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
95
|
+
exitCode = actionCode ?? 0
|
|
96
|
+
return exitCode
|
|
97
|
+
} finally {
|
|
98
|
+
endRunTimer(exitCode)
|
|
70
99
|
}
|
|
71
|
-
|
|
72
|
-
return actionCode ?? 0
|
|
73
100
|
}
|
|
74
101
|
|
|
75
102
|
async function runDefaultInvocation(argv: string[], cwd: string): Promise<number> {
|
|
@@ -238,6 +265,15 @@ function parseInitPreset(value: string): InitPreset {
|
|
|
238
265
|
|
|
239
266
|
async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation = false): Promise<number> {
|
|
240
267
|
const foundConfig = await findConfigPath(cwd)
|
|
268
|
+
const storedSnapshots = await loadStoredSnapshots(cwd)
|
|
269
|
+
|
|
270
|
+
if (format !== 'status') {
|
|
271
|
+
writeRunContextHeader(cwd, defaultInvocation ? 'check' : `check:${format}`, foundConfig?.path, storedSnapshots)
|
|
272
|
+
if (shouldShowRunLogs()) {
|
|
273
|
+
writeSubtleInfo('Analyzing current ESLint configuration...\n')
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
241
277
|
if (!foundConfig) {
|
|
242
278
|
writeSubtleInfo(
|
|
243
279
|
'Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n'
|
|
@@ -257,8 +293,6 @@ async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation
|
|
|
257
293
|
|
|
258
294
|
throw error
|
|
259
295
|
}
|
|
260
|
-
const storedSnapshots = await loadStoredSnapshots(cwd)
|
|
261
|
-
|
|
262
296
|
if (storedSnapshots.size === 0) {
|
|
263
297
|
const summary = summarizeSnapshots(currentSnapshots)
|
|
264
298
|
process.stdout.write(
|
|
@@ -286,6 +320,7 @@ async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation
|
|
|
286
320
|
}
|
|
287
321
|
|
|
288
322
|
const changes = compareSnapshotMaps(storedSnapshots, currentSnapshots)
|
|
323
|
+
const eslintVersionsByGroup = shouldShowRunLogs() ? await resolveGroupEslintVersions(cwd) : new Map<string, string[]>()
|
|
289
324
|
|
|
290
325
|
if (format === 'status') {
|
|
291
326
|
if (changes.length === 0) {
|
|
@@ -301,6 +336,7 @@ async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation
|
|
|
301
336
|
if (format === 'diff') {
|
|
302
337
|
if (changes.length === 0) {
|
|
303
338
|
process.stdout.write('Great news: no snapshot changes detected.\n')
|
|
339
|
+
writeEslintVersionSummary(eslintVersionsByGroup)
|
|
304
340
|
return 0
|
|
305
341
|
}
|
|
306
342
|
|
|
@@ -312,11 +348,17 @@ async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation
|
|
|
312
348
|
return 1
|
|
313
349
|
}
|
|
314
350
|
|
|
315
|
-
return printWhatChanged(changes, currentSnapshots)
|
|
351
|
+
return printWhatChanged(changes, currentSnapshots, eslintVersionsByGroup)
|
|
316
352
|
}
|
|
317
353
|
|
|
318
354
|
async function executeUpdate(cwd: string, printSummary: boolean): Promise<number> {
|
|
319
355
|
const foundConfig = await findConfigPath(cwd)
|
|
356
|
+
const storedSnapshots = await loadStoredSnapshots(cwd)
|
|
357
|
+
writeRunContextHeader(cwd, 'update', foundConfig?.path, storedSnapshots)
|
|
358
|
+
if (shouldShowRunLogs()) {
|
|
359
|
+
writeSubtleInfo('Analyzing current ESLint configuration...\n')
|
|
360
|
+
}
|
|
361
|
+
|
|
320
362
|
if (!foundConfig) {
|
|
321
363
|
writeSubtleInfo(
|
|
322
364
|
'Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n'
|
|
@@ -340,13 +382,25 @@ async function executeUpdate(cwd: string, printSummary: boolean): Promise<number
|
|
|
340
382
|
|
|
341
383
|
if (printSummary) {
|
|
342
384
|
const summary = summarizeSnapshots(currentSnapshots)
|
|
343
|
-
|
|
385
|
+
const color = createColorizer()
|
|
386
|
+
const eslintVersionsByGroup = shouldShowRunLogs() ? await resolveGroupEslintVersions(cwd) : new Map<string, string[]>()
|
|
387
|
+
writeSectionTitle('Summary', color)
|
|
388
|
+
process.stdout.write(
|
|
389
|
+
`Baseline updated: ${summary.groups} groups, ${summary.rules} rules.\nSeverity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.\n`
|
|
390
|
+
)
|
|
391
|
+
writeEslintVersionSummary(eslintVersionsByGroup)
|
|
344
392
|
}
|
|
345
393
|
|
|
346
394
|
return 0
|
|
347
395
|
}
|
|
348
396
|
|
|
349
397
|
async function executePrint(cwd: string, format: PrintFormat): Promise<void> {
|
|
398
|
+
const foundConfig = await findConfigPath(cwd)
|
|
399
|
+
const storedSnapshots = await loadStoredSnapshots(cwd)
|
|
400
|
+
writeRunContextHeader(cwd, `print:${format}`, foundConfig?.path, storedSnapshots)
|
|
401
|
+
if (shouldShowRunLogs()) {
|
|
402
|
+
writeSubtleInfo('Analyzing current ESLint configuration...\n')
|
|
403
|
+
}
|
|
350
404
|
const currentSnapshots = await computeCurrentSnapshots(cwd)
|
|
351
405
|
|
|
352
406
|
if (format === 'short') {
|
|
@@ -363,6 +417,11 @@ async function executePrint(cwd: string, format: PrintFormat): Promise<void> {
|
|
|
363
417
|
|
|
364
418
|
async function executeConfig(cwd: string, format: PrintFormat): Promise<void> {
|
|
365
419
|
const foundConfig = await findConfigPath(cwd)
|
|
420
|
+
const storedSnapshots = await loadStoredSnapshots(cwd)
|
|
421
|
+
writeRunContextHeader(cwd, `config:${format}`, foundConfig?.path, storedSnapshots)
|
|
422
|
+
if (shouldShowRunLogs()) {
|
|
423
|
+
writeSubtleInfo('Resolving effective runtime configuration...\n')
|
|
424
|
+
}
|
|
366
425
|
const config = await loadConfig(cwd)
|
|
367
426
|
const resolved = await resolveWorkspaceAssignments(cwd, config)
|
|
368
427
|
const payload = {
|
|
@@ -400,23 +459,23 @@ async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSn
|
|
|
400
459
|
let extractedCount = 0
|
|
401
460
|
let lastExtractionError: string | undefined
|
|
402
461
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
462
|
+
const sampledAbs = sampled.map((sampledRel) => path.resolve(workspaceAbs, sampledRel))
|
|
463
|
+
const results = await extractRulesForWorkspaceSamples(workspaceAbs, sampledAbs)
|
|
464
|
+
|
|
465
|
+
for (const result of results) {
|
|
466
|
+
if (result.rules) {
|
|
467
|
+
extractedForGroup.push(result.rules)
|
|
407
468
|
extractedCount += 1
|
|
408
|
-
|
|
409
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
410
|
-
if (
|
|
411
|
-
message.startsWith('Invalid JSON from eslint --print-config') ||
|
|
412
|
-
message.startsWith('Empty ESLint print-config output')
|
|
413
|
-
) {
|
|
414
|
-
lastExtractionError = message
|
|
415
|
-
continue
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
throw error
|
|
469
|
+
continue
|
|
419
470
|
}
|
|
471
|
+
|
|
472
|
+
const message = result.error instanceof Error ? result.error.message : String(result.error)
|
|
473
|
+
if (isRecoverableExtractionError(message)) {
|
|
474
|
+
lastExtractionError = message
|
|
475
|
+
continue
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
throw result.error ?? new Error(message)
|
|
420
479
|
}
|
|
421
480
|
|
|
422
481
|
if (extractedCount === 0) {
|
|
@@ -434,6 +493,15 @@ async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSn
|
|
|
434
493
|
return snapshots
|
|
435
494
|
}
|
|
436
495
|
|
|
496
|
+
function isRecoverableExtractionError(message: string): boolean {
|
|
497
|
+
return (
|
|
498
|
+
message.startsWith('Invalid JSON from eslint --print-config') ||
|
|
499
|
+
message.startsWith('Empty ESLint print-config output') ||
|
|
500
|
+
message.includes('File ignored because of a matching ignore pattern') ||
|
|
501
|
+
message.includes('File ignored by default')
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
|
|
437
505
|
async function resolveWorkspaceAssignments(cwd: string, config: Awaited<ReturnType<typeof loadConfig>>) {
|
|
438
506
|
const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput })
|
|
439
507
|
|
|
@@ -594,8 +662,8 @@ async function runInit(
|
|
|
594
662
|
|
|
595
663
|
async function askInitPreferences(): Promise<{ target: InitTarget; preset: InitPreset }> {
|
|
596
664
|
const { select } = await import('@inquirer/prompts')
|
|
597
|
-
const target = await askInitTarget(select)
|
|
598
|
-
const preset = await askInitPreset(select)
|
|
665
|
+
const target = await runPromptWithPausedTimer(() => askInitTarget(select))
|
|
666
|
+
const preset = await runPromptWithPausedTimer(() => askInitPreset(select))
|
|
599
667
|
return { target, preset }
|
|
600
668
|
}
|
|
601
669
|
|
|
@@ -625,8 +693,10 @@ async function askInitPreset(
|
|
|
625
693
|
}
|
|
626
694
|
|
|
627
695
|
function askQuestion(rl: ReturnType<typeof createInterface>, prompt: string): Promise<string> {
|
|
696
|
+
pauseRunTimer()
|
|
628
697
|
return new Promise((resolve) => {
|
|
629
698
|
rl.question(prompt, (answer) => {
|
|
699
|
+
resumeRunTimer()
|
|
630
700
|
resolve(answer)
|
|
631
701
|
})
|
|
632
702
|
})
|
|
@@ -802,11 +872,13 @@ async function askRecommendedGroupAssignments(workspaces: string[]): Promise<Map
|
|
|
802
872
|
'Recommended setup: default group "*" is a dynamic catch-all for every discovered workspace.\n'
|
|
803
873
|
)
|
|
804
874
|
process.stdout.write('Select only workspaces that should move to explicit static groups.\n')
|
|
805
|
-
const overrides = await
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
875
|
+
const overrides = await runPromptWithPausedTimer(() =>
|
|
876
|
+
checkbox<string>({
|
|
877
|
+
message: 'Choose exception workspaces (leave empty to keep all in default "*"):',
|
|
878
|
+
choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
|
|
879
|
+
pageSize: Math.min(12, Math.max(4, workspaces.length))
|
|
880
|
+
})
|
|
881
|
+
)
|
|
810
882
|
|
|
811
883
|
const assignments = new Map<string, number>()
|
|
812
884
|
let nextGroup = 1
|
|
@@ -816,13 +888,15 @@ async function askRecommendedGroupAssignments(workspaces: string[]): Promise<Map
|
|
|
816
888
|
nextGroup += 1
|
|
817
889
|
}
|
|
818
890
|
|
|
819
|
-
const selected = await
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
891
|
+
const selected = await runPromptWithPausedTimer(() =>
|
|
892
|
+
select<number | 'new'>({
|
|
893
|
+
message: `Select group for ${workspace}`,
|
|
894
|
+
choices: [
|
|
895
|
+
...usedGroups.map((group) => ({ name: `group-${group}`, value: group })),
|
|
896
|
+
{ name: `create new group (group-${nextGroup})`, value: 'new' }
|
|
897
|
+
]
|
|
898
|
+
})
|
|
899
|
+
)
|
|
826
900
|
const groupNumber = selected === 'new' ? nextGroup : selected
|
|
827
901
|
assignments.set(workspace, groupNumber)
|
|
828
902
|
}
|
|
@@ -873,27 +947,34 @@ if (isDirectCliExecution()) {
|
|
|
873
947
|
void main()
|
|
874
948
|
}
|
|
875
949
|
|
|
876
|
-
function printWhatChanged(
|
|
950
|
+
function printWhatChanged(
|
|
951
|
+
changes: Array<{ groupId: string; diff: SnapshotDiff }>,
|
|
952
|
+
currentSnapshots: Map<string, BuiltSnapshot>,
|
|
953
|
+
eslintVersionsByGroup: GroupEslintVersions
|
|
954
|
+
): number {
|
|
877
955
|
const color = createColorizer()
|
|
878
956
|
const currentSummary = summarizeSnapshots(currentSnapshots)
|
|
879
957
|
const changeSummary = summarizeChanges(changes)
|
|
880
958
|
|
|
881
959
|
if (changes.length === 0) {
|
|
882
960
|
process.stdout.write(color.green('Great news: no snapshot drift detected.\n'))
|
|
961
|
+
writeSectionTitle('Summary', color)
|
|
883
962
|
process.stdout.write(
|
|
884
|
-
|
|
963
|
+
`- baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules\n- severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off\n`
|
|
885
964
|
)
|
|
965
|
+
writeEslintVersionSummary(eslintVersionsByGroup)
|
|
886
966
|
return 0
|
|
887
967
|
}
|
|
888
968
|
|
|
889
969
|
process.stdout.write(color.red('Heads up: snapshot drift detected.\n'))
|
|
970
|
+
writeSectionTitle('Summary', color)
|
|
890
971
|
process.stdout.write(
|
|
891
|
-
|
|
892
|
-
)
|
|
893
|
-
process.stdout.write(
|
|
894
|
-
`Current rules: ${currentSummary.rules} (severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off)\n\n`
|
|
972
|
+
`- changed groups: ${changes.length}\n- introduced rules: ${changeSummary.introduced}\n- removed rules: ${changeSummary.removed}\n- severity changes: ${changeSummary.severity}\n- options changes: ${changeSummary.options}\n- workspace membership changes: ${changeSummary.workspace}\n- current baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules\n- current severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off\n\n`
|
|
895
973
|
)
|
|
974
|
+
writeEslintVersionSummary(eslintVersionsByGroup)
|
|
975
|
+
process.stdout.write('\n')
|
|
896
976
|
|
|
977
|
+
writeSectionTitle('Changes', color)
|
|
897
978
|
for (const change of changes) {
|
|
898
979
|
process.stdout.write(color.bold(`group ${change.groupId}\n`))
|
|
899
980
|
const lines = formatDiff(change.groupId, change.diff).split('\n').slice(1)
|
|
@@ -908,6 +989,10 @@ function printWhatChanged(changes: Array<{ groupId: string; diff: SnapshotDiff }
|
|
|
908
989
|
return 1
|
|
909
990
|
}
|
|
910
991
|
|
|
992
|
+
function writeSectionTitle(title: string, color: ReturnType<typeof createColorizer>): void {
|
|
993
|
+
process.stdout.write(`${color.bold(title)}\n`)
|
|
994
|
+
}
|
|
995
|
+
|
|
911
996
|
function summarizeChanges(changes: Array<{ groupId: string; diff: SnapshotDiff }>) {
|
|
912
997
|
let introduced = 0
|
|
913
998
|
let removed = 0
|
|
@@ -925,22 +1010,7 @@ function summarizeChanges(changes: Array<{ groupId: string; diff: SnapshotDiff }
|
|
|
925
1010
|
}
|
|
926
1011
|
|
|
927
1012
|
function summarizeSnapshots(snapshots: Map<string, BuiltSnapshot>) {
|
|
928
|
-
|
|
929
|
-
let error = 0
|
|
930
|
-
let warn = 0
|
|
931
|
-
let off = 0
|
|
932
|
-
for (const snapshot of snapshots.values()) {
|
|
933
|
-
for (const entry of Object.values(snapshot.rules)) {
|
|
934
|
-
rules += 1
|
|
935
|
-
if (entry[0] === 'error') {
|
|
936
|
-
error += 1
|
|
937
|
-
} else if (entry[0] === 'warn') {
|
|
938
|
-
warn += 1
|
|
939
|
-
} else {
|
|
940
|
-
off += 1
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
}
|
|
1013
|
+
const { rules, error, warn, off } = countRuleSeverities([...snapshots.values()].map((snapshot) => snapshot.rules))
|
|
944
1014
|
return { groups: snapshots.size, rules, error, warn, off }
|
|
945
1015
|
}
|
|
946
1016
|
|
|
@@ -974,11 +1044,234 @@ function writeSubtleInfo(text: string): void {
|
|
|
974
1044
|
process.stdout.write(color.dim(text))
|
|
975
1045
|
}
|
|
976
1046
|
|
|
1047
|
+
function resolveInvocationLabel(argv: string[]): string {
|
|
1048
|
+
const commandToken = argv.find((entry) => !entry.startsWith('-'))
|
|
1049
|
+
if (commandToken) {
|
|
1050
|
+
return commandToken
|
|
1051
|
+
}
|
|
1052
|
+
if (argv.includes('-u') || argv.includes('--update')) {
|
|
1053
|
+
return 'update'
|
|
1054
|
+
}
|
|
1055
|
+
if (argv.includes('-h') || argv.includes('--help')) {
|
|
1056
|
+
return 'help'
|
|
1057
|
+
}
|
|
1058
|
+
return 'check'
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function shouldShowRunLogs(): boolean {
|
|
1062
|
+
if (process.env.ESLINT_CONFIG_SNAPSHOT_NO_PROGRESS === '1') {
|
|
1063
|
+
return false
|
|
1064
|
+
}
|
|
1065
|
+
return process.stdout.isTTY === true
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function beginRunTimer(label: string): void {
|
|
1069
|
+
if (!shouldShowRunLogs()) {
|
|
1070
|
+
activeRunTimer = undefined
|
|
1071
|
+
return
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
activeRunTimer = {
|
|
1075
|
+
label,
|
|
1076
|
+
startedAtMs: Date.now(),
|
|
1077
|
+
pausedMs: 0,
|
|
1078
|
+
pauseStartedAtMs: undefined
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function endRunTimer(exitCode: number): void {
|
|
1083
|
+
if (!activeRunTimer || !shouldShowRunLogs()) {
|
|
1084
|
+
return
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (activeRunTimer.pauseStartedAtMs !== undefined) {
|
|
1088
|
+
activeRunTimer.pausedMs += Date.now() - activeRunTimer.pauseStartedAtMs
|
|
1089
|
+
activeRunTimer.pauseStartedAtMs = undefined
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const elapsedMs = Math.max(0, Date.now() - activeRunTimer.startedAtMs - activeRunTimer.pausedMs)
|
|
1093
|
+
const color = createColorizer()
|
|
1094
|
+
const status = exitCode === 0 ? color.green('done') : color.red('failed')
|
|
1095
|
+
const seconds = (elapsedMs / 1000).toFixed(2)
|
|
1096
|
+
writeSubtleInfo(`Run ${status} in ${seconds}s\n`)
|
|
1097
|
+
activeRunTimer = undefined
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function pauseRunTimer(): void {
|
|
1101
|
+
if (!activeRunTimer || activeRunTimer.pauseStartedAtMs !== undefined) {
|
|
1102
|
+
return
|
|
1103
|
+
}
|
|
1104
|
+
activeRunTimer.pauseStartedAtMs = Date.now()
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function resumeRunTimer(): void {
|
|
1108
|
+
if (!activeRunTimer || activeRunTimer.pauseStartedAtMs === undefined) {
|
|
1109
|
+
return
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
activeRunTimer.pausedMs += Date.now() - activeRunTimer.pauseStartedAtMs
|
|
1113
|
+
activeRunTimer.pauseStartedAtMs = undefined
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
async function runPromptWithPausedTimer<T>(prompt: () => Promise<T>): Promise<T> {
|
|
1117
|
+
pauseRunTimer()
|
|
1118
|
+
try {
|
|
1119
|
+
return await prompt()
|
|
1120
|
+
} finally {
|
|
1121
|
+
resumeRunTimer()
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function readCliVersion(): string {
|
|
1126
|
+
if (cachedCliVersion !== undefined) {
|
|
1127
|
+
return cachedCliVersion
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const scriptPath = process.argv[1]
|
|
1131
|
+
if (!scriptPath) {
|
|
1132
|
+
cachedCliVersion = 'unknown'
|
|
1133
|
+
return cachedCliVersion
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
let current = path.resolve(path.dirname(scriptPath))
|
|
1137
|
+
while (true) {
|
|
1138
|
+
const packageJsonPath = path.join(current, 'package.json')
|
|
1139
|
+
if (existsSync(packageJsonPath)) {
|
|
1140
|
+
try {
|
|
1141
|
+
const raw = readFileSync(packageJsonPath, 'utf8')
|
|
1142
|
+
const parsed = JSON.parse(raw) as { version?: string }
|
|
1143
|
+
cachedCliVersion = parsed.version ?? 'unknown'
|
|
1144
|
+
return cachedCliVersion
|
|
1145
|
+
} catch {
|
|
1146
|
+
break
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const parent = path.dirname(current)
|
|
1151
|
+
if (parent === current) {
|
|
1152
|
+
break
|
|
1153
|
+
}
|
|
1154
|
+
current = parent
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
cachedCliVersion = 'unknown'
|
|
1158
|
+
return cachedCliVersion
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function writeRunContextHeader(
|
|
1162
|
+
cwd: string,
|
|
1163
|
+
commandLabel: string,
|
|
1164
|
+
configPath: string | undefined,
|
|
1165
|
+
storedSnapshots: Map<string, StoredSnapshot>
|
|
1166
|
+
): void {
|
|
1167
|
+
if (!shouldShowRunLogs()) {
|
|
1168
|
+
return
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const color = createColorizer()
|
|
1172
|
+
process.stdout.write(color.bold(`eslint-config-snapshot v${readCliVersion()}\n`))
|
|
1173
|
+
process.stdout.write(`Command: ${commandLabel}\n`)
|
|
1174
|
+
process.stdout.write(`Repository: ${cwd}\n`)
|
|
1175
|
+
process.stdout.write(`Config: ${formatConfigSource(cwd, configPath)}\n`)
|
|
1176
|
+
process.stdout.write(`Baseline: ${formatStoredSnapshotSummary(storedSnapshots)}\n\n`)
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function formatConfigSource(cwd: string, configPath: string | undefined): string {
|
|
1180
|
+
if (!configPath) {
|
|
1181
|
+
return 'built-in defaults'
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const rel = normalizePath(path.relative(cwd, configPath))
|
|
1185
|
+
if (path.basename(configPath) === 'package.json') {
|
|
1186
|
+
return `${rel} (eslint-config-snapshot field)`
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
return rel
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function formatStoredSnapshotSummary(storedSnapshots: Map<string, StoredSnapshot>): string {
|
|
1193
|
+
if (storedSnapshots.size === 0) {
|
|
1194
|
+
return 'none'
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const summary = summarizeStoredSnapshots(storedSnapshots)
|
|
1198
|
+
return `${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off)`
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
async function resolveGroupEslintVersions(cwd: string): Promise<GroupEslintVersions> {
|
|
1202
|
+
const config = await loadConfig(cwd)
|
|
1203
|
+
const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config)
|
|
1204
|
+
const result = new Map<string, string[]>()
|
|
1205
|
+
|
|
1206
|
+
for (const group of assignments) {
|
|
1207
|
+
const versions = new Set<string>()
|
|
1208
|
+
for (const workspaceRel of group.workspaces) {
|
|
1209
|
+
const workspaceAbs = path.resolve(discovery.rootAbs, workspaceRel)
|
|
1210
|
+
versions.add(resolveEslintVersionForWorkspace(workspaceAbs))
|
|
1211
|
+
}
|
|
1212
|
+
result.set(group.name, [...versions].sort((a, b) => a.localeCompare(b)))
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
return result
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function writeEslintVersionSummary(eslintVersionsByGroup: GroupEslintVersions): void {
|
|
1219
|
+
if (!shouldShowRunLogs() || eslintVersionsByGroup.size === 0) {
|
|
1220
|
+
return
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const allVersions = new Set<string>()
|
|
1224
|
+
for (const versions of eslintVersionsByGroup.values()) {
|
|
1225
|
+
for (const version of versions) {
|
|
1226
|
+
allVersions.add(version)
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const sortedAllVersions = [...allVersions].sort((a, b) => a.localeCompare(b))
|
|
1231
|
+
if (sortedAllVersions.length === 1) {
|
|
1232
|
+
process.stdout.write(`- eslint runtime: ${sortedAllVersions[0]} (all groups)\n`)
|
|
1233
|
+
return
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
process.stdout.write('- eslint runtime by group:\n')
|
|
1237
|
+
const sortedEntries = [...eslintVersionsByGroup.entries()].sort((a, b) => a[0].localeCompare(b[0]))
|
|
1238
|
+
for (const [groupName, versions] of sortedEntries) {
|
|
1239
|
+
process.stdout.write(` - ${groupName}: ${versions.join(', ')}\n`)
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function summarizeStoredSnapshots(snapshots: Map<string, StoredSnapshot>) {
|
|
1244
|
+
const { rules, error, warn, off } = countRuleSeverities([...snapshots.values()].map((snapshot) => snapshot.rules))
|
|
1245
|
+
return { groups: snapshots.size, rules, error, warn, off }
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function countRuleSeverities(ruleObjects: RuleObject[]) {
|
|
1249
|
+
let rules = 0
|
|
1250
|
+
let error = 0
|
|
1251
|
+
let warn = 0
|
|
1252
|
+
let off = 0
|
|
1253
|
+
|
|
1254
|
+
for (const rulesObject of ruleObjects) {
|
|
1255
|
+
for (const entry of Object.values(rulesObject)) {
|
|
1256
|
+
rules += 1
|
|
1257
|
+
if (entry[0] === 'error') {
|
|
1258
|
+
error += 1
|
|
1259
|
+
} else if (entry[0] === 'warn') {
|
|
1260
|
+
warn += 1
|
|
1261
|
+
} else {
|
|
1262
|
+
off += 1
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
return { rules, error, warn, off }
|
|
1268
|
+
}
|
|
1269
|
+
|
|
977
1270
|
function formatShortPrint(
|
|
978
1271
|
snapshots: Array<{
|
|
979
1272
|
groupId: string
|
|
980
1273
|
workspaces: string[]
|
|
981
|
-
rules:
|
|
1274
|
+
rules: RuleObject
|
|
982
1275
|
}>
|
|
983
1276
|
): string {
|
|
984
1277
|
const lines: string[] = []
|
|
@@ -8,8 +8,11 @@ import { buildRecommendedConfigFromAssignments, runCli } from '../src/index.js'
|
|
|
8
8
|
const fixtureTemplateRoot = path.resolve('test/fixtures/repo')
|
|
9
9
|
let tmpDir = ''
|
|
10
10
|
let fixtureRoot = ''
|
|
11
|
+
let previousNoProgress = ''
|
|
11
12
|
|
|
12
13
|
beforeEach(async () => {
|
|
14
|
+
previousNoProgress = process.env.ESLINT_CONFIG_SNAPSHOT_NO_PROGRESS ?? ''
|
|
15
|
+
process.env.ESLINT_CONFIG_SNAPSHOT_NO_PROGRESS = '1'
|
|
13
16
|
tmpDir = await mkdtemp(path.join(os.tmpdir(), 'snapshot-cli-integration-'))
|
|
14
17
|
fixtureRoot = path.join(tmpDir, 'repo')
|
|
15
18
|
await cp(fixtureTemplateRoot, fixtureRoot, { recursive: true })
|
|
@@ -37,6 +40,11 @@ beforeEach(async () => {
|
|
|
37
40
|
})
|
|
38
41
|
|
|
39
42
|
afterEach(async () => {
|
|
43
|
+
if (previousNoProgress === '') {
|
|
44
|
+
delete process.env.ESLINT_CONFIG_SNAPSHOT_NO_PROGRESS
|
|
45
|
+
} else {
|
|
46
|
+
process.env.ESLINT_CONFIG_SNAPSHOT_NO_PROGRESS = previousNoProgress
|
|
47
|
+
}
|
|
40
48
|
if (tmpDir) {
|
|
41
49
|
await rm(tmpDir, { recursive: true, force: true })
|
|
42
50
|
tmpDir = ''
|