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.

Source of truth

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.

TaskDepends onCached?Persistent?Purpose
build^buildYesNoCompile every workspace (tsup, Next.js, etc.) into dist/ or .next/
lint^buildYesNoRun ESLint across all workspaces
type-check^buildYesNoRun tsc --noEmit across all workspaces
test^buildYesNoRun the test suite in each workspace that defines one
dev— (none)NoYesStart 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/docs depends on @itu/docs-ui, @itu/mdx, @itu/ui, etc.
  • When you run turbo type-check on apps/docs, Turbo first runs build on every one of those dependencies so their compiled dist/ output exists.
  • That's why lint, type-check, and test all declare dependsOn: ["^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:

TaskDeclared outputsNotes
builddist/**, .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
devn/aPersistent 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 caseToolExample
Run a script across the whole monorepoturbo (via pnpm)pnpm build
Run a script in one package, respecting its dependency graphturbopnpm turbo run build --filter=@itu/docs
Run a script in one package directly, no graphpnpmpnpm --filter @itu/docs dev
Add a dependency to a single workspacepnpmpnpm --filter @itu/ui add clsx
Install everythingpnpmpnpm install

The root package.json scripts are thin wrappers around turbo:

bash
pnpm dev # -> turbo dev --concurrency 20
pnpm build # -> turbo build
pnpm lint # -> turbo lint
pnpm type-check # -> turbo type-check
pnpm test # -> turbo test
5 lines of bash code

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):

bash
pnpm --filter @itu/docs dev
1 line of bash code
bash
pnpm --filter @itu/ui run a11y button
1 line of bash code
bash
pnpm --filter @itu/ui exec tsx scripts/extract-variants.ts
1 line of bash code

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

bash
# Full pipeline: build every workspace in dependency order, cached
pnpm build
2 lines of bash code
bash
# Type-check the whole monorepo. Will run upstream builds first.
pnpm type-check
2 lines of bash code
bash
# Lint everything
pnpm lint
2 lines of bash code
bash
# Start all dev servers concurrently (persistent)
pnpm dev
2 lines of bash code
bash
# Start just the docs site in dev mode (skips Turbo, faster iteration)
pnpm --filter @itu/docs dev
2 lines of bash code
bash
# Build just the UI package and everything it depends on
pnpm turbo run build --filter=@itu/ui
2 lines of bash code

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:

bash
pnpm turbo run build --force
1 line of bash code

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.