This is an alpha, sneak peek of Monorepo Maestros. For this iteration, I'm getting all of my thoughts down. In the future, we'll have better information architecture, graphics, and other awesomeness. Your feedback is welcome!

ESLint

ESLint is the most common linter used in the JavaScript world today. Setting it up in a monorepo can be difficult and piecing together the various configurations can be frustrating if you don't know what you need to do.

But we're maestros! We can do this. Let's take a look at how to happily set up ESLint for JavaScript monorepo success.

Setting up ESLint

As we head into getting ESLint set up, let's remember our requirements for conducting a monorepo symphony:

Creating presets

To create some presets for our workspaces, we'll set up a workspace with our configurations in tooling/eslint-config.

  • tooling
    • eslint-config
      • .eslintrc.js
      • next.js
      • node.js
      • svelte.js
      • package.json
  • Our package.json will be relatively simple, installing eslint and exporting the files where we'll keep our presets.

    tooling/eslint-config/package.json
    {
    "name": "@repo/lint",
    "version": "0.0.0",
    "files": ["node.js", "next.js", "svelte.js"],
    "scripts": {
    "lint": "eslint ."
    },
    "dependencies": {
    "@next/eslint-plugin-next": "latest", // We'll need this for the Next.js config in a moment.
    "eslint": "^8.40.0"
    }
    }
    tooling/eslint-config/package.json
    {
    "name": "@repo/lint",
    "version": "0.0.0",
    "files": ["node.js", "next.js", "svelte.js"],
    "scripts": {
    "lint": "eslint ."
    },
    "dependencies": {
    "@next/eslint-plugin-next": "latest", // We'll need this for the Next.js config in a moment.
    "eslint": "^8.40.0"
    }
    }

    The lint script in package.json is for linting the eslint-config workspace itself. It is not the script that runs in your other workspaces.

    It's typical that not all of our workspaces will use the exact same linting configuration. As an example, default exports tend to be inadvisable for JavaScript modules but some frameworks require default exports to work properly (e.g. A Next.js page.js file needs a default export). We can account for this by creating multiple base configurations.

    We'll create a node.js file for simple Node apps:

    tooling/eslint-config/node.js
    module.exports = {
    ignorePatterns: ['node_modules/', '**/.eslintrc.js', 'dist/'],
    root: true,
    };
    tooling/eslint-config/node.js
    module.exports = {
    ignorePatterns: ['node_modules/', '**/.eslintrc.js', 'dist/'],
    root: true,
    };

    And another one to use in our Next.js apps:

    tooling/eslint-config/next.js
    const { rules } = require('./utils/rules');

    module.exports = {
    extends: ['next'],
    ignorePatterns: ['**/.next/**', '**/.eslintrc.js'],
    root: true,
    };
    tooling/eslint-config/next.js
    const { rules } = require('./utils/rules');

    module.exports = {
    extends: ['next'],
    ignorePatterns: ['**/.next/**', '**/.eslintrc.js'],
    root: true,
    };

    Adding presets to workspaces

    Now, we'll want to use these presets out in a workspace. To do so, we'll need to do two things:

    1. Create a .eslintrc.js file in the workspace.
    packages/logger/.eslintrc.js
    /** @type {import("eslint").Linter.Config} */
    module.exports = {
    extends: [require.resolve('@repo/lint/node')], // Installed in next step
    root: true, // Very important!
    };
    packages/logger/.eslintrc.js
    /** @type {import("eslint").Linter.Config} */
    module.exports = {
    extends: [require.resolve('@repo/lint/node')], // Installed in next step
    root: true, // Very important!
    };

    Note the root: true property! This tells ESLint that it does NOT need to look outside of your workspace for more ESLint configuration files.

    1. Install our @repo/lint package to the workspace and create a lint script.
    packages/logger/package.json
    {
    "name": "@repo/logger",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "lint": "eslint ."
    },
    "devDependencies": {
    "@repo/lint": "workspace:*",
    "eslint": "^8.42.0"
    }
    }
    packages/logger/package.json
    {
    "name": "@repo/logger",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "lint": "eslint ."
    },
    "devDependencies": {
    "@repo/lint": "workspace:*",
    "eslint": "^8.42.0"
    }
    }

    Overriding rules in a workspace

    To build off of our presets for any specific needs in a particular workspace, you can leverage the overrides property of ESLint. It may look something like this:

    packages/ui/.eslintrc.js
    module.exports = {
    extends: [require.resolve("@repo/lint/node")]
    root: true,
    overrides: [
    // Your overrides here.
    ],
    };
    packages/ui/.eslintrc.js
    module.exports = {
    extends: [require.resolve("@repo/lint/node")]
    root: true,
    overrides: [
    // Your overrides here.
    ],
    };

    Write a pipeline

    Once we've created our linting scripts in any workspaces that we want to lint, it's time to build up our Turborepo pipeline.

    turbo.json
    {
    "pipeline": {
    "topo": {
    "dependsOn": ["^topo"]
    },
    "lint": {
    "outputs": ["node_modules/.cache/.eslintcache"],
    "dependsOn": ["^topo"]
    }
    }
    }
    turbo.json
    {
    "pipeline": {
    "topo": {
    "dependsOn": ["^topo"]
    },
    "lint": {
    "outputs": ["node_modules/.cache/.eslintcache"],
    "dependsOn": ["^topo"]
    }
    }
    }

    Run our lint tasks

    With all of that ready to go, we're now ready to run our tasks!

    In the root of our monorepo, we will create these scripts:

    package.json
    {
    "scripts": {
    "lint": "turbo lint --continue -- --cache --cache-location 'node_modules/.cache/.eslintcache'",
    "lint:fix": "turbo lint --continue -- --fix --cache --cache-location 'node_modules/.cache/.eslintcache'"
    }
    }
    package.json
    {
    "scripts": {
    "lint": "turbo lint --continue -- --cache --cache-location 'node_modules/.cache/.eslintcache'",
    "lint:fix": "turbo lint --continue -- --fix --cache --cache-location 'node_modules/.cache/.eslintcache'"
    }
    }

    Run pnpm lint! On the first run, the command will create caches in each workspace both at the ESLint and Turborepo layers.

    Breaking down the script

    There are a few key parts to this script. Breaking it down piece by piece:

    With this all in place, you can run your linting tasks with incredible speed.