eslint-config-agent 3.0.4 โ†’ 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.md +592 -26
  3. package/configs/base-plugins.js +32 -0
  4. package/configs/config-files.js +19 -3
  5. package/configs/examples.js +12 -1
  6. package/configs/javascript.js +12 -4
  7. package/configs/length-rule-scope.js +27 -0
  8. package/configs/overrides.js +3 -14
  9. package/configs/test-files.js +40 -1
  10. package/configs/tsx.js +10 -0
  11. package/configs/typescript.js +44 -2
  12. package/exports/ddd.d.ts +10 -0
  13. package/exports/ddd.js +1 -3
  14. package/exports/incremental.d.ts +11 -0
  15. package/exports/incremental.js +39 -0
  16. package/exports/recommended-incremental.d.ts +11 -0
  17. package/exports/recommended-incremental.js +46 -0
  18. package/exports/recommended.d.ts +14 -0
  19. package/exports/recommended.js +110 -0
  20. package/exports/to-warnings.d.ts +20 -0
  21. package/exports/to-warnings.js +41 -0
  22. package/index.d.ts +18 -0
  23. package/index.js +883 -7
  24. package/package.json +46 -17
  25. package/plugins/index.js +6 -0
  26. package/rules/index.js +20 -22
  27. package/rules/no-inline-union-types/index.js +8 -6
  28. package/rules/no-process-env-properties/index.js +4 -4
  29. package/rules/no-record-literal-types/index.js +3 -3
  30. package/rules/plugin/import/index.js +41 -0
  31. package/rules/plugin/index.js +0 -2
  32. package/rules/plugin/n/index.js +16 -2
  33. package/rules/plugin/n/no-process-env/index.js +4 -4
  34. package/rules/plugin/react/index.js +150 -2
  35. package/rules/plugin/typescript-eslint/index.js +406 -0
  36. package/rules/require-spec-file-tsx/README.md +57 -0
  37. package/rules/require-spec-file-tsx/helpers.js +93 -0
  38. package/rules/require-spec-file-tsx/index.js +109 -0
  39. 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+ with flat configuration format
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**: 18.x or higher
61
- - **ESLint**: 9.x
62
- - **TypeScript**: 4.5+ (optional, for TypeScript projects)
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
- ### Install peer dependencies
112
+ ### Dependencies are bundled
78
113
 
79
- The configuration requires several peer dependencies. Install them based on your project needs:
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
- # Required peer dependencies
83
- npm install --save-dev eslint@^9.34.0 @eslint/js@^9.34.0 @typescript-eslint/eslint-plugin@^8.40.0 @typescript-eslint/parser@^8.40.0 typescript-eslint@^8.40.0 eslint-plugin-react@^7.37.5 eslint-plugin-react-hooks@^5.2.0 eslint-plugin-import@^2.32.0 globals@^16.3.0 @eslint/eslintrc@^3.3.1
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
- Or use your package manager's peer dependency command:
90
-
91
- ```bash
92
- # Auto-install peer dependencies with npm 7+
93
- npx install-peerdeps --dev eslint-config-agent
94
-
95
- # With pnpm
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,192 @@ 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
+ - **`consistent-return`**: Requires every `return` statement in a function to
487
+ either always specify a value or never specify one. A function that returns
488
+ a value on one branch and falls through (or hits a bare `return;`) on another
489
+ silently yields `undefined` on the unhandled paths โ€” a quiet,
490
+ plausible-but-wrong mistake the type checker does not reliably catch, since
491
+ an inferred `T | undefined` return type checks cleanly either way. Not
492
+ auto-fixable: only the author knows whether the missing branch should return
493
+ a value or the value-returning branch should stop returning one.
494
+ - **`no-extra-bind`**: Forbids `.bind()` on a function that never references
495
+ `this` (and binds no arguments) โ€” `(() => x).bind(obj)`,
496
+ `function () { return 1 }.bind(this)`, `handler.bind(this)` where `handler`
497
+ ignores `this`. The bind allocates a new wrapper on every evaluation and
498
+ returns one that behaves identically to the original, so it is pure overhead
499
+ that also misleads the reader into thinking the receiver matters โ€” the same
500
+ "looks meaningful but is dead" clutter `no-useless-return` /
501
+ `no-useless-concat` already remove, and the reflexive `.bind(this)` an AI
502
+ assistant appends to a callback by habit. Auto-fixable with `eslint --fix`.
503
+
504
+ ### Import Hygiene
505
+
506
+ - **`import/no-duplicates`**: Collapses multiple import statements from the same
507
+ module into one, so dependencies on a module are visible in a single place.
508
+ - **`import/no-mutable-exports`**: Forbids exporting mutable bindings
509
+ (`export let` / `export var`). Mutable exports create shared mutable state
510
+ across modules โ€” a subtle, hard-to-trace footgun that AI assistants often
511
+ reach for. Export `const` (or a getter) instead.
512
+ - **`import/no-cycle`**: Forbids circular dependencies between modules. Cycles
513
+ cause order-dependent runtime bugs (a module observing a half-initialized
514
+ import as `undefined`) and signal tangled module boundaries. The TypeScript
515
+ parser is wired into `import/parsers` so this analysis also works across
516
+ `.ts`/`.tsx` files, not just plain JavaScript.
517
+ - **`import/no-self-import`**: Forbids a module importing itself, a degenerate
518
+ cycle that is always a mistake.
519
+ - **`import/no-empty-named-blocks`**: Forbids empty named import blocks
520
+ (`import {} from 'mod'`). An empty block is the residue of deleting the last
521
+ named binding โ€” the statement imports nothing yet still reads as if it pulls
522
+ names in, leaving a dead dependency edge behind. Use a bare side-effect
523
+ import (`import 'mod'`) or remove the line. Auto-fixable.
524
+ - **`unused-imports/no-unused-imports`**: Forbids imports that are never
525
+ referenced. The core `no-unused-vars` rules are turned off in this config, so
526
+ nothing else flagged dead imports โ€” yet an unused `import` is pure noise: it
527
+ has no runtime effect, slows resolution/bundling, and implies a dependency
528
+ that does not exist. AI assistants frequently leave these behind after editing
529
+ a file. Provided by `eslint-plugin-unused-imports`, which (unlike the base
530
+ rule) auto-fixes them โ€” an unused import is always safe to delete. Scoped to
531
+ imports only; unused locals and parameters are intentionally left untouched.
532
+ Auto-fixable.
533
+ - **`@typescript-eslint/consistent-type-imports`**: Forces `import type { โ€ฆ }`
534
+ for imports used only as types (TypeScript files). A type-only import is
535
+ erased at compile time, so writing it as a value import leaves a binding that
536
+ looks like a runtime dependency โ€” it can drag a module (and its side effects)
537
+ into the emitted JS even though nothing uses the value, and it breaks under
538
+ `verbatimModuleSyntax` / `isolatedModules`. Splitting type and value imports
539
+ keeps the emitted module graph honest and every import's intent legible. Uses
540
+ `fixStyle: 'separate-type-imports'` (a distinct `import type` statement rather
541
+ than the inline `import { type X }` form). Auto-fixable.
542
+ - **`@typescript-eslint/consistent-type-exports`**: The export-side mirror of
543
+ `consistent-type-imports` โ€” forces `export type { โ€ฆ }` for re-exports that
544
+ only carry types. A type-only name re-exported through a plain `export { โ€ฆ }`
545
+ is erased at compile time, so the value-shaped statement leaves a runtime
546
+ export edge for something with no runtime existence: bundlers keep the source
547
+ module (and its side effects) alive, and the re-export breaks under
548
+ `verbatimModuleSyntax` / `isolatedModules`. Splitting type and value
549
+ re-exports keeps the emitted module graph honest and a barrel file's
550
+ value-vs-type surface legible. Auto-fixable.
551
+ - **`no-useless-rename`**: Forbids renaming an import, export, or destructured
552
+ binding to the exact name it already has โ€” `import { foo as foo } from
553
+ './foo'`, `export { bar as bar }`, `const { baz: baz } = obj`. The `as`/`:`
554
+ clause reads as if it transforms the binding, so a reader stops to compare
555
+ both sides only to discover they are identical: pure punctuation noise that
556
+ adds nothing. Exactly the ceremony an AI assistant spells out mechanically
557
+ for an alias it never needed. Auto-fixable with `eslint --fix`.
558
+
559
+ ### Bundled Custom Rules
560
+
561
+ Beyond the third-party plugins, the package ships a set of in-house rules that
562
+ encode its explicit-over-clever stance. Most are implemented as
563
+ [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax)
564
+ selectors and applied automatically by the shared config; two
565
+ (`custom/no-default-class-export` and `custom/require-spec-file-tsx`) are real
566
+ plugin rules exposed under the `custom` namespace. You do not need to enable any
567
+ of them by hand โ€” they come on with the config โ€” but they are listed here so you
568
+ know what is enforcing each error.
569
+
570
+ #### Type-system rules (TypeScript files)
571
+
572
+ | Rule | What it enforces |
573
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
574
+ | `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. |
575
+ | `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. |
576
+ | `no-record-literal-types` | Bans `Record<...>` keyed by string literals. Use a named interface or type with explicit keys instead. |
577
+ | `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. |
578
+
579
+ #### Control-flow & switch rules
580
+
581
+ | Rule | What it enforces |
582
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------------------- |
583
+ | `nullish-coalescing` | Bans the `??` operator in favor of explicit null/undefined checks that spell out the intended branch. |
584
+ | `switch-case-explicit-return` | Bans a bare `return;` inside a `switch` case โ€” each case must return an explicit value. |
585
+ | `switch-statements-return-type` | Requires an explicit return type on any function, arrow, or function expression that contains a `switch` (TS). |
586
+ | `switch-case-functions-return-type` | Requires an explicit return type on the functions produced for switch-case branches (TS). |
587
+
588
+ #### Export & module rules
589
+
590
+ | Rule | What it enforces |
591
+ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
592
+ | `no-empty-exports` | Bans the `export { ... }` specifier syntax; use a direct, single export per file instead. |
593
+ | `custom/no-default-class-export` | Disallows `export default class` in favor of a named class export, so the class keeps a stable, searchable name. |
594
+ | `no-process-env-properties` | Bans direct `process.env.X` access. Read `process.env` as a whole object (for example, validate it once) instead. |
595
+
596
+ #### Spec-file & size rules
597
+
598
+ | Rule | What it enforces |
599
+ | ------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
600
+ | `custom/require-spec-file-tsx` | Requires a `.spec` sibling for `.tsx`/`.jsx` components, mirroring `ddd/require-spec-file` for React/Preact code. |
601
+ | `error-only-exports` | Exempts files that export only `Error` subclasses from the spec-file requirement (no testable logic to cover). |
602
+ | `max-file-lines` | `max-lines`: warns above 70 lines, errors above 100 (comments and blank lines skipped). |
603
+ | `max-function-lines` | `max-lines-per-function`: warns above 50 lines, errors above 70 (comments and blank lines skipped). |
604
+ | `no-trailing-spaces` | Flags trailing whitespace so diffs stay clean and invisible characters never sneak into source. |
605
+
212
606
  ### Framework-Specific Features
