@useinsider/eslint-config 3.0.2 → 4.0.0-beta.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.
Files changed (57) hide show
  1. package/README.md +285 -64
  2. package/dist/builders/configFiles.d.ts +13 -0
  3. package/dist/builders/oxlint.d.ts +51 -0
  4. package/dist/builders/oxlintOwnership.d.ts +43 -0
  5. package/dist/cli.cjs +584 -27
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/configs/dependencies.d.ts +3 -0
  8. package/dist/{javascript-VBXqjVi8.cjs → javascript-DH8-N38w.cjs} +5 -5
  9. package/dist/{javascript-VBXqjVi8.cjs.map → javascript-DH8-N38w.cjs.map} +1 -1
  10. package/dist/{javascript-dom-5jRL_yKP.cjs → javascript-dom-CvtQ6_9W.cjs} +4 -4
  11. package/dist/{javascript-dom-5jRL_yKP.cjs.map → javascript-dom-CvtQ6_9W.cjs.map} +1 -1
  12. package/dist/{javascript-node-C1AU_4X2.cjs → javascript-node-BNsy3loY.cjs} +4 -4
  13. package/dist/{javascript-node-C1AU_4X2.cjs.map → javascript-node-BNsy3loY.cjs.map} +1 -1
  14. package/dist/{jest-CHzjzBtf.cjs → jest-4Es9Ggaa.cjs} +3 -3
  15. package/dist/{jest-CHzjzBtf.cjs.map → jest-4Es9Ggaa.cjs.map} +1 -1
  16. package/dist/main.cjs +452 -73
  17. package/dist/main.cjs.map +1 -1
  18. package/dist/main.d.ts +9 -2
  19. package/dist/main.js +435 -56
  20. package/dist/main.js.map +1 -1
  21. package/dist/oxlintOwnership-7hpdB3d1.cjs +199 -0
  22. package/dist/oxlintOwnership-7hpdB3d1.cjs.map +1 -0
  23. package/dist/oxlintOwnership-Cx0hgA_J.js +127 -0
  24. package/dist/oxlintOwnership-Cx0hgA_J.js.map +1 -0
  25. package/dist/removePluginsFromConfigs-D0oSUj2C.cjs.map +1 -1
  26. package/dist/removePluginsFromConfigs-DzqX2Ju2.js.map +1 -1
  27. package/dist/{stylistic-Dio0AM2E.cjs → stylistic-CoP3MT1Y.cjs} +4 -4
  28. package/dist/{stylistic-Dio0AM2E.cjs.map → stylistic-CoP3MT1Y.cjs.map} +1 -1
  29. package/dist/{typescript-C1icmOxY.cjs → typescript-D3VeLnpn.cjs} +7 -7
  30. package/dist/{typescript-C1icmOxY.cjs.map → typescript-D3VeLnpn.cjs.map} +1 -1
  31. package/dist/{typescript-dom-RUJn2WCZ.cjs → typescript-dom-iB_xBTAD.cjs} +4 -4
  32. package/dist/{typescript-dom-RUJn2WCZ.cjs.map → typescript-dom-iB_xBTAD.cjs.map} +1 -1
  33. package/dist/{typescript-node-CBEzcc56.cjs → typescript-node-Cq6qhj53.cjs} +4 -4
  34. package/dist/{typescript-node-CBEzcc56.cjs.map → typescript-node-Cq6qhj53.cjs.map} +1 -1
  35. package/dist/utils/ensureConfigStructure.d.ts +9 -4
  36. package/dist/utils/ensureOxlintConfig.d.ts +9 -0
  37. package/dist/utils/silenceRules.d.ts +9 -0
  38. package/dist/{vitest-BomI8gtu.cjs → vitest-BlGQswK2.cjs} +3 -3
  39. package/dist/{vitest-BomI8gtu.cjs.map → vitest-BlGQswK2.cjs.map} +1 -1
  40. package/dist/{vue2-azKwRgaJ.cjs → vue2-Da1i_oVn.cjs} +8 -8
  41. package/dist/{vue2-azKwRgaJ.cjs.map → vue2-Da1i_oVn.cjs.map} +1 -1
  42. package/dist/{vue2-options-api-ma3J-bvM.cjs → vue2-options-api-BSdrUm8H.cjs} +8 -8
  43. package/dist/{vue2-options-api-ma3J-bvM.cjs.map → vue2-options-api-BSdrUm8H.cjs.map} +1 -1
  44. package/dist/{vue2-typescript-CZSHzp7I.cjs → vue2-typescript-dXLRSGNP.cjs} +10 -10
  45. package/dist/{vue2-typescript-CZSHzp7I.cjs.map → vue2-typescript-dXLRSGNP.cjs.map} +1 -1
  46. package/dist/{vue2-typescript-options-api-B8OzZlje.cjs → vue2-typescript-options-api-LM7U5dWq.cjs} +10 -10
  47. package/dist/{vue2-typescript-options-api-B8OzZlje.cjs.map → vue2-typescript-options-api-LM7U5dWq.cjs.map} +1 -1
  48. package/dist/{vue3-Df5UQ1Ys.cjs → vue3-621EM2Ii.cjs} +8 -8
  49. package/dist/{vue3-Df5UQ1Ys.cjs.map → vue3-621EM2Ii.cjs.map} +1 -1
  50. package/dist/{vue3-options-api-Bx7yYHza.cjs → vue3-options-api-CC9_fAiq.cjs} +8 -8
  51. package/dist/{vue3-options-api-Bx7yYHza.cjs.map → vue3-options-api-CC9_fAiq.cjs.map} +1 -1
  52. package/dist/{vue3-typescript-C0O1JZlk.cjs → vue3-typescript-DONe-IKz.cjs} +10 -10
  53. package/dist/{vue3-typescript-C0O1JZlk.cjs.map → vue3-typescript-DONe-IKz.cjs.map} +1 -1
  54. package/dist/{vue3-typescript-options-api-cPal4O-K.cjs → vue3-typescript-options-api-D4pqpDFs.cjs} +10 -10
  55. package/dist/{vue3-typescript-options-api-cPal4O-K.cjs.map → vue3-typescript-options-api-D4pqpDFs.cjs.map} +1 -1
  56. package/package.json +8 -5
  57. package/dist/rolldown-runtime-VH7oDXx4.cjs +0 -28
