Monorepo давно перестал быть экзотикой: многие зрелые компании переносят код десятков сервисов, библиотек и утилит под одну крышу. Это упрощает переиспользование компонентов, синхронизацию изменений, тестирование и CI. Однако масштабирование Monorepo — особенно когда число пакетов переваливает за несколько сотен — требует строгой стратегии управления зависимостями.
В 2025 году большинство современных frontend-инфраструктур переходят на связку pnpm
и turborepo
(сокр. Turbo). pnpm обеспечивает детерминированное, изолированное и максимально производительное управление пакетами, в то время как Turbo даёт кэшированное, параллельное выполнение задач и оптимальную инвалидацию на основе зависимости между пакетами.
Однако даже при использовании этих мощных инструментов можно легко наступить на грабли: случайное повышение зависимости в неподходящем месте, неправильная настройка workspace protocol, alias-конфликты, устаревшие линки, конфликт версий при сборке. Поговорим, как на практике выстроить монорепозиторий с разумной и предсказуемой моделью зависимостей, используя фичи pnpm
≥8.10 и Turbo ≥2.
Архитектура: Scalable Monorepo с слоями и правилами импорта
Подход с flat-партицией становится крайне проблематичным при большом количестве пакетов. Вместо этого имеет смысл стратифицировать Monorepo на понятные уровни. Минимальная базовая стратификация:
packages/core/**
— фундаментальные утилиты, независимые от бизнес-логикиpackages/ui/**
— дизайн-система, переиспользуемые компонентыpackages/feature/**
— независимые функции/модули (например,auth
,checkout
)apps/**
— конечные приложения (SPA, BFF, SSR, mobile bridge и т.п.)
Основное правило: зависимость разрешается только вниз по иерархии (core ← ui ← feature ← apps). Обратные ссылки запрещаются линтером на этапе precommit/husky/hint.
Это даёт стабильность: core не знает ничего об apps, ui не потянет случайно фичу, а фича не сломает общую утилиту.
pnpm workspaces: workspace protocol и точечные зависимости
pnpm использует workspace:
префикс для обозначения внутренних зависимостей пакетов внутри монорепозитория. Например:
{
"name": "checkout",
"version": "1.0.0",
"dependencies": {
"@acme/ui": "workspace:^",
"@acme/validators": "workspace:~"
}
}
Важно, что workspace:*
— это не просто alias. По умолчанию pnpm будет резолвить зависимости строго по версионному семантическому диапазону. Использование workspace:^ оптимально: оно указывает, что несовместимые мажорные версии недопустимы.
В 2025 году многие команды полностью отказываются от *
и latest
, переходя к строгому контролю:
- Обновление общей зависимости делается вручную через
pnpm -r update @acme/ui
с указанием версии - Любое повышение зависимости фиксируется в PR с описанием мотивации
Это минимизирует ситуации снежного кома, когда одна зависимость обновляется, и три десятка пакетов собираются без тестов, но уже со сломанным API.
Автоматическая проверка несовместимых зависимостей
Добавим в pnpmfile.js
хук фильтрации нежелательных зависимостей:
module.exports = {
hooks: {
readPackage(package) {
const invalid = Object.entries(package.dependencies ?? {})
.filter(([name, version]) => version === '*' || version.startsWith('workspace:*'));
if (invalid.length > 0) {
throw new Error(
`Package ${package.name} has invalid dependency versions:\n` +
invalid.map(([name, ver]) => ` - ${name}: ${ver}`).join('\n')
);
}
return package;
}
}
};
Теперь попытка добавить workspace:*
или *
приведёт к ошибке при установке зависимостей.
Turbo: декларативное кеширование и валидация задач
Turbo использует граф зависимостей между пакетами и задачами (pipeline
) и позволяет точно понять, какие пакеты надо пересобрать или протестировать при изменении любого файла.
Пример turbo.json
:
{
"pipeline": {
"build": {
"outputs": ["dist/**", "build/**"],
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {}
}
}
Когда изменяется файл в packages/validators/parse.ts
, Turbo пересобирает:
- сам
validators
(build) - все зависимости, которые зависят от него (например,
checkout
) - запускает только актуальные
test
,lint
Turbo работает эффективно только при корректной указанной зависимости между задачами и строгом output tracking. Лишнее wildcard-описание выходов (outputs
: ["*"]
) ведёт к разрушению кеша. Всегда указывайте явно артефакты задачи (например, Dist + Types или coverage).
Почему не Yarn Berry или npm Workspaces?
Yarn 4 остаётся конкурентом для pnpm, особенно с его constraints.pro
и Zero-Installs. Но к 2025 году pnpm выигрывает по следующим причинам:
- Изоляция node_modules через symlink-стратегию без
.zip
- Более быстрое выполнение
install
даже в проектах с 1000+ пакетами - Простота настройки и интеграции с Turbo (Yarn требует custom executor)
- Прямой
node_modules
-layout без необходимости.yarnrc.yml
и Plug'n'Play
npm workspaces в свою очередь остаются слишком примитивными, не поддерживают правила workspace semantic ranges, а глобальное управление devDependencies
у них грубо говоря отсутствует.
CI: Локальный кеш + удалённый stateful execution
Turbo поддерживает удалённый кеш артефактов через Vercel Remote Caching API. К середине 2025 его начали внедрять в enterprise-командах даже без использования Vercel CI. Это позволяет кешировать билдов, тестов и линтов даже между CI-runner'ами.
Пример настройки .env
:
TURBO_TOKEN=your-secret
TURBO_TEAM=my-team-id
Команды в CI остаются прежними: turbo run build --filter=...
, но теперь сборки кэшируются на уровне артефактов, а не шагов Workflows.
Важно: используйте --force
для invalidation при подозрении на расхождение кеш-состояния.
Советы и выводы
- Вносите версионный контроль в каждый
internal
пакет: local version !== "*" - Принудительно отключите
workspace:*
- Внедрите пересекающую проверку и аналитический слой: кто зависит от чего? Для этого можно использовать инструменты
depcruise
,madge
,nx graph
- Настройте репо как "readable-first": шапки
README.md
внутри каждого пакета, чёткое описание входов-выходов и связей - Никогда не делайте devDependencies глобальными —
devDependencies
пакета должны быть только для его задач, иначе Turbo будет неправильно инвалидацировать кэш - Версионируйте общие типы (
@acme/types
) по SemVer и не используйтеpatch
-version dependency если у TypeScript есть breaking-changes
Pnpm + Turbo — это не просто пакетный менеджер и таск-раннер. Это слой предсказуемости, который должен быть детерминированным от команды к команде. Чем раньше вы выстроите договорённости и правила в Monorepo, тем проще будет масштабировать архитектуру к десяткам команд и сотням пакетов без потери контроля.