Монорепо для модулей
Контекст
Платформа имеет общий код между приложениями. Нужно организовать переиспользование без дублирования.
Решение
- Приложения — отдельные репозитории (независимый деплой)
- Модули — монорепо на pnpm workspaces (версионируются вместе)
Структура репозиториев
# Приложения — ОТДЕЛЬНЫЕ репозитории
app-one/ # Приложение 1
app-two/ # Приложение 2
app-three/ # Приложение 3
# Модули — МОНОРЕПО
platform-core/
├── packages/
│ ├── @scope/helpers/
│ ├── @scope/types/
│ └── @scope/ui/
├── layers/
│ ├── @scope/auth-layer/
│ └── @scope/base-layer/
├── pnpm-workspace.yaml
└── package.json
Обоснование
Почему приложения отдельно
| Критерий | Обоснование |
|---|---|
| Независимый деплой | Изменения в одном приложении не требуют релиза другого |
| Изоляция | Проблемы в одном приложении не блокируют другие |
Почему модули вместе
| Критерий | Обоснование |
|---|---|
| Атомарные изменения | Изменение типа сразу доступно везде |
| Единый lockfile | Консистентные версии |
| Проще тестировать | Один CI для всех модулей |
Конфигурация монорепо
pnpm-workspace.yaml
packages:
- 'packages/*'
- 'layers/*'
package.json
{
"name": "platform-core",
"private": true,
"scripts": {
"build": "pnpm -r build",
"dev": "pnpm -r --parallel dev",
"lint": "pnpm -r lint",
"typecheck": "pnpm -r typecheck",
"publish": "pnpm -r publish"
}
}
Использование модулей в приложениях
// app-admin/package.json
{
"dependencies": {
"@scope/api-client": "^1.0.0",
"@scope/helpers": "^1.0.0",
"@scope/types": "^1.0.0"
}
}
// app-admin/nuxt.config.ts
export default defineNuxtConfig({
extends: ['@scope/base-layer'],
})
Команды для работы с монорепо
Если вы привыкли работать с одним проектом, вот как те же действия выполняются в монорепо.
Базовые команды
| Обычный проект | Монорепо | Описание |
|---|---|---|
pnpm install | pnpm install | Установить все зависимости всех пакетов |
pnpm build | pnpm build | Собрать все пакеты |
pnpm dev | pnpm dev | Запустить dev-режим для всех пакетов |
pnpm lint | pnpm lint | Линтинг всех пакетов |
pnpm test | pnpm test | Тесты всех пакетов |
Работа с конкретным пакетом (--filter)
Ключевая команда: pnpm --filter <package-name> <command>
# Собрать только helpers
pnpm --filter @scope/helpers build
# Запустить dev только для ui-kit
pnpm --filter @scope/ui dev
# Запустить тесты только для types
pnpm --filter @scope/types test
# Линтинг только base-layer
pnpm --filter @scope/base-layer lint
Добавление зависимостей
# Добавить lodash в @scope/helpers
pnpm --filter @scope/helpers add lodash
# Добавить dev-зависимость в @scope/types
pnpm --filter @scope/types add -D typescript
# Добавить зависимость на другой пакет монорепо
pnpm --filter @scope/ui add @scope/helpers
# Добавить зависимость во ВСЕ пакеты
pnpm -r add dayjs
# Добавить зависимость в корень монорепо (dev-tools)
pnpm add -w -D eslint prettier
Удаление зависимостей
# Удалить lodash из @scope/helpers
pnpm --filter @scope/helpers remove lodash
# Удалить из всех пакетов
pnpm -r remove lodash
Обновление зависимостей
# Обновить lodash в конкретном пакете
pnpm --filter @scope/helpers update lodash
# Обновить все зависимости во всех пакетах
pnpm -r update
# Интерактивное обновление
pnpm -r update -i
Выполнение произвольных команд
# Выполнить npm script в конкретном пакете
pnpm --filter @scope/helpers run typecheck
# Выполнить команду во всех пакетах параллельно
pnpm -r --parallel run typecheck
# Выполнить команду последовательно (с учётом зависимостей)
pnpm -r run build
Фильтрация по паттернам
# Все пакеты в packages/
pnpm --filter "./packages/*" build
# Все пакеты в layers/
pnpm --filter "./layers/*" build
# Пакеты, начинающиеся с @scope/
pnpm --filter "@scope/*" build
# Пакет и все его зависимости
pnpm --filter @scope/ui... build
# Пакет и все зависящие от него
pnpm --filter ...@scope/types build
Просмотр информации
# Список всех пакетов в монорепо
pnpm -r list --depth -1
# Граф зависимостей между пакетами
pnpm -r list --depth 0
# Почему установлена зависимость
pnpm --filter @scope/helpers why lodash
Типичные сценарии
Разработка одного пакета
cd platform-core
# Запустить dev-режим только для ui-kit
pnpm --filter @scope/ui dev
# В другом терминале — запустить тесты в watch-режиме
pnpm --filter @scope/ui test:watch
Добавление нового пакета
# Создать структуру нового пакета
mkdir -p packages/new-package
cd packages/new-package
# Инициализировать package.json
pnpm init
# Вернуться в корень и установить зависимости
cd ../..
pnpm install
Подготовка к публикации
# Проверить что всё собирается
pnpm build
# Проверить линтинг
pnpm lint
# Запустить тесты
pnpm test
# Собрать и опубликовать
pnpm publish -r --no-git-checks
Краткая шпаргалка
| Флаг | Описание |
|---|---|
--filter <name> | Выполнить для конкретного пакета |
-r | Выполнить рекурсивно для всех пакетов |
--parallel | Выполнить параллельно (не ждать завершения) |
-w | Выполнить в корне workspace |
...pkg | Пакет и все зависящие от него |
pkg... | Пакет и все его зависимости |
Hot Reload между модулями
При разработке модулей в монорепо важно, чтобы изменения в одном пакете автоматически подхватывались в зависимых пакетах.
Настройка stub-режима
Nuxt модули поддерживают stub-режим для разработки. Это позволяет изменениям в исходниках сразу отражаться без пересборки.
# Запустить модуль в stub-режиме (пересобирает при изменениях)
pnpm --filter @scope/helpers dev:prepare
# Или для всех модулей
pnpm -r dev:prepare
Конфигурация package.json модуля
{
"scripts": {
"dev": "nuxt-module-build build --stub && nuxt dev playground",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare",
"build": "nuxt-module-build build"
}
}
Как это работает
--stubсоздаёт stub-файлы вместо полной сборки- Stub-файлы используют
jitiдля транспиляции на лету - Изменения в
src/сразу видны в приложениях-потребителях
Разработка нескольких модулей одновременно
# Терминал 1: Запустить helpers в stub-режиме
pnpm --filter @scope/helpers dev:prepare
# Терминал 2: Запустить ui в stub-режиме (зависит от helpers)
pnpm --filter @scope/ui dev:prepare
# Терминал 3: Запустить playground приложение
pnpm --filter @scope/ui dev
Альтернатива: параллельный запуск
# Запустить dev:prepare для всех модулей параллельно
pnpm -r --parallel dev:prepare
Настройка в приложении-потребителе
Если вы разрабатываете приложение, которое использует модули из монорепо локально:
// nuxt.config.ts приложения
export default defineNuxtConfig({
modules: [
'@scope/helpers',
'@scope/ui',
],
// Для hot reload при изменениях в node_modules/@scope/*
watch: ['~/node_modules/@scope/*/dist'],
})
Использование workspace protocol
В package.json приложения используйте workspace:* для локальной разработки:
{
"dependencies": {
"@scope/helpers": "workspace:*",
"@scope/ui": "workspace:*"
}
}
При публикации pnpm автоматически заменит workspace:* на актуальную версию.
Публикация в Package Registry
Модули публикуются в приватный Package Registry. Подробнее см. GitLab CE.
Настройка .npmrc в монорепо
# .npmrc
@scope:registry=https://gitlab.example.com/api/v4/projects/PROJECT_ID/packages/npm/
//gitlab.example.com/api/v4/projects/PROJECT_ID/packages/npm/:_authToken=${GITLAB_TOKEN}
Настройка package.json пакетов
// packages/helpers/package.json
{
"name": "@scope/helpers",
"version": "1.0.0",
"publishConfig": {
"@scope:registry": "https://gitlab.example.com/api/v4/projects/PROJECT_ID/packages/npm/"
}
}
CI/CD публикация
# .gitlab-ci.yml
publish:
stage: publish
script:
- pnpm build
- pnpm publish -r --no-git-checks
only:
- tags
Последствия
Положительные
- Приложения деплоятся независимо
- Модули версионируются вместе
- Общий код не дублируется
Отрицательные
- Больше репозиториев для управления