@cognirail/eslint-config
v2.1.3
Published
Opinionated ESLint flat config + Prettier for TypeScript — AI coding guardrails
Maintainers
Readme
@cognirail/eslint-config
Opinionated ESLint flat config + Prettier for TypeScript projects.
Designed as AI coding guardrails — strict type checks, complexity detection, modern JS enforcement, and no inline escape hatches. One package, zero config hassle.
Included Plugins
| Plugin | Purpose |
| ------------------------------------- | ----------------------------------------------------------------- |
| typescript-eslint strictTypeChecked | Strict TS rules + type-aware analysis |
| eslint-plugin-unicorn | Modern JS idioms enforcement |
| eslint-plugin-sonarjs | Code smell + bug detection |
| eslint-plugin-import-x | Import organization + TS resolver |
| eslint-plugin-n | Node.js best practices |
| eslint-config-prettier | Disable formatting rules (let Prettier handle it) |
| eslint-import-resolver-typescript | TypeScript-aware import resolution for import-x |
| @cspell/eslint-plugin | Spell checking for identifiers, comments, and JSX text (optional) |
Requirements
- Node.js ^20.19.0 || ^22.13.0 || >=24
- ESLint >= 10.4.1
- TypeScript >= 6.0.3 (optional — works without it when
disableTypeChecked: true) - Prettier >= 3.8.3 (optional — only needed if using
./prettierexport)
Quick Start
1. Install
pnpm add -D @cognirail/eslint-config eslint typescript prettier2. ESLint — create eslint.config.mjs
import { defineConfig } from '@cognirail/eslint-config';
export default defineConfig();Also supports
eslint.config.tswith typescript-eslint v8+.
3. Prettier — add to package.json
{
"prettier": "@cognirail/eslint-config/prettier"
}4. Add lint scripts
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint --fix . && prettier --write .",
"format:check": "prettier --check ."
}
}Done. Run pnpm lint to check, pnpm lint:fix to auto-fix.
Note: Your project must have a
tsconfig.jsonfor type-aware rules to work. If you don't have one, setdisableTypeChecked: truein the options.
5. (Optional) Spell Checking
Requires Node.js >= 22.18.0 because @cspell/eslint-plugin v10 uses that runtime baseline.
pnpm add -D @cspell/eslint-plugin// eslint.config.mjs
import { defineConfig } from '@cognirail/eslint-config';
import { cspell } from '@cognirail/eslint-config/cspell';
export default [...defineConfig(), ...cspell()];The CSpell config includes a built-in word list for common tech ecosystem terms (AI/DB/tooling). To add project-specific words, pass them via the words option:
export default [...defineConfig(), ...cspell({ words: ['myapp', 'myterm'] })];Alternatively, create a cspell.json at project root — CSpell auto-discovers it.
6. (Optional) Barrel Import Enforcement
Enforce barrel imports at architecture boundaries — prevent deep path imports like @agent/runtime/internal/executor and force consumers to use @agent/runtime.
// eslint.config.mjs
import { defineConfig } from '@cognirail/eslint-config';
import { barrel } from '@cognirail/eslint-config/barrel';
export default [
...defineConfig({ tsconfigRootDir: import.meta.dirname }),
...barrel({
boundaries: [
{
publicAlias: '@agent/runtime',
internalGlobs: ['src/agent/runtime/**'],
consumers: ['src/agent/**', 'src/tools/**', 'test/**'],
},
],
}),
];| Option | Type | Description |
| ----------------------------------- | ---------- | -------------------------------------------------------------------------------------------- |
| boundaries[].publicAlias | string | Public alias to import from. Blocks imports below this alias. |
| boundaries[].internalGlobs | string[] | Files inside the implementation boundary; ignored so internals can import each other. |
| boundaries[].consumers | string[] | Files where deep imports from the public alias are forbidden. |
| boundaries[].allowInternalFrom | string[] | Additional globs allowed to import internals. |
| boundaries[].ignores | string[] | Extra ignores for this boundary. |
| layers / enforcedIn / ignores | — | Legacy API kept for existing configs; prefer boundaries for new architecture declarations. |
The rule only applies to boundary consumers; files matching internalGlobs, allowInternalFrom, or ignores are not restricted.
7. (Optional) Agent Project Preset
Use ./agent when the project contains agent runtime/tool/prompt/eval code and should enforce role-based file naming. This is the core agent preset; spell checking remains opt-in through ./cspell.
// eslint.config.mjs
import { defineAgentConfig } from '@cognirail/eslint-config/agent';
export default defineAgentConfig({
tsconfigRootDir: import.meta.dirname,
boundaries: [
{
publicAlias: '@agent/runtime',
internalGlobs: ['src/agent/runtime/**'],
consumers: ['src/agent/**', 'src/tools/**', 'test/**'],
},
],
});Default agent role suffixes:
| Suffix | Expected shape |
| ----------- | -------------------- |
| runtime | any |
| executor | class or function |
| planner | class or function |
| tool | schema or function |
| prompt | text or factory |
| eval | any |
| adapter | class or function |
| schema | any |
| memory | class or function |
| type | interface/type alias |
| interface | interface/type alias |
8. (Optional) Role-Based File Naming
Use ./role-naming directly for non-Nest, non-agent projects with their own role suffix table.
import { defineConfig } from '@cognirail/eslint-config';
import { roleNaming } from '@cognirail/eslint-config/role-naming';
export default [
...defineConfig({ tsconfigRootDir: import.meta.dirname }),
...roleNaming({
suffixes: {
service: { kind: 'class-or-function' },
schema: { kind: 'any' },
type: { kind: 'types' },
},
}),
];9. (Optional) NestJS File Naming
Enforce the NestJS file-naming convention: role-suffix whitelist, content↔suffix binding, ambient declaration placement, and a single test-file convention. Self-contained — adds no external ESLint plugin dependency.
General form:
<kebab-base>.<role>.tsThe role suffix describes the dominant symbol's role, not merely its technical shape. One file should have one role: if a *.service.ts grows DTOs, interfaces, or exported utility types, split them into the role-specific files. Narrow private utility types that are not externally imported may stay close to the implementation.
// eslint.config.mjs
import { defineConfig } from '@cognirail/eslint-config';
import { nestNaming } from '@cognirail/eslint-config/nest-naming';
export default [
...defineConfig({ tsconfigRootDir: import.meta.dirname }),
...nestNaming({ files: ['src/**/*.ts'] }),
];Rules:
| Rule | Catches |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| nest-naming/allowed-suffix | Filename suffix not in the role whitelist (e.g. *.widget.ts); non-kebab base (CreateUser.dto.ts) |
| nest-naming/suffix-kind | Content doesn't match suffix: *.dto.ts written as interface/type; a class inside *.interface.ts / *.type.ts; exported interfaces/types left in the wrong role file |
| nest-naming/suffix-decorator | A *.controller.ts / *.service.ts / *.module.ts / *.guard.ts class missing its @Controller / @Injectable / @Module decorator (severity = enforceDecorator, default warn) |
| nest-naming/test-suffix | Wrong test convention: *.test.ts when Nest's default jest testRegex only matches *.spec.ts (silent skip) |
| nest-naming/declaration-file | Ambient declare ... declarations hidden in normal implementation files, or project domain types placed in *.d.ts |
| nest-naming/no-global-types-folder | Project types placed in a global types/ or src/types/ folder instead of being co-located with the owning module |
Suffixes are graded by runtime shape. class files emit JS and participate in DI/decorators/new; interface/type files disappear after compilation; any files are allowed because they may be functions, constants, or mixed framework forms.
Class suffixes with enforced decorators:
| Suffix | Role | Decorator |
| ------------- | ------------------------------------------------------ | ------------------- |
| controller | HTTP/RPC entrypoint; orchestration, not business logic | @Controller |
| service | Injectable application/domain logic | @Injectable |
| module | DI organization unit | @Module |
| guard | CanActivate access control | @Injectable |
| pipe | PipeTransform validation/transformation | @Injectable |
| interceptor | NestInterceptor around advice | @Injectable |
| filter | ExceptionFilter handling | @Catch |
| resolver | GraphQL resolver | @Resolver |
| gateway | WebSocket gateway | @WebSocketGateway |
Class suffixes without fixed decorator enforcement:
| Suffix | Role |
| ------------ | -------------------------------------------------------------------- |
| dto | Boundary data contract with runtime validation; DTOs must be classes |
| entity | Persistence mapping / domain entity |
| repository | Custom repository |
| strategy | Passport strategy |
| subscriber | TypeORM event subscriber |
Compile-time suffixes:
| Suffix | Must contain | Forbidden |
| ----------- | ----------------------- | --------- |
| interface | interface declaration | class |
| type | type alias | class |
Content-free suffixes:
| Suffixes | Typical content |
| --------------------------------------- | ---------------------------------------------------- |
| decorator | Custom decorator functions / factories |
| middleware | Function middleware or injectable class |
| constant / constants | Exported constants |
| enum | Enum declarations |
| config | registerAs() functions or config objects |
| schema / model | Mongoose / GraphQL mixed forms |
| validator | @ValidatorConstraint class or validation functions |
| util / utils / helper / helpers | Pure helper functions |
| mock / fixture | Test helpers |
Content binding decisions:
- DTOs must be classes.
class-validatorrelies on runtime objects;interface/typeDTOs compile away and can silently bypassValidationPipe. *.interface.tsand*.type.tsmust not contain classes.- Use
*.interface.tsforimplements, declaration merging,extends, and ordinary object shapes. - Use
*.type.tsfor union, intersection, mapped, conditional, tuple, and utility types. - Do not use
*.d.tsfor project domain types. Reserve*.d.tsfor ambient declarations such as third-party typings,declare global, anddeclare module. - Do not centralize project types in a global
types/folder; co-locate them with the owning module. - Base names must be kebab-case:
create-user.dto.ts, notCreateUser.dto.ts.
Test suffixes:
| Type | Suffix | Reason |
| --------- | -------------------------- | ---------------------------------------- |
| Unit test | *.spec.ts | Matches Nest's default Jest testRegex |
| E2E test | *.e2e-spec.ts | Matches Nest's e2e Jest config |
| Forbidden | *.test.ts / *.tests.ts | Can be silently skipped in Nest projects |
| Option | Type | Default | Description |
| ------------------ | -------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------- |
| files | string[] | ['src/**/*.ts'] | Globs the rules apply to. |
| ignores | string[] | [] | Globs to exclude. |
| suffixes | Record<string, { kind; decorator? }> | — | Merge on top of the default suffix table (add project suffixes / relax decorators). |
| testSuffix | 'spec' \| 'test' \| false | 'spec' | Which single test convention to enforce; false disables. |
| kebabCase | boolean | true | Require kebab-case base names. |
| enforceDecorator | boolean \| 'warn' | 'warn' | true => error, false => off. Start at warn to surface noise from abstract bases before promoting to error. |
Test files (
*.spec.ts,*.e2e-spec.ts) and suffix-less files (index.ts,main.ts) are exempt fromallowed-suffixandsuffix-kind. Identifier naming is handled separately by the base config: classes, interfaces, type aliases, enums, enum members, and type parameters usePascalCase; interfaces cannot use HungarianIprefixes.
Options
import { defineConfig } from '@cognirail/eslint-config';
export default defineConfig({
// Path to tsconfig.json directory (default: process.cwd())
tsconfigRootDir: import.meta.dirname,
// Additional ignore patterns (extends defaults, doesn't replace)
ignores: ['**/generated/**', '**/proto/**'],
// Additional abbreviations for unicorn/prevent-abbreviations (merged with defaults)
abbreviations: { src: true, dest: true },
// Additional prevent-abbreviations ignore patterns (merged with defaults)
abbreviationIgnore: ['(^|[._-])api($|[._-])'],
// Additional file globs where devDependency imports are allowed (merged with defaults)
testFiles: ['**/__tests__/**', '**/e2e/**'],
// Rule overrides (applied last). Keep these for explicit project exceptions,
// not for lowering the guardrail baseline.
overrides: {
'sonarjs/cognitive-complexity': ['error', 20],
},
// Falls back to strict + stylistic (non-type-aware variants),
// still provides substantial linting without a tsconfig.json.
disableTypeChecked: false,
});Default Ignores
These glob patterns are always ignored: dist/**, build/**, output/**, node_modules/**, coverage/**, .next/**, .nuxt/**, .output/**.
JS/CJS File Handling
JS files (*.js, *.mjs, *.cjs) automatically have type-checked rules and return-type requirements disabled. CommonJS files (*.cjs, *.cts) parse with sourceType: 'commonjs', so the config works in mixed TS/JS projects out of the box.
Inline Config Policy
Inline ESLint config comments are disabled via linterOptions.noInlineConfig, and ai-guardrails/no-inline-eslint-config reports ESLint directive comments as errors. AI-generated code should not be able to silence guardrails with eslint-disable comments. Use ignores for generated output and explicit top-level overrides for rare project-level exceptions.
Prettier Config
The bundled Prettier config uses these settings:
{
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100,
"arrowParens": "always",
"bracketSpacing": true,
"bracketSameLine": true,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "ignore",
"vueIndentScriptAndStyle": true
}AI Guardrail Rules
Key rules that catch common AI-generated code issues:
| Rule | AI Problem | Effect |
| -------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------- |
| @typescript-eslint/no-floating-promises | AI writes someAsync() without await | Forces promise handling |
| @typescript-eslint/no-misused-promises | AI passes async to non-promise-expecting APIs | Catches async misuse |
| @typescript-eslint/explicit-function-return-type | AI omits return types | Forces explicit declarations |
| @typescript-eslint/no-explicit-any | AI falls back to any | Enforces proper typing |
| @typescript-eslint/consistent-type-imports | AI mixes value/type imports | Forces import type {} |
| @typescript-eslint/prefer-nullish-coalescing | AI writes x \|\| fallback instead of x ?? fallback | Prevents falsy-value bugs |
| @typescript-eslint/prefer-optional-chain | AI writes manual if (a && a.b) chains | Enforces a?.b |
| @typescript-eslint/strict-boolean-expressions | AI relies on truthy/falsy coercion | Forces explicit boolean intent |
| @typescript-eslint/switch-exhaustiveness-check | AI misses union cases | Forces exhaustive discriminated-union handling |
| ai-guardrails/no-inline-eslint-config | AI tries to silence lint with comments | Forces exceptions into reviewed config |
| sonarjs/cognitive-complexity (<=15) | AI writes deeply nested logic | Limits function complexity |
| unicorn/prefer-node-protocol | AI writes import fs from 'fs' | Forces node:fs prefix |
| unicorn/no-array-for-each | AI uses .forEach() | Enforces for...of |
| curly / eqeqeq | AI writes terse or coercive conditionals | Forces explicit blocks and equality |
| no-console / no-debugger | AI leaves ad-hoc diagnostics in production paths | Blocks stray debugging artifacts |
| prefer-const / no-var | AI uses let or var unnecessarily | Forces immutable bindings |
Notable Unicorn Customizations
prevent-abbreviations: Allows common short names:args,ctx,db,e2e,Env,env,err,fn,params,Prod,prod,props,ref,req,res,util,utils; ignores conventional*.e2e.*filename segmentsno-null: Disabled — Node.js/TS ecosystem usesnullpervasivelyno-array-reduce: Disabled —reduceis idiomatic for data aggregationprefer-module: Disabled — supports mixed CJS/ESM codebasesprefer-top-level-await: Disabled formain.ts— CommonJS entry files cannot use top-level awaitfilename-case: Allows kebab-case, camelCase, and PascalCase
Notable TypeScript Customizations
no-extraneous-class: Allows classes with decorators — required by NestJS (@Module), Angular (@NgModule), and similar frameworksrestrict-template-expressions: Allowsnumberin template literals — safe and ubiquitous in practicestrict-boolean-expressions: Disallows truthy/falsy shortcuts for strings, numbers, nullable values, and objects; write the actual conditionswitch-exhaustiveness-check: Requires discriminated-union switches to handle every case; redundantdefaultcases do not hide missing variantsnaming-convention: EnforcesPascalCasefor interfaces and type aliases; forbids Hungarian notation prefixes (IUser→User,TProps→Props)
Notable Node.js Plugin Customizations
no-missing-import: Disabled —eslint-plugin-import-xwith TypeScript resolver already covers thisno-process-exit: Disabled —process.exit()is valid in CLI tools and application entry pointsno-unpublished-import/no-unpublished-require: Disabled for test, spec, and config files — devDependencies imports are expected there
Notable Import-x Customizations
named: Disabled for TypeScript files — TypeScript compiler already validates named exports
Notable SonarJS Customizations
cognitive-complexity: Set to 15 (default is 10)no-nested-functions: Disabled — closures and callbacks are core JavaScript/TypeScript patternstodo-tag/fixme-tag: Set towarn— allows TODO markers during development without blocking CI- Cross-plugin dedup: 9 rules disabled where
typescript-eslintalready provides equivalent checks:no-array-delete,prefer-regexp-exec,deprecation,argument-type,different-types-comparison,no-try-promise,no-async-constructor,function-return-type,redundant-type-aliases
Environment
Targets modern Node.js runtimes (^20.19.0 || ^22.13.0 || >=24) with ecmaVersion: 'latest' and Node + ES2025 globals by default. Uses eslint-plugin-n's flat/recommended-module preset (ESM-first), with *.cjs and *.cts parsed as CommonJS.
VSCode Setup
// .vscode/settings.json
{
"eslint.useFlatConfig": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
},
}Recommended extensions: ESLint, Prettier, Error Lens.
Migration from @me-tool
@cognirail/eslint-config is the new package name for @me-tool/eslint-prettier-ts-config.
pnpm remove @me-tool/eslint-prettier-ts-config
pnpm add -D @cognirail/eslint-configThen replace imports and Prettier references:
- import { defineConfig } from '@me-tool/eslint-prettier-ts-config';
+ import { defineConfig } from '@cognirail/eslint-config';
- "prettier": "@me-tool/eslint-prettier-ts-config/prettier"
+ "prettier": "@cognirail/eslint-config/prettier"Migration from v1
v2 is a breaking change:
Update the package
pnpm add -D @cognirail/eslint-config@2 eslint@latest pnpm remove @me-tool/eslint-prettier-config # remove old baseReplace config file: Delete
.eslintrc.js/.eslintrc.json, createeslint.config.mjsas shown in Quick Start.Update Prettier:
- "prettier": "@me-tool/eslint-prettier-ts-config/.prettierrc.js" + "prettier": "@cognirail/eslint-config/prettier"Update lint-staged: Remove
git addfrom commands (auto-staged since lint-staged v10).{ "lint-staged": { "*.{ts,mts}": ["eslint --fix", "prettier --write"], "*.{js,mjs,cjs}": ["eslint --fix", "prettier --write"], "*.{json,md,yml,css}": ["prettier --write"] } }
New rules in v2
v2 adds significantly more rules than v1. Expect new lint errors on existing code, particularly from:
eslint-plugin-unicorn(modern JS patterns)eslint-plugin-sonarjs(cognitive complexity)@typescript-eslintstrict type checks (no-floating-promises,explicit-function-return-type, etc.)
Run eslint --fix . to auto-fix what's possible, then address remaining issues manually.
License
ISC
