Монорепозитории
Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в packages/ и какие ограничения действуют для общих пакетов.
Определение
Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв ui, infra и shared.
Базовая структура
Каждое приложение внутри apps/ сохраняет собственную SLM-структуру в src/.
repo/
├── apps/
│ ├── web/
│ │ └── src/
│ │ ├── app/
│ │ ├── layouts/
│ │ ├── screens/
│ │ ├── widgets/
│ │ ├── business/
│ │ ├── infra/
│ │ ├── ui/
│ │ └── shared/
│ └── admin/
│ └── src/
│ └── ...
└── packages/
├── ui/
│ ├── button/ # самостоятельный пакет UI-модуля
│ ├── input/ # самостоятельный пакет UI-модуля
│ └── modal/ # самостоятельный пакет UI-модуля
├── infra/
│ ├── theme/ # самостоятельный пакет infra-модуля
│ ├── backend-api/ # самостоятельный пакет infra-модуля
│ └── logger/ # самостоятельный пакет infra-модуля
└── shared/ # единый shared-пакет
├── package.json
└── src/
├── lib/ # переиспользуемые утилиты
├── helpers/ # переиспользуемые helpers
└── index.tsapps/{app}/src — граница SLM-приложения. packages/* находятся выше SLM и не добавляют новые архитектурные слои.
Группировка frontend-пакетов
Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: ui, infra, shared.
packages/ui/* # пакеты UI-модулей
packages/infra/* # пакеты infra-модулей
packages/shared # единый shared-пакетЭта группировка повторяет названия SLM-слоёв для навигации, но сама не является слоистой архитектурой внутри packages/. Монорепозиторий может содержать другие пакеты: tooling, конфиги, SDK, схемы, e2e и другие технические пакеты вне SLM.
Пакет и модуль
Пакет не равен SLM-модулю: модуль — архитектурная единица внутри слоя приложения, package — единица монорепозитория для переиспользования, владения, сборки и публикации.
В packages/ui/* размещаются пакеты самостоятельных UI-модулей. В packages/infra/* размещаются пакеты самостоятельных инфраструктурных модулей. packages/shared устроен иначе: это единый пакет для переиспользуемых утилит, helpers и другого фундаментального кода без привязки к конкретному приложению.
packages/ui/button/
packages/ui/modal/
packages/infra/theme/
packages/infra/backend-api/
packages/shared/Что остаётся в приложении
Слои app, layouts, screens, widgets и business остаются внутри конкретного приложения.
apps/web/src/app/
apps/web/src/layouts/
apps/web/src/screens/
apps/web/src/widgets/
apps/web/src/business/app, layouts и screens привязаны к роутингу, каркасу и страницам конкретного приложения. widgets не выносятся в пакеты, потому что это слой композиции интерфейса приложения.
business не выносится в packages/*. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой.
Что можно выносить
В пакеты выносится только код из ui, infra и shared, который потенциально будет использоваться в двух и более фронтенд-приложениях монорепозитория.
| Группа | Что выносить | Пример |
|---|---|---|
packages/ui/* | Самостоятельные UI-модули без бизнес-логики | packages/ui/button |
packages/infra/* | Самостоятельные технические сервисы | packages/infra/backend-api |
packages/shared | Общие утилиты, helpers и фундаментальный код | packages/shared |
Пакет можно создавать сразу, если модуль имеет общую природу и ожидается его переиспользование между приложениями. App-specific код остаётся внутри приложения.
UI-пакеты
В packages/ui/* размещаются переиспользуемые UI-модули.
packages/ui/button/
├── package.json
└── src/
├── button.tsx
├── styles/
├── types/
└── index.tsUI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц.
Infra-пакеты
В packages/infra/* размещаются переиспользуемые инфраструктурные модули.
packages/infra/backend-api/
├── package.json
└── src/
├── clients/
├── config/
├── types/
└── index.tsПривязанные к конкретному приложению сервисы остаются в apps/{app}/src/infra. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок.
Shared-пакет
packages/shared является единым пакетом.
packages/shared/
├── package.json
└── src/
├── lib/
├── helpers/
└── index.tsВ packages/shared сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте.
Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий shared не выносятся.
Имена пакетов и импорты
Путь импорта задаётся name в package.json, а не расположением директории.
{
"name": "@repo/theme"
}packages/infra/theme/package.jsonimport { ThemeProvider } from '@repo/theme'Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены.
// Хорошо
import { Button } from '@repo/button'
// Плохо
import { Button } from '@repo/button/src/button'Зависимости
На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений.
apps → packages
packages -/→ appsВнутри приложения продолжает действовать обычное направление зависимостей SLM.
app → [ layouts | screens ] → widgets → business → infra → ui → sharedПакеты не должны нарушать природу своей группы: packages/ui/* не импортирует packages/infra/*, packages/shared не импортирует другие группы, а packages/infra/* не знает о приложениях.
Когда не выносить
Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API.
Фактическое использование в одном приложении не запрещает пакет, если модуль имеет общую природу и потенциально нужен нескольким приложениям.
# Плохо
apps/web/src/screens/home/parts/promo-section/
packages/ui/promo-section/Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным parts/-модулем.
Конфигурационные пакеты
Конфигурационные пакеты не относятся к SLM-архитектуре.
Если в монорепозитории есть общие настройки TypeScript, ESLint, сборки или форматирования, они относятся к tooling-инфраструктуре репозитория. Такие пакеты могут находиться в packages/, но их структура зависит от выбранного инструментария и не участвует в правилах слоёв внутри src/.
Правила
- SLM применяется внутри каждого
apps/{app}/src. - Frontend-пакеты, вынесенные из SLM-приложений, группируются в
packages/ui,packages/infra,packages/shared. - Группы
packages/ui,packages/infra,packages/sharedне являются SLM-слоями. - В
packages/ui/*размещаются пакеты самостоятельных UI-модулей. - В
packages/infra/*размещаются пакеты самостоятельных инфраструктурных модулей. packages/sharedявляется единым пакетом для переиспользуемых утилит и helpers.- Модуль можно размещать в пакете, если он потенциально будет использоваться в двух и более фронтенд-приложениях.
business,app,layouts,screens,widgetsне выносятся в пакеты.- Проектные стили, типы приложения и продуктовые конфиги не выносятся в
packages/shared. - Пакеты не импортируют приложения.
- Межпакетные импорты идут только через публичный API.
- Deep imports внутрь пакетов запрещены.
- Локальная колокация важнее преждевременного выноса в
packages/*.