Архитектура

Монорепо для модулей

Контекст

Платформа имеет общий код между приложениями. Нужно организовать переиспользование без дублирования.

Решение

  • Приложения — отдельные репозитории (независимый деплой)
  • Модули — монорепо на 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 installpnpm installУстановить все зависимости всех пакетов
pnpm buildpnpm buildСобрать все пакеты
pnpm devpnpm devЗапустить dev-режим для всех пакетов
pnpm lintpnpm lintЛинтинг всех пакетов
pnpm testpnpm 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"
  }
}

Как это работает

  1. --stub создаёт stub-файлы вместо полной сборки
  2. Stub-файлы используют jiti для транспиляции на лету
  3. Изменения в 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

Последствия

Положительные

  • Приложения деплоятся независимо
  • Модули версионируются вместе
  • Общий код не дублируется

Отрицательные

  • Больше репозиториев для управления

Связанные решения