@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 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), grouping their
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
@@ -253,6 +253,22 @@ Other behaviours:
253
253
  re-lint (its own dirty count), rather than eagerly spinning up a worker per
254
254
  CPU. The fast pass is cheap per file, so it packs far more files per worker
255
255
  (see `FAST_FILES_PER_WORKER`) than the type-aware pass.
256
+ - Sizes the heap of every ESLint child that builds a TypeScript program (the
257
+ type-aware pass, the full pass and the fix child). Each worker holds its own
258
+ program, so the limit that matters is workers × heap. By default the runner
259
+ splits half the machine's memory (or its cgroup limit, when lower) across
260
+ those workers, and raises Node's heap limit when the share is larger. When the
261
+ budget cannot give each worker twice Node's default heap, it runs fewer
262
+ workers: every worker loads the program of nearly every project, so fewer,
263
+ larger workers need far less memory in total than more, smaller ones. The flag
264
+ goes in the child's `NODE_OPTIONS`, which every ESLint worker inherits; oxlint
265
+ and the fast pass never get it. `--print` shows the chosen workers, files per
266
+ worker and heap on stderr. If a child still runs out of memory, the runner
267
+ prints a hint that names `LINT_TYPED_HEAP_MB`. A size set in
268
+ `LINT_TYPED_HEAP_MB` also caps the workers at budget ÷ size. An explicit
269
+ `LINT_MAX_WORKERS` or `--concurrency` is never reduced. A
270
+ `--max-old-space-size` already in `NODE_OPTIONS` wins over all of this: the
271
+ runner adds nothing and keeps the worker count.
256
272
  - Keeps a separate ESLint cache per pass so they never invalidate one another:
257
273
  `.eslintcache-fast-<key>` (fast pass), `.eslintcache-typeaware-<key>`
