@eslint-config-snapshot/api 0.8.0 → 0.14.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 +45 -0
- package/dist/index.cjs +434 -91
- package/dist/index.js +434 -91
- package/package.json +2 -1
- package/src/config.ts +32 -8
- package/src/diff.ts +51 -17
- package/src/extract.ts +19 -0
- package/src/index.ts +1 -1
- package/src/sampling.ts +351 -40
- package/src/snapshot.ts +41 -44
- package/test/config.test.ts +8 -4
- package/test/diff.test.ts +49 -1
- package/test/sampling.test.ts +50 -11
- package/test/snapshot.test.ts +31 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eslint-config-snapshot/api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@manypkg/get-packages": "^3.1.0",
|
|
28
28
|
"cosmiconfig": "^9.0.0",
|
|
29
|
+
"debug": "^4.4.3",
|
|
29
30
|
"fast-glob": "^3.3.3",
|
|
30
31
|
"picomatch": "^4.0.3"
|
|
31
32
|
}
|
package/src/config.ts
CHANGED
|
@@ -24,7 +24,7 @@ export type SnapshotConfig = {
|
|
|
24
24
|
maxFilesPerWorkspace: number
|
|
25
25
|
includeGlobs: string[]
|
|
26
26
|
excludeGlobs: string[]
|
|
27
|
-
|
|
27
|
+
tokenHints?: string[] | string[][]
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -35,10 +35,9 @@ export const DEFAULT_CONFIG: SnapshotConfig = {
|
|
|
35
35
|
groups: [{ name: 'default', match: ['**/*'] }]
|
|
36
36
|
},
|
|
37
37
|
sampling: {
|
|
38
|
-
maxFilesPerWorkspace:
|
|
39
|
-
includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'],
|
|
40
|
-
excludeGlobs: ['**/node_modules/**', '**/dist/**']
|
|
41
|
-
hintGlobs: []
|
|
38
|
+
maxFilesPerWorkspace: 10,
|
|
39
|
+
includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs,md,mdx,json,css}'],
|
|
40
|
+
excludeGlobs: ['**/node_modules/**', '**/dist/**']
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
|
|
@@ -130,10 +129,35 @@ export function getConfigScaffold(preset: ConfigPreset = 'minimal'): string {
|
|
|
130
129
|
groups: [{ name: 'default', match: ['**/*'] }]
|
|
131
130
|
},
|
|
132
131
|
sampling: {
|
|
133
|
-
maxFilesPerWorkspace:
|
|
134
|
-
includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'],
|
|
132
|
+
maxFilesPerWorkspace: 10,
|
|
133
|
+
includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs,md,mdx,json,css}'],
|
|
135
134
|
excludeGlobs: ['**/node_modules/**', '**/dist/**'],
|
|
136
|
-
|
|
135
|
+
tokenHints: [
|
|
136
|
+
'chunk',
|
|
137
|
+
'conf',
|
|
138
|
+
'config',
|
|
139
|
+
'container',
|
|
140
|
+
'controller',
|
|
141
|
+
'helpers',
|
|
142
|
+
'mock',
|
|
143
|
+
'mocks',
|
|
144
|
+
'presentation',
|
|
145
|
+
'repository',
|
|
146
|
+
'route',
|
|
147
|
+
'routes',
|
|
148
|
+
'schema',
|
|
149
|
+
'setup',
|
|
150
|
+
'spec',
|
|
151
|
+
'stories',
|
|
152
|
+
'style',
|
|
153
|
+
'styles',
|
|
154
|
+
'test',
|
|
155
|
+
'type',
|
|
156
|
+
'types',
|
|
157
|
+
'utils',
|
|
158
|
+
'view',
|
|
159
|
+
'views'
|
|
160
|
+
]
|
|
137
161
|
}
|
|
138
162
|
}\n`
|
|
139
163
|
}
|
package/src/diff.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { canonicalizeJson, sortUnique } from './core.js'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { NormalizedRuleEntry } from './extract.js'
|
|
4
|
+
import type { SnapshotFile, SnapshotRuleEntry } from './snapshot.js'
|
|
4
5
|
|
|
5
6
|
export type RuleSeverityChange = {
|
|
6
7
|
rule: string
|
|
@@ -43,37 +44,56 @@ export function diffSnapshots(before: SnapshotFile, after: SnapshotFile): Snapsh
|
|
|
43
44
|
for (const name of beforeNames.filter((entry) => afterNames.includes(entry))) {
|
|
44
45
|
const oldEntry = beforeRules[name]
|
|
45
46
|
const newEntry = afterRules[name]
|
|
47
|
+
const oldVariants = toVariants(oldEntry)
|
|
48
|
+
const newVariants = toVariants(newEntry)
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
const oldSeverity = summarizeSeveritySet(oldVariants)
|
|
51
|
+
const newSeverity = summarizeSeveritySet(newVariants)
|
|
52
|
+
if (oldSeverity !== newSeverity) {
|
|
48
53
|
severityChanges.push({
|
|
49
54
|
rule: name,
|
|
50
|
-
before:
|
|
51
|
-
after:
|
|
55
|
+
before: oldSeverity,
|
|
56
|
+
after: newSeverity
|
|
52
57
|
})
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
const
|
|
56
|
-
const
|
|
60
|
+
const oldSerialized = JSON.stringify(oldVariants)
|
|
61
|
+
const newSerialized = JSON.stringify(newVariants)
|
|
62
|
+
if (oldSerialized === newSerialized) {
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
const oldIsOnlyOff = oldVariants.every((entry) => entry[0] === 'off')
|
|
67
|
+
const newIsOnlyOff = newVariants.every((entry) => entry[0] === 'off')
|
|
68
|
+
if (oldIsOnlyOff || newIsOnlyOff) {
|
|
59
69
|
// Treat off->off option removal/addition as removed/introduced config intent.
|
|
60
|
-
if (
|
|
61
|
-
|
|
70
|
+
if (oldIsOnlyOff && newIsOnlyOff) {
|
|
71
|
+
const oldHasOptions = oldVariants.some((variant) => variant.length > 1)
|
|
72
|
+
const newHasOptions = newVariants.some((variant) => variant.length > 1)
|
|
73
|
+
if (oldHasOptions && !newHasOptions) {
|
|
74
|
+
removedRules.push(name)
|
|
75
|
+
} else if (!oldHasOptions && newHasOptions) {
|
|
76
|
+
introducedRules.push(name)
|
|
77
|
+
} else if (oldVariants.length > newVariants.length) {
|
|
62
78
|
removedRules.push(name)
|
|
63
|
-
} else if (
|
|
79
|
+
} else if (oldVariants.length < newVariants.length) {
|
|
64
80
|
introducedRules.push(name)
|
|
81
|
+
} else {
|
|
82
|
+
optionChanges.push({
|
|
83
|
+
rule: name,
|
|
84
|
+
before: oldVariants,
|
|
85
|
+
after: newVariants
|
|
86
|
+
})
|
|
65
87
|
}
|
|
66
88
|
}
|
|
67
89
|
continue
|
|
68
90
|
}
|
|
69
91
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
})
|
|
76
|
-
}
|
|
92
|
+
optionChanges.push({
|
|
93
|
+
rule: name,
|
|
94
|
+
before: oldVariants,
|
|
95
|
+
after: newVariants
|
|
96
|
+
})
|
|
77
97
|
}
|
|
78
98
|
|
|
79
99
|
const beforeWorkspaces = sortUnique(before.workspaces)
|
|
@@ -91,6 +111,20 @@ export function diffSnapshots(before: SnapshotFile, after: SnapshotFile): Snapsh
|
|
|
91
111
|
}
|
|
92
112
|
}
|
|
93
113
|
|
|
114
|
+
function toVariants(entry: SnapshotRuleEntry): NormalizedRuleEntry[] {
|
|
115
|
+
if (!Array.isArray(entry[0])) {
|
|
116
|
+
return [canonicalizeJson(entry as NormalizedRuleEntry)]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (entry as NormalizedRuleEntry[]).map((variant) => canonicalizeJson(variant))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function summarizeSeveritySet(variants: NormalizedRuleEntry[]): string {
|
|
123
|
+
const severityOrder: Array<'error' | 'warn' | 'off'> = ['error', 'warn', 'off']
|
|
124
|
+
const severities = new Set(variants.map((variant) => variant[0]))
|
|
125
|
+
return severityOrder.filter((severity) => severities.has(severity)).join('|')
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
export function hasDiff(diff: SnapshotDiff): boolean {
|
|
95
129
|
return (
|
|
96
130
|
diff.introducedRules.length > 0 ||
|
package/src/extract.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import createDebug from 'debug'
|
|
1
2
|
import { spawnSync } from 'node:child_process'
|
|
2
3
|
import { existsSync, readFileSync } from 'node:fs'
|
|
3
4
|
import { createRequire } from 'node:module'
|
|
@@ -6,6 +7,8 @@ import { pathToFileURL } from 'node:url'
|
|
|
6
7
|
|
|
7
8
|
import { canonicalizeJson, normalizeSeverity } from './core.js'
|
|
8
9
|
|
|
10
|
+
const debugExtract = createDebug('eslint-config-snapshot:extract')
|
|
11
|
+
|
|
9
12
|
export type NormalizedRuleEntry = [severity: 'off' | 'warn' | 'error'] | [severity: 'off' | 'warn' | 'error', options: unknown]
|
|
10
13
|
|
|
11
14
|
export type ExtractedWorkspaceRules = Map<string, NormalizedRuleEntry>
|
|
@@ -66,12 +69,17 @@ function findPackageRoot(entryAbs: string): string {
|
|
|
66
69
|
|
|
67
70
|
export function extractRulesFromPrintConfig(workspaceAbs: string, fileAbs: string): ExtractedWorkspaceRules {
|
|
68
71
|
const eslintBin = resolveEslintBinForWorkspace(workspaceAbs)
|
|
72
|
+
const commandArgs = [eslintBin, '--print-config', fileAbs]
|
|
73
|
+
const startedAt = Date.now()
|
|
74
|
+
debugExtract('spawn: cwd=%s cmd=%s %o', workspaceAbs, process.execPath, commandArgs)
|
|
69
75
|
const proc = spawnSync(process.execPath, [eslintBin, '--print-config', fileAbs], {
|
|
70
76
|
cwd: workspaceAbs,
|
|
71
77
|
encoding: 'utf8'
|
|
72
78
|
})
|
|
79
|
+
debugExtract('spawn: done status=%s elapsedMs=%d', String(proc.status), Date.now() - startedAt)
|
|
73
80
|
|
|
74
81
|
if (proc.status !== 0) {
|
|
82
|
+
debugExtract('spawn: stderr=%s', proc.stderr.trim())
|
|
75
83
|
throw new Error(`Failed to run eslint --print-config for ${fileAbs}`)
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -112,6 +120,7 @@ export async function extractRulesForWorkspaceSamples(
|
|
|
112
120
|
workspaceAbs: string,
|
|
113
121
|
fileAbsList: string[]
|
|
114
122
|
): Promise<WorkspaceExtractionResult[]> {
|
|
123
|
+
debugExtract('workspace=%s sampleCount=%d', workspaceAbs, fileAbsList.length)
|
|
115
124
|
const evaluate = await createWorkspaceEvaluator(workspaceAbs)
|
|
116
125
|
const results: WorkspaceExtractionResult[] = []
|
|
117
126
|
|
|
@@ -121,10 +130,18 @@ export async function extractRulesForWorkspaceSamples(
|
|
|
121
130
|
results.push({ fileAbs, rules })
|
|
122
131
|
} catch (error: unknown) {
|
|
123
132
|
const normalizedError = error instanceof Error ? error : new Error(String(error))
|
|
133
|
+
debugExtract('extract failed: workspace=%s file=%s error=%s', workspaceAbs, fileAbs, normalizedError.message)
|
|
124
134
|
results.push({ fileAbs, error: normalizedError })
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
137
|
|
|
138
|
+
debugExtract(
|
|
139
|
+
'workspace=%s extracted=%d failed=%d',
|
|
140
|
+
workspaceAbs,
|
|
141
|
+
results.filter((entry) => entry.rules !== undefined).length,
|
|
142
|
+
results.filter((entry) => entry.error !== undefined).length
|
|
143
|
+
)
|
|
144
|
+
|
|
128
145
|
return results
|
|
129
146
|
}
|
|
130
147
|
|
|
@@ -142,6 +159,7 @@ async function createWorkspaceEvaluator(
|
|
|
142
159
|
|
|
143
160
|
const ESLintClass = eslintModule.ESLint ?? eslintModule.default?.ESLint
|
|
144
161
|
if (ESLintClass) {
|
|
162
|
+
debugExtract('workspace=%s evaluator=eslint-api', workspaceAbs)
|
|
145
163
|
const eslint = new ESLintClass({ cwd: workspaceAbs })
|
|
146
164
|
return async (fileAbs: string) => {
|
|
147
165
|
const config = await eslint.calculateConfigForFile(fileAbs)
|
|
@@ -156,6 +174,7 @@ async function createWorkspaceEvaluator(
|
|
|
156
174
|
// fall through to subprocess-based extractor
|
|
157
175
|
}
|
|
158
176
|
|
|
177
|
+
debugExtract('workspace=%s evaluator=spawn-print-config', workspaceAbs)
|
|
159
178
|
return (fileAbs: string) => Promise.resolve(extractRulesFromPrintConfig(workspaceAbs, fileAbs))
|
|
160
179
|
}
|
|
161
180
|
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ export {
|
|
|
16
16
|
export type { ExtractedWorkspaceRules, NormalizedRuleEntry, WorkspaceExtractionResult } from './extract.js'
|
|
17
17
|
|
|
18
18
|
export { aggregateRules, buildSnapshot, readSnapshotFile, writeSnapshotFile } from './snapshot.js'
|
|
19
|
-
export type { SnapshotFile } from './snapshot.js'
|
|
19
|
+
export type { SnapshotFile, SnapshotRuleEntry } from './snapshot.js'
|
|
20
20
|
|
|
21
21
|
export { diffSnapshots, hasDiff } from './diff.js'
|
|
22
22
|
export type { RuleOptionChange, RuleSeverityChange, SnapshotDiff, WorkspaceMembershipChange } from './diff.js'
|