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!