Turbo pipeline
How work flows through the monorepo via Turborepo's task graph.
Turborepo is the task orchestrator for this monorepo. It understands the dependency graph between workspaces and runs tasks in the correct order — across every package, in parallel where possible, and with caching so unchanged work is skipped.
Everything Turbo does is declared in a single file at the repo root: turbo.json. If you want to know what happens when you run pnpm build, that file is the answer.
The authoritative task definitions live in turbo.json at the repo root. If this page and turbo.json ever disagree, turbo.json wins — please fix this page.
The task pipeline
Five tasks are configured. Four of them are cached, deterministic build-like operations. One of them (dev) is a persistent, long-running process.
| Task | Depends on | Cached? | Persistent? | Purpose |
|---|---|---|---|---|
build | ^build | Yes | No | Compile every workspace (tsup, Next.js, etc.) into dist/ or .next/ |
lint | ^build | Yes | No | Run ESLint across all workspaces |
type-check | ^build | Yes | No | Run tsc --noEmit across all workspaces |
test | ^build | Yes | No | Run the test suite in each workspace that defines one |
dev | — (none) | No | Yes | Start all dev servers concurrently with hot reload |
Reading dependsOn
The ^build entry is Turbo's way of saying "before you run my task, make sure the build task has finished in every package I depend on." The caret prefix means "upstream dependencies" — the packages listed in dependencies / devDependencies inside this workspace's package.json.
Concretely:
apps/docsdepends on@itu/docs-ui,@itu/mdx,@itu/ui, etc.- When you run
turbo type-checkonapps/docs, Turbo first runsbuildon every one of those dependencies so their compileddist/output exists. - That's why
lint,type-check, andtestall declaredependsOn: ["^build"]— each of them needs the compiled output of upstream packages to do its job correctly.
The build task itself also declares dependsOn: ["^build"], which chains the graph upward: build a leaf package, then its dependents, then their dependents, all the way to the apps.
Outputs and caching
Cached tasks tell Turbo what files they produce so it can restore them on a cache hit:
| Task | Declared outputs | Notes |
|---|---|---|
build | dist/**, .next/** | Captures both tsup bundle output and Next.js build output |
lint | — (none declared) | No file outputs — cached based on inputs + exit code |
type-check | — (none declared) | No file outputs — cached based on inputs + exit code |
test | — (none declared) | No file outputs — cached based on inputs + exit code |
dev | n/a | Persistent task, never cached |
When a task has no declared outputs, Turbo still caches the fact that it succeeded for a given input hash. The next time you run it with unchanged inputs, Turbo replays the saved logs and skips re-running.
Why dev is different
The dev task is marked persistent: true and cache: false. Persistent tasks never exit on their own — they're long-running watchers. Turbo refuses to allow other tasks to depend on a persistent task (because they'd never start), and it disables caching (because there's nothing cacheable about a process that runs forever).
pnpm vs turbo
Both pnpm and turbo can run scripts, but they solve different problems. Use the right tool for the job:
| Use case | Tool | Example |
|---|---|---|
| Run a script across the whole monorepo | turbo (via pnpm) | pnpm build |
| Run a script in one package, respecting its dependency graph | turbo | pnpm turbo run build --filter=@itu/docs |
| Run a script in one package directly, no graph | pnpm | pnpm --filter @itu/docs dev |
| Add a dependency to a single workspace | pnpm | pnpm --filter @itu/ui add clsx |
| Install everything | pnpm | pnpm install |
The root package.json scripts are thin wrappers around turbo:
pnpm dev # -> turbo dev --concurrency 20 pnpm build # -> turbo build pnpm lint # -> turbo lint pnpm type-check # -> turbo type-check pnpm test # -> turbo test
So when you run pnpm build at the repo root, you're getting the full pipeline-aware build: dependency ordering, parallelism, and caching. That's almost always what you want.
When to reach for pnpm --filter
Use pnpm --filter <package> <script> when you want to run a script in exactly one workspace and you know its dependencies are already built (or don't matter for what you're doing):
pnpm --filter @itu/docs dev
pnpm --filter @itu/ui run a11y button
pnpm --filter @itu/ui exec tsx scripts/extract-variants.ts
These skip Turbo entirely and invoke the script directly in that one package. They're faster for iteration, but they won't rebuild upstream dependencies for you.
Common commands
# Full pipeline: build every workspace in dependency order, cached pnpm build
# Type-check the whole monorepo. Will run upstream builds first. pnpm type-check
# Lint everything pnpm lint
# Start all dev servers concurrently (persistent) pnpm dev
# Start just the docs site in dev mode (skips Turbo, faster iteration) pnpm --filter @itu/docs dev
# Build just the UI package and everything it depends on pnpm turbo run build --filter=@itu/ui
Cache hits and misses
Turbo hashes the inputs of every task (source files, package.json, relevant config). If the hash matches a previous run, the task is a cache hit — its output is replayed instantly and you'll see FULL TURBO in the output.
If you ever want to force a rebuild, use:
pnpm turbo run build --force
But usually, a cache hit is exactly what you want. It means the work is already done.
CI runs the same Turbo commands. That means CI benefits from the cache too — once remote caching is wired up (see the Turbo docs), a PR that only touches apps/docs won't re-lint or re-type-check untouched packages.
What's next?
- The pre-push git hook is where the a11y audit plugs into this pipeline.
- The a11y audit runs outside of Turbo but is triggered by the same diff-driven approach.