@eslint-config-snapshot/cli 0.1.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/dist/index.cjs +720 -0
- package/dist/index.js +697 -0
- package/package.json +26 -0
- package/project.json +35 -0
- package/src/index.ts +861 -0
- package/test/cli.integration.test.ts +247 -0
- package/test/cli.npm-isolated.integration.test.ts +109 -0
- package/test/cli.pnpm-isolated.integration.test.ts +140 -0
- package/test/cli.terminal.integration.test.ts +370 -0
- package/test/fixtures/npm-isolated-template/eslint-config-snapshot.config.mjs +16 -0
- package/test/fixtures/npm-isolated-template/package.json +7 -0
- package/test/fixtures/npm-isolated-template/packages/ws-a/.eslintrc.cjs +7 -0
- package/test/fixtures/npm-isolated-template/packages/ws-a/package.json +7 -0
- package/test/fixtures/npm-isolated-template/packages/ws-a/src/index.ts +1 -0
- package/test/fixtures/npm-isolated-template/packages/ws-b/.eslintrc.cjs +7 -0
- package/test/fixtures/npm-isolated-template/packages/ws-b/package.json +7 -0
- package/test/fixtures/npm-isolated-template/packages/ws-b/src/index.ts +1 -0
- package/test/fixtures/repo/eslint-config-snapshot.config.mjs +16 -0
- package/test/fixtures/repo/package.json +7 -0
- package/test/fixtures/repo/packages/ws-a/node_modules/eslint/bin/eslint.js +1 -0
- package/test/fixtures/repo/packages/ws-a/node_modules/eslint/package.json +4 -0
- package/test/fixtures/repo/packages/ws-a/package.json +4 -0
- package/test/fixtures/repo/packages/ws-a/src/index.ts +1 -0
- package/test/fixtures/repo/packages/ws-b/node_modules/eslint/bin/eslint.js +1 -0
- package/test/fixtures/repo/packages/ws-b/node_modules/eslint/package.json +4 -0
- package/test/fixtures/repo/packages/ws-b/package.json +4 -0
- package/test/fixtures/repo/packages/ws-b/src/index.ts +1 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { parseInitPresetChoice, parseInitTargetChoice, runCli } from '../src/index.js'
|
|
7
|
+
|
|
8
|
+
const fixtureRoot = path.resolve('test/fixtures/repo')
|
|
9
|
+
|
|
10
|
+
afterAll(async () => {
|
|
11
|
+
await rm(path.join(fixtureRoot, '.eslint-config-snapshot'), { recursive: true, force: true })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
await rm(path.join(fixtureRoot, '.eslint-config-snapshot'), { recursive: true, force: true })
|
|
16
|
+
await mkdir(path.join(fixtureRoot, 'packages/ws-a/node_modules/eslint/bin'), { recursive: true })
|
|
17
|
+
await mkdir(path.join(fixtureRoot, 'packages/ws-b/node_modules/eslint/bin'), { recursive: true })
|
|
18
|
+
|
|
19
|
+
await writeFile(
|
|
20
|
+
path.join(fixtureRoot, 'packages/ws-a/node_modules/eslint/bin/eslint.js'),
|
|
21
|
+
"console.log(JSON.stringify({ rules: { 'no-console': 1, eqeqeq: [2, 'always'] } }))\n"
|
|
22
|
+
)
|
|
23
|
+
await writeFile(
|
|
24
|
+
path.join(fixtureRoot, 'packages/ws-a/node_modules/eslint/package.json'),
|
|
25
|
+
JSON.stringify({ name: 'eslint', version: '9.0.0' }, null, 2)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
await writeFile(
|
|
29
|
+
path.join(fixtureRoot, 'packages/ws-b/node_modules/eslint/bin/eslint.js'),
|
|
30
|
+
"console.log(JSON.stringify({ rules: { 'no-console': 2, 'no-debugger': 0 } }))\n"
|
|
31
|
+
)
|
|
32
|
+
await writeFile(
|
|
33
|
+
path.join(fixtureRoot, 'packages/ws-b/node_modules/eslint/package.json'),
|
|
34
|
+
JSON.stringify({ name: 'eslint', version: '9.0.0' }, null, 2)
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('cli integration', () => {
|
|
39
|
+
it('parses init interactive target choices from numeric and aliases', () => {
|
|
40
|
+
expect(parseInitTargetChoice('')).toBe('package-json')
|
|
41
|
+
expect(parseInitTargetChoice('1')).toBe('package-json')
|
|
42
|
+
expect(parseInitTargetChoice('package')).toBe('package-json')
|
|
43
|
+
expect(parseInitTargetChoice('pkg')).toBe('package-json')
|
|
44
|
+
expect(parseInitTargetChoice('2')).toBe('file')
|
|
45
|
+
expect(parseInitTargetChoice('file')).toBe('file')
|
|
46
|
+
expect(parseInitTargetChoice('invalid')).toBeUndefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('parses init interactive preset choices from numeric and aliases', () => {
|
|
50
|
+
expect(parseInitPresetChoice('')).toBe('minimal')
|
|
51
|
+
expect(parseInitPresetChoice('1')).toBe('minimal')
|
|
52
|
+
expect(parseInitPresetChoice('min')).toBe('minimal')
|
|
53
|
+
expect(parseInitPresetChoice('2')).toBe('full')
|
|
54
|
+
expect(parseInitPresetChoice('full')).toBe('full')
|
|
55
|
+
expect(parseInitPresetChoice('invalid')).toBeUndefined()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('snapshot writes deterministic snapshot files', async () => {
|
|
59
|
+
const code = await runCli('snapshot', fixtureRoot)
|
|
60
|
+
expect(code).toBe(0)
|
|
61
|
+
|
|
62
|
+
const snapshotRaw = await readFile(path.join(fixtureRoot, '.eslint-config-snapshot/default.json'), 'utf8')
|
|
63
|
+
const snapshot = JSON.parse(snapshotRaw)
|
|
64
|
+
|
|
65
|
+
expect(snapshot).toEqual({
|
|
66
|
+
formatVersion: 1,
|
|
67
|
+
groupId: 'default',
|
|
68
|
+
workspaces: ['packages/ws-a', 'packages/ws-b'],
|
|
69
|
+
rules: {
|
|
70
|
+
eqeqeq: ['error', 'always'],
|
|
71
|
+
'no-console': ['error'],
|
|
72
|
+
'no-debugger': ['off']
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expect(snapshotRaw.endsWith('\n')).toBe(true)
|
|
77
|
+
expect(snapshotRaw.includes('src/index.ts')).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('compare returns non-zero when snapshots changed', async () => {
|
|
81
|
+
expect(await runCli('snapshot', fixtureRoot)).toBe(0)
|
|
82
|
+
|
|
83
|
+
await writeFile(
|
|
84
|
+
path.join(fixtureRoot, 'packages/ws-a/node_modules/eslint/bin/eslint.js'),
|
|
85
|
+
"console.log(JSON.stringify({ rules: { 'no-console': 1, eqeqeq: 0 } }))\n"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const code = await runCli('compare', fixtureRoot)
|
|
89
|
+
expect(code).toBe(1)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('status is minimal and exits 0 when clean', async () => {
|
|
93
|
+
await runCli('snapshot', fixtureRoot)
|
|
94
|
+
|
|
95
|
+
const writeSpy = vi.spyOn(process.stdout, 'write')
|
|
96
|
+
const code = await runCli('status', fixtureRoot)
|
|
97
|
+
expect(code).toBe(0)
|
|
98
|
+
expect(writeSpy).toHaveBeenCalledWith('clean\n')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('print emits aggregated rules and exits 0', async () => {
|
|
102
|
+
const writeSpy = vi.spyOn(process.stdout, 'write')
|
|
103
|
+
const code = await runCli('print', fixtureRoot)
|
|
104
|
+
expect(code).toBe(0)
|
|
105
|
+
expect(writeSpy).toHaveBeenCalled()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('print --short emits compact human-readable output', async () => {
|
|
109
|
+
const writeSpy = vi.spyOn(process.stdout, 'write')
|
|
110
|
+
const code = await runCli('print', fixtureRoot, ['--short'])
|
|
111
|
+
expect(code).toBe(0)
|
|
112
|
+
expect(writeSpy).toHaveBeenCalledWith(
|
|
113
|
+
`group: default
|
|
114
|
+
workspaces (2): packages/ws-a, packages/ws-b
|
|
115
|
+
rules (3): error 2, warn 0, off 1
|
|
116
|
+
eqeqeq: error "always"
|
|
117
|
+
no-console: error
|
|
118
|
+
no-debugger: off
|
|
119
|
+
`
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('init creates scaffold config file when target=file', async () => {
|
|
124
|
+
const tmp = await mkdtemp(path.join(os.tmpdir(), 'snapshot-init-'))
|
|
125
|
+
const code = await runCli('init', tmp, ['--yes', '--target', 'file', '--preset', 'full'])
|
|
126
|
+
expect(code).toBe(0)
|
|
127
|
+
|
|
128
|
+
const content = await readFile(path.join(tmp, 'eslint-config-snapshot.config.mjs'), 'utf8')
|
|
129
|
+
expect(content).toContain("workspaceInput: { mode: 'discover' }")
|
|
130
|
+
|
|
131
|
+
await rm(tmp, { recursive: true, force: true })
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('init writes minimal config to package.json when target=package-json', async () => {
|
|
135
|
+
const tmp = await mkdtemp(path.join(os.tmpdir(), 'snapshot-init-pkg-'))
|
|
136
|
+
await writeFile(path.join(tmp, 'package.json'), JSON.stringify({ name: 'fixture', private: true }, null, 2))
|
|
137
|
+
|
|
138
|
+
const code = await runCli('init', tmp, ['--yes', '--target', 'package-json', '--preset', 'minimal'])
|
|
139
|
+
expect(code).toBe(0)
|
|
140
|
+
|
|
141
|
+
const packageJsonRaw = await readFile(path.join(tmp, 'package.json'), 'utf8')
|
|
142
|
+
const parsed = JSON.parse(packageJsonRaw) as {
|
|
143
|
+
'eslint-config-snapshot'?: Record<string, unknown>
|
|
144
|
+
}
|
|
145
|
+
expect(parsed['eslint-config-snapshot']).toEqual({})
|
|
146
|
+
|
|
147
|
+
await rm(tmp, { recursive: true, force: true })
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('help prints usage and exits 0', async () => {
|
|
151
|
+
const writeSpy = vi.spyOn(process.stdout, 'write')
|
|
152
|
+
const code = await runCli('--help', fixtureRoot)
|
|
153
|
+
expect(code).toBe(0)
|
|
154
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('Usage:'))
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('runs update mode without command', async () => {
|
|
158
|
+
const writeSpy = vi.spyOn(process.stdout, 'write')
|
|
159
|
+
const code = await runCli(undefined, fixtureRoot, ['--update'])
|
|
160
|
+
expect(code).toBe(0)
|
|
161
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('Baseline updated:'))
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('supports canonical check and update commands', async () => {
|
|
165
|
+
expect(await runCli('update', fixtureRoot)).toBe(0)
|
|
166
|
+
expect(await runCli('check', fixtureRoot)).toBe(0)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('supports ordered multi-group matching with first match wins', async () => {
|
|
170
|
+
const tmp = await mkdtemp(path.join(os.tmpdir(), 'snapshot-cli-grouped-'))
|
|
171
|
+
await cp(fixtureRoot, tmp, { recursive: true })
|
|
172
|
+
|
|
173
|
+
await writeFile(
|
|
174
|
+
path.join(tmp, 'eslint-config-snapshot.config.mjs'),
|
|
175
|
+
`export default {
|
|
176
|
+
workspaceInput: { mode: 'manual', workspaces: ['packages/ws-a', 'packages/ws-b'] },
|
|
177
|
+
grouping: {
|
|
178
|
+
mode: 'match',
|
|
179
|
+
groups: [
|
|
180
|
+
{ name: 'modern', match: ['packages/**', '!packages/ws-b'] },
|
|
181
|
+
{ name: 'legacy', match: ['packages/ws-b'] }
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
sampling: {
|
|
185
|
+
maxFilesPerWorkspace: 8,
|
|
186
|
+
includeGlobs: ['**/*.ts'],
|
|
187
|
+
excludeGlobs: ['**/node_modules/**'],
|
|
188
|
+
hintGlobs: []
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
`
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const code = await runCli('snapshot', tmp)
|
|
195
|
+
expect(code).toBe(0)
|
|
196
|
+
|
|
197
|
+
const modern = JSON.parse(await readFile(path.join(tmp, '.eslint-config-snapshot/modern.json'), 'utf8'))
|
|
198
|
+
const legacy = JSON.parse(await readFile(path.join(tmp, '.eslint-config-snapshot/legacy.json'), 'utf8'))
|
|
199
|
+
|
|
200
|
+
expect(modern.workspaces).toEqual(['packages/ws-a'])
|
|
201
|
+
expect(modern.rules).toEqual({
|
|
202
|
+
eqeqeq: ['error', 'always'],
|
|
203
|
+
'no-console': ['warn']
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
expect(legacy.workspaces).toEqual(['packages/ws-b'])
|
|
207
|
+
expect(legacy.rules).toEqual({
|
|
208
|
+
'no-console': ['error'],
|
|
209
|
+
'no-debugger': ['off']
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
await rm(tmp, { recursive: true, force: true })
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('supports standalone mode with workspace path group ids', async () => {
|
|
216
|
+
const tmp = await mkdtemp(path.join(os.tmpdir(), 'snapshot-cli-standalone-'))
|
|
217
|
+
await cp(fixtureRoot, tmp, { recursive: true })
|
|
218
|
+
|
|
219
|
+
await writeFile(
|
|
220
|
+
path.join(tmp, 'eslint-config-snapshot.config.mjs'),
|
|
221
|
+
`export default {
|
|
222
|
+
workspaceInput: { mode: 'manual', workspaces: ['packages/ws-a', 'packages/ws-b'] },
|
|
223
|
+
grouping: { mode: 'standalone' },
|
|
224
|
+
sampling: {
|
|
225
|
+
maxFilesPerWorkspace: 8,
|
|
226
|
+
includeGlobs: ['**/*.ts'],
|
|
227
|
+
excludeGlobs: ['**/node_modules/**'],
|
|
228
|
+
hintGlobs: []
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
`
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const snapshotCode = await runCli('snapshot', tmp)
|
|
235
|
+
expect(snapshotCode).toBe(0)
|
|
236
|
+
|
|
237
|
+
const wsAPath = path.join(tmp, '.eslint-config-snapshot/packages/ws-a.json')
|
|
238
|
+
const wsBPath = path.join(tmp, '.eslint-config-snapshot/packages/ws-b.json')
|
|
239
|
+
expect(JSON.parse(await readFile(wsAPath, 'utf8')).groupId).toBe('packages/ws-a')
|
|
240
|
+
expect(JSON.parse(await readFile(wsBPath, 'utf8')).groupId).toBe('packages/ws-b')
|
|
241
|
+
|
|
242
|
+
const compareCode = await runCli('compare', tmp)
|
|
243
|
+
expect(compareCode).toBe(0)
|
|
244
|
+
|
|
245
|
+
await rm(tmp, { recursive: true, force: true })
|
|
246
|
+
})
|
|
247
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { access, cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
6
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const templateRoot = path.resolve('test/fixtures/npm-isolated-template')
|
|
9
|
+
const cliDist = path.resolve('dist/index.js')
|
|
10
|
+
|
|
11
|
+
let fixtureRoot = ''
|
|
12
|
+
|
|
13
|
+
function npmCmd(): string {
|
|
14
|
+
const base = path.dirname(process.execPath)
|
|
15
|
+
return process.platform === 'win32' ? path.join(base, 'npm.cmd') : path.join(base, 'npm')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function run(command: string, args: string[], cwd: string): { status: number; stdout: string; stderr: string } {
|
|
19
|
+
const proc = spawnSync(command, args, {
|
|
20
|
+
cwd,
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
env: { ...process.env },
|
|
23
|
+
shell: process.platform === 'win32'
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
status: proc.status ?? 1,
|
|
28
|
+
stdout: proc.stdout ?? '',
|
|
29
|
+
stderr: `${proc.stderr ?? ''}${proc.error ? `\n${String(proc.error)}` : ''}`
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runWithRetry(
|
|
34
|
+
command: string,
|
|
35
|
+
args: string[],
|
|
36
|
+
cwd: string,
|
|
37
|
+
retries = 2
|
|
38
|
+
): Promise<{ status: number; stdout: string; stderr: string }> {
|
|
39
|
+
let attempt = 0
|
|
40
|
+
let lastResult = run(command, args, cwd)
|
|
41
|
+
while (lastResult.status !== 0 && attempt < retries) {
|
|
42
|
+
attempt += 1
|
|
43
|
+
await delay(1000 * attempt)
|
|
44
|
+
lastResult = run(command, args, cwd)
|
|
45
|
+
}
|
|
46
|
+
return lastResult
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('cli npm-isolated integration', () => {
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
const tmpBase = await mkdtemp(path.join(os.tmpdir(), 'snapshot-npm-it-'))
|
|
52
|
+
fixtureRoot = path.join(tmpBase, 'repo')
|
|
53
|
+
await cp(templateRoot, fixtureRoot, { recursive: true })
|
|
54
|
+
|
|
55
|
+
const wsA = path.join(fixtureRoot, 'packages/ws-a')
|
|
56
|
+
const wsB = path.join(fixtureRoot, 'packages/ws-b')
|
|
57
|
+
|
|
58
|
+
const installA = await runWithRetry(npmCmd(), ['install', '--no-audit', '--no-fund', '--workspaces=false'], wsA)
|
|
59
|
+
expect(installA.status, `${installA.stdout}\n${installA.stderr}`).toBe(0)
|
|
60
|
+
await access(path.join(wsA, 'node_modules/eslint/package.json'))
|
|
61
|
+
|
|
62
|
+
const installB = await runWithRetry(npmCmd(), ['install', '--no-audit', '--no-fund', '--workspaces=false'], wsB)
|
|
63
|
+
expect(installB.status, `${installB.stdout}\n${installB.stderr}`).toBe(0)
|
|
64
|
+
await access(path.join(wsB, 'node_modules/eslint/package.json'))
|
|
65
|
+
}, 180000)
|
|
66
|
+
|
|
67
|
+
afterAll(async () => {
|
|
68
|
+
if (fixtureRoot) {
|
|
69
|
+
await rm(path.dirname(fixtureRoot), { recursive: true, force: true })
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('runs commands in isolated subprocesses with workspace-local npm eslint', async () => {
|
|
74
|
+
const snapshot = run(process.execPath, [cliDist, 'snapshot'], fixtureRoot)
|
|
75
|
+
expect(snapshot.status, snapshot.stderr).toBe(0)
|
|
76
|
+
|
|
77
|
+
const snapshotRaw = await readFile(path.join(fixtureRoot, '.eslint-config-snapshot/default.json'), 'utf8')
|
|
78
|
+
const parsed = JSON.parse(snapshotRaw)
|
|
79
|
+
expect(parsed).toEqual({
|
|
80
|
+
formatVersion: 1,
|
|
81
|
+
groupId: 'default',
|
|
82
|
+
workspaces: ['packages/ws-a', 'packages/ws-b'],
|
|
83
|
+
rules: {
|
|
84
|
+
eqeqeq: ['error', 'always'],
|
|
85
|
+
'no-console': ['error'],
|
|
86
|
+
'no-debugger': ['off']
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const compareClean = run(process.execPath, [cliDist, 'compare'], fixtureRoot)
|
|
91
|
+
expect(compareClean.status, compareClean.stdout + compareClean.stderr).toBe(0)
|
|
92
|
+
|
|
93
|
+
const statusClean = run(process.execPath, [cliDist, 'status'], fixtureRoot)
|
|
94
|
+
expect(statusClean.status).toBe(0)
|
|
95
|
+
expect(statusClean.stdout).toContain('clean')
|
|
96
|
+
|
|
97
|
+
const printOut = run(process.execPath, [cliDist, 'print'], fixtureRoot)
|
|
98
|
+
expect(printOut.status, printOut.stderr).toBe(0)
|
|
99
|
+
expect(printOut.stdout).toContain('"groupId": "default"')
|
|
100
|
+
|
|
101
|
+
await writeFile(
|
|
102
|
+
path.join(fixtureRoot, 'packages/ws-a/.eslintrc.cjs'),
|
|
103
|
+
"module.exports = { root: true, rules: { 'no-console': 'warn', eqeqeq: 'off' } }\n"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const compareChanged = run(process.execPath, [cliDist, 'compare'], fixtureRoot)
|
|
107
|
+
expect(compareChanged.status).toBe(1)
|
|
108
|
+
}, 180000)
|
|
109
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { access, cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
6
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const templateRoot = path.resolve('test/fixtures/npm-isolated-template')
|
|
9
|
+
const cliDist = path.resolve('dist/index.js')
|
|
10
|
+
|
|
11
|
+
let fixtureRoot = ''
|
|
12
|
+
|
|
13
|
+
type RunResult = {
|
|
14
|
+
status: number
|
|
15
|
+
stdout: string
|
|
16
|
+
stderr: string
|
|
17
|
+
errorCode?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type ExecCandidate = {
|
|
21
|
+
command: string
|
|
22
|
+
argsPrefix: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getPnpmCandidates(): ExecCandidate[] {
|
|
26
|
+
const candidates: ExecCandidate[] = []
|
|
27
|
+
const execPath = process.env.npm_execpath
|
|
28
|
+
if (execPath && execPath.toLowerCase().includes('pnpm')) {
|
|
29
|
+
candidates.push({ command: process.execPath, argsPrefix: [execPath] })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
candidates.push({ command: 'pnpm', argsPrefix: [] })
|
|
33
|
+
if (process.platform === 'win32') {
|
|
34
|
+
candidates.push({ command: 'pnpm.cmd', argsPrefix: [] })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return candidates
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function run(command: string, args: string[], cwd: string): RunResult {
|
|
41
|
+
const proc = spawnSync(command, args, {
|
|
42
|
+
cwd,
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
env: { ...process.env },
|
|
45
|
+
shell: process.platform === 'win32'
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
status: proc.status ?? 1,
|
|
50
|
+
stdout: proc.stdout ?? '',
|
|
51
|
+
stderr: `${proc.stderr ?? ''}${proc.error ? `\n${String(proc.error)}` : ''}`,
|
|
52
|
+
errorCode: proc.error?.code
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runWithRetry(
|
|
57
|
+
command: string,
|
|
58
|
+
args: string[],
|
|
59
|
+
cwd: string,
|
|
60
|
+
retries = 2
|
|
61
|
+
): Promise<RunResult> {
|
|
62
|
+
let attempt = 0
|
|
63
|
+
let lastResult = run(command, args, cwd)
|
|
64
|
+
while (lastResult.status !== 0 && attempt < retries) {
|
|
65
|
+
attempt += 1
|
|
66
|
+
await delay(1000 * attempt)
|
|
67
|
+
lastResult = run(command, args, cwd)
|
|
68
|
+
}
|
|
69
|
+
return lastResult
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runPnpmWithRetry(args: string[], cwd: string, retries = 2): Promise<RunResult> {
|
|
73
|
+
const candidates = getPnpmCandidates()
|
|
74
|
+
let lastResult: RunResult = { status: 1, stdout: '', stderr: 'pnpm command not found' }
|
|
75
|
+
|
|
76
|
+
for (const candidate of candidates) {
|
|
77
|
+
const attempt = await runWithRetry(candidate.command, [...candidate.argsPrefix, ...args], cwd, retries)
|
|
78
|
+
if (attempt.status === 0) {
|
|
79
|
+
return attempt
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
lastResult = attempt
|
|
83
|
+
if (attempt.errorCode !== 'ENOENT' && !attempt.stderr.includes('ENOENT')) {
|
|
84
|
+
return attempt
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return lastResult
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('cli pnpm-isolated integration', () => {
|
|
92
|
+
beforeAll(async () => {
|
|
93
|
+
const tmpBase = await mkdtemp(path.join(os.tmpdir(), 'snapshot-pnpm-it-'))
|
|
94
|
+
fixtureRoot = path.join(tmpBase, 'repo')
|
|
95
|
+
await cp(templateRoot, fixtureRoot, { recursive: true })
|
|
96
|
+
|
|
97
|
+
const wsA = path.join(fixtureRoot, 'packages/ws-a')
|
|
98
|
+
const wsB = path.join(fixtureRoot, 'packages/ws-b')
|
|
99
|
+
|
|
100
|
+
const installA = await runPnpmWithRetry(['install', '--ignore-workspace', '--no-frozen-lockfile'], wsA)
|
|
101
|
+
expect(installA.status, `${installA.stdout}\n${installA.stderr}`).toBe(0)
|
|
102
|
+
await access(path.join(wsA, 'node_modules/eslint/package.json'))
|
|
103
|
+
|
|
104
|
+
const installB = await runPnpmWithRetry(['install', '--ignore-workspace', '--no-frozen-lockfile'], wsB)
|
|
105
|
+
expect(installB.status, `${installB.stdout}\n${installB.stderr}`).toBe(0)
|
|
106
|
+
await access(path.join(wsB, 'node_modules/eslint/package.json'))
|
|
107
|
+
}, 180000)
|
|
108
|
+
|
|
109
|
+
afterAll(async () => {
|
|
110
|
+
if (fixtureRoot) {
|
|
111
|
+
await rm(path.dirname(fixtureRoot), { recursive: true, force: true })
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('runs commands with workspace-local eslint installed by pnpm in isolated mode', async () => {
|
|
116
|
+
const snapshot = run(process.execPath, [cliDist, 'snapshot'], fixtureRoot)
|
|
117
|
+
expect(snapshot.status, snapshot.stderr).toBe(0)
|
|
118
|
+
|
|
119
|
+
const snapshotRaw = await readFile(path.join(fixtureRoot, '.eslint-config-snapshot/default.json'), 'utf8')
|
|
120
|
+
const parsed = JSON.parse(snapshotRaw)
|
|
121
|
+
expect(parsed).toEqual({
|
|
122
|
+
formatVersion: 1,
|
|
123
|
+
groupId: 'default',
|
|
124
|
+
workspaces: ['packages/ws-a', 'packages/ws-b'],
|
|
125
|
+
rules: {
|
|
126
|
+
eqeqeq: ['error', 'always'],
|
|
127
|
+
'no-console': ['error'],
|
|
128
|
+
'no-debugger': ['off']
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
await writeFile(
|
|
133
|
+
path.join(fixtureRoot, 'packages/ws-a/.eslintrc.cjs'),
|
|
134
|
+
"module.exports = { root: true, rules: { 'no-console': 'warn', eqeqeq: 'off' } }\n"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const compareChanged = run(process.execPath, [cliDist, 'compare'], fixtureRoot)
|
|
138
|
+
expect(compareChanged.status).toBe(1)
|
|
139
|
+
}, 180000)
|
|
140
|
+
})
|