@useinsider/eslint-config 4.0.0-beta.1 → 4.0.0-beta.3

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
@@ -27,8 +27,8 @@ export const { useInsiderESLint, useInsiderOXC } = useInsider({
27
27
  });
28
28
  ```
29
29
 
30
- ```js
31
- // eslint.config.mjs
30
+ ```ts
31
+ // eslint.config.ts
32
32
  import { useInsiderESLint } from './insider.lint.ts';
33
33
 
34
34
  export default useInsiderESLint();
@@ -52,6 +52,13 @@ Three constraints this layout is not free to break:
52
52
  - **The `./insider.lint.ts` import keeps its real extension.** Neither
53
53
  ESLint's loader nor oxlint's applies TypeScript's `.js` → `.ts` specifier
54
54
  rewriting, so `'./insider.lint.js'` does not resolve.
55
+ - **The ESLint config needs a TypeScript extension too** — `eslint.config.ts`
56
+ or `eslint.config.mts`. ESLint routes those through jiti, which resolves the
57
+ TypeScript lint module on every supported Node version. A `.js`/`.mjs`/`.cjs`
58
+ config is loaded by Node directly, so importing `./insider.lint.ts` from one
59
+ works only where Node's type stripping is unflagged (Node >= 22.18) and dies
60
+ with `ERR_UNKNOWN_FILE_EXTENSION` on Node 20. Both CLI commands therefore
61
+ rename an existing JavaScript flat config and delete the original.
55
62
  - **The oxlint config must be `oxlint.config.ts` or `oxlint.config.mts`.**
56
63
  oxlint silently ignores `oxlint.config.{cts,js,mjs,cjs}` and falls through
57
64
  to the nearest ancestor config. In a package with no `"type": "module"`,
@@ -59,7 +66,11 @@ Three constraints this layout is not free to break:
59
66
  - **A TypeScript project needs `allowImportingTsExtensions: true`** in the
60
67
  `tsconfig.json` that covers the layout files, or `tsc` rejects the import
61
68
  with `TS5097`. Both CLI flows offer to set it for you, following `extends`
62
- first so a shared base that already carries it is left alone.
69
+ first so a shared base that already carries it is left alone. TypeScript only
70
+ accepts the flag on a project with `noEmit`, `emitDeclarationOnly` or
71
+ `rewriteRelativeImportExtensions` — on an emitting project it fails with
72
+ `TS5096` — so a project that emits is never edited; the CLI explains what to
73
+ turn on instead.
63
74
  - **`oxlint` runs with `--disable-nested-config`.** The emitted config carries
64
75
  options oxlint accepts only in a root config, and a discovered nested config
65
76
  carrying them aborts the whole run. Disabling nested discovery also makes
@@ -83,7 +94,9 @@ is a sign your scope coverage is incomplete — describe the extra files inside
83
94
  - [Strict and spell-checker knobs](#strict-and-spell-checker-knobs)
84
95
  - [API reference](#api-reference)
85
96
  - [Unused disable directives](#unused-disable-directives)
97
+ - [Lint scripts](#lint-scripts)
86
98
  - [Migrating to v4](#migrating-to-v4)
99
+ - [The guided migration offer](#the-guided-migration-offer)
87
100
  - [Migrating from v1.x](#migrating-from-v1x)
88
101
  - [First lint after install](#first-lint-after-install)
89
102
  - [Troubleshooting](#troubleshooting)
@@ -105,14 +118,17 @@ npx @useinsider/eslint-config
105
118
  The CLI walks an interactive prompt (project type, language, framework, test
106
119
  runner, jQuery globals, tsconfig layout), installs every required linter and
107
120
  plugin pinned to the versions this package was built against, writes the three
108
- layout files at the project root, sets
109
- `"lint": "oxlint --disable-nested-config && eslint ."` in your `package.json`,
110
- and offers to add `allowImportingTsExtensions: true` to your `tsconfig.json`.
111
- That last edit is surgical — comments, key order and indentation survive — and
112
- declining it prints the exact snippet to paste.
113
-
114
- To migrate an existing v1 `eslint.config.{js,mjs,cjs}` or a legacy
115
- `.eslintrc.{js,cjs}`:
121
+ layout files at the project root, wires oxlint into the script that actually
122
+ invokes ESLint (see [Lint scripts](#lint-scripts)), and offers to add
123
+ `allowImportingTsExtensions: true` to the tsconfig that really governs those
124
+ files. In a solution-style project that is the referenced `tsconfig.node.json`,
125
+ not the root — and if an applicable config already sets the flag, or no tsconfig
126
+ type-checks those files, you are not asked at all. That edit is surgical —
127
+ comments, key order and indentation survive — and declining it prints the exact
128
+ snippet to paste.
129
+
130
+ To migrate an existing `eslint.config.{js,mjs,cjs}` — v3's scope-keyed call or
131
+ v1's `useInsider({ preset, config })` — or a legacy `.eslintrc.{js,cjs}`:
116
132
 
117
133
  ```bash
