Перейти к содержимому

57. Monorepo с Nx

Привет! Яша здесь. Nx — это инструмент для управления monorepo, который превращает хаос из 50 репозиториев в одну хорошо организованную рабочую область. Разберём как Nx помогает масштабировать Angular-проекты 🚀


Проблема: Большая компания с несколькими Angular-приложениями и общим кодом.

Без Nx:

  • Каждое приложение — отдельный репозиторий
  • Общие компоненты дублируются или публикуются в NPM
  • Нет уверенности что изменение одного кода не сломает другой
  • CI/CD запускает всё всегда (долго и дорого)

С Nx:

  • Один репозиторий — все приложения и библиотеки вместе
  • nx affected — запускает только то, что изменилось
  • Граф зависимостей — видно кто от кого зависит
  • Кэш — не пересобирать если ничего не изменилось

Окно терминала
npx create-nx-workspace@latest my-org --preset=angular-monorepo
# Добавить Nx к существующему Angular проекту
ng add @nx/angular
# Структура workspace
# my-org/
# ├── apps/
# │ ├── shop/ ← Angular приложение
# │ └── admin/ ← Angular приложение
# ├── libs/
# │ ├── ui/ ← UI компоненты (shared)
# │ ├── feature-auth/ ← Feature модуль
# │ ├── data-access/ ← HTTP сервисы
# │ └── util/ ← Утилиты
# ├── nx.json
# └── package.json ← один package.json на всех

Окно терминала
# Создать приложение
nx generate @nx/angular:application shop --routing --style=scss
# Создать библиотеку
nx generate @nx/angular:library ui-components --directory=shared --buildable
# Типы библиотек (Convention от Nx)
nx generate @nx/angular:library feature-login --directory=auth # feature/
nx generate @nx/angular:library ui-button --directory=shared # ui/
nx generate @nx/angular:library data-access-users --directory=users # data-access/
nx generate @nx/angular:library util-validators --directory=shared # util/

apps/
shop/ ← компонует feature libraries
admin/ ← компонует feature libraries
libs/
├── feature/ ← «умные» компоненты, страницы
│ └── auth/ ← LoginPage, RegisterPage (специфичны для приложения)
├── ui/ ← «тупые» презентационные компоненты
│ └── shared/ ← Button, Input, Modal (переиспользуемые везде)
├── data-access/ ← HTTP сервисы, NgRx, состояние
│ └── users/ ← UserService, userReducer
└── util/ ← чистые функции, pipes, validators
└── shared/ ← formatDate, validators (нет Angular зависимостей)
libs/ui/shared/src/lib/button/button.component.ts
@Component({
selector: 'ui-button',
standalone: true,
template: `<button [class]="variant" [disabled]="disabled"><ng-content /></button>`,
})
export class ButtonComponent {
@Input() variant: 'primary' | 'secondary' = 'primary';
@Input() disabled = false;
}
// libs/ui/shared/src/index.ts — barrel export
export { ButtonComponent } from './lib/button/button.component';
export { InputComponent } from './lib/input/input.component';
export { ModalComponent } from './lib/modal/modal.component';
// Использование в приложении:
// import { ButtonComponent } from '@my-org/ui/shared';

// tsconfig.base.json — автоматически обновляется Nx
{
"compilerOptions": {
"paths": {
"@my-org/ui/shared": ["libs/ui/shared/src/index.ts"],
"@my-org/feature/auth": ["libs/feature/auth/src/index.ts"],
"@my-org/data-access/users": ["libs/data-access/users/src/index.ts"],
"@my-org/util/shared": ["libs/util/shared/src/index.ts"]
}
}
}
// ✅ Правильный импорт через алиас (не относительный путь!)
import { ButtonComponent } from '@my-org/ui/shared';
import { UserService } from '@my-org/data-access/users';
import { validateEmail } from '@my-org/util/shared';
// ❌ Неправильно — относительный путь между библиотеками
import { ButtonComponent } from '../../libs/ui/shared/src/lib/button/button.component';

Окно терминала
# Показать что изменилось относительно main
nx affected:apps # приложения затронутые изменениями
nx affected:libs # библиотеки затронутые изменениями
# Запустить тесты только для изменённых проектов
nx affected:test
# Собрать только изменённые приложения
nx affected:build
# Запустить lint для всех затронутых
nx affected:lint --parallel=5
# Относительно конкретного коммита
nx affected:test --base=HEAD~3 --head=HEAD

Окно терминала
# Открыть интерактивный граф в браузере
nx graph
# Граф только для конкретного проекта
nx graph --focus=shop
# Просмотр затронутых проектов
nx affected:graph
Визуализация зависимостей:
shop ──→ feature/auth ──→ data-access/users ──→ util/shared
──→ ui/shared
──→ data-access/cart
admin ──→ feature/auth
──→ data-access/users
──→ ui/shared

// .eslintrc.json — запрет на нарушение архитектуры
{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": ["type:ui", "type:data-access", "type:util"]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:util"]
},
{
"sourceTag": "type:data-access",
"onlyDependOnLibsWithTags": ["type:util"]
},
{
"sourceTag": "scope:shop",
"onlyDependOnLibsWithTags": ["scope:shop", "scope:shared"]
}
]
}
]
}
}

Окно терминала
# Без Nx Cloud: каждая машина строит с нуля
# С Nx Cloud: результаты кэшируются в облаке
# Подключить Nx Cloud
nx connect-to-nx-cloud
# Результат: если сборка уже была на другой машине — она возьмётся из кэша
# nx build shop → retrieved from cache in 2 seconds (instead of 45 seconds!)
// nx.json — настройка кэша
{
"tasksRunnerOptions": {
"default": {
"runner": "nx-cloud",
"options": {
"cacheableOperations": ["build", "test", "lint", "e2e"],
"accessToken": "your-nx-cloud-token"
}
}
}
}

tools/generators/feature-module/index.ts
// Кастомный генератор для создания feature по стандарту команды
import { Tree, formatFiles, generateFiles, names } from '@nx/devkit';
export default async function (tree: Tree, options: { name: string; scope: string }) {
const { fileName, className } = names(options.name);
generateFiles(tree, join(__dirname, 'files'), `libs/feature/${fileName}`, {
...options,
fileName,
className,
tmpl: '',
});
await formatFiles(tree);
}
// Запуск: nx generate @my-org/tools:feature-module --name=checkout --scope=shop

// project.json — кастомный executor
{
"targets": {
"build-storybook": {
"executor": "@nx/storybook:build",
"options": { "uiFramework": "@storybook/angular" }
},
"analyze": {
"executor": "@nx/webpack:webpack",
"options": { "statsJson": true }
}
}
}

Визуализатор графа зависимостей Nx monorepo: