Skip to content

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.

  • 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 features when you want one simple opt-in/opt-out map for optional configs.
  • Use optionMergeStrategy when you want strict replace behavior.
  • Use detection for granular auto-detection control.
  • Use projects for package-aware monorepo configuration.
  • Use ignores for extra global ignore globs alongside the composed config (same as a leading flat-config object with only ignores).
  • Add local flat-config overrides as extra defineConfig() arguments instead of rebuilding the array yourself.
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
})

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 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.

PresetMeaning
BasicCore JavaScript rules only.
NodeCore + TypeScript + Node globals.
BrowserCore + TypeScript + Browser globals.
WorkerCore + TypeScript + worker globals.
LibraryTypeScript package/library defaults with Prettier and best-practice rules.
AppBrowser app defaults with TypeScript, Prettier, and Vitest.
CIUniversal TypeScript defaults with CI strict severities.
MonorepoMixed-workspace defaults for package-aware configs.
AllTypeScript 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.

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.

FrameworkOption
Reactframeworks.react
Next.jsframeworks.next
Astroframeworks.astro
Vueframeworks.vue
Svelteframeworks.svelte
Solidframeworks.solid
Angularframeworks.angular
NestJSframeworks.nest
Honoframeworks.hono
Expoframeworks.expo
Qwikframeworks.qwik
React Router and Remix projectsframeworks['react-router']
Viteframeworks.vite
Slidevframeworks.slidev

Scalars always follow this order:

  1. Explicit options passed to defineConfig({}).
  2. Preset defaults.
  3. 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).

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.

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/**']
})

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.

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].

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 } = manifest

over 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 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 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']
}
})

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.

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 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.

import { defineConfig, Library } from '@santi020k/eslint-config-basic'
export default await defineConfig({
frameworks: { 'react-router': true },
libraries: [Library.Tailwind]
})
import { defineConfig, Testing } from '@santi020k/eslint-config-basic'
export default await defineConfig({
frameworks: { astro: true, svelte: true },
testing: [Testing.Vitest]
})
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({
strict: true
})

Strict mode accepts profiles:

ValueBehavior
false or 'recommended'Keep recommended rule severities.
true or 'ci'Promote warnings to errors.
'pedantic'Promote warnings and enable built-in best-practice rules.

Gitignore integration is enabled by default.

import { defineConfig, Setting } from '@santi020k/eslint-config-basic'
export default await defineConfig({
settings: [Setting.NoGitignore]
})

This repo can generate a JSON schema for EslintConfigOptions:

Terminal window
pnpm run build:schema

The output file is eslint-config-schema.json at the repository root.

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.