Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik20 min czytania

Turborepo, szybsze budowanie w monorepozytorium

Turborepo przyspiesza budowanie w monorepozytorium przez pamięć podręczną wyników i równoległość. Konfiguracja zadań, pamięć zdalna i typowe błędy.

Turborepo, szybsze budowanie w monorepozytorium

Turborepo to narzędzie, które skraca czas budowania w repozytorium zawierającym wiele pakietów. Robi to dwoma sposobami: zapamiętuje wyniki zadań i pomija te, których wejście się nie zmieniło, oraz uruchamia równolegle wszystko, co nie zależy od siebie nawzajem. Rozwija je Vercel, licencja to MIT, a bieżąca wersja narzędzia to 2.10.9.

Problem, który to rozwiązuje

Monorepozytorium ma jedną oczywistą zaletę i jedną nieoczywistą wadę, a to narzędzie dotyka wyłącznie tej drugiej.

Zaleta jest znana: wspólny kod trzymasz w jednym miejscu, zmiana w bibliotece i w aplikacji idzie jednym zgłoszeniem, a wersje zależności wewnętrznych nie rozjeżdżają się nigdy. Wada ujawnia się przy pierwszym poważnym wzroście: budowanie całości zaczyna trwać coraz dłużej, bo przy każdej zmianie przebudowywane jest wszystko, także paczki, których nikt nie tknął.

Pomysł jest prosty i sprowadza się do dwóch obserwacji. Po pierwsze, jeśli wejście zadania nie zmieniło się, to wyjście też się nie zmieni, więc można je odtworzyć z pamięci zamiast liczyć od nowa. Po drugie, pakiety w monorepozytorium tworzą graf zależności, a zadania niepowiązane ze sobą można wykonać równolegle zamiast po kolei.

Efekt bywa dramatyczny właśnie dlatego, że w typowym repozytorium, na przykład z kilkoma aplikacjami na Next.js, większość zmian dotyka niewielkiej części kodu. Zmiana w jednej aplikacji nie wymaga przebudowania czterech pozostałych ani bibliotek, od których ta aplikacja nie zależy, a bez narzędzia tego rodzaju i tak by się to działo.

Warto natomiast wiedzieć, czego to nie robi. Nie zarządza zależnościami, bo od tego są przestrzenie roboczych menedżera pakietów. Nie publikuje pakietów ani nie wersjonuje. Nie przyspiesza też samego budowania pojedynczego pakietu, tylko pomija je, gdy da się pominąć. Jeśli Twój pojedynczy build trwa osiem minut, po wdrożeniu nadal będzie trwał osiem minut, tyle że rzadziej.

Klucz pamięci podręcznej, czyli sedno sprawy

Cała skuteczność zależy od tego, co narzędzie uzna za wejście zadania, i to jest miejsce, w którym powstaje najwięcej problemów.

Klucz liczony jest ze skrótu obejmującego zawartość plików źródłowych pakietu, jego zależności, wersje narzędzi i zmienne środowiskowe zadeklarowane w konfiguracji. Jeśli wszystko to się zgadza, wynik pochodzi z pamięci. Jeśli cokolwiek się zmieniło, zadanie wykonuje się od nowa.

Z tego wynika najczęstsza przyczyna kłopotów: zmienne środowiskowe, których nie zadeklarowano. Jeśli budowanie zależy od zmiennej wskazującej adres API, a ta zmienna nie jest wymieniona w konfiguracji, narzędzie uzna dwa różne wejścia za identyczne i poda wynik zbudowany dla innego środowiska. Objawem jest aplikacja produkcyjna odpytująca serwer testowy, a przyczyna leży w jednej brakującej linii konfiguracji.

Odwrotny problem też występuje i jest mniej groźny, za to irytujący. Zbyt szeroko określone wejścia sprawiają, że pamięć nigdy nie trafia, bo cokolwiek w repozytorium się zmieni, klucz się zmienia razem z tym. Zawężenie wejść do plików, które faktycznie wpływają na wynik, potrafi zamienić narzędzie z bezużytecznego w skuteczne.