258
274
  (type-aware pass), and `.eslintcache-<key>` (the full config, used by CI,
@@ -341,6 +357,32 @@ determined it fails open and runs both engines as before. Explicit `--eslint` /
341
357
 
342
358
  Any trailing positional arguments are treated as paths to lint (default `.`).
343
359
 
360
+ ### Large file selections
361
+
362
+ A git hook that hands the runner thousands of files should not put them on a
363
+ command line: `cmd.exe` stops at 8,191 characters and `CreateProcessW` at
364
+ 32,767, so tools end up splitting the list into batches that each start another
365
+ lint stack. Pass the list as data instead:
366
+
367
+ ```bash
368
+ git diff --cached --name-only -z --relative --diff-filter=ACMR | isentinel-lint --files0-from -
369
+ ```
370
+
371
+ `--files0-from <file|->` reads NUL-terminated UTF-8 paths from a file, or from
372
+ stdin for `-` (the `git -z` / `find -print0` shape). Relative entries resolve
373
+ against the working directory, exactly like positional paths (hence `--relative`
374
+ above when running from a sub-package). The paths go through the same planning,
375
+ caching and type-aware skip as positional ones, in one run. An empty list lints
376
+ nothing and exits 0 — it never falls back to `.`. A list that is not valid UTF-8
377
+ or holds an empty entry is refused, as is combining the flag with positional
378
+ paths.
379
+
380
+ The runner never puts paths on a command line itself either: every oxlint and
381
+ ESLint child is launched as `node <bootstrap> <engine bin>` with no shell, and
382
+ its full argument list is streamed to it over stdin. As a consequence ESLint's
383
+ own `--stdin` / `--stdin-filename` cannot be forwarded through `--eslint-args`
384
+ or `--`.
385
+
344
386
  ### Flags
345
387
 
346
388
  | Flag | Description |
@@ -355,6 +397,7 @@ Any trailing positional arguments are treated as paths to lint (default `.`).
355
397
  | `--concurrency <n\|off>` | Override the concurrency heuristic with a fixed worker count. |
356
398
  | `--eslint-args "<args>"` | Extra arguments forwarded verbatim to ESLint. |
357
399
  | `--oxlint-args "<args>"` | Extra arguments forwarded verbatim to oxlint. |
400
+ | `--files0-from <file\|->` | Read the paths to lint from a NUL-delimited UTF-8 list (`-` for stdin); an empty list exits 0. |
358
401
  | `--print` | Print the composed commands without running them. |
359
402
  | `-- <args>` | Forward args to the single selected tool (needs `--eslint`/`--oxlint`). |
360
403
  | `-h`, `--help` | Show help. |
@@ -367,6 +410,7 @@ Any trailing positional arguments are treated as paths to lint (default `.`).
367
410
  | `FILES_PER_WORKER` | Target files a single type-aware ESLint worker handles before adding another (default `350`). |
368
411
  | `FAST_FILES_PER_WORKER` | Same, for the fast (syntactic) pass, which packs far more files per worker (default `800`). |
369
412
  | `LINT_MAX_WORKERS` | Upper bound on ESLint workers (default a quarter of available CPUs). |
413
+ | `LINT_TYPED_HEAP_MB` | Heap per worker for the program-building ESLint children: `auto` (default), a size in MB, or `off` (Node's default). |
370
414
  | `ESLINT_TYPE_AWARE` | `off` or `only`; the type-aware mode `--type-aware` sets for the ESLint child (`false` is a legacy alias for `off`). |
371
415
  | `CI` | When set, the runner uses the single full pass with `--cache-strategy content` (see below). |
372
416
 
package/dist/cli.mjs CHANGED
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import { existsSync, readFileSync } from "fs";
10
10
  import { execSync } from "node:child_process";
11
11
  //#region package.json
12
- var version = "6.0.0";
12
+ var version = "6.1.0";
13
13
  var package_default = {
14
14
  name: "@isentinel/eslint-config",
15
15
  version,
package/dist/index.d.mts CHANGED
@@ -28502,7 +28502,14 @@ declare function loadSmallRulesPlugin(): NonNullable<TypedFlatConfigItem["plugin
28502
28502
  declare function smallRules(options?: OptionsComponentExtensions & OptionsFiles & OptionsIsInEditor & OptionsStylistic): Array<TypedFlatConfigItem>;
28503
28503
  //#endregion
28504
28504
  //#region src/eslint/configs/sonarjs.d.ts
28505
- declare function sonarjs({ isInEditor, roblox }: OptionsHasRoblox & Required<OptionsIsInEditor>): Promise<Array<TypedFlatConfigItem>>;
28505
+ declare function sonarjs({ complementIgnores, isInEditor, roblox }: OptionsHasRoblox & Required<OptionsIsInEditor> & {
28506
+ /**
28507
+ * When set, re-apply the non-roblox rules to every source file except
28508
+ * these globs (the roblox scope), so the complement is linted as
28509
+ * standard-TS/Node land.
28510
+ */
28511
+ complementIgnores?: Array<string>;
28512
+ }): Promise<Array<TypedFlatConfigItem>>;
28506
28513
  //#endregion
28507
28514
  //#region src/eslint/configs/sort.d.ts
28508
28515
  /**
package/dist/index.mjs CHANGED
@@ -4523,7 +4523,8 @@ function sonarjsTestRules({ jest = false, roblox = true } = {}) {
4523
4523
  }
4524
4524
  //#endregion
4525
4525
  //#region src/eslint/configs/sonarjs.ts
4526
- async function sonarjs({ isInEditor, roblox }) {
4526
+ init_globs();
4527
+ async function sonarjs({ complementIgnores, isInEditor, roblox }) {
4527
4528
  return [{
4528
4529
  name: "isentinel/sonarjs",
4529
4530
  plugins: { sonar: await interopDefault(import("eslint-plugin-sonarjs")) },
@@ -4531,7 +4532,15 @@ async function sonarjs({ isInEditor, roblox }) {
4531
4532
  isInEditor,
4532
4533
  roblox
4533
4534
  })
4534
- }];
4535
+ }, ...complementIgnores ? [{
4536
+ name: "isentinel/sonarjs/complement",
4537
+ files: [GLOB_SRC],
4538
+ ignores: complementIgnores,
4539
+ rules: sonarjsRules({
4540
+ isInEditor,
4541
+ roblox: false
4542
+ })
4543
+ }] : []];
4535
4544
  }
4536
4545
  //#endregion
4537
4546
  //#region src/eslint/configs/sort.ts
@@ -5938,7 +5947,6 @@ function typescriptTypeAwareRules({ roblox = true } = {}) {
5938
5947
  "ts/no-confusing-void-expression": "error",
5939
5948
  "ts/no-deprecated": "error",
5940
5949
  "ts/no-duplicate-type-constituents": "error",
5941
- "ts/no-empty-object-type": "error",
5942
5950
  "ts/no-floating-promises": ["error", {
5943
5951
  checkThenables: roblox,
5944
5952
  ignoreVoid: true
@@ -20314,6 +20322,7 @@ async function isentinel(options, ...userConfigs) {
20314
20322
  roblox: enableRoblox,
20315
20323
  stylistic: stylisticOptions
20316
20324
  }), promise(), sonarjs({
20325
+ ...needsComplementOverlay ? { complementIgnores: robloxScopedFiles } : {},
20317
20326
  isInEditor,
20318
20327
  roblox: enableRoblox
20319
20328
  }), typescript({
@@ -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 {};