Монорепозиторий (monorepo) представляет собой единый репозиторий, содержащий несколько связанных проектов, пакетов или приложений. Вместо хранения каждого пакета в отдельном репозитории вся кодовая база располагается в общей структуре каталогов.
Типичная структура монорепозитория выглядит следующим образом:
project/
├── packages/
│ ├── core/
│ ├── ui/
│ ├── utils/
│ └── api/
├── apps/
│ ├── admin/
│ └── client/
├── package.json
└── rollup.config.js
Каждый пакет может обладать собственным package.json,
набором исходников и конфигурацией сборки.
Подобный подход широко применяется при разработке:
При использовании Rollup работа с монорепозиториями требует понимания особенностей разрешения модулей, локальных зависимостей и организации процесса сборки.
Предположим, существует два пакета:
packages/
├── core/
└── ui/
Пакет ui зависит от core.
Файл:
// packages/ui/src/index.js
import { formatDate } from '@company/core';
В монорепозитории зависимость физически находится рядом, однако Rollup должен корректно определить её расположение.
Для этого обычно используются:
Современный npm поддерживает рабочие пространства.
Корневой package.json:
{
"private": true,
"workspaces": [
"packages/*"
]
}
Пакет core:
{
"name": "@company/core",
"version": "1.0.0"
}
Пакет ui:
{
"name": "@company/ui",
"version": "1.0.0",
"dependencies": {
"@company/core": "1.0.0"
}
}
После выполнения установки npm автоматически создаёт связи между пакетами.
В результате импорт выглядит так же, как импорт внешней зависимости:
import { formatDate } from '@company/core';
Rollup получает возможность работать с локальным пакетом через стандартный механизм поиска модулей.
Большинство локальных пакетов подключается через пакет:
import resolve from '@rollup/plugin-node-resolve';
Конфигурация:
export default {
input: 'src/index.js',
plugins: [
resolve()
]
};
Плагин выполняет поиск зависимостей:
node_modules/
Однако в случае workspaces многие локальные пакеты представлены символьными ссылками.
Например:
node_modules/
└── @company/
└── core -> ../. ./packages/core
Для Rollup такая структура практически не отличается от обычной зависимости.
По умолчанию Rollup разворачивает символьные ссылки до реального пути.
Например:
node_modules/@company/core
может быть преобразован в:
packages/core
Иногда это вызывает проблемы:
Для изменения поведения используется параметр:
export default {
preserveSymlinks: true
};
Теперь Rollup будет использовать символическую ссылку как самостоятельный путь.
Это особенно важно при работе с крупными монорепозиториями.
Часто требуется собирать сразу несколько библиотек.
Пример структуры:
packages/
├── core/
├── ui/
├── icons/
└── utils/
Каждый пакет имеет собственную точку входа.
Можно экспортировать массив конфигураций:
export default [
{
input: 'packages/core/src/index.js',
output: {
file: 'packages/core/dist/index.js',
format: 'esm'
}
},
{
input: 'packages/ui/src/index.js',
output: {
file: 'packages/ui/dist/index.js',
format: 'esm'
}
}
];
Rollup последовательно выполнит сборку всех пакетов.
При большом количестве пакетов ручное описание становится неудобным.
Вместо этого конфигурация может генерироваться автоматически.
Пример:
import fs from 'fs';
const packages = fs.readdirSync('./packages');
export default packages.map(name => ({
input: `packages/${name}/src/index.js`,
output: {
file: `packages/${name}/dist/index.js`,
format: 'esm'
}
}));
Подобный подход позволяет масштабировать сборочную систему без постоянного редактирования конфигурации.
Иногда пакет не должен включать код соседнего пакета в свой бандл.
Например:
import { logger } from '@company/core';
Можно объявить зависимость внешней:
export default {
external: [
'@company/core'
]
};
В результате импорт сохранится:
import { logger } from '@company/core';
в итоговом файле.
Такой подход особенно полезен при публикации набора взаимосвязанных библиотек.
Другой вариант — включать локальные пакеты непосредственно в сборку.
Конфигурация:
export default {
plugins: [
resolve()
]
};
При отсутствии настройки external Rollup анализирует код
зависимого пакета и объединяет его с основным бандлом.
Получается единый файл:
ui bundle
├─ ui code
├─ core code
└─ shared utilities
Преимущества:
Недостатки:
Рассмотрим структуру:
core
└─ react
ui
└─ react
Если оба пакета содержат собственные экземпляры React, возможно появление ошибок:
Invalid hook call
Причина заключается в загрузке нескольких копий библиотеки.
Часто проблему решают через peerDependencies.
Пакет:
{
"peerDependencies": {
"react": "^18.0.0"
}
}
После этого React устанавливается только на верхнем уровне монорепозитория.
Для библиотек внутри монорепозитория рекомендуется следующая схема.
Пакет ui:
{
"peerDependencies": {
"@company/core": "^1.0.0"
}
}
Пакет не содержит собственную копию зависимости.
Во время выполнения используется экземпляр, предоставленный потребителем.
Это позволяет избежать:
Во многих проектах пакеты используют одинаковые настройки.
Например:
import resolve from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
export function createConfig(input, output) {
return {
input,
output,
plugins: [
resolve(),
terser()
]
};
}
Использование:
import { createConfig } from './build/config.js';
export default [
createConfig(
'packages/core/src/index.js',
{
file: 'packages/core/dist/index.js',
format: 'esm'
}
)
];
Такой подход обеспечивает единообразие сборки.
Одним из преимуществ Rollup является способность удалять неиспользуемый код даже при работе между пакетами.
Пакет:
// core
export function used() {}
export function unused() {}
Другой пакет:
import { used } from '@company/core';
used();
Во время сборки функция:
unused()
может быть полностью исключена из результата.
Для монорепозиториев это особенно важно, поскольку общий объём кода часто достигает десятков или сотен тысяч строк.
Зависимости между пакетами образуют граф.
Пример:
utils
↓
core
↓
ui
↓
app
Сборка должна происходить в определённой последовательности.
Некоторые инструменты автоматически вычисляют порядок:
Если порядок нарушен, возможны ошибки:
Cannot find module
или
Package not built
Поэтому в крупных монорепозиториях обычно реализуется механизм определения зависимостей между пакетами.
Полная пересборка десятков пакетов может занимать значительное время.
Инкрементальная стратегия предполагает пересборку только изменённых модулей.
Пример сценария:
Изменён:
packages/utils
Требуется пересобрать:
utils
core
ui
app
Но не требуется пересобирать остальные независимые пакеты.
В сочетании с watch-режимом Rollup это существенно ускоряет разработку.
Rollup способен отслеживать изменения файлов:
rollup -c -w
При работе с несколькими пакетами система наблюдения может реагировать на изменения во всех директориях:
packages/core/src
packages/ui/src
packages/utils/src
После изменения исходников автоматически запускается новая сборка.
Это особенно удобно при разработке библиотек, тесно связанных между собой.
Каждый пакет может публиковаться одновременно в нескольких форматах.
Пример:
export default {
input: 'src/index.js',
output: [
{
dir: 'dist/esm',
format: 'esm'
},
{
dir: 'dist/cjs',
format: 'cjs'
}
]
};
Структура после сборки:
dist/
├── esm/
└── cjs/
Подобный подход часто используется для библиотек внутри монорепозитория, которые должны поддерживать разные среды выполнения.
Иногда результаты всех пакетов складываются в единый каталог.
Пример:
dist/
├── core/
├── ui/
├── icons/
└── utils/
Конфигурация:
output: {
dir: `dist/${packageName}`,
format: 'esm'
}
Централизованное хранение упрощает:
Наиболее распространённые ошибки:
Could not resolve '@company/core'
Причины:
node-resolve.Multiple React instances detected
Причины:
peerDependencies.core → ui → core
Последствия:
"@company/core": "^1.0.0"
и
"@company/core": "^2.0.0"
в разных пакетах могут приводить к конфликтам и неоднозначному поведению менеджера пакетов.
При организации монорепозитория с Rollup обычно придерживаются следующих принципов:
package.json;peerDependencies, когда
это оправдано;@rollup/plugin-node-resolve;Такой подход позволяет эффективно масштабировать кодовую базу, состоящую из множества взаимосвязанных библиотек и приложений, сохраняя преимущества Rollup в области оптимизации, анализа зависимостей и генерации компактных сборок.