213
607
 
214
608
  #### React Support
@@ -233,6 +627,10 @@ This ESLint configuration prioritizes **explicit code** over convenient shortcut
233
627
  - All TypeScript/JavaScript source files (`.ts`, `.js`, `.tsx`, `.jsx`)
234
628
  - Implementation files that contain business logic
235
629
 
630
+ > `.ts`/`.js` files are checked by `ddd/require-spec-file`; `.tsx`/`.jsx`
631
+ > components are checked by the bundled `custom/require-spec-file-tsx` rule, so
632
+ > React/Preact components are held to the same spec-file requirement.
633
+
236
634
  **What files are excluded:**
237
635
 
238
636
  - Test files themselves (`.spec.ts`, `.test.js`, etc.)
@@ -256,6 +654,22 @@ src/
256
654
  โ””โ”€โ”€ config.ts # โš ๏ธ Excluded (config file)
257
655
  ```
258
656
 
657
+ **Spec file naming โ€” `.spec` vs `.test`:**
658
+
659
+ The sibling that satisfies the requirement for a source file must be named
660
+ `<name>.spec.<ext>` (for example, `url-manager.ts` โ†’ `url-manager.spec.ts`). A
661
+ `<name>.test.<ext>` sibling is **not** accepted as that source file's spec, even
662
+ though `.test.*` files are themselves excluded from needing a spec of their own.
663
+
664
+ In other words, `.test.*` and `.spec.*` are both treated as test files (so they
665
+ never require their _own_ spec), but only the `.spec.*` name counts when checking
666
+ that a source file has a corresponding test. If your project uses the `.test.*`
667
+ convention, you have two options:
668
+
669
+ 1. **Rename** test files to `<name>.spec.<ext>`, or
670
+ 2. **Scope the rule down** for the affected paths (see
671
+ [Adopting in an Existing Project](#adopting-in-an-existing-project) below).
672
+
259
673
  **Disabling for specific files:**
260
674
 
261
675
  If you have files that only export simple Error classes or other boilerplate without testable logic, you can:
@@ -293,6 +707,36 @@ If you have files that only export simple Error classes or other boilerplate wit
293
707
  ]
294
708
  ```
