@eslint-config-snapshot/api 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 +610 -0
- package/dist/index.js +554 -0
- package/package.json +24 -0
- package/project.json +35 -0
- package/src/config.ts +139 -0
- package/src/core.ts +55 -0
- package/src/diff.ts +103 -0
- package/src/extract.ts +119 -0
- package/src/index.ts +20 -0
- package/src/sampling.ts +136 -0
- package/src/snapshot.ts +76 -0
- package/src/workspace.ts +130 -0
- package/test/api.test.ts +10 -0
- package/test/config.test.ts +100 -0
- package/test/core.test.ts +24 -0
- package/test/diff.test.ts +60 -0
- package/test/extract.test.ts +140 -0
- package/test/sampling.test.ts +91 -0
- package/test/snapshot.test.ts +64 -0
- package/test/workspace.test.ts +25 -0
- package/tsconfig.json +12 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
// src/core.ts
|
|
2
|
+
function normalizePath(input) {
|
|
3
|
+
const withSlashes = input.replaceAll("\\", "/");
|
|
4
|
+
const collapsed = withSlashes.replaceAll(/\/+/g, "/");
|
|
5
|
+
const withoutTrailing = collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
|
|
6
|
+
return withoutTrailing === "" ? "." : withoutTrailing;
|
|
7
|
+
}
|
|
8
|
+
function sortUnique(list) {
|
|
9
|
+
return [...new Set(list.map((item) => normalizePath(item)))].sort();
|
|
10
|
+
}
|
|
11
|
+
function canonicalizeJson(value) {
|
|
12
|
+
if (value === null || value === void 0) {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
return value.map((entry) => canonicalizeJson(entry));
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "object") {
|
|
19
|
+
const record = value;
|
|
20
|
+
const result = {};
|
|
21
|
+
for (const key of Object.keys(record).sort()) {
|
|
22
|
+
const entry = record[key];
|
|
23
|
+
if (entry !== void 0) {
|
|
24
|
+
result[key] = canonicalizeJson(entry);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function compareSeverity(a, b) {
|
|
32
|
+
const rank = { off: 0, warn: 1, error: 2 };
|
|
33
|
+
return rank[a] - rank[b];
|
|
34
|
+
}
|
|
35
|
+
function normalizeSeverity(value) {
|
|
36
|
+
if (value === 0 || value === "off") {
|
|
37
|
+
return "off";
|
|
38
|
+
}
|
|
39
|
+
if (value === 1 || value === "warn") {
|
|
40
|
+
return "warn";
|
|
41
|
+
}
|
|
42
|
+
if (value === 2 || value === "error") {
|
|
43
|
+
return "error";
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Unsupported severity: ${String(value)}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/workspace.ts
|
|
49
|
+
import { getPackages } from "@manypkg/get-packages";
|
|
50
|
+
import path from "path";
|
|
51
|
+
import picomatch from "picomatch";
|
|
52
|
+
async function discoverWorkspaces(options) {
|
|
53
|
+
const cwd = options?.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
54
|
+
const workspaceInput = options?.workspaceInput ?? { mode: "discover" };
|
|
55
|
+
if (workspaceInput.mode === "manual") {
|
|
56
|
+
const rootAbs2 = path.resolve(workspaceInput.rootAbs ?? cwd);
|
|
57
|
+
return {
|
|
58
|
+
rootAbs: rootAbs2,
|
|
59
|
+
workspacesRel: sortUnique(workspaceInput.workspaces)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const { rootDir, packages } = await getPackages(cwd);
|
|
63
|
+
const workspacesAbs = packages.map((pkg) => pkg.dir);
|
|
64
|
+
const rootAbs = rootDir ? path.resolve(rootDir) : lowestCommonAncestor(workspacesAbs);
|
|
65
|
+
const workspacesRel = sortUnique(workspacesAbs.map((entry) => normalizePath(path.relative(rootAbs, entry))));
|
|
66
|
+
return {
|
|
67
|
+
rootAbs,
|
|
68
|
+
workspacesRel
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function assignGroupsByMatch(workspacesRel, groups) {
|
|
72
|
+
const sortedWorkspaces = sortUnique([...workspacesRel]);
|
|
73
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const group of groups) {
|
|
75
|
+
assignments.set(group.name, []);
|
|
76
|
+
}
|
|
77
|
+
const unmatched = [];
|
|
78
|
+
for (const workspace of sortedWorkspaces) {
|
|
79
|
+
let assigned = false;
|
|
80
|
+
for (const group of groups) {
|
|
81
|
+
if (matchesWorkspace(workspace, group.match)) {
|
|
82
|
+
assignments.get(group.name)?.push(workspace);
|
|
83
|
+
assigned = true;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!assigned) {
|
|
88
|
+
unmatched.push(workspace);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (unmatched.length > 0) {
|
|
92
|
+
throw new Error(`Unmatched workspaces: ${unmatched.join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
return groups.map((group) => ({
|
|
95
|
+
name: group.name,
|
|
96
|
+
workspaces: assignments.get(group.name) ?? []
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
function matchesWorkspace(workspace, patterns) {
|
|
100
|
+
const positives = patterns.filter((pattern) => !pattern.startsWith("!"));
|
|
101
|
+
const negatives = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
|
|
102
|
+
const isPositiveMatch = positives.some((pattern) => picomatch(pattern, { dot: true })(workspace));
|
|
103
|
+
if (!isPositiveMatch) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const isNegativeMatch = negatives.some((pattern) => picomatch(pattern, { dot: true })(workspace));
|
|
107
|
+
return !isNegativeMatch;
|
|
108
|
+
}
|
|
109
|
+
function lowestCommonAncestor(paths) {
|
|
110
|
+
if (paths.length === 0) {
|
|
111
|
+
return process.cwd();
|
|
112
|
+
}
|
|
113
|
+
const segments = paths.map((entry) => path.resolve(entry).split(path.sep));
|
|
114
|
+
const minLen = Math.min(...segments.map((parts) => parts.length));
|
|
115
|
+
const common = [];
|
|
116
|
+
for (let index = 0; index < minLen; index += 1) {
|
|
117
|
+
const value = segments[0][index];
|
|
118
|
+
if (segments.every((parts) => parts[index] === value)) {
|
|
119
|
+
common.push(value);
|
|
120
|
+
} else {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (common.length === 0) {
|
|
125
|
+
return path.parse(path.resolve(paths[0])).root;
|
|
126
|
+
}
|
|
127
|
+
return common.join(path.sep);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/sampling.ts
|
|
131
|
+
import fg from "fast-glob";
|
|
132
|
+
import picomatch2 from "picomatch";
|
|
133
|
+
async function sampleWorkspaceFiles(workspaceAbs, config) {
|
|
134
|
+
const all = await fg(config.includeGlobs, {
|
|
135
|
+
cwd: workspaceAbs,
|
|
136
|
+
ignore: config.excludeGlobs,
|
|
137
|
+
onlyFiles: true,
|
|
138
|
+
dot: true,
|
|
139
|
+
unique: true
|
|
140
|
+
});
|
|
141
|
+
const normalized = sortUnique(all.map((entry) => normalizePath(entry)));
|
|
142
|
+
if (normalized.length === 0) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
if (normalized.length <= config.maxFilesPerWorkspace) {
|
|
146
|
+
return normalized;
|
|
147
|
+
}
|
|
148
|
+
if (config.hintGlobs.length === 0) {
|
|
149
|
+
return selectDistributed(normalized, config.maxFilesPerWorkspace);
|
|
150
|
+
}
|
|
151
|
+
const hinted = normalized.filter((entry) => config.hintGlobs.some((pattern) => picomatch2(pattern, { dot: true })(entry)));
|
|
152
|
+
const notHinted = normalized.filter((entry) => !hinted.includes(entry));
|
|
153
|
+
return selectDistributed([...hinted, ...notHinted], config.maxFilesPerWorkspace);
|
|
154
|
+
}
|
|
155
|
+
function selectDistributed(files, count) {
|
|
156
|
+
if (files.length <= count) {
|
|
157
|
+
return files;
|
|
158
|
+
}
|
|
159
|
+
const selected = [];
|
|
160
|
+
const selectedSet = /* @__PURE__ */ new Set();
|
|
161
|
+
const tokenSeen = /* @__PURE__ */ new Set();
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
if (selected.length >= count) {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
const token = getPrimaryToken(file);
|
|
167
|
+
if (!token || tokenSeen.has(token)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
tokenSeen.add(token);
|
|
171
|
+
selected.push(file);
|
|
172
|
+
selectedSet.add(file);
|
|
173
|
+
}
|
|
174
|
+
if (selected.length >= count) {
|
|
175
|
+
return sortUnique(selected).slice(0, count);
|
|
176
|
+
}
|
|
177
|
+
const remaining = files.filter((file) => !selectedSet.has(file));
|
|
178
|
+
const needed = count - selected.length;
|
|
179
|
+
const spaced = pickUniformly(remaining, needed);
|
|
180
|
+
return sortUnique([...selected, ...spaced]).slice(0, count);
|
|
181
|
+
}
|
|
182
|
+
function pickUniformly(files, count) {
|
|
183
|
+
if (count <= 0 || files.length === 0) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
if (files.length <= count) {
|
|
187
|
+
return files;
|
|
188
|
+
}
|
|
189
|
+
if (count === 1) {
|
|
190
|
+
return [files[0]];
|
|
191
|
+
}
|
|
192
|
+
const picked = [];
|
|
193
|
+
const usedIndices = /* @__PURE__ */ new Set();
|
|
194
|
+
for (let index = 0; index < count; index += 1) {
|
|
195
|
+
const raw = Math.round(index * (files.length - 1) / (count - 1));
|
|
196
|
+
const safeIndex = nextFreeIndex(raw, usedIndices, files.length);
|
|
197
|
+
usedIndices.add(safeIndex);
|
|
198
|
+
picked.push(files[safeIndex]);
|
|
199
|
+
}
|
|
200
|
+
return picked;
|
|
201
|
+
}
|
|
202
|
+
function nextFreeIndex(candidate, used, max) {
|
|
203
|
+
if (!used.has(candidate)) {
|
|
204
|
+
return candidate;
|
|
205
|
+
}
|
|
206
|
+
for (let delta = 1; delta < max; delta += 1) {
|
|
207
|
+
const forward = candidate + delta;
|
|
208
|
+
if (forward < max && !used.has(forward)) {
|
|
209
|
+
return forward;
|
|
210
|
+
}
|
|
211
|
+
const backward = candidate - delta;
|
|
212
|
+
if (backward >= 0 && !used.has(backward)) {
|
|
213
|
+
return backward;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return candidate;
|
|
217
|
+
}
|
|
218
|
+
function getPrimaryToken(file) {
|
|
219
|
+
const parts = file.split("/");
|
|
220
|
+
const basename = parts.slice(-1)[0];
|
|
221
|
+
if (!basename) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const nameOnly = basename.replace(/\.[^.]+$/u, "");
|
|
225
|
+
const expanded = nameOnly.replaceAll(/([a-z])([A-Z])/gu, "$1 $2").replaceAll(/[_\-.]+/gu, " ").toLowerCase();
|
|
226
|
+
const token = expanded.split(/\s+/u).find((entry) => entry.length > 1 && !GENERIC_TOKENS.has(entry));
|
|
227
|
+
return token ?? null;
|
|
228
|
+
}
|
|
229
|
+
var GENERIC_TOKENS = /* @__PURE__ */ new Set(["src", "index", "main", "test", "spec", "package", "packages", "lib", "dist"]);
|
|
230
|
+
|
|
231
|
+
// src/extract.ts
|
|
232
|
+
import { spawnSync } from "child_process";
|
|
233
|
+
import { existsSync, readFileSync } from "fs";
|
|
234
|
+
import { createRequire } from "module";
|
|
235
|
+
import path2 from "path";
|
|
236
|
+
function resolveEslintBinForWorkspace(workspaceAbs) {
|
|
237
|
+
const anchor = path2.join(workspaceAbs, "__snapshot_anchor__.cjs");
|
|
238
|
+
const req = createRequire(anchor);
|
|
239
|
+
try {
|
|
240
|
+
return req.resolve("eslint/bin/eslint.js");
|
|
241
|
+
} catch {
|
|
242
|
+
try {
|
|
243
|
+
const eslintEntry = req.resolve("eslint");
|
|
244
|
+
const eslintRoot = findPackageRoot(eslintEntry);
|
|
245
|
+
const packageJsonPath = path2.join(eslintRoot, "package.json");
|
|
246
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
247
|
+
const relativeBin = resolveBinPath(packageJson.bin);
|
|
248
|
+
const binAbs = path2.resolve(eslintRoot, relativeBin);
|
|
249
|
+
if (existsSync(binAbs)) {
|
|
250
|
+
return binAbs;
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
throw new Error(`Unable to resolve eslint from workspace: ${workspaceAbs}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function resolveBinPath(bin) {
|
|
258
|
+
if (typeof bin === "string") {
|
|
259
|
+
return bin;
|
|
260
|
+
}
|
|
261
|
+
if (typeof bin?.eslint === "string") {
|
|
262
|
+
return bin.eslint;
|
|
263
|
+
}
|
|
264
|
+
return "bin/eslint.js";
|
|
265
|
+
}
|
|
266
|
+
function findPackageRoot(entryAbs) {
|
|
267
|
+
let current = path2.dirname(entryAbs);
|
|
268
|
+
while (true) {
|
|
269
|
+
const packageJsonPath = path2.join(current, "package.json");
|
|
270
|
+
if (existsSync(packageJsonPath)) {
|
|
271
|
+
return current;
|
|
272
|
+
}
|
|
273
|
+
const parent = path2.dirname(current);
|
|
274
|
+
if (parent === current) {
|
|
275
|
+
throw new Error("Package root not found");
|
|
276
|
+
}
|
|
277
|
+
current = parent;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function extractRulesFromPrintConfig(workspaceAbs, fileAbs) {
|
|
281
|
+
const eslintBin = resolveEslintBinForWorkspace(workspaceAbs);
|
|
282
|
+
const proc = spawnSync(process.execPath, [eslintBin, "--print-config", fileAbs], {
|
|
283
|
+
cwd: workspaceAbs,
|
|
284
|
+
encoding: "utf8"
|
|
285
|
+
});
|
|
286
|
+
if (proc.status !== 0) {
|
|
287
|
+
throw new Error(`Failed to run eslint --print-config for ${fileAbs}`);
|
|
288
|
+
}
|
|
289
|
+
const stdout = proc.stdout.trim();
|
|
290
|
+
if (stdout.length === 0 || stdout === "undefined") {
|
|
291
|
+
throw new Error(`Empty ESLint print-config output for ${fileAbs}`);
|
|
292
|
+
}
|
|
293
|
+
let parsed;
|
|
294
|
+
try {
|
|
295
|
+
parsed = JSON.parse(stdout);
|
|
296
|
+
} catch {
|
|
297
|
+
throw new Error(`Invalid JSON from eslint --print-config for ${fileAbs}`);
|
|
298
|
+
}
|
|
299
|
+
const rules = parsed.rules ?? {};
|
|
300
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
301
|
+
for (const [ruleName, ruleConfig] of Object.entries(rules)) {
|
|
302
|
+
normalized.set(ruleName, normalizeRuleEntry(ruleConfig));
|
|
303
|
+
}
|
|
304
|
+
return normalized;
|
|
305
|
+
}
|
|
306
|
+
function normalizeRuleEntry(raw) {
|
|
307
|
+
if (Array.isArray(raw)) {
|
|
308
|
+
if (raw.length === 0) {
|
|
309
|
+
throw new Error("Rule configuration array cannot be empty");
|
|
310
|
+
}
|
|
311
|
+
const severity = normalizeSeverity(raw[0]);
|
|
312
|
+
const rest = raw.slice(1).map((item) => canonicalizeJson(item));
|
|
313
|
+
if (rest.length === 0) {
|
|
314
|
+
return [severity];
|
|
315
|
+
}
|
|
316
|
+
if (rest.length === 1) {
|
|
317
|
+
return [severity, rest[0]];
|
|
318
|
+
}
|
|
319
|
+
return [severity, canonicalizeJson(rest)];
|
|
320
|
+
}
|
|
321
|
+
return [normalizeSeverity(raw)];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/snapshot.ts
|
|
325
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
326
|
+
import path3 from "path";
|
|
327
|
+
function aggregateRules(ruleMaps) {
|
|
328
|
+
const aggregated = /* @__PURE__ */ new Map();
|
|
329
|
+
for (const rules of ruleMaps) {
|
|
330
|
+
for (const [ruleName, nextEntry] of rules.entries()) {
|
|
331
|
+
const currentEntry = aggregated.get(ruleName);
|
|
332
|
+
if (!currentEntry) {
|
|
333
|
+
aggregated.set(ruleName, canonicalizeJson(nextEntry));
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const severityCmp = compareSeverity(nextEntry[0], currentEntry[0]);
|
|
337
|
+
if (severityCmp > 0) {
|
|
338
|
+
aggregated.set(ruleName, canonicalizeJson(nextEntry));
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (severityCmp < 0) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const currentOptions = currentEntry.length > 1 ? canonicalizeJson(currentEntry[1]) : void 0;
|
|
345
|
+
const nextOptions = nextEntry.length > 1 ? canonicalizeJson(nextEntry[1]) : void 0;
|
|
346
|
+
if (JSON.stringify(currentOptions) !== JSON.stringify(nextOptions)) {
|
|
347
|
+
throw new Error(`Conflicting rule options for ${ruleName} at severity ${currentEntry[0]}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return new Map([...aggregated.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
352
|
+
}
|
|
353
|
+
function buildSnapshot(groupId, workspaces, rules) {
|
|
354
|
+
const sortedRules = [...rules.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
355
|
+
const rulesObject = {};
|
|
356
|
+
for (const [name, config] of sortedRules) {
|
|
357
|
+
rulesObject[name] = canonicalizeJson(config);
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
formatVersion: 1,
|
|
361
|
+
groupId,
|
|
362
|
+
workspaces: sortUnique(workspaces),
|
|
363
|
+
rules: rulesObject
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async function writeSnapshotFile(snapshotDirAbs, snapshot) {
|
|
367
|
+
await mkdir(snapshotDirAbs, { recursive: true });
|
|
368
|
+
const filePath = path3.join(snapshotDirAbs, `${snapshot.groupId}.json`);
|
|
369
|
+
await mkdir(path3.dirname(filePath), { recursive: true });
|
|
370
|
+
const payload = JSON.stringify(snapshot, null, 2);
|
|
371
|
+
await writeFile(filePath, `${payload}
|
|
372
|
+
`, "utf8");
|
|
373
|
+
return filePath;
|
|
374
|
+
}
|
|
375
|
+
async function readSnapshotFile(fileAbs) {
|
|
376
|
+
const raw = await readFile(fileAbs, "utf8");
|
|
377
|
+
return JSON.parse(raw);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/diff.ts
|
|
381
|
+
function diffSnapshots(before, after) {
|
|
382
|
+
const beforeRules = before.rules;
|
|
383
|
+
const afterRules = after.rules;
|
|
384
|
+
const beforeNames = Object.keys(beforeRules).sort();
|
|
385
|
+
const afterNames = Object.keys(afterRules).sort();
|
|
386
|
+
const introducedRules = afterNames.filter((name) => !beforeNames.includes(name));
|
|
387
|
+
const removedRules = beforeNames.filter((name) => !afterNames.includes(name));
|
|
388
|
+
const severityChanges = [];
|
|
389
|
+
const optionChanges = [];
|
|
390
|
+
for (const name of beforeNames.filter((entry) => afterNames.includes(entry))) {
|
|
391
|
+
const oldEntry = beforeRules[name];
|
|
392
|
+
const newEntry = afterRules[name];
|
|
393
|
+
if (oldEntry[0] !== newEntry[0]) {
|
|
394
|
+
severityChanges.push({
|
|
395
|
+
rule: name,
|
|
396
|
+
before: oldEntry[0],
|
|
397
|
+
after: newEntry[0]
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
const oldOptions = oldEntry.length > 1 ? canonicalizeJson(oldEntry[1]) : void 0;
|
|
401
|
+
const newOptions = newEntry.length > 1 ? canonicalizeJson(newEntry[1]) : void 0;
|
|
402
|
+
if (oldEntry[0] === "off" || newEntry[0] === "off") {
|
|
403
|
+
if (oldEntry[0] === "off" && newEntry[0] === "off") {
|
|
404
|
+
if (oldOptions !== void 0 && newOptions === void 0) {
|
|
405
|
+
removedRules.push(name);
|
|
406
|
+
} else if (oldOptions === void 0 && newOptions !== void 0) {
|
|
407
|
+
introducedRules.push(name);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (JSON.stringify(oldOptions) !== JSON.stringify(newOptions)) {
|
|
413
|
+
optionChanges.push({
|
|
414
|
+
rule: name,
|
|
415
|
+
before: oldOptions,
|
|
416
|
+
after: newOptions
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const beforeWorkspaces = sortUnique(before.workspaces);
|
|
421
|
+
const afterWorkspaces = sortUnique(after.workspaces);
|
|
422
|
+
return {
|
|
423
|
+
introducedRules: sortUnique(introducedRules),
|
|
424
|
+
removedRules: sortUnique(removedRules),
|
|
425
|
+
severityChanges,
|
|
426
|
+
optionChanges,
|
|
427
|
+
workspaceMembershipChanges: {
|
|
428
|
+
added: afterWorkspaces.filter((ws) => !beforeWorkspaces.includes(ws)),
|
|
429
|
+
removed: beforeWorkspaces.filter((ws) => !afterWorkspaces.includes(ws))
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function hasDiff(diff) {
|
|
434
|
+
return diff.introducedRules.length > 0 || diff.removedRules.length > 0 || diff.severityChanges.length > 0 || diff.optionChanges.length > 0 || diff.workspaceMembershipChanges.added.length > 0 || diff.workspaceMembershipChanges.removed.length > 0;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/config.ts
|
|
438
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
439
|
+
import path4 from "path";
|
|
440
|
+
var DEFAULT_CONFIG = {
|
|
441
|
+
workspaceInput: { mode: "discover" },
|
|
442
|
+
grouping: {
|
|
443
|
+
mode: "match",
|
|
444
|
+
groups: [{ name: "default", match: ["**/*"] }]
|
|
445
|
+
},
|
|
446
|
+
sampling: {
|
|
447
|
+
maxFilesPerWorkspace: 8,
|
|
448
|
+
includeGlobs: ["**/*.{js,jsx,ts,tsx,cjs,mjs}"],
|
|
449
|
+
excludeGlobs: ["**/node_modules/**", "**/dist/**"],
|
|
450
|
+
hintGlobs: []
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
var SPEC_SEARCH_PLACES = [
|
|
454
|
+
".eslint-config-snapshot.js",
|
|
455
|
+
".eslint-config-snapshot.cjs",
|
|
456
|
+
".eslint-config-snapshot.mjs",
|
|
457
|
+
"eslint-config-snapshot.config.js",
|
|
458
|
+
"eslint-config-snapshot.config.cjs",
|
|
459
|
+
"eslint-config-snapshot.config.mjs",
|
|
460
|
+
"package.json",
|
|
461
|
+
".eslint-config-snapshotrc",
|
|
462
|
+
".eslint-config-snapshotrc.json",
|
|
463
|
+
".eslint-config-snapshotrc.yaml",
|
|
464
|
+
".eslint-config-snapshotrc.yml",
|
|
465
|
+
".eslint-config-snapshotrc.js",
|
|
466
|
+
".eslint-config-snapshotrc.cjs",
|
|
467
|
+
".eslint-config-snapshotrc.mjs"
|
|
468
|
+
];
|
|
469
|
+
async function loadConfig(cwd) {
|
|
470
|
+
const found = await findConfigPath(cwd);
|
|
471
|
+
if (!found) {
|
|
472
|
+
return DEFAULT_CONFIG;
|
|
473
|
+
}
|
|
474
|
+
return found.config;
|
|
475
|
+
}
|
|
476
|
+
async function findConfigPath(cwd) {
|
|
477
|
+
const root = path4.resolve(cwd ?? process.cwd());
|
|
478
|
+
const explorer = cosmiconfig("eslint-config-snapshot", {
|
|
479
|
+
searchPlaces: SPEC_SEARCH_PLACES,
|
|
480
|
+
stopDir: root
|
|
481
|
+
});
|
|
482
|
+
const result = await explorer.search(root);
|
|
483
|
+
if (!result) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
const maybeConfig = await loadUserConfig(result.config);
|
|
487
|
+
const config = {
|
|
488
|
+
...DEFAULT_CONFIG,
|
|
489
|
+
...maybeConfig,
|
|
490
|
+
grouping: {
|
|
491
|
+
...DEFAULT_CONFIG.grouping,
|
|
492
|
+
...maybeConfig.grouping
|
|
493
|
+
},
|
|
494
|
+
sampling: {
|
|
495
|
+
...DEFAULT_CONFIG.sampling,
|
|
496
|
+
...maybeConfig.sampling
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
return {
|
|
500
|
+
path: result.filepath,
|
|
501
|
+
config
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
async function loadUserConfig(rawConfig) {
|
|
505
|
+
const resolved = typeof rawConfig === "function" ? await rawConfig() : rawConfig;
|
|
506
|
+
if (resolved === null || resolved === void 0) {
|
|
507
|
+
return {};
|
|
508
|
+
}
|
|
509
|
+
if (typeof resolved !== "object" || Array.isArray(resolved)) {
|
|
510
|
+
throw new TypeError("Invalid config export: expected object, function, or async function returning an object");
|
|
511
|
+
}
|
|
512
|
+
return resolved;
|
|
513
|
+
}
|
|
514
|
+
function getConfigScaffold(preset = "minimal") {
|
|
515
|
+
if (preset === "minimal") {
|
|
516
|
+
return "export default {}\n";
|
|
517
|
+
}
|
|
518
|
+
return `export default {
|
|
519
|
+
workspaceInput: { mode: 'discover' },
|
|
520
|
+
grouping: {
|
|
521
|
+
mode: 'match',
|
|
522
|
+
groups: [{ name: 'default', match: ['**/*'] }]
|
|
523
|
+
},
|
|
524
|
+
sampling: {
|
|
525
|
+
maxFilesPerWorkspace: 8,
|
|
526
|
+
includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'],
|
|
527
|
+
excludeGlobs: ['**/node_modules/**', '**/dist/**'],
|
|
528
|
+
hintGlobs: []
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
`;
|
|
532
|
+
}
|
|
533
|
+
export {
|
|
534
|
+
DEFAULT_CONFIG,
|
|
535
|
+
aggregateRules,
|
|
536
|
+
assignGroupsByMatch,
|
|
537
|
+
buildSnapshot,
|
|
538
|
+
canonicalizeJson,
|
|
539
|
+
compareSeverity,
|
|
540
|
+
diffSnapshots,
|
|
541
|
+
discoverWorkspaces,
|
|
542
|
+
extractRulesFromPrintConfig,
|
|
543
|
+
findConfigPath,
|
|
544
|
+
getConfigScaffold,
|
|
545
|
+
hasDiff,
|
|
546
|
+
loadConfig,
|
|
547
|
+
normalizePath,
|
|
548
|
+
normalizeSeverity,
|
|
549
|
+
readSnapshotFile,
|
|
550
|
+
resolveEslintBinForWorkspace,
|
|
551
|
+
sampleWorkspaceFiles,
|
|
552
|
+
sortUnique,
|
|
553
|
+
writeSnapshotFile
|
|
554
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eslint-config-snapshot/api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=20.0.0"
|
|
7
|
+
},
|
|
8
|
+
"main": "dist/index.cjs",
|
|
9
|
+
"module": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@manypkg/get-packages": "^3.1.0",
|
|
20
|
+
"cosmiconfig": "^9.0.0",
|
|
21
|
+
"fast-glob": "^3.3.3",
|
|
22
|
+
"picomatch": "^4.0.3"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "api",
|
|
3
|
+
"sourceRoot": "packages/api/src",
|
|
4
|
+
"targets": {
|
|
5
|
+
"build": {
|
|
6
|
+
"executor": "nx:run-commands",
|
|
7
|
+
"options": {
|
|
8
|
+
"cwd": "packages/api",
|
|
9
|
+
"command": "pnpm tsup src/index.ts --format esm,cjs --out-dir dist --clean"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"typecheck": {
|
|
13
|
+
"executor": "nx:run-commands",
|
|
14
|
+
"options": {
|
|
15
|
+
"cwd": "packages/api",
|
|
16
|
+
"command": "pnpm tsc -p tsconfig.json --noEmit"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"lint": {
|
|
20
|
+
"executor": "nx:run-commands",
|
|
21
|
+
"options": {
|
|
22
|
+
"cwd": "packages/api",
|
|
23
|
+
"command": "pnpm eslint ."
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"test": {
|
|
27
|
+
"executor": "nx:run-commands",
|
|
28
|
+
"options": {
|
|
29
|
+
"cwd": "packages/api",
|
|
30
|
+
"command": "pnpm vitest run"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|