@santi020k/eslint-config-basic 3.1.0 → 3.2.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 +26 -0
- package/IMPROVEMENTS.md +270 -0
- package/dist/cli-migration.js +367 -15
- package/dist/cli-preset.d.ts +31 -0
- package/dist/cli-preset.js +315 -0
- package/dist/cli.d.ts +13 -1
- package/dist/cli.js +359 -49
- package/dist/define-config-metadata.d.ts +10 -0
- package/dist/define-config-metadata.js +18 -0
- package/dist/frameworks.js +4 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +63 -6
- package/dist/integrations.js +3 -1
- package/dist/optional-package-errors.d.ts +3 -0
- package/dist/optional-package-errors.js +14 -0
- package/package.json +8 -7
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createRequire, findPackageJSON } from "node:module";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
detectProjectOptions,
|
|
7
|
+
Preset
|
|
8
|
+
} from "@santi020k/eslint-config-core";
|
|
9
|
+
import { getDefineConfigMetadata } from "./define-config-metadata.js";
|
|
10
|
+
import { defineConfig } from "./index.js";
|
|
11
|
+
import { isMissingRequestedPackage } from "./optional-package-errors.js";
|
|
12
|
+
import { resolvePreset } from "./resolvers.js";
|
|
13
|
+
const PRESETS = new Set(Object.values(Preset));
|
|
14
|
+
const CONFIG_FILENAMES = [
|
|
15
|
+
"eslint.config.js",
|
|
16
|
+
"eslint.config.mjs",
|
|
17
|
+
"eslint.config.cjs",
|
|
18
|
+
"eslint.config.ts",
|
|
19
|
+
"eslint.config.mts",
|
|
20
|
+
"eslint.config.cts"
|
|
21
|
+
];
|
|
22
|
+
const loadDefineConfigMetadata = async (cwd) => {
|
|
23
|
+
const configPath = CONFIG_FILENAMES.map((name) => join(cwd, name)).find((path) => existsSync(path));
|
|
24
|
+
if (!configPath) return void 0;
|
|
25
|
+
try {
|
|
26
|
+
const loaded = await import(`${pathToFileURL(configPath).href}?basic-eslint-preset=${Date.now()}`);
|
|
27
|
+
return getDefineConfigMetadata(await loaded.default);
|
|
28
|
+
} catch {
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const PRESET_FEATURE_PACKAGES = {
|
|
33
|
+
extensions: "@santi020k/eslint-config-extensions",
|
|
34
|
+
formats: "@santi020k/eslint-config-formats",
|
|
35
|
+
libraries: "@santi020k/eslint-config-libraries",
|
|
36
|
+
testing: "@santi020k/eslint-config-testing",
|
|
37
|
+
tools: "@santi020k/eslint-config-tools"
|
|
38
|
+
};
|
|
39
|
+
const FRAMEWORK_PACKAGES = {
|
|
40
|
+
angular: "@santi020k/eslint-config-angular",
|
|
41
|
+
astro: "@santi020k/eslint-config-astro",
|
|
42
|
+
expo: "@santi020k/eslint-config-expo",
|
|
43
|
+
hono: "@santi020k/eslint-config-hono",
|
|
44
|
+
lit: "@santi020k/eslint-config-lit",
|
|
45
|
+
nest: "@santi020k/eslint-config-nest",
|
|
46
|
+
next: "@santi020k/eslint-config-next",
|
|
47
|
+
nuxt: "@santi020k/eslint-config-nuxt",
|
|
48
|
+
preact: "@santi020k/eslint-config-preact",
|
|
49
|
+
qwik: "@santi020k/eslint-config-qwik",
|
|
50
|
+
react: "@santi020k/eslint-config-react",
|
|
51
|
+
"react-router": "@santi020k/eslint-config-react-router",
|
|
52
|
+
slidev: "@santi020k/eslint-config-slidev",
|
|
53
|
+
solid: "@santi020k/eslint-config-solid",
|
|
54
|
+
svelte: "@santi020k/eslint-config-svelte",
|
|
55
|
+
"tanstack-start": "@santi020k/eslint-config-tanstack-start",
|
|
56
|
+
vite: "@santi020k/eslint-config-vite",
|
|
57
|
+
vue: "@santi020k/eslint-config-vue"
|
|
58
|
+
};
|
|
59
|
+
const FRAMEWORK_PREFIXES = /* @__PURE__ */ new Set([
|
|
60
|
+
"@angular-eslint",
|
|
61
|
+
"@eslint-react",
|
|
62
|
+
"@next/next",
|
|
63
|
+
"astro",
|
|
64
|
+
"lit",
|
|
65
|
+
"qwik",
|
|
66
|
+
"react",
|
|
67
|
+
"react-hooks",
|
|
68
|
+
"react-refresh",
|
|
69
|
+
"solid",
|
|
70
|
+
"svelte",
|
|
71
|
+
"vue"
|
|
72
|
+
]);
|
|
73
|
+
const DOMAIN_PREFIXES = /* @__PURE__ */ new Set([
|
|
74
|
+
"better-tailwindcss",
|
|
75
|
+
"drizzle",
|
|
76
|
+
"graphql-eslint",
|
|
77
|
+
"i18next",
|
|
78
|
+
"prisma",
|
|
79
|
+
"storybook",
|
|
80
|
+
"tailwindcss",
|
|
81
|
+
"tanstack-query",
|
|
82
|
+
"turbo",
|
|
83
|
+
"zod"
|
|
84
|
+
]);
|
|
85
|
+
const FORMAT_PREFIXES = /* @__PURE__ */ new Set([
|
|
86
|
+
"@stylistic",
|
|
87
|
+
"css",
|
|
88
|
+
"html",
|
|
89
|
+
"jsonc",
|
|
90
|
+
"markdown",
|
|
91
|
+
"mdx",
|
|
92
|
+
"prettier",
|
|
93
|
+
"toml",
|
|
94
|
+
"yaml"
|
|
95
|
+
]);
|
|
96
|
+
const MULTI_SEGMENT_RULE_PREFIXES = /* @__PURE__ */ new Set(["@next/next"]);
|
|
97
|
+
const getRulePrefix = (rule) => {
|
|
98
|
+
const segments = rule.split("/");
|
|
99
|
+
if (!rule.startsWith("@")) return segments[0];
|
|
100
|
+
const multiSegmentPrefix = segments.slice(0, 2).join("/");
|
|
101
|
+
return MULTI_SEGMENT_RULE_PREFIXES.has(multiSegmentPrefix) ? multiSegmentPrefix : segments[0];
|
|
102
|
+
};
|
|
103
|
+
const getRuleGroup = (rule) => {
|
|
104
|
+
const prefix = getRulePrefix(rule);
|
|
105
|
+
if (prefix === "security" || prefix === "no-secrets" || rule.includes("security") || rule.includes("no-unsanitized")) return "security";
|
|
106
|
+
if (FRAMEWORK_PREFIXES.has(prefix)) return "framework";
|
|
107
|
+
if (DOMAIN_PREFIXES.has(prefix)) return "domain";
|
|
108
|
+
if (FORMAT_PREFIXES.has(prefix) || rule.includes("format") || rule.includes("spacing")) return "formatting";
|
|
109
|
+
return "correctness";
|
|
110
|
+
};
|
|
111
|
+
const getSeverity = (value) => Array.isArray(value) ? value[0] : value;
|
|
112
|
+
const isEnabled = (value) => {
|
|
113
|
+
const severity = getSeverity(value);
|
|
114
|
+
return severity !== 0 && severity !== "off" && severity !== void 0;
|
|
115
|
+
};
|
|
116
|
+
const normalizeRule = (value) => JSON.stringify(value);
|
|
117
|
+
const loadProjectEslint = (cwd, overrideConfig) => {
|
|
118
|
+
const projectRequire = createRequire(join(cwd, "package.json"));
|
|
119
|
+
const eslintModule = projectRequire("eslint");
|
|
120
|
+
if (!eslintModule.ESLint) throw new Error("The installed eslint package does not expose the ESLint API.");
|
|
121
|
+
return new eslintModule.ESLint({
|
|
122
|
+
cwd,
|
|
123
|
+
...overrideConfig ? { overrideConfig, overrideConfigFile: true } : {}
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
const getResolvablePresetOptions = (cwd, presetOptions) => {
|
|
127
|
+
const projectPackageUrl = pathToFileURL(join(cwd, "package.json"));
|
|
128
|
+
const options = { ...presetOptions };
|
|
129
|
+
const detectedOptions = detectProjectOptions(cwd);
|
|
130
|
+
const missingPackages = /* @__PURE__ */ new Set();
|
|
131
|
+
const detection = options.detection;
|
|
132
|
+
const detectionControls = typeof detection === "object" ? detection : {};
|
|
133
|
+
const detectionEnabled = (category) => detection !== false && detectionControls[category] !== false;
|
|
134
|
+
for (const [category, specifier] of Object.entries(PRESET_FEATURE_PACKAGES)) {
|
|
135
|
+
const categoryKey = category;
|
|
136
|
+
const explicit = options[categoryKey];
|
|
137
|
+
const detected = detectionEnabled(categoryKey) ? detectedOptions[categoryKey] : [];
|
|
138
|
+
const selected = [.../* @__PURE__ */ new Set([
|
|
139
|
+
...Array.isArray(explicit) ? explicit : [],
|
|
140
|
+
...Array.isArray(detected) ? detected : []
|
|
141
|
+
])];
|
|
142
|
+
if (selected.length === 0) continue;
|
|
143
|
+
try {
|
|
144
|
+
findPackageJSON(specifier, projectPackageUrl);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (!isMissingRequestedPackage(error, specifier)) throw error;
|
|
147
|
+
Object.assign(options, {
|
|
148
|
+
[category]: [],
|
|
149
|
+
detection: {
|
|
150
|
+
...typeof options.detection === "object" ? options.detection : detectionControls,
|
|
151
|
+
[category]: false
|
|
152
|
+
},
|
|
153
|
+
features: {
|
|
154
|
+
...options.features,
|
|
155
|
+
...Object.fromEntries(selected.map((feature) => [feature, false]))
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
missingPackages.add(specifier);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const frameworkDetectionEnabled = detection !== false && detectionControls.frameworks !== false;
|
|
162
|
+
const detectedFrameworks = frameworkDetectionEnabled ? detectedOptions.detectedFrameworks ?? [] : [];
|
|
163
|
+
const configuredFrameworks = Object.entries(options.frameworks ?? {}).filter((entry) => entry[1] === true).map(([framework]) => framework);
|
|
164
|
+
const selectedFrameworks = [.../* @__PURE__ */ new Set([...detectedFrameworks, ...configuredFrameworks])];
|
|
165
|
+
const resolvableFrameworks = {
|
|
166
|
+
...options.frameworks
|
|
167
|
+
};
|
|
168
|
+
for (const framework of selectedFrameworks) {
|
|
169
|
+
const specifier = FRAMEWORK_PACKAGES[framework];
|
|
170
|
+
try {
|
|
171
|
+
findPackageJSON(specifier, projectPackageUrl);
|
|
172
|
+
resolvableFrameworks[framework] = true;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (!isMissingRequestedPackage(error, specifier)) throw error;
|
|
175
|
+
Reflect.deleteProperty(resolvableFrameworks, framework);
|
|
176
|
+
missingPackages.add(specifier);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!resolvableFrameworks.react) {
|
|
180
|
+
delete resolvableFrameworks.expo;
|
|
181
|
+
delete resolvableFrameworks.next;
|
|
182
|
+
delete resolvableFrameworks["react-router"];
|
|
183
|
+
if (!resolvableFrameworks.solid) delete resolvableFrameworks["tanstack-start"];
|
|
184
|
+
}
|
|
185
|
+
if (!resolvableFrameworks.vue) {
|
|
186
|
+
delete resolvableFrameworks.nuxt;
|
|
187
|
+
delete resolvableFrameworks.slidev;
|
|
188
|
+
}
|
|
189
|
+
if (selectedFrameworks.length > 0) {
|
|
190
|
+
options.autoFrameworks = false;
|
|
191
|
+
options.frameworks = resolvableFrameworks;
|
|
192
|
+
}
|
|
193
|
+
return { missingPackages: [...missingPackages], options };
|
|
194
|
+
};
|
|
195
|
+
const createCompatibilityConfig = (preset, addedRules, changedRules) => [
|
|
196
|
+
`// Temporary compatibility override generated for Preset.${preset.charAt(0).toUpperCase()}${preset.slice(1)}.`,
|
|
197
|
+
"// Remove entries as the project adopts the preset findings.",
|
|
198
|
+
"export default {",
|
|
199
|
+
` name: 'eslint-config-basic/preset-${preset}-compatibility',`,
|
|
200
|
+
" rules: {",
|
|
201
|
+
...Object.keys(addedRules).sort().map((rule) => ` ${JSON.stringify(rule)}: 'off',`),
|
|
202
|
+
...Object.keys(changedRules).sort().map((rule) => ` ${JSON.stringify(rule)}: ${JSON.stringify(changedRules[rule].from)},`),
|
|
203
|
+
" },",
|
|
204
|
+
"}",
|
|
205
|
+
""
|
|
206
|
+
].join("\n");
|
|
207
|
+
const createPresetReport = async (cwd, presetName, file = "eslint.config.js") => {
|
|
208
|
+
const normalizedPreset = presetName.toLowerCase();
|
|
209
|
+
if (!PRESETS.has(normalizedPreset)) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Unknown preset "${presetName}". Use one of: ${[...PRESETS].sort().join(", ")}.`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const preset = normalizedPreset;
|
|
215
|
+
const presetOptions = resolvePreset(preset);
|
|
216
|
+
const current = await loadProjectEslint(cwd).calculateConfigForFile(file);
|
|
217
|
+
const currentMetadata = await loadDefineConfigMetadata(cwd);
|
|
218
|
+
const currentOptions = currentMetadata?.options ?? {};
|
|
219
|
+
const targetOptions = {
|
|
220
|
+
...presetOptions,
|
|
221
|
+
...currentOptions,
|
|
222
|
+
features: {
|
|
223
|
+
...presetOptions.features,
|
|
224
|
+
...currentOptions.features
|
|
225
|
+
},
|
|
226
|
+
frameworks: {
|
|
227
|
+
...presetOptions.frameworks,
|
|
228
|
+
...currentOptions.frameworks
|
|
229
|
+
},
|
|
230
|
+
preset,
|
|
231
|
+
root: currentOptions.root ?? cwd
|
|
232
|
+
};
|
|
233
|
+
const { missingPackages, options: resolvablePresetOptions } = getResolvablePresetOptions(cwd, targetOptions);
|
|
234
|
+
if (!current) {
|
|
235
|
+
throw new Error(`ESLint did not calculate a current configuration for ${file}. Check that the file is not ignored.`);
|
|
236
|
+
}
|
|
237
|
+
const selectedConfig = await defineConfig(
|
|
238
|
+
resolvablePresetOptions,
|
|
239
|
+
...currentMetadata?.extraConfigs ?? []
|
|
240
|
+
);
|
|
241
|
+
const selected = await loadProjectEslint(cwd, selectedConfig).calculateConfigForFile(file);
|
|
242
|
+
if (!selected) {
|
|
243
|
+
throw new Error(`The ${normalizedPreset} preset did not produce a configuration for ${file}.`);
|
|
244
|
+
}
|
|
245
|
+
const currentRules = current.rules ?? {};
|
|
246
|
+
const selectedRules = selected.rules ?? {};
|
|
247
|
+
const added = {};
|
|
248
|
+
const changed = {};
|
|
249
|
+
const removed = {};
|
|
250
|
+
for (const [rule, value] of Object.entries(selectedRules)) {
|
|
251
|
+
const currentValue = currentRules[rule];
|
|
252
|
+
if (isEnabled(value) && !isEnabled(currentValue)) {
|
|
253
|
+
added[rule] = value;
|
|
254
|
+
} else if (isEnabled(value) && isEnabled(currentValue) && normalizeRule(value) !== normalizeRule(currentValue)) {
|
|
255
|
+
changed[rule] = { from: currentValue, to: value };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const [rule, value] of Object.entries(currentRules)) {
|
|
259
|
+
if (isEnabled(value) && !isEnabled(selectedRules[rule])) removed[rule] = value;
|
|
260
|
+
}
|
|
261
|
+
const groups = Object.fromEntries(
|
|
262
|
+
["formatting", "correctness", "security", "framework", "domain"].map((group) => [
|
|
263
|
+
group,
|
|
264
|
+
Object.keys(added).filter((rule) => getRuleGroup(rule) === group).sort()
|
|
265
|
+
])
|
|
266
|
+
);
|
|
267
|
+
return {
|
|
268
|
+
added,
|
|
269
|
+
changed,
|
|
270
|
+
file,
|
|
271
|
+
groups,
|
|
272
|
+
missingPackages,
|
|
273
|
+
preset: normalizedPreset,
|
|
274
|
+
presetOptions,
|
|
275
|
+
removed,
|
|
276
|
+
totals: {
|
|
277
|
+
added: Object.keys(added).length,
|
|
278
|
+
changed: Object.keys(changed).length,
|
|
279
|
+
removed: Object.keys(removed).length
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
const handleExplainPreset = async (cwd = process.cwd(), preset, options = {}) => {
|
|
284
|
+
const report = await createPresetReport(cwd, preset, options.file);
|
|
285
|
+
let compatibilityFile = null;
|
|
286
|
+
if (options.compatibility) {
|
|
287
|
+
const output = options.output ?? `.eslint-preset-${report.preset}-compat.mjs`;
|
|
288
|
+
const outputPath = join(cwd, output);
|
|
289
|
+
if (existsSync(outputPath)) {
|
|
290
|
+
throw new Error(`${output} already exists; remove it or choose another path with --output.`);
|
|
291
|
+
}
|
|
292
|
+
writeFileSync(outputPath, createCompatibilityConfig(report.preset, report.added, report.changed));
|
|
293
|
+
compatibilityFile = relative(cwd, outputPath);
|
|
294
|
+
}
|
|
295
|
+
const payload = { ...report, compatibilityFile };
|
|
296
|
+
if (options.json) {
|
|
297
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
console.log([
|
|
301
|
+
`Preset adoption report: ${report.preset}`,
|
|
302
|
+
`- Representative file: ${report.file}`,
|
|
303
|
+
`- Newly enabled rules: ${report.totals.added}`,
|
|
304
|
+
`- Changed rule options: ${report.totals.changed}`,
|
|
305
|
+
`- Rules no longer enabled: ${report.totals.removed}`,
|
|
306
|
+
`- Optional packs not installed (excluded from comparison): ${report.missingPackages.length > 0 ? report.missingPackages.join(", ") : "none"}`,
|
|
307
|
+
"",
|
|
308
|
+
...Object.entries(report.groups).map(([group, rules]) => `- ${group}: ${rules.length}${rules.length > 0 ? ` (${rules.join(", ")})` : ""}`),
|
|
309
|
+
...compatibilityFile ? ["", `Compatibility override written to ${compatibilityFile}. Import it after the preset config.`] : ["", "Pass --compatibility to write a temporary override that disables newly enabled rules."]
|
|
310
|
+
].join("\n"));
|
|
311
|
+
};
|
|
312
|
+
export {
|
|
313
|
+
createPresetReport,
|
|
314
|
+
handleExplainPreset
|
|
315
|
+
};
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
interface InstallResult {
|
|
2
|
+
error?: Error;
|
|
3
|
+
status: null | number;
|
|
4
|
+
stderr?: Buffer | string;
|
|
5
|
+
stdout?: Buffer | string;
|
|
6
|
+
}
|
|
7
|
+
type InstallRunner = (command: string, args: string[], options: {
|
|
8
|
+
cwd: string;
|
|
9
|
+
encoding: 'utf8';
|
|
10
|
+
maxBuffer: number;
|
|
11
|
+
stdio: 'pipe';
|
|
12
|
+
}) => InstallResult;
|
|
1
13
|
declare const handleInit: (cwd?: string, check?: boolean, explicit?: boolean) => void;
|
|
2
14
|
declare const handleUpdate: (cwd?: string) => void;
|
|
3
15
|
declare const handleExplain: (cwd?: string, json?: boolean) => void;
|
|
@@ -15,7 +27,7 @@ interface DuplicateEslintInfo {
|
|
|
15
27
|
*/
|
|
16
28
|
declare const findDuplicateEslint: (cwd?: string) => DuplicateEslintInfo | null;
|
|
17
29
|
declare const handleDoctor: (cwd?: string, json?: boolean, liteInstall?: boolean, fix?: boolean) => Promise<void>;
|
|
18
|
-
declare const handleInstall: (cwd?: string, dryRun?: boolean) => void;
|
|
30
|
+
declare const handleInstall: (cwd?: string, dryRun?: boolean, runner?: InstallRunner) => void;
|
|
19
31
|
declare const handleDocs: (cwd?: string) => void;
|
|
20
32
|
declare const handleMigrate: (cwd?: string, write?: boolean, json?: boolean, target?: string, check?: boolean, full?: boolean) => void;
|
|
21
33
|
declare const runCli: (argv?: string[], cwd?: string) => void;
|