295
709
 
710
+ ### Language Safety
711
+
712
+ - **`no-var`**: Forbids `var`. Function-scoped, hoisted bindings leak out of the
713
+ block they appear to belong to and read as `undefined` before their
714
+ declaration runs, producing order-dependent bugs that `let`/`const` make
715
+ impossible. `var` is exactly the legacy shortcut an AI assistant trained on
716
+ older code reaches for, so banning it keeps every binding block-scoped and
717
+ its lifetime legible. The rule is auto-fixable, so existing code can adopt it
718
+ with `eslint --fix`.
719
+ - **`radix`** (`'always'`): Requires an explicit base for `parseInt` โ€”
720
+ `parseInt(str, 10)`, never `parseInt(str)`. With the base omitted it is
721
+ inferred from the string, so a leading `0x` is parsed as hex and
722
+ `parseInt(userInput)` silently uses a base the author never chose. The
723
+ wrong-number result still type-checks, so only the data flow is broken โ€” the
724
+ same class of _implicit_ behavior the config already bans via `eqeqeq` and
725
+ `no-implicit-coercion`. Not auto-fixable: only the author knows the intended
726
+ base.
727
+
728
+ ### Honest Suppressions
729
+
730
+ 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.
731
+
732
+ 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.
733
+
734
+ ```typescript
735
+ // eslint-disable-next-line ddd/require-spec-file -- once the spec exists this
736
+ // directive is unused, and ESLint now flags it so you delete it.
737
+ export const value = 1
738
+ ```
739
+
296
740
  ### Configuration Philosophy
297
741
 
