@eslint-config-snapshot/api 0.3.2 → 0.4.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,11 @@
1
1
  # @eslint-config-snapshot/api
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Minor release with improved CLI output consistency and faster workspace extraction using ESLint API fallback strategy.
8
+
3
9
  ## 0.3.2
4
10
 
5
11
  ### 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,
@@ -288,6 +289,7 @@ var import_node_child_process = require("child_process");
288
289
  var import_node_fs = require("fs");
289
290
  var import_node_module = require("module");
290
291
  var import_node_path2 = __toESM(require("path"), 1);
292
+ var import_node_url = require("url");
291
293
  function resolveEslintBinForWorkspace(workspaceAbs) {
292
294
  const anchor = import_node_path2.default.join(workspaceAbs, "__snapshot_anchor__.cjs");
293
295
  const req = (0, import_node_module.createRequire)(anchor);
@@ -352,6 +354,44 @@ function extractRulesFromPrintConfig(workspaceAbs, fileAbs) {
352
354
  throw new Error(`Invalid JSON from eslint --print-config for ${fileAbs}`);
353
355
  }
354
356
  const rules = parsed.rules ?? {};
357
+ return normalizeRules(rules);
358
+ }
359
+ async function extractRulesForWorkspaceSamples(workspaceAbs, fileAbsList) {
360
+ const evaluate = await createWorkspaceEvaluator(workspaceAbs);
361
+ const results = [];
362
+ for (const fileAbs of fileAbsList) {
363
+ try {
364
+ const rules = await evaluate(fileAbs);
365
+ results.push({ fileAbs, rules });
366
+ } catch (error) {
367
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
368
+ results.push({ fileAbs, error: normalizedError });
369
+ }
370
+ }
371
+ return results;
372
+ }
373
+ async function createWorkspaceEvaluator(workspaceAbs) {
374
+ try {
375
+ const anchor = import_node_path2.default.join(workspaceAbs, "__snapshot_anchor__.cjs");
376
+ const req = (0, import_node_module.createRequire)(anchor);
377
+ const eslintModuleEntry = req.resolve("eslint");
378
+ const eslintModule = await import((0, import_node_url.pathToFileURL)(eslintModuleEntry).href);
379
+ const ESLintClass = eslintModule.ESLint ?? eslintModule.default?.ESLint;
380
+ if (ESLintClass) {
381
+ const eslint = new ESLintClass({ cwd: workspaceAbs });
382
+ return async (fileAbs) => {
383
+ const config = await eslint.calculateConfigForFile(fileAbs);
384
+ if (!config || typeof config !== "object") {
385
+ throw new Error(`Empty ESLint print-config output for ${fileAbs}`);
386
+ }
387
+ return normalizeRules(config.rules ?? {});
388
+ };
389
+ }
390
+ } catch {
391
+ }
392
+ return (fileAbs) => Promise.resolve(extractRulesFromPrintConfig(workspaceAbs, fileAbs));
393
+ }
394
+ function normalizeRules(rules) {
355
395
  const normalized = /* @__PURE__ */ new Map();
356
396
  for (const [ruleName, ruleConfig] of Object.entries(rules)) {
357
397
  normalized.set(ruleName, normalizeRuleEntry(ruleConfig));
@@ -607,6 +647,7 @@ function getConfigScaffold(preset = "minimal") {
607
647
  compareSeverity,
608
648
  diffSnapshots,
609
649
  discoverWorkspaces,
650
+ extractRulesForWorkspaceSamples,
610
651
  extractRulesFromPrintConfig,
611
652
  findConfigPath,
612
653
  getConfigScaffold,
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,44 @@ 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
+ async function extractRulesForWorkspaceSamples(workspaceAbs, fileAbsList) {
304
+ const evaluate = await createWorkspaceEvaluator(workspaceAbs);
305
+ const results = [];
306
+ for (const fileAbs of fileAbsList) {
307
+ try {
308
+ const rules = await evaluate(fileAbs);
309
+ results.push({ fileAbs, rules });
310
+ } catch (error) {
311
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
312
+ results.push({ fileAbs, error: normalizedError });
313
+ }
314
+ }
315
+ return results;
316
+ }
317
+ async function createWorkspaceEvaluator(workspaceAbs) {
318
+ try {
319
+ const anchor = path2.join(workspaceAbs, "__snapshot_anchor__.cjs");
320
+ const req = createRequire(anchor);
321
+ const eslintModuleEntry = req.resolve("eslint");
322
+ const eslintModule = await import(pathToFileURL(eslintModuleEntry).href);
323
+ const ESLintClass = eslintModule.ESLint ?? eslintModule.default?.ESLint;
324
+ if (ESLintClass) {
325
+ const eslint = new ESLintClass({ cwd: workspaceAbs });
326
+ return async (fileAbs) => {
327
+ const config = await eslint.calculateConfigForFile(fileAbs);
328
+ if (!config || typeof config !== "object") {
329
+ throw new Error(`Empty ESLint print-config output for ${fileAbs}`);
330
+ }
331
+ return normalizeRules(config.rules ?? {});
332
+ };
333
+ }
334
+ } catch {
335
+ }
336
+ return (fileAbs) => Promise.resolve(extractRulesFromPrintConfig(workspaceAbs, fileAbs));
337
+ }
338
+ function normalizeRules(rules) {
300
339
  const normalized = /* @__PURE__ */ new Map();
301
340
  for (const [ruleName, ruleConfig] of Object.entries(rules)) {
302
341
  normalized.set(ruleName, normalizeRuleEntry(ruleConfig));
@@ -551,6 +590,7 @@ export {
551
590
  compareSeverity,
552
591
  diffSnapshots,
553
592
  discoverWorkspaces,
593
+ extractRulesForWorkspaceSamples,
554
594
  extractRulesFromPrintConfig,
555
595
  findConfigPath,
556
596
  getConfigScaffold,
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.4.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,61 @@ 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 async function extractRulesForWorkspaceSamples(
95
+ workspaceAbs: string,
96
+ fileAbsList: string[]
97
+ ): Promise<WorkspaceExtractionResult[]> {
98
+ const evaluate = await createWorkspaceEvaluator(workspaceAbs)
99
+ const results: WorkspaceExtractionResult[] = []
100
+
101
+ for (const fileAbs of fileAbsList) {
102
+ try {
103
+ const rules = await evaluate(fileAbs)
104
+ results.push({ fileAbs, rules })
105
+ } catch (error: unknown) {
106
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
107
+ results.push({ fileAbs, error: normalizedError })
108
+ }
109
+ }
110
+
111
+ return results
112
+ }
113
+
114
+ async function createWorkspaceEvaluator(
115
+ workspaceAbs: string
116
+ ): Promise<(fileAbs: string) => Promise<ExtractedWorkspaceRules>> {
117
+ try {
118
+ const anchor = path.join(workspaceAbs, '__snapshot_anchor__.cjs')
119
+ const req = createRequire(anchor)
120
+ const eslintModuleEntry = req.resolve('eslint')
121
+ const eslintModule = (await import(pathToFileURL(eslintModuleEntry).href)) as {
122
+ ESLint?: new (options: { cwd: string }) => { calculateConfigForFile: (fileAbs: string) => Promise<{ rules?: Record<string, unknown> } | undefined> }
123
+ default?: { ESLint?: new (options: { cwd: string }) => { calculateConfigForFile: (fileAbs: string) => Promise<{ rules?: Record<string, unknown> } | undefined> } }
124
+ }
125
+
126
+ const ESLintClass = eslintModule.ESLint ?? eslintModule.default?.ESLint
127
+ if (ESLintClass) {
128
+ const eslint = new ESLintClass({ cwd: workspaceAbs })
129
+ return async (fileAbs: string) => {
130
+ const config = await eslint.calculateConfigForFile(fileAbs)
131
+ if (!config || typeof config !== 'object') {
132
+ throw new Error(`Empty ESLint print-config output for ${fileAbs}`)
133
+ }
134
+
135
+ return normalizeRules(config.rules ?? {})
136
+ }
137
+ }
138
+ } catch {
139
+ // fall through to subprocess-based extractor
140
+ }
141
+
142
+ return (fileAbs: string) => Promise.resolve(extractRulesFromPrintConfig(workspaceAbs, fileAbs))
143
+ }
144
+
145
+ function normalizeRules(rules: Record<string, unknown>): ExtractedWorkspaceRules {
89
146
  const normalized = new Map<string, NormalizedRuleEntry>()
90
147
 
91
148
  for (const [ruleName, ruleConfig] of Object.entries(rules)) {
package/src/index.ts CHANGED
@@ -7,8 +7,8 @@ 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 { extractRulesForWorkspaceSamples, extractRulesFromPrintConfig, resolveEslintBinForWorkspace } from './extract.js'
11
+ export type { ExtractedWorkspaceRules, NormalizedRuleEntry, WorkspaceExtractionResult } from './extract.js'
12
12
 
13
13
  export { aggregateRules, buildSnapshot, readSnapshotFile, writeSnapshotFile } from './snapshot.js'
14
14
  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
  })