September 22, 2023
Turborepo: Simplifying Monorepo Management
📘 TLDR: Turporepo helps manage monorepos based on a package manager’s workspace feature by combining similar commands and caching results in a local or a remote cache. In addition, PLOP-based (opens in a new tab) generators help with faster code scaffolding.
Introduction
There are a number of pros and cons to using a monorepo repository, but for me what makes it worth the trouble is the ability to share code within the project, especially when these projects share a lot of similar frameworks and libraries. Data models, type declarations, and interfaces don’t change between a client and a server often. Synchronizing it all by hand is tedious, however, setting up a package repository just for that might be an overkill. A common use for the monorepo is having a shared design system or a component library again without setting up a package repository. I have mostly used Nx (opens in a new tab) in the past for the purpose of organizing a monorepo and decided to take a look at another tool on the market.
These are my first impressions of a Turborepo (opens in a new tab) - a Vercel (opens in a new tab) product calling itself an intelligent build system optimized for JavaScript and TypeScript codebases.
A short overview from a hands-on perspective follows.
Workspaces
The core of any monorepo in a JavaScript world is a concept called “workspace” which is just a set of tools implemented (usually) by a package manager that allows for linking a package from a repository folder structure without involving a package registry. I recommend checking out a dedicated workspace documentation:
Setting Up
After running a simple command pnpm dlx create-turbo@latest
and choosing an installation folder we end up with a ready-made monorepo sample with two Next.js (Turborepo is a Vercel product after all) in an ./app
folder and ./packages
folder containing a mock react library, TypeScript and ESLint configs, each in their own folder. You might want to clean up everything after installation if you don’t intend to use Next.js in your own project, but this setup is perfect for our overview.
Each folder contains a package.json
file, root folder has it’s own package.json
for tools such as prettier
.
root
├── apps
│ ├── docs
│ ├── web
├── node_modules
├── packages
│ ├── eslint-config-custom
│ ├── tsconfig
│ └── ui
Dependencies after initial setup
Let’s check out apps
packages first, two Next.js apps unsurprisingly have the same set of dependencies:
"next": "^13.4.19",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"ui": "workspace:*"
We can see that dependencies of apps are isolated from each other and that we can reference a package from ./packages
folder by name using workspace:*
where a package version would be.
Now let’s examine package.json
in ./packages/ui
folder.
{
"name": "ui",
"version": "0.0.0",
"main": "./index.tsx",
"types": "./index.tsx",
"license": "MIT",
"scripts": {
"lint": "eslint .",
"generate:component": "turbo gen react-component"
},
"devDependencies": {
...
"eslint-config-custom": "workspace:*",
"react": "^18.2.0",
"tsconfig": "workspace:*",
"typescript": "^4.5.2"
}
}
We can see that the package's name is the same as a folder name, so we need to watch which one is used when referencing a package from a monorepo.
Property main
points to the location of package exports and types
are where types are located. Both share the same file in this example, so we will also explore splitting types and module export.
Packages eslint-config-custom
and tsconfig
are both parts of the monorepo and use workspace:*
.
Another thing to note here is a "generate:component"
command which hints to us that
there is a codegen feature. (opens in a new tab)
For tsconfig
we have no dependencies:
{
"name": "tsconfig",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
}
}
/*Docs version*/
{
"name": "tsconfig",
"files": ["base.json", "nextjs.json", "react-library.json"]
}
Packages sharing tsconfig
are using extends
property in their tsconfig.json
to share
And in a root folder we can find a set of useful commands:
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
Turbo running
As long as a command is present in a turbo.json
running turbo *command_name*
will execute this command as long as it it’s present in a package.json
.
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
}
}
}
All results will be cached and Turborepo will use a cached result of an operation as long as no changes to the codebase are made and the cache is not explicitly turned off (see a dev
command above).
That allows us to run just one build/lint/format command on all of our repos and don’t sacrifice much of the development speed.
Generators
It might seem like code generation is an unusual addition to a monorepo tool, however, once your project grows beyond one framework it will become progressively harder to ensure code consistency and make even creating a new component a daunting task.
It’s going to be much easier to start working on something within a monorepo when we use Generators (opens in a new tab).
With Turborepo we can generate pretty much anything like:
- A workspace
turbo gen workspace
- A component
turbo gen react-component
- Or create a custom generator based on PLOP (opens in a new tab)
In addition, there are a few nice things that help organize generators:
- They are detected automatically
- You don’t have to install
plop
- Generators are running from a workspace where they are defined
Conclusion
Turborepo does not have a lot of features, instead, it identifies the core pain points of running a monorepo project and tries to solve them efficiently. These points are
- Running tasks
- Generating code
I don’t have much to say regarding generating code, but it’s very clear how much time only the remote cache feature can save for any decent-sized team.
By leaving the task of managing workspaces to a package manager of your choice Turborepo makes it easy to adopt it into any project. If you have decided on using a monorepo in your project it’s pretty easy to add a Turborepo on top of it and get the benefits even if just have one UI library, shared configuration, and an app, it will help to keep the project organized setup times short and the DX smooth.
Other monorepo tools
I don’t want to make a direct comparison of monorepo tools here. Feel free to check out these awesome projects:
Tags: monorepo, turborepo, tools, overview, workspaces