Коротко: Разбираем архитектуру монорепозитория на Turborepo: настройка Just-in-Time пакетов, шаринг UI-компонентов, TypeScript-типов и конфигураций без лишнего оверхеда.
Когда кодовая база разрастается до нескольких взаимосвязанных фронтенд-приложений (например, публичный портал на Next.js, панель администратора на Vite и мобильное веб-приложение), разработчики неизбежно сталкиваются с дублированием логики. Начинается ручное копирование интерфейсов DTO, контрактов API, валидаторов и типовых UI-компонентов.
Попытка вынести общий код в отдельные внешние npm-пакеты часто оборачивается инфраструктурным оверхедом: любое мелкое изменение в кнопке или схеме данных требует сборки пакета, публикации новой версии в реестр, обновления зависимостей в целевых репозиториях и разбора конфликтующих версий.
Решением этой проблемы становится монорепозиторий. Инструмент Turborepo в связке с механизмом рабочих пространств (workspaces) пакетных менеджеров объединяет приложения и общие модули в одном репозитории, обеспечивая прозрачный шаринг кода и высокую скорость работы за счет интеллектуального кэширования задач.
Базовая анатомия Turborepo-монорепозитория
Turborepo не заменяет пакетный менеджер, а выполняет роль легковесного оркестратора задач: строит граф зависимостей между пакетами, распараллеливает выполнение скриптов и кэширует артефакты сборки.
Роль пакетного менеджера и Workspaces
Управление зависимостями и создание символических ссылок (symlinks) между локальными пакетами делегируется пакетному менеджеру (pnpm, Yarn, npm или Bun).
Обязательный фундамент архитектуры:
- Единый lockfile (
pnpm-lock.yaml,yarn.lockилиpackage-lock.json) — фиксирует дерево внешних зависимостей и обеспечивает детерминированность сборок. - Корневой
package.json— определяет глобальные dev-зависимости (включая CLIturbo) и общие скрипты запуска. - Конфигурационный файл
turbo.json— задает пайплайны выполнения задач и условия кэширования. - Манифест рабочих пространств (например,
pnpm-workspace.yaml).
Структура директорий
Классический подход к организации директорий разделяет конечные сервисы и переиспользуемые модули:
├── apps/
│ ├── web/ # Приложение Next.js (SSR / App Router)
│ └── admin/ # Панель управления на Vite / React SPA
├── packages/
│ ├── ui/ # Внутренняя UI-библиотека
│ ├── types/ # Общие интерфейсы, DTO и схемы API
│ ├── typescript-config/ # Базовые конфигурации tsconfig
│ └── eslint-config/ # Стандартизированные правила линтинга
├── pnpm-workspace.yaml
├── package.json
└── turbo.json
В файле pnpm-workspace.yaml шаблоны задаются без избыточной вложенности:
packages:
- "apps/*"
- "packages/*"
Turborepo рассчитан на плоскую структуру пакетов. Попытка организовать вложенные структуры вида packages/** (например, размещение отдельного package.json внутри packages/ui/button/) не поддерживается и ломает построение топологического графа.
Нейминг внутренних пакетов
Чтобы избежать коллизий с публичными пакетами из реестра npm, для всех внутренних пакетов монорепозитория обязательно задается единый namespace: префикс @repo/* либо имя компании/продукта (@acme/*).
Примеры корректных названий:
@repo/ui@repo/types@repo/typescript-config
Шаринг TypeScript-типов и контрактов
Выделение общих типов в автономный пакет создает единый источник правды для серверных ответов, моделей данных и клиентских состояний.
Паттерн Just-in-Time (JIT) для пакетов типов
Для модуля с типами не требуется отдельная стадия компиляции через tsc или бандлеры. Оптимальным решением является паттерн Just-in-Time (JIT) Package, когда пакет напрямую экспортирует исходные .ts-файлы, а компилятор приложения-потребителя разбирает их на лету.
Структура пакета @repo/types:
packages/types/
├── src/
│ ├── api.ts
│ ├── user.ts
│ └── index.ts
├── package.json
└── tsconfig.json
Настройка package.json и subpath exports
Поле exports в манифесте packages/types/package.json задает явные точки входа:
{
"name": "@repo/types",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.ts",
"./api": "./src/api.ts",
"./user": "./src/user.ts"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*"
}
}
Такая структура позволяет приложениям импортировать изолированные контексты без загрузки всей кодовой базы типов:
import type { UserProfile, UserRole } from "@repo/types/user";
import type { ApiResponse } from "@repo/types/api";
Базовые конфигурации TypeScript
Чтобы исключить рассинхронизацию настроек компилятора, параметры выносятся в пакет @repo/typescript-config. В нем создаются базовые пресеты: base.json, nextjs.json, react-library.json.
Пример packages/typescript-config/base.json:
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}
Конфигурация packages/types/tsconfig.json упрощается до наследования:
{
"extends": "@repo/typescript-config/base.json",
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
Создание и переиспользование внутренней UI-библиотеки
Организация UI-кита внутри монорепозитория требует выбора правильного формата поставки: компиляция перед запуском или потребление исходников.
JIT-пакеты против Compiled-пакетов
| Параметр | Just-in-Time (JIT) | Compiled Packages |
|---|---|---|
| Формат экспорта | Исходники .tsx / .ts |
Скомпилированный JS (dist/) и типы .d.ts |
| Сборка пакета | Не требуется | Требуется (tsup, esbuild, tsc) |
| DX / HMR | Мгновенный Hot Reload в приложении | Требуется watch-режим на сборку UI-пакета |
| Применение | Однородный стек (например, Next.js + React) | Разнородный стек, сборка под стороннюю публикацию |
Для Next.js-приложений стандартом является подход JIT. В файле next.config.js целевого приложения достаточно указать:
module.exports = {
transpilePackages: ["@repo/ui"],
};
Настройка пакета @repo/ui через subpath exports
Использование единого index.ts (barrel file), агрегирующего все компоненты дизайн-системы, замедляет холодный старт и ухудшает tree-shaking. Рекомендуется использовать точечные экспорты.
Манифест packages/ui/package.json:
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"exports": {
"./button": "./src/components/button.tsx",
"./card": "./src/components/card.tsx",
"./styles.css": "./src/styles.css"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"react": "^18.3.0",
"typescript": "^5.4.0"
}
}
Изоляция React через peerDependencies
Чтобы избежать рассинхронизации контекста хуков React (Invalid Hook Call Warning), React и React DOM внутри UI-библиотеки должны быть строго определены как peerDependencies. Это гарантирует, что компонент будет использовать тот же экземпляр React runtime, который установлен в приложении-потребителе.
Оркестрация сборки и кэширование в turbo.json
Turborepo строит направленный ациклический граф (DAG) для всех задач монорепозитория. Правильная конфигурация кэша исключает повторное выполнение неизмененных этапов.
Пример корневого turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"type-check": {
"dependsOn": ["^type-check"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Ключевые директивы:
dependsOn: ["^build"]— префикс^задает топологический порядок: задачаbuildв приложении начнется только после успешной сборки всех пакетов, от которых оно зависит.inputs— массив путей к файлам, формирующим хэш задачи. Если исходники пакета и его окружение не менялись, Turborepo мгновенно восстанавливает результат из локального или удаленного кэша (FULL TURBO).outputs— перечень каталогов с артефактами сборки, подлежащими архивации.cache: false— директива для долгоживущих процессов (dev-серверы), исключающая их из механизма кэширования.
Типичные ошибки при проектировании монорепозитория
- Циклические зависимости между внутренними пакетами. Если
@repo/uiобращается к утилитам из@repo/utils, а@repo/utilsимпортирует константы из@repo/ui, топологическая сортировка графа становится невозможной. Вся общая низкоуровневая логика должна спускаться на уровень ниже — например, в@repo/primitivesили@repo/tokens. - Относительные импорты между пакетами. Конструкции вида
import { Button } from '../../packages/ui/src/button'нарушают модульные границы и приводят к падению production-сборщиков. Вся коммуникация внутри монорепозитория должна осуществляться исключительно через имена пакетов:import { Button } from '@repo/ui/button'. - Рассинхронизация версий React. Если одно приложение использует React 18, а другое — React 19, это может приводить к конфликтам типов JSX и хуков. Для фиксации версий зависимостей по всему дереву проекта используйте секцию
pnpm.overrides(для pnpm),resolutions(для Yarn) илиoverrides(для npm).
Часто задаваемые вопросы (FAQ)
Чем подход Just-in-Time (JIT) пакетов отличается от Compiled пакетов?
JIT-пакеты отдают необработанные исходные файлы TypeScript (.ts / .tsx). Компиляция и минификация выполняются непосредственно сборщиком конечного приложения. Compiled-пакеты перед использованием собираются во внутреннюю директорию dist/ с генерацией .js и деклараций .d.ts.
Как избежать конфликта нескольких копий React в рантайме?
Необходимо размещать react и react-dom в секции peerDependencies пакета @repo/ui, а для этапа разработки и автодополнения подключать их в devDependencies. Это предотвращает дублирование runtime-бандла React внутри node_modules UI-пакета.
Зачем использовать subpath exports вместо одного файла index.ts?
Экспорт через явные подпути (@repo/ui/button, @repo/ui/modal) избавляет приложение от необходимости парсить громоздкие barrel-файлы. Это снижает объем используемой памяти компилятором TypeScript, предотвращает попадание неиспользуемого кода в бандл и ускоряет Fast Refresh.
Можно ли запускать Turborepo без pnpm?
Да, Turborepo поддерживает npm, Yarn и Bun. Однако pnpm остается популярным выбором для монорепозиториев благодаря жесткой изоляции зависимостей (symlinked node_modules), исключающей использование необъявленных пакетов (phantom dependencies), и экономии дискового пространства за счет глобального контентно-адресуемого хранилища.
Как настроить переиспользование Tailwind CSS в монорепозитории?
Оптимальный паттерн — создание пакета @repo/tailwind-config, экспортирующего базовый конфигурационный объект. В tailwind.config.js приложений и UI-пакета этот конфиг подключается через поле presets. Для корректного сканирования классов в content приложения добавляется путь к исходникам UI-компонентов: ../../packages/ui/src/**/*.{ts,tsx}.
Заключение
Использование Turborepo обеспечивает структурированный и производительный подход к совместному использованию кода во фронтенд-системах. Разделение архитектуры на изолированные модули с пространством имен @repo/*, отказ от громоздких barrel-файлов в пользу subpath exports и корректная изоляция React в peerDependencies позволяют масштабировать кодовую базу без деградации скорости сборки и качества разработки.




.svg.webp)