Trzecia rzecz to wyjścia. Zadanie musi zadeklarować, które pliki produkuje, bo tylko te trafią do pamięci i tylko te zostaną odtworzone przy trafieniu. Zapomniany katalog wyjściowy objawia się tak, że zadanie kończy się natychmiast, ale efektów jego pracy nie ma, co bywa mylnie brane za błąd samego narzędzia.

Warto przy tym wiedzieć, że narzędzie ma tryb wyjaśniający, dlaczego pamięć nie trafiła. Uruchomienie zadania z podniesionym poziomem szczegółowości pokazuje wyliczony skrót i to, co się na niego złożyło, więc zamiast zgadywać, można porównać dwa uruchomienia i zobaczyć, który element wejścia się różni. To jest pierwsza rzecz do zrobienia, gdy czasy budowania nie poprawiły się po wdrożeniu, i zwykle wystarcza do znalezienia przyczyny w kilka minut.

Osobna sprawa dotyczy zadań, które nie produkują plików, na przykład sprawdzania typów albo uruchamiania testów. Ich wynikiem jest sam fakt powodzenia, więc deklaruje się je z pustą listą wyjść. Pamięć podręczna nadal działa i nadal ma sens, bo pomijane jest samo wykonanie, a to zwykle najdroższa część.

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

  1. Inteligentne cache'owanie - Nie buduje tego, co już zbudowane
  2. Parallel execution - Wykorzystuje wszystkie rdzenie CPU
  3. Remote caching - Współdziel cache między zespołem i CI
  4. Incremental builds - Buduj tylko to, co się zmieniło
  5. Pipeline definition - Definiuj zależności między taskami
  6. Zero config - Działa z npm, pnpm, yarn workspaces
  7. Vercel integration - Natywna integracja z Vercel
  8. Pruned subsets - Deployuj tylko potrzebne pakiety

Turborepo a inne systemy budowania

CechaTurborepoNxLernaRush
CachingLokalny + zdalnyLokalny + zdalnyLokalny + zdalny (przez Nx)Lokalny + zdalny
SetupMinimalRozbudowanyProstySkomplikowany
Learning curveŁatwaŚredniaŁatwaTrudna
Remote cacheVercel (darmowa)Nx CloudNx CloudAzure Blob lub S3
EcosystemRosnącyDużyUtrzymywany przez zespół NxEnterprise
Task runnerWbudowanyWbudowanynpm/yarnCustom
CenaFree + Vercel tiersFree + Cloud tiersFreeFree

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

Code
Bash
# 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-monorepo

Dodanie do istniejącego projektu

Code
Bash
# Zainstaluj Turborepo
npm install turbo --save-dev

# Lub globalnie
npm install turbo --global

# Z pnpm
pnpm add turbo --save-dev --workspace-root

Struktura projektu

Code
TEXT
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)
└── .gitignore

Root package.json

Code
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

Code
JSON
{
  "$schema": "https://turborepo.dev/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:

Code
JSON
{
  "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

Code
JSON
{
  "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"]
    }
  }
}

Wyniki i pamięć podręczna

Code
JSON
{
  "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

Code
JSON
{
  "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

Code
JSON
{
  // 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

Code
Bash
# 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.svg

Filtrowanie pakietów

Code
Bash
# 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

Code
Bash
# 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

Code
Bash
# 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-only

Zdalna pamięć podręczna

Konfiguracja z Vercel

Code
Bash
# Login do Vercel
npx turbo login

# Połącz projekt
npx turbo link

# Teraz cache jest współdzielony!
turbo build

Weryfikacja zdalnej pamięci

Code
Bash
# Zapisz podsumowanie przebiegu do pliku JSON
turbo build --summarize

# Na koniec przebiegu w konsoli pojawi się:
#  Tasks:    7 successful, 7 total
# Cached:    5 cached, 7 total
#   Time:    1.2s
# Summary:    .turbo/runs/<id>.json

Sam podział na trafienia lokalne i zdalne widać dopiero w tym pliku: każde zadanie ma tam pole cache z local, remote i status, więc to jest miejsce, w którym potwierdzasz, że wynik faktycznie przyszedł ze wspólnego magazynu.

Zdalna pamięć we własnej infrastrukturze

Code
TypeScript
// 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)
Code
Bash
# Użycie własnego serwera
turbo build --api="http://localhost:3001" --token="secret"

Konfiguracja w CI/CD

.github/workflows/ci.yml
YAML
# .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
JSON
// 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"
  }
}
TSpackages/ui/src/index.ts
TypeScript
// 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
JSON
// 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"
  }
}
JSpackages/config-eslint/index.js
JavaScript
// 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
JSON
// 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:*"
  }
}
JSapps/web/.eslintrc.js
JavaScript
// apps/web/.eslintrc.js
module.exports = {
  extends: ['@repo/config-eslint'],
  parserOptions: {
    project: './tsconfig.json'
  }
}

