--- url: /slm-design/architecture.md description: 'Назначение архитектуры, ключевые принципы и карта разделов документации' --- # SLM Design Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. ## Разделы спецификации Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше: * [Слои](/architecture/layers) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя. * [Модули](/architecture/modules) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента. * [Сегменты](/architecture/segments) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов. * [Монорепозитории](/architecture/monorepo) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business. Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории. ## Преимущества ### Вертикальная организация домена Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы. ### Dependency Injection без фреймворков Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий. ### Разделение ответственности без перегрузки слоёв Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода. ### Горизонтальная инкапсуляция Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга. ### Колокация по умолчанию Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями. ### Явное разделение каркаса и контента Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью. ### Масштабирование через группировку При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции). ### Адаптация к монорепозиториям SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы. ## Происхождение SLM Design вырос на основе: * **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей * **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое * **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию * **Colocation Principle** — код живёт рядом с местом использования ## Пример структуры проекта ```text src/ ├── app/ │ ├── layouts/ │ ├── main/ │ └── dashboard/ │ ├── screens/ │ ├── home/ │ ├── products/ │ ├── product-detail/ │ └── about/ │ ├── widgets/ │ ├── page-heading/ │ ├── hero-section/ │ └── promo-banner/ │ ├── business/ │ ├── auth/ │ ├── catalog/ │ ├── orders/ │ └── chat/ │ ├── infra/ │ ├── theme/ │ ├── i18n/ │ ├── backend-api/ │ └── logger/ │ ├── ui/ │ ├── button/ │ ├── input/ │ ├── modal/ │ ├── toast/ │ └── dropdown/ │ └── shared/ ├── lib/ ├── types/ └── styles/ ``` ## Принципы * **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле. * **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости. * **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API. * **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда. --- --- url: /slm-design/architecture/layers.md description: >- Иерархия слоёв от app до shared, правила зависимостей и зона ответственности каждого слоя --- # Слои Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом. ## Определение **Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.** ## Группы слоёв Слои делятся на три группы: | Группа | Слои | Описание | |--------|------|----------| | Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы | | Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит | | Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги | ## Направление зависимостей Любой импорт между модулями — только через публичный API. ``` app → [ layouts | screens ] → widgets → business → infra → ui → shared ``` * `layouts` и `screens` — параллельные слои, не импортируют друг друга * Модули одного слоя в группе «Композиция» изолированы друг от друга * Модули одного слоя `infra` и `ui` могут импортировать друг друга через публичный API * Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую * Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях ## Слой App Точка входа приложения. Отвечает за запуск, роутинг и композицию маршрутов из layout и screen. В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации. ### Требования * Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация * Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов * Провайдеры и гарды — только подключает готовые из нижних слоёв, не реализует * Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы * Никем не импортируется ## Слой Layouts Каркас страницы: общие элементы, одинаковые для группы маршрутов (header, footer, sidebar). ```text src/layouts/ ├── main/ ├── dashboard/ └── auth/ ``` ### Требования * Содержит только модули * Не содержит бизнес-логику * Контекстно-зависимые блоки принимает через пропсы от `app`, не импортирует напрямую ## Слой Screens Контент конкретной страницы: собирает её из модулей нижних слоёв. ```text src/screens/ ├── home/ ├── products/ ├── product-detail/ ├── about/ └── contacts/ ``` Когда количество страниц затрудняет навигацию — вводится группировка по разделам. Группа — папка для организации, не модуль (без `index.ts`). ```text src/screens/ ├── shop/ │ ├── home/ │ ├── products/ │ ├── product-detail/ │ └── cart/ ├── account/ │ ├── profile/ │ ├── settings/ │ └── order-history/ └── info/ ├── about/ ├── contacts/ └── faq/ ``` ### Требования * Содержит только модули * Не содержит бизнес-логику * Локальные одноразовые секции живут внутри screen-модуля, не выносятся в `widgets`/`business` ## Слой Widgets Составной блок интерфейса, который компонует модули ядра, но не принадлежит конкретному бизнес-домену. Widget появляется когда блок используется в нескольких screens или layouts. Если блок принадлежит домену — он живёт в `business/{area}/`, даже если переиспользуется. Если блок нужен только в одном месте — это `screens/{name}/parts/` или `layouts/{name}/parts/`, а не widget. ```text src/widgets/ ├── page-heading/ ├── hero-section/ ├── onboarding-checklist/ ├── promo-banner/ └── error-boundary/ ``` ### Требования * Не принадлежит конкретному бизнес-домену. Если блок доменный — он живёт в `business/` * Используется в нескольких screens или layouts ## Слой Business Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами. Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `import type`. Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки. ```text src/business/ ├── auth/ ├── catalog/ ├── orders/ ├── checkout/ └── chat/ ``` Когда количество доменов затрудняет навигацию — вводится группировка по субдоменам. Группа — папка для организации, не модуль (без `index.ts`). ```text src/business/ ├── commerce/ │ ├── catalog/ │ ├── cart/ │ ├── orders/ │ └── checkout/ └── communication/ ├── chat/ └── notifications/ ``` ### Требования * Один модуль = один бизнес-домен * Циклические зависимости между доменами запрещены * Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты * Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую * Доменные типы (`User`, `Product`) живут здесь, не в `shared/` ## Слой infra Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль. Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). ```text src/infra/ ├── theme/ ├── i18n/ ├── backend-api/ ├── maps-api/ ├── logger/ ├── feature-flags/ └── realtime/ ``` ### Требования * Один модуль = один техсервис * Импортирует `infra/`, `ui/`, `shared/` ## Слой UI UI-кит без бизнес-логики: button, carousel, toast, modal. Слой входит в группу «Ядро». Импортирует `ui/` и `shared/`. Компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`. ```text src/ui/ ├── button/ ├── input/ ├── icon/ ├── carousel/ ├── modal/ ├── toast/ ├── dropdown/ ├── tabs/ └── tooltip/ ``` Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (`button`, `icon`, `input`) не импортируют композиции. Композиции (`carousel`, `modal`, `dropdown`) строятся на примитивах. ```text src/ui/ ├── primitives/ │ ├── button/ │ ├── input/ │ ├── icon/ │ └── badge/ └── composites/ ├── carousel/ ├── modal/ ├── dropdown/ ├── tabs/ └── tooltip/ ``` ### Требования * Не содержит бизнес-логику * Импортирует только `ui/` и `shared/` ## Слой Shared Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене. Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует. Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь. ```text src/shared/ ├── lib/ ├── types/ ├── styles/ └── sprites/ ``` ### Требования * Не имеет runtime-состояния --- --- url: /slm-design/architecture/modules.md description: >- Структура модуля, типы (UI, бизнес, инфра), публичный API, отличие модуля от компонента --- # Модули Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом. ## Определение **Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.** Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно. Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность. Главная граница модуля — не папка, а ответственность. ## Компонент **Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.** Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля. > Компонент отображает. Модуль организует. Компонент не может: * Импортировать код проекта за пределами родительского модуля. * Владеть архитектурными зависимостями. * Содержать любые компоненты. * Содержать любые модули. * Делать внешние запросы. * Самостоятельно получать данные. * Выбирать источник данных. * Композировать данные. * Вызывать сценарные хуки. * Оркестрировать сценарий. * Композировать модули. * Решать, как устроен процесс. * Содержать бизнес-логику. * Содержать сценарную логику. Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль. ```text auth/ ├── ui/ │ └── logout-button/ │ ├── logout-button.tsx │ ├── styles/ │ │ └── logout-button.module.css │ ├── types/ │ │ └── logout-button-props.type.ts │ └── index.ts └── index.ts ``` ## Что считается модулем Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу. Примеры модулей: * `screens/home/` — модуль страницы. * `widgets/page-heading/` — модуль виджета. * `business/auth/` — модуль бизнес-домена. * `infra/theme/` — модуль инфраструктурного сервиса. * `ui/button/` — модуль UI-kit сущности. * `screens/home/parts/hero-section/` — вложенный модуль страницы. Не считаются модулями: * `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты. * `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`. * `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента. ## Типы модулей Тип модуля определяет обязательный корневой файл и стартовую структуру. ### UI-модуль Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне: ```text header/ ├── header.tsx └── index.ts ``` `ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу. ### Бизнес-модуль Бизнес-модуль — модуль, который строится вокруг публичного runtime API. Бизнес-модуль обязан иметь фабрику в корне: ```text auth/ ├── auth.factory.ts ├── index.ts └── types/ ``` Фабрика возвращает публичный runtime API модуля. ### Инфраструктурный модуль Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции. Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса. ```text theme/ ├── index.ts ├── config/ ├── hooks/ ├── styles/ └── ui/ ``` ```text backend-api/ ├── backend-api.client.ts ├── config/ ├── types/ └── index.ts ``` ## Структура Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности. ```text {module-name}/ ├── {module-name}.factory.ts # фабрика (для business-модулей) ├── {module-name}.tsx # корневой файл модуля (опционален) ├── ui/ # компоненты модуля ├── parts/ # вложенные модули ├── hooks/ # хуки ├── stores/ # сторы состояния ├── services/ # внешние источники данных ├── mappers/ # трансформация данных между форматами ├── types/ # типы ├── styles/ # стили ├── lib/ # утилиты модуля ├── config/ # константы и конфигурация └── index.ts # публичный API ``` Подробное описание сегментов — в разделе [Сегменты](/architecture/segments). ## Публичный API Внешний код импортирует модуль только через публичный API. ```ts // Хорошо import { customerFactory } from '@/business/customer' import type { Customer } from '@/business/customer' ``` ```ts // Плохо import { validateToken } from '@/business/auth/lib/tokens' ``` `index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи. Внутренние сегменты модуля остаются деталями реализации. Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика. ```ts // business/customer/index.ts export { customerFactory } from './customer.factory' export type { Customer } from './types/customer.type' export type { CustomerApi } from './types/customer-api.type' export type { CustomerDeps } from './types/customer-deps.type' export type { CustomerFactory } from './types/customer-factory.type' ``` ## Фабрика Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля. Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика. Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type` — `import type` не является runtime-зависимостью. Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция». ### Примеры Пример реализации фабрики в React см. в [Создание фабрики](/examples/react/factory). Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](/examples/react/factory-composition). Пример композиции фабрик через React Provider см. в [Композиция через Provider](/examples/react/composition-provider). ## Жизненный цикл Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности. * Нужен на одной странице → `screens/{name}/parts/` * Появился в 2+ местах → поднимается по природе: * абстрактный UI → `ui/` * блок с данными/логикой → `widgets/` * представление бизнес-домена → `business/{area}/parts/` Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. --- --- url: /slm-design/architecture/segments.md description: >- Сегменты внутри модуля (ui/, model/, lib/ и др.), назначение и правила размещения файлов --- # Сегменты Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит. ## Определение **Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.** ## Обзор | Сегмент | Содержимое | |---------|------------| | `ui/` | Презентационные компоненты родительского модуля | | `parts/` | Вложенные модули со своими сегментами | | `hooks/` | React-хуки | | `stores/` | Сторы состояния | | `services/` | Работа с внешними источниками данных | | `mappers/` | Трансформация данных между форматами | | `types/` | TypeScript-типы и интерфейсы | | `styles/` | Стили | | `lib/` | Утилиты и хелперы модуля | | `config/` | Константы и конфигурация | ## Сегмент ui/ Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля. Компонент в `ui/`: * Находится в собственной папке. * Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`. * Не содержит любые компоненты. * Не содержит любые модули. * Не импортирует код проекта за пределами родительского модуля. * Не делает внешние запросы. * Не вызывает сценарные хуки. * Не получает данные самостоятельно, не выбирает источник данных и не композирует данные. * Не содержит бизнес-логику или сценарную логику. Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](/architecture/modules#компонент). Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`. ```text user/ ├── ui/ │ ├── user-avatar/ │ │ ├── user-avatar.tsx │ │ ├── styles/ │ │ │ └── user-avatar.module.css │ │ ├── types/ │ │ │ └── user-avatar-props.type.ts │ │ └── index.ts │ └── user-status/ │ ├── user-status.tsx │ └── index.ts ├── types/ ├── hooks/ ├── user.tsx └── index.ts ``` Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `ui/`, а модуль в `parts/`. ## Сегмент parts/ Вложенные модули со своими сегментами. `parts/` содержит только модули: каждый элемент `parts/` — папка полноценного модуля с собственным публичным API. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не размещаются. ```text home/ ├── parts/ │ ├── hero-section/ │ │ ├── hero-section.tsx │ │ ├── styles/ │ │ ├── parts/ │ │ │ └── top-banner/ │ │ │ ├── top-banner.tsx │ │ │ └── index.ts │ │ └── index.ts │ └── features-section/ │ ├── features-section.tsx │ ├── hooks/ │ └── index.ts ├── home.screen.tsx └── index.ts ``` Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — компонент родительского модуля без собственной архитектурной ответственности. Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке. Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше. ## Сегмент hooks/ React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты. ```text hooks/ ├── use-auth.hook.ts ├── use-session.hook.ts └── use-permissions.hook.ts ``` ## Сегмент stores/ Сторы состояния модуля. Конкретная реализация зависит от выбранного стейт-менеджера (Zustand, MobX, Redux и т.д.). ```text stores/ ├── auth.store.ts └── session.store.ts ``` ## Сегмент services/ Работа с внешними источниками данных: API-вызовы, запросы, подписки. ```text services/ ├── auth.service.ts └── token.service.ts ``` ## Сегмент mappers/ Функции трансформации данных из одного формата в другой: DTO в доменный тип, доменный тип в DTO, доменный тип в ViewModel. ```text mappers/ ├── map-user.ts ├── map-product.ts └── map-order-to-dto.ts ``` ## Сегмент types/ TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов. ```text types/ ├── user.type.ts └── session.type.ts ``` ## Сегмент styles/ Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.). ```text styles/ ├── auth.module.css └── login-form.module.css ``` ## Сегмент lib/ Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов. ```text lib/ ├── validate-email.ts └── format-phone.ts ``` Отличие от `shared/lib/`: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в `shared/lib/`. ## Сегмент config/ Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения. ```text config/ ├── routes.ts └── constants.ts ``` --- --- url: /slm-design/architecture/monorepo.md description: >- Правила применения SLM Design для frontend-проектов, находящихся в монорепозитории --- # Монорепозитории Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов. ## Определение **Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв `ui`, `infra` и `shared`.** ## Базовая структура Каждое приложение внутри `apps/` сохраняет собственную SLM-структуру в `src/`. ```text 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.ts ``` `apps/{app}/src` — граница SLM-приложения. `packages/*` находятся выше SLM и не добавляют новые архитектурные слои. ## Группировка frontend-пакетов Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: `ui`, `infra`, `shared`. ```text 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 и другого фундаментального кода без привязки к конкретному приложению. ```text packages/ui/button/ packages/ui/modal/ packages/infra/theme/ packages/infra/backend-api/ packages/shared/ ``` ## Что остаётся в приложении Слои `app`, `layouts`, `screens`, `widgets` и `business` остаются внутри конкретного приложения. ```text 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-модули. ```text packages/ui/button/ ├── package.json └── src/ ├── button.tsx ├── styles/ ├── types/ └── index.ts ``` UI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц. ## Infra-пакеты В `packages/infra/*` размещаются переиспользуемые инфраструктурные модули. ```text packages/infra/backend-api/ ├── package.json └── src/ ├── clients/ ├── config/ ├── types/ └── index.ts ``` Привязанные к конкретному приложению сервисы остаются в `apps/{app}/src/infra`. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок. ## Shared-пакет `packages/shared` является единым пакетом. ```text packages/shared/ ├── package.json └── src/ ├── lib/ ├── helpers/ └── index.ts ``` В `packages/shared` сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте. Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий `shared` не выносятся. ## Имена пакетов и импорты Путь импорта задаётся `name` в `package.json`, а не расположением директории. ```json { "name": "@repo/theme" } ``` ```text packages/infra/theme/package.json ``` ```ts import { ThemeProvider } from '@repo/theme' ``` Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены. ```ts // Хорошо import { Button } from '@repo/button' // Плохо import { Button } from '@repo/button/src/button' ``` ## Зависимости На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений. ```text apps → packages packages -/→ apps ``` Внутри приложения продолжает действовать обычное направление зависимостей SLM. ```text app → [ layouts | screens ] → widgets → business → infra → ui → shared ``` Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях. ## Когда не выносить Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API. Фактическое использование в одном приложении не запрещает пакет, если модуль имеет общую природу и потенциально нужен нескольким приложениям. ```text # Плохо 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/*`. --- --- url: /slm-design/examples/react/factory.md description: Пример создания фабрики business-модуля в React-проекте --- # Создание фабрики Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API. ## Структура business-модуля Фабрика лежит в корне business-модуля. Типы публичного API и зависимостей размещаются в `types/`. ```text business/customer/ ├── customer.factory.ts ├── hooks/ ├── types/ │ ├── customer.type.ts │ ├── customer-api.type.ts │ └── customer-factory.type.ts ├── ui/ └── index.ts ``` ## Тип публичного API Публичный API описывает runtime-возможности, которые модуль отдаёт потребителям: хуки, компоненты и сценарные методы. ```ts // business/customer/types/customer-api.type.ts import type { ReactNode } from 'react' import type { Customer } from './customer.type' export type CustomerCardProps = { customer: Customer } export type CustomerApi = { useCustomer: () => Customer | null CustomerCard: (props: CustomerCardProps) => ReactNode } ``` ```ts // business/customer/types/customer-factory.type.ts import type { CustomerApi } from './customer-api.type' export type CustomerFactory = () => CustomerApi ``` ## Фабрика без зависимостей Если модулю не нужны другие домены в runtime, фабрика создаётся без аргументов. ```ts // business/customer/customer.factory.ts import { useCustomer } from './hooks/use-customer.hook' import { CustomerCard } from './ui/customer-card' import type { CustomerFactory } from './types/customer-factory.type' export const customerFactory: CustomerFactory = () => { return { useCustomer, CustomerCard, } } ``` ```ts // business/customer/index.ts export { customerFactory } from './customer.factory' export type { Customer } from './types/customer.type' export type { CustomerApi } from './types/customer-api.type' export type { CustomerFactory } from './types/customer-factory.type' ``` ## Фабрика с зависимостями Если модулю нужен другой домен в runtime, зависимость передаётся аргументом фабрики. Тип зависимости описывает только нужную часть API. ```ts // business/order/types/order-deps.type.ts import type { CustomerApi } from '@/business/customer' export type OrderDeps = { customer: Pick } ``` ```ts // business/order/types/order-factory.type.ts import type { OrderApi } from './order-api.type' import type { OrderDeps } from './order-deps.type' export type OrderFactory = (deps: OrderDeps) => OrderApi ``` ```ts // business/order/order.factory.ts import { createUseOrder } from './hooks/use-order.hook' import { OrderCard } from './ui/order-card' import type { OrderFactory } from './types/order-factory.type' export const orderFactory: OrderFactory = (deps) => { const useOrder = createUseOrder(deps) return { useOrder, OrderCard, } } ``` --- --- url: /slm-design/examples/react/factory-composition.md description: Пример композиции business-фабрик на уровне screen-модуля в React-проекте --- # Композиция фабрик Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов. ## Идея Композиция фабрик выполняется в модуле-потребителе: screen, layout или другом модуле группы «Композиция». Business-модули не импортируют runtime-код друг друга напрямую, а cross-domain зависимости получают только через аргументы фабрик. ## Структура screen-модуля ```text screens/home/ ├── home.screen.tsx └── index.ts ``` ## Сборка фабрик Файл: `screens/home/home.screen.tsx`. ```tsx import { customerFactory } from '@/business/customer' import { orderFactory } from '@/business/order' const customer = customerFactory() const order = orderFactory({ customer }) const { useOrder, OrderCard } = order export const HomeScreen = () => { const currentOrder = useOrder() return } ``` `customerFactory` создаётся первой, потому что `orderFactory` зависит от части API домена `customer`. Модуль `order` не импортирует `customer` в runtime — зависимость передаётся снаружи. ## Публичный API screen-модуля Файл: `screens/home/index.ts`. ```ts export { HomeScreen } from './home.screen' ``` Screen экспортирует только собственный публичный API. Собранные экземпляры business API остаются деталями реализации screen-модуля. --- --- url: /slm-design/examples/react/composition-provider.md description: Пример композиции бизнес-фабрик screen-модуля через React Provider --- # Композиция через Provider Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя. ## Идея Screen получает готовый API бизнес-доменов через React Context. Граф фабрик собирается снаружи, например в роутере, а внутренние `parts/` достают нужные домены через хук без пропс-дриллинга. ## Принципы 1. **Принадлежность.** Provider, Context и хук принадлежат конкретному screen-модулю и лежат в его сегментах. 2. **Внутренний тип.** Тип композиции не экспортируется наружу — это закрывает доступ из бизнес-модулей. 3. **Внутренний хук.** Хук доступа не экспортируется — доступен только внутри screen и его `parts/`. 4. **Публичный Provider.** Только Provider экспортируется через `index.ts`, чтобы роутер мог обернуть screen. 5. **Сборка снаружи.** Граф фабрик собирается в роутере или другом композиторе, screen фабрики не вызывает. 6. **Запрет для бизнеса.** Бизнес-модули не используют провайдеры композиции. Cross-domain зависимости передаются только через аргументы фабрики. ## Структура модуля ```text screens/main/ ├── main.screen.tsx ├── providers/ │ └── main-composition.provider.tsx ├── hooks/ │ └── use-main-composition.hook.ts ├── types/ │ └── main-composition.type.ts ├── parts/ │ └── featured-products/ │ ├── featured-products.tsx │ └── index.ts └── index.ts ``` Сегмент `providers/` — расширение стандартного набора SLM. Спецификация это разрешает: команда сама определяет, какие сегменты используются. ## Распределение по сегментам | Файл | Сегмент | Назначение | |------|---------|------------| | `main-composition.type.ts` | `types/` | TypeScript-тип композиции | | `main-composition.provider.tsx` | `providers/` | Context и Provider-компонент | | `use-main-composition.hook.ts` | `hooks/` | React-хук доступа | | `main.screen.tsx` | корень | Корневой компонент screen-модуля | | `featured-products/` | `parts/` | Вложенный модуль со своим публичным API | ## Тип композиции Файл: `screens/main/types/main-composition.type.ts`. Тип описывает, какие бизнес-домены доступны на этой странице. Он не экспортируется через `index.ts`, чтобы другие модули не зависели от внутренней формы композиции screen. ```ts import type { CatalogApi } from '@/business/catalog' import type { CartApi } from '@/business/cart' export type MainComposition = { catalog: CatalogApi cart: CartApi } ``` ## Context и Provider Файл: `screens/main/providers/main-composition.provider.tsx`. Context — внутренняя деталь Provider, наружу он не экспортируется. Значение `null` по умолчанию нужно, чтобы хук мог проверить отсутствие Provider в дереве. Provider-компонент экспортируется через `index.ts`. Роутер передаёт в `value` уже собранный граф фабрик со стабильной ссылкой. ```tsx import { createContext, type ReactNode } from 'react' import type { MainComposition } from '../types/main-composition.type' export const MainCompositionContext = createContext(null) type Props = { value: MainComposition children: ReactNode } export const MainCompositionProvider = ({ value, children }: Props) => ( {children} ) ``` ## Хук доступа Файл: `screens/main/hooks/use-main-composition.hook.ts`. Хук остаётся внутренним и не экспортируется через `index.ts` модуля. Он доступен только внутри screen и его `parts/`. Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева. ```ts import { useContext } from 'react' import { MainCompositionContext } from '../providers/main-composition.provider' export const useMainComposition = () => { const ctx = useContext(MainCompositionContext) if (!ctx) { throw new Error('useMainComposition must be used within MainCompositionProvider') } return ctx } ``` ## Сборка графа в роутере Файл: `app/router.tsx`. Роутер или другой композитор собирает граф фабрик в точке использования screen. Каждый домен получает свои зависимости через аргументы фабрики. Фабрики вызываются вне React-компонента, если не зависят от runtime-параметров. Так API доменов не пересоздаётся на каждый рендер route-компонента. ```tsx import { MainScreen, MainCompositionProvider } from '@/screens/main' import { catalogFactory } from '@/business/catalog' import { cartFactory } from '@/business/cart' import { authFactory } from '@/business/auth' const auth = authFactory() const catalog = catalogFactory() const cart = cartFactory({ auth }) const MainRoute = () => ( ) ``` ## Корневой компонент screen Файл: `screens/main/main.screen.tsx`. Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные `useCategories` и `CategoryList`, а не обращение к фабричному API через точку. ```tsx import { useMainComposition } from './hooks/use-main-composition.hook' import { FeaturedProducts } from './parts/featured-products' export const MainScreen = () => { const { catalog } = useMainComposition() const { useCategories, CategoryList } = catalog const categories = useCategories() return (
) } ``` ## Вложенный part Файл: `screens/main/parts/featured-products/featured-products.tsx`. Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props. Из API доменов достаются готовые сущности: `useFeatured`, `ProductCard` и `addItem`. Компонент работает с ними напрямую. ```tsx import { useMainComposition } from '../../hooks/use-main-composition.hook' export const FeaturedProducts = () => { const { catalog, cart } = useMainComposition() const { useFeatured, ProductCard } = catalog const { addItem } = cart const products = useFeatured() return (
{products.map((product) => ( addItem(product.id)} /> ))}
) } ``` Файл: `screens/main/parts/featured-products/index.ts`. ```ts export { FeaturedProducts } from './featured-products' ``` ## Публичный API screen-модуля Файл: `screens/main/index.ts`. Наружу экспортируются только screen и его Provider. `MainComposition`, `MainCompositionContext` и `useMainComposition` остаются деталями реализации. ```ts export { MainScreen } from './main.screen' export { MainCompositionProvider } from './providers/main-composition.provider' ``` ## Почему тип композиции не экспортируется Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen. Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen. ```ts import type { MainComposition } from '@/screens/main' ``` Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля. ## Почему хук не экспортируется Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его `parts/`. ## Почему Provider экспортируется Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева. ## Стабильность value Фабрики создаются на уровне модуля, поэтому `catalog` и `cart` сохраняют ссылки между рендерами `MainRoute`. Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию. ## Расширение на другие screen-модули Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов. ```text screens/checkout/providers/checkout-composition.provider.tsx screens/checkout/hooks/use-checkout-composition.hook.ts screens/checkout/types/checkout-composition.type.ts ``` Имена включают имя screen-модуля. Не используйте универсальные названия вроде `useComposition` или `useScope`: по имени файла должно быть понятно, к какой странице привязан Context.