Murad fixed a single line in web, and the CI checks again build and test everything: shared, ui, api, web. Nine minutes of waiting for a line that touches neither api nor shared. A package manager cannot help here: it installs and links packages, but it does not remember what has already been done. That is the job of a task orchestrator, and the most widespread one is Turborepo. In this lesson you will set it up, understand the task graph and the cache, and see how nine minutes turn into a few seconds.
Why a task orchestrator
The pnpm -r run build command already goes from dependencies to dependents, but such a run has three weak spots.
- No memory. The sources have not changed, yet
buildruns again. - All or nothing. You cannot say "check only what my change touches".
- The result is not shared. What your computer built, neither a colleague nor CI will get.
Turborepo is a fast program written in Rust: it runs the scripts from package.json along the dependency graph, parallelizes everything it can and remembers the results. It does not replace pnpm: pnpm installs dependencies and links packages, while Turborepo runs tasks. It is installed once, in the root of the workspace. It takes the information about the package manager from the packageManager field of the root package.json:
pnpm add -D -w turbo
pnpm exec turbo --versionAdd wrapper scripts to the root package.json and put the .turbo folder into .gitignore — Turborepo keeps its cache and logs there:
{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"check-types": "turbo run check-types"
},
"devDependencies": {
"turbo": "^2.11.0"
}
}💡 In pnpm 12 the order of tasks can be described without Turborepo too, with the
taskssetting, and the experimentalpnpm pipelinecommand has a cache. Turborepo is still the most widespread choice, with a ready remote cache and a rich set of commands.
Two graphs: packages and tasks
The package graph builds itself: if apps/web/package.json has "@repo/shared": "workspace:*", then web depends on shared. The task graph you describe in turbo.json: which task has to wait for which.
{
"$schema": "https://turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**", "!.next/dev/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"check-types": {
"dependsOn": ["^check-types"]
},
"dev": {
"dependsOn": ["^build"],
"cache": false,
"persistent": true
}
}
}Every task in tasks is tied to the scripts of the same name in the packages' package.json files. The main keys: