@isentinel/eslint-config 6.0.0-beta.9 → 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 CHANGED
@@ -38,6 +38,16 @@ import isentinel from "@isentinel/eslint-config";
38
38
  export default isentinel();
39
39
  ```
40
40
 
41
+ `@isentinel/eslint-config/eslint` is an explicit alias of the root export, for
42
+ symmetry with `@isentinel/eslint-config/oxlint`:
43
+
44
+ ```ts
45
+ // eslint.config.ts
46
+ import { isentinel } from "@isentinel/eslint-config/eslint";
47
+
48
+ export default isentinel();
49
+ ```
50
+
41
51
  #### Optional: TypeScript Config Support
42
52
 
43
53
  If you want to use `eslint.config.ts` instead of `.js`, install `jiti` v2.0.0 or
@@ -91,17 +101,20 @@ export default isentinel(
91
101
 
92
102
  ### Add script for package.json
93
103
 
94
- For example:
104
+ The package ships an `isentinel-lint` bin that runs oxlint and ESLint together.
105
+ The starter wizard adds these for you; to wire them up manually:
95
106
 
96
107
  ```json
97
108
  {
98
109
  "scripts": {
99
- "lint": "eslint",
100
- "lint:fix": "eslint --fix"
110
+ "lint": "isentinel-lint",
111
+ "lint:fix": "isentinel-lint --fix"
101
112
  }
102
113
  }
103
114
  ```
104
115
 
116
+ See [`isentinel-lint`](#isentinel-lint) for what it does and every flag.
117
+
105
118
  ## Recommended Settings
106
119
 
107
120
  ### TSConfig
@@ -194,6 +207,264 @@ Add the following settings to your `.vscode/settings.json`:
194
207
  }
195
208
  ```
196
209
 
210
+ ## isentinel-lint
211
+
212
+ The package ships a second bin, `isentinel-lint`, a hybrid runner that drives
213
+ [oxlint](https://oxc.rs/docs/guide/usage/linter/) and ESLint together. Point
214
+ your `lint` script at it and forget the wiring (the starter wizard does this for
215
+ you):
216
+
217
+ ```json
218
+ {
219
+ "scripts": {
220
+ "lint": "isentinel-lint",
221
+ "lint:fix": "isentinel-lint --fix"
222
+ }
223
+ }
224
+ ```
225
+
226
+ It resolves the `eslint` and `oxlint` binaries from your project's
227
+ `node_modules`, so both must be installed locally.
228
+
229
+ ### What it does
230
+
231
+ By default (a local run, no `--type-aware`, no `--fix`) it runs **three**
232
+ children concurrently via
233
+ [`concurrently`](https://www.npmjs.com/package/concurrently)'s core (no shell),
234
+ grouping their output:
235
+
236
+ - **oxlint** — the full oxlint pass (including its type-aware rules).
237
+ - **fast pass** — ESLint with only the syntactic (non-type-aware) rules, over
238
+ every lintable file (TS/JS, JSON, YAML, TOML, Markdown, Lua). This is where
239
+ non-TS files are linted.
240
+ - **type-aware pass** — ESLint with only the type-aware rules, over the TS/JS
241
+ family. The preset's config splits exactly in two, so `fast ∪ type-aware`
242
+ reproduces the full config rule-for-rule.
243
+
244
+ The type-aware pass is the expensive one (it builds a TypeScript program), so
245
+ the runner **skips it entirely when nothing type-relevant changed** since the
246
+ last run — it uses the TypeScript builder to find the files whose type-aware
247
+ results could have changed, and if that set is empty it prints a short note to
248
+ stderr and does not start the pass. The fast pass and oxlint always run.
249
+
250
+ Other behaviours:
251
+
252
+ - Sizes each ESLint pass's `--concurrency` from how many files it will actually
253
+ re-lint (its own dirty count), rather than eagerly spinning up a worker per
254
+ CPU. The fast pass is cheap per file, so it packs far more files per worker
255
+ (see `FAST_FILES_PER_WORKER`) than the type-aware pass.
256
+ - Keeps a separate ESLint cache per pass so they never invalidate one another:
257
+ `.eslintcache-fast-<key>` (fast pass), `.eslintcache-typeaware-<key>`
258
+ (type-aware pass), and `.eslintcache-<key>` (the full config, used by CI,
259
+ `--type-aware=full` and the fix child below). A change to an
260
+ ESLint/oxlint/Prettier/tsconfig file or a lockfile clears each cache that is
261
+ actually older than that change; a change to the root `package.json`
262
+ resolution surface (`exports`, `imports`, `main`, `module`, `types`,
263
+ `typesVersions`, `dependencies`, `devDependencies`, `peerDependencies`) clears
264
+ only the type-aware caches (a syntactic lint cannot be affected by
265
+ resolution). Add `.eslintcache*` to `.gitignore` — a bare `.eslintcache` entry
266
+ does not match the suffixed names.
267
+ - Splits every cache by **config variant**. ESLint stores a config hash per
268
+ cache entry, so two runs that resolve even slightly different configs wipe
269
+ each other's entries when they share one cache file — an agent session and a
270
+ human session alternating against one cache re-lint the whole project in both
271
+ directions, every time. The `<key>` above is an 8-character hash of the inputs
272
+ that make this preset resolve a different config: agent session, editor
273
+ session and CI. Each variant gets its own cache (and its own builder /
274
+ `package.json`-hash state), so nothing is invalidated; the variants simply
275
+ stop overwriting each other. Git-hook runs deliberately share the plain
276
+ no-agent variant.
277
+
278
+ If your own `eslint.config.*` branches on something the runner cannot see —
279
+ your own agent/editor check, a feature flag, or an explicit `isAgent`,
280
+ `isInEditor` or `defaultSeverity` option — set `ISENTINEL_LINT_CACHE_KEY` to a
281
+ value naming that branch. It is folded into the key, giving those configs
282
+ separate caches too.
283
+
284
+ - Treats `--fix` as a check followed by a fix, in three steps. `oxlint --fix`
285
+ goes first and alone, since it rewrites the files the ESLint children are
286
+ about to read. The run's ESLint passes then run as ordinary **checks** —
287
+ concurrent, own caches, no writes. By default those are the fast and
288
+ type-aware passes — the single full pass in CI; an explicit `--type-aware`
289
+ checks whichever passes that mode selects instead (`full` the single full
290
+ pass, `only` the type-aware pass alone, `off` the fast pass alone). Finally
291
+ **one** ESLint child applies fixes to **only the files those checks had
292
+ messages about**, using the full config (a superset of every pass, so it can
293
+ fix anything any of them found).
294
+
295
+ When the checks report nothing, which is the common case, that last child
296
+ never spawns and `--fix` costs what a check run costs. Being the run's only
297
+ ESLint writer, it also cannot race anything.
298
+
299
+ The file list comes from the check passes' own caches: ESLint stores each
300
+ file's result beside its cache entry and replays it on a cache hit, so a warm
301
+ cache is that pass's whole-tree verdict — including for a pass that
302
+ auto-skipped, whose verdict stands precisely because nothing it cares about
303
+ changed. `--no-cache` leaves no verdict to read, so the fix child falls back
304
+ to linting everything it was given. A target outside the working directory is
305
+ invisible to the listing the verdict is read against, so it is handed to the
306
+ fix child as it stands, alongside the reported files; that child runs without
307
+ the cache, since a stale entry for a file no verdict covers would let ESLint
308
+ skip it.
309
+
310
+ - Lets every child run to completion and returns non-zero if any failed — an
311
+ ordinary lint error in one tool no longer kills the others mid-run.
312
+
313
+ > **Unused eslint-disable directives.** The default two-pass mode does **not**
314
+ > report unused `eslint-disable` directives: a directive naming a rule that
315
+ > lives in the _other_ pass would look unused to the pass that runs it and
316
+ > produce a false positive. CI and `--type-aware=full` runs use the single full
317
+ > config and still report them.
318
+ >
319
+ > A local `--fix` inherits this from its checks. The fix child runs the full
320
+ > config and would report an unused directive, but it only ever sees the files
321
+ > the checks reported, and a file whose _only_ problem is an unused directive is
322
+ > not one of them — so those directives survive a local fix run. In CI the check
323
+ > is the full config, so they are reported and removed as before.
324
+
325
+ ### Hybrid detection
326
+
327
+ Running both engines only makes sense when the ESLint config is in **hybrid
328
+ mode** (`oxlint: true`), which drops every oxlint-covered rule from the ESLint
329
+ side so the two engines never overlap. A config that omits `oxlint: true` runs
330
+ every mapped rule in _both_ engines — duplicate diagnostics, and under `--fix` a
331
+ tug-of-war as each rewrites toward its own option resolution. So whenever both
332
+ engines would run (default mode or `--fix`), the runner checks whether the
333
+ resolved config is hybrid; if it is not, it **runs ESLint only, skips oxlint,
334
+ and prints one stderr warning** (enable hybrid mode, or pass `--oxlint` to run
335
+ oxlint explicitly). The check is cheap: the factory records the hybrid status in
336
+ `node_modules/.cache/isentinel-lint/` on every config evaluation, and the runner
337
+ trusts that cache unless your ESLint config is newer, in which case it probes
338
+ `eslint --print-config` once and caches the result. If the status cannot be
339
+ determined it fails open and runs both engines as before. Explicit `--eslint` /
340
+ `--oxlint` runs skip the check entirely.
341
+
342
+ Any trailing positional arguments are treated as paths to lint (default `.`).
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
+
370
+ ### Flags
371
+
372
+ | Flag | Description |
373
+ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
374
+ | `--eslint` | Run only ESLint. |
375
+ | `--oxlint` | Run only oxlint. |
376
+ | `--fix` | Check as usual, then fix only the files the checks reported (one child). |
377
+ | `--agents`, `--no-agents` | Force agent-friendly output from both linters on or off (default: on in an agent session). |
378
+ | `--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`. |
379
+ | `--no-oxlint-type-aware` | Skip oxlint's type-aware rules (no `oxlint-tsgolint` needed). |
380
+ | `--no-cache` | Disable ESLint's on-disk cache. |
381
+ | `--concurrency <n\|off>` | Override the concurrency heuristic with a fixed worker count. |
382
+ | `--eslint-args "<args>"` | Extra arguments forwarded verbatim to ESLint. |
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. |
385
+ | `--print` | Print the composed commands without running them. |
386
+ | `-- <args>` | Forward args to the single selected tool (needs `--eslint`/`--oxlint`). |
387
+ | `-h`, `--help` | Show help. |
388
+ | `-v`, `--version` | Show the version. |
389
+
390
+ ### Environment
391
+
392
+ | Variable | Effect |
393
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
394
+ | `FILES_PER_WORKER` | Target files a single type-aware ESLint worker handles before adding another (default `350`). |
395
+ | `FAST_FILES_PER_WORKER` | Same, for the fast (syntactic) pass, which packs far more files per worker (default `800`). |
396
+ | `LINT_MAX_WORKERS` | Upper bound on ESLint workers (default a quarter of available CPUs). |
397
+ | `ESLINT_TYPE_AWARE` | `off` or `only`; the type-aware mode `--type-aware` sets for the ESLint child (`false` is a legacy alias for `off`). |
398
+ | `CI` | When set, the runner uses the single full pass with `--cache-strategy content` (see below). |
399
+
400
+ ### Type-aware oxlint and tsgolint
401
+
402
+ By default the runner passes `--type-aware` to oxlint, which needs
403
+ [`oxlint-tsgolint`](https://www.npmjs.com/package/oxlint-tsgolint). If it is not
404
+ installed the runner errors out rather than silently skipping type-aware rules.
405
+ Install it, or pass `--no-oxlint-type-aware` to run oxlint without them:
406
+
407
+ ```bash
408
+ pnpm i -D oxlint oxlint-tsgolint
409
+ ```
410
+
411
+ `--type-aware=off` also drops oxlint's type-aware pass, since the fast mode
412
+ skips type-aware linting entirely.
413
+
414
+ ### CI
415
+
416
+ When a `CI` environment variable is set the runner skips the two-pass split and
417
+ runs a single full-config ESLint pass (alongside oxlint). CI cores are few, so
418
+ the two-pass parallelism does not pay off, and the full config is the only one
419
+ that reports unused `eslint-disable` directives — which you want enforced in CI.
420
+ The pass also switches to `--cache-strategy content` so caches key on file
421
+ contents rather than timestamps, which are unreliable across fresh checkouts.
422
+
423
+ ### Agent output
424
+
425
+ `--agents` emits machine-readable output for AI agents: oxlint runs with
426
+ `--format agent`, and ESLint uses the formatter shipped at
427
+ `@isentinel/eslint-config/formatter-agents`, which the runner resolves and
428
+ passes to `eslint --format` for you.
429
+
430
+ It is on by default whenever an AI coding agent session is detected, so agents
431
+ get agent-shaped output without passing the flag. Detection follows
432
+ [`std-env`](https://github.com/unjs/std-env)'s agent table (Claude Code, Codex,
433
+ Cursor, Gemini CLI, opencode, replit, auggie, goose, junie, devin, kiro, pi),
434
+ plus the `AI_AGENT` variable as an explicit override. Git hook and lint-staged
435
+ runs are excluded — those are human-initiated. Pass `--no-agents` to force human
436
+ output, or set `AI_AGENT` to force detection on.
437
+
438
+ ### Known limitations
439
+
440
+ The incremental machinery favours speed, and a few edges are deliberately left
441
+ to self-heal or need a one-off nudge:
442
+
443
+ - **Config changes reached through an imported module are invisible to a
444
+ _skipped_ typed pass.** The runner busts caches on the mtime of your
445
+ `eslint.config.*`, tsconfigs and lockfiles, but not on files those configs
446
+ `import`. ESLint's own per-entry config hash would still catch such a change
447
+ once the pass runs — but if the typed pass was auto-skipped (nothing
448
+ type-relevant looked dirty), it never runs to notice. Touch your
449
+ `eslint.config.*` (or run once with `--no-cache`) after editing a module it
450
+ imports.
451
+ - **mtime granularity.** Cache freshness is compared by modification time, so
452
+ the usual coarse-filesystem-timestamp race that affects ESLint's own cache
453
+ applies here too (edits within the same clock tick as the last run can be
454
+ missed). CI uses `--cache-strategy content` to sidestep it; locally, a second
455
+ run settles it.
456
+ - **Solution-style / project-reference tsconfigs.** The TypeScript builder used
457
+ to find type-affected files does not follow project references, so in a
458
+ solution-style setup builder invalidation silently becomes a no-op and the
459
+ runner falls back to plain mtime dirtiness. Type-aware results stay correct
460
+ (ESLint still lints the dirty files); only the cross-file "an imported type
461
+ changed" invalidation is skipped.
462
+ - **Hybrid status tracks `eslint.config.*` mtimes only.** If you toggle hybrid
463
+ mode (`oxlint: true`) from a module the config _imports_ rather than the
464
+ config file itself, the runner trusts its cached hybrid decision for one more
465
+ run before the factory's passive write corrects it — it self-heals on the next
466
+ invocation.
467
+
197
468
  ## Customization
198
469
 
199
470
  Normally you only need to import the `isentinel` preset:
@@ -526,6 +797,107 @@ otherwise, you can install them manually:
526
797
  pnpm i -D eslint-plugin-react-x eslint-plugin-react-jsx eslint-plugin-react-naming-convention eslint-plugin-jest
527
798
  ```
528
799
 
800
+ #### Anti-Slop
801
+
802
+ Enable `eslint-plugin-flawless` rules that catch patterns AI-written code tends
803
+ to produce (object-typed parameters, `unknown` returns, unsafe dictionary types,
804
+ conditional empty-object spreads, `Reflect` access) with `antiSlop: true`:
805
+
806
+ ```ts
807
+ // eslint.config.ts
808
+ import isentinel from "@isentinel/eslint-config";
809
+
810
+ export default isentinel({
811
+ antiSlop: true,
812
+ });
813
+ ```
814
+
815
+ #### Naming Conventions
816
+
817
+ Enable the opinionated `flawless/naming-convention` rules with `naming: true`,
818
+ or pass an object to add new selector entries or override the built-in defaults:
819
+
820
+ ```ts
821
+ // eslint.config.ts
822
+ import isentinel from "@isentinel/eslint-config";
823
+
824
+ export default isentinel({
825
+ naming: {
826
+ selectors: [
827
+ // Override the default variable format
828
+ { format: ["snake_case"], selector: "variable" },
829
+ // Add a new selector not covered by the defaults
830
+ { format: ["PascalCase"], prefix: ["I"], selector: "interface" },
831
+ ],
832
+ // Applied only to .tsx files, before `selectors`
833
+ selectorsTsx: [{ format: ["StrictPascalCase"], selector: "function" }],
834
+ },
835
+ });
836
+ ```
837
+
838
+ Entries in `selectors` apply to both the TS and TSX configs; `selectorsTsx`
839
+ applies to the TSX config only. User entries are prepended before the defaults,
840
+ and the rule applies the first matching entry after sorting by specificity — so
841
+ an entry with the same specificity as a default overrides it, while new
842
+ selectors are simply added. A less specific entry cannot override a more
843
+ specific default. The `overridesTypeAware` escape hatch still replaces the whole
844
+ rule if you need full control.
845
+
846
+ ##### Roblox names
847
+
848
+ `strictCamelCase` and `StrictPascalCase` reject two capitals in a row, so a
849
+ `CFrame` has to be stored in `targetCframe` and the identifier stops matching
850
+ the type it holds. `allowedWords` lets listed words keep their own casing inside
851
+ a name, while the rest of the name is still checked. It is off by default:
852
+
853
+ ```ts
854
+ // eslint.config.ts
855
+ import isentinel from "@isentinel/eslint-config";
856
+
857
+ export default isentinel({
858
+ naming: {
859
+ // Use ROBLOX_ALLOWED_WORDS, generated from `@rbxts/types`
860
+ allowedWords: true,
861
+ },
862
+ });
863
+ ```
864
+
865
+ ```ts
866
+ const targetCFrame = new CFrame(); // ok
867
+ const listLayoutUIPadding = 0; // ok
868
+ const autoZIndex = 0; // ok - properties count, not just types
869
+ const target_CFrame = 0; // still an error
870
+ const fooXYBar = 0; // still an error - not a Roblox name
871
+ const colorToHSV = 0; // still an error - methods do not count, use `colorToHsv`
872
+ ```
873
+
874
+ The generated list covers type names and data properties, not methods. A call
875
+ spells the API out at the call site, where the naming rules do not reach, so
876
+ `Color3.ToHSV` is no reason to let the variable holding its result skip them.
877
+
878
+ Pass an array to use exactly those words instead. Spread `ROBLOX_ALLOWED_WORDS`
879
+ to extend the list rather than replace it:
880
+
881
+ ```ts
882
+ import isentinel, { ROBLOX_ALLOWED_WORDS } from "@isentinel/eslint-config";
883
+
884
+ export default isentinel({
885
+ naming: {
886
+ allowedWords: [...ROBLOX_ALLOWED_WORDS, "HTTP", "URL"],
887
+ },
888
+ });
889
+ ```
890
+
891
+ The list reaches every selector, including your own `selectors` entries. A
892
+ selector opts out with its own `allowedWords: []`. Only the two strict formats
893
+ honour it, so it can never loosen `snake_case` or `UPPER_CASE`, and a word only
894
+ matches at a hump boundary, so it cannot split an existing hump.
895
+
896
+ At the **start** of a `strictCamelCase` name a word also matches with its first
897
+ character lowercased, since that is the only spelling the format allows there —
898
+ `motor6DWeld` and `targetMotor6DPart` both pass. Everywhere else the word has to
899
+ be spelled as the API spells it, so `targetmotor6DPart` is still an error.
900
+
529
901
  #### Oxlint
530
902
 
531
903
  The config can run alongside (or be replaced by)
@@ -590,6 +962,53 @@ otherwise, you can install them manually:
590
962
  pnpm i -D eslint-plugin-eslint-plugin
591
963
  ```
592
964
 
965
+ #### Co-located Tests
966
+
967
+ `projectStructure` requires every source file to sit next to its test file,
968
+ through
969
+ [eslint-plugin-project-structure](https://github.com/Igorkowalski94/eslint-plugin-project-structure).
970
+ `src/place-service.ts` then has to be joined by `src/place-service.spec.ts`.
971
+ Only `enforceExistence` is driven here, so the rule never enforces naming:
972
+
973
+ ```ts
974
+ // eslint.config.ts
975
+ import isentinel from "@isentinel/eslint-config";
976
+
977
+ export default isentinel({
978
+ projectStructure: {
979
+ // The plugin's name placeholders, plus `{ext}` for the extension of the
980
+ // file that matched. Default: ["{node-name}.spec.{ext}"]
981
+ enforceExistence: "__tests__/{node-name}.test.{ext}",
982
+ // Default: every source file the preset lints
983
+ files: ["src/**/*.{ts,tsx}"],
984
+ // Default: tests, `.d.ts` files and build configs
985
+ ignores: ["**/index.ts"],
986
+ // Default: `process.cwd()`
987
+ projectRoot: import.meta.dirname,
988
+ // Restricts the check to one subtree. Default: all of `projectRoot`
989
+ structureRoot: "src",
990
+ },
991
+ });
992
+ ```
993
+
994
+ ```bash
995
+ pnpm i -D eslint-plugin-project-structure
996
+ ```
997
+
998
+ `{node-name}` matches the kebab-case filenames the preset enforces; the others
999
+ are `{nodeName}`, `{NodeName}`, `{node_name}` and `{NODE_NAME}`. A file already
1000
+ matching a template is exempt from it. A source file with no test disables the
1001
+ rule at the source.
1002
+
1003
+ The rule needs no type information, so oxlint runs it too: the same option is
1004
+ accepted by the oxlint factory, and hybrid mode (`oxlint: true`) hands the check
1005
+ to oxlint.
1006
+
1007
+ Two caveats. Deleting `foo.spec.ts` does not change `foo.ts`, so a cached run
1008
+ replays the old clean result — the check is only sound uncached. And the plugin
1009
+ writes `projectStructure.cache.json` into `projectRoot` while it has something
1010
+ to report, so gitignore it.
1011
+
593
1012
  ### Git hooks
594
1013
 
595
1014
  If you want to apply lint and auto-fix before every commit, use
@@ -619,6 +1038,37 @@ mise use hk pkl # or see https://hk.jdx.dev for other install methods
619
1038
  hk install
620
1039
  ```
621
1040
 
1041
+ ### pnpm's global virtual store
1042
+
1043
+ Only relevant if you have set
1044
+ [`enableGlobalVirtualStore: true`](https://pnpm.io/global-virtual-store).
1045
+ Otherwise pnpm's hoisting hides the problem and there is nothing to do.
1046
+
1047
+ With the setting on, a package's real location moves out of your project, so
1048
+ TypeScript resolves each dependency's imports from its own declared dependencies
1049
+ alone. Several packages in this preset's tree import types they never declare —
1050
+ `eslint-flat-config-utils` imports from `eslint` without declaring it at all.
1051
+ `skipLibCheck` hides the resulting `TS2307`, but the type still degrades to
1052
+ `any`, which surfaces as misleading reports elsewhere:
1053
+
1054
+ ```text
1055
+ typescript(no-unsafe-argument): Unsafe argument of type Linter.Config[]
1056
+ assigned to a parameter of type TypedFlatConfigItem[]
1057
+ ```
1058
+
1059
+ Since `isentinel()` returns a `FlatConfigComposer` from that package, this hits
1060
+ your `eslint.config.ts` directly. Add the companion pnpm plugin, which declares
1061
+ the missing imports for you:
1062
+
1063
+ ```bash
1064
+ pnpm add --config @isentinel/pnpm-plugin-eslint-config
1065
+ ```
1066
+
1067
+ pnpm auto-loads any config dependency whose name matches its plugin pattern, so
1068
+ there is nothing to configure. Use the CLI rather than hand-editing
1069
+ `pnpm-workspace.yaml`: config dependencies are pinned to an exact version and
1070
+ verified against an integrity hash that `pnpm add --config` records for you.
1071
+
622
1072
  ## View what rules are enabled
623
1073
 
624
1074
  There is a visual tool to help you view what rules are enabled in your project
@@ -0,0 +1,8 @@
1
+ import module from "node:module";
2
+
3
+ // ESLint's own bin enables this, so its CLI already benefits; a language server
4
+ // loading `dist/index.mjs` directly never does. Only the bins call it —
5
+ // `enableCompileCache` is a host-process side effect and the package declares
6
+ // `"sideEffects": false`. Importing it as a module keeps it ahead of the entry:
7
+ // static imports are evaluated before any statement of the importer.
8
+ module.enableCompileCache();
package/bin/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import "../dist/cli.js";
3
+ import "./compile-cache.js";
4
+ import "../dist/cli.mjs";
package/bin/lint.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "./compile-cache.js";
4
+ import "../dist/lint-cli.mjs";
package/dist/cli.d.mts CHANGED
@@ -1 +1 @@
1
- export { };
1
+ export {}