@useinsider/eslint-config 3.0.2 → 4.0.0-beta.2
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 +376 -64
- package/dist/builders/configFiles.d.ts +13 -0
- package/dist/builders/oxlint.d.ts +51 -0
- package/dist/builders/oxlintOwnership.d.ts +43 -0
- package/dist/builders/scopeBlocks.d.ts +6 -0
- package/dist/cli.cjs +1028 -86
- package/dist/cli.cjs.map +1 -1
- package/dist/configs/dependencies.d.ts +3 -0
- package/dist/{javascript-VBXqjVi8.cjs → javascript-DH8-N38w.cjs} +5 -5
- package/dist/{javascript-VBXqjVi8.cjs.map → javascript-DH8-N38w.cjs.map} +1 -1
- package/dist/{javascript-dom-5jRL_yKP.cjs → javascript-dom-CvtQ6_9W.cjs} +4 -4
- package/dist/{javascript-dom-5jRL_yKP.cjs.map → javascript-dom-CvtQ6_9W.cjs.map} +1 -1
- package/dist/{javascript-node-C1AU_4X2.cjs → javascript-node-BNsy3loY.cjs} +4 -4
- package/dist/{javascript-node-C1AU_4X2.cjs.map → javascript-node-BNsy3loY.cjs.map} +1 -1
- package/dist/{jest-CHzjzBtf.cjs → jest-4Es9Ggaa.cjs} +3 -3
- package/dist/{jest-CHzjzBtf.cjs.map → jest-4Es9Ggaa.cjs.map} +1 -1
- package/dist/main.cjs +763 -78
- package/dist/main.cjs.map +1 -1
- package/dist/main.d.ts +9 -2
- package/dist/main.js +746 -61
- package/dist/main.js.map +1 -1
- package/dist/oxlintOwnership-7hpdB3d1.cjs +199 -0
- package/dist/oxlintOwnership-7hpdB3d1.cjs.map +1 -0
- package/dist/oxlintOwnership-Cx0hgA_J.js +127 -0
- package/dist/oxlintOwnership-Cx0hgA_J.js.map +1 -0
- package/dist/removePluginsFromConfigs-D0oSUj2C.cjs.map +1 -1
- package/dist/removePluginsFromConfigs-DzqX2Ju2.js.map +1 -1
- package/dist/{stylistic-Dio0AM2E.cjs → stylistic-CoP3MT1Y.cjs} +4 -4
- package/dist/{stylistic-Dio0AM2E.cjs.map → stylistic-CoP3MT1Y.cjs.map} +1 -1
- package/dist/{typescript-C1icmOxY.cjs → typescript-D3VeLnpn.cjs} +7 -7
- package/dist/{typescript-C1icmOxY.cjs.map → typescript-D3VeLnpn.cjs.map} +1 -1
- package/dist/{typescript-dom-RUJn2WCZ.cjs → typescript-dom-iB_xBTAD.cjs} +4 -4
- package/dist/{typescript-dom-RUJn2WCZ.cjs.map → typescript-dom-iB_xBTAD.cjs.map} +1 -1
- package/dist/{typescript-node-CBEzcc56.cjs → typescript-node-Cq6qhj53.cjs} +4 -4
- package/dist/{typescript-node-CBEzcc56.cjs.map → typescript-node-Cq6qhj53.cjs.map} +1 -1
- package/dist/utils/ensureConfigStructure.d.ts +15 -6
- package/dist/utils/ensureOxlintConfig.d.ts +10 -0
- package/dist/utils/layoutDiagnosis.d.ts +63 -0
- package/dist/utils/offerMigration.d.ts +42 -0
- package/dist/utils/silenceRules.d.ts +9 -0
- package/dist/{vitest-BomI8gtu.cjs → vitest-BlGQswK2.cjs} +3 -3
- package/dist/{vitest-BomI8gtu.cjs.map → vitest-BlGQswK2.cjs.map} +1 -1
- package/dist/{vue2-azKwRgaJ.cjs → vue2-Da1i_oVn.cjs} +8 -8
- package/dist/{vue2-azKwRgaJ.cjs.map → vue2-Da1i_oVn.cjs.map} +1 -1
- package/dist/{vue2-options-api-ma3J-bvM.cjs → vue2-options-api-BSdrUm8H.cjs} +8 -8
- package/dist/{vue2-options-api-ma3J-bvM.cjs.map → vue2-options-api-BSdrUm8H.cjs.map} +1 -1
- package/dist/{vue2-typescript-CZSHzp7I.cjs → vue2-typescript-dXLRSGNP.cjs} +10 -10
- package/dist/{vue2-typescript-CZSHzp7I.cjs.map → vue2-typescript-dXLRSGNP.cjs.map} +1 -1
- package/dist/{vue2-typescript-options-api-B8OzZlje.cjs → vue2-typescript-options-api-LM7U5dWq.cjs} +10 -10
- package/dist/{vue2-typescript-options-api-B8OzZlje.cjs.map → vue2-typescript-options-api-LM7U5dWq.cjs.map} +1 -1
- package/dist/{vue3-Df5UQ1Ys.cjs → vue3-621EM2Ii.cjs} +8 -8
- package/dist/{vue3-Df5UQ1Ys.cjs.map → vue3-621EM2Ii.cjs.map} +1 -1
- package/dist/{vue3-options-api-Bx7yYHza.cjs → vue3-options-api-CC9_fAiq.cjs} +8 -8
- package/dist/{vue3-options-api-Bx7yYHza.cjs.map → vue3-options-api-CC9_fAiq.cjs.map} +1 -1
- package/dist/{vue3-typescript-C0O1JZlk.cjs → vue3-typescript-DONe-IKz.cjs} +10 -10
- package/dist/{vue3-typescript-C0O1JZlk.cjs.map → vue3-typescript-DONe-IKz.cjs.map} +1 -1
- package/dist/{vue3-typescript-options-api-cPal4O-K.cjs → vue3-typescript-options-api-D4pqpDFs.cjs} +10 -10
- package/dist/{vue3-typescript-options-api-cPal4O-K.cjs.map → vue3-typescript-options-api-D4pqpDFs.cjs.map} +1 -1
- package/package.json +8 -5
- package/dist/rolldown-runtime-VH7oDXx4.cjs +0 -28
package/README.md
CHANGED
|
@@ -1,19 +1,90 @@
|
|
|
1
1
|
# @useinsider/eslint-config
|
|
2
2
|
|
|
3
|
-
Shared
|
|
4
|
-
scope-keyed config object and returns
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
```ts
|
|
31
|
+
// eslint.config.ts
|
|
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 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.
|
|
62
|
+
- **The oxlint config must be `oxlint.config.ts` or `oxlint.config.mts`.**
|
|
63
|
+
oxlint silently ignores `oxlint.config.{cts,js,mjs,cjs}` and falls through
|
|
64
|
+
to the nearest ancestor config. In a package with no `"type": "module"`,
|
|
65
|
+
use `.mts` for both generated files to avoid Node's module-type warning.
|
|
66
|
+
- **A TypeScript project needs `allowImportingTsExtensions: true`** in the
|
|
67
|
+
`tsconfig.json` that covers the layout files, or `tsc` rejects the import
|
|
68
|
+
with `TS5097`. Both CLI flows offer to set it for you, following `extends`
|
|
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.
|
|
74
|
+
- **`oxlint` runs with `--disable-nested-config`.** The emitted config carries
|
|
75
|
+
options oxlint accepts only in a root config, and a discovered nested config
|
|
76
|
+
carrying them aborts the whole run. Disabling nested discovery also makes
|
|
77
|
+
the config's own `ignorePatterns` authoritative. In a monorepo each package
|
|
78
|
+
runs its own `lint` script, so every config is the root of its own run.
|
|
79
|
+
|
|
80
|
+
**Do not `await` `useInsiderESLint()`. Do not spread either renderer. Do not
|
|
81
|
+
wrap them in an array.** If you find yourself wanting to append a block, that
|
|
82
|
+
is a sign your scope coverage is incomplete — describe the extra files inside
|
|
83
|
+
`useInsider({...})` instead.
|
|
14
84
|
|
|
15
85
|
## Table of Contents
|
|
16
86
|
|
|
87
|
+
- [The three-file layout](#the-three-file-layout)
|
|
17
88
|
- [Prerequisites](#prerequisites)
|
|
18
89
|
- [Quick start](#quick-start)
|
|
19
90
|
- [Manual setup](#manual-setup)
|
|
@@ -22,6 +93,10 @@ inside `useInsider({...})` instead.
|
|
|
22
93
|
- [Vue 2 + JavaScript with jQuery globals](#vue-2--javascript-with-jquery-globals)
|
|
23
94
|
- [Strict and spell-checker knobs](#strict-and-spell-checker-knobs)
|
|
24
95
|
- [API reference](#api-reference)
|
|
96
|
+
- [Unused disable directives](#unused-disable-directives)
|
|
97
|
+
- [Lint scripts](#lint-scripts)
|
|
98
|
+
- [Migrating to v4](#migrating-to-v4)
|
|
99
|
+
- [The guided migration offer](#the-guided-migration-offer)
|
|
25
100
|
- [Migrating from v1.x](#migrating-from-v1x)
|
|
26
101
|
- [First lint after install](#first-lint-after-install)
|
|
27
102
|
- [Troubleshooting](#troubleshooting)
|
|
@@ -29,8 +104,9 @@ inside `useInsider({...})` instead.
|
|
|
29
104
|
|
|
30
105
|
## Prerequisites
|
|
31
106
|
|
|
32
|
-
- Node.js
|
|
107
|
+
- Node.js 24.19+
|
|
33
108
|
- ESLint 9+
|
|
109
|
+
- oxlint 1.83+
|
|
34
110
|
- pnpm, npm, or yarn
|
|
35
111
|
|
|
36
112
|
## Quick start
|
|
@@ -40,9 +116,16 @@ npx @useinsider/eslint-config
|
|
|
40
116
|
```
|
|
41
117
|
|
|
42
118
|
The CLI walks an interactive prompt (project type, language, framework, test
|
|
43
|
-
runner, jQuery globals, tsconfig layout), installs every required
|
|
44
|
-
plugin pinned to the versions this package was built against,
|
|
45
|
-
|
|
119
|
+
runner, jQuery globals, tsconfig layout), installs every required linter and
|
|
120
|
+
plugin pinned to the versions this package was built against, writes the three
|
|
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.
|
|
46
129
|
|
|
47
130
|
To migrate an existing v1 `eslint.config.{js,mjs,cjs}` or a legacy
|
|
48
131
|
`.eslintrc.{js,cjs}`:
|
|
@@ -51,20 +134,34 @@ To migrate an existing v1 `eslint.config.{js,mjs,cjs}` or a legacy
|
|
|
51
134
|
npx @useinsider/eslint-config migrate [path]
|
|
52
135
|
```
|
|
53
136
|
|
|
54
|
-
The migrate flow auto-detects the source format
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
rules and non-`@useinsider/eslint-config/*` extends are
|
|
59
|
-
loud warning so you can review what changed). Legacy
|
|
60
|
-
are not supported in this release — convert them
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
137
|
+
The migrate flow auto-detects the source format, prints a summary plus any
|
|
138
|
+
dropped custom rules, asks for confirmation, then splits the config into the
|
|
139
|
+
three-file layout: the `useInsider()` call moves into `insider.lint.*` and
|
|
140
|
+
both linter configs become thin re-exports. Legacy `.eslintrc.{js,cjs}` is
|
|
141
|
+
also handled (custom rules and non-`@useinsider/eslint-config/*` extends are
|
|
142
|
+
dropped with a loud warning so you can review what changed). Legacy
|
|
143
|
+
`.eslintrc.{json,yml,yaml}` are not supported in this release — convert them
|
|
144
|
+
to `.eslintrc.{js,cjs}` first.
|
|
145
|
+
|
|
146
|
+
Migrate also rewrites your source: an `// eslint-disable-next-line <rule>`
|
|
147
|
+
naming a rule oxlint has taken over becomes `// oxlint-disable-next-line
|
|
148
|
+
<rule>`. This is not cosmetic — the emitted oxlint config refuses the
|
|
149
|
+
`eslint-disable` spelling (see [Unused disable
|
|
150
|
+
directives](#unused-disable-directives)), so an un-migrated comment suppresses
|
|
151
|
+
nothing on either side. A comment listing rules from both sides is split into
|
|
152
|
+
one comment per linter. A `-line` or `-next-line` directive targets one exact
|
|
153
|
+
physical line, so both halves stay on a single line as block comments — a
|
|
154
|
+
newline between them would point the first half at the second instead of at
|
|
155
|
+
the code. Only a whole-file `eslint-disable` is split across two lines. The rewrite reads each
|
|
156
|
+
file's syntax tree and touches only real comment nodes, so a directive spelled
|
|
157
|
+
out inside a string or template literal is left alone.
|
|
158
|
+
|
|
159
|
+
Example `insider.lint.ts` for a Node + TypeScript project:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
65
162
|
import { useInsider } from '@useinsider/eslint-config';
|
|
66
163
|
|
|
67
|
-
export
|
|
164
|
+
export const { useInsiderESLint, useInsiderOXC } = useInsider({
|
|
68
165
|
typescriptNode: [
|
|
69
166
|
{
|
|
70
167
|
files: ['**/*.{ts,tsx}'],
|
|
@@ -79,6 +176,7 @@ pins):
|
|
|
79
176
|
|
|
80
177
|
- `@useinsider/eslint-config`
|
|
81
178
|
- `eslint`
|
|
179
|
+
- `oxlint`
|
|
82
180
|
- `globals`
|
|
83
181
|
- `typescript`
|
|
84
182
|
- `typescript-eslint`
|
|
@@ -92,19 +190,23 @@ or `@vitest/eslint-plugin`).
|
|
|
92
190
|
Install the package first:
|
|
93
191
|
|
|
94
192
|
```bash
|
|
95
|
-
pnpm add -E -D @useinsider/eslint-config
|
|
193
|
+
pnpm add -E -D @useinsider/eslint-config oxlint
|
|
96
194
|
```
|
|
97
195
|
|
|
98
|
-
Then create
|
|
99
|
-
|
|
100
|
-
each entry becomes its
|
|
196
|
+
Then create the [three layout files](#the-three-file-layout) and call
|
|
197
|
+
`useInsider(...)` from `insider.lint.ts` with the scopes your project needs.
|
|
198
|
+
Each scope holds an array of `{ files, ... }` entries; each entry becomes its
|
|
199
|
+
own block in both emitted configs.
|
|
200
|
+
|
|
201
|
+
Every example below shows the body of `insider.lint.ts`; the two linter
|
|
202
|
+
configs never change.
|
|
101
203
|
|
|
102
204
|
### Node + TypeScript
|
|
103
205
|
|
|
104
|
-
```
|
|
206
|
+
```ts
|
|
105
207
|
import { useInsider } from '@useinsider/eslint-config';
|
|
106
208
|
|
|
107
|
-
export
|
|
209
|
+
export const { useInsiderESLint, useInsiderOXC } = useInsider({
|
|
108
210
|
typescriptNode: [
|
|
109
211
|
{
|
|
110
212
|
files: ['**/*.{ts,tsx}'],
|
|
@@ -122,10 +224,10 @@ export default useInsider({
|
|
|
122
224
|
A project using `tsconfig.app.json` for source and `tsconfig.test.json` for
|
|
123
225
|
tests, with `RouterLink` and `RouterView` as globally-registered components.
|
|
124
226
|
|
|
125
|
-
```
|
|
227
|
+
```ts
|
|
126
228
|
import { useInsider } from '@useinsider/eslint-config';
|
|
127
229
|
|
|
128
|
-
export
|
|
230
|
+
export const { useInsiderESLint, useInsiderOXC } = useInsider({
|
|
129
231
|
vue3Typescript: [
|
|
130
232
|
{
|
|
131
233
|
files: ['src/**/*.{ts,tsx,vue}'],
|
|
@@ -152,10 +254,10 @@ paths if a single scope needs more than one project reference.
|
|
|
152
254
|
|
|
153
255
|
### Vue 2 + JavaScript with jQuery globals
|
|
154
256
|
|
|
155
|
-
```
|
|
257
|
+
```ts
|
|
156
258
|
import { useInsider } from '@useinsider/eslint-config';
|
|
157
259
|
|
|
158
|
-
export
|
|
260
|
+
export const { useInsiderESLint, useInsiderOXC } = useInsider({
|
|
159
261
|
vue2: [
|
|
160
262
|
{ files: ['src/**/*.{js,vue}'] },
|
|
161
263
|
],
|
|
@@ -179,10 +281,10 @@ layered on top of it.
|
|
|
179
281
|
`strict` and `spellChecker` are flat cross-cutting options. They sit at the
|
|
180
282
|
top level of the config, not inside a scope entry.
|
|
181
283
|
|
|
182
|
-
```
|
|
284
|
+
```ts
|
|
183
285
|
import { useInsider } from '@useinsider/eslint-config';
|
|
184
286
|
|
|
185
|
-
export
|
|
287
|
+
export const { useInsiderESLint, useInsiderOXC } = useInsider({
|
|
186
288
|
typescriptNode: [
|
|
187
289
|
{
|
|
188
290
|
files: ['**/*.{ts,tsx}'],
|
|
@@ -247,10 +349,10 @@ Every scope entry shape also accepts two common fields:
|
|
|
247
349
|
A scope entry with per-entry globals — declare project-specific bindings
|
|
248
350
|
without spreading the `useInsider` return into your own array:
|
|
249
351
|
|
|
250
|
-
```
|
|
352
|
+
```ts
|
|
251
353
|
import { useInsider } from '@useinsider/eslint-config';
|
|
252
354
|
|
|
253
|
-
export
|
|
355
|
+
export const { useInsiderESLint, useInsiderOXC } = useInsider({
|
|
254
356
|
ignores: ['dist', 'coverage'],
|
|
255
357
|
javascriptDom: [
|
|
256
358
|
{
|
|
@@ -286,28 +388,239 @@ the language scopes and gets overridden by any of them that also match.
|
|
|
286
388
|
> `*.config.*` and `eslint.config.*` are already covered by the defaults, so
|
|
287
389
|
> most projects can simply drop the key.
|
|
288
390
|
|
|
289
|
-
`useInsider` returns `
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
391
|
+
`useInsider` is synchronous and returns `{ useInsiderESLint, useInsiderOXC }`.
|
|
392
|
+
`useInsiderESLint()` returns `Promise<Linter.Config[]>` — export it without
|
|
393
|
+
awaiting, ESLint 9 resolves it at load time. `useInsiderOXC()` also returns a
|
|
394
|
+
Promise, and oxlint does *not* await a default export, so `oxlint.config.ts`
|
|
395
|
+
must use top-level `await`. The `UseInsiderResult` type alias is exported for
|
|
396
|
+
TypeScript callers that wrap the renderers in a helper of their own.
|
|
397
|
+
|
|
398
|
+
Two file-matching differences between the linters are handled for you, but
|
|
399
|
+
worth knowing:
|
|
400
|
+
|
|
401
|
+
- An unanchored oxlint glob matches at any depth, so `files: ['*.mjs']` would
|
|
402
|
+
also claim `.husky/install.mjs`. The emitted oxlint config anchors every
|
|
403
|
+
glob that does not already start with `**`, restoring ESLint's semantics.
|
|
404
|
+
- oxlint discovers a nested `oxlint.config.*` in any subdirectory and lints
|
|
405
|
+
that directory with it, **ignoring the parent config's `ignorePatterns`**.
|
|
406
|
+
The generated lint script passes `--disable-nested-config`, which turns that
|
|
407
|
+
off and makes the root config's `ignorePatterns` authoritative. It is also
|
|
408
|
+
required rather than optional: the emitted config carries root-only
|
|
409
|
+
`options`, and oxlint aborts with a config-parse error the moment it
|
|
410
|
+
discovers those in a nested file — which a monorepo whose packages each ran
|
|
411
|
+
this CLI always has. Run oxlint once per package, from that package.
|
|
412
|
+
|
|
413
|
+
## Unused disable directives
|
|
414
|
+
|
|
415
|
+
Both linters report a disable comment that no longer suppresses anything, and
|
|
416
|
+
each one judges only its own spelling:
|
|
417
|
+
|
|
418
|
+
- The emitted oxlint config sets `options.reportUnusedDisableDirectives` to
|
|
419
|
+
`warn`, so a stale `// oxlint-disable-*` is reported.
|
|
420
|
+
- The emitted ESLint config pins
|
|
421
|
+
`linterOptions.reportUnusedDisableDirectives` to `warn`, so a stale
|
|
422
|
+
`// eslint-disable-*` is reported.
|
|
423
|
+
|
|
424
|
+
It also sets `options.respectEslintDisableDirectives: false`, which is what
|
|
425
|
+
makes the pair sound. oxlint judges an `eslint-disable` against the rules *it*
|
|
426
|
+
ran, so with the default (`true`) a comment naming `@cspell/spellchecker`,
|
|
427
|
+
`sonarjs/*`, `@stylistic/*` or `jsdoc/check-tag-names` — every rule the split
|
|
428
|
+
deliberately keeps on the ESLint side — is reported as unused. Refusing the
|
|
429
|
+
`eslint-disable` spelling removes that false positive entirely, at the cost of
|
|
430
|
+
making the ownership split explicit in your source: a rule oxlint owns must be
|
|
431
|
+
suppressed with `oxlint-disable`, a rule ESLint owns with `eslint-disable`.
|
|
432
|
+
`migrate` performs that rewrite for you; an un-migrated comment is reported by
|
|
433
|
+
ESLint as an unused directive, so nothing fails silently.
|
|
434
|
+
|
|
435
|
+
Neither severity fails the build on its own. Escalate with
|
|
436
|
+
`oxlint --deny-warnings` and `eslint . --max-warnings 0` if you want that.
|
|
294
437
|
|
|
295
438
|
`silenceRules()` (named export) is also available as a global registration
|
|
296
439
|
hook. Call it at module top level to register rule IDs to demote *before*
|
|
297
440
|
`useInsider` is invoked; the per-call `silenceRules: [...]` option layers
|
|
298
441
|
on top.
|
|
299
442
|
|
|
300
|
-
```
|
|
443
|
+
```ts
|
|
301
444
|
import { useInsider, silenceRules } from '@useinsider/eslint-config';
|
|
302
445
|
|
|
303
446
|
silenceRules(['no-magic-numbers']);
|
|
304
447
|
|
|
305
|
-
export
|
|
448
|
+
export const { useInsiderESLint, useInsiderOXC } = useInsider({ /* ... */ });
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Lint scripts
|
|
452
|
+
|
|
453
|
+
Both linters have to run, and oxlint has to run *first* — the ESLint side
|
|
454
|
+
turns off every rule oxlint owns, so ESLint alone silently drops those checks.
|
|
455
|
+
The canonical script is:
|
|
456
|
+
|
|
457
|
+
```json
|
|
458
|
+
{ "scripts": { "lint": "oxlint --disable-nested-config && eslint ." } }
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
The CLI never replaces a script it did not author. It finds the **one** script
|
|
462
|
+
that really invokes the `eslint` binary and **prepends** oxlint to it, leaving
|
|
463
|
+
the original command verbatim inside the new value. A composed `lint` that fans
|
|
464
|
+
out to a type-check and a stylelint pass is therefore untouched:
|
|
465
|
+
|
|
466
|
+
```diff
|
|
467
|
+
"lint": "run-p lint:type lint:es lint:style",
|
|
468
|
+
"lint:es": "npm run lint:es-ci -- --cache",
|
|
469
|
+
- "lint:es-ci": "NODE_OPTIONS=--max-old-space-size=6144 eslint .",
|
|
470
|
+
+ "lint:es-ci": "oxlint --disable-nested-config && NODE_OPTIONS=--max-old-space-size=6144 eslint .",
|
|
471
|
+
"lint:style": "npm run lint:style-ci -- --cache",
|
|
472
|
+
"lint:type": "run-p lint:type:app lint:type:test lint:type:node",
|
|
306
473
|
```
|
|
307
474
|
|
|
475
|
+
Prepending rather than appending is deliberate: a `--` passthrough
|
|
476
|
+
(`npm run lint:es-ci -- --cache`) lands on the *last* command, so `--cache`
|
|
477
|
+
still reaches ESLint. A script whose name ends in a `--fix` invocation gets
|
|
478
|
+
`oxlint --disable-nested-config --fix`. Every rewrite is printed with its
|
|
479
|
+
previous value.
|
|
480
|
+
|
|
481
|
+
Exactly one script is rewritten. When several entry points run ESLint —
|
|
482
|
+
`lint`, `lint:fix`, `lint:ci` — prepending to all of them would run oxlint once
|
|
483
|
+
per entry point over the same tree, so the CLI picks the entry point `lint`
|
|
484
|
+
designates (itself, or the leaf it delegates to) and reports the rest with the
|
|
485
|
+
prefix to add if you want oxlint on them too.
|
|
486
|
+
|
|
487
|
+
If no script invokes `eslint` and a `lint` script already exists, nothing is
|
|
488
|
+
written — the conflict is reported with the command to add by hand.
|
|
489
|
+
|
|
490
|
+
## Migrating to v4
|
|
491
|
+
|
|
492
|
+
v4 requires **Node.js 24.19+** (`engines.node` was `>=20` through v3). Check
|
|
493
|
+
your CI images and any repo pinning an older Node before upgrading: npm and
|
|
494
|
+
pnpm warn with `EBADENGINE`, and fail outright under `engine-strict`.
|
|
495
|
+
|
|
496
|
+
v4 splits the lint run across ESLint and oxlint. Run the CLI to do all of
|
|
497
|
+
this automatically:
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
npx @useinsider/eslint-config migrate
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### The guided migration offer
|
|
504
|
+
|
|
505
|
+
You do not have to know that command exists. Running lint against a v3 config
|
|
506
|
+
under v4 detects the outdated structure and, in an interactive terminal, offers
|
|
507
|
+
to run the migration there and then:
|
|
508
|
+
|
|
509
|
+
```
|
|
510
|
+
@useinsider/eslint-config: eslint.config.js still uses the v3 single-file layout.
|
|
511
|
+
/repo/eslint.config.js calls `useInsider()` itself, and
|
|
512
|
+
insider.lint.ts and oxlint.config.ts are missing.
|
|
513
|
+
|
|
514
|
+
Starting the guided migration. It shows every change before applying it.
|
|
515
|
+
Skip it with INSIDER_NO_PROMPT=true.
|
|
516
|
+
|
|
517
|
+
◆ eslint.config.js still uses the v3 single-file layout. Migrate it now?
|
|
518
|
+
│ ● Yes / ○ No
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Accepting runs the normal `migrate` flow — same summary, same confirmations —
|
|
522
|
+
and then exits non-zero asking you to re-run lint, because this process already
|
|
523
|
+
loaded the configuration that was just rewritten.
|
|
524
|
+
|
|
525
|
+
The offer is suppressed, and the failure message leads with the `migrate`
|
|
526
|
+
command instead, whenever prompting would be wrong: under oxlint (whose stdout
|
|
527
|
+
is its report), in CI, without a TTY, with `INSIDER_NO_PROMPT=true`, or with
|
|
528
|
+
`INSIDER_SKIP_CONFIG_VALIDATION=true`.
|
|
529
|
+
|
|
530
|
+
The directive rewrite is reported as its own step, with the file count shown
|
|
531
|
+
*before* anything is written, so you can take it as a separate commit or
|
|
532
|
+
decline it and run `migrate` again later.
|
|
533
|
+
|
|
534
|
+
What changes:
|
|
535
|
+
|
|
536
|
+
- **`eslint-disable` no longer suppresses an oxlint-owned rule. This is the
|
|
537
|
+
largest behavior change in v4.** The emitted oxlint config sets
|
|
538
|
+
`options.respectEslintDisableDirectives: false`, so oxlint reads only the
|
|
539
|
+
`oxlint-disable` spelling. A comment left in the v3 form suppresses nothing,
|
|
540
|
+
and because ESLint no longer runs that rule it reports the comment as an
|
|
541
|
+
unused directive rather than failing quietly. `migrate` rewrites the affected
|
|
542
|
+
comments for you (AST-based — it touches only real comment nodes).
|
|
543
|
+
|
|
544
|
+
```diff
|
|
545
|
+
- // eslint-disable-next-line no-console -- CLI banner
|
|
546
|
+
+ // oxlint-disable-next-line no-console -- CLI banner
|
|
547
|
+
console.info(banner);
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Rules the split keeps on the ESLint side — `@cspell/*`, `sonarjs/*`,
|
|
551
|
+
`@stylistic/*`, `jsdoc/check-tag-names`, `no-inner-declarations`, every
|
|
552
|
+
type-aware `@typescript-eslint` rule — keep the `eslint-disable` spelling.
|
|
553
|
+
See [Unused disable directives](#unused-disable-directives) for why refusing
|
|
554
|
+
the eslint spelling is what makes stale-directive reporting sound in both
|
|
555
|
+
directions.
|
|
556
|
+
|
|
557
|
+
- **`useInsider()` is now synchronous and returns two renderers.** A v3
|
|
558
|
+
`export default useInsider({...})` no longer produces a flat-config array.
|
|
559
|
+
|
|
560
|
+
```diff
|
|
561
|
+
- // eslint.config.mjs
|
|
562
|
+
- import { useInsider } from '@useinsider/eslint-config';
|
|
563
|
+
-
|
|
564
|
+
- export default useInsider({ typescriptNode: [{ files: ['src/**/*.ts'], tsconfigPaths: ['tsconfig.json'] }] });
|
|
565
|
+
+ // insider.lint.ts
|
|
566
|
+
+ import { useInsider } from '@useinsider/eslint-config';
|
|
567
|
+
+
|
|
568
|
+
+ export const { useInsiderESLint, useInsiderOXC } = useInsider({
|
|
569
|
+
+ typescriptNode: [{ files: ['src/**/*.ts'], tsconfigPaths: ['tsconfig.json'] }],
|
|
570
|
+
+ });
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
- **Two new sibling files** — `eslint.config.*` and `oxlint.config.*`, each a
|
|
574
|
+
thin re-export. See [the three-file layout](#the-three-file-layout).
|
|
575
|
+
- **The lint command runs both linters**: `oxlint --disable-nested-config && eslint .`.
|
|
576
|
+
`--disable-nested-config` is required, not stylistic. Without it oxlint
|
|
577
|
+
discovers any nested `oxlint.config.*` in the tree and lints that subtree
|
|
578
|
+
with it, bypassing the root config's `ignorePatterns`. Worse, the emitted
|
|
579
|
+
config carries `options.*`, which oxlint accepts **only** in a root config —
|
|
580
|
+
a nested config carrying `options` aborts the whole run, even when its
|
|
581
|
+
directory is listed in `ignorePatterns`.
|
|
582
|
+
- **Monorepo consumers run oxlint per package.** oxlint has no workspace mode,
|
|
583
|
+
so each package owns its own three files and its own `lint` script, and each
|
|
584
|
+
script carries `--disable-nested-config`. Running one oxlint pass from the
|
|
585
|
+
repo root over sibling packages is not equivalent.
|
|
586
|
+
- **Only `oxlint.config.ts` and `oxlint.config.mts` are discovered.** Any other
|
|
587
|
+
extension — `.js`, `.mjs`, `.cjs`, `.cts` — is ignored *without a word*, and
|
|
588
|
+
oxlint falls through to the nearest ancestor config. The config appears live
|
|
589
|
+
while none of its rules run. `ensureOxlintConfig()` fails the ESLint side
|
|
590
|
+
when neither accepted filename sits beside the lint module, so the mistake
|
|
591
|
+
surfaces rather than silently disarming half the rule set.
|
|
592
|
+
- **`oxlint` is version-tracked like `eslint` and `globals`.** A consumer
|
|
593
|
+
whose installed `oxlint` is older than this package's pin is upgraded
|
|
594
|
+
automatically on the next lint run, because an older oxlint silently drops
|
|
595
|
+
whatever rules that release did not yet implement. Opt out with
|
|
596
|
+
`silenceDependencyWarning(['oxlint'])`.
|
|
597
|
+
- **`tsconfig.json` gains `allowImportingTsExtensions: true`.** `eslint.config.ts`
|
|
598
|
+
and `oxlint.config.ts` must import the lint module by its real extension —
|
|
599
|
+
nothing here applies TypeScript's `.js` → `.ts` rewriting — and `tsc` fails
|
|
600
|
+
with `TS5097` otherwise. The CLI prompts to add it and resolves the full
|
|
601
|
+
`extends` chain first, so a shared base that already sets it is left alone;
|
|
602
|
+
declining prints the snippet to paste. A project that emits is never edited —
|
|
603
|
+
TypeScript rejects the flag there with `TS5096`.
|
|
604
|
+
- **`@typescript-eslint` type-aware rules, `sonarjs`, `@stylistic`, `@cspell`,
|
|
605
|
+
`jsdoc/check-tag-names` and `no-inner-declarations` stay with ESLint.** Two
|
|
606
|
+
of those are permanent, for different reasons:
|
|
607
|
+
- `jsdoc/check-tag-names` is driven by `settings.jsdoc.tagNamePreference`
|
|
608
|
+
(the TypeScript scopes remap `@desc`/`@callback` onto `@use`/`@watch`), and
|
|
609
|
+
oxlint has no equivalent of flat-config `settings`.
|
|
610
|
+
- `no-inner-declarations` is implemented by both, but they disagree on the
|
|
611
|
+
default for an omitted option: ESLint 9 defaults `blockScopedFunctions` to
|
|
612
|
+
`allow`, oxlint to `disallow`. Handing oxlint the bare `'error'` would
|
|
613
|
+
reject every block-scoped function declaration ESLint accepts.
|
|
614
|
+
- **Stale disable directives warn on both sides, and neither fails the build
|
|
615
|
+
on its own.** Escalate with `oxlint --deny-warnings` and
|
|
616
|
+
`eslint . --max-warnings 0`.
|
|
617
|
+
|
|
308
618
|
## Migrating from v1.x
|
|
309
619
|
|
|
310
|
-
|
|
620
|
+
This section describes the v1 → v2 step. The scope-keyed config object it
|
|
621
|
+
introduces is still the v4 shape; only the wiring around the call changed
|
|
622
|
+
again, which [Migrating to v4](#migrating-to-v4) covers. Do both steps if you
|
|
623
|
+
are coming from v1.
|
|
311
624
|
|
|
312
625
|
- **`preset: string[]`** → top-level scope properties. The order in the
|
|
313
626
|
v1 `preset` array no longer matters — the new shape has one property per
|
|
@@ -365,13 +678,12 @@ v2 is a hard break. The summary:
|
|
|
365
678
|
paths-targeted strict checks, set `strict: [...]` instead of
|
|
366
679
|
`strict: true`.
|
|
367
680
|
|
|
368
|
-
- **Shape change**: `useInsider(...)`
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
the call, including jQuery files via the dedicated `jquery` scope.
|
|
681
|
+
- **Shape change**: `useInsider(...)` stopped being an awaited, spreadable
|
|
682
|
+
array. In v2 it became `export default useInsider({...})`; in v4 it became
|
|
683
|
+
the renderer destructuring shown in
|
|
684
|
+
[Migrating to v4](#migrating-to-v4). Either way there is no supported
|
|
685
|
+
override path — describe every glob you want linted inside the call,
|
|
686
|
+
including jQuery files via the dedicated `jquery` scope.
|
|
375
687
|
|
|
376
688
|
- **Removed**: the `preset` array, the `config: Linter.Config` escape
|
|
377
689
|
hatch, and the `config` preset itself. Configuration-file linting is
|
|
@@ -381,15 +693,15 @@ v2 is a hard break. The summary:
|
|
|
381
693
|
- **Kept**: `silenceRules` accepts the same `string[]` shape both as a
|
|
382
694
|
top-level option and via the named `silenceRules()` export.
|
|
383
695
|
`silenceDependencyWarning()` works unchanged. Side-effect lines such as
|
|
384
|
-
`silenceDependencyWarning(true)` continue to sit above the
|
|
385
|
-
|
|
696
|
+
`silenceDependencyWarning(true)` continue to sit above the `useInsider(...)`
|
|
697
|
+
call, now in `insider.lint.*`.
|
|
386
698
|
|
|
387
699
|
## First lint after install
|
|
388
700
|
|
|
389
|
-
The first time you run lint after installing
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
701
|
+
The first time you run lint after installing, `ensureDependencies` may detect
|
|
702
|
+
that your project's `eslint` or `globals` version is older than the pin and
|
|
703
|
+
auto-install them, exiting with code `1`. Run lint again and it will complete
|
|
704
|
+
normally.
|
|
393
705
|
|
|
394
706
|
## Troubleshooting
|
|
395
707
|
|
|
@@ -399,12 +711,12 @@ If you see this error, your project is missing a peer plugin this config
|
|
|
399
711
|
needs. The error message lists the install command. To silence the warning
|
|
400
712
|
permanently:
|
|
401
713
|
|
|
402
|
-
```
|
|
714
|
+
```ts
|
|
403
715
|
import { useInsider, silenceDependencyWarning } from '@useinsider/eslint-config';
|
|
404
716
|
|
|
405
717
|
silenceDependencyWarning(['@cspell/eslint-plugin', '@stylistic/eslint-plugin']);
|
|
406
718
|
|
|
407
|
-
export
|
|
719
|
+
export const { useInsiderESLint, useInsiderOXC } = useInsider({ /* ... */ });
|
|
408
720
|
```
|
|
409
721
|
|
|
410
722
|
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;
|