У великомасштабних проєктах розробка 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.