eslint-config-agent
v3.1.0
Published
ESLint configuration package with TypeScript support
Maintainers
Readme
eslint-config-agent
A comprehensive ESLint configuration package that provides TypeScript, React, and Preact linting rules with strict coding standards designed for enterprise-grade applications and AI-assisted development.
Why eslint-config-agent?
Designed for the AI Development Era 🤖
In an age where AI coding assistants and code generators are increasingly common, maintaining readable and maintainable code becomes critical. This ESLint configuration is specifically designed to prevent AI agents from generating unmaintainable or unreadable code that can leave developers lost in their own codebase.
Key Benefits
- 🤖 AI-Friendly Rules: Prevents AI assistants from writing shortcuts that hurt long-term maintainability
- 🔍 Explicit Over Clever: Forces clear, readable patterns instead of "clever" but obscure code
- 🚀 Production-Ready: Battle-tested configuration used in production environments
- 🔒 Type Safety First: Enforces explicit null/undefined checks instead of optional chaining
- ⚡ Modern ESLint: Built for ESLint 9 with flat configuration format
- 🎯 Framework Agnostic: Works seamlessly with React, Preact, and pure TypeScript
- 📦 Zero Config: Works out of the box with sensible defaults
- 🔧 Extensible: Easy to customize and extend for your specific needs
The Problem with AI-Generated Code
AI coding assistants often generate code that:
- Uses convenient shortcuts like
?.and??that hide potential runtime issues - Creates complex nested structures that are hard to debug
- Prioritizes brevity over clarity
- Makes assumptions about data structures that may not hold over time
Our Solution
This configuration enforces patterns that:
- Make null/undefined handling explicit and clear
- Keep functions at manageable lengths (≤100 lines)
- Require proper file organization and naming
- Ensure consistent, readable code structure
Key Features
- 🛠️ TypeScript First: Full TypeScript ESLint integration with advanced type checking
- ⚛️ React & Preact: Complete support for both React and Preact projects
- 🔐 Strict Standards: Enforces explicit null/undefined checks, requires strict equality (
===/!==), and disallows optional chaining and nullish coalescing for better code clarity - 📏 Code Quality: Function length limits (100 lines), trailing space detection, and consistent formatting
- 🧪 DDD by Default: Requires spec files for all source files to ensure comprehensive test coverage
- 🚀 Modern ESLint: Uses the latest flat configuration format (ESLint 9)
- 📋 Comprehensive Testing: 12+ test categories with automated validation
- 🔄 CI/CD Ready: Zero-warning configuration for production builds
Installation
Prerequisites
- Node.js: 20.x or higher
- ESLint: 9.x (see ESLint version compatibility below)
- TypeScript: 4.8.4 or higher (optional, only for TypeScript projects — see TypeScript version compatibility below)
ESLint version compatibility
This package targets ESLint 9.x and is not yet compatible with ESLint 10.
Several of the bundled plugins still call APIs that ESLint 10 removed (for
example context.getFilename()), so running the config under ESLint 10 fails at
lint time with errors such as:
TypeError: Error while loading rule 'default/no-localhost':
context.getFilename is not a functionIf you are on ESLint 10, pin ESLint to the latest 9.x release until ESLint 10 support lands. Progress is tracked in the project issues.
TypeScript version compatibility
For TypeScript projects, the bundled @typescript-eslint parser supports
TypeScript 4.8.4 or higher. TypeScript itself is not bundled — the parser
loads whatever typescript your project already has installed, so it is declared
as an optional peerDependency.
If your project pins an older TypeScript (for example a legacy 4.2.x), linting
fails up front with an opaque parser crash on every file rather than a clear
version message:
Parsing error: ts9__default.default.isTokenKind is not a functionIf you hit this, upgrade your project's typescript to >=4.8.4 (any recent
4.9 / 5.x release works). Installing this package will also surface a peer
dependency warning when the resolved TypeScript is too old.
Install the package
# Using npm
npm install --save-dev eslint-config-agent
# Using pnpm (recommended)
pnpm add -D eslint-config-agent
# Using yarn
yarn add -D eslint-config-agentDependencies are bundled
There is no separate peer-dependency step. ESLint and every plugin this
configuration uses (@typescript-eslint/*, typescript-eslint,
eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-import,
eslint-plugin-preact, globals, and the rest) ship as regular dependencies
of eslint-config-agent. Installing the package pulls them in automatically,
so the config works out of the box.
# That's it — the single install above is all you need
npm install --save-dev eslint-config-agentNote: If your project already depends on ESLint or any of these plugins, your package manager will deduplicate them against the versions bundled here.
The one exception is
typescript: it is an optional peer dependency (the parser uses your project's own TypeScript), so TypeScript projects must havetypescript >=4.8.4installed. See TypeScript version compatibility above.
Usage
Quick Start
Create an eslint.config.js file in your project root:
import config from 'eslint-config-agent'
export default configAvailable presets (entry points)
The package ships several entry points via its package.json#exports map.
Import whichever one matches how much strictness your project is ready for:
| Import specifier | Strictness | When to use |
| --------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| eslint-config-agent | Strict | The full, opinionated config. Best for greenfield projects that adopt every convention. |
| eslint-config-agent/recommended | Relaxed | The strict config with the most divisive rules pre-disabled. Best for incremental adoption. |
| 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. |
| 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. |
| 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. |
// Strict (default)
import config from 'eslint-config-agent'
// Relaxed, for incremental adoption
import recommended from 'eslint-config-agent/recommended'
// Backward-compatible alias of the default export
import ddd from 'eslint-config-agent/ddd'Each export is a flat-config array, so you can spread it and append your own override layers (see Advanced Configuration).
Recommended (relaxed) preset
The default export is intentionally strict — it assumes a greenfield project that follows every convention from day one. Existing codebases often can't, and end up copy-pasting the same block of rule overrides just to get the config to load without a wall of errors.
The eslint-config-agent/recommended preset bundles those common overrides for
you. It keeps the core quality rules but disables the most opinionated ones
(ddd/require-spec-file and its .tsx/.jsx counterpart
custom/require-spec-file-tsx, so React/Preact components are not forced to
ship a spec file up front either, single-export, required-exports, the custom
error/* rules, jsdoc/require-jsdoc (so existing code is not forced to
document every exported function and class up front — the jsdoc content rules
stay on, so any JSDoc you do write is still validated),
default/no-default-params, @typescript-eslint/consistent-type-definitions,
jsx-classname/require-classname (which otherwise errors on Tailwind-only
classNames), and the no-restricted-syntax bans on optional chaining /
nullish coalescing / type assertions), so idiomatic TypeScript and
React/Preact + Tailwind code passes during incremental adoption.
import recommended from 'eslint-config-agent/recommended'
export default recommendedRe-enable any individual rule by appending your own override layer:
import recommended from 'eslint-config-agent/recommended'
export default [
...recommended,
{
rules: {
// Opt back into a stricter rule once your code is ready for it
'ddd/require-spec-file': 'warn',
},
},
]Incremental (warn-level) preset
The recommended preset above turns rules off. When you instead want to keep
every rule reporting but stop them from failing CI — so you can see the full
backlog and burn it down gradually — use the incremental preset. It is the
full eslint-config-agent ruleset with every error-level rule downgraded to a
warning, so eslint exits 0 and pnpm lint stays green while still surfacing
everything:
import incremental from 'eslint-config-agent/incremental'
export default incrementalThis replaces the config.map(toWarnings) helper that adopting projects used to
copy-paste by hand. To enforce a rule as a hard error before the rest of the
backlog is cleared, append your own override layer — it wins over the warned
defaults:
import incremental from 'eslint-config-agent/incremental'
export default [
...incremental,
// Rules you are ready to enforce as hard errors today:
{
rules: {
eqeqeq: ['error', 'always'],
},
},
]Keep your CI lint step at eslint . during migration; switch it to
eslint . --max-warnings 0 once the warnings are cleared.
The toWarnings helper
The incremental preset warn-levels the whole ruleset. When you instead
want to compose your own flat config — warn-level the shared ruleset but keep a
handful of rules as hard errors from day one — import the same
toWarnings severity-downgrade helper the incremental presets use internally,
instead of copy-pasting it:
import config from 'eslint-config-agent'
import { toWarnings } from 'eslint-config-agent/to-warnings'
export default [
...config.map(toWarnings),
// Rules you are ready to enforce as hard errors today:
{
rules: {
eqeqeq: ['error', 'always'],
},
},
]toWarnings takes a single flat-config block and returns it with every
error-level rule downgraded to a warning. Blocks without a rules object are
returned untouched, and off/warn rules are left exactly as they are.
Recommended + incremental (relaxed, warn-level) preset
recommended and incremental each solve half of the first-run problem on an
existing codebase: recommended turns the most divisive rules off but keeps
everything else at error level (a real backlog still fails CI), while
incremental downgrades everything to a warning but keeps the divisive
rules firing as a wall of warnings on idiomatic TypeScript and
React/Preact + Tailwind code.
The recommended-incremental preset combines both — the divisive rules disabled
and every surviving rule downgraded to a warning. It is the gentlest on-ramp
for a large legacy codebase: eslint exits 0, the noisiest rules are silent,
and the remaining quality rules surface as warnings you can burn down before
tightening back up:
import recommendedIncremental from 'eslint-config-agent/recommended-incremental'
export default recommendedIncrementalAs with the other presets, append your own override layer to enforce a rule as a hard error before the rest of the backlog is cleared — it wins over the warned defaults:
import recommendedIncremental from 'eslint-config-agent/recommended-incremental'
export default [
...recommendedIncremental,
// Rules you are ready to enforce as hard errors today:
{
rules: {
eqeqeq: ['error', 'always'],
},
},
]Advanced Configuration
Extending with Custom Rules
import baseConfig from 'eslint-config-agent'
export default [
...baseConfig,
{
rules: {
// Override or add your custom rules
'no-console': 'warn',
'@typescript-eslint/explicit-function-return-type': 'error',
},
},
]Project-Specific Ignores
import baseConfig from 'eslint-config-agent'
export default [
...baseConfig,
{
ignores: ['build/**', 'dist/**', 'coverage/**', '*.config.js'],
},
]File-Specific Overrides
import baseConfig from 'eslint-config-agent'
export default [
...baseConfig,
{
files: ['**/*.test.{ts,tsx,js,jsx}'],
rules: {
// Relax rules for test files
'max-lines-per-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
{
files: ['scripts/**/*.js'],
languageOptions: {
globals: {
process: 'readonly',
console: 'readonly',
},
},
},
]Integration with Popular Tools
VS Code Setup
Add to your .vscode/settings.json:
{
"eslint.enable": true,
"eslint.format.enable": true,
"eslint.useFlatConfig": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}Package.json Scripts
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint:ci": "eslint . --max-warnings 0"
}
}Rules & Configuration
Core Philosophy
This ESLint configuration prioritizes explicit code over convenient shortcuts, especially important when working with AI coding assistants. Instead of allowing potentially unsafe operations like optional chaining, it enforces explicit null/undefined checks that make your intentions clear and your code more maintainable.
Why This Matters for AI Development:
- 🧠 Human-Readable: Code that humans can easily understand and modify
- 🔍 Debuggable: Clear error paths and explicit state handling
- 📚 Self-Documenting: Code that explains its intent without extensive comments
- 🛠️ Maintainable: Patterns that remain clear even as the codebase grows
Control Flow & Readability
no-else-return(allowElseIf: false): Forbids anelse/else ifblock when the precedingifalready exits viareturn. Once theifbranch returns, theelseonly adds nesting that hides the real control flow. Removing it flattens the code into guard-clause style — the same goal as the bundledearly-returnplugin. Auto-fixable witheslint --fix.no-lonely-if: Forbids anifstatement as the only statement inside anelseblock, requiringelse ifinstead. The loneif-in-elseadds an indentation level that hides what is really a flat chain of conditions — the same needless nestingno-else-returnand the bundledearly-returnplugin already push back on. Auto-fixable witheslint --fix.no-nested-ternary: Forbids a ternary inside another ternary, the archetypal "clever but unreadable" construct. Useif/elseor an early return instead.prefer-template: Forbids building strings with+concatenation ('Hello ' + name + '!') in favor of a template literal (`Hello ${name}!`). Chaining+scatters the literal text across operators, hides where text ends and a value begins, and leans on the same implicit coercion the bundledno-implicit-coercionban already targets whenever a non-string operand sneaks in. The template literal keeps the final shape of the string visible at a glance — the same clarity goal aseqeqeqandno-implicit-coercion. Auto-fixable witheslint --fix.no-object-constructor: Forbids theObjectconstructor (new Object()andObject()) in favor of the{}literal. The constructor form is more verbose and a trap —Object(x)with a non-object argument returns that value instead of a fresh object — while the literal is unambiguous. The object-creation sibling of the bundled wrapper-constructor and coercion bans. Auto-fixable witheslint --fix.prefer-regex-literals(disallowRedundantWrapping: true): Forbids theRegExpconstructor for a static pattern (new RegExp('\\d+')) in favor of a regex literal (/\d+/). The string form double-escapes every backslash, so a single missed one silently changes the match with no error, and the pattern is only validated when the constructor runs. The regex-shaped sibling of the bundledno-object-constructor/no-new-wrappers/no-new-funcbans.new RegExp(variable)(a genuinely dynamic pattern) is left alone.no-promise-executor-return: Forbids returning a value from aPromiseexecutor — the function passed tonew Promise(...). The constructor discards the return value, sonew Promise((resolve) => resolve(work()))(or an async executor whose returned promise is never awaited) runs its work unobserved and lets rejections go unhandled. A correctness check, like the bundledarray-callback-return. Use a block body that callsresolve/rejectwithout returning.no-await-in-loop: Forbidsawaitinside a loop body. Awaiting on every iteration serializes work that could run concurrently, so the loop pays the sum of every promise's latency instead of the max — a batch of independent network/DB calls becomes an N-times slower stall. A quiet performance bug the type checker cannot see, and the throughput side of the async-hygiene family (no-floating-promises,promise-function-async,return-await). Run the independent work withPromise.all/Promise.allSettledover a.mapinstead. When the iterations are genuinely dependent (each needs the previous result, an ordered write, a deliberate rate limit) the serialawaitis correct, so the rule has no auto-fix — those loops opt out with// eslint-disable-next-line no-await-in-loop.no-throw-literal: Forbids throwing a non-Errorvalue —throw 'boom',throw { code: 500 },throw 42. A thrown literal carries no stack trace and breaks everycatchthat relies oninstanceof Erroror reads.message/.stack, so the consumer's error handling silently misfires. A correctness check, like the bundledarray-callback-return. Throw a realError(or a subclass) instead.default-case-last: Requires thedefaultclause of aswitchto come last.defaultmatches only when nocasedoes, so adefaultplaced before later cases reads as if those cases were unreachable, and a mid-switchdefaultthat omitsbreaksilently falls through into the cases below it. Pinningdefaultto the end keeps its order-independent meaning legible. Not auto-fixable: moving a clause that omitsbreakcould change behavior.consistent-return: Requires everyreturnstatement in a function to either always specify a value or never specify one. A function that returns a value on one branch and falls through (or hits a barereturn;) on another silently yieldsundefinedon the unhandled paths — a quiet, plausible-but-wrong mistake the type checker does not reliably catch, since an inferredT | undefinedreturn type checks cleanly either way. Not auto-fixable: only the author knows whether the missing branch should return a value or the value-returning branch should stop returning one.no-extra-bind: Forbids.bind()on a function that never referencesthis(and binds no arguments) —(() => x).bind(obj),function () { return 1 }.bind(this),handler.bind(this)wherehandlerignoresthis. The bind allocates a new wrapper on every evaluation and returns one that behaves identically to the original, so it is pure overhead that also misleads the reader into thinking the receiver matters — the same "looks meaningful but is dead" clutterno-useless-return/no-useless-concatalready remove, and the reflexive.bind(this)an AI assistant appends to a callback by habit. Auto-fixable witheslint --fix.
Import Hygiene
import/no-duplicates: Collapses multiple import statements from the same module into one, so dependencies on a module are visible in a single place.import/no-mutable-exports: Forbids exporting mutable bindings (export let/export var). Mutable exports create shared mutable state across modules — a subtle, hard-to-trace footgun that AI assistants often reach for. Exportconst(or a getter) instead.import/no-cycle: Forbids circular dependencies between modules. Cycles cause order-dependent runtime bugs (a module observing a half-initialized import asundefined) and signal tangled module boundaries. The TypeScript parser is wired intoimport/parsersso this analysis also works across.ts/.tsxfiles, not just plain JavaScript.import/no-self-import: Forbids a module importing itself, a degenerate cycle that is always a mistake.import/no-empty-named-blocks: Forbids empty named import blocks (import {} from 'mod'). An empty block is the residue of deleting the last named binding — the statement imports nothing yet still reads as if it pulls names in, leaving a dead dependency edge behind. Use a bare side-effect import (import 'mod') or remove the line. Auto-fixable.unused-imports/no-unused-imports: Forbids imports that are never referenced. The coreno-unused-varsrules are turned off in this config, so nothing else flagged dead imports — yet an unusedimportis pure noise: it has no runtime effect, slows resolution/bundling, and implies a dependency that does not exist. AI assistants frequently leave these behind after editing a file. Provided byeslint-plugin-unused-imports, which (unlike the base rule) auto-fixes them — an unused import is always safe to delete. Scoped to imports only; unused locals and parameters are intentionally left untouched. Auto-fixable.@typescript-eslint/consistent-type-imports: Forcesimport type { … }for imports used only as types (TypeScript files). A type-only import is erased at compile time, so writing it as a value import leaves a binding that looks like a runtime dependency — it can drag a module (and its side effects) into the emitted JS even though nothing uses the value, and it breaks underverbatimModuleSyntax/isolatedModules. Splitting type and value imports keeps the emitted module graph honest and every import's intent legible. UsesfixStyle: 'separate-type-imports'(a distinctimport typestatement rather than the inlineimport { type X }form). Auto-fixable.@typescript-eslint/consistent-type-exports: The export-side mirror ofconsistent-type-imports— forcesexport type { … }for re-exports that only carry types. A type-only name re-exported through a plainexport { … }is erased at compile time, so the value-shaped statement leaves a runtime export edge for something with no runtime existence: bundlers keep the source module (and its side effects) alive, and the re-export breaks underverbatimModuleSyntax/isolatedModules. Splitting type and value re-exports keeps the emitted module graph honest and a barrel file's value-vs-type surface legible. Auto-fixable.no-useless-rename: Forbids renaming an import, export, or destructured binding to the exact name it already has —import { foo as foo } from './foo',export { bar as bar },const { baz: baz } = obj. Theas/:clause reads as if it transforms the binding, so a reader stops to compare both sides only to discover they are identical: pure punctuation noise that adds nothing. Exactly the ceremony an AI assistant spells out mechanically for an alias it never needed. Auto-fixable witheslint --fix.
Bundled Custom Rules
Beyond the third-party plugins, the package ships a set of in-house rules that
encode its explicit-over-clever stance. Most are implemented as
no-restricted-syntax
selectors and applied automatically by the shared config; two
(custom/no-default-class-export and custom/require-spec-file-tsx) are real
plugin rules exposed under the custom namespace. You do not need to enable any
of them by hand — they come on with the config — but they are listed here so you
know what is enforcing each error.
Type-system rules (TypeScript files)
| Rule | What it enforces |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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. |
| 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. |
| no-record-literal-types | Bans Record<...> keyed by string literals. Use a named interface or type with explicit keys instead. |
| 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. |
Control-flow & switch rules
| Rule | What it enforces |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| nullish-coalescing | Bans the ?? operator in favor of explicit null/undefined checks that spell out the intended branch. |
| switch-case-explicit-return | Bans a bare return; inside a switch case — each case must return an explicit value. |
| switch-statements-return-type | Requires an explicit return type on any function, arrow, or function expression that contains a switch (TS). |
| switch-case-functions-return-type | Requires an explicit return type on the functions produced for switch-case branches (TS). |
Export & module rules
| Rule | What it enforces |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| no-empty-exports | Bans the export { ... } specifier syntax; use a direct, single export per file instead. |
| custom/no-default-class-export | Disallows export default class in favor of a named class export, so the class keeps a stable, searchable name. |
| no-process-env-properties | Bans direct process.env.X access. Read process.env as a whole object (for example, validate it once) instead. |
Spec-file & size rules
| Rule | What it enforces |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| custom/require-spec-file-tsx | Requires a .spec sibling for .tsx/.jsx components, mirroring ddd/require-spec-file for React/Preact code. |
| error-only-exports | Exempts files that export only Error subclasses from the spec-file requirement (no testable logic to cover). |
| max-file-lines | max-lines: warns above 70 lines, errors above 100 (comments and blank lines skipped). |
| max-function-lines | max-lines-per-function: warns above 50 lines, errors above 70 (comments and blank lines skipped). |
| no-trailing-spaces | Flags trailing whitespace so diffs stay clean and invisible characters never sneak into source. |
Framework-Specific Features
React Support
- React 17+ automatic JSX runtime support
- Comprehensive hooks validation (rules-of-hooks, exhaustive-deps)
- Component prop validation
- JSX accessibility hints
Preact Support (Optional)
- Automatic detection and configuration when
eslint-plugin-preactis installed - Compatible with Preact-specific patterns and optimizations
- Shared configuration with React rules where applicable
Spec File Requirements (DDD)
By default, this configuration requires that every source file has a corresponding spec file. This ensures comprehensive test coverage and encourages test-driven development.
What files require specs:
- All TypeScript/JavaScript source files (
.ts,.js,.tsx,.jsx) - Implementation files that contain business logic
.ts/.jsfiles are checked byddd/require-spec-file;.tsx/.jsxcomponents are checked by the bundledcustom/require-spec-file-tsxrule, so React/Preact components are held to the same spec-file requirement.
What files are excluded:
- Test files themselves (
.spec.ts,.test.js, etc.) - Configuration files (
.config.js,eslint.config.js, etc.) - Index/barrel files (
index.ts,index.js) - Type definition files (
.d.ts) - Storybook files (
.stories.tsx) - Example files in
examples/directories - Error files (
.error.ts,-error.ts,errors/,exceptions/)
Example structure:
src/
├── utils.ts # Requires: utils.spec.ts
├── utils.spec.ts # ✅ Spec file present
├── components/
│ ├── Button.tsx # Requires: Button.spec.tsx
│ ├── Button.spec.tsx # ✅ Spec file present
│ └── index.ts # ⚠️ Excluded (index file)
└── config.ts # ⚠️ Excluded (config file)Spec file naming — .spec vs .test:
The sibling that satisfies the requirement for a source file must be named
<name>.spec.<ext> (for example, url-manager.ts → url-manager.spec.ts). A
<name>.test.<ext> sibling is not accepted as that source file's spec, even
though .test.* files are themselves excluded from needing a spec of their own.
In other words, .test.* and .spec.* are both treated as test files (so they
never require their own spec), but only the .spec.* name counts when checking
that a source file has a corresponding test. If your project uses the .test.*
convention, you have two options:
- Rename test files to
<name>.spec.<ext>, or - Scope the rule down for the affected paths (see Adopting in an Existing Project below).
Disabling for specific files:
If you have files that only export simple Error classes or other boilerplate without testable logic, you can:
Use a naming convention (automatically excluded):
my-error.ts,configuration-error.tsmy.error.ts,configuration.error.ts- Place in
errors/orexceptions/directory
Add an inline comment for non-conventional names:
/* eslint-disable ddd/require-spec-file */ export class ConfigurationError extends Error { constructor(message: string) { super(message) this.name = 'ConfigurationError' } }Disable for entire directories in your
eslint.config.js:import baseConfig from 'eslint-config-agent' export default [ ...baseConfig, { files: ['src/legacy/**/*.ts'], rules: { 'ddd/require-spec-file': 'off', }, }, ]
Language Safety
no-var: Forbidsvar. Function-scoped, hoisted bindings leak out of the block they appear to belong to and read asundefinedbefore their declaration runs, producing order-dependent bugs thatlet/constmake impossible.varis exactly the legacy shortcut an AI assistant trained on older code reaches for, so banning it keeps every binding block-scoped and its lifetime legible. The rule is auto-fixable, so existing code can adopt it witheslint --fix.radix('always'): Requires an explicit base forparseInt—parseInt(str, 10), neverparseInt(str). With the base omitted it is inferred from the string, so a leading0xis parsed as hex andparseInt(userInput)silently uses a base the author never chose. The wrong-number result still type-checks, so only the data flow is broken — the same class of implicit behavior the config already bans viaeqeqeqandno-implicit-coercion. Not auto-fixable: only the author knows the intended base.
Honest Suppressions
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.
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.
// eslint-disable-next-line ddd/require-spec-file -- once the spec exists this
// directive is unused, and ESLint now flags it so you delete it.
export const value = 1Configuration Philosophy
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.
For migration instructions from legacy ESLint configurations, see MIGRATION.md.
For troubleshooting common issues and frequently asked questions, see FAQ.md.
For development setup, testing guidelines, and contribution instructions, see CONTRIBUTING.md.
For version history and changelog information, see CHANGELOG.md or the releases page.
Adopting in an Existing Project
On a brand-new project this config is "zero config" — you start clean and stay
clean. On an established codebase, the strict ruleset is intentionally
opinionated and will typically surface a large batch of pre-existing violations
the first time you run it (missing spec files, ?./?? usage, missing JSDoc,
literal error messages, and so on). That is expected — it is the gap between the
old standard and this one, not a bug.
Rather than block CI on a green-field cleanup or weaken the config permanently,
adopt it gradually. The recommended on-ramp is to keep the full ruleset but
temporarily demote the rules that produce the most pre-existing noise to warn,
so CI stays green while the warnings are burned down over time and promoted back
to error.
import baseConfig from 'eslint-config-agent'
export default [
...baseConfig,
{
// Migration on-ramp: demote the highest-volume rules to warnings so an
// existing codebase can adopt the config without a CI-blocking cleanup.
// Remove entries here as each rule is driven to zero, then enjoy the full
// strictness with nothing left to relax.
rules: {
'ddd/require-spec-file': 'warn',
'jsdoc/require-jsdoc': 'warn',
'error/no-literal-error-message': 'warn',
},
},
]To demote every rule at once instead of hand-picking the noisiest ones, use
the incremental preset
(import incremental from 'eslint-config-agent/incremental') — it ships the
config.map(toWarnings) pattern so you don't have to copy-paste it.
Keep your CI lint step at eslint . (which fails only on errors) during
migration, and switch it to eslint . --max-warnings 0 once the warnings are
cleared. To scope the relaxation to only the legacy parts of the tree, attach
the override to a files glob instead of applying it globally:
import baseConfig from 'eslint-config-agent'
export default [
...baseConfig,
{
files: ['src/legacy/**/*.{ts,tsx}'],
rules: {
'ddd/require-spec-file': 'warn',
},
},
]This way new code is held to the full standard immediately while the legacy
surface is tightened incrementally. For migrating from a legacy .eslintrc
config format to flat config, see MIGRATION.md.
Type-aware linting and the project service
This config turns on type-aware rules (typescript-eslint's
strictTypeChecked + stylisticTypeChecked presets) for .ts, .tsx, .mts,
and .cts files. To read type information it enables
parserOptions.projectService: true, which asks TypeScript for the program that
owns each linted file. Pure JavaScript files are linted without type information,
so this only affects TypeScript projects.
Because of that, every TypeScript file you lint must be covered by a
tsconfig.json. When a file is not part of any project, ESLint fails to parse it
with:
Parsing error: <file> was not found by the project service.
Consider either including it in the tsconfig.json or to the "allowDefaultProject"
option in the project service.This most often hits stray files that live outside your tsconfig.json's
include — eslint.config.js, vite.config.ts, *.config.*, or one-off
scripts. Three ways to resolve it, in order of preference:
Add the file to your
tsconfig.jsoninclude. Best when the file really belongs to the project (most app/source files).Allow a few loose config files through the default project. Append an override after the base config so the project service falls back to an inferred program for a small allow-list:
import baseConfig from 'eslint-config-agent' export default [ ...baseConfig, { languageOptions: { parserOptions: { // Lint these files even though they are outside tsconfig include. projectService: { allowDefaultProject: [ '*.config.js', '*.config.ts', 'eslint.config.js', ], }, tsconfigRootDir: import.meta.dirname, }, }, }, ]allowDefaultProjectonly accepts a short list of files that are not already in atsconfig.json; globbing whole directories through it is rejected bytypescript-eslint.Ignore the file if it should not be linted at all — add it to the
ignoresarray of an override (see Project-Specific Ignores).
License
MIT License - See LICENSE file for details.
Links & Resources
- 📦 npm Package
- 🐙 GitHub Repository
- 📋 Issues & Bug Reports
- 🔄 Releases & Changelog
- 📖 ESLint Flat Config Documentation
Support
This project stands in solidarity with the people of Ukraine 🇺🇦 and Israel 🇮🇱.
Made with ❤️ by the eslint-config-agent team. Contributions welcome!
