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.

But we're maestros! We can do this. Let's take a look at how to happily set up ESLint for 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 including the files for 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 a next.js file 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'],
    overrides: [
    {
    files: [
    'pages/**',
    'src/pages/**',
    'next.config.js',
    'app/**/{head,layout,loading,page,error,not-found}.tsx',
    'contentlayer.config.ts',
    ],
    rules: {
    'import/no-default-export': 'off',
    },
    },
    ],
    root: true,
    };
    tooling/eslint-config/next.js
    const { rules } = require('./utils/rules');

    module.exports = {
    extends: ['next'],
    ignorePatterns: ['**/.next/**', '**/.eslintrc.js'],
    overrides: [
    {
    files: [
    'pages/**',
    'src/pages/**',
    'next.config.js',
    'app/**/{head,layout,loading,page,error,not-found}.tsx',
    'contentlayer.config.ts',
    ],
    rules: {
    'import/no-default-export': 'off',
    },
    },
    ],
    root: true,
    };

    Using your newfound knowledge, you can also add any extra configurations that you see fit.

    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 doesn't need to look outside of your workspace for any more configuration. By default, ESLint will look upwards in your project for more configuration.

    root: true prevents this. All of the config to be used within the workspace is now in the extends key and the workspace file itself.

    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 . --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0",
    "lint:fix": "eslint . --fix --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0"
    },
    "devDependencies": {
    "@repo/lint": "workspace:*",
    "eslint": "^8.42.0"
    }
    }
    packages/logger/package.json
    {
    "name": "@repo/logger",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "lint": "eslint . --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0",
    "lint:fix": "eslint . --fix --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0"
    },
    "devDependencies": {
    "@repo/lint": "workspace:*",
    "eslint": "^8.42.0"
    }
    }

    We're including some flags on these commands. You can learn more about them in the ESLint CLI documentation but, to make a long story short:

    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.
    ],
    };

    Linting the root

    You can also ensure that you're linting any JavaScript/TypeScript files that aren't in your workspaces.

    Add a root .eslintrc.js

    We'll first need a configuration file for our ESLint to use.

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

    Create a linting task

    We'll now create a root linting task that looks like the ones from our workspaces - but with one key addition.

    package.json
    {
    "name": "my-monorepo",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "lint": "eslint . \"!apps/** !packages/** !tooling/**\" --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0",
    "lint:fix": "eslint . \"!apps/** !packages/** !tooling/**\" --fix --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0"
    },
    "devDependencies": {
    "@repo/lint": "workspace:*",
    "eslint": "^8.42.0"
    }
    }
    package.json
    {
    "name": "my-monorepo",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "lint": "eslint . \"!apps/** !packages/** !tooling/**\" --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0",
    "lint:fix": "eslint . \"!apps/** !packages/** !tooling/**\" --fix --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0"
    },
    "devDependencies": {
    "@repo/lint": "workspace:*",
    "eslint": "^8.42.0"
    }
    }

    The \"!apps/** !packages/** !tooling/**\" phrase ensures that our root ESLint task doesn't try to lint our workspaces - because our workspaces are responsible for themselves!

    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 pipelines.

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

    We're using a few Turborepo techniques in the pipelines above to keep things speedy.

    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",
    "lint:fix": "turbo lint:fix --continue"
    }
    }
    package.json
    {
    "scripts": {
    "lint": "turbo lint --continue",
    "lint:fix": "turbo lint:fix --continue"
    }
    }

    Note: --continue runs the rest of your tasks even if one fails.

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

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