В крупномасштабных проектах разработка UI-компонентов, утилит и конфигураций в изолированных полирепозиториях (отдельный Git-репозиторий на каждую библиотеку) приводит к организационному параличу:
npm link, чреватого рассинхронизацией зависимостей.Архитектура монорепозитория (Monorepo) на базе пакетного менеджера pnpm workspaces и оркестратора задач Turborepo устраняет эти барьеры.
Сочетание pnpm и Turborepo формирует современный промышленный стандарт:
content-addressable store) в pnpm предотвращает эффект фантомных зависимостей (Phantom Dependencies), присущий npm и yarn.В монорепозитории кодовая база логически разделена на исполняемые приложения (apps/*) и переиспользуемые разделяемые пакеты (packages/*):
┌──────────────────────────────────────────────────────────────────────────┐
│ ROOT MONOREPO │
│ pnpm-workspace.yaml | turbo.json | package.json │
└─────────────────────┬──────────────────────────────┬─────────────────────┘
│ │
┌─────────────▼────────────┐ ┌─────────────▼────────────┐
│ apps/web │ │ apps/docs │
│ (Next.js 15+) │ │ (Storybook) │
└─────────────┬────────────┘ └─────────────┬────────────┘
│ depends on: │ depends on:
│ - @repo/ui │ - @repo/ui
│ - @repo/tailwind-config │ - @repo/tailwind-config
│ │
┌─────────────┴──────────────────────────────┴────────────┐
│ SHARED PACKAGES │
│ │
│ ┌───────────────────┐ ┌────────────────────────────┐ │
│ │ @repo/ui │ │ @repo/tailwind-config │ │
│ │ (React 19 Kit) │ │ (Tailwind v4 Presets) │ │
│ └─────────┬─────────┘ └────────────────────────────┘ │
│ │ depends on: │
│ ┌─────────▼─────────┐ ┌────────────────────────────┐ │
│ │ @repo/tsconfig │ │ @repo/eslint-config │ │
│ │ (Strict TS) │ │ (Shared Linting Rules) │ │
│ └───────────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘Когда разработчик запускает команду pnpm build, Turborepo:
inputs), переменных окружения и зависимостей целевого пакета.dist/ из кэша (>>> FULL TURBO), пропуская компиляцию TypeScript и бандлинг за 50 мс.Создайте директорию packages/analytics и инициализируйте package.json с уникальным именем @repo/analytics.
Подключите @repo/tsconfig и @repo/eslint-config в devDependencies через протокол workspace:*.
Зафиксируйте карту экспортов в package.json и вынесите react и react-dom в peerDependencies.
В apps/web/package.json добавьте зависимость '@repo/analytics': 'workspace:*'.
Запустите pnpm turbo run build --filter=@repo/analytics... для проверки ациклического графа DAG.
Каким образом архитектура pnpm workspaces защищает проект от появления фантомных зависимостей (Phantom Dependencies), свойственных классическому npm?
@company/ui потребляется одновременно веб-порталом на Next.js, мобильным клиентом на React Native и панелью администратора на Vite.source в package.json, исключая необходимость запускать watch-билд для каждого пакета.--filter=...[HEAD^1] запускает сборку и тесты строго для затронутого пакета и зависящих от него приложений.pnpm update -r react@latest, исключая версионный зоопарк.packages:
- 'apps/*'
- 'packages/*'Конфигурационный файл turbo.json определяет топологию конвейера сборки:
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}dependsOn: ["^build"]: символ ^ указывает топологическую зависимость: перед сборкой приложения apps/web Turborepo обязан сначала собрать все пакеты из packages/*, от которых оно зависит.cache: false, persistent: true: режим для долгоживущих dev-серверов.Пакет компонентов @repo/ui не должен бандлить собственный экземпляр React. Вместо этого он объявляет react и react-dom в блоке peerDependencies:
{
"name": "@repo/ui",
"version": "1.4.0",
"private": false,
"type": "module",
"exports": {
"./button": {
"types": "./dist/button.d.ts",
"import": "./dist/button.js"
},
"./card": {
"types": "./dist/card.d.ts",
"import": "./dist/card.js"
},
"./styles.css": "./dist/index.css"
},
"scripts": {
"build": "tsup src/index.ts src/button.tsx src/card.tsx --dts --format esm",
"dev": "tsup src/index.ts --watch --dts --format esm",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.2"
}
}Разработчик вносит изменения в @repo/ui и запускает pnpm changeset, выбирая тип версии (patch, minor, major).
GitHub Actions выполняет turbo run test build строго для затронутых пакетов.
Бот создает автоматический Version Packages PR, бампая версии в package.json и собирая CHANGELOG.md.
После аппрува релизного PR пакеты публикуются в npm/GitHub Packages, создаются Git-теги.
Что означает директива dependsOn: ['^build'] в задаче 'build' файла turbo.json для приложения apps/web?
Ниже представлен production-конфиг бандлера tsup для пакета дизайн-системы, формирующий изолированные tree-shakeable ESM-модули с типами и директивами React 19:
import { defineConfig, type Options } from 'tsup';
export default defineConfig((options: Options) => ({
// Множественные точки входа для независимого tree-shaking
entry: [
'src/index.ts',
'src/components/Button/index.ts',
'src/components/Card/index.ts',
'src/components/Modal/index.ts',
],
format: ['esm'],
dts: true,
clean: !options.watch,
sourcemap: true,
minify: !options.watch,
target: 'es2022',
external: ['react', 'react-dom'],
// Сохранение баннеров и директивы 'use client' для поддержки Next.js App Router
banner: {
js: "'use client';",
},
esbuildOptions(esbuildOptions) {
esbuildOptions.jsx = 'automatic';
},
}));При плоской установке зависимостей приложение apps/web может импортировать библиотеку (например, lodash), которая не объявлена в его собственном package.json, но установлена в packages/ui. При деплое в изолированном Docker-контейнере сборка упадет с ошибкой Module not found.
// ❌ Антипаттерн: отсутствие явной зависимости в apps/web/package.json
{
"name": "web",
"dependencies": {
"@repo/ui": "workspace:*"
// lodash забыт, но локально работает из-за плоского hoisting!
}
}
// ✅ Правильное решение: явное объявление всех прямых зависимостей
{
"name": "web",
"dependencies": {
"@repo/ui": "workspace:*",
"lodash": "^4.17.21"
}
}Если в turbo.json не ограничить массив inputs, Turborepo будет инвалидировать кэш сборки при изменении любого файла репозитория (например, README.md или скриншота в документации).
// ❌ Антипаттерн: любой коммит в markdown сбрасывает весь кэш сборки
"build": {
"outputs": ["dist/**"]
}
// ✅ Правильное решение: точный перечень исходных файлов
"build": {
"inputs": ["src/**", "package.json", "tsconfig.json"],
"outputs": ["dist/**"]
}Если пакет @repo/auth зависит от @repo/ui, а @repo/ui импортирует типы или хуки из @repo/auth, Turborepo не сможет построить граф выполнения (DAG) и выдаст ошибку Circular dependency detected.
Решение: выносить общие интерфейсы и примитивы в независимый низкоуровневый пакет @repo/types или @repo/contracts.