Налаштування Monorepo (Turborepo) для веб-проекту
Turborepo — build system для monorepo від Vercel. Не менеджер пакетів та не лінтер — саме інструмент для розумного запуску завдань з кешуванням результатів. Головна ідея: якщо вхідні файли завдання не змінилися, вихід береться з кешу. На практиці це означає turbo build за 3 секунди замість 4 хвилин при повторному запуску.
Коли Turborepo, а не просто npm workspaces
npm/yarn/pnpm workspaces вже дають monorepo-структуру з загальними залежностями. Turborepo додає поверх них:
- Паралельний запуск з урахуванням залежностей між пакетами
- Інкрементальний кеш (локальний + віддалений)
- Граф завдань з явними залежностями
- Фільтрацію: запустити лише змінені пакети
Якщо проект невеликий (2–3 пакети) та команда маленька — workspaces без Turbo достатньо. При 5+ пакетах та CI/CD — Turbo починає економити реальний час.
Структура проекту
my-project/
├── apps/
│ ├── web/ # Next.js фронтенд
│ ├── admin/ # Vite + React admin panel
│ └── api/ # Node.js/Express backend
├── packages/
│ ├── ui/ # загальні React-компоненти
│ ├── config/
│ │ ├── eslint/ # конфіг ESLint
│ │ ├── typescript/ # базові tsconfig
│ │ └── tailwind/ # tailwind preset
│ ├── utils/ # загальні утиліти (formatDate, тощо)
│ └── types/ # загальні TypeScript-типи
├── package.json # оголошення workspaces
├── turbo.json # конфігурація Turborepo
└── pnpm-workspace.yaml # якщо використовуємо pnpm
Інініціалізація
# Створюємо новий monorepo
npx create-turbo@latest my-project
cd my-project
# Або додаємо Turbo у існуючий проект
pnpm add turbo --save-dev --workspace-root
Конфігурація turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [".env"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "package.json", "tsconfig.json"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["NODE_ENV", "API_URL"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"inputs": ["src/**", "*.ts", "*.tsx", ".eslintrc*"],
"outputs": []
},
"typecheck": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"],
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**", "test/**", "vitest.config.*"],
"outputs": ["coverage/**"],
"env": ["TEST_DATABASE_URL"]
},
"test:e2e": {
"dependsOn": ["build"],
"inputs": ["e2e/**", "playwright.config.*"],
"outputs": ["test-results/**"],
"cache": false
},
"db:generate": {
"cache": false,
"inputs": ["prisma/schema.prisma"]
}
}
}
^build означає «спочатку зібрати всі залежності цього пакета». Turbo автоматично визначає порядок: packages/ui зберуться до apps/web, тому що web залежить від ui.
Налаштування пакетів
// packages/ui/package.json
{
"name": "@acme/ui",
"version": "0.0.0",
"private": true,
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./styles": "./dist/styles.css"
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts",
"dev": "tsup src/index.ts --format esm --dts --watch",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@acme/eslint-config": "*",
"@acme/typescript-config": "*",
"tsup": "^8.0.0"
},
"peerDependencies": {
"react": "^18.0.0"
}
}
// apps/web/package.json
{
"name": "@acme/web",
"private": true,
"dependencies": {
"@acme/ui": "*",
"@acme/utils": "*"
}
}
"*" — workspace-протокол: pnpm/yarn розв'яжуть у локальний пакет. У npm потрібно вказувати "workspace:*" явно.
Спільний TypeScript конфіг
// packages/config/typescript/base.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"lib": ["ES2020"],
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
// packages/config/typescript/react.json
{
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2020", "DOM", "DOM.Iterable"]
}
}
// apps/web/tsconfig.json
{
"extends": "@acme/typescript-config/react.json",
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] }
},
"include": ["src", "next-env.d.ts"]
}
Віддалений кеш
Локальний кеш працює лише на одній машині. Для команди та CI потрібен remote cache. Vercel Remote Cache — безплатно для open source, платно для приватних:
# Авторизація (один раз)
npx turbo login
npx turbo link
# Або self-hosted через ducktape/turborepo-remote-cache
Варіант self-hosted через turborepo-remote-cache (відкритий сервер):
# docker-compose.yml для сервера remote cache
services:
turbo-cache:
image: ducktors/turborepo-remote-cache:latest
ports:
- "3000:3000"
environment:
TURBO_TOKEN: "your-secret-token"
STORAGE_PROVIDER: "s3"
S3_BUCKET: "turbo-cache-bucket"
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}"
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}"
// turbo.json — endpoint remote cache
{
"remoteCache": {
"signature": true
}
}
# Запуск з remote cache
TURBO_TOKEN=your-secret-token TURBO_API=https://cache.internal.example.com \
turbo build
CI/CD з Turborepo
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
ci:
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # потрібно для --filter=...[HEAD^1]
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
# Запускаємо лише завдання для змінених пакетів
- run: pnpm turbo lint typecheck test --filter=...[HEAD^1]
# Build завжди — деплой потребує актуального кешу
- run: pnpm turbo build
--filter=...[HEAD^1] — «все, що змінилося з попереднього коміту, плюс всі залежні від цього пакети». Якщо змінили packages/ui — пересобираються і apps/web, і apps/admin.
Типові проблеми
Залежність на runtime конфіг — якщо пакет читає .env при збиранні, Turbo закешує з конкретним значенням змінної. Явно вказуйте env у pipeline:
"build": {
"env": ["NEXT_PUBLIC_API_URL", "DATABASE_URL"]
}
Циклічні залежності — Turbo упадеe з помилкою. Рефакторинг: виділити спільну залежність у третій пакет.
Dev-режим та кеш — dev повинен мати "cache": false та "persistent": true. Іначе Turbo не буде запускати watchers паралельно.
Сроки
Налаштування Turborepo з нуля для проекту з 5–8 пакетами — два-три дні: створення структури, конфігурація pipeline, спільні tsconfig/eslint, налаштування remote cache, адаптація CI/CD. Перенесення існуючого проекту в monorepo додає тиждень на реструктуризацію імпортів та розв'язання залежностей.







