@eslint-config-snapshot/cli 0.1.0 → 0.1.5
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 +41 -0
- package/README.md +59 -0
- package/package.json +10 -2
- package/project.json +2 -2
- package/src/index.ts +467 -467
- package/test/cli.integration.test.ts +167 -167
- package/test/cli.npm-isolated.integration.test.ts +76 -76
- package/test/cli.pnpm-isolated.integration.test.ts +49 -49
- package/test/cli.terminal.integration.test.ts +197 -197
- package/test/fixtures/npm-isolated-template/package.json +7 -7
- package/test/fixtures/npm-isolated-template/packages/ws-a/.eslintrc.cjs +1 -1
- package/test/fixtures/npm-isolated-template/packages/ws-a/src/index.ts +1 -1
- package/test/fixtures/npm-isolated-template/packages/ws-b/.eslintrc.cjs +1 -1
- package/test/fixtures/npm-isolated-template/packages/ws-b/src/index.ts +1 -1
- package/test/fixtures/repo/eslint-config-snapshot.config.mjs +1 -1
- package/test/fixtures/repo/package.json +1 -1
- package/test/fixtures/repo/packages/ws-a/package.json +1 -1
- package/test/fixtures/repo/packages/ws-a/src/index.ts +1 -1
- package/test/fixtures/repo/packages/ws-b/package.json +1 -1
- package/test/fixtures/repo/packages/ws-b/src/index.ts +1 -1
- package/tsconfig.json +12 -12
package/src/index.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
aggregateRules,
|
|
4
|
-
assignGroupsByMatch,
|
|
5
|
-
buildSnapshot,
|
|
6
|
-
diffSnapshots,
|
|
7
|
-
discoverWorkspaces,
|
|
8
|
-
extractRulesFromPrintConfig,
|
|
9
|
-
findConfigPath,
|
|
10
|
-
getConfigScaffold,
|
|
11
|
-
hasDiff,
|
|
12
|
-
loadConfig,
|
|
13
|
-
readSnapshotFile,
|
|
14
|
-
sampleWorkspaceFiles,
|
|
3
|
+
aggregateRules,
|
|
4
|
+
assignGroupsByMatch,
|
|
5
|
+
buildSnapshot,
|
|
6
|
+
diffSnapshots,
|
|
7
|
+
discoverWorkspaces,
|
|
8
|
+
extractRulesFromPrintConfig,
|
|
9
|
+
findConfigPath,
|
|
10
|
+
getConfigScaffold,
|
|
11
|
+
hasDiff,
|
|
12
|
+
loadConfig,
|
|
13
|
+
readSnapshotFile,
|
|
14
|
+
sampleWorkspaceFiles,
|
|
15
15
|
writeSnapshotFile
|
|
16
16
|
} from '@eslint-config-snapshot/api'
|
|
17
17
|
import { Command, CommanderError, InvalidArgumentError } from 'commander'
|
|
@@ -19,15 +19,15 @@ import fg from 'fast-glob'
|
|
|
19
19
|
import { access, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
20
20
|
import path from 'node:path'
|
|
21
21
|
import { createInterface } from 'node:readline'
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
|
|
23
|
+
|
|
24
24
|
const SNAPSHOT_DIR = '.eslint-config-snapshot'
|
|
25
25
|
const UPDATE_HINT = 'Tip: when you intentionally accept changes, run `eslint-config-snapshot --update` to refresh the baseline.\n'
|
|
26
|
-
|
|
27
|
-
type BuiltSnapshot = Awaited<ReturnType<typeof buildSnapshot>>
|
|
28
|
-
type StoredSnapshot = Awaited<ReturnType<typeof readSnapshotFile>>
|
|
29
|
-
type SnapshotDiff = ReturnType<typeof diffSnapshots>
|
|
30
|
-
type CheckFormat = 'summary' | 'status' | 'diff'
|
|
26
|
+
|
|
27
|
+
type BuiltSnapshot = Awaited<ReturnType<typeof buildSnapshot>>
|
|
28
|
+
type StoredSnapshot = Awaited<ReturnType<typeof readSnapshotFile>>
|
|
29
|
+
type SnapshotDiff = ReturnType<typeof diffSnapshots>
|
|
30
|
+
type CheckFormat = 'summary' | 'status' | 'diff'
|
|
31
31
|
type PrintFormat = 'json' | 'short'
|
|
32
32
|
type InitTarget = 'file' | 'package-json'
|
|
33
33
|
type InitPreset = 'minimal' | 'full'
|
|
@@ -35,109 +35,109 @@ type InitPreset = 'minimal' | 'full'
|
|
|
35
35
|
type RootOptions = {
|
|
36
36
|
update?: boolean
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
export async function runCli(command: string | undefined, cwd: string, flags: string[] = []): Promise<number> {
|
|
40
|
-
const argv = command ? [command, ...flags] : [...flags]
|
|
41
|
-
return runArgv(argv, cwd)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function runArgv(argv: string[], cwd: string): Promise<number> {
|
|
45
|
-
const hasCommandToken = argv.some((token) => !token.startsWith('-'))
|
|
46
|
-
if (!hasCommandToken) {
|
|
47
|
-
return runDefaultInvocation(argv, cwd)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
let actionCode: number | undefined
|
|
51
|
-
|
|
52
|
-
const program = createProgram(cwd, (code) => {
|
|
53
|
-
actionCode = code
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
await program.parseAsync(argv, { from: 'user' })
|
|
58
|
-
} catch (error: unknown) {
|
|
59
|
-
if (error instanceof CommanderError) {
|
|
60
|
-
if (error.code === 'commander.helpDisplayed') {
|
|
61
|
-
return 0
|
|
62
|
-
}
|
|
63
|
-
return error.exitCode
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
67
|
-
process.stderr.write(`${message}\n`)
|
|
68
|
-
return 1
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return actionCode ?? 0
|
|
72
|
-
}
|
|
73
|
-
|
|
38
|
+
|
|
39
|
+
export async function runCli(command: string | undefined, cwd: string, flags: string[] = []): Promise<number> {
|
|
40
|
+
const argv = command ? [command, ...flags] : [...flags]
|
|
41
|
+
return runArgv(argv, cwd)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runArgv(argv: string[], cwd: string): Promise<number> {
|
|
45
|
+
const hasCommandToken = argv.some((token) => !token.startsWith('-'))
|
|
46
|
+
if (!hasCommandToken) {
|
|
47
|
+
return runDefaultInvocation(argv, cwd)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let actionCode: number | undefined
|
|
51
|
+
|
|
52
|
+
const program = createProgram(cwd, (code) => {
|
|
53
|
+
actionCode = code
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await program.parseAsync(argv, { from: 'user' })
|
|
58
|
+
} catch (error: unknown) {
|
|
59
|
+
if (error instanceof CommanderError) {
|
|
60
|
+
if (error.code === 'commander.helpDisplayed') {
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
63
|
+
return error.exitCode
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
67
|
+
process.stderr.write(`${message}\n`)
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return actionCode ?? 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
74
|
async function runDefaultInvocation(argv: string[], cwd: string): Promise<number> {
|
|
75
|
-
const known = new Set(['-u', '--update', '-h', '--help'])
|
|
76
|
-
for (const token of argv) {
|
|
77
|
-
if (!known.has(token)) {
|
|
78
|
-
process.stderr.write(`error: unknown option '${token}'\n`)
|
|
79
|
-
return 1
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (argv.includes('-h') || argv.includes('--help')) {
|
|
84
|
-
const program = createProgram(cwd, () => {
|
|
85
|
-
// no-op
|
|
86
|
-
})
|
|
87
|
-
program.outputHelp()
|
|
88
|
-
return 0
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (argv.includes('-u') || argv.includes('--update')) {
|
|
92
|
-
return executeUpdate(cwd, true)
|
|
93
|
-
}
|
|
94
|
-
|
|
75
|
+
const known = new Set(['-u', '--update', '-h', '--help'])
|
|
76
|
+
for (const token of argv) {
|
|
77
|
+
if (!known.has(token)) {
|
|
78
|
+
process.stderr.write(`error: unknown option '${token}'\n`)
|
|
79
|
+
return 1
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (argv.includes('-h') || argv.includes('--help')) {
|
|
84
|
+
const program = createProgram(cwd, () => {
|
|
85
|
+
// no-op
|
|
86
|
+
})
|
|
87
|
+
program.outputHelp()
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (argv.includes('-u') || argv.includes('--update')) {
|
|
92
|
+
return executeUpdate(cwd, true)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
95
|
return executeCheck(cwd, 'summary', true)
|
|
96
96
|
}
|
|
97
|
-
|
|
98
|
-
function createProgram(cwd: string, onActionExit: (code: number) => void): Command {
|
|
99
|
-
const program = new Command()
|
|
100
|
-
|
|
101
|
-
program
|
|
102
|
-
.name('eslint-config-snapshot')
|
|
103
|
-
.description('Deterministic ESLint config snapshot drift checker for workspaces')
|
|
104
|
-
.showHelpAfterError('(add --help for usage)')
|
|
105
|
-
.option('-u, --update', 'Update snapshots (default mode only)')
|
|
106
|
-
|
|
107
|
-
program.hook('preAction', (thisCommand) => {
|
|
108
|
-
const opts = thisCommand.opts<RootOptions>()
|
|
109
|
-
if (opts.update) {
|
|
110
|
-
throw new Error('--update can only be used without a command')
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
program
|
|
115
|
-
.command('check')
|
|
116
|
-
.description('Compare current state against stored snapshots')
|
|
117
|
-
.option('--format <format>', 'Output format: summary|status|diff', parseCheckFormat, 'summary')
|
|
118
|
-
.action(async (opts: { format: CheckFormat }) => {
|
|
119
|
-
onActionExit(await executeCheck(cwd, opts.format))
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
program
|
|
123
|
-
.command('update')
|
|
124
|
-
.alias('snapshot')
|
|
125
|
-
.description('Compute and write snapshots to .eslint-config-snapshot/')
|
|
126
|
-
.action(async () => {
|
|
127
|
-
onActionExit(await executeUpdate(cwd, true))
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
program
|
|
131
|
-
.command('print')
|
|
132
|
-
.description('Print aggregated rules')
|
|
133
|
-
.option('--format <format>', 'Output format: json|short', parsePrintFormat, 'json')
|
|
134
|
-
.option('--short', 'Alias for --format short')
|
|
135
|
-
.action(async (opts: { format: PrintFormat; short?: boolean }) => {
|
|
136
|
-
const format: PrintFormat = opts.short ? 'short' : opts.format
|
|
137
|
-
await executePrint(cwd, format)
|
|
138
|
-
onActionExit(0)
|
|
139
|
-
})
|
|
140
|
-
|
|
97
|
+
|
|
98
|
+
function createProgram(cwd: string, onActionExit: (code: number) => void): Command {
|
|
99
|
+
const program = new Command()
|
|
100
|
+
|
|
101
|
+
program
|
|
102
|
+
.name('eslint-config-snapshot')
|
|
103
|
+
.description('Deterministic ESLint config snapshot drift checker for workspaces')
|
|
104
|
+
.showHelpAfterError('(add --help for usage)')
|
|
105
|
+
.option('-u, --update', 'Update snapshots (default mode only)')
|
|
106
|
+
|
|
107
|
+
program.hook('preAction', (thisCommand) => {
|
|
108
|
+
const opts = thisCommand.opts<RootOptions>()
|
|
109
|
+
if (opts.update) {
|
|
110
|
+
throw new Error('--update can only be used without a command')
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
program
|
|
115
|
+
.command('check')
|
|
116
|
+
.description('Compare current state against stored snapshots')
|
|
117
|
+
.option('--format <format>', 'Output format: summary|status|diff', parseCheckFormat, 'summary')
|
|
118
|
+
.action(async (opts: { format: CheckFormat }) => {
|
|
119
|
+
onActionExit(await executeCheck(cwd, opts.format))
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
program
|
|
123
|
+
.command('update')
|
|
124
|
+
.alias('snapshot')
|
|
125
|
+
.description('Compute and write snapshots to .eslint-config-snapshot/')
|
|
126
|
+
.action(async () => {
|
|
127
|
+
onActionExit(await executeUpdate(cwd, true))
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
program
|
|
131
|
+
.command('print')
|
|
132
|
+
.description('Print aggregated rules')
|
|
133
|
+
.option('--format <format>', 'Output format: json|short', parsePrintFormat, 'json')
|
|
134
|
+
.option('--short', 'Alias for --format short')
|
|
135
|
+
.action(async (opts: { format: PrintFormat; short?: boolean }) => {
|
|
136
|
+
const format: PrintFormat = opts.short ? 'short' : opts.format
|
|
137
|
+
await executePrint(cwd, format)
|
|
138
|
+
onActionExit(0)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
141
|
program
|
|
142
142
|
.command('init')
|
|
143
143
|
.description('Initialize config (file or package.json)')
|
|
@@ -164,46 +164,46 @@ Examples:
|
|
|
164
164
|
.action(async (opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean }) => {
|
|
165
165
|
onActionExit(await runInit(cwd, opts))
|
|
166
166
|
})
|
|
167
|
-
|
|
168
|
-
// Backward-compatible aliases kept out of help.
|
|
169
|
-
program
|
|
170
|
-
.command('compare', { hidden: true })
|
|
171
|
-
.action(async () => {
|
|
172
|
-
onActionExit(await executeCheck(cwd, 'diff'))
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
program
|
|
176
|
-
.command('status', { hidden: true })
|
|
177
|
-
.action(async () => {
|
|
178
|
-
onActionExit(await executeCheck(cwd, 'status'))
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
program
|
|
182
|
-
.command('what-changed', { hidden: true })
|
|
183
|
-
.action(async () => {
|
|
184
|
-
onActionExit(await executeCheck(cwd, 'summary'))
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
program.exitOverride()
|
|
188
|
-
return program
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function parseCheckFormat(value: string): CheckFormat {
|
|
192
|
-
const normalized = value.trim().toLowerCase()
|
|
193
|
-
if (normalized === 'summary' || normalized === 'status' || normalized === 'diff') {
|
|
194
|
-
return normalized
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
throw new InvalidArgumentError('Expected one of: summary, status, diff')
|
|
198
|
-
}
|
|
199
|
-
|
|
167
|
+
|
|
168
|
+
// Backward-compatible aliases kept out of help.
|
|
169
|
+
program
|
|
170
|
+
.command('compare', { hidden: true })
|
|
171
|
+
.action(async () => {
|
|
172
|
+
onActionExit(await executeCheck(cwd, 'diff'))
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
program
|
|
176
|
+
.command('status', { hidden: true })
|
|
177
|
+
.action(async () => {
|
|
178
|
+
onActionExit(await executeCheck(cwd, 'status'))
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
program
|
|
182
|
+
.command('what-changed', { hidden: true })
|
|
183
|
+
.action(async () => {
|
|
184
|
+
onActionExit(await executeCheck(cwd, 'summary'))
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
program.exitOverride()
|
|
188
|
+
return program
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function parseCheckFormat(value: string): CheckFormat {
|
|
192
|
+
const normalized = value.trim().toLowerCase()
|
|
193
|
+
if (normalized === 'summary' || normalized === 'status' || normalized === 'diff') {
|
|
194
|
+
return normalized
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
throw new InvalidArgumentError('Expected one of: summary, status, diff')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
200
|
function parsePrintFormat(value: string): PrintFormat {
|
|
201
|
-
const normalized = value.trim().toLowerCase()
|
|
202
|
-
if (normalized === 'json' || normalized === 'short') {
|
|
203
|
-
return normalized
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
throw new InvalidArgumentError('Expected one of: json, short')
|
|
201
|
+
const normalized = value.trim().toLowerCase()
|
|
202
|
+
if (normalized === 'json' || normalized === 'short') {
|
|
203
|
+
return normalized
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw new InvalidArgumentError('Expected one of: json, short')
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
function parseInitTarget(value: string): InitTarget {
|
|
@@ -223,7 +223,7 @@ function parseInitPreset(value: string): InitPreset {
|
|
|
223
223
|
|
|
224
224
|
throw new InvalidArgumentError('Expected one of: minimal, full')
|
|
225
225
|
}
|
|
226
|
-
|
|
226
|
+
|
|
227
227
|
async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation = false): Promise<number> {
|
|
228
228
|
const foundConfig = await findConfigPath(cwd)
|
|
229
229
|
if (!foundConfig) {
|
|
@@ -272,9 +272,9 @@ async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation
|
|
|
272
272
|
process.stdout.write('Run `eslint-config-snapshot --update` to create your first baseline.\n')
|
|
273
273
|
return 1
|
|
274
274
|
}
|
|
275
|
-
|
|
276
|
-
const changes = compareSnapshotMaps(storedSnapshots, currentSnapshots)
|
|
277
|
-
|
|
275
|
+
|
|
276
|
+
const changes = compareSnapshotMaps(storedSnapshots, currentSnapshots)
|
|
277
|
+
|
|
278
278
|
if (format === 'status') {
|
|
279
279
|
if (changes.length === 0) {
|
|
280
280
|
process.stdout.write('clean\n')
|
|
@@ -285,13 +285,13 @@ async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation
|
|
|
285
285
|
writeSubtleInfo(UPDATE_HINT)
|
|
286
286
|
return 1
|
|
287
287
|
}
|
|
288
|
-
|
|
288
|
+
|
|
289
289
|
if (format === 'diff') {
|
|
290
290
|
if (changes.length === 0) {
|
|
291
291
|
process.stdout.write('Great news: no snapshot changes detected.\n')
|
|
292
292
|
return 0
|
|
293
293
|
}
|
|
294
|
-
|
|
294
|
+
|
|
295
295
|
for (const change of changes) {
|
|
296
296
|
process.stdout.write(`${formatDiff(change.groupId, change.diff)}\n`)
|
|
297
297
|
}
|
|
@@ -299,10 +299,10 @@ async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation
|
|
|
299
299
|
|
|
300
300
|
return 1
|
|
301
301
|
}
|
|
302
|
-
|
|
303
|
-
return printWhatChanged(changes, currentSnapshots)
|
|
304
|
-
}
|
|
305
|
-
|
|
302
|
+
|
|
303
|
+
return printWhatChanged(changes, currentSnapshots)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
306
|
async function executeUpdate(cwd: string, printSummary: boolean): Promise<number> {
|
|
307
307
|
const foundConfig = await findConfigPath(cwd)
|
|
308
308
|
if (!foundConfig) {
|
|
@@ -310,7 +310,7 @@ async function executeUpdate(cwd: string, printSummary: boolean): Promise<number
|
|
|
310
310
|
'Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n'
|
|
311
311
|
)
|
|
312
312
|
}
|
|
313
|
-
|
|
313
|
+
|
|
314
314
|
let currentSnapshots: Map<string, BuiltSnapshot>
|
|
315
315
|
try {
|
|
316
316
|
currentSnapshots = await computeCurrentSnapshots(cwd)
|
|
@@ -325,194 +325,194 @@ async function executeUpdate(cwd: string, printSummary: boolean): Promise<number
|
|
|
325
325
|
throw error
|
|
326
326
|
}
|
|
327
327
|
await writeSnapshots(cwd, currentSnapshots)
|
|
328
|
-
|
|
329
|
-
if (printSummary) {
|
|
330
|
-
const summary = summarizeSnapshots(currentSnapshots)
|
|
328
|
+
|
|
329
|
+
if (printSummary) {
|
|
330
|
+
const summary = summarizeSnapshots(currentSnapshots)
|
|
331
331
|
process.stdout.write(`Baseline updated: ${summary.groups} groups, ${summary.rules} rules.\n`)
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return 0
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
async function executePrint(cwd: string, format: PrintFormat): Promise<void> {
|
|
338
|
-
const currentSnapshots = await computeCurrentSnapshots(cwd)
|
|
339
|
-
|
|
340
|
-
if (format === 'short') {
|
|
341
|
-
process.stdout.write(formatShortPrint([...currentSnapshots.values()]))
|
|
342
|
-
return
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const output = [...currentSnapshots.values()].map((snapshot) => ({
|
|
346
|
-
groupId: snapshot.groupId,
|
|
347
|
-
rules: snapshot.rules
|
|
348
|
-
}))
|
|
349
|
-
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSnapshot>> {
|
|
353
|
-
const config = await loadConfig(cwd)
|
|
354
|
-
const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput })
|
|
355
|
-
|
|
356
|
-
const assignments =
|
|
357
|
-
config.grouping.mode === 'standalone'
|
|
358
|
-
? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] }))
|
|
359
|
-
: assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: 'default', match: ['**/*'] }])
|
|
360
|
-
|
|
361
|
-
const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false
|
|
362
|
-
if (!allowEmptyGroups) {
|
|
363
|
-
const empty = assignments.filter((group) => group.workspaces.length === 0)
|
|
364
|
-
if (empty.length > 0) {
|
|
365
|
-
throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(', ')}`)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const snapshots = new Map<string, BuiltSnapshot>()
|
|
370
|
-
|
|
371
|
-
for (const group of assignments) {
|
|
372
|
-
const extractedForGroup = []
|
|
373
|
-
|
|
374
|
-
for (const workspaceRel of group.workspaces) {
|
|
375
|
-
const workspaceAbs = path.resolve(discovery.rootAbs, workspaceRel)
|
|
376
|
-
const sampled = await sampleWorkspaceFiles(workspaceAbs, config.sampling)
|
|
377
|
-
let extractedCount = 0
|
|
378
|
-
let lastExtractionError: string | undefined
|
|
379
|
-
|
|
380
|
-
for (const sampledRel of sampled) {
|
|
381
|
-
const sampledAbs = path.resolve(workspaceAbs, sampledRel)
|
|
382
|
-
try {
|
|
383
|
-
extractedForGroup.push(extractRulesFromPrintConfig(workspaceAbs, sampledAbs))
|
|
384
|
-
extractedCount += 1
|
|
385
|
-
} catch (error: unknown) {
|
|
386
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
387
|
-
if (
|
|
388
|
-
message.startsWith('Invalid JSON from eslint --print-config') ||
|
|
389
|
-
message.startsWith('Empty ESLint print-config output')
|
|
390
|
-
) {
|
|
391
|
-
lastExtractionError = message
|
|
392
|
-
continue
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
throw error
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (extractedCount === 0) {
|
|
400
|
-
const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : ''
|
|
401
|
-
throw new Error(
|
|
402
|
-
`Unable to extract ESLint config for workspace ${workspaceRel}. All sampled files were ignored or produced non-JSON output.${context}`
|
|
403
|
-
)
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const aggregated = aggregateRules(extractedForGroup)
|
|
408
|
-
snapshots.set(group.name, buildSnapshot(group.name, group.workspaces, aggregated))
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return snapshots
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
async function loadStoredSnapshots(cwd: string): Promise<Map<string, StoredSnapshot>> {
|
|
415
|
-
const dir = path.join(cwd, SNAPSHOT_DIR)
|
|
416
|
-
const files = await fg('**/*.json', { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true })
|
|
417
|
-
const snapshots = new Map<string, StoredSnapshot>()
|
|
418
|
-
const sortedFiles = [...files].sort((a, b) => a.localeCompare(b))
|
|
419
|
-
|
|
420
|
-
for (const file of sortedFiles) {
|
|
421
|
-
const snapshot = await readSnapshotFile(file)
|
|
422
|
-
snapshots.set(snapshot.groupId, snapshot)
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return snapshots
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async function writeSnapshots(cwd: string, snapshots: Map<string, BuiltSnapshot>): Promise<void> {
|
|
429
|
-
await mkdir(path.join(cwd, SNAPSHOT_DIR), { recursive: true })
|
|
430
|
-
for (const snapshot of snapshots.values()) {
|
|
431
|
-
await writeSnapshotFile(path.join(cwd, SNAPSHOT_DIR), snapshot)
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function compareSnapshotMaps(before: Map<string, StoredSnapshot>, after: Map<string, BuiltSnapshot>) {
|
|
436
|
-
const ids = [...new Set([...before.keys(), ...after.keys()])].sort()
|
|
437
|
-
const changes: Array<{ groupId: string; diff: SnapshotDiff }> = []
|
|
438
|
-
|
|
439
|
-
for (const id of ids) {
|
|
440
|
-
const prev =
|
|
441
|
-
before.get(id) ??
|
|
442
|
-
({
|
|
443
|
-
formatVersion: 1,
|
|
444
|
-
groupId: id,
|
|
445
|
-
workspaces: [],
|
|
446
|
-
rules: {}
|
|
447
|
-
} as const)
|
|
448
|
-
|
|
449
|
-
const next =
|
|
450
|
-
after.get(id) ??
|
|
451
|
-
({
|
|
452
|
-
formatVersion: 1,
|
|
453
|
-
groupId: id,
|
|
454
|
-
workspaces: [],
|
|
455
|
-
rules: {}
|
|
456
|
-
} as const)
|
|
457
|
-
|
|
458
|
-
const diff = diffSnapshots(prev, next)
|
|
459
|
-
if (hasDiff(diff)) {
|
|
460
|
-
changes.push({ groupId: id, diff })
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
return changes
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function formatDiff(groupId: string, diff: SnapshotDiff): string {
|
|
468
|
-
const lines = [`group: ${groupId}`]
|
|
469
|
-
|
|
470
|
-
addListSection(lines, 'introduced rules', diff.introducedRules)
|
|
471
|
-
addListSection(lines, 'removed rules', diff.removedRules)
|
|
472
|
-
|
|
473
|
-
if (diff.severityChanges.length > 0) {
|
|
474
|
-
lines.push('severity changed:')
|
|
475
|
-
for (const change of diff.severityChanges) {
|
|
476
|
-
lines.push(` - ${change.rule}: ${change.before} -> ${change.after}`)
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const optionChanges = getDisplayOptionChanges(diff)
|
|
481
|
-
if (optionChanges.length > 0) {
|
|
482
|
-
lines.push('options changed:')
|
|
483
|
-
for (const change of optionChanges) {
|
|
484
|
-
lines.push(` - ${change.rule}: ${formatValue(change.before)} -> ${formatValue(change.after)}`)
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
addListSection(lines, 'workspaces added', diff.workspaceMembershipChanges.added)
|
|
489
|
-
addListSection(lines, 'workspaces removed', diff.workspaceMembershipChanges.removed)
|
|
490
|
-
|
|
491
|
-
return lines.join('\n')
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function addListSection(lines: string[], title: string, values: string[]): void {
|
|
495
|
-
if (values.length === 0) {
|
|
496
|
-
return
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
lines.push(`${title}:`)
|
|
500
|
-
for (const value of values) {
|
|
501
|
-
lines.push(` - ${value}`)
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function formatValue(value: unknown): string {
|
|
506
|
-
const serialized = JSON.stringify(value)
|
|
507
|
-
return serialized === undefined ? 'undefined' : serialized
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function getDisplayOptionChanges(diff: SnapshotDiff): SnapshotDiff['optionChanges'] {
|
|
511
|
-
const removedRules = new Set(diff.removedRules)
|
|
512
|
-
const severityChangedRules = new Set(diff.severityChanges.map((change) => change.rule))
|
|
513
|
-
return diff.optionChanges.filter((change) => !removedRules.has(change.rule) && !severityChangedRules.has(change.rule))
|
|
514
|
-
}
|
|
515
|
-
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return 0
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function executePrint(cwd: string, format: PrintFormat): Promise<void> {
|
|
338
|
+
const currentSnapshots = await computeCurrentSnapshots(cwd)
|
|
339
|
+
|
|
340
|
+
if (format === 'short') {
|
|
341
|
+
process.stdout.write(formatShortPrint([...currentSnapshots.values()]))
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const output = [...currentSnapshots.values()].map((snapshot) => ({
|
|
346
|
+
groupId: snapshot.groupId,
|
|
347
|
+
rules: snapshot.rules
|
|
348
|
+
}))
|
|
349
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSnapshot>> {
|
|
353
|
+
const config = await loadConfig(cwd)
|
|
354
|
+
const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput })
|
|
355
|
+
|
|
356
|
+
const assignments =
|
|
357
|
+
config.grouping.mode === 'standalone'
|
|
358
|
+
? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] }))
|
|
359
|
+
: assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: 'default', match: ['**/*'] }])
|
|
360
|
+
|
|
361
|
+
const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false
|
|
362
|
+
if (!allowEmptyGroups) {
|
|
363
|
+
const empty = assignments.filter((group) => group.workspaces.length === 0)
|
|
364
|
+
if (empty.length > 0) {
|
|
365
|
+
throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(', ')}`)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const snapshots = new Map<string, BuiltSnapshot>()
|
|
370
|
+
|
|
371
|
+
for (const group of assignments) {
|
|
372
|
+
const extractedForGroup = []
|
|
373
|
+
|
|
374
|
+
for (const workspaceRel of group.workspaces) {
|
|
375
|
+
const workspaceAbs = path.resolve(discovery.rootAbs, workspaceRel)
|
|
376
|
+
const sampled = await sampleWorkspaceFiles(workspaceAbs, config.sampling)
|
|
377
|
+
let extractedCount = 0
|
|
378
|
+
let lastExtractionError: string | undefined
|
|
379
|
+
|
|
380
|
+
for (const sampledRel of sampled) {
|
|
381
|
+
const sampledAbs = path.resolve(workspaceAbs, sampledRel)
|
|
382
|
+
try {
|
|
383
|
+
extractedForGroup.push(extractRulesFromPrintConfig(workspaceAbs, sampledAbs))
|
|
384
|
+
extractedCount += 1
|
|
385
|
+
} catch (error: unknown) {
|
|
386
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
387
|
+
if (
|
|
388
|
+
message.startsWith('Invalid JSON from eslint --print-config') ||
|
|
389
|
+
message.startsWith('Empty ESLint print-config output')
|
|
390
|
+
) {
|
|
391
|
+
lastExtractionError = message
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
throw error
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (extractedCount === 0) {
|
|
400
|
+
const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : ''
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Unable to extract ESLint config for workspace ${workspaceRel}. All sampled files were ignored or produced non-JSON output.${context}`
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const aggregated = aggregateRules(extractedForGroup)
|
|
408
|
+
snapshots.set(group.name, buildSnapshot(group.name, group.workspaces, aggregated))
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return snapshots
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function loadStoredSnapshots(cwd: string): Promise<Map<string, StoredSnapshot>> {
|
|
415
|
+
const dir = path.join(cwd, SNAPSHOT_DIR)
|
|
416
|
+
const files = await fg('**/*.json', { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true })
|
|
417
|
+
const snapshots = new Map<string, StoredSnapshot>()
|
|
418
|
+
const sortedFiles = [...files].sort((a, b) => a.localeCompare(b))
|
|
419
|
+
|
|
420
|
+
for (const file of sortedFiles) {
|
|
421
|
+
const snapshot = await readSnapshotFile(file)
|
|
422
|
+
snapshots.set(snapshot.groupId, snapshot)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return snapshots
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function writeSnapshots(cwd: string, snapshots: Map<string, BuiltSnapshot>): Promise<void> {
|
|
429
|
+
await mkdir(path.join(cwd, SNAPSHOT_DIR), { recursive: true })
|
|
430
|
+
for (const snapshot of snapshots.values()) {
|
|
431
|
+
await writeSnapshotFile(path.join(cwd, SNAPSHOT_DIR), snapshot)
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function compareSnapshotMaps(before: Map<string, StoredSnapshot>, after: Map<string, BuiltSnapshot>) {
|
|
436
|
+
const ids = [...new Set([...before.keys(), ...after.keys()])].sort()
|
|
437
|
+
const changes: Array<{ groupId: string; diff: SnapshotDiff }> = []
|
|
438
|
+
|
|
439
|
+
for (const id of ids) {
|
|
440
|
+
const prev =
|
|
441
|
+
before.get(id) ??
|
|
442
|
+
({
|
|
443
|
+
formatVersion: 1,
|
|
444
|
+
groupId: id,
|
|
445
|
+
workspaces: [],
|
|
446
|
+
rules: {}
|
|
447
|
+
} as const)
|
|
448
|
+
|
|
449
|
+
const next =
|
|
450
|
+
after.get(id) ??
|
|
451
|
+
({
|
|
452
|
+
formatVersion: 1,
|
|
453
|
+
groupId: id,
|
|
454
|
+
workspaces: [],
|
|
455
|
+
rules: {}
|
|
456
|
+
} as const)
|
|
457
|
+
|
|
458
|
+
const diff = diffSnapshots(prev, next)
|
|
459
|
+
if (hasDiff(diff)) {
|
|
460
|
+
changes.push({ groupId: id, diff })
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return changes
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function formatDiff(groupId: string, diff: SnapshotDiff): string {
|
|
468
|
+
const lines = [`group: ${groupId}`]
|
|
469
|
+
|
|
470
|
+
addListSection(lines, 'introduced rules', diff.introducedRules)
|
|
471
|
+
addListSection(lines, 'removed rules', diff.removedRules)
|
|
472
|
+
|
|
473
|
+
if (diff.severityChanges.length > 0) {
|
|
474
|
+
lines.push('severity changed:')
|
|
475
|
+
for (const change of diff.severityChanges) {
|
|
476
|
+
lines.push(` - ${change.rule}: ${change.before} -> ${change.after}`)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const optionChanges = getDisplayOptionChanges(diff)
|
|
481
|
+
if (optionChanges.length > 0) {
|
|
482
|
+
lines.push('options changed:')
|
|
483
|
+
for (const change of optionChanges) {
|
|
484
|
+
lines.push(` - ${change.rule}: ${formatValue(change.before)} -> ${formatValue(change.after)}`)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
addListSection(lines, 'workspaces added', diff.workspaceMembershipChanges.added)
|
|
489
|
+
addListSection(lines, 'workspaces removed', diff.workspaceMembershipChanges.removed)
|
|
490
|
+
|
|
491
|
+
return lines.join('\n')
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function addListSection(lines: string[], title: string, values: string[]): void {
|
|
495
|
+
if (values.length === 0) {
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
lines.push(`${title}:`)
|
|
500
|
+
for (const value of values) {
|
|
501
|
+
lines.push(` - ${value}`)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function formatValue(value: unknown): string {
|
|
506
|
+
const serialized = JSON.stringify(value)
|
|
507
|
+
return serialized === undefined ? 'undefined' : serialized
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function getDisplayOptionChanges(diff: SnapshotDiff): SnapshotDiff['optionChanges'] {
|
|
511
|
+
const removedRules = new Set(diff.removedRules)
|
|
512
|
+
const severityChangedRules = new Set(diff.severityChanges.map((change) => change.rule))
|
|
513
|
+
return diff.optionChanges.filter((change) => !removedRules.has(change.rule) && !severityChangedRules.has(change.rule))
|
|
514
|
+
}
|
|
515
|
+
|
|
516
516
|
async function runInit(
|
|
517
517
|
cwd: string,
|
|
518
518
|
opts: { target?: InitTarget; preset?: InitPreset; force?: boolean; yes?: boolean } = {}
|
|
@@ -635,12 +635,12 @@ async function runInitInFile(cwd: string, preset: InitPreset, force: boolean): P
|
|
|
635
635
|
const candidates = [
|
|
636
636
|
'.eslint-config-snapshot.js',
|
|
637
637
|
'.eslint-config-snapshot.cjs',
|
|
638
|
-
'.eslint-config-snapshot.mjs',
|
|
639
|
-
'eslint-config-snapshot.config.js',
|
|
640
|
-
'eslint-config-snapshot.config.cjs',
|
|
641
|
-
'eslint-config-snapshot.config.mjs'
|
|
642
|
-
]
|
|
643
|
-
|
|
638
|
+
'.eslint-config-snapshot.mjs',
|
|
639
|
+
'eslint-config-snapshot.config.js',
|
|
640
|
+
'eslint-config-snapshot.config.cjs',
|
|
641
|
+
'eslint-config-snapshot.config.mjs'
|
|
642
|
+
]
|
|
643
|
+
|
|
644
644
|
for (const candidate of candidates) {
|
|
645
645
|
try {
|
|
646
646
|
await access(path.join(cwd, candidate))
|
|
@@ -652,7 +652,7 @@ async function runInitInFile(cwd: string, preset: InitPreset, force: boolean): P
|
|
|
652
652
|
// continue
|
|
653
653
|
}
|
|
654
654
|
}
|
|
655
|
-
|
|
655
|
+
|
|
656
656
|
const target = path.join(cwd, 'eslint-config-snapshot.config.mjs')
|
|
657
657
|
await writeFile(target, getConfigScaffold(preset), 'utf8')
|
|
658
658
|
process.stdout.write(`Created ${path.basename(target)}\n`)
|
|
@@ -704,7 +704,7 @@ function getFullPresetObject() {
|
|
|
704
704
|
}
|
|
705
705
|
}
|
|
706
706
|
}
|
|
707
|
-
|
|
707
|
+
|
|
708
708
|
export async function main(): Promise<void> {
|
|
709
709
|
const code = await runArgv(process.argv.slice(2), process.cwd())
|
|
710
710
|
process.exit(code)
|
|
@@ -723,12 +723,12 @@ function isDirectCliExecution(): boolean {
|
|
|
723
723
|
if (isDirectCliExecution()) {
|
|
724
724
|
void main()
|
|
725
725
|
}
|
|
726
|
-
|
|
727
|
-
function printWhatChanged(changes: Array<{ groupId: string; diff: SnapshotDiff }>, currentSnapshots: Map<string, BuiltSnapshot>): number {
|
|
728
|
-
const color = createColorizer()
|
|
729
|
-
const currentSummary = summarizeSnapshots(currentSnapshots)
|
|
730
|
-
const changeSummary = summarizeChanges(changes)
|
|
731
|
-
|
|
726
|
+
|
|
727
|
+
function printWhatChanged(changes: Array<{ groupId: string; diff: SnapshotDiff }>, currentSnapshots: Map<string, BuiltSnapshot>): number {
|
|
728
|
+
const color = createColorizer()
|
|
729
|
+
const currentSummary = summarizeSnapshots(currentSnapshots)
|
|
730
|
+
const changeSummary = summarizeChanges(changes)
|
|
731
|
+
|
|
732
732
|
if (changes.length === 0) {
|
|
733
733
|
process.stdout.write(color.green('Great news: no snapshot drift detected.\n'))
|
|
734
734
|
process.stdout.write(
|
|
@@ -738,76 +738,76 @@ function printWhatChanged(changes: Array<{ groupId: string; diff: SnapshotDiff }
|
|
|
738
738
|
}
|
|
739
739
|
|
|
740
740
|
process.stdout.write(color.red('Heads up: snapshot drift detected.\n'))
|
|
741
|
-
process.stdout.write(
|
|
742
|
-
`Changed groups: ${changes.length} | introduced: ${changeSummary.introduced} | removed: ${changeSummary.removed} | severity: ${changeSummary.severity} | options: ${changeSummary.options} | workspace membership: ${changeSummary.workspace}\n`
|
|
743
|
-
)
|
|
741
|
+
process.stdout.write(
|
|
742
|
+
`Changed groups: ${changes.length} | introduced: ${changeSummary.introduced} | removed: ${changeSummary.removed} | severity: ${changeSummary.severity} | options: ${changeSummary.options} | workspace membership: ${changeSummary.workspace}\n`
|
|
743
|
+
)
|
|
744
744
|
process.stdout.write(
|
|
745
745
|
`Current rules: ${currentSummary.rules} (severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off)\n\n`
|
|
746
746
|
)
|
|
747
|
-
|
|
747
|
+
|
|
748
748
|
for (const change of changes) {
|
|
749
|
-
process.stdout.write(color.bold(`group ${change.groupId}\n`))
|
|
750
|
-
const lines = formatDiff(change.groupId, change.diff).split('\n').slice(1)
|
|
751
|
-
for (const line of lines) {
|
|
752
|
-
const decorated = decorateDiffLine(line, color)
|
|
753
|
-
process.stdout.write(`${decorated}\n`)
|
|
754
|
-
}
|
|
755
|
-
process.stdout.write('\n')
|
|
749
|
+
process.stdout.write(color.bold(`group ${change.groupId}\n`))
|
|
750
|
+
const lines = formatDiff(change.groupId, change.diff).split('\n').slice(1)
|
|
751
|
+
for (const line of lines) {
|
|
752
|
+
const decorated = decorateDiffLine(line, color)
|
|
753
|
+
process.stdout.write(`${decorated}\n`)
|
|
754
|
+
}
|
|
755
|
+
process.stdout.write('\n')
|
|
756
756
|
}
|
|
757
757
|
writeSubtleInfo(UPDATE_HINT)
|
|
758
758
|
|
|
759
759
|
return 1
|
|
760
760
|
}
|
|
761
|
-
|
|
762
|
-
function summarizeChanges(changes: Array<{ groupId: string; diff: SnapshotDiff }>) {
|
|
763
|
-
let introduced = 0
|
|
764
|
-
let removed = 0
|
|
765
|
-
let severity = 0
|
|
766
|
-
let options = 0
|
|
767
|
-
let workspace = 0
|
|
768
|
-
for (const change of changes) {
|
|
769
|
-
introduced += change.diff.introducedRules.length
|
|
770
|
-
removed += change.diff.removedRules.length
|
|
771
|
-
severity += change.diff.severityChanges.length
|
|
772
|
-
options += getDisplayOptionChanges(change.diff).length
|
|
773
|
-
workspace += change.diff.workspaceMembershipChanges.added.length + change.diff.workspaceMembershipChanges.removed.length
|
|
774
|
-
}
|
|
775
|
-
return { introduced, removed, severity, options, workspace }
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function summarizeSnapshots(snapshots: Map<string, BuiltSnapshot>) {
|
|
779
|
-
let rules = 0
|
|
780
|
-
let error = 0
|
|
781
|
-
let warn = 0
|
|
782
|
-
let off = 0
|
|
783
|
-
for (const snapshot of snapshots.values()) {
|
|
784
|
-
for (const entry of Object.values(snapshot.rules)) {
|
|
785
|
-
rules += 1
|
|
786
|
-
if (entry[0] === 'error') {
|
|
787
|
-
error += 1
|
|
788
|
-
} else if (entry[0] === 'warn') {
|
|
789
|
-
warn += 1
|
|
790
|
-
} else {
|
|
791
|
-
off += 1
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
return { groups: snapshots.size, rules, error, warn, off }
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function decorateDiffLine(line: string, color: ReturnType<typeof createColorizer>): string {
|
|
799
|
-
if (line.startsWith('introduced rules:') || line.startsWith('workspaces added:')) {
|
|
800
|
-
return color.green(`+ ${line}`)
|
|
801
|
-
}
|
|
802
|
-
if (line.startsWith('removed rules:') || line.startsWith('workspaces removed:')) {
|
|
803
|
-
return color.red(`- ${line}`)
|
|
804
|
-
}
|
|
805
|
-
if (line.startsWith('severity changed:') || line.startsWith('options changed:')) {
|
|
806
|
-
return color.yellow(`~ ${line}`)
|
|
807
|
-
}
|
|
808
|
-
return line
|
|
809
|
-
}
|
|
810
|
-
|
|
761
|
+
|
|
762
|
+
function summarizeChanges(changes: Array<{ groupId: string; diff: SnapshotDiff }>) {
|
|
763
|
+
let introduced = 0
|
|
764
|
+
let removed = 0
|
|
765
|
+
let severity = 0
|
|
766
|
+
let options = 0
|
|
767
|
+
let workspace = 0
|
|
768
|
+
for (const change of changes) {
|
|
769
|
+
introduced += change.diff.introducedRules.length
|
|
770
|
+
removed += change.diff.removedRules.length
|
|
771
|
+
severity += change.diff.severityChanges.length
|
|
772
|
+
options += getDisplayOptionChanges(change.diff).length
|
|
773
|
+
workspace += change.diff.workspaceMembershipChanges.added.length + change.diff.workspaceMembershipChanges.removed.length
|
|
774
|
+
}
|
|
775
|
+
return { introduced, removed, severity, options, workspace }
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function summarizeSnapshots(snapshots: Map<string, BuiltSnapshot>) {
|
|
779
|
+
let rules = 0
|
|
780
|
+
let error = 0
|
|
781
|
+
let warn = 0
|
|
782
|
+
let off = 0
|
|
783
|
+
for (const snapshot of snapshots.values()) {
|
|
784
|
+
for (const entry of Object.values(snapshot.rules)) {
|
|
785
|
+
rules += 1
|
|
786
|
+
if (entry[0] === 'error') {
|
|
787
|
+
error += 1
|
|
788
|
+
} else if (entry[0] === 'warn') {
|
|
789
|
+
warn += 1
|
|
790
|
+
} else {
|
|
791
|
+
off += 1
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return { groups: snapshots.size, rules, error, warn, off }
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function decorateDiffLine(line: string, color: ReturnType<typeof createColorizer>): string {
|
|
799
|
+
if (line.startsWith('introduced rules:') || line.startsWith('workspaces added:')) {
|
|
800
|
+
return color.green(`+ ${line}`)
|
|
801
|
+
}
|
|
802
|
+
if (line.startsWith('removed rules:') || line.startsWith('workspaces removed:')) {
|
|
803
|
+
return color.red(`- ${line}`)
|
|
804
|
+
}
|
|
805
|
+
if (line.startsWith('severity changed:') || line.startsWith('options changed:')) {
|
|
806
|
+
return color.yellow(`~ ${line}`)
|
|
807
|
+
}
|
|
808
|
+
return line
|
|
809
|
+
}
|
|
810
|
+
|
|
811
811
|
function createColorizer() {
|
|
812
812
|
const enabled = process.stdout.isTTY && process.env.NO_COLOR === undefined && process.env.TERM !== 'dumb'
|
|
813
813
|
const wrap = (code: string, text: string) => (enabled ? `\u001B[${code}m${text}\u001B[0m` : text)
|
|
@@ -824,38 +824,38 @@ function writeSubtleInfo(text: string): void {
|
|
|
824
824
|
const color = createColorizer()
|
|
825
825
|
process.stdout.write(color.dim(text))
|
|
826
826
|
}
|
|
827
|
-
|
|
828
|
-
function formatShortPrint(
|
|
829
|
-
snapshots: Array<{
|
|
830
|
-
groupId: string
|
|
831
|
-
workspaces: string[]
|
|
832
|
-
rules: Record<string, [severity: 'off' | 'warn' | 'error'] | [severity: 'off' | 'warn' | 'error', options: unknown]>
|
|
833
|
-
}>
|
|
834
|
-
): string {
|
|
835
|
-
const lines: string[] = []
|
|
836
|
-
const sorted = [...snapshots].sort((a, b) => a.groupId.localeCompare(b.groupId))
|
|
837
|
-
|
|
838
|
-
for (const snapshot of sorted) {
|
|
839
|
-
const ruleNames = Object.keys(snapshot.rules).sort()
|
|
840
|
-
const severityCounts = { error: 0, warn: 0, off: 0 }
|
|
841
|
-
|
|
842
|
-
for (const name of ruleNames) {
|
|
843
|
-
const severity = snapshot.rules[name][0]
|
|
844
|
-
severityCounts[severity] += 1
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
lines.push(
|
|
848
|
-
`group: ${snapshot.groupId}`,
|
|
849
|
-
`workspaces (${snapshot.workspaces.length}): ${snapshot.workspaces.length > 0 ? snapshot.workspaces.join(', ') : '(none)'}`,
|
|
850
|
-
`rules (${ruleNames.length}): error ${severityCounts.error}, warn ${severityCounts.warn}, off ${severityCounts.off}`
|
|
851
|
-
)
|
|
852
|
-
|
|
853
|
-
for (const ruleName of ruleNames) {
|
|
854
|
-
const entry = snapshot.rules[ruleName]
|
|
855
|
-
const suffix = entry.length > 1 ? ` ${JSON.stringify(entry[1])}` : ''
|
|
856
|
-
lines.push(`${ruleName}: ${entry[0]}${suffix}`)
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
return `${lines.join('\n')}\n`
|
|
861
|
-
}
|
|
827
|
+
|
|
828
|
+
function formatShortPrint(
|
|
829
|
+
snapshots: Array<{
|
|
830
|
+
groupId: string
|
|
831
|
+
workspaces: string[]
|
|
832
|
+
rules: Record<string, [severity: 'off' | 'warn' | 'error'] | [severity: 'off' | 'warn' | 'error', options: unknown]>
|
|
833
|
+
}>
|
|
834
|
+
): string {
|
|
835
|
+
const lines: string[] = []
|
|
836
|
+
const sorted = [...snapshots].sort((a, b) => a.groupId.localeCompare(b.groupId))
|
|
837
|
+
|
|
838
|
+
for (const snapshot of sorted) {
|
|
839
|
+
const ruleNames = Object.keys(snapshot.rules).sort()
|
|
840
|
+
const severityCounts = { error: 0, warn: 0, off: 0 }
|
|
841
|
+
|
|
842
|
+
for (const name of ruleNames) {
|
|
843
|
+
const severity = snapshot.rules[name][0]
|
|
844
|
+
severityCounts[severity] += 1
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
lines.push(
|
|
848
|
+
`group: ${snapshot.groupId}`,
|
|
849
|
+
`workspaces (${snapshot.workspaces.length}): ${snapshot.workspaces.length > 0 ? snapshot.workspaces.join(', ') : '(none)'}`,
|
|
850
|
+
`rules (${ruleNames.length}): error ${severityCounts.error}, warn ${severityCounts.warn}, off ${severityCounts.off}`
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
for (const ruleName of ruleNames) {
|
|
854
|
+
const entry = snapshot.rules[ruleName]
|
|
855
|
+
const suffix = entry.length > 1 ? ` ${JSON.stringify(entry[1])}` : ''
|
|
856
|
+
lines.push(`${ruleName}: ${entry[0]}${suffix}`)
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return `${lines.join('\n')}\n`
|
|
861
|
+
}
|