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

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

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

Начать курс

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

Разработка сложных проектов на JavaScript (Node.js) часто сталкивается с проблемой управления кодом. Когда у вас есть несколько пакетов, библиотек или микросервисов, каждый из которых живет в своем репозитории, это приводит к дублированию кода, сложности синхронизации версий и замедлению CI/CD. Решение — монорепозиторий (monorepo). В этой статье мы разберем, как организовать монорепозиторий с JavaScript на практике: от выбора инструментов до настройки сборки и публикации.

Что такое монорепозиторий и зачем он нужен?

Монорепозиторий — это подход к управлению кодом, при котором все пакеты, библиотеки и приложения проекта хранятся в одном репозитории. В отличие от мультирепозиториев (каждый пакет в своем репо), монорепозиторий предлагает:

  • Единую историю изменений: легко отслеживать, как изменения в одной части влияют на другую.
  • Переиспользование кода: общие утилиты и компоненты живут в одном месте без публикации в npm.
  • Упрощенный рефакторинг: изменение API одного пакета сразу видно во всех зависимых.
  • Единые настройки CI/CD: один конфигурационный файл для всех пакетов.

Для JavaScript (Node.js) монорепозиторий особенно актуален, так как позволяет избавиться от «ада зависимостей» и ускорить разработку. Рассмотрим практическую реализацию.

Инструменты для монорепозитория в JavaScript

На рынке есть несколько популярных инструментов. Мы рассмотрим два основных: npm workspaces (встроенный в npm) и Lerna (расширенный функционал).

1. npm workspaces (встроенная поддержка)

Начиная с npm v7, пакетный менеджер поддерживает workspaces. Это самый простой способ начать. Пример структуры проекта:

my-monorepo/
├── packages/
│   ├── core/
│   │   ├── package.json
│   │   └── index.js
│   ├── utils/
│   │   ├── package.json
│   │   └── helpers.js
│   └── app/
│       ├── package.json
│       └── server.js
├── package.json
└── lerna.json (опционально)

Корневой package.json:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["packages/*"]
}

Теперь все пакеты в packages/* автоматически линкуются. Чтобы установить зависимости, достаточно выполнить npm install в корне. Зависимости между пакетами указываются стандартно:

// packages/app/package.json
{
  "name": "@my/app",
  "dependencies": {
    "@my/core": "*",
    "@my/utils": "*"
  }
}

npm workspaces создает симлинки в папке node_modules, поэтому импорты работают как с обычными пакетами.

2. Lerna (продвинутое управление)

Lerna — это инструмент для управления монорепозиториями с возможностью публикации, версионирования и выполнения скриптов параллельно. Установка:

npm install --global lerna

Инициализация в проекте:

lerna init

Файл lerna.json:

{
  "version": "independent",
  "npmClient": "npm",
  "packages": ["packages/*"]
}

Режим independent позволяет каждому пакету иметь свою версию. Основные команды:

  • lerna bootstrap — устанавливает зависимости и линкует пакеты.
  • lerna run build — выполняет скрипт build во всех пакетах.
  • lerna publish — публикует измененные пакеты в npm.

Практическая организация кода

Рассмотрим пример настройки монорепозитория для веб-приложения с бэкендом на Node.js и общими утилитами.

Шаг 1: Базовая структура

Создадим три пакета:

  • @my/core — общие функции (валидация, логирование).
  • @my/api — REST API сервер.
  • @my/worker — фоновый обработчик задач.

Корневой package.json:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["packages/*"],
  "scripts": {
    "build": "lerna run build",
    "test": "lerna run test",
    "start:api": "lerna run start --scope=@my/api"
  },
  "devDependencies": {
    "lerna": "^7.0.0",
    "typescript": "^5.0.0"
  }
}

Шаг 2: Настройка TypeScript (опционально)

Если вы используете TypeScript, создайте корневой tsconfig.json:

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES2020",
    "declaration": true,
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./packages"
  },
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/api" },
    { "path": "./packages/worker" }
  ]
}

В каждом пакете создайте свой tsconfig.json с ссылкой на корневой:

// packages/core/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

Шаг 3: Пример кода с переиспользованием

Пакет @my/core (src/logger.ts):

export class Logger {
  log(message: string): void {
    console.log(`[${new Date().toISOString()}] ${message}`);
  }
}

Пакет

Блоги

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