exadev-eslint-config 2.19.0 → 2.19.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 +192 -117
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
> A real ESLint plugin (not a shareable config) exposing custom rules shared across ExaDev projects. Also published under the unscoped alias `exadev-eslint-config`.
|
|
6
6
|
|
|
7
|
+
**Contents:** [Why](#why) · [Getting started](#getting-started) · [The lighter option](#the-lighter-option-the-plugin-named-export) · [Optional features](#optional-features) · [Rules](#rules) · [Barrel policy](#barrel-policy) · [Development](#development) · [License](#license)
|
|
8
|
+
|
|
7
9
|
## Why
|
|
8
10
|
|
|
9
11
|
Multiple ExaDev repos carried identical copies of a handful of custom ESLint rules (barrel/index discipline, re-export placement, pointless-alias detection). This package is the single source of truth for those rules. Only the *rules* are centralized — not a consumer's whole `eslint.config.ts`, since file-scoping, tsconfig wiring, and runtime-isomorphism import bans are genuinely project-specific. Each consumer keeps its own `eslint.config.ts`, importing rule implementations from here.
|
|
10
12
|
|
|
11
13
|
## Getting started
|
|
12
14
|
|
|
13
|
-
Consumers need `eslint >=10.0.0` and `typescript-eslint >=8.0.0` as required peer dependencies. Importing anything from this package resolves `typescript-eslint`, since both the default export and `plugin` share the same root module — ESM/CJS module evaluation runs a module's entire top-level import graph regardless of which export the caller reads (see [Architecture](#architecture)).
|
|
14
|
-
|
|
15
15
|
```sh
|
|
16
16
|
pnpm add -D @exadev/eslint-config typescript-eslint eslint
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Requires `eslint >=10.0.0` and `typescript-eslint >=8.0.0` as peer dependencies. Importing anything from this package resolves `typescript-eslint`, since the default export and the `plugin` named export share one root module — see [Architecture](#architecture).
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
22
|
// eslint.config.ts
|
|
@@ -34,18 +34,67 @@ export default tseslint.config(
|
|
|
34
34
|
);
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
**Remove your own `tseslint.configs.recommended`/`recommendedTypeChecked`/`strictTypeChecked`/`stylisticTypeChecked` spreads.** The default export already includes `strictTypeChecked` (which subsumes both plain `recommended` and `recommendedTypeChecked`) plus `stylisticTypeChecked`, and registers the `@typescript-eslint` plugin/parser itself — flat config rejects two different plugin object instances registered under the same namespace. You still supply your own `languageOptions.parserOptions.project`/`projectService` pointing at your tsconfig(s).
|
|
38
|
+
|
|
39
|
+
### What the default export includes
|
|
40
|
+
|
|
41
|
+
**typescript-eslint presets:**
|
|
42
|
+
|
|
43
|
+
- `strictTypeChecked` + `stylisticTypeChecked` — already covers `no-deprecated`, `no-misused-spread`, `no-mixed-enums`, `no-unnecessary-condition`, `use-unknown-in-catch-callback-variable`, `return-await`, `related-getter-setter-pairs`, `no-unnecessary-type-parameters`, and more (not re-listed individually below).
|
|
44
|
+
|
|
45
|
+
**This package's own rules** (full details in [Rules](#rules)):
|
|
46
|
+
|
|
47
|
+
- `exadev/barrel-policy` at its auto-detecting default — see [Barrel policy](#barrel-policy)
|
|
48
|
+
- `exadev/no-object-assign`
|
|
49
|
+
- `exadev/no-mutable-union-array-param`
|
|
50
|
+
- `exadev/no-array-isarray-mutation`
|
|
51
|
+
- `exadev/no-enum-number-widening`
|
|
52
|
+
- `exadev/no-enum-reverse-lookup-widening`
|
|
53
|
+
- `exadev/no-map-instanceof-mutation`
|
|
54
|
+
- `exadev/no-set-instanceof-mutation`
|
|
55
|
+
- `exadev/prefer-readonly-array-param`
|
|
56
|
+
- `exadev/prefer-readonly-object-param`
|
|
57
|
+
- `exadev/prefer-numeric-sort-compare`
|
|
58
|
+
- `exadev/no-pointless-reassignment`
|
|
59
|
+
- `exadev/test-file-kind`
|
|
60
|
+
|
|
61
|
+
**Individual rule tuning**, each with its own reasoning:
|
|
62
|
+
|
|
63
|
+
- **`linterOptions.noInlineConfig`** — no `eslint-disable` comments of any kind.
|
|
64
|
+
- **`consistent-type-assertions`** — bans all type assertions (relaxed in test files, see below).
|
|
65
|
+
- **`consistent-type-imports`** and **`consistent-type-exports`** — plain presence, no extra config.
|
|
66
|
+
- **`consistent-return`** — a function can't implicitly return `undefined` on one path and a real value on another.
|
|
67
|
+
- *Why:* that split is usually a bug, not a deliberate design.
|
|
68
|
+
- **`no-non-null-assertion`** — bans the `!` operator.
|
|
69
|
+
- *Why:* it's the same manual-override escape hatch as a type assertion, under a different spelling.
|
|
70
|
+
- **`no-redeclare`** and **`no-shadow`** — plain presence, no extra config.
|
|
71
|
+
- **`no-use-before-define`** set to `{ functions: false }` — everything except function declarations must be defined before use.
|
|
72
|
+
- *Why:* `let`/`const`/`class`/enum bindings have a genuine temporal-dead-zone crash risk, but function declarations are fully hoisted and safe to call before their point of textual declaration — this codebase's own rule files consistently define helper functions after the logic that calls them.
|
|
73
|
+
- **`ban-ts-comment`** — bans `@ts-expect-error` outright (relaxed in test files, see below).
|
|
74
|
+
- **`method-signature-style`** set to `'property'`.
|
|
75
|
+
- *Why:* method-shorthand signatures are checked bivariantly under `strictFunctionTypes`, which is unsound.
|
|
76
|
+
- **`prefer-readonly`**, **`promise-function-async`**, **`require-array-sort-compare`** — plain presence, no extra config.
|
|
77
|
+
- **`strict-void-return`** — bans passing a value-returning function where a void-returning one is expected (e.g. `arr.forEach(x => otherArray.push(x))`).
|
|
78
|
+
- *Why:* not yet in any typescript-eslint preset; this typechecks today only because of TS's own void-return contravariance leniency.
|
|
79
|
+
- **`switch-exhaustiveness-check`** — plain presence, no extra config.
|
|
80
|
+
- **`strict-boolean-expressions`** at the rule's own bare defaults.
|
|
81
|
+
- *Why:* an unambiguous non-nullable truthy check stays allowed; an ambiguous nullable check does not.
|
|
82
|
+
- **`no-magic-numbers`** — tuned to exempt array indexes, enum members, readonly class properties, default parameter values, numeric literal types (e.g. `type Indent = 2 | 4`), and the handful of universally-idiomatic bare numbers (`-1`, `0`, `1`, `2`).
|
|
83
|
+
- **`max-lines`** set to `{ max: 800, skipBlankLines: true, skipComments: true }`.
|
|
84
|
+
- *Why:* counting only real code means a file isn't pushed over the limit by whitespace or its own WHY-explanation comments.
|
|
85
|
+
- **`no-warning-comments`** — bans any comment containing `Stryker disable`.
|
|
86
|
+
- *Why:* that's Stryker's own mutation-testing suppression directive, invisible to `noInlineConfig` above since it isn't an eslint-disable comment.
|
|
87
|
+
|
|
88
|
+
**Test files** (`**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`) get two narrow relaxations of this package's own additions, and only these two:
|
|
89
|
+
|
|
90
|
+
- **`@ts-expect-error`** reverts to `allow-with-description`.
|
|
91
|
+
- *Why:* a compile-time-only assertion of a type failure is a legitimate test pattern; `@ts-ignore`/`@ts-nocheck` stay banned since `@ts-expect-error` is strictly better.
|
|
92
|
+
- **`consistent-type-assertions`** relaxes to `assertionStyle: 'as'`.
|
|
93
|
+
- *Why:* the legacy `<Type>value` form stays banned everywhere.
|
|
94
|
+
|
|
95
|
+
Nothing else inherited from the presets is relaxed.
|
|
96
|
+
|
|
97
|
+
## The lighter option: the `plugin` named export
|
|
49
98
|
|
|
50
99
|
For a project that wants only this package's own rules without the full type-checked bundle, import the named `plugin` export and wire rules individually:
|
|
51
100
|
|
|
@@ -99,42 +148,18 @@ export default tseslint.config(
|
|
|
99
148
|
);
|
|
100
149
|
```
|
|
101
150
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
## Optional React and Next.js support
|
|
105
|
-
|
|
106
|
-
`import exadev from '@exadev/eslint-config'` keeps working unchanged — it's now literally `exadevConfig()` called with no arguments, no migration required. React/hooks/a11y and Next.js rule blocks are folded in automatically, with no separate import or config needed, gated on two independent, always-both-required conditions:
|
|
107
|
-
|
|
108
|
-
1. **The corresponding package must actually be resolvable.** `eslint-plugin-react`, `eslint-plugin-react-hooks`, `eslint-plugin-jsx-a11y`, and `@next/eslint-plugin-next` are all *optional* peer dependencies (`peerDependenciesMeta.<pkg>.optional: true`) — install only whichever your project actually needs:
|
|
109
|
-
```sh
|
|
110
|
-
pnpm add -D eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y # React support
|
|
111
|
-
pnpm add -D @next/eslint-plugin-next # Next.js support
|
|
112
|
-
```
|
|
113
|
-
If none of these resolve, `@exadev/eslint-config`'s default export is byte-for-byte identical to the plain TypeScript ruleset — nothing about the base package changes.
|
|
114
|
-
2. **For React specifically, the file must actually be `.jsx`/`.tsx`.** The React/hooks/a11y rule block is scoped to `files: ['**/*.jsx', '**/*.tsx']`, so even if `eslint-plugin-react` is resolvable only incidentally (e.g. hoisted as a transitive dependency of something unrelated in a monorepo, with zero real JSX anywhere in the linted project), its rules are never matched against a file that isn't JSX — ESLint's flat-config `files` matching happens per linted file, at lint time, not at config-build time. `@next/eslint-plugin-next`'s block carries no such glob: its own presence is already an unambiguous signal on its own (nothing installs it except a real Next.js project).
|
|
115
|
-
|
|
116
|
-
React support pairs `eslint-plugin-react`'s `flat/recommended` with its own `flat/jsx-runtime` config, which turns `react/react-in-jsx-scope` and `react/jsx-uses-react` back off. `flat/recommended` alone assumes the classic runtime, where every file using JSX needs `import React` in scope; the automatic JSX runtime, the default since React 17 and the only mode Next.js's own compiler supports, needs no such import. Without this pairing, a consumer on the automatic runtime would see `react/react-in-jsx-scope` fire on every JSX file in the project.
|
|
117
|
-
|
|
118
|
-
### Explicit control
|
|
151
|
+
`plugin.configs.recommended`/`plugin.configs.barrel` carry no `files`/`ignores` and are safe unscoped — `no-side-effects-in-index` and `no-non-barrel-reexport` each check `context.filename` themselves (self-scoping). For a barrel not at `src/index.ts`, or a project-specific exception, layer an override on top (e.g. `{ files: ['lib/other.ts'], rules: { 'exadev/no-non-barrel-reexport': 'off' } }`) rather than wiring all four rules individually.
|
|
119
152
|
|
|
120
|
-
|
|
153
|
+
## Optional features
|
|
121
154
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// ...your own config...
|
|
129
|
-
{
|
|
130
|
-
files: ['**/*.tsx'],
|
|
131
|
-
plugins: { exadev: plugin },
|
|
132
|
-
extends: [plugin.configs.react], // throws if eslint-plugin-react isn't installed
|
|
133
|
-
},
|
|
134
|
-
);
|
|
135
|
-
```
|
|
155
|
+
| Feature | Default | Control it with |
|
|
156
|
+
| --- | --- | --- |
|
|
157
|
+
| [React & Next.js linting](#optional-react-and-nextjs-support) | Auto-detected: on if the relevant peer package is installed | `exadevConfig({ react, nextjs })` |
|
|
158
|
+
| [Gitignore-derived ignores](#gitignore-derived-ignores) | On if the project has a `.gitignore` | `exadevConfig({ gitignore })` |
|
|
159
|
+
| [RFC 8785 canonical JSON formatting](#rfc-8785-canonical-json-formatting) | Always on | Not optional |
|
|
160
|
+
| [package.json key ordering](#optional-packagejson-key-ordering) | On, unless the project already has a syncpack config | `exadevConfig({ packageJsonKeyOrder })` |
|
|
136
161
|
|
|
137
|
-
|
|
162
|
+
Every tri-state option above (`true`/`false`/`undefined`) is passed through the named `exadevConfig(options, ...userConfigs)` factory export:
|
|
138
163
|
|
|
139
164
|
```ts
|
|
140
165
|
// eslint.config.ts
|
|
@@ -152,15 +177,49 @@ export default tseslint.config(
|
|
|
152
177
|
);
|
|
153
178
|
```
|
|
154
179
|
|
|
180
|
+
Trailing arguments are arbitrary flat-config objects, appended in order after everything else — `exadevConfig({}, { rules: { 'no-console': 'warn' } })` is equivalent to spreading the default export plus one more config object. `import exadev from '@exadev/eslint-config'` (the default export) is just `exadevConfig()` called with no arguments.
|
|
181
|
+
|
|
182
|
+
### Optional React and Next.js support
|
|
183
|
+
|
|
184
|
+
React/hooks/a11y and Next.js rule blocks fold in automatically, with no separate import or config needed, gated on two independent, always-both-required conditions:
|
|
185
|
+
|
|
186
|
+
1. **The corresponding package must actually be resolvable.** `eslint-plugin-react`, `eslint-plugin-react-hooks`, `eslint-plugin-jsx-a11y`, and `@next/eslint-plugin-next` are all *optional* peer dependencies (`peerDependenciesMeta.<pkg>.optional: true`) — install only whichever your project actually needs:
|
|
187
|
+
```sh
|
|
188
|
+
pnpm add -D eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y # React support
|
|
189
|
+
pnpm add -D @next/eslint-plugin-next # Next.js support
|
|
190
|
+
```
|
|
191
|
+
If none of these resolve, `@exadev/eslint-config`'s default export is byte-for-byte identical to the plain TypeScript ruleset — nothing about the base package changes.
|
|
192
|
+
2. **For React specifically, the file must actually be `.jsx`/`.tsx`.** The React/hooks/a11y rule block is scoped to `files: ['**/*.jsx', '**/*.tsx']`, so even if `eslint-plugin-react` is resolvable only incidentally (e.g. hoisted as a transitive dependency of something unrelated in a monorepo, with zero real JSX anywhere in the linted project), its rules never match a file that isn't JSX. `@next/eslint-plugin-next`'s block carries no such glob: its own presence is already an unambiguous signal (nothing installs it except a real Next.js project).
|
|
193
|
+
|
|
194
|
+
React support pairs `eslint-plugin-react`'s `flat/recommended` with its own `flat/jsx-runtime` config, turning `react/react-in-jsx-scope` and `react/jsx-uses-react` back off. `flat/recommended` alone assumes the classic runtime, where every file using JSX needs `import React` in scope; the automatic JSX runtime (the default since React 17, and the only mode Next.js's own compiler supports) needs no such import. Without this pairing, a consumer on the automatic runtime would see `react/react-in-jsx-scope` fire on every JSX file.
|
|
195
|
+
|
|
196
|
+
**Explicit control**, for anyone who doesn't want to rely on auto-detection:
|
|
197
|
+
|
|
198
|
+
- **`plugin.configs.react`/`plugin.configs.nextjs`** — explicit tier selection, mirroring `plugin.configs.recommended`/`.barrel`. Unlike those two, selecting `.react`/`.nextjs` is itself an explicit request: it **throws** a clear, actionable error if the underlying peer isn't installed, rather than silently returning nothing.
|
|
199
|
+
```ts
|
|
200
|
+
import { plugin } from '@exadev/eslint-config';
|
|
201
|
+
import tseslint from 'typescript-eslint';
|
|
202
|
+
|
|
203
|
+
export default tseslint.config(
|
|
204
|
+
// ...your own config...
|
|
205
|
+
{
|
|
206
|
+
files: ['**/*.tsx'],
|
|
207
|
+
plugins: { exadev: plugin },
|
|
208
|
+
extends: [plugin.configs.react], // throws if eslint-plugin-react isn't installed
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
```
|
|
212
|
+
- **`exadevConfig({ react, nextjs })`** — see the tri-state table below.
|
|
213
|
+
|
|
155
214
|
| Value | React (`options.react`) | Next.js (`options.nextjs`) |
|
|
156
215
|
| --- | --- | --- |
|
|
157
216
|
| `true` | Force on — throws if `eslint-plugin-react` isn't resolvable | Force on — throws if `@next/eslint-plugin-next` isn't resolvable |
|
|
158
217
|
| `false` | Force off — always `[]`, no resolution attempted | Force off — always `[]`, no resolution attempted |
|
|
159
218
|
| `undefined` / omitted | Auto-detect (the default) | Auto-detect (the default) |
|
|
160
219
|
|
|
161
|
-
|
|
220
|
+
**Compatibility note:** a consumer who already has `eslint-plugin-react`/`@next/eslint-plugin-next` resolvable for unrelated reasons (e.g. hoisted in a monorepo) and writes `.jsx`/`.tsx` files may see new rule activity the moment they upgrade to a version of this package that ships React/Next.js support, with zero action on their part. This is the normal, widely-accepted ESLint-ecosystem convention that adding rules to a shared/recommended config is a minor bump even though it can newly trip an existing `--max-warnings 0` gate — not a breaking change. Use the `react`/`nextjs` options above to force it off explicitly if needed.
|
|
162
221
|
|
|
163
|
-
|
|
222
|
+
### Gitignore-derived ignores
|
|
164
223
|
|
|
165
224
|
`exadevConfig()`'s default output includes an `ignores` block derived directly from your project's own `.gitignore` (via [`@eslint/config-helpers`](https://www.npmjs.com/package/@eslint/config-helpers)'s `includeIgnoreFile`), so a generated directory your `.gitignore` already knows about (`dist/`, `coverage/`, a tool's own report output) is never linted, without hand-duplicating that list in `eslint.config.ts` too. This closes a real gap: a `.gitignore`d directory that nothing previously linted broadly enough to reach could still get linted the moment a wide-reaching rule (this package's own bundled RFC 8785 JSON canonicalization, say) started matching every file its glob covers.
|
|
166
225
|
|
|
@@ -172,7 +231,7 @@ Trailing arguments are arbitrary flat-config objects, appended in order after ev
|
|
|
172
231
|
|
|
173
232
|
Needs no peer to install — `@eslint/config-helpers` is bundled into this package's own build.
|
|
174
233
|
|
|
175
|
-
|
|
234
|
+
### RFC 8785 canonical JSON formatting
|
|
176
235
|
|
|
177
236
|
Every JSON file is linted against [`eslint-plugin-json-canonical`](https://github.com/ExaDev/eslint-plugin-json-canonical) v2 — plain UTF-16 code-unit key ordering, canonical number formatting, canonical string escaping, and (as of that plugin's own v2) pretty-printed layout (2-space indentation, one member/element per line, a trailing newline), per [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785). This is bundled unconditionally, the same way jsdoc/tsdoc support is: `eslint-plugin-json-canonical` is a plain dependency of this package, so every consumer already has it. There is no option to turn it off.
|
|
178
237
|
|
|
@@ -182,7 +241,7 @@ The plugin's own full-canonicalization rule (`no-insignificant-whitespace`, whic
|
|
|
182
241
|
|
|
183
242
|
`**/package.json` gets everything the plain-JSON config gives every other file — including pretty-printed layout — except `json/sort-keys`, turned back off in its own override block, since its key order is the separate, syncpack-aware concern the next section covers.
|
|
184
243
|
|
|
185
|
-
|
|
244
|
+
### Optional package.json key ordering
|
|
186
245
|
|
|
187
246
|
`exadevConfig({ packageJsonKeyOrder: true })` enables `exadev/package-json-key-order` for `**/package.json`, requiring the same key order [`syncpack format`](https://syncpack.dev/command/format) would produce — `sortFirst` fields (`name`, `description`, `version`, `author` by default) pinned to the top in that exact order, then every other top-level key alphabetically; and, inside each `sortAz`-listed field's own object or array value (`dependencies`, `devDependencies`, `scripts`, `keywords`, and the rest of syncpack's own default list), its members/elements sorted the same way. Confirmed directly against real `syncpack@15` output, not assumed from its docs — see this rule's own source comment for the exact reverse-engineering method (a symbol-before-digit-before-letter, case-insensitive comparison syncpack's docs don't specify precisely enough to derive from prose alone).
|
|
188
247
|
|
|
@@ -196,32 +255,32 @@ This exists for a project that wants real `package.json` canonicalization withou
|
|
|
196
255
|
|
|
197
256
|
Like React/Next.js support, this needs its own optional peer resolvable — `pnpm add -D @eslint/json` — and, unlike them, also needs its `json/json` language registered for the file (this option's own config block does that for you; nothing extra to wire up).
|
|
198
257
|
|
|
199
|
-
Bundled into `exadevConfig()`'s default output the same way React/Next.js auto-detection is
|
|
258
|
+
Bundled into `exadevConfig()`'s default output the same way React/Next.js auto-detection is — `packageJsonKeyOrder: true`/`false` only forces the tri-state explicitly, it isn't the only way to reach it. Not part of `plugin.configs.recommended`, and not available as a `plugin.configs.packageJsonKeyOrder` explicit-tier config the way `.react`/`.nextjs` are, since wiring it through `plugin.configs` would need `plugin.ts` and this option's own config builder to import each other.
|
|
200
259
|
|
|
201
260
|
## Rules
|
|
202
261
|
|
|
203
262
|
| Rule | Fixable | Description |
|
|
204
263
|
| --- | --- | --- |
|
|
205
|
-
| `barrel-policy` | | Umbrella
|
|
206
|
-
| `no-index-files` | | Bans any `index.*` file outright (mode 1). The strictest policy. |
|
|
207
|
-
| `no-non-barrel-index` | | Only `src/index.ts` may be named `index
|
|
208
|
-
| `no-non-barrel-reexport` | ✓ | Re-exports belong only in a barrel
|
|
209
|
-
| `no-side-effects-in-index` | | A barrel
|
|
210
|
-
| `barrel-direct-siblings-only` | | A barrel may re-export only from a direct sibling (`./module`), never a nested path, parent, or bare package specifier (mode 3). |
|
|
211
|
-
| `no-control-flow` | | Bans `if`/`switch
|
|
212
|
-
| `no-pointless-reassignment` | ✓ | `const foo = bar` where both sides are plain identifiers
|
|
213
|
-
| `no-object-assign` | ✓/suggestion |
|
|
214
|
-
| `no-mutable-union-array-param` | ✓ | A function parameter typed as an array of a union (`(string \| number)[]`) accepts a narrower caller array (`number[]`) by covariance; calling `push`/`unshift`/`splice`/`fill`/`copyWithin` on it can then insert a value the caller's own array was never declared to hold. Autofix marks the parameter `readonly`, turning the mutating call into a real compile error to resolve deliberately. Requires no type information. |
|
|
215
|
-
| `prefer-readonly-array-param` | ✓ | A narrower, safely-autofixable sibling of `@typescript-eslint/prefer-readonly-parameter-types` scoped to array/tuple parameter shapes only: fires unconditionally on every non-readonly array or tuple parameter, regardless of whether the function body mutates it, in any parameter position (a plain identifier, a rest parameter, a default-valued parameter, or a constructor parameter property) and any function-like shape (a concrete function/arrow/method, or a declaration-only ambient function, interface method, function type alias, call/construct signature, or abstract/ambient class method). A union containing an array/tuple member is fixed on that member alone. Autofix prepends `readonly ` (or renames `Array<T>` to `ReadonlyArray<T>`), turning any resulting mutation into a real compile error to resolve deliberately. Requires no type information — registered in both `plugin.configs.recommended` and the default (type-checked) export. |
|
|
216
|
-
| `prefer-readonly-object-param` | ✓ | The object-shape sibling of `prefer-readonly-array-param` above, scoped to "flat" object parameters where a shallow fix is provably sufficient: an inline `{ ... }` literal or a reference to a plain named type/interface where every property (and index-signature value, if any) is itself a primitive, a literal/union of primitives, or a callback — with no nested object, array, tuple, Map, Set, class instance, union, intersection, or unconstrained type parameter anywhere in the shape. Autofix wraps the parameter's own type annotation in `Readonly<...>`, which TypeScript's own deep-readonly check accepts as fully sufficient for a shape this flat. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended` — to resolve each property's real type via the checker. |
|
|
217
|
-
| `no-array-isarray-mutation` | |
|
|
218
|
-
| `no-map-instanceof-mutation` | | `Map` is declared as extending `ReadonlyMap`, so `instanceof Map` narrows a parameter or local variable whose real type includes a `ReadonlyMap` — bare, unioned, or reached through a type alias — straight past the readonly guarantee to the full mutable interface; calling `set`/`delete`/`clear` there can mutate a caller's genuinely read-only map. Recognises the direct `if (input instanceof Map)` guard (braced or not), the early-return/early-throw idiom, `&&`, the ternary form, and the else-of-a-negated-test form. No autofix: rewriting the mutating call into a copy-first pattern is not safely mechanical in the presence of aliasing. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
219
|
-
| `no-set-instanceof-mutation` | |
|
|
220
|
-
| `no-enum-number-widening` | | A bare
|
|
221
|
-
| `no-enum-reverse-lookup-widening` | suggestion | Indexing a numeric enum's reverse mapping (`Direction[n]`) with a bare (non-literal) `number`, or with a different enum's member, types as plain `string` for any index, including one outside the enum's actual members, where it genuinely returns `undefined` at runtime — `tsc` does not range-check even a numeric literal index here. When the indexed expression is the init of a variable with an explicit `: string` annotation, a suggestion widens it to `: string \| undefined`, forcing later uses as a bare `string` to surface as real compile errors; every other syntactic position gets a plain report with no fix, and no case gets a full `--fix` autofix. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
222
|
-
| `prefer-numeric-sort-compare` | suggestion | A deliberately narrow addition alongside `@typescript-eslint/require-array-sort-compare` (which already flags any bare `.sort()`/`.toSorted()` except on a plain string array, with no fix): when the array's element type is definitively `number`, a suggestion offers an ascending compare function (`(a, b) => a - b`), since the default comparator sorts lexicographically (`[1, 2, 10].sort()` becomes `[1, 10, 2]`). Not a full autofix — descending order is a real, if less common, alternative intent. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`, since it needs the checker to confirm the array's element type. |
|
|
223
|
-
| `package-json-key-order` | ✓ | Requires `package.json`'s keys to
|
|
224
|
-
| `test-file-kind` | |
|
|
264
|
+
| `barrel-policy` | | **Umbrella rule selecting a whole index-file barrel policy.** One `{ mode }` option covers the four barrel rules below. See [Barrel policy](#barrel-policy). |
|
|
265
|
+
| `no-index-files` | | **Bans any `index.*` file outright** (mode 1). The strictest policy. |
|
|
266
|
+
| `no-non-barrel-index` | | **Only `src/index.ts` may be named `index.*`** — any other `index.ts`/`.js`/etc would be silently selected by a consumer's bare directory import. |
|
|
267
|
+
| `no-non-barrel-reexport` | ✓ | **Re-exports belong only in a barrel.** Catches the split form across two statements (`import { x } from './y'; export { x };` or `export default x;`) which no AST selector alone can match. Autofix deletes the export and the now-pointless import when it was the import's only use. Self-scopes away from any index file. |
|
|
268
|
+
| `no-side-effects-in-index` | | **A barrel may contain only re-export statements** — nothing that could execute at import time. Self-scopes to any index file. |
|
|
269
|
+
| `barrel-direct-siblings-only` | | **A barrel may re-export only from a direct sibling** (`./module`), never a nested path, parent, or bare package specifier (mode 3). |
|
|
270
|
+
| `no-control-flow` | | **Bans `if`/`switch`/loops/the ternary operator outright.** Not part of `recommended` or `barrel` — ordinary code legitimately needs control flow, so this is opt-in, wired via a consumer's own `files` glob for the specific packages that want it (a composition-root package selecting an adapter/strategy by a validated key, say): a lookup table replaces a branch, a declarative array method (`map`/`filter`/`some`/`every`/...) replaces a loop. Requires no type information. |
|
|
271
|
+
| `no-pointless-reassignment` | ✓ | **Flags a `const` alias that adds no transformation** (`const foo = bar` where both sides are plain identifiers). Autofix rewrites every read to the original name and deletes the declaration (including its `export` keyword, when exported). Still reported but deliberately not auto-fixable where collapsing the alias would change meaning: an explicit type annotation (`const exhaustive: never = item` — the annotation is the point), a read where the original name is shadowed, a read as a shorthand object property, more than one declarator in the statement, or a source that is written to anywhere. |
|
|
272
|
+
| `no-object-assign` | ✓/suggestion | **`Object.assign` skips the type-checking object spread gets** — it doesn't check a source object's properties against the target's declared types. A fresh object-literal target autofixes to `{ ...target, ...source }`; mutating an existing reassignable binding offers a suggestion only (changes the object's identity); a `const` binding or a non-statement call site gets a plain report with no fix. |
|
|
273
|
+
| `no-mutable-union-array-param` | ✓ | **A union-typed array parameter can be mutated with a value the caller's narrower array never declared.** A function parameter typed as an array of a union (`(string \| number)[]`) accepts a narrower caller array (`number[]`) by covariance; calling `push`/`unshift`/`splice`/`fill`/`copyWithin` on it can then insert a value the caller's own array was never declared to hold. Autofix marks the parameter `readonly`, turning the mutating call into a real compile error to resolve deliberately. Requires no type information. |
|
|
274
|
+
| `prefer-readonly-array-param` | ✓ | **Every non-readonly array/tuple parameter should be `readonly`.** A narrower, safely-autofixable sibling of `@typescript-eslint/prefer-readonly-parameter-types` scoped to array/tuple parameter shapes only: fires unconditionally on every non-readonly array or tuple parameter, regardless of whether the function body mutates it, in any parameter position (a plain identifier, a rest parameter, a default-valued parameter, or a constructor parameter property) and any function-like shape (a concrete function/arrow/method, or a declaration-only ambient function, interface method, function type alias, call/construct signature, or abstract/ambient class method). A union containing an array/tuple member is fixed on that member alone. Autofix prepends `readonly ` (or renames `Array<T>` to `ReadonlyArray<T>`), turning any resulting mutation into a real compile error to resolve deliberately. Requires no type information — registered in both `plugin.configs.recommended` and the default (type-checked) export. |
|
|
275
|
+
| `prefer-readonly-object-param` | ✓ | **A flat object parameter's type should be wrapped in `Readonly<...>`.** The object-shape sibling of `prefer-readonly-array-param` above, scoped to "flat" object parameters where a shallow fix is provably sufficient: an inline `{ ... }` literal or a reference to a plain named type/interface where every property (and index-signature value, if any) is itself a primitive, a literal/union of primitives, or a callback — with no nested object, array, tuple, Map, Set, class instance, union, intersection, or unconstrained type parameter anywhere in the shape. Autofix wraps the parameter's own type annotation in `Readonly<...>`, which TypeScript's own deep-readonly check accepts as fully sufficient for a shape this flat. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended` — to resolve each property's real type via the checker. |
|
|
276
|
+
| `no-array-isarray-mutation` | | **`Array.isArray` narrowing discards a `readonly` array's own guarantee.** Its type declaration narrows to plain `any[]`, discarding the `readonly` guarantee of any array type in the narrowed parameter's or local variable's real type — a bare `readonly T[]`, a `ReadonlyArray<T>`, one behind a type alias, or one alongside other union members — inside the guarded branch; calling `push`/`unshift`/`splice`/`fill`/`copyWithin` there can mutate a caller's genuinely readonly array. Recognises the direct `if (Array.isArray(x))` guard (braced or not), the early-return/early-throw idiom, `&&`, the ternary form, and the else-of-a-negated-test form. No autofix: re-adding `readonly` is a no-op (the guard already discarded it) and rewriting the mutating call into a copy-first pattern is not safely mechanical in the presence of aliasing. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended` — specifically to see through a type alias and to catch a bare, non-union readonly array parameter or local variable, neither visible from its own syntax alone. |
|
|
277
|
+
| `no-map-instanceof-mutation` | | **`instanceof Map` narrowing discards a `ReadonlyMap`'s own guarantee.** `Map` is declared as extending `ReadonlyMap`, so `instanceof Map` narrows a parameter or local variable whose real type includes a `ReadonlyMap` — bare, unioned, or reached through a type alias — straight past the readonly guarantee to the full mutable interface; calling `set`/`delete`/`clear` there can mutate a caller's genuinely read-only map. Recognises the direct `if (input instanceof Map)` guard (braced or not), the early-return/early-throw idiom, `&&`, the ternary form, and the else-of-a-negated-test form. No autofix: rewriting the mutating call into a copy-first pattern is not safely mechanical in the presence of aliasing. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
278
|
+
| `no-set-instanceof-mutation` | | **`instanceof Set` narrowing discards a `ReadonlySet`'s own guarantee**, straight to the fully mutable `Set` interface, with no way to preserve the read-only guarantee through the narrowing; calling `add`/`delete`/`clear` there can mutate a caller's genuinely read-only set. Recognises the same guard idioms as `no-map-instanceof-mutation` above. No autofix, for the same aliasing reason. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
279
|
+
| `no-enum-number-widening` | | **A bare `number` is accepted anywhere a numeric enum is expected**, without checking it is actually one of the enum's members — only a numeric *literal* gets range-checked by `tsc`. No autofix: the only provably safe fix is a genuine runtime membership check against the enum's own values, which is a behavioural choice a mechanical fix cannot responsibly make. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
280
|
+
| `no-enum-reverse-lookup-widening` | suggestion | **A numeric enum's reverse lookup can silently type as `string` for an out-of-range index.** Indexing a numeric enum's reverse mapping (`Direction[n]`) with a bare (non-literal) `number`, or with a different enum's member, types as plain `string` for any index, including one outside the enum's actual members, where it genuinely returns `undefined` at runtime — `tsc` does not range-check even a numeric literal index here. When the indexed expression is the init of a variable with an explicit `: string` annotation, a suggestion widens it to `: string \| undefined`, forcing later uses as a bare `string` to surface as real compile errors; every other syntactic position gets a plain report with no fix, and no case gets a full `--fix` autofix. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
281
|
+
| `prefer-numeric-sort-compare` | suggestion | **`.sort()` on a number array sorts lexicographically by default.** A deliberately narrow addition alongside `@typescript-eslint/require-array-sort-compare` (which already flags any bare `.sort()`/`.toSorted()` except on a plain string array, with no fix): when the array's element type is definitively `number`, a suggestion offers an ascending compare function (`(a, b) => a - b`), since the default comparator sorts lexicographically (`[1, 2, 10].sort()` becomes `[1, 10, 2]`). Not a full autofix — descending order is a real, if less common, alternative intent. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`, since it needs the checker to confirm the array's element type. |
|
|
282
|
+
| `package-json-key-order` | ✓ | **Requires `package.json`'s keys to match `syncpack format`'s order.** See [Optional package.json key ordering](#optional-packagejson-key-ordering) — opt-in via `exadevConfig({ packageJsonKeyOrder: true })`, not part of `recommended`/`barrel`. A JSON-language rule (`@eslint/json`'s `json/json`), not a TSESLint one — needs no type information and doesn't apply to any `.ts`/`.js` file. |
|
|
283
|
+
| `test-file-kind` | | **A test file's name must declare its own test kind.** A filename suffix immediately before `.test`/`.spec` (e.g. `foo.unit.test.ts`), one of a configurable `{ kinds }` set (default: `unit`, `integration`, `e2e`). A naming-discipline rule, not a content classifier — it checks only the filename, never what the file actually tests. Self-scoped to real test/spec files (`context.filename`), so it never misfires when applied unscoped and never relies on a consumer's own `files` config. Requires no type information. |
|
|
225
284
|
|
|
226
285
|
## Barrel policy
|
|
227
286
|
|
|
@@ -234,11 +293,20 @@ Bundled into `exadevConfig()`'s default output the same way React/Next.js auto-d
|
|
|
234
293
|
| `'single'` (`plugin.configs.barrel`'s explicit choice) | exactly `src/index.ts` | only re-exports | anywhere |
|
|
235
294
|
| `'siblings'` | any `index.ts` | only re-exports | a direct sibling only (`./module`) |
|
|
236
295
|
|
|
237
|
-
`'auto'
|
|
296
|
+
Notes on `'auto'`:
|
|
238
297
|
|
|
239
|
-
|
|
298
|
+
- It only ever resolves to `banned` or `single` — there's no single-signal auto-equivalent for `siblings` (which package.json field would suggest "any index file, not just the entry point"?), so a project wanting that policy states it explicitly, e.g. to permit any index file as a barrel rather than just `src/index.ts` (flat-config later blocks override earlier rule settings):
|
|
299
|
+
```ts
|
|
300
|
+
...exadev,
|
|
301
|
+
{ rules: { 'exadev/barrel-policy': ['error', { mode: 'siblings' }] } }, // any index file may be a barrel, not just src/index.ts
|
|
302
|
+
```
|
|
303
|
+
- `private: true` in `package.json` is not consulted by the detection: a pnpm workspace package is routinely both `private` and a genuine import target for sibling packages via `exports`, so `private` says nothing about whether a barrel is warranted.
|
|
240
304
|
|
|
241
|
-
|
|
305
|
+
In every mode, re-exports are banned outside a permitted barrel, and a permitted barrel may contain only re-export statements. The umbrella composes the identical predicates the standalone rules use (shared in [`src/rules/barrel-helpers.ts`](src/rules/barrel-helpers.ts)). It is non-fixable — the autofix lives on `no-non-barrel-reexport`.
|
|
306
|
+
|
|
307
|
+
## Development
|
|
308
|
+
|
|
309
|
+
### Build, test, and lint
|
|
242
310
|
|
|
243
311
|
```sh
|
|
244
312
|
pnpm install # requires Node >=20 and pnpm 11.6.0 (pinned via packageManager)
|
|
@@ -248,53 +316,60 @@ pnpm test
|
|
|
248
316
|
pnpm build
|
|
249
317
|
```
|
|
250
318
|
|
|
251
|
-
Each rule has a co-located `*.unit.test.ts` exercising it with ESLint's `RuleTester` under Vitest. `vitest.setup.ts` wires `RuleTester.describe`/`.it`/`.itOnly` to Vitest's `describe`/`it` explicitly (no `test.globals`). Each test uses typescript-eslint's parser for TypeScript-only fixtures; none need type information.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
`pnpm
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
`
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
- `
|
|
319
|
+
- Each rule has a co-located `*.unit.test.ts` exercising it with ESLint's `RuleTester` under Vitest. [`vitest.setup.ts`](vitest.setup.ts) wires `RuleTester.describe`/`.it`/`.itOnly` to Vitest's `describe`/`it` explicitly (no `test.globals`). Each test uses typescript-eslint's parser for TypeScript-only fixtures; none need type information.
|
|
320
|
+
- Every test file's own name declares its kind via a filename suffix immediately before `.test`/`.spec` — `.unit`, `.integration`, or `.e2e` by default (`exadev/test-file-kind`, part of `recommended`; see [Rules](#rules)) — so a file's test kind is always visible from its name alone, without opening it, and downstream tooling (e.g. a Vitest project split by test kind) can select by filename glob rather than by convention nobody enforces. This package's own tests are exclusively `.unit.test.ts` today (a `.internal.unit.test.ts` variant exists for a handful of files that also test non-exported internals directly, `internal` just being an ordinary extra name segment — see [`no-mutable-union-array-param.internal.unit.test.ts`](src/rules/no-mutable-union-array-param.internal.unit.test.ts)).
|
|
321
|
+
- `pnpm test` always measures coverage (`@vitest/coverage-v8`), scoped to `src/**/*.ts` excluding `*.test.ts`. Text output in terminal; `html`/`lcov` in `coverage/` (gitignored alongside `.eslintcache` and `dist/`).
|
|
322
|
+
- The `lint`/`typecheck`/`test`/`build` npm scripts wrap turbo tasks named `_lint`/`_typecheck`/`_test`/`_build` — run `pnpm build`, not `turbo run build`.
|
|
323
|
+
- `pnpm build` runs `tsdown` from [`src/index.ts`](src/index.ts), bundling the whole module graph into ESM + CJS + declarations. `prepublishOnly` re-runs lint, typecheck, `test`, `tsdown`, `publint`, and `attw --pack`.
|
|
324
|
+
|
|
325
|
+
### Architecture
|
|
326
|
+
|
|
327
|
+
<details>
|
|
328
|
+
<summary>Expand for implementation internals (not needed for ordinary consumption)</summary>
|
|
329
|
+
|
|
330
|
+
- [`src/plugin.ts`](src/plugin.ts) builds a `TSESLint.FlatConfig.Plugin` combining [`src/rules/`](src/rules) into a flat `rules` map.
|
|
331
|
+
- That's `@typescript-eslint/utils`'s own type, not ESLint's own `ESLint.Plugin` — the latter can't hold a rule built with `ESLintUtils.RuleCreator`.
|
|
332
|
+
- `configs.recommended`, `.barrel`, `.react`, and `.nextjs` are getters in the object literal, since each references the fully-built `plugin` (`plugins: { exadev: plugin }`), which a plain property initializer can't do mid-construction.
|
|
333
|
+
- `recommended` ships `barrel-policy` at `mode: 'banned'`; `barrel` at `mode: 'single'`; `.react`/`.nextjs` call `buildReactConfig`/`buildNextjsConfig` with `enabled: true` (see below).
|
|
334
|
+
- [`src/config-types.ts`](src/config-types.ts) holds `ConfigValue`/`ConfigArrayValue` (`ConfigArrayValue = Extract<ConfigValue, unknown[]>`, the array-only member of ESLint's own config-value union), shared by every file below rather than redefined per file.
|
|
335
|
+
- *Why:* annotating a config array with the wider `ConfigValue` union directly broke `...exadev` with `TS2488` ("must have a Symbol.iterator method").
|
|
336
|
+
- [`src/optional-plugin.ts`](src/optional-plugin.ts) is the lazy-resolution helper behind React/Next.js support.
|
|
337
|
+
- `tryRequire` wraps `createRequire(import.meta.url)` in try/catch, returning `unknown` (never a cast) so every call site narrows explicitly before use.
|
|
338
|
+
- `readFlatConfig` walks a property path through that `unknown` value via a real type guard, normalizing a stray legacy top-level `parserOptions` key into `languageOptions.parserOptions` along the way.
|
|
339
|
+
- Confirmed necessary: `eslint-plugin-jsx-a11y`'s own `configs.recommended` export carries exactly this legacy shape, which flat config's schema rejects outright rather than ignores.
|
|
340
|
+
- [`src/react.ts`](src/react.ts)/[`src/nextjs.ts`](src/nextjs.ts) each export a `build*Config(options)` function: resolve the relevant optional peer(s) via `tryRequire`, extract their real flat config via `readFlatConfig`, and return an array of 0-or-more config blocks.
|
|
341
|
+
- `[]` if unresolvable and not explicitly forced on; a thrown `Error` if explicitly forced on (`enabled: true`) and still unresolvable.
|
|
342
|
+
- `react.ts`'s blocks are scoped to `files: ['**/*.jsx', '**/*.tsx']`; `nextjs.ts`'s is not (see [Optional React and Next.js support](#optional-react-and-nextjs-support) for why).
|
|
343
|
+
- [`src/create-config.ts`](src/create-config.ts) is config assembly's single source of truth.
|
|
344
|
+
- `exadevConfig(options, ...userConfigs)` concatenates `recommendedTypeChecked` with both builders' output (each fed the matching tri-state option) and any trailing user configs.
|
|
345
|
+
- `defaultConfig` is `exadevConfig()` evaluated once, eagerly, at module load.
|
|
346
|
+
- [`src/index.ts`](src/index.ts) is the entry point, still a pure re-export barrel: `export { defaultConfig as default, exadevConfig } from './create-config'; export { default as plugin } from './plugin';`.
|
|
347
|
+
- Required by `no-side-effects-in-index`/`no-non-barrel-reexport`, both of which assume this file contains nothing but `export ... from ...`.
|
|
348
|
+
- All exports share one root module, so importing `{ plugin }` alone still resolves `typescript-eslint` via the sibling re-export — an accepted trade-off (an earlier separate-subpath split proved more awkward in practice).
|
|
349
|
+
- React/Next.js support never adds to this cost: none of the four optional packages are ever statically imported, only passed as a runtime string to `createRequire`'s resolver, so their absence never affects module evaluation for a consumer who doesn't use them.
|
|
350
|
+
- [`pnpm-workspace.yaml`](pnpm-workspace.yaml) declares an empty `packages: []` — not a real workspace, just giving turbo a root for local task caching.
|
|
351
|
+
|
|
352
|
+
</details>
|
|
353
|
+
|
|
354
|
+
### Conventions
|
|
355
|
+
|
|
356
|
+
- [`eslint.config.ts`](eslint.config.ts) dogfoods this package's own factory export on itself (`import { exadevConfig } from './src/index'`), spreading `exadevConfig({ react: false, nextjs: false })` — forced off explicitly, not the plain auto-detecting default, since `eslint-plugin-react`/`@next/eslint-plugin-next` are real devDependencies of *this* repo (needed to test [`src/react.ts`](src/react.ts)/[`src/nextjs.ts`](src/nextjs.ts)'s own "package is resolvable" branch) even though this repo is neither a React nor a Next.js project. `no-side-effects-in-index` and `no-non-barrel-reexport` self-scope to [`src/index.ts`](src/index.ts) internally, so no `files`/`ignores` wiring is needed here. Plugin construction lives in [`src/plugin.ts`](src/plugin.ts) specifically so `src/index.ts` stays a pure re-export point.
|
|
357
|
+
- [`tsconfig.json`](tsconfig.json) enables `verbatimModuleSyntax` (`import type`/`export type` required for type-only imports — also enforced by `consistent-type-imports`) and `noUncheckedIndexedAccess` (narrow indexed access before use rather than asserting).
|
|
358
|
+
- Conventional commits are enforced by commitlint, restricted to the type-enum defined once in [`release.config.ts`](release.config.ts)'s `commitTypes` — both commitlint and semantic-release derive from that single list.
|
|
359
|
+
|
|
360
|
+
### Gotchas and quirks
|
|
361
|
+
|
|
362
|
+
- [`.attw.json`](.attw.json) ignores `false-export-default`: tsdown/rolldown's CJS output for this plugin's sole default export doesn't emit the `export =` form `arethetypeswrong` wants under legacy `node10` resolution. The modes ESLint flat config uses (`node16`, `bundler`) are unaffected, so the rule is suppressed rather than changing the default-export shape.
|
|
363
|
+
- [`src/index.ts`](src/index.ts) mixing a default export with a named one triggers rolldown's `MIXED_EXPORTS` warning: a raw CommonJS `require()` would see the raw exports object instead of the default. ESM `import` (the actual consumer path) resolves both correctly; `attw --pack` and `publint` report no problems, so the warning is accepted (see [`tsdown.config.ts`](tsdown.config.ts)).
|
|
289
364
|
- Husky hooks: `pre-commit` runs lint-staged (`eslint --fix` on staged `*.ts`), `commit-msg` runs commitlint, `pre-push` runs typecheck + test + build.
|
|
290
365
|
- The CI release job sets `HUSKY=0` (commit-msg hook skips the automated release commit) and blanks `NPM_TOKEN`/`NODE_AUTH_TOKEN` explicitly so an inherited token can't win over OIDC trusted publishing.
|
|
291
|
-
- A consumer who already has `eslint-plugin-react`/`@next/eslint-plugin-next` resolvable for unrelated reasons (e.g. hoisted in a monorepo) and writes `.jsx`/`.tsx` files may see new rule activity the moment they upgrade to a version of this package that ships React/Next.js support — with zero action on their part.
|
|
366
|
+
- A consumer who already has `eslint-plugin-react`/`@next/eslint-plugin-next` resolvable for unrelated reasons (e.g. hoisted in a monorepo) and writes `.jsx`/`.tsx` files may see new rule activity the moment they upgrade to a version of this package that ships React/Next.js support — with zero action on their part. See the compatibility note under [Optional React and Next.js support](#optional-react-and-nextjs-support).
|
|
292
367
|
|
|
293
|
-
|
|
368
|
+
### Contributing
|
|
294
369
|
|
|
295
370
|
Conventional commits are enforced by a husky `commit-msg` hook and re-checked in CI. CI runs commitlint, lint, and typecheck+test+build+attw on every push and pull request; the release job runs only on push to `main`, after all pass.
|
|
296
371
|
|
|
297
|
-
|
|
372
|
+
### Release
|
|
298
373
|
|
|
299
374
|
Conventional commits drive [semantic-release](https://semantic-release.gitbook.io/semantic-release) on every push to `main`: version bump, `CHANGELOG.md`, GitHub Release, and npm publish via OIDC (no stored token). A second CI job republishes the identical build under the unscoped alias `exadev-eslint-config`.
|
|
300
375
|
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED