Монорепозиторий с JavaScript: организация кода и лучшие практики
Современная разработка на JavaScript (Node.js) часто сталкивается с проблемой управления кодом в больших проектах. Когда количество микросервисов, библиотек и приложений растет, традиционные многомодульные репозитории (Polyrepo) начинают создавать хаос: дублирование конфигураций, сложности с синхронизацией версий и долгий CI/CD. Решение — монорепозиторий (monorepo). В этой статье мы разберем, как организовать код в монорепозитории на JavaScript, какие инструменты использовать и как избежать типичных ошибок.
Что такое монорепозиторий и зачем он нужен?
Монорепозиторий — это подход, при котором весь код проекта (несколько приложений, библиотек, сервисов) хранится в одном репозитории. В отличие от Polyrepo, где каждый компонент живет в своем Git-репозитории, monorepo объединяет все в единую структуру.
Преимущества монорепозитория для JavaScript:
- Единая версия зависимостей — все пакеты используют одни и те же версии библиотек, что исключает конфликты.
- Переиспользование кода — общие утилиты и компоненты легко импортируются между подпроектами без публикации в npm.
- Атомарные коммиты — изменения в нескольких пакетах можно делать в одном коммите, что упрощает ревью и откат.
- Упрощенный CI/CD — один пайплайн для всего проекта, кэширование сборок и тестов.
Однако у монорепозитория есть и минусы: он требует строгой дисциплины в организации кода и правильного выбора инструментов. Далее мы рассмотрим, как организовать структуру и какие инструменты использовать.
Инструменты для управления монорепозиторием в Node.js
Экосистема JavaScript предлагает несколько зрелых инструментов для работы с monorepo. Рассмотрим три самых популярных:
1. Lerna
Классический инструмент, который долгое время был стандартом. Lerna управляет публикацией пакетов, версионированием и запуском скриптов во всех пакетах. Пример конфигурации lerna.json:
{
"version": "independent",
"packages": ["packages/*"],
"npmClient": "yarn"
}
Команда lerna bootstrap устанавливает зависимости для всех пакетов и связывает их между собой.
2. Nx (Nrwl)
Более современный и мощный инструмент с расширенной системой кэширования, графами зависимостей и генераторами кода. Nx поддерживает не только JavaScript, но и TypeScript, Angular, React, Next.js. Пример команды для создания библиотеки:
npx nx generate @nrwl/node:library my-lib --directory=shared
Nx автоматически анализирует зависимости и пересобирает только измененные пакеты.
3. Turborepo
Созданный Vercel, Turborepo фокусируется на скорости сборки и простоте. Он использует умное кэширование и параллельное выполнение задач. Пример turbo.json:
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
}
}
}
Turborepo идеально подходит для проектов с большим количеством пакетов, где важна скорость CI.
Организация структуры проекта в монорепозитории
Правильная структура папок — основа успешного monorepo. Рассмотрим типовую структуру для Node.js проекта:
my-monorepo/
├── packages/
│ ├── shared-utils/ # Общие утилиты
│ │ ├── src/
│ │ │ └── index.js
│ │ └── package.json
│ ├── api-gateway/ # API-шлюз
│ │ ├── src/
│ │ │ └── server.js
│ │ └── package.json
│ └── web-app/ # Веб-приложение (React/Vue)
│ ├── src/
│ │ └── App.js
│ └── package.json
├── tools/ # Скрипты и конфигурации
│ ├── eslint/
│ └── jest/
├── package.json # Корневой package.json
├── lerna.json # или turbo.json / nx.json
└── README.md
Ключевые принципы организации:
- Изолированные пакеты — каждый пакет имеет свой
package.jsonи собственную точку входа. - Единые тулы — ESLint, Prettier, Jest настраиваются один раз в корне и применяются ко всем пакетам.
- Общие скрипты — в корневом
package.jsonопределяются скрипты для запуска всех пакетов (например,"test": "lerna run test").
Пример: создание простого монорепозитория с Lerna
Давайте создадим минимальный монорепозиторий с двумя пакетами: библиотекой утилит и приложением, которое ее использует.
Шаг 1: Инициализация
mkdir my-monorepo
cd my-monorepo
npm init -y
npm install --save-dev lerna
npx lerna init
Шаг 2: Создание пакетов
Создадим пакет shared-utils:
mkdir -p packages/shared-utils/src
cd packages/shared-utils
npm init -y
Содержимое packages/shared-utils/src/index.js:
function greet(name) {
return `Hello, ${name}!`;
}
module.exports = { greet };
Теперь создадим пакет web-app:
mkdir -p packages/web-app/src
cd packages/web-app
npm init -y
npm install shared-utils@0.0.0
Содержимое packages/web-app/src/index.js:
const { greet } = require('shared-utils');
console.log(greet('World'));
Шаг 3: