Logo

Setup ESLint with Turborepo

15 Aug 2025

Features

  • Prettier formatting enforced (prettier/prettier)
  • Unused imports/vars auto-deleted (eslint-plugin-unused-imports)
  • Import ordering (import/order)
  • Path-alias resolution via TS paths or project references
  • Turbo monorepo guard & only-warn mode
  • One shared config → zero duplication
  • Fix-all-on-save in VS Code

Steps

1. Create the shared package

bun create --template none packages/eslint-config

packages/eslint-config/package.json

{
  "name": "@repo/eslint-config",
  "version": "0.0.0",
  "main": "base.js",
  "peerDependencies": { "eslint": "*" }
}

2. Install every dependency once

at the repo root

bun add -D \
  eslint@^9 \
  @eslint/js \
  @eslint/eslintrc \
  typescript-eslint \
  eslint-plugin-import \
  eslint-import-resolver-typescript \
  eslint-plugin-unused-imports \
  eslint-plugin-prettier \
  eslint-config-prettier \
  eslint-plugin-turbo \
  eslint-plugin-only-warn

(All packages in the monorepo will resolve these because Bun hoists them.)


3. Drop in your packages/eslint-config/base.js

// packages/eslint-config/base.js
import js from '@eslint/js';
import eslintConfigPrettier from 'eslint-config-prettier';
import turboPlugin from 'eslint-plugin-turbo';
import tseslint from 'typescript-eslint';
import onlyWarn from 'eslint-plugin-only-warn';
 
import importPlugin   from 'eslint-plugin-import';
import prettierPlugin from 'eslint-plugin-prettier';
import unusedImports  from 'eslint-plugin-unused-imports';
 
import { FlatCompat } from '@eslint/eslintrc';
const compat = new FlatCompat({ baseDirectory: import.meta.url });
 
/** @type {import('eslint').Linter.FlatConfig[]} */
export const config = [
  /* 1️⃣ Built-in & TS rules (already flat) */
  js.configs.recommended,
  eslintConfigPrettier,
  ...tseslint.configs.recommended,
 
  /* 2️⃣ Classic presets auto-converted by FlatCompat */
  ...compat.extends(
    'plugin:import/recommended',
    'plugin:import/typescript',
    'plugin:prettier/recommended'
  ),
 
  /* 3️⃣ Shared plugins & rules */
  {
    plugins: {
      turbo: turboPlugin,
      'only-warn': onlyWarn,
      import: importPlugin,
      'unused-imports': unusedImports,
      prettier: prettierPlugin,
    },
    rules: {
      'turbo/no-undeclared-env-vars': 'warn',
 
      /* Strip dead code */
      'unused-imports/no-unused-imports': 'error',
      'unused-imports/no-unused-vars': [
        'warn',
        { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' },
      ],
 
      /* Surface Prettier differences */
      'prettier/prettier': 'error',
    },
  },
 
  /* 4️⃣ Make import/no-unresolved obey tsconfig paths */
  {
    settings: {
      'import/resolver': {
        typescript: {
          project: ['./tsconfig.json', './packages/*/tsconfig.json'],
        },
        node: {
          extensions: ['.js', '.jsx', '.ts', '.tsx'],
          moduleDirectory: ['node_modules', 'packages'],
        },
      },
    },
  },
 
  /* 5️⃣ Import ordering */
  {
    rules: {
      'import/order': [
        'error',
        {
          groups: [
            'builtin',
            'external',
            'internal',
            ['parent', 'sibling', 'index'],
            'type',
            'object',
          ],
          'newlines-between': 'always',
          alphabetize: { order: 'asc', caseInsensitive: true },
        },
      ],
    },
  },
 
  /* 6️⃣ Ignore dist folders repo-wide */
  { ignores: ['dist/**'] },
];

4. Consume the shared config in each workspace

apps/admin/eslint.config.js (repeat for every app/lib or just make one root file):

import tseslint from 'typescript-eslint';
import { config as base } from '@repo/eslint-config/base';
 
export default tseslint.config(
  ...base,
  // optional per-package adjustments
  { files: ['**/*.{ts,tsx}'] }
);

5. Expose your alias/path map in tsconfig

Example for an app workspace:

// apps/admin/tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",           // required
    "paths": {
      "@/*": ["src/*"]        // alias used like "@/components/Button"
    }
  }
}

The resolver introduced in step 4 now lets import/no-unresolved understand @/….


6. Add npm scripts & Turborepo pipeline

Root package.json

{
  "scripts": {
    "lint": "turbo run lint",
    "lint:fix": "turbo run lint:fix"
  }
}

turbo.json

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "lint":      { "outputs": [] },
    "lint:fix":  { "outputs": [] }
  }
}

Each package’s package.json

{
  "scripts": {
    "lint": "eslint --cache 'src/**/*.{ts,tsx,js,jsx}'",
    "lint:fix": "eslint --fix --cache 'src/**/*.{ts,tsx,js,jsx}'"
  }
}

7. Enable one-click formatting in VS Code

.vscode/settings.json (workspace-level)

{
  "eslint.experimental.useFlatConfig": true,
  "editor.codeActionsOnSave": { "source.fixAll.eslint": true },
  "editor.defaultFormatter": "dbaeumer.vscode-eslint",
  "eslint.workingDirectories": [{ "mode": "auto" }]
}

Requires ESLint extension ≥ v2.5.


8. Smoke test

bun run lint:fix        # repo-wide auto-fix

Open a file with:

  • an unused import
  • messy indentation
  • imports out of order

Hit ⌘S:

  • unused lines vanish (unused-imports)
  • file is Prettier-formatted (prettier/prettier)
  • imports regroup & alphabetise (import/order)

Congratulations — you now have a single, flat-config-based ESLint setup that:

  • Runs everywhere in the Turborepo
  • Understands TS path aliases
  • Keeps code formatted & tidy on every save
  • Surfaces Turbo env-var issues
  • Emits only warnings (not errors) for unspecified rules via only-warn

Happy linting!