Монорепозиторий с JavaScript: организация кода на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Монорепозиторий с 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 поддерживает фиксированную версию (все пакеты имеют одну версию) и независимую (каждый пакет версионируется отдельно). Второй вариант удобен, когда пакеты развиваются с разной скоростью.

Практические советы по организации кода

Чтобы монорепозиторий приносил пользу, а не хаос, следуйте нескольким правилам:

  1. Разделяйте ответственность. Каждый пакет должен решать одну задачу. Например: @myapp/ui — компоненты, @myapp/utils — утилиты, @myapp/api — клиент API.
  2. Используйте единые конфигурации. Вынесите общие настройки ESLint, Prettier, TypeScript в корневой пакет.
  3. Автоматизируйте всё. Настройте pre-commit хуки (husky), которые проверяют линтер и тесты только для изменённых пакетов

Блоги

Рекомендаций по книгам