eslint-config-agent 3.0.3 โ†’ 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/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,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