Монорепозиторий с JavaScript: организация кода на практике
В современной веб-разработке на JavaScript (Node.js) всё чаще встаёт вопрос: как эффективно организовать код, когда у вас несколько пакетов, библиотек или микросервисов, которые зависят друг от друга? Традиционный подход с отдельными репозиториями (multirepo) может привести к дублированию кода, сложностям с синхронизацией версий и долгому CI/CD. Решение — монорепозиторий (monorepo). В этой статье мы разберём, что такое монорепозиторий, зачем он нужен и как организовать его на практике с использованием JavaScript.
Что такое монорепозиторий и зачем он нужен?
Монорепозиторий — это подход, при котором код нескольких проектов (пакетов, библиотек, сервисов) хранится в одном репозитории. Это не просто «сложить всё в одну папку», а продуманная архитектура, которая решает множество проблем:
- Единая версия зависимостей: вы избегаете ситуации, когда в разных пакетах используются разные версии одной и той же библиотеки.
- Лёгкий рефакторинг: изменения в общей библиотеке сразу видны во всех проектах, которые её используют.
- Упрощённый CI/CD: один репозиторий — одна конфигурация сборки и тестирования.
- Совместное использование кода: утилиты, типы и конфигурации доступны всем пакетам без публикации в npm.
Однако монорепозиторий подходит не всем. Он требует дисциплины в команде и хороших инструментов. Для JavaScript (Node.js) основными инструментами являются npm workspaces (встроенный в npm 7+) и Lerna (более продвинутый инструмент).
Настройка базового монорепозитория с npm workspaces
Начнём с простого примера. Допустим, у нас есть проект, который состоит из двух пакетов: @myapp/core (ядро с бизнес-логикой) и @myapp/server (сервер на Express).
Создадим структуру папок:
my-monorepo/
├── packages/
│ ├── core/
│ │ ├── src/
│ │ │ └── index.js
│ │ ├── package.json
│ │ └── ...
│ └── server/
│ ├── src/
│ │ └── index.js
│ ├── package.json
│ └── ...
├── package.json
└── ...
Корневой package.json должен содержать поле workspaces:
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"start": "node packages/server/src/index.js",
"test": "npm run test --workspaces"
}
}
Теперь в packages/core/package.json:
{
"name": "@myapp/core",
"version": "1.0.0",
"main": "src/index.js"
}
А в packages/server/package.json добавляем зависимость от core:
{
"name": "@myapp/server",
"version": "1.0.0",
"dependencies": {
"@myapp/core": "*",
"express": "^4.18.0"
}
}
После выполнения npm install в корне, npm создаст симлинки между пакетами. Теперь в коде сервера мы можем импортировать core напрямую:
// packages/server/src/index.js
const express = require('express');
const { calculate } = require('@myapp/core');
const app = express();
app.get('/', (req, res) => {
res.send(calculate(2, 3));
});
app.listen(3000);
Это базовый пример, но он уже показывает главное преимущество: изменения в @myapp/core сразу видны в @myapp/server без публикации в реестр.
Продвинутая организация с Lerna
Когда пакетов становится больше 5-10, npm workspaces может не хватать. На помощь приходит Lerna — инструмент, который добавляет мощные возможности:
- Запуск команд во всех или выбранных пакетах.
- Автоматическое управление версиями (fixed или independent mode).
- Публикация только изменённых пакетов.
Установим Lerna глобально или как dev-зависимость:
npm install --save-dev lerna
Создадим lerna.json:
{
"version": "independent",
"npmClient": "npm",
"packages": ["packages/*"]
}
Теперь мы можем использовать команды Lerna:
npx lerna bootstrap # Установка зависимостей и связывание пакетов
npx lerna run test # Запуск тестов во всех пакетах
npx lerna changed # Показать, какие пакеты изменились
Пример скрипта для запуска всех тестов с Lerna:
// package.json
{
"scripts": {
"test": "lerna run test --since main --stream"
}
}
Флаг --since main запускает тесты только для пакетов, изменённых относительно ветки main — это экономит время в CI/CD.
Для более сложных сценариев Lerna поддерживает фиксированную версию (все пакеты имеют одну версию) и независимую (каждый пакет версионируется отдельно). Второй вариант удобен, когда пакеты развиваются с разной скоростью.
Практические советы по организации кода
Чтобы монорепозиторий приносил пользу, а не хаос, следуйте нескольким правилам:
- Разделяйте ответственность. Каждый пакет должен решать одну задачу. Например:
@myapp/ui— компоненты,@myapp/utils— утилиты,@myapp/api— клиент API. - Используйте единые конфигурации. Вынесите общие настройки ESLint, Prettier, TypeScript в корневой пакет.
- Автоматизируйте всё. Настройте pre-commit хуки (husky), которые проверяют линтер и тесты только для изменённых пакетов