По мере масштабирования инженерной экосистемы компании разработка редко ограничивается одним веб-приложением. Возникает семейство взаимосвязанных продуктов: публичный портал (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/.