@isentinel/eslint-config 6.0.0 → 6.0.1
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 +29 -2
- package/dist/cli.mjs +1 -1
- package/dist/lint-bootstrap.d.mts +1 -0
- package/dist/lint-bootstrap.mjs +98 -0
- package/dist/lint-cli.mjs +399 -113
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -230,8 +230,8 @@ It resolves the `eslint` and `oxlint` binaries from your project's
|
|
|
230
230
|
|
|
231
231
|
By default (a local run, no `--type-aware`, no `--fix`) it runs **three**
|
|
232
232
|
children concurrently via
|
|
233
|
-
[`concurrently`](https://www.npmjs.com/package/concurrently)
|
|
234
|
-
output:
|
|
233
|
+
[`concurrently`](https://www.npmjs.com/package/concurrently)'s core (no shell),
|
|
234
|
+
grouping their output:
|
|
235
235
|
|
|
236
236
|
- **oxlint** — the full oxlint pass (including its type-aware rules).
|
|
237
237
|
- **fast pass** — ESLint with only the syntactic (non-type-aware) rules, over
|
|
@@ -341,6 +341,32 @@ determined it fails open and runs both engines as before. Explicit `--eslint` /
|
|
|
341
341
|
|
|
342
342
|
Any trailing positional arguments are treated as paths to lint (default `.`).
|
|
343
343
|
|
|
344
|
+
### Large file selections
|
|
345
|
+
|
|
346
|
+
A git hook that hands the runner thousands of files should not put them on a
|
|
347
|
+
command line: `cmd.exe` stops at 8,191 characters and `CreateProcessW` at
|
|
348
|
+
32,767, so tools end up splitting the list into batches that each start another
|
|
349
|
+
lint stack. Pass the list as data instead:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
git diff --cached --name-only -z --relative --diff-filter=ACMR | isentinel-lint --files0-from -
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`--files0-from <file|->` reads NUL-terminated UTF-8 paths from a file, or from
|
|
356
|
+
stdin for `-` (the `git -z` / `find -print0` shape). Relative entries resolve
|
|
357
|
+
against the working directory, exactly like positional paths (hence `--relative`
|
|
358
|
+
above when running from a sub-package). The paths go through the same planning,
|
|
359
|
+
caching and type-aware skip as positional ones, in one run. An empty list lints
|
|
360
|
+
nothing and exits 0 — it never falls back to `.`. A list that is not valid UTF-8
|
|
361
|
+
or holds an empty entry is refused, as is combining the flag with positional
|
|
362
|
+
paths.
|
|
363
|
+
|
|
364
|
+
The runner never puts paths on a command line itself either: every oxlint and
|
|
365
|
+
ESLint child is launched as `node <bootstrap> <engine bin>` with no shell, and
|
|
366
|
+
its full argument list is streamed to it over stdin. As a consequence ESLint's
|
|
367
|
+
own `--stdin` / `--stdin-filename` cannot be forwarded through `--eslint-args`
|
|
368
|
+
or `--`.
|
|
369
|
+
|
|
344
370
|
### Flags
|
|
345
371
|
|
|
346
372
|
| Flag | Description |
|
|
@@ -355,6 +381,7 @@ Any trailing positional arguments are treated as paths to lint (default `.`).
|
|
|
355
381
|
| `--concurrency <n\|off>` | Override the concurrency heuristic with a fixed worker count. |
|
|
356
382
|
| `--eslint-args "<args>"` | Extra arguments forwarded verbatim to ESLint. |
|
|
357
383
|
| `--oxlint-args "<args>"` | Extra arguments forwarded verbatim to oxlint. |
|
|
384
|
+
| `--files0-from <file\|->` | Read the paths to lint from a NUL-delimited UTF-8 list (`-` for stdin); an empty list exits 0. |
|
|
358
385
|
| `--print` | Print the composed commands without running them. |
|
|
359
386
|
| `-- <args>` | Forward args to the single selected tool (needs `--eslint`/`--oxlint`). |
|
|
360
387
|
| `-h`, `--help` | Show help. |
|
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.
|
|
12
|
+
var version = "6.0.1";
|
|
13
13
|
var package_default = {
|
|
14
14
|
name: "@isentinel/eslint-config",
|
|
15
15
|
version,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
//#region src/lint-cli/bootstrap.ts
|
|
5
|
+
/**
|
|
6
|
+
* Engine bootstrap for the lint runner. Spawned as
|
|
7
|
+
* `node <this file> <engine bin>` with the engine's complete argument list as a
|
|
8
|
+
* JSON string array on stdin; it decodes that list, installs it as
|
|
9
|
+
* `process.argv` and then loads the engine's own bin in this same process.
|
|
10
|
+
*
|
|
11
|
+
* It exists to keep file lists off OS command lines: a hook selecting thousands
|
|
12
|
+
* of files would otherwise overflow `CreateProcessW` (32,767 chars) or cmd.exe
|
|
13
|
+
* (8,191) and have to be split into batches, each one starting another lint
|
|
14
|
+
* stack. The command line that launches this process has the same two
|
|
15
|
+
* arguments however many files the run lints.
|
|
16
|
+
*
|
|
17
|
+
* Kept free of imports beyond Node's own so it loads in milliseconds and
|
|
18
|
+
* nothing it pulls in can observe the placeholder `process.argv`.
|
|
19
|
+
*/
|
|
20
|
+
/** The exit code for a launch this bootstrap could not complete. */
|
|
21
|
+
const BOOTSTRAP_FAILURE = 2;
|
|
22
|
+
/**
|
|
23
|
+
* Read stdin to its end. Asynchronous rather than `readFileSync(0)`, which can
|
|
24
|
+
* fail with `EAGAIN` on a non-blocking pipe before the writer has finished.
|
|
25
|
+
*
|
|
26
|
+
* @returns The bytes stdin carried.
|
|
27
|
+
*/
|
|
28
|
+
async function readStdin() {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8"));
|
|
31
|
+
return Buffer.concat(chunks);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether a decoded payload is an array of strings. Restated rather than
|
|
35
|
+
* imported from the package's guards, so this entry stays dependency-free.
|
|
36
|
+
*
|
|
37
|
+
* @param value - The decoded payload.
|
|
38
|
+
* @returns Whether every element is a string.
|
|
39
|
+
*/
|
|
40
|
+
function isStringArray(value) {
|
|
41
|
+
return Array.isArray(value) && value.every((element) => typeof element === "string");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Decode the engine argument list the runner streamed in.
|
|
45
|
+
*
|
|
46
|
+
* @param bytes - The raw stdin payload.
|
|
47
|
+
* @returns The arguments, verbatim.
|
|
48
|
+
*/
|
|
49
|
+
function decodeArguments(bytes) {
|
|
50
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
51
|
+
const parsed = JSON.parse(decoder.decode(bytes));
|
|
52
|
+
if (!isStringArray(parsed)) throw new TypeError("expected a JSON array of argument strings on stdin");
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read and validate everything the launch needs. Only this phase is the
|
|
57
|
+
* bootstrap's own: its failures are reported as such and exit 2.
|
|
58
|
+
*
|
|
59
|
+
* @returns The launch.
|
|
60
|
+
* @rejects {Error} When the usage, stdin or argument payload is invalid.
|
|
61
|
+
*/
|
|
62
|
+
async function prepare() {
|
|
63
|
+
const [bin, ...extra] = process.argv.slice(2);
|
|
64
|
+
if (bin === void 0 || bin.length === 0 || extra.length > 0) throw new Error("usage: node bootstrap <engine bin> (arguments as JSON on stdin)");
|
|
65
|
+
return {
|
|
66
|
+
args: decodeArguments(await readStdin()),
|
|
67
|
+
bin
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Prepare the launch, then become the engine.
|
|
72
|
+
*
|
|
73
|
+
* The engine import stays outside the catch around {@link prepare}: its
|
|
74
|
+
* failures are left to Node's default handling, so they surface (stack and
|
|
75
|
+
* exit code included) exactly as they would from a direct `node <bin>` launch.
|
|
76
|
+
*
|
|
77
|
+
* @rejects {Error} Whatever the engine's own module evaluation throws.
|
|
78
|
+
*/
|
|
79
|
+
async function main() {
|
|
80
|
+
let launch;
|
|
81
|
+
try {
|
|
82
|
+
launch = await prepare();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
85
|
+
process.stderr.write(`isentinel-lint: could not start the engine: ${message}\n`);
|
|
86
|
+
process.exitCode = BOOTSTRAP_FAILURE;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
process.argv = [
|
|
90
|
+
process.execPath,
|
|
91
|
+
launch.bin,
|
|
92
|
+
...launch.args
|
|
93
|
+
];
|
|
94
|
+
await import(pathToFileURL(launch.bin).href);
|
|
95
|
+
}
|
|
96
|
+
main();
|
|
97
|
+
//#endregion
|
|
98
|
+
export {};
|
package/dist/lint-cli.mjs
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
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";
|
|
14
17
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import os, { availableParallelism } from "node:os";
|
|
16
18
|
import fileEntryCache from "file-entry-cache";
|
|
17
19
|
//#region \0rolldown/runtime.js
|
|
18
20
|
var __create = Object.create;
|
|
@@ -38,7 +40,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
38
40
|
}) : target, mod));
|
|
39
41
|
//#endregion
|
|
40
42
|
//#region package.json
|
|
41
|
-
var version = "6.0.
|
|
43
|
+
var version = "6.0.1";
|
|
42
44
|
//#endregion
|
|
43
45
|
//#region src/lint-cli/lib/cli/types.ts
|
|
44
46
|
/**
|
|
@@ -48,6 +50,78 @@ var version = "6.0.0";
|
|
|
48
50
|
var CliError = class extends Error {
|
|
49
51
|
name = "CliError";
|
|
50
52
|
};
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/lint-cli/lib/cli/file-list.ts
|
|
55
|
+
/** The `--files0-from` value that names stdin rather than a file. */
|
|
56
|
+
const STDIN_SOURCE = "-";
|
|
57
|
+
/**
|
|
58
|
+
* Split a NUL-delimited, UTF-8 path list (the `git ls-files -z` / `find
|
|
59
|
+
* -print0` shape) into its paths.
|
|
60
|
+
*
|
|
61
|
+
* Every path is terminated by a NUL, though the last terminator may be left off
|
|
62
|
+
* as GNU's own `--files0-from` allows. Nothing else is interpreted: spaces,
|
|
63
|
+
* newlines, quotes, shell metacharacters and leading hyphens all stay part of
|
|
64
|
+
* the name. The bytes must be valid UTF-8 — a lossy decode would lint a file
|
|
65
|
+
* that is not the one selected — and no entry may be empty, since an empty
|
|
66
|
+
* path is never a file and most likely a broken producer.
|
|
67
|
+
*
|
|
68
|
+
* @param bytes - The raw list.
|
|
69
|
+
* @returns The paths, in list order. Empty for an empty list.
|
|
70
|
+
*/
|
|
71
|
+
function decodeFileList(bytes) {
|
|
72
|
+
const decoder = new TextDecoder("utf-8", {
|
|
73
|
+
fatal: true,
|
|
74
|
+
ignoreBOM: true
|
|
75
|
+
});
|
|
76
|
+
let text;
|
|
77
|
+
try {
|
|
78
|
+
text = decoder.decode(bytes);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new CliError("--files0-from: the file list is not valid UTF-8.");
|
|
81
|
+
}
|
|
82
|
+
if (text.length === 0) return [];
|
|
83
|
+
const entries = text.split("\0");
|
|
84
|
+
if (entries.at(-1) === "") entries.pop();
|
|
85
|
+
const empty = entries.indexOf("");
|
|
86
|
+
if (empty !== -1) throw new CliError(`--files0-from: entry ${empty + 1} of the file list is empty; expected NUL-terminated paths.`);
|
|
87
|
+
return entries;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Fill the run's paths from its `--files0-from` list, reading it once. Options
|
|
91
|
+
* without a list are returned as they are.
|
|
92
|
+
*
|
|
93
|
+
* An empty list stays empty: the caller treats it as nothing to lint, and it
|
|
94
|
+
* must never become the whole-tree default `.`.
|
|
95
|
+
*
|
|
96
|
+
* @param options - The parsed CLI options.
|
|
97
|
+
* @param cwd - The directory a relative list path resolves against.
|
|
98
|
+
* @param stdin - The stream `-` reads.
|
|
99
|
+
* @returns The options with `paths` set from the list.
|
|
100
|
+
* @rejects {CliError} When the list cannot be read or is malformed.
|
|
101
|
+
*/
|
|
102
|
+
async function readFileList(options, cwd, stdin) {
|
|
103
|
+
const source = options.filesFrom;
|
|
104
|
+
if (source === void 0) return options;
|
|
105
|
+
const bytes = source === STDIN_SOURCE ? await readAll(stdin) : await readFile(source, cwd);
|
|
106
|
+
return {
|
|
107
|
+
...options,
|
|
108
|
+
paths: decodeFileList(bytes)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function readFile(source, cwd) {
|
|
112
|
+
try {
|
|
113
|
+
return await fs.readFile(path.resolve(cwd, source));
|
|
114
|
+
} catch (err) {
|
|
115
|
+
throw new CliError(`--files0-from: cannot read "${source}": ${err instanceof Error ? err.message : String(err)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function readAll(stream) {
|
|
119
|
+
const chunks = [];
|
|
120
|
+
for await (const chunk of stream) if (typeof chunk === "string") chunks.push(Buffer$1.from(chunk, "utf8"));
|
|
121
|
+
else if (chunk instanceof Uint8Array) chunks.push(Buffer$1.from(chunk));
|
|
122
|
+
else throw new CliError("--files0-from: stdin produced a non-byte chunk.");
|
|
123
|
+
return Buffer$1.concat(chunks);
|
|
124
|
+
}
|
|
51
125
|
/**
|
|
52
126
|
* The real TS/JS-family file extensions {@link GLOB_SRC_EXT} encodes (the glob
|
|
53
127
|
* additionally matches non-existent combinations such as `cjsx`). Kept next to
|
|
@@ -288,6 +362,13 @@ function splitArgs(input) {
|
|
|
288
362
|
//#endregion
|
|
289
363
|
//#region src/lint-cli/lib/cli/options.ts
|
|
290
364
|
/**
|
|
365
|
+
* The ESLint flags that make it lint source text read from its own stdin. The
|
|
366
|
+
* engine's stdin carries its argument list (see `bootstrap.ts`), so these can
|
|
367
|
+
* never work through the runner and are refused rather than silently linting
|
|
368
|
+
* empty input.
|
|
369
|
+
*/
|
|
370
|
+
const STDIN_SOURCE_FLAGS = ["--stdin", "--stdin-filename"];
|
|
371
|
+
/**
|
|
291
372
|
* Parse and validate an `isentinel-lint` argv slice (without the node/bin
|
|
292
373
|
* prefix). Throws {@link CliError} on any invalid combination.
|
|
293
374
|
*
|
|
@@ -305,7 +386,11 @@ function parseArguments(argv, environment = process$1.env) {
|
|
|
305
386
|
const tail = separator === -1 ? [] : argv.slice(separator);
|
|
306
387
|
const eslintExtract = extractValueOption(head, "eslint-args");
|
|
307
388
|
const oxlintExtract = extractValueOption(eslintExtract.rest, "oxlint-args");
|
|
308
|
-
const
|
|
389
|
+
const filesExtract = extractValueOption(oxlintExtract.rest, "files0-from");
|
|
390
|
+
if (filesExtract.occurrences > 1) throw new CliError("Option --files0-from may only be given once.");
|
|
391
|
+
const filesFrom = filesExtract.value;
|
|
392
|
+
if (filesFrom?.length === 0) throw new CliError("Option --files0-from requires a path, or - for stdin.");
|
|
393
|
+
const parsed = yargs([...filesExtract.rest, ...tail]).scriptName("isentinel-lint").parserConfiguration({
|
|
309
394
|
"boolean-negation": true,
|
|
310
395
|
"populate--": true
|
|
311
396
|
}).option("eslint", {
|
|
@@ -359,18 +444,22 @@ function parseArguments(argv, environment = process$1.env) {
|
|
|
359
444
|
if (eslintOnly) eslintArgs.push(...passthrough);
|
|
360
445
|
else oxlintArgs.push(...passthrough);
|
|
361
446
|
}
|
|
447
|
+
const stdinFlag = eslintArgs.find(isStdinSourceFlag);
|
|
448
|
+
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
449
|
const paths = parsed._.map(String).filter((value) => value.length > 0);
|
|
450
|
+
if (filesFrom !== void 0 && paths.length > 0) throw new CliError("Cannot combine --files0-from with positional paths; put every path in the list.");
|
|
363
451
|
return {
|
|
364
452
|
agents: parsed.agents ?? isInAgentSession(environment),
|
|
365
453
|
cache: parsed.cache,
|
|
366
454
|
concurrency: parseConcurrency(parsed.concurrency),
|
|
367
455
|
eslint: eslintOnly,
|
|
368
456
|
eslintArgs,
|
|
457
|
+
filesFrom,
|
|
369
458
|
fix,
|
|
370
459
|
oxlint: oxlintOnly,
|
|
371
460
|
oxlintArgs,
|
|
372
461
|
oxlintTypeAware: parsed.oxlintTypeAware,
|
|
373
|
-
paths: paths.length > 0 ? paths : ["."],
|
|
462
|
+
paths: filesFrom !== void 0 || paths.length > 0 ? paths : ["."],
|
|
374
463
|
print: parsed.print === true,
|
|
375
464
|
typeAware
|
|
376
465
|
};
|
|
@@ -389,6 +478,7 @@ function extractValueOption(argv, name) {
|
|
|
389
478
|
const inline = `${flag}=`;
|
|
390
479
|
const rest = [];
|
|
391
480
|
let value;
|
|
481
|
+
let occurrences = 0;
|
|
392
482
|
for (let index = 0; index < argv.length; index += 1) {
|
|
393
483
|
const token = argv[index];
|
|
394
484
|
if (token === void 0) continue;
|
|
@@ -396,20 +486,26 @@ function extractValueOption(argv, name) {
|
|
|
396
486
|
const next = argv[index + 1];
|
|
397
487
|
if (next === void 0) throw new CliError(`Option ${flag} requires a value.`);
|
|
398
488
|
value = next;
|
|
489
|
+
occurrences += 1;
|
|
399
490
|
index += 1;
|
|
400
491
|
continue;
|
|
401
492
|
}
|
|
402
493
|
if (token.startsWith(inline)) {
|
|
403
494
|
value = token.slice(inline.length);
|
|
495
|
+
occurrences += 1;
|
|
404
496
|
continue;
|
|
405
497
|
}
|
|
406
498
|
rest.push(token);
|
|
407
499
|
}
|
|
408
500
|
return {
|
|
501
|
+
occurrences,
|
|
409
502
|
rest,
|
|
410
503
|
value
|
|
411
504
|
};
|
|
412
505
|
}
|
|
506
|
+
function isStdinSourceFlag(token) {
|
|
507
|
+
return STDIN_SOURCE_FLAGS.some((flag) => token === flag || token.startsWith(`${flag}=`));
|
|
508
|
+
}
|
|
413
509
|
function parseConcurrency(value) {
|
|
414
510
|
if (value === void 0) return;
|
|
415
511
|
if (value === "off") return "off";
|
|
@@ -514,7 +610,7 @@ function statePath(cwd, name, ...parts) {
|
|
|
514
610
|
*/
|
|
515
611
|
function readFileIfPresent(filePath) {
|
|
516
612
|
try {
|
|
517
|
-
return fs.readFileSync(filePath, "utf8");
|
|
613
|
+
return fs$1.readFileSync(filePath, "utf8");
|
|
518
614
|
} catch {
|
|
519
615
|
return;
|
|
520
616
|
}
|
|
@@ -555,12 +651,12 @@ function writeState(filePath, data) {
|
|
|
555
651
|
})}\n`;
|
|
556
652
|
const temporary = `${filePath}.${process$1.pid}.tmp`;
|
|
557
653
|
try {
|
|
558
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
559
|
-
fs.writeFileSync(temporary, content);
|
|
560
|
-
fs.renameSync(temporary, filePath);
|
|
654
|
+
fs$1.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
655
|
+
fs$1.writeFileSync(temporary, content);
|
|
656
|
+
fs$1.renameSync(temporary, filePath);
|
|
561
657
|
} catch {
|
|
562
658
|
try {
|
|
563
|
-
fs.rmSync(temporary, { force: true });
|
|
659
|
+
fs$1.rmSync(temporary, { force: true });
|
|
564
660
|
} catch {}
|
|
565
661
|
}
|
|
566
662
|
}
|
|
@@ -595,7 +691,7 @@ function swapState(filePath, value) {
|
|
|
595
691
|
function touchState(filePath) {
|
|
596
692
|
try {
|
|
597
693
|
const now = Date.now() / 1e3;
|
|
598
|
-
fs.utimesSync(filePath, now, now);
|
|
694
|
+
fs$1.utimesSync(filePath, now, now);
|
|
599
695
|
} catch {}
|
|
600
696
|
}
|
|
601
697
|
/**
|
|
@@ -688,7 +784,23 @@ function resolveAgentsFormatter() {
|
|
|
688
784
|
*/
|
|
689
785
|
function resolveIgnoredHelper() {
|
|
690
786
|
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));
|
|
787
|
+
return fs$1.existsSync(built) ? built : fileURLToPath(new URL("../../ignored-child.ts", import.meta.url));
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Resolve the engine bootstrap shipped alongside this entry (built from
|
|
791
|
+
* `src/lint-cli/bootstrap.ts` to `lint-bootstrap.mjs`), which every engine
|
|
792
|
+
* child is launched through so its arguments travel over stdin.
|
|
793
|
+
*
|
|
794
|
+
* Falls back to the TypeScript source exactly as {@link resolveIgnoredHelper}
|
|
795
|
+
* does, for the same reason: the fixture tests run from `src`. Unlike that
|
|
796
|
+
* helper there is no degraded mode — without the bootstrap no engine runs — so
|
|
797
|
+
* the shipped path is checked by the integration tests against the build.
|
|
798
|
+
*
|
|
799
|
+
* @returns The absolute path to the bootstrap module.
|
|
800
|
+
*/
|
|
801
|
+
function resolveBootstrap() {
|
|
802
|
+
const built = fileURLToPath(new URL("./lint-bootstrap.mjs", import.meta.url));
|
|
803
|
+
return fs$1.existsSync(built) ? built : fileURLToPath(new URL("../../bootstrap.ts", import.meta.url));
|
|
692
804
|
}
|
|
693
805
|
/**
|
|
694
806
|
* The path a package's manifest declares as the bin of the package's own name,
|
|
@@ -735,65 +847,28 @@ function resolveManifest(name, cwd) {
|
|
|
735
847
|
}
|
|
736
848
|
}
|
|
737
849
|
//#endregion
|
|
738
|
-
//#region src/lint-cli/lib/exec/
|
|
739
|
-
|
|
850
|
+
//#region src/lint-cli/lib/exec/execute.ts
|
|
851
|
+
/** The signals that cancel a run, matching `concurrently`'s `KillOnSignal`. */
|
|
852
|
+
const CANCEL_SIGNALS = [
|
|
853
|
+
"SIGHUP",
|
|
854
|
+
"SIGINT",
|
|
855
|
+
"SIGTERM"
|
|
856
|
+
];
|
|
740
857
|
/**
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
* @param command - The child command to render.
|
|
746
|
-
* @returns The shell-equivalent command line.
|
|
858
|
+
* The first cancelling signal received while this run's children were running.
|
|
859
|
+
* Shared across `execute` calls on purpose: a run is a sequence of them (three
|
|
860
|
+
* for a `--fix` run), and once the user has cancelled one of them none of
|
|
861
|
+
* the rest may start. {@link resetCancellation} Scopes it to one run.
|
|
747
862
|
*/
|
|
748
|
-
|
|
749
|
-
const environmentPrefix = Object.entries(command.env).flatMap(([key, value]) => value === void 0 ? [] : [`${key}=${quotePosix(value)}`]).join(" ");
|
|
750
|
-
const body = [command.bin, ...command.args].map(quotePosix).join(" ");
|
|
751
|
-
return environmentPrefix.length > 0 ? `${environmentPrefix} ${body}` : body;
|
|
752
|
-
}
|
|
863
|
+
let cancelledBy;
|
|
753
864
|
/**
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
* @param nodePath - Absolute path to the Node executable.
|
|
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.
|
|
865
|
+
* Forget any cancellation recorded by an earlier run in this process. Called
|
|
866
|
+
* as a run starts, so a second `runLint` in the same process is not refused
|
|
867
|
+
* for a signal the first one received.
|
|
762
868
|
*/
|
|
763
|
-
function
|
|
764
|
-
|
|
765
|
-
nodePath,
|
|
766
|
-
binJsPath,
|
|
767
|
-
...args
|
|
768
|
-
];
|
|
769
|
-
if (platform === "win32") {
|
|
770
|
-
const offending = tokens.find((token) => token.includes("%"));
|
|
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("'", "'\\''")}'`;
|
|
869
|
+
function resetCancellation() {
|
|
870
|
+
cancelledBy = void 0;
|
|
779
871
|
}
|
|
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)}"`;
|
|
794
|
-
}
|
|
795
|
-
//#endregion
|
|
796
|
-
//#region src/lint-cli/lib/exec/execute.ts
|
|
797
872
|
/** Prefix colour per child label for `concurrently`; kept visually distinct. */
|
|
798
873
|
const PREFIX_COLOR = {
|
|
799
874
|
eslint: "blue",
|
|
@@ -818,7 +893,8 @@ const PREFIX_COLOR = {
|
|
|
818
893
|
* @returns The aggregated exit code.
|
|
819
894
|
*/
|
|
820
895
|
async function execute(commands, cwd, sequential) {
|
|
821
|
-
|
|
896
|
+
const code = await (sequential ? runSequential(commands, cwd) : startGroup(commands, cwd));
|
|
897
|
+
return cancelledCode() ?? code;
|
|
822
898
|
}
|
|
823
899
|
/**
|
|
824
900
|
* Run a staged set of children: spawn `commands` now, then resolve the rest and
|
|
@@ -845,13 +921,19 @@ async function execute(commands, cwd, sequential) {
|
|
|
845
921
|
async function executeStaged(commands, cwd, resolveDeferred) {
|
|
846
922
|
const eager = startGroup(commands, cwd);
|
|
847
923
|
const deferred = resolveDeferred();
|
|
848
|
-
|
|
924
|
+
await new Promise((resolve) => {
|
|
925
|
+
setImmediate(resolve);
|
|
926
|
+
});
|
|
927
|
+
if (cancelledBy !== void 0 || deferred.length === 0) {
|
|
928
|
+
const eagerCode = await eager;
|
|
929
|
+
return cancelledCode() ?? eagerCode;
|
|
930
|
+
}
|
|
849
931
|
const held = createHeldOutput();
|
|
850
932
|
const later = startGroup(deferred, cwd, held.stream);
|
|
851
933
|
const eagerCode = await eager;
|
|
852
934
|
held.release();
|
|
853
935
|
const deferredCode = await later;
|
|
854
|
-
return eagerCode === 0 ? deferredCode : eagerCode;
|
|
936
|
+
return cancelledCode() ?? (eagerCode === 0 ? deferredCode : eagerCode);
|
|
855
937
|
}
|
|
856
938
|
/**
|
|
857
939
|
* Announce a child on stderr once it has been spawned. `concurrently` groups
|
|
@@ -867,30 +949,162 @@ function announce(command) {
|
|
|
867
949
|
process$1.stderr.write(`${prefix} spawned ${command.bin}\n`);
|
|
868
950
|
}
|
|
869
951
|
/**
|
|
952
|
+
* The names the children of one group are registered and logged under: the
|
|
953
|
+
* engine, never its arguments. `concurrently` prints the name on each child's
|
|
954
|
+
* exit line, so it must stay short however many files the child lints. The
|
|
955
|
+
* line already carries the pass as its prefix (`[fast] eslint exited with code
|
|
956
|
+
* 1`), so the pass only joins the name when two children share an engine.
|
|
957
|
+
*
|
|
958
|
+
* @param commands - The group's children.
|
|
959
|
+
* @returns Each child paired with a name unique within the group.
|
|
960
|
+
*/
|
|
961
|
+
function nameCommands(commands) {
|
|
962
|
+
const taken = /* @__PURE__ */ new Set();
|
|
963
|
+
return commands.map((command) => {
|
|
964
|
+
const base = commands.filter((other) => other.bin === command.bin).length > 1 ? `${command.bin} (${command.label})` : command.bin;
|
|
965
|
+
let name = base;
|
|
966
|
+
let suffix = 1;
|
|
967
|
+
while (taken.has(name)) {
|
|
968
|
+
suffix += 1;
|
|
969
|
+
name = `${base} #${String(suffix)}`;
|
|
970
|
+
}
|
|
971
|
+
taken.add(name);
|
|
972
|
+
return [name, command];
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Spawn an engine child through the bootstrap and stream it its arguments.
|
|
977
|
+
*
|
|
978
|
+
* The OS command line is always `node <bootstrap> <bin>`, with no shell, so its
|
|
979
|
+
* length does not depend on the file list and no path is ever interpreted by a
|
|
980
|
+
* shell. The complete argument list follows as JSON on the child's stdin,
|
|
981
|
+
* which is why stdin is always a pipe here whatever the caller asked for.
|
|
982
|
+
*
|
|
983
|
+
* @param launch - The child and its resolved bin.
|
|
984
|
+
* @param options - Spawn options (cwd, env, stdout/stderr handling).
|
|
985
|
+
* @returns The spawned child.
|
|
986
|
+
*/
|
|
987
|
+
function launchEngine({ bin, command }, options) {
|
|
988
|
+
const stdio = Array.isArray(options.stdio) ? options.stdio : [];
|
|
989
|
+
const child = spawn(process$1.execPath, [resolveBootstrap(), bin], {
|
|
990
|
+
...options,
|
|
991
|
+
shell: false,
|
|
992
|
+
stdio: [
|
|
993
|
+
"pipe",
|
|
994
|
+
stdio[1] ?? "inherit",
|
|
995
|
+
stdio[2] ?? "inherit"
|
|
996
|
+
]
|
|
997
|
+
});
|
|
998
|
+
child.stdin?.on("error", () => {});
|
|
999
|
+
child.stdin?.end(JSON.stringify(command.args));
|
|
1000
|
+
return child;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Kill a group's children and stop it spawning more when its output stream
|
|
1004
|
+
* fails — typically `EPIPE` once the reader of a piped report (`| head`) has
|
|
1005
|
+
* gone. Mirrors `concurrently`'s own `OutputErrorHandler`, which its package
|
|
1006
|
+
* exports do not expose.
|
|
1007
|
+
*
|
|
1008
|
+
* @param outputStream - The stream the group writes to.
|
|
1009
|
+
* @param abortController - Aborts the group.
|
|
1010
|
+
* @returns The flow controller.
|
|
1011
|
+
*/
|
|
1012
|
+
function killOnOutputError(outputStream, abortController) {
|
|
1013
|
+
return { handle: (commands) => {
|
|
1014
|
+
function onError() {
|
|
1015
|
+
for (const command of commands) command.kill();
|
|
1016
|
+
abortController.abort();
|
|
1017
|
+
}
|
|
1018
|
+
outputStream.on("error", onError);
|
|
1019
|
+
return {
|
|
1020
|
+
commands,
|
|
1021
|
+
onFinish: () => {
|
|
1022
|
+
outputStream.off("error", onError);
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
} };
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Record a cancelling signal for as long as children are running, and hand it
|
|
1029
|
+
* to `onSignal`. Listening at all replaces Node's default of exiting on the
|
|
1030
|
+
* signal, which is what lets the children be stopped and reaped first rather
|
|
1031
|
+
* than orphaned.
|
|
1032
|
+
*
|
|
1033
|
+
* @param onSignal - Forwards the signal to the running children; omitted when
|
|
1034
|
+
* something else (`concurrently`'s `KillOnSignal`) already does.
|
|
1035
|
+
* @returns Removes the listeners.
|
|
1036
|
+
*/
|
|
1037
|
+
function trackCancellation(onSignal) {
|
|
1038
|
+
function listener(signal) {
|
|
1039
|
+
cancelledBy ??= signal;
|
|
1040
|
+
onSignal?.(signal);
|
|
1041
|
+
}
|
|
1042
|
+
for (const signal of CANCEL_SIGNALS) process$1.on(signal, listener);
|
|
1043
|
+
return () => {
|
|
1044
|
+
for (const signal of CANCEL_SIGNALS) process$1.off(signal, listener);
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
870
1048
|
* Start every command as one `concurrently` group and announce the children.
|
|
871
1049
|
* Unlike the previous `killOthersOn: ["failure"]` behaviour, an ordinary lint
|
|
872
1050
|
* failure in one child no longer kills its siblings — each runs to the end so
|
|
873
1051
|
* the user keeps every result. The resolved code is non-zero when any child
|
|
874
1052
|
* exited non-zero; the promise never rejects.
|
|
875
1053
|
*
|
|
1054
|
+
* Built on `concurrently`'s core rather than its default export, because the
|
|
1055
|
+
* wrapper always launches a command *string* through a shell (`cmd.exe` on
|
|
1056
|
+
* Windows), and that string would have to carry every lint target. Here the
|
|
1057
|
+
* group's spawner launches each engine directly through the bootstrap (see
|
|
1058
|
+
* {@link launchEngine}); the command string is only the label `concurrently`
|
|
1059
|
+
* logs. The controllers are the ones the wrapper wires that do anything under
|
|
1060
|
+
* the options this runner always used — output and error logging, exit lines,
|
|
1061
|
+
* signal forwarding and the broken-output-pipe guard — and nothing else.
|
|
1062
|
+
*
|
|
876
1063
|
* @param commands - The child commands to run.
|
|
877
1064
|
* @param cwd - The working directory.
|
|
878
1065
|
* @param outputStream - Where the group writes its child output; defaults to
|
|
879
1066
|
* stdout. A staged run passes a held stream (see {@link createHeldOutput}).
|
|
880
1067
|
* @returns The aggregated exit code.
|
|
881
1068
|
*/
|
|
882
|
-
async function startGroup(commands, cwd, outputStream) {
|
|
883
|
-
|
|
1069
|
+
async function startGroup(commands, cwd, outputStream = process$1.stdout) {
|
|
1070
|
+
if (cancelledBy !== void 0 || commands.length === 0) return 0;
|
|
1071
|
+
const launches = new Map(nameCommands(commands).map(([name, command]) => {
|
|
1072
|
+
return [name, {
|
|
1073
|
+
bin: resolveLocalBin(command.bin, cwd),
|
|
1074
|
+
command
|
|
1075
|
+
}];
|
|
1076
|
+
}));
|
|
1077
|
+
const logger = new Logger({});
|
|
1078
|
+
const abortController = new AbortController();
|
|
1079
|
+
const stopTracking = trackCancellation();
|
|
1080
|
+
const { result } = createConcurrently(Array.from(launches, ([name, { command }]) => {
|
|
884
1081
|
return {
|
|
885
1082
|
name: command.label,
|
|
886
|
-
command:
|
|
1083
|
+
command: name,
|
|
887
1084
|
env: command.env,
|
|
888
1085
|
prefixColor: PREFIX_COLOR[command.label]
|
|
889
1086
|
};
|
|
890
1087
|
}), {
|
|
1088
|
+
abortSignal: abortController.signal,
|
|
1089
|
+
controllers: [
|
|
1090
|
+
new LogError({ logger }),
|
|
1091
|
+
new LogOutput({ logger }),
|
|
1092
|
+
new LogExit({ logger }),
|
|
1093
|
+
new KillOnSignal({
|
|
1094
|
+
abortController,
|
|
1095
|
+
process: process$1
|
|
1096
|
+
}),
|
|
1097
|
+
killOnOutputError(outputStream, abortController)
|
|
1098
|
+
],
|
|
891
1099
|
cwd,
|
|
892
1100
|
group: true,
|
|
893
|
-
|
|
1101
|
+
logger,
|
|
1102
|
+
outputStream,
|
|
1103
|
+
spawn: (name, options) => {
|
|
1104
|
+
const launch = launches.get(name);
|
|
1105
|
+
if (launch === void 0) throw new Error(`isentinel-lint: no engine registered as "${name}".`);
|
|
1106
|
+
return launchEngine(launch, options);
|
|
1107
|
+
}
|
|
894
1108
|
});
|
|
895
1109
|
for (const command of commands) announce(command);
|
|
896
1110
|
try {
|
|
@@ -898,12 +1112,29 @@ async function startGroup(commands, cwd, outputStream) {
|
|
|
898
1112
|
return 0;
|
|
899
1113
|
} catch {
|
|
900
1114
|
return 1;
|
|
1115
|
+
} finally {
|
|
1116
|
+
stopTracking();
|
|
901
1117
|
}
|
|
902
1118
|
}
|
|
1119
|
+
function signalExitCode(signal) {
|
|
1120
|
+
return 128 + os.constants.signals[signal];
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* The exit code for a cancelled run (`128 + signal number`, as a shell reports
|
|
1124
|
+
* it), or `undefined` when nothing cancelled it.
|
|
1125
|
+
*
|
|
1126
|
+
* @returns The exit code, when cancelled.
|
|
1127
|
+
*/
|
|
1128
|
+
function cancelledCode() {
|
|
1129
|
+
return cancelledBy === void 0 ? void 0 : signalExitCode(cancelledBy);
|
|
1130
|
+
}
|
|
903
1131
|
async function spawnChild(command, cwd) {
|
|
904
|
-
const
|
|
1132
|
+
const bin = resolveLocalBin(command.bin, cwd);
|
|
905
1133
|
return new Promise((resolve) => {
|
|
906
|
-
const child =
|
|
1134
|
+
const child = launchEngine({
|
|
1135
|
+
bin,
|
|
1136
|
+
command
|
|
1137
|
+
}, {
|
|
907
1138
|
cwd,
|
|
908
1139
|
env: {
|
|
909
1140
|
...process$1.env,
|
|
@@ -912,17 +1143,23 @@ async function spawnChild(command, cwd) {
|
|
|
912
1143
|
stdio: "inherit"
|
|
913
1144
|
});
|
|
914
1145
|
announce(command);
|
|
1146
|
+
const stopTracking = trackCancellation((signal) => {
|
|
1147
|
+
child.kill(signal);
|
|
1148
|
+
});
|
|
915
1149
|
child.on("error", () => {
|
|
1150
|
+
stopTracking();
|
|
916
1151
|
resolve(1);
|
|
917
1152
|
});
|
|
918
|
-
child.on("close", (code) => {
|
|
919
|
-
|
|
1153
|
+
child.on("close", (code, signal) => {
|
|
1154
|
+
stopTracking();
|
|
1155
|
+
resolve(code ?? (signal === null ? 1 : signalExitCode(signal)));
|
|
920
1156
|
});
|
|
921
1157
|
});
|
|
922
1158
|
}
|
|
923
1159
|
async function runSequential(commands, cwd) {
|
|
924
1160
|
let exitCode = 0;
|
|
925
1161
|
for (const command of commands) {
|
|
1162
|
+
if (cancelledBy !== void 0) break;
|
|
926
1163
|
const code = await spawnChild(command, cwd);
|
|
927
1164
|
if (code !== 0) exitCode = code;
|
|
928
1165
|
}
|
|
@@ -961,6 +1198,26 @@ function createHeldOutput() {
|
|
|
961
1198
|
};
|
|
962
1199
|
}
|
|
963
1200
|
//#endregion
|
|
1201
|
+
//#region src/lint-cli/lib/exec/shell.ts
|
|
1202
|
+
const SAFE_TOKEN = /^[\w@+=:,./-]+$/;
|
|
1203
|
+
/**
|
|
1204
|
+
* Render a command as a shell-equivalent line (env prefix + logical binary +
|
|
1205
|
+
* arguments) for `--print`. Uses POSIX quoting for stable, cross-platform
|
|
1206
|
+
* output.
|
|
1207
|
+
*
|
|
1208
|
+
* @param command - The child command to render.
|
|
1209
|
+
* @returns The shell-equivalent command line.
|
|
1210
|
+
*/
|
|
1211
|
+
function formatCommandLine(command) {
|
|
1212
|
+
const environmentPrefix = Object.entries(command.env).flatMap(([key, value]) => value === void 0 ? [] : [`${key}=${quotePosix(value)}`]).join(" ");
|
|
1213
|
+
const body = [command.bin, ...command.args].map(quotePosix).join(" ");
|
|
1214
|
+
return environmentPrefix.length > 0 ? `${environmentPrefix} ${body}` : body;
|
|
1215
|
+
}
|
|
1216
|
+
function quotePosix(token) {
|
|
1217
|
+
if (token.length > 0 && SAFE_TOKEN.test(token)) return token;
|
|
1218
|
+
return `'${token.replaceAll("'", "'\\''")}'`;
|
|
1219
|
+
}
|
|
1220
|
+
//#endregion
|
|
964
1221
|
//#region src/lint-cli/lib/plan/command.ts
|
|
965
1222
|
/**
|
|
966
1223
|
* Compose the oxlint child command.
|
|
@@ -974,7 +1231,7 @@ function composeOxlintCommand(options, context) {
|
|
|
974
1231
|
if (options.agents) args.push("--format", "agent");
|
|
975
1232
|
if (context.oxlintTypeAware) args.push("--type-aware");
|
|
976
1233
|
if (options.fix) args.push("--fix");
|
|
977
|
-
args.push("--no-error-on-unmatched-pattern", ...options.oxlintArgs, ...context.paths);
|
|
1234
|
+
args.push("--no-error-on-unmatched-pattern", ...options.oxlintArgs, ...pathArguments(context.paths));
|
|
978
1235
|
return {
|
|
979
1236
|
args,
|
|
980
1237
|
bin: "oxlint",
|
|
@@ -996,7 +1253,7 @@ function composeEslintCommand(options, context) {
|
|
|
996
1253
|
if (options.cache && context.ci) args.push("--cache-strategy", "content");
|
|
997
1254
|
if (options.agents) args.push("--format", context.agentsFormatterPath);
|
|
998
1255
|
if (context.fix) args.push("--fix");
|
|
999
|
-
args.push(...options.eslintArgs, ...context.paths);
|
|
1256
|
+
args.push(...options.eslintArgs, ...pathArguments(context.paths));
|
|
1000
1257
|
return {
|
|
1001
1258
|
args,
|
|
1002
1259
|
bin: "eslint",
|
|
@@ -1004,6 +1261,19 @@ function composeEslintCommand(options, context) {
|
|
|
1004
1261
|
label: context.eslintLabel
|
|
1005
1262
|
};
|
|
1006
1263
|
}
|
|
1264
|
+
/**
|
|
1265
|
+
* The lint targets as the tail of an engine's argument list. A relative target
|
|
1266
|
+
* that starts with `-` would be read as a flag, so it is passed as `./<path>`.
|
|
1267
|
+
* A `--` separator is not enough: ESLint's bin scans the raw argv for
|
|
1268
|
+
* `--init`, `--mcp`, `--stdin` and `--debug` regardless of it, so a listed file
|
|
1269
|
+
* named `--init` would start the config wizard.
|
|
1270
|
+
*
|
|
1271
|
+
* @param paths - The lint targets.
|
|
1272
|
+
* @returns The arguments that pass them as paths.
|
|
1273
|
+
*/
|
|
1274
|
+
function pathArguments(paths) {
|
|
1275
|
+
return paths.map((target) => target.startsWith("-") ? `./${target}` : target);
|
|
1276
|
+
}
|
|
1007
1277
|
//#endregion
|
|
1008
1278
|
//#region src/lint-cli/lib/plan/compose.ts
|
|
1009
1279
|
/** The stderr notice emitted when a lint target resolves outside the cwd. */
|
|
@@ -1170,7 +1440,7 @@ function applyHashBust(run, bust, hash) {
|
|
|
1170
1440
|
const cleared = [];
|
|
1171
1441
|
for (const base of bust.caches) {
|
|
1172
1442
|
const cacheFilePath = path.resolve(run.cwd, cacheFileFor(base, run.key));
|
|
1173
|
-
fs.rmSync(cacheFilePath, { force: true });
|
|
1443
|
+
fs$1.rmSync(cacheFilePath, { force: true });
|
|
1174
1444
|
cleared.push(cacheFilePath);
|
|
1175
1445
|
}
|
|
1176
1446
|
return {
|
|
@@ -1447,7 +1717,7 @@ function normalizePath(filePath, platform = process$1.platform) {
|
|
|
1447
1717
|
* @returns The loaded cache, or `undefined` when the file does not exist.
|
|
1448
1718
|
*/
|
|
1449
1719
|
function openCache(cacheFilePath, useChecksum) {
|
|
1450
|
-
if (!fs.existsSync(cacheFilePath)) return;
|
|
1720
|
+
if (!fs$1.existsSync(cacheFilePath)) return;
|
|
1451
1721
|
const cache = load(cacheFilePath, useChecksum);
|
|
1452
1722
|
return {
|
|
1453
1723
|
filesWithMessages: (files) => filesWithMessagesIn(cache, files),
|
|
@@ -1457,7 +1727,7 @@ function openCache(cacheFilePath, useChecksum) {
|
|
|
1457
1727
|
}
|
|
1458
1728
|
function safeMtimeMs(filePath) {
|
|
1459
1729
|
try {
|
|
1460
|
-
return fs.statSync(filePath).mtimeMs;
|
|
1730
|
+
return fs$1.statSync(filePath).mtimeMs;
|
|
1461
1731
|
} catch {
|
|
1462
1732
|
return;
|
|
1463
1733
|
}
|
|
@@ -1474,7 +1744,7 @@ function safeMtimeMs(filePath) {
|
|
|
1474
1744
|
function listCacheFiles(cwd) {
|
|
1475
1745
|
let entries;
|
|
1476
1746
|
try {
|
|
1477
|
-
entries = fs.readdirSync(cwd);
|
|
1747
|
+
entries = fs$1.readdirSync(cwd);
|
|
1478
1748
|
} catch {
|
|
1479
1749
|
return [];
|
|
1480
1750
|
}
|
|
@@ -1482,7 +1752,7 @@ function listCacheFiles(cwd) {
|
|
|
1482
1752
|
}
|
|
1483
1753
|
function removeCacheFile(cacheFilePath) {
|
|
1484
1754
|
try {
|
|
1485
|
-
fs.rmSync(cacheFilePath, { force: true });
|
|
1755
|
+
fs$1.rmSync(cacheFilePath, { force: true });
|
|
1486
1756
|
} catch {}
|
|
1487
1757
|
}
|
|
1488
1758
|
/**
|
|
@@ -3491,7 +3761,7 @@ const WALK_BUDGET = 8192;
|
|
|
3491
3761
|
function findWorkspaceRoot(cwd) {
|
|
3492
3762
|
let current = cwd;
|
|
3493
3763
|
for (;;) {
|
|
3494
|
-
for (const marker of WORKSPACE_ROOT_MARKERS) if (fs.existsSync(path.join(current, marker))) return current;
|
|
3764
|
+
for (const marker of WORKSPACE_ROOT_MARKERS) if (fs$1.existsSync(path.join(current, marker))) return current;
|
|
3495
3765
|
const parent = path.dirname(current);
|
|
3496
3766
|
if (parent === current) return cwd;
|
|
3497
3767
|
current = parent;
|
|
@@ -3535,7 +3805,7 @@ function findWorkspaceMembers(root) {
|
|
|
3535
3805
|
function walk(directory, depth) {
|
|
3536
3806
|
let entries;
|
|
3537
3807
|
try {
|
|
3538
|
-
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
3808
|
+
entries = fs$1.readdirSync(directory, { withFileTypes: true });
|
|
3539
3809
|
} catch {
|
|
3540
3810
|
return true;
|
|
3541
3811
|
}
|
|
@@ -3545,7 +3815,7 @@ function findWorkspaceMembers(root) {
|
|
|
3545
3815
|
if (budget < 0) return false;
|
|
3546
3816
|
const absolute = path.join(directory, entry.name);
|
|
3547
3817
|
const relative = toPosix(path.relative(root, absolute));
|
|
3548
|
-
if (isMember(relative) && fs.existsSync(path.join(absolute, "package.json"))) members.push(absolute);
|
|
3818
|
+
if (isMember(relative) && fs$1.existsSync(path.join(absolute, "package.json"))) members.push(absolute);
|
|
3549
3819
|
if (depth < maxDepth && !walk(absolute, depth + 1)) return false;
|
|
3550
3820
|
}
|
|
3551
3821
|
return true;
|
|
@@ -3873,7 +4143,7 @@ function oxlintTargets(cwd, targets) {
|
|
|
3873
4143
|
const extension = path.extname(target).toLowerCase();
|
|
3874
4144
|
if (extension === "" || TYPE_AWARE_EXTENSION_SET.has(extension)) return true;
|
|
3875
4145
|
try {
|
|
3876
|
-
return fs.statSync(path.resolve(cwd, target)).isDirectory();
|
|
4146
|
+
return fs$1.statSync(path.resolve(cwd, target)).isDirectory();
|
|
3877
4147
|
} catch {
|
|
3878
4148
|
return false;
|
|
3879
4149
|
}
|
|
@@ -3910,7 +4180,7 @@ function collectAncestorBustFiles(cwd) {
|
|
|
3910
4180
|
for (;;) {
|
|
3911
4181
|
let entries = [];
|
|
3912
4182
|
try {
|
|
3913
|
-
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
4183
|
+
entries = fs$1.readdirSync(current, { withFileTypes: true });
|
|
3914
4184
|
} catch {
|
|
3915
4185
|
entries = [];
|
|
3916
4186
|
}
|
|
@@ -3933,7 +4203,7 @@ function collectAncestorBustFiles(cwd) {
|
|
|
3933
4203
|
* @returns The normalized cwd-relative posix target.
|
|
3934
4204
|
*/
|
|
3935
4205
|
function normalizeTarget(target, cwd) {
|
|
3936
|
-
let value = path.isAbsolute(target) ? toPosix(path.relative(cwd, target)) : toPosix(target);
|
|
4206
|
+
let value = path.isAbsolute(target) ? toPosix(path.relative(cwd, target)) : path.posix.normalize(toPosix(target));
|
|
3937
4207
|
if (value.startsWith("./")) value = value.slice(2);
|
|
3938
4208
|
while (value.endsWith("/")) value = value.slice(0, -1);
|
|
3939
4209
|
return value;
|
|
@@ -3960,8 +4230,15 @@ function isOutsideCwd(normalizedTarget) {
|
|
|
3960
4230
|
*/
|
|
3961
4231
|
function matchTargets(normalized) {
|
|
3962
4232
|
if (normalized.some((target) => target === "" || target === ".")) return () => true;
|
|
4233
|
+
const targets = new Set(normalized);
|
|
3963
4234
|
return (relative) => {
|
|
3964
|
-
|
|
4235
|
+
let candidate = relative;
|
|
4236
|
+
for (;;) {
|
|
4237
|
+
if (targets.has(candidate)) return true;
|
|
4238
|
+
const slash = candidate.lastIndexOf("/");
|
|
4239
|
+
if (slash === -1) return false;
|
|
4240
|
+
candidate = candidate.slice(0, slash);
|
|
4241
|
+
}
|
|
3965
4242
|
};
|
|
3966
4243
|
}
|
|
3967
4244
|
function isTypeAwareFile(filePath) {
|
|
@@ -3974,6 +4251,7 @@ function gitListFiles(cwd, pathSpecs) {
|
|
|
3974
4251
|
try {
|
|
3975
4252
|
return execFileSync("git", [
|
|
3976
4253
|
"ls-files",
|
|
4254
|
+
"-z",
|
|
3977
4255
|
"--cached",
|
|
3978
4256
|
"--others",
|
|
3979
4257
|
"--exclude-standard",
|
|
@@ -3983,7 +4261,7 @@ function gitListFiles(cwd, pathSpecs) {
|
|
|
3983
4261
|
cwd,
|
|
3984
4262
|
encoding: "utf8",
|
|
3985
4263
|
maxBuffer: 64 * 1024 * 1024
|
|
3986
|
-
}).split("\
|
|
4264
|
+
}).split("\0").filter((line) => line.length > 0);
|
|
3987
4265
|
} catch {
|
|
3988
4266
|
return;
|
|
3989
4267
|
}
|
|
@@ -3991,7 +4269,7 @@ function gitListFiles(cwd, pathSpecs) {
|
|
|
3991
4269
|
function walkDirectory(root, current, accumulator) {
|
|
3992
4270
|
let entries;
|
|
3993
4271
|
try {
|
|
3994
|
-
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
4272
|
+
entries = fs$1.readdirSync(current, { withFileTypes: true });
|
|
3995
4273
|
} catch {
|
|
3996
4274
|
return;
|
|
3997
4275
|
}
|
|
@@ -4010,7 +4288,7 @@ function walkFallback(cwd, targets) {
|
|
|
4010
4288
|
const absolute = path.resolve(cwd, target);
|
|
4011
4289
|
let stat;
|
|
4012
4290
|
try {
|
|
4013
|
-
stat = fs.statSync(absolute);
|
|
4291
|
+
stat = fs$1.statSync(absolute);
|
|
4014
4292
|
} catch {
|
|
4015
4293
|
continue;
|
|
4016
4294
|
}
|
|
@@ -4049,11 +4327,12 @@ function gitIgnoredTargets(cwd, targets) {
|
|
|
4049
4327
|
const output = execFileSync("git", [
|
|
4050
4328
|
"check-ignore",
|
|
4051
4329
|
"--no-index",
|
|
4052
|
-
"--stdin"
|
|
4330
|
+
"--stdin",
|
|
4331
|
+
"-z"
|
|
4053
4332
|
], {
|
|
4054
4333
|
cwd,
|
|
4055
4334
|
encoding: "utf8",
|
|
4056
|
-
input: targets.join("\
|
|
4335
|
+
input: `${targets.join("\0")}\0`,
|
|
4057
4336
|
maxBuffer: 64 * 1024 * 1024,
|
|
4058
4337
|
stdio: [
|
|
4059
4338
|
"pipe",
|
|
@@ -4061,7 +4340,7 @@ function gitIgnoredTargets(cwd, targets) {
|
|
|
4061
4340
|
"ignore"
|
|
4062
4341
|
]
|
|
4063
4342
|
});
|
|
4064
|
-
return new Set(output.split("\
|
|
4343
|
+
return new Set(output.split("\0").filter(Boolean));
|
|
4065
4344
|
} catch {
|
|
4066
4345
|
return /* @__PURE__ */ new Set();
|
|
4067
4346
|
}
|
|
@@ -4334,7 +4613,7 @@ function queryIgnoredFiles(cwd, targets) {
|
|
|
4334
4613
|
"ignore"
|
|
4335
4614
|
]
|
|
4336
4615
|
});
|
|
4337
|
-
const parsed = JSON.parse(fs.readFileSync(outFile, "utf8"));
|
|
4616
|
+
const parsed = JSON.parse(fs$1.readFileSync(outFile, "utf8"));
|
|
4338
4617
|
if (!isRecord(parsed)) return;
|
|
4339
4618
|
const { basePath, entries, ignored, mode } = parsed;
|
|
4340
4619
|
if (mode === "answers" && isStringArray(ignored)) return {
|
|
@@ -4350,7 +4629,7 @@ function queryIgnoredFiles(cwd, targets) {
|
|
|
4350
4629
|
} catch {
|
|
4351
4630
|
return;
|
|
4352
4631
|
} finally {
|
|
4353
|
-
fs.rmSync(outFile, { force: true });
|
|
4632
|
+
fs$1.rmSync(outFile, { force: true });
|
|
4354
4633
|
}
|
|
4355
4634
|
}
|
|
4356
4635
|
//#endregion
|
|
@@ -4393,7 +4672,7 @@ function readHybridStatus(cwd) {
|
|
|
4393
4672
|
function writeHybridStatus(cwd, oxlint) {
|
|
4394
4673
|
let installed;
|
|
4395
4674
|
try {
|
|
4396
|
-
installed = fs.existsSync(path.resolve(cwd, "node_modules"));
|
|
4675
|
+
installed = fs$1.existsSync(path.resolve(cwd, "node_modules"));
|
|
4397
4676
|
} catch {
|
|
4398
4677
|
return;
|
|
4399
4678
|
}
|
|
@@ -5096,7 +5375,7 @@ function computeAffectedFiles({ key, cwd }, mode) {
|
|
|
5096
5375
|
if (entryReadable) warnOnce(`no TypeScript files resolved from ${path.basename(configPath)}; skipping type-aware cache invalidation`);
|
|
5097
5376
|
return;
|
|
5098
5377
|
}
|
|
5099
|
-
fs.mkdirSync(stateDirectory(cwd), { recursive: true });
|
|
5378
|
+
fs$1.mkdirSync(stateDirectory(cwd), { recursive: true });
|
|
5100
5379
|
const resolutionGate = computeResolutionGate(cwd);
|
|
5101
5380
|
const affected = /* @__PURE__ */ new Set();
|
|
5102
5381
|
let warmProjects = 0;
|
|
@@ -5143,7 +5422,7 @@ function computeAffectedFiles({ key, cwd }, mode) {
|
|
|
5143
5422
|
function hasBuilderState({ key, cwd }, mode) {
|
|
5144
5423
|
const prefix = `${path.basename(statePath(cwd, BUILD_INFO_STATE, modeSuffix(mode), key))}-`;
|
|
5145
5424
|
try {
|
|
5146
|
-
return fs.readdirSync(stateDirectory(cwd)).some((name) => name.startsWith(prefix) && !name.endsWith(".tmp"));
|
|
5425
|
+
return fs$1.readdirSync(stateDirectory(cwd)).some((name) => name.startsWith(prefix) && !name.endsWith(".tmp"));
|
|
5147
5426
|
} catch {
|
|
5148
5427
|
return false;
|
|
5149
5428
|
}
|
|
@@ -5358,10 +5637,10 @@ function persistBuilderState(builder, buildInfoPath) {
|
|
|
5358
5637
|
if (path.normalize(fileName) !== normalizedBuildInfo) return;
|
|
5359
5638
|
const temporary = `${fileName}.${process$1.pid}.tmp`;
|
|
5360
5639
|
try {
|
|
5361
|
-
fs.writeFileSync(temporary, data);
|
|
5362
|
-
fs.renameSync(temporary, fileName);
|
|
5640
|
+
fs$1.writeFileSync(temporary, data);
|
|
5641
|
+
fs$1.renameSync(temporary, fileName);
|
|
5363
5642
|
} catch (err) {
|
|
5364
|
-
fs.rmSync(temporary, { force: true });
|
|
5643
|
+
fs$1.rmSync(temporary, { force: true });
|
|
5365
5644
|
throw err;
|
|
5366
5645
|
}
|
|
5367
5646
|
});
|
|
@@ -5376,7 +5655,7 @@ function persistBuilderState(builder, buildInfoPath) {
|
|
|
5376
5655
|
*/
|
|
5377
5656
|
function writeGateState(gatePath, gateValue) {
|
|
5378
5657
|
if (gateValue === void 0) {
|
|
5379
|
-
fs.rmSync(gatePath, { force: true });
|
|
5658
|
+
fs$1.rmSync(gatePath, { force: true });
|
|
5380
5659
|
return;
|
|
5381
5660
|
}
|
|
5382
5661
|
writeState(gatePath, gateValue);
|
|
@@ -5397,7 +5676,7 @@ function writeGateState(gatePath, gateValue) {
|
|
|
5397
5676
|
* @returns The affected result.
|
|
5398
5677
|
*/
|
|
5399
5678
|
function runBuilder({ buildInfoPath, gatePath, project, resolutionGate, ts }) {
|
|
5400
|
-
const firstRun = !fs.existsSync(buildInfoPath);
|
|
5679
|
+
const firstRun = !fs$1.existsSync(buildInfoPath);
|
|
5401
5680
|
const options = {
|
|
5402
5681
|
...project.options,
|
|
5403
5682
|
composite: false,
|
|
@@ -5480,7 +5759,7 @@ function applyTypeAwareInvalidation(run, { alreadyDirty, cache, cacheLocation, m
|
|
|
5480
5759
|
}
|
|
5481
5760
|
const threshold = resolveAffectedBustThreshold(run.environment);
|
|
5482
5761
|
if (affectedTargets.length > threshold) {
|
|
5483
|
-
fs.rmSync(cacheLocation, { force: true });
|
|
5762
|
+
fs$1.rmSync(cacheLocation, { force: true });
|
|
5484
5763
|
return {
|
|
5485
5764
|
busted: true,
|
|
5486
5765
|
firstRun: false,
|
|
@@ -5767,11 +6046,15 @@ function stageDescriptors(descriptors, run, staging) {
|
|
|
5767
6046
|
* @param argv - The argument slice (without the node/bin prefix).
|
|
5768
6047
|
* @param cwd - The working directory (defaults to `process.cwd()`; injected in tests).
|
|
5769
6048
|
* @param environment - The process environment (defaults to `process.env`).
|
|
6049
|
+
* @param stdin - Where `--files0-from -` reads its list (defaults to
|
|
6050
|
+
* `process.stdin`).
|
|
5770
6051
|
* @returns The process exit code.
|
|
5771
6052
|
* @rejects {CliError} When the arguments are invalid or a tool is missing.
|
|
5772
6053
|
*/
|
|
5773
|
-
async function runLint(argv, cwd = process$1.cwd(), environment = process$1.env) {
|
|
5774
|
-
|
|
6054
|
+
async function runLint(argv, cwd = process$1.cwd(), environment = process$1.env, stdin = process$1.stdin) {
|
|
6055
|
+
resetCancellation();
|
|
6056
|
+
const options = await readFileList(parseArguments(argv, environment), cwd, stdin);
|
|
6057
|
+
if (options.paths.length === 0) return 0;
|
|
5775
6058
|
const staged = plan(options, resolveRunContext(cwd, environment, !options.print));
|
|
5776
6059
|
const { commands, notice } = compose(staged.eager, options);
|
|
5777
6060
|
if (options.print) {
|
|
@@ -5824,7 +6107,7 @@ async function runChecks(commands, cwd, staged, options) {
|
|
|
5824
6107
|
}
|
|
5825
6108
|
//#endregion
|
|
5826
6109
|
//#region src/lint-cli/index.ts
|
|
5827
|
-
const HELP = `isentinel-lint [flags] [paths...]
|
|
6110
|
+
const HELP = `isentinel-lint [flags] [paths... | --files0-from <file|->]
|
|
5828
6111
|
|
|
5829
6112
|
Runs oxlint and ESLint together, sizing ESLint's --concurrency from how many
|
|
5830
6113
|
files actually need re-linting and managing per-mode caches.
|
|
@@ -5844,6 +6127,9 @@ Flags:
|
|
|
5844
6127
|
--concurrency <n|off> Override the concurrency heuristic.
|
|
5845
6128
|
--eslint-args "<args>" Extra arguments for ESLint.
|
|
5846
6129
|
--oxlint-args "<args>" Extra arguments for oxlint.
|
|
6130
|
+
--files0-from <file|-> Read the paths to lint from a NUL-delimited UTF-8
|
|
6131
|
+
list (- for stdin) instead of positional arguments.
|
|
6132
|
+
An empty list lints nothing and exits 0.
|
|
5847
6133
|
--print Print the composed commands without running them.
|
|
5848
6134
|
-- <args> Forward args to the single selected tool.
|
|
5849
6135
|
-h, --help Show this help.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isentinel/eslint-config",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "iSentinel's ESLint config",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint-config",
|
|
@@ -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.
|
|
148
|
+
"@isentinel/eslint-config": "6.0.1"
|
|
149
149
|
},
|
|
150
150
|
"peerDependencies": {
|
|
151
151
|
"@vitest/eslint-plugin": "^1.6.4",
|