@isentinel/eslint-config 6.0.0 → 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 +46 -2
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +8 -1
- package/dist/index.mjs +12 -3
- package/dist/lint-bootstrap.d.mts +1 -0
- package/dist/lint-bootstrap.mjs +98 -0
- package/dist/lint-cli.mjs +691 -129
- package/dist/oxlint.d.mts +7 -2
- package/dist/oxlint.mjs +22 -16
- package/package.json +4 -4
package/dist/lint-cli.mjs
CHANGED
|
@@ -1,18 +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";
|
|
4
7
|
import yargs from "yargs";
|
|
5
8
|
import "find-up-simple";
|
|
6
|
-
import fs from "node:fs";
|
|
7
|
-
import path from "node:path";
|
|
9
|
+
import fs$1 from "node:fs";
|
|
8
10
|
import "semver";
|
|
9
11
|
import crypto from "node:crypto";
|
|
10
12
|
import ansis from "ansis";
|
|
11
|
-
import
|
|
13
|
+
import { KillOnSignal, LogError, LogExit, LogOutput, Logger, createConcurrently } from "concurrently";
|
|
12
14
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
15
|
+
import os, { availableParallelism } from "node:os";
|
|
13
16
|
import { Writable } from "node:stream";
|
|
17
|
+
import v8 from "node:v8";
|
|
14
18
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import os, { availableParallelism } from "node:os";
|
|
16
19
|
import fileEntryCache from "file-entry-cache";
|
|
17
20
|
//#region \0rolldown/runtime.js
|
|
18
21
|
var __create = Object.create;
|
|
@@ -38,7 +41,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
38
41
|
}) : target, mod));
|
|
39
42
|
//#endregion
|
|
40
43
|
//#region package.json
|
|
41
|
-
var version = "6.
|
|
44
|
+
var version = "6.1.0";
|
|
42
45
|
//#endregion
|
|
43
46
|
//#region src/lint-cli/lib/cli/types.ts
|
|
44
47
|
/**
|
|
@@ -48,6 +51,78 @@ var version = "6.0.0";
|
|
|
48
51
|
var CliError = class extends Error {
|
|
49
52
|
name = "CliError";
|
|
50
53
|
};
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/lint-cli/lib/cli/file-list.ts
|
|
56
|
+
/** The `--files0-from` value that names stdin rather than a file. */
|
|
57
|
+
const STDIN_SOURCE = "-";
|
|
58
|
+
/**
|
|
59
|
+
* Split a NUL-delimited, UTF-8 path list (the `git ls-files -z` / `find
|
|
60
|
+
* -print0` shape) into its paths.
|
|
61
|
+
*
|
|
62
|
+
* Every path is terminated by a NUL, though the last terminator may be left off
|
|
63
|
+
* as GNU's own `--files0-from` allows. Nothing else is interpreted: spaces,
|
|
64
|
+
* newlines, quotes, shell metacharacters and leading hyphens all stay part of
|
|
65
|
+
* the name. The bytes must be valid UTF-8 — a lossy decode would lint a file
|
|
66
|
+
* that is not the one selected — and no entry may be empty, since an empty
|
|
67
|
+
* path is never a file and most likely a broken producer.
|
|
68
|
+
*
|
|
69
|
+
* @param bytes - The raw list.
|
|
70
|
+
* @returns The paths, in list order. Empty for an empty list.
|
|
71
|
+
*/
|
|
72
|
+
function decodeFileList(bytes) {
|
|
73
|
+
const decoder = new TextDecoder("utf-8", {
|
|
74
|
+
fatal: true,
|
|
75
|
+
ignoreBOM: true
|
|
76
|
+
});
|
|
77
|
+
let text;
|
|
78
|
+
try {
|
|
79
|
+
text = decoder.decode(bytes);
|
|
80
|
+
} catch {
|
|
81
|
+
throw new CliError("--files0-from: the file list is not valid UTF-8.");
|
|
82
|
+
}
|
|
83
|
+
if (text.length === 0) return [];
|
|
84
|
+
const entries = text.split("\0");
|
|
85
|
+
if (entries.at(-1) === "") entries.pop();
|
|
86
|
+
const empty = entries.indexOf("");
|
|
87
|
+
if (empty !== -1) throw new CliError(`--files0-from: entry ${empty + 1} of the file list is empty; expected NUL-terminated paths.`);
|
|
88
|
+
return entries;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Fill the run's paths from its `--files0-from` list, reading it once. Options
|
|
92
|
+
* without a list are returned as they are.
|
|
93
|
+
*
|
|
94
|
+
* An empty list stays empty: the caller treats it as nothing to lint, and it
|
|
95
|
+
* must never become the whole-tree default `.`.
|
|
96
|
+
*
|
|
97
|
+
* @param options - The parsed CLI options.
|
|
98
|
+
* @param cwd - The directory a relative list path resolves against.
|
|
99
|
+
* @param stdin - The stream `-` reads.
|
|
100
|
+
* @returns The options with `paths` set from the list.
|
|
101
|
+
* @rejects {CliError} When the list cannot be read or is malformed.
|
|
102
|
+
*/
|
|
103
|
+
async function readFileList(options, cwd, stdin) {
|
|
104
|
+
const source = options.filesFrom;
|
|
105
|
+
if (source === void 0) return options;
|
|
106
|
+
const bytes = source === STDIN_SOURCE ? await readAll(stdin) : await readFile(source, cwd);
|
|
107
|
+
return {
|
|
108
|
+
...options,
|
|
109
|
+
paths: decodeFileList(bytes)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function readFile(source, cwd) {
|
|
113
|
+
try {
|
|
114
|
+
return await fs.readFile(path.resolve(cwd, source));
|
|
115
|
+
} catch (err) {
|
|
116
|
+
throw new CliError(`--files0-from: cannot read "${source}": ${err instanceof Error ? err.message : String(err)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function readAll(stream) {
|
|
120
|
+
const chunks = [];
|
|
121
|
+
for await (const chunk of stream) if (typeof chunk === "string") chunks.push(Buffer$1.from(chunk, "utf8"));
|
|
122
|
+
else if (chunk instanceof Uint8Array) chunks.push(Buffer$1.from(chunk));
|
|
123
|
+
else throw new CliError("--files0-from: stdin produced a non-byte chunk.");
|
|
124
|
+
return Buffer$1.concat(chunks);
|
|
125
|
+
}
|
|
51
126
|
/**
|
|
52
127
|
* The real TS/JS-family file extensions {@link GLOB_SRC_EXT} encodes (the glob
|
|
53
128
|
* additionally matches non-existent combinations such as `cjsx`). Kept next to
|
|
@@ -288,6 +363,13 @@ function splitArgs(input) {
|
|
|
288
363
|
//#endregion
|
|
289
364
|
//#region src/lint-cli/lib/cli/options.ts
|
|
290
365
|
/**
|
|
366
|
+
* The ESLint flags that make it lint source text read from its own stdin. The
|
|
367
|
+
* engine's stdin carries its argument list (see `bootstrap.ts`), so these can
|
|
368
|
+
* never work through the runner and are refused rather than silently linting
|
|
369
|
+
* empty input.
|
|
370
|
+
*/
|
|
371
|
+
const STDIN_SOURCE_FLAGS = ["--stdin", "--stdin-filename"];
|
|
372
|
+
/**
|
|
291
373
|
* Parse and validate an `isentinel-lint` argv slice (without the node/bin
|
|
292
374
|
* prefix). Throws {@link CliError} on any invalid combination.
|
|
293
375
|
*
|
|
@@ -305,7 +387,11 @@ function parseArguments(argv, environment = process$1.env) {
|
|
|
305
387
|
const tail = separator === -1 ? [] : argv.slice(separator);
|
|
306
388
|
const eslintExtract = extractValueOption(head, "eslint-args");
|
|
307
389
|
const oxlintExtract = extractValueOption(eslintExtract.rest, "oxlint-args");
|
|
308
|
-
const
|
|
390
|
+
const filesExtract = extractValueOption(oxlintExtract.rest, "files0-from");
|
|
391
|
+
if (filesExtract.occurrences > 1) throw new CliError("Option --files0-from may only be given once.");
|
|
392
|
+
const filesFrom = filesExtract.value;
|
|
393
|
+
if (filesFrom?.length === 0) throw new CliError("Option --files0-from requires a path, or - for stdin.");
|
|
394
|
+
const parsed = yargs([...filesExtract.rest, ...tail]).scriptName("isentinel-lint").parserConfiguration({
|
|
309
395
|
"boolean-negation": true,
|
|
310
396
|
"populate--": true
|
|
311
397
|
}).option("eslint", {
|
|
@@ -359,18 +445,22 @@ function parseArguments(argv, environment = process$1.env) {
|
|
|
359
445
|
if (eslintOnly) eslintArgs.push(...passthrough);
|
|
360
446
|
else oxlintArgs.push(...passthrough);
|
|
361
447
|
}
|
|
448
|
+
const stdinFlag = eslintArgs.find(isStdinSourceFlag);
|
|
449
|
+
if (stdinFlag !== void 0) throw new CliError(`Cannot forward ${stdinFlag} to ESLint: isentinel-lint lints files, not source text on stdin. Run ESLint directly to lint stdin.`);
|
|
362
450
|
const paths = parsed._.map(String).filter((value) => value.length > 0);
|
|
451
|
+
if (filesFrom !== void 0 && paths.length > 0) throw new CliError("Cannot combine --files0-from with positional paths; put every path in the list.");
|
|
363
452
|
return {
|
|
364
453
|
agents: parsed.agents ?? isInAgentSession(environment),
|
|
365
454
|
cache: parsed.cache,
|
|
366
455
|
concurrency: parseConcurrency(parsed.concurrency),
|
|
367
456
|
eslint: eslintOnly,
|
|
368
457
|
eslintArgs,
|
|
458
|
+
filesFrom,
|
|
369
459
|
fix,
|
|
370
460
|
oxlint: oxlintOnly,
|
|
371
461
|
oxlintArgs,
|
|
372
462
|
oxlintTypeAware: parsed.oxlintTypeAware,
|
|
373
|
-
paths: paths.length > 0 ? paths : ["."],
|
|
463
|
+
paths: filesFrom !== void 0 || paths.length > 0 ? paths : ["."],
|
|
374
464
|
print: parsed.print === true,
|
|
375
465
|
typeAware
|
|
376
466
|
};
|
|
@@ -389,6 +479,7 @@ function extractValueOption(argv, name) {
|
|
|
389
479
|
const inline = `${flag}=`;
|
|
390
480
|
const rest = [];
|
|
391
481
|
let value;
|
|
482
|
+
let occurrences = 0;
|
|
392
483
|
for (let index = 0; index < argv.length; index += 1) {
|
|
393
484
|
const token = argv[index];
|
|
394
485
|
if (token === void 0) continue;
|
|
@@ -396,20 +487,26 @@ function extractValueOption(argv, name) {
|
|
|
396
487
|
const next = argv[index + 1];
|
|
397
488
|
if (next === void 0) throw new CliError(`Option ${flag} requires a value.`);
|
|
398
489
|
value = next;
|
|
490
|
+
occurrences += 1;
|
|
399
491
|
index += 1;
|
|
400
492
|
continue;
|
|
401
493
|
}
|
|
402
494
|
if (token.startsWith(inline)) {
|
|
403
495
|
value = token.slice(inline.length);
|
|
496
|
+
occurrences += 1;
|
|
404
497
|
continue;
|
|
405
498
|
}
|
|
406
499
|
rest.push(token);
|
|
407
500
|
}
|
|
408
501
|
return {
|
|
502
|
+
occurrences,
|
|
409
503
|
rest,
|
|
410
504
|
value
|
|
411
505
|
};
|
|
412
506
|
}
|
|
507
|
+
function isStdinSourceFlag(token) {
|
|
508
|
+
return STDIN_SOURCE_FLAGS.some((flag) => token === flag || token.startsWith(`${flag}=`));
|
|
509
|
+
}
|
|
413
510
|
function parseConcurrency(value) {
|
|
414
511
|
if (value === void 0) return;
|
|
415
512
|
if (value === "off") return "off";
|
|
@@ -474,6 +571,188 @@ function resolveRunContext(cwd, environment, mutate) {
|
|
|
474
571
|
};
|
|
475
572
|
}
|
|
476
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
|
|
477
756
|
//#region src/lint-cli/lib/state.ts
|
|
478
757
|
/**
|
|
479
758
|
* Directory (relative to the project root) where the CLI keeps its persisted
|
|
@@ -514,7 +793,7 @@ function statePath(cwd, name, ...parts) {
|
|
|
514
793
|
*/
|
|
515
794
|
function readFileIfPresent(filePath) {
|
|
516
795
|
try {
|
|
517
|
-
return fs.readFileSync(filePath, "utf8");
|
|
796
|
+
return fs$1.readFileSync(filePath, "utf8");
|
|
518
797
|
} catch {
|
|
519
798
|
return;
|
|
520
799
|
}
|
|
@@ -555,12 +834,12 @@ function writeState(filePath, data) {
|
|
|
555
834
|
})}\n`;
|
|
556
835
|
const temporary = `${filePath}.${process$1.pid}.tmp`;
|
|
557
836
|
try {
|
|
558
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
559
|
-
fs.writeFileSync(temporary, content);
|
|
560
|
-
fs.renameSync(temporary, filePath);
|
|
837
|
+
fs$1.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
838
|
+
fs$1.writeFileSync(temporary, content);
|
|
839
|
+
fs$1.renameSync(temporary, filePath);
|
|
561
840
|
} catch {
|
|
562
841
|
try {
|
|
563
|
-
fs.rmSync(temporary, { force: true });
|
|
842
|
+
fs$1.rmSync(temporary, { force: true });
|
|
564
843
|
} catch {}
|
|
565
844
|
}
|
|
566
845
|
}
|
|
@@ -595,7 +874,7 @@ function swapState(filePath, value) {
|
|
|
595
874
|
function touchState(filePath) {
|
|
596
875
|
try {
|
|
597
876
|
const now = Date.now() / 1e3;
|
|
598
|
-
fs.utimesSync(filePath, now, now);
|
|
877
|
+
fs$1.utimesSync(filePath, now, now);
|
|
599
878
|
} catch {}
|
|
600
879
|
}
|
|
601
880
|
/**
|
|
@@ -688,7 +967,23 @@ function resolveAgentsFormatter() {
|
|
|
688
967
|
*/
|
|
689
968
|
function resolveIgnoredHelper() {
|
|
690
969
|
const built = fileURLToPath(new URL("./lint-ignored.mjs", import.meta.url));
|
|
691
|
-
return fs.existsSync(built) ? built : fileURLToPath(new URL("../../ignored-child.ts", import.meta.url));
|
|
970
|
+
return fs$1.existsSync(built) ? built : fileURLToPath(new URL("../../ignored-child.ts", import.meta.url));
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Resolve the engine bootstrap shipped alongside this entry (built from
|
|
974
|
+
* `src/lint-cli/bootstrap.ts` to `lint-bootstrap.mjs`), which every engine
|
|
975
|
+
* child is launched through so its arguments travel over stdin.
|
|
976
|
+
*
|
|
977
|
+
* Falls back to the TypeScript source exactly as {@link resolveIgnoredHelper}
|
|
978
|
+
* does, for the same reason: the fixture tests run from `src`. Unlike that
|
|
979
|
+
* helper there is no degraded mode — without the bootstrap no engine runs — so
|
|
980
|
+
* the shipped path is checked by the integration tests against the build.
|
|
981
|
+
*
|
|
982
|
+
* @returns The absolute path to the bootstrap module.
|
|
983
|
+
*/
|
|
984
|
+
function resolveBootstrap() {
|
|
985
|
+
const built = fileURLToPath(new URL("./lint-bootstrap.mjs", import.meta.url));
|
|
986
|
+
return fs$1.existsSync(built) ? built : fileURLToPath(new URL("../../bootstrap.ts", import.meta.url));
|
|
692
987
|
}
|
|
693
988
|
/**
|
|
694
989
|
* The path a package's manifest declares as the bin of the package's own name,
|
|
@@ -735,65 +1030,33 @@ function resolveManifest(name, cwd) {
|
|
|
735
1030
|
}
|
|
736
1031
|
}
|
|
737
1032
|
//#endregion
|
|
738
|
-
//#region src/lint-cli/lib/exec/
|
|
739
|
-
const SAFE_TOKEN = /^[\w@+=:,./-]+$/;
|
|
1033
|
+
//#region src/lint-cli/lib/exec/execute.ts
|
|
740
1034
|
/**
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
* output.
|
|
744
|
-
*
|
|
745
|
-
* @param command - The child command to render.
|
|
746
|
-
* @returns The shell-equivalent command line.
|
|
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.
|
|
747
1037
|
*/
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1038
|
+
const OUT_OF_MEMORY = /ERR_WORKER_OUT_OF_MEMORY|heap out of memory/;
|
|
1039
|
+
/** The signals that cancel a run, matching `concurrently`'s `KillOnSignal`. */
|
|
1040
|
+
const CANCEL_SIGNALS = [
|
|
1041
|
+
"SIGHUP",
|
|
1042
|
+
"SIGINT",
|
|
1043
|
+
"SIGTERM"
|
|
1044
|
+
];
|
|
753
1045
|
/**
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
* @
|
|
758
|
-
* @param binJsPath - Absolute path to the tool's JavaScript entry.
|
|
759
|
-
* @param args - The tool arguments.
|
|
760
|
-
* @param platform - The platform whose shell quoting rules to apply.
|
|
761
|
-
* @returns The shell command string.
|
|
1046
|
+
* The first cancelling signal received while this run's children were running.
|
|
1047
|
+
* Shared across `execute` calls on purpose: a run is a sequence of them (three
|
|
1048
|
+
* for a `--fix` run), and once the user has cancelled one of them none of
|
|
1049
|
+
* the rest may start. {@link resetCancellation} Scopes it to one run.
|
|
762
1050
|
*/
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
if (offending !== void 0) throw new CliError(`Cannot safely pass "${offending}" to cmd.exe: "%" triggers environment-variable expansion even inside quotes. Remove it, or run the tool directly.`);
|
|
772
|
-
}
|
|
773
|
-
const quote = platform === "win32" ? quoteWindows : quotePosix;
|
|
774
|
-
return tokens.map(quote).join(" ");
|
|
775
|
-
}
|
|
776
|
-
function quotePosix(token) {
|
|
777
|
-
if (token.length > 0 && SAFE_TOKEN.test(token)) return token;
|
|
778
|
-
return `'${token.replaceAll("'", "'\\''")}'`;
|
|
779
|
-
}
|
|
780
|
-
function quoteWindows(token) {
|
|
781
|
-
if (token.length > 0 && SAFE_TOKEN.test(token)) return token;
|
|
782
|
-
let escaped = "";
|
|
783
|
-
let slashes = 0;
|
|
784
|
-
for (const char of token) {
|
|
785
|
-
if (char === "\\") {
|
|
786
|
-
slashes += 1;
|
|
787
|
-
escaped += char;
|
|
788
|
-
continue;
|
|
789
|
-
}
|
|
790
|
-
escaped += char === "\"" ? `${"\\".repeat(slashes)}\\"` : char;
|
|
791
|
-
slashes = 0;
|
|
792
|
-
}
|
|
793
|
-
return `"${escaped}${"\\".repeat(slashes)}"`;
|
|
1051
|
+
let cancelledBy;
|
|
1052
|
+
/**
|
|
1053
|
+
* Forget any cancellation recorded by an earlier run in this process. Called
|
|
1054
|
+
* as a run starts, so a second `runLint` in the same process is not refused
|
|
1055
|
+
* for a signal the first one received.
|
|
1056
|
+
*/
|
|
1057
|
+
function resetCancellation() {
|
|
1058
|
+
cancelledBy = void 0;
|
|
794
1059
|
}
|
|
795
|
-
//#endregion
|
|
796
|
-
//#region src/lint-cli/lib/exec/execute.ts
|
|
797
1060
|
/** Prefix colour per child label for `concurrently`; kept visually distinct. */
|
|
798
1061
|
const PREFIX_COLOR = {
|
|
799
1062
|
eslint: "blue",
|
|
@@ -802,6 +1065,17 @@ const PREFIX_COLOR = {
|
|
|
802
1065
|
typed: "cyan"
|
|
803
1066
|
};
|
|
804
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
|
+
/**
|
|
805
1079
|
* Run the composed children and aggregate their exit codes.
|
|
806
1080
|
*
|
|
807
1081
|
* Sequential when the caller asks for it: a lone child gains nothing from the
|
|
@@ -818,7 +1092,8 @@ const PREFIX_COLOR = {
|
|
|
818
1092
|
* @returns The aggregated exit code.
|
|
819
1093
|
*/
|
|
820
1094
|
async function execute(commands, cwd, sequential) {
|
|
821
|
-
|
|
1095
|
+
const code = await (sequential ? runSequential(commands, cwd) : startGroup(commands, cwd));
|
|
1096
|
+
return cancelledCode() ?? code;
|
|
822
1097
|
}
|
|
823
1098
|
/**
|
|
824
1099
|
* Run a staged set of children: spawn `commands` now, then resolve the rest and
|
|
@@ -845,13 +1120,19 @@ async function execute(commands, cwd, sequential) {
|
|
|
845
1120
|
async function executeStaged(commands, cwd, resolveDeferred) {
|
|
846
1121
|
const eager = startGroup(commands, cwd);
|
|
847
1122
|
const deferred = resolveDeferred();
|
|
848
|
-
|
|
1123
|
+
await new Promise((resolve) => {
|
|
1124
|
+
setImmediate(resolve);
|
|
1125
|
+
});
|
|
1126
|
+
if (cancelledBy !== void 0 || deferred.length === 0) {
|
|
1127
|
+
const eagerCode = await eager;
|
|
1128
|
+
return cancelledCode() ?? eagerCode;
|
|
1129
|
+
}
|
|
849
1130
|
const held = createHeldOutput();
|
|
850
1131
|
const later = startGroup(deferred, cwd, held.stream);
|
|
851
1132
|
const eagerCode = await eager;
|
|
852
1133
|
held.release();
|
|
853
1134
|
const deferredCode = await later;
|
|
854
|
-
return eagerCode === 0 ? deferredCode : eagerCode;
|
|
1135
|
+
return cancelledCode() ?? (eagerCode === 0 ? deferredCode : eagerCode);
|
|
855
1136
|
}
|
|
856
1137
|
/**
|
|
857
1138
|
* Announce a child on stderr once it has been spawned. `concurrently` groups
|
|
@@ -867,30 +1148,188 @@ function announce(command) {
|
|
|
867
1148
|
process$1.stderr.write(`${prefix} spawned ${command.bin}\n`);
|
|
868
1149
|
}
|
|
869
1150
|
/**
|
|
1151
|
+
* The names the children of one group are registered and logged under: the
|
|
1152
|
+
* engine, never its arguments. `concurrently` prints the name on each child's
|
|
1153
|
+
* exit line, so it must stay short however many files the child lints. The
|
|
1154
|
+
* line already carries the pass as its prefix (`[fast] eslint exited with code
|
|
1155
|
+
* 1`), so the pass only joins the name when two children share an engine.
|
|
1156
|
+
*
|
|
1157
|
+
* @param commands - The group's children.
|
|
1158
|
+
* @returns Each child paired with a name unique within the group.
|
|
1159
|
+
*/
|
|
1160
|
+
function nameCommands(commands) {
|
|
1161
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1162
|
+
return commands.map((command) => {
|
|
1163
|
+
const base = commands.filter((other) => other.bin === command.bin).length > 1 ? `${command.bin} (${command.label})` : command.bin;
|
|
1164
|
+
let name = base;
|
|
1165
|
+
let suffix = 1;
|
|
1166
|
+
while (taken.has(name)) {
|
|
1167
|
+
suffix += 1;
|
|
1168
|
+
name = `${base} #${String(suffix)}`;
|
|
1169
|
+
}
|
|
1170
|
+
taken.add(name);
|
|
1171
|
+
return [name, command];
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Spawn an engine child through the bootstrap and stream it its arguments.
|
|
1176
|
+
*
|
|
1177
|
+
* The OS command line is always `node <bootstrap> <bin>`, with no shell, so its
|
|
1178
|
+
* length does not depend on the file list and no path is ever interpreted by a
|
|
1179
|
+
* shell. The complete argument list follows as JSON on the child's stdin,
|
|
1180
|
+
* which is why stdin is always a pipe here whatever the caller asked for.
|
|
1181
|
+
*
|
|
1182
|
+
* @param launch - The child and its resolved bin.
|
|
1183
|
+
* @param options - Spawn options (cwd, env, stdout/stderr handling).
|
|
1184
|
+
* @returns The spawned child.
|
|
1185
|
+
*/
|
|
1186
|
+
function launchEngine({ bin, command }, options) {
|
|
1187
|
+
const stdio = Array.isArray(options.stdio) ? options.stdio : [];
|
|
1188
|
+
const child = spawn(process$1.execPath, [resolveBootstrap(), bin], {
|
|
1189
|
+
...options,
|
|
1190
|
+
shell: false,
|
|
1191
|
+
stdio: [
|
|
1192
|
+
"pipe",
|
|
1193
|
+
stdio[1] ?? "inherit",
|
|
1194
|
+
stdio[2] ?? "inherit"
|
|
1195
|
+
]
|
|
1196
|
+
});
|
|
1197
|
+
child.stdin?.on("error", () => {});
|
|
1198
|
+
child.stdin?.end(JSON.stringify(command.args));
|
|
1199
|
+
return child;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Kill a group's children and stop it spawning more when its output stream
|
|
1203
|
+
* fails — typically `EPIPE` once the reader of a piped report (`| head`) has
|
|
1204
|
+
* gone. Mirrors `concurrently`'s own `OutputErrorHandler`, which its package
|
|
1205
|
+
* exports do not expose.
|
|
1206
|
+
*
|
|
1207
|
+
* @param outputStream - The stream the group writes to.
|
|
1208
|
+
* @param abortController - Aborts the group.
|
|
1209
|
+
* @returns The flow controller.
|
|
1210
|
+
*/
|
|
1211
|
+
function killOnOutputError(outputStream, abortController) {
|
|
1212
|
+
return { handle: (commands) => {
|
|
1213
|
+
function onError() {
|
|
1214
|
+
for (const command of commands) command.kill();
|
|
1215
|
+
abortController.abort();
|
|
1216
|
+
}
|
|
1217
|
+
outputStream.on("error", onError);
|
|
1218
|
+
return {
|
|
1219
|
+
commands,
|
|
1220
|
+
onFinish: () => {
|
|
1221
|
+
outputStream.off("error", onError);
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
} };
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Record a cancelling signal for as long as children are running, and hand it
|
|
1228
|
+
* to `onSignal`. Listening at all replaces Node's default of exiting on the
|
|
1229
|
+
* signal, which is what lets the children be stopped and reaped first rather
|
|
1230
|
+
* than orphaned.
|
|
1231
|
+
*
|
|
1232
|
+
* @param onSignal - Forwards the signal to the running children; omitted when
|
|
1233
|
+
* something else (`concurrently`'s `KillOnSignal`) already does.
|
|
1234
|
+
* @returns Removes the listeners.
|
|
1235
|
+
*/
|
|
1236
|
+
function trackCancellation(onSignal) {
|
|
1237
|
+
function listener(signal) {
|
|
1238
|
+
cancelledBy ??= signal;
|
|
1239
|
+
onSignal?.(signal);
|
|
1240
|
+
}
|
|
1241
|
+
for (const signal of CANCEL_SIGNALS) process$1.on(signal, listener);
|
|
1242
|
+
return () => {
|
|
1243
|
+
for (const signal of CANCEL_SIGNALS) process$1.off(signal, listener);
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
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
|
+
/**
|
|
870
1268
|
* Start every command as one `concurrently` group and announce the children.
|
|
871
1269
|
* Unlike the previous `killOthersOn: ["failure"]` behaviour, an ordinary lint
|
|
872
1270
|
* failure in one child no longer kills its siblings — each runs to the end so
|
|
873
1271
|
* the user keeps every result. The resolved code is non-zero when any child
|
|
874
1272
|
* exited non-zero; the promise never rejects.
|
|
875
1273
|
*
|
|
1274
|
+
* Built on `concurrently`'s core rather than its default export, because the
|
|
1275
|
+
* wrapper always launches a command *string* through a shell (`cmd.exe` on
|
|
1276
|
+
* Windows), and that string would have to carry every lint target. Here the
|
|
1277
|
+
* group's spawner launches each engine directly through the bootstrap (see
|
|
1278
|
+
* {@link launchEngine}); the command string is only the label `concurrently`
|
|
1279
|
+
* logs. The controllers are the ones the wrapper wires that do anything under
|
|
1280
|
+
* the options this runner always used — output and error logging, exit lines,
|
|
1281
|
+
* signal forwarding and the broken-output-pipe guard — and nothing else.
|
|
1282
|
+
*
|
|
876
1283
|
* @param commands - The child commands to run.
|
|
877
1284
|
* @param cwd - The working directory.
|
|
878
1285
|
* @param outputStream - Where the group writes its child output; defaults to
|
|
879
1286
|
* stdout. A staged run passes a held stream (see {@link createHeldOutput}).
|
|
880
1287
|
* @returns The aggregated exit code.
|
|
881
1288
|
*/
|
|
882
|
-
async function startGroup(commands, cwd, outputStream) {
|
|
883
|
-
|
|
1289
|
+
async function startGroup(commands, cwd, outputStream = process$1.stdout) {
|
|
1290
|
+
if (cancelledBy !== void 0 || commands.length === 0) return 0;
|
|
1291
|
+
const launches = new Map(nameCommands(commands).map(([name, command]) => {
|
|
1292
|
+
return [name, {
|
|
1293
|
+
bin: resolveLocalBin(command.bin, cwd),
|
|
1294
|
+
command
|
|
1295
|
+
}];
|
|
1296
|
+
}));
|
|
1297
|
+
const logger = new Logger({});
|
|
1298
|
+
const exhausted = /* @__PURE__ */ new Set();
|
|
1299
|
+
const abortController = new AbortController();
|
|
1300
|
+
const stopTracking = trackCancellation();
|
|
1301
|
+
const { result } = createConcurrently(Array.from(launches, ([name, { command }]) => {
|
|
884
1302
|
return {
|
|
885
1303
|
name: command.label,
|
|
886
|
-
command:
|
|
1304
|
+
command: name,
|
|
887
1305
|
env: command.env,
|
|
888
1306
|
prefixColor: PREFIX_COLOR[command.label]
|
|
889
1307
|
};
|
|
890
1308
|
}), {
|
|
1309
|
+
abortSignal: abortController.signal,
|
|
1310
|
+
controllers: [
|
|
1311
|
+
new LogError({ logger }),
|
|
1312
|
+
new LogOutput({ logger }),
|
|
1313
|
+
new LogExit({ logger }),
|
|
1314
|
+
new KillOnSignal({
|
|
1315
|
+
abortController,
|
|
1316
|
+
process: process$1
|
|
1317
|
+
}),
|
|
1318
|
+
killOnOutputError(outputStream, abortController)
|
|
1319
|
+
],
|
|
891
1320
|
cwd,
|
|
892
1321
|
group: true,
|
|
893
|
-
|
|
1322
|
+
logger,
|
|
1323
|
+
outputStream,
|
|
1324
|
+
spawn: (name, options) => {
|
|
1325
|
+
const launch = launches.get(name);
|
|
1326
|
+
if (launch === void 0) throw new Error(`isentinel-lint: no engine registered as "${name}".`);
|
|
1327
|
+
const child = launchEngine(launch, options);
|
|
1328
|
+
if (launch.command.watchHeap) watchOutOfMemory(child, () => {
|
|
1329
|
+
exhausted.add(launch.command.label);
|
|
1330
|
+
});
|
|
1331
|
+
return child;
|
|
1332
|
+
}
|
|
894
1333
|
});
|
|
895
1334
|
for (const command of commands) announce(command);
|
|
896
1335
|
try {
|
|
@@ -898,31 +1337,70 @@ async function startGroup(commands, cwd, outputStream) {
|
|
|
898
1337
|
return 0;
|
|
899
1338
|
} catch {
|
|
900
1339
|
return 1;
|
|
1340
|
+
} finally {
|
|
1341
|
+
stopTracking();
|
|
1342
|
+
for (const label of exhausted) process$1.stderr.write(outOfMemoryHint(label));
|
|
901
1343
|
}
|
|
902
1344
|
}
|
|
1345
|
+
function signalExitCode(signal) {
|
|
1346
|
+
return 128 + os.constants.signals[signal];
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* The exit code for a cancelled run (`128 + signal number`, as a shell reports
|
|
1350
|
+
* it), or `undefined` when nothing cancelled it.
|
|
1351
|
+
*
|
|
1352
|
+
* @returns The exit code, when cancelled.
|
|
1353
|
+
*/
|
|
1354
|
+
function cancelledCode() {
|
|
1355
|
+
return cancelledBy === void 0 ? void 0 : signalExitCode(cancelledBy);
|
|
1356
|
+
}
|
|
903
1357
|
async function spawnChild(command, cwd) {
|
|
904
|
-
const
|
|
1358
|
+
const bin = resolveLocalBin(command.bin, cwd);
|
|
1359
|
+
const watched = command.watchHeap;
|
|
1360
|
+
let exhausted = false;
|
|
905
1361
|
return new Promise((resolve) => {
|
|
906
|
-
const child =
|
|
1362
|
+
const child = launchEngine({
|
|
1363
|
+
bin,
|
|
1364
|
+
command
|
|
1365
|
+
}, {
|
|
907
1366
|
cwd,
|
|
908
1367
|
env: {
|
|
909
1368
|
...process$1.env,
|
|
910
1369
|
...command.env
|
|
911
1370
|
},
|
|
912
|
-
stdio:
|
|
1371
|
+
stdio: [
|
|
1372
|
+
"inherit",
|
|
1373
|
+
"inherit",
|
|
1374
|
+
watched ? "pipe" : "inherit"
|
|
1375
|
+
]
|
|
913
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
|
+
}
|
|
914
1385
|
announce(command);
|
|
1386
|
+
const stopTracking = trackCancellation((signal) => {
|
|
1387
|
+
child.kill(signal);
|
|
1388
|
+
});
|
|
915
1389
|
child.on("error", () => {
|
|
1390
|
+
stopTracking();
|
|
916
1391
|
resolve(1);
|
|
917
1392
|
});
|
|
918
|
-
child.on("close", (code) => {
|
|
919
|
-
|
|
1393
|
+
child.on("close", (code, signal) => {
|
|
1394
|
+
stopTracking();
|
|
1395
|
+
if (exhausted) process$1.stderr.write(outOfMemoryHint(command.label));
|
|
1396
|
+
resolve(code ?? (signal === null ? 1 : signalExitCode(signal)));
|
|
920
1397
|
});
|
|
921
1398
|
});
|
|
922
1399
|
}
|
|
923
1400
|
async function runSequential(commands, cwd) {
|
|
924
1401
|
let exitCode = 0;
|
|
925
1402
|
for (const command of commands) {
|
|
1403
|
+
if (cancelledBy !== void 0) break;
|
|
926
1404
|
const code = await spawnChild(command, cwd);
|
|
927
1405
|
if (code !== 0) exitCode = code;
|
|
928
1406
|
}
|
|
@@ -961,6 +1439,26 @@ function createHeldOutput() {
|
|
|
961
1439
|
};
|
|
962
1440
|
}
|
|
963
1441
|
//#endregion
|
|
1442
|
+
//#region src/lint-cli/lib/exec/shell.ts
|
|
1443
|
+
const SAFE_TOKEN = /^[\w@+=:,./-]+$/;
|
|
1444
|
+
/**
|
|
1445
|
+
* Render a command as a shell-equivalent line (env prefix + logical binary +
|
|
1446
|
+
* arguments) for `--print`. Uses POSIX quoting for stable, cross-platform
|
|
1447
|
+
* output.
|
|
1448
|
+
*
|
|
1449
|
+
* @param command - The child command to render.
|
|
1450
|
+
* @returns The shell-equivalent command line.
|
|
1451
|
+
*/
|
|
1452
|
+
function formatCommandLine(command) {
|
|
1453
|
+
const environmentPrefix = Object.entries(command.env).flatMap(([key, value]) => value === void 0 ? [] : [`${key}=${quotePosix(value)}`]).join(" ");
|
|
1454
|
+
const body = [command.bin, ...command.args].map(quotePosix).join(" ");
|
|
1455
|
+
return environmentPrefix.length > 0 ? `${environmentPrefix} ${body}` : body;
|
|
1456
|
+
}
|
|
1457
|
+
function quotePosix(token) {
|
|
1458
|
+
if (token.length > 0 && SAFE_TOKEN.test(token)) return token;
|
|
1459
|
+
return `'${token.replaceAll("'", "'\\''")}'`;
|
|
1460
|
+
}
|
|
1461
|
+
//#endregion
|
|
964
1462
|
//#region src/lint-cli/lib/plan/command.ts
|
|
965
1463
|
/**
|
|
966
1464
|
* Compose the oxlint child command.
|
|
@@ -974,12 +1472,13 @@ function composeOxlintCommand(options, context) {
|
|
|
974
1472
|
if (options.agents) args.push("--format", "agent");
|
|
975
1473
|
if (context.oxlintTypeAware) args.push("--type-aware");
|
|
976
1474
|
if (options.fix) args.push("--fix");
|
|
977
|
-
args.push("--no-error-on-unmatched-pattern", ...options.oxlintArgs, ...context.paths);
|
|
1475
|
+
args.push("--no-error-on-unmatched-pattern", ...options.oxlintArgs, ...pathArguments(context.paths));
|
|
978
1476
|
return {
|
|
979
1477
|
args,
|
|
980
1478
|
bin: "oxlint",
|
|
981
1479
|
env: {},
|
|
982
|
-
label: "oxc"
|
|
1480
|
+
label: "oxc",
|
|
1481
|
+
watchHeap: false
|
|
983
1482
|
};
|
|
984
1483
|
}
|
|
985
1484
|
/**
|
|
@@ -996,14 +1495,30 @@ function composeEslintCommand(options, context) {
|
|
|
996
1495
|
if (options.cache && context.ci) args.push("--cache-strategy", "content");
|
|
997
1496
|
if (options.agents) args.push("--format", context.agentsFormatterPath);
|
|
998
1497
|
if (context.fix) args.push("--fix");
|
|
999
|
-
args.push(...options.eslintArgs, ...context.paths);
|
|
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;
|
|
1000
1501
|
return {
|
|
1001
1502
|
args,
|
|
1002
1503
|
bin: "eslint",
|
|
1003
|
-
env:
|
|
1004
|
-
label: context.eslintLabel
|
|
1504
|
+
env: environment,
|
|
1505
|
+
label: context.eslintLabel,
|
|
1506
|
+
watchHeap: context.heap?.watch ?? false
|
|
1005
1507
|
};
|
|
1006
1508
|
}
|
|
1509
|
+
/**
|
|
1510
|
+
* The lint targets as the tail of an engine's argument list. A relative target
|
|
1511
|
+
* that starts with `-` would be read as a flag, so it is passed as `./<path>`.
|
|
1512
|
+
* A `--` separator is not enough: ESLint's bin scans the raw argv for
|
|
1513
|
+
* `--init`, `--mcp`, `--stdin` and `--debug` regardless of it, so a listed file
|
|
1514
|
+
* named `--init` would start the config wizard.
|
|
1515
|
+
*
|
|
1516
|
+
* @param paths - The lint targets.
|
|
1517
|
+
* @returns The arguments that pass them as paths.
|
|
1518
|
+
*/
|
|
1519
|
+
function pathArguments(paths) {
|
|
1520
|
+
return paths.map((target) => target.startsWith("-") ? `./${target}` : target);
|
|
1521
|
+
}
|
|
1007
1522
|
//#endregion
|
|
1008
1523
|
//#region src/lint-cli/lib/plan/compose.ts
|
|
1009
1524
|
/** The stderr notice emitted when a lint target resolves outside the cwd. */
|
|
@@ -1036,6 +1551,7 @@ function composePasses(passes, runPlan, options) {
|
|
|
1036
1551
|
concurrency: pass.concurrency,
|
|
1037
1552
|
eslintLabel: pass.descriptor.label,
|
|
1038
1553
|
fix: false,
|
|
1554
|
+
heap: pass.heap,
|
|
1039
1555
|
paths: options.paths,
|
|
1040
1556
|
typeAwareEnv: pass.descriptor.typeAwareEnv
|
|
1041
1557
|
}));
|
|
@@ -1170,7 +1686,7 @@ function applyHashBust(run, bust, hash) {
|
|
|
1170
1686
|
const cleared = [];
|
|
1171
1687
|
for (const base of bust.caches) {
|
|
1172
1688
|
const cacheFilePath = path.resolve(run.cwd, cacheFileFor(base, run.key));
|
|
1173
|
-
fs.rmSync(cacheFilePath, { force: true });
|
|
1689
|
+
fs$1.rmSync(cacheFilePath, { force: true });
|
|
1174
1690
|
cleared.push(cacheFilePath);
|
|
1175
1691
|
}
|
|
1176
1692
|
return {
|
|
@@ -1447,7 +1963,7 @@ function normalizePath(filePath, platform = process$1.platform) {
|
|
|
1447
1963
|
* @returns The loaded cache, or `undefined` when the file does not exist.
|
|
1448
1964
|
*/
|
|
1449
1965
|
function openCache(cacheFilePath, useChecksum) {
|
|
1450
|
-
if (!fs.existsSync(cacheFilePath)) return;
|
|
1966
|
+
if (!fs$1.existsSync(cacheFilePath)) return;
|
|
1451
1967
|
const cache = load(cacheFilePath, useChecksum);
|
|
1452
1968
|
return {
|
|
1453
1969
|
filesWithMessages: (files) => filesWithMessagesIn(cache, files),
|
|
@@ -1457,7 +1973,7 @@ function openCache(cacheFilePath, useChecksum) {
|
|
|
1457
1973
|
}
|
|
1458
1974
|
function safeMtimeMs(filePath) {
|
|
1459
1975
|
try {
|
|
1460
|
-
return fs.statSync(filePath).mtimeMs;
|
|
1976
|
+
return fs$1.statSync(filePath).mtimeMs;
|
|
1461
1977
|
} catch {
|
|
1462
1978
|
return;
|
|
1463
1979
|
}
|
|
@@ -1474,7 +1990,7 @@ function safeMtimeMs(filePath) {
|
|
|
1474
1990
|
function listCacheFiles(cwd) {
|
|
1475
1991
|
let entries;
|
|
1476
1992
|
try {
|
|
1477
|
-
entries = fs.readdirSync(cwd);
|
|
1993
|
+
entries = fs$1.readdirSync(cwd);
|
|
1478
1994
|
} catch {
|
|
1479
1995
|
return [];
|
|
1480
1996
|
}
|
|
@@ -1482,7 +1998,7 @@ function listCacheFiles(cwd) {
|
|
|
1482
1998
|
}
|
|
1483
1999
|
function removeCacheFile(cacheFilePath) {
|
|
1484
2000
|
try {
|
|
1485
|
-
fs.rmSync(cacheFilePath, { force: true });
|
|
2001
|
+
fs$1.rmSync(cacheFilePath, { force: true });
|
|
1486
2002
|
} catch {}
|
|
1487
2003
|
}
|
|
1488
2004
|
/**
|
|
@@ -3491,7 +4007,7 @@ const WALK_BUDGET = 8192;
|
|
|
3491
4007
|
function findWorkspaceRoot(cwd) {
|
|
3492
4008
|
let current = cwd;
|
|
3493
4009
|
for (;;) {
|
|
3494
|
-
for (const marker of WORKSPACE_ROOT_MARKERS) if (fs.existsSync(path.join(current, marker))) return current;
|
|
4010
|
+
for (const marker of WORKSPACE_ROOT_MARKERS) if (fs$1.existsSync(path.join(current, marker))) return current;
|
|
3495
4011
|
const parent = path.dirname(current);
|
|
3496
4012
|
if (parent === current) return cwd;
|
|
3497
4013
|
current = parent;
|
|
@@ -3535,7 +4051,7 @@ function findWorkspaceMembers(root) {
|
|
|
3535
4051
|
function walk(directory, depth) {
|
|
3536
4052
|
let entries;
|
|
3537
4053
|
try {
|
|
3538
|
-
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
4054
|
+
entries = fs$1.readdirSync(directory, { withFileTypes: true });
|
|
3539
4055
|
} catch {
|
|
3540
4056
|
return true;
|
|
3541
4057
|
}
|
|
@@ -3545,7 +4061,7 @@ function findWorkspaceMembers(root) {
|
|
|
3545
4061
|
if (budget < 0) return false;
|
|
3546
4062
|
const absolute = path.join(directory, entry.name);
|
|
3547
4063
|
const relative = toPosix(path.relative(root, absolute));
|
|
3548
|
-
if (isMember(relative) && fs.existsSync(path.join(absolute, "package.json"))) members.push(absolute);
|
|
4064
|
+
if (isMember(relative) && fs$1.existsSync(path.join(absolute, "package.json"))) members.push(absolute);
|
|
3549
4065
|
if (depth < maxDepth && !walk(absolute, depth + 1)) return false;
|
|
3550
4066
|
}
|
|
3551
4067
|
return true;
|
|
@@ -3873,7 +4389,7 @@ function oxlintTargets(cwd, targets) {
|
|
|
3873
4389
|
const extension = path.extname(target).toLowerCase();
|
|
3874
4390
|
if (extension === "" || TYPE_AWARE_EXTENSION_SET.has(extension)) return true;
|
|
3875
4391
|
try {
|
|
3876
|
-
return fs.statSync(path.resolve(cwd, target)).isDirectory();
|
|
4392
|
+
return fs$1.statSync(path.resolve(cwd, target)).isDirectory();
|
|
3877
4393
|
} catch {
|
|
3878
4394
|
return false;
|
|
3879
4395
|
}
|
|
@@ -3910,7 +4426,7 @@ function collectAncestorBustFiles(cwd) {
|
|
|
3910
4426
|
for (;;) {
|
|
3911
4427
|
let entries = [];
|
|
3912
4428
|
try {
|
|
3913
|
-
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
4429
|
+
entries = fs$1.readdirSync(current, { withFileTypes: true });
|
|
3914
4430
|
} catch {
|
|
3915
4431
|
entries = [];
|
|
3916
4432
|
}
|
|
@@ -3933,7 +4449,7 @@ function collectAncestorBustFiles(cwd) {
|
|
|
3933
4449
|
* @returns The normalized cwd-relative posix target.
|
|
3934
4450
|
*/
|
|
3935
4451
|
function normalizeTarget(target, cwd) {
|
|
3936
|
-
let value = path.isAbsolute(target) ? toPosix(path.relative(cwd, target)) : toPosix(target);
|
|
4452
|
+
let value = path.isAbsolute(target) ? toPosix(path.relative(cwd, target)) : path.posix.normalize(toPosix(target));
|
|
3937
4453
|
if (value.startsWith("./")) value = value.slice(2);
|
|
3938
4454
|
while (value.endsWith("/")) value = value.slice(0, -1);
|
|
3939
4455
|
return value;
|
|
@@ -3960,8 +4476,15 @@ function isOutsideCwd(normalizedTarget) {
|
|
|
3960
4476
|
*/
|
|
3961
4477
|
function matchTargets(normalized) {
|
|
3962
4478
|
if (normalized.some((target) => target === "" || target === ".")) return () => true;
|
|
4479
|
+
const targets = new Set(normalized);
|
|
3963
4480
|
return (relative) => {
|
|
3964
|
-
|
|
4481
|
+
let candidate = relative;
|
|
4482
|
+
for (;;) {
|
|
4483
|
+
if (targets.has(candidate)) return true;
|
|
4484
|
+
const slash = candidate.lastIndexOf("/");
|
|
4485
|
+
if (slash === -1) return false;
|
|
4486
|
+
candidate = candidate.slice(0, slash);
|
|
4487
|
+
}
|
|
3965
4488
|
};
|
|
3966
4489
|
}
|
|
3967
4490
|
function isTypeAwareFile(filePath) {
|
|
@@ -3974,6 +4497,7 @@ function gitListFiles(cwd, pathSpecs) {
|
|
|
3974
4497
|
try {
|
|
3975
4498
|
return execFileSync("git", [
|
|
3976
4499
|
"ls-files",
|
|
4500
|
+
"-z",
|
|
3977
4501
|
"--cached",
|
|
3978
4502
|
"--others",
|
|
3979
4503
|
"--exclude-standard",
|
|
@@ -3983,7 +4507,7 @@ function gitListFiles(cwd, pathSpecs) {
|
|
|
3983
4507
|
cwd,
|
|
3984
4508
|
encoding: "utf8",
|
|
3985
4509
|
maxBuffer: 64 * 1024 * 1024
|
|
3986
|
-
}).split("\
|
|
4510
|
+
}).split("\0").filter((line) => line.length > 0);
|
|
3987
4511
|
} catch {
|
|
3988
4512
|
return;
|
|
3989
4513
|
}
|
|
@@ -3991,7 +4515,7 @@ function gitListFiles(cwd, pathSpecs) {
|
|
|
3991
4515
|
function walkDirectory(root, current, accumulator) {
|
|
3992
4516
|
let entries;
|
|
3993
4517
|
try {
|
|
3994
|
-
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
4518
|
+
entries = fs$1.readdirSync(current, { withFileTypes: true });
|
|
3995
4519
|
} catch {
|
|
3996
4520
|
return;
|
|
3997
4521
|
}
|
|
@@ -4010,7 +4534,7 @@ function walkFallback(cwd, targets) {
|
|
|
4010
4534
|
const absolute = path.resolve(cwd, target);
|
|
4011
4535
|
let stat;
|
|
4012
4536
|
try {
|
|
4013
|
-
stat = fs.statSync(absolute);
|
|
4537
|
+
stat = fs$1.statSync(absolute);
|
|
4014
4538
|
} catch {
|
|
4015
4539
|
continue;
|
|
4016
4540
|
}
|
|
@@ -4049,11 +4573,12 @@ function gitIgnoredTargets(cwd, targets) {
|
|
|
4049
4573
|
const output = execFileSync("git", [
|
|
4050
4574
|
"check-ignore",
|
|
4051
4575
|
"--no-index",
|
|
4052
|
-
"--stdin"
|
|
4576
|
+
"--stdin",
|
|
4577
|
+
"-z"
|
|
4053
4578
|
], {
|
|
4054
4579
|
cwd,
|
|
4055
4580
|
encoding: "utf8",
|
|
4056
|
-
input: targets.join("\
|
|
4581
|
+
input: `${targets.join("\0")}\0`,
|
|
4057
4582
|
maxBuffer: 64 * 1024 * 1024,
|
|
4058
4583
|
stdio: [
|
|
4059
4584
|
"pipe",
|
|
@@ -4061,7 +4586,7 @@ function gitIgnoredTargets(cwd, targets) {
|
|
|
4061
4586
|
"ignore"
|
|
4062
4587
|
]
|
|
4063
4588
|
});
|
|
4064
|
-
return new Set(output.split("\
|
|
4589
|
+
return new Set(output.split("\0").filter(Boolean));
|
|
4065
4590
|
} catch {
|
|
4066
4591
|
return /* @__PURE__ */ new Set();
|
|
4067
4592
|
}
|
|
@@ -4334,7 +4859,7 @@ function queryIgnoredFiles(cwd, targets) {
|
|
|
4334
4859
|
"ignore"
|
|
4335
4860
|
]
|
|
4336
4861
|
});
|
|
4337
|
-
const parsed = JSON.parse(fs.readFileSync(outFile, "utf8"));
|
|
4862
|
+
const parsed = JSON.parse(fs$1.readFileSync(outFile, "utf8"));
|
|
4338
4863
|
if (!isRecord(parsed)) return;
|
|
4339
4864
|
const { basePath, entries, ignored, mode } = parsed;
|
|
4340
4865
|
if (mode === "answers" && isStringArray(ignored)) return {
|
|
@@ -4350,7 +4875,7 @@ function queryIgnoredFiles(cwd, targets) {
|
|
|
4350
4875
|
} catch {
|
|
4351
4876
|
return;
|
|
4352
4877
|
} finally {
|
|
4353
|
-
fs.rmSync(outFile, { force: true });
|
|
4878
|
+
fs$1.rmSync(outFile, { force: true });
|
|
4354
4879
|
}
|
|
4355
4880
|
}
|
|
4356
4881
|
//#endregion
|
|
@@ -4393,7 +4918,7 @@ function readHybridStatus(cwd) {
|
|
|
4393
4918
|
function writeHybridStatus(cwd, oxlint) {
|
|
4394
4919
|
let installed;
|
|
4395
4920
|
try {
|
|
4396
|
-
installed = fs.existsSync(path.resolve(cwd, "node_modules"));
|
|
4921
|
+
installed = fs$1.existsSync(path.resolve(cwd, "node_modules"));
|
|
4397
4922
|
} catch {
|
|
4398
4923
|
return;
|
|
4399
4924
|
}
|
|
@@ -4572,6 +5097,7 @@ function resolveWorkerLimits(environment, availableParallelism, ci) {
|
|
|
4572
5097
|
const explicit = parsePositiveInteger(environment["LINT_MAX_WORKERS"]);
|
|
4573
5098
|
const maxWorkers = explicit ?? (ci ? availableParallelism : Math.floor(availableParallelism / 4));
|
|
4574
5099
|
return {
|
|
5100
|
+
explicitMaxWorkers: explicit !== void 0,
|
|
4575
5101
|
filesPerWorker,
|
|
4576
5102
|
maxWorkers,
|
|
4577
5103
|
typedMaxWorkers: explicit ?? Math.min(maxWorkers, 6)
|
|
@@ -4664,6 +5190,17 @@ function selectPasses(options, ci) {
|
|
|
4664
5190
|
return [FAST_PASS, TYPED_PASS];
|
|
4665
5191
|
}
|
|
4666
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
|
+
/**
|
|
4667
5204
|
* The worker cap for a pass. `invalidation` already records whether a pass
|
|
4668
5205
|
* builds a TypeScript program — the fast pass runs no builder precisely because
|
|
4669
5206
|
* it builds none — so it doubles as the cap selector rather than restating the
|
|
@@ -4674,7 +5211,7 @@ function selectPasses(options, ci) {
|
|
|
4674
5211
|
* @returns The worker cap for this pass.
|
|
4675
5212
|
*/
|
|
4676
5213
|
function maxWorkersFor(descriptor, limits) {
|
|
4677
|
-
return descriptor
|
|
5214
|
+
return buildsProgram(descriptor) ? limits.typedMaxWorkers : limits.maxWorkers;
|
|
4678
5215
|
}
|
|
4679
5216
|
//#endregion
|
|
4680
5217
|
//#region src/lint-cli/lib/plan/fix.ts
|
|
@@ -4739,13 +5276,18 @@ function collectFixTargets(passes, run, files) {
|
|
|
4739
5276
|
* @returns The fix child, or `undefined` when nothing was reported.
|
|
4740
5277
|
*/
|
|
4741
5278
|
function planFixChild(passes, run, inputs) {
|
|
4742
|
-
const { files, limits, options } = inputs;
|
|
5279
|
+
const { files, heap, limits, options } = inputs;
|
|
4743
5280
|
const covered = options.cache ? collectFixTargets(passes, run, files) : [];
|
|
4744
5281
|
const uncovered = options.cache ? files.outsideCwdTargets : options.paths;
|
|
4745
5282
|
const targets = [...covered, ...uncovered];
|
|
4746
5283
|
if (targets.length === 0) return;
|
|
4747
5284
|
const cacheFile = cacheFileFor(CACHE_FILE_DEFAULT, run.key);
|
|
4748
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
|
+
});
|
|
4749
5291
|
return composeEslintCommand({
|
|
4750
5292
|
...options,
|
|
4751
5293
|
cache: options.cache && uncovered.length === 0
|
|
@@ -4753,13 +5295,13 @@ function planFixChild(passes, run, inputs) {
|
|
|
4753
5295
|
agentsFormatterPath: inputs.agentsFormatterPath,
|
|
4754
5296
|
cacheLocation: cacheFile,
|
|
4755
5297
|
ci: run.ci,
|
|
4756
|
-
concurrency
|
|
4757
|
-
dirtyCount: targets.length,
|
|
4758
|
-
filesPerWorker: limits.filesPerWorker,
|
|
4759
|
-
maxWorkers: limits.typedMaxWorkers
|
|
4760
|
-
}),
|
|
5298
|
+
concurrency,
|
|
4761
5299
|
eslintLabel: FULL_PASS.label,
|
|
4762
5300
|
fix: true,
|
|
5301
|
+
heap: planPassHeap(FULL_PASS.label, heap, {
|
|
5302
|
+
concurrency,
|
|
5303
|
+
dirtyCount: targets.length
|
|
5304
|
+
}),
|
|
4763
5305
|
paths: targets,
|
|
4764
5306
|
typeAwareEnv: FULL_PASS.typeAwareEnv
|
|
4765
5307
|
});
|
|
@@ -5096,7 +5638,7 @@ function computeAffectedFiles({ key, cwd }, mode) {
|
|
|
5096
5638
|
if (entryReadable) warnOnce(`no TypeScript files resolved from ${path.basename(configPath)}; skipping type-aware cache invalidation`);
|
|
5097
5639
|
return;
|
|
5098
5640
|
}
|
|
5099
|
-
fs.mkdirSync(stateDirectory(cwd), { recursive: true });
|
|
5641
|
+
fs$1.mkdirSync(stateDirectory(cwd), { recursive: true });
|
|
5100
5642
|
const resolutionGate = computeResolutionGate(cwd);
|
|
5101
5643
|
const affected = /* @__PURE__ */ new Set();
|
|
5102
5644
|
let warmProjects = 0;
|
|
@@ -5143,7 +5685,7 @@ function computeAffectedFiles({ key, cwd }, mode) {
|
|
|
5143
5685
|
function hasBuilderState({ key, cwd }, mode) {
|
|
5144
5686
|
const prefix = `${path.basename(statePath(cwd, BUILD_INFO_STATE, modeSuffix(mode), key))}-`;
|
|
5145
5687
|
try {
|
|
5146
|
-
return fs.readdirSync(stateDirectory(cwd)).some((name) => name.startsWith(prefix) && !name.endsWith(".tmp"));
|
|
5688
|
+
return fs$1.readdirSync(stateDirectory(cwd)).some((name) => name.startsWith(prefix) && !name.endsWith(".tmp"));
|
|
5147
5689
|
} catch {
|
|
5148
5690
|
return false;
|
|
5149
5691
|
}
|
|
@@ -5358,10 +5900,10 @@ function persistBuilderState(builder, buildInfoPath) {
|
|
|
5358
5900
|
if (path.normalize(fileName) !== normalizedBuildInfo) return;
|
|
5359
5901
|
const temporary = `${fileName}.${process$1.pid}.tmp`;
|
|
5360
5902
|
try {
|
|
5361
|
-
fs.writeFileSync(temporary, data);
|
|
5362
|
-
fs.renameSync(temporary, fileName);
|
|
5903
|
+
fs$1.writeFileSync(temporary, data);
|
|
5904
|
+
fs$1.renameSync(temporary, fileName);
|
|
5363
5905
|
} catch (err) {
|
|
5364
|
-
fs.rmSync(temporary, { force: true });
|
|
5906
|
+
fs$1.rmSync(temporary, { force: true });
|
|
5365
5907
|
throw err;
|
|
5366
5908
|
}
|
|
5367
5909
|
});
|
|
@@ -5376,7 +5918,7 @@ function persistBuilderState(builder, buildInfoPath) {
|
|
|
5376
5918
|
*/
|
|
5377
5919
|
function writeGateState(gatePath, gateValue) {
|
|
5378
5920
|
if (gateValue === void 0) {
|
|
5379
|
-
fs.rmSync(gatePath, { force: true });
|
|
5921
|
+
fs$1.rmSync(gatePath, { force: true });
|
|
5380
5922
|
return;
|
|
5381
5923
|
}
|
|
5382
5924
|
writeState(gatePath, gateValue);
|
|
@@ -5397,7 +5939,7 @@ function writeGateState(gatePath, gateValue) {
|
|
|
5397
5939
|
* @returns The affected result.
|
|
5398
5940
|
*/
|
|
5399
5941
|
function runBuilder({ buildInfoPath, gatePath, project, resolutionGate, ts }) {
|
|
5400
|
-
const firstRun = !fs.existsSync(buildInfoPath);
|
|
5942
|
+
const firstRun = !fs$1.existsSync(buildInfoPath);
|
|
5401
5943
|
const options = {
|
|
5402
5944
|
...project.options,
|
|
5403
5945
|
composite: false,
|
|
@@ -5480,7 +6022,7 @@ function applyTypeAwareInvalidation(run, { alreadyDirty, cache, cacheLocation, m
|
|
|
5480
6022
|
}
|
|
5481
6023
|
const threshold = resolveAffectedBustThreshold(run.environment);
|
|
5482
6024
|
if (affectedTargets.length > threshold) {
|
|
5483
|
-
fs.rmSync(cacheLocation, { force: true });
|
|
6025
|
+
fs$1.rmSync(cacheLocation, { force: true });
|
|
5484
6026
|
return {
|
|
5485
6027
|
busted: true,
|
|
5486
6028
|
firstRun: false,
|
|
@@ -5539,12 +6081,12 @@ function sizePasses(descriptors, run, inputs) {
|
|
|
5539
6081
|
* @returns The number of dirty files.
|
|
5540
6082
|
*/
|
|
5541
6083
|
function mutatingDirtyCount(descriptor, cacheLocation, targetFiles, { clearedCaches, run }) {
|
|
5542
|
-
const
|
|
6084
|
+
const program = buildsProgram(descriptor);
|
|
5543
6085
|
const mode = descriptor.invalidation === "only" ? "only" : void 0;
|
|
5544
|
-
if (clearedCaches.has(cacheLocation) && (!
|
|
6086
|
+
if (clearedCaches.has(cacheLocation) && (!program || hasBuilderState(run, mode))) return targetFiles.length;
|
|
5545
6087
|
const cache = openCache(cacheLocation, run.ci);
|
|
5546
6088
|
const dirty = new Set((cache?.getUpdatedFiles(targetFiles) ?? targetFiles).map((file) => normalizePath(file)));
|
|
5547
|
-
if (
|
|
6089
|
+
if (program) {
|
|
5548
6090
|
const outcome = applyTypeAwareInvalidation(run, {
|
|
5549
6091
|
alreadyDirty: dirty,
|
|
5550
6092
|
cache,
|
|
@@ -5601,17 +6143,24 @@ function sizePass(descriptor, context) {
|
|
|
5601
6143
|
const dirtyCount = passDirtyCount(descriptor, cacheFile, context);
|
|
5602
6144
|
const conservative = context.files.outsideCwdTargets.length > 0;
|
|
5603
6145
|
const filesPerWorker = descriptor.filesPerWorker(context.limits, context.run.environment);
|
|
5604
|
-
const
|
|
6146
|
+
const program = buildsProgram(descriptor);
|
|
6147
|
+
const passMaxWorkers = maxWorkersFor(descriptor, context.limits);
|
|
6148
|
+
const maxWorkers = program ? capWorkersForHeap(passMaxWorkers, context.limits.explicitMaxWorkers, context.heap) : passMaxWorkers;
|
|
5605
6149
|
const sizingDirtyCount = conservative ? maxWorkers * filesPerWorker : dirtyCount;
|
|
5606
6150
|
const concurrency = context.options.concurrency ?? computeWorkerCount({
|
|
5607
6151
|
dirtyCount: sizingDirtyCount,
|
|
5608
6152
|
filesPerWorker,
|
|
5609
6153
|
maxWorkers
|
|
5610
6154
|
});
|
|
6155
|
+
const heap = program ? planPassHeap(descriptor.label, context.heap, {
|
|
6156
|
+
concurrency,
|
|
6157
|
+
dirtyCount: sizingDirtyCount
|
|
6158
|
+
}) : void 0;
|
|
5611
6159
|
if (context.run.mutate && context.multiPass && descriptor === TYPED_PASS && !conservative && dirtyCount === 0) return {
|
|
5612
6160
|
cacheFile,
|
|
5613
6161
|
concurrency,
|
|
5614
6162
|
descriptor,
|
|
6163
|
+
heap,
|
|
5615
6164
|
shouldRun: false,
|
|
5616
6165
|
skipReason: TYPED_SKIP_NOTICE
|
|
5617
6166
|
};
|
|
@@ -5619,6 +6168,7 @@ function sizePass(descriptor, context) {
|
|
|
5619
6168
|
cacheFile,
|
|
5620
6169
|
concurrency,
|
|
5621
6170
|
descriptor,
|
|
6171
|
+
heap,
|
|
5622
6172
|
shouldRun: true,
|
|
5623
6173
|
skipReason: void 0
|
|
5624
6174
|
};
|
|
@@ -5639,9 +6189,10 @@ function sizePass(descriptor, context) {
|
|
|
5639
6189
|
*
|
|
5640
6190
|
* @param options - The parsed CLI options.
|
|
5641
6191
|
* @param run - The run context (cwd, variant key, environment, mutate).
|
|
6192
|
+
* @param memory - The machine's memory facts (injected in tests).
|
|
5642
6193
|
* @returns The staged run plan.
|
|
5643
6194
|
*/
|
|
5644
|
-
function plan(options, run) {
|
|
6195
|
+
function plan(options, run, memory = probeMemory()) {
|
|
5645
6196
|
const { ci, cwd, environment, mutate } = run;
|
|
5646
6197
|
const runEslint = !options.oxlint;
|
|
5647
6198
|
const oxlintTypeAware = resolveOxlintTypeAware(options);
|
|
@@ -5666,6 +6217,7 @@ function plan(options, run) {
|
|
|
5666
6217
|
const files = collectRepoFiles(cwd, options.paths);
|
|
5667
6218
|
const newestBustMtime = maxMtimeMs(files.bustFiles);
|
|
5668
6219
|
const limits = resolveWorkerLimits(environment, availableParallelism(), ci);
|
|
6220
|
+
const heap = resolveHeapBudget(environment, memory);
|
|
5669
6221
|
const oxlintDecision = resolveOxlintRun(run, {
|
|
5670
6222
|
files,
|
|
5671
6223
|
runEslint,
|
|
@@ -5685,6 +6237,7 @@ function plan(options, run) {
|
|
|
5685
6237
|
const inputs = {
|
|
5686
6238
|
clearedCaches,
|
|
5687
6239
|
files: sizingFiles,
|
|
6240
|
+
heap,
|
|
5688
6241
|
limits,
|
|
5689
6242
|
multiPass: descriptors.length > 1,
|
|
5690
6243
|
newestBustMtime,
|
|
@@ -5712,6 +6265,7 @@ function plan(options, run) {
|
|
|
5712
6265
|
return planFixChild(passes, run, {
|
|
5713
6266
|
agentsFormatterPath,
|
|
5714
6267
|
files: sizingFiles,
|
|
6268
|
+
heap,
|
|
5715
6269
|
limits,
|
|
5716
6270
|
options
|
|
5717
6271
|
});
|
|
@@ -5767,15 +6321,20 @@ function stageDescriptors(descriptors, run, staging) {
|
|
|
5767
6321
|
* @param argv - The argument slice (without the node/bin prefix).
|
|
5768
6322
|
* @param cwd - The working directory (defaults to `process.cwd()`; injected in tests).
|
|
5769
6323
|
* @param environment - The process environment (defaults to `process.env`).
|
|
6324
|
+
* @param stdin - Where `--files0-from -` reads its list (defaults to
|
|
6325
|
+
* `process.stdin`).
|
|
5770
6326
|
* @returns The process exit code.
|
|
5771
6327
|
* @rejects {CliError} When the arguments are invalid or a tool is missing.
|
|
5772
6328
|
*/
|
|
5773
|
-
async function runLint(argv, cwd = process$1.cwd(), environment = process$1.env) {
|
|
5774
|
-
|
|
6329
|
+
async function runLint(argv, cwd = process$1.cwd(), environment = process$1.env, stdin = process$1.stdin) {
|
|
6330
|
+
resetCancellation();
|
|
6331
|
+
const options = await readFileList(parseArguments(argv, environment), cwd, stdin);
|
|
6332
|
+
if (options.paths.length === 0) return 0;
|
|
5775
6333
|
const staged = plan(options, resolveRunContext(cwd, environment, !options.print));
|
|
5776
6334
|
const { commands, notice } = compose(staged.eager, options);
|
|
5777
6335
|
if (options.print) {
|
|
5778
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);
|
|
5779
6338
|
return 0;
|
|
5780
6339
|
}
|
|
5781
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.");
|
|
@@ -5824,7 +6383,7 @@ async function runChecks(commands, cwd, staged, options) {
|
|
|
5824
6383
|
}
|
|
5825
6384
|
//#endregion
|
|
5826
6385
|
//#region src/lint-cli/index.ts
|
|
5827
|
-
const HELP = `isentinel-lint [flags] [paths...]
|
|
6386
|
+
const HELP = `isentinel-lint [flags] [paths... | --files0-from <file|->]
|
|
5828
6387
|
|
|
5829
6388
|
Runs oxlint and ESLint together, sizing ESLint's --concurrency from how many
|
|
5830
6389
|
files actually need re-linting and managing per-mode caches.
|
|
@@ -5844,6 +6403,9 @@ Flags:
|
|
|
5844
6403
|
--concurrency <n|off> Override the concurrency heuristic.
|
|
5845
6404
|
--eslint-args "<args>" Extra arguments for ESLint.
|
|
5846
6405
|
--oxlint-args "<args>" Extra arguments for oxlint.
|
|
6406
|
+
--files0-from <file|-> Read the paths to lint from a NUL-delimited UTF-8
|
|
6407
|
+
list (- for stdin) instead of positional arguments.
|
|
6408
|
+
An empty list lints nothing and exits 0.
|
|
5847
6409
|
--print Print the composed commands without running them.
|
|
5848
6410
|
-- <args> Forward args to the single selected tool.
|
|
5849
6411
|
-h, --help Show this help.
|