У процесі масштабування інженерної екосистеми компанії розробка рідко обмежується одним вебдодатком. Виникає сімейство взаємопов'язаних продуктів: публічний портал (B2C), закрита панель керування (B2B Admin Dashboard), документація, лендинги та маркетингові мікросайти. Зберігання цих проєктів в ізольованих polyrepo-репозиторіях породжує масове дублювання коду: дизайн-система (UI-Kit), типи TypeScript, конфігурації лінтерів та бізнес-утиліти копіюються між репозиторіями, а зміна однієї кнопки вимагає десятка синхронних пулл-реквестів.
Монорепозиторій (Monorepo) вирішує цю проблему, консолідуючи кодову базу в єдиному версіонованому просторі. Внутрішні бібліотеки зв'язуються напряму без необхідності проміжної публікації у публічний або приватний npm registry.
Проте монорепозиторії без спеціалізованих інструментів збірки катастрофічно деградують за швидкістю: зміна одного файлу в документації не повинна викликати перезбірку великого порталу та панелі адміністратора. Для оркестрації завдань, керування графом залежностей та обчислення кешу застосовується Turborepo. Він виключає повторні обчислення завдяки інтелектуальному хешуванню та механізму Remote Caching.
В архітектурі монорепозиторію на базі pnpm Workspaces діє чіткий розподіл відповідальності:
apps/* — термінальні споживачі (Next.js-додатки). Вони імпортують спільні бібліотеки, але жодні інші проєкти ніколи не імпортують код з apps/.packages/* — спільні бібліотеки (UI-компоненти, схеми даних, конфігурації, клієнти БД).КОРІНЬ МОНОРЕПОЗИТОРІЮ
┌─────────────────────────────────────────────────┐
│ pnpm-workspace.yaml | turbo.json | package.json │
└────────────────────────┬────────────────────────┘
│
┌───────────────────────┴───────────────────────┐
▼ ▼
[apps/ (Додатки)] [packages/ (Бібліотеки)]
┌───────────────────────────────┐ ┌─────────────────────────────────┐
│ apps/web (Next.js 15 App) │ │ packages/ui (Design System) │
│ apps/admin (Next.js Admin) │ ────────────► │ packages/types (Інтерфейси) │
│ apps/docs (Документація) │ workspace:* │ packages/tsconfig (TS Конфіги) │
└───────────────────────────────┘ │ packages/database (Prisma/ORM) │
│ └─────────────────────────────────┘
│ turbo prune ▲
▼ │
┌───────────────────────────────┐ │
│ Docker OCI Image (@acme/web) │ ──────────────────────────────┘
│ (Містить лише web + ui) │
└───────────────────────────────┘Створення компонента Modal у packages/ui з експортом через package.json.
Додавання '@acme/ui': 'workspace:*' у маніфести залежних apps.
Додавання пакета до transpilePackages у next.config.ts та @source у globals.css.
Перевірка топології завдань у turbo.json та запуск паралельної валідації pnpm turbo check-types.
Виконання pnpm turbo build зі збереженням артефактів у Remote Cache.
Команда розробила UI-Kit у packages/ui монорепозиторію pnpm. Розробник додав react у секцію dependencies маніфесту packages/ui/package.json. Під час імпорту компонентів у Next.js 15 сторінка падає з помилкою 'Invalid Hook Call: Hooks can only be called inside the body of a function component'. Чому стався збій та як його виправити?
pnpm-workspace.yaml#У корені монорепозиторію файл pnpm-workspace.yaml визначає межі сканування пакетів:
packages:
- "apps/*"
- "packages/*"turbo.json#У стандарті Turborepo 2.x коренева секція називається tasks (замість застарілого pipeline), а для захисту від Cache Poisoning явно декларуються змінні оточення:
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"globalEnv": ["NODE_ENV", "VERCEL_ENV"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"],
"env": ["NEXT_PUBLIC_API_URL", "DATABASE_URL"]
},
"lint": {
"dependsOn": ["^lint"]
},
"check-types": {
"dependsOn": ["^check-types"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}У пакеті дизайн-системи packages/ui React оголошується суворо в peerDependencies, виключаючи дублювання інстансів:
{
"name": "@acme/ui",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.ts"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"react": "^19.0.0",
"typescript": "^5.7.0"
}
}Підключення у внутрішньому додатку apps/web/package.json оформлюється через протокол workspace:*:
{
"name": "@acme/web",
"version": "0.1.0",
"private": true,
"dependencies": {
"@acme/ui": "workspace:*",
"next": "^15.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}transpilePackages#Оскільки пакет @acme/ui постачає чисті нескомпільовані TypeScript-файли, компілятор Next.js (Turbopack) компілює їх на льоту:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
transpilePackages: ['@acme/ui'],
};
export default nextConfig;Замість дублювання налаштувань компілятора в кожному проєкті створюється базовий пакет packages/typescript-config/nextjs.json:
{
"extends": "@acme/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"]
}У Tailwind CSS v4 для сканування класів усередині спільного пакета дизайн-системи використовується директива @source в кореневому CSS додатка:
@import "tailwindcss";
/* Вказуємо компілятору Tailwind v4 сканувати вихідники спільного UI-пакета */
@source "../../../../packages/ui/src/**/*.tsx";У CI/CD пайплайні монорепозиторію збірка Docker-образу apps/web триває 12 хвилин, а після оновлення значення NEXT_PUBLIC_API_URL у .env продакшен продовжує звертатися до старого тестового API. Як вирішити обидві проблеми за допомогою Turborepo?
Якщо у внутрішньому пакеті @acme/ui вказати react у секції dependencies, пакетний менеджер встановить ізольовану копію React у packages/ui/node_modules. При рендерингу в Next.js виникне конфлікт двох різних інстансів React (один у додатку, другий у пакеті), що призведе до падіння хуків useState/useEffect.
Рішення: Оголошуйте react та react-dom у внутрішніх бібліотеках суворо в peerDependencies, а в кореневому package.json фіксуйте єдину версію через pnpm overrides:
{
"pnpm": {
"overrides": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
}Якщо додаток використовує змінну NEXT_PUBLIC_API_URL, але вона не задекларована в масиві env завдання build у turbo.json, Turborepo не включить її значення в обчислення хешу завдання. При зміні URL у файлі .env збирач вважатиме, що вихідний код не змінився, і віддасть із кешу білд зі старою адресою API.
Рішення: Завжди явно перераховуйте змінні оточення в tasks.build.env файлу turbo.json.
Спроба зібрати Docker-образ для apps/web прямим копіюванням усього кореня монорепозиторію (COPY . .). Контейнер затягне непотрібні додатки (apps/admin, apps/docs), збільшивши розмір образу на гігабайти та зламавши кешування шарів при будь-яких змінах у сусідніх проєктах.
Рішення: Використовуйте команду turbo prune @acme/web --docker на етапі збірки: вона підготує ізольований зріз монорепозиторію, що містить лише маніфести залежностей та код потрібного додатка.
Додатки (apps/) є термінальними споживачами архітектури. Прямий імпорт утиліти або компонента з apps/admin в apps/web руйнує автономність деплою та призводить до циклічних залежностей. Будь-який спільний код зобов'язаний виноситися в директорію packages/.