118
134
  npx @useinsider/eslint-config migrate [path]
@@ -121,11 +137,25 @@ npx @useinsider/eslint-config migrate [path]
121
137
  The migrate flow auto-detects the source format, prints a summary plus any
122
138
  dropped custom rules, asks for confirmation, then splits the config into the
123
139
  three-file layout: the `useInsider()` call moves into `insider.lint.*` and
124
- both linter configs become thin re-exports. Legacy `.eslintrc.{js,cjs}` is
125
- also handled (custom rules and non-`@useinsider/eslint-config/*` extends are
126
- dropped with a loud warning so you can review what changed). Legacy
127
- `.eslintrc.{json,yml,yaml}` are not supported in this release — convert them
128
- to `.eslintrc.{js,cjs}` first.
140
+ both linter configs become thin re-exports.
141
+
142
+ A **v3** config takes the shorter route. v4 accepts the same options v3 did,
143
+ so the call is re-housed rather than re-rendered: the file is copied into
144
+ `insider.lint.*` byte-for-byte and only the export statement changes to
145
+ `export const { useInsiderESLint, useInsiderOXC } = useInsider({ ... })`. Your
146
+ comments, `satisfies` clauses, computed values and formatting survive. An
147
+ `await` on the call and an array wrapping it are dropped, since `useInsider()`
148
+ is synchronous in v4 and returns the two renderers. Shapes with nowhere to go
149
+ in a lint module are refused by name rather than half-migrated: more than one
150
+ `useInsider()` call, a default export mixing the call with hand-written
151
+ flat-config blocks, a `defineConfig()` wrapper, and a call reached through a
152
+ variable. An `.eslintignore` beside a v3 config is left alone — flat config
153
+ never read it, and the re-housed call is not rewritten to fold it in.
154
+
155
+ Legacy `.eslintrc.{js,cjs}` is also handled (custom rules and
156
+ non-`@useinsider/eslint-config/*` extends are dropped with a loud warning so
157
+ you can review what changed). Legacy `.eslintrc.{json,yml,yaml}` are not
158
+ supported in this release — convert them to `.eslintrc.{js,cjs}` first.
129
159
 
130
160
  Migrate also rewrites your source: an `// eslint-disable-next-line <rule>`
131
161
  naming a rule oxlint has taken over becomes `// oxlint-disable-next-line
@@ -432,8 +462,51 @@ silenceRules(['no-magic-numbers']);
432
462
  export const { useInsiderESLint, useInsiderOXC } = useInsider({ /* ... */ });
433
463
  ```
434
464
 
465
+ ## Lint scripts
466
+
467
+ Both linters have to run, and oxlint has to run *first* — the ESLint side
468
+ turns off every rule oxlint owns, so ESLint alone silently drops those checks.
469
+ The canonical script is:
470
+
471
+ ```json
472
+ { "scripts": { "lint": "oxlint --disable-nested-config && eslint ." } }
473
+ ```
474
+
475
+ The CLI never replaces a script it did not author. It finds the **one** script
476
+ that really invokes the `eslint` binary and **prepends** oxlint to it, leaving
477
+ the original command verbatim inside the new value. A composed `lint` that fans
478
+ out to a type-check and a stylelint pass is therefore untouched:
479
+
480
+ ```diff
481
+ "lint": "run-p lint:type lint:es lint:style",
482
+ "lint:es": "npm run lint:es-ci -- --cache",
483
+ - "lint:es-ci": "NODE_OPTIONS=--max-old-space-size=6144 eslint .",
484
+ + "lint:es-ci": "oxlint --disable-nested-config && NODE_OPTIONS=--max-old-space-size=6144 eslint .",
485
+ "lint:style": "npm run lint:style-ci -- --cache",
486
+ "lint:type": "run-p lint:type:app lint:type:test lint:type:node",
487
+ ```
488
+
489
+ Prepending rather than appending is deliberate: a `--` passthrough
490
+ (`npm run lint:es-ci -- --cache`) lands on the *last* command, so `--cache`
491
+ still reaches ESLint. A script whose name ends in a `--fix` invocation gets
492
+ `oxlint --disable-nested-config --fix`. Every rewrite is printed with its
493
+ previous value.
494
+
495
+ Exactly one script is rewritten. When several entry points run ESLint —
496
+ `lint`, `lint:fix`, `lint:ci` — prepending to all of them would run oxlint once
497
+ per entry point over the same tree, so the CLI picks the entry point `lint`
498
+ designates (itself, or the leaf it delegates to) and reports the rest with the
499
+ prefix to add if you want oxlint on them too.
500
+
501
+ If no script invokes `eslint` and a `lint` script already exists, nothing is
502
+ written — the conflict is reported with the command to add by hand.
503
+
435
504
  ## Migrating to v4
