Monorepo
How the Zeno monorepo is organized, and how Turborepo runs and caches its tasks
Zeno is a pnpm workspace, split into packages that each cover one concern. Turborepo runs their tasks in dependency order and caches the outputs.
Why a monorepo?
A monorepo helps even if you only ship a single application:
- Shared tooling: TypeScript configs, the Tailwind theme and the Vitest setup live in dedicated packages, and the root
biome.jsoncholds the linting rules. Every app inherits them without copy-pasting. - Atomic changes: Update a UI component and the app that consumes it in a single commit, with a single CI run that validates everything together.
- Incremental adoption: Start with one app. When you add a second one (a marketing site, an admin panel, a backend for a mobile app), it goes under
apps/and uses the same packages. - Enforced boundaries: Each package declares its public API in the
exportsfield of itspackage.json, so other packages can't import its internals.
Turborepo & remote caching
Turborepo reads the task graph from turbo.json and the dependencies between packages. It caches each task's outputs on disk, and when a task's inputs haven't changed, it restores the cached outputs instead of running the task again.
Turborepo Remote Caching shares that cache between machines, CI included. When you enable it, Turborepo stores task outputs in a shared cache, hosted by Vercel or on your own server. This means:
- A task that already ran with the same inputs on another developer's machine or in an earlier CI run doesn't run again. Turborepo downloads its cached output instead.
- CI only does work that no earlier run has cached, so pipelines get faster.
- Cache entries are keyed by a hash of the task's inputs and scoped to your team, so a task only reuses outputs built from the same inputs.
Structure
zeno/
├── apps/ # Applications
│ └── docs/ # Documentation site (@zeno-lib/docs)
├── packages/ # Shared libraries
│ ├── ui/ # React components (@zeno-lib/ui)
│ ├── typescript/ # TypeScript configs (@zeno-lib/typescript)
│ ├── supabase/ # Supabase integration (@zeno-lib/supabase)
│ ├── test/ # Vitest config (@zeno-lib/test)
│ └── e2e/ # Playwright E2E tests (@zeno-lib/e2e)
├── turbo.json # Turborepo task configuration
└── pnpm-workspace.yaml # Workspace definitionCommands
# Development
pnpm dev # Start all dev servers
# Building
pnpm build # Build all packages
# Type checking
pnpm types:check # Check TypeScript across all packages
# Testing
pnpm test # Run unit tests
pnpm e2e # Run E2E tests
# Linting
pnpm lint # Check code quality
pnpm lint:fix # Auto-fix issues
# CI
pnpm ci # Run full CI pipelineFiltering
Run commands for a specific package using Turborepo filters:
pnpm turbo run dev --filter @zeno-lib/docs
pnpm turbo run build --filter @zeno-lib/uiAdding dependencies
# Add to the root workspace
pnpm add -D <package> -w
# Add to a specific package
pnpm add <package> --filter @zeno-lib/uiWorkspace references
Reference internal packages in any package.json using workspace:*:
{
"dependencies": {
"@zeno-lib/ui": "workspace:*",
"@zeno-lib/typescript": "workspace:*"
}
}