@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/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 fs$1 from "node:fs";
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.1";
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$1.readFileSync(filePath, "utf8");
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$1.mkdirSync(path.dirname(filePath), { recursive: true });
655
- fs$1.writeFileSync(temporary, content);
656
- fs$1.renameSync(temporary, filePath);
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$1.rmSync(temporary, { force: true });
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$1.utimesSync(filePath, now, now);
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$1.existsSync(built) ? built : fileURLToPath(new URL("../../ignored-child.ts", import.meta.url));
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$1.existsSync(built) ? built : fileURLToPath(new URL("../../bootstrap.ts", import.meta.url));
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
- return launchEngine(launch, options);
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: "inherit"
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: { ESLINT_TYPE_AWARE: context.typeAwareEnv },
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$1.rmSync(cacheFilePath, { force: true });
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$1.existsSync(cacheFilePath)) return;
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$1.statSync(filePath).mtimeMs;
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$1.readdirSync(cwd);
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$1.rmSync(cacheFilePath, { force: true });
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$1.existsSync(path.join(current, marker))) return current;
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$1.readdirSync(directory, { withFileTypes: true });
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$1.existsSync(path.join(absolute, "package.json"))) members.push(absolute);
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$1.statSync(path.resolve(cwd, target)).isDirectory();
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$1.readdirSync(current, { withFileTypes: true });
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$1.readdirSync(current, { withFileTypes: true });
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$1.statSync(absolute);
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$1.readFileSync(outFile, "utf8"));
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$1.rmSync(outFile, { force: true });
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$1.existsSync(path.resolve(cwd, "node_modules"));
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, 6)
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.invalidation === "none" ? limits.maxWorkers : limits.typedMaxWorkers;
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: options.concurrency ?? computeWorkerCount({
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$1.mkdirSync(stateDirectory(cwd), { recursive: true });
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$1.readdirSync(stateDirectory(cwd)).some((name) => name.startsWith(prefix) && !name.endsWith(".tmp"));
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$1.writeFileSync(temporary, data);
5641
- fs$1.renameSync(temporary, fileName);
5990
+ fs.writeFileSync(temporary, data);
5991
+ fs.renameSync(temporary, fileName);
5642
5992
  } catch (err) {
5643
- fs$1.rmSync(temporary, { force: true });
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$1.rmSync(gatePath, { force: true });
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$1.existsSync(buildInfoPath);
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$1.rmSync(cacheLocation, { force: true });
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 buildsProgram = descriptor.invalidation !== "none";
6171
+ const program = buildsProgram(descriptor);
5822
6172
  const mode = descriptor.invalidation === "only" ? "only" : void 0;
5823
- if (clearedCaches.has(cacheLocation) && (!buildsProgram || hasBuilderState(run, mode))) return targetFiles.length;
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 (buildsProgram) {
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 maxWorkers = maxWorkersFor(descriptor, context.limits);
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
- for (const command of commands) process$1.stdout.write(`${formatCommandLine(command)}\n`);
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
- if (fixChild === void 0) return oxlintCode || checks.code;
6073
- return oxlintCode || await execute([fixChild], cwd, true);
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