@isentinel/eslint-config 6.0.1 → 6.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/README.md +115 -0
- package/dist/cli.mjs +231 -30
- package/dist/index.d.mts +84 -36
- package/dist/index.mjs +793 -200
- package/dist/lint-cli.mjs +527 -131
- package/dist/oxfmt.d.mts +132 -0
- package/dist/oxfmt.mjs +722 -0
- package/dist/oxlint.d.mts +21 -2
- package/dist/oxlint.mjs +462 -61
- package/package.json +10 -5
package/dist/lint-cli.mjs
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import process$1 from "node:process";
|
|
3
3
|
import { isPackageExists } from "local-pkg";
|
|
4
|
-
import { Buffer as Buffer$1 } from "node:buffer";
|
|
5
|
-
import fs from "node:fs/promises";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import yargs from "yargs";
|
|
8
4
|
import "find-up-simple";
|
|
9
|
-
import
|
|
5
|
+
import "jsonc-eslint-parser";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
10
8
|
import "semver";
|
|
9
|
+
import { Buffer as Buffer$1 } from "node:buffer";
|
|
10
|
+
import fs$1 from "node:fs/promises";
|
|
11
|
+
import yargs from "yargs";
|
|
11
12
|
import crypto from "node:crypto";
|
|
12
13
|
import ansis from "ansis";
|
|
13
14
|
import { KillOnSignal, LogError, LogExit, LogOutput, Logger, createConcurrently } from "concurrently";
|
|
14
15
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
15
16
|
import os, { availableParallelism } from "node:os";
|
|
16
17
|
import { Writable } from "node:stream";
|
|
18
|
+
import v8 from "node:v8";
|
|
17
19
|
import { fileURLToPath } from "node:url";
|
|
18
20
|
import fileEntryCache from "file-entry-cache";
|
|
19
21
|
//#region \0rolldown/runtime.js
|
|
@@ -40,7 +42,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
40
42
|
}) : target, mod));
|
|
41
43
|
//#endregion
|
|
42
44
|
//#region package.json
|
|
43
|
-
var version = "6.0
|
|
45
|
+
var version = "6.2.0";
|
|
44
46
|
//#endregion
|
|
45
47
|
//#region src/lint-cli/lib/cli/types.ts
|
|
46
48
|
/**
|
|
@@ -50,78 +52,6 @@ var version = "6.0.1";
|
|
|
50
52
|
var CliError = class extends Error {
|
|
51
53
|
name = "CliError";
|
|
52
54
|
};
|
|
53
|
-
//#endregion
|
|
54
|
-
//#region src/lint-cli/lib/cli/file-list.ts
|
|
55
|
-
/** The `--files0-from` value that names stdin rather than a file. */
|
|
56
|
-
const STDIN_SOURCE = "-";
|
|
57
|
-
/**
|
|
58
|
-
* Split a NUL-delimited, UTF-8 path list (the `git ls-files -z` / `find
|
|
59
|
-
* -print0` shape) into its paths.
|
|
60
|
-
*
|
|
61
|
-
* Every path is terminated by a NUL, though the last terminator may be left off
|
|
62
|
-
* as GNU's own `--files0-from` allows. Nothing else is interpreted: spaces,
|
|
63
|
-
* newlines, quotes, shell metacharacters and leading hyphens all stay part of
|
|
64
|
-
* the name. The bytes must be valid UTF-8 — a lossy decode would lint a file
|
|
65
|
-
* that is not the one selected — and no entry may be empty, since an empty
|
|
66
|
-
* path is never a file and most likely a broken producer.
|
|
67
|
-
*
|
|
68
|
-
* @param bytes - The raw list.
|
|
69
|
-
* @returns The paths, in list order. Empty for an empty list.
|
|
70
|
-
*/
|
|
71
|
-
function decodeFileList(bytes) {
|
|
72
|
-
const decoder = new TextDecoder("utf-8", {
|
|
73
|
-
fatal: true,
|
|
74
|
-
ignoreBOM: true
|
|
75
|
-
});
|
|
76
|
-
let text;
|
|
77
|
-
try {
|
|
78
|
-
text = decoder.decode(bytes);
|
|
79
|
-
} catch {
|
|
80
|
-
throw new CliError("--files0-from: the file list is not valid UTF-8.");
|
|
81
|
-
}
|
|
82
|
-
if (text.length === 0) return [];
|
|
83
|
-
const entries = text.split("\0");
|
|
84
|
-
if (entries.at(-1) === "") entries.pop();
|
|
85
|
-
const empty = entries.indexOf("");
|
|
86
|
-
if (empty !== -1) throw new CliError(`--files0-from: entry ${empty + 1} of the file list is empty; expected NUL-terminated paths.`);
|
|
87
|
-
return entries;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Fill the run's paths from its `--files0-from` list, reading it once. Options
|
|
91
|
-
* without a list are returned as they are.
|
|
92
|
-
*
|
|
93
|
-
* An empty list stays empty: the caller treats it as nothing to lint, and it
|
|
94
|
-
* must never become the whole-tree default `.`.
|
|
95
|
-
*
|
|
96
|
-
* @param options - The parsed CLI options.
|
|
97
|
-
* @param cwd - The directory a relative list path resolves against.
|
|
98
|
-
* @param stdin - The stream `-` reads.
|
|
99
|
-
* @returns The options with `paths` set from the list.
|
|
100
|
-
* @rejects {CliError} When the list cannot be read or is malformed.
|
|
101
|
-
*/
|
|
102
|
-
async function readFileList(options, cwd, stdin) {
|
|
103
|
-
const source = options.filesFrom;
|
|
104
|
-
if (source === void 0) return options;
|
|
105
|
-
const bytes = source === STDIN_SOURCE ? await readAll(stdin) : await readFile(source, cwd);
|
|
106
|
-
return {
|
|
107
|
-
...options,
|
|
108
|
-
paths: decodeFileList(bytes)
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
async function readFile(source, cwd) {
|
|
112
|
-
try {
|
|
113
|
-
return await fs.readFile(path.resolve(cwd, source));
|
|
114
|
-
} catch (err) {
|
|
115
|
-
throw new CliError(`--files0-from: cannot read "${source}": ${err instanceof Error ? err.message : String(err)}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
async function readAll(stream) {
|
|
119
|
-
const chunks = [];
|
|
120
|
-
for await (const chunk of stream) if (typeof chunk === "string") chunks.push(Buffer$1.from(chunk, "utf8"));
|
|
121
|
-
else if (chunk instanceof Uint8Array) chunks.push(Buffer$1.from(chunk));
|
|
122
|
-
else throw new CliError("--files0-from: stdin produced a non-byte chunk.");
|
|
123
|
-
return Buffer$1.concat(chunks);
|
|
124
|
-
}
|
|
125
55
|
/**
|
|
126
56
|
* The real TS/JS-family file extensions {@link GLOB_SRC_EXT} encodes (the glob
|
|
127
57
|
* additionally matches non-existent combinations such as `cjsx`). Kept next to
|
|
@@ -183,6 +113,10 @@ function isRecord(value) {
|
|
|
183
113
|
function isStringArray(value) {
|
|
184
114
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
185
115
|
}
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/quiet-warnings.ts
|
|
118
|
+
/** Set in an ESLint child that must not repeat the preset's config warnings. */
|
|
119
|
+
const QUIET_WARNINGS_ENV = "ISENTINEL_QUIET_CONFIG_WARNINGS";
|
|
186
120
|
createRequire(import.meta.url);
|
|
187
121
|
/**
|
|
188
122
|
* Whether the environment signals a CI run. Treats an unset, empty, `"false"`
|
|
@@ -301,6 +235,120 @@ function environmentMatcher(name, pattern, { noTty = false } = {}) {
|
|
|
301
235
|
return value !== void 0 && pattern.test(value);
|
|
302
236
|
};
|
|
303
237
|
}
|
|
238
|
+
/** JSON oxfmt config files oxfmt discovers. */
|
|
239
|
+
const OXFMT_RC_FILES = [".oxfmtrc.json", ".oxfmtrc.jsonc"];
|
|
240
|
+
/**
|
|
241
|
+
* Oxfmt config modules oxfmt discovers. It loads other module extensions only
|
|
242
|
+
* through `--config`, never by search.
|
|
243
|
+
*/
|
|
244
|
+
const OXFMT_CONFIG_MODULES = ["oxfmt.config.ts", "oxfmt.config.mts"];
|
|
245
|
+
OXFMT_CONFIG_MODULES[0];
|
|
246
|
+
/** Every file name oxfmt discovers. One directory may hold only one of them. */
|
|
247
|
+
const OXFMT_CONFIG_NAMES = [...OXFMT_RC_FILES, ...OXFMT_CONFIG_MODULES];
|
|
248
|
+
/**
|
|
249
|
+
* List the oxfmt config files in one directory, without walking up.
|
|
250
|
+
*
|
|
251
|
+
* @param directory - Directory to check.
|
|
252
|
+
* @returns The file names found, sorted.
|
|
253
|
+
*/
|
|
254
|
+
function oxfmtConfigNamesIn(directory) {
|
|
255
|
+
return OXFMT_CONFIG_NAMES.filter((name) => fs.existsSync(path.join(directory, name))).sort();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Find the oxfmt config the way oxfmt does: walk up from `cwd` to the
|
|
259
|
+
* filesystem root and take the first directory that holds one. Like oxfmt,
|
|
260
|
+
* refuse a directory that holds more than one.
|
|
261
|
+
*
|
|
262
|
+
* @param cwd - Directory to start from.
|
|
263
|
+
* @returns The config file, if any.
|
|
264
|
+
* @throws When one directory holds more than one oxfmt config file.
|
|
265
|
+
*/
|
|
266
|
+
function findOxfmtConfig(cwd = process$1.cwd()) {
|
|
267
|
+
let directory = path.resolve(cwd);
|
|
268
|
+
for (;;) {
|
|
269
|
+
const [first, second] = oxfmtConfigNamesIn(directory);
|
|
270
|
+
if (second !== void 0) throw new Error(`[@isentinel/eslint-config] Both '${first}' and '${second}' found in ${directory}. Keep one oxfmt config file.`);
|
|
271
|
+
if (first !== void 0) return {
|
|
272
|
+
isModule: OXFMT_CONFIG_MODULES.some((name) => name === first),
|
|
273
|
+
path: path.join(directory, first)
|
|
274
|
+
};
|
|
275
|
+
const parent = path.dirname(directory);
|
|
276
|
+
if (parent === directory) return;
|
|
277
|
+
directory = parent;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/lint-cli/lib/cli/file-list.ts
|
|
282
|
+
/** The `--files0-from` value that names stdin rather than a file. */
|
|
283
|
+
const STDIN_SOURCE = "-";
|
|
284
|
+
/**
|
|
285
|
+
* Split a NUL-delimited, UTF-8 path list (the `git ls-files -z` / `find
|
|
286
|
+
* -print0` shape) into its paths.
|
|
287
|
+
*
|
|
288
|
+
* Every path is terminated by a NUL, though the last terminator may be left off
|
|
289
|
+
* as GNU's own `--files0-from` allows. Nothing else is interpreted: spaces,
|
|
290
|
+
* newlines, quotes, shell metacharacters and leading hyphens all stay part of
|
|
291
|
+
* the name. The bytes must be valid UTF-8 — a lossy decode would lint a file
|
|
292
|
+
* that is not the one selected — and no entry may be empty, since an empty
|
|
293
|
+
* path is never a file and most likely a broken producer.
|
|
294
|
+
*
|
|
295
|
+
* @param bytes - The raw list.
|
|
296
|
+
* @returns The paths, in list order. Empty for an empty list.
|
|
297
|
+
*/
|
|
298
|
+
function decodeFileList(bytes) {
|
|
299
|
+
const decoder = new TextDecoder("utf-8", {
|
|
300
|
+
fatal: true,
|
|
301
|
+
ignoreBOM: true
|
|
302
|
+
});
|
|
303
|
+
let text;
|
|
304
|
+
try {
|
|
305
|
+
text = decoder.decode(bytes);
|
|
306
|
+
} catch {
|
|
307
|
+
throw new CliError("--files0-from: the file list is not valid UTF-8.");
|
|
308
|
+
}
|
|
309
|
+
if (text.length === 0) return [];
|
|
310
|
+
const entries = text.split("\0");
|
|
311
|
+
if (entries.at(-1) === "") entries.pop();
|
|
312
|
+
const empty = entries.indexOf("");
|
|
313
|
+
if (empty !== -1) throw new CliError(`--files0-from: entry ${empty + 1} of the file list is empty; expected NUL-terminated paths.`);
|
|
314
|
+
return entries;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Fill the run's paths from its `--files0-from` list, reading it once. Options
|
|
318
|
+
* without a list are returned as they are.
|
|
319
|
+
*
|
|
320
|
+
* An empty list stays empty: the caller treats it as nothing to lint, and it
|
|
321
|
+
* must never become the whole-tree default `.`.
|
|
322
|
+
*
|
|
323
|
+
* @param options - The parsed CLI options.
|
|
324
|
+
* @param cwd - The directory a relative list path resolves against.
|
|
325
|
+
* @param stdin - The stream `-` reads.
|
|
326
|
+
* @returns The options with `paths` set from the list.
|
|
327
|
+
* @rejects {CliError} When the list cannot be read or is malformed.
|
|
328
|
+
*/
|
|
329
|
+
async function readFileList(options, cwd, stdin) {
|
|
330
|
+
const source = options.filesFrom;
|
|
331
|
+
if (source === void 0) return options;
|
|
332
|
+
const bytes = source === STDIN_SOURCE ? await readAll(stdin) : await readFile(source, cwd);
|
|
333
|
+
return {
|
|
334
|
+
...options,
|
|
335
|
+
paths: decodeFileList(bytes)
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
async function readFile(source, cwd) {
|
|
339
|
+
try {
|
|
340
|
+
return await fs$1.readFile(path.resolve(cwd, source));
|
|
341
|
+
} catch (err) {
|
|
342
|
+
throw new CliError(`--files0-from: cannot read "${source}": ${err instanceof Error ? err.message : String(err)}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async function readAll(stream) {
|
|
346
|
+
const chunks = [];
|
|
347
|
+
for await (const chunk of stream) if (typeof chunk === "string") chunks.push(Buffer$1.from(chunk, "utf8"));
|
|
348
|
+
else if (chunk instanceof Uint8Array) chunks.push(Buffer$1.from(chunk));
|
|
349
|
+
else throw new CliError("--files0-from: stdin produced a non-byte chunk.");
|
|
350
|
+
return Buffer$1.concat(chunks);
|
|
351
|
+
}
|
|
304
352
|
//#endregion
|
|
305
353
|
//#region src/lint-cli/lib/cli/parse.ts
|
|
306
354
|
/**
|
|
@@ -412,6 +460,10 @@ function parseArguments(argv, environment = process$1.env) {
|
|
|
412
460
|
default: true,
|
|
413
461
|
description: "Use ESLint's on-disk cache.",
|
|
414
462
|
type: "boolean"
|
|
463
|
+
}).option("oxfmt", {
|
|
464
|
+
default: true,
|
|
465
|
+
description: "Run oxfmt when an oxfmt.config.* file exists.",
|
|
466
|
+
type: "boolean"
|
|
415
467
|
}).option("oxlint-type-aware", {
|
|
416
468
|
default: true,
|
|
417
469
|
description: "Pass --type-aware to oxlint.",
|
|
@@ -456,6 +508,7 @@ function parseArguments(argv, environment = process$1.env) {
|
|
|
456
508
|
eslintArgs,
|
|
457
509
|
filesFrom,
|
|
458
510
|
fix,
|
|
511
|
+
oxfmt: parsed.oxfmt,
|
|
459
512
|
oxlint: oxlintOnly,
|
|
460
513
|
oxlintArgs,
|
|
461
514
|
oxlintTypeAware: parsed.oxlintTypeAware,
|
|
@@ -570,6 +623,188 @@ function resolveRunContext(cwd, environment, mutate) {
|
|
|
570
623
|
};
|
|
571
624
|
}
|
|
572
625
|
//#endregion
|
|
626
|
+
//#region src/lint-cli/lib/plan/heap.ts
|
|
627
|
+
/**
|
|
628
|
+
* Environment variable that sets the heap of a program-building ESLint child.
|
|
629
|
+
*/
|
|
630
|
+
const HEAP_ENV = "LINT_TYPED_HEAP_MB";
|
|
631
|
+
/**
|
|
632
|
+
* Share of the machine's memory the program-building workers may claim between
|
|
633
|
+
* them. The rest is left for the oxlint child, the fast pass, the editor and
|
|
634
|
+
* the OS, all of which run alongside the type-aware pass locally.
|
|
635
|
+
*/
|
|
636
|
+
const HEAP_BUDGET_SHARE = .5;
|
|
637
|
+
const BYTES_PER_MB = 1024 * 1024;
|
|
638
|
+
const MAX_OLD_SPACE = /--max[-_]old[-_]space[-_]size\b/;
|
|
639
|
+
/**
|
|
640
|
+
* Read the machine's memory facts.
|
|
641
|
+
*
|
|
642
|
+
* @returns The available memory and V8's default heap limit, in MB.
|
|
643
|
+
*/
|
|
644
|
+
function probeMemory() {
|
|
645
|
+
const total = os.totalmem();
|
|
646
|
+
const constrained = process$1.constrainedMemory();
|
|
647
|
+
return {
|
|
648
|
+
availableMb: Math.floor((constrained > 0 ? Math.min(total, constrained) : total) / BYTES_PER_MB),
|
|
649
|
+
defaultHeapMb: Math.floor(v8.getHeapStatistics().heap_size_limit / BYTES_PER_MB)
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Resolve the heap setting from the environment. An invalid {@link HEAP_ENV}
|
|
654
|
+
* falls back to `auto`, as the other numeric knobs fall back to their
|
|
655
|
+
* defaults.
|
|
656
|
+
*
|
|
657
|
+
* @param environment - Where {@link HEAP_ENV} and `NODE_OPTIONS` are read.
|
|
658
|
+
* @returns Which of the four modes applies, with its size when fixed.
|
|
659
|
+
*/
|
|
660
|
+
function resolveHeapSetting(environment) {
|
|
661
|
+
if (MAX_OLD_SPACE.test(environment["NODE_OPTIONS"] ?? "")) return { kind: "inherited" };
|
|
662
|
+
const raw = environment[HEAP_ENV]?.trim();
|
|
663
|
+
if (raw === "off") return { kind: "off" };
|
|
664
|
+
const mb = parseBoundedInteger(raw, 1);
|
|
665
|
+
return mb === void 0 ? { kind: "auto" } : {
|
|
666
|
+
kind: "fixed",
|
|
667
|
+
mb
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* The memory all program-building workers may claim together, in MB.
|
|
672
|
+
*
|
|
673
|
+
* @param memory - What the machine offers.
|
|
674
|
+
* @returns The budget share of the available memory, in MB.
|
|
675
|
+
*/
|
|
676
|
+
function heapBudgetMb(memory) {
|
|
677
|
+
return Math.floor(memory.availableMb * HEAP_BUDGET_SHARE);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* The most program-building workers the budget can hold, or `undefined` when
|
|
681
|
+
* the setting places no limit. Each worker is counted at the heap it may grow
|
|
682
|
+
* to: the requested size, or {@link AUTO_HEAP_FLOOR} times Node's default
|
|
683
|
+
* under `auto`. Never below one.
|
|
684
|
+
*
|
|
685
|
+
* @param budget - The setting and the machine it applies to.
|
|
686
|
+
* @returns How many workers fit, if the setting limits them.
|
|
687
|
+
*/
|
|
688
|
+
function heapWorkerCap({ memory, setting }) {
|
|
689
|
+
if (setting.kind !== "auto" && setting.kind !== "fixed") return;
|
|
690
|
+
const perWorker = setting.kind === "fixed" ? setting.mb : memory.defaultHeapMb * 2;
|
|
691
|
+
return Math.max(1, Math.floor(heapBudgetMb(memory) / perWorker));
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* The heap each worker of a program-building pass gets, or `undefined` for
|
|
695
|
+
* Node's default. `auto` splits the budget across the workers and only raises
|
|
696
|
+
* the limit: a share below the default adds no flag.
|
|
697
|
+
*
|
|
698
|
+
* @param budget - The setting and the machine it applies to.
|
|
699
|
+
* @param concurrency - The pass's resolved concurrency (`off` lints on the
|
|
700
|
+
* main thread, which is one heap).
|
|
701
|
+
* @returns The size in MB to pass to Node, if it should change.
|
|
702
|
+
*/
|
|
703
|
+
function heapPerWorkerMb({ memory, setting }, concurrency) {
|
|
704
|
+
if (setting.kind === "fixed") return setting.mb;
|
|
705
|
+
if (setting.kind !== "auto") return;
|
|
706
|
+
const share = Math.floor(heapBudgetMb(memory) / workerCount(concurrency));
|
|
707
|
+
return share > memory.defaultHeapMb ? share : void 0;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Pair the run's heap setting with the machine it applies to.
|
|
711
|
+
*
|
|
712
|
+
* @param environment - Where {@link HEAP_ENV} and `NODE_OPTIONS` are read.
|
|
713
|
+
* @param memory - What the machine offers.
|
|
714
|
+
* @returns Everything sizing needs to give a program-building pass its heap.
|
|
715
|
+
*/
|
|
716
|
+
function resolveHeapBudget(environment, memory) {
|
|
717
|
+
return {
|
|
718
|
+
memory,
|
|
719
|
+
nodeOptions: environment["NODE_OPTIONS"],
|
|
720
|
+
setting: resolveHeapSetting(environment)
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* The worker cap of a program-building pass once the heap budget is applied.
|
|
725
|
+
* An explicit `LINT_MAX_WORKERS` is never tightened.
|
|
726
|
+
*
|
|
727
|
+
* @param maxWorkers - The pass's cap before the budget.
|
|
728
|
+
* @param explicit - Whether `LINT_MAX_WORKERS` set that cap.
|
|
729
|
+
* @param budget - The setting and the machine it applies to.
|
|
730
|
+
* @returns The cap to size the pass against.
|
|
731
|
+
*/
|
|
732
|
+
function capWorkersForHeap(maxWorkers, explicit, budget) {
|
|
733
|
+
const cap = explicit ? void 0 : heapWorkerCap(budget);
|
|
734
|
+
return cap === void 0 ? maxWorkers : Math.min(maxWorkers, cap);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Plan the heap of one program-building child from its sizing.
|
|
738
|
+
*
|
|
739
|
+
* @param label - The child's label, for the `--print` line.
|
|
740
|
+
* @param budget - The setting and the machine it applies to.
|
|
741
|
+
* @param sizing - The child's concurrency and file count.
|
|
742
|
+
* @returns Its `NODE_OPTIONS`, its `--print` line and whether it is watched.
|
|
743
|
+
*/
|
|
744
|
+
function planPassHeap(label, budget, sizing) {
|
|
745
|
+
const heapMb = heapPerWorkerMb(budget, sizing.concurrency);
|
|
746
|
+
return {
|
|
747
|
+
nodeOptions: heapNodeOptions(budget, heapMb),
|
|
748
|
+
note: describeHeapPlan(label, budget, sizing, heapMb),
|
|
749
|
+
watch: budget.setting.kind !== "off"
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* The number of heaps a concurrency value runs: `off` lints on the main
|
|
754
|
+
* thread, which is one.
|
|
755
|
+
*
|
|
756
|
+
* @param concurrency - A resolved ESLint concurrency.
|
|
757
|
+
* @returns How many threads lint.
|
|
758
|
+
*/
|
|
759
|
+
function workerCount(concurrency) {
|
|
760
|
+
return concurrency === "off" ? 1 : concurrency;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* The `NODE_OPTIONS` a program-building child runs under: the inherited value
|
|
764
|
+
* with the heap flag appended. Node's `worker_threads` take their heap limit
|
|
765
|
+
* from `--max-old-space-size` when it is set, so this one flag reaches every
|
|
766
|
+
* ESLint worker.
|
|
767
|
+
*
|
|
768
|
+
* @param budget - Carries the inherited `NODE_OPTIONS`.
|
|
769
|
+
* @param heapMb - The size in MB for each worker, if it should change.
|
|
770
|
+
* @returns The value to set, or `undefined` to leave `NODE_OPTIONS` alone.
|
|
771
|
+
*/
|
|
772
|
+
function heapNodeOptions({ nodeOptions }, heapMb) {
|
|
773
|
+
if (heapMb === void 0) return;
|
|
774
|
+
const flag = `--max-old-space-size=${String(heapMb)}`;
|
|
775
|
+
const inherited = nodeOptions?.trim() ?? "";
|
|
776
|
+
return inherited.length > 0 ? `${inherited} ${flag}` : flag;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* How the `--print` line names a heap the runner left alone.
|
|
780
|
+
*
|
|
781
|
+
* @param budget - Says whether the user's `NODE_OPTIONS` set it.
|
|
782
|
+
* @returns The user's heap, or Node's default with its size.
|
|
783
|
+
*/
|
|
784
|
+
function unchangedHeap({ memory, setting }) {
|
|
785
|
+
return setting.kind === "inherited" ? "the NODE_OPTIONS heap" : `Node's default heap (${String(memory.defaultHeapMb)} MB)`;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Describe the memory plan of a program-building pass for `--print`.
|
|
789
|
+
*
|
|
790
|
+
* @param label - The pass's child label.
|
|
791
|
+
* @param budget - The setting and the machine it applies to.
|
|
792
|
+
* @param sizing - The pass's concurrency and file count.
|
|
793
|
+
* @param heapMb - The size given to each worker, if it changed.
|
|
794
|
+
* @returns One stderr line naming the workers, files per worker and heap.
|
|
795
|
+
*/
|
|
796
|
+
function describeHeapPlan(label, budget, { concurrency, dirtyCount }, heapMb) {
|
|
797
|
+
const workers = workerCount(concurrency);
|
|
798
|
+
const heap = heapMb === void 0 ? unchangedHeap(budget) : `${String(heapMb)} MB heap`;
|
|
799
|
+
const source = {
|
|
800
|
+
auto: `auto, budget ${String(heapBudgetMb(budget.memory))} MB`,
|
|
801
|
+
fixed: HEAP_ENV,
|
|
802
|
+
inherited: "NODE_OPTIONS",
|
|
803
|
+
off: `${HEAP_ENV}=off`
|
|
804
|
+
}[budget.setting.kind];
|
|
805
|
+
return `isentinel-lint: ${label} pass: ${String(workers)} worker(s), ~${String(Math.ceil(dirtyCount / workers))} files per worker, ${heap} per worker (${source}).\n`;
|
|
806
|
+
}
|
|
807
|
+
//#endregion
|
|
573
808
|
//#region src/lint-cli/lib/state.ts
|
|
574
809
|
/**
|
|
575
810
|
* Directory (relative to the project root) where the CLI keeps its persisted
|
|
@@ -610,7 +845,7 @@ function statePath(cwd, name, ...parts) {
|
|
|
610
845
|
*/
|
|
611
846
|
function readFileIfPresent(filePath) {
|
|
612
847
|
try {
|
|
613
|
-
return fs
|
|
848
|
+
return fs.readFileSync(filePath, "utf8");
|
|
614
849
|
} catch {
|
|
615
850
|
return;
|
|
616
851
|
}
|
|
@@ -651,12 +886,12 @@ function writeState(filePath, data) {
|
|
|
651
886
|
})}\n`;
|
|
652
887
|
const temporary = `${filePath}.${process$1.pid}.tmp`;
|
|
653
888
|
try {
|
|
654
|
-
fs
|
|
655
|
-
fs
|
|
656
|
-
fs
|
|
889
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
890
|
+
fs.writeFileSync(temporary, content);
|
|
891
|
+
fs.renameSync(temporary, filePath);
|
|
657
892
|
} catch {
|
|
658
893
|
try {
|
|
659
|
-
fs
|
|
894
|
+
fs.rmSync(temporary, { force: true });
|
|
660
895
|
} catch {}
|
|
661
896
|
}
|
|
662
897
|
}
|
|
@@ -691,7 +926,7 @@ function swapState(filePath, value) {
|
|
|
691
926
|
function touchState(filePath) {
|
|
692
927
|
try {
|
|
693
928
|
const now = Date.now() / 1e3;
|
|
694
|
-
fs
|
|
929
|
+
fs.utimesSync(filePath, now, now);
|
|
695
930
|
} catch {}
|
|
696
931
|
}
|
|
697
932
|
/**
|
|
@@ -784,7 +1019,7 @@ function resolveAgentsFormatter() {
|
|
|
784
1019
|
*/
|
|
785
1020
|
function resolveIgnoredHelper() {
|
|
786
1021
|
const built = fileURLToPath(new URL("./lint-ignored.mjs", import.meta.url));
|
|
787
|
-
return fs
|
|
1022
|
+
return fs.existsSync(built) ? built : fileURLToPath(new URL("../../ignored-child.ts", import.meta.url));
|
|
788
1023
|
}
|
|
789
1024
|
/**
|
|
790
1025
|
* Resolve the engine bootstrap shipped alongside this entry (built from
|
|
@@ -800,7 +1035,7 @@ function resolveIgnoredHelper() {
|
|
|
800
1035
|
*/
|
|
801
1036
|
function resolveBootstrap() {
|
|
802
1037
|
const built = fileURLToPath(new URL("./lint-bootstrap.mjs", import.meta.url));
|
|
803
|
-
return fs
|
|
1038
|
+
return fs.existsSync(built) ? built : fileURLToPath(new URL("../../bootstrap.ts", import.meta.url));
|
|
804
1039
|
}
|
|
805
1040
|
/**
|
|
806
1041
|
* The path a package's manifest declares as the bin of the package's own name,
|
|
@@ -848,6 +1083,11 @@ function resolveManifest(name, cwd) {
|
|
|
848
1083
|
}
|
|
849
1084
|
//#endregion
|
|
850
1085
|
//#region src/lint-cli/lib/exec/execute.ts
|
|
1086
|
+
/**
|
|
1087
|
+
* What V8 writes when a heap fills: `ERR_WORKER_OUT_OF_MEMORY` from a worker,
|
|
1088
|
+
* `JavaScript heap out of memory` from the main thread.
|
|
1089
|
+
*/
|
|
1090
|
+
const OUT_OF_MEMORY = /ERR_WORKER_OUT_OF_MEMORY|heap out of memory/;
|
|
851
1091
|
/** The signals that cancel a run, matching `concurrently`'s `KillOnSignal`. */
|
|
852
1092
|
const CANCEL_SIGNALS = [
|
|
853
1093
|
"SIGHUP",
|
|
@@ -874,9 +1114,21 @@ const PREFIX_COLOR = {
|
|
|
874
1114
|
eslint: "blue",
|
|
875
1115
|
fast: "blue",
|
|
876
1116
|
oxc: "magenta",
|
|
1117
|
+
oxfmt: "yellow",
|
|
877
1118
|
typed: "cyan"
|
|
878
1119
|
};
|
|
879
1120
|
/**
|
|
1121
|
+
* The hint printed after a program-building child ran out of heap, below the
|
|
1122
|
+
* engine's own stack trace.
|
|
1123
|
+
*
|
|
1124
|
+
* @param label - The child's label.
|
|
1125
|
+
* @returns One stderr line.
|
|
1126
|
+
*/
|
|
1127
|
+
function outOfMemoryHint(label) {
|
|
1128
|
+
return `isentinel-lint: the ${label} ESLint pass ran out of memory. Set ${HEAP_ENV}=<mb> to give each worker more heap; the runner starts fewer workers to keep them in budget.
|
|
1129
|
+
`;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
880
1132
|
* Run the composed children and aggregate their exit codes.
|
|
881
1133
|
*
|
|
882
1134
|
* Sequential when the caller asks for it: a lone child gains nothing from the
|
|
@@ -1045,6 +1297,27 @@ function trackCancellation(onSignal) {
|
|
|
1045
1297
|
};
|
|
1046
1298
|
}
|
|
1047
1299
|
/**
|
|
1300
|
+
* Watch a child's stderr for V8's out-of-memory report. Reads alongside
|
|
1301
|
+
* whatever else consumes the stream, and never alters it.
|
|
1302
|
+
*
|
|
1303
|
+
* @param child - The spawned child.
|
|
1304
|
+
* @param onExhausted - Called once, when the report is seen.
|
|
1305
|
+
*/
|
|
1306
|
+
function watchOutOfMemory(child, onExhausted) {
|
|
1307
|
+
let tail = "";
|
|
1308
|
+
let seen = false;
|
|
1309
|
+
child.stderr?.on("data", (chunk) => {
|
|
1310
|
+
if (seen) return;
|
|
1311
|
+
const text = tail + String(chunk);
|
|
1312
|
+
if (OUT_OF_MEMORY.test(text)) {
|
|
1313
|
+
seen = true;
|
|
1314
|
+
onExhausted();
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
tail = text.slice(-64);
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1048
1321
|
* Start every command as one `concurrently` group and announce the children.
|
|
1049
1322
|
* Unlike the previous `killOthersOn: ["failure"]` behaviour, an ordinary lint
|
|
1050
1323
|
* failure in one child no longer kills its siblings — each runs to the end so
|
|
@@ -1075,6 +1348,7 @@ async function startGroup(commands, cwd, outputStream = process$1.stdout) {
|
|
|
1075
1348
|
}];
|
|
1076
1349
|
}));
|
|
1077
1350
|
const logger = new Logger({});
|
|
1351
|
+
const exhausted = /* @__PURE__ */ new Set();
|
|
1078
1352
|
const abortController = new AbortController();
|
|
1079
1353
|
const stopTracking = trackCancellation();
|
|
1080
1354
|
const { result } = createConcurrently(Array.from(launches, ([name, { command }]) => {
|
|
@@ -1103,7 +1377,11 @@ async function startGroup(commands, cwd, outputStream = process$1.stdout) {
|
|
|
1103
1377
|
spawn: (name, options) => {
|
|
1104
1378
|
const launch = launches.get(name);
|
|
1105
1379
|
if (launch === void 0) throw new Error(`isentinel-lint: no engine registered as "${name}".`);
|
|
1106
|
-
|
|
1380
|
+
const child = launchEngine(launch, options);
|
|
1381
|
+
if (launch.command.watchHeap) watchOutOfMemory(child, () => {
|
|
1382
|
+
exhausted.add(launch.command.label);
|
|
1383
|
+
});
|
|
1384
|
+
return child;
|
|
1107
1385
|
}
|
|
1108
1386
|
});
|
|
1109
1387
|
for (const command of commands) announce(command);
|
|
@@ -1114,6 +1392,7 @@ async function startGroup(commands, cwd, outputStream = process$1.stdout) {
|
|
|
1114
1392
|
return 1;
|
|
1115
1393
|
} finally {
|
|
1116
1394
|
stopTracking();
|
|
1395
|
+
for (const label of exhausted) process$1.stderr.write(outOfMemoryHint(label));
|
|
1117
1396
|
}
|
|
1118
1397
|
}
|
|
1119
1398
|
function signalExitCode(signal) {
|
|
@@ -1130,6 +1409,8 @@ function cancelledCode() {
|
|
|
1130
1409
|
}
|
|
1131
1410
|
async function spawnChild(command, cwd) {
|
|
1132
1411
|
const bin = resolveLocalBin(command.bin, cwd);
|
|
1412
|
+
const watched = command.watchHeap;
|
|
1413
|
+
let exhausted = false;
|
|
1133
1414
|
return new Promise((resolve) => {
|
|
1134
1415
|
const child = launchEngine({
|
|
1135
1416
|
bin,
|
|
@@ -1140,8 +1421,20 @@ async function spawnChild(command, cwd) {
|
|
|
1140
1421
|
...process$1.env,
|
|
1141
1422
|
...command.env
|
|
1142
1423
|
},
|
|
1143
|
-
stdio:
|
|
1424
|
+
stdio: [
|
|
1425
|
+
"inherit",
|
|
1426
|
+
"inherit",
|
|
1427
|
+
watched ? "pipe" : "inherit"
|
|
1428
|
+
]
|
|
1144
1429
|
});
|
|
1430
|
+
if (watched) {
|
|
1431
|
+
child.stderr?.on("data", (chunk) => {
|
|
1432
|
+
process$1.stderr.write(chunk);
|
|
1433
|
+
});
|
|
1434
|
+
watchOutOfMemory(child, () => {
|
|
1435
|
+
exhausted = true;
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1145
1438
|
announce(command);
|
|
1146
1439
|
const stopTracking = trackCancellation((signal) => {
|
|
1147
1440
|
child.kill(signal);
|
|
@@ -1152,6 +1445,7 @@ async function spawnChild(command, cwd) {
|
|
|
1152
1445
|
});
|
|
1153
1446
|
child.on("close", (code, signal) => {
|
|
1154
1447
|
stopTracking();
|
|
1448
|
+
if (exhausted) process$1.stderr.write(outOfMemoryHint(command.label));
|
|
1155
1449
|
resolve(code ?? (signal === null ? 1 : signalExitCode(signal)));
|
|
1156
1450
|
});
|
|
1157
1451
|
});
|
|
@@ -1236,7 +1530,37 @@ function composeOxlintCommand(options, context) {
|
|
|
1236
1530
|
args,
|
|
1237
1531
|
bin: "oxlint",
|
|
1238
1532
|
env: {},
|
|
1239
|
-
label: "oxc"
|
|
1533
|
+
label: "oxc",
|
|
1534
|
+
watchHeap: false
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Compose the oxfmt child command. Oxfmt receives the run's own targets and
|
|
1539
|
+
* applies its config's `ignorePatterns` to them, so a whole-tree run passes
|
|
1540
|
+
* `.` and lets oxfmt walk the tree itself. Like every engine child it is
|
|
1541
|
+
* launched through the bootstrap, so a long file list never reaches the OS
|
|
1542
|
+
* command line..
|
|
1543
|
+
*
|
|
1544
|
+
* @param options - The parsed CLI options.
|
|
1545
|
+
* @param context - The child's settings.
|
|
1546
|
+
* @param context.configPath - The config module, passed as `--config` so oxfmt
|
|
1547
|
+
* uses the file the lint factories read.
|
|
1548
|
+
* @param context.write - Format the files in place rather than check them.
|
|
1549
|
+
* @returns The oxfmt child command.
|
|
1550
|
+
*/
|
|
1551
|
+
function composeOxfmtCommand(options, { configPath, write }) {
|
|
1552
|
+
return {
|
|
1553
|
+
args: [
|
|
1554
|
+
write ? "--write" : "--check",
|
|
1555
|
+
"--config",
|
|
1556
|
+
configPath,
|
|
1557
|
+
"--no-error-on-unmatched-pattern",
|
|
1558
|
+
...pathArguments(options.paths)
|
|
1559
|
+
],
|
|
1560
|
+
bin: "oxfmt",
|
|
1561
|
+
env: {},
|
|
1562
|
+
label: "oxfmt",
|
|
1563
|
+
watchHeap: false
|
|
1240
1564
|
};
|
|
1241
1565
|
}
|
|
1242
1566
|
/**
|
|
@@ -1254,11 +1578,15 @@ function composeEslintCommand(options, context) {
|
|
|
1254
1578
|
if (options.agents) args.push("--format", context.agentsFormatterPath);
|
|
1255
1579
|
if (context.fix) args.push("--fix");
|
|
1256
1580
|
args.push(...options.eslintArgs, ...pathArguments(context.paths));
|
|
1581
|
+
const environment = { ESLINT_TYPE_AWARE: context.typeAwareEnv };
|
|
1582
|
+
if (context.quietWarnings === true) environment[QUIET_WARNINGS_ENV] = "1";
|
|
1583
|
+
if (context.heap?.nodeOptions !== void 0) environment["NODE_OPTIONS"] = context.heap.nodeOptions;
|
|
1257
1584
|
return {
|
|
1258
1585
|
args,
|
|
1259
1586
|
bin: "eslint",
|
|
1260
|
-
env:
|
|
1261
|
-
label: context.eslintLabel
|
|
1587
|
+
env: environment,
|
|
1588
|
+
label: context.eslintLabel,
|
|
1589
|
+
watchHeap: context.heap?.watch ?? false
|
|
1262
1590
|
};
|
|
1263
1591
|
}
|
|
1264
1592
|
/**
|
|
@@ -1306,7 +1634,9 @@ function composePasses(passes, runPlan, options) {
|
|
|
1306
1634
|
concurrency: pass.concurrency,
|
|
1307
1635
|
eslintLabel: pass.descriptor.label,
|
|
1308
1636
|
fix: false,
|
|
1637
|
+
heap: pass.heap,
|
|
1309
1638
|
paths: options.paths,
|
|
1639
|
+
quietWarnings: commands.length > 0,
|
|
1310
1640
|
typeAwareEnv: pass.descriptor.typeAwareEnv
|
|
1311
1641
|
}));
|
|
1312
1642
|
}
|
|
@@ -1440,7 +1770,7 @@ function applyHashBust(run, bust, hash) {
|
|
|
1440
1770
|
const cleared = [];
|
|
1441
1771
|
for (const base of bust.caches) {
|
|
1442
1772
|
const cacheFilePath = path.resolve(run.cwd, cacheFileFor(base, run.key));
|
|
1443
|
-
fs
|
|
1773
|
+
fs.rmSync(cacheFilePath, { force: true });
|
|
1444
1774
|
cleared.push(cacheFilePath);
|
|
1445
1775
|
}
|
|
1446
1776
|
return {
|
|
@@ -1717,7 +2047,7 @@ function normalizePath(filePath, platform = process$1.platform) {
|
|
|
1717
2047
|
* @returns The loaded cache, or `undefined` when the file does not exist.
|
|
1718
2048
|
*/
|
|
1719
2049
|
function openCache(cacheFilePath, useChecksum) {
|
|
1720
|
-
if (!fs
|
|
2050
|
+
if (!fs.existsSync(cacheFilePath)) return;
|
|
1721
2051
|
const cache = load(cacheFilePath, useChecksum);
|
|
1722
2052
|
return {
|
|
1723
2053
|
filesWithMessages: (files) => filesWithMessagesIn(cache, files),
|
|
@@ -1727,7 +2057,7 @@ function openCache(cacheFilePath, useChecksum) {
|
|
|
1727
2057
|
}
|
|
1728
2058
|
function safeMtimeMs(filePath) {
|
|
1729
2059
|
try {
|
|
1730
|
-
return fs
|
|
2060
|
+
return fs.statSync(filePath).mtimeMs;
|
|
1731
2061
|
} catch {
|
|
1732
2062
|
return;
|
|
1733
2063
|
}
|
|
@@ -1744,7 +2074,7 @@ function safeMtimeMs(filePath) {
|
|
|
1744
2074
|
function listCacheFiles(cwd) {
|
|
1745
2075
|
let entries;
|
|
1746
2076
|
try {
|
|
1747
|
-
entries = fs
|
|
2077
|
+
entries = fs.readdirSync(cwd);
|
|
1748
2078
|
} catch {
|
|
1749
2079
|
return [];
|
|
1750
2080
|
}
|
|
@@ -1752,7 +2082,7 @@ function listCacheFiles(cwd) {
|
|
|
1752
2082
|
}
|
|
1753
2083
|
function removeCacheFile(cacheFilePath) {
|
|
1754
2084
|
try {
|
|
1755
|
-
fs
|
|
2085
|
+
fs.rmSync(cacheFilePath, { force: true });
|
|
1756
2086
|
} catch {}
|
|
1757
2087
|
}
|
|
1758
2088
|
/**
|
|
@@ -3761,7 +4091,7 @@ const WALK_BUDGET = 8192;
|
|
|
3761
4091
|
function findWorkspaceRoot(cwd) {
|
|
3762
4092
|
let current = cwd;
|
|
3763
4093
|
for (;;) {
|
|
3764
|
-
for (const marker of WORKSPACE_ROOT_MARKERS) if (fs
|
|
4094
|
+
for (const marker of WORKSPACE_ROOT_MARKERS) if (fs.existsSync(path.join(current, marker))) return current;
|
|
3765
4095
|
const parent = path.dirname(current);
|
|
3766
4096
|
if (parent === current) return cwd;
|
|
3767
4097
|
current = parent;
|
|
@@ -3805,7 +4135,7 @@ function findWorkspaceMembers(root) {
|
|
|
3805
4135
|
function walk(directory, depth) {
|
|
3806
4136
|
let entries;
|
|
3807
4137
|
try {
|
|
3808
|
-
entries = fs
|
|
4138
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
3809
4139
|
} catch {
|
|
3810
4140
|
return true;
|
|
3811
4141
|
}
|
|
@@ -3815,7 +4145,7 @@ function findWorkspaceMembers(root) {
|
|
|
3815
4145
|
if (budget < 0) return false;
|
|
3816
4146
|
const absolute = path.join(directory, entry.name);
|
|
3817
4147
|
const relative = toPosix(path.relative(root, absolute));
|
|
3818
|
-
if (isMember(relative) && fs
|
|
4148
|
+
if (isMember(relative) && fs.existsSync(path.join(absolute, "package.json"))) members.push(absolute);
|
|
3819
4149
|
if (depth < maxDepth && !walk(absolute, depth + 1)) return false;
|
|
3820
4150
|
}
|
|
3821
4151
|
return true;
|
|
@@ -4033,6 +4363,8 @@ const CACHE_BUST_PATTERNS = [
|
|
|
4033
4363
|
"*.config.*",
|
|
4034
4364
|
"**/tsconfig*.json",
|
|
4035
4365
|
".oxlintrc*",
|
|
4366
|
+
".oxfmtrc*",
|
|
4367
|
+
"oxfmt.config.*",
|
|
4036
4368
|
".prettierrc*",
|
|
4037
4369
|
"pnpm-lock.yaml",
|
|
4038
4370
|
"package-lock.json",
|
|
@@ -4143,7 +4475,7 @@ function oxlintTargets(cwd, targets) {
|
|
|
4143
4475
|
const extension = path.extname(target).toLowerCase();
|
|
4144
4476
|
if (extension === "" || TYPE_AWARE_EXTENSION_SET.has(extension)) return true;
|
|
4145
4477
|
try {
|
|
4146
|
-
return fs
|
|
4478
|
+
return fs.statSync(path.resolve(cwd, target)).isDirectory();
|
|
4147
4479
|
} catch {
|
|
4148
4480
|
return false;
|
|
4149
4481
|
}
|
|
@@ -4180,7 +4512,7 @@ function collectAncestorBustFiles(cwd) {
|
|
|
4180
4512
|
for (;;) {
|
|
4181
4513
|
let entries = [];
|
|
4182
4514
|
try {
|
|
4183
|
-
entries = fs
|
|
4515
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
4184
4516
|
} catch {
|
|
4185
4517
|
entries = [];
|
|
4186
4518
|
}
|
|
@@ -4269,7 +4601,7 @@ function gitListFiles(cwd, pathSpecs) {
|
|
|
4269
4601
|
function walkDirectory(root, current, accumulator) {
|
|
4270
4602
|
let entries;
|
|
4271
4603
|
try {
|
|
4272
|
-
entries = fs
|
|
4604
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
4273
4605
|
} catch {
|
|
4274
4606
|
return;
|
|
4275
4607
|
}
|
|
@@ -4288,7 +4620,7 @@ function walkFallback(cwd, targets) {
|
|
|
4288
4620
|
const absolute = path.resolve(cwd, target);
|
|
4289
4621
|
let stat;
|
|
4290
4622
|
try {
|
|
4291
|
-
stat = fs
|
|
4623
|
+
stat = fs.statSync(absolute);
|
|
4292
4624
|
} catch {
|
|
4293
4625
|
continue;
|
|
4294
4626
|
}
|
|
@@ -4613,7 +4945,7 @@ function queryIgnoredFiles(cwd, targets) {
|
|
|
4613
4945
|
"ignore"
|
|
4614
4946
|
]
|
|
4615
4947
|
});
|
|
4616
|
-
const parsed = JSON.parse(fs
|
|
4948
|
+
const parsed = JSON.parse(fs.readFileSync(outFile, "utf8"));
|
|
4617
4949
|
if (!isRecord(parsed)) return;
|
|
4618
4950
|
const { basePath, entries, ignored, mode } = parsed;
|
|
4619
4951
|
if (mode === "answers" && isStringArray(ignored)) return {
|
|
@@ -4629,7 +4961,7 @@ function queryIgnoredFiles(cwd, targets) {
|
|
|
4629
4961
|
} catch {
|
|
4630
4962
|
return;
|
|
4631
4963
|
} finally {
|
|
4632
|
-
fs
|
|
4964
|
+
fs.rmSync(outFile, { force: true });
|
|
4633
4965
|
}
|
|
4634
4966
|
}
|
|
4635
4967
|
//#endregion
|
|
@@ -4672,7 +5004,7 @@ function readHybridStatus(cwd) {
|
|
|
4672
5004
|
function writeHybridStatus(cwd, oxlint) {
|
|
4673
5005
|
let installed;
|
|
4674
5006
|
try {
|
|
4675
|
-
installed = fs
|
|
5007
|
+
installed = fs.existsSync(path.resolve(cwd, "node_modules"));
|
|
4676
5008
|
} catch {
|
|
4677
5009
|
return;
|
|
4678
5010
|
}
|
|
@@ -4851,9 +5183,10 @@ function resolveWorkerLimits(environment, availableParallelism, ci) {
|
|
|
4851
5183
|
const explicit = parsePositiveInteger(environment["LINT_MAX_WORKERS"]);
|
|
4852
5184
|
const maxWorkers = explicit ?? (ci ? availableParallelism : Math.floor(availableParallelism / 4));
|
|
4853
5185
|
return {
|
|
5186
|
+
explicitMaxWorkers: explicit !== void 0,
|
|
4854
5187
|
filesPerWorker,
|
|
4855
5188
|
maxWorkers,
|
|
4856
|
-
typedMaxWorkers: explicit ?? Math.min(maxWorkers,
|
|
5189
|
+
typedMaxWorkers: explicit ?? Math.min(maxWorkers, 5)
|
|
4857
5190
|
};
|
|
4858
5191
|
}
|
|
4859
5192
|
/**
|
|
@@ -4943,6 +5276,17 @@ function selectPasses(options, ci) {
|
|
|
4943
5276
|
return [FAST_PASS, TYPED_PASS];
|
|
4944
5277
|
}
|
|
4945
5278
|
/**
|
|
5279
|
+
* Whether a pass builds a TypeScript program in each of its workers. The pass
|
|
5280
|
+
* that runs no builder is exactly the one that builds no program, so
|
|
5281
|
+
* `invalidation` records both.
|
|
5282
|
+
*
|
|
5283
|
+
* @param descriptor - The pass.
|
|
5284
|
+
* @returns Whether its workers hold a program, and so need heap sizing.
|
|
5285
|
+
*/
|
|
5286
|
+
function buildsProgram(descriptor) {
|
|
5287
|
+
return descriptor.invalidation !== "none";
|
|
5288
|
+
}
|
|
5289
|
+
/**
|
|
4946
5290
|
* The worker cap for a pass. `invalidation` already records whether a pass
|
|
4947
5291
|
* builds a TypeScript program — the fast pass runs no builder precisely because
|
|
4948
5292
|
* it builds none — so it doubles as the cap selector rather than restating the
|
|
@@ -4953,7 +5297,7 @@ function selectPasses(options, ci) {
|
|
|
4953
5297
|
* @returns The worker cap for this pass.
|
|
4954
5298
|
*/
|
|
4955
5299
|
function maxWorkersFor(descriptor, limits) {
|
|
4956
|
-
return descriptor
|
|
5300
|
+
return buildsProgram(descriptor) ? limits.typedMaxWorkers : limits.maxWorkers;
|
|
4957
5301
|
}
|
|
4958
5302
|
//#endregion
|
|
4959
5303
|
//#region src/lint-cli/lib/plan/fix.ts
|
|
@@ -5018,13 +5362,18 @@ function collectFixTargets(passes, run, files) {
|
|
|
5018
5362
|
* @returns The fix child, or `undefined` when nothing was reported.
|
|
5019
5363
|
*/
|
|
5020
5364
|
function planFixChild(passes, run, inputs) {
|
|
5021
|
-
const { files, limits, options } = inputs;
|
|
5365
|
+
const { files, heap, limits, options } = inputs;
|
|
5022
5366
|
const covered = options.cache ? collectFixTargets(passes, run, files) : [];
|
|
5023
5367
|
const uncovered = options.cache ? files.outsideCwdTargets : options.paths;
|
|
5024
5368
|
const targets = [...covered, ...uncovered];
|
|
5025
5369
|
if (targets.length === 0) return;
|
|
5026
5370
|
const cacheFile = cacheFileFor(CACHE_FILE_DEFAULT, run.key);
|
|
5027
5371
|
if (covered.length > 0) openCache(path.resolve(run.cwd, cacheFile), run.ci)?.removeEntries(covered);
|
|
5372
|
+
const concurrency = options.concurrency ?? computeWorkerCount({
|
|
5373
|
+
dirtyCount: targets.length,
|
|
5374
|
+
filesPerWorker: limits.filesPerWorker,
|
|
5375
|
+
maxWorkers: capWorkersForHeap(limits.typedMaxWorkers, limits.explicitMaxWorkers, heap)
|
|
5376
|
+
});
|
|
5028
5377
|
return composeEslintCommand({
|
|
5029
5378
|
...options,
|
|
5030
5379
|
cache: options.cache && uncovered.length === 0
|
|
@@ -5032,14 +5381,15 @@ function planFixChild(passes, run, inputs) {
|
|
|
5032
5381
|
agentsFormatterPath: inputs.agentsFormatterPath,
|
|
5033
5382
|
cacheLocation: cacheFile,
|
|
5034
5383
|
ci: run.ci,
|
|
5035
|
-
concurrency
|
|
5036
|
-
dirtyCount: targets.length,
|
|
5037
|
-
filesPerWorker: limits.filesPerWorker,
|
|
5038
|
-
maxWorkers: limits.typedMaxWorkers
|
|
5039
|
-
}),
|
|
5384
|
+
concurrency,
|
|
5040
5385
|
eslintLabel: FULL_PASS.label,
|
|
5041
5386
|
fix: true,
|
|
5387
|
+
heap: planPassHeap(FULL_PASS.label, heap, {
|
|
5388
|
+
concurrency,
|
|
5389
|
+
dirtyCount: targets.length
|
|
5390
|
+
}),
|
|
5042
5391
|
paths: targets,
|
|
5392
|
+
quietWarnings: true,
|
|
5043
5393
|
typeAwareEnv: FULL_PASS.typeAwareEnv
|
|
5044
5394
|
});
|
|
5045
5395
|
}
|
|
@@ -5375,7 +5725,7 @@ function computeAffectedFiles({ key, cwd }, mode) {
|
|
|
5375
5725
|
if (entryReadable) warnOnce(`no TypeScript files resolved from ${path.basename(configPath)}; skipping type-aware cache invalidation`);
|
|
5376
5726
|
return;
|
|
5377
5727
|
}
|
|
5378
|
-
fs
|
|
5728
|
+
fs.mkdirSync(stateDirectory(cwd), { recursive: true });
|
|
5379
5729
|
const resolutionGate = computeResolutionGate(cwd);
|
|
5380
5730
|
const affected = /* @__PURE__ */ new Set();
|
|
5381
5731
|
let warmProjects = 0;
|
|
@@ -5422,7 +5772,7 @@ function computeAffectedFiles({ key, cwd }, mode) {
|
|
|
5422
5772
|
function hasBuilderState({ key, cwd }, mode) {
|
|
5423
5773
|
const prefix = `${path.basename(statePath(cwd, BUILD_INFO_STATE, modeSuffix(mode), key))}-`;
|
|
5424
5774
|
try {
|
|
5425
|
-
return fs
|
|
5775
|
+
return fs.readdirSync(stateDirectory(cwd)).some((name) => name.startsWith(prefix) && !name.endsWith(".tmp"));
|
|
5426
5776
|
} catch {
|
|
5427
5777
|
return false;
|
|
5428
5778
|
}
|
|
@@ -5637,10 +5987,10 @@ function persistBuilderState(builder, buildInfoPath) {
|
|
|
5637
5987
|
if (path.normalize(fileName) !== normalizedBuildInfo) return;
|
|
5638
5988
|
const temporary = `${fileName}.${process$1.pid}.tmp`;
|
|
5639
5989
|
try {
|
|
5640
|
-
fs
|
|
5641
|
-
fs
|
|
5990
|
+
fs.writeFileSync(temporary, data);
|
|
5991
|
+
fs.renameSync(temporary, fileName);
|
|
5642
5992
|
} catch (err) {
|
|
5643
|
-
fs
|
|
5993
|
+
fs.rmSync(temporary, { force: true });
|
|
5644
5994
|
throw err;
|
|
5645
5995
|
}
|
|
5646
5996
|
});
|
|
@@ -5655,7 +6005,7 @@ function persistBuilderState(builder, buildInfoPath) {
|
|
|
5655
6005
|
*/
|
|
5656
6006
|
function writeGateState(gatePath, gateValue) {
|
|
5657
6007
|
if (gateValue === void 0) {
|
|
5658
|
-
fs
|
|
6008
|
+
fs.rmSync(gatePath, { force: true });
|
|
5659
6009
|
return;
|
|
5660
6010
|
}
|
|
5661
6011
|
writeState(gatePath, gateValue);
|
|
@@ -5676,7 +6026,7 @@ function writeGateState(gatePath, gateValue) {
|
|
|
5676
6026
|
* @returns The affected result.
|
|
5677
6027
|
*/
|
|
5678
6028
|
function runBuilder({ buildInfoPath, gatePath, project, resolutionGate, ts }) {
|
|
5679
|
-
const firstRun = !fs
|
|
6029
|
+
const firstRun = !fs.existsSync(buildInfoPath);
|
|
5680
6030
|
const options = {
|
|
5681
6031
|
...project.options,
|
|
5682
6032
|
composite: false,
|
|
@@ -5759,7 +6109,7 @@ function applyTypeAwareInvalidation(run, { alreadyDirty, cache, cacheLocation, m
|
|
|
5759
6109
|
}
|
|
5760
6110
|
const threshold = resolveAffectedBustThreshold(run.environment);
|
|
5761
6111
|
if (affectedTargets.length > threshold) {
|
|
5762
|
-
fs
|
|
6112
|
+
fs.rmSync(cacheLocation, { force: true });
|
|
5763
6113
|
return {
|
|
5764
6114
|
busted: true,
|
|
5765
6115
|
firstRun: false,
|
|
@@ -5818,12 +6168,12 @@ function sizePasses(descriptors, run, inputs) {
|
|
|
5818
6168
|
* @returns The number of dirty files.
|
|
5819
6169
|
*/
|
|
5820
6170
|
function mutatingDirtyCount(descriptor, cacheLocation, targetFiles, { clearedCaches, run }) {
|
|
5821
|
-
const
|
|
6171
|
+
const program = buildsProgram(descriptor);
|
|
5822
6172
|
const mode = descriptor.invalidation === "only" ? "only" : void 0;
|
|
5823
|
-
if (clearedCaches.has(cacheLocation) && (!
|
|
6173
|
+
if (clearedCaches.has(cacheLocation) && (!program || hasBuilderState(run, mode))) return targetFiles.length;
|
|
5824
6174
|
const cache = openCache(cacheLocation, run.ci);
|
|
5825
6175
|
const dirty = new Set((cache?.getUpdatedFiles(targetFiles) ?? targetFiles).map((file) => normalizePath(file)));
|
|
5826
|
-
if (
|
|
6176
|
+
if (program) {
|
|
5827
6177
|
const outcome = applyTypeAwareInvalidation(run, {
|
|
5828
6178
|
alreadyDirty: dirty,
|
|
5829
6179
|
cache,
|
|
@@ -5880,17 +6230,24 @@ function sizePass(descriptor, context) {
|
|
|
5880
6230
|
const dirtyCount = passDirtyCount(descriptor, cacheFile, context);
|
|
5881
6231
|
const conservative = context.files.outsideCwdTargets.length > 0;
|
|
5882
6232
|
const filesPerWorker = descriptor.filesPerWorker(context.limits, context.run.environment);
|
|
5883
|
-
const
|
|
6233
|
+
const program = buildsProgram(descriptor);
|
|
6234
|
+
const passMaxWorkers = maxWorkersFor(descriptor, context.limits);
|
|
6235
|
+
const maxWorkers = program ? capWorkersForHeap(passMaxWorkers, context.limits.explicitMaxWorkers, context.heap) : passMaxWorkers;
|
|
5884
6236
|
const sizingDirtyCount = conservative ? maxWorkers * filesPerWorker : dirtyCount;
|
|
5885
6237
|
const concurrency = context.options.concurrency ?? computeWorkerCount({
|
|
5886
6238
|
dirtyCount: sizingDirtyCount,
|
|
5887
6239
|
filesPerWorker,
|
|
5888
6240
|
maxWorkers
|
|
5889
6241
|
});
|
|
6242
|
+
const heap = program ? planPassHeap(descriptor.label, context.heap, {
|
|
6243
|
+
concurrency,
|
|
6244
|
+
dirtyCount: sizingDirtyCount
|
|
6245
|
+
}) : void 0;
|
|
5890
6246
|
if (context.run.mutate && context.multiPass && descriptor === TYPED_PASS && !conservative && dirtyCount === 0) return {
|
|
5891
6247
|
cacheFile,
|
|
5892
6248
|
concurrency,
|
|
5893
6249
|
descriptor,
|
|
6250
|
+
heap,
|
|
5894
6251
|
shouldRun: false,
|
|
5895
6252
|
skipReason: TYPED_SKIP_NOTICE
|
|
5896
6253
|
};
|
|
@@ -5898,6 +6255,7 @@ function sizePass(descriptor, context) {
|
|
|
5898
6255
|
cacheFile,
|
|
5899
6256
|
concurrency,
|
|
5900
6257
|
descriptor,
|
|
6258
|
+
heap,
|
|
5901
6259
|
shouldRun: true,
|
|
5902
6260
|
skipReason: void 0
|
|
5903
6261
|
};
|
|
@@ -5918,9 +6276,10 @@ function sizePass(descriptor, context) {
|
|
|
5918
6276
|
*
|
|
5919
6277
|
* @param options - The parsed CLI options.
|
|
5920
6278
|
* @param run - The run context (cwd, variant key, environment, mutate).
|
|
6279
|
+
* @param memory - The machine's memory facts (injected in tests).
|
|
5921
6280
|
* @returns The staged run plan.
|
|
5922
6281
|
*/
|
|
5923
|
-
function plan(options, run) {
|
|
6282
|
+
function plan(options, run, memory = probeMemory()) {
|
|
5924
6283
|
const { ci, cwd, environment, mutate } = run;
|
|
5925
6284
|
const runEslint = !options.oxlint;
|
|
5926
6285
|
const oxlintTypeAware = resolveOxlintTypeAware(options);
|
|
@@ -5945,6 +6304,7 @@ function plan(options, run) {
|
|
|
5945
6304
|
const files = collectRepoFiles(cwd, options.paths);
|
|
5946
6305
|
const newestBustMtime = maxMtimeMs(files.bustFiles);
|
|
5947
6306
|
const limits = resolveWorkerLimits(environment, availableParallelism(), ci);
|
|
6307
|
+
const heap = resolveHeapBudget(environment, memory);
|
|
5948
6308
|
const oxlintDecision = resolveOxlintRun(run, {
|
|
5949
6309
|
files,
|
|
5950
6310
|
runEslint,
|
|
@@ -5964,6 +6324,7 @@ function plan(options, run) {
|
|
|
5964
6324
|
const inputs = {
|
|
5965
6325
|
clearedCaches,
|
|
5966
6326
|
files: sizingFiles,
|
|
6327
|
+
heap,
|
|
5967
6328
|
limits,
|
|
5968
6329
|
multiPass: descriptors.length > 1,
|
|
5969
6330
|
newestBustMtime,
|
|
@@ -5991,6 +6352,7 @@ function plan(options, run) {
|
|
|
5991
6352
|
return planFixChild(passes, run, {
|
|
5992
6353
|
agentsFormatterPath,
|
|
5993
6354
|
files: sizingFiles,
|
|
6355
|
+
heap,
|
|
5994
6356
|
limits,
|
|
5995
6357
|
options
|
|
5996
6358
|
});
|
|
@@ -6057,20 +6419,51 @@ async function runLint(argv, cwd = process$1.cwd(), environment = process$1.env,
|
|
|
6057
6419
|
if (options.paths.length === 0) return 0;
|
|
6058
6420
|
const staged = plan(options, resolveRunContext(cwd, environment, !options.print));
|
|
6059
6421
|
const { commands, notice } = compose(staged.eager, options);
|
|
6422
|
+
const oxfmtConfig = findOxfmtModule(options, cwd);
|
|
6423
|
+
const formatCommand = oxfmtConfig === void 0 ? void 0 : composeOxfmtCommand(options, {
|
|
6424
|
+
configPath: oxfmtConfig,
|
|
6425
|
+
write: options.fix
|
|
6426
|
+
});
|
|
6060
6427
|
if (options.print) {
|
|
6061
|
-
|
|
6428
|
+
const printed = formatCommand === void 0 ? commands : [...commands, formatCommand];
|
|
6429
|
+
for (const command of printed) process$1.stdout.write(`${formatCommandLine(command)}\n`);
|
|
6430
|
+
for (const pass of staged.eager.passes) if (pass.shouldRun && pass.heap !== void 0) process$1.stderr.write(pass.heap.note);
|
|
6062
6431
|
return 0;
|
|
6063
6432
|
}
|
|
6064
6433
|
if (commands.some((command) => command.bin === "oxlint" && command.args.includes("--type-aware")) && !isPackageExists("oxlint-tsgolint", { paths: [cwd] })) throw new CliError("oxlint-tsgolint is not installed, so oxlint cannot run type-aware rules. Install oxlint-tsgolint, or pass --no-oxlint-type-aware to skip type-aware linting.");
|
|
6434
|
+
if (formatCommand !== void 0 && !isPackageExists("oxfmt", { paths: [cwd] })) throw new CliError("oxfmt is not installed, but this project has an `oxfmt.config.*` file. Install oxfmt, or pass --no-oxfmt to skip formatting.");
|
|
6065
6435
|
if (notice !== void 0) process$1.stderr.write(notice);
|
|
6066
6436
|
const { resolveFixChild } = staged;
|
|
6067
|
-
if (resolveFixChild === void 0) return (await runChecks(commands, cwd, staged, options)).code;
|
|
6437
|
+
if (resolveFixChild === void 0) return (await runChecks(formatCommand === void 0 ? commands : [...commands, formatCommand], cwd, staged, options)).code;
|
|
6068
6438
|
const byBin = Object.groupBy(commands, (command) => command.bin);
|
|
6069
6439
|
const oxlintCode = await execute(byBin.oxlint ?? [], cwd, true);
|
|
6070
6440
|
const checks = await runChecks(byBin.eslint ?? [], cwd, staged, options);
|
|
6071
6441
|
const fixChild = resolveFixChild(checks.passes);
|
|
6072
|
-
|
|
6073
|
-
|
|
6442
|
+
const lintCode = fixChild === void 0 ? oxlintCode || checks.code : oxlintCode || await execute([fixChild], cwd, true);
|
|
6443
|
+
if (formatCommand === void 0) return lintCode;
|
|
6444
|
+
const formatCode = await execute([formatCommand], cwd, true);
|
|
6445
|
+
return lintCode || formatCode;
|
|
6446
|
+
}
|
|
6447
|
+
/**
|
|
6448
|
+
* The oxfmt config module the run formats with. The oxfmt pass is on when the
|
|
6449
|
+
* project runs oxfmt as its own tool (an `oxfmt.config.ts` or
|
|
6450
|
+
* `oxfmt.config.mts`, found the way oxfmt finds it), unless `--no-oxfmt` or a
|
|
6451
|
+
* single-engine flag (`--eslint`, `--oxlint`) says otherwise.
|
|
6452
|
+
*
|
|
6453
|
+
* @param options - The parsed CLI options.
|
|
6454
|
+
* @param cwd - The working directory.
|
|
6455
|
+
* @returns The config module path, or `undefined` to skip the pass.
|
|
6456
|
+
* @throws {CliError} When one directory holds more than one oxfmt config.
|
|
6457
|
+
*/
|
|
6458
|
+
function findOxfmtModule(options, cwd) {
|
|
6459
|
+
if (!options.oxfmt || options.eslint || options.oxlint) return;
|
|
6460
|
+
let config;
|
|
6461
|
+
try {
|
|
6462
|
+
config = findOxfmtConfig(cwd);
|
|
6463
|
+
} catch (err) {
|
|
6464
|
+
throw new CliError(err instanceof Error ? err.message : String(err));
|
|
6465
|
+
}
|
|
6466
|
+
return config?.isModule === true ? config.path : void 0;
|
|
6074
6467
|
}
|
|
6075
6468
|
/**
|
|
6076
6469
|
* Run a run's ESLint check children and report which passes they covered.
|
|
@@ -6117,6 +6510,9 @@ Flags:
|
|
|
6117
6510
|
--oxlint Run only oxlint.
|
|
6118
6511
|
--fix Check as usual, then fix only the files the checks
|
|
6119
6512
|
reported, in one further ESLint child.
|
|
6513
|
+
--no-oxfmt Skip the oxfmt pass. It runs when the project has an
|
|
6514
|
+
oxfmt.config.* file: oxfmt --check alongside the
|
|
6515
|
+
linters, or oxfmt --write last with --fix.
|
|
6120
6516
|
--agents, --no-agents Emit agent-friendly output. On by default when an AI
|
|
6121
6517
|
agent session is detected (AI_AGENT, CLAUDECODE, ...).
|
|
6122
6518
|
--type-aware=off|only|full
|