@eslint-config-snapshot/api 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @eslint-config-snapshot/api
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add ESLint runtime version reporting by group in CLI summaries and improve pnpm/corepack isolated test resilience.
8
+
9
+ ## 0.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Minor release with improved CLI output consistency and faster workspace extraction using ESLint API fallback strategy.
14
+
3
15
  ## 0.3.2
4
16
 
5
17
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  compareSeverity: () => compareSeverity,
39
39
  diffSnapshots: () => diffSnapshots,
40
40
  discoverWorkspaces: () => discoverWorkspaces,
41
+ extractRulesForWorkspaceSamples: () => extractRulesForWorkspaceSamples,
41
42
  extractRulesFromPrintConfig: () => extractRulesFromPrintConfig,
42
43
  findConfigPath: () => findConfigPath,
43
44
  getConfigScaffold: () => getConfigScaffold,
@@ -47,6 +48,7 @@ __export(index_exports, {
47
48
  normalizeSeverity: () => normalizeSeverity,
48
49
  readSnapshotFile: () => readSnapshotFile,
49
50
  resolveEslintBinForWorkspace: () => resolveEslintBinForWorkspace,
51
+ resolveEslintVersionForWorkspace: () => resolveEslintVersionForWorkspace,
50
52
  sampleWorkspaceFiles: () => sampleWorkspaceFiles,
51
53
  sortUnique: () => sortUnique,
52
54
  writeSnapshotFile: () => writeSnapshotFile
@@ -288,6 +290,7 @@ var import_node_child_process = require("child_process");
288
290
  var import_node_fs = require("fs");
289
291
  var import_node_module = require("module");
290
292
  var import_node_path2 = __toESM(require("path"), 1);
293
+ var import_node_url = require("url");
291
294
  function resolveEslintBinForWorkspace(workspaceAbs) {
292
295
  const anchor = import_node_path2.default.join(workspaceAbs, "__snapshot_anchor__.cjs");
293
296
  const req = (0, import_node_module.createRequire)(anchor);
@@ -352,6 +355,57 @@ function extractRulesFromPrintConfig(workspaceAbs, fileAbs) {
352
355
  throw new Error(`Invalid JSON from eslint --print-config for ${fileAbs}`);
353
356
  }
354
357
  const rules = parsed.rules ?? {};
358
+ return normalizeRules(rules);
359
+ }
360
+ function resolveEslintVersionForWorkspace(workspaceAbs) {
361
+ const anchor = import_node_path2.default.join(workspaceAbs, "__snapshot_anchor__.cjs");
362
+ const req = (0, import_node_module.createRequire)(anchor);
363
+ try {
364
+ const packageJsonPath = req.resolve("eslint/package.json");
365
+ const packageJson = JSON.parse((0, import_node_fs.readFileSync)(packageJsonPath, "utf8"));
366
+ if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
367
+ return packageJson.version;
368
+ }
369
+ } catch {
370
+ }
371
+ return "unknown";
372
+ }
373
+ async function extractRulesForWorkspaceSamples(workspaceAbs, fileAbsList) {
374
+ const evaluate = await createWorkspaceEvaluator(workspaceAbs);
375
+ const results = [];
376
+ for (const fileAbs of fileAbsList) {
377
+ try {
378
+ const rules = await evaluate(fileAbs);
379
+ results.push({ fileAbs, rules });
380
+ } catch (error) {
381
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
382
+ results.push({ fileAbs, error: normalizedError });
383
+ }
384
+ }
385
+ return results;
386
+ }
387
+ async function createWorkspaceEvaluator(workspaceAbs) {
388
+ try {
389
+ const anchor = import_node_path2.default.join(workspaceAbs, "__snapshot_anchor__.cjs");
390
+ const req = (0, import_node_module.createRequire)(anchor);
391
+ const eslintModuleEntry = req.resolve("eslint");
392
+ const eslintModule = await import((0, import_node_url.pathToFileURL)(eslintModuleEntry).href);
393
+ const ESLintClass = eslintModule.ESLint ?? eslintModule.default?.ESLint;
394
+ if (ESLintClass) {
395
+ const eslint = new ESLintClass({ cwd: workspaceAbs });
396
+ return async (fileAbs) => {
397
+ const config = await eslint.calculateConfigForFile(fileAbs);
398
+ if (!config || typeof config !== "object") {
399
+ throw new Error(`Empty ESLint print-config output for ${fileAbs}`);
400
+ }
401
+ return normalizeRules(config.rules ?? {});
402
+ };
403
+ }
404
+ } catch {
405
+ }
406
+ return (fileAbs) => Promise.resolve(extractRulesFromPrintConfig(workspaceAbs, fileAbs));
407
+ }
408
+ function normalizeRules(rules) {
355
409
  const normalized = /* @__PURE__ */ new Map();
356
410
  for (const [ruleName, ruleConfig] of Object.entries(rules)) {
357
411
  normalized.set(ruleName, normalizeRuleEntry(ruleConfig));
@@ -607,6 +661,7 @@ function getConfigScaffold(preset = "minimal") {
607
661
  compareSeverity,
608
662
  diffSnapshots,
609
663
  discoverWorkspaces,
664
+ extractRulesForWorkspaceSamples,
610
665
  extractRulesFromPrintConfig,
611
666
  findConfigPath,
612
667
  getConfigScaffold,
@@ -616,6 +671,7 @@ function getConfigScaffold(preset = "minimal") {
616
671
  normalizeSeverity,
617
672
  readSnapshotFile,
618
673
  resolveEslintBinForWorkspace,
674
+ resolveEslintVersionForWorkspace,
619
675
  sampleWorkspaceFiles,
620
676
  sortUnique,
621
677
  writeSnapshotFile
package/dist/index.js CHANGED
@@ -233,6 +233,7 @@ import { spawnSync } from "child_process";
233
233
  import { existsSync, readFileSync } from "fs";
234
234
  import { createRequire } from "module";
235
235
  import path2 from "path";
236
+ import { pathToFileURL } from "url";
236
237
  function resolveEslintBinForWorkspace(workspaceAbs) {
237
238
  const anchor = path2.join(workspaceAbs, "__snapshot_anchor__.cjs");
238
239
  const req = createRequire(anchor);
@@ -297,6 +298,57 @@ function extractRulesFromPrintConfig(workspaceAbs, fileAbs) {
297
298
  throw new Error(`Invalid JSON from eslint --print-config for ${fileAbs}`);
298
299
  }
299
300
  const rules = parsed.rules ?? {};
301
+ return normalizeRules(rules);
302
+ }
303
+ function resolveEslintVersionForWorkspace(workspaceAbs) {
304
+ const anchor = path2.join(workspaceAbs, "__snapshot_anchor__.cjs");
305
+ const req = createRequire(anchor);
306
+ try {
307
+ const packageJsonPath = req.resolve("eslint/package.json");
308
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
309
+ if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
310
+ return packageJson.version;
311
+ }
312
+ } catch {
313
+ }
314
+ return "unknown";
315
+ }
316
+ async function extractRulesForWorkspaceSamples(workspaceAbs, fileAbsList) {
317
+ const evaluate = await createWorkspaceEvaluator(workspaceAbs);
318
+ const results = [];
319
+ for (const fileAbs of fileAbsList) {
320
+ try {
321
+ const rules = await evaluate(fileAbs);
322
+ results.push({ fileAbs, rules });
323
+ } catch (error) {
324
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
325
+ results.push({ fileAbs, error: normalizedError });
326
+ }
327
+ }
328
+ return results;
329
+ }
330
+ async function createWorkspaceEvaluator(workspaceAbs) {
331
+ try {
332
+ const anchor = path2.join(workspaceAbs, "__snapshot_anchor__.cjs");
333
+ const req = createRequire(anchor);
334
+ const eslintModuleEntry = req.resolve("eslint");
335
+ const eslintModule = await import(pathToFileURL(eslintModuleEntry).href);
336
+ const ESLintClass = eslintModule.ESLint ?? eslintModule.default?.ESLint;
337
+ if (ESLintClass) {
338
+ const eslint = new ESLintClass({ cwd: workspaceAbs });
339
+ return async (fileAbs) => {
340
+ const config = await eslint.calculateConfigForFile(fileAbs);
341
+ if (!config || typeof config !== "object") {
342
+ throw new Error(`Empty ESLint print-config output for ${fileAbs}`);
343
+ }
344
+ return normalizeRules(config.rules ?? {});
345
+ };
346
+ }
347
+ } catch {
348
+ }
349
+ return (fileAbs) => Promise.resolve(extractRulesFromPrintConfig(workspaceAbs, fileAbs));
350
+ }
351
+ function normalizeRules(rules) {
300
352
  const normalized = /* @__PURE__ */ new Map();
301
353
  for (const [ruleName, ruleConfig] of Object.entries(rules)) {
302
354
  normalized.set(ruleName, normalizeRuleEntry(ruleConfig));
@@ -551,6 +603,7 @@ export {
551
603
  compareSeverity,
552
604
  diffSnapshots,
553
605
  discoverWorkspaces,
606
+ extractRulesForWorkspaceSamples,
554
607
  extractRulesFromPrintConfig,
555
608
  findConfigPath,
556
609
  getConfigScaffold,
@@ -560,6 +613,7 @@ export {
560
613
  normalizeSeverity,
561
614
  readSnapshotFile,
562
615
  resolveEslintBinForWorkspace,
616
+ resolveEslintVersionForWorkspace,
563
617
  sampleWorkspaceFiles,
564
618
  sortUnique,
565
619
  writeSnapshotFile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint-config-snapshot/api",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
package/src/extract.ts CHANGED
@@ -2,12 +2,14 @@ import { spawnSync } from 'node:child_process'
2
2
  import { existsSync, readFileSync } from 'node:fs'
3
3
  import { createRequire } from 'node:module'
4
4
  import path from 'node:path'
5
+ import { pathToFileURL } from 'node:url'
5
6
 
6
7
  import { canonicalizeJson, normalizeSeverity } from './core.js'
7
8
 
8
9
  export type NormalizedRuleEntry = [severity: 'off' | 'warn' | 'error'] | [severity: 'off' | 'warn' | 'error', options: unknown]
9
10
 
10
11
  export type ExtractedWorkspaceRules = Map<string, NormalizedRuleEntry>
12
+ export type WorkspaceExtractionResult = { fileAbs: string; rules?: ExtractedWorkspaceRules; error?: Error }
11
13
 
12
14
  export function resolveEslintBinForWorkspace(workspaceAbs: string): string {
13
15
  const anchor = path.join(workspaceAbs, '__snapshot_anchor__.cjs')
@@ -86,6 +88,78 @@ export function extractRulesFromPrintConfig(workspaceAbs: string, fileAbs: strin
86
88
  }
87
89
 
88
90
  const rules = (parsed as { rules?: Record<string, unknown> }).rules ?? {}
91
+ return normalizeRules(rules)
92
+ }
93
+
94
+ export function resolveEslintVersionForWorkspace(workspaceAbs: string): string {
95
+ const anchor = path.join(workspaceAbs, '__snapshot_anchor__.cjs')
96
+ const req = createRequire(anchor)
97
+
98
+ try {
99
+ const packageJsonPath = req.resolve('eslint/package.json')
100
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { version?: string }
101
+ if (typeof packageJson.version === 'string' && packageJson.version.length > 0) {
102
+ return packageJson.version
103
+ }
104
+ } catch {
105
+ // fall through to deterministic unknown marker
106
+ }
107
+
108
+ return 'unknown'
109
+ }
110
+
111
+ export async function extractRulesForWorkspaceSamples(
112
+ workspaceAbs: string,
113
+ fileAbsList: string[]
114
+ ): Promise<WorkspaceExtractionResult[]> {
115
+ const evaluate = await createWorkspaceEvaluator(workspaceAbs)
116
+ const results: WorkspaceExtractionResult[] = []
117
+
118
+ for (const fileAbs of fileAbsList) {
119
+ try {
120
+ const rules = await evaluate(fileAbs)
121
+ results.push({ fileAbs, rules })
122
+ } catch (error: unknown) {
123
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
124
+ results.push({ fileAbs, error: normalizedError })
125
+ }
126
+ }
127
+
128
+ return results
129
+ }
130
+
131
+ async function createWorkspaceEvaluator(
132
+ workspaceAbs: string
133
+ ): Promise<(fileAbs: string) => Promise<ExtractedWorkspaceRules>> {
134
+ try {
135
+ const anchor = path.join(workspaceAbs, '__snapshot_anchor__.cjs')
136
+ const req = createRequire(anchor)
137
+ const eslintModuleEntry = req.resolve('eslint')
138
+ const eslintModule = (await import(pathToFileURL(eslintModuleEntry).href)) as {
139
+ ESLint?: new (options: { cwd: string }) => { calculateConfigForFile: (fileAbs: string) => Promise<{ rules?: Record<string, unknown> } | undefined> }
140
+ default?: { ESLint?: new (options: { cwd: string }) => { calculateConfigForFile: (fileAbs: string) => Promise<{ rules?: Record<string, unknown> } | undefined> } }
141
+ }
142
+
143
+ const ESLintClass = eslintModule.ESLint ?? eslintModule.default?.ESLint
144
+ if (ESLintClass) {
145
+ const eslint = new ESLintClass({ cwd: workspaceAbs })
146
+ return async (fileAbs: string) => {
147
+ const config = await eslint.calculateConfigForFile(fileAbs)
148
+ if (!config || typeof config !== 'object') {
149
+ throw new Error(`Empty ESLint print-config output for ${fileAbs}`)
150
+ }
151
+
152
+ return normalizeRules(config.rules ?? {})
153
+ }
154
+ }
155
+ } catch {
156
+ // fall through to subprocess-based extractor
157
+ }
158
+
159
+ return (fileAbs: string) => Promise.resolve(extractRulesFromPrintConfig(workspaceAbs, fileAbs))
160
+ }
161
+
162
+ function normalizeRules(rules: Record<string, unknown>): ExtractedWorkspaceRules {
89
163
  const normalized = new Map<string, NormalizedRuleEntry>()
90
164
 
91
165
  for (const [ruleName, ruleConfig] of Object.entries(rules)) {
package/src/index.ts CHANGED
@@ -7,8 +7,13 @@ export type { GroupAssignment, GroupDefinition, WorkspaceDiscovery, WorkspaceInp
7
7
  export { sampleWorkspaceFiles } from './sampling.js'
8
8
  export type { SamplingConfig } from './sampling.js'
9
9
 
10
- export { extractRulesFromPrintConfig, resolveEslintBinForWorkspace } from './extract.js'
11
- export type { ExtractedWorkspaceRules, NormalizedRuleEntry } from './extract.js'
10
+ export {
11
+ extractRulesForWorkspaceSamples,
12
+ extractRulesFromPrintConfig,
13
+ resolveEslintBinForWorkspace,
14
+ resolveEslintVersionForWorkspace
15
+ } from './extract.js'
16
+ export type { ExtractedWorkspaceRules, NormalizedRuleEntry, WorkspaceExtractionResult } from './extract.js'
12
17
 
13
18
  export { aggregateRules, buildSnapshot, readSnapshotFile, writeSnapshotFile } from './snapshot.js'
14
19
  export type { SnapshotFile } from './snapshot.js'
@@ -3,7 +3,7 @@ import os from 'node:os'
3
3
  import path from 'node:path'
4
4
  import { afterAll, describe, expect, it } from 'vitest'
5
5
 
6
- import { extractRulesFromPrintConfig, resolveEslintBinForWorkspace } from '../src/index.js'
6
+ import { extractRulesForWorkspaceSamples, extractRulesFromPrintConfig, resolveEslintBinForWorkspace } from '../src/index.js'
7
7
 
8
8
  const workspace = path.join(os.tmpdir(), `snapshot-extract-${Date.now()}`)
9
9
 
@@ -137,4 +137,29 @@ describe('extract', () => {
137
137
  `Failed to run eslint --print-config for ${fileAbs}`
138
138
  )
139
139
  })
140
+
141
+ it('extracts multiple sampled files in one workspace call', async () => {
142
+ const multiWorkspace = `${workspace}-multi`
143
+ await mkdir(path.join(multiWorkspace, 'node_modules/eslint/bin'), { recursive: true })
144
+ await mkdir(path.join(multiWorkspace, 'src'), { recursive: true })
145
+ await writeFile(path.join(multiWorkspace, 'node_modules/eslint/package.json'), JSON.stringify({ name: 'eslint', version: '0.0.0' }, null, 2))
146
+ await writeFile(
147
+ path.join(multiWorkspace, 'node_modules/eslint/bin/eslint.js'),
148
+ "console.log(JSON.stringify({ rules: { 'no-console': 1 } }))\n"
149
+ )
150
+
151
+ const fileA = path.join(multiWorkspace, 'src/a.ts')
152
+ const fileB = path.join(multiWorkspace, 'src/b.ts')
153
+ await writeFile(fileA, 'export const a = 1\n')
154
+ await writeFile(fileB, 'export const b = 1\n')
155
+
156
+ const extracted = await extractRulesForWorkspaceSamples(multiWorkspace, [fileA, fileB])
157
+ expect(extracted).toHaveLength(2)
158
+ for (const entry of extracted) {
159
+ expect(entry.error).toBeUndefined()
160
+ expect(entry.rules ? Object.fromEntries(entry.rules.entries()) : {}).toEqual({
161
+ 'no-console': ['warn']
162
+ })
163
+ }
164
+ })
140
165
  })