@isentinel/eslint-config 6.0.0-beta.12 → 6.0.0-beta.13

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
@@ -91,17 +91,20 @@ export default isentinel(
91
91
 
92
92
  ### Add script for package.json
93
93
 
94
- For example:
94
+ The package ships an `isentinel-lint` bin that runs oxlint and ESLint together.
95
+ The starter wizard adds these for you; to wire them up manually:
95
96
 
96
97
  ```json
97
98
  {
98
99
  "scripts": {
99
- "lint": "eslint",
100
- "lint:fix": "eslint --fix"
100
+ "lint": "isentinel-lint",
101
+ "lint:fix": "isentinel-lint --fix"
101
102
  }
102
103
  }
103
104
  ```
104
105
 
106
+ See [`isentinel-lint`](#isentinel-lint) for what it does and every flag.
107
+
105
108
  ## Recommended Settings
106
109
 
107
110
  ### TSConfig
@@ -194,6 +197,199 @@ Add the following settings to your `.vscode/settings.json`:
194
197
  }
195
198
  ```
196
199
 
200
+ ## isentinel-lint
201
+
202
+ The package ships a second bin, `isentinel-lint`, a hybrid runner that drives
203
+ [oxlint](https://oxc.rs/docs/guide/usage/linter/) and ESLint together. Point
204
+ your `lint` script at it and forget the wiring (the starter wizard does this for
205
+ you):
206
+
207
+ ```json
208
+ {
209
+ "scripts": {
210
+ "lint": "isentinel-lint",
211
+ "lint:fix": "isentinel-lint --fix"
212
+ }
213
+ }
214
+ ```
215
+
216
+ It resolves the `eslint` and `oxlint` binaries from your project's
217
+ `node_modules`, so both must be installed locally.
218
+
219
+ ### What it does
220
+
221
+ By default (a local run, no `--type-aware`, no `--fix`) it runs **three**
222
+ children concurrently via
223
+ [`concurrently`](https://www.npmjs.com/package/concurrently), grouping their
224
+ output:
225
+
226
+ - **oxlint** — the full oxlint pass (including its type-aware rules).
227
+ - **fast pass** — ESLint with only the syntactic (non-type-aware) rules, over
228
+ every lintable file (TS/JS, JSON, YAML, TOML, Markdown, Lua). This is where
229
+ non-TS files are linted.
230
+ - **type-aware pass** — ESLint with only the type-aware rules, over the TS/JS
231
+ family. The preset's config splits exactly in two, so `fast ∪ type-aware`
232
+ reproduces the full config rule-for-rule.
233
+
234
+ The type-aware pass is the expensive one (it builds a TypeScript program), so
235
+ the runner **skips it entirely when nothing type-relevant changed** since the
236
+ last run — it uses the TypeScript builder to find the files whose type-aware
237
+ results could have changed, and if that set is empty it prints a short note to
238
+ stderr and does not start the pass. The fast pass and oxlint always run.
239
+
240
+ Other behaviours:
241
+
242
+ - Sizes each ESLint pass's `--concurrency` from how many files it will actually
243
+ re-lint (its own dirty count), rather than eagerly spinning up a worker per
244
+ CPU. The fast pass is cheap per file, so it packs far more files per worker
245
+ (see `FAST_FILES_PER_WORKER`) than the type-aware pass.
246
+ - Keeps a separate ESLint cache per pass so they never invalidate one another:
247
+ `.eslintcache-fast-<key>` (fast pass), `.eslintcache-typeaware-<key>`
248
+ (type-aware pass), and `.eslintcache-<key>` (the full single-pass config used
249
+ by `--fix`, CI and `--type-aware=full`). A change to an
250
+ ESLint/oxlint/Prettier/tsconfig file or a lockfile clears each cache that is
251
+ actually older than that change; a change to the root `package.json`
252
+ resolution surface (`exports`, `imports`, `main`, `module`, `types`,
253
+ `typesVersions`, `dependencies`, `devDependencies`, `peerDependencies`) clears
254
+ only the type-aware caches (a syntactic lint cannot be affected by
255
+ resolution). Add `.eslintcache*` to `.gitignore` — a bare `.eslintcache` entry
256
+ does not match the suffixed names.
257
+ - Splits every cache by **config variant**. ESLint stores a config hash per
258
+ cache entry, so two runs that resolve even slightly different configs wipe
259
+ each other's entries when they share one cache file — an agent session and a
260
+ human session alternating against one cache re-lint the whole project in both
261
+ directions, every time. The `<key>` above is an 8-character hash of the inputs
262
+ that make this preset resolve a different config: agent session, editor
263
+ session and CI. Each variant gets its own cache (and its own builder /
264
+ `package.json`-hash state), so nothing is invalidated; the variants simply
265
+ stop overwriting each other. Git-hook runs deliberately share the plain
266
+ no-agent variant.
267
+
268
+ If your own `eslint.config.*` branches on something the runner cannot see —
269
+ your own agent/editor check, a feature flag, or an explicit `isAgent`,
270
+ `isInEditor` or `defaultSeverity` option — set `ISENTINEL_LINT_CACHE_KEY` to a
271
+ value naming that branch. It is folded into the key, giving those configs
272
+ separate caches too.
273
+
274
+ - Runs `--fix` sequentially (`oxlint --fix`, then `eslint --fix`) so two writers
275
+ never race on the same files.
276
+ - Lets every child run to completion and returns non-zero if any failed — an
277
+ ordinary lint error in one tool no longer kills the others mid-run.
278
+
279
+ > **Unused eslint-disable directives.** The default two-pass mode does **not**
280
+ > report unused `eslint-disable` directives: a directive naming a rule that
281
+ > lives in the _other_ pass would look unused to the pass that runs it and
282
+ > produce a false positive. `--fix`, CI and `--type-aware=full` runs use the
283
+ > single full config and still report unused directives.
284
+
285
+ ### Hybrid detection
286
+
287
+ Running both engines only makes sense when the ESLint config is in **hybrid
288
+ mode** (`oxlint: true`), which drops every oxlint-covered rule from the ESLint
289
+ side so the two engines never overlap. A config that omits `oxlint: true` runs
290
+ every mapped rule in _both_ engines — duplicate diagnostics, and under `--fix` a
291
+ tug-of-war as each rewrites toward its own option resolution. So whenever both
292
+ engines would run (default mode or `--fix`), the runner checks whether the
293
+ resolved config is hybrid; if it is not, it **runs ESLint only, skips oxlint,
294
+ and prints one stderr warning** (enable hybrid mode, or pass `--oxlint` to run
295
+ oxlint explicitly). The check is cheap: the factory records the hybrid status in
296
+ `node_modules/.cache/isentinel-lint/` on every config evaluation, and the runner
297
+ trusts that cache unless your ESLint config is newer, in which case it probes
298
+ `eslint --print-config` once and caches the result. If the status cannot be
299
+ determined it fails open and runs both engines as before. Explicit `--eslint` /
300
+ `--oxlint` runs skip the check entirely.
301
+
302
+ Any trailing positional arguments are treated as paths to lint (default `.`).
303
+
304
+ ### Flags
305
+
306
+ | Flag | Description |
307
+ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
308
+ | `--eslint` | Run only ESLint. |
309
+ | `--oxlint` | Run only oxlint. |
310
+ | `--fix` | Apply fixes: `oxlint --fix` then `eslint --fix` (sequential). |
311
+ | `--agents` | Emit agent-friendly output from both linters. |
312
+ | `--type-aware=off\|only\|full` | Force a single ESLint pass: `off` fast-only, `only` type-aware-only, `full` the whole config in one pass. Cannot mix with `--fix`. |
313
+ | `--no-oxlint-type-aware` | Skip oxlint's type-aware rules (no `oxlint-tsgolint` needed). |
314
+ | `--no-cache` | Disable ESLint's on-disk cache. |
315
+ | `--concurrency <n\|off>` | Override the concurrency heuristic with a fixed worker count. |
316
+ | `--eslint-args "<args>"` | Extra arguments forwarded verbatim to ESLint. |
317
+ | `--oxlint-args "<args>"` | Extra arguments forwarded verbatim to oxlint. |
318
+ | `--print` | Print the composed commands without running them. |
319
+ | `-- <args>` | Forward args to the single selected tool (needs `--eslint`/`--oxlint`). |
320
+ | `-h`, `--help` | Show help. |
321
+ | `-v`, `--version` | Show the version. |
322
+
323
+ ### Environment
324
+
325
+ | Variable | Effect |
326
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
327
+ | `FILES_PER_WORKER` | Target files a single type-aware ESLint worker handles before adding another (default `350`). |
328
+ | `FAST_FILES_PER_WORKER` | Same, for the fast (syntactic) pass, which packs far more files per worker (default `800`). |
329
+ | `LINT_MAX_WORKERS` | Upper bound on ESLint workers (default a quarter of available CPUs). |
330
+ | `ESLINT_TYPE_AWARE` | `off` or `only`; the type-aware mode `--type-aware` sets for the ESLint child (`false` is a legacy alias for `off`). |
331
+ | `CI` | When set, the runner uses the single full pass with `--cache-strategy content` (see below). |
332
+
333
+ ### Type-aware oxlint and tsgolint
334
+
335
+ By default the runner passes `--type-aware` to oxlint, which needs
336
+ [`oxlint-tsgolint`](https://www.npmjs.com/package/oxlint-tsgolint). If it is not
337
+ installed the runner errors out rather than silently skipping type-aware rules.
338
+ Install it, or pass `--no-oxlint-type-aware` to run oxlint without them:
339
+
340
+ ```bash
341
+ pnpm i -D oxlint oxlint-tsgolint
342
+ ```
343
+
344
+ `--type-aware=off` also drops oxlint's type-aware pass, since the fast mode
345
+ skips type-aware linting entirely.
346
+
347
+ ### CI
348
+
349
+ When a `CI` environment variable is set the runner skips the two-pass split and
350
+ runs a single full-config ESLint pass (alongside oxlint). CI cores are few, so
351
+ the two-pass parallelism does not pay off, and the full config is the only one
352
+ that reports unused `eslint-disable` directives — which you want enforced in CI.
353
+ The pass also switches to `--cache-strategy content` so caches key on file
354
+ contents rather than timestamps, which are unreliable across fresh checkouts.
355
+
356
+ ### Agent output
357
+
358
+ `--agents` emits machine-readable output for AI agents: oxlint runs with
359
+ `--format agent`, and ESLint uses the formatter shipped at
360
+ `@isentinel/eslint-config/formatter-agents`, which the runner resolves and
361
+ passes to `eslint --format` for you.
362
+
363
+ ### Known limitations
364
+
365
+ The incremental machinery favours speed, and a few edges are deliberately left
366
+ to self-heal or need a one-off nudge:
367
+
368
+ - **Config changes reached through an imported module are invisible to a
369
+ _skipped_ typed pass.** The runner busts caches on the mtime of your
370
+ `eslint.config.*`, tsconfigs and lockfiles, but not on files those configs
371
+ `import`. ESLint's own per-entry config hash would still catch such a change
372
+ once the pass runs — but if the typed pass was auto-skipped (nothing
373
+ type-relevant looked dirty), it never runs to notice. Touch your
374
+ `eslint.config.*` (or run once with `--no-cache`) after editing a module it
375
+ imports.
376
+ - **mtime granularity.** Cache freshness is compared by modification time, so
377
+ the usual coarse-filesystem-timestamp race that affects ESLint's own cache
378
+ applies here too (edits within the same clock tick as the last run can be
379
+ missed). CI uses `--cache-strategy content` to sidestep it; locally, a second
380
+ run settles it.
381
+ - **Solution-style / project-reference tsconfigs.** The TypeScript builder used
382
+ to find type-affected files does not follow project references, so in a
383
+ solution-style setup builder invalidation silently becomes a no-op and the
384
+ runner falls back to plain mtime dirtiness. Type-aware results stay correct
385
+ (ESLint still lints the dirty files); only the cross-file "an imported type
386
+ changed" invalidation is skipped.
387
+ - **Hybrid status tracks `eslint.config.*` mtimes only.** If you toggle hybrid
388
+ mode (`oxlint: true`) from a module the config _imports_ rather than the
389
+ config file itself, the runner trusts its cached hybrid decision for one more
390
+ run before the factory's passive write corrects it — it self-heals on the next
391
+ invocation.
392
+
197
393
  ## Customization
198
394
 
199
395
  Normally you only need to import the `isentinel` preset:
package/bin/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import "../dist/cli.js";
3
+ import "../dist/cli.mjs";
package/bin/lint.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "../dist/lint-cli.mjs";