ESLint

Purpose and Scope

ESLint integration in Turborepo is about more than running a linter from the repository root. The first-party ESLint guide frames ESLint as a static analysis tool for finding and fixing problems in JavaScript code, then adapts that tool to a monorepo by sharing configuration through a workspace package. The reader problem is consistency: every app and package should lint with the same baseline rules, but each workspace should still be able to compose the configuration it needs. Turborepo then treats linting as a normal task that can be parallelized and cached across the workspace graph.

Sources: apps/docs/content/docs/guides/tools/eslint.mdx

The guide assumes a repository created with create-turbo, or at least a repository with a similar layout. That assumption matters because the examples depend on conventional workspace folders such as apps and packages, with a reusable configuration package under packages/eslint-config. The shared CreateTurboCallout component is used across tool guides to make that starting point explicit, so this page follows the same model: create or adapt a workspace structure first, then wire ESLint configuration into packages, and finally register lint scripts with Turborepo tasks.

Sources: apps/docs/content/docs/guides/tools/create-turbo-callout.tsx, apps/docs/content/docs/guides/tools/eslint.mdx

Relevant Source Files

  • apps/docs/content/docs/guides/tools/eslint.mdx - Primary ESLint integration guide. It defines the monorepo file structure, shared configuration package, Flat Config and legacy configuration paths, and the lint-task setup flow.
  • apps/docs/content/docs/guides/tools/biome.mdx - Neighboring tool guide that contrasts per-package tasks with root tasks and explains why tool speed and cache-hit tradeoffs affect task design.
  • apps/docs/content/docs/guides/tools/create-turbo-callout.tsx - Shared callout used by tool guides to state that examples assume create-turbo or a similar repository structure.
  • apps/docs/content/docs/guides/tools/docker.mdx - Tool guide showing the same documentation pattern of explaining a monorepo-specific problem before introducing the Turborepo command or task strategy.
  • apps/docs/content/docs/guides/tools/index.mdx - Tools overview that positions ESLint alongside Biome, Jest, Docker, TypeScript, Vitest, and other integrations.
  • apps/docs/content/docs/guides/tools/jest.mdx - Tool guide demonstrating the general task pattern used by ESLint too: add package scripts, register tasks in turbo.json, and distinguish cacheable one-shot commands from persistent watch commands.

Monorepo Configuration Model

The recommended ESLint layout creates a package named @repo/eslint-config in packages/eslint-config and gives applications or libraries their own eslint.config.js files. The shared package owns files such as base.js, next.js, and react-internal.js, which are exported from its package.json so other workspaces can import only the configuration they need. This gives the repository a single dependency and rule-management location while preserving package-level composition. In practice, web apps can import a Next.js-specific config, React packages can import an internal React config, and generic packages can fall back to the base config.

Sources: apps/docs/content/docs/guides/tools/eslint.mdx

This shape also improves cache behavior because lint commands become stable, package-local tasks rather than a single large command that always observes the entire repository. The Biome guide is useful contrast: Biome is described as unusually fast, so the docs recommend a root task for that tool even while acknowledging cache-hit tradeoffs. ESLint usually benefits from the opposite pattern: define scripts in packages that actually lint source files, let Turborepo discover matching lint tasks, and keep the shared configuration package as a dependency so rule changes invalidate only the work that depends on them.

Sources: apps/docs/content/docs/guides/tools/eslint.mdx, apps/docs/content/docs/guides/tools/biome.mdx

A package consuming the shared Flat Config should add @repo/eslint-config as a development dependency using the package manager’s workspace protocol where appropriate. The ESLint guide shows pnpm and bun using workspace:* and npm or yarn examples using * for the local package reference. After that, the package-level eslint.config.js imports the exported configuration from @repo/eslint-config. This split is important: package.json expresses the dependency relationship, while eslint.config.js expresses the linting behavior that applies in that workspace.

Sources: apps/docs/content/docs/guides/tools/eslint.mdx

./apps/web/package.json
{
  "devDependencies": {
    "@repo/eslint-config": "workspace:*"
  }
}

Flat Config, Legacy Config, and Turborepo Packages

For ESLint v9, the modern path is Flat Config. The guide’s file tree places eslint.config.js in each app and package that needs linting, with packages/eslint-config exporting reusable JavaScript modules. The official reference snippets for eslint-config-turbo and eslint-plugin-turbo use the same Flat Config style: import turboConfig from eslint-config-turbo/flat when you want the packaged recommended configuration, or import turbo from eslint-plugin-turbo when you want direct access to the plugin and its recommended config. Both approaches are meant to surface environment variables that are used in source code but not declared for Turborepo hashing.

Sources: apps/docs/content/docs/guides/tools/eslint.mdx

./packages/eslint-config/base.js
import turboConfig from "eslint-config-turbo/flat";
 
export default [
  ...turboConfig,
  // Other shared repository configuration
];
./packages/eslint-config/base.js
import turbo from "eslint-plugin-turbo";
 
export default [turbo.configs["flat/recommended"]];

The published ESLint integration has two layers. eslint-config-turbo is the convenient shared config package: install it where the repository’s ESLint configuration is maintained, then extend or spread its exported config. eslint-plugin-turbo is the lower-level plugin package: install it when you want to register the turbo plugin yourself and configure rules directly. The central rule exposed in the official snippets is turbo/no-undeclared-env-vars, and it accepts an allowList option for patterns that should not be treated as undeclared. This rule ties editor feedback to Turborepo’s environment hashing model.

