@isentinel/eslint-config 6.0.1 → 6.1.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 CHANGED
@@ -253,6 +253,22 @@ Other behaviours:
253
253
  re-lint (its own dirty count), rather than eagerly spinning up a worker per
254
254
  CPU. The fast pass is cheap per file, so it packs far more files per worker
255
255
  (see `FAST_FILES_PER_WORKER`) than the type-aware pass.
256
+ - Sizes the heap of every ESLint child that builds a TypeScript program (the
257
+ type-aware pass, the full pass and the fix child). Each worker holds its own
258
+ program, so the limit that matters is workers × heap. By default the runner
259
+ splits half the machine's memory (or its cgroup limit, when lower) across
260
+ those workers, and raises Node's heap limit when the share is larger. When the
261
+ budget cannot give each worker twice Node's default heap, it runs fewer
262
+ workers: every worker loads the program of nearly every project, so fewer,
263
+ larger workers need far less memory in total than more, smaller ones. The flag
264
+ goes in the child's `NODE_OPTIONS`, which every ESLint worker inherits; oxlint
265
+ and the fast pass never get it. `--print` shows the chosen workers, files per
266
+ worker and heap on stderr. If a child still runs out of memory, the runner
267
+ prints a hint that names `LINT_TYPED_HEAP_MB`. A size set in
268
+ `LINT_TYPED_HEAP_MB` also caps the workers at budget ÷ size. An explicit
269
+ `LINT_MAX_WORKERS` or `--concurrency` is never reduced. A
270
+ `--max-old-space-size` already in `NODE_OPTIONS` wins over all of this: the
271
+ runner adds nothing and keeps the worker count.
256
272
  - Keeps a separate ESLint cache per pass so they never invalidate one another:
257
273
  `.eslintcache-fast-<key>` (fast pass), `.eslintcache-typeaware-<key>`
