Configuration
The main package composes the final flat config array from
@santi020k/eslint-config-basic and the optional framework or integration
packages installed by the project.
Mental Model
Section titled “Mental Model”- Start with
defineConfig()and no arguments; add options or overrides only when detection needs help. - Let project detection enable TypeScript, frameworks, runtime, and supported tooling.
- Make options explicit when you want stable, reviewable config.
- Use booleans for installed optional framework configs.
- Prefer matching strings for concise configs; enums remain available when they improve discoverability.
- Use
featureswhen you want one simple opt-in/opt-out map for optional configs. - Use
optionMergeStrategywhen you want strict replace behavior. - Use
detectionfor granular auto-detection control. - Use
projectsfor package-aware monorepo configuration. - Use
ignoresfor extra global ignore globs alongside the composed config (same as a leading flat-config object with onlyignores). - Add local flat-config overrides as extra
defineConfig()arguments instead of rebuilding the array yourself.
Core Composition Model
Section titled “Core Composition Model”import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ extensions: ['unicorn', 'security'], formats: ['markdown', 'mdx'], frameworks: { react: true }, libraries: ['tailwind', 'i18next'], optionMergeStrategy: 'merge', root: import.meta.dirname, runtime: 'browser', testing: ['vitest'], tools: ['prettier'], typescript: true})Recommended v3 Project Config
Section titled “Recommended v3 Project Config”For application packages, prefer defineConfig() with no arguments when no
options are needed. A direct call automatically uses the directory containing
eslint.config.* as its stable detection root. Add explicit options when the
project needs specific framework, TypeScript, or integration choices. Install
the matching category packs for the optional choices below.
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ frameworks: { next: true }, nextMode: 'app-router', root: import.meta.dirname, tailwind: { entryPoint: 'src/app/globals.css', ignore: ['^prose-custom$', '^icon-wrapper$'], noUnknownClasses: 'warn' }, testing: ['vitest'], typescript: true})Set root when the intended project root differs from the directory containing
eslint.config.*. It anchors dependency detection, TypeScript, Tailwind,
subprojects, and .gitignore to that alternate directory.
Optional Configs
Section titled “Optional Configs”Optional configs are grouped into five categories: extensions, formats, libraries, testing, and tools. You can enable them with enums or with the matching string values.
import { defineConfig, Library, Testing, Tool } from '@santi020k/eslint-config-basic'
export default await defineConfig({ libraries: [Library.Zod, 'tailwind'], testing: [Testing.Playwright, 'vitest'], tools: [Tool.Prettier, 'cspell']})For the simplest manual configuration, use features. Keys are the same public string names used by the enums.
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ features: { 'astro-doctor': true, boundaries: true, cspell: true, 'github-actions': true, playwright: true, prettier: true, tailwind: true, unicorn: false, zod: true }})features participates in the same merge flow as the category arrays. true enables an optional config, and false disables it even if it was detected or enabled by a preset. integrations is an alias for features.
Presets
Section titled “Presets”| Preset | Meaning |
|---|---|
Basic | Core JavaScript rules only. |
Node | Core + TypeScript + Node globals. |
Browser | Core + TypeScript + Browser globals. |
Worker | Core + TypeScript + worker globals. |
Library | TypeScript package/library defaults with Prettier and best-practice rules. |
App | Browser app defaults with TypeScript, Prettier, and Vitest. |
CI | Universal TypeScript defaults with CI strict severities. |
Monorepo | Mixed-workspace defaults for package-aware configs. |
All | TypeScript plus every optional integration; requires the integrations package. |
Presets do not force a framework. Frameworks come from project detection or the frameworks option.
With the lean basic package, presets that select optional features require
their category packs. The full package already includes all five.
Frameworks
Section titled “Frameworks”import { defineConfig, NextMode } from '@santi020k/eslint-config-basic'
export default await defineConfig({ frameworks: { next: true }, nextMode: NextMode.AppRouter})Next.js, Expo, React Router, and detected Remix projects automatically include React rules. You can still pass imported config arrays or factories for advanced cases, but app configs should prefer booleans.
| Framework | Option |
|---|---|
| React | frameworks.react |
| Next.js | frameworks.next |
| Astro | frameworks.astro |
| Vue | frameworks.vue |
| Svelte | frameworks.svelte |
| Solid | frameworks.solid |
| Angular | frameworks.angular |
| NestJS | frameworks.nest |
| Hono | frameworks.hono |
| Expo | frameworks.expo |
| Qwik | frameworks.qwik |
| React Router and Remix projects | frameworks['react-router'] |
| Vite | frameworks.vite |
| Slidev | frameworks.slidev |
Configuration Priority
Section titled “Configuration Priority”Scalars always follow this order:
- Explicit options passed to
defineConfig({}). - Preset defaults.
- Auto-detection from
package.json,tsconfig.json, and project structure.
List options (libraries, testing, formats, tools, extensions) and frameworks use:
optionMergeStrategy: 'merge'(default): detected + preset + explicit are combined and deduplicated.optionMergeStrategy: 'replace': explicit values replace preset/detected values.
features and integrations are applied to the optional-config lists too. Use true to add a config and false to remove it from the final merged set.
Use autoFrameworks: false when you want manual framework control only (no detected framework auto-enable).
Detection Controls
Section titled “Detection Controls”Use detection: false to disable all auto-detection, or pass an object to disable specific categories while keeping the rest automatic.
import { defineConfig, Library, Testing } from '@santi020k/eslint-config-basic'
export default await defineConfig({ detection: { formats: true, frameworks: true, libraries: false, testing: false, tools: true }, libraries: [Library.Tailwind], testing: [Testing.Vitest]})Supported detection keys are typescript, frameworks, libraries, testing, formats, tools, extensions, runtime, nextMode, and projects.
Additional global ignores
Section titled “Additional global ignores”Pass ignores when you want repo-specific globs inside defineConfig() instead of a separate array entry. Patterns behave like ESLint flat config global ignores (relative to the ESLint working directory). They are not merged from presets or auto-detection. For projects sub-configs, patterns are not rewritten with the subfolder prefix; use paths that make sense from the config file’s working directory.
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ ignores: ['dist/**', 'packages/*/dist/**', 'coverage/**']})Local Overrides
Section titled “Local Overrides”Pass flat-config entries after the options object when a project needs a small rule exception, custom globals, or a file-specific override. The generated config stays first, and local entries are appended last.
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig( { frameworks: { astro: true }, typescript: true }, { files: ['**/*.astro'], rules: { '@typescript-eslint/no-unsafe-return': 'off' } })defineConfig() automatically attaches already-loaded plugin objects to every
override passed as an argument when that override references a plugin rule. This
keeps each emitted rule block independently valid in ESLint 10. Prefer this form
for all overrides known while the config is being built.
An override appended after the promise resolves is outside the composer’s
attachment pass. For overrides discovered dynamically, wrap the completed array
with attachReferencedPlugins():
import { attachReferencedPlugins, defineConfig} from '@santi020k/eslint-config-basic'
const generated = await defineConfig()const lateOverrides = await loadProjectOverrides()
export default attachReferencedPlugins([...generated, ...lateOverrides])The helper can attach only plugins already registered somewhere in the supplied array. Import and register a genuinely new plugin in the normal ESLint flat config form.
Default ignores
Section titled “Default ignores”The composed config ships a default ignore block (dist, build, coverage, framework output folders, node_modules, and similar). It also ignores common generated-code folders and files such as __generated__, generated, codegen, *.generated.*, *.gen.*, GraphQL generated output, and .prisma. AI coding-assistant artifact folders — .agent, .agents, .aider*, .claude, .clinerules, .codex, .copilot, .cursor, .gemini, .kiro, .opencode, .roo, and .windsurf — are ignored too. Disable the whole block with settings: [Setting.NoDefaultIgnores], or disable only generated-code ignores with settings: [Setting.NoGeneratedCodeIgnores].
Project Root
Section titled “Project Root”root is the shared base for dependency detection, TypeScript, Tailwind,
workspace packages, and .gitignore:
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ root: import.meta.dirname})Advanced configs can still override detectRootDir or
typescript.tsconfigRootDir when those roots intentionally differ.
Type-aware mode syntax-lints **/*.config.{ts,mts,cts} by default because
tooling config files commonly sit outside an application’s tsconfig. Add other
out-of-project files with untypedFiles instead of importing
typescript-eslint only to spread disableTypeChecked:
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ typescript: { untypedFiles: ['templates/**/*.ts'] }})Set untypedFiles: false to require type information for every TypeScript
file. projectService also accepts the native options object when
allowDefaultProject or defaultProject is genuinely needed.
Workspace packages that contain .d.ts, .d.mts, or .d.cts declarations but
no supported tsconfig are detected as syntax-only TypeScript projects. This
keeps handwritten public declarations lintable without sending them to the
project service. Add a package-local tsconfig when type-aware linting is useful;
for JavaScript packages, enable allowJs and include both the JavaScript source
and declaration files. Prefer explicit public declarations, Astro Props
interfaces, and JSDoc parameter types over suppressing unsafe-type rules.
The shared line-length rule ignores URLs, strings, and template literals.
Astro disables the JavaScript-oriented rule for .astro documents because
wrapping declarative tags, SVG path data, or embedded previews can change or
obscure their output. For other generated templates, keep the exception
file-scoped:
export default await defineConfig( {}, { files: ['generated/**/*.{js,html}'], rules: { '@stylistic/max-len': 'off' } })External schemas may keep their published property spelling. Object properties,
quoted keys, computed access, TypeScript property declarations, and destructured
keys renamed to a local camel-case binding do not trigger the camelcase rule.
Local variables and functions are still checked, so prefer:
const { theme_ntp_background: themeBackground } = manifestover introducing a snake-case local identifier. If an external adapter requires an exceptional local binding too, use a source-level directive with a short schema-specific justification instead of disabling the rule project-wide.
When an arrow function’s concise body is too long for one line, wrap the expression in parentheses rather than changing it to a return-only block:
const result = value => ( createLongResult(value, { enabled: true }))This form satisfies func-style, arrow-body-style,
implicit-arrow-linebreak, and max-len together and remains stable under
autofix.
Setup functions that return cleanup callbacks should use an explicit no-op result when setup cannot proceed:
const setup = (element: HTMLElement | null) => { if (!element) return () => undefined
const listener = () => element.removeAttribute('data-active')
element.addEventListener('click', listener)
return () => element.removeEventListener('click', listener)}This preserves the cleanup contract without an empty callback and keeps the setup function in the preferred expression style.
Tailwind Options
Section titled “Tailwind Options”Tailwind is auto-detected when the project depends on Tailwind packages. Use tailwind when the entry point or project-specific class ignores need to be explicit:
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ tailwind: { entryPoint: 'src/styles/global.css', ignore: ['^prose-custom$', '^icon-wrapper$'], noUnknownClasses: 'warn' }})Use noUnknownClasses: false when a project uses many generated or framework-provided classes but should keep the rest of the Tailwind rules.
Relative entry points are resolved from root, so project-scoped monorepo configs work when ESLint runs from the repository root. Set tailwind.cwd only when Tailwind must resolve from a different directory.
With a CSS entry point, the composer follows local relative imports and
automatically allows exact standalone selectors and static Tailwind v4
@utility names. It also discovers selectors in Astro component-local <style>
blocks. Explicit @plugin declarations and root tailwind.config.* references
enable the narrow compatibility classes provided by Tailwind Typography, Forms,
and the legacy Aspect Ratio plugin; for example, Typography’s not-prose is
recognized without a consumer ignore.
The detector deliberately does not guess dynamic @utility name-* families,
remote sources, or arbitrary runtime-generated package classes. Install package
providers such as tw-animate-css so Tailwind can compile their imports, then
add only any remaining project-specific classes as anchored ignore patterns.
In MDX, use a JSX comment for a narrow ESLint directive. An HTML comment is parsed as MDX content and can fail before ESLint applies the directive:
{/* eslint-disable better-tailwindcss/no-unknown-classes -- semantic metadata classes */}Set tailwind: false to disable auto-detected Tailwind linting for a package.
Testing Files
Section titled “Testing Files”Testing integrations ship with default file globs. Override them only when your project stores tests somewhere unusual:
Playwright defaults are limited to its config files, explicit e2e, functional,
or Playwright folders, and .e2e.* or .playwright.* file names. Generic test
folders and names such as tests/unit/** and tests/*.spec.* are not
Playwright-owned by default because they may belong to Vitest or Jest.
import { defineConfig, Testing } from '@santi020k/eslint-config-basic'
export default await defineConfig({ testing: [Testing.Playwright], testingFiles: { playwright: ['tests/**/*.ts'] }})Monorepo Projects
Section titled “Monorepo Projects”Use projects to scope package-specific presets and integrations to workspace folders. With preset: Preset.Monorepo, workspace project detection is enabled by default for common workspace folders and package.json#workspaces. Outside the monorepo preset, use detection: { projects: true } to opt in.
Use projectDefaults for settings shared by most workspace packages. Arrays and option maps merge with each project by default; a project can set optionMergeStrategy: 'replace' when it needs an isolated value.
import { defineConfig, Extension, Preset, Runtime, Tool } from '@santi020k/eslint-config-basic'
export default await defineConfig({ detection: { projects: true }, preset: Preset.Monorepo, projectDefaults: { extensions: [Extension.Unicorn], tools: [Tool.Prettier], typescript: true }, projects: { 'apps/api': { runtime: Runtime.Node }, 'apps/web': { frameworks: { next: true }, preset: Preset.App } }})Each project key is treated as a folder relative to the repo root. The generated project entries and inherited defaults are scoped to that folder.
Full Example
Section titled “Full Example”import { defineConfig, Extension, Format, Library, Testing, Tool } from '@santi020k/eslint-config-basic'
export default await defineConfig({ extensions: [ Extension.Unicorn, Extension.Sonarjs, Extension.Perfectionist, Extension.Security, Extension.Regexp, Extension.BestPractices, Extension.Boundaries ], formats: [ Format.Mdx, Format.Markdown, Format.Jsonc, Format.Graphql, Format.PackageJson, Format.Yaml, Format.Toml ], frameworks: { next: true, react: true }, libraries: [ Library.AiSdk, Library.OpenAiAgents, Library.Mastra, Library.Mcp, Library.Tailwind, Library.TanstackQuery, Library.TanstackRouter, Library.Storybook, Library.I18next, Library.Prisma, Library.Drizzle, Library.Typeorm, Library.MikroOrm, Library.Sequelize ], strict: true, testing: [ Testing.Vitest, Testing.Playwright, Testing.TestingLibrary, Testing.Cypress ], tools: [ Tool.Prettier, Tool.Cspell, Tool.Command, Tool.GithubActions, Tool.Docker, Tool.Nx, Tool.Jsdoc, Tool.Swagger ], typescript: 'strict'})Import Sorting
Section titled “Import Sorting”Import sorting is enabled by default via simple-import-sort. The built-in groups handle Node built-ins, framework virtual modules (Vite, Astro, SvelteKit, Nuxt), internal UI and app layers, styles, external npm packages, and relative imports — all without any configuration.
For monorepos where you want your own workspace packages to sort in a dedicated block before external npm packages, pass workspacePrefixes to defineConfig:
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ workspacePrefixes: ['@acme'] // @acme/* sorts before react, lodash, etc.})See the Core Package page for the full group order and more details.
Common Patterns
Section titled “Common Patterns”React Router or Remix + Tailwind
Section titled “React Router or Remix + Tailwind”import { defineConfig, Library } from '@santi020k/eslint-config-basic'
export default await defineConfig({ frameworks: { 'react-router': true }, libraries: [Library.Tailwind]})Astro + Svelte + Vitest
Section titled “Astro + Svelte + Vitest”import { defineConfig, Testing } from '@santi020k/eslint-config-basic'
export default await defineConfig({ frameworks: { astro: true, svelte: true }, testing: [Testing.Vitest]})Strict Mode
Section titled “Strict Mode”import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ strict: true})Strict mode accepts profiles:
| Value | Behavior |
|---|---|
false or 'recommended' | Keep recommended rule severities. |
true or 'ci' | Promote warnings to errors. |
'pedantic' | Promote warnings and enable built-in best-practice rules. |
Settings
Section titled “Settings”Gitignore integration is enabled by default.
import { defineConfig, Setting } from '@santi020k/eslint-config-basic'
export default await defineConfig({ settings: [Setting.NoGitignore]})Related Pages
Section titled “Related Pages”Schema
Section titled “Schema”This repo can generate a JSON schema for EslintConfigOptions:
pnpm run build:schemaThe output file is eslint-config-schema.json at the repository root.
Formatting preferences
Section titled “Formatting preferences”Choose common style preferences without repeating individual rule names:
export default await defineConfig({ formatting: { arrowParens: 'always', commaDangle: 'always-multiline', quotes: 'double', semi: true }})Every field is optional. arrowParens accepts always or as-needed,
commaDangle accepts always-multiline or never, quotes accepts double or
single, and semi is a boolean. Omitted fields retain existing defaults.
Preferences use warning severity; strict: 'ci' or strict: 'pedantic' promotes
them to errors. Correctness rules are unchanged. Prettier compatibility and local
flat-config overrides take precedence. Root preferences are inherited by
projects; each project can override individual fields. Use
optionMergeStrategy: 'replace' in a project to replace its inherited formatting
object instead of merging fields.
Did this page help?
Your answer stays in this browser. Share details on GitHub to help improve the docs.