@gpahal/eslint-config
v0.0.147
Published
Composable flat ESLint config for TypeScript, React, Next.js, Astro, Tailwind CSS, Node, HTML, Turbo, Vitest, Playwright, Storybook, JSON, YAML, TOML and Markdown
Maintainers
Readme
@gpahal/eslint-config
Composable flat ESLint config for TypeScript, React, Next.js, Astro, Tailwind CSS, Node, HTML, Turbo, Vitest, Playwright, Storybook, JSON, YAML, TOML and Markdown.
- Install
- Quick start
- Presets at a glance
- Common tasks
- Recipes
- Migrating
- Troubleshooting
- Reference
- [Setups][]
- [Options][]
- [Lists and merging][]
- [Config order][]
- [Composer methods][]
- [Where your configs apply][]
- [Preset scopes][]
- [Browser and server code][]
- [Typed config files][]
- [Startup cost][]
- What's on by default
- [Duplicate rules][]
- Own rules
- Markdown rules
- Known issues
Install
pnpm add -D @gpahal/eslint-config eslint typescript@~6.0.3Requires Node 24.22 or later and eslint ^10.11. typescript ~6.0.3 is an optional peer that every project but a JavaScript-only one needs. TypeScript 7 is not supported right now. defineConfig checks the installed versions when the config loads and throws with the install command when one is missing or out of range, as in peer dependencies: needs eslint ^10.11, but 10.0.0 is installed. A newer ESLint loads with a GPAHAL_PEER_NEWER_THAN_RANGE warning.
The root README's Supported setups explains why to pin typescript@~6.0.3 and skip unrs-resolver's build script with pnpm 12.
Each preset needs its plugins, which are optional peer dependencies with their ranges in peerDependencies:
| Option | Install |
| --- | --- |
| react | @eslint-react/eslint-plugin, eslint-plugin-react-hooks, eslint-plugin-jsx-a11y-x, and eslint-plugin-react-refresh unless refresh is false |
| nextjs | The react plugins and @next/eslint-plugin-next |
| astro | eslint-plugin-astro, and eslint-plugin-jsx-a11y-x where ESLint runs, since eslint-plugin-astro loads it from the working folder. In a monorepo linted from its root, install it at the root. Without it there, the astro/jsx-a11y/* rules are off, with a warning |
| tailwindcss | eslint-plugin-better-tailwindcss, tailwindcss |
| html | @html-eslint/parser, @html-eslint/eslint-plugin |
| turbo | eslint-plugin-turbo |
| vitest | @vitest/eslint-plugin |
| playwright | eslint-plugin-playwright |
| storybook | eslint-plugin-storybook |
| antislop | eslint-plugin-slop, unless slop is false |
| node | Nothing extra |
A missing plugin, or one older than its range, fails the config with an error that names the option and the install command. One newer than the range loads, with a warning once per version.
Quick start
Install the base packages, Node's types and the plugins for the React and Vitest presets:
pnpm add -D @gpahal/eslint-config eslint typescript@~6.0.3 @types/node@24 @eslint-react/eslint-plugin \
eslint-plugin-react-hooks eslint-plugin-jsx-a11y-x eslint-plugin-react-refresh @vitest/eslint-plugin// eslint.config.mjs
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
react: true,
vitest: true,
unicorn: { rules: { 'unicorn/filename-case': ['error', { case: 'camelCase' }] } },
jsdoc: false,
})// tsconfig.json
{
"compilerOptions": { "strict": true, "jsx": "react-jsx", "types": ["node"] },
"include": ["src"]
}root is the project folder, usually the config file's folder, and every path and glob option is relative to it. An area is one group of rules with its own option, such as unicorn: its rules reach every file the area lints, and false turns it off. Every rule the presets turn on is an error, including those that plugins' recommended configs set to warn, except vitest/no-disabled-tests, since a skipped test is often deliberate.
Type-aware linting needs a tsconfig.json that includes your TypeScript files. Config files that no tsconfig includes, such as eslint.config.mjs, get type information from the default project, so list node in types, as Give config files Node types explains. Set "type": "module" in package.json too, or defineConfig warns that Node runs .js files as CommonJS.
The root README's setup recipes add Prettier, Stylelint and @gpahal/tsconfig, and Recipes cover common frameworks.
Presets at a glance
The presets are off by default. Install lists each preset's plugins and [Options][] lists every option.
| Option | Default | Turns on | Sub-options and their defaults |
| --- | --- | --- | --- |
| typeAware | On | Type information from typescript-eslint's project service, and the rules that need it | javascript: false, so JavaScript files are linted without it. allowDefaultProject: the config files that no tsconfig includes, as [Typed config files][] explains |
| typescript | Always on | typescript-eslint's strict and stylistic type-checked configs | library: false: true requires declared types on exported functions and methods, as TypeScript's isolatedDeclarations does, outside tests, configs and scripts. exhaustiveSwitch: 'strict': a switch over a union must list every member even with a default case; 'default' lets default cover them. strictBooleanExpressions: true: no any, numbers or nullable strings or numbers in conditions. rulesTypeAware: rules for the files with type information |
| imports | On | eslint-plugin-import-x | removeUnused: false: true makes --fix remove unused imports, outside an editor. cycles: false: true, or { maxDepth }, turns on import-x/no-cycle |
| e18e | On | @e18e/eslint-plugin | modernization: true, performanceImprovements: true, moduleReplacements: false: true turns on e18e/ban-dependencies |
| markdown | On | .md files and their code blocks | math: false: true parses $ and $$ math |
| react | Off | @eslint-react, React Hooks and jsx-a11y-x rules, and browser globals | scope: every React file. refresh: true: react-refresh/only-export-components, which keeps hot reloading working. compiler: detected; with it on, the React Compiler's rules run, such as react-hooks/incompatible-library |
| nextjs | Off | react, the Next.js recommended and Core Web Vitals rules, and @gpahal/nextjs-server-component | scope: react.scope. rootDir: '.': the apps' folders, which set scope when it's unset |
| astro | Off | eslint-plugin-astro's recommended and jsx-a11y rules for .astro files | rules |
| tailwindcss | Off | eslint-plugin-better-tailwindcss | entryPoint: required, the Tailwind CSS entry stylesheet. settings: the plugin's other settings. scope: every code file, and .astro and .html files with astro and html |
| node | Off | eslint-plugin-n's recommended rules | scope: every code file. typeStripping: false: true reports what Node's type stripping can't run, and .js specifiers of .ts files |
| html | Off | html-eslint's recommended rules for .html files, without layout ones | rules |
| turbo | Off | Environment variables missing from turbo.json | rules |
| vitest | Off | Vitest's rules and globals | files: testFiles, without Playwright's files |
| playwright | Off | eslint-plugin-playwright's recommended rules | files: the code files in e2e and playwright folders, and *.e2e.* files |
| storybook | Off | eslint-plugin-storybook's recommended rules, for Storybook 9 or later | files: *.stories.* files and the code files in .storybook folders |
| antislop | Off | eslint-plugin-slop's rules and sonarjs's rules for redundant, duplicated and dead code | slop: true, sonarjs: true. cognitiveComplexity: 25: the limit of sonarjs/cognitive-complexity, or false for none |
| browser, server | Off | Browser globals and reports of Node's, or reports of browser globals | files: required |
| commonjs | Off | Lints .js files as CommonJS | files: every .js file |
react.compiler is on when root's package.json, or a workspace package's, lists babel-plugin-react-compiler or react-compiler-runtime in dependencies or devDependencies. Set it to true or false to override the detection. The other React Hooks rules always run. react-refresh/only-export-components allows React Router's route-module exports, such as loader and meta, in every React file, and Next.js's page exports in nextjs's files.
Common tasks
Change or turn off a rule
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
javascript: { rules: { 'no-console': ['error', { allow: ['info', 'warn', 'error'] }] } },
typescript: { rules: { '@typescript-eslint/no-explicit-any': 'off' } },
})An area's rules take only that area's rules, such as unicorn/* for unicorn.rules, and an unknown rule throws. They come before the exemptions, the configs that turn rules off for kinds of files, such as non-null assertions in tests, so they don't turn an exempted rule back on. Use configs for that, as [Config order][] explains.
Turn a rule off for tests
import { defineConfig, files } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
configs: [{ files: [...files.test], rules: { 'sonarjs/no-hardcoded-passwords': 'off' } }],
})configs come after the presets and the exemptions, so they win. files holds the default globs of each kind of file; files.test is the default testFiles. [Where your configs apply][] explains which files each rule of your config reaches.
Add ignores and test files
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
ignores: (defaults) => [...defaults, 'fixtures/**'],
testFiles: (defaults) => [...defaults, 'e2e/**/*.ts'],
})ignores and testFiles take a function, which gets a copy of the defaults, or an array, which replaces them. A ! glob un-ignores in ignores, as in '!**/dist', and leaves out files in testFiles. Unlike antfu's config, ignores: ['dist'] replaces the defaults, so ESLint lints lockfiles and build output again; pass a function to add to them.
Limit a preset to part of a monorepo
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
react: { scope: ['apps/web/**'] },
nextjs: { rootDir: ['apps/web'] },
node: { scope: ['apps/api/**', 'scripts/**'] },
})scope limits react, nextjs, node and tailwindcss to matching files. A ! glob excludes files, as in ['apps/web/**', '!apps/web/vendor/**']. [Preset scopes][] explains how react and nextjs share their scopes.
Set up a monorepo
// eslint.config.mjs in the monorepo's root
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
react: { scope: ['apps/web/**'] },
configs: [{ files: ['packages/cli/**'], rules: { 'no-console': 'off' } }],
})Use one config at the workspace root. typescript-eslint keeps one project service per process, so configs per package conflict in one ESLint run or editor server when their config files, compiler options or type packages differ, as [Typed config files][] explains. Limit presets to packages with scope, and set package rules with configs and files.
With a config per package, import-x/no-extraneous-dependencies reads each package's own package.json, so list the tools that a package's configs and tests import, such as vitest, in its devDependencies.
Separate browser and server code
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
browser: { files: ['src/client/**'] },
server: { files: ['src/app/api/**', 'src/server/**'] },
})browser gives its files browser globals, such as document, and reports Node's globals and built-in modules in them. Without browser, react, nextjs or astro, code files have only Node's globals. server reports browser globals, including localStorage, sessionStorage and Storage, which Node defines only behind a flag, and wins for a file both match. [Browser and server code][] covers typeof checks, browser ports of built-ins and process.env.
Override with the composer
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({ root: import.meta.dirname })
.overrideRules({ '@typescript-eslint/no-unused-vars': 'warn' })
.override('gpahal/javascript/rules', { ignores: ['vendor/**'] })
.remove('gpahal/fixes/report-only')
.append({ files: ['scripts/**'], rules: { 'no-console': 'error' } })Overrides through configs or composer methods get validation and keep the final guards, such as the config that turns formatting rules off. [Composer methods][] lists the methods.
Register a plugin of your own
import somePlugin from 'eslint-plugin-some'
import { defineConfig, untyped } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
configs: [
{ plugins: { some: somePlugin }, rules: { 'some/rule': 'error' } },
untyped({ rules: { 'unicorn/newer-rule': 'error' } }),
],
})A config that registers plugins takes any rule name, and untyped lets a config name a rule newer than the generated types, such as unicorn/newer-rule here. The rule must exist in the loaded plugin. A plugin's preset that turns on formatting rules throws with every such rule in one error; pass it through withoutFormattingRules, which returns copies without the formatting rules that allowFormattingRules describes, as in configs: withoutFormattingRules(somePlugin.configs.recommended).
Speed up startup
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
typeAware: { allowDefaultProject: ['packages/*/vitest.config.ts'] },
sonarjs: false,
})An explicit typeAware.allowDefaultProject array skips parsing the tsconfigs, and replaces the detected list, so list every such file. sonarjs: false skips loading eslint-plugin-sonarjs, at the cost of its bug rules. The folder walk still runs, for gitignore and imports. [Startup cost][] has the measurements.
Lint some files without type information
// eslint.config.mjs
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
typeAware: { ignores: ['src/generated/**', 'legacy/**'] },
})typeAware.ignores lists code files to lint without type information, as JavaScript files are, such as generated code or a folder that no tsconfig includes. The type-aware rules are off in them, and the core rules that typed rules replace, such as no-implied-eval, run instead. The project service never opens them, so they get no was not found by the project service error and don't count toward the [default project's limit][Typed config files]. A ! glob leaves out files, as in ['src/generated/**', '!src/generated/typed/**'].
Give config files Node types
// tsconfig.json in root
{ "compilerOptions": { "types": ["node"] }, "include": ["src"] }Config files that no tsconfig includes get type information from a default project with the compiler options of root's tsconfig.json. List node in its types and install @types/node; otherwise defineConfig warns, and type-checked rules report on the config files. A types list replaces the inherited one, so keep inherited entries, as in "types": ["vite/client", "node"]. A solution-style tsconfig.json with "files": [] and references, as in Vite's templates, needs the same compilerOptions.
Lint a JavaScript project
// eslint.config.mjs
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({ root: import.meta.dirname, typeAware: { javascript: true } })// jsconfig.json
{
"compilerOptions": { "jsx": "react-jsx", "types": ["node"] },
"include": ["src", "*.config.mjs"]
}Without typeAware.javascript, JavaScript files are linted without type information, and a project without TypeScript files or a tsconfig.json or jsconfig.json at root needs only defineConfig({ root: import.meta.dirname }). With it, the type-checked rules run on JavaScript files, which must be in a jsconfig or tsconfig. The default project reads only root's tsconfig.json, so the jsconfig includes the config files. For .jsx files, install @types/react, or the no-unsafe-* rules report their JSX.
A project with no TypeScript file outside the ignores and dot folders, no tsconfig.json or jsconfig.json at root and no typeAware.javascript is linted without loading typescript-eslint. JavaScript files get ESLint's parser and the core twins of typescript-eslint's rules, such as no-unused-vars. astro, typeAware.allowDefaultProject, typescript.rulesTypeAware, an @typescript-eslint/* rule in typescript.rules or a config in configs that names typescript-eslint loads it. A composer method can't load it, so name typescript-eslint rules in configs.
Lint a CommonJS package
With "type": "commonjs" in package.json, as npm init -y writes it, Node runs .js files as CommonJS, while the configs lint them as ES modules. commonjs: true lints them as .cjs files: as scripts with Node's CommonJS globals, require() allowed and ESM syntax reported by @gpahal/no-esm-in-commonjs. With node, they get eslint-plugin-n's script config. Without a commonjs option, such a package with .js or .jsx files gets a GPAHAL_COMMONJS_PACKAGE warning.
// eslint.config.js
const { defineConfig } = require('@gpahal/eslint-config')
module.exports = defineConfig({ root: __dirname, commonjs: true })commonjs: { files: ['scripts/**'] } makes only those .js files CommonJS. Other files a glob matches, such as scripts/a.ts, stay ES modules, and a glob that can't match a .js file throws.
Find the config of a file
npx eslint --print-config README.md/1_0.ts--print-config prints the merged config of one path, here the first code block of a Markdown file, which ESLint lints as a virtual file. Compare the rules of two paths to see what an exemption or your config changes. npx @eslint/config-inspector shows every config by name, with the files it matches and the rules it sets.
Adopt in an existing codebase
// eslint.config.mjs
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
typescript: {
rules: {
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/method-signature-style': 'off',
},
},
unicorn: { rules: { 'unicorn/filename-case': 'off' } },
commentLayout: false,
})These five conventions often report on existing TypeScript code and find no bugs. Turn them off in a codebase with other conventions, and keep them for a new project. Most of their reports have fixes, so to adopt one later, turn it on and run eslint --fix. For the other rules' reports, see the root README's Adopt in an existing codebase.
Recipes
Each recipe adds ESLint to a project that a framework's starter created. The root README's setup recipes add Prettier, Stylelint and the scripts that run the fixers in order.
Next.js
For an app from create-next-app, without its ESLint setup:
pnpm add -D @gpahal/eslint-config @gpahal/tsconfig eslint typescript@~6.0.3 @types/node@24 @eslint-react/eslint-plugin \
eslint-plugin-react-hooks eslint-plugin-jsx-a11y-x eslint-plugin-react-refresh @next/eslint-plugin-next// eslint.config.mjs
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({ root: import.meta.dirname, nextjs: true })// tsconfig.json
{
"extends": "@gpahal/tsconfig/nextjs-app",
"compilerOptions": { "paths": { "@/*": ["./*"] } }
}// package.json
{
"type": "module",
"scripts": {
"fix:eslint": "next typegen && eslint --fix .",
"check:eslint": "next typegen && eslint --max-warnings 0 .",
"check:types": "next typegen && tsc --noEmit"
}
}next typegen writes the route types, such as PageProps<'/blog/[slug]'>, into .next/types, which a fresh checkout lacks; without them, the type-aware rules report every use as an error type. @gpahal/tsconfig/nextjs-app lists node in types; with a tsconfig of your own, set "types": ["node"].
create-next-app sets up Tailwind CSS by default. Then also install eslint-plugin-better-tailwindcss, pass tailwindcss: { entryPoint: 'app/globals.css' }, and use the Tailwind options of the Prettier and Stylelint configs, as the root README's setup recipes do. The template's own code then gets reports too:
- The starter's classes get
better-tailwindcss/enforce-canonical-classesandbetter-tailwindcss/no-deprecated-classesreports, such asroundedforrounded-sm. Both rules are report-only, so write the classes they suggest. - Stylelint reports
--font-geist-sansand--font-geist-monoinapp/globals.csswithno-unknown-custom-properties, sincenext/fontsets them at runtime. Declare them in a stub that nothing imports, and passreferenceFiles: ['app/fonts.css']to the Stylelint config, as its custom property reports section shows:
/* app/fonts.css */
:root {
--font-geist-sans: initial;
--font-geist-mono: initial;
}Vite with React, Vitest and Tailwind CSS
For an app from create-vite's react-ts template, with Tailwind CSS through @tailwindcss/vite and @import 'tailwindcss' in src/index.css:
pnpm add -D @gpahal/eslint-config eslint typescript@~6.0.3 @types/node@24 @eslint-react/eslint-plugin eslint-plugin-react-hooks \
eslint-plugin-jsx-a11y-x eslint-plugin-react-refresh vitest @vitest/eslint-plugin tailwindcss @tailwindcss/vite \
eslint-plugin-better-tailwindcss// eslint.config.mjs
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
react: true,
vitest: true,
tailwindcss: { entryPoint: 'src/index.css' },
})// tsconfig.json
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
"compilerOptions": { "types": ["node"] }
}The template's tsconfig.json is a solution that references the app's and the config files' projects. eslint.config.mjs is in neither, so it gets the default project, with the compiler options of tsconfig.json. The template's own code gets reports too:
App.tsxgetsunicorn/filename-case, since component files are kebab-case too: rename itapp.tsx, or set the rule's options inunicorn.rules.App.tsxgetsbetter-tailwindcss/no-unknown-classesfor the classes thatApp.cssdefines, and@eslint-react/dom-no-unsafe-target-blankfor itstarget="_blank"links withoutrel.main.tsxgetsno-non-null-assertionandunicorn/prefer-query-selector.
A static site
For HTML files with scripts in a js folder, and no TypeScript:
pnpm add -D @gpahal/eslint-config eslint typescript@~6.0.3 @html-eslint/parser @html-eslint/eslint-plugin// package.json
{ "private": true, "type": "module" }// eslint.config.mjs
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
html: true,
browser: { files: ['js/**'] },
})"type": "module" lints the scripts as ES modules, and "private": true keeps the rules for published packages off. browser gives the scripts browser globals; without it, they get no-undef on every one.
Storybook and Playwright
For the Vite app above, with Storybook 9 or later and the Playwright tests that create-playwright writes to tests:
pnpm add -D eslint-plugin-storybook eslint-plugin-playwright// eslint.config.mjs
import { defineConfig } from '@gpahal/eslint-config'
export default defineConfig({
root: import.meta.dirname,
react: true,
vitest: true,
tailwindcss: { entryPoint: 'src/index.css' },
storybook: true,
playwright: { files: ['tests/**'] },
})Add the new files to the template's projects: "include": ["src", ".storybook"] in tsconfig.app.json, and "include": ["vite.config.ts", "vitest.shims.d.ts", "playwright.config.ts", "tests"] in tsconfig.node.json. TypeScript's ** skips dot folders, so the tsconfig names .storybook.
The default playwright.files are the code files in e2e and playwright folders, so create-playwright's testDir: './tests' needs files: ['tests/**'], or GPAHAL_PLAYWRIGHT_FILES_UNMATCHED warns. Vitest's rules leave out Playwright's files. create-playwright's playwright.config.ts gets strict-boolean-expressions on process.env.CI: compare with undefined, as in retries: process.env.CI === undefined ? 0 : 2.
Migrating
From @antfu/eslint-config
- Rules keep their plugins' own names, and there's no
renamePlugins:ts/rules are@typescript-eslint/,node/rulesn/,test/rulesvitest/,yaml/rulesyml/, and imports useimport-x/. Rename them in your configs and disable comments. - Prettier formats, so there are no stylistic rules. A config that turns on a formatting rule throws unless
allowFormattingRuleslists it. - A preset's
overridesare its area'srules, as intypescript: { rules: { … } }. Rules for other files go inconfigs, withfiles. ignores: [...]replaces the default ignores; pass a function, as inignores: (defaults) => [...defaults, 'dist'].rootis required, and presets are off until you set them.defineConfigwarns of a framework inpackage.jsonwhose option isn't set.- antfu's
lessOpinionatedhas no option here; Adopt in an existing codebase turns off the noisiest conventions.
From eslint-config-next or next lint
Next.js 16 removed next lint. Uninstall eslint-config-next, delete the config that extends it, and use nextjs: true. Run next typegen && eslint . in place of next lint, so the route types exist before the type-aware rules read them. Set nextjs.rootDir when the app isn't at root. Next.js has the full setup.
From typescript-eslint and eslint-config-prettier
Replace tseslint.config(...), parserOptions.projectService and tsconfigRootDir with defineConfig({ root: import.meta.dirname }). Drop eslint-config-prettier: the presets leave out the rules it turns off. Move your rules into the areas' rules or into configs with files, and your languageOptions.globals into browser, server or configs.
Troubleshooting
defineConfig prints each setup warning once per process, with a code. Fix the cause, or silence the code with the warnings option, as in warnings: { GPAHAL_COMMONJS_PACKAGE: false }.
Warnings
| Code | Cause | Fix |
| --- | --- | --- |
| GPAHAL_FRAMEWORK_OPTION_UNSET | A package.json lists a framework or tool, such as next or vitest, whose option isn't set | Set the option, and install its plugins, or set it to false |
| GPAHAL_COMMONJS_PACKAGE | root's package.json has "type": "commonjs" or no "type", so Node runs .js files as CommonJS | Set "type": "module", commonjs: true, or commonjs: false |
| GPAHAL_NO_TSCONFIG | No tsconfig.json at or under root, so the project service reports each TypeScript file as not found, and config files get no Node types | Add a tsconfig.json at root with "types": ["node"], or set typeAware: false |
| GPAHAL_FILES_OUTSIDE_TSCONFIG | TypeScript files, such as scripts/*.ts, that no tsconfig includes and the default project doesn't take, so the project service reports each as not found. It names up to 10 | Add them to a tsconfig's include, or to typeAware.allowDefaultProject |
| GPAHAL_DEFAULT_PROJECT_NODE_TYPES | Files in the default project lack Node's types: root's tsconfig.json is missing, fails to parse, includes no files or doesn't list node in types, or @types/node isn't installed | The fix the warning names, usually "types": ["node"], keeping inherited entries, and @types/node |
| GPAHAL_LARGE_DEFAULT_PROJECT | More than 16 files in the default project, each about 8 times slower to type than a file in a tsconfig | Include them in a tsconfig, list only some in typeAware.allowDefaultProject, or ignore folders such as fixtures |
| GPAHAL_ASTRO_TYPES_MISSING | tsconfig.json, or a tsconfig it extends, includes .astro/types.d.ts, which doesn't exist, as in a fresh checkout | Run astro sync before ESLint, as in astro sync && eslint . |
| GPAHAL_ASTRO_JSX_A11Y_MISSING | eslint-plugin-jsx-a11y-x isn't installed where ESLint runs, so the astro/jsx-a11y/* rules are off | Install it there, at the root of a monorepo |
| GPAHAL_PLAYWRIGHT_FILES_UNMATCHED | playwright.config.* sets a testDir whose tests playwright.files doesn't match | Set playwright: { files: ['tests/**'] }, the glob the warning names |
| GPAHAL_GITIGNORE_WILDCARDS | A .gitignore rule has more than 3 * in one path segment, which ESLint can take seconds per path to match, so it's skipped | Rewrite the rule with at most 3 |
| GPAHAL_ROOT_GITIGNORED | A .gitignore or info/exclude rule ignores root or a folder above it, so git ignores every file. The warning says whether ESLint, which gets the rule rebased onto root, does too | Un-ignore the folder with a ! rule, or set gitignore: false |
| GPAHAL_PEER_NEWER_THAN_RANGE | ESLint or a preset's plugin is newer than the tested range, so its rules may break | Install a version in the range if they do |
| GPAHAL_SONARJS_TYPESCRIPT_COPY | eslint-plugin-sonarjs loads another TypeScript copy than typescript-eslint, so its type-aware rules can misreport | Run pnpm dedupe or npm dedupe. With pnpm and typescript installed as an alias, add the override that the warning names to overrides in pnpm-workspace.yaml |
Errors
peer dependencies: needs eslint ^10.11, but 10.0.0 is installed, ortypescript ~6.0.3: run the install command the error names. TypeScript 7 is not supported right now.<file> was not found by the project service: a TypeScript file that no tsconfig includes, whichGPAHAL_FILES_OUTSIDE_TSCONFIGnames. Add it to a tsconfig'sincludeor totypeAware.allowDefaultProject. A file in a dot folder, such as.storybook/main.ts, needs the folder named ininclude.Too many files (>N) have matched the default project: more files matchedtypeAware.allowDefaultProjectthan the limitdefineConfigset, which typescript-eslint keeps for the process. A function's globs count only files outside dot folders, so list a dot-folder glob in an array. In an editor, run ESLint: Restart ESLint Server after adding config files.Unsafe assignment of an error typed valuein a config file: the default project lacks Node's types or the package's types. Follow theGPAHAL_DEFAULT_PROJECT_NODE_TYPESwarning.`<file>` is in the default project, and a tsconfig project also includes it through an import: add the file to that tsconfig'sinclude, or filter it out oftypeAware.allowDefaultProjectwith the function the error shows.the config of <root> changed in this process, afterproject service: an editor's ESLint server reloaded an edited config, and typescript-eslint keeps the first project service. Restart the server, or run ESLint again.the config of <root> sets … in the config of <other root>, whose project service this process already uses: one run loads configs of several packages whose project services differ. Use one config at the workspace root, as [Set up a monorepo][] explains.ESLint reaches <file> through <path>, another path to <root>: ESLint runs through a symbolic link or another case ofroot's path. Lint throughroot, as ineslint src, pass the real path,import.meta.dirname, asroot, and in an editor open the folder at the pathrootnames.Unexpected undescribed directive commentat column 0:require-descriptionreports a directive without a reason at column 0 on purpose, so no directive on that line can hide the report. Add the reason, as in// eslint-disable-next-line no-console -- CLI output.is a rule that eslint-config-prettier turns offoris a layout rule for files that Prettier formats: your config turns on a formatting rule. Turn it off, list it inallowFormattingRules, or pass a plugin's preset throughwithoutFormattingRules.is a rule that needs type information, withtypeAware: false: remove the rule ortypeAware: false.needs type information, which <glob> files don't get: your config turns on a type-aware rule only for files without type information. Apply the fix the error names, or remove the rule.Internal error: Atomics.wait() failed: timed-out, with abetter-tailwindcss/*rule: eslint-plugin-better-tailwindcss loads Tailwind CSS in worker threads and gives each call 30 seconds, which a busy machine, such as a CI runner with several ESLint processes, can exceed. Raise it with theSYNCKIT_TIMEOUTenvironment variable, in milliseconds, as inSYNCKIT_TIMEOUT=300000 eslint ..returns a composer, which ESLint reads as a config object: await the composer before putting it in an array orextends, as in...(await defineConfig({ … })).
The root README covers editor setup and the errors of installing and running the tools together.
Reference
Setups
| Project | Options | tsconfig | Type information |
| --- | --- | --- | --- |
| JavaScript | root | None | None |
| Static site | html: true and browser: { files } for its scripts | None | None |
| JavaScript checked with checkJs | typeAware: { javascript: true } | A jsconfig.json that lists node in types and includes the config files | The files it includes |
| TypeScript library | typescript: { library: true } | A tsconfig.json that lists node in types when config files import Node modules | Its files, and the config files through the default project |
| Node scripts or app | node: true, with typeStripping: true when Node runs the .ts sources | @gpahal/tsconfig/node-app | Its files, and the config files through the default project |
| React with Vite | react: true | A solution-style tsconfig.json with "types": ["node"], referencing the app's and the root config files' tsconfigs | The referenced projects' files, and the other config files through the default project |
| Astro | astro: true | The app in tsconfig.json, with "types": ["node"]; run astro sync before ESLint | .ts, .tsx and .astro files, and the root config files through the default project |
| Next.js | nextjs: true, with rootDir when the app isn't at root; run next typegen before ESLint | @gpahal/tsconfig/nextjs-app, which lists node in types | Its files, and the config files that it leaves out through the default project |
| Monorepo, or an editor with several workspace folders | One config at the workspace root, with scope for package presets | One per package | Each package's files through its tsconfig; [one project service][Typed config files] serves them all |
Options
defineConfig returns a composer, which ESLint awaits for the configs. Its options, rules and methods are typed, so a misspelled rule or config name fails to type-check and throws when ESLint loads the config. ESLint loads an eslint.config.ts with jiti installed, or with eslint --flag unstable_native_nodejs_ts_config, and // @ts-check type-checks a JavaScript config. EslintConfigOptions documents every option.
Globs are relative to root. An empty glob, a lone !, a glob that starts with / or .., an absolute Windows glob, such as C:/src/** or //server/share/**, and a glob with a \ separator throw, in the options and in your configs' files and ignores. testFiles, each files option, typeAware.ignores and each scope take brace globs, such as src/{client,shared}/**, and ! globs, which leave out the files they match. A list of only ! globs throws, and so does a glob that can't match the files the option is for. Test and story globs apply only to the code files they match.
| Option | Default | What it does |
| --- | --- | --- |
| root | Required | The project folder, usually import.meta.dirname, the base of every path and glob. It throws when it's relative or isn't an existing folder |
| typeAware | true | Type information for TypeScript files, false to lint without it, or { javascript, allowDefaultProject, ignores }. allowDefaultProject defaults to the config files that no tsconfig lists. ignores lists code files to lint without type information. With false, disable comments for typed rules are reported as unused; remove them, or run ESLint with --report-unused-disable-directives-severity off |
| testFiles | Test files and folders, and test setup files such as vitest.setup.ts and setupTests.ts | Test code, with looser rules and dev dependencies allowed |
| commonjs | Off | Lints .js files as CommonJS, as .cjs files are. true is every .js file, and { files } lists them. false turns off the GPAHAL_COMMONJS_PACKAGE warning |
| ignores | Framework and tool output, such as dist, .next, coverage and storybook-static, Vite's and Vitest's *.timestamp-* config bundles, Yarn's .yarn/releases and .yarn/plugins, generated and minified files, and lockfiles | Globs ESLint ignores. build, out, target and tmp aren't ignored, since they can name source folders; list them in .gitignore |
| gitignore | true | Ignore what the repository's info/exclude and .gitignore files ignore, and its submodule folders, but not git's global core.excludesFile. { files } adds ignore files |
| browser, server | Off | { files, rules } for browser or server code, or false |
| configs | [] | Your own configs, after the presets and exemptions |
| allowFormattingRules | [] | Formatting rules, those eslint-config-prettier turns off and the layout rules of html-eslint and eslint-plugin-astro, such as @html-eslint/indent and astro/semi, that configs, the composer's methods and areas' rules may turn on. Listing one that a preset leaves out, such as unicorn/empty-brace-spaces, keeps it on |
| warnings | {} | The setup warnings to silence, by code, as in { GPAHAL_COMMONJS_PACKAGE: false }. An unknown code or a non-boolean value throws |
| isInEditor | Detected | In an editor, prefer-const reports without fixing. Detection reads the variables that VS Code, JetBrains IDEs, Vim, Neovim, Zed and Emacs's shells set. For editors it misses, such as Emacs's eglot and lsp-mode, Sublime Text's LSP and Helix, set it from a variable of the editor's ESLint server settings, as in isInEditor: process.env.ESLINT_IN_EDITOR === '1' |
| javascript, typescript | Always on | Core and typescript-eslint rules, { rules }. typescript also takes rulesTypeAware, library, exhaustiveSwitch and strictBooleanExpressions, as Presets at a glance describes. rulesTypeAware, exhaustiveSwitch and strictBooleanExpressions throw with typeAware: false |
| imports, unicorn, regexp, sonarjs, commentLayout, e18e, eslintComments, jsdoc, json, yaml, toml, packageJson, markdown | On | Areas, each false or { rules }. imports.removeUnused makes --fix remove unused imports, keeping a value import's module loaded, as [Report-only fixes][] explains. imports.cycles turns on import-x/no-cycle; { maxDepth } limits how many imports deep it follows. It's off by default for its cost. e18e takes modernization and performanceImprovements, on by default, and moduleReplacements, off since e18e/ban-dependencies reports packages such as axios. All three false throws unless rules turns a rule on; use e18e: false |
| pnpm | On in a pnpm workspace: the nearest pnpm-workspace.yaml at or above root, up to the git root, is at root or its packages globs match root | eslint-plugin-pnpm rules. true without pnpm-workspace.yaml at or above root throws |
| react, nextjs, node | Off | Presets that take scope. react takes refresh and compiler, nextjs rootDir, and node typeStripping. nextjs with react: false throws |
| tailwindcss | Off | { entryPoint, settings, scope, rules }, or false. entryPoint, the Tailwind CSS entry stylesheet, is required and must exist, so true throws. It takes a path relative to root, an absolute path, or a file: URL, such as import.meta.resolve('./src/styles.css'). settings are eslint-plugin-better-tailwindcss's other settings; settings.entryPoint throws |
| astro, html, turbo | Off | Presets, true or { rules } |
| vitest | Off | Vitest rules and globals for testFiles, or { files }, but not the files of playwright |
| playwright | Off | eslint-plugin-playwright's recommended rules for end-to-end tests, or { files }. They get the test exemptions, and not Vitest's rules and globals |
| storybook | Off | eslint-plugin-storybook's recommended rules for stories, and storybook/no-uninstalled-addons for .storybook/main.*, or { files }. In stories, react-hooks/rules-of-hooks and import-x/no-anonymous-default-export are off, as in Storybook's config. MDX stories aren't linted. On Storybook 8, storybook/no-renderer-packages reports every story |
| antislop | Off | Rules for code that reads as generated without care, from eslint-plugin-slop and sonarjs, which slop: false and sonarjs: false turn off. slop/no-em-dash skips JSON, YAML and TOML files unless rules turns it on. cognitiveComplexity sets the limit of sonarjs/cognitive-complexity, and throws with sonarjs: false. slop: false and sonarjs: false together throw unless rules turns a rule on; use antislop: false |
gitignore walks every folder under root that git doesn't ignore each time the config loads, so .gitignore edits apply on a reload. The same walk serves imports and the default project, so gitignore: false alone saves little startup time. As with ESLint's includeIgnoreFile, a .gitignore rule also ignores files that git tracks. An unreadable ignore file throws; an unreadable folder is skipped. With core.ignorecase, as on macOS and Windows, the rules match paths in any case, as git matches them.
Invalid options, globs and user configs throw a TypeError. Checks that need plugins, such as missing peers and unknown rules, reject when ESLint awaits the config. The error for missing or outdated peers lists every preset's, with one install command.
GPAHAL_FRAMEWORK_OPTION_UNSET reads the dependencies and devDependencies of root's package.json and of workspace packages; a published package, without "private": true, counts only its dependencies and required peers. Setting the option, even to false, turns the warning off.
packageJson and the manifest checks of pnpm skip **/fixtures/** and **/__fixtures__/**, and so do the rules their rules options turn on.
With node, set engines.node in package.json, or n/no-unsupported-features/* assumes Node >=16.0.0 and reports fetch and import.meta.dirname. Config files and scripts folders outside a package's src folder are checked against Node >=24.22.0, the Node they run on. A settings.n.version in your config overrides both. n/hashbang is off in TypeScript files, whose bin names the built file, and in scripts folders.
node: { typeStripping: true } is for an app that Node runs from its .ts sources, such as one on @gpahal/tsconfig/node-app. It turns on @gpahal/no-js-specifier-for-ts, since Node loads the file a ./b.js specifier names rather than b.ts, and @gpahal/no-unstrippable-syntax. TypeScript's erasableSyntaxOnly reports most of the same syntax; the rule adds decorators, accessor fields and named module declarations.
tailwindcss takes one entryPoint and one settings.cwd, so every file in its scope is checked against one stylesheet and one Tailwind CSS install. In a monorepo whose apps have their own stylesheets, scope it to one app, or run ESLint once per app. settings.cwd, where the plugin loads Tailwind CSS from, defaults to root, and the configs reject when Tailwind CSS isn't installed there.
ESLint takes one plugin object per name. A config that doesn't come from defineConfig registers the preset's wrapped plugins, whose rules report at Markdown's own columns, through these exports:
- With
antislop,slopasslopPluginfrom@gpahal/eslint-config/slop. - With
eslintComments,@eslint-community/eslint-commentsaseslintCommentsPluginfrom@gpahal/eslint-config/eslint-comments.
Your config that registers a plugin under a name a preset uses, such as package-json, must pass the same object, or defineConfig rejects and names the area option to use instead. For slop and @eslint-community/eslint-comments, the stock plugin is swapped for the wrapped one.
Lists and merging
An array replaces a list, and a function transforms a copy of its defaults; scope takes only arrays. The root README's Lists and merging lists each option's defaults and limits.
ignores: [] drops every default ignore, leaving what ESLint ignores itself, node_modules and .git, and what gitignore reads.
A rule's options never merge with the presets': a later setting replaces them. So your no-restricted-globals replaces the confusing browser globals that browser code gets; list the names you want to keep too.
Config order
The configs are named gpahal/<area>/<part>, and a later config wins for a rule that both set. They come in this order:
- This package's configs: the presets, then the areas'
rules, then the exemptions, which turn rules off for kinds of files. - Your
configs. - The code-block configs, for Markdown code blocks and Astro
<script>blocks. - The configs that the composer's
.append()adds.
The guards come last, after the configs an .onResolved() callback returns too: gpahal/formatting/off turns off the formatting rules that allowFormattingRules doesn't list, gpahal/disable-type-checked/… turns off type-aware rules in files without type information, and gpahal/root-spelling… checks the path of each linted file. No composer method or callback sees them, so .remove(), .override() or .insertBefore() with a guard's name throws.
Configs after the composer's, such as a later item in a native config array, --rule or an inline eslint comment, come after the guards too. The patches that drop a twin rule's report, as [Duplicate rules][] describes, read which rules are on from the composer's configs, so turning a twin off only there, such as prefer-template with --rule, drops both reports. Turn rules off with an area's rules or configs instead.
| Files | Exemptions |
| --- | --- |
| Tests, and the files of playwright | Non-null and unsafe type assertions, unbound methods, empty functions, no-promise-executor-return, unicorn/consistent-function-scoping and unicorn/no-global-object-property-assignment. no-unnecessary-condition doesn't check type predicates. With vitest, vitest/unbound-method takes over. With antislop, sonarjs/no-identical-functions is off |
| .cjs, .cts, and the .js files of commonjs | require() calls; no top-level await advice |
| Config files | Anonymous default exports, any file name case, console, and eslint-disable directives without a reason |
| Scripts and command-line tools | console |
| Browser code, and with react, nextjs or astro their files | Iterator helpers, such as unicorn/prefer-iterator-helpers, since the browser lib lacks ES2025's iterator helpers, and unicorn/prefer-top-level-await, since Vite 5 and 6 build for es2020. server files keep them |
| Code blocks | Rules that don't fit excerpts, such as @typescript-eslint/no-unused-vars, @typescript-eslint/no-unused-expressions, import-x/no-unresolved, every e18e/* rule, and no-labels, no-lone-blocks and no-unused-labels, since an object-literal excerpt parses as a labelled block |
| .astro files, not their <script> blocks | @typescript-eslint/no-unsafe-*, restrict-template-expressions and unicorn/prefer-module, as Known issues explains |
Composer methods
Methods queue changes in call order, with .onResolved callbacks last, and ESLint applies them when it awaits the composer. Invalid configs, unknown names and invalid callback results reject with the call named in the error, as in `.override('gpahal/javascript/rules')`, and the item for an added config, as in `.append()`: item `[0]`.
| Method | Effect |
| --- | --- |
| .override(name, config) | Merges into matching configs. rules, plugins, settings, languageOptions and linterOptions merge one level deep, and so do languageOptions.parserOptions and languageOptions.globals. Other keys, such as files and ignores, replace theirs. A key set to undefined changes nothing. config can also be a function that gets each matching config and returns its replacement |
| .overrideRules(rules) | Changes existing enabled rules, preserving exemptions; rejects new enabled rules. A severity alone, such as 'warn', keeps the rule's options, and an entry with options replaces them. null removes a rule, and undefined leaves it, though its name must still be a known rule |
| .removeRules(...rules) | Removes the named rules |
| .disableRulesFix(...rules) | Makes the named rules report without fixing in every file, in a config named user/disable-rules-fix after the configs as they stand. A rule that a later change registers is covered too. Suggestions stay |
| .remove(...names) | Removes matching configs. Removing the only config that registers a plugin, such as gpahal/setup, rejects while the resolved configs turn on that plugin's rules |
| .insertBefore(name, ...configs), .insertAfter(name, ...configs) | Inserts before the first or after the last matching config |
| .append(...configs) | Adds configs after the others, before the guards |
| .onResolved(callback) | Inspects or changes the configs after every other method's changes, whatever the call order |
| .clone() | Returns a new composer with a copy of the changes queued so far, so two configs can diverge from a shared base |
The setup warnings about scanned files wait until ESLint awaits the composer, so they leave out files that a global ignores config of .append(), .insertBefore() or .insertAfter() ignores.
Every resolved config gets an absolute basePath, root by default, which keeps globs rooted when the config file is elsewhere or loaded with --config. ESLint rejects a basePath inside its native extends, so to use eslint/config's defineConfig, spread the awaited composer at the top level:
import { defineConfig as eslintDefineConfig } from 'eslint/config'
import { defineConfig } from '@gpahal/eslint-config'
export default eslintDefineConfig(...(await defineConfig({ root: import.meta.dirname })))Inserted configs, and new configs that an .onResolved() callback returns, are scoped and checked as configs are. Changes to existing configs are checked in their existing scope, so a change that turns a rule on where it can't run, or widens a code config's files to non-code files, throws. Invalid arguments, and spreading a composer before awaiting it, throw immediately.
Where your configs apply
Each rule of your config reaches the files where it can run. A config is one of three kinds, by whether it sets files and language:
| Your config | Code files | JSON, YAML, TOML, Markdown and HTML files | JavaScript and TypeScript code blocks | Rules that need type information |
| --- | --- | --- | --- | --- |
| Without files or language, as { rules: { 'no-console': 'off' } } | Every one | None; a rule for them throws | Every one | Code files |
| With files, as { files: ['src/**'], rules: { 'unicorn/no-null': 'error' } } | The ones files matches | The ones files matches, for a rule that runs on them | The ones files matches, such as files.codeBlock | Code files that files matches, as [['src/**', '**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}']] |
| With language, as { files: ['**/*.code-workspace'], language: 'json/jsonc', rules: { … } } | The ones files matches, parsed in that language | The ones files matches, parsed in that language | The ones files matches, parsed in that language | Not limited, as in ESLint |
- A config without
filesorlanguagealso reaches files that another of your configs givesfilesand a parser, and skips files that one of your configs gives anotherlanguage. It skips them by file name, so{ files: ['cfg/*.code-workspace'], language: 'json/jsonc' }makes it skip every.code-workspacefile. - A rule turned off applies to every file, unless the config has
languageOptionsorprocessor. - In a config with
files, a rule skips the files it can't run on.unicorn/no-nullskips every non-code file, andunicorn/expiring-todo-commentsruns on JSONC files but not JSON ones. A rule withoutmeta.languages, such as a core rule, counts as a JavaScript rule, except eslint-comments' rules, which run in every language. - A config with
languageruns any rule withoutmeta.languages, as ESLint does, and throws on a rule whosemeta.languagesleaves out its language. - The config's
ignoresand negated globs infilesentries, such as[['src/**', '!**/*.json']], leave out the files they skip. Patterns whose extension can't be constrained, such as**/*.t*, are checked conservatively.
A config whose rules reach different files is split into one config per scope once the plugins load. Each part keeps the config's name and comes after one with its plugins alone, which registers them for every file, so a directive comment that names a plugin's rule finds it.
Your config without a name gets one from its source, such as user/configs[0], user/configs[1][0] in a nested array, user/append[0] or user/onResolved[0]. Each call numbers its configs from 0, so .override() and .remove() with a name reach every config that has it. .override() with a split config's name reaches every part. With files, ignores or language, or with a function, it changes the config before the split and splits the result again.
defineConfig throws, naming the fix, on your config that wouldn't do what it says:
- an item that isn't a config or an array of them,
files: [], or a string inextendsthat doesn't name a config of a plugin the same config registers; - formatting rules that
allowFormattingRulesdoesn't list, all in one error; - a rule for JSON, YAML, TOML, Markdown or HTML files without
filesorlanguage, or withfilesthat can match files where it can't run, such asfiles: ['**/*.ts']withjson/no-duplicate-keys; - a rule that needs type information with
typeAware: false, or in a config whose everyfilesentry matches only files without it, such as['**/*.js']withouttypeAware.javascript; languageOptions.parserOptions.projectServiceortsconfigRootDir; usetypeAware.allowDefaultProject,typeAware.ignoresorroot;- a change to a
gpahal/ignores/*config that sets a key other thanignores,nameandbasePath.
Give package-json/* rules files: ['**/package.json'], not files.json, which also matches JSONC files such as tsconfig.json.
Preset scopes
By default, nextjs uses react.scope, and React uses nextjs.scope, so nextjs: { scope: ['apps/web/**'] } keeps both presets in the app when react is unset or set without scope. With both scopes set, React covers react.scope and nextjs's files. When react is unset, nextjs turns React on.
Without nextjs.scope, nextjs.rootDir sets it: nextjs: { rootDir: ['apps/web', 'apps/docs'] } keeps both presets in apps/web/** and apps/docs/**. @next/next/no-html-link-for-pages checks each app's links against its own pages folder, and @gpahal/nextjs-server-component checks the route entry files in each app's app and src/app folders.
Browser and server code
browser and server allow a global as the operand of typeof, as in typeof window !== 'undefined', since typeof of an undeclared global doesn't read it. typeof window.location reads window, so it's reported.
An import of a built-in's name, such as import { Buffer } from 'buffer', is reported even when package.json declares a browser port of that name, such as the buffer or events package. Disable @gpahal/no-node-imports for that import with a reason.
In browser files, process.env and its variables, such as process.env.NODE_ENV, pass because bundlers replace them, and so do globalThis.process and globalThis.process.env. Other uses are reported, such as process.argv and const { env } = process.
Typed config files
Config files that no tsconfig includes get type information from the default project: a fallback TypeScript program that typescript-eslint builds for the files typeAware.allowDefaultProject lists, with the compiler options of root's tsconfig.json. TypeScript 6 leaves Node's types out unless types lists node or *, and typescript-eslint silently uses its default options when that tsconfig is missing or has errors, so defineConfig warns, as Warnings describes.
The default project covers *.config.{ts,mts,cts,js,mjs,cjs}, including infixes such as vitest.config.e2e.ts, dotfile configs such as .prettierrc.ts, and Storybook's files in a .storybook folder, in folders outside the ignores and dot folders. TypeScript's ** skips dot folders, so a tsconfig covers .storybook only when its include names the folder. The files are found when the config loads, so new or renamed ones need an ESLint server restart in editors.
To find these files, defineConfig reads the include, files and extends of each tsconfig the project service would open, not imports. A config file that a project includes only through an import, such as src/x.ts importing ../site.config.ts, stays in the default project, and linting it throws. Add it to that tsconfig's include, or leave it out with a function, such as typeAware: { allowDefaultProject: (files) => files.filter((file) => file !== 'site.config.ts') }. The function gets paths escaped as globs, so a file in a [slug] folder arrives as app/\[slug\]/next.config.ts. An explicit array skips parsing the tsconfigs and the GPAHAL_FILES_OUTSIDE_TSCONFIG check.
defineConfig sets the default project's file limit from the files the paths and globs match, with at least 8 files of headroom, and typescript-eslint keeps the first limit for the process. A function's globs count only code files outside dot folders, so for a glob that matches files in a dot folder, use an array that lists every default-project file. typescript-eslint matches allowDefaultProject against paths relative to root and allows no **.
Run ESLint through the same path spelling as root. ESLint matches files by the path it was given, while Node resolves symbolic links in import.meta.dirname, so on macOS, where /tmp links to /private/tmp, a root under /tmp never matches. The gpahal/root-spelling* guards throw on a file reached through another path to root, naming both paths.
typescript-eslint keeps one project service per process, with the options of the first config that uses it. When one run lints several packages, each with its own defineConfig, they share the service only when their config files match the same globs and their tsconfigs have the same compiler options and type packages; otherwise linting throws an error that names both configs. In a monorepo, use [one config at the root][Set up a monorepo].
Startup cost
A small project took about 0.9 seconds to reach a ready config, and a 100-package workspace about 1.0 seconds, over five fresh processes on macOS ARM64. Plugin loading is much of the cost, so turning off type-aware rules doesn't remove it. An explicit typeAware.allowDefaultProject brought the workspace to about 0.95 seconds.
ESLint's --cache caches lint results, not config loading; add .eslintcache to .gitignore. A cached result stays while the file and the config are unchanged, so a type-aware report that depends on another file goes stale. Run without --cache in CI.
With type information, each --concurrency worker builds its own TypeScript program, so memory grows with the worker count while time drops little. Linting 300 TypeScript files took about 4.9 s and 1.2 GiB in one process, and 4.1 s and 3.0 GiB with --concurrency=4, on an Apple M3 Max with Node 24.22. Pick the worker count by the memory you have.
In a full config, sonarjs: false saves about 150 ms and 5 MiB of a small TypeScript project's 0.96 s and 368 MiB, at the cost of its bug rules, including no-hardcoded-secrets and no-hardcoded-passwords. With antislop, set antislop: { sonarjs: false } too, since its sonarjs rules load the plugin as well.
What's on by default
@eslint/jsrecommended and bug-catching core rules, such aseqeqeqinsmartmode andno-consolewithconsole.warnandconsole.errorallowed.guard-for-in,grouped-accessor-pairs,no-alert,no-loop-func,no-object-constructor,prefer-object-spreadandno-unneeded-ternaryare on, and typescript-eslint'sdefault-param-lastreplaces the core one.- typescript-eslint's strict and stylistic type-checked configs, with
no-unnecessary-condition,unbound-method, andswitch-exhaustiveness-checkwithdefaultcases checked.no-unreachableis on in TypeScript files too, since framework templates' tsconfigs don't setallowUnreachableCode: false.require-awaitis off, sincepromise-function-asyncmakes every function that returns a promise async.promise-function-asyncleaves alone a function without a return type that returns React'sReactNode, as a component that returnschildrendoes: React awaits the promise inReactNode, andasyncwould make every render a promise.strict-boolean-expressionsreports numbers, nullable strings and nullable numbers in conditions. A nullable boolean passes, as in{isOpen && <Menu />}.no-floating-promisesdoesn't takevoidas handling a promise.node:test'stest,describeand hooks, and React Router'snavigate(), pass as statements.no-misused-promisesreports a promise returned wherevoidis expected, such as an async JSX event handler. To start async work from a synchronous callback, writerun().catch(report).no-unsafe-type-assertionreports an assertion that narrows, such as(await res.json()) as User: parse the value with a schema or check it with a type guard. It's off in test files.strict-void-returnis off. To turn it on, add it totypescript.rulesTypeAwarewith'@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }], which it covers, and'no-promise-executor-return': 'off'.
- eslint-plugin-sonarjs's bug rules and its TLS and crypto rules, such as
unverified-certificateandhashing, the recommended configs of eslint-plugin-unicorn, eslint-plugin-regexp and eslint-plugin-import-x,@e18e/eslint-plugin, eslint-comments, which requires a reason after--in aneslint-disable, and eslint-plugin-jsdoc.import-x/no-extraneous-dependenciesallows dev dependencies in tests, scripts, stories, benchmarks, declaration files, mocks,.storybook,.vitepressand config files outside a package'ssrcfolder. It reads thepackage.jsonnearest the file.import-x/no-relative-packagesreports a relative import of another package's file, which skips itsexports, except in those dev files.import-x/namedandimport-x/namespacerun only in JavaScript files without type inf
