Настройка Monorepo (Lerna) для веб-проекта
Lerna — один из первых инструментов для JavaScript monorepo. Долгое время был де-факто стандартом, потом потерял популярность, а с версии 6+ возродился под управлением Nrwl (создателей Nx) с интеграцией с Nx под капотом. Сегодня Lerna — хороший выбор для проектов, которым нужно управление версиями пакетов и публикация в npm, но не нужна сложная infrastructure Nx.
Когда Lerna, а не Turborepo или Nx
Lerna имеет смысл, когда:
- Проект — библиотека или набор пакетов, которые публикуются в npm
- Нужно автоматическое управление версиями (semver) и CHANGELOG
- Команда небольшая, сложная инфраструктура избыточна
Для закрытого продукта без публикации в npm — лучше Turborepo или Nx.
Инициализация
# Новый monorepo
npx lerna init --packages="packages/*" --independent
cd my-monorepo
# Или добавить в существующий
npm install lerna --save-dev
npx lerna init
Флаг --independent означает, что каждый пакет версионируется независимо. Альтернатива — fixed mode, когда все пакеты имеют одну версию (как в React/Vue monorepo).
Конфигурация lerna.json
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "independent",
"npmClient": "pnpm",
"command": {
"publish": {
"conventionalCommits": true,
"createRelease": "github",
"message": "chore(release): publish",
"registry": "https://registry.npmjs.org",
"allowBranch": ["main", "next"]
},
"version": {
"conventionalCommits": true,
"conventionalChangelogConfig": "@conventional-changelog/conventionalcommits",
"changelogPreset": "angular",
"gitTagVersion": true,
"push": true
},
"bootstrap": {
"npmClientArgs": ["--no-package-lock"]
}
},
"useWorkspaces": true,
"useNx": true
}
conventionalCommits: true — Lerna определяет тип версионирования по формату коммитов: fix: → patch, feat: → minor, BREAKING CHANGE → major.
Структура для библиотеки компонентов
my-ui-library/
├── packages/
│ ├── button/
│ │ ├── src/
│ │ │ ├── Button.tsx
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── input/
│ ├── modal/
│ ├── table/
│ └── theme/
├── apps/
│ └── docs/ # Storybook или документационный сайт
├── package.json
├── lerna.json
└── pnpm-workspace.yaml
// packages/button/package.json
{
"name": "@acme/button",
"version": "1.2.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": ["dist"],
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": ">=17.0.0"
},
"devDependencies": {
"@acme/theme": "workspace:*",
"tsup": "^8.0.0",
"vitest": "^1.0.0"
}
}
Управление версиями
# Определить, что изменилось (dry run)
npx lerna changed
# Обновить версии интерактивно
npx lerna version
# Автоматически по conventional commits
npx lerna version --conventional-commits --yes
# Версия с предрелизным тегом (для beta)
npx lerna version --conventional-commits --conventional-prerelease --preid=beta
При запуске lerna version происходит:
- Определяет изменившиеся пакеты
- Предлагает новые версии по semver правилам
- Обновляет
package.jsonи зависимости между пакетами - Генерирует CHANGELOG.md
- Создаёт git commit и теги
Публикация
# Публикация всех непубликованных пакетов
npx lerna publish from-package
# Публикация только изменившихся (после lerna version)
npx lerna publish from-git
# В private registry (Verdaccio, GitHub Packages, JFrog)
npx lerna publish from-git --registry=https://npm.pkg.github.com
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
permissions:
contents: write
packages: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # нужно для conventional commits анализа
token: ${{ secrets.GITHUB_TOKEN }}
- uses: pnpm/action-setup@v3
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
- name: Build all packages
run: npx lerna run build
- name: Version and publish
run: |
git config user.email "[email protected]"
git config user.name "CI Bot"
npx lerna version --conventional-commits --yes --no-push
npx lerna publish from-git --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Запуск задач с Nx под капотом
Lerna 6+ использует Nx для умного запуска задач:
# Запустить build для всех пакетов параллельно с кешем
npx lerna run build
# Только для изменившихся пакетов
npx lerna run test --since=main
# Для конкретного пакета и его зависимостей
npx lerna run build --scope=@acme/modal --include-dependents
// nx.json (создаётся автоматически при --useNx)
{
"extends": "nx/presets/npm.json",
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
},
"test": {
"cache": true
}
}
}
Работа с зависимостями между пакетами
# Добавить зависимость в конкретный пакет
pnpm add react --filter @acme/button
# Добавить локальный пакет как зависимость
pnpm add @acme/theme --filter @acme/button --workspace
# Добавить devDependency во все пакеты
pnpm add -D typescript --recursive
Если пакет A зависит от пакета B внутри monorepo, и B изменился — lerna run build --scope=A --include-dependencies пересоберёт B перед A.
Типичные проблемы
Версия зависимости не обновляется при lerna version — проверьте, что зависимость указана без ^ или ~, иначе Lerna не сделает хардкодный апдейт. Флаг --force-publish обновит все пакеты принудительно.
CHANGELOG дублирует записи — Lerna по умолчанию добавляет только коммиты, затронувшие файлы данного пакета. Используйте --changelog-include-commits-root-path если нужны корневые коммиты.
Публикация падает на CI из-за npm publish --dry-run в .npmrc — убедитесь, что dry-run=false в CI окружении.
Сроки
Настройка Lerna для набора npm-пакетов с нуля — два-три дня: конфигурация, настройка конвенциональных коммитов (commitlint + husky), CI/CD для автоматической публикации, документирование процесса релиза для команды.







