@isentinel/eslint-config 6.1.0 → 6.3.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/README.md +106 -0
- package/dist/cli.mjs +231 -30
- package/dist/index.d.mts +55 -34
- package/dist/index.mjs +873 -209
- package/dist/lint-cli.mjs +234 -114
- package/dist/oxfmt.d.mts +132 -0
- package/dist/oxfmt.mjs +750 -0
- package/dist/oxlint.d.mts +14 -0
- package/dist/oxlint.mjs +417 -63
- package/package.json +8 -3
package/dist/oxfmt.mjs
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { getStaticJSONValue, parseJSON } from "jsonc-eslint-parser";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { getStaticYAMLValue, parseYAML } from "yaml-eslint-parser";
|
|
7
|
+
const GLOB_JS = "**/*.{,c,m}js";
|
|
8
|
+
const GLOB_JSX = "**/*.{,c,m}jsx";
|
|
9
|
+
const GLOB_TS = "**/*.{,c,m}ts";
|
|
10
|
+
const GLOB_TSX = "**/*.{,c,m}tsx";
|
|
11
|
+
const GLOB_CSS = "**/*.css";
|
|
12
|
+
const GLOB_POSTCSS = "**/*.{p,post}css";
|
|
13
|
+
const GLOB_LESS = "**/*.less";
|
|
14
|
+
const GLOB_SCSS = "**/*.scss";
|
|
15
|
+
const GLOB_JSON5 = "**/*.json5";
|
|
16
|
+
const GLOB_ALL_JSON = "**/*.json{,5,c}";
|
|
17
|
+
const GLOB_MARKDOWN = "**/*.md";
|
|
18
|
+
const GLOB_YAML = "**/*.y{,a}ml";
|
|
19
|
+
const GLOB_EXCLUDE = [
|
|
20
|
+
"**/.pnpm-store",
|
|
21
|
+
"**/bun.lockb",
|
|
22
|
+
"**/dist",
|
|
23
|
+
"**/node_modules",
|
|
24
|
+
"**/package-lock.json",
|
|
25
|
+
"**/pnpm-lock.yaml",
|
|
26
|
+
"**/yarn.lock",
|
|
27
|
+
"**/skills-lock.json",
|
|
28
|
+
"**/.cache",
|
|
29
|
+
"**/.changeset",
|
|
30
|
+
"**/.agents",
|
|
31
|
+
"**/.claude/**/*",
|
|
32
|
+
"!**/.claude/settings.json",
|
|
33
|
+
"**/.gemini",
|
|
34
|
+
"**/.kilocode",
|
|
35
|
+
"**/.opencode",
|
|
36
|
+
"**/.history",
|
|
37
|
+
"**/.idea",
|
|
38
|
+
"**/.next",
|
|
39
|
+
"**/.nuxt",
|
|
40
|
+
"**/.output",
|
|
41
|
+
"**/.svelte-kit",
|
|
42
|
+
"**/.temp",
|
|
43
|
+
"**/.tmp",
|
|
44
|
+
"**/.vercel",
|
|
45
|
+
"**/.vite-inspect",
|
|
46
|
+
"**/.vitepress/cache",
|
|
47
|
+
"**/.worktree",
|
|
48
|
+
"**/.yarn",
|
|
49
|
+
"**/coverage",
|
|
50
|
+
"**/out",
|
|
51
|
+
"**/output",
|
|
52
|
+
"**/temp",
|
|
53
|
+
"**/tmp",
|
|
54
|
+
"**/vite.config.*.timestamp-*",
|
|
55
|
+
"**/eslint-suppressions.json",
|
|
56
|
+
"**/*.min.*",
|
|
57
|
+
"**/CHANGELOG*.md",
|
|
58
|
+
"**/LICENSE*",
|
|
59
|
+
"**/__snapshots__",
|
|
60
|
+
"**/auto-import{,s}.d.ts",
|
|
61
|
+
"**/components.d.ts",
|
|
62
|
+
"**/roblox.yml",
|
|
63
|
+
"**/.scratch/**",
|
|
64
|
+
"**/scratch/**"
|
|
65
|
+
];
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/formatted-files.ts
|
|
68
|
+
/**
|
|
69
|
+
* The files the preset formats with oxfmt. ESLint mode enables `oxfmt/oxfmt`
|
|
70
|
+
* on these globs; the standalone oxfmt config ignores every other file. One
|
|
71
|
+
* list keeps both modes formatting the same files.
|
|
72
|
+
*
|
|
73
|
+
* Imported by the standalone oxfmt config, so keep it light.
|
|
74
|
+
*/
|
|
75
|
+
/** Formatted globs, by `oxfmt/oxfmt` config block. */
|
|
76
|
+
const FORMATTED_FILES = {
|
|
77
|
+
css: [GLOB_CSS, GLOB_POSTCSS],
|
|
78
|
+
graphql: ["**/*.graphql"],
|
|
79
|
+
html: ["**/*.html"],
|
|
80
|
+
javascript: [GLOB_JS, GLOB_JSX],
|
|
81
|
+
json: [GLOB_ALL_JSON],
|
|
82
|
+
less: [GLOB_LESS],
|
|
83
|
+
markdown: [GLOB_MARKDOWN],
|
|
84
|
+
scss: [GLOB_SCSS],
|
|
85
|
+
typescript: [GLOB_TS, GLOB_TSX],
|
|
86
|
+
yaml: [GLOB_YAML]
|
|
87
|
+
};
|
|
88
|
+
/** The blocks `formatters` turns off, with the key that does it. */
|
|
89
|
+
const LANGUAGE_BLOCKS = [
|
|
90
|
+
["css", "css"],
|
|
91
|
+
["scss", "css"],
|
|
92
|
+
["less", "css"],
|
|
93
|
+
["html", "html"],
|
|
94
|
+
["markdown", "markdown"],
|
|
95
|
+
["graphql", "graphql"],
|
|
96
|
+
["json", "json"],
|
|
97
|
+
["yaml", "yaml"]
|
|
98
|
+
];
|
|
99
|
+
/**
|
|
100
|
+
* Resolve `formatters` to one flag per language.
|
|
101
|
+
*
|
|
102
|
+
* @param formatters - The `formatters` option.
|
|
103
|
+
* @returns Whether each language is formatted.
|
|
104
|
+
*/
|
|
105
|
+
function resolveFormattedLanguages(formatters = true) {
|
|
106
|
+
const enabled = formatters !== false;
|
|
107
|
+
const toggles = typeof formatters === "object" ? formatters : {};
|
|
108
|
+
return {
|
|
109
|
+
css: enabled && toggles.css !== false,
|
|
110
|
+
graphql: enabled && toggles.graphql !== false,
|
|
111
|
+
html: enabled && toggles.html !== false,
|
|
112
|
+
json: enabled && toggles.json !== false,
|
|
113
|
+
markdown: enabled && toggles.markdown !== false,
|
|
114
|
+
yaml: enabled && toggles.yaml !== false
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Every glob the preset formats.
|
|
119
|
+
*
|
|
120
|
+
* @param options - Component extensions and language toggles.
|
|
121
|
+
* @returns The formatted globs.
|
|
122
|
+
*/
|
|
123
|
+
function formattedFiles({ componentExts: componentExtensions = [], formatters = true } = {}) {
|
|
124
|
+
const languages = resolveFormattedLanguages(formatters);
|
|
125
|
+
return [
|
|
126
|
+
...FORMATTED_FILES.javascript,
|
|
127
|
+
...FORMATTED_FILES.typescript,
|
|
128
|
+
...componentExtensions.map((extension) => `**/*.${extension}`),
|
|
129
|
+
...LANGUAGE_BLOCKS.filter(([, language]) => languages[language]).flatMap(([block]) => {
|
|
130
|
+
return FORMATTED_FILES[block];
|
|
131
|
+
})
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Expand the brace sets in a glob (`*.{,c,m}js` to `*.js`, `*.cjs`, `*.mjs`).
|
|
136
|
+
*
|
|
137
|
+
* @param glob - A glob.
|
|
138
|
+
* @returns The globs it stands for.
|
|
139
|
+
*/
|
|
140
|
+
function expandBraces(glob) {
|
|
141
|
+
const open = glob.indexOf("{");
|
|
142
|
+
if (open === -1) return [glob];
|
|
143
|
+
let depth = 0;
|
|
144
|
+
let close = -1;
|
|
145
|
+
const commas = [];
|
|
146
|
+
for (let index = open; index < glob.length; index++) {
|
|
147
|
+
const character = glob[index];
|
|
148
|
+
if (character === "{") depth++;
|
|
149
|
+
else if (character === "}") {
|
|
150
|
+
depth--;
|
|
151
|
+
if (depth === 0) {
|
|
152
|
+
close = index;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
} else if (character === "," && depth === 1) commas.push(index);
|
|
156
|
+
}
|
|
157
|
+
if (close === -1) return [glob];
|
|
158
|
+
const bounds = [
|
|
159
|
+
open,
|
|
160
|
+
...commas,
|
|
161
|
+
close
|
|
162
|
+
];
|
|
163
|
+
const alternatives = bounds.slice(0, -1).map((start, index) => {
|
|
164
|
+
return glob.slice(start + 1, bounds[index + 1]);
|
|
165
|
+
});
|
|
166
|
+
const prefix = glob.slice(0, open);
|
|
167
|
+
const suffix = glob.slice(close + 1);
|
|
168
|
+
return alternatives.flatMap((alternative) => expandBraces(`${prefix}${alternative}${suffix}`));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Oxfmt ignore patterns that leave only the formatted files: ignore
|
|
172
|
+
* everything, then re-include directories and each formatted glob. Ignore
|
|
173
|
+
* files take no brace sets, so those are expanded.
|
|
174
|
+
*
|
|
175
|
+
* @param options - Component extensions and language toggles.
|
|
176
|
+
* @returns Gitignore-style patterns.
|
|
177
|
+
*/
|
|
178
|
+
function unformattedFilePatterns(options = {}) {
|
|
179
|
+
const included = formattedFiles(options).flatMap((glob) => expandBraces(glob));
|
|
180
|
+
return [
|
|
181
|
+
"*",
|
|
182
|
+
"!*/",
|
|
183
|
+
...Array.from(new Set(included), (glob) => `!${glob}`)
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/prettier-config.ts
|
|
188
|
+
/**
|
|
189
|
+
* Synchronous resolution of the effective Prettier-shaped formatting settings
|
|
190
|
+
* (`printWidth`, `tabWidth`, `useTabs`, ...) shared by both factories.
|
|
191
|
+
*
|
|
192
|
+
* These settings do not only drive formatting: `printWidth` and `tabWidth`
|
|
193
|
+
* feed rule options such as `flawless/arrow-return-style`'s `maxLen`,
|
|
194
|
+
* `tabWidth` and `useOxfmt.printWidth`. The ESLint and oxlint factories must
|
|
195
|
+
* therefore resolve them identically, or the same rule reaches opposite
|
|
196
|
+
* conclusions in each engine and `--fix` ping-pongs the code between both
|
|
197
|
+
* forms.
|
|
198
|
+
*
|
|
199
|
+
* Prettier's own `resolveConfig` is async-only since v3, and the oxlint
|
|
200
|
+
* factory is synchronous, so resolution is reimplemented here against the
|
|
201
|
+
* config formats that can be read synchronously. TypeScript config files
|
|
202
|
+
* (`prettier.config.ts` and friends) need a loader and are skipped.
|
|
203
|
+
*/
|
|
204
|
+
/** Preset defaults, overridden by any project-level configuration. */
|
|
205
|
+
const PRETTIER_DEFAULTS = {
|
|
206
|
+
arrowParens: "always",
|
|
207
|
+
printWidth: 100,
|
|
208
|
+
quoteProps: "consistent",
|
|
209
|
+
semi: true,
|
|
210
|
+
singleQuote: false,
|
|
211
|
+
tabWidth: 4,
|
|
212
|
+
trailingComma: "all",
|
|
213
|
+
useTabs: true
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Config file names searched in each directory, in Prettier's own precedence
|
|
217
|
+
* order. `package.json` is handled separately (it only counts when it carries
|
|
218
|
+
* a `prettier` key).
|
|
219
|
+
*/
|
|
220
|
+
const CONFIG_FILES = [
|
|
221
|
+
".prettierrc",
|
|
222
|
+
".prettierrc.json",
|
|
223
|
+
".prettierrc.jsonc",
|
|
224
|
+
".prettierrc.json5",
|
|
225
|
+
".prettierrc.yaml",
|
|
226
|
+
".prettierrc.yml",
|
|
227
|
+
".prettierrc.js",
|
|
228
|
+
".prettierrc.cjs",
|
|
229
|
+
".prettierrc.mjs",
|
|
230
|
+
"prettier.config.js",
|
|
231
|
+
"prettier.config.cjs",
|
|
232
|
+
"prettier.config.mjs"
|
|
233
|
+
];
|
|
234
|
+
/**
|
|
235
|
+
* EditorConfig section matched against a representative source file rather
|
|
236
|
+
* than `package.json`: the settings feed JS/TS rule options, so per-extension
|
|
237
|
+
* sections must resolve the way they do for the files those rules lint.
|
|
238
|
+
*/
|
|
239
|
+
const EDITORCONFIG_PROBE_FILE = "index.ts";
|
|
240
|
+
const LINE_BREAK = /\r?\n/;
|
|
241
|
+
const LEADING_SLASH = /^\//;
|
|
242
|
+
const REGEXP_SPECIAL = /[$()*+.?[\\\]^{|}]/g;
|
|
243
|
+
/**
|
|
244
|
+
* Glob constructs expanded by {@link expandGlobToken}; anything else is
|
|
245
|
+
* escaped.
|
|
246
|
+
*/
|
|
247
|
+
const GLOB_TOKEN = /\*\*\/?|\{[^{}]*\}|[$()*+.?[\\\]^{|}]/g;
|
|
248
|
+
/**
|
|
249
|
+
* Resolve the project's Prettier configuration, EditorConfig included, without
|
|
250
|
+
* awaiting.
|
|
251
|
+
*
|
|
252
|
+
* @param cwd - Directory to start the upward search from.
|
|
253
|
+
* @returns The resolved options, or an empty object when none are found.
|
|
254
|
+
*/
|
|
255
|
+
function resolveProjectPrettierConfig(cwd = process.cwd()) {
|
|
256
|
+
return {
|
|
257
|
+
...resolveEditorConfigOptions(cwd),
|
|
258
|
+
...resolvePrettierRcOptions(cwd)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Resolve the effective formatting settings: preset defaults, overridden by
|
|
263
|
+
* the project's EditorConfig and Prettier configuration, overridden by the
|
|
264
|
+
* options passed to the factory.
|
|
265
|
+
*
|
|
266
|
+
* @param prettierOptions - Explicit `formatters.prettierOptions`.
|
|
267
|
+
* @param cwd - Directory to resolve project configuration from.
|
|
268
|
+
* @returns The merged settings.
|
|
269
|
+
*/
|
|
270
|
+
function resolvePrettierSettings(prettierOptions = {}, cwd = process.cwd()) {
|
|
271
|
+
return {
|
|
272
|
+
...PRETTIER_DEFAULTS,
|
|
273
|
+
...resolveProjectPrettierConfig(cwd),
|
|
274
|
+
...prettierOptions
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Turn the nearest `.editorconfig`'s sections into oxfmt overrides, the way
|
|
279
|
+
* the `oxfmt/oxfmt` rule's config loader does: `[*]` is skipped (it feeds the
|
|
280
|
+
* root options), and each other section becomes an override whose globs are
|
|
281
|
+
* relative to `cwd`. A key set in the oxfmt config root wins over the
|
|
282
|
+
* sections.
|
|
283
|
+
*
|
|
284
|
+
* @param cwd - The config directory.
|
|
285
|
+
* @param root - The oxfmt config root options.
|
|
286
|
+
* @returns The overrides, in file order.
|
|
287
|
+
*/
|
|
288
|
+
function editorConfigOverrides(cwd = process.cwd(), root = {}) {
|
|
289
|
+
for (const directory of ancestorDirectories(cwd)) {
|
|
290
|
+
const contents = readFileOrUndefined(path.join(directory, ".editorconfig"));
|
|
291
|
+
if (contents === void 0) continue;
|
|
292
|
+
const prefix = path.relative(cwd, directory).split(path.sep).join("/");
|
|
293
|
+
return parseEditorConfig(contents).sections.flatMap(({ pattern, properties }) => {
|
|
294
|
+
const options = Object.fromEntries(Object.entries(editorConfigToOxfmt(properties)).filter(([key]) => !Object.hasOwn(root, key)));
|
|
295
|
+
if (pattern === "*" || Object.keys(options).length === 0) return [];
|
|
296
|
+
return [{
|
|
297
|
+
files: [prefix === "" ? pattern : `${prefix}/${pattern.replace(LEADING_SLASH, "")}`],
|
|
298
|
+
options
|
|
299
|
+
}];
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
function readFileOrUndefined(filePath) {
|
|
305
|
+
try {
|
|
306
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
307
|
+
} catch {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function parseJsonLike(contents) {
|
|
312
|
+
try {
|
|
313
|
+
return getStaticJSONValue(parseJSON(contents, { jsonSyntax: "json5" }));
|
|
314
|
+
} catch {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function isRecord$1(value) {
|
|
319
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
320
|
+
}
|
|
321
|
+
function readPackageJsonPrettierKey(filePath) {
|
|
322
|
+
const contents = readFileOrUndefined(filePath);
|
|
323
|
+
if (contents === void 0) return;
|
|
324
|
+
const parsed = parseJsonLike(contents);
|
|
325
|
+
const prettierKey = isRecord$1(parsed) ? parsed["prettier"] : void 0;
|
|
326
|
+
return isRecord$1(prettierKey) ? prettierKey : void 0;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Load a JS Prettier config. Node's `require` handles ESM entry points
|
|
330
|
+
* synchronously on the versions this package supports, so both module systems
|
|
331
|
+
* work; a config that cannot be loaded is treated as absent rather than fatal.
|
|
332
|
+
*
|
|
333
|
+
* @param filePath - Absolute path of the config module.
|
|
334
|
+
* @returns The exported options, or `undefined`.
|
|
335
|
+
*/
|
|
336
|
+
function requireConfigModule(filePath) {
|
|
337
|
+
try {
|
|
338
|
+
const loaded = createRequire(filePath)(filePath);
|
|
339
|
+
const value = isRecord$1(loaded) && "default" in loaded ? loaded["default"] : loaded;
|
|
340
|
+
return isRecord$1(value) ? value : void 0;
|
|
341
|
+
} catch {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function parseYamlLike(contents) {
|
|
346
|
+
try {
|
|
347
|
+
return getStaticYAMLValue(parseYAML(contents));
|
|
348
|
+
} catch {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function readConfigFile(filePath) {
|
|
353
|
+
const extension = path.extname(filePath);
|
|
354
|
+
if (extension === ".js" || extension === ".cjs" || extension === ".mjs") return fs.existsSync(filePath) ? requireConfigModule(filePath) : void 0;
|
|
355
|
+
const contents = readFileOrUndefined(filePath);
|
|
356
|
+
if (contents === void 0) return;
|
|
357
|
+
const parsed = parseJsonLike(contents) ?? parseYamlLike(contents);
|
|
358
|
+
return isRecord$1(parsed) ? parsed : void 0;
|
|
359
|
+
}
|
|
360
|
+
function ancestorDirectories(cwd) {
|
|
361
|
+
const directories = [];
|
|
362
|
+
let current = path.resolve(cwd);
|
|
363
|
+
while (true) {
|
|
364
|
+
directories.push(current);
|
|
365
|
+
const parent = path.dirname(current);
|
|
366
|
+
if (parent === current) return directories;
|
|
367
|
+
current = parent;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Walk up from `cwd` looking for a Prettier configuration file.
|
|
372
|
+
*
|
|
373
|
+
* @param cwd - Directory to start the upward search from.
|
|
374
|
+
* @returns The first configuration found, or an empty object.
|
|
375
|
+
*/
|
|
376
|
+
function resolvePrettierRcOptions(cwd) {
|
|
377
|
+
for (const directory of ancestorDirectories(cwd)) {
|
|
378
|
+
const fromPackageJson = readPackageJsonPrettierKey(path.join(directory, "package.json"));
|
|
379
|
+
if (fromPackageJson !== void 0) return fromPackageJson;
|
|
380
|
+
for (const filename of CONFIG_FILES) {
|
|
381
|
+
const config = readConfigFile(path.join(directory, filename));
|
|
382
|
+
if (config !== void 0) return config;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return {};
|
|
386
|
+
}
|
|
387
|
+
function editorConfigToPrettier(properties) {
|
|
388
|
+
const options = {};
|
|
389
|
+
const indentStyle = properties["indent_style"];
|
|
390
|
+
if (indentStyle === "tab" || indentStyle === "space") options.useTabs = indentStyle === "tab";
|
|
391
|
+
const indentSize = properties["indent_size"];
|
|
392
|
+
const width = indentSize === void 0 || indentSize === "tab" ? properties["tab_width"] : indentSize;
|
|
393
|
+
const tabWidth = Number.parseInt(width ?? "", 10);
|
|
394
|
+
if (Number.isFinite(tabWidth)) options.tabWidth = tabWidth;
|
|
395
|
+
const printWidth = Number.parseInt(properties["max_line_length"] ?? "", 10);
|
|
396
|
+
if (Number.isFinite(printWidth)) options.printWidth = printWidth;
|
|
397
|
+
const endOfLine = properties["end_of_line"];
|
|
398
|
+
if (endOfLine === "cr" || endOfLine === "crlf" || endOfLine === "lf") options.endOfLine = endOfLine;
|
|
399
|
+
return options;
|
|
400
|
+
}
|
|
401
|
+
function parseEditorConfig(contents) {
|
|
402
|
+
const sections = [];
|
|
403
|
+
let current;
|
|
404
|
+
let isRoot = false;
|
|
405
|
+
for (const rawLine of contents.split(LINE_BREAK)) {
|
|
406
|
+
const line = rawLine.trim();
|
|
407
|
+
if (line === "" || line.startsWith("#") || line.startsWith(";")) continue;
|
|
408
|
+
if (line.startsWith("[") && line.endsWith("]")) {
|
|
409
|
+
current = {
|
|
410
|
+
pattern: line.slice(1, -1),
|
|
411
|
+
properties: {}
|
|
412
|
+
};
|
|
413
|
+
sections.push(current);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const separator = line.indexOf("=");
|
|
417
|
+
if (separator === -1) continue;
|
|
418
|
+
const key = line.slice(0, separator).trim().toLowerCase();
|
|
419
|
+
const value = line.slice(separator + 1).trim();
|
|
420
|
+
if (current === void 0) {
|
|
421
|
+
isRoot ||= key === "root" && value.toLowerCase() === "true";
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
current.properties[key] = value;
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
isRoot,
|
|
428
|
+
sections
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function escapeRegExp(value) {
|
|
432
|
+
return value.replaceAll(REGEXP_SPECIAL, (character) => `\\${character}`);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Expand one glob token into its regular-expression equivalent.
|
|
436
|
+
*
|
|
437
|
+
* @param token - A token matched by {@link GLOB_TOKEN}.
|
|
438
|
+
* @returns The regular-expression source for the token.
|
|
439
|
+
*/
|
|
440
|
+
function expandGlobToken(token) {
|
|
441
|
+
if (token.startsWith("**")) return "(?:.*)";
|
|
442
|
+
if (token === "*") return "[^/]*";
|
|
443
|
+
if (token === "?") return "[^/]";
|
|
444
|
+
if (token.startsWith("{")) return `(?:${token.slice(1, -1).split(",").map((alternative) => escapeRegExp(alternative)).join("|")})`;
|
|
445
|
+
return `\\${token}`;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Match an EditorConfig section pattern against a path relative to the
|
|
449
|
+
* `.editorconfig` file. Supports the `*`, `**`, `?` and `{a,b}` constructs;
|
|
450
|
+
* numeric ranges and character classes are rare enough in formatting sections
|
|
451
|
+
* to fall through as non-matches.
|
|
452
|
+
*
|
|
453
|
+
* @param pattern - The section pattern.
|
|
454
|
+
* @param relativePath - Path relative to the `.editorconfig` directory.
|
|
455
|
+
* @returns Whether the section applies.
|
|
456
|
+
*/
|
|
457
|
+
function matchesEditorConfigPattern(pattern, relativePath) {
|
|
458
|
+
const normalized = pattern.includes("/") ? pattern.replace(LEADING_SLASH, "") : `**/${pattern}`;
|
|
459
|
+
const subject = relativePath.split(path.sep).join("/");
|
|
460
|
+
const source = normalized.replaceAll(GLOB_TOKEN, (token) => expandGlobToken(token));
|
|
461
|
+
return new RegExp(`^${source}$`).test(subject);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Resolve EditorConfig settings for a representative source file and translate
|
|
465
|
+
* them into their Prettier equivalents.
|
|
466
|
+
*
|
|
467
|
+
* @param cwd - Directory to start the upward search from.
|
|
468
|
+
* @returns The translated options.
|
|
469
|
+
*/
|
|
470
|
+
function resolveEditorConfigOptions(cwd) {
|
|
471
|
+
const properties = {};
|
|
472
|
+
for (const directory of ancestorDirectories(cwd).toReversed()) {
|
|
473
|
+
const contents = readFileOrUndefined(path.join(directory, ".editorconfig"));
|
|
474
|
+
if (contents === void 0) continue;
|
|
475
|
+
const { isRoot, sections } = parseEditorConfig(contents);
|
|
476
|
+
const relative = path.relative(directory, path.join(cwd, EDITORCONFIG_PROBE_FILE));
|
|
477
|
+
const forProbe = sections.filter(({ pattern }) => matchesEditorConfigPattern(pattern, relative)).reduce((accumulator, section) => Object.assign(accumulator, section.properties), {});
|
|
478
|
+
if (isRoot) for (const key of Object.keys(properties)) delete properties[key];
|
|
479
|
+
Object.assign(properties, forProbe);
|
|
480
|
+
}
|
|
481
|
+
return editorConfigToPrettier(properties);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Map one `.editorconfig` section to oxfmt options, as the rule's config
|
|
485
|
+
* loader does.
|
|
486
|
+
*
|
|
487
|
+
* @param properties - The section's properties.
|
|
488
|
+
* @returns The oxfmt options it sets.
|
|
489
|
+
*/
|
|
490
|
+
function editorConfigToOxfmt(properties) {
|
|
491
|
+
const options = {};
|
|
492
|
+
const endOfLine = properties["end_of_line"];
|
|
493
|
+
if (endOfLine === "cr" || endOfLine === "crlf" || endOfLine === "lf") options["endOfLine"] = endOfLine;
|
|
494
|
+
const indentStyle = properties["indent_style"];
|
|
495
|
+
if (indentStyle === "tab" || indentStyle === "space") options["useTabs"] = indentStyle === "tab";
|
|
496
|
+
const quoteType = properties["quote_type"];
|
|
497
|
+
if (quoteType === "single" || quoteType === "double") options["singleQuote"] = quoteType === "single";
|
|
498
|
+
const indentSize = Number(properties["indent_size"]);
|
|
499
|
+
const tabWidth = Number(properties["tab_width"]);
|
|
500
|
+
if (options["useTabs"] === false && properties["indent_size"] && Number.isFinite(indentSize)) options["tabWidth"] = indentSize;
|
|
501
|
+
else if (properties["tab_width"] && Number.isFinite(tabWidth)) options["tabWidth"] = tabWidth;
|
|
502
|
+
const printWidth = Number(properties["max_line_length"]);
|
|
503
|
+
if (properties["max_line_length"] && Number.isFinite(printWidth)) options["printWidth"] = printWidth;
|
|
504
|
+
const finalNewline = properties["insert_final_newline"];
|
|
505
|
+
if (finalNewline === "true" || finalNewline === "false") options["insertFinalNewline"] = finalNewline === "true";
|
|
506
|
+
return options;
|
|
507
|
+
}
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/guards.ts
|
|
510
|
+
/**
|
|
511
|
+
* Internal runtime type guards. Prefer these over `as` assertions so values
|
|
512
|
+
* crossing untyped boundaries (`JSON.parse`, dynamic `import`, plugin objects)
|
|
513
|
+
* are validated at runtime rather than asserted away.
|
|
514
|
+
*/
|
|
515
|
+
/**
|
|
516
|
+
* Whether a value is a non-null, non-array object usable as a string-keyed
|
|
517
|
+
* record. Narrows `unknown` without an assertion.
|
|
518
|
+
*
|
|
519
|
+
* @param value - The value to test.
|
|
520
|
+
* @returns Whether the value is a plain object.
|
|
521
|
+
*/
|
|
522
|
+
function isRecord(value) {
|
|
523
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
524
|
+
}
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/rules/oxfmt.ts
|
|
527
|
+
const UNSUPPORTED_PRETTIER_KEYS = /* @__PURE__ */ new Set([
|
|
528
|
+
"experimentalOperatorPosition",
|
|
529
|
+
"experimentalTernaries",
|
|
530
|
+
"jsdocPreferCodeFences",
|
|
531
|
+
"jsdocPrintWidth",
|
|
532
|
+
"parser",
|
|
533
|
+
"plugins",
|
|
534
|
+
"tsdoc"
|
|
535
|
+
]);
|
|
536
|
+
const defaultSortImports = {
|
|
537
|
+
customGroups: [{
|
|
538
|
+
elementNamePattern: ["react"],
|
|
539
|
+
groupName: "react"
|
|
540
|
+
}, {
|
|
541
|
+
elementNamePattern: ["@*/**"],
|
|
542
|
+
groupName: "scoped"
|
|
543
|
+
}],
|
|
544
|
+
groups: [
|
|
545
|
+
"react",
|
|
546
|
+
"scoped",
|
|
547
|
+
[
|
|
548
|
+
"type-builtin",
|
|
549
|
+
"type-external",
|
|
550
|
+
"builtin",
|
|
551
|
+
"external"
|
|
552
|
+
],
|
|
553
|
+
[
|
|
554
|
+
"type-internal",
|
|
555
|
+
"internal",
|
|
556
|
+
"type-parent",
|
|
557
|
+
"type-sibling",
|
|
558
|
+
"type-index",
|
|
559
|
+
"parent",
|
|
560
|
+
"sibling",
|
|
561
|
+
"index"
|
|
562
|
+
],
|
|
563
|
+
"unknown"
|
|
564
|
+
],
|
|
565
|
+
newlinesBetween: true
|
|
566
|
+
};
|
|
567
|
+
/**
|
|
568
|
+
* Migrate Prettier options to oxfmt options, dropping unsupported keys.
|
|
569
|
+
*
|
|
570
|
+
* @param prettierOptions - The Prettier options to migrate.
|
|
571
|
+
* @returns The migrated oxfmt options.
|
|
572
|
+
*/
|
|
573
|
+
function migratePrettierOptions(prettierOptions) {
|
|
574
|
+
const oxfmtOptions = {};
|
|
575
|
+
for (const [key, value] of Object.entries(prettierOptions)) {
|
|
576
|
+
if (UNSUPPORTED_PRETTIER_KEYS.has(key)) continue;
|
|
577
|
+
if (key === "endOfLine" && value === "auto") continue;
|
|
578
|
+
oxfmtOptions[key] = key === "overrides" ? migratePrettierOverrides(value) : value;
|
|
579
|
+
}
|
|
580
|
+
return oxfmtOptions;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Migrate Prettier `overrides`: oxfmt wants `files` as an array, and the
|
|
584
|
+
* nested options migrated like the root ones.
|
|
585
|
+
*
|
|
586
|
+
* @param overrides - The `overrides` value, of unknown shape.
|
|
587
|
+
* @returns The overrides in oxfmt's shape; empty when not an array.
|
|
588
|
+
*/
|
|
589
|
+
function migratePrettierOverrides(overrides) {
|
|
590
|
+
if (!Array.isArray(overrides)) return [];
|
|
591
|
+
return overrides.flatMap((override) => {
|
|
592
|
+
if (!isRecord(override)) return [];
|
|
593
|
+
const { excludeFiles, files, options } = override;
|
|
594
|
+
return [{
|
|
595
|
+
...excludeFiles === void 0 ? {} : { excludeFiles: stringsOf(excludeFiles) },
|
|
596
|
+
files: stringsOf(files),
|
|
597
|
+
options: isRecord(options) ? migratePrettierOptions(options) : {}
|
|
598
|
+
}];
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Anchor slash-less override globs to the config directory. The oxfmt CLI
|
|
603
|
+
* matches `*.json` at any depth, but the ESLint rule matches it against the
|
|
604
|
+
* path from the config directory, so only top-level files; `./*.json` means
|
|
605
|
+
* the latter to both.
|
|
606
|
+
*
|
|
607
|
+
* @template T - The override shape.
|
|
608
|
+
* @param overrides - Oxfmt overrides.
|
|
609
|
+
* @returns The overrides with anchored globs.
|
|
610
|
+
*/
|
|
611
|
+
function anchorOverrideGlobs(overrides) {
|
|
612
|
+
return overrides.map((override) => {
|
|
613
|
+
return {
|
|
614
|
+
...override,
|
|
615
|
+
...override.excludeFiles ? { excludeFiles: override.excludeFiles.map(anchorGlob) } : {},
|
|
616
|
+
files: override.files.map(anchorGlob)
|
|
617
|
+
};
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Build the effective oxfmt options shared between the ESLint and oxlint
|
|
622
|
+
* factories.
|
|
623
|
+
*
|
|
624
|
+
* @param options - The option sources, in increasing precedence.
|
|
625
|
+
* @returns The effective oxfmt options.
|
|
626
|
+
*/
|
|
627
|
+
function buildOxfmtOptions({ oxfmtConfigOptions = {}, oxfmtOptions, prettierOptions = {} }) {
|
|
628
|
+
return {
|
|
629
|
+
sortImports: defaultSortImports,
|
|
630
|
+
sortPackageJson: false,
|
|
631
|
+
...migratePrettierOptions(prettierOptions),
|
|
632
|
+
...oxfmtConfigOptions,
|
|
633
|
+
...oxfmtOptions
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Markdown formatting options layered over the base options. Shared by the
|
|
638
|
+
* ESLint config and the standalone oxfmt config so both format Markdown the
|
|
639
|
+
* same way.
|
|
640
|
+
*
|
|
641
|
+
* Oxfmt would format code blocks with these options too, at the prose width.
|
|
642
|
+
* It leaves them alone instead, and ESLint formats each block with the options
|
|
643
|
+
* of its own language (see `isentinel/oxfmt/markdown-code/*`).
|
|
644
|
+
*
|
|
645
|
+
* @param jsdocPrintWidth - Width for Markdown prose; 80 when unset.
|
|
646
|
+
* @returns Options applied on top of the base for Markdown files.
|
|
647
|
+
*/
|
|
648
|
+
function markdownOxfmtOptions(jsdocPrintWidth) {
|
|
649
|
+
return {
|
|
650
|
+
embeddedLanguageFormatting: "off",
|
|
651
|
+
printWidth: Number(jsdocPrintWidth) || 80,
|
|
652
|
+
proseWrap: "always",
|
|
653
|
+
tabWidth: 2
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function stringsOf(value) {
|
|
657
|
+
return [value].flat().filter((item) => typeof item === "string");
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* JSON5 formatting options layered over the base options. `jsonc/quote-props`
|
|
661
|
+
* owns the quoting of JSON5 keys; oxfmt's `consistent` would strip the quotes
|
|
662
|
+
* it adds, and `--fix` would loop.
|
|
663
|
+
*/
|
|
664
|
+
const JSON5_OXFMT_OPTIONS = { quoteProps: "preserve" };
|
|
665
|
+
/** YAML formatting options layered over the base options. */
|
|
666
|
+
const YAML_OXFMT_OPTIONS = {
|
|
667
|
+
tabWidth: 2,
|
|
668
|
+
useTabs: false
|
|
669
|
+
};
|
|
670
|
+
/**
|
|
671
|
+
* Anchor one slash-less glob to the config directory, keeping its negation.
|
|
672
|
+
*
|
|
673
|
+
* @param glob - An override glob.
|
|
674
|
+
* @returns The anchored glob.
|
|
675
|
+
*/
|
|
676
|
+
function anchorGlob(glob) {
|
|
677
|
+
const negated = glob.startsWith("!");
|
|
678
|
+
const body = negated ? glob.slice(1) : glob;
|
|
679
|
+
const anchored = body.includes("/") ? body : `./${body}`;
|
|
680
|
+
return negated ? `!${anchored}` : anchored;
|
|
681
|
+
}
|
|
682
|
+
//#endregion
|
|
683
|
+
//#region src/oxfmt/index.ts
|
|
684
|
+
/**
|
|
685
|
+
* Build the preset's oxfmt config.
|
|
686
|
+
*
|
|
687
|
+
* Root format options start from the same defaults the ESLint and oxlint
|
|
688
|
+
* factories use (preset defaults < `.editorconfig` < `.prettierrc`), and every
|
|
689
|
+
* key in `config` replaces its base value. `ignorePatterns` and `overrides`
|
|
690
|
+
* are the exception: the user's entries are appended to the preset's, so a
|
|
691
|
+
* user override wins over the preset's Markdown, YAML and JSON5 overrides.
|
|
692
|
+
* The `.prettierrc` overrides come last, as in ESLint mode. Slash-less
|
|
693
|
+
* override globs are anchored to the config directory (`*.json` becomes
|
|
694
|
+
* `./*.json`), so they match the files the ESLint rule matches.
|
|
695
|
+
*
|
|
696
|
+
* Oxfmt formats the files ESLint mode formats, and no others: TOML, `.mdx` and
|
|
697
|
+
* the like stay as they are.
|
|
698
|
+
*
|
|
699
|
+
* @param config - Options that replace the preset's, plus extra ignore
|
|
700
|
+
* patterns and overrides.
|
|
701
|
+
* @param config.componentExts - Component extensions to format, as the
|
|
702
|
+
* factories' `componentExts`.
|
|
703
|
+
* @param config.cwd - Directory to resolve `.prettierrc` and `.editorconfig`
|
|
704
|
+
* from (default `process.cwd()`).
|
|
705
|
+
* @param config.formatters - Languages to format besides JS and TS, as the
|
|
706
|
+
* factories' `formatters`.
|
|
707
|
+
* @param config.ignorePatterns - Appended to the preset's ignore patterns.
|
|
708
|
+
* @param config.overrides - Appended to the preset's overrides.
|
|
709
|
+
* @returns The oxfmt config.
|
|
710
|
+
*/
|
|
711
|
+
function isentinelOxfmt({ componentExts, cwd, formatters, ignorePatterns = [], overrides = [], ...rest } = {}) {
|
|
712
|
+
const prettierSettings = resolvePrettierSettings({}, cwd);
|
|
713
|
+
const base = buildOxfmtOptions({ prettierOptions: prettierSettings });
|
|
714
|
+
const prettierOverrides = migratePrettierOverrides(prettierSettings["overrides"]);
|
|
715
|
+
const languages = resolveFormattedLanguages(formatters);
|
|
716
|
+
const sectionOverrides = editorConfigOverrides(cwd, rest);
|
|
717
|
+
return {
|
|
718
|
+
...base,
|
|
719
|
+
...rest,
|
|
720
|
+
ignorePatterns: [
|
|
721
|
+
...unformattedFilePatterns({
|
|
722
|
+
componentExts,
|
|
723
|
+
formatters
|
|
724
|
+
}),
|
|
725
|
+
...GLOB_EXCLUDE,
|
|
726
|
+
...ignorePatterns
|
|
727
|
+
],
|
|
728
|
+
overrides: [
|
|
729
|
+
...languages.markdown ? [{
|
|
730
|
+
files: [GLOB_MARKDOWN],
|
|
731
|
+
options: markdownOxfmtOptions(prettierSettings["jsdocPrintWidth"])
|
|
732
|
+
}] : [],
|
|
733
|
+
...languages.yaml ? [{
|
|
734
|
+
files: [GLOB_YAML],
|
|
735
|
+
options: { ...YAML_OXFMT_OPTIONS }
|
|
736
|
+
}] : [],
|
|
737
|
+
...languages.json ? [{
|
|
738
|
+
files: [GLOB_JSON5],
|
|
739
|
+
options: { ...JSON5_OXFMT_OPTIONS }
|
|
740
|
+
}] : [],
|
|
741
|
+
...anchorOverrideGlobs([
|
|
742
|
+
...sectionOverrides,
|
|
743
|
+
...overrides,
|
|
744
|
+
...prettierOverrides
|
|
745
|
+
])
|
|
746
|
+
]
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
//#endregion
|
|
750
|
+
export { isentinelOxfmt };
|