@santi020k/eslint-config-basic 2.1.1 → 3.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/CHANGELOG.md +83 -0
- package/dist/agent-skill-generator.d.ts +11 -2
- package/dist/agent-skill-generator.js +59 -8
- package/dist/cli-advanced.d.ts +23 -0
- package/dist/cli-advanced.js +217 -0
- package/dist/cli-migration.d.ts +26 -0
- package/dist/cli-migration.js +782 -0
- package/dist/cli-workflows.d.ts +63 -0
- package/dist/cli-workflows.js +444 -0
- package/dist/cli.d.ts +5 -4
- package/dist/cli.js +539 -88
- package/dist/frameworks.d.ts +1 -25
- package/dist/frameworks.js +2 -23
- package/dist/index.d.ts +4 -10
- package/dist/index.js +160 -262
- package/dist/integrations.d.ts +6 -1
- package/dist/integrations.js +44 -1
- package/dist/recommended.d.ts +5 -0
- package/dist/recommended.js +5 -0
- package/dist/resolvers.js +1 -6
- package/dist/tailwind.d.ts +6 -0
- package/dist/tailwind.js +25 -0
- package/package.json +137 -31
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
interface CommandResult {
|
|
2
|
+
status: number;
|
|
3
|
+
stderr: string;
|
|
4
|
+
stdout: string;
|
|
5
|
+
}
|
|
6
|
+
type CommandRunner = (executable: string, args: string[], cwd: string) => CommandResult;
|
|
7
|
+
interface BaselineOptions {
|
|
8
|
+
json?: boolean;
|
|
9
|
+
preset?: string;
|
|
10
|
+
prune?: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface ProfileOptions {
|
|
13
|
+
concurrency?: string;
|
|
14
|
+
files?: string[];
|
|
15
|
+
json?: boolean;
|
|
16
|
+
maxDurationMs?: number;
|
|
17
|
+
maxRuleTimeMs?: number;
|
|
18
|
+
maxWarnings?: number;
|
|
19
|
+
}
|
|
20
|
+
interface SnapshotOptions {
|
|
21
|
+
check?: boolean;
|
|
22
|
+
files?: string[];
|
|
23
|
+
json?: boolean;
|
|
24
|
+
snapshotPath?: string;
|
|
25
|
+
}
|
|
26
|
+
interface SnapshotDiff {
|
|
27
|
+
added: string[];
|
|
28
|
+
changed: {
|
|
29
|
+
after: unknown;
|
|
30
|
+
before: unknown;
|
|
31
|
+
rule: string;
|
|
32
|
+
}[];
|
|
33
|
+
file: string;
|
|
34
|
+
removed: string[];
|
|
35
|
+
}
|
|
36
|
+
interface EslintSnapshotFile {
|
|
37
|
+
globals: Record<string, unknown>;
|
|
38
|
+
languageOptions: {
|
|
39
|
+
ecmaVersion?: unknown;
|
|
40
|
+
sourceType?: unknown;
|
|
41
|
+
};
|
|
42
|
+
plugins: string[];
|
|
43
|
+
rules: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
interface EslintSnapshot {
|
|
46
|
+
files: Record<string, EslintSnapshotFile>;
|
|
47
|
+
version: 1;
|
|
48
|
+
}
|
|
49
|
+
interface ProjectEslint {
|
|
50
|
+
calculateConfigForFile: (filePath: string) => Promise<unknown>;
|
|
51
|
+
isPathIgnored?: (filePath: string) => Promise<boolean>;
|
|
52
|
+
}
|
|
53
|
+
declare const defaultCommandRunner: CommandRunner;
|
|
54
|
+
declare const handleBaseline: (cwd?: string, options?: BaselineOptions, runner?: CommandRunner) => void;
|
|
55
|
+
declare const handleProfile: (cwd?: string, options?: ProfileOptions, runner?: CommandRunner) => void;
|
|
56
|
+
declare const findRepresentativeFiles: (cwd: string) => string[];
|
|
57
|
+
declare const createConfigSnapshot: (cwd: string, files?: string[], eslint?: ProjectEslint) => Promise<EslintSnapshot>;
|
|
58
|
+
declare const diffConfigSnapshots: (previous: EslintSnapshot, current: EslintSnapshot) => SnapshotDiff[];
|
|
59
|
+
declare const handleSnapshot: (cwd?: string, options?: SnapshotOptions, eslint?: ProjectEslint) => Promise<void>;
|
|
60
|
+
declare const handleSnapshotDiff: (cwd?: string, options?: SnapshotOptions, eslint?: ProjectEslint) => Promise<void>;
|
|
61
|
+
declare const isDirectory: (path: string) => boolean;
|
|
62
|
+
|
|
63
|
+
export { type BaselineOptions, type CommandResult, type CommandRunner, type ProfileOptions, type SnapshotDiff, type SnapshotOptions, createConfigSnapshot, defaultCommandRunner, diffConfigSnapshots, findRepresentativeFiles, handleBaseline, handleProfile, handleSnapshot, handleSnapshotDiff, isDirectory };
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
5
|
+
const SNAPSHOT_FILENAME = ".eslint-config-snapshot.json";
|
|
6
|
+
const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
7
|
+
".astro",
|
|
8
|
+
".cjs",
|
|
9
|
+
".css",
|
|
10
|
+
".cts",
|
|
11
|
+
".gql",
|
|
12
|
+
".graphql",
|
|
13
|
+
".html",
|
|
14
|
+
".js",
|
|
15
|
+
".json",
|
|
16
|
+
".json5",
|
|
17
|
+
".jsonc",
|
|
18
|
+
".jsx",
|
|
19
|
+
".md",
|
|
20
|
+
".mdx",
|
|
21
|
+
".mjs",
|
|
22
|
+
".mts",
|
|
23
|
+
".svelte",
|
|
24
|
+
".toml",
|
|
25
|
+
".ts",
|
|
26
|
+
".tsx",
|
|
27
|
+
".vue",
|
|
28
|
+
".yaml",
|
|
29
|
+
".yml"
|
|
30
|
+
]);
|
|
31
|
+
const IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
32
|
+
".git",
|
|
33
|
+
".next",
|
|
34
|
+
".nuxt",
|
|
35
|
+
".output",
|
|
36
|
+
".turbo",
|
|
37
|
+
"build",
|
|
38
|
+
"coverage",
|
|
39
|
+
"dist",
|
|
40
|
+
"node_modules"
|
|
41
|
+
]);
|
|
42
|
+
const defaultCommandRunner = (executable, args, cwd) => {
|
|
43
|
+
const result = spawnSync(executable, args, {
|
|
44
|
+
cwd,
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
env: process.env,
|
|
47
|
+
maxBuffer: 50 * 1024 * 1024
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
status: result.status ?? 1,
|
|
51
|
+
stderr: result.stderr,
|
|
52
|
+
stdout: result.stdout
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const readJson = (filePath) => {
|
|
56
|
+
if (!existsSync(filePath)) return null;
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const resolveProjectRequire = (cwd) => createRequire(join(cwd, "package.json"));
|
|
64
|
+
const resolveEslintCli = (cwd) => {
|
|
65
|
+
const projectRequire = resolveProjectRequire(cwd);
|
|
66
|
+
const packagePath = projectRequire.resolve("eslint/package.json");
|
|
67
|
+
const metadata = readJson(packagePath);
|
|
68
|
+
const bin = typeof metadata?.bin === "string" ? metadata.bin : metadata?.bin?.eslint;
|
|
69
|
+
if (!bin) throw new Error("The project ESLint CLI could not be resolved. Install eslint before running this command.");
|
|
70
|
+
return resolve(dirname(packagePath), bin);
|
|
71
|
+
};
|
|
72
|
+
const runEslint = (cwd, args, runner) => runner(process.execPath, [resolveEslintCli(cwd), ...args], cwd);
|
|
73
|
+
const outputCommandFailure = (label, result) => {
|
|
74
|
+
const details = result.stderr.trim() || result.stdout.trim() || `exit code ${result.status}`;
|
|
75
|
+
throw new Error(`${label} failed: ${details}`);
|
|
76
|
+
};
|
|
77
|
+
const createBackupPath = (filePath, suffix) => {
|
|
78
|
+
const preferred = `${filePath}.${suffix}.bak`;
|
|
79
|
+
if (!existsSync(preferred)) return preferred;
|
|
80
|
+
let index = 2;
|
|
81
|
+
while (existsSync(`${preferred}.${index}`)) index++;
|
|
82
|
+
return `${preferred}.${index}`;
|
|
83
|
+
};
|
|
84
|
+
const updateConfigStrictPreset = (cwd, preset) => {
|
|
85
|
+
if (!["ci", "pedantic"].includes(preset)) {
|
|
86
|
+
throw new Error(`Unsupported baseline preset "${preset}". Use "ci" or "pedantic".`);
|
|
87
|
+
}
|
|
88
|
+
const configPath = [
|
|
89
|
+
"eslint.config.js",
|
|
90
|
+
"eslint.config.mjs",
|
|
91
|
+
"eslint.config.cjs",
|
|
92
|
+
"eslint.config.ts",
|
|
93
|
+
"eslint.config.mts",
|
|
94
|
+
"eslint.config.cts"
|
|
95
|
+
].map((name) => join(cwd, name)).find((path) => existsSync(path));
|
|
96
|
+
if (!configPath) throw new Error("No eslint.config.* file was found. Run basic-eslint init first.");
|
|
97
|
+
const content = readFileSync(configPath, "utf8");
|
|
98
|
+
if (new RegExp(`strict\\s*:\\s*['"]${preset}['"]`).test(content)) return null;
|
|
99
|
+
const zeroArgumentPattern = /\bdefineConfig\(\s*\)/;
|
|
100
|
+
if (!zeroArgumentPattern.test(content)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Cannot safely add strict: "${preset}" to a customized config. Add it manually, then run basic-eslint baseline without --preset.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const backupPath = createBackupPath(configPath, "baseline");
|
|
106
|
+
writeFileSync(backupPath, content);
|
|
107
|
+
writeFileSync(configPath, content.replace(zeroArgumentPattern, `defineConfig({ strict: '${preset}' })`));
|
|
108
|
+
return basename(backupPath);
|
|
109
|
+
};
|
|
110
|
+
const handleBaseline = (cwd = process.cwd(), options = {}, runner = defaultCommandRunner) => {
|
|
111
|
+
const backup = options.preset ? updateConfigStrictPreset(cwd, options.preset) : null;
|
|
112
|
+
const args = options.prune ? [".", "--prune-suppressions"] : [".", "--fix", "--suppress-all"];
|
|
113
|
+
const result = runEslint(cwd, args, runner);
|
|
114
|
+
if (result.status !== 0) outputCommandFailure("ESLint baseline", result);
|
|
115
|
+
const payload = {
|
|
116
|
+
action: options.prune ? "pruned" : "created",
|
|
117
|
+
backup,
|
|
118
|
+
preset: options.preset ?? null,
|
|
119
|
+
suppressionsFile: join(cwd, "eslint-suppressions.json")
|
|
120
|
+
};
|
|
121
|
+
if (options.json) {
|
|
122
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
console.log(options.prune ? "\u2705 Pruned unused ESLint suppressions." : "\u2705 Created or refreshed eslint-suppressions.json for existing violations.");
|
|
126
|
+
if (backup) console.log(`\u{1F4E6} Backed up the previous config to ${backup}.`);
|
|
127
|
+
};
|
|
128
|
+
const getStatsPasses = (result) => result.stats?.times?.passes ?? [];
|
|
129
|
+
const summarizeProfileResults = (concurrency, results, durationMs, status) => {
|
|
130
|
+
const ruleTimes = /* @__PURE__ */ new Map();
|
|
131
|
+
let parseMs = 0;
|
|
132
|
+
let ruleMs = 0;
|
|
133
|
+
for (const result of results) {
|
|
134
|
+
for (const pass of getStatsPasses(result)) {
|
|
135
|
+
parseMs += pass.parse?.total ?? 0;
|
|
136
|
+
for (const [rule, timing] of Object.entries(pass.rules ?? {})) {
|
|
137
|
+
const time = timing.total ?? 0;
|
|
138
|
+
ruleMs += time;
|
|
139
|
+
ruleTimes.set(rule, (ruleTimes.get(rule) ?? 0) + time);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
concurrency,
|
|
145
|
+
durationMs,
|
|
146
|
+
errorCount: results.reduce((total, result) => total + (result.errorCount ?? 0), 0),
|
|
147
|
+
fatalErrorCount: results.reduce((total, result) => total + (result.fatalErrorCount ?? 0), 0),
|
|
148
|
+
fileCount: results.length,
|
|
149
|
+
parseMs,
|
|
150
|
+
ruleMs,
|
|
151
|
+
slowestRules: [...ruleTimes.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15).map(([rule, timeMs]) => ({ rule, timeMs })),
|
|
152
|
+
status,
|
|
153
|
+
warningCount: results.reduce((total, result) => total + (result.warningCount ?? 0), 0)
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
const runProfile = (cwd, files, concurrency, runner) => {
|
|
157
|
+
const startedAt = performance.now();
|
|
158
|
+
const result = runEslint(
|
|
159
|
+
cwd,
|
|
160
|
+
[...files, "--stats", "--format", "json", "--concurrency", concurrency],
|
|
161
|
+
runner
|
|
162
|
+
);
|
|
163
|
+
const durationMs = performance.now() - startedAt;
|
|
164
|
+
let parsed = [];
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(result.stdout);
|
|
167
|
+
} catch {
|
|
168
|
+
outputCommandFailure("ESLint profile", result);
|
|
169
|
+
}
|
|
170
|
+
return summarizeProfileResults(concurrency, parsed, durationMs, result.status);
|
|
171
|
+
};
|
|
172
|
+
const round = (value) => Math.round(value * 100) / 100;
|
|
173
|
+
const handleProfile = (cwd = process.cwd(), options = {}, runner = defaultCommandRunner) => {
|
|
174
|
+
for (const [name, value] of [
|
|
175
|
+
["max-duration", options.maxDurationMs],
|
|
176
|
+
["max-rule-time", options.maxRuleTimeMs],
|
|
177
|
+
["max-warnings", options.maxWarnings]
|
|
178
|
+
]) {
|
|
179
|
+
if (value !== void 0 && (!Number.isFinite(value) || value < 0)) {
|
|
180
|
+
throw new Error(`--${name} must be a non-negative number.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const files = options.files?.length ? options.files : ["."];
|
|
184
|
+
const concurrencyModes = options.concurrency ? [options.concurrency] : ["off", "auto"];
|
|
185
|
+
const runs = concurrencyModes.map((concurrency) => runProfile(cwd, files, concurrency, runner));
|
|
186
|
+
const successfulRuns = runs.filter((run) => run.fatalErrorCount === 0 && run.status < 2);
|
|
187
|
+
const recommendation = successfulRuns.length > 0 ? successfulRuns.toSorted((a, b) => a.durationMs - b.durationMs)[0].concurrency : null;
|
|
188
|
+
const budgetRun = successfulRuns.toSorted((a, b) => a.durationMs - b.durationMs)[0] ?? runs[0];
|
|
189
|
+
const violations = successfulRuns.length === 0 ? ["ESLint profiling did not produce a successful run."] : [];
|
|
190
|
+
if (options.maxDurationMs !== void 0 && budgetRun.durationMs > options.maxDurationMs) {
|
|
191
|
+
violations.push(
|
|
192
|
+
`Duration ${round(budgetRun.durationMs)}ms exceeds the ${options.maxDurationMs}ms budget.`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (options.maxWarnings !== void 0 && budgetRun.warningCount > options.maxWarnings) {
|
|
196
|
+
violations.push(
|
|
197
|
+
`Warnings ${budgetRun.warningCount} exceed the ${options.maxWarnings} warning budget.`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const slowestRule = budgetRun.slowestRules.at(0);
|
|
201
|
+
if (options.maxRuleTimeMs !== void 0 && slowestRule && slowestRule.timeMs > options.maxRuleTimeMs) {
|
|
202
|
+
violations.push(
|
|
203
|
+
`${slowestRule.rule} took ${round(slowestRule.timeMs)}ms, exceeding the ${options.maxRuleTimeMs}ms rule budget.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const payload = {
|
|
207
|
+
budget: {
|
|
208
|
+
limits: {
|
|
209
|
+
maxDurationMs: options.maxDurationMs ?? null,
|
|
210
|
+
maxRuleTimeMs: options.maxRuleTimeMs ?? null,
|
|
211
|
+
maxWarnings: options.maxWarnings ?? null
|
|
212
|
+
},
|
|
213
|
+
passed: violations.length === 0,
|
|
214
|
+
violations
|
|
215
|
+
},
|
|
216
|
+
recommendation,
|
|
217
|
+
runs: runs.map((run) => ({
|
|
218
|
+
...run,
|
|
219
|
+
durationMs: round(run.durationMs),
|
|
220
|
+
parseMs: round(run.parseMs),
|
|
221
|
+
ruleMs: round(run.ruleMs),
|
|
222
|
+
slowestRules: run.slowestRules.map((item) => ({ ...item, timeMs: round(item.timeMs) }))
|
|
223
|
+
}))
|
|
224
|
+
};
|
|
225
|
+
if (options.json) {
|
|
226
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
227
|
+
if (!payload.budget.passed) process.exitCode = 1;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
console.log("ESLint performance profile:");
|
|
231
|
+
for (const run of payload.runs) {
|
|
232
|
+
console.log(
|
|
233
|
+
`- concurrency=${run.concurrency}: ${run.durationMs}ms, ${run.fileCount} files, ${run.errorCount} errors, ${run.warningCount} warnings`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (payload.recommendation) console.log(`- Recommended concurrency: ${payload.recommendation}`);
|
|
237
|
+
if (!payload.budget.passed) {
|
|
238
|
+
console.log("Budget violations:");
|
|
239
|
+
for (const violation of payload.budget.violations) console.log(`- ${violation}`);
|
|
240
|
+
process.exitCode = 1;
|
|
241
|
+
}
|
|
242
|
+
const slowestRules = payload.runs[0]?.slowestRules ?? [];
|
|
243
|
+
if (slowestRules.length > 0) {
|
|
244
|
+
console.log("Slowest rules:");
|
|
245
|
+
for (const item of slowestRules.slice(0, 10)) {
|
|
246
|
+
console.log(`- ${item.rule}: ${item.timeMs}ms`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const walkSourceFiles = (cwd, directory = cwd, output = []) => {
|
|
251
|
+
if (output.length >= 5e3) return output;
|
|
252
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
253
|
+
if (output.length >= 5e3) break;
|
|
254
|
+
if (entry.isDirectory()) {
|
|
255
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
256
|
+
walkSourceFiles(cwd, join(directory, entry.name), output);
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (SOURCE_EXTENSIONS.has(extname(entry.name))) {
|
|
261
|
+
output.push(relative(cwd, join(directory, entry.name)));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return output;
|
|
265
|
+
};
|
|
266
|
+
const classifyRepresentativeFile = (filePath) => {
|
|
267
|
+
if (/eslint\.config\.[cm]?[jt]s$/.test(filePath)) return "config";
|
|
268
|
+
if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(filePath)) return "test";
|
|
269
|
+
return extname(filePath);
|
|
270
|
+
};
|
|
271
|
+
const findRepresentativeFiles = (cwd) => {
|
|
272
|
+
const selected = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const filePath of walkSourceFiles(cwd)) {
|
|
274
|
+
const category = classifyRepresentativeFile(filePath);
|
|
275
|
+
if (!selected.has(category)) selected.set(category, filePath);
|
|
276
|
+
}
|
|
277
|
+
return [...selected.values()].sort();
|
|
278
|
+
};
|
|
279
|
+
const findUnignoredRepresentativeFiles = async (cwd, eslint) => {
|
|
280
|
+
const selected = /* @__PURE__ */ new Map();
|
|
281
|
+
for (const filePath of walkSourceFiles(cwd)) {
|
|
282
|
+
const category = classifyRepresentativeFile(filePath);
|
|
283
|
+
if (selected.has(category)) continue;
|
|
284
|
+
const config = eslint.isPathIgnored ? void 0 : await eslint.calculateConfigForFile(filePath);
|
|
285
|
+
const ignored = eslint.isPathIgnored ? await eslint.isPathIgnored(filePath) : config === null || config === void 0;
|
|
286
|
+
if (!ignored) selected.set(category, filePath);
|
|
287
|
+
}
|
|
288
|
+
return [...selected.values()].sort();
|
|
289
|
+
};
|
|
290
|
+
const loadProjectEslint = (cwd) => {
|
|
291
|
+
const projectRequire = resolveProjectRequire(cwd);
|
|
292
|
+
const eslintModule = projectRequire("eslint");
|
|
293
|
+
if (!eslintModule.ESLint) throw new Error("The installed eslint package does not expose the ESLint API.");
|
|
294
|
+
return new eslintModule.ESLint({ cwd });
|
|
295
|
+
};
|
|
296
|
+
const normalizeSerializableObject = (value) => {
|
|
297
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
298
|
+
return Object.fromEntries(
|
|
299
|
+
Object.entries(value).filter(([, entry]) => {
|
|
300
|
+
try {
|
|
301
|
+
JSON.stringify(entry);
|
|
302
|
+
return true;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}).sort(([a], [b]) => a.localeCompare(b))
|
|
307
|
+
);
|
|
308
|
+
};
|
|
309
|
+
const getObjectKeys = (value) => {
|
|
310
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
311
|
+
return Object.keys(value).sort();
|
|
312
|
+
};
|
|
313
|
+
const normalizeConfig = (config) => {
|
|
314
|
+
const resolved = config && typeof config === "object" ? config : {};
|
|
315
|
+
return {
|
|
316
|
+
globals: normalizeSerializableObject(resolved.languageOptions?.globals),
|
|
317
|
+
languageOptions: {
|
|
318
|
+
ecmaVersion: resolved.languageOptions?.ecmaVersion,
|
|
319
|
+
sourceType: resolved.languageOptions?.sourceType
|
|
320
|
+
},
|
|
321
|
+
plugins: getObjectKeys(resolved.plugins),
|
|
322
|
+
rules: normalizeSerializableObject(resolved.rules)
|
|
323
|
+
};
|
|
324
|
+
};
|
|
325
|
+
const createConfigSnapshot = async (cwd, files, eslint = loadProjectEslint(cwd)) => {
|
|
326
|
+
const representativeFiles = files?.length ? files : await findUnignoredRepresentativeFiles(cwd, eslint);
|
|
327
|
+
if (representativeFiles.length === 0) {
|
|
328
|
+
throw new Error("No representative source files were found. Pass one or more --file paths.");
|
|
329
|
+
}
|
|
330
|
+
const entries = await Promise.all(representativeFiles.map(async (filePath) => {
|
|
331
|
+
const config = await eslint.calculateConfigForFile(filePath);
|
|
332
|
+
return [filePath, normalizeConfig(config)];
|
|
333
|
+
}));
|
|
334
|
+
return {
|
|
335
|
+
files: Object.fromEntries(entries),
|
|
336
|
+
version: 1
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
const sameValue = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
340
|
+
const getSnapshotValues = (snapshot) => {
|
|
341
|
+
if (!snapshot) return {};
|
|
342
|
+
return {
|
|
343
|
+
...snapshot.rules,
|
|
344
|
+
...Object.fromEntries(
|
|
345
|
+
Object.entries(snapshot.globals).map(([name, value]) => [`globals:${name}`, value])
|
|
346
|
+
),
|
|
347
|
+
...Object.fromEntries(
|
|
348
|
+
Object.entries(snapshot.languageOptions).filter(([, value]) => value !== void 0).map(([name, value]) => [`languageOptions:${name}`, value])
|
|
349
|
+
),
|
|
350
|
+
...Object.fromEntries(snapshot.plugins.map((name) => [`plugins:${name}`, true]))
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
const diffConfigSnapshots = (previous, current) => {
|
|
354
|
+
const files = /* @__PURE__ */ new Set([...Object.keys(previous.files), ...Object.keys(current.files)]);
|
|
355
|
+
const diffs = [];
|
|
356
|
+
for (const file of [...files].sort()) {
|
|
357
|
+
const before = getSnapshotValues(previous.files[file]);
|
|
358
|
+
const after = getSnapshotValues(current.files[file]);
|
|
359
|
+
const added = Object.keys(after).filter((rule) => !(rule in before)).sort();
|
|
360
|
+
const removed = Object.keys(before).filter((rule) => !(rule in after)).sort();
|
|
361
|
+
const changed = Object.keys(after).filter((rule) => rule in before && !sameValue(before[rule], after[rule])).sort().map((rule) => ({ after: after[rule], before: before[rule], rule }));
|
|
362
|
+
if (added.length > 0 || removed.length > 0 || changed.length > 0) {
|
|
363
|
+
diffs.push({ added, changed, file, removed });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return diffs;
|
|
367
|
+
};
|
|
368
|
+
const outputSnapshotDiff = (diffs) => {
|
|
369
|
+
if (diffs.length === 0) {
|
|
370
|
+
console.log("\u2705 Effective ESLint configuration matches the saved snapshot.");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
console.log(`ESLint configuration changed for ${diffs.length} representative file(s):`);
|
|
374
|
+
for (const diff of diffs) {
|
|
375
|
+
console.log(`- ${diff.file}`);
|
|
376
|
+
if (diff.added.length > 0) console.log(` Added: ${diff.added.join(", ")}`);
|
|
377
|
+
if (diff.removed.length > 0) console.log(` Removed: ${diff.removed.join(", ")}`);
|
|
378
|
+
for (const change of diff.changed) {
|
|
379
|
+
console.log(` Changed ${change.rule}: ${JSON.stringify(change.before)} \u2192 ${JSON.stringify(change.after)}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
const handleSnapshot = async (cwd = process.cwd(), options = {}, eslint) => {
|
|
384
|
+
const snapshotPath = resolve(cwd, options.snapshotPath ?? SNAPSHOT_FILENAME);
|
|
385
|
+
const previous = readJson(snapshotPath);
|
|
386
|
+
if (options.check) {
|
|
387
|
+
if (!previous) {
|
|
388
|
+
if (options.json) {
|
|
389
|
+
console.log(JSON.stringify({ exists: false, snapshotPath }, null, 2));
|
|
390
|
+
} else {
|
|
391
|
+
console.error(`\u274C Snapshot is missing: ${relative(cwd, snapshotPath)}`);
|
|
392
|
+
}
|
|
393
|
+
process.exitCode = 1;
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const files = options.files?.length ? options.files : Object.keys(previous.files);
|
|
397
|
+
const current2 = await createConfigSnapshot(cwd, files, eslint);
|
|
398
|
+
const diffs = diffConfigSnapshots(previous, current2);
|
|
399
|
+
if (options.json) console.log(JSON.stringify({ diffs, snapshotPath }, null, 2));
|
|
400
|
+
else outputSnapshotDiff(diffs);
|
|
401
|
+
if (diffs.length > 0) process.exitCode = 1;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const current = await createConfigSnapshot(cwd, options.files, eslint);
|
|
405
|
+
writeFileSync(snapshotPath, `${JSON.stringify(current, null, 2)}
|
|
406
|
+
`);
|
|
407
|
+
if (options.json) {
|
|
408
|
+
console.log(JSON.stringify({
|
|
409
|
+
files: Object.keys(current.files),
|
|
410
|
+
snapshotPath
|
|
411
|
+
}, null, 2));
|
|
412
|
+
} else {
|
|
413
|
+
console.log(`\u2705 Saved effective ESLint configuration to ${relative(cwd, snapshotPath)}.`);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
const handleSnapshotDiff = async (cwd = process.cwd(), options = {}, eslint) => {
|
|
417
|
+
const snapshotPath = resolve(cwd, options.snapshotPath ?? SNAPSHOT_FILENAME);
|
|
418
|
+
const previous = readJson(snapshotPath);
|
|
419
|
+
if (!previous) throw new Error(`Snapshot is missing: ${relative(cwd, snapshotPath)}. Run basic-eslint snapshot first.`);
|
|
420
|
+
const files = options.files?.length ? options.files : Object.keys(previous.files);
|
|
421
|
+
const current = await createConfigSnapshot(cwd, files, eslint);
|
|
422
|
+
const diffs = diffConfigSnapshots(previous, current);
|
|
423
|
+
if (options.json) console.log(JSON.stringify({ diffs, snapshotPath }, null, 2));
|
|
424
|
+
else outputSnapshotDiff(diffs);
|
|
425
|
+
if (diffs.length > 0) process.exitCode = 1;
|
|
426
|
+
};
|
|
427
|
+
const isDirectory = (path) => {
|
|
428
|
+
try {
|
|
429
|
+
return statSync(path).isDirectory();
|
|
430
|
+
} catch {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
export {
|
|
435
|
+
createConfigSnapshot,
|
|
436
|
+
defaultCommandRunner,
|
|
437
|
+
diffConfigSnapshots,
|
|
438
|
+
findRepresentativeFiles,
|
|
439
|
+
handleBaseline,
|
|
440
|
+
handleProfile,
|
|
441
|
+
handleSnapshot,
|
|
442
|
+
handleSnapshotDiff,
|
|
443
|
+
isDirectory
|
|
444
|
+
};
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
declare const handleInit: (cwd?: string, check?: boolean) => void;
|
|
1
|
+
declare const handleInit: (cwd?: string, check?: boolean, explicit?: boolean) => void;
|
|
2
2
|
declare const handleUpdate: (cwd?: string) => void;
|
|
3
3
|
declare const handleExplain: (cwd?: string, json?: boolean) => void;
|
|
4
4
|
declare const handleInspect: (cwd?: string, json?: boolean) => Promise<void>;
|
|
@@ -14,9 +14,10 @@ interface DuplicateEslintInfo {
|
|
|
14
14
|
* between editor and CLI.
|
|
15
15
|
*/
|
|
16
16
|
declare const findDuplicateEslint: (cwd?: string) => DuplicateEslintInfo | null;
|
|
17
|
-
declare const handleDoctor: (cwd?: string, json?: boolean, liteInstall?: boolean) => Promise<void>;
|
|
17
|
+
declare const handleDoctor: (cwd?: string, json?: boolean, liteInstall?: boolean, fix?: boolean) => Promise<void>;
|
|
18
|
+
declare const handleInstall: (cwd?: string, dryRun?: boolean) => void;
|
|
18
19
|
declare const handleDocs: (cwd?: string) => void;
|
|
19
|
-
declare const handleMigrate: (cwd?: string, write?: boolean, json?: boolean) => void;
|
|
20
|
+
declare const handleMigrate: (cwd?: string, write?: boolean, json?: boolean, target?: string, check?: boolean, full?: boolean) => void;
|
|
20
21
|
declare const runCli: (argv?: string[], cwd?: string) => void;
|
|
21
22
|
|
|
22
|
-
export { findDuplicateEslint, handleDocs, handleDoctor, handleExplain, handleInit, handleInspect, handleMigrate, handleUpdate, runCli };
|
|
23
|
+
export { findDuplicateEslint, handleDocs, handleDoctor, handleExplain, handleInit, handleInspect, handleInstall, handleMigrate, handleUpdate, runCli };
|