TypeScript Config Sharing

packages/config-typescript/base.json
JSON
// 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
JSON
// 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
JSON
// 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
JSON
// 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
JSON
// apps/web/turbo.json
{
  "$schema": "https://turborepo.dev/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

Code
Bash
# Stwórz folder generators
mkdir -p turbo/generators
TSturbo/generators/config.ts
TypeScript
// 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

Code
Bash
# Lista dostępnych generatorów
turbo gen

# Uruchom konkretny generator
turbo gen package
turbo gen app

# Z odpowiedziami na pytania generatora
turbo gen package --args my-lib lib

Templates

Code
HANDLEBARS
{{!-- turbo/generators/templates/package.json.hbs --}}
{
  "name": "@repo/{{name}}",
  "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

Code
DOCKERFILE
# 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 --from=pruner /app/out/json/ .
COPY --from=pruner /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 --from=installer /app/ .
COPY --from=pruner /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 --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public

CMD ["node", "apps/web/server.js"]

Pruning dla CI

Code
Bash
# Stwórz pruned subset
turbo prune web --docker

# Wynik w out/:
# out/
# ├── json/           # package.json files
# ├── full/           # Full source code
# └── pnpm-lock.yaml  # Pruned lockfile

Diagnozowanie problemów

Cache Debugging

Code
Bash
# Pokaż dlaczego task został wykonany
turbo build --summarize

# Pokaż hash computation
turbo build --dry-run=json | jq

# Force cache miss
turbo build --force

# Wyczyść lokalną pamięć podręczną (domyślnie .turbo/cache)
rm -rf .turbo/cache

Task Graph

Code
Bash
# Generuj graf zależności
turbo build --graph=graph.html
turbo build --graph=graph.svg
turbo build --graph=graph.mermaid

# Pokaż w konsoli
turbo build --graph

Szczegółowe dzienniki

Code
Bash
# 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-only

Common Issues

Code
Bash
# Problem: Cache nie działa
# Rozwiązanie: Sprawdź inputs i outputs

# Problem: Task wykonuje się mimo braku zmian
# Sprawdź env variables w podsumowaniu zapisanym do .turbo/runs
turbo build --summarize
jq '.tasks[].environmentVariables' .turbo/runs/*.json

# Problem: Circular dependency
turbo build --graph  # Wizualizuj zależności

# Problem: Slow builds
turbo build --profile=profile.json
# Analizuj w Chrome DevTools

Dobre praktyki

Struktura pakietów

Code
TEXT
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 client

Naming Conventions

Code
JSON
// Używaj scope dla pakietów
{
  "name": "@repo/ui",        // ok
  "name": "@mycompany/ui",   // ok
  "name": "ui",              // zle, konflikt z pakietami npm
}

Dependency Management

Code
JSON
// 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

Code
JSON
{
  "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

Wdrożenie na Vercelu

vercel.json
JSON
// vercel.json
{
  "buildCommand": "pnpm turbo build --filter=web",
  "installCommand": "pnpm install",
  "framework": "nextjs",
  "outputDirectory": "apps/web/.next"
}

GitHub Actions

.github/workflows/ci.yml
YAML
# .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 lint

Changesets (versioning)

Code
Bash
npm install @changesets/cli -D
npx changeset init
.changeset/config.json
JSON
// .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?

  1. Włącz remote caching
  2. Użyj --filter dla changed packages
  3. Parallel jobs w CI
  4. Optymalizuj inputs/outputs

Czy trzeba korzystać z pamięci zdalnej u Vercela?

Nie. Mechanizm jest otwarty i istnieją niezależne implementacje serwera pamięci podręcznej, które można postawić u siebie albo na własnym magazynie obiektów. Wygoda gotowej usługi jest realna, ale nie jest to warunek działania narzędzia.

Pamięć zdalna i dlaczego zmienia najwięcej

Pamięć lokalna przyspiesza pracę jednej osoby. Pamięć współdzielona przyspiesza pracę całego zespołu i serwera budującego, a to jest różnica jakościowa, nie ilościowa.

Mechanizm jest prosty: wynik zadania trafia do wspólnego magazynu, opisany tym samym kluczem co lokalnie. Kiedy ktoś inny, albo serwer budujący, wykona to samo zadanie na tym samym wejściu, pobierze gotowy wynik zamiast liczyć go ponownie.

Największy zysk widać w potoku ciągłej integracji, na przykład przy wdrożeniach przez Vercel, i to z powodu, o którym łatwo zapomnieć. Serwer budujący startuje zwykle z czystego stanu, więc bez pamięci współdzielonej przebudowuje wszystko przy każdym zgłoszeniu zmian, nawet jeśli zmieniono jeden plik w jednym pakiecie. Z pamięcią współdzieloną pobiera wyniki wyprodukowane wcześniej, także na czyimś laptopie.

Drugi zysk dotyczy nowej osoby w zespole. Pierwsze uruchomienie po sklonowaniu repozytorium potrafi trwać kilkanaście minut, a z pamięcią współdzieloną kończy się na pobraniu gotowych artefaktów.

Warto natomiast pomyśleć o dwóch rzeczach, zanim się to włączy. Pierwsza to zaufanie: pamięć zawiera zbudowane artefakty, więc każdy, kto może do niej zapisywać, może wstrzyknąć do procesu budowania cudzy wynik. W repozytorium publicznym albo przy szerokim dostępie warto ograniczyć zapis do serwera budującego, a resztę zespołu zostawić z prawem odczytu.

Druga to koszt transferu. Przy dużych artefaktach pobranie wyniku bywa wolniejsze niż jego zbudowanie, zwłaszcza przy słabym łączu. Jeśli po włączeniu pamięci zdalnej czasy się nie poprawiły, sprawdź rozmiar tego, co ląduje w magazynie, zanim zaczniesz szukać przyczyny gdzie indziej.

Typowe błędy

Pierwszy to niezadeklarowane zmienne środowiskowe, opisane wyżej. Jest to najgroźniejszy z tych błędów, bo nie objawia się awarią, tylko cichym podaniem wyniku zbudowanego dla innego środowiska.

Drugi to traktowanie tego narzędzia jako zamiennika menedżera pakietów. Przestrzenie robocze definiuje menedżer pakietów, a narzędzie jedynie czyta z nich graf zależności. Brak poprawnie skonfigurowanych przestrzeni oznacza, że graf jest pusty, a wtedy nie ma czego zrównoleglać, i to jest pierwsza rzecz do sprawdzenia, gdy zrównoleglanie nie działa.

Trzeci to zadania, które nie są deterministyczne. Jeśli budowanie wstawia do wyniku znacznik czasu albo losowy identyfikator, dwa uruchomienia z tym samym wejściem dadzą różne wyjścia. Pamięć podręczna nadal działa, ale przestaje mieć sens, bo porównania artefaktów przestają cokolwiek znaczyć.

Czwarty to zbyt drobny podział na pakiety. Monorepozytorium z pięćdziesięcioma pakietami, z których każdy zawiera trzy pliki, spędza więcej czasu na obsłudze grafu niż na pracy. Podział warto prowadzić wzdłuż granic, które faktycznie mają znaczenie przy wdrażaniu i przy własności kodu.

Piąty to zapominanie o tym, że pamięć podręczna nie zastępuje testów, także tych pisanych w Vitest. Pominięte zadanie to zadanie, którego nikt nie wykonał, więc jeśli test wykrywa problem zależny od czegoś spoza zadeklarowanych wejść, po prostu się nie uruchomi. Świadomość, co dokładnie wchodzi w skład klucza, jest tu warunkiem zaufania do wyniku.

Dokumentację znajdziesz na stronie Turborepo, a kod źródłowy w repozytorium projektu.