436
505
 
506
+ v4 requires **Node.js 24.19+** (`engines.node` was `>=20` through v3). Check
507
+ your CI images and any repo pinning an older Node before upgrading: npm and
508
+ pnpm warn with `EBADENGINE`, and fail outright under `engine-strict`.
509
+
437
510
  v4 splits the lint run across ESLint and oxlint. Run the CLI to do all of
438
511
  this automatically:
439
512
 
@@ -441,6 +514,37 @@ this automatically:
441
514
  npx @useinsider/eslint-config migrate
442
515
  ```
443
516
 
517
+ ### The guided migration offer
518
+
519
+ You do not have to know that command exists. Running lint against a v3 config
520
+ under v4 detects the outdated structure and, in an interactive terminal, offers
521
+ to run the migration there and then:
522
+
523
+ ```
524
+ @useinsider/eslint-config: eslint.config.js still uses the v3 single-file layout.
525
+ /repo/eslint.config.js calls `useInsider()` itself, and
526
+ insider.lint.ts and oxlint.config.ts are missing.
527
+
528
+ Starting the guided migration. It shows every change before applying it.
529
+ Skip it with INSIDER_NO_PROMPT=true.
530
+
531
+ ◆ eslint.config.js still uses the v3 single-file layout. Migrate it now?
532
+ │ ● Yes / ○ No
533
+ ```
534
+
535
+ Accepting runs the normal `migrate` flow — same summary, same confirmations —
536
+ and then exits non-zero asking you to re-run lint, because this process already
537
+ loaded the configuration that was just rewritten.
538
+
539
+ The offer is suppressed, and the failure message leads with the `migrate`
540
+ command instead, whenever prompting would be wrong: under oxlint (whose stdout
541
+ is its report), in CI, without a TTY, with `INSIDER_NO_PROMPT=true`, or with
542
+ `INSIDER_SKIP_CONFIG_VALIDATION=true`.
543
+
544
+ The directive rewrite is reported as its own step, with the file count shown
545
+ *before* anything is written, so you can take it as a separate commit or
546
+ decline it and run `migrate` again later.
547
+
444
548
  What changes:
445
549
 
446
550
  - **`eslint-disable` no longer suppresses an oxlint-owned rule. This is the
@@ -509,7 +613,8 @@ What changes:
509
613
  nothing here applies TypeScript's `.js` → `.ts` rewriting — and `tsc` fails
510
614
  with `TS5097` otherwise. The CLI prompts to add it and resolves the full
511
615
  `extends` chain first, so a shared base that already sets it is left alone;
512
- declining prints the snippet to paste.
616
+ declining prints the snippet to paste. A project that emits is never edited —
617
+ TypeScript rejects the flag there with `TS5096`.
513
618
  - **`@typescript-eslint` type-aware rules, `sonarjs`, `@stylistic`, `@cspell`,
514
619
  `jsdoc/check-tag-names` and `no-inner-declarations` stay with ESLint.** Two
515
620
  of those are permanent, for different reasons:
@@ -44,5 +44,11 @@ declare const scopeMap: {
44
44
  };
45
45
  export type ScopeName = keyof typeof scopeMap;
46
46
  export declare function isScopeName(key: string): key is ScopeName;
47
+ /**
48
+ * Every scope name `useInsider()` accepts, for error messages that have to spell the options out.
49
+ * @example
50
+ * scopeNames().includes('typescriptNode'); // true
51
+ */
52
+ export declare function scopeNames(): ScopeName[];
47
53
  export declare function buildScopeBlocks(scope: ScopeName, entries: readonly ScopeEntry[]): Promise<Linter.Config[]>;
48
54
  export {};