@eslint-config-snapshot/api 0.1.0 → 0.1.4
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 +25 -0
- package/package.json +9 -1
- package/project.json +2 -2
- package/src/config.ts +116 -116
- package/src/core.ts +1 -1
- package/src/diff.ts +1 -1
- package/src/extract.ts +119 -119
- package/src/sampling.ts +136 -136
- package/src/snapshot.ts +1 -1
- package/src/workspace.ts +130 -130
- package/test/api.test.ts +1 -1
- package/test/config.test.ts +100 -100
- package/test/core.test.ts +1 -1
- package/test/extract.test.ts +140 -140
- package/test/sampling.test.ts +91 -91
- package/test/snapshot.test.ts +64 -64
- package/test/workspace.test.ts +1 -1
- package/tsconfig.json +12 -12
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @eslint-config-snapshot/api
|
|
2
|
+
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fix npm trusted publishing provenance metadata by adding repository/homepage/bugs fields to package manifests.
|
|
8
|
+
|
|
9
|
+
## 0.1.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Release patch version to validate trusted publishing flow with protected release environment.
|
|
14
|
+
|
|
15
|
+
## 0.1.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Test release bump to validate automated publish flow with aligned package versions and tag.
|
|
20
|
+
|
|
21
|
+
## 0.1.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Release patch version for API and CLI packages using the new Changesets workflow.
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eslint-config-snapshot/api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/gabrielmoreira/eslint-config-snapshot"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/gabrielmoreira/eslint-config-snapshot#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/gabrielmoreira/eslint-config-snapshot/issues"
|
|
12
|
+
},
|
|
5
13
|
"engines": {
|
|
6
14
|
"node": ">=20.0.0"
|
|
7
15
|
},
|
package/project.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
import { cosmiconfig } from 'cosmiconfig'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { cosmiconfig } from 'cosmiconfig'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
|
|
5
5
|
export type SnapshotConfig = {
|
|
6
|
-
workspaceInput:
|
|
7
|
-
| {
|
|
8
|
-
mode: 'discover'
|
|
9
|
-
}
|
|
10
|
-
| {
|
|
11
|
-
mode: 'manual'
|
|
12
|
-
rootAbs?: string
|
|
13
|
-
workspaces: string[]
|
|
14
|
-
}
|
|
15
|
-
grouping: {
|
|
16
|
-
mode: 'match' | 'standalone'
|
|
17
|
-
allowEmptyGroups?: boolean
|
|
18
|
-
groups?: Array<{
|
|
19
|
-
name: string
|
|
20
|
-
match: string[]
|
|
21
|
-
}>
|
|
22
|
-
}
|
|
23
|
-
sampling: {
|
|
24
|
-
maxFilesPerWorkspace: number
|
|
25
|
-
includeGlobs: string[]
|
|
26
|
-
excludeGlobs: string[]
|
|
27
|
-
hintGlobs: string[]
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
6
|
+
workspaceInput:
|
|
7
|
+
| {
|
|
8
|
+
mode: 'discover'
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
mode: 'manual'
|
|
12
|
+
rootAbs?: string
|
|
13
|
+
workspaces: string[]
|
|
14
|
+
}
|
|
15
|
+
grouping: {
|
|
16
|
+
mode: 'match' | 'standalone'
|
|
17
|
+
allowEmptyGroups?: boolean
|
|
18
|
+
groups?: Array<{
|
|
19
|
+
name: string
|
|
20
|
+
match: string[]
|
|
21
|
+
}>
|
|
22
|
+
}
|
|
23
|
+
sampling: {
|
|
24
|
+
maxFilesPerWorkspace: number
|
|
25
|
+
includeGlobs: string[]
|
|
26
|
+
excludeGlobs: string[]
|
|
27
|
+
hintGlobs: string[]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
31
|
export const DEFAULT_CONFIG: SnapshotConfig = {
|
|
32
|
-
workspaceInput: { mode: 'discover' },
|
|
33
|
-
grouping: {
|
|
34
|
-
mode: 'match',
|
|
35
|
-
groups: [{ name: 'default', match: ['**/*'] }]
|
|
36
|
-
},
|
|
37
|
-
sampling: {
|
|
38
|
-
maxFilesPerWorkspace: 8,
|
|
39
|
-
includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'],
|
|
40
|
-
excludeGlobs: ['**/node_modules/**', '**/dist/**'],
|
|
41
|
-
hintGlobs: []
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const SPEC_SEARCH_PLACES = [
|
|
46
|
-
'.eslint-config-snapshot.js',
|
|
47
|
-
'.eslint-config-snapshot.cjs',
|
|
48
|
-
'.eslint-config-snapshot.mjs',
|
|
49
|
-
'eslint-config-snapshot.config.js',
|
|
50
|
-
'eslint-config-snapshot.config.cjs',
|
|
51
|
-
'eslint-config-snapshot.config.mjs',
|
|
52
|
-
'package.json',
|
|
53
|
-
'.eslint-config-snapshotrc',
|
|
54
|
-
'.eslint-config-snapshotrc.json',
|
|
55
|
-
'.eslint-config-snapshotrc.yaml',
|
|
56
|
-
'.eslint-config-snapshotrc.yml',
|
|
57
|
-
'.eslint-config-snapshotrc.js',
|
|
58
|
-
'.eslint-config-snapshotrc.cjs',
|
|
59
|
-
'.eslint-config-snapshotrc.mjs'
|
|
60
|
-
]
|
|
61
|
-
|
|
32
|
+
workspaceInput: { mode: 'discover' },
|
|
33
|
+
grouping: {
|
|
34
|
+
mode: 'match',
|
|
35
|
+
groups: [{ name: 'default', match: ['**/*'] }]
|
|
36
|
+
},
|
|
37
|
+
sampling: {
|
|
38
|
+
maxFilesPerWorkspace: 8,
|
|
39
|
+
includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'],
|
|
40
|
+
excludeGlobs: ['**/node_modules/**', '**/dist/**'],
|
|
41
|
+
hintGlobs: []
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SPEC_SEARCH_PLACES = [
|
|
46
|
+
'.eslint-config-snapshot.js',
|
|
47
|
+
'.eslint-config-snapshot.cjs',
|
|
48
|
+
'.eslint-config-snapshot.mjs',
|
|
49
|
+
'eslint-config-snapshot.config.js',
|
|
50
|
+
'eslint-config-snapshot.config.cjs',
|
|
51
|
+
'eslint-config-snapshot.config.mjs',
|
|
52
|
+
'package.json',
|
|
53
|
+
'.eslint-config-snapshotrc',
|
|
54
|
+
'.eslint-config-snapshotrc.json',
|
|
55
|
+
'.eslint-config-snapshotrc.yaml',
|
|
56
|
+
'.eslint-config-snapshotrc.yml',
|
|
57
|
+
'.eslint-config-snapshotrc.js',
|
|
58
|
+
'.eslint-config-snapshotrc.cjs',
|
|
59
|
+
'.eslint-config-snapshotrc.mjs'
|
|
60
|
+
]
|
|
61
|
+
|
|
62
62
|
export async function loadConfig(cwd?: string): Promise<SnapshotConfig> {
|
|
63
|
-
const found = await findConfigPath(cwd)
|
|
64
|
-
if (!found) {
|
|
65
|
-
return DEFAULT_CONFIG
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return found.config
|
|
69
|
-
}
|
|
70
|
-
|
|
63
|
+
const found = await findConfigPath(cwd)
|
|
64
|
+
if (!found) {
|
|
65
|
+
return DEFAULT_CONFIG
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return found.config
|
|
69
|
+
}
|
|
70
|
+
|
|
71
71
|
export async function findConfigPath(
|
|
72
72
|
cwd?: string
|
|
73
73
|
): Promise<{ path: string; config: SnapshotConfig } | null> {
|
|
74
|
-
const root = path.resolve(cwd ?? process.cwd())
|
|
75
|
-
const explorer = cosmiconfig('eslint-config-snapshot', {
|
|
76
|
-
searchPlaces: SPEC_SEARCH_PLACES,
|
|
77
|
-
stopDir: root
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
const result = await explorer.search(root)
|
|
81
|
-
if (!result) {
|
|
82
|
-
return null
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const maybeConfig = await loadUserConfig(result.config)
|
|
86
|
-
|
|
74
|
+
const root = path.resolve(cwd ?? process.cwd())
|
|
75
|
+
const explorer = cosmiconfig('eslint-config-snapshot', {
|
|
76
|
+
searchPlaces: SPEC_SEARCH_PLACES,
|
|
77
|
+
stopDir: root
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const result = await explorer.search(root)
|
|
81
|
+
if (!result) {
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const maybeConfig = await loadUserConfig(result.config)
|
|
86
|
+
|
|
87
87
|
const config: SnapshotConfig = {
|
|
88
|
-
...DEFAULT_CONFIG,
|
|
89
|
-
...maybeConfig,
|
|
90
|
-
grouping: {
|
|
91
|
-
...DEFAULT_CONFIG.grouping,
|
|
92
|
-
...maybeConfig.grouping
|
|
93
|
-
},
|
|
94
|
-
sampling: {
|
|
95
|
-
...DEFAULT_CONFIG.sampling,
|
|
96
|
-
...maybeConfig.sampling
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
path: result.filepath,
|
|
102
|
-
config
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
88
|
+
...DEFAULT_CONFIG,
|
|
89
|
+
...maybeConfig,
|
|
90
|
+
grouping: {
|
|
91
|
+
...DEFAULT_CONFIG.grouping,
|
|
92
|
+
...maybeConfig.grouping
|
|
93
|
+
},
|
|
94
|
+
sampling: {
|
|
95
|
+
...DEFAULT_CONFIG.sampling,
|
|
96
|
+
...maybeConfig.sampling
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
path: result.filepath,
|
|
102
|
+
config
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
106
|
async function loadUserConfig(rawConfig: unknown): Promise<Partial<SnapshotConfig>> {
|
|
107
|
-
const resolved = typeof rawConfig === 'function' ? await rawConfig() : rawConfig
|
|
108
|
-
if (resolved === null || resolved === undefined) {
|
|
109
|
-
return {}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (typeof resolved !== 'object' || Array.isArray(resolved)) {
|
|
113
|
-
throw new TypeError('Invalid config export: expected object, function, or async function returning an object')
|
|
114
|
-
}
|
|
115
|
-
|
|
107
|
+
const resolved = typeof rawConfig === 'function' ? await rawConfig() : rawConfig
|
|
108
|
+
if (resolved === null || resolved === undefined) {
|
|
109
|
+
return {}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof resolved !== 'object' || Array.isArray(resolved)) {
|
|
113
|
+
throw new TypeError('Invalid config export: expected object, function, or async function returning an object')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
116
|
return resolved as Partial<SnapshotConfig>
|
|
117
|
-
}
|
|
118
|
-
|
|
117
|
+
}
|
|
118
|
+
|
|
119
119
|
export type ConfigPreset = 'minimal' | 'full'
|
|
120
120
|
|
|
121
121
|
export function getConfigScaffold(preset: ConfigPreset = 'minimal'): string {
|
|
@@ -127,13 +127,13 @@ export function getConfigScaffold(preset: ConfigPreset = 'minimal'): string {
|
|
|
127
127
|
workspaceInput: { mode: 'discover' },
|
|
128
128
|
grouping: {
|
|
129
129
|
mode: 'match',
|
|
130
|
-
groups: [{ name: 'default', match: ['**/*'] }]
|
|
131
|
-
},
|
|
132
|
-
sampling: {
|
|
133
|
-
maxFilesPerWorkspace: 8,
|
|
134
|
-
includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'],
|
|
135
|
-
excludeGlobs: ['**/node_modules/**', '**/dist/**'],
|
|
136
|
-
hintGlobs: []
|
|
130
|
+
groups: [{ name: 'default', match: ['**/*'] }]
|
|
131
|
+
},
|
|
132
|
+
sampling: {
|
|
133
|
+
maxFilesPerWorkspace: 8,
|
|
134
|
+
includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'],
|
|
135
|
+
excludeGlobs: ['**/node_modules/**', '**/dist/**'],
|
|
136
|
+
hintGlobs: []
|
|
137
137
|
}
|
|
138
138
|
}\n`
|
|
139
139
|
}
|
package/src/core.ts
CHANGED
package/src/diff.ts
CHANGED
package/src/extract.ts
CHANGED
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process'
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
-
import { createRequire } from 'node:module'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
|
|
6
|
-
import { canonicalizeJson, normalizeSeverity } from './core.js'
|
|
7
|
-
|
|
8
|
-
export type NormalizedRuleEntry = [severity: 'off' | 'warn' | 'error'] | [severity: 'off' | 'warn' | 'error', options: unknown]
|
|
9
|
-
|
|
10
|
-
export type ExtractedWorkspaceRules = Map<string, NormalizedRuleEntry>
|
|
11
|
-
|
|
12
|
-
export function resolveEslintBinForWorkspace(workspaceAbs: string): string {
|
|
13
|
-
const anchor = path.join(workspaceAbs, '__snapshot_anchor__.cjs')
|
|
14
|
-
const req = createRequire(anchor)
|
|
15
|
-
try {
|
|
16
|
-
return req.resolve('eslint/bin/eslint.js')
|
|
17
|
-
} catch {
|
|
18
|
-
try {
|
|
19
|
-
const eslintEntry = req.resolve('eslint')
|
|
20
|
-
const eslintRoot = findPackageRoot(eslintEntry)
|
|
21
|
-
const packageJsonPath = path.join(eslintRoot, 'package.json')
|
|
22
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { bin?: string | Record<string, string> }
|
|
23
|
-
const relativeBin = resolveBinPath(packageJson.bin)
|
|
24
|
-
const binAbs = path.resolve(eslintRoot, relativeBin)
|
|
25
|
-
|
|
26
|
-
if (existsSync(binAbs)) {
|
|
27
|
-
return binAbs
|
|
28
|
-
}
|
|
29
|
-
} catch {
|
|
30
|
-
// ignore fallback errors and throw deterministic workspace-scoped message below
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
throw new Error(`Unable to resolve eslint from workspace: ${workspaceAbs}`)
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function resolveBinPath(bin: string | Record<string, string> | undefined): string {
|
|
38
|
-
if (typeof bin === 'string') {
|
|
39
|
-
return bin
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (typeof bin?.eslint === 'string') {
|
|
43
|
-
return bin.eslint
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return 'bin/eslint.js'
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function findPackageRoot(entryAbs: string): string {
|
|
50
|
-
let current = path.dirname(entryAbs)
|
|
51
|
-
while (true) {
|
|
52
|
-
const packageJsonPath = path.join(current, 'package.json')
|
|
53
|
-
if (existsSync(packageJsonPath)) {
|
|
54
|
-
return current
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const parent = path.dirname(current)
|
|
58
|
-
if (parent === current) {
|
|
59
|
-
throw new Error('Package root not found')
|
|
60
|
-
}
|
|
61
|
-
current = parent
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function extractRulesFromPrintConfig(workspaceAbs: string, fileAbs: string): ExtractedWorkspaceRules {
|
|
66
|
-
const eslintBin = resolveEslintBinForWorkspace(workspaceAbs)
|
|
67
|
-
const proc = spawnSync(process.execPath, [eslintBin, '--print-config', fileAbs], {
|
|
68
|
-
cwd: workspaceAbs,
|
|
69
|
-
encoding: 'utf8'
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
if (proc.status !== 0) {
|
|
73
|
-
throw new Error(`Failed to run eslint --print-config for ${fileAbs}`)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const stdout = proc.stdout.trim()
|
|
77
|
-
if (stdout.length === 0 || stdout === 'undefined') {
|
|
78
|
-
throw new Error(`Empty ESLint print-config output for ${fileAbs}`)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let parsed: unknown
|
|
82
|
-
try {
|
|
83
|
-
parsed = JSON.parse(stdout)
|
|
84
|
-
} catch {
|
|
85
|
-
throw new Error(`Invalid JSON from eslint --print-config for ${fileAbs}`)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const rules = (parsed as { rules?: Record<string, unknown> }).rules ?? {}
|
|
89
|
-
const normalized = new Map<string, NormalizedRuleEntry>()
|
|
90
|
-
|
|
91
|
-
for (const [ruleName, ruleConfig] of Object.entries(rules)) {
|
|
92
|
-
normalized.set(ruleName, normalizeRuleEntry(ruleConfig))
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return normalized
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function normalizeRuleEntry(raw: unknown): NormalizedRuleEntry {
|
|
99
|
-
if (Array.isArray(raw)) {
|
|
100
|
-
if (raw.length === 0) {
|
|
101
|
-
throw new Error('Rule configuration array cannot be empty')
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const severity = normalizeSeverity(raw[0])
|
|
105
|
-
const rest = raw.slice(1).map((item) => canonicalizeJson(item))
|
|
106
|
-
|
|
107
|
-
if (rest.length === 0) {
|
|
108
|
-
return [severity]
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (rest.length === 1) {
|
|
112
|
-
return [severity, rest[0]]
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return [severity, canonicalizeJson(rest)]
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return [normalizeSeverity(raw)]
|
|
119
|
-
}
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { canonicalizeJson, normalizeSeverity } from './core.js'
|
|
7
|
+
|
|
8
|
+
export type NormalizedRuleEntry = [severity: 'off' | 'warn' | 'error'] | [severity: 'off' | 'warn' | 'error', options: unknown]
|
|
9
|
+
|
|
10
|
+
export type ExtractedWorkspaceRules = Map<string, NormalizedRuleEntry>
|
|
11
|
+
|
|
12
|
+
export function resolveEslintBinForWorkspace(workspaceAbs: string): string {
|
|
13
|
+
const anchor = path.join(workspaceAbs, '__snapshot_anchor__.cjs')
|
|
14
|
+
const req = createRequire(anchor)
|
|
15
|
+
try {
|
|
16
|
+
return req.resolve('eslint/bin/eslint.js')
|
|
17
|
+
} catch {
|
|
18
|
+
try {
|
|
19
|
+
const eslintEntry = req.resolve('eslint')
|
|
20
|
+
const eslintRoot = findPackageRoot(eslintEntry)
|
|
21
|
+
const packageJsonPath = path.join(eslintRoot, 'package.json')
|
|
22
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { bin?: string | Record<string, string> }
|
|
23
|
+
const relativeBin = resolveBinPath(packageJson.bin)
|
|
24
|
+
const binAbs = path.resolve(eslintRoot, relativeBin)
|
|
25
|
+
|
|
26
|
+
if (existsSync(binAbs)) {
|
|
27
|
+
return binAbs
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore fallback errors and throw deterministic workspace-scoped message below
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new Error(`Unable to resolve eslint from workspace: ${workspaceAbs}`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveBinPath(bin: string | Record<string, string> | undefined): string {
|
|
38
|
+
if (typeof bin === 'string') {
|
|
39
|
+
return bin
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (typeof bin?.eslint === 'string') {
|
|
43
|
+
return bin.eslint
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return 'bin/eslint.js'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findPackageRoot(entryAbs: string): string {
|
|
50
|
+
let current = path.dirname(entryAbs)
|
|
51
|
+
while (true) {
|
|
52
|
+
const packageJsonPath = path.join(current, 'package.json')
|
|
53
|
+
if (existsSync(packageJsonPath)) {
|
|
54
|
+
return current
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parent = path.dirname(current)
|
|
58
|
+
if (parent === current) {
|
|
59
|
+
throw new Error('Package root not found')
|
|
60
|
+
}
|
|
61
|
+
current = parent
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function extractRulesFromPrintConfig(workspaceAbs: string, fileAbs: string): ExtractedWorkspaceRules {
|
|
66
|
+
const eslintBin = resolveEslintBinForWorkspace(workspaceAbs)
|
|
67
|
+
const proc = spawnSync(process.execPath, [eslintBin, '--print-config', fileAbs], {
|
|
68
|
+
cwd: workspaceAbs,
|
|
69
|
+
encoding: 'utf8'
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (proc.status !== 0) {
|
|
73
|
+
throw new Error(`Failed to run eslint --print-config for ${fileAbs}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const stdout = proc.stdout.trim()
|
|
77
|
+
if (stdout.length === 0 || stdout === 'undefined') {
|
|
78
|
+
throw new Error(`Empty ESLint print-config output for ${fileAbs}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let parsed: unknown
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(stdout)
|
|
84
|
+
} catch {
|
|
85
|
+
throw new Error(`Invalid JSON from eslint --print-config for ${fileAbs}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rules = (parsed as { rules?: Record<string, unknown> }).rules ?? {}
|
|
89
|
+
const normalized = new Map<string, NormalizedRuleEntry>()
|
|
90
|
+
|
|
91
|
+
for (const [ruleName, ruleConfig] of Object.entries(rules)) {
|
|
92
|
+
normalized.set(ruleName, normalizeRuleEntry(ruleConfig))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return normalized
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeRuleEntry(raw: unknown): NormalizedRuleEntry {
|
|
99
|
+
if (Array.isArray(raw)) {
|
|
100
|
+
if (raw.length === 0) {
|
|
101
|
+
throw new Error('Rule configuration array cannot be empty')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const severity = normalizeSeverity(raw[0])
|
|
105
|
+
const rest = raw.slice(1).map((item) => canonicalizeJson(item))
|
|
106
|
+
|
|
107
|
+
if (rest.length === 0) {
|
|
108
|
+
return [severity]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (rest.length === 1) {
|
|
112
|
+
return [severity, rest[0]]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [severity, canonicalizeJson(rest)]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return [normalizeSeverity(raw)]
|
|
119
|
+
}
|