package/README.md CHANGED
@@ -1,19 +1,79 @@
1
1
  # @useinsider/eslint-config
2
2
 
3
- Shared ESLint 9 flat-config for Insider projects. `useInsider()` takes a single
4
- scope-keyed config object and returns a `defineConfig([...])` array — one
5
- entry per file scope you describe. There is no opaque preset list, no
6
- freeform `config` escape hatch, and no expectation that you spread the result
7
- into your own array.
8
-
9
- The default export is the resolved Promise returned by `useInsider(...)`;
10
- ESLint 9 awaits it automatically. **Do not `await` it. Do not spread it. Do
11
- not wrap it in an array.** If you find yourself wanting to append a block,
12
- that is a sign your scope coverage is incomplete — describe the extra files
13
- inside `useInsider({...})` instead.
3
+ Shared lint configuration for Insider projects, split across two linters.
4
+ `useInsider()` takes a single scope-keyed config object and returns two
5
+ renderers: `useInsiderESLint()` for ESLint 9 flat config and `useInsiderOXC()`
6
+ for oxlint. Every rule oxlint implements natively is turned **off** on the
7
+ ESLint side and enabled on the oxlint side from the same description, so the
8
+ two configs cannot drift. There is no opaque preset list, no freeform `config`
9
+ escape hatch, and no expectation that you spread either result into your own
10
+ array.
11
+
12
+ Because ESLint alone would now silently skip every rule oxlint owns, both
13
+ linters must run. `useInsider()` refuses to start if the oxlint config is
14
+ missing.
15
+
16
+ ## The three-file layout
17
+
18
+ `useInsider()` is called from exactly one module, and each linter's config is
19
+ a thin re-export of its renderer:
20
+
21
+ ```ts
22
+ // insider.lint.ts
23
+ import { useInsider } from '@useinsider/eslint-config';
24
+
25
+ export const { useInsiderESLint, useInsiderOXC } = useInsider({
26
+ // scopes go here
27
+ });
28
+ ```
29
+
30
+ ```js
31
+ // eslint.config.mjs
32
+ import { useInsiderESLint } from './insider.lint.ts';
33
+
34
+ export default useInsiderESLint();
35
+ ```
36
+
37
+ ```ts
38
+ // oxlint.config.ts
39
+ import { useInsiderOXC } from './insider.lint.ts';
40
+
41
+ export default await useInsiderOXC();
42
+ ```
43
+
44
+ Then run both:
45
+
46
+ ```bash
47
+ oxlint --disable-nested-config && eslint .
48
+ ```
49
+
50
+ Three constraints this layout is not free to break:
51
+
52
+ - **The `./insider.lint.ts` import keeps its real extension.** Neither
53
+ ESLint's loader nor oxlint's applies TypeScript's `.js` → `.ts` specifier
54
+ rewriting, so `'./insider.lint.js'` does not resolve.
55
+ - **The oxlint config must be `oxlint.config.ts` or `oxlint.config.mts`.**
56
+ oxlint silently ignores `oxlint.config.{cts,js,mjs,cjs}` and falls through
57
+ to the nearest ancestor config. In a package with no `"type": "module"`,
58
+ use `.mts` for both generated files to avoid Node's module-type warning.
59
+ - **A TypeScript project needs `allowImportingTsExtensions: true`** in the
60
+ `tsconfig.json` that covers the layout files, or `tsc` rejects the import
61
+ 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.
63
+ - **`oxlint` runs with `--disable-nested-config`.** The emitted config carries
64
+ options oxlint accepts only in a root config, and a discovered nested config
65
+ carrying them aborts the whole run. Disabling nested discovery also makes
66
+ the config's own `ignorePatterns` authoritative. In a monorepo each package
67
+ runs its own `lint` script, so every config is the root of its own run.
68
+
69
+ **Do not `await` `useInsiderESLint()`. Do not spread either renderer. Do not
70
+ wrap them in an array.** If you find yourself wanting to append a block, that
71
+ is a sign your scope coverage is incomplete — describe the extra files inside
72
+ `useInsider({...})` instead.
14
73
 
