Монорепозиторий с JavaScript: организация кода для масштабируемых проектов
С ростом сложности JavaScript-проектов (особенно на Node.js) перед разработчиками встаёт вопрос: как организовать код, чтобы он оставался поддерживаемым, переиспользуемым и легко масштабируемым? Традиционный подход — мультирепозиторий (polyrepo), когда каждый сервис или библиотека живут в своём Git-репозитории. Однако для многих команд удобнее и эффективнее оказывается монорепозиторий (monorepo) — единый репозиторий, содержащий несколько проектов (пакетов).
В этой статье мы разберём, что такое монорепозиторий, какие инструменты для JavaScript/Node.js существуют, и как правильно организовать структуру кода.
Что такое монорепозиторий и зачем он нужен?
Монорепозиторий — это стратегия управления кодом, при которой все компоненты системы (фронтенд, бэкенд, общие библиотеки, конфигурации) хранятся в одном репозитории. В контексте JavaScript это часто означает набор пакетов (packages), каждый из которых может быть опубликован отдельно или использован как внутренняя зависимость.
Преимущества монорепозитория
- Единая версия зависимостей: Все пакеты используют один
node_modules(благодаря hoisting), что уменьшает дублирование и конфликты версий. - Простой рефакторинг: Изменения в общей библиотеке сразу видны во всех потребителях в рамках одного PR.
- Сквозная атомарность: Один коммит может содержать изменения в нескольких пакетах, что упрощает откат и CI/CD.
- Переиспользование кода: Общие утилиты, типы и конфигурации легко шарить между проектами.
- Единые инструменты: Один линтер, один тест-раннер, один форматтер для всего репозитория.
Недостатки (и как их нивелировать)
- Рост размера репозитория: Git может замедлиться. Решение — использовать
git sparse-checkoutили глубинные клоны. - Сложность настройки CI: Нужно умное кэширование и запуск только изменённых пакетов (помогают инструменты вроде Nx или Turborepo).
- Повышенные требования к дисциплине: Чёткие правила оформления коммитов и версионирования обязательны.
Инструменты для организации монорепозитория в JavaScript
Выбор инструмента зависит от размера команды и сложности проекта. Рассмотрим три самых популярных варианта.
1. Yarn / npm Workspaces (базовый уровень)
Это встроенная возможность менеджеров пакетов. Позволяет управлять несколькими пакетами в одном репозитории, используя единый node_modules.
Пример структуры:
my-monorepo/
├── package.json # корневой package.json
├── packages/
│ ├── utils/ # общая библиотека
│ │ └── package.json
│ ├── server/ # бэкенд приложение
│ │ └── package.json
│ └── client/ # фронтенд (если нужно)
│ └── package.json
└── node_modules/
Корневой package.json:
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*"
]
}
Пакет packages/server/package.json:
{
"name": "@myorg/server",
"version": "1.0.0",
"dependencies": {
"@myorg/utils": "*" // ссылается на локальный пакет
}
}
Плюсы: простота, не требует дополнительных зависимостей. Минусы: нет умного кэширования и параллельного запуска задач.
2. Lerna (классика для монорепозиториев)
Lerna уже много лет является стандартом. Он работает поверх Yarn/npm Workspaces и добавляет возможности для публикации пакетов и выполнения скриптов.
Установка:
npm install --global lerna
Инициализация:
lerna init
Пример команды для запуска тестов во всех пакетах:
lerna run test --since master
Lerna умеет автоматически определять, какие пакеты изменились, и запускать задачи только для них. Однако его развитие замедлилось, и многие переходят на более современные инструменты.
3. Nx и Turborepo (современный стандарт)
Оба инструмента решают главную проблему монорепозиториев — скорость. Они используют граф зависимостей и распределённое кэширование, чтобы не перезапускать то, что уже было сделано.
Пример с Turborepo:
Установка:
npm install turbo --save-dev
Корневой turbo.json:
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
}
}
}
Теперь при запуске turbo run build Turborepo построит граф зависимостей и запустит сборку пакетов параллельно, кэшируя результаты.
Nx предлагает похожий функционал, но имеет более мощную интеграцию с генераторами кода и плагинами для Angular, React, NestJS и т.д.
Практический пример: создание простого монорепозитория с двумя пакетами
Создадим структуру с общей библиотекой @myorg/utils и приложением @myorg/app, которое её использует.
Шаг 1: Инициализация
mkdir my-monorepo && cd my-monorepo
npm init -y
npm install --save-dev typescript @types/node ts-node
Корневой package.json:
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"]
}