Sources: apps/docs/content/docs/guides/tools/eslint.mdx

./packages/eslint-config/base.js
import turbo from "eslint-plugin-turbo";
 
export default [
  {
    plugins: {
      turbo,
    },
    rules: {
      "turbo/no-undeclared-env-vars": [
        "error",
        {
          allowList: ["^ENV_[A-Z]+$"],
        },
      ],
    },
  },
];

ESLint v8 projects can still use the legacy eslintrc style. The official reference examples describe extending turbo in an eslintrc file, relying on ESLint’s convention that eslint-config- can be omitted from the package name. Teams migrating from legacy configuration to Flat Config should treat the shared @repo/eslint-config package as the stable boundary. Move rules and plugin wiring inside that package first, then update individual workspace config files to import the new exported modules. That keeps the repository-wide linting contract intact while changing only the configuration format.

Sources: apps/docs/content/docs/guides/tools/eslint.mdx

Setting Up the lint Task

Once configuration is importable, every workspace that should be linted needs a package script. A typical application or package script is lint: eslint ., though the exact command can include extensions, cache options, or framework-specific flags. Turborepo does not need to understand ESLint internals; it needs a task name that appears in package.json scripts and a matching task entry in turbo.json. That registration lets turbo run lint execute the script wherever it exists, schedule work in parallel, and reuse cached results when task inputs have not changed.

Sources: apps/docs/content/docs/guides/tools/eslint.mdx, apps/docs/content/docs/guides/tools/jest.mdx

./apps/web/package.json
{
  "name": "web",
  "scripts": {
    "lint": "eslint ."
  }
}
./turbo.json
{
  "tasks": {
    "lint": {}
  }
}

The Jest guide reinforces the same task mechanics: install the tool in packages that need it, add scripts to those packages, and register a task in turbo.json so Turborepo can parallelize and cache the work. It also distinguishes normal tasks, which complete and can be cached, from watch-mode tasks, which stay alive and should be configured separately with cache disabled and persistent enabled. Apply the same distinction to linting if you introduce an ESLint watch command or editor-oriented long-running process; keep the ordinary lint task cacheable for CI and pre-merge checks.

Sources: apps/docs/content/docs/guides/tools/jest.mdx, apps/docs/content/docs/guides/tools/eslint.mdx

Environment-Variable Linting

The Turborepo-specific reason to use eslint-config-turbo or eslint-plugin-turbo is environment-variable safety. Turborepo hashes task inputs to decide whether cached results can be restored. If application code reads an environment variable that is not accounted for in turbo.json, the task may not be invalidated when that value changes. The turbo/no-undeclared-env-vars rule addresses this problem at authoring time by highlighting process.env usage that is not represented in Turborepo’s environment configuration. That makes cache correctness visible in the editor and in ESLint output.

Sources: apps/docs/content/docs/guides/tools/eslint.mdx

Use allowList sparingly. It is appropriate for variables that follow a deliberate naming convention or are intentionally excluded from Turborepo’s hash inputs, but it should not become a blanket escape hatch for application configuration. A practical workflow is to start with eslint-config-turbo in the shared base config, run turbo run lint across the repository, and then either declare real environment dependencies in turbo.json or add narrow allowList patterns with a short comment in the shared config. This preserves both developer ergonomics and cache correctness.

Sources: apps/docs/content/docs/guides/tools/eslint.mdx

Compact Reference

ConcernRecommended source-level shapeNotes
Shared config packagepackages/eslint-config exporting base.js, next.js, react-internal.jsCentralizes ESLint dependencies and reusable rules.
App or package configapps/web/eslint.config.js or packages/ui/eslint.config.jsImports the shared config needed by that workspace.
Package dependency@repo/eslint-config in workspace devDependenciesUse workspace:* with pnpm or bun when following the guide examples.
Turbo tasktasks.lint in turbo.jsonRuns package lint scripts through turbo run lint.
Config packageeslint-config-turbo/flatConvenient Flat Config integration for Turborepo rules.
Plugin packageeslint-plugin-turboDirect access to turbo/no-undeclared-env-vars and plugin configs.
Environment ruleturbo/no-undeclared-env-varsFlags environment variables used in code but not accounted for in Turborepo hashing.

Execution Flow and Next Steps

A good implementation sequence is: create or identify packages/eslint-config, install ESLint and the Turborepo ESLint package there, export reusable configs, import those configs from each workspace’s eslint.config.js, add lint scripts where source should be checked, and finally register lint in turbo.json. Run turbo run lint locally before moving the task into CI. If cache behavior is surprising, compare the ESLint setup with other tool guides: Biome shows when a root task may be reasonable, while Jest shows how to separate cacheable one-shot work from persistent watch processes.

Sources: apps/docs/content/docs/guides/tools/index.mdx, apps/docs/content/docs/guides/tools/biome.mdx, apps/docs/content/docs/guides/tools/jest.mdx

After ESLint is working, read the TypeScript and configuration pages to make linting part of a broader quality workflow. Type-aware lint rules often depend on project references and shared tsconfig packages, while Turborepo task inputs, outputs, and environment declarations determine cache correctness. For deployment-oriented repositories, the Docker guide is a useful companion because it shows the same principle at image-build time: reduce work by declaring the relevant inputs precisely, then let Turborepo and the surrounding tool reuse previous results safely.

Sources: apps/docs/content/docs/guides/tools/docker.mdx, apps/docs/content/docs/guides/tools/index.mdx