15
74
  ## Table of Contents
16
75
 
76
+ - [The three-file layout](#the-three-file-layout)
17
77
  - [Prerequisites](#prerequisites)
18
78
  - [Quick start](#quick-start)
19
79
  - [Manual setup](#manual-setup)
@@ -22,6 +82,8 @@ inside `useInsider({...})` instead.
22
82
  - [Vue 2 + JavaScript with jQuery globals](#vue-2--javascript-with-jquery-globals)
23
83
  - [Strict and spell-checker knobs](#strict-and-spell-checker-knobs)
24
84
  - [API reference](#api-reference)
85
+ - [Unused disable directives](#unused-disable-directives)
86
+ - [Migrating to v4](#migrating-to-v4)
25
87
  - [Migrating from v1.x](#migrating-from-v1x)
26
88
  - [First lint after install](#first-lint-after-install)
27
89
  - [Troubleshooting](#troubleshooting)
@@ -29,8 +91,9 @@ inside `useInsider({...})` instead.
29
91
 
30
92
  ## Prerequisites
31
93
 
32
- - Node.js 20+
94
+ - Node.js 24.19+
33
95
  - ESLint 9+
96
+ - oxlint 1.83+
34
97
  - pnpm, npm, or yarn
35
98
 
36
99
  ## Quick start
@@ -40,9 +103,13 @@ npx @useinsider/eslint-config
40
103
  ```
41
104
 
42
105
  The CLI walks an interactive prompt (project type, language, framework, test
43
- runner, jQuery globals, tsconfig layout), installs every required ESLint
44
- plugin pinned to the versions this package was built against, and writes a
45
- starter `eslint.config.mjs` at the project root.
106
+ runner, jQuery globals, tsconfig layout), installs every required linter and
107
+ 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.
46
113
 
47
114
  To migrate an existing v1 `eslint.config.{js,mjs,cjs}` or a legacy
48
115
  `.eslintrc.{js,cjs}`:
@@ -51,20 +118,34 @@ To migrate an existing v1 `eslint.config.{js,mjs,cjs}` or a legacy
51
118
  npx @useinsider/eslint-config migrate [path]
52
119
  ```
53
120
 
54
- The migrate flow auto-detects the source format. For flat configs it
55
- parses the v1 file, prints a summary plus any dropped custom rules, asks
56
- for confirmation, then rewrites the file in place. For legacy `.eslintrc`
57
- files it writes a fresh `eslint.config.mjs` next to the original (custom
58
- rules and non-`@useinsider/eslint-config/*` extends are dropped with a
59
- loud warning so you can review what changed). Legacy `.eslintrc.{json,yml,yaml}`
60
- are not supported in this release — convert them to `.eslintrc.{js,cjs}` first.
61
-
62
- Example output for a Node + TypeScript project:
63
-
64
- ```js
121
+ The migrate flow auto-detects the source format, prints a summary plus any
122
+ dropped custom rules, asks for confirmation, then splits the config into the
123
+ 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.
129
+
130
+ Migrate also rewrites your source: an `// eslint-disable-next-line <rule>`
131
+ naming a rule oxlint has taken over becomes `// oxlint-disable-next-line
132
+ <rule>`. This is not cosmetic — the emitted oxlint config refuses the
133
+ `eslint-disable` spelling (see [Unused disable
134
+ directives](#unused-disable-directives)), so an un-migrated comment suppresses
135
+ nothing on either side. A comment listing rules from both sides is split into
136
+ one comment per linter. A `-line` or `-next-line` directive targets one exact
137
+ physical line, so both halves stay on a single line as block comments — a
138
+ newline between them would point the first half at the second instead of at
139
+ the code. Only a whole-file `eslint-disable` is split across two lines. The rewrite reads each
140
+ file's syntax tree and touches only real comment nodes, so a directive spelled
141
+ out inside a string or template literal is left alone.
142
+
143
+ Example `insider.lint.ts` for a Node + TypeScript project:
144
+
145
+ ```ts
65
146
  import { useInsider } from '@useinsider/eslint-config';
66
147
 
67
- export default useInsider({
148
+ export const { useInsiderESLint, useInsiderOXC } = useInsider({
68
149
  typescriptNode: [
69
150
  {
70
151
  files: ['**/*.{ts,tsx}'],
@@ -79,6 +160,7 @@ pins):
79
160
 
80
161
  - `@useinsider/eslint-config`
81
162
  - `eslint`
163
+ - `oxlint`
82
164
  - `globals`
83
165
  - `typescript`
84
166
  - `typescript-eslint`
@@ -92,19 +174,23 @@ or `@vitest/eslint-plugin`).
92
174
  Install the package first:
93
175
 
94
176
  ```bash
95
- pnpm add -E -D @useinsider/eslint-config
177
+ pnpm add -E -D @useinsider/eslint-config oxlint
96
178
  ```
97
179
 
98
- Then create `eslint.config.mjs` and call `useInsider(...)` with the scopes
99
- your project needs. Each scope holds an array of `{ files, ... }` entries;
100
- each entry becomes its own flat-config block.
180
+ Then create the [three layout files](#the-three-file-layout) and call
181
+ `useInsider(...)` from `insider.lint.ts` with the scopes your project needs.
182
+ Each scope holds an array of `{ files, ... }` entries; each entry becomes its
183
+ own block in both emitted configs.
184
+
185
+ Every example below shows the body of `insider.lint.ts`; the two linter
186
+ configs never change.
101
187
 
102
188
  ### Node + TypeScript
103
189
 
104
- ```js
190
+ ```ts
105
191
  import { useInsider } from '@useinsider/eslint-config';
106
192
 
107
- export default useInsider({
193
+ export const { useInsiderESLint, useInsiderOXC } = useInsider({
108
194
  typescriptNode: [
109
195
  {
110
196
  files: ['**/*.{ts,tsx}'],
@@ -122,10 +208,10 @@ export default useInsider({
122
208
  A project using `tsconfig.app.json` for source and `tsconfig.test.json` for
123
209
  tests, with `RouterLink` and `RouterView` as globally-registered components.
124
210
 
125
- ```js
211
+ ```ts
126
212
  import { useInsider } from '@useinsider/eslint-config';
127
213
 
128
- export default useInsider({
214
+ export const { useInsiderESLint, useInsiderOXC } = useInsider({
129
215
  vue3Typescript: [
130
216
  {
131
217
  files: ['src/**/*.{ts,tsx,vue}'],
@@ -152,10 +238,10 @@ paths if a single scope needs more than one project reference.
152
238
 
153
239
  ### Vue 2 + JavaScript with jQuery globals
154
240
 
155
- ```js
241
+ ```ts
156
242
  import { useInsider } from '@useinsider/eslint-config';
157
243
 
158
- export default useInsider({
244
+ export const { useInsiderESLint, useInsiderOXC } = useInsider({
159
245
  vue2: [
160
246
  { files: ['src/**/*.{js,vue}'] },
161
247
  ],
@@ -179,10 +265,10 @@ layered on top of it.
179
265
  `strict` and `spellChecker` are flat cross-cutting options. They sit at the
180
266
  top level of the config, not inside a scope entry.
181
267
 
182
- ```js
268
+ ```ts
183
269
  import { useInsider } from '@useinsider/eslint-config';
184
270
 
185
- export default useInsider({
271
+ export const { useInsiderESLint, useInsiderOXC } = useInsider({
186
272
  typescriptNode: [
187
273
  {
188
274
  files: ['**/*.{ts,tsx}'],
@@ -247,10 +333,10 @@ Every scope entry shape also accepts two common fields:
247
333
  A scope entry with per-entry globals — declare project-specific bindings
248
334
  without spreading the `useInsider` return into your own array:
249
335
 
250
- ```js
336
+ ```ts
251
337
  import { useInsider } from '@useinsider/eslint-config';
252
338
 
253
- export default useInsider({
339
+ export const { useInsiderESLint, useInsiderOXC } = useInsider({
254
340
  ignores: ['dist', 'coverage'],
255
341
  javascriptDom: [
256
342
  {
@@ -286,28 +372,164 @@ the language scopes and gets overridden by any of them that also match.
286
372
  > `*.config.*` and `eslint.config.*` are already covered by the defaults, so
287
373
  > most projects can simply drop the key.
288
374
 
289
- `useInsider` returns `Promise<Linter.Config[]>`. Export it directly — ESLint
290
- 9 resolves the Promise at load time. `useInsider`'s return type carries
291
- through, so no JSDoc annotation is needed; the `UseInsiderResult` type alias
292
- is exported for TypeScript callers that wrap `useInsider` in a helper of
293
- their own.
375
+ `useInsider` is synchronous and returns `{ useInsiderESLint, useInsiderOXC }`.
376
+ `useInsiderESLint()` returns `Promise<Linter.Config[]>` — export it without
377
+ awaiting, ESLint 9 resolves it at load time. `useInsiderOXC()` also returns a
378
+ Promise, and oxlint does *not* await a default export, so `oxlint.config.ts`
379
+ must use top-level `await`. The `UseInsiderResult` type alias is exported for
380
+ TypeScript callers that wrap the renderers in a helper of their own.
381
+
382
+ Two file-matching differences between the linters are handled for you, but
383
+ worth knowing:
384
+
385
+ - An unanchored oxlint glob matches at any depth, so `files: ['*.mjs']` would
386
+ also claim `.husky/install.mjs`. The emitted oxlint config anchors every
387
+ glob that does not already start with `**`, restoring ESLint's semantics.
388
+ - oxlint discovers a nested `oxlint.config.*` in any subdirectory and lints
389
+ that directory with it, **ignoring the parent config's `ignorePatterns`**.
390
+ The generated lint script passes `--disable-nested-config`, which turns that
391
+ off and makes the root config's `ignorePatterns` authoritative. It is also
392
+ required rather than optional: the emitted config carries root-only
393
+ `options`, and oxlint aborts with a config-parse error the moment it
394
+ discovers those in a nested file — which a monorepo whose packages each ran
395
+ this CLI always has. Run oxlint once per package, from that package.
396
+
397
+ ## Unused disable directives
398
+
399
+ Both linters report a disable comment that no longer suppresses anything, and
400
+ each one judges only its own spelling:
401
+
402
+ - The emitted oxlint config sets `options.reportUnusedDisableDirectives` to
403
+ `warn`, so a stale `// oxlint-disable-*` is reported.
404
+ - The emitted ESLint config pins
405
+ `linterOptions.reportUnusedDisableDirectives` to `warn`, so a stale
406
+ `// eslint-disable-*` is reported.
407
+
408
+ It also sets `options.respectEslintDisableDirectives: false`, which is what
409
+ makes the pair sound. oxlint judges an `eslint-disable` against the rules *it*
410
+ ran, so with the default (`true`) a comment naming `@cspell/spellchecker`,
411
+ `sonarjs/*`, `@stylistic/*` or `jsdoc/check-tag-names` — every rule the split
412
+ deliberately keeps on the ESLint side — is reported as unused. Refusing the
413
+ `eslint-disable` spelling removes that false positive entirely, at the cost of
414
+ making the ownership split explicit in your source: a rule oxlint owns must be
415
+ suppressed with `oxlint-disable`, a rule ESLint owns with `eslint-disable`.
416
+ `migrate` performs that rewrite for you; an un-migrated comment is reported by
417
+ ESLint as an unused directive, so nothing fails silently.
418
+
419
+ Neither severity fails the build on its own. Escalate with
420
+ `oxlint --deny-warnings` and `eslint . --max-warnings 0` if you want that.
294
421
 
295
422
  `silenceRules()` (named export) is also available as a global registration
296
423
  hook. Call it at module top level to register rule IDs to demote *before*
297
424
  `useInsider` is invoked; the per-call `silenceRules: [...]` option layers
298
425
  on top.
299
426
 
300
- ```js
427
+ ```ts
301
428
  import { useInsider, silenceRules } from '@useinsider/eslint-config';
302
429
 
303
430
  silenceRules(['no-magic-numbers']);
304
431
 
305
- export default useInsider({ /* ... */ });
432
+ export const { useInsiderESLint, useInsiderOXC } = useInsider({ /* ... */ });
306
433
  ```
307
434
 
435
+ ## Migrating to v4
436
+
437
+ v4 splits the lint run across ESLint and oxlint. Run the CLI to do all of
438
+ this automatically:
439
+
440
+ ```bash
441
+ npx @useinsider/eslint-config migrate
442
+ ```
443
+
444
+ What changes:
445
+
446
+ - **`eslint-disable` no longer suppresses an oxlint-owned rule. This is the
447
+ largest behavior change in v4.** The emitted oxlint config sets
448
+ `options.respectEslintDisableDirectives: false`, so oxlint reads only the
449
+ `oxlint-disable` spelling. A comment left in the v3 form suppresses nothing,
450
+ and because ESLint no longer runs that rule it reports the comment as an
451
+ unused directive rather than failing quietly. `migrate` rewrites the affected
452
+ comments for you (AST-based — it touches only real comment nodes).
453
+
454
+ ```diff
455
+ - // eslint-disable-next-line no-console -- CLI banner
456
+ + // oxlint-disable-next-line no-console -- CLI banner
457
+ console.info(banner);
458
+ ```
459
+
460
+ Rules the split keeps on the ESLint side — `@cspell/*`, `sonarjs/*`,
461
+ `@stylistic/*`, `jsdoc/check-tag-names`, `no-inner-declarations`, every
462
+ type-aware `@typescript-eslint` rule — keep the `eslint-disable` spelling.
463
+ See [Unused disable directives](#unused-disable-directives) for why refusing
464
+ the eslint spelling is what makes stale-directive reporting sound in both
465
+ directions.
466
+
467
+ - **`useInsider()` is now synchronous and returns two renderers.** A v3
468
+ `export default useInsider({...})` no longer produces a flat-config array.
469
+
470
+ ```diff
471
+ - // eslint.config.mjs
472
+ - import { useInsider } from '@useinsider/eslint-config';
473
+ -
474
+ - export default useInsider({ typescriptNode: [{ files: ['src/**/*.ts'], tsconfigPaths: ['tsconfig.json'] }] });
475
+ + // insider.lint.ts
476
+ + import { useInsider } from '@useinsider/eslint-config';
477
+ +
478
+ + export const { useInsiderESLint, useInsiderOXC } = useInsider({
479
+ + typescriptNode: [{ files: ['src/**/*.ts'], tsconfigPaths: ['tsconfig.json'] }],
480
+ + });
481
+ ```
482
+
483
+ - **Two new sibling files** — `eslint.config.*` and `oxlint.config.*`, each a
484
+ thin re-export. See [the three-file layout](#the-three-file-layout).
485
+ - **The lint command runs both linters**: `oxlint --disable-nested-config && eslint .`.
486
+ `--disable-nested-config` is required, not stylistic. Without it oxlint
487
+ discovers any nested `oxlint.config.*` in the tree and lints that subtree
488
+ with it, bypassing the root config's `ignorePatterns`. Worse, the emitted
489
+ config carries `options.*`, which oxlint accepts **only** in a root config —
490
+ a nested config carrying `options` aborts the whole run, even when its
491
+ directory is listed in `ignorePatterns`.
492
+ - **Monorepo consumers run oxlint per package.** oxlint has no workspace mode,
493
+ so each package owns its own three files and its own `lint` script, and each
494
+ script carries `--disable-nested-config`. Running one oxlint pass from the
495
+ repo root over sibling packages is not equivalent.
496
+ - **Only `oxlint.config.ts` and `oxlint.config.mts` are discovered.** Any other
497
+ extension — `.js`, `.mjs`, `.cjs`, `.cts` — is ignored *without a word*, and
498
+ oxlint falls through to the nearest ancestor config. The config appears live
499
+ while none of its rules run. `ensureOxlintConfig()` fails the ESLint side
500
+ when neither accepted filename sits beside the lint module, so the mistake
501
+ surfaces rather than silently disarming half the rule set.
502
+ - **`oxlint` is version-tracked like `eslint` and `globals`.** A consumer
503
+ whose installed `oxlint` is older than this package's pin is upgraded
504
+ automatically on the next lint run, because an older oxlint silently drops
505
+ whatever rules that release did not yet implement. Opt out with
506
+ `silenceDependencyWarning(['oxlint'])`.
507
+ - **`tsconfig.json` gains `allowImportingTsExtensions: true`.** `eslint.config.ts`
508
+ and `oxlint.config.ts` must import the lint module by its real extension —
509
+ nothing here applies TypeScript's `.js` → `.ts` rewriting — and `tsc` fails
510
+ with `TS5097` otherwise. The CLI prompts to add it and resolves the full
511
+ `extends` chain first, so a shared base that already sets it is left alone;
512
+ declining prints the snippet to paste.
513
+ - **`@typescript-eslint` type-aware rules, `sonarjs`, `@stylistic`, `@cspell`,
514
+ `jsdoc/check-tag-names` and `no-inner-declarations` stay with ESLint.** Two
515
+ of those are permanent, for different reasons:
516
+ - `jsdoc/check-tag-names` is driven by `settings.jsdoc.tagNamePreference`
517
+ (the TypeScript scopes remap `@desc`/`@callback` onto `@use`/`@watch`), and
518
+ oxlint has no equivalent of flat-config `settings`.
519
+ - `no-inner-declarations` is implemented by both, but they disagree on the
520
+ default for an omitted option: ESLint 9 defaults `blockScopedFunctions` to
521
+ `allow`, oxlint to `disallow`. Handing oxlint the bare `'error'` would
522
+ reject every block-scoped function declaration ESLint accepts.
523
+ - **Stale disable directives warn on both sides, and neither fails the build
524
+ on its own.** Escalate with `oxlint --deny-warnings` and
525
+ `eslint . --max-warnings 0`.
526
+
308
527
  ## Migrating from v1.x
309
528
 
310
- v2 is a hard break. The summary:
529
+ This section describes the v1 → v2 step. The scope-keyed config object it
530
+ introduces is still the v4 shape; only the wiring around the call changed
531
+ again, which [Migrating to v4](#migrating-to-v4) covers. Do both steps if you
532
+ are coming from v1.
311
533
 
312
534
  - **`preset: string[]`** → top-level scope properties. The order in the
313
535
  v1 `preset` array no longer matters — the new shape has one property per
@@ -365,13 +587,12 @@ v2 is a hard break. The summary:
365
587
  paths-targeted strict checks, set `strict: [...]` instead of
366
588
  `strict: true`.
367
589
 
368
- - **Shape change**: `useInsider(...)` is now exported directly as the
369
- default export, without `await`, without spread, and without an array
370
- wrap. ESLint 9 resolves the Promise on load. The old pattern
371
- `export default [...await useInsider({...})]` still parses, but the
372
- preferred shape is `export default useInsider({...})`. There is no
373
- supported override path — describe every glob you want linted inside
374
- the call, including jQuery files via the dedicated `jquery` scope.
590
+ - **Shape change**: `useInsider(...)` stopped being an awaited, spreadable
591
+ array. In v2 it became `export default useInsider({...})`; in v4 it became
592
+ the renderer destructuring shown in
593
+ [Migrating to v4](#migrating-to-v4). Either way there is no supported
594
+ override path — describe every glob you want linted inside the call,
595
+ including jQuery files via the dedicated `jquery` scope.
375
596
 
376
597
  - **Removed**: the `preset` array, the `config: Linter.Config` escape
377
598
  hatch, and the `config` preset itself. Configuration-file linting is
@@ -381,15 +602,15 @@ v2 is a hard break. The summary:
381
602
  - **Kept**: `silenceRules` accepts the same `string[]` shape both as a
382
603
  top-level option and via the named `silenceRules()` export.
383
604
  `silenceDependencyWarning()` works unchanged. Side-effect lines such as
384
- `silenceDependencyWarning(true)` continue to sit above the
385
- `export default useInsider({...})` call.
605
+ `silenceDependencyWarning(true)` continue to sit above the `useInsider(...)`
606
+ call, now in `insider.lint.*`.
386
607
 
387
608
  ## First lint after install
388
609
 
389
- The first time you run lint after installing v2, `ensureDependencies` may
390
- detect that your project's `eslint` or `globals` version is older than the
391
- pin and auto-install them, exiting with code `1`. Run lint again and it
392
- will complete normally.
610
+ The first time you run lint after installing, `ensureDependencies` may detect
611
+ that your project's `eslint` or `globals` version is older than the pin and
612
+ auto-install them, exiting with code `1`. Run lint again and it will complete
613
+ normally.
393
614
 
394
615
  ## Troubleshooting
395
616
 
@@ -399,12 +620,12 @@ If you see this error, your project is missing a peer plugin this config
399
620
  needs. The error message lists the install command. To silence the warning
400
621
  permanently:
401
622
 
402
- ```js
623
+ ```ts
403
624
  import { useInsider, silenceDependencyWarning } from '@useinsider/eslint-config';
404
625
 
405
626
  silenceDependencyWarning(['@cspell/eslint-plugin', '@stylistic/eslint-plugin']);
406
627
 
407
- export default useInsider({ /* ... */ });
628
+ export const { useInsiderESLint, useInsiderOXC } = useInsider({ /* ... */ });
408
629
  ```
409
630
 
410
631
  Passing `true` silences every warning; that is not recommended because the
@@ -9,4 +9,17 @@ import type { Linter } from 'eslint';
9
9
  * `configFiles: string[]` knob.
10
10
  */
11
11
  export declare const DEFAULT_CONFIG_FILE_GLOBS: readonly string[];
12
+ export declare const LINT_TOPOLOGY_NAME = "insider/lint-topology";
13
+ /**
14
+ * The three files `useInsider()` requires a consumer to own. They are always linted with
15
+ * {@link buildLintTopologyBlocks}, independently of the `configFiles` option.
16
+ */
17
+ export declare const LINT_TOPOLOGY_GLOBS: readonly string[];
18
+ /**
19
+ * Relaxations the mandated three-file layout needs in order to lint clean. Emitted unconditionally
20
+ * because the layout exists whether or not the consumer passes `configFiles`.
21
+ * @example
22
+ * buildLintTopologyBlocks()[0].rules['import-x/extensions']; // 'off'
23
+ */
24
+ export declare function buildLintTopologyBlocks(): Linter.Config[];
12
25
  export declare function buildConfigFilesBlocks(value: false | string[] | undefined): Linter.Config[];
@@ -0,0 +1,51 @@
1
+ import type { Linter } from 'eslint';
2
+ export declare const OXLINT_SUPPRESSION_NAME = "insider/oxlint-suppression";
3
+ export declare const OXLINT_SFC_EXCEPTION_NAME = "insider/oxlint-suppression-sfc-exception";
4
+ export declare const UNUSED_DIRECTIVE_BLOCK_NAME = "insider/unused-disable-directives";
5
+ export declare const UNUSED_DIRECTIVE_SEVERITY = "warn";
6
+ export declare function isOxlintSuppressionBlock(block: Linter.Config): boolean;
7
+ export interface OxlintOverride {
8
+ files: string[];
9
+ excludeFiles?: string[];
10
+ rules: Record<string, Linter.RuleEntry>;
11
+ }
12
+ export interface OxlintRendererOptions {
13
+ respectEslintDisableDirectives: boolean;
14
+ reportUnusedDisableDirectives: typeof UNUSED_DIRECTIVE_SEVERITY;
15
+ }
16
+ export interface OxlintRendererConfig {
17
+ plugins: string[];
18
+ categories: Record<string, 'off'>;
19
+ ignorePatterns?: string[];
20
+ options: OxlintRendererOptions;
21
+ rules: Record<string, Linter.RuleEntry>;
22
+ overrides: OxlintOverride[];
23
+ }
24
+ export declare const OXLINT_DIRECTIVE_OPTIONS: OxlintRendererOptions;
25
+ /**
26
+ * The always-on ESLint block that makes a stale `eslint-disable` visible. ESLint 9 defaults this to
27
+ * `warn`, but the split relies on it, so it is pinned rather than inherited.
28
+ * @example
29
+ * buildUnusedDirectiveBlocks()[0].linterOptions; // { reportUnusedDisableDirectives: 'warn' }
30
+ */
31
+ export declare function buildUnusedDirectiveBlocks(): Linter.Config[];
32
+ /**
33
+ * ESLint blocks that turn off every rule the emitted oxlint config takes over. Rules oxlint cannot
34
+ * check inside an SFC are suppressed everywhere except `.vue`/`.svelte`/`.astro`, so a Vue consumer
35
+ * keeps them.
36
+ * @example
37
+ * buildOxlintSuppressionBlocks([{ files: ['src/**\/*.ts'], rules: { 'no-var': 'error' } }]);
38
+ * // [{ files: ['src/**\/*.ts'], rules: { 'no-var': 'off' } }]
39
+ */
40
+ export declare function buildOxlintSuppressionBlocks(blocks: readonly Linter.Config[]): Linter.Config[];
41
+ export declare function anchorOxlintGlob(glob: string): string;
42
+ export declare function anchorOxlintIgnorePattern(pattern: string): string;
43
+ /**
44
+ * The oxlint half of the split, derived from the same emitted ESLint blocks the suppression layer
45
+ * reads. Every oxlint category is pinned off so the config enables exactly the preset's own rules
46
+ * and nothing oxlint would otherwise turn on by default.
47
+ * @example
48
+ * buildOxlintConfig([{ files: ['src/**\/*.ts'], rules: { 'no-var': 'error' } }], ['dist']);
49
+ * // { plugins: ['eslint'], categories: { correctness: 'off', ... }, ignorePatterns: ['/dist'], ... }
50
+ */
51
+ export declare function buildOxlintConfig(blocks: readonly Linter.Config[], ignores: readonly string[] | undefined): OxlintRendererConfig;
@@ -0,0 +1,43 @@
1
+ import type { Linter } from 'eslint';
2
+ export declare const VUE_LIKE_GLOBS: readonly string[];
3
+ export declare const OXLINT_CATEGORIES: readonly string[];
4
+ export declare const SETTINGS_DEPENDENT_RULES: readonly string[];
5
+ export declare const DEFAULT_DIVERGENT_RULES: readonly string[];
6
+ /**
7
+ * The rules `eslint-plugin-oxlint` re-enables for `.vue`/`.svelte`/`.astro`, read from the plugin's
8
+ * own `flat/all` split so the list tracks upstream instead of being hand-maintained.
9
+ * @example
10
+ * readVueUnsafeRules().includes('@typescript-eslint/no-unused-vars'); // true
11
+ */
12
+ export declare function readVueUnsafeRules(): string[];
13
+ /**
14
+ * Whether oxlint owns this ESLint rule in the current split — implemented natively, not type-aware,
15
+ * not nursery, and not driven by a flat-config `settings` key oxlint cannot read.
16
+ * @example
17
+ * isOxlintOwned('import-x/no-duplicates'); // true — oxlint implements it as `import/no-duplicates`
18
+ * isOxlintOwned('sonarjs/no-identical-functions'); // false
19
+ */
20
+ export declare function isOxlintOwned(eslintRuleName: string): boolean;
21
+ /**
22
+ * Whether ESLint must keep running this rule on SFC-like files because oxlint cannot parse their
23
+ * templates. Callers must exclude `**\/*.vue`, `**\/*.svelte` and `**\/*.astro` from its suppression.
24
+ * @example
25
+ * isVueUnsafeRule('@typescript-eslint/no-unused-vars'); // true
26
+ */
27
+ export declare function isVueUnsafeRule(eslintRuleName: string): boolean;
28
+ /**
29
+ * Translates an ESLint rule name into the name oxlint accepts in its config.
30
+ * @example
31
+ * toOxlintRuleName('no-var'); // 'eslint/no-var'
32
+ * toOxlintRuleName('import-x/first'); // 'import/first'
33
+ * toOxlintRuleName('@typescript-eslint/array-type'); // 'typescript/array-type'
34
+ */
35
+ export declare function toOxlintRuleName(eslintRuleName: string): string;
36
+ /**
37
+ * The oxlint plugin id that must appear in the config's `plugins` array for a rule to be loaded.
38
+ * @example
39
+ * toOxlintPluginName('import-x/first'); // 'import'
40
+ * toOxlintPluginName('no-var'); // 'eslint'
41
+ */
42
+ export declare function toOxlintPluginName(eslintRuleName: string): string;
43
+ export declare function isRuleDisabled(entry: Linter.RuleEntry | undefined): boolean;