ESLint in a monorepo

Installing ESLint

Keeping a single version of ESLint is recommended for simplicity. Because of this, we suggest installing ESLint as devDependency at the root of your monorepo, or utilizing a tool like syncpack (opens in a new tab) to maintain the same version across workspaces.

Sharing config

Sharing an ESLint config across workspaces can be a boon to productivity by making all your workspaces more consistent.

Let's imagine a monorepo like this:

apps
├─ docs
│  ├─ package.json
│  └─ .eslintrc.js
└─ web
   ├─ package.json
   └─ .eslintrc.js
packages
└─ eslint-config-custom
   ├─ next.js
   ├─ library.js
   └─ package.json

We've got a package called eslint-config-custom, and two applications, each with their own .eslintrc.js.

Our eslint-config-custom package

Our eslint-config-custom file contains two files, next.js, and library.js. These are two different ESLint configs, which we can use in different workspaces, depending on our needs.

Let's investigate the next.js lint configuration:

packages/eslint-config-custom/next.js
const { resolve } = require("node:path");
 
const project = resolve(process.cwd(), "tsconfig.json");
 
/*
 * This is a custom ESLint configuration for use with
 * Next.js apps.
 *
 * This config extends the Vercel Engineering Style Guide.
 * For more information, see https://github.com/vercel/style-guide
 *
 */
 
module.exports = {
  extends: [
    "@vercel/style-guide/eslint/node",
    "@vercel/style-guide/eslint/typescript",
    "@vercel/style-guide/eslint/browser",
    "@vercel/style-guide/eslint/react",
    "@vercel/style-guide/eslint/next",
    // turborepo custom eslint configuration configures the following rules:
    //  - https://github.com/vercel/turbo/blob/main/packages/eslint-plugin-turbo/docs/rules/no-undeclared-env-vars.md
    "eslint-config-turbo",
  ].map(require.resolve),
  parserOptions: {
    project,
  },
  globals: {
    React: true,
    JSX: true,
  },
  settings: {
    "import/resolver": {
      typescript: {
        project,
      },
    },
  },
  ignorePatterns: ["node_modules/", "dist/"],
  // add rules configurations here
  rules: {
    "import/no-default-export": "off",
  },
};

It's a typical ESLint config that extends the Vercel styleguide (opens in a new tab), nothing fancy.

The package.json looks like this:

packages/eslint-config-custom/package.json
{
  "name": "eslint-config-custom",
  "license": "MIT",
  "version": "0.0.0",
  "private": true,
  "devDependencies": {
    "@vercel/style-guide": "^4.0.2",
    "eslint-config-turbo": "^1.10.12"
  }
}
 

Note that the ESLint dependencies are all listed here. This is useful - it means we don't need to re-specify the dependencies inside the apps which import eslint-config-custom.

How to use the eslint-config-custom package

In our web app, we first need to add eslint-config-custom as a dependency.

apps/web/package.json
{
  "dependencies": {
    "eslint-config-custom": "*"
  }
}

We can then import the config like this:

apps/web/.eslintrc.js
module.exports = {
  root: true,
  extends: ["custom/next"],
};

By adding custom/next to our extends array, we're telling ESLint to look for a package called eslint-config-custom, and reference the file next.js.

You can avoid specifying the file by setting the entrypoint for your package. For example, setting main: 'next.js' in the package.json, could be loaded as simply extends: ["custom"] in your .eslintrc.js.

Summary

This setup ships by default when you create a new monorepo with npx create-turbo@latest. You can also look at our basic example (opens in a new tab) to see a working version.

Setting up a lint task

We recommend following the setup in the basics section, with one alteration.

Each package.json script should look like this:

packages/*/package.json
{
  "scripts": {
    "lint": "eslint ."
  }
}