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!
Prettier
Prettier is a great formatter for quickly taking care of simple, syntax consistency in your codebase. You can automatically standardize your entire codebase to have the same stylistic qualities and never worry about it again.
Prettier has one simple job: standardize syntax. That simplicity can be reflected in the way that we set up Prettier since there isn't a need for overriding configurations in workspaces.
We will still be able to format our workspaces as their own separate tasks to take advantage of caching but we will only have one file in our repository for establishing style.
In your root package.json
, you'll need to do two things:
- Install
prettier
(and prettier-plugin-packagejson
if you'd like to format those).
- Add your formatting scripts.
{
"name": "my-project",
"version": "0.1.0",
"scripts": {
"format": "prettier . \"!apps/** !packages/** !tooling/**\" --check --cache --cache-location='node_modules/.cache/.prettiercache'",
"format:fix": "prettier . \"!apps/** !packages/** !tooling/**\" --write --cache --cache-location='node_modules/.cache/.prettiercache' --log-level=warn"
},
"devDependencies": {
"prettier": "^3.0.3",
"prettier-plugin-packagejson": "^2.4.3",
"turbo": "^1.10.13"
}
}
{
"name": "my-project",
"version": "0.1.0",
"scripts": {
"format": "prettier . \"!apps/** !packages/** !tooling/**\" --check --cache --cache-location='node_modules/.cache/.prettiercache'",
"format:fix": "prettier . \"!apps/** !packages/** !tooling/**\" --write --cache --cache-location='node_modules/.cache/.prettiercache' --log-level=warn"
},
"devDependencies": {
"prettier": "^3.0.3",
"prettier-plugin-packagejson": "^2.4.3",
"turbo": "^1.10.13"
}
}
Note: Prettier ^3.0.0 currently breaks the VSCode plugin for monorepos. We're sticking with ^2.8.0 for now.
{
"name": "my-project",
"version": "0.1.0",
"scripts": {
"format": "prettier . \"!apps/** !packages/** !tooling/**\" --check --cache --cache-location='node_modules/.cache/.prettiercache'",
"format:fix": "prettier . \"!apps/** !packages/** !tooling/**\" --write --cache --cache-location='node_modules/.cache/.prettiercache' --log-level=warn"
},
"devDependencies": {
"prettier": "^3.0.3",
"prettier-plugin-packagejson": "^2.4.3",
"turbo": "^1.10.13"
}
}
{
"name": "my-project",
"version": "0.1.0",
"scripts": {
"format": "prettier . \"!apps/** !packages/** !tooling/**\" --check --cache --cache-location='node_modules/.cache/.prettiercache'",
"format:fix": "prettier . \"!apps/** !packages/** !tooling/**\" --write --cache --cache-location='node_modules/.cache/.prettiercache' --log-level=warn"
},
"devDependencies": {
"prettier": "^3.0.3",
"prettier-plugin-packagejson": "^2.4.3",
"turbo": "^1.10.13"
}
}
Note: Prettier ^3.0.0 currently breaks the VSCode plugin for monorepos. We're sticking with ^2.8.0 for now.
We're including some flags on these commands so our tasks go as fast as possible. You can learn more about them in the Prettier CLI documentation but, to make a long story short:
- Ignore file globs: We don't want our root Prettier task to format our workspaces (because they will format themselves).
--cache
: Use cached assets to make Prettier runs faster.
--cache-location
: Assign a specific location for the Prettier cache file.
--loglevel
: Prevent writing every file that Prettier sees to the console.
In the root of your project, we'll write our rules for the repository:
// Purely for demonstration! Adjust to your liking.
/** @type {import("prettier").Options} */
const config = {
tabWidth: 2,
semi: false,
singleQuote: true,
};
export default config;
// Purely for demonstration! Adjust to your liking.
/** @type {import("prettier").Options} */
const config = {
tabWidth: 2,
semi: false,
singleQuote: true,
};
export default config;
// Purely for demonstration! Adjust to your liking.
/** @type {import("prettier").Options} */
const config = {
tabWidth: 2,
semi: false,
singleQuote: true,
};
export default config;
// Purely for demonstration! Adjust to your liking.
/** @type {import("prettier").Options} */
const config = {
tabWidth: 2,
semi: false,
singleQuote: true,
};
export default config;
Both our root and workspace Prettier scripts will end up using this configuration.
Let's get our workspaces formatted by adding scripts for these tasks. These are the same ones from the root.
{
"name": "@repo/ui",
"version": "0.0.0",
"scripts": {
"format": "prettier . --check --cache --cache-location='node_modules/.cache/.prettiercache'",
"format:fix": "prettier . --write --cache --cache-location='node_modules/.cache/.prettiercache' --log-level=warn"
}
}
{
"name": "@repo/ui",
"version": "0.0.0",
"scripts": {
"format": "prettier . --check --cache --cache-location='node_modules/.cache/.prettiercache'",
"format:fix": "prettier . --write --cache --cache-location='node_modules/.cache/.prettiercache' --log-level=warn"
}
}
{
"name": "@repo/ui",
"version": "0.0.0",
"scripts": {
"format": "prettier . --check --cache --cache-location='node_modules/.cache/.prettiercache'",
"format:fix": "prettier . --write --cache --cache-location='node_modules/.cache/.prettiercache' --log-level=warn"
}
}
{
"name": "@repo/ui",
"version": "0.0.0",
"scripts": {
"format": "prettier . --check --cache --cache-location='node_modules/.cache/.prettiercache'",
"format:fix": "prettier . --write --cache --cache-location='node_modules/.cache/.prettiercache' --log-level=warn"
}
}
Prettier does not look above it's execution context for ignore files. This means that you'll need to create a .prettierignore
file for each of your workspaces.
You will always want to ignore formatting for the output directory of your builds. For instance, in a workspace for a Next.js app, you'll want to add an ignore file like this one:
Once we've created our formatting scripts in any workspaces that we want to format, it's time to build up our Turborepo pipelines.
{
"pipeline": {
"topo": {
"dependsOn": ["^topo"]
},
"//#format": {
"outputs": ["node_modules/.cache/.prettiercache"]
},
"format": {
"dependsOn": ["^topo"],
"outputs": ["node_modules/.cache/.prettiercache"]
},
"//#format:fix": {
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
},
"format:fix": {
"dependsOn": ["^topo"],
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
}
}
}
{
"pipeline": {
"topo": {
"dependsOn": ["^topo"]
},
"//#format": {
"outputs": ["node_modules/.cache/.prettiercache"]
},
"format": {
"dependsOn": ["^topo"],
"outputs": ["node_modules/.cache/.prettiercache"]
},
"//#format:fix": {
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
},
"format:fix": {
"dependsOn": ["^topo"],
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
}
}
}
{
"pipeline": {
"topo": {
"dependsOn": ["^topo"]
},
"//#format": {
"outputs": ["node_modules/.cache/.prettiercache"]
},
"format": {
"dependsOn": ["^topo"],
"outputs": ["node_modules/.cache/.prettiercache"]
},
"//#format:fix": {
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
},
"format:fix": {
"dependsOn": ["^topo"],
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
}
}
}
{
"pipeline": {
"topo": {
"dependsOn": ["^topo"]
},
"//#format": {
"outputs": ["node_modules/.cache/.prettiercache"]
},
"format": {
"dependsOn": ["^topo"],
"outputs": ["node_modules/.cache/.prettiercache"]
},
"//#format:fix": {
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
},
"format:fix": {
"dependsOn": ["^topo"],
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
}
}
}
//#
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 Prettier caching: We're about to create a
.prettiercache
file in our workspaces in the next step. In the event that our task misses cache, we will still have the .prettiercache
to use to speed up our task. Caching this file as a Turborepo output
ensures that we have the .prettiercache
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:
{
"scripts": {
"format": "turbo format --continue",
"format:fix": "turbo format:fix --continue"
}
}
{
"scripts": {
"format": "turbo format --continue",
"format:fix": "turbo format:fix --continue"
}
}
Note: --continue runs the rest of your tasks even if one fails.
{
"scripts": {
"format": "turbo format --continue",
"format:fix": "turbo format:fix --continue"
}
}
{
"scripts": {
"format": "turbo format --continue",
"format:fix": "turbo format:fix --continue"
}
}
Note: --continue runs the rest of your tasks even if one fails.
Run pnpm format
! On the first run, the command will create caches in each workspace both at the Prettier and Turborepo layers.
- Running
pnpm format
(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 Prettier cache file at
node_modules/.cache/.prettiercache
in each workspace to format as fast as possible.
- You now run your
pnpm format:fix
command to see if Prettier can find any auto-fixable problems.
With this all in place, you can run your linting tasks with incredible speed.