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.
As we head into getting ESLint set up, let's remember our requirements for conducting a monorepo symphony:
We want our workspaces to be as self-contained as possible.
We want our workspace tasks to happen within our workspaces.
We want to encourage standardization across the entirety of our monorepo while still allowing for flexibility in workspaces when needed.
One-way configuration flow. When you need to understand what you're configuring for a package, you always have a singular "place" to start from and then you can work outwards.
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"
}
}
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.
Now, we'll want to use these presets out in a workspace. To do so, we'll need to do two things:
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!
};
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.
Install our @repo/lint package to the workspace and create a lint script.
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:
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!
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
}
}
}
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.
//# for root tasks: Tasks that need to be ran for the root of your repository must begin with //#.
Recursively depending on topo: This is a little trick from the Turborepo documentation. In short, this dependsOn pattern flattens your task graph so that everything runs in parallel while still respecting changes in workspace dependencies. (If that sounds confusing, don't worry about it for now; just trust that it works. We'll be writing up a doc for this but, for the time being, let it be magic.) ✨
Using ESLint caching: We're about to create an .eslintcache file in our workspaces in the next step. In the event that our task misses cache, we will still have the .eslintcache to use to speed up our task. Caching this file as a Turborepo output ensures that we have the .eslintcache shared across our machines as often as possible so we can use it in as many places as we can.
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.
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.
Running pnpm lint (without changing any code) will give you a >>> FULL TURBO. Awesome!
Changing some code in one workspace will hit cache for all your workspaces except that specific one. That workspace will use the ESLint cache file at node_modules/.cache/.eslintcache in each workspace to lint as fast as possible.
You can also run your pnpm lint:fix command to see if ESLint can find any auto-fixable problems.
With this all in place, you can run your linting tasks with incredible speed.