298
742
  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 +747,129 @@ For troubleshooting common issues and frequently asked questions, see [FAQ.md](F
303
747
 
304
748
  For development setup, testing guidelines, and contribution instructions, see [CONTRIBUTING.md](CONTRIBUTING.md).
305
749
 
306
- For version history and changelog information, see [CHANGELOG.md](CHANGELOG.md) or the [releases page](https://github.com/tupe12334/eslint-config/releases).
750
+ For version history and changelog information, see [CHANGELOG.md](CHANGELOG.md) or the [releases page](https://github.com/tupe12334/eslint-config-agent/releases).
751
+
752
+ ## Adopting in an Existing Project
753
+
754
+ On a brand-new project this config is "zero config" โ€” you start clean and stay
755
+ clean. On an **established codebase**, the strict ruleset is intentionally
756
+ opinionated and will typically surface a large batch of pre-existing violations
757
+ the first time you run it (missing spec files, `?.`/`??` usage, missing JSDoc,
758
+ literal error messages, and so on). That is expected โ€” it is the gap between the
759
+ old standard and this one, not a bug.
760
+
761
+ Rather than block CI on a green-field cleanup or weaken the config permanently,
762
+ adopt it **gradually**. The recommended on-ramp is to keep the full ruleset but
763
+ temporarily demote the rules that produce the most pre-existing noise to `warn`,
764
+ so CI stays green while the warnings are burned down over time and promoted back
765
+ to `error`.
766
+
767
+ ```javascript
768
+ import baseConfig from 'eslint-config-agent'
769
+
770
+ export default [
771
+ ...baseConfig,
772
+ {
773
+ // Migration on-ramp: demote the highest-volume rules to warnings so an
774
+ // existing codebase can adopt the config without a CI-blocking cleanup.
775
+ // Remove entries here as each rule is driven to zero, then enjoy the full
776
+ // strictness with nothing left to relax.
777
+ rules: {
778
+ 'ddd/require-spec-file': 'warn',
779
+ 'jsdoc/require-jsdoc': 'warn',
780
+ 'error/no-literal-error-message': 'warn',
781
+ },
782
+ },
783
+ ]
784
+ ```
785
+
786
+ To demote **every** rule at once instead of hand-picking the noisiest ones, use
787
+ the [`incremental` preset](#incremental-warn-level-preset)
788
+ (`import incremental from 'eslint-config-agent/incremental'`) โ€” it ships the
789
+ `config.map(toWarnings)` pattern so you don't have to copy-paste it.
790
+
791
+ Keep your CI lint step at `eslint .` (which fails only on errors) during
792
+ migration, and switch it to `eslint . --max-warnings 0` once the warnings are
793
+ cleared. To scope the relaxation to only the legacy parts of the tree, attach
794
+ the override to a `files` glob instead of applying it globally:
795
+
796
+ ```javascript
797
+ import baseConfig from 'eslint-config-agent'
798
+
799
+ export default [
800
+ ...baseConfig,
801
+ {
802
+ files: ['src/legacy/**/*.{ts,tsx}'],
803
+ rules: {
804
+ 'ddd/require-spec-file': 'warn',
805
+ },
806
+ },
807
+ ]
808
+ ```
809
+
810
+ This way new code is held to the full standard immediately while the legacy
811
+ surface is tightened incrementally. For migrating from a legacy `.eslintrc`
812
+ config format to flat config, see [MIGRATION.md](MIGRATION.md).
813
+
814
+ ### Type-aware linting and the project service
815
+
816
+ This config turns on **type-aware** rules (`typescript-eslint`'s
817
+ `strictTypeChecked` + `stylisticTypeChecked` presets) for `.ts`, `.tsx`, `.mts`,
818
+ and `.cts` files. To read type information it enables
819
+ `parserOptions.projectService: true`, which asks TypeScript for the program that
820
+ owns each linted file. Pure JavaScript files are linted without type information,
821
+ so this only affects TypeScript projects.
822
+
823
+ Because of that, every TypeScript file you lint must be covered by a
824
+ `tsconfig.json`. When a file is not part of any project, ESLint fails to parse it
825
+ with:
826
+
827
+ ```text
828
+ Parsing error: <file> was not found by the project service.
829
+ Consider either including it in the tsconfig.json or to the "allowDefaultProject"
830
+ option in the project service.
831
+ ```
832
+
833
+ This most often hits stray files that live outside your `tsconfig.json`'s
834
+ `include` โ€” `eslint.config.js`, `vite.config.ts`, `*.config.*`, or one-off
835
+ scripts. Three ways to resolve it, in order of preference:
836
+
837
+ 1. **Add the file to your `tsconfig.json` `include`.** Best when the file really
838
+ belongs to the project (most app/source files).
839
+ 2. **Allow a few loose config files through the default project.** Append an
840
+ override after the base config so the project service falls back to an
841
+ inferred program for a small allow-list:
842
+
843
+ ```javascript
844
+ import baseConfig from 'eslint-config-agent'
845
+
846
+ export default [
847
+ ...baseConfig,
848
+ {
849
+ languageOptions: {
850
+ parserOptions: {
851
+ // Lint these files even though they are outside tsconfig include.
852
+ projectService: {
853
+ allowDefaultProject: [
854
+ '*.config.js',
855
+ '*.config.ts',
856
+ 'eslint.config.js',
857
+ ],
858
+ },
859
+ tsconfigRootDir: import.meta.dirname,
860
+ },
861
+ },
862
+ },
863
+ ]
864
+ ```
865
+
866
+ `allowDefaultProject` only accepts a short list of files that are **not**
867
+ already in a `tsconfig.json`; globbing whole directories through it is
868
+ rejected by `typescript-eslint`.
869
+
870
+ 3. **Ignore the file** if it should not be linted at all โ€” add it to the
871
+ `ignores` array of an override (see [Project-Specific
872
+ Ignores](#project-specific-ignores)).
307
873
 
308
874
  ## License
309
875
 
@@ -312,9 +878,9 @@ For version history and changelog information, see [CHANGELOG.md](CHANGELOG.md)
312
878
  ## Links & Resources
313
879
 
314
880
  - **๐Ÿ“ฆ [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)**
881
+ - **๐Ÿ™ [GitHub Repository](https://github.com/tupe12334/eslint-config-agent)**
882
+ - **๐Ÿ“‹ [Issues & Bug Reports](https://github.com/tupe12334/eslint-config-agent/issues)**
883
+ - **๐Ÿ”„ [Releases & Changelog](https://github.com/tupe12334/eslint-config-agent/releases)**
318
884
  - **๐Ÿ“– [ESLint Flat Config Documentation](https://eslint.org/docs/latest/use/configure/configuration-files)**
319
885
 
320
886
  ## Support