Monorepo
This package has first-class monorepo support through the projects option and the Monorepo preset. A single eslint.config.mjs at the repo root can scope different presets, runtimes, and frameworks to each workspace package.
ESLint 10 also resolves eslint.config.* starting from each linted file’s directory rather than the working directory. That means workspace packages can ship their own eslint.config.mjs (each calling defineConfig() with package-specific options) and a single ESLint run from the repo root picks up every config automatically. Use projects when you want one root config to rule them all; use per-package configs when packages should own their lint setup — both work with v10.
Minimal Setup
Section titled “Minimal Setup”Preset.Monorepo enables Prettier and best-practice integrations. Install
@santi020k/eslint-config-tools and
@santi020k/eslint-config-extensions with the lean basic package; the full
package already includes them.
import { defineConfig } from '@santi020k/eslint-config-basic'
export default defineConfig()Workspaces are inferred from workspaces, pnpm-workspace.yaml, Turborepo,
or Nx. The root receives safe universal defaults, while each package’s
package.json activates only the frameworks and integrations used in that
package. Root devDependencies do not leak framework or library rules across the
workspace.
Use the factory when you need local overrides or want to disable package detection:
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ detection: { projects: false }})Scoping Packages with projects
Section titled “Scoping Packages with projects”Use projects to assign different options to workspace folders. Each key is a path relative to the repo root and each value is a partial EslintConfigOptions object.
import { defineConfig, Preset, Runtime } from '@santi020k/eslint-config-basic'
export default await defineConfig({ preset: Preset.Monorepo, projects: { 'apps/api': { preset: Preset.Node, runtime: Runtime.Node }, 'apps/web': { frameworks: { next: true }, preset: Preset.App }, 'packages/cli': { preset: Preset.Library, runtime: Runtime.Node }, 'packages/ui': { frameworks: { react: true }, preset: Preset.Library } }})Each project entry generates ESLint config entries scoped to that folder — rules and globals from apps/api only apply to files under apps/api/.
Workspace Root
Section titled “Workspace Root”By default each project entry uses its own folder for detection and TypeScript. The config directory is the workspace root by default. Set a different root only when the config file lives elsewhere:
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ root: import.meta.dirname})Set typescript.tsconfigRootDir inside a project only when its TypeScript root
intentionally differs from its package folder.
Turborepo / pnpm Workspaces Pattern
Section titled “Turborepo / pnpm Workspaces Pattern”For repos using Turborepo with pnpm workspaces, the recommended pattern is:
import { defineConfig, Preset, Runtime } from '@santi020k/eslint-config-basic'
export default await defineConfig({ preset: Preset.Monorepo, root: import.meta.dirname, projects: { 'apps/dashboard': { frameworks: { next: true }, preset: Preset.App }, 'apps/marketing': { frameworks: { astro: true }, preset: Preset.App }, 'packages/api': { preset: Preset.Node, runtime: Runtime.Node }, 'packages/core': { preset: Preset.Library, runtime: Runtime.Universal }, 'workers/auth': { preset: Preset.Library, runtime: Runtime.Worker } }})Merge Strategy
Section titled “Merge Strategy”By default, project-level libraries, testing, formats, tools, and extensions are merged with root-level values (optionMergeStrategy: 'merge'). Use 'replace' at the project level when a specific package should override the root completely. These arrays accept enum values or matching strings, and features can add or remove optional configs inside each project:
import { defineConfig, Preset, Testing } from '@santi020k/eslint-config-basic'
export default await defineConfig({ preset: Preset.Monorepo, projects: { 'apps/e2e': { features: { prettier: false }, optionMergeStrategy: 'replace', testing: [Testing.Playwright] } }, testing: [Testing.Vitest]})Ignoring Paths
Section titled “Ignoring Paths”Pass ignores at the root level to exclude generated folders across the whole repo:
import { defineConfig, Preset } from '@santi020k/eslint-config-basic'
export default await defineConfig({ ignores: [ 'dist/**', 'packages/*/dist/**', 'apps/*/dist/**', 'coverage/**', '.turbo/**' ], preset: Preset.Monorepo})[!NOTE] Patterns in
ignoresare relative to ESLint’s working directory. They are not automatically prefixed with theprojectssubfolder paths — use repo-root-relative globs when needed.
Troubleshooting
Section titled “Troubleshooting”Detected frameworks bleed between packages
This should not happen in v3: root framework and library detection is
suppressed for workspaces, then recomputed per package. Run doctor to inspect
the detected package roots if a package is scoped incorrectly.
Framework detection can also use source evidence when a package manifest is
intentionally neutral. A workspace package containing .astro files receives
the Astro parser and processor even when it does not declare astro. This
supports shared component and theme packages without adding a runtime
dependency solely for lint configuration.
TypeScript parser rejects files from another package
Each package needs its own tsconfig.json that covers its source. Mark tooling,
templates, or generated TypeScript as syntax-only when it sits outside that
project:
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ projects: { 'packages/ui': { frameworks: { react: true }, typescript: { untypedFiles: ['templates/**/*.ts'] } } }})The doctor command reports missing projects scoping
Run npx @santi020k/eslint-config-basic doctor — it detects workspace packages that are not represented in projects and suggests which ones to add.
Related Pages
Section titled “Related Pages”- Presets — available preset values and what each enables
- Runtime — runtime enum and auto-detection rules
- Configuration — full option reference
- CLI —
doctorandinspectcommands for diagnosing monorepo setups
Independently linted apps
Section titled “Independently linted apps”Run basic-eslint doctor --json or basic-eslint doctor --verbose to review each
package’s local config and lint script. The lintOwnership inventory reports
what is declared; it does not prove which config a wrapper command executes.
Review root/package overlap before changing ownership.
For an Astro website beside an Expo app that already owns its lint command, keep root-owned TypeScript linting scoped to the website:
import { defineConfig } from '@santi020k/eslint-config-basic'
export default await defineConfig({ detection: false, ignores: ['apps/mobile/**'], typescript: false, projects: { 'apps/web': { frameworks: { astro: true }, typescript: 'type-aware' } }})Install the Astro adapter at the root. The mobile app keeps its existing Expo
config and lint/typecheck commands. Keep the website’s tsconfig inside
apps/web; do not disable type-aware linting across all apps merely because the
workspace has multiple tsconfigs. detection: false disables every automatic
feature in this example; select additional feature packs explicitly as needed.
Root formatting preferences are inherited by projects and merge field by field
with project preferences. projectDefaults.formatting can set shared workspace
preferences, and a project can use optionMergeStrategy: 'replace' to replace its
inherited formatting object.
Did this page help?
Your answer stays in this browser. Share details on GitHub to help improve the docs.