eslint-config-agent 3.0.4 โ 3.0.5
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/CHANGELOG.md +112 -0
- package/README.md +577 -26
- package/configs/base-plugins.js +32 -0
- package/configs/config-files.js +19 -3
- package/configs/examples.js +12 -1
- package/configs/javascript.js +12 -4
- package/configs/overrides.js +1 -1
- package/configs/test-files.js +40 -1
- package/configs/tsx.js +10 -0
- package/configs/typescript.js +21 -2
- package/exports/ddd.d.ts +10 -0
- package/exports/ddd.js +1 -3
- package/exports/incremental.d.ts +11 -0
- package/exports/incremental.js +39 -0
- package/exports/recommended-incremental.d.ts +11 -0
- package/exports/recommended-incremental.js +46 -0
- package/exports/recommended.d.ts +11 -0
- package/exports/recommended.js +81 -0
- package/exports/to-warnings.d.ts +20 -0
- package/exports/to-warnings.js +41 -0
- package/index.d.ts +18 -0
- package/index.js +594 -7
- package/package.json +44 -16
- package/plugins/index.js +6 -0
- package/rules/index.js +20 -22
- package/rules/no-inline-union-types/index.js +8 -6
- package/rules/no-process-env-properties/index.js +4 -4
- package/rules/no-record-literal-types/index.js +3 -3
- package/rules/plugin/import/index.js +41 -0
- package/rules/plugin/index.js +0 -2
- package/rules/plugin/n/index.js +2 -2
- package/rules/plugin/n/no-process-env/index.js +4 -4
- package/rules/plugin/react/index.js +69 -0
- package/rules/plugin/typescript-eslint/index.js +204 -0
- package/rules/require-spec-file-tsx/README.md +57 -0
- package/rules/require-spec-file-tsx/index.js +109 -0
- package/rules/plugin/jsx-a11y/index.js +0 -3
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ In an age where AI coding assistants and code generators are increasingly common
|
|
|
19
19
|
- **๐ Explicit Over Clever**: Forces clear, readable patterns instead of "clever" but obscure code
|
|
20
20
|
- **๐ Production-Ready**: Battle-tested configuration used in production environments
|
|
21
21
|
- **๐ Type Safety First**: Enforces explicit null/undefined checks instead of optional chaining
|
|
22
|
-
- **โก Modern ESLint**: Built for ESLint 9
|
|
22
|
+
- **โก Modern ESLint**: Built for ESLint 9 with flat configuration format
|
|
23
23
|
- **๐ฏ Framework Agnostic**: Works seamlessly with React, Preact, and pure TypeScript
|
|
24
24
|
- **๐ฆ Zero Config**: Works out of the box with sensible defaults
|
|
25
25
|
- **๐ง Extensible**: Easy to customize and extend for your specific needs
|
|
@@ -46,10 +46,10 @@ This configuration enforces patterns that:
|
|
|
46
46
|
|
|
47
47
|
- **๐ ๏ธ TypeScript First**: Full TypeScript ESLint integration with advanced type checking
|
|
48
48
|
- **โ๏ธ React & Preact**: Complete support for both React and Preact projects
|
|
49
|
-
- **๐ Strict Standards**: Enforces explicit null/undefined checks, disallows optional chaining and nullish coalescing for better code clarity
|
|
49
|
+
- **๐ Strict Standards**: Enforces explicit null/undefined checks, requires strict equality (`===`/`!==`), and disallows optional chaining and nullish coalescing for better code clarity
|
|
50
50
|
- **๐ Code Quality**: Function length limits (100 lines), trailing space detection, and consistent formatting
|
|
51
51
|
- **๐งช DDD by Default**: Requires spec files for all source files to ensure comprehensive test coverage
|
|
52
|
-
- **๐ Modern ESLint**: Uses the latest flat configuration format (ESLint 9
|
|
52
|
+
- **๐ Modern ESLint**: Uses the latest flat configuration format (ESLint 9)
|
|
53
53
|
- **๐ Comprehensive Testing**: 12+ test categories with automated validation
|
|
54
54
|
- **๐ CI/CD Ready**: Zero-warning configuration for production builds
|
|
55
55
|
|
|
@@ -57,9 +57,44 @@ This configuration enforces patterns that:
|
|
|
57
57
|
|
|
58
58
|
### Prerequisites
|
|
59
59
|
|
|
60
|
-
- **Node.js**:
|
|
61
|
-
- **ESLint**: 9.x
|
|
62
|
-
- **TypeScript**: 4.
|
|
60
|
+
- **Node.js**: 20.x or higher
|
|
61
|
+
- **ESLint**: 9.x (see [ESLint version compatibility](#eslint-version-compatibility) below)
|
|
62
|
+
- **TypeScript**: 4.8.4 or higher (optional, only for TypeScript projects โ see [TypeScript version compatibility](#typescript-version-compatibility) below)
|
|
63
|
+
|
|
64
|
+
#### ESLint version compatibility
|
|
65
|
+
|
|
66
|
+
This package targets **ESLint 9.x** and is **not yet compatible with ESLint 10**.
|
|
67
|
+
|
|
68
|
+
Several of the bundled plugins still call APIs that ESLint 10 removed (for
|
|
69
|
+
example `context.getFilename()`), so running the config under ESLint 10 fails at
|
|
70
|
+
lint time with errors such as:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
TypeError: Error while loading rule 'default/no-localhost':
|
|
74
|
+
context.getFilename is not a function
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If you are on ESLint 10, pin ESLint to the latest 9.x release until ESLint 10
|
|
78
|
+
support lands. Progress is tracked in the project issues.
|
|
79
|
+
|
|
80
|
+
#### TypeScript version compatibility
|
|
81
|
+
|
|
82
|
+
For TypeScript projects, the bundled `@typescript-eslint` parser supports
|
|
83
|
+
**TypeScript 4.8.4 or higher**. TypeScript itself is _not_ bundled โ the parser
|
|
84
|
+
loads whatever `typescript` your project already has installed, so it is declared
|
|
85
|
+
as an optional `peerDependency`.
|
|
86
|
+
|
|
87
|
+
If your project pins an older TypeScript (for example a legacy `4.2.x`), linting
|
|
88
|
+
fails up front with an opaque parser crash on **every** file rather than a clear
|
|
89
|
+
version message:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
Parsing error: ts9__default.default.isTokenKind is not a function
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If you hit this, upgrade your project's `typescript` to `>=4.8.4` (any recent
|
|
96
|
+
4.9 / 5.x release works). Installing this package will also surface a peer
|
|
97
|
+
dependency warning when the resolved TypeScript is too old.
|
|
63
98
|
|
|
64
99
|
### Install the package
|
|
65
100
|
|
|
@@ -74,27 +109,27 @@ pnpm add -D eslint-config-agent
|
|
|
74
109
|
yarn add -D eslint-config-agent
|
|
75
110
|
```
|
|
76
111
|
|
|
77
|
-
###
|
|
112
|
+
### Dependencies are bundled
|
|
78
113
|
|
|
79
|
-
|
|
114
|
+
There is **no separate peer-dependency step**. ESLint and every plugin this
|
|
115
|
+
configuration uses (`@typescript-eslint/*`, `typescript-eslint`,
|
|
116
|
+
`eslint-plugin-react`, `eslint-plugin-react-hooks`, `eslint-plugin-import`,
|
|
117
|
+
`eslint-plugin-preact`, `globals`, and the rest) ship as regular dependencies
|
|
118
|
+
of `eslint-config-agent`. Installing the package pulls them in automatically,
|
|
119
|
+
so the config works out of the box.
|
|
80
120
|
|
|
81
121
|
```bash
|
|
82
|
-
#
|
|
83
|
-
npm install --save-dev eslint
|
|
84
|
-
|
|
85
|
-
# For Preact projects (optional)
|
|
86
|
-
npm install --save-dev eslint-plugin-preact@^0.1.0
|
|
122
|
+
# That's it โ the single install above is all you need
|
|
123
|
+
npm install --save-dev eslint-config-agent
|
|
87
124
|
```
|
|
88
125
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
#
|
|
96
|
-
pnpm install --save-dev $(pnpm info eslint-config-agent peerDependencies --json | jq -r 'to_entries[] | "\(.key)@\(.value)"')
|
|
97
|
-
```
|
|
126
|
+
> **Note:** If your project already depends on ESLint or any of these plugins,
|
|
127
|
+
> your package manager will deduplicate them against the versions bundled here.
|
|
128
|
+
>
|
|
129
|
+
> The one exception is `typescript`: it is an _optional_ peer dependency (the
|
|
130
|
+
> parser uses your project's own TypeScript), so TypeScript projects must have
|
|
131
|
+
> `typescript >=4.8.4` installed. See
|
|
132
|
+
> [TypeScript version compatibility](#typescript-version-compatibility) above.
|
|
98
133
|
|
|
99
134
|
## Usage
|
|
100
135
|
|
|
@@ -108,6 +143,179 @@ import config from 'eslint-config-agent'
|
|
|
108
143
|
export default config
|
|
109
144
|
```
|
|
110
145
|
|
|
146
|
+
### Available presets (entry points)
|
|
147
|
+
|
|
148
|
+
The package ships several entry points via its `package.json#exports` map.
|
|
149
|
+
Import whichever one matches how much strictness your project is ready for:
|
|
150
|
+
|
|
151
|
+
| Import specifier | Strictness | When to use |
|
|
152
|
+
| --------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
153
|
+
| `eslint-config-agent` | Strict | The full, opinionated config. Best for greenfield projects that adopt every convention. |
|
|
154
|
+
| `eslint-config-agent/recommended` | Relaxed | The strict config with the most divisive rules pre-disabled. Best for incremental adoption. |
|
|
155
|
+
| `eslint-config-agent/incremental` | Warn-level | The full ruleset with every error downgraded to a warning, so CI stays green while the whole backlog is still reported. |
|
|
156
|
+
| `eslint-config-agent/recommended-incremental` | Relaxed + warn | The gentlest on-ramp: the divisive rules disabled _and_ everything else downgraded to a warning. Best for large legacy codebases. |
|
|
157
|
+
| `eslint-config-agent/ddd` | Strict | Backward-compatible alias of the default export (the DDD `require-spec-file` rules now ship in the base config). Equivalent to `eslint-config-agent`. |
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
// Strict (default)
|
|
161
|
+
import config from 'eslint-config-agent'
|
|
162
|
+
|
|
163
|
+
// Relaxed, for incremental adoption
|
|
164
|
+
import recommended from 'eslint-config-agent/recommended'
|
|
165
|
+
|
|
166
|
+
// Backward-compatible alias of the default export
|
|
167
|
+
import ddd from 'eslint-config-agent/ddd'
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Each export is a flat-config array, so you can spread it and append your own
|
|
171
|
+
override layers (see [Advanced Configuration](#advanced-configuration)).
|
|
172
|
+
|
|
173
|
+
### Recommended (relaxed) preset
|
|
174
|
+
|
|
175
|
+
The default export is intentionally strict โ it assumes a greenfield project
|
|
176
|
+
that follows every convention from day one. Existing codebases often can't, and
|
|
177
|
+
end up copy-pasting the same block of rule overrides just to get the config to
|
|
178
|
+
load without a wall of errors.
|
|
179
|
+
|
|
180
|
+
The `eslint-config-agent/recommended` preset bundles those common overrides for
|
|
181
|
+
you. It keeps the core quality rules but disables the most opinionated ones
|
|
182
|
+
(`ddd/require-spec-file` and its `.tsx`/`.jsx` counterpart
|
|
183
|
+
`custom/require-spec-file-tsx`, so React/Preact components are not forced to
|
|
184
|
+
ship a spec file up front either, `single-export`, `required-exports`, the custom
|
|
185
|
+
`error/*` rules, `jsdoc/require-jsdoc` (so existing code is not forced to
|
|
186
|
+
document every exported function and class up front โ the jsdoc _content_ rules
|
|
187
|
+
stay on, so any JSDoc you do write is still validated),
|
|
188
|
+
`default/no-default-params`, `@typescript-eslint/consistent-type-definitions`,
|
|
189
|
+
`jsx-classname/require-classname` (which otherwise errors on Tailwind-only
|
|
190
|
+
`className`s), and the `no-restricted-syntax` bans on optional chaining /
|
|
191
|
+
nullish coalescing / type assertions), so idiomatic TypeScript and
|
|
192
|
+
React/Preact + Tailwind code passes during incremental adoption.
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
import recommended from 'eslint-config-agent/recommended'
|
|
196
|
+
|
|
197
|
+
export default recommended
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Re-enable any individual rule by appending your own override layer:
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
import recommended from 'eslint-config-agent/recommended'
|
|
204
|
+
|
|
205
|
+
export default [
|
|
206
|
+
...recommended,
|
|
207
|
+
{
|
|
208
|
+
rules: {
|
|
209
|
+
// Opt back into a stricter rule once your code is ready for it
|
|
210
|
+
'ddd/require-spec-file': 'warn',
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Incremental (warn-level) preset
|
|
217
|
+
|
|
218
|
+
The `recommended` preset above _turns rules off_. When you instead want to keep
|
|
219
|
+
**every** rule reporting but stop them from failing CI โ so you can see the full
|
|
220
|
+
backlog and burn it down gradually โ use the `incremental` preset. It is the
|
|
221
|
+
full `eslint-config-agent` ruleset with every error-level rule downgraded to a
|
|
222
|
+
warning, so `eslint` exits `0` and `pnpm lint` stays green while still surfacing
|
|
223
|
+
everything:
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
import incremental from 'eslint-config-agent/incremental'
|
|
227
|
+
|
|
228
|
+
export default incremental
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
This replaces the `config.map(toWarnings)` helper that adopting projects used to
|
|
232
|
+
copy-paste by hand. To enforce a rule as a hard error before the rest of the
|
|
233
|
+
backlog is cleared, append your own override layer โ it wins over the warned
|
|
234
|
+
defaults:
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
import incremental from 'eslint-config-agent/incremental'
|
|
238
|
+
|
|
239
|
+
export default [
|
|
240
|
+
...incremental,
|
|
241
|
+
// Rules you are ready to enforce as hard errors today:
|
|
242
|
+
{
|
|
243
|
+
rules: {
|
|
244
|
+
eqeqeq: ['error', 'always'],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
]
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Keep your CI lint step at `eslint .` during migration; switch it to
|
|
251
|
+
`eslint . --max-warnings 0` once the warnings are cleared.
|
|
252
|
+
|
|
253
|
+
#### The `toWarnings` helper
|
|
254
|
+
|
|
255
|
+
The `incremental` preset warn-levels the **whole** ruleset. When you instead
|
|
256
|
+
want to compose your own flat config โ warn-level the shared ruleset but keep a
|
|
257
|
+
handful of rules as hard errors from day one โ import the same
|
|
258
|
+
`toWarnings` severity-downgrade helper the incremental presets use internally,
|
|
259
|
+
instead of copy-pasting it:
|
|
260
|
+
|
|
261
|
+
```javascript
|
|
262
|
+
import config from 'eslint-config-agent'
|
|
263
|
+
import { toWarnings } from 'eslint-config-agent/to-warnings'
|
|
264
|
+
|
|
265
|
+
export default [
|
|
266
|
+
...config.map(toWarnings),
|
|
267
|
+
// Rules you are ready to enforce as hard errors today:
|
|
268
|
+
{
|
|
269
|
+
rules: {
|
|
270
|
+
eqeqeq: ['error', 'always'],
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
]
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`toWarnings` takes a single flat-config block and returns it with every
|
|
277
|
+
error-level rule downgraded to a warning. Blocks without a `rules` object are
|
|
278
|
+
returned untouched, and `off`/`warn` rules are left exactly as they are.
|
|
279
|
+
|
|
280
|
+
### Recommended + incremental (relaxed, warn-level) preset
|
|
281
|
+
|
|
282
|
+
`recommended` and `incremental` each solve half of the first-run problem on an
|
|
283
|
+
existing codebase: `recommended` turns the most divisive rules **off** but keeps
|
|
284
|
+
everything else at **error** level (a real backlog still fails CI), while
|
|
285
|
+
`incremental` downgrades **everything** to a **warning** but keeps the divisive
|
|
286
|
+
rules firing as a wall of warnings on idiomatic TypeScript and
|
|
287
|
+
React/Preact + Tailwind code.
|
|
288
|
+
|
|
289
|
+
The `recommended-incremental` preset combines both โ the divisive rules disabled
|
|
290
|
+
_and_ every surviving rule downgraded to a warning. It is the gentlest on-ramp
|
|
291
|
+
for a large legacy codebase: `eslint` exits `0`, the noisiest rules are silent,
|
|
292
|
+
and the remaining quality rules surface as warnings you can burn down before
|
|
293
|
+
tightening back up:
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
import recommendedIncremental from 'eslint-config-agent/recommended-incremental'
|
|
297
|
+
|
|
298
|
+
export default recommendedIncremental
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
As with the other presets, append your own override layer to enforce a rule as a
|
|
302
|
+
hard error before the rest of the backlog is cleared โ it wins over the warned
|
|
303
|
+
defaults:
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
import recommendedIncremental from 'eslint-config-agent/recommended-incremental'
|
|
307
|
+
|
|
308
|
+
export default [
|
|
309
|
+
...recommendedIncremental,
|
|
310
|
+
// Rules you are ready to enforce as hard errors today:
|
|
311
|
+
{
|
|
312
|
+
rules: {
|
|
313
|
+
eqeqeq: ['error', 'always'],
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
]
|
|
317
|
+
```
|
|
318
|
+
|
|
111
319
|
### Advanced Configuration
|
|
112
320
|
|
|
113
321
|
#### Extending with Custom Rules
|
|
@@ -209,6 +417,177 @@ This ESLint configuration prioritizes **explicit code** over convenient shortcut
|
|
|
209
417
|
- **๐ Self-Documenting**: Code that explains its intent without extensive comments
|
|
210
418
|
- **๐ ๏ธ Maintainable**: Patterns that remain clear even as the codebase grows
|
|
211
419
|
|
|
420
|
+
### Control Flow & Readability
|
|
421
|
+
|
|
422
|
+
- **`no-else-return`** (`allowElseIf: false`): Forbids an `else`/`else if` block
|
|
423
|
+
when the preceding `if` already exits via `return`. Once the `if` branch
|
|
424
|
+
returns, the `else` only adds nesting that hides the real control flow.
|
|
425
|
+
Removing it flattens the code into guard-clause style โ the same goal as the
|
|
426
|
+
bundled `early-return` plugin. Auto-fixable with `eslint --fix`.
|
|
427
|
+
- **`no-lonely-if`**: Forbids an `if` statement as the only statement inside an
|
|
428
|
+
`else` block, requiring `else if` instead. The lone `if`-in-`else` adds an
|
|
429
|
+
indentation level that hides what is really a flat chain of conditions โ the
|
|
430
|
+
same needless nesting `no-else-return` and the bundled `early-return` plugin
|
|
431
|
+
already push back on. Auto-fixable with `eslint --fix`.
|
|
432
|
+
- **`no-nested-ternary`**: Forbids a ternary inside another ternary, the
|
|
433
|
+
archetypal "clever but unreadable" construct. Use `if`/`else` or an early
|
|
434
|
+
return instead.
|
|
435
|
+
- **`prefer-template`**: Forbids building strings with `+` concatenation
|
|
436
|
+
(`'Hello ' + name + '!'`) in favor of a template literal
|
|
437
|
+
(`` `Hello ${name}!` ``). Chaining `+` scatters the literal text across
|
|
438
|
+
operators, hides where text ends and a value begins, and leans on the same
|
|
439
|
+
implicit coercion the bundled `no-implicit-coercion` ban already targets
|
|
440
|
+
whenever a non-string operand sneaks in. The template literal keeps the final
|
|
441
|
+
shape of the string visible at a glance โ the same clarity goal as `eqeqeq`
|
|
442
|
+
and `no-implicit-coercion`. Auto-fixable with `eslint --fix`.
|
|
443
|
+
- **`no-object-constructor`**: Forbids the `Object` constructor (`new Object()`
|
|
444
|
+
and `Object()`) in favor of the `{}` literal. The constructor form is more
|
|
445
|
+
verbose and a trap โ `Object(x)` with a non-object argument returns that value
|
|
446
|
+
instead of a fresh object โ while the literal is unambiguous. The
|
|
447
|
+
object-creation sibling of the bundled wrapper-constructor and coercion bans.
|
|
448
|
+
Auto-fixable with `eslint --fix`.
|
|
449
|
+
- **`prefer-regex-literals`** (`disallowRedundantWrapping: true`): Forbids the
|
|
450
|
+
`RegExp` constructor for a static pattern (`new RegExp('\\d+')`) in favor of a
|
|
451
|
+
regex literal (`/\d+/`). The string form double-escapes every backslash, so a
|
|
452
|
+
single missed one silently changes the match with no error, and the pattern is
|
|
453
|
+
only validated when the constructor runs. The regex-shaped sibling of the
|
|
454
|
+
bundled `no-object-constructor` / `no-new-wrappers` / `no-new-func` bans.
|
|
455
|
+
`new RegExp(variable)` (a genuinely dynamic pattern) is left alone.
|
|
456
|
+
- **`no-promise-executor-return`**: Forbids returning a value from a `Promise`
|
|
457
|
+
executor โ the function passed to `new Promise(...)`. The constructor discards
|
|
458
|
+
the return value, so `new Promise((resolve) => resolve(work()))` (or an async
|
|
459
|
+
executor whose returned promise is never awaited) runs its work unobserved and
|
|
460
|
+
lets rejections go unhandled. A correctness check, like the bundled
|
|
461
|
+
`array-callback-return`. Use a block body that calls `resolve`/`reject`
|
|
462
|
+
without returning.
|
|
463
|
+
- **`no-await-in-loop`**: Forbids `await` inside a loop body. Awaiting on every
|
|
464
|
+
iteration serializes work that could run concurrently, so the loop pays the
|
|
465
|
+
_sum_ of every promise's latency instead of the _max_ โ a batch of
|
|
466
|
+
independent network/DB calls becomes an N-times slower stall. A quiet
|
|
467
|
+
performance bug the type checker cannot see, and the throughput side of the
|
|
468
|
+
async-hygiene family (`no-floating-promises`, `promise-function-async`,
|
|
469
|
+
`return-await`). Run the independent work with `Promise.all`/
|
|
470
|
+
`Promise.allSettled` over a `.map` instead. When the iterations are genuinely
|
|
471
|
+
dependent (each needs the previous result, an ordered write, a deliberate
|
|
472
|
+
rate limit) the serial `await` is correct, so the rule has no auto-fix โ those
|
|
473
|
+
loops opt out with `// eslint-disable-next-line no-await-in-loop`.
|
|
474
|
+
- **`no-throw-literal`**: Forbids throwing a non-`Error` value โ `throw 'boom'`,
|
|
475
|
+
`throw { code: 500 }`, `throw 42`. A thrown literal carries no stack trace and
|
|
476
|
+
breaks every `catch` that relies on `instanceof Error` or reads
|
|
477
|
+
`.message`/`.stack`, so the consumer's error handling silently misfires. A
|
|
478
|
+
correctness check, like the bundled `array-callback-return`. Throw a real
|
|
479
|
+
`Error` (or a subclass) instead.
|
|
480
|
+
- **`default-case-last`**: Requires the `default` clause of a `switch` to come
|
|
481
|
+
last. `default` matches only when no `case` does, so a `default` placed before
|
|
482
|
+
later cases reads as if those cases were unreachable, and a mid-`switch`
|
|
483
|
+
`default` that omits `break` silently falls through into the cases below it.
|
|
484
|
+
Pinning `default` to the end keeps its order-independent meaning legible. Not
|
|
485
|
+
auto-fixable: moving a clause that omits `break` could change behavior.
|
|
486
|
+
- **`no-extra-bind`**: Forbids `.bind()` on a function that never references
|
|
487
|
+
`this` (and binds no arguments) โ `(() => x).bind(obj)`,
|
|
488
|
+
`function () { return 1 }.bind(this)`, `handler.bind(this)` where `handler`
|
|
489
|
+
ignores `this`. The bind allocates a new wrapper on every evaluation and
|
|
490
|
+
returns one that behaves identically to the original, so it is pure overhead
|
|
491
|
+
that also misleads the reader into thinking the receiver matters โ the same
|
|
492
|
+
"looks meaningful but is dead" clutter `no-useless-return` /
|
|
493
|
+
`no-useless-concat` already remove, and the reflexive `.bind(this)` an AI
|
|
494
|
+
assistant appends to a callback by habit. Auto-fixable with `eslint --fix`.
|
|
495
|
+
|
|
496
|
+
### Import Hygiene
|
|
497
|
+
|
|
498
|
+
- **`import/no-duplicates`**: Collapses multiple import statements from the same
|
|
499
|
+
module into one, so dependencies on a module are visible in a single place.
|
|
500
|
+
- **`import/no-mutable-exports`**: Forbids exporting mutable bindings
|
|
501
|
+
(`export let` / `export var`). Mutable exports create shared mutable state
|
|
502
|
+
across modules โ a subtle, hard-to-trace footgun that AI assistants often
|
|
503
|
+
reach for. Export `const` (or a getter) instead.
|
|
504
|
+
- **`import/no-cycle`**: Forbids circular dependencies between modules. Cycles
|
|
505
|
+
cause order-dependent runtime bugs (a module observing a half-initialized
|
|
506
|
+
import as `undefined`) and signal tangled module boundaries. The TypeScript
|
|
507
|
+
parser is wired into `import/parsers` so this analysis also works across
|
|
508
|
+
`.ts`/`.tsx` files, not just plain JavaScript.
|
|
509
|
+
- **`import/no-self-import`**: Forbids a module importing itself, a degenerate
|
|
510
|
+
cycle that is always a mistake.
|
|
511
|
+
- **`import/no-empty-named-blocks`**: Forbids empty named import blocks
|
|
512
|
+
(`import {} from 'mod'`). An empty block is the residue of deleting the last
|
|
513
|
+
named binding โ the statement imports nothing yet still reads as if it pulls
|
|
514
|
+
names in, leaving a dead dependency edge behind. Use a bare side-effect
|
|
515
|
+
import (`import 'mod'`) or remove the line. Auto-fixable.
|
|
516
|
+
- **`unused-imports/no-unused-imports`**: Forbids imports that are never
|
|
517
|
+
referenced. The core `no-unused-vars` rules are turned off in this config, so
|
|
518
|
+
nothing else flagged dead imports โ yet an unused `import` is pure noise: it
|
|
519
|
+
has no runtime effect, slows resolution/bundling, and implies a dependency
|
|
520
|
+
that does not exist. AI assistants frequently leave these behind after editing
|
|
521
|
+
a file. Provided by `eslint-plugin-unused-imports`, which (unlike the base
|
|
522
|
+
rule) auto-fixes them โ an unused import is always safe to delete. Scoped to
|
|
523
|
+
imports only; unused locals and parameters are intentionally left untouched.
|
|
524
|
+
Auto-fixable.
|
|
525
|
+
- **`@typescript-eslint/consistent-type-imports`**: Forces `import type { โฆ }`
|
|
526
|
+
for imports used only as types (TypeScript files). A type-only import is
|
|
527
|
+
erased at compile time, so writing it as a value import leaves a binding that
|
|
528
|
+
looks like a runtime dependency โ it can drag a module (and its side effects)
|
|
529
|
+
into the emitted JS even though nothing uses the value, and it breaks under
|
|
530
|
+
`verbatimModuleSyntax` / `isolatedModules`. Splitting type and value imports
|
|
531
|
+
keeps the emitted module graph honest and every import's intent legible. Uses
|
|
532
|
+
`fixStyle: 'separate-type-imports'` (a distinct `import type` statement rather
|
|
533
|
+
than the inline `import { type X }` form). Auto-fixable.
|
|
534
|
+
- **`@typescript-eslint/consistent-type-exports`**: The export-side mirror of
|
|
535
|
+
`consistent-type-imports` โ forces `export type { โฆ }` for re-exports that
|
|
536
|
+
only carry types. A type-only name re-exported through a plain `export { โฆ }`
|
|
537
|
+
is erased at compile time, so the value-shaped statement leaves a runtime
|
|
538
|
+
export edge for something with no runtime existence: bundlers keep the source
|
|
539
|
+
module (and its side effects) alive, and the re-export breaks under
|
|
540
|
+
`verbatimModuleSyntax` / `isolatedModules`. Splitting type and value
|
|
541
|
+
re-exports keeps the emitted module graph honest and a barrel file's
|
|
542
|
+
value-vs-type surface legible. Auto-fixable.
|
|
543
|
+
|
|
544
|
+
### Bundled Custom Rules
|
|
545
|
+
|
|
546
|
+
Beyond the third-party plugins, the package ships a set of in-house rules that
|
|
547
|
+
encode its explicit-over-clever stance. Most are implemented as
|
|
548
|
+
[`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax)
|
|
549
|
+
selectors and applied automatically by the shared config; two
|
|
550
|
+
(`custom/no-default-class-export` and `custom/require-spec-file-tsx`) are real
|
|
551
|
+
plugin rules exposed under the `custom` namespace. You do not need to enable any
|
|
552
|
+
of them by hand โ they come on with the config โ but they are listed here so you
|
|
553
|
+
know what is enforcing each error.
|
|
554
|
+
|
|
555
|
+
#### Type-system rules (TypeScript files)
|
|
556
|
+
|
|
557
|
+
| Rule | What it enforces |
|
|
558
|
+
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
559
|
+
| `no-type-assertions` | Bans `as` type assertions (and the `as (typeof X)[number]` indexed-access form). `as const` is the only allowed assertion โ use a real type otherwise. |
|
|
560
|
+
| `no-inline-union-types` | Requires a named type alias instead of an inline union (in function signatures and in interface/class properties, whether the members are literals or not), so unions carry a name that documents their intent. |
|
|
561
|
+
| `no-record-literal-types` | Bans `Record<...>` keyed by string literals. Use a named interface or type with explicit keys instead. |
|
|
562
|
+
| `no-trivial-type-aliases` | Bans aliases that add no meaning โ primitive aliases, direct type references, and bare literal aliases. Unions, generics, mapped and conditional types stay allowed. |
|
|
563
|
+
|
|
564
|
+
#### Control-flow & switch rules
|
|
565
|
+
|
|
566
|
+
| Rule | What it enforces |
|
|
567
|
+
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
568
|
+
| `nullish-coalescing` | Bans the `??` operator in favor of explicit null/undefined checks that spell out the intended branch. |
|
|
569
|
+
| `switch-case-explicit-return` | Bans a bare `return;` inside a `switch` case โ each case must return an explicit value. |
|
|
570
|
+
| `switch-statements-return-type` | Requires an explicit return type on any function, arrow, or function expression that contains a `switch` (TS). |
|
|
571
|
+
| `switch-case-functions-return-type` | Requires an explicit return type on the functions produced for switch-case branches (TS). |
|
|
572
|
+
|
|
573
|
+
#### Export & module rules
|
|
574
|
+
|
|
575
|
+
| Rule | What it enforces |
|
|
576
|
+
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
577
|
+
| `no-empty-exports` | Bans the `export { ... }` specifier syntax; use a direct, single export per file instead. |
|
|
578
|
+
| `custom/no-default-class-export` | Disallows `export default class` in favor of a named class export, so the class keeps a stable, searchable name. |
|
|
579
|
+
| `no-process-env-properties` | Bans direct `process.env.X` access. Read `process.env` as a whole object (for example, validate it once) instead. |
|
|
580
|
+
|
|
581
|
+
#### Spec-file & size rules
|
|
582
|
+
|
|
583
|
+
| Rule | What it enforces |
|
|
584
|
+
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
|
585
|
+
| `custom/require-spec-file-tsx` | Requires a `.spec` sibling for `.tsx`/`.jsx` components, mirroring `ddd/require-spec-file` for React/Preact code. |
|
|
586
|
+
| `error-only-exports` | Exempts files that export only `Error` subclasses from the spec-file requirement (no testable logic to cover). |
|
|
587
|
+
| `max-file-lines` | `max-lines`: warns above 70 lines, errors above 100 (comments and blank lines skipped). |
|
|
588
|
+
| `max-function-lines` | `max-lines-per-function`: warns above 50 lines, errors above 70 (comments and blank lines skipped). |
|
|
589
|
+
| `no-trailing-spaces` | Flags trailing whitespace so diffs stay clean and invisible characters never sneak into source. |
|
|
590
|
+
|
|
212
591
|
### Framework-Specific Features
|
|
213
592
|
|
|
214
593
|
#### React Support
|
|
@@ -233,6 +612,10 @@ This ESLint configuration prioritizes **explicit code** over convenient shortcut
|
|
|
233
612
|
- All TypeScript/JavaScript source files (`.ts`, `.js`, `.tsx`, `.jsx`)
|
|
234
613
|
- Implementation files that contain business logic
|
|
235
614
|
|
|
615
|
+
> `.ts`/`.js` files are checked by `ddd/require-spec-file`; `.tsx`/`.jsx`
|
|
616
|
+
> components are checked by the bundled `custom/require-spec-file-tsx` rule, so
|
|
617
|
+
> React/Preact components are held to the same spec-file requirement.
|
|
618
|
+
|
|
236
619
|
**What files are excluded:**
|
|
237
620
|
|
|
238
621
|
- Test files themselves (`.spec.ts`, `.test.js`, etc.)
|
|
@@ -256,6 +639,22 @@ src/
|
|
|
256
639
|
โโโ config.ts # โ ๏ธ Excluded (config file)
|
|
257
640
|
```
|
|
258
641
|
|
|
642
|
+
**Spec file naming โ `.spec` vs `.test`:**
|
|
643
|
+
|
|
644
|
+
The sibling that satisfies the requirement for a source file must be named
|
|
645
|
+
`<name>.spec.<ext>` (for example, `url-manager.ts` โ `url-manager.spec.ts`). A
|
|
646
|
+
`<name>.test.<ext>` sibling is **not** accepted as that source file's spec, even
|
|
647
|
+
though `.test.*` files are themselves excluded from needing a spec of their own.
|
|
648
|
+
|
|
649
|
+
In other words, `.test.*` and `.spec.*` are both treated as test files (so they
|
|
650
|
+
never require their _own_ spec), but only the `.spec.*` name counts when checking
|
|
651
|
+
that a source file has a corresponding test. If your project uses the `.test.*`
|
|
652
|
+
convention, you have two options:
|
|
653
|
+
|
|
654
|
+
1. **Rename** test files to `<name>.spec.<ext>`, or
|
|
655
|
+
2. **Scope the rule down** for the affected paths (see
|
|
656
|
+
[Adopting in an Existing Project](#adopting-in-an-existing-project) below).
|
|
657
|
+
|
|
259
658
|
**Disabling for specific files:**
|
|
260
659
|
|
|
261
660
|
If you have files that only export simple Error classes or other boilerplate without testable logic, you can:
|
|
@@ -293,6 +692,36 @@ If you have files that only export simple Error classes or other boilerplate wit
|
|
|
293
692
|
]
|
|
294
693
|
```
|
|
295
694
|
|
|
695
|
+
### Language Safety
|
|
696
|
+
|
|
697
|
+
- **`no-var`**: Forbids `var`. Function-scoped, hoisted bindings leak out of the
|
|
698
|
+
block they appear to belong to and read as `undefined` before their
|
|
699
|
+
declaration runs, producing order-dependent bugs that `let`/`const` make
|
|
700
|
+
impossible. `var` is exactly the legacy shortcut an AI assistant trained on
|
|
701
|
+
older code reaches for, so banning it keeps every binding block-scoped and
|
|
702
|
+
its lifetime legible. The rule is auto-fixable, so existing code can adopt it
|
|
703
|
+
with `eslint --fix`.
|
|
704
|
+
- **`radix`** (`'always'`): Requires an explicit base for `parseInt` โ
|
|
705
|
+
`parseInt(str, 10)`, never `parseInt(str)`. With the base omitted it is
|
|
706
|
+
inferred from the string, so a leading `0x` is parsed as hex and
|
|
707
|
+
`parseInt(userInput)` silently uses a base the author never chose. The
|
|
708
|
+
wrong-number result still type-checks, so only the data flow is broken โ the
|
|
709
|
+
same class of _implicit_ behavior the config already bans via `eqeqeq` and
|
|
710
|
+
`no-implicit-coercion`. Not auto-fixable: only the author knows the intended
|
|
711
|
+
base.
|
|
712
|
+
|
|
713
|
+
### Honest Suppressions
|
|
714
|
+
|
|
715
|
+
The config sets `linterOptions.reportUnusedDisableDirectives` to `error`. An `eslint-disable` comment that no longer suppresses anything is reported as an error instead of being silently ignored.
|
|
716
|
+
|
|
717
|
+
This keeps every suppression honest: once a rule is satisfied (or renamed), the stale directive surfaces immediately so it can be removed, rather than quietly widening the set of unchecked code over time. It pairs naturally with the config's explicit-over-clever philosophy.
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
// eslint-disable-next-line ddd/require-spec-file -- once the spec exists this
|
|
721
|
+
// directive is unused, and ESLint now flags it so you delete it.
|
|
722
|
+
export const value = 1
|
|
723
|
+
```
|
|
724
|
+
|
|
296
725
|
### Configuration Philosophy
|
|
297
726
|
|
|
298
727
|
This configuration focuses on enforcing patterns that improve long-term maintainability while reducing noise from less impactful rules. The ruleset is carefully curated to balance developer productivity with code quality.
|
|
@@ -303,7 +732,129 @@ For troubleshooting common issues and frequently asked questions, see [FAQ.md](F
|
|
|
303
732
|
|
|
304
733
|
For development setup, testing guidelines, and contribution instructions, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
305
734
|
|
|
306
|
-
For version history and changelog information, see [CHANGELOG.md](CHANGELOG.md) or the [releases page](https://github.com/tupe12334/eslint-config/releases).
|
|
735
|
+
For version history and changelog information, see [CHANGELOG.md](CHANGELOG.md) or the [releases page](https://github.com/tupe12334/eslint-config-agent/releases).
|
|
736
|
+
|
|
737
|
+
## Adopting in an Existing Project
|
|
738
|
+
|
|
739
|
+
On a brand-new project this config is "zero config" โ you start clean and stay
|
|
740
|
+
clean. On an **established codebase**, the strict ruleset is intentionally
|
|
741
|
+
opinionated and will typically surface a large batch of pre-existing violations
|
|
742
|
+
the first time you run it (missing spec files, `?.`/`??` usage, missing JSDoc,
|
|
743
|
+
literal error messages, and so on). That is expected โ it is the gap between the
|
|
744
|
+
old standard and this one, not a bug.
|
|
745
|
+
|
|
746
|
+
Rather than block CI on a green-field cleanup or weaken the config permanently,
|
|
747
|
+
adopt it **gradually**. The recommended on-ramp is to keep the full ruleset but
|
|
748
|
+
temporarily demote the rules that produce the most pre-existing noise to `warn`,
|
|
749
|
+
so CI stays green while the warnings are burned down over time and promoted back
|
|
750
|
+
to `error`.
|
|
751
|
+
|
|
752
|
+
```javascript
|
|
753
|
+
import baseConfig from 'eslint-config-agent'
|
|
754
|
+
|
|
755
|
+
export default [
|
|
756
|
+
...baseConfig,
|
|
757
|
+
{
|
|
758
|
+
// Migration on-ramp: demote the highest-volume rules to warnings so an
|
|
759
|
+
// existing codebase can adopt the config without a CI-blocking cleanup.
|
|
760
|
+
// Remove entries here as each rule is driven to zero, then enjoy the full
|
|
761
|
+
// strictness with nothing left to relax.
|
|
762
|
+
rules: {
|
|
763
|
+
'ddd/require-spec-file': 'warn',
|
|
764
|
+
'jsdoc/require-jsdoc': 'warn',
|
|
765
|
+
'error/no-literal-error-message': 'warn',
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
]
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
To demote **every** rule at once instead of hand-picking the noisiest ones, use
|
|
772
|
+
the [`incremental` preset](#incremental-warn-level-preset)
|
|
773
|
+
(`import incremental from 'eslint-config-agent/incremental'`) โ it ships the
|
|
774
|
+
`config.map(toWarnings)` pattern so you don't have to copy-paste it.
|
|
775
|
+
|
|
776
|
+
Keep your CI lint step at `eslint .` (which fails only on errors) during
|
|
777
|
+
migration, and switch it to `eslint . --max-warnings 0` once the warnings are
|
|
778
|
+
cleared. To scope the relaxation to only the legacy parts of the tree, attach
|
|
779
|
+
the override to a `files` glob instead of applying it globally:
|
|
780
|
+
|
|
781
|
+
```javascript
|
|
782
|
+
import baseConfig from 'eslint-config-agent'
|
|
783
|
+
|
|
784
|
+
export default [
|
|
785
|
+
...baseConfig,
|
|
786
|
+
{
|
|
787
|
+
files: ['src/legacy/**/*.{ts,tsx}'],
|
|
788
|
+
rules: {
|
|
789
|
+
'ddd/require-spec-file': 'warn',
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
]
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
This way new code is held to the full standard immediately while the legacy
|
|
796
|
+
surface is tightened incrementally. For migrating from a legacy `.eslintrc`
|
|
797
|
+
config format to flat config, see [MIGRATION.md](MIGRATION.md).
|
|
798
|
+
|
|
799
|
+
### Type-aware linting and the project service
|
|
800
|
+
|
|
801
|
+
This config turns on **type-aware** rules (`typescript-eslint`'s
|
|
802
|
+
`strictTypeChecked` + `stylisticTypeChecked` presets) for `.ts`, `.tsx`, `.mts`,
|
|
803
|
+
and `.cts` files. To read type information it enables
|
|
804
|
+
`parserOptions.projectService: true`, which asks TypeScript for the program that
|
|
805
|
+
owns each linted file. Pure JavaScript files are linted without type information,
|
|
806
|
+
so this only affects TypeScript projects.
|
|
807
|
+
|
|
808
|
+
Because of that, every TypeScript file you lint must be covered by a
|
|
809
|
+
`tsconfig.json`. When a file is not part of any project, ESLint fails to parse it
|
|
810
|
+
with:
|
|
811
|
+
|
|
812
|
+
```text
|
|
813
|
+
Parsing error: <file> was not found by the project service.
|
|
814
|
+
Consider either including it in the tsconfig.json or to the "allowDefaultProject"
|
|
815
|
+
option in the project service.
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
This most often hits stray files that live outside your `tsconfig.json`'s
|
|
819
|
+
`include` โ `eslint.config.js`, `vite.config.ts`, `*.config.*`, or one-off
|
|
820
|
+
scripts. Three ways to resolve it, in order of preference:
|
|
821
|
+
|
|
822
|
+
1. **Add the file to your `tsconfig.json` `include`.** Best when the file really
|
|
823
|
+
belongs to the project (most app/source files).
|
|
824
|
+
2. **Allow a few loose config files through the default project.** Append an
|
|
825
|
+
override after the base config so the project service falls back to an
|
|
826
|
+
inferred program for a small allow-list:
|
|
827
|
+
|
|
828
|
+
```javascript
|
|
829
|
+
import baseConfig from 'eslint-config-agent'
|
|
830
|
+
|
|
831
|
+
export default [
|
|
832
|
+
...baseConfig,
|
|
833
|
+
{
|
|
834
|
+
languageOptions: {
|
|
835
|
+
parserOptions: {
|
|
836
|
+
// Lint these files even though they are outside tsconfig include.
|
|
837
|
+
projectService: {
|
|
838
|
+
allowDefaultProject: [
|
|
839
|
+
'*.config.js',
|
|
840
|
+
'*.config.ts',
|
|
841
|
+
'eslint.config.js',
|
|
842
|
+
],
|
|
843
|
+
},
|
|
844
|
+
tsconfigRootDir: import.meta.dirname,
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
]
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
`allowDefaultProject` only accepts a short list of files that are **not**
|
|
852
|
+
already in a `tsconfig.json`; globbing whole directories through it is
|
|
853
|
+
rejected by `typescript-eslint`.
|
|
854
|
+
|
|
855
|
+
3. **Ignore the file** if it should not be linted at all โ add it to the
|
|
856
|
+
`ignores` array of an override (see [Project-Specific
|
|
857
|
+
Ignores](#project-specific-ignores)).
|
|
307
858
|
|
|
308
859
|
## License
|
|
309
860
|
|
|
@@ -312,9 +863,9 @@ For version history and changelog information, see [CHANGELOG.md](CHANGELOG.md)
|
|
|
312
863
|
## Links & Resources
|
|
313
864
|
|
|
314
865
|
- **๐ฆ [npm Package](https://www.npmjs.com/package/eslint-config-agent)**
|
|
315
|
-
- **๐ [GitHub Repository](https://github.com/tupe12334/eslint-config)**
|
|
316
|
-
- **๐ [Issues & Bug Reports](https://github.com/tupe12334/eslint-config/issues)**
|
|
317
|
-
- **๐ [Releases & Changelog](https://github.com/tupe12334/eslint-config/releases)**
|
|
866
|
+
- **๐ [GitHub Repository](https://github.com/tupe12334/eslint-config-agent)**
|
|
867
|
+
- **๐ [Issues & Bug Reports](https://github.com/tupe12334/eslint-config-agent/issues)**
|
|
868
|
+
- **๐ [Releases & Changelog](https://github.com/tupe12334/eslint-config-agent/releases)**
|
|
318
869
|
- **๐ [ESLint Flat Config Documentation](https://eslint.org/docs/latest/use/configure/configuration-files)**
|
|
319
870
|
|
|
320
871
|
## Support
|