@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 +453 -3
- package/bin/compile-cache.js +8 -0
- package/bin/index.js +2 -1
- package/bin/lint.js +4 -0
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +729 -0
- package/dist/formatter-agents.d.mts +22 -0
- package/dist/formatter-agents.mjs +69 -0
- package/dist/index.d.mts +4355 -1876
- package/dist/index.mjs +7040 -1588
- package/dist/lint-bootstrap.d.mts +1 -0
- package/dist/lint-bootstrap.mjs +98 -0
- package/dist/lint-cli.d.mts +1 -0
- package/dist/lint-cli.mjs +6158 -0
- package/dist/lint-ignored.d.mts +1 -0
- package/dist/lint-ignored.mjs +283 -0
- package/dist/oxlint.d.mts +4588 -2039
- package/dist/oxlint.mjs +7159 -1360
- package/package.json +76 -54
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
|
-
|
|
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": "
|
|
100
|
-
"lint: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
package/bin/lint.js
ADDED
package/dist/cli.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|