lintmax 0.1.44

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.
@@ -0,0 +1,822 @@
1
+ import { c as SHARED_OVERRIDE_SYMBOL_KEY, i as DEFAULT_SHARED_IGNORE_PATTERNS, n as BIOME_PATTERN_RULE_OVERRIDES, r as BIOME_RULES_OFF, s as OXLINT_PATTERN_RULE_OVERRIDES, t as BIOME_IGNORE_PATTERNS } from "./constants-BRYEJmsZ.mjs";
2
+ import { a as assertJsonSerializable, c as findUnknownRules, d as normalizeObjectListInput, f as normalizePathListInput, h as stripPluginNamespace, i as joinPath, l as isRecord, m as normalizeTailwindOption, n as fromFileUrl, o as assertObject, p as normalizeRulesOffInput, s as assertOptionalString, t as dirnamePath, u as normalizeIgnorePattern } from "./path-Cu_Nf2ct.mjs";
3
+ import { file, spawnSync, write } from "bun";
4
+ import { lstatSync } from "node:fs";
5
+ import { join, relative } from "node:path";
6
+ //#region src/core.ts
7
+ /** biome-ignore-all lint/style/useConsistentMemberAccessibility: biome+eslint conflict */
8
+ var CliExitError = class extends Error {
9
+ code;
10
+ name = "CliExitError";
11
+ constructor({ code, message }) {
12
+ super(message ?? "");
13
+ this.code = code;
14
+ }
15
+ };
16
+ const decoder = new TextDecoder();
17
+ const cacheDir = "node_modules/.cache/lintmax";
18
+ const cwd = process.cwd();
19
+ const ignoreEntries = [".cache/", ".eslintcache"];
20
+ const lintmaxRoot = dirnamePath(dirnamePath(fromFileUrl(import.meta.url)));
21
+ const PRETTIER_MD_ARGS = [
22
+ "--single-quote",
23
+ "--no-semi",
24
+ "--trailing-comma",
25
+ "none",
26
+ "--print-width",
27
+ "80",
28
+ "--arrow-parens",
29
+ "avoid",
30
+ "--tab-width",
31
+ "2",
32
+ "--prose-wrap",
33
+ "preserve"
34
+ ];
35
+ const parseJsonObject = ({ text }) => {
36
+ const parsed = JSON.parse(text);
37
+ return isRecord(parsed) ? parsed : {};
38
+ };
39
+ const decodeText = (bytes) => decoder.decode(bytes ?? new Uint8Array());
40
+ const pathExists = async ({ path }) => file(path).exists();
41
+ const readJson = async ({ path }) => {
42
+ if (!await pathExists({ path })) return {};
43
+ try {
44
+ return parseJsonObject({ text: await file(path).text() });
45
+ } catch {
46
+ return {};
47
+ }
48
+ };
49
+ const readRequiredJson = async ({ path }) => {
50
+ const text = await file(path).text();
51
+ return JSON.parse(text);
52
+ };
53
+ const writeJson = async ({ data, path }) => write(path, `${JSON.stringify(data, null, 2)}\n`);
54
+ const ensureDirectory = ({ directory }) => {
55
+ const result = spawnSync({
56
+ cmd: [
57
+ "mkdir",
58
+ "-p",
59
+ directory
60
+ ],
61
+ stderr: "pipe",
62
+ stdout: "pipe"
63
+ });
64
+ if (result.exitCode === 0) return;
65
+ const stderr = decodeText(result.stderr).trim();
66
+ throw new CliExitError({
67
+ code: result.exitCode,
68
+ message: stderr.length > 0 ? stderr : `Failed to create directory: ${directory}`
69
+ });
70
+ };
71
+ const resolvePackageJsonPath = async ({ pkg }) => {
72
+ try {
73
+ return fromFileUrl(import.meta.resolve(`${pkg}/package.json`));
74
+ } catch {
75
+ const consumerCandidate = joinPath(cwd, "node_modules", pkg, "package.json");
76
+ if (await pathExists({ path: consumerCandidate })) return consumerCandidate;
77
+ return null;
78
+ }
79
+ };
80
+ const resolveBin = async ({ bin, pkg }) => {
81
+ const packageJsonPath = await resolvePackageJsonPath({ pkg });
82
+ if (!packageJsonPath) throw new CliExitError({
83
+ code: 1,
84
+ message: `Cannot find ${pkg} — run: bun add -d ${pkg}`
85
+ });
86
+ const pkgJson = await readRequiredJson({ path: packageJsonPath });
87
+ return joinPath(dirnamePath(packageJsonPath), typeof pkgJson.bin === "string" ? pkgJson.bin : pkgJson.bin?.[bin] ?? "");
88
+ };
89
+ const run = ({ args, command, env, label, silent = false }) => {
90
+ const result = spawnSync({
91
+ cmd: [command, ...args],
92
+ cwd,
93
+ env,
94
+ stderr: silent ? "pipe" : "inherit",
95
+ stdout: silent ? "pipe" : "inherit"
96
+ });
97
+ if (result.exitCode === 0) return;
98
+ if (silent) {
99
+ process.stderr.write(`[${label}]\n`);
100
+ const stdout = decodeText(result.stdout);
101
+ if (stdout.length > 0) process.stderr.write(stdout);
102
+ const stderr = decodeText(result.stderr);
103
+ if (stderr.length > 0) process.stderr.write(stderr);
104
+ }
105
+ throw new CliExitError({ code: result.exitCode });
106
+ };
107
+ const runCapture = ({ args, command, env }) => {
108
+ const result = spawnSync({
109
+ cmd: [command, ...args],
110
+ cwd,
111
+ env,
112
+ stderr: "pipe",
113
+ stdout: "pipe"
114
+ });
115
+ return {
116
+ exitCode: result.exitCode,
117
+ stderr: decodeText(result.stderr),
118
+ stdout: decodeText(result.stdout)
119
+ };
120
+ };
121
+ const readVersion = async () => {
122
+ return (await readRequiredJson({ path: joinPath(lintmaxRoot, "package.json") })).version;
123
+ };
124
+ const usage = ({ version }) => {
125
+ process.stdout.write(`lintmax v${version}\n\n`);
126
+ process.stdout.write("Usage: lintmax <command>\n\n");
127
+ process.stdout.write("Commands:\n");
128
+ process.stdout.write(" init Scaffold config files for a new project\n");
129
+ process.stdout.write(" fix Auto-fix and format all files\n");
130
+ process.stdout.write(" check Check all files without modifying\n");
131
+ process.stdout.write(" rules List all enabled rules\n");
132
+ process.stdout.write(" --version Show version\n");
133
+ };
134
+ //#endregion
135
+ //#region src/index.ts
136
+ const SHARED_OVERRIDE_KEYS = [
137
+ "biome",
138
+ "eslint",
139
+ "oxlint"
140
+ ];
141
+ const ESLINT_IMPORT_MARKER_KEY = "__lintmaxImportRef";
142
+ const ESLINT_IMPORT_SENTINEL = "eslint-import";
143
+ const pkgRoot = dirnamePath(fromFileUrl(import.meta.resolve("../package.json")));
144
+ const normalizeBiomeIgnorePattern = ({ pattern }) => {
145
+ if (pattern.startsWith("!!")) return pattern;
146
+ const trimmed = normalizeIgnorePattern({ pattern });
147
+ return trimmed.startsWith("**/") ? `!!${trimmed}` : `!!**/${trimmed}`;
148
+ };
149
+ const resolveSchemaPath = async ({ cwd }) => {
150
+ try {
151
+ return fromFileUrl(import.meta.resolve("@biomejs/biome/configuration_schema.json"));
152
+ } catch (error) {
153
+ if (!(error instanceof Error)) throw error;
154
+ }
155
+ const consumerCandidate = joinPath(cwd, "node_modules", "@biomejs", "biome", "configuration_schema.json");
156
+ if (await file(consumerCandidate).exists()) return consumerCandidate;
157
+ throw new Error("Cannot find module @biomejs/biome/configuration_schema.json");
158
+ };
159
+ const resolveBiomeSchema = async ({ cwd }) => {
160
+ const schema = await readRequiredJson({ path: await resolveSchemaPath({ cwd }) });
161
+ const rulesProps = schema.$defs.Rules?.properties ?? {};
162
+ const categories = Object.keys(rulesProps).filter((k) => k !== "recommended");
163
+ const ruleMap = /* @__PURE__ */ new Map();
164
+ for (const cat of categories) {
165
+ const key = cat.charAt(0).toUpperCase() + cat.slice(1);
166
+ const props = schema.$defs[key]?.properties;
167
+ if (props) {
168
+ for (const rule of Object.keys(props)) if (rule !== "recommended" && rule !== "all") ruleMap.set(rule, cat);
169
+ }
170
+ }
171
+ return {
172
+ categories,
173
+ ruleMap
174
+ };
175
+ };
176
+ const extractRuleNames = (rules) => {
177
+ const names = [];
178
+ for (const key of Object.keys(rules)) names.push(stripPluginNamespace({ rule: key }));
179
+ return names;
180
+ };
181
+ const assertKnownBiomeRuleNames = ({ categoryMap, label, rules }) => {
182
+ const unknown = findUnknownRules({
183
+ knownRules: new Set(categoryMap.keys()),
184
+ normalizeRule: (rule) => stripPluginNamespace({ rule }),
185
+ rules
186
+ });
187
+ if (unknown.length > 0) throw new Error(`${label} contains unknown biome rules: ${unknown.join(", ")}`);
188
+ };
189
+ const assertKnownOxlintRuleNames = ({ label, rules, knownRules }) => {
190
+ const unknown = findUnknownRules({
191
+ knownRules,
192
+ rules
193
+ });
194
+ if (unknown.length > 0) throw new Error(`${label} contains unknown oxlint rules: ${unknown.join(", ")}`);
195
+ };
196
+ const groupByCategory = ({ categoryMap, ruleNames }) => {
197
+ const result = {};
198
+ for (const rule of ruleNames) {
199
+ const cat = categoryMap.get(rule);
200
+ if (cat) {
201
+ result[cat] ??= {};
202
+ result[cat][rule] = "off";
203
+ }
204
+ }
205
+ return result;
206
+ };
207
+ const normalizeSharedOverrideItem = ({ index, item, label }) => {
208
+ const files = normalizePathListInput({
209
+ label: `${label}[${index}].files`,
210
+ value: item.files
211
+ });
212
+ const biomeRules = normalizeRulesOffInput({
213
+ label: `${label}[${index}].biome`,
214
+ value: item.biome
215
+ });
216
+ const eslintRules = normalizeRulesOffInput({
217
+ label: `${label}[${index}].eslint`,
218
+ value: item.eslint
219
+ });
220
+ const oxlintRules = normalizeRulesOffInput({
221
+ label: `${label}[${index}].oxlint`,
222
+ value: item.oxlint
223
+ });
224
+ if (!(biomeRules || eslintRules || oxlintRules)) throw new Error(`${label}[${index}] must define at least one action: biome, eslint, or oxlint`);
225
+ return {
226
+ biomeRules,
227
+ eslintRules,
228
+ files,
229
+ oxlintRules
230
+ };
231
+ };
232
+ const normalizeSharedOverrides = ({ label, value }) => {
233
+ const out = [];
234
+ if (value === void 0) return out;
235
+ const obj = assertObject({
236
+ label,
237
+ value
238
+ });
239
+ const entries = [];
240
+ for (const [pattern, rawOverride] of Object.entries(obj)) {
241
+ const override = assertObject({
242
+ label: `${label}.${pattern}`,
243
+ value: rawOverride
244
+ });
245
+ for (const key of Object.keys(override)) if (!SHARED_OVERRIDE_KEYS.includes(key)) throw new Error(`${label}.${pattern}.${key} is not supported. Use biome, eslint, or oxlint.`);
246
+ entries.push({
247
+ ...override,
248
+ files: [pattern]
249
+ });
250
+ }
251
+ for (const [i, item] of entries.entries()) out.push(normalizeSharedOverrideItem({
252
+ index: i,
253
+ item,
254
+ label
255
+ }));
256
+ return out;
257
+ };
258
+ const collectSharedRuleOverrides = ({ ruleKey, sharedOverrides }) => {
259
+ const out = [];
260
+ for (const override of sharedOverrides) {
261
+ const rules = override[ruleKey];
262
+ if (rules) out.push({
263
+ files: override.files,
264
+ rules
265
+ });
266
+ }
267
+ return out;
268
+ };
269
+ const parseLinterOffOverrides = ({ assertKnownRules, fileKey, label, requireOff = false, value }) => {
270
+ const out = [];
271
+ for (const [i, override] of normalizeObjectListInput({
272
+ label,
273
+ value
274
+ }).entries()) {
275
+ const itemLabel = requireOff ? `${label}[${i}]` : label;
276
+ if (requireOff && override[fileKey] === void 0) throw new Error(`${label}[${i}].${fileKey} is required`);
277
+ if (requireOff && override.off === void 0) throw new Error(`${label}[${i}].off is required`);
278
+ const normalizedOverrideRules = normalizeRulesOffInput({
279
+ label: `${itemLabel}.off`,
280
+ value: override.off
281
+ });
282
+ if (normalizedOverrideRules) {
283
+ assertKnownRules?.(normalizedOverrideRules);
284
+ out.push({
285
+ files: normalizePathListInput({
286
+ label: `${itemLabel}.${fileKey}`,
287
+ value: override[fileKey]
288
+ }),
289
+ rules: normalizedOverrideRules
290
+ });
291
+ }
292
+ }
293
+ return out;
294
+ };
295
+ const parseTopLevelSyncScalars = ({ options }) => {
296
+ if (options.compact !== void 0 && typeof options.compact !== "boolean") throw new Error("compact must be a boolean");
297
+ const tailwind = normalizeTailwindOption({
298
+ label: "tailwind",
299
+ value: options.tailwind
300
+ });
301
+ assertOptionalString({
302
+ label: "tsconfigRootDir",
303
+ value: options.tsconfigRootDir
304
+ });
305
+ return {
306
+ sharedOverrides: options.overrides === void 0 ? [] : normalizeSharedOverrides({
307
+ label: "overrides",
308
+ value: options.overrides
309
+ }),
310
+ tailwind,
311
+ topLevelIgnores: options.ignores === void 0 ? [] : normalizePathListInput({
312
+ label: "ignores",
313
+ value: options.ignores
314
+ }),
315
+ tsconfigRootDir: options.tsconfigRootDir
316
+ };
317
+ };
318
+ const parseLinterSyncCommon = ({ linter, value }) => {
319
+ const parsed = {};
320
+ if (value.ignores !== void 0) parsed.ignores = normalizePathListInput({
321
+ label: `${linter}.ignores`,
322
+ value: value.ignores
323
+ });
324
+ const normalizedOffRules = normalizeRulesOffInput({
325
+ label: `${linter}.off`,
326
+ value: value.off
327
+ });
328
+ if (normalizedOffRules) parsed.off = Object.keys(normalizedOffRules);
329
+ return parsed;
330
+ };
331
+ const validateBiomeSyncConfig = ({ biome }) => {
332
+ const biomeValue = assertObject({
333
+ label: "biome config",
334
+ value: biome
335
+ });
336
+ const parsedCommon = parseLinterSyncCommon({
337
+ linter: "biome",
338
+ value: biomeValue
339
+ });
340
+ const parsedOverrides = parseLinterOffOverrides({
341
+ fileKey: "includes",
342
+ label: "biome.overrides",
343
+ requireOff: true,
344
+ value: biomeValue.overrides
345
+ }).map((override) => ({
346
+ includes: override.files,
347
+ off: Object.keys(override.rules)
348
+ }));
349
+ const parsed = { ...parsedCommon };
350
+ if (parsedOverrides.length > 0) parsed.overrides = parsedOverrides;
351
+ return parsed;
352
+ };
353
+ const parseEslintAppendEntry = ({ index, item }) => {
354
+ const { $lintmax: marker } = item;
355
+ if (marker === ESLINT_IMPORT_SENTINEL) {
356
+ const allowedKeys = new Set([
357
+ "$lintmax",
358
+ "files",
359
+ "from",
360
+ "ignores",
361
+ "name"
362
+ ]);
363
+ for (const key of Object.keys(item)) if (!allowedKeys.has(key)) throw new Error(`eslint.append[${index}].${key} is not supported for eslint-import entries. Use from, name, files, ignores.`);
364
+ assertOptionalString({
365
+ label: `eslint.append[${index}].from`,
366
+ value: item.from
367
+ });
368
+ const { from } = item;
369
+ if (typeof from !== "string" || from.trim().length === 0) throw new Error(`eslint.append[${index}].from must be a non-empty string`);
370
+ assertOptionalString({
371
+ label: `eslint.append[${index}].name`,
372
+ value: item.name
373
+ });
374
+ return {
375
+ files: item.files === void 0 ? void 0 : normalizePathListInput({
376
+ label: `eslint.append[${index}].files`,
377
+ value: item.files
378
+ }),
379
+ from,
380
+ ignores: item.ignores === void 0 ? void 0 : normalizePathListInput({
381
+ label: `eslint.append[${index}].ignores`,
382
+ value: item.ignores
383
+ }),
384
+ name: typeof item.name === "string" && item.name.trim().length > 0 ? item.name : "default"
385
+ };
386
+ }
387
+ if ("$lintmax" in item) throw new Error(`eslint.append[${index}].$lintmax has unsupported value. Use eslintImport(...) or remove the $lintmax key.`);
388
+ assertJsonSerializable({
389
+ label: `eslint.append[${index}]`,
390
+ value: item
391
+ });
392
+ return { config: item };
393
+ };
394
+ const validateEslintSyncConfig = ({ eslint }) => {
395
+ const eslintValue = assertObject({
396
+ label: "eslint config",
397
+ value: eslint
398
+ });
399
+ const parsed = { ...parseLinterSyncCommon({
400
+ linter: "eslint",
401
+ value: eslintValue
402
+ }) };
403
+ if (eslintValue.tailwind !== void 0) throw new Error("eslint.tailwind is not supported in sync config. Use top-level tailwind.");
404
+ if (eslintValue.tsconfigRootDir !== void 0) throw new Error("eslint.tsconfigRootDir is not supported in sync config. Use top-level tsconfigRootDir.");
405
+ if (eslintValue.append !== void 0) parsed.append = normalizeObjectListInput({
406
+ label: "eslint.append",
407
+ value: eslintValue.append
408
+ }).map((item, i) => parseEslintAppendEntry({
409
+ index: i,
410
+ item
411
+ }));
412
+ return parsed;
413
+ };
414
+ const validateOxlintSyncConfig = ({ oxlint }) => {
415
+ const oxlintValue = assertObject({
416
+ label: "oxlint config",
417
+ value: oxlint
418
+ });
419
+ const parsedCommon = parseLinterSyncCommon({
420
+ linter: "oxlint",
421
+ value: oxlintValue
422
+ });
423
+ const parsedOverrides = parseLinterOffOverrides({
424
+ fileKey: "files",
425
+ label: "oxlint.overrides",
426
+ requireOff: true,
427
+ value: oxlintValue.overrides
428
+ }).map((override) => ({
429
+ files: override.files,
430
+ off: Object.keys(override.rules)
431
+ }));
432
+ const parsed = { ...parsedCommon };
433
+ if (parsedOverrides.length > 0) parsed.overrides = parsedOverrides;
434
+ return parsed;
435
+ };
436
+ const validateSyncOptions = ({ options }) => {
437
+ if (!options) return {
438
+ sharedOverrides: [],
439
+ topLevelIgnores: []
440
+ };
441
+ const parsed = { ...parseTopLevelSyncScalars({ options }) };
442
+ if (options.biome) parsed.biome = validateBiomeSyncConfig({ biome: options.biome });
443
+ if (options.eslint) parsed.eslint = validateEslintSyncConfig({ eslint: options.eslint });
444
+ if (options.oxlint) parsed.oxlint = validateOxlintSyncConfig({ oxlint: options.oxlint });
445
+ return parsed;
446
+ };
447
+ const mergeUniquePatterns = ({ into, patterns }) => {
448
+ if (!patterns) return;
449
+ for (const raw of patterns) {
450
+ const pattern = normalizeIgnorePattern({ pattern: raw });
451
+ if (pattern.length > 0 && !into.includes(pattern)) into.push(pattern);
452
+ }
453
+ };
454
+ const mergeIgnorePatternGroups = ({ groups }) => {
455
+ const merged = [];
456
+ for (const group of groups) mergeUniquePatterns({
457
+ into: merged,
458
+ patterns: group
459
+ });
460
+ return merged;
461
+ };
462
+ const buildUserIgnorePatterns = ({ topLevelIgnores }) => mergeIgnorePatternGroups({ groups: [topLevelIgnores] });
463
+ const buildLinterOptionsWithSharedOverrides = ({ createEmpty, ruleKey, sharedOverrides, source, mapSharedOverride }) => {
464
+ if (!(source || sharedOverrides.length > 0)) return source;
465
+ const next = source ? { ...source } : createEmpty();
466
+ next.off = source?.off;
467
+ const sharedRuleOverrides = collectSharedRuleOverrides({
468
+ ruleKey,
469
+ sharedOverrides
470
+ }).map(mapSharedOverride);
471
+ const localOverrides = source?.overrides ?? [];
472
+ const mergedOverrides = [...sharedRuleOverrides, ...localOverrides];
473
+ if (mergedOverrides.length > 0) next.overrides = mergedOverrides;
474
+ return next;
475
+ };
476
+ const buildBiomeOptions = ({ biomeSource, sharedOverrides }) => buildLinterOptionsWithSharedOverrides({
477
+ createEmpty: () => ({}),
478
+ mapSharedOverride: (override) => ({
479
+ includes: override.files,
480
+ off: Object.keys(override.rules)
481
+ }),
482
+ ruleKey: "biomeRules",
483
+ sharedOverrides,
484
+ source: biomeSource
485
+ });
486
+ const buildOxlintOptions = ({ oxlintSource, sharedOverrides }) => buildLinterOptionsWithSharedOverrides({
487
+ createEmpty: () => ({}),
488
+ mapSharedOverride: (override) => ({
489
+ files: override.files,
490
+ off: Object.keys(override.rules)
491
+ }),
492
+ ruleKey: "oxlintRules",
493
+ sharedOverrides,
494
+ source: oxlintSource
495
+ });
496
+ const buildEslintIgnorePatterns = ({ eslintSource, userIgnorePatterns }) => mergeIgnorePatternGroups({ groups: [userIgnorePatterns, eslintSource?.ignores === void 0 ? void 0 : normalizePathListInput({
497
+ label: "eslint.ignores",
498
+ value: eslintSource.ignores
499
+ })] });
500
+ const buildEslintOptions = ({ eslintSource, sharedOverrides, tailwind, tsconfigRootDir, userIgnorePatterns }) => {
501
+ const eslintIgnores = buildEslintIgnorePatterns({
502
+ eslintSource,
503
+ userIgnorePatterns
504
+ });
505
+ const sharedRuleOverrides = collectSharedRuleOverrides({
506
+ ruleKey: "eslintRules",
507
+ sharedOverrides
508
+ }).map((override) => ({
509
+ files: override.files,
510
+ rules: override.rules
511
+ }));
512
+ const sharedOverrideAppendIndexes = sharedRuleOverrides.map((_, index) => index);
513
+ const eslintImportRefs = [];
514
+ if (eslintSource || sharedOverrides.length > 0 || tailwind !== void 0 || tsconfigRootDir !== void 0) {
515
+ const eslintOptions = { off: eslintSource?.off };
516
+ if (eslintIgnores.length > 0) eslintOptions.ignores = eslintIgnores;
517
+ const appendEntries = [];
518
+ for (const entry of eslintSource?.append ?? []) if ("config" in entry) appendEntries.push(entry.config);
519
+ else {
520
+ const importRefIndex = eslintImportRefs.push({
521
+ exportName: entry.name,
522
+ files: entry.files,
523
+ ignores: entry.ignores,
524
+ source: entry.from
525
+ });
526
+ appendEntries.push({ [ESLINT_IMPORT_MARKER_KEY]: importRefIndex - 1 });
527
+ }
528
+ const mergedAppend = [...sharedRuleOverrides, ...appendEntries];
529
+ if (mergedAppend.length > 0) eslintOptions.append = mergedAppend;
530
+ eslintOptions.tailwind = normalizeTailwindOption({
531
+ label: "tailwind",
532
+ value: tailwind
533
+ }) ?? true;
534
+ eslintOptions.tsconfigRootDir = tsconfigRootDir;
535
+ return {
536
+ eslintImportRefs,
537
+ eslintOptions,
538
+ sharedOverrideAppendIndexes
539
+ };
540
+ }
541
+ if (eslintIgnores.length === 0) return {
542
+ eslintImportRefs,
543
+ eslintOptions: void 0,
544
+ sharedOverrideAppendIndexes
545
+ };
546
+ return {
547
+ eslintImportRefs,
548
+ eslintOptions: {
549
+ ignores: eslintIgnores,
550
+ tailwind: true
551
+ },
552
+ sharedOverrideAppendIndexes
553
+ };
554
+ };
555
+ const normalizeLinterOffOverrides = ({ assertKnownRules, fileKey, label, value }) => parseLinterOffOverrides({
556
+ assertKnownRules,
557
+ fileKey,
558
+ label,
559
+ value
560
+ });
561
+ const normalizeKnownOffRules = ({ assertKnownRules, label, value }) => {
562
+ const normalizedRules = normalizeRulesOffInput({
563
+ label,
564
+ value
565
+ });
566
+ if (!normalizedRules) return;
567
+ assertKnownRules?.(normalizedRules);
568
+ return normalizedRules;
569
+ };
570
+ const createBiomeConfig = async ({ cwd, options, sharedIgnorePatterns }) => {
571
+ const { categories, ruleMap } = await resolveBiomeSchema({ cwd });
572
+ const allRulesOff = [...BIOME_RULES_OFF];
573
+ const normalizedRules = normalizeKnownOffRules({
574
+ assertKnownRules: (rules) => assertKnownBiomeRuleNames({
575
+ categoryMap: ruleMap,
576
+ label: "biome.off",
577
+ rules
578
+ }),
579
+ label: "biome.off",
580
+ value: options?.off
581
+ });
582
+ if (normalizedRules) for (const key of Object.keys(normalizedRules)) {
583
+ const ruleName = stripPluginNamespace({ rule: key });
584
+ if (!allRulesOff.includes(ruleName)) allRulesOff.push(ruleName);
585
+ }
586
+ const ignorePatterns = [...BIOME_IGNORE_PATTERNS];
587
+ const mergedIgnorePatterns = mergeIgnorePatternGroups({ groups: [sharedIgnorePatterns, options?.ignores === void 0 ? void 0 : normalizePathListInput({
588
+ label: "biome.ignores",
589
+ value: options.ignores
590
+ })] });
591
+ for (const pattern of mergedIgnorePatterns) {
592
+ const negated = normalizeBiomeIgnorePattern({ pattern });
593
+ if (!ignorePatterns.includes(negated)) ignorePatterns.push(negated);
594
+ }
595
+ const overrides = [{
596
+ css: { parser: { tailwindDirectives: true } },
597
+ includes: ["**"],
598
+ linter: { rules: groupByCategory({
599
+ categoryMap: ruleMap,
600
+ ruleNames: allRulesOff
601
+ }) }
602
+ }];
603
+ for (const override of BIOME_PATTERN_RULE_OVERRIDES) overrides.push({
604
+ includes: [...override.includes],
605
+ linter: { rules: groupByCategory({
606
+ categoryMap: ruleMap,
607
+ ruleNames: override.rules
608
+ }) }
609
+ });
610
+ for (const override of normalizeLinterOffOverrides({
611
+ assertKnownRules: (rules) => assertKnownBiomeRuleNames({
612
+ categoryMap: ruleMap,
613
+ label: "biome.overrides.off",
614
+ rules
615
+ }),
616
+ fileKey: "includes",
617
+ label: "biome.overrides",
618
+ value: options?.overrides
619
+ })) overrides.push({
620
+ includes: override.files,
621
+ linter: { rules: groupByCategory({
622
+ categoryMap: ruleMap,
623
+ ruleNames: extractRuleNames(override.rules)
624
+ }) }
625
+ });
626
+ return {
627
+ $schema: "https://biomejs.dev/schemas/latest/schema.json",
628
+ assist: { actions: { source: { organizeImports: "off" } } },
629
+ css: {
630
+ formatter: {
631
+ enabled: true,
632
+ quoteStyle: "single"
633
+ },
634
+ parser: { tailwindDirectives: true }
635
+ },
636
+ files: {
637
+ experimentalScannerIgnores: ignorePatterns.map((p) => p.startsWith("!!") ? p.slice(2) : p),
638
+ includes: ["**", ...ignorePatterns]
639
+ },
640
+ formatter: {
641
+ indentStyle: "space",
642
+ lineWidth: 123
643
+ },
644
+ javascript: { formatter: {
645
+ arrowParentheses: "asNeeded",
646
+ bracketSameLine: true,
647
+ jsxQuoteStyle: "single",
648
+ quoteStyle: "single",
649
+ semicolons: "asNeeded",
650
+ trailingCommas: "none"
651
+ } },
652
+ json: { formatter: { trailingCommas: "none" } },
653
+ linter: {
654
+ domains: {
655
+ next: "all",
656
+ project: "all",
657
+ qwik: "all",
658
+ react: "all",
659
+ solid: "all",
660
+ tailwind: "all",
661
+ test: "all",
662
+ vue: "all"
663
+ },
664
+ rules: Object.fromEntries(categories.map((c) => [c, "error"]))
665
+ },
666
+ overrides
667
+ };
668
+ };
669
+ const createOxlintConfig = async ({ options, sharedIgnorePatterns }) => {
670
+ const base = await readRequiredJson({ path: joinPath(pkgRoot, "oxlintrc.json") });
671
+ const knownRules = new Set([
672
+ ...Object.keys(base.rules),
673
+ ...(base.overrides ?? []).flatMap((override) => Object.keys(override.rules)),
674
+ ...OXLINT_PATTERN_RULE_OVERRIDES.flatMap((override) => Object.keys(override.rules))
675
+ ]);
676
+ const mergedIgnorePatterns = mergeIgnorePatternGroups({ groups: [sharedIgnorePatterns, options?.ignores === void 0 ? void 0 : normalizePathListInput({
677
+ label: "oxlint.ignores",
678
+ value: options.ignores
679
+ })] });
680
+ base.overrides ??= [];
681
+ for (const override of OXLINT_PATTERN_RULE_OVERRIDES) base.overrides.push({
682
+ files: [...override.files],
683
+ rules: { ...override.rules }
684
+ });
685
+ if (mergedIgnorePatterns.length > 0) {
686
+ base.ignorePatterns ??= [];
687
+ for (const pattern of mergedIgnorePatterns) if (!base.ignorePatterns.includes(pattern)) base.ignorePatterns.push(pattern);
688
+ }
689
+ if (!options) return base;
690
+ const normalizedRules = normalizeKnownOffRules({
691
+ assertKnownRules: (rules) => assertKnownOxlintRuleNames({
692
+ knownRules,
693
+ label: "oxlint.off",
694
+ rules
695
+ }),
696
+ label: "oxlint.off",
697
+ value: options.off
698
+ });
699
+ if (normalizedRules) for (const [key, value] of Object.entries(normalizedRules)) base.rules[key] = value;
700
+ for (const override of normalizeLinterOffOverrides({
701
+ assertKnownRules: (rules) => assertKnownOxlintRuleNames({
702
+ knownRules,
703
+ label: "oxlint.overrides.off",
704
+ rules
705
+ }),
706
+ fileKey: "files",
707
+ label: "oxlint.overrides",
708
+ value: options.overrides
709
+ })) base.overrides.push({
710
+ files: override.files,
711
+ rules: override.rules
712
+ });
713
+ return base;
714
+ };
715
+ const resolveSyncImportSource = ({ cwd, dir, source }) => {
716
+ if (!(source.startsWith(".") || source.startsWith(".."))) return source;
717
+ const relativeSource = relative(dir, joinPath(cwd, source)).replaceAll("\\", "/");
718
+ return relativeSource.startsWith(".") ? relativeSource : `./${relativeSource}`;
719
+ };
720
+ const discoverSymlinkPatterns = ({ root }) => {
721
+ const result = spawnSync({
722
+ cmd: [
723
+ "git",
724
+ "-C",
725
+ root,
726
+ "ls-files",
727
+ "-z",
728
+ "--cached",
729
+ "--others",
730
+ "--exclude-standard"
731
+ ],
732
+ stderr: "pipe",
733
+ stdout: "pipe"
734
+ });
735
+ if (result.exitCode !== 0) return [];
736
+ const entries = new TextDecoder().decode(result.stdout).split("\0").filter((e) => e.length > 0);
737
+ const out = [];
738
+ for (const entry of entries) {
739
+ let isSymlink;
740
+ try {
741
+ isSymlink = lstatSync(join(root, entry)).isSymbolicLink();
742
+ } catch {
743
+ isSymlink = false;
744
+ }
745
+ if (isSymlink) out.push(entry);
746
+ }
747
+ return out;
748
+ };
749
+ const sync = async (options) => {
750
+ const { biome, eslint, oxlint, sharedOverrides, tailwind, topLevelIgnores, tsconfigRootDir } = validateSyncOptions({ options });
751
+ const cwd = process.cwd();
752
+ const dir = joinPath(cwd, cacheDir);
753
+ const userIgnorePatterns = buildUserIgnorePatterns({ topLevelIgnores });
754
+ const sharedIgnorePatterns = mergeIgnorePatternGroups({ groups: [
755
+ DEFAULT_SHARED_IGNORE_PATTERNS,
756
+ discoverSymlinkPatterns({ root: cwd }),
757
+ userIgnorePatterns
758
+ ] });
759
+ const biomeOptions = buildBiomeOptions({
760
+ biomeSource: biome,
761
+ sharedOverrides
762
+ });
763
+ const oxlintOptions = buildOxlintOptions({
764
+ oxlintSource: oxlint,
765
+ sharedOverrides
766
+ });
767
+ const { eslintImportRefs, eslintOptions, sharedOverrideAppendIndexes } = buildEslintOptions({
768
+ eslintSource: eslint,
769
+ sharedOverrides,
770
+ tailwind,
771
+ tsconfigRootDir,
772
+ userIgnorePatterns
773
+ });
774
+ ensureDirectory({ directory: dir });
775
+ const biomeConfig = await createBiomeConfig({
776
+ cwd,
777
+ options: biomeOptions,
778
+ sharedIgnorePatterns
779
+ });
780
+ const oxlintConfig = await createOxlintConfig({
781
+ options: oxlintOptions,
782
+ sharedIgnorePatterns
783
+ });
784
+ const normalizedImportRefs = eslintImportRefs.map((importRef) => ({
785
+ exportName: importRef.exportName,
786
+ files: importRef.files,
787
+ ignores: importRef.ignores,
788
+ source: resolveSyncImportSource({
789
+ cwd,
790
+ dir,
791
+ source: importRef.source
792
+ })
793
+ }));
794
+ const importStatements = normalizedImportRefs.map((importRef, index) => `import * as __lintmaxAppendImport${index} from ${JSON.stringify(importRef.source)}`).join("\n");
795
+ const importEntries = normalizedImportRefs.map((importRef, index) => ` { exportName: ${JSON.stringify(importRef.exportName)}, files: ${JSON.stringify(importRef.files ?? null)}, ignores: ${JSON.stringify(importRef.ignores ?? null)}, module: __lintmaxAppendImport${index} }`).join(",\n");
796
+ const importExpansion = normalizedImportRefs.length === 0 ? "" : `\nconst appendImports = [\n${importEntries}\n]\nconst normalizedAppend = []\nfor (const entry of options.append ?? []) {\n if (entry && typeof entry === 'object' && !Array.isArray(entry) && ${JSON.stringify(ESLINT_IMPORT_MARKER_KEY)} in entry) {\n const importIndex = entry[${JSON.stringify(ESLINT_IMPORT_MARKER_KEY)}]\n if (typeof importIndex !== 'number' || !Number.isInteger(importIndex))\n throw new Error('Invalid eslint import marker index in generated config')\n const importRef = appendImports[importIndex]\n if (!importRef) throw new Error(\`Missing eslint import ref for index \${importIndex}\`)\n const imported = importRef.module[importRef.exportName]\n const importedEntries = Array.isArray(imported) ? imported : [imported]\n for (const importedEntry of importedEntries) {\n if (!importedEntry || typeof importedEntry !== 'object' || Array.isArray(importedEntry))\n throw new Error(\`Imported eslint append from \${importRef.exportName} must resolve to config object(s)\`)\n normalizedAppend.push({\n ...importedEntry,\n ...(Array.isArray(importRef.files) ? { files: [...importRef.files] } : {}),\n ...(Array.isArray(importRef.ignores) ? { ignores: [...importRef.ignores] } : {})\n })\n }\n continue\n }\n normalizedAppend.push(entry)\n}\noptions.append = normalizedAppend\n`;
797
+ const eslintConfig = eslintOptions ? `${importStatements.length > 0 ? `${importStatements}\n` : ""}import { eslint } from 'lintmax/eslint'\nconst options = ${JSON.stringify(eslintOptions)}\nfor (const index of ${JSON.stringify(sharedOverrideAppendIndexes)}) {\n const entry = options.append?.[index]\n if (entry && typeof entry === 'object') entry[Symbol.for(${JSON.stringify(SHARED_OVERRIDE_SYMBOL_KEY)})] = true\n}${importExpansion}export default eslint(options)\n` : "export { default } from 'lintmax/eslint'\n";
798
+ const runtimeConfig = {
799
+ comments: options?.comments !== false,
800
+ compact: options?.compact !== false
801
+ };
802
+ await write(joinPath(dir, "biome.json"), `${JSON.stringify(biomeConfig, null, 2)}\n`);
803
+ await write(joinPath(dir, ".oxlintrc.json"), `${JSON.stringify(oxlintConfig, null, 2)}\n`);
804
+ await write(joinPath(dir, "eslint.generated.mjs"), eslintConfig);
805
+ await write(joinPath(dir, "lintmax.json"), `${JSON.stringify(runtimeConfig, null, 2)}\n`);
806
+ };
807
+ const eslintImport = ({ files, from, ignores, name }) => {
808
+ const entry = {
809
+ $lintmax: ESLINT_IMPORT_SENTINEL,
810
+ from
811
+ };
812
+ if (files !== void 0) entry.files = files;
813
+ if (ignores !== void 0) entry.ignores = ignores;
814
+ if (name !== void 0) entry.name = name;
815
+ return entry;
816
+ };
817
+ const defineConfig = (options) => {
818
+ validateSyncOptions({ options });
819
+ return options;
820
+ };
821
+ //#endregion
822
+ export { run as _, PRETTIER_MD_ARGS as a, writeJson as b, decodeText as c, lintmaxRoot as d, pathExists as f, resolveBin as g, readVersion as h, CliExitError as i, ensureDirectory as l, readRequiredJson as m, eslintImport as n, cacheDir as o, readJson as p, sync as r, cwd as s, defineConfig as t, ignoreEntries as u, runCapture as v, usage as y };