258
274
  (type-aware pass), and `.eslintcache-<key>` (the full config, used by CI,
@@ -394,6 +410,7 @@ or `--`.
394
410
  | `FILES_PER_WORKER` | Target files a single type-aware ESLint worker handles before adding another (default `350`). |
395
411
  | `FAST_FILES_PER_WORKER` | Same, for the fast (syntactic) pass, which packs far more files per worker (default `800`). |
396
412
  | `LINT_MAX_WORKERS` | Upper bound on ESLint workers (default a quarter of available CPUs). |
413
+ | `LINT_TYPED_HEAP_MB` | Heap per worker for the program-building ESLint children: `auto` (default), a size in MB, or `off` (Node's default). |
397
414
  | `ESLINT_TYPE_AWARE` | `off` or `only`; the type-aware mode `--type-aware` sets for the ESLint child (`false` is a legacy alias for `off`). |
398
415
  | `CI` | When set, the runner uses the single full pass with `--cache-strategy content` (see below). |
399
416
 
package/dist/cli.mjs CHANGED
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import { existsSync, readFileSync } from "fs";
10
10
  import { execSync } from "node:child_process";
11
11
  //#region package.json
12
- var version = "6.0.1";
12
+ var version = "6.1.0";
13
13
  var package_default = {
14
14
  name: "@isentinel/eslint-config",
15
15
  version,
package/dist/index.d.mts CHANGED
@@ -28502,7 +28502,14 @@ declare function loadSmallRulesPlugin(): NonNullable<TypedFlatConfigItem["plugin
28502
28502
  declare function smallRules(options?: OptionsComponentExtensions & OptionsFiles & OptionsIsInEditor & OptionsStylistic): Array<TypedFlatConfigItem>;
28503
28503
  //#endregion
28504
28504
  //#region src/eslint/configs/sonarjs.d.ts
28505
- declare function sonarjs({ isInEditor, roblox }: OptionsHasRoblox & Required<OptionsIsInEditor>): Promise<Array<TypedFlatConfigItem>>;
28505
+ declare function sonarjs({ complementIgnores, isInEditor, roblox }: OptionsHasRoblox & Required<OptionsIsInEditor> & {
28506
+ /**
28507
+ * When set, re-apply the non-roblox rules to every source file except
28508
+ * these globs (the roblox scope), so the complement is linted as
28509
+ * standard-TS/Node land.
28510
+ */
28511
+ complementIgnores?: Array<string>;
28512
+ }): Promise<Array<TypedFlatConfigItem>>;
28506
28513
  //#endregion
28507
28514
  //#region src/eslint/configs/sort.d.ts
28508
28515
  /**
package/dist/index.mjs CHANGED
@@ -4523,7 +4523,8 @@ function sonarjsTestRules({ jest = false, roblox = true } = {}) {
4523
4523
  }
4524
4524
  //#endregion
4525
4525
  //#region src/eslint/configs/sonarjs.ts
4526
- async function sonarjs({ isInEditor, roblox }) {
4526
+ init_globs();
4527
+ async function sonarjs({ complementIgnores, isInEditor, roblox }) {
4527
4528
  return [{
4528
4529
  name: "isentinel/sonarjs",
4529
4530
  plugins: { sonar: await interopDefault(import("eslint-plugin-sonarjs")) },
@@ -4531,7 +4532,15 @@ async function sonarjs({ isInEditor, roblox }) {
4531
4532
  isInEditor,
4532
4533
  roblox
4533
4534
  })
4534
- }];
4535
+ }, ...complementIgnores ? [{
4536
+ name: "isentinel/sonarjs/complement",
4537
+ files: [GLOB_SRC],
4538
+ ignores: complementIgnores,
4539
+ rules: sonarjsRules({
4540
+ isInEditor,
4541
+ roblox: false
4542
+ })
4543
+ }] : []];
4535
4544
  }
4536
4545
  //#endregion
4537
4546
  //#region src/eslint/configs/sort.ts
@@ -5938,7 +5947,6 @@ function typescriptTypeAwareRules({ roblox = true } = {}) {
5938
5947
  "ts/no-confusing-void-expression": "error",
5939
5948
  "ts/no-deprecated": "error",
5940
5949
  "ts/no-duplicate-type-constituents": "error",
5941
- "ts/no-empty-object-type": "error",
5942
5950
  "ts/no-floating-promises": ["error", {
5943
5951
  checkThenables: roblox,
5944
5952
  ignoreVoid: true
@@ -20314,6 +20322,7 @@ async function isentinel(options, ...userConfigs) {
20314
20322
  roblox: enableRoblox,
20315
20323
  stylistic: stylisticOptions
20316
20324
  }), promise(), sonarjs({
20325
+ ...needsComplementOverlay ? { complementIgnores: robloxScopedFiles } : {},
20317
20326
  isInEditor,
20318
20327
  roblox: enableRoblox
20319
20328
  }), typescript({
package/dist/lint-cli.mjs CHANGED
@@ -14,6 +14,7 @@ import { KillOnSignal, LogError, LogExit, LogOutput, Logger, createConcurrently
14
14
  import { execFileSync, spawn, spawnSync } from "node:child_process";
15
15
  import os, { availableParallelism } from "node:os";
16
16
  import { Writable } from "node:stream";
17
+ import v8 from "node:v8";
17
18
  import { fileURLToPath } from "node:url";
18
19
  import fileEntryCache from "file-entry-cache";
19
20
  //#region \0rolldown/runtime.js
@@ -40,7 +41,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
40
41
  }) : target, mod));
41
42
  //#endregion
42
43
  //#region package.json
43
- var version = "6.0.1";
44
+ var version = "6.1.0";
44
45
  //#endregion
45
46
  //#region src/lint-cli/lib/cli/types.ts
46
47
  /**
@@ -570,6 +571,188 @@ function resolveRunContext(cwd, environment, mutate) {
570
571
  };
571
572
  }
572
573
  //#endregion
574
+ //#region src/lint-cli/lib/plan/heap.ts
575
+ /**
576
+ * Environment variable that sets the heap of a program-building ESLint child.
577
+ */
578
+ const HEAP_ENV = "LINT_TYPED_HEAP_MB";
579
+ /**
580
+ * Share of the machine's memory the program-building workers may claim between
581
+ * them. The rest is left for the oxlint child, the fast pass, the editor and
582
+ * the OS, all of which run alongside the type-aware pass locally.
583
+ */
584
+ const HEAP_BUDGET_SHARE = .5;
585
+ const BYTES_PER_MB = 1024 * 1024;
586
+ const MAX_OLD_SPACE = /--max[-_]old[-_]space[-_]size\b/;
587
+ /**
588
+ * Read the machine's memory facts.
589
+ *
590
+ * @returns The available memory and V8's default heap limit, in MB.
591
+ */
592
+ function probeMemory() {
593
+ const total = os.totalmem();
594
+ const constrained = process$1.constrainedMemory();
595
+ return {
596
+ availableMb: Math.floor((constrained > 0 ? Math.min(total, constrained) : total) / BYTES_PER_MB),
597
+ defaultHeapMb: Math.floor(v8.getHeapStatistics().heap_size_limit / BYTES_PER_MB)
598
+ };
599
+ }
600
+ /**
601
+ * Resolve the heap setting from the environment. An invalid {@link HEAP_ENV}
602
+ * falls back to `auto`, as the other numeric knobs fall back to their
603
+ * defaults.
604
+ *
605
+ * @param environment - Where {@link HEAP_ENV} and `NODE_OPTIONS` are read.
606
+ * @returns Which of the four modes applies, with its size when fixed.
607
+ */
608
+ function resolveHeapSetting(environment) {
609
+ if (MAX_OLD_SPACE.test(environment["NODE_OPTIONS"] ?? "")) return { kind: "inherited" };
610
+ const raw = environment[HEAP_ENV]?.trim();
611
+ if (raw === "off") return { kind: "off" };
612
+ const mb = parseBoundedInteger(raw, 1);
613
+ return mb === void 0 ? { kind: "auto" } : {
614
+ kind: "fixed",
615
+ mb
616
+ };
617
+ }
618
+ /**
619
+ * The memory all program-building workers may claim together, in MB.
620
+ *
621
+ * @param memory - What the machine offers.
622
+ * @returns The budget share of the available memory, in MB.
623
+ */
624
+ function heapBudgetMb(memory) {
625
+ return Math.floor(memory.availableMb * HEAP_BUDGET_SHARE);
626
+ }
627
+ /**
628
+ * The most program-building workers the budget can hold, or `undefined` when
629
+ * the setting places no limit. Each worker is counted at the heap it may grow
630
+ * to: the requested size, or {@link AUTO_HEAP_FLOOR} times Node's default
631
+ * under `auto`. Never below one.
632
+ *
633
+ * @param budget - The setting and the machine it applies to.
634
+ * @returns How many workers fit, if the setting limits them.
635
+ */
636
+ function heapWorkerCap({ memory, setting }) {
637
+ if (setting.kind !== "auto" && setting.kind !== "fixed") return;
638
+ const perWorker = setting.kind === "fixed" ? setting.mb : memory.defaultHeapMb * 2;
639
+ return Math.max(1, Math.floor(heapBudgetMb(memory) / perWorker));
640
+ }
641
+ /**
642
+ * The heap each worker of a program-building pass gets, or `undefined` for
643
+ * Node's default. `auto` splits the budget across the workers and only raises
644
+ * the limit: a share below the default adds no flag.
645
+ *
646
+ * @param budget - The setting and the machine it applies to.
647
+ * @param concurrency - The pass's resolved concurrency (`off` lints on the
648
+ * main thread, which is one heap).
649
+ * @returns The size in MB to pass to Node, if it should change.
650
+ */
651
+ function heapPerWorkerMb({ memory, setting }, concurrency) {
652
+ if (setting.kind === "fixed") return setting.mb;
653
+ if (setting.kind !== "auto") return;
654
+ const share = Math.floor(heapBudgetMb(memory) / workerCount(concurrency));
655
+ return share > memory.defaultHeapMb ? share : void 0;
656
+ }
657
+ /**
658
+ * Pair the run's heap setting with the machine it applies to.
659
+ *
660
+ * @param environment - Where {@link HEAP_ENV} and `NODE_OPTIONS` are read.
661
+ * @param memory - What the machine offers.
662
+ * @returns Everything sizing needs to give a program-building pass its heap.
663
+ */
664
+ function resolveHeapBudget(environment, memory) {
665
+ return {
666
+ memory,
667
+ nodeOptions: environment["NODE_OPTIONS"],
668
+ setting: resolveHeapSetting(environment)
669
+ };
670
+ }
671
+ /**
672
+ * The worker cap of a program-building pass once the heap budget is applied.
673
+ * An explicit `LINT_MAX_WORKERS` is never tightened.
674
+ *
675
+ * @param maxWorkers - The pass's cap before the budget.
676
+ * @param explicit - Whether `LINT_MAX_WORKERS` set that cap.
677
+ * @param budget - The setting and the machine it applies to.
678
+ * @returns The cap to size the pass against.
679
+ */
680
+ function capWorkersForHeap(maxWorkers, explicit, budget) {
681
+ const cap = explicit ? void 0 : heapWorkerCap(budget);
682
+ return cap === void 0 ? maxWorkers : Math.min(maxWorkers, cap);
683
+ }
684
+ /**
685
+ * Plan the heap of one program-building child from its sizing.
686
+ *
687
+ * @param label - The child's label, for the `--print` line.
688
+ * @param budget - The setting and the machine it applies to.
689
+ * @param sizing - The child's concurrency and file count.
690
+ * @returns Its `NODE_OPTIONS`, its `--print` line and whether it is watched.
691
+ */
692
+ function planPassHeap(label, budget, sizing) {
693
+ const heapMb = heapPerWorkerMb(budget, sizing.concurrency);
694
+ return {
695
+ nodeOptions: heapNodeOptions(budget, heapMb),
696
+ note: describeHeapPlan(label, budget, sizing, heapMb),
697
+ watch: budget.setting.kind !== "off"
698
+ };
699
+ }
700
+ /**
701
+ * The number of heaps a concurrency value runs: `off` lints on the main
702
+ * thread, which is one.
703
+ *
704
+ * @param concurrency - A resolved ESLint concurrency.
705
+ * @returns How many threads lint.
706
+ */
707
+ function workerCount(concurrency) {
708
+ return concurrency === "off" ? 1 : concurrency;
709
+ }
710
+ /**
711
+ * The `NODE_OPTIONS` a program-building child runs under: the inherited value
712
+ * with the heap flag appended. Node's `worker_threads` take their heap limit
713
+ * from `--max-old-space-size` when it is set, so this one flag reaches every
714
+ * ESLint worker.
715
+ *
716
+ * @param budget - Carries the inherited `NODE_OPTIONS`.
717
+ * @param heapMb - The size in MB for each worker, if it should change.
718
+ * @returns The value to set, or `undefined` to leave `NODE_OPTIONS` alone.
719
+ */
720
+ function heapNodeOptions({ nodeOptions }, heapMb) {
721
+ if (heapMb === void 0) return;
722
+ const flag = `--max-old-space-size=${String(heapMb)}`;
723
+ const inherited = nodeOptions?.trim() ?? "";
724
+ return inherited.length > 0 ? `${inherited} ${flag}` : flag;
725
+ }
726
+ /**
727
+ * How the `--print` line names a heap the runner left alone.
728
+ *
729
+ * @param budget - Says whether the user's `NODE_OPTIONS` set it.
730
+ * @returns The user's heap, or Node's default with its size.
731
+ */
732
+ function unchangedHeap({ memory, setting }) {
733
+ return setting.kind === "inherited" ? "the NODE_OPTIONS heap" : `Node's default heap (${String(memory.defaultHeapMb)} MB)`;
734
+ }
735
+ /**
736
+ * Describe the memory plan of a program-building pass for `--print`.
737
+ *
738
+ * @param label - The pass's child label.
739
+ * @param budget - The setting and the machine it applies to.
740
+ * @param sizing - The pass's concurrency and file count.
741
+ * @param heapMb - The size given to each worker, if it changed.
742
+ * @returns One stderr line naming the workers, files per worker and heap.
743
+ */
744
+ function describeHeapPlan(label, budget, { concurrency, dirtyCount }, heapMb) {
745
+ const workers = workerCount(concurrency);
746
+ const heap = heapMb === void 0 ? unchangedHeap(budget) : `${String(heapMb)} MB heap`;
747
+ const source = {
748
+ auto: `auto, budget ${String(heapBudgetMb(budget.memory))} MB`,
749
+ fixed: HEAP_ENV,
750
+ inherited: "NODE_OPTIONS",
751
+ off: `${HEAP_ENV}=off`
752
+ }[budget.setting.kind];
753
+ return `isentinel-lint: ${label} pass: ${String(workers)} worker(s), ~${String(Math.ceil(dirtyCount / workers))} files per worker, ${heap} per worker (${source}).\n`;
754
+ }
755
+ //#endregion
573
756
  //#region src/lint-cli/lib/state.ts
574
757
  /**
575
758
  * Directory (relative to the project root) where the CLI keeps its persisted
@@ -848,6 +1031,11 @@ function resolveManifest(name, cwd) {
848
1031
  }
849
1032
  //#endregion
850
1033
  //#region src/lint-cli/lib/exec/execute.ts
1034
+ /**
1035
+ * What V8 writes when a heap fills: `ERR_WORKER_OUT_OF_MEMORY` from a worker,
1036
+ * `JavaScript heap out of memory` from the main thread.
1037
+ */
1038
+ const OUT_OF_MEMORY = /ERR_WORKER_OUT_OF_MEMORY|heap out of memory/;
851
1039
  /** The signals that cancel a run, matching `concurrently`'s `KillOnSignal`. */
852
1040
  const CANCEL_SIGNALS = [
853
1041
  "SIGHUP",
@@ -877,6 +1065,17 @@ const PREFIX_COLOR = {
877
1065
  typed: "cyan"
878
1066
  };
879
1067
  /**
1068
+ * The hint printed after a program-building child ran out of heap, below the
1069
+ * engine's own stack trace.
1070
+ *
1071
+ * @param label - The child's label.
1072
+ * @returns One stderr line.
1073
+ */
1074
+ function outOfMemoryHint(label) {
1075
+ 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.
1076
+ `;
1077
+ }
1078
+ /**
880
1079
  * Run the composed children and aggregate their exit codes.
881
1080
  *
882
1081
  * Sequential when the caller asks for it: a lone child gains nothing from the
@@ -1045,6 +1244,27 @@ function trackCancellation(onSignal) {
1045
1244
  };
1046
1245
  }
1047
1246
  /**
1247
+ * Watch a child's stderr for V8's out-of-memory report. Reads alongside
1248
+ * whatever else consumes the stream, and never alters it.
1249
+ *
1250
+ * @param child - The spawned child.
1251
+ * @param onExhausted - Called once, when the report is seen.
1252
+ */
1253
+ function watchOutOfMemory(child, onExhausted) {
1254
+ let tail = "";
1255
+ let seen = false;
1256
+ child.stderr?.on("data", (chunk) => {
1257
+ if (seen) return;
1258
+ const text = tail + String(chunk);
1259
+ if (OUT_OF_MEMORY.test(text)) {
1260
+ seen = true;
1261
+ onExhausted();
1262
+ return;
1263
+ }
1264
+ tail = text.slice(-64);
1265
+ });
1266
+ }
1267
+ /**
1048
1268
  * Start every command as one `concurrently` group and announce the children.
1049
1269
  * Unlike the previous `killOthersOn: ["failure"]` behaviour, an ordinary lint
1050
1270
  * failure in one child no longer kills its siblings — each runs to the end so
@@ -1075,6 +1295,7 @@ async function startGroup(commands, cwd, outputStream = process$1.stdout) {
1075
1295
  }];
1076
1296
  }));
1077
1297
  const logger = new Logger({});
1298
+ const exhausted = /* @__PURE__ */ new Set();
1078
1299
  const abortController = new AbortController();
1079
1300
  const stopTracking = trackCancellation();
1080
1301
  const { result } = createConcurrently(Array.from(launches, ([name, { command }]) => {
@@ -1103,7 +1324,11 @@ async function startGroup(commands, cwd, outputStream = process$1.stdout) {
1103
1324
  spawn: (name, options) => {
1104
1325
  const launch = launches.get(name);
1105
1326
  if (launch === void 0) throw new Error(`isentinel-lint: no engine registered as "${name}".`);
1106
- return launchEngine(launch, options);
1327
+ const child = launchEngine(launch, options);
1328
+ if (launch.command.watchHeap) watchOutOfMemory(child, () => {
1329
+ exhausted.add(launch.command.label);
1330
+ });
1331
+ return child;
1107
1332
  }
1108
1333
  });
1109
1334
  for (const command of commands) announce(command);
@@ -1114,6 +1339,7 @@ async function startGroup(commands, cwd, outputStream = process$1.stdout) {
1114
1339
  return 1;
1115
1340
  } finally {
1116
1341
  stopTracking();
1342
+ for (const label of exhausted) process$1.stderr.write(outOfMemoryHint(label));
1117
1343
  }
1118
1344
  }
1119
1345
  function signalExitCode(signal) {
@@ -1130,6 +1356,8 @@ function cancelledCode() {
1130
1356
  }
1131
1357
  async function spawnChild(command, cwd) {
1132
1358
  const bin = resolveLocalBin(command.bin, cwd);
1359
+ const watched = command.watchHeap;
1360
+ let exhausted = false;
1133
1361
  return new Promise((resolve) => {
1134
1362
  const child = launchEngine({
1135
1363
  bin,
@@ -1140,8 +1368,20 @@ async function spawnChild(command, cwd) {
1140
1368
  ...process$1.env,
1141
1369
  ...command.env
1142
1370
  },
1143
- stdio: "inherit"
1371
+ stdio: [
1372
+ "inherit",
1373
+ "inherit",
1374
+ watched ? "pipe" : "inherit"
1375
+ ]
1144
1376
  });
1377
+ if (watched) {
1378
+ child.stderr?.on("data", (chunk) => {
1379
+ process$1.stderr.write(chunk);
1380
+ });
1381
+ watchOutOfMemory(child, () => {
1382
+ exhausted = true;
1383
+ });
1384
+ }
1145
1385
  announce(command);
1146
1386
  const stopTracking = trackCancellation((signal) => {
1147
1387
  child.kill(signal);
@@ -1152,6 +1392,7 @@ async function spawnChild(command, cwd) {
1152
1392
  });
1153
1393
  child.on("close", (code, signal) => {
1154
1394
  stopTracking();
1395
+ if (exhausted) process$1.stderr.write(outOfMemoryHint(command.label));
1155
1396
  resolve(code ?? (signal === null ? 1 : signalExitCode(signal)));
1156
1397
  });
1157
1398
  });
@@ -1236,7 +1477,8 @@ function composeOxlintCommand(options, context) {
1236
1477
  args,
1237
1478
  bin: "oxlint",
1238
1479
  env: {},
1239
- label: "oxc"
1480
+ label: "oxc",
1481
+ watchHeap: false
1240
1482
  };
1241
1483
  }
1242
1484
  /**
@@ -1254,11 +1496,14 @@ function composeEslintCommand(options, context) {
1254
1496
  if (options.agents) args.push("--format", context.agentsFormatterPath);
1255
1497
  if (context.fix) args.push("--fix");
1256
1498
  args.push(...options.eslintArgs, ...pathArguments(context.paths));
1499
+ const environment = { ESLINT_TYPE_AWARE: context.typeAwareEnv };
1500
+ if (context.heap?.nodeOptions !== void 0) environment["NODE_OPTIONS"] = context.heap.nodeOptions;
1257
1501
  return {
1258
1502
  args,
1259
1503
  bin: "eslint",
1260
- env: { ESLINT_TYPE_AWARE: context.typeAwareEnv },
1261
- label: context.eslintLabel
1504
+ env: environment,
1505
+ label: context.eslintLabel,
1506
+ watchHeap: context.heap?.watch ?? false
1262
1507
  };
1263
1508
  }
1264
1509
  /**
@@ -1306,6 +1551,7 @@ function composePasses(passes, runPlan, options) {
1306
1551
  concurrency: pass.concurrency,
1307
1552
  eslintLabel: pass.descriptor.label,
1308
1553
  fix: false,
1554
+ heap: pass.heap,
1309
1555
  paths: options.paths,
1310
1556
  typeAwareEnv: pass.descriptor.typeAwareEnv
1311
1557
  }));
@@ -4851,6 +5097,7 @@ function resolveWorkerLimits(environment, availableParallelism, ci) {
4851
5097
  const explicit = parsePositiveInteger(environment["LINT_MAX_WORKERS"]);
4852
5098
  const maxWorkers = explicit ?? (ci ? availableParallelism : Math.floor(availableParallelism / 4));
4853
5099
  return {
5100
+ explicitMaxWorkers: explicit !== void 0,
4854
5101
  filesPerWorker,
4855
5102
  maxWorkers,
4856
5103
  typedMaxWorkers: explicit ?? Math.min(maxWorkers, 6)
@@ -4943,6 +5190,17 @@ function selectPasses(options, ci) {
4943
5190
  return [FAST_PASS, TYPED_PASS];
4944
5191
  }
4945
5192
  /**
5193
+ * Whether a pass builds a TypeScript program in each of its workers. The pass
5194
+ * that runs no builder is exactly the one that builds no program, so
5195
+ * `invalidation` records both.
5196
+ *
5197
+ * @param descriptor - The pass.
5198
+ * @returns Whether its workers hold a program, and so need heap sizing.
5199
+ */
5200
+ function buildsProgram(descriptor) {
5201
+ return descriptor.invalidation !== "none";
5202
+ }
5203
+ /**
4946
5204
  * The worker cap for a pass. `invalidation` already records whether a pass
4947
5205
  * builds a TypeScript program — the fast pass runs no builder precisely because
4948
5206
  * it builds none — so it doubles as the cap selector rather than restating the
@@ -4953,7 +5211,7 @@ function selectPasses(options, ci) {
4953
5211
  * @returns The worker cap for this pass.
4954
5212
  */
4955
5213
  function maxWorkersFor(descriptor, limits) {
4956
- return descriptor.invalidation === "none" ? limits.maxWorkers : limits.typedMaxWorkers;
5214
+ return buildsProgram(descriptor) ? limits.typedMaxWorkers : limits.maxWorkers;
4957
5215
  }
4958
5216
  //#endregion
4959
5217
  //#region src/lint-cli/lib/plan/fix.ts
@@ -5018,13 +5276,18 @@ function collectFixTargets(passes, run, files) {
5018
5276
  * @returns The fix child, or `undefined` when nothing was reported.
5019
5277
  */
5020
5278
  function planFixChild(passes, run, inputs) {
5021
- const { files, limits, options } = inputs;
5279
+ const { files, heap, limits, options } = inputs;
5022
5280
  const covered = options.cache ? collectFixTargets(passes, run, files) : [];
5023
5281
  const uncovered = options.cache ? files.outsideCwdTargets : options.paths;
5024
5282
  const targets = [...covered, ...uncovered];
5025
5283
  if (targets.length === 0) return;
5026
5284
  const cacheFile = cacheFileFor(CACHE_FILE_DEFAULT, run.key);
5027
5285
  if (covered.length > 0) openCache(path.resolve(run.cwd, cacheFile), run.ci)?.removeEntries(covered);
5286
+ const concurrency = options.concurrency ?? computeWorkerCount({
5287
+ dirtyCount: targets.length,
5288
+ filesPerWorker: limits.filesPerWorker,
5289
+ maxWorkers: capWorkersForHeap(limits.typedMaxWorkers, limits.explicitMaxWorkers, heap)
5290
+ });
5028
5291
  return composeEslintCommand({
5029
5292
  ...options,
5030
5293
  cache: options.cache && uncovered.length === 0
@@ -5032,13 +5295,13 @@ function planFixChild(passes, run, inputs) {
5032
5295
  agentsFormatterPath: inputs.agentsFormatterPath,
5033
5296
  cacheLocation: cacheFile,
5034
5297
  ci: run.ci,
5035
- concurrency: options.concurrency ?? computeWorkerCount({
5036
- dirtyCount: targets.length,
5037
- filesPerWorker: limits.filesPerWorker,
5038
- maxWorkers: limits.typedMaxWorkers
5039
- }),
5298
+ concurrency,
5040
5299
  eslintLabel: FULL_PASS.label,
5041
5300
  fix: true,
5301
+ heap: planPassHeap(FULL_PASS.label, heap, {
5302
+ concurrency,
5303
+ dirtyCount: targets.length
5304
+ }),
5042
5305
  paths: targets,
5043
5306
  typeAwareEnv: FULL_PASS.typeAwareEnv
5044
5307
  });
@@ -5818,12 +6081,12 @@ function sizePasses(descriptors, run, inputs) {
5818
6081
  * @returns The number of dirty files.
5819
6082
  */
5820
6083
  function mutatingDirtyCount(descriptor, cacheLocation, targetFiles, { clearedCaches, run }) {
5821
- const buildsProgram = descriptor.invalidation !== "none";
6084
+ const program = buildsProgram(descriptor);
5822
6085
  const mode = descriptor.invalidation === "only" ? "only" : void 0;
5823
- if (clearedCaches.has(cacheLocation) && (!buildsProgram || hasBuilderState(run, mode))) return targetFiles.length;
6086
+ if (clearedCaches.has(cacheLocation) && (!program || hasBuilderState(run, mode))) return targetFiles.length;
5824
6087
  const cache = openCache(cacheLocation, run.ci);
5825
6088
  const dirty = new Set((cache?.getUpdatedFiles(targetFiles) ?? targetFiles).map((file) => normalizePath(file)));
5826
- if (buildsProgram) {
6089
+ if (program) {
5827
6090
  const outcome = applyTypeAwareInvalidation(run, {
5828
6091
  alreadyDirty: dirty,
5829
6092
  cache,
@@ -5880,17 +6143,24 @@ function sizePass(descriptor, context) {
5880
6143
  const dirtyCount = passDirtyCount(descriptor, cacheFile, context);
5881
6144
  const conservative = context.files.outsideCwdTargets.length > 0;
5882
6145
  const filesPerWorker = descriptor.filesPerWorker(context.limits, context.run.environment);
5883
- const maxWorkers = maxWorkersFor(descriptor, context.limits);
6146
+ const program = buildsProgram(descriptor);
6147
+ const passMaxWorkers = maxWorkersFor(descriptor, context.limits);
6148
+ const maxWorkers = program ? capWorkersForHeap(passMaxWorkers, context.limits.explicitMaxWorkers, context.heap) : passMaxWorkers;
5884
6149
  const sizingDirtyCount = conservative ? maxWorkers * filesPerWorker : dirtyCount;
5885
6150
  const concurrency = context.options.concurrency ?? computeWorkerCount({
5886
6151
  dirtyCount: sizingDirtyCount,
5887
6152
  filesPerWorker,
5888
6153
  maxWorkers
5889
6154
  });
6155
+ const heap = program ? planPassHeap(descriptor.label, context.heap, {
6156
+ concurrency,
6157
+ dirtyCount: sizingDirtyCount
6158
+ }) : void 0;
5890
6159
  if (context.run.mutate && context.multiPass && descriptor === TYPED_PASS && !conservative && dirtyCount === 0) return {
5891
6160
  cacheFile,
5892
6161
  concurrency,
5893
6162
  descriptor,
6163
+ heap,
5894
6164
  shouldRun: false,
5895
6165
  skipReason: TYPED_SKIP_NOTICE
5896
6166
  };
@@ -5898,6 +6168,7 @@ function sizePass(descriptor, context) {
5898
6168
  cacheFile,
5899
6169
  concurrency,
5900
6170
  descriptor,
6171
+ heap,
5901
6172
  shouldRun: true,
5902
6173
  skipReason: void 0
5903
6174
  };
@@ -5918,9 +6189,10 @@ function sizePass(descriptor, context) {
5918
6189
  *
5919
6190
  * @param options - The parsed CLI options.
5920
6191
  * @param run - The run context (cwd, variant key, environment, mutate).
6192
+ * @param memory - The machine's memory facts (injected in tests).
5921
6193
  * @returns The staged run plan.
5922
6194
  */
5923
- function plan(options, run) {
6195
+ function plan(options, run, memory = probeMemory()) {
5924
6196
  const { ci, cwd, environment, mutate } = run;
5925
6197
  const runEslint = !options.oxlint;
5926
6198
  const oxlintTypeAware = resolveOxlintTypeAware(options);
@@ -5945,6 +6217,7 @@ function plan(options, run) {
5945
6217
  const files = collectRepoFiles(cwd, options.paths);
5946
6218
  const newestBustMtime = maxMtimeMs(files.bustFiles);
5947
6219
  const limits = resolveWorkerLimits(environment, availableParallelism(), ci);
6220
+ const heap = resolveHeapBudget(environment, memory);
5948
6221
  const oxlintDecision = resolveOxlintRun(run, {
5949
6222
  files,
5950
6223
  runEslint,
@@ -5964,6 +6237,7 @@ function plan(options, run) {
5964
6237
  const inputs = {
5965
6238
  clearedCaches,
5966
6239
  files: sizingFiles,
6240
+ heap,
5967
6241
  limits,
5968
6242
  multiPass: descriptors.length > 1,
5969
6243
  newestBustMtime,
@@ -5991,6 +6265,7 @@ function plan(options, run) {
5991
6265
  return planFixChild(passes, run, {
5992
6266
  agentsFormatterPath,
5993
6267
  files: sizingFiles,
6268
+ heap,
5994
6269
  limits,
5995
6270
  options
5996
6271
  });
@@ -6059,6 +6334,7 @@ async function runLint(argv, cwd = process$1.cwd(), environment = process$1.env,
6059
6334
  const { commands, notice } = compose(staged.eager, options);
6060
6335
  if (options.print) {
6061
6336
  for (const command of commands) process$1.stdout.write(`${formatCommandLine(command)}\n`);
6337
+ for (const pass of staged.eager.passes) if (pass.shouldRun && pass.heap !== void 0) process$1.stderr.write(pass.heap.note);
6062
6338
  return 0;
6063
6339
  }
6064
6340
  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.");
package/dist/oxlint.d.mts CHANGED
@@ -1382,7 +1382,10 @@ interface OxlintMainRuleDefaults {
1382
1382
  "*": "off";
1383
1383
  };
1384
1384
  "prefer-const": {
1385
- "*": "error";
1385
+ "*": ["error", {
1386
+ "destructuring": "all";
1387
+ "ignoreReadBeforeAssign": true;
1388
+ }];
1386
1389
  };
1387
1390
  "prefer-destructuring": {
1388
1391
  "*": "off";
@@ -2080,7 +2083,9 @@ interface OxlintMainRuleDefaults {
2080
2083
  "*": "off";
2081
2084
  };
2082
2085
  "typescript/no-empty-object-type": {
2083
- "*": "error";
2086
+ "*": ["error", {
2087
+ "allowInterfaces": "always";
2088
+ }];
2084
2089
  };
2085
2090
  "typescript/no-explicit-any": {
2086
2091
  "*": "off";
package/dist/oxlint.mjs CHANGED
@@ -8479,11 +8479,15 @@ function sonarjsTestRules({ jest = false, roblox = true } = {}) {
8479
8479
  }
8480
8480
  //#endregion
8481
8481
  //#region src/oxlint/configs/sonarjs.ts
8482
- function oxlintSonarjs(options) {
8482
+ function oxlintSonarjs({ excludeFiles, isInEditor, roblox }) {
8483
8483
  return createOxlintConfigs({
8484
- name: "isentinel/sonarjs",
8484
+ name: excludeFiles ? "isentinel/sonarjs/complement" : "isentinel/sonarjs",
8485
+ ...excludeFiles ? { excludeFiles } : {},
8485
8486
  files: [GLOB_SRC],
8486
- rules: sonarjsRules(options)
8487
+ rules: sonarjsRules({
8488
+ isInEditor,
8489
+ roblox
8490
+ })
8487
8491
  });
8488
8492
  }
8489
8493
  //#endregion
@@ -9006,7 +9010,6 @@ function typescriptTypeAwareRules({ roblox = true } = {}) {
9006
9010
  "ts/no-confusing-void-expression": "error",
9007
9011
  "ts/no-deprecated": "error",
9008
9012
  "ts/no-duplicate-type-constituents": "error",
9009
- "ts/no-empty-object-type": "error",
9010
9013
  "ts/no-floating-promises": ["error", {
9011
9014
  checkThenables: roblox,
9012
9015
  ignoreVoid: true
@@ -9107,7 +9110,6 @@ function typescriptRecommendedOverrides() {
9107
9110
  "no-unsafe-negation": "off",
9108
9111
  "no-var": "error",
9109
9112
  "no-with": "off",
9110
- "prefer-const": "error",
9111
9113
  "prefer-rest-params": "error",
9112
9114
  "prefer-spread": "error"
9113
9115
  };
@@ -9488,7 +9490,21 @@ function isentinel(factoryOptions, ...userConfigs) {
9488
9490
  componentExts: componentExtensions,
9489
9491
  stylistic: stylisticOptions
9490
9492
  }));
9491
- if (needsComplementOverlay) configs.push(oxlintTypescript({
9493
+ if (needsComplementOverlay) configs.push(oxlintJavascript({
9494
+ ...getOverrides(options, "javascript"),
9495
+ excludeFiles: robloxScopedFiles,
9496
+ isInEditor,
9497
+ roblox: false,
9498
+ stylistic: stylisticOptions
9499
+ }), oxlintImports({
9500
+ excludeFiles: robloxScopedFiles,
9501
+ roblox: false,
9502
+ stylistic: stylisticOptions
9503
+ }), oxlintSonarjs({
9504
+ excludeFiles: robloxScopedFiles,
9505
+ isInEditor,
9506
+ roblox: false
9507
+ }), oxlintTypescript({
9492
9508
  ...resolveSubOptions(options, "typescript"),
9493
9509
  ...getOverrides(options, "typescript"),
9494
9510
  componentExts: componentExtensions,
@@ -9496,21 +9512,11 @@ function isentinel(factoryOptions, ...userConfigs) {
9496
9512
  roblox: false,
9497
9513
  stylistic: stylisticOptions,
9498
9514
  typeAware
9499
- }), oxlintJavascript({
9500
- ...getOverrides(options, "javascript"),
9501
- excludeFiles: robloxScopedFiles,
9502
- isInEditor,
9503
- roblox: false,
9504
- stylistic: stylisticOptions
9505
9515
  }), oxlintUnicorn({
9506
9516
  ...resolveSubOptions(options, "unicorn"),
9507
9517
  excludeFiles: robloxScopedFiles,
9508
9518
  roblox: false,
9509
9519
  stylistic: stylisticOptions
9510
- }), oxlintImports({
9511
- excludeFiles: robloxScopedFiles,
9512
- roblox: false,
9513
- stylistic: stylisticOptions
9514
9520
  }));
9515
9521
  if (enableE18e !== false && hasComplement) configs.push(oxlintE18e({
9516
9522
  excludeFiles: robloxScopedFiles,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isentinel/eslint-config",
3
- "version": "6.0.1",
3
+ "version": "6.1.0",
4
4
  "description": "iSentinel's ESLint config",
5
5
  "keywords": [
6
6
  "eslint-config",
@@ -67,7 +67,7 @@
67
67
  "@typescript-eslint/eslint-plugin": "8.65.0",
68
68
  "@typescript-eslint/parser": "8.65.0",
69
69
  "ansis": "4.3.1",
70
- "concurrently": "10.0.4",
70
+ "concurrently": "10.0.6",
71
71
  "eslint-config-flat-gitignore": "2.3.0",
72
72
  "eslint-config-prettier": "10.1.8",
73
73
  "eslint-flat-config-utils": "3.2.0",
@@ -98,7 +98,7 @@
98
98
  "globals": "17.7.0",
99
99
  "jsonc-eslint-parser": "3.1.0",
100
100
  "local-pkg": "1.2.1",
101
- "oxfmt": "0.63.0",
101
+ "oxfmt": "0.72.0",
102
102
  "oxlint-plugin-eslint": "1.78.0",
103
103
  "oxlint-plugin-oxlint-comments": "0.1.0",
104
104
  "prettier": "3.9.6",
@@ -145,7 +145,7 @@
145
145
  "typescript": "6.0.3",
146
146
  "unplugin-unused": "0.5.7",
147
147
  "vitest": "4.1.10",
148
- "@isentinel/eslint-config": "6.0.1"
148
+ "@isentinel/eslint-config": "6.1.0"
149
149
  },
150
150
  "peerDependencies": {
151
151
  "@vitest/eslint-plugin": "^1.6.4",