Turborepo - Kompletny Przewodnik po Monorepo Build System
Czym jest Turborepo?
Turborepo to wysokowydajny build system stworzony specjalnie dla JavaScript i TypeScript monorepos. Przejęty przez Vercel w 2021 roku, stał się jednym z najpopularniejszych narzędzi do zarządzania dużymi bazami kodu. Główną ideą Turborepo jest przyspieszenie buildów poprzez inteligentne cache'owanie, parallel execution i incremental builds.
W tradycyjnym podejściu, budowanie monorepo oznacza uruchamianie tych samych tasków wielokrotnie, nawet gdy kod się nie zmienił. Turborepo rozwiązuje ten problem poprzez zapamiętywanie wyników poprzednich buildów i pomijanie niepotrzebnej pracy. Rezultat? Buildy, które trwały minuty, teraz kończą się w sekundach.
Dlaczego Turborepo?
Kluczowe zalety Turborepo
- Inteligentne cache'owanie - Nie buduje tego, co już zbudowane
- Parallel execution - Wykorzystuje wszystkie rdzenie CPU
- Remote caching - Współdziel cache między zespołem i CI
- Incremental builds - Buduj tylko to, co się zmieniło
- Pipeline definition - Definiuj zależności między taskami
- Zero config - Działa z npm, pnpm, yarn workspaces
- Vercel integration - Natywna integracja z Vercel
- Pruned subsets - Deployuj tylko potrzebne pakiety
Turborepo vs Inne Build Systems
| Cecha | Turborepo | Nx | Lerna | Rush |
|---|---|---|---|---|
| Caching | Lokalny + Remote | Lokalny + Remote | Brak (plugin) | Lokalny |
| Setup | Minimal | Rozbudowany | Prosty | Skomplikowany |
| Learning curve | Łatwa | Średnia | Łatwa | Trudna |
| Remote cache | Vercel (free) | Nx Cloud | Brak | Azure |
| Ecosystem | Rosnący | Duży | Legacy | Enterprise |
| Task runner | Wbudowany | Wbudowany | npm/yarn | Custom |
| Cena | Free + Vercel tiers | Free + Cloud tiers | Free | Free |
Kiedy wybrać Turborepo?
- Szybkość jest priorytetem - najszybszy czas do działającego buildu
- Już używasz Vercel - natywna integracja
- Prosty setup - minimalna konfiguracja
- Masz npm/pnpm/yarn workspaces - zero migration effort
- Potrzebujesz remote cache - współdzielenie między CI i devami
Instalacja i Konfiguracja
Nowy projekt
# Stwórz nowe monorepo z Turborepo
npx create-turbo@latest my-monorepo
# Lub z konkretnym template
npx create-turbo@latest my-monorepo --example with-tailwind
npx create-turbo@latest my-monorepo --example kitchen-sink
# Z pnpm
pnpm dlx create-turbo@latest my-monorepoDodanie do istniejącego projektu
# Zainstaluj Turborepo
npm install turbo --save-dev
# Lub globalnie
npm install turbo --global
# Z pnpm
pnpm add turbo --save-dev --workspace-rootStruktura projektu
my-monorepo/
├── apps/
│ ├── web/ # Next.js app
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── docs/ # Dokumentacja
│ │ ├── src/
│ │ └── package.json
│ └── admin/ # Panel admin
│ └── package.json
├── packages/
│ ├── ui/ # Shared UI components
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── config-eslint/ # Shared ESLint config
│ │ └── package.json
│ ├── config-typescript/ # Shared TS config
│ │ └── package.json
│ └── utils/ # Shared utilities
│ ├── src/
│ └── package.json
├── package.json # Root package.json
├── turbo.json # Turborepo config
├── pnpm-workspace.yaml # Workspace config (pnpm)
└── .gitignoreRoot package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"test": "turbo test",
"clean": "turbo clean",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"devDependencies": {
"turbo": "^2.0.0",
"prettier": "^3.0.0"
},
"packageManager": "pnpm@8.15.0"
}turbo.json - Główna konfiguracja
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [".env"],
"globalEnv": ["NODE_ENV", "CI"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"],
"env": ["DATABASE_URL", "API_KEY"]
},
"lint": {
"dependsOn": ["^lint"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"inputs": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"]
},
"clean": {
"cache": false
}
}
}Pipeline i Tasks
Zrozumienie Pipeline
Pipeline definiuje relacje między taskami w monorepo:
{
"tasks": {
"build": {
// ^ oznacza "najpierw zbuduj dependencies"
"dependsOn": ["^build"],
// Pliki wyjściowe do cache'owania
"outputs": ["dist/**", ".next/**"]
},
"test": {
// Zależy od lokalnego build (bez ^)
"dependsOn": ["build"],
// Pliki wejściowe wpływające na cache
"inputs": ["src/**", "test/**"]
},
"deploy": {
// Zależy od build i test tego samego pakietu
"dependsOn": ["build", "test"]
}
}
}Typy zależności
{
"tasks": {
"build": {
// ^build - build dependencies first (topological)
"dependsOn": ["^build"]
},
"test": {
// build - build THIS package first (same package)
"dependsOn": ["build"]
},
"deploy": {
// Oba typy razem
"dependsOn": ["^build", "test", "lint"]
},
"e2e": {
// Zależność od konkretnego pakietu
"dependsOn": ["web#build"]
}
}
}Outputs i Caching
{
"tasks": {
"build": {
"outputs": [
"dist/**",
".next/**",
"!.next/cache/**", // Exclude cache
"build/**"
]
},
"lint": {
// Brak outputs = task nie produkuje plików
"outputs": []
},
"test": {
"outputs": ["coverage/**"]
}
}
}Inputs - kontrola cache invalidation
{
"tasks": {
"build": {
// Tylko te pliki wpływają na cache
"inputs": [
"src/**/*.ts",
"src/**/*.tsx",
"package.json",
"tsconfig.json"
]
},
"lint": {
"inputs": [
"src/**/*.ts",
"src/**/*.tsx",
".eslintrc.js",
"package.json"
]
},
"test": {
"inputs": [
"$TURBO_DEFAULT$", // Domyślne inputs
"jest.config.js",
"test/**"
]
}
}
}Environment Variables
{
// Globalne env - wpływają na wszystkie taski
"globalEnv": ["CI", "NODE_ENV"],
// Globalne dependencies - pliki wpływające na wszystko
"globalDependencies": [".env", "tsconfig.base.json"],
"tasks": {
"build": {
// Task-specific env
"env": ["DATABASE_URL", "API_KEY", "NEXT_PUBLIC_*"],
"passThroughEnv": ["AWS_SECRET_KEY"] // Nie wpływa na cache
}
}
}Uruchamianie Tasków
Podstawowe komendy
# Uruchom task we wszystkich pakietach
turbo build
turbo lint
turbo test
# Dev mode (bez cache, persistent)
turbo dev
# Verbose output
turbo build --verbosity=2
# Dry run - pokaż co zostanie uruchomione
turbo build --dry-run
# Pokaż graph zależności
turbo build --graph
turbo build --graph=graph.pngFiltrowanie pakietów
# Tylko konkretny pakiet
turbo build --filter=web
turbo build --filter=@repo/ui
# Pakiet i jego dependencies
turbo build --filter=web...
# Pakiet i jego dependents (pakiety zależne od niego)
turbo build --filter=...@repo/ui
# Pakiety w konkretnym folderze
turbo build --filter="./apps/*"
turbo build --filter="./packages/*"
# Wykluczenie pakietów
turbo build --filter="!docs"
# Kombinacje
turbo build --filter="web..." --filter="!docs"
# Tylko zmienione od ostatniego commita
turbo build --filter="[HEAD^1]"
turbo build --filter="...[main...HEAD]"Zaawansowane filtry
# Pakiety zmienione w PR
turbo build --filter="[origin/main...HEAD]"
# Pakiety zależne od zmienionego ui
turbo build --filter="...@repo/ui[HEAD^1]"
# Wszystkie apps
turbo build --filter="./apps/**"
# Pakiety z konkretnym tagiem w package.json
turbo build --filter="@repo/*"Opcje wykonania
# Limit parallel tasks
turbo build --concurrency=4
turbo build --concurrency=50% # 50% CPU cores
# Kontynuuj mimo błędów
turbo build --continue
# Force rebuild (ignoruj cache)
turbo build --force
# Output logs
turbo build --output-logs=full
turbo build --output-logs=hash-only
turbo build --output-logs=new-only
turbo build --output-logs=errors-onlyRemote Caching
Konfiguracja z Vercel
# Login do Vercel
npx turbo login
# Połącz projekt
npx turbo link
# Teraz cache jest współdzielony!
turbo buildWeryfikacja Remote Cache
# Sprawdź status
turbo build --summarize
# Output pokaże:
# Cache: 5 cached, 2 computed
# Remote: 3 downloaded, 2 uploadedSelf-hosted Remote Cache
// Własny serwer cache (przykład z Express)
import express from 'express'
import { createHash } from 'crypto'
const app = express()
const cache = new Map<string, Buffer>()
// GET artifact
app.get('/v8/artifacts/:hash', (req, res) => {
const artifact = cache.get(req.params.hash)
if (artifact) {
res.send(artifact)
} else {
res.status(404).send('Not found')
}
})
// PUT artifact
app.put('/v8/artifacts/:hash', express.raw({ limit: '50mb' }), (req, res) => {
cache.set(req.params.hash, req.body)
res.status(200).send('OK')
})
app.listen(3001)# Użycie własnego serwera
turbo build --api="http://localhost:3001" --token="secret"Konfiguracja w CI/CD
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install
- run: pnpm build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}Workspace Packages
Package z UI komponentami
// packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.ts",
"./button": "./src/button.tsx",
"./card": "./src/card.tsx",
"./styles.css": "./styles.css"
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"lint": "eslint src/",
"test": "vitest run"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@repo/config-typescript": "workspace:*",
"tsup": "^8.0.0",
"typescript": "^5.0.0"
}
}// packages/ui/src/index.ts
export { Button } from './button'
export { Card } from './card'
export { Input } from './input'
export type { ButtonProps, CardProps, InputProps } from './types'Shared Config Packages
// packages/config-eslint/package.json
{
"name": "@repo/config-eslint",
"version": "0.0.0",
"private": true,
"main": "index.js",
"dependencies": {
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-react": "^7.0.0",
"eslint-plugin-react-hooks": "^4.0.0"
}
}// packages/config-eslint/index.js
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'react'],
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-unused-vars': 'warn'
},
settings: {
react: { version: 'detect' }
}
}Używanie shared packages
// apps/web/package.json
{
"name": "web",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "eslint . --ext .ts,.tsx"
},
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"next": "^14.0.0",
"react": "^18.0.0"
},
"devDependencies": {
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*"
}
}// apps/web/.eslintrc.js
module.exports = {
extends: ['@repo/config-eslint'],
parserOptions: {
project: './tsconfig.json'
}
}TypeScript Config Sharing
// packages/config-typescript/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
}
}// packages/config-typescript/nextjs.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "ES2022"],
"module": "ESNext",
"target": "ES2022",
"jsx": "preserve",
"plugins": [{ "name": "next" }]
}
}// apps/web/tsconfig.json
{
"extends": "@repo/config-typescript/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}Package-specific Tasks
Nadpisywanie tasków per-package
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
// Nadpisanie dla konkretnego pakietu
"web#build": {
"dependsOn": ["^build"],
"outputs": [".next/**"],
"env": ["NEXT_PUBLIC_API_URL"]
},
// Nadpisanie dla docs
"docs#build": {
"dependsOn": ["^build"],
"outputs": [".docusaurus/**", "build/**"]
}
}
}Lokalne turbo.json w pakiecie
// apps/web/turbo.json
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"], // Dziedzicz z root turbo.json
"tasks": {
"build": {
// Nadpisz tylko specyficzne opcje
"env": ["ANALYZE", "NEXT_PUBLIC_SENTRY_DSN"]
},
"dev": {
"persistent": true,
"cache": false
}
}
}Generators (Codegen)
Tworzenie generatora
# Stwórz folder generators
mkdir -p turbo/generators// turbo/generators/config.ts
import type { PlopTypes } from '@turbo/gen'
export default function generator(plop: PlopTypes.NodePlopAPI): void {
// Generator nowego pakietu
plop.setGenerator('package', {
description: 'Stwórz nowy package',
prompts: [
{
type: 'input',
name: 'name',
message: 'Nazwa pakietu:'
},
{
type: 'list',
name: 'type',
message: 'Typ pakietu:',
choices: ['lib', 'config', 'tool']
}
],
actions: [
{
type: 'add',
path: 'packages/{{name}}/package.json',
templateFile: 'templates/package.json.hbs'
},
{
type: 'add',
path: 'packages/{{name}}/src/index.ts',
templateFile: 'templates/index.ts.hbs'
},
{
type: 'add',
path: 'packages/{{name}}/tsconfig.json',
templateFile: 'templates/tsconfig.json.hbs'
}
]
})
// Generator nowej app
plop.setGenerator('app', {
description: 'Stwórz nową aplikację',
prompts: [
{
type: 'input',
name: 'name',
message: 'Nazwa aplikacji:'
},
{
type: 'list',
name: 'framework',
message: 'Framework:',
choices: ['next', 'remix', 'astro']
}
],
actions: (data) => {
const actions: PlopTypes.ActionType[] = []
if (data?.framework === 'next') {
actions.push({
type: 'addMany',
destination: 'apps/{{name}}',
templateFiles: 'templates/next/**/*',
base: 'templates/next'
})
}
return actions
}
})
}Użycie generatora
# Lista dostępnych generatorów
turbo gen
# Uruchom konkretny generator
turbo gen package
turbo gen app
# Z argumentami
turbo gen package --name=my-lib --type=libTemplates
{
"name": "@repo/",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"lint": "eslint src/",
"test": "vitest run"
},
"devDependencies": {
"@repo/config-typescript": "workspace:*",
"tsup": "^8.0.0",
"typescript": "^5.0.0"
}
}Pruned Deployments
Docker z Pruned Monorepo
# Dockerfile
FROM node:20-alpine AS base
# Pruner stage
FROM base AS pruner
RUN npm install -g turbo
WORKDIR /app
COPY . .
RUN turbo prune web --docker
# Installer stage
FROM base AS installer
WORKDIR /app
# Instaluj tylko potrzebne dependencies
COPY /app/out/json/ .
COPY /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable pnpm && pnpm install --frozen-lockfile
# Builder stage
FROM base AS builder
WORKDIR /app
COPY /app/ .
COPY /app/out/full/ .
RUN corepack enable pnpm && pnpm turbo build --filter=web
# Runner stage
FROM base AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
COPY /app/apps/web/.next/standalone ./
COPY /app/apps/web/.next/static ./apps/web/.next/static
COPY /app/apps/web/public ./apps/web/public
CMD ["node", "apps/web/server.js"]Pruning dla CI
# Stwórz pruned subset
turbo prune web --docker
# Wynik w out/:
# out/
# ├── json/ # package.json files
# ├── full/ # Full source code
# └── pnpm-lock.yaml # Pruned lockfileDebugging i Troubleshooting
Cache Debugging
# Pokaż dlaczego task został wykonany
turbo build --summarize
# Pokaż hash computation
turbo build --dry-run=json | jq
# Force cache miss
turbo build --force
# Wyczyść cache
turbo clean
rm -rf node_modules/.cache/turboTask Graph
# Generuj graf zależności
turbo build --graph=graph.html
turbo build --graph=graph.png
turbo build --graph=graph.json
# Pokaż w konsoli
turbo build --graphVerbose Logging
# Poziomy verbosity
turbo build --verbosity=0 # Quiet
turbo build --verbosity=1 # Default
turbo build --verbosity=2 # Verbose
# Pokaż wszystkie logi
turbo build --output-logs=full
# Tylko errory
turbo build --output-logs=errors-onlyCommon Issues
# Problem: Cache nie działa
# Rozwiązanie: Sprawdź inputs i outputs
# Problem: Task wykonuje się mimo braku zmian
# Sprawdź env variables
turbo build --summarize | grep -A 10 "environment"
# Problem: Circular dependency
turbo build --graph # Wizualizuj zależności
# Problem: Slow builds
turbo build --profile=profile.json
# Analizuj w Chrome DevToolsBest Practices
Struktura pakietów
packages/
├── core/ # Biznesowa logika (bez UI)
├── ui/ # Shared komponenty React
├── hooks/ # Shared React hooks
├── utils/ # Helpers, formatters
├── types/ # Shared TypeScript types
├── config-*/ # Shared configs (eslint, ts, prettier)
└── api-client/ # Generated API clientNaming Conventions
// Używaj scope dla pakietów
{
"name": "@repo/ui", // ✅
"name": "@mycompany/ui", // ✅
"name": "ui", // ❌ Konflikt z npm packages
}Dependency Management
// Root package.json - shared devDependencies
{
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.0.0",
"prettier": "^3.0.0"
}
}
// Package - tylko specyficzne deps
{
"dependencies": {
"@repo/ui": "workspace:*" // Internal dependency
},
"devDependencies": {
"@repo/config-eslint": "workspace:*"
}
}Task Organization
{
"tasks": {
// Build pipeline
"build": { "dependsOn": ["^build"] },
"build:production": { "dependsOn": ["^build", "lint", "test"] },
// Dev
"dev": { "cache": false, "persistent": true },
// Quality
"lint": { "outputs": [] },
"lint:fix": { "outputs": [], "cache": false },
"typecheck": { "outputs": [] },
"test": { "dependsOn": ["build"] },
"test:watch": { "cache": false, "persistent": true },
// Maintenance
"clean": { "cache": false }
}
}Integracje
Vercel Deployment
// vercel.json
{
"buildCommand": "pnpm turbo build --filter=web",
"installCommand": "pnpm install",
"framework": "nextjs",
"outputDirectory": "apps/web/.next"
}GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Potrzebne dla --filter=[HEAD^1]
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm turbo build
- name: Test
run: pnpm turbo test
- name: Lint
run: pnpm turbo lintChangesets (versioning)
npm install @changesets/cli -D
npx changeset init// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@repo/config-*"]
}FAQ - Najczęściej Zadawane Pytania
Czy Turborepo wymaga Vercel?
Nie! Turborepo jest open-source i działa bez Vercel. Remote caching można hostować samodzielnie lub używać Vercel jako wygodnej opcji.
Jak migrować z Lerna/Nx?
Turborepo jest addytywny - możesz dodać turbo.json do istniejącego monorepo bez usuwania innych narzędzi. Stopniowo przenoś taski do Turborepo.
Czy mogę używać npm zamiast pnpm?
Tak! Turborepo działa z npm workspaces, pnpm workspaces i yarn workspaces. pnpm jest rekomendowany ze względu na wydajność.
Jak debugować problemy z cache?
Użyj turbo build --summarize żeby zobaczyć co wpływa na cache hash. Sprawdź env variables, inputs i outputs w turbo.json.
Czy Turborepo wspiera monorepo z różnymi językami?
Turborepo jest zoptymalizowany dla JavaScript/TypeScript, ale może orchestrować dowolne taski. Dla polyglot monorepo rozważ Nx lub Bazel.
Jaka jest różnica między dependsOn: ["^build"] a dependsOn: ["build"]?
^build- najpierw zbuduj dependencies (pakiety od których zależymy)build- najpierw zbuduj w tym samym pakiecie
Jak zoptymalizować CI build time?
- Włącz remote caching
- Użyj
--filterdla changed packages - Parallel jobs w CI
- Optymalizuj inputs/outputs
Podsumowanie
Turborepo to potężne narzędzie do zarządzania monorepo, które oferuje:
- Inteligentne caching - lokalne i remote
- Parallel execution - maksymalne wykorzystanie CPU
- Minimal config - działa out-of-the-box
- Vercel integration - natywna integracja
- Incremental adoption - łatwa migracja
Idealny dla zespołów szukających prostego, ale potężnego rozwiązania do monorepo bez zbędnej złożoności.