mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
chore: init
This commit is contained in:
12
docs/README.md
Normal file
12
docs/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# SLM Design
|
||||
|
||||
`docs/` - файлы документации по SLM-архитектуре.
|
||||
|
||||
Используй эту документацию как источник истины для задач по SLM Design: слоям, модулям, сегментам, публичному API, business factory, composition modules и монорепозиториям.
|
||||
|
||||
## Структура
|
||||
|
||||
- `canons/` - основные каноны SLM Design.
|
||||
- `examples/` - дополнительные примеры реализации.
|
||||
|
||||
Точка входа: `canons/index.md`.
|
||||
331
docs/canons/business-factory.md
Normal file
331
docs/canons/business-factory.md
Normal file
@@ -0,0 +1,331 @@
|
||||
---
|
||||
title: Business-фабрика
|
||||
description: Контракт business-модуля, runtime-зависимости, logic API, доменные ошибки и место сборки фабрик
|
||||
---
|
||||
|
||||
# Business-фабрика
|
||||
|
||||
Раздел фиксирует архитектурный контракт business-модуля: фабрика описывает доменный logic API приложения и не знает о конкретном backend, SDK, storage, state/query runtime, browser API или React tree.
|
||||
|
||||
## Главный принцип
|
||||
|
||||
Business-модуль описывает домен приложения, а не форму backend API, SDK, storage, external hook, state manager или внешнего сервиса.
|
||||
|
||||
Фабрика должна отвечать на вопрос: какой API нужен приложению для работы с доменом. Она не должна отвечать на вопрос: какие методы есть у конкретного REST-клиента, SDK или browser API.
|
||||
|
||||
## Когда создавать business-модуль
|
||||
|
||||
Полный контракт business-модуля — фабрика, `deps`, доменные типы, доменные ошибки, adapters, сборка в `compositions/business/{domain}`, обязательные factory/assembly tests и colocated tests для внутренней runtime-safe логики. Он применяется к любому модулю, созданному в `business`.
|
||||
|
||||
Создавай business-модуль, когда выполняется хотя бы одно условие:
|
||||
|
||||
- сценарии нужны нескольким страницам, composition modules или другим доменам;
|
||||
- у логики есть собственная доменная модель и доменные ошибки, которые нельзя выразить типами внешнего API;
|
||||
- доменный сценарий зависит от внешних runtime-capabilities (backend, SDK, product storage, source hook, domain store, event, browser API), которые нужно изолировать за `deps`;
|
||||
- домен нужно тестировать независимо от UI и конкретного backend.
|
||||
|
||||
Не создавай business-модуль заранее, если логика нужна одной странице, не имеет доменной модели, product I/O и domain state. Presentation-only store или browser interaction, принадлежащие UI scope, сами по себе не создают business-домен. Такая page-local orchestration живёт в соответствующем composition module.
|
||||
|
||||
Наличие product I/O отменяет page-local исключение. Даже если источник нужен одной странице, consumer composition не обращается к нему напрямую: объяви business-контракт, dependency adapter и domain errors.
|
||||
|
||||
«Облегчённого» business-модуля не существует: если модуль создан в `business/`, контракт применяется целиком. Подъём вызревшей логики из composition module в business-модуль — обычный рефакторинг: объяви доменные типы и `deps`, перенеси сценарии в services и hooks, собери фабрику и покрой её factory-level тестами.
|
||||
|
||||
## Обязательные правила
|
||||
|
||||
- Фабрика лежит в корне business-модуля: `business/{domain}/{domain}.factory.ts`.
|
||||
- Фабрика принимает runtime-зависимости только через `deps`.
|
||||
- Фабрика возвращает только logic API домена: hooks, selectors, command/query methods, scenario services.
|
||||
- Фабрика не возвращает React-компоненты, layouts, guards, boundaries, providers или page-level wrappers.
|
||||
- Business-модуль не содержит React-компоненты. UI-решения домена размещаются в `compositions`; полностью универсальные UI-контролы размещаются в `ui`.
|
||||
- Business-модуль не импортирует реальные SDK, generated operations, HTTP-клиенты, storage, env, browser API или composition-сборку.
|
||||
- Business-модуль не импортирует React state/effect runtime, SWR/query runtime, Zustand/Redux/MobX store runtime или event bus implementation.
|
||||
- Source/query hooks, domain state stores, subscriptions, clock, random и technical services передаются через business-owned `deps`.
|
||||
- Public contract business-модуля использует собственные доменные типы, а не DTO внешнего API.
|
||||
- `index.ts` business-модуля экспортирует только фабрику и type-only экспорты. Исключений нет.
|
||||
- Runtime-сборка business-фабрики выполняется вне `business`, обычно в `compositions/business/{domain}`.
|
||||
- Для каждой runtime-capability создаётся явный dependency adapter. Adapter не пишется inline внутри builder.
|
||||
- Из public API business выходят только собственные domain errors со стабильным `code`.
|
||||
- Factory-level и assembly tests обязательны и входят в критерий завершения модуля.
|
||||
|
||||
## Проектирование фабрики
|
||||
|
||||
Проектирование начинается со сценариев домена, а не с API-клиента.
|
||||
|
||||
Порядок работы:
|
||||
|
||||
1. Опиши публичные сценарии, которые нужны приложению.
|
||||
2. Опиши доменные типы, которыми должен оперировать UI и другие business-модули.
|
||||
3. Проведи inventory всех runtime-capabilities: data sources, hooks, stores, events, browser APIs, env, clock и cross-domain APIs.
|
||||
4. Опиши минимальные внешние возможности в `{Domain}Deps`.
|
||||
5. Назови внешние возможности бизнес-языком, а не языком backend endpoint'ов и библиотек.
|
||||
6. Опиши domain error codes для каждого публичного сценария.
|
||||
7. Собери фабрику из внутренних services, hook wrappers, mappers и helpers поверх переданных deps.
|
||||
8. Верни наружу только `{Domain}Api`.
|
||||
9. Реализуй каждую dependency отдельным adapter в `compositions/business/{domain}`.
|
||||
|
||||
Правильные вопросы:
|
||||
|
||||
- Не `какой endpoint дернуть?`, а `какой бизнес-сценарий выполняем?`.
|
||||
- Не `какой DTO пришёл?`, а `какую доменную модель отдаём наружу?`.
|
||||
- Не `как называется метод SDK?`, а `какая внешняя возможность нужна домену?`.
|
||||
- Не `как устроен backend сейчас?`, а `какой стабильный контракт нужен приложению?`.
|
||||
|
||||
## Контракт зависимостей
|
||||
|
||||
`{Domain}Deps` описывает не технические инструменты, а возможности, которые нужны домену.
|
||||
|
||||
Правила:
|
||||
|
||||
- имя зависимости отражает бизнес-возможность: `phoneAuth`, `session`, `profile`, `agreements`, `notifications`;
|
||||
- методы внутри зависимости называют сценарное действие без повторения endpoint names: `phoneAuth.requestCode`, `phoneAuth.verifyCode`, `profile.getCurrentUser`, `agreements.saveUserAgreements`;
|
||||
- параметры используют доменные типы business-модуля;
|
||||
- результат внешней границы принимается как `unknown`, если данные требуют runtime-нормализации;
|
||||
- пустой успешный ответ описывается как `Promise<void>`, если body не нужен;
|
||||
- dependency не должна возвращать generated DTO как доменный тип;
|
||||
- mapper, normalizer или domain error не передаётся через `deps`, если это часть доменной логики.
|
||||
- source/query hook описывается business-owned result type, без `SWRConfiguration`, `UseQueryResult` и других library types;
|
||||
- domain state описывается business-owned port, без `StoreApi` и concrete state manager types;
|
||||
- subscription возвращает cleanup function;
|
||||
- недетерминированные clock/random/id capabilities передаются явно;
|
||||
|
||||
Плохо:
|
||||
|
||||
```ts
|
||||
import type { ApiRequestClient, BoundApi } from '@vendor/api-sdk'
|
||||
import type { v1PatientProfileList } from '@vendor/api-sdk/operations'
|
||||
|
||||
type UserApiTree = {
|
||||
patient: {
|
||||
profile: typeof v1PatientProfileList
|
||||
}
|
||||
}
|
||||
|
||||
export type UserDeps = {
|
||||
api: BoundApi<UserApiTree, ApiRequestClient>
|
||||
}
|
||||
```
|
||||
|
||||
Проблема: business-модуль знает про SDK, generated operation и форму внешнего клиента.
|
||||
|
||||
Хорошо:
|
||||
|
||||
```ts
|
||||
import type { AuthState } from './auth-state.type'
|
||||
import type { VerifyPhoneCodeData } from './verify-phone-code-data.type'
|
||||
|
||||
export type AuthDeps = {
|
||||
phoneAuth: {
|
||||
requestCode: (phone: string) => Promise<unknown>
|
||||
resendCode: (challengeId: string) => Promise<unknown>
|
||||
verifyCode: (data: VerifyPhoneCodeData) => Promise<unknown>
|
||||
}
|
||||
session: {
|
||||
setToken: (token?: string | null) => void
|
||||
useToken: () => string | null | undefined
|
||||
}
|
||||
sessionEvents: {
|
||||
onInvalidated: (listener: () => void) => () => void
|
||||
}
|
||||
state: {
|
||||
create: (initialState: AuthState) => {
|
||||
get: () => AuthState
|
||||
set: (state: AuthState) => void
|
||||
useState: () => AuthState
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Конкретный SDK, storage, source hook, store или mock подключается в `compositions/business/auth` через адаптер. Business-модуль знает только о своём `AuthDeps`.
|
||||
|
||||
## Публичный API фабрики
|
||||
|
||||
`{Domain}Api` описывает logic API домена, который фабрика возвращает наружу в runtime.
|
||||
|
||||
Правила:
|
||||
|
||||
- API говорит на языке домена;
|
||||
- API не повторяет endpoint names;
|
||||
- API не отдаёт DTO внешнего сервиса;
|
||||
- API не раскрывает внутренние `create*` services;
|
||||
- API остаётся стабильным при замене backend, SDK или storage;
|
||||
- API может возвращать hooks и selectors, созданные поверх переданных dependency hooks/state ports; business не импортирует concrete hook/store runtime;
|
||||
- API не возвращает React-компоненты, UI-компоненты, layouts, guards, boundaries или page-level wrappers.
|
||||
|
||||
Плохо:
|
||||
|
||||
```ts
|
||||
export type AuthApi = {
|
||||
AuthGuard: ComponentType<AuthGuardProps>
|
||||
LoginButton: ComponentType<LoginButtonProps>
|
||||
useAuth: ReturnType<typeof createAuthHook>
|
||||
}
|
||||
```
|
||||
|
||||
Проблема: factory output смешивает доменную логику с UI и начинает собирать React tree.
|
||||
|
||||
Хорошо:
|
||||
|
||||
```ts
|
||||
export type AuthApi = {
|
||||
requestPhoneCode: ReturnType<typeof createRequestPhoneCode>
|
||||
resendPhoneCode: ReturnType<typeof createResendPhoneCode>
|
||||
verifyPhoneCode: ReturnType<typeof createVerifyPhoneCode>
|
||||
useAuth: ReturnType<typeof createAuthHook>
|
||||
useIsAuthenticated: ReturnType<typeof createIsAuthenticatedHook>
|
||||
startSessionInvalidationTracking: () => () => void
|
||||
}
|
||||
```
|
||||
|
||||
Такой API сообщает composition-слою доменное состояние и действия, но не навязывает UI-решение.
|
||||
|
||||
## Реализация фабрики
|
||||
|
||||
Фабрика связывает внутренние части business-модуля с переданными зависимостями.
|
||||
|
||||
```ts
|
||||
import { createCurrentUserHook } from './hooks/use-current-user.hook'
|
||||
import { createStoredUserAgreementsGetter } from './services/get-stored-user-agreements.service'
|
||||
import { createUpdateCurrentUserProfile } from './services/update-current-user-profile.service'
|
||||
import type { UserFactory } from './types/user-factory.type'
|
||||
|
||||
export const userFactory: UserFactory = (deps) => {
|
||||
const { authApi, profile, storage } = deps
|
||||
|
||||
return {
|
||||
getStoredUserAgreements: createStoredUserAgreementsGetter(storage),
|
||||
updateCurrentUserProfile: createUpdateCurrentUserProfile(profile),
|
||||
useCurrentUser: createCurrentUserHook({ authApi, profile }),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Фабрика не должна:
|
||||
|
||||
- создавать API-клиент;
|
||||
- выбирать backend endpoint;
|
||||
- читать env;
|
||||
- обращаться к browser storage напрямую;
|
||||
- делать запросы при создании API;
|
||||
- импортировать composition-сборку;
|
||||
- подстраивать свой API под конкретный внешний сервис;
|
||||
- создавать или возвращать React-компоненты;
|
||||
- импортировать React state/effect APIs;
|
||||
- импортировать SWR, query library или state manager runtime;
|
||||
- создавать concrete store, query client или event bus;
|
||||
- подписываться на external event без dependency contract и явного cleanup.
|
||||
|
||||
## Runtime-границы и ошибки
|
||||
|
||||
Любая dependency фабрики считается ненадёжной runtime-границей.
|
||||
|
||||
Business-модуль защищает публичный контракт от таких случаев:
|
||||
|
||||
- dependency вернула `null`, `undefined`, пустой body или объект неправильной формы;
|
||||
- dependency вернула rejected promise;
|
||||
- dependency синхронно выбросила исключение;
|
||||
- storage содержит устаревшие или битые данные;
|
||||
- внешний сервис поменял форму ответа без изменения TypeScript-типов.
|
||||
|
||||
Защита выполняется внутри business-модуля через mappers, normalizers, type guards и domain errors. Fallback допустим только для валидного доменного исхода, явно представленного dependency contract, а не для technical failure или malformed response.
|
||||
|
||||
Наружу не должны протекать ошибки SDK, HTTP-клиента, storage, query hook, store, generated API или browser API. Потребитель business API всегда получает только собственную domain error со стабильным `code`, а не `message`, `status`, `response`, `stack` или форму внешней ошибки.
|
||||
|
||||
```ts
|
||||
if (isDomainError<AuthErrorCode>(error) && error.code === 'AUTH_PHONE_CODE_VERIFY_FAILED') {
|
||||
showInvalidCodeMessage()
|
||||
}
|
||||
```
|
||||
|
||||
Business всегда преобразует rejected promise, synchronous throw, source hook error и невалидную успешную структуру в собственную domain error.
|
||||
|
||||
## Сборка business-фабрики
|
||||
|
||||
Реальные runtime-зависимости подключаются в composition module `compositions/business/{domain}`.
|
||||
|
||||
```text
|
||||
src/
|
||||
├── business/
|
||||
│ └── auth/
|
||||
│ ├── auth.factory.ts
|
||||
│ ├── types/
|
||||
│ └── index.ts
|
||||
└── compositions/
|
||||
└── business/
|
||||
└── auth/
|
||||
├── create-auth-business.ts
|
||||
├── adapters/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Если business-домены сгруппированы, сборка повторяет тот же относительный путь: `business/app/auth` соответствует `compositions/business/app/auth`, `business/cms/content` соответствует `compositions/business/cms/content`. Группировка не является default-структурой.
|
||||
|
||||
Правила:
|
||||
|
||||
- `business/{domain}` объявляет фабрику и контракт `deps`.
|
||||
- `compositions/business/{domain}` отдельными adapters адаптирует SDK, storage, infra-клиенты, source hooks, stores, events и другие runtime-capabilities к этому контракту.
|
||||
- Builder явно создаёт или получает scoped runtime instances без I/O, создаёт adapters поверх них, передаёт adapters фабрике и возвращает API; integration logic не пишется inline.
|
||||
- `create{Domain}Business()` возвращает готовый `{Domain}Api`.
|
||||
- Browser/application `create{Domain}Business()` принимает аргументы только для API других уже собранных business-фабрик.
|
||||
- SDK, storage, env, HTTP-клиенты и browser API не передаются в `create{Domain}Business()` как deps.
|
||||
- Если домен не зависит от других business API, `create{Domain}Business()` вызывается без аргументов.
|
||||
- Request-scoped builder отделяет cross-domain API от `requestScopeInput`. В input находятся только framework/request data; concrete client factory импортируется внутри integration module. Request input используется только для создания adapters и не передаётся factory как raw dependency.
|
||||
- Конечный граф API собирается в месте, которое владеет lifecycle: page composition, route composition, provider, request scope или test setup.
|
||||
- `infra` не собирает business-фабрики, потому что не должен импортировать `business`.
|
||||
- `app` не реализует business-сборку, а только подключает готовые composition modules к фреймворку.
|
||||
|
||||
```ts
|
||||
import { createAuthBusiness } from '@/compositions/business/auth'
|
||||
import { createUserBusiness } from '@/compositions/business/user'
|
||||
|
||||
const authApi = createAuthBusiness()
|
||||
const userApi = createUserBusiness({ authApi })
|
||||
```
|
||||
|
||||
Подробный пример см. в [Business composition](../examples/business-composition.md).
|
||||
|
||||
## Public API файла index.ts
|
||||
|
||||
Жёсткое правило без исключений: `business/{domain}/index.ts` экспортирует только фабрику и type-only экспорты.
|
||||
|
||||
```ts
|
||||
export { userFactory } from './user.factory'
|
||||
|
||||
export type { User } from './types/user.type'
|
||||
export type { UserApi } from './types/user-api.type'
|
||||
export type { UserDeps } from './types/user-deps.type'
|
||||
export type { UserFactory } from './types/user-factory.type'
|
||||
export type { UserErrorCode } from './types/user-error-code.type'
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
Business-модуль тестируется через public API фабрики. Factory-level тесты обязательны, импортируют модуль только через `business/{domain}` и не используют deep imports во внутренние `services`, `hooks`, `mappers` или `lib`.
|
||||
|
||||
Сборка в `compositions/business/{domain}` обязательно тестируется отдельно: эти тесты проверяют adapters, корректность передачи deps, отсутствие I/O при создании API и lifecycle cleanup, но не заменяют сценарные тесты business-модуля.
|
||||
|
||||
Colocated unit tests обязательны там, где есть mappers, normalizers, type guards, domain errors или другая внутренняя runtime-safe логика. Они являются дополнительным уровнем, а не заменой factory-level и assembly tests.
|
||||
|
||||
Подробный пример см. в [Business testing](../examples/business-testing.md).
|
||||
|
||||
## Чеклист
|
||||
|
||||
- Фабрика лежит в корне business-модуля.
|
||||
- Фабрика принимает все runtime-зависимости через `deps`.
|
||||
- В `deps` нет SDK, generated operations, HTTP-клиентов, backend DTO, StoreApi и query-library types.
|
||||
- Контракты зависимостей названы бизнес-языком.
|
||||
- Source/query hooks, stores, events и platform APIs переданы через `deps`.
|
||||
- Business не импортирует React/SWR/query/store runtime.
|
||||
- Все public types принадлежат business-модулю.
|
||||
- Внешние ответы нормализуются перед попаданием в public API.
|
||||
- Все source errors без исключений заменяются собственными domain errors.
|
||||
- Public API фабрики не повторяет внешний API.
|
||||
- Public API фабрики не возвращает React-компоненты.
|
||||
- Business-модуль не содержит React-компоненты.
|
||||
- `index.ts` экспортирует только фабрику и type-only экспорты, без исключений.
|
||||
- Runtime-сборка живёт в `compositions/business/{domain}`.
|
||||
- Для каждой runtime-capability существует private adapter.
|
||||
- Builder не содержит inline integration logic.
|
||||
- Business-модуль можно подключить к другому backend через адаптер без изменения фабрики.
|
||||
- Factory-level и assembly tests созданы и выполняются.
|
||||
293
docs/canons/business-runtime-boundary.md
Normal file
293
docs/canons/business-runtime-boundary.md
Normal file
@@ -0,0 +1,293 @@
|
||||
---
|
||||
title: Runtime-граница business
|
||||
description: Строгая изоляция business-фабрики от источников данных, state/query runtime, infra, browser API и внешних ошибок
|
||||
---
|
||||
|
||||
# Runtime-граница business
|
||||
|
||||
## Главный инвариант
|
||||
|
||||
Business-модуль выполняет доменную композицию только над:
|
||||
|
||||
- собственными типами и детерминированной логикой;
|
||||
- capabilities, переданными фабрике через `{Domain}Deps`.
|
||||
|
||||
Business не вызывает runtime-возможность, если она не была передана фабрике. Это относится не только к данным и `infra`, но и к hooks, stores, subscriptions, browser API и другим concrete runtime-механизмам.
|
||||
|
||||
Фабрика отвечает на вопрос «какой стабильный доменный API нужен приложению», а не «какими библиотеками и источниками он реализован».
|
||||
|
||||
## Что разрешено внутри business
|
||||
|
||||
Business может напрямую использовать:
|
||||
|
||||
- собственные domain types;
|
||||
- собственные services, mappers, normalizers, validators и type guards;
|
||||
- собственные domain errors;
|
||||
- детерминированные вычисления без I/O, runtime-state и скрытого окружения;
|
||||
- чистые библиотеки вроде schema validators, decimal/date utilities, если результат определяется только явными аргументами и типы библиотеки не становятся public contract;
|
||||
- type-only контракты других business API для cross-domain dependencies.
|
||||
|
||||
## Что передаётся через deps
|
||||
|
||||
Через `{Domain}Deps` передавай любую runtime-capability:
|
||||
|
||||
| Capability | Примеры concrete implementation |
|
||||
|---|---|
|
||||
| Product source | REST SDK, GraphQL client, CMS, storage |
|
||||
| Source/query hook | SWR, TanStack Query, Apollo hook |
|
||||
| Domain state runtime | Zustand, Redux, MobX, RxJS store |
|
||||
| Technical service | logger, telemetry, notifications, i18n engine |
|
||||
| Platform API | `window`, navigation, clipboard, geolocation |
|
||||
| Lifecycle event | unauthorized event, socket event, subscription |
|
||||
| Environment | env/config provider, feature runtime configuration |
|
||||
| Nondeterminism | clock, timer, random, ID generator |
|
||||
| Cross-domain behavior | ограниченный API другой business-фабрики |
|
||||
|
||||
Не импортируй concrete implementation в business даже в том случае, если библиотека используется только в одном внутреннем файле.
|
||||
|
||||
## Запрещённые imports
|
||||
|
||||
Production-код `business/**` не импортирует напрямую ни runtime values, ни types из concrete runtimes:
|
||||
|
||||
- `infra`, `compositions`, `app`;
|
||||
- SDK, generated operations и HTTP clients;
|
||||
- storage implementation;
|
||||
- React state/effect APIs;
|
||||
- SWR, TanStack Query, Apollo и другие query runtimes;
|
||||
- Zustand, Redux, MobX, RxJS stores;
|
||||
- browser и framework runtime APIs;
|
||||
- event bus implementation;
|
||||
- env и process-specific configuration.
|
||||
|
||||
Запрет нельзя обойти через `import type`, alias, barrel, type cast или helper в `shared`. Type-only разрешены собственные contracts, детерминированный `shared`, чистые libraries и суженные public API других business-доменов.
|
||||
|
||||
## Доменный шлюз данных
|
||||
|
||||
Business API является единственной продуктовой границей для потребительского кода.
|
||||
|
||||
```text
|
||||
page / layout / screen / widget
|
||||
→ {Domain}Api
|
||||
→ business scenario
|
||||
→ {Domain}Deps
|
||||
→ private dependency adapter
|
||||
→ infra client / SDK / storage / external source
|
||||
```
|
||||
|
||||
Внешний сервис остаётся физическим источником данных. Business является единственным источником доменной истины: он определяет модель, сценарий, нормализацию, fallback и ошибки.
|
||||
|
||||
Обычный consumer composition не создаёт параллельный продуктовый контракт поверх DTO или client. Единственная зона concrete product integration внутри `compositions` — `compositions/business/{domain}`.
|
||||
|
||||
## Business-owned deps
|
||||
|
||||
`{Domain}Deps` принадлежит business-модулю и описывает необходимые возможности доменным языком.
|
||||
|
||||
Требования:
|
||||
|
||||
- группируй методы по capability, а не по имени SDK/client;
|
||||
- принимай доменные аргументы;
|
||||
- принимай внешние результаты как `unknown`, если нужна runtime-проверка;
|
||||
- описывай собственную минимальную форму dependency hook/result;
|
||||
- описывай собственный state port, а не `StoreApi` конкретной библиотеки;
|
||||
- возвращай cleanup из subscription capability;
|
||||
- не передавай mapper, normalizer или domain error через deps;
|
||||
- не используй generated DTO как доменную модель;
|
||||
- не передавай целый client, если домену нужны два конкретных действия.
|
||||
|
||||
Плохо:
|
||||
|
||||
```ts
|
||||
import type { StoreApi } from 'zustand'
|
||||
import type { AdminApiClient } from '@vendor/admin-sdk'
|
||||
|
||||
export type AuthDeps = {
|
||||
api: AdminApiClient
|
||||
store: StoreApi<AuthState>
|
||||
}
|
||||
```
|
||||
|
||||
Хорошо:
|
||||
|
||||
```ts
|
||||
import type { AuthState } from './auth-state.type'
|
||||
import type { VerifyPhoneCodeData } from './verify-phone-code-data.type'
|
||||
|
||||
export type SourceHookResult = {
|
||||
data: unknown
|
||||
error: unknown
|
||||
isLoading: boolean
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
export type AuthDeps = {
|
||||
phoneAuth: {
|
||||
requestCode: (phone: string) => Promise<unknown>
|
||||
resendCode: (challengeId: string) => Promise<unknown>
|
||||
verifyCode: (data: VerifyPhoneCodeData) => Promise<unknown>
|
||||
}
|
||||
session: {
|
||||
setToken: (token?: string | null) => void
|
||||
useToken: () => string | null | undefined
|
||||
}
|
||||
sessionEvents: {
|
||||
onInvalidated: (listener: () => void) => () => void
|
||||
}
|
||||
state: {
|
||||
create: (initialState: AuthState) => {
|
||||
get: () => AuthState
|
||||
set: (state: AuthState) => void
|
||||
useState: () => AuthState
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency adapters
|
||||
|
||||
Adapter находится снаружи business и реализует конкретную часть `{Domain}Deps`.
|
||||
|
||||
```text
|
||||
compositions/business/auth/
|
||||
├── create-auth-business.ts
|
||||
├── adapters/
|
||||
│ ├── admin-auth-session.adapter.ts
|
||||
│ ├── browser-auth-navigation.adapter.ts
|
||||
│ ├── zustand-auth-state.adapter.ts
|
||||
│ └── admin-auth-session-events.adapter.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Правила adapter:
|
||||
|
||||
- импортирует type-only business contract;
|
||||
- импортирует concrete infra/runtime implementation;
|
||||
- преобразует доменные аргументы в transport arguments;
|
||||
- возвращает raw/unknown runtime result, если business должен его проверить;
|
||||
- пробрасывает source error без создания domain error;
|
||||
- не реализует бизнес-правило;
|
||||
- не выбирает доменный fallback;
|
||||
- не экспортируется через public API integration module;
|
||||
- тестируется на wiring, payload и соответствие dependency contract.
|
||||
|
||||
Каждая runtime-capability получает явный adapter. Не скрывай несколько разных integrations как inline-функции внутри builder.
|
||||
|
||||
## Dependency hooks
|
||||
|
||||
Если business должен предоставить hook, concrete hook передаётся через deps.
|
||||
|
||||
```text
|
||||
SWR/TanStack hook
|
||||
→ dependency adapter
|
||||
→ business-owned source hook contract
|
||||
→ business wrapper hook
|
||||
→ domain result / domain error
|
||||
```
|
||||
|
||||
Business wrapper может:
|
||||
|
||||
- вызвать переданный dependency hook;
|
||||
- нормализовать `data` в доменную модель;
|
||||
- заменить source error доменной ошибкой;
|
||||
- вычислить domain state и selectors;
|
||||
- вернуть собственный стабильный result type.
|
||||
|
||||
Dependency hook обязан быть non-throwing и non-Suspense: technical failure возвращается в `error: unknown`, а не выбрасывается во время вызова hook. Business wrapper обязан заменить этот `error` собственной domain error. Публичные callbacks dependency result, например `refresh`, также оборачиваются business и не могут выдать source error наружу.
|
||||
|
||||
Business wrapper не импортирует query library. В public contract не протекают `SWRConfiguration`, `UseQueryResult`, query keys, cache implementation или library-specific mutate API без отдельного domain contract.
|
||||
|
||||
## Domain state
|
||||
|
||||
Business владеет:
|
||||
|
||||
- моделью доменного state;
|
||||
- допустимыми переходами;
|
||||
- commands и selectors;
|
||||
- реакцией на dependency results и events.
|
||||
|
||||
Business не владеет concrete state manager. Business выбирает initial domain state и передаёт его в `deps.state.create(initialState)`; adapter только создаёт concrete store с переданным значением.
|
||||
|
||||
Zustand/Redux/MobX adapter реализует business-owned state adapter factory и создаётся в `compositions/business/{domain}`. Business-фабрика получает adapter factory через `deps`, передаёт initial domain state и получает concrete port.
|
||||
|
||||
Локальный UI-state является другим случаем. Состояние раскрытия sidebar, выбранной вкладки или шага локального UI-flow может использовать concrete state manager непосредственно внутри владеющего composition module, если оно не подменяет доменное состояние и product data boundary.
|
||||
|
||||
## Доменные ошибки
|
||||
|
||||
Из любого публичного метода, command, query или hook business-модуля выходят только собственные доменные ошибки.
|
||||
|
||||
Business обязан преобразовать:
|
||||
|
||||
- rejected promise dependency;
|
||||
- synchronous throw dependency;
|
||||
- source hook error;
|
||||
- ошибку store/storage/browser API;
|
||||
- невалидный успешный ответ;
|
||||
- неизвестную runtime-ошибку.
|
||||
|
||||
Domain error содержит стабильный `code`. Исходная ошибка может сохраняться только в `cause` для диагностики.
|
||||
|
||||
Потребитель не ориентируется на:
|
||||
|
||||
- `message` внешней ошибки;
|
||||
- HTTP status;
|
||||
- `response`;
|
||||
- SDK error class;
|
||||
- stack или transport code.
|
||||
|
||||
Adapter не импортирует и не создаёт domain error. Error mapping всегда выполняет business.
|
||||
|
||||
Rejected promise, synchronous throw, source error и malformed response всегда превращаются в domain error. Fallback допустим только для валидного доменного исхода, явно представленного dependency contract, например корректного отсутствия данных. Fallback не используется для поглощения technical failure или contract drift.
|
||||
|
||||
## Чистый builder
|
||||
|
||||
`compositions/business/{domain}/create-{domain}-business.ts` только:
|
||||
|
||||
1. явно создаёт или получает runtime instances нужного lifecycle без I/O;
|
||||
2. создаёт adapters поверх этих instances;
|
||||
3. передаёт adapters и API других доменов в factory;
|
||||
4. возвращает готовый `{Domain}Api`.
|
||||
|
||||
Builder не содержит:
|
||||
|
||||
- inline SDK calls;
|
||||
- `window`/storage operations;
|
||||
- domain mapping;
|
||||
- domain errors;
|
||||
- Zustand/SWR setup вперемешку с другими dependencies;
|
||||
- product request при создании API;
|
||||
- скрытую subscription без cleanup contract.
|
||||
|
||||
Если capability имеет lifecycle, business API предоставляет domain-level `start`/`subscribe` operation, которая использует dependency и возвращает cleanup wrapper. Business преобразует ошибки регистрации, callback и cleanup в собственные domain errors. Builder остаётся без side effects, а владелец scope запускает operation после mount/commit и вызывает cleanup при завершении lifecycle.
|
||||
|
||||
Browser/application builder без cross-domain dependencies вызывается без аргументов. Для request scope integration module определяет отдельный `requestScopeInput` только с framework/request data, например headers, cookies, request ID и abort signal. Concrete client factory импортируется и вызывается внутри integration module. Request input используется только для создания adapters, не передаётся в business как raw client и не экспортируется consumer compositions.
|
||||
|
||||
## Graph и lifecycle
|
||||
|
||||
Per-domain builder не является владельцем полного graph.
|
||||
|
||||
Владелец graph:
|
||||
|
||||
- выбирает application-lifetime composition/route/page/request/test scope;
|
||||
- собирает домены в ацикличном порядке;
|
||||
- создаёт ровно необходимый набор API;
|
||||
- использует точный graph type;
|
||||
- управляет cleanup subscriptions/resources;
|
||||
- не повторяет adapter wiring;
|
||||
- не импортирует raw infra для «досборки» домена.
|
||||
|
||||
Не используй generic `Partial<Business>` с последующим `as Business`. Если scope содержит только Auth, его contract должен обещать только Auth.
|
||||
|
||||
## Cross-domain dependencies
|
||||
|
||||
Один business-домен не импортирует runtime другого домена. Он объявляет нужную capability в своих `Deps` через type-only API другого домена, по возможности суженный `Pick`.
|
||||
|
||||
Готовый API передаётся builder-у при сборке graph:
|
||||
|
||||
```text
|
||||
createAuthBusiness()
|
||||
→ createUserBusiness({ authApi })
|
||||
→ createOrdersBusiness({ userApi })
|
||||
```
|
||||
|
||||
Runtime-цикл означает ошибочную границу доменов. Не скрывай цикл service locator, lazy import или глобальным event bus.
|
||||
|
||||
Подробный контракт фабрики находится в [Business-фабрике](./business-factory.md). Практическая сборка показана в [Business composition](../examples/business-composition.md).
|
||||
216
docs/canons/decision-process.md
Normal file
216
docs/canons/decision-process.md
Normal file
@@ -0,0 +1,216 @@
|
||||
---
|
||||
title: Процесс архитектурного решения
|
||||
description: Обязательный порядок классификации задачи, выбора владельца, слоя, scope и стратегии изменений
|
||||
---
|
||||
|
||||
# Процесс архитектурного решения
|
||||
|
||||
Не изменяй файлы, пока не принято архитектурное решение. Название папки, существующий похожий код и удобный импорт не доказывают правильность размещения.
|
||||
|
||||
## Карточка решения
|
||||
|
||||
Перед реализацией определи:
|
||||
|
||||
| Вопрос | Что зафиксировать |
|
||||
|---|---|
|
||||
| Роль изменения | Framework wiring, продуктовый сценарий, интеграция, композиция интерфейса, технический сервис, UI или чистый фундамент |
|
||||
| Владелец | Домен, route/page scope, composition module, infra-модуль, UI-модуль или локальный consumer |
|
||||
| Данные | Продуктовые данные, техническое состояние, framework input, локальное UI-state или отсутствуют |
|
||||
| Runtime-возможности | Источники данных, hooks, stores, SDK, browser API, events, clock, random, env и другие внешние capabilities |
|
||||
| Место | Приложение или package, слой, модуль, вложенный модуль и сегмент |
|
||||
| Публичная граница | Что действительно нужно экспортировать и кто будет consumer |
|
||||
| Путь данных | От consumer до business API, dependency adapter и конкретного источника |
|
||||
| Lifecycle | Кто создаёт instance, сколько instances допустимо и кто выполняет cleanup |
|
||||
| Стратегия | Локальная правка, новый модуль, новый business-контракт, adapter, перенос или исправление public API |
|
||||
| Проверки | Typecheck, тесты, import graph, public API, lifecycle и архитектурные инварианты |
|
||||
|
||||
Карточку не обязательно выводить пользователю, если решение очевидно. Но агент обязан уметь обосновать каждый пункт до изменения файлов.
|
||||
|
||||
## Сбор контекста
|
||||
|
||||
Перед выбором места:
|
||||
|
||||
1. Прочитай локальные инструкции приложения или package.
|
||||
2. Найди фактическую границу SLM: `src/`, `apps/{app}/src` или другой локальный root.
|
||||
3. Проверь существующие слои, группы и соседние модули. Не создавай новую параллельную структуру без необходимости.
|
||||
4. Проверь aliases, package exports и реальную разрешимость импортов.
|
||||
5. Найди текущих consumers, public API и runtime import graph изменяемой ответственности.
|
||||
6. Проверь существующие templates или generators после архитектурного выбора. Шаблон не принимает решение за SLM.
|
||||
7. Отдельно найди product I/O, hooks, stores, subscriptions, browser API и другие runtime-возможности.
|
||||
8. Проверь, нет ли уже business-домена, которому принадлежит сценарий.
|
||||
|
||||
Не считай неиспользуемый provider, пустой context, тип будущего graph или ссылку на несуществующий домен готовой архитектурой. Решение должно быть достижимо из runtime entry point и иметь реальных consumers.
|
||||
|
||||
## Выбор роли
|
||||
|
||||
Классифицируй ответственность в следующем порядке.
|
||||
|
||||
### Framework wiring
|
||||
|
||||
Если код существует только из-за фреймворка, размести его в `app`:
|
||||
|
||||
- route-файл;
|
||||
- bootstrap;
|
||||
- framework error entry;
|
||||
- подключение глобальных ресурсов;
|
||||
- тонкое подключение готового composition module.
|
||||
|
||||
`app` не реализует продуктовую композицию, business graph, store, provider или экран.
|
||||
|
||||
### Продуктовый сценарий
|
||||
|
||||
Если код определяет пользовательский сценарий, доменную модель, продуктовый state, бизнес-правило, нормализацию внешних данных, error mapping или доменный переход после ошибки, владелец находится в `business/{domain}`.
|
||||
|
||||
Визуальная реакция на готовый domain error принадлежит consumer composition: сообщение, error screen, redirect, retry control и UI fallback выбираются по стабильному доменному `code`.
|
||||
|
||||
Любой новый внешний источник продуктовых данных требует business-контракта. Колокация внешних вызовов в page/screen/widget services не является допустимым упрощением.
|
||||
|
||||
### Интеграция business-домена
|
||||
|
||||
Если код реализует `{Domain}Deps` через SDK, HTTP, storage, browser API, state/query runtime, event bus или другой concrete runtime, размести его в `compositions/business/{domain}`.
|
||||
|
||||
Это интеграционный composition module, а не business-домен и не обычная page/screen/widget composition.
|
||||
|
||||
### Продуктовая композиция
|
||||
|
||||
Если код собирает route/page/layout/screen/widget, управляет UI-state, provider scope или lifecycle готового business graph, размести его в соответствующем composition module.
|
||||
|
||||
Потребительский composition module получает продуктовые данные только через `{Domain}Api`. Он не импортирует product SDK, generated operations, product storage adapter или конкретный источник.
|
||||
|
||||
### Технический сервис
|
||||
|
||||
Если код предоставляет техническую возможность без продуктовой модели и сценариев, размести его в `infra`:
|
||||
|
||||
- HTTP client;
|
||||
- SDK wrapper;
|
||||
- logger;
|
||||
- theme engine;
|
||||
- i18n engine;
|
||||
- telemetry transport;
|
||||
- технический realtime client.
|
||||
|
||||
Composition может использовать технический infra-сервис напрямую, если сервис не становится обходным путём к продуктовым данным. Если capability нужна business, она всё равно передаётся через business-owned `deps` и adapter.
|
||||
|
||||
### Универсальный UI
|
||||
|
||||
Если сущность отображает интерфейс, не знает продуктовый сценарий и применима независимо от конкретной composition, размести её в `ui`.
|
||||
|
||||
### Чистый фундамент
|
||||
|
||||
Если код детерминирован, не имеет runtime-state, не знает продукт и переиспользуется несколькими владельцами, рассмотри `shared`. По умолчанию оставляй код рядом с первым владельцем.
|
||||
|
||||
## Выбор scope
|
||||
|
||||
Выбирай минимальный scope, который полностью владеет ответственностью:
|
||||
|
||||
1. Нужен одному component/module и не имеет самостоятельной ответственности: оставь внутри владельца.
|
||||
2. Нужен как самостоятельная часть одного module: создай nested module в `parts/`.
|
||||
3. Нужен нескольким частям одной page/route ветки: подними в общий composition scope этой ветки.
|
||||
4. Нужен нескольким composition modules и остаётся продуктовой композицией: создай отдельный composition module.
|
||||
5. Является доменным сценарием или product data boundary: создай или расширь `business/{domain}`.
|
||||
6. Является техническим сервисом: создай или расширь `infra/{service}`.
|
||||
7. Является универсальным UI: создай или расширь `ui/{module}`.
|
||||
8. Выноси в package только `ui`, `infra` или `shared` код с реальным вторым consumer либо явно зафиксированным межприложенческим ownership/reuse-контрактом.
|
||||
|
||||
Не поднимай код выше ради короткого импорта. Не создавай `shared`, общий provider, generic business context или package «на будущее».
|
||||
|
||||
## Component, module и group
|
||||
|
||||
Применяй решение последовательно:
|
||||
|
||||
1. Только отображает готовые props и не владеет зависимостями: component в `ui/` родительского module.
|
||||
2. Владеет сценарием, данными, state, dependency, lifecycle или внутренней декомпозицией: самостоятельный module.
|
||||
3. Самостоятельный module, локальный для владельца: nested module в `parts/`.
|
||||
4. Папка только классифицирует конечные modules: group без `index.ts`, state и runtime logic.
|
||||
5. `ui/`, `parts/`, `hooks/`, `types/`, `services/` и другие служебные папки внутри module: segments, а не modules.
|
||||
|
||||
Если component начинает получать данные, выбирать источник, вызывать сценарный hook или управлять процессом, не добавляй логику в component. Измени архитектурную форму сущности.
|
||||
|
||||
## Выбор стратегии
|
||||
|
||||
### Новый продуктовый сценарий
|
||||
|
||||
1. Найди домен-владелец.
|
||||
2. Спроектируй `{Domain}Api`, доменные типы и доменные ошибки.
|
||||
3. Опиши минимальные runtime-capabilities в `{Domain}Deps`.
|
||||
4. Реализуй детерминированную доменную логику.
|
||||
5. Создай отдельные adapters в `compositions/business/{domain}`.
|
||||
6. Собери фабрику чистым builder.
|
||||
7. Подключи API во владельце lifecycle graph.
|
||||
8. Используй API из потребительских compositions.
|
||||
9. Добавь factory-level и assembly tests.
|
||||
|
||||
### Прямой product I/O вне business boundary
|
||||
|
||||
Не расширяй существующее нарушение.
|
||||
|
||||
1. Определи сценарий и домен.
|
||||
2. Перенеси контракт данных в business-owned `Deps`.
|
||||
3. Перенеси нормализацию, fallback и error mapping в business.
|
||||
4. Оставь concrete source call в dependency adapter.
|
||||
5. Замени прямой вызов на `{Domain}Api`.
|
||||
6. Закрой adapter и source details из public API.
|
||||
|
||||
### Новый store или dependency hook
|
||||
|
||||
Сначала определи, является state локальным UI-state или доменным state.
|
||||
|
||||
- State является локальным UI-state, если сбрасывается вместе с UI scope, управляет только представлением и не хранит продуктовый факт или product data cache. Такой state может принадлежать composition module и использовать выбранный state manager внутри владельца.
|
||||
- State является доменным, если выражает продуктовый факт, инвариант, доступен через business API или участвует в бизнес-сценарии.
|
||||
- Доменный state принадлежит business-контракту. Фабрика получает state adapter factory через `deps`, выбирает initial domain state и создаёт concrete port через adapter.
|
||||
- Source/query hook реализуется adapter-ом; business вызывает только dependency hook и возвращает собственный доменный hook/result.
|
||||
|
||||
### Сборка graph
|
||||
|
||||
1. Собери каждый домен отдельным `compositions/business/{domain}` builder.
|
||||
2. Определи DAG cross-domain зависимостей.
|
||||
3. Выбери один явный lifecycle scope: application-lifetime composition, route, page, request или test. Слой `app` только подключает application composition.
|
||||
4. Создавай graph у владельца scope, а не в случайном screen/widget или на module scope без обоснования.
|
||||
5. Передавай consumers точный graph type. Не используй `Partial<Graph>` с приведением к полному типу.
|
||||
6. Для subscriptions, timers и resources зафиксируй cleanup/dispose.
|
||||
|
||||
### Архитектурное ревью
|
||||
|
||||
Проверяй не только пути файлов, но и семантику:
|
||||
|
||||
- business-shaped код вне `business`;
|
||||
- product graph в `infra`;
|
||||
- type-only imports, которые фактически переносят ownership;
|
||||
- provider, который не создаёт и не получает instance от явного владельца;
|
||||
- orphan modules и providers, недостижимые из entry point;
|
||||
- public API, раскрывающий raw store, context, adapter или generated types;
|
||||
- отсутствующие tests обязательного business-контракта.
|
||||
|
||||
## Условия остановки
|
||||
|
||||
Останови реализацию и сначала исправь решение, если:
|
||||
|
||||
- владелец ответственности не определён;
|
||||
- один state или source имеет несколько конкурирующих владельцев;
|
||||
- business требует прямого runtime или type-only import concrete runtime;
|
||||
- graph создаёт runtime-цикл;
|
||||
- lifecycle instance или cleanup не определён;
|
||||
- public API нужен только для обхода границы;
|
||||
- шаблон генерирует архитектуру, противоречащую принятому решению;
|
||||
- изменение требует незапрошенной миграции нескольких независимых областей.
|
||||
|
||||
## Локальные материалы
|
||||
|
||||
Основной процесс достаточен для типового решения. Открывай только материал, который нужен текущей ветке задачи.
|
||||
|
||||
| Ситуация | Материал |
|
||||
|---|---|
|
||||
| Нужна полная карта допустимых файлов, root entries, segments и tests | [Атлас файлов SLM](./file-atlas.md) |
|
||||
| Задача затрагивает product I/O, source hook, domain store, event, lifecycle или external errors | [Runtime-граница business](./business-runtime-boundary.md) |
|
||||
| Выполняется архитектурное ревью или финальная проверка реализации | [Архитектурная проверка](./validation.md) |
|
||||
| Неясен layer, направление import или роль `app/compositions/business/infra/ui/shared` | [Слои](./layers.md) |
|
||||
| Нужно отличить module, component, group, nested module или спроектировать public API | [Модули](./modules.md) |
|
||||
| Проектируется factory, Api, Deps, domain error или сборка домена | [Business-фабрика](./business-factory.md) |
|
||||
| Неясно размещение hook/store/service/mapper/provider/type/style | [Сегменты](./segments.md) |
|
||||
| Решается вынос из `apps/*/src` в `packages/*` | [Монорепозитории](./monorepo.md) |
|
||||
| Нужен полный пример adapters, builder, state runtime и graph lifecycle | [Business composition](../examples/business-composition.md) |
|
||||
| Нужна матрица factory-level, assembly и colocated tests | [Тестирование business-модулей](../examples/business-testing.md) |
|
||||
| Нужен page/route provider, локальный UI store и доступ к готовому graph | [Композиция через Provider](../examples/react/composition-provider.md) |
|
||||
| Команда выбирает организацию groups внутри `compositions` | [Структуры compositions](../examples/react/composition-structures.md) |
|
||||
|
||||
Не используй карту как scaffold checklist. Наличие возможной папки не означает, что её нужно создать.
|
||||
508
docs/canons/file-atlas.md
Normal file
508
docs/canons/file-atlas.md
Normal file
@@ -0,0 +1,508 @@
|
||||
---
|
||||
title: Атлас файлов SLM
|
||||
description: Карта слоёв, типов modules, root files, segments, public API, tests и запрещённых файловых сочетаний
|
||||
---
|
||||
|
||||
# Атлас файлов SLM
|
||||
|
||||
Используй атлас после того, как определены роль изменения и владелец ответственности. Не выбирай архитектуру по желаемому имени файла.
|
||||
|
||||
Атлас исчерпывает стандартные архитектурные роли SLM, но не является закрытым списком framework-файлов. Команда может добавить локальный segment или suffix, если его ответственность не дублирует существующую, не нарушает направление зависимостей и закреплена в локальных инструкциях.
|
||||
|
||||
## Единицы структуры
|
||||
|
||||
| Единица | Что означает | Имеет public API | Владеет runtime |
|
||||
|---|---|---|---|
|
||||
| Layer | Верхнеуровневая область ответственности внутри SLM root | Нет общего требования | Зависит от layer |
|
||||
| Group | Навигационная папка для modules или других groups | Нет, `index.ts` запрещён | Нет |
|
||||
| Module | Самостоятельный владелец одной ответственности | Да | Может |
|
||||
| Segment | Папка внутри module по назначению файлов | Нет отдельного внешнего API | Только как часть владельца |
|
||||
| Component | Презентационная часть родительского module в `ui/` | Локальный `index.ts` допустим | Нет архитектурного runtime |
|
||||
| Root file | Главный entry/contract конкретного module | Экспортируется module `index.ts` | Зависит от типа module |
|
||||
|
||||
Сначала определи module, затем root file, затем необходимые segments. Не создавай все папки из атласа заранее.
|
||||
|
||||
## Карта SLM root
|
||||
|
||||
```text
|
||||
src/
|
||||
├── app/ # framework wiring
|
||||
├── compositions/ # product tree, graph owners и business integrations
|
||||
├── business/ # доменные контракты и сценарии
|
||||
├── infra/ # технические runtime-сервисы
|
||||
├── ui/ # универсальные UI modules
|
||||
└── shared/ # детерминированный фундамент
|
||||
```
|
||||
|
||||
В monorepo вместо `src/` границей приложения обычно является `apps/{app}/src/`. Packages находятся выше SLM root и не являются дополнительными слоями.
|
||||
|
||||
## Root files modules
|
||||
|
||||
| Pattern | Роль | Где допустим |
|
||||
|---|---|---|
|
||||
| `{name}.page.tsx` | Готовая page composition | `compositions` |
|
||||
| `{name}.layout.tsx` | Product layout composition | `compositions` |
|
||||
| `{name}.screen.tsx` | Уникальный screen leaf/branch | `compositions` |
|
||||
| `{name}.widget.tsx` | Самостоятельный composition block | `compositions` |
|
||||
| `{name}.route.tsx` | Route composition и route lifecycle | `compositions` |
|
||||
| `{name}.entry.tsx` | Готовая точка подключения product tree | `compositions` |
|
||||
| `{scope}-business-composition.ts` | Non-visual сборка business graph/scope | `compositions` |
|
||||
| `{name}.ts` | Другой non-visual root, названный по ответственности | `compositions`, nested modules |
|
||||
| `{name}.tsx` | Root UI/module component без специальной роли | `compositions`, `ui`, nested modules |
|
||||
| `{domainName}.factory.ts` | Единственный runtime entry business-домена | `business/{domainPath}` |
|
||||
| `create-{domainName}-business.ts` | Builder одной business-фабрики | `compositions/business/{domainPath}` |
|
||||
| `{name}.client.ts` | Технический client | `infra/{service}` |
|
||||
| `{name}.service.ts` | Технический root service, если service и есть module entry | `infra/{service}` |
|
||||
| `index.ts` | Public API конечного module | В module; запрещён у group |
|
||||
|
||||
Root suffix не определяет owner автоматически. Например, `profile.store.ts` остаётся domain store или page UI store в зависимости от смысла state.
|
||||
|
||||
## Layer App
|
||||
|
||||
`app` содержит только файлы, требуемые framework/runtime entry.
|
||||
|
||||
Возможные файлы:
|
||||
|
||||
| Файл или pattern | Назначение |
|
||||
|---|---|
|
||||
| `main.tsx`, `bootstrap.tsx` | Запуск приложения |
|
||||
| `app.tsx` | Тонкое подключение application entry/providers |
|
||||
| `app-router.tsx`, `router.tsx` | Framework route registry |
|
||||
| `page.tsx`, `layout.tsx`, `route.ts`, `error.tsx`, `not-found.tsx` | Framework-convention files |
|
||||
| `middleware.ts` | Framework middleware boundary |
|
||||
| framework metadata/config files | Только framework contract |
|
||||
|
||||
Правила:
|
||||
|
||||
- framework file импортирует готовый entry/route/page composition через public API;
|
||||
- product tree собирается в `compositions`, не в `app`;
|
||||
- business graph, domain store, screen, widget и product provider в `app` запрещены;
|
||||
- у `app` нет SLM modules и общего `index.ts`;
|
||||
- framework-specific server/client правила определяет профильный framework skill.
|
||||
|
||||
Минимальный пример:
|
||||
|
||||
```text
|
||||
src/app/
|
||||
├── app-router.tsx
|
||||
├── app.tsx
|
||||
└── main.tsx
|
||||
|
||||
src/compositions/entries/profile/
|
||||
├── profile.entry.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Consumer composition module
|
||||
|
||||
Consumer composition собирает product UI и использует готовые `{Domain}Api`.
|
||||
|
||||
```text
|
||||
compositions/{group}/{name}/
|
||||
├── {name}.{page|layout|screen|widget|route|entry}.tsx # visual entry, если нужен
|
||||
├── {name}-business-composition.ts # business graph, если module владеет им
|
||||
├── {name}.ts # другой non-visual entry, если нужен
|
||||
├── ui/ # presentation components
|
||||
├── parts/ # nested modules
|
||||
├── providers/ # provider владельца scope
|
||||
├── guards/ # route/UI guards над готовым DomainApi
|
||||
├── hooks/ # access/orchestration hooks
|
||||
├── stores/ # только локальный UI-state
|
||||
├── services/ # orchestration готовых API
|
||||
├── mappers/ # domain result -> ViewModel
|
||||
├── types/ # module-owned types
|
||||
├── errors/ # только composition/UI errors, не domain errors
|
||||
├── lib/ # локальные deterministic helpers
|
||||
├── config/ # module configuration
|
||||
├── styles/ # styles module
|
||||
├── tests/ # scope/integration tests при необходимости
|
||||
└── index.ts # public API
|
||||
```
|
||||
|
||||
Не все segments обязательны. Создавай только используемые.
|
||||
|
||||
Composition module не обязан иметь `.tsx`: request/application business graph использует `{scope}-business-composition.ts`, а другая non-visual orchestration может иметь root `.ts`, названный по ответственности, и public `index.ts`.
|
||||
|
||||
Guard принадлежит composition scope, использует готовый `{Domain}Api` и выбирает route/UI outcome. Guard не получает product data напрямую, не создаёт business graph и не импортирует source runtime.
|
||||
|
||||
Consumer composition не содержит:
|
||||
|
||||
- product SDK/client/generated operation;
|
||||
- product storage adapter;
|
||||
- source/query adapter домена;
|
||||
- domain store implementation;
|
||||
- domain mapper/error;
|
||||
- business factory implementation.
|
||||
|
||||
Product data поступает только через `{Domain}Api`. `stores/` хранит presentation-only state: sidebar, tab, local step, transient form UI. Product entities и domain state туда не копируются.
|
||||
|
||||
### Page/route graph owner
|
||||
|
||||
Если composition владеет business graph и lifecycle, возможна структура:
|
||||
|
||||
```text
|
||||
compositions/routes/profile/
|
||||
├── profile.route.tsx
|
||||
├── profile-business-composition.ts
|
||||
├── providers/
|
||||
│ ├── profile-business.provider.tsx
|
||||
│ └── profile-business.context.ts
|
||||
├── hooks/
|
||||
│ └── use-profile-business.hook.ts
|
||||
├── types/
|
||||
│ └── profile-business.type.ts
|
||||
├── tests/
|
||||
│ └── profile-business-lifecycle.test.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Правила graph owner:
|
||||
|
||||
- graph type перечисляет только реально доступные API;
|
||||
- `Partial<Business>` с cast к полному graph запрещён;
|
||||
- graph создаётся в lifecycle владельца;
|
||||
- domain lifecycle operation запускается после commit и возвращает cleanup;
|
||||
- raw SDK/client/event bus не импортируется для досборки домена;
|
||||
- screen/widget не создаёт тот же graph повторно.
|
||||
|
||||
## Integration module business
|
||||
|
||||
`compositions/business/{domainPath}` является единственным module, который знает одновременно business dependency contract и concrete runtime.
|
||||
|
||||
`{domainPath}` означает полный относительный путь конечного business module, а `{domainName}` — имя его последней папки. При groups integration path зеркалирует business path:
|
||||
|
||||
```text
|
||||
business/app/auth/
|
||||
compositions/business/app/auth/
|
||||
|
||||
business/cms/content/
|
||||
compositions/business/cms/content/
|
||||
```
|
||||
|
||||
```text
|
||||
compositions/business/{domainPath}/
|
||||
├── create-{domainName}-business.ts # обязательный builder
|
||||
├── create-{domainName}-business.test.ts # обязательный assembly test
|
||||
├── adapters/ # обязательны при runtime dependencies
|
||||
│ ├── {source}.adapter.ts
|
||||
│ ├── {storage}.adapter.ts
|
||||
│ ├── {query}-hook.adapter.ts
|
||||
│ ├── {state-manager}-state.adapter.ts
|
||||
│ ├── {event-source}-events.adapter.ts
|
||||
│ └── {platform}-navigation.adapter.ts
|
||||
├── types/ # builder/cross-domain/request input types
|
||||
│ ├── create-{domainName}-business-deps.type.ts
|
||||
│ └── create-{domainName}-business-request-input.type.ts
|
||||
├── testing/ # assembly fixtures, не public API
|
||||
└── index.ts # builder и type-only integration inputs
|
||||
```
|
||||
|
||||
Builder:
|
||||
|
||||
1. Явно создаёт или получает runtime instances нужного lifecycle без I/O.
|
||||
2. Создаёт adapters поверх runtime instances.
|
||||
3. Передаёт adapters и cross-domain API фабрике.
|
||||
4. Возвращает готовый `{Domain}Api`.
|
||||
|
||||
Browser/application builder без cross-domain dependencies вызывается без аргументов. Request-scoped builder отдельно принимает `requestScopeInput` с request data; concrete client factory импортируется integration module.
|
||||
|
||||
Integration module не содержит:
|
||||
|
||||
- domain mapper/normalizer;
|
||||
- domain error;
|
||||
- business scenario;
|
||||
- React provider/layout/screen/widget;
|
||||
- full application graph;
|
||||
- exported private adapter.
|
||||
|
||||
## Business module
|
||||
|
||||
Каждый `business/{domainPath}` имеет полный factory contract.
|
||||
|
||||
```text
|
||||
business/{domainPath}/
|
||||
├── {domainName}.factory.ts # обязательно
|
||||
├── index.ts # обязательно
|
||||
├── types/ # обязательно для contracts
|
||||
│ ├── {domainName}-api.type.ts
|
||||
│ ├── {domainName}-deps.type.ts
|
||||
│ ├── {domainName}-factory.type.ts
|
||||
│ ├── {domainName}-error.type.ts
|
||||
│ ├── {domainName}-error-code.type.ts
|
||||
│ ├── {entity}.type.ts
|
||||
│ ├── {source}-hook-result.type.ts
|
||||
│ └── {domainName}-state.type.ts
|
||||
├── errors/
|
||||
│ └── {domainName}-business.error.ts
|
||||
├── services/
|
||||
│ └── {scenario}.service.ts
|
||||
├── hooks/
|
||||
│ └── use-{scenario}.hook.ts # wrapper над dependency hook
|
||||
├── mappers/
|
||||
│ └── map-{entity}.ts # unknown -> domain
|
||||
├── lib/
|
||||
│ └── {domain-helper}.ts
|
||||
├── config/
|
||||
│ └── {domainName}.config.ts # deterministic domain constants
|
||||
└── tests/
|
||||
└── {domainName}-factory/
|
||||
├── public-api.test.ts
|
||||
├── {scenario}.test.ts
|
||||
└── testing/
|
||||
└── create-{domainName}-deps.mock.ts
|
||||
```
|
||||
|
||||
Обязательный минимум:
|
||||
|
||||
- `{domainName}.factory.ts`;
|
||||
- `{Domain}Api`, `{Domain}Deps`, `{Domain}Factory`;
|
||||
- domain error codes и собственная domain error для runtime failure;
|
||||
- `index.ts` с одним runtime export фабрики и type-only exports;
|
||||
- factory-level tests каждого public runtime operation;
|
||||
- integration builder и assembly test для каждого business-модуля, включая dependency-free factory.
|
||||
|
||||
Условные файлы:
|
||||
|
||||
- `services/` только при выделенном сценарии;
|
||||
- `hooks/` только для wrapper над dependency hook;
|
||||
- `mappers/`/`normalizers/`/type guards при внешнем `unknown`;
|
||||
- `errors/` при runtime operations;
|
||||
- colocated tests обязательны для каждого mapper, normalizer, type guard и domain error;
|
||||
- colocated tests services/hooks обязательны при самостоятельной branching, race или другой runtime-safe логике.
|
||||
|
||||
В business запрещены:
|
||||
|
||||
- `ui/`, React components, providers, layouts и guards;
|
||||
- concrete `stores/` Zustand/Redux/MobX;
|
||||
- `adapters/` concrete runtime;
|
||||
- SDK/client/generated DTO;
|
||||
- SWR/TanStack Query/Apollo runtime или types;
|
||||
- React state/effect runtime или types;
|
||||
- storage/browser/event/env implementation;
|
||||
- raw external error в public API.
|
||||
|
||||
## Infra module
|
||||
|
||||
Infra module владеет техническим сервисом без продуктовой модели.
|
||||
|
||||
```text
|
||||
infra/{service}/
|
||||
├── {service}.client.ts # если module является client
|
||||
├── {service}.service.ts # если module является service
|
||||
├── client.ts # допустимый technical entry
|
||||
├── config/
|
||||
├── clients/
|
||||
├── services/
|
||||
├── transports/
|
||||
├── hooks/ # technical hooks
|
||||
├── providers/ # provider technical service
|
||||
├── ui/ # только technical UI, например theme tooling
|
||||
├── errors/ # transport/technical errors
|
||||
├── types/ # technical contracts/DTO
|
||||
├── lib/
|
||||
├── tests/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Различай два вида adapter:
|
||||
|
||||
| Вид | Где живёт | Что знает |
|
||||
|---|---|---|
|
||||
| Transport adapter/client | `infra` | Протокол, SDK, HTTP, transport types |
|
||||
| Domain dependency adapter | `compositions/business/{domainPath}` | Business `Deps` и конкретный infra runtime |
|
||||
|
||||
Infra не содержит business graph, domain state, product provider или domain error. Type-only imports business API не дают infra права агрегировать product graph.
|
||||
|
||||
## UI module
|
||||
|
||||
UI module предоставляет универсальный UI без business logic и product I/O.
|
||||
|
||||
```text
|
||||
ui/{name}/
|
||||
├── {name}.tsx # обязательный root component
|
||||
├── ui/ # внутренние presentation components
|
||||
├── parts/ # nested UI modules при самостоятельной роли
|
||||
├── hooks/ # presentation behavior
|
||||
├── stores/ # только локальный UI-state
|
||||
├── providers/ # UI scope provider
|
||||
├── types/ # props и UI contracts
|
||||
├── styles/
|
||||
├── lib/ # presentation helpers
|
||||
├── tests/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
UI module может строиться на других UI modules и `shared`. Он не импортирует business, infra или compositions, не получает product data самостоятельно и не выбирает источник.
|
||||
|
||||
## Shared
|
||||
|
||||
`shared` содержит только детерминированный фундамент без знания о продукте и runtime-state.
|
||||
|
||||
```text
|
||||
shared/
|
||||
├── lib/
|
||||
│ └── {utility}/
|
||||
│ ├── {utility}.ts
|
||||
│ ├── {utility}.test.ts
|
||||
│ └── index.ts
|
||||
├── types/
|
||||
├── styles/
|
||||
├── config/ # только product-agnostic constants
|
||||
├── assets/ # product-agnostic assets при локальном соглашении
|
||||
└── sprites/ # специализированная группа assets, если используется
|
||||
```
|
||||
|
||||
Shared не содержит:
|
||||
|
||||
- product/domain types;
|
||||
- stores и mutable singletons;
|
||||
- SDK/client wrappers;
|
||||
- React providers;
|
||||
- environment-dependent services;
|
||||
- imports из других SLM layers.
|
||||
|
||||
## Components внутри ui segment
|
||||
|
||||
Presentation component родительского module имеет плоскую структуру:
|
||||
|
||||
```text
|
||||
{module}/ui/{component}/
|
||||
├── {component}.tsx
|
||||
├── types/
|
||||
│ └── {component}-props.type.ts
|
||||
├── styles/
|
||||
│ └── {component}.module.css
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
В папке component запрещены:
|
||||
|
||||
- `hooks/`, `stores/`, `services/`, `providers/`, `parts/`;
|
||||
- source calls и scenario hooks;
|
||||
- imports project code вне parent module, кроме разрешённых UI modules;
|
||||
- nested components как отдельные architectural folders.
|
||||
|
||||
Если это требуется, сущность становится module и перемещается в `parts/` либо на общий composition/UI уровень.
|
||||
|
||||
## Nested modules в parts
|
||||
|
||||
Каждый элемент `parts/` является полноценным module:
|
||||
|
||||
```text
|
||||
{parent}/parts/{part}/
|
||||
├── {part}.tsx # visual root, если нужен
|
||||
├── {part}.ts # non-visual root, если нужен
|
||||
├── ui/
|
||||
├── parts/
|
||||
├── hooks/
|
||||
├── stores/ # только state ответственности part
|
||||
├── types/
|
||||
├── styles/
|
||||
├── tests/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Одиночные `.tsx`, `.ts` или style files непосредственно в `parts/` запрещены. Если nested module нужен за пределами parent, подними его в минимальный общий scope.
|
||||
|
||||
## Segment matrix
|
||||
|
||||
| Segment | Consumer composition | Business integration | Business | Infra | UI | Shared |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `ui/` | Да | Нет | Нет | Условно, technical UI | Да | Нет |
|
||||
| `parts/` | Да | Нет | Нет | Условно | Да | Нет |
|
||||
| `providers/` | Да | Нет | Нет | Да | Условно | Нет |
|
||||
| `guards/` | Да, над готовым DomainApi | Нет | Нет | Нет | Нет | Нет |
|
||||
| `hooks/` | Да | Нет | Только wrappers над deps | Technical hooks | Presentation hooks | Нет |
|
||||
| `stores/` | Только UI-state | Нет, используй `adapters/` | Нет concrete stores | Technical state | Только UI-state | Нет |
|
||||
| `services/` | Orchestration готовых API | Нет, используй `adapters/` | Domain scenarios над deps | Technical services | Presentation-only | Нет |
|
||||
| `adapters/` | Нет product adapters | Да | Нет | Только transport adapters по локальному соглашению | Нет | Нет |
|
||||
| `mappers/` | Domain -> ViewModel | Нет, transport adaptation остаётся в adapter | `unknown` -> domain | Transport mapping | View mapping | Чистые generic transforms |
|
||||
| `errors/` | UI/composition errors | Нет domain errors | Domain errors | Technical/transport errors | UI errors | Только generic errors |
|
||||
| `types/` | Module contracts | Integration input types | Domain contracts | Technical contracts/DTO | Props/UI contracts | Product-agnostic types |
|
||||
| `styles/` | Да | Нет | Нет | Условно | Да | Global/foundation styles |
|
||||
| `lib/` | Local helpers | Assembly helpers | Domain deterministic helpers | Technical helpers | Presentation helpers | Generic deterministic helpers |
|
||||
| `config/` | Composition constants | Runtime assembly config | Domain constants | Technical config/env | UI constants | Product-agnostic constants |
|
||||
| `tests/` | Scope/integration tests | Обязательные assembly tests | Обязательные factory tests | Technical tests | UI tests | Unit tests |
|
||||
|
||||
## Имена обычных файлов
|
||||
|
||||
| Pattern | Назначение |
|
||||
|---|---|
|
||||
| `{name}.type.ts` | Module-owned type |
|
||||
| `{name}-props.type.ts` | Component props |
|
||||
| `{name}.hook.ts`, `use-{name}.hook.ts` | Hook владельца |
|
||||
| `{name}.store.ts` | Concrete store только допустимого owner/scope |
|
||||
| `{name}.service.ts` | Scenario или technical service по layer |
|
||||
| `{name}.adapter.ts` | Adapter с явно определённым видом |
|
||||
| `map-{name}.ts`, `normalize-{name}.ts` | Mapper/normalizer владельца |
|
||||
| `{name}.provider.tsx` | Provider владельца scope |
|
||||
| `{name}.guard.tsx` | Route/UI guard consumer composition |
|
||||
| `{name}.context.ts`, `{name}.context.tsx` | Private context implementation |
|
||||
| `{name}.error.ts` | Error соответствующего layer |
|
||||
| `{name}.config.ts` | Configuration владельца |
|
||||
| `{name}.constant.ts` | Константа владельца |
|
||||
| `{name}.module.css` | Styles конкретного module/component |
|
||||
| `{name}.test.ts`, `{name}.test.tsx` | Colocated test |
|
||||
|
||||
Suffix описывает техническую форму, но не переносит ownership. `user.type.ts` не становится shared только потому, что это type; `auth.store.ts` не становится infra только потому, что использует Zustand.
|
||||
|
||||
## Public API по типам modules
|
||||
|
||||
| Module | Runtime exports | Type exports | Не экспортировать |
|
||||
|---|---|---|---|
|
||||
| Consumer composition | Entry/provider/access hooks | Props, state/view types | Raw context/store factory/internal parts |
|
||||
| Business integration | `create{Domain}Business` | Cross-domain/request input types | Adapters, clients, mocks |
|
||||
| Business | Только `{domainName}Factory` | Api, Deps, Factory, domain/error types | Services, hooks, mappers, error class |
|
||||
| Infra | Минимальный technical API | Technical contracts | Mutable internals и generated tree без необходимости |
|
||||
| UI | Root component и доказанные UI helpers | Props/UI types | Internal components/store/context |
|
||||
| Nested module | Root entry | Props/module types | Parent internals |
|
||||
|
||||
Если дочерние layout/screen/widget импортируют access hooks владельца scope, не экспортируй из того же public API готовый entry, который импортирует эти дочерние modules. Раздели scope API и ready entry на отдельные composition modules, чтобы не создать runtime-цикл.
|
||||
|
||||
## Tests map
|
||||
|
||||
| Проверяемая граница | Размещение | Обязательность |
|
||||
|---|---|---|
|
||||
| Business public contract | `business/{domainPath}/tests/{domainName}-factory/` | Обязательно |
|
||||
| Business mapper/normalizer/type guard/domain error | Рядом с файлом | Обязательно для каждого такого файла |
|
||||
| Business service/hook wrapper | Рядом с файлом | При самостоятельной branching/race/runtime-safe логике |
|
||||
| Adapter и builder wiring | `compositions/business/{domainPath}/*.test.ts` | Обязательно для каждого business-модуля |
|
||||
| Graph lifecycle/provider | Tests graph owner module | При state/subscriptions/resources |
|
||||
| Consumer composition | Рядом или `tests/` module | По поведению scope |
|
||||
| Infra transport/service | Внутри infra module | По technical contract |
|
||||
| UI module/component | Внутри UI module | По интерактивному поведению |
|
||||
|
||||
## Запрещённые структуры
|
||||
|
||||
```text
|
||||
business/auth/ui/ # React UI внутри business
|
||||
business/auth/stores/auth.store.ts # concrete Zustand/Redux store
|
||||
business/auth/adapters/backend.adapter.ts # concrete runtime adapter
|
||||
business/auth/types/sdk-response.type.ts # generated/external DTO contract
|
||||
|
||||
compositions/pages/profile/services/api.ts # прямой product source
|
||||
compositions/screens/profile/store.ts # product/domain cache в screen
|
||||
|
||||
infra/business/ # product graph/provider в infra
|
||||
shared/user.type.ts # product domain type в shared
|
||||
|
||||
business/app/index.ts # group с public API
|
||||
compositions/pages/index.ts # group с public API
|
||||
|
||||
{module}/parts/hero.tsx # файл вместо nested module
|
||||
{module}/ui/card/hooks/ # component с собственной логикой
|
||||
```
|
||||
|
||||
## Новый тип файла или segment
|
||||
|
||||
Если нужной роли нет в атласе:
|
||||
|
||||
1. Назови ответственность файла без технического suffix.
|
||||
2. Определи module-владельца и допустимые зависимости.
|
||||
3. Проверь, не является ли файл существующим `service`, `adapter`, `mapper`, `provider` или nested module.
|
||||
4. Создай новый segment только для нескольких файлов с одной устойчивой ролью.
|
||||
5. Не создавай global segment на уровне `src/`.
|
||||
6. Зафиксируй локальное соглашение, если pattern будет повторяться.
|
||||
7. Обнови template, если новый pattern стал обязательной повторяемой структурой.
|
||||
|
||||
Не подгоняй ответственность под красивое дерево. Минимальный корректный module лучше полного scaffold без реального поведения.
|
||||
148
docs/canons/index.md
Normal file
148
docs/canons/index.md
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
title: SLM Design
|
||||
description: Назначение архитектуры, ключевые принципы и карта разделов документации
|
||||
---
|
||||
|
||||
# Основы SLM Design
|
||||
|
||||
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
||||
|
||||
## Рабочий алгоритм
|
||||
|
||||
1. Заполни архитектурную карточку из [процесса принятия решения](./decision-process.md): роль, владелец, данные, runtime-capabilities, место, public API, lifecycle и проверки.
|
||||
2. Сначала выбери слой ответственности, затем module scope, затем segments и только после этого конкретные файлы.
|
||||
3. Для product data и domain state примени [runtime-границу business](./business-runtime-boundary.md).
|
||||
4. Выбирай минимальное корректное место и не создавай общий provider, store, package или business-контракт «на будущее».
|
||||
5. После реализации пройди [архитектурную проверку](./validation.md). Задача не завершена без обязательных business и assembly tests.
|
||||
|
||||
## Дополнительные примеры
|
||||
|
||||
Каноны ниже достаточны для базового архитектурного решения. Если нужен подробный пример реализации, открой конкретный файл:
|
||||
|
||||
- [Композиция через Provider](../examples/react/composition-provider.md) — page-level provider, store и business composition.
|
||||
- [Структуры compositions](../examples/react/composition-structures.md) — допустимые структуры слоя `compositions`.
|
||||
- [Business composition](../examples/business-composition.md) — runtime-сборка business-фабрик в `compositions/business/{domain}`.
|
||||
- [Тестирование business-модулей](../examples/business-testing.md) — factory-level тесты и тесты сборки.
|
||||
|
||||
## Разделы спецификации
|
||||
|
||||
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
|
||||
|
||||
- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
|
||||
- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
|
||||
- [Атлас файлов SLM](./file-atlas.md) — root files, segments, структуры всех типов modules, public API и tests.
|
||||
- [Business-фабрика](./business-factory.md) — контракт business-модуля, logic API, deps, доменные ошибки и сборка фабрик.
|
||||
- [Runtime-граница business](./business-runtime-boundary.md) — capabilities фабрики, adapters, hooks, stores и безусловные domain errors.
|
||||
- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
|
||||
- [Монорепозитории](./monorepo.md) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business/compositions.
|
||||
- [Архитектурная проверка](./validation.md) — блокирующие gates создания, рефакторинга и ревью.
|
||||
|
||||
Рекомендуемый порядок чтения: процесс решения → runtime-граница business → архитектурная проверка → атлас или подробности только нужной ветки.
|
||||
|
||||
## Преимущества
|
||||
|
||||
### Единый слой композиции
|
||||
|
||||
Страницы, маршруты и крупные продуктовые части интерфейса собираются в `compositions`. Слой не навязывает жёсткую структуру: команда может использовать `pages/layouts/screens/widgets` или другую организацию под свой фреймворк и продукт.
|
||||
|
||||
### Вертикальная организация домена
|
||||
|
||||
Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы, hooks, services и mappers живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: доменная логика локализована.
|
||||
|
||||
### Dependency Injection без фреймворков
|
||||
|
||||
Runtime-зависимости business-модуля реализуются через фабрики — модуль декларирует что ему нужно, а composition-сборка предоставляет зависимости. Домены изолированы от SDK, storage и backend-клиентов без DI-контейнеров и шин событий.
|
||||
|
||||
### Разделение ответственности без перегрузки слоёв
|
||||
|
||||
Композиция приложения (`compositions/`), сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — разные слои с разной природой. Ни один слой не превращается в свалку разнородного кода.
|
||||
|
||||
### Графовая композиция там, где она нужна
|
||||
|
||||
Внутри `compositions` допускается граф импортов через публичный API. Это позволяет page-level store, provider или сборку business-фабрик использовать одновременно в layout, screen и widget, не перенося продуктовый runtime-state в `infra` или `shared`.
|
||||
|
||||
### Горизонтальная инкапсуляция
|
||||
|
||||
Вложенные модули (`parts/`) и публичные API позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга.
|
||||
|
||||
### Колокация по умолчанию
|
||||
|
||||
Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями.
|
||||
|
||||
### Масштабирование через группировку
|
||||
|
||||
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: композиции по страницам и маршрутам, бизнес-домены по субдоменам, UI-компоненты по уровню абстракции.
|
||||
|
||||
### Адаптация к монорепозиториям
|
||||
|
||||
SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. `compositions` и бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы.
|
||||
|
||||
## Происхождение
|
||||
|
||||
SLM Design вырос на основе:
|
||||
|
||||
- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей
|
||||
- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое
|
||||
- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию
|
||||
- **Colocation Principle** — код живёт рядом с местом использования
|
||||
|
||||
## Пример структуры проекта
|
||||
|
||||
```text
|
||||
src/
|
||||
├── app/
|
||||
│
|
||||
├── compositions/
|
||||
│ ├── business/
|
||||
│ │ ├── auth/
|
||||
│ │ └── user/
|
||||
│ ├── pages/
|
||||
│ │ ├── home/
|
||||
│ │ ├── profile/
|
||||
│ │ └── product-detail/
|
||||
│ ├── layouts/
|
||||
│ │ ├── main/
|
||||
│ │ └── dashboard/
|
||||
│ ├── screens/
|
||||
│ │ ├── home/
|
||||
│ │ └── profile/
|
||||
│ └── widgets/
|
||||
│ ├── page-heading/
|
||||
│ └── promo-banner/
|
||||
│
|
||||
├── business/
|
||||
│ ├── auth/
|
||||
│ ├── catalog/
|
||||
│ ├── orders/
|
||||
│ └── chat/
|
||||
│
|
||||
├── infra/
|
||||
│ ├── theme/
|
||||
│ ├── i18n/
|
||||
│ ├── backend-api/
|
||||
│ └── logger/
|
||||
│
|
||||
├── ui/
|
||||
│ ├── button/
|
||||
│ ├── input/
|
||||
│ ├── modal/
|
||||
│ ├── toast/
|
||||
│ └── dropdown/
|
||||
│
|
||||
└── shared/
|
||||
├── lib/
|
||||
├── types/
|
||||
└── styles/
|
||||
```
|
||||
|
||||
## Принципы
|
||||
|
||||
- **Композиция — отдельный слой.** Страницы, маршруты и крупные продуктовые части интерфейса собираются в `compositions`.
|
||||
- **Структура композиции свободна.** Команда сама выбирает организацию внутри `compositions`; базовая рекомендация — `pages/layouts/screens/widgets`.
|
||||
- **Домен — единое целое.** Доменная модель, сценарии, типы, services и mappers живут в одном business-модуле. Concrete data/state/query hooks передаются фабрике через adapters.
|
||||
- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости.
|
||||
- **Зависимости однонаправлены за пределами compositions.** `app` подключает `compositions`; `compositions` связывает `business`, `infra`, `ui` и `shared`; `business` вызывает concrete runtime-возможности только через переданные фабрике `deps`.
|
||||
- **Product data проходит через business.** Page, layout, screen и widget не обращаются к product source напрямую.
|
||||
- **Ошибки принадлежат домену.** Из business API выходят только собственные domain errors со стабильным `code`.
|
||||
- **Внутри compositions допустим граф.** Composition modules могут импортировать друг друга через public API.
|
||||
- **Архитектура — каркас, не клетка.** Правила фиксируют границы ответственности и public API, а внутреннюю форму композиции определяет команда.
|
||||
285
docs/canons/layers.md
Normal file
285
docs/canons/layers.md
Normal file
@@ -0,0 +1,285 @@
|
||||
---
|
||||
title: Слои
|
||||
description: Иерархия слоёв от app до shared, правила зависимостей и зона ответственности каждого слоя
|
||||
---
|
||||
|
||||
# Слои
|
||||
|
||||
Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом.
|
||||
|
||||
## Определение
|
||||
|
||||
**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.**
|
||||
|
||||
## Группы слоёв
|
||||
|
||||
Слои делятся на три группы:
|
||||
|
||||
| Группа | Слои | Описание |
|
||||
|--------|------|----------|
|
||||
| Композиция | `app`, `compositions` | Подключают приложение к фреймворку и собирают страницы, маршруты и крупные продуктовые части интерфейса |
|
||||
| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит |
|
||||
| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги |
|
||||
|
||||
## Направление зависимостей
|
||||
|
||||
Любой импорт между модулями — только через публичный API.
|
||||
|
||||
```text
|
||||
app → compositions
|
||||
compositions → business | infra | ui | shared
|
||||
business → shared
|
||||
infra → infra | shared
|
||||
ui → ui | shared
|
||||
shared -/→ SLM-слои
|
||||
```
|
||||
|
||||
- `app` подключает приложение к фреймворку и импортирует готовые composition modules
|
||||
- `compositions` импортирует `business`, `infra`, `ui`, `shared`
|
||||
- `business` импортирует только собственные файлы, детерминированный `shared`, чистые библиотеки и type-only контракты других business-модулей
|
||||
- `infra` импортирует `infra` и `shared`
|
||||
- `ui` импортирует `ui` и `shared`
|
||||
- `shared` не импортирует другие SLM-слои
|
||||
- `business`, `infra`, `ui`, `shared` не импортируют `compositions`
|
||||
- Внутри `compositions` направление импортов между composition modules не фиксируется, но импорты разрешены только через публичный API и не должны создавать runtime-циклы
|
||||
- Модули `business` вызывают любую runtime-capability только через `deps` фабрики; source/query hooks, stores, events и platform APIs также являются dependencies
|
||||
- `import type` не разрешает переносить ownership: business не импортирует generated DTO, SDK/client/store/query types, а infra не объявляет product graph через type-only business imports
|
||||
- Pure deterministic third-party libraries допустимы внутри business, если не имеют I/O/state/hidden environment и не протекают в public contract
|
||||
|
||||
## Слой App
|
||||
|
||||
Точка входа приложения. Отвечает за запуск, роутинг и подключение composition modules к фреймворку.
|
||||
|
||||
В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации.
|
||||
|
||||
### Требования
|
||||
|
||||
- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация
|
||||
- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов
|
||||
- Провайдеры, guards, layouts, screens и страницы — только подключает готовые из `compositions`, не реализует
|
||||
- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы
|
||||
- Никем не импортируется
|
||||
|
||||
## Слой Compositions
|
||||
|
||||
`compositions/` — слой сборки страниц, маршрутов и крупных продуктовых частей интерфейса.
|
||||
|
||||
На этом слое собираются page, layout, screen, widget и другие composition modules. Они связываются между собой и с нижними слоями: `business`, `infra`, `ui`, `shared`.
|
||||
|
||||
SLM не фиксирует жёсткую структуру внутри `compositions`. Команда выбирает организацию под фреймворк, роутинг, CMS и продуктовую задачу.
|
||||
|
||||
Базовая рекомендация:
|
||||
|
||||
```text
|
||||
src/compositions/
|
||||
├── business/
|
||||
├── pages/
|
||||
├── layouts/
|
||||
├── screens/
|
||||
└── widgets/
|
||||
```
|
||||
|
||||
`business`, `pages`, `layouts`, `screens` и `widgets` внутри `compositions` не являются отдельными SLM-слоями. Это группы composition modules.
|
||||
|
||||
`compositions/business/{domain}` используется для runtime-сборки business-фабрик с реальными зависимостями приложения. Это не business-слой, а composition module, который адаптирует `infra`, SDK, storage и browser API к `deps` business-фабрики.
|
||||
|
||||
Внутри `compositions` различай три роли:
|
||||
|
||||
| Роль | Ответственность |
|
||||
|---|---|
|
||||
| Integration module | `compositions/business/{domain}` реализует adapters и собирает одну business-фабрику |
|
||||
| Graph owner | Page/route/provider/request scope собирает готовые business API и управляет lifecycle |
|
||||
| Consumer composition | Page/layout/screen/widget вызывает `{Domain}Api` и не знает concrete product source |
|
||||
|
||||
Consumer composition получает продуктовые данные только через business API. Прямые SDK/HTTP/generated/storage вызовы разрешены только dependency adapters интеграционного модуля домена.
|
||||
|
||||
Технический infra-сервис без product data можно использовать в composition напрямую. Если такой сервис нужен business, он всё равно описывается business-owned capability и передаётся фабрике через adapter.
|
||||
|
||||
Если business-домены сгруппированы, `compositions/business` повторяет ту же группировку: `business/app/auth` соответствует `compositions/business/app/auth`, `business/cms/content` соответствует `compositions/business/cms/content`. Это не default-структура, а способ сохранить навигацию в крупных проектах.
|
||||
|
||||
Composition module может содержать обычные сегменты SLM: `ui/`, `parts/`, `hooks/`, `stores/`, `services/`, `mappers/`, `types/`, `styles/`, `lib/`, `config/`, `providers/`.
|
||||
|
||||
Page-level store, provider, guard или business composition размещаются внутри page composition module, если они нужны всей странице.
|
||||
|
||||
```text
|
||||
compositions/pages/profile/
|
||||
├── profile.page.tsx
|
||||
├── profile-business-composition.ts
|
||||
├── providers/
|
||||
├── hooks/
|
||||
├── stores/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Layout, screen и widget могут получать через public API page composition локальный UI-state и готовые `{Domain}Api`. Product data не копируется в page store как параллельный источник истины.
|
||||
|
||||
```ts
|
||||
import { useProfilePageStore } from '@/compositions/pages/profile'
|
||||
```
|
||||
|
||||
Внутри `compositions` направление импортов между composition modules не фиксируется. Допустим граф, но все импорты идут только через public API.
|
||||
|
||||
```ts
|
||||
// Хорошо
|
||||
import { useProfilePageStore } from '@/compositions/pages/profile'
|
||||
|
||||
// Плохо
|
||||
import { useProfilePageStore } from '@/compositions/pages/profile/hooks/use-profile-page-store.hook'
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- `compositions` содержит composition modules страниц, маршрутов и крупных продуктовых частей интерфейса
|
||||
- Структура внутри `compositions` выбирается командой
|
||||
- Базовая рекомендация: `business/`, `pages/`, `layouts/`, `screens/`, `widgets/`
|
||||
- `business`, `pages`, `layouts`, `screens`, `widgets` внутри `compositions` являются группами composition modules
|
||||
- `compositions/business/{domain}` собирает конкретную business-фабрику с runtime-зависимостями; при группировке используется зеркальный путь `compositions/business/{group}/{domain}`
|
||||
- Concrete product sources, source/query hooks, domain stores, browser APIs и events подключаются только adapters интеграционного module
|
||||
- Page/layout/screen/widget используют product data только через `{Domain}Api`
|
||||
- Builder одной фабрики явно создаёт или получает scoped runtime instances без I/O, создаёт adapters поверх них и передаёт adapters factory; integration logic не пишется inline
|
||||
- Providers, stores, guards и business composition размещаются внутри того composition module, которому они принадлежат
|
||||
- Внутри `compositions` импорты между composition modules разрешены в любую сторону, но только через public API
|
||||
- Runtime-циклы между composition modules запрещены
|
||||
- Deep imports внутрь composition modules запрещены
|
||||
- `business`, `infra`, `ui` и `shared` не импортируют `compositions`
|
||||
|
||||
## Слой Business
|
||||
|
||||
Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, hooks, services, mappers и доменной логикой.
|
||||
|
||||
Слой входит в группу «Ядро». Импортирует собственные файлы, детерминированный `shared/`, чистые библиотеки и type-only контракты других business-модулей. Каждый бизнес-модуль создаёт публичный API фабрики в корне. Любые runtime-capabilities передаются через аргументы фабрики.
|
||||
|
||||
Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель и dependency contracts, `hooks/` и `services/` — wrappers над переданными capabilities, `mappers/` — доменная нормализация, `lib/` — детерминированные доменные утилиты.
|
||||
|
||||
Business-модуль не содержит React-компоненты, layouts, guards, providers и page-level wrappers. Визуальный fallback, route boundary и привязка logic API к React tree размещаются в `compositions`; error mapping и допустимый доменный fallback остаются в business.
|
||||
|
||||
```text
|
||||
src/business/
|
||||
├── auth/
|
||||
├── catalog/
|
||||
├── orders/
|
||||
├── checkout/
|
||||
└── chat/
|
||||
```
|
||||
|
||||
Когда количество доменов затрудняет навигацию — можно ввести группировку по крупным предметным областям. Группа — папка для организации, не модуль и не public API.
|
||||
|
||||
```text
|
||||
src/business/
|
||||
├── app/
|
||||
│ ├── auth/
|
||||
│ ├── profile/
|
||||
│ └── orders/
|
||||
└── cms/
|
||||
├── content/
|
||||
├── media/
|
||||
└── navigation/
|
||||
```
|
||||
|
||||
`app` и `cms` здесь являются группами доменов. Модулями остаются конечные папки: `auth`, `profile`, `content`, `media`, `navigation`.
|
||||
|
||||
### Требования
|
||||
|
||||
- Один модуль = один бизнес-домен
|
||||
- Business-домены можно группировать при необходимости; группа не является модулем и не содержит `index.ts`
|
||||
- Циклические зависимости между доменами запрещены
|
||||
- Публичный API фабрики — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты, без исключений
|
||||
- Фабрика возвращает только logic API: hooks, selectors, command/query methods, scenario services
|
||||
- Business-модуль не содержит React-компоненты и не возвращает компоненты из фабрики
|
||||
- Любые runtime-capabilities — только через `deps` фабрики: product sources, hooks, stores, events, technical services, env и browser APIs
|
||||
- Business не импортирует React/SWR/query/store runtime; concrete hooks и stores реализуются adapters
|
||||
- Реальные SDK, API-клиенты, storage, state/query runtime, env и browser API подключаются в `compositions/business/{domain}` или зеркальном пути при группировке
|
||||
- Business нормализует внешние результаты и подставляет только собственные domain errors
|
||||
- Доменные типы (`User`, `Product`) живут здесь, не в `shared/`
|
||||
|
||||
## Слой infra
|
||||
|
||||
Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль.
|
||||
|
||||
Слой входит в группу «Ядро». Импортирует `infra/` и `shared/`.
|
||||
|
||||
Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
|
||||
|
||||
```text
|
||||
src/infra/
|
||||
├── theme/
|
||||
├── i18n/
|
||||
├── backend-api/
|
||||
├── maps-api/
|
||||
├── logger/
|
||||
├── feature-flags/
|
||||
└── realtime/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Один модуль = один техсервис
|
||||
- Импортирует `infra/` и `shared/`
|
||||
- Не содержит продуктовые composition modules конкретных страниц или маршрутов
|
||||
|
||||
## Слой 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-состояния
|
||||
- Не знает о продуктовых composition modules
|
||||
300
docs/canons/modules.md
Normal file
300
docs/canons/modules.md
Normal file
@@ -0,0 +1,300 @@
|
||||
---
|
||||
title: Модули
|
||||
description: Структура модуля, типы (композиционный, UI, бизнес, инфра), публичный API, отличие модуля от компонента
|
||||
---
|
||||
|
||||
# Модули
|
||||
|
||||
Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом.
|
||||
|
||||
## Определение
|
||||
|
||||
**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.**
|
||||
|
||||
Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно.
|
||||
|
||||
Модуль не обязан быть UI-блоком. Это может быть page composition, layout composition, screen composition, widget composition, бизнес-домен, инфраструктурный сервис или UI-kit сущность.
|
||||
|
||||
Главная граница модуля — не папка, а ответственность.
|
||||
|
||||
## Компонент
|
||||
|
||||
**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.**
|
||||
|
||||
Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля.
|
||||
|
||||
> Компонент отображает. Модуль организует.
|
||||
|
||||
Компонент не может:
|
||||
|
||||
- Импортировать код проекта за пределами родительского модуля. Единственное исключение — компоненты слоя `ui`, если правила слоёв разрешают родительскому модулю импортировать `ui`.
|
||||
- Владеть архитектурными зависимостями.
|
||||
- Содержать вложенные компоненты: папка компонента включает только `{name}.tsx`, `index.ts`, `styles/`, `types/`.
|
||||
- Содержать вложенные модули.
|
||||
- Делать внешние запросы.
|
||||
- Самостоятельно получать данные.
|
||||
- Выбирать источник данных.
|
||||
- Композировать данные.
|
||||
- Вызывать сценарные хуки.
|
||||
- Оркестрировать сценарий.
|
||||
- Композировать модули.
|
||||
- Решать, как устроен процесс.
|
||||
- Содержать бизнес-логику.
|
||||
- Содержать сценарную логику.
|
||||
|
||||
Компонент может рендерить другие компоненты: соседние компоненты из `ui/` своего модуля, компоненты слоя `ui` и элементы, переданные через props. Запрет «содержать» относится к структуре папки, а не к JSX-разметке: декомпозиция компонентов остаётся плоской внутри `ui/` родительского модуля.
|
||||
|
||||
Если компоненту требуется что-то из запрещённого списка, он перестаёт быть компонентом и должен быть оформлен как модуль.
|
||||
|
||||
```text
|
||||
auth/
|
||||
├── ui/
|
||||
│ └── logout-button/
|
||||
│ ├── logout-button.tsx
|
||||
│ ├── styles/
|
||||
│ │ └── logout-button.module.css
|
||||
│ ├── types/
|
||||
│ │ └── logout-button-props.type.ts
|
||||
│ └── index.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Что считается модулем
|
||||
|
||||
Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу.
|
||||
|
||||
## Группы модулей
|
||||
|
||||
Слой может содержать не только модули, но и группы модулей. По умолчанию модуль лежит прямо в слое; группу вводят только когда модулей много и нужна явная классификация.
|
||||
|
||||
Группа — навигационная папка внутри слоя или другой группы. Она классифицирует модули по типу, предметной области, продуктовой зоне или runtime-назначению, но сама не является модулем.
|
||||
|
||||
Жёсткие правила группы:
|
||||
|
||||
- группа не имеет public API;
|
||||
- группа не содержит `index.ts`;
|
||||
- группа не импортируется внешним кодом;
|
||||
- группа не владеет логикой, состоянием, deps или runtime-сборкой;
|
||||
- группа может содержать другие группы и конечные модули.
|
||||
|
||||
Модулем считается конечная папка с самостоятельной ответственностью и публичной границей.
|
||||
|
||||
```text
|
||||
src/business/
|
||||
├── app/ # группа
|
||||
│ ├── auth/ # business-модуль
|
||||
│ └── profile/ # business-модуль
|
||||
└── cms/ # группа
|
||||
├── content/ # business-модуль
|
||||
└── media/ # business-модуль
|
||||
```
|
||||
|
||||
Примеры модулей:
|
||||
|
||||
- `compositions/pages/home/` — модуль page composition.
|
||||
- `compositions/layouts/main/` — модуль layout composition.
|
||||
- `compositions/screens/profile/` — модуль screen composition.
|
||||
- `compositions/widgets/page-heading/` — модуль widget composition.
|
||||
- `business/auth/` — модуль бизнес-домена.
|
||||
- `infra/theme/` — модуль инфраструктурного сервиса.
|
||||
- `ui/button/` — модуль UI-kit сущности.
|
||||
- `compositions/pages/home/parts/hero-section/` — вложенный модуль page composition.
|
||||
|
||||
Не считаются модулями:
|
||||
|
||||
- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/`, `providers/` — это сегменты.
|
||||
- `compositions/pages/`, `business/app/`, `business/cms/` — это группы, если в них нет `index.ts`.
|
||||
- `compositions/pages/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента.
|
||||
|
||||
## Типы модулей
|
||||
|
||||
Тип модуля определяет обязательный корневой файл и стартовую структуру.
|
||||
|
||||
### Композиционный модуль
|
||||
|
||||
Композиционный модуль — модуль внутри `compositions`, который участвует в сборке страниц, маршрутов и крупных продуктовых частей интерфейса.
|
||||
|
||||
Он может быть page, layout, screen, widget, block, entry-point, CMS-entry, route segment или другим типом композиции, выбранным командой.
|
||||
|
||||
```text
|
||||
compositions/pages/profile/
|
||||
├── profile.page.tsx
|
||||
├── profile-business-composition.ts
|
||||
├── providers/
|
||||
├── hooks/
|
||||
├── stores/
|
||||
├── parts/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Композиционный модуль может импортировать другие composition modules через public API. Это отличие слоя `compositions`: внутри него допускается графовая композиция.
|
||||
|
||||
При этом deep imports запрещены.
|
||||
|
||||
```ts
|
||||
// Хорошо
|
||||
import { useProfilePageStore } from '@/compositions/pages/profile'
|
||||
|
||||
// Плохо
|
||||
import { useProfilePageStore } from '@/compositions/pages/profile/hooks/use-profile-page-store.hook'
|
||||
```
|
||||
|
||||
### UI-модуль
|
||||
|
||||
Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне:
|
||||
|
||||
```text
|
||||
button/
|
||||
├── button.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу.
|
||||
|
||||
### Бизнес-модуль
|
||||
|
||||
Бизнес-модуль — модуль, который строится вокруг публичного API фабрики.
|
||||
|
||||
Business-модуль содержит доменную логику, типы, hooks, services, mappers и helpers. Он не содержит React-компоненты и не возвращает компоненты из фабрики.
|
||||
|
||||
Бизнес-модуль обязан иметь фабрику в корне:
|
||||
|
||||
```text
|
||||
auth/
|
||||
├── auth.factory.ts
|
||||
├── index.ts
|
||||
└── types/
|
||||
```
|
||||
|
||||
Фабрика возвращает публичный API модуля для использования в runtime.
|
||||
|
||||
### Инфраструктурный модуль
|
||||
|
||||
Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции.
|
||||
|
||||
Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса.
|
||||
|
||||
```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/ # компоненты модуля, кроме business-модулей
|
||||
├── parts/ # вложенные модули
|
||||
├── providers/ # провайдеры модуля
|
||||
├── hooks/ # хуки
|
||||
├── stores/ # сторы состояния
|
||||
├── services/ # сценарии и операции модуля
|
||||
├── mappers/ # трансформация на границе ответственности
|
||||
├── types/ # типы
|
||||
├── styles/ # стили
|
||||
├── lib/ # утилиты модуля
|
||||
├── config/ # константы и конфигурация
|
||||
└── index.ts # публичный API
|
||||
```
|
||||
|
||||
Подробное описание сегментов — в разделе [Сегменты](./segments.md).
|
||||
|
||||
## Публичный 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 экспорты. Это жёсткое правило без исключений.
|
||||
|
||||
```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'
|
||||
```
|
||||
|
||||
Composition module экспортирует через `index.ts` только безопасный контракт, который нужен другим composition modules или `app`: page/layout/screen/widget, provider, hooks доступа, типы. Внутренние stores, context objects и функции создания состояния не экспортируются без необходимости.
|
||||
|
||||
Stateful module по умолчанию не экспортирует raw context, `StoreApi`, mutable singleton, persistence key или concrete adapter. Экспортируй domain/technical commands, selectors и access hooks. Каждый mutable export требует реального внешнего consumer и отдельного обоснования.
|
||||
|
||||
Если layout, screen или widget импортируют hooks из page composition, не смешивайте в одном public API готовую page composition и hooks для дочерних модулей: это может создать runtime-цикл.
|
||||
|
||||
```ts
|
||||
// compositions/pages/profile/index.ts
|
||||
export { ProfilePageProvider } from './providers/profile-page.provider'
|
||||
export { useProfilePageStore } from './hooks/use-profile-page-store.hook'
|
||||
export { useProfileBusinessComposition } from './hooks/use-profile-business-composition.hook'
|
||||
|
||||
export type { ProfilePageState } from './types/profile-page-state.type'
|
||||
```
|
||||
|
||||
## Фабрика
|
||||
|
||||
Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный logic API фабрики.
|
||||
|
||||
Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика.
|
||||
|
||||
Фабрика не возвращает React-компоненты, layouts, guards, boundaries, providers или page-level wrappers. Business-модуль не содержит React-компоненты: UI-решения домена размещаются в `compositions`, а полностью универсальные UI-компоненты — в `ui`.
|
||||
|
||||
Модуль без runtime-capabilities экспортирует фабрику без аргументов. Модуль с зависимостями экспортирует фабрику, принимающую `deps`: API других доменов и внешние возможности, описанные бизнес-языком. Source/query hooks, state stores, subscriptions, browser API, clock и другие runtime-механизмы также считаются dependencies. Типы всегда экспортируются напрямую через `export type`, но `import type` не разрешает протащить в business generated DTO, SDK/store/query contracts.
|
||||
|
||||
Runtime-сборка фабрики с реальными SDK, storage, infra-клиентами, source hooks, stores, events и browser API происходит в `compositions/business/{domain}` через отдельные adapters. Builder явно создаёт или получает scoped runtime instances без I/O, создаёт adapters поверх них и передаёт adapters factory. Конечный граф business API собирается в месте, которое владеет lifecycle: page composition, route composition, application-lifetime composition provider, request scope или test setup.
|
||||
|
||||
### Примеры
|
||||
|
||||
Подробные правила фабрики см. в [Business-фабрика](./business-factory.md).
|
||||
|
||||
Пример runtime-сборки business-фабрик см. в [Business composition](../examples/business-composition.md).
|
||||
|
||||
Пример page-level Provider в React см. в [Композиция через Provider](../examples/react/composition-provider.md).
|
||||
|
||||
Примеры разных структур слоя `compositions` см. в [Структуры compositions](../examples/react/composition-structures.md).
|
||||
|
||||
## Жизненный цикл
|
||||
|
||||
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
|
||||
|
||||
- Нужен одной странице, route branch или крупной продуктовой части интерфейса → внутри соответствующего composition module.
|
||||
- Нужен нескольким частям одной страницы → внутри page composition или другого общего composition scope.
|
||||
- Нужен нескольким страницам или маршрутам → отдельный composition module внутри `compositions`.
|
||||
- Абстрактный UI без бизнес-логики → `ui/`.
|
||||
- Сценарий, product data contract, domain state, тип или доменная логика → `business/{domain}/`.
|
||||
- Concrete implementation business dependency → adapter в `compositions/business/{domain}/`.
|
||||
- Технический сервис → `infra/`.
|
||||
- Общая чистая утилита → `shared/`.
|
||||
|
||||
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||
229
docs/canons/monorepo.md
Normal file
229
docs/canons/monorepo.md
Normal file
@@ -0,0 +1,229 @@
|
||||
---
|
||||
title: Монорепозитории
|
||||
description: Правила применения SLM Design для frontend-проектов, находящихся в монорепозитории
|
||||
---
|
||||
|
||||
# Монорепозитории
|
||||
|
||||
Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов.
|
||||
|
||||
## Определение
|
||||
|
||||
**Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв `ui`, `infra` и `shared`.**
|
||||
|
||||
## Базовая структура
|
||||
|
||||
Каждое приложение внутри `apps/` сохраняет собственную SLM-структуру в `src/`.
|
||||
|
||||
```text
|
||||
repo/
|
||||
├── apps/
|
||||
│ ├── web/
|
||||
│ │ └── src/
|
||||
│ │ ├── app/
|
||||
│ │ ├── compositions/
|
||||
│ │ ├── 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`, `compositions` и `business` остаются внутри конкретного приложения.
|
||||
|
||||
```text
|
||||
apps/web/src/app/
|
||||
apps/web/src/compositions/
|
||||
apps/web/src/business/
|
||||
```
|
||||
|
||||
`app` привязан к фреймворку и entry points приложения.
|
||||
|
||||
`compositions` привязан к страницам, маршрутам и крупным продуктовым частям интерфейса конкретного приложения. Этот слой не выносится в `packages/*`, потому что отражает продуктовую сборку приложения.
|
||||
|
||||
`business` не выносится в `packages/*`. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой.
|
||||
|
||||
## Что можно выносить
|
||||
|
||||
В пакеты выносится только код из `ui`, `infra` и `shared`, который уже нужен двум фронтенд-приложениям либо имеет явно зафиксированный межприложенческий ownership/reuse-контракт.
|
||||
|
||||
| Группа | Что выносить | Пример |
|
||||
|--------|--------------|--------|
|
||||
| `packages/ui/*` | Самостоятельные UI-модули без бизнес-логики | `packages/ui/button` |
|
||||
| `packages/infra/*` | Самостоятельные технические сервисы | `packages/infra/backend-api` |
|
||||
| `packages/shared` | Общие утилиты, helpers и фундаментальный код | `packages/shared` |
|
||||
|
||||
По умолчанию начинай внутри приложения. Пакет можно создать до второго consumer только при явном архитектурном решении, известном consumer и стабильном межприложенческом контракте. Абстрактная «общая природа» сама по себе недостаточна.
|
||||
|
||||
## 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: `app` подключает `compositions`, `compositions` связывает `business`, `infra`, `ui` и `shared`, а `business` вызывает concrete runtime-capabilities только через переданные фабрике `deps`.
|
||||
|
||||
Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях.
|
||||
|
||||
## Когда не выносить
|
||||
|
||||
Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API.
|
||||
|
||||
Фактическое использование в одном приложении допустимо для package только при зафиксированном втором consumer или внешнем ownership/reuse-контракте. Иначе сохрани локальную колокацию.
|
||||
|
||||
```text
|
||||
# Плохо
|
||||
apps/web/src/compositions/pages/home/parts/promo-section/
|
||||
packages/ui/promo-section/
|
||||
```
|
||||
|
||||
Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным composition module.
|
||||
|
||||
## Конфигурационные пакеты
|
||||
|
||||
Конфигурационные пакеты не относятся к 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.
|
||||
- Модуль можно размещать в package при реальном втором consumer или явном межприложенческом ownership/reuse-контракте.
|
||||
- `app`, `compositions` и `business` не выносятся в пакеты.
|
||||
- Проектные стили, типы приложения и продуктовые конфиги не выносятся в `packages/shared`.
|
||||
- Пакеты не импортируют приложения.
|
||||
- Межпакетные импорты идут только через публичный API.
|
||||
- Deep imports внутрь пакетов запрещены.
|
||||
- Локальная колокация важнее преждевременного выноса в `packages/*`.
|
||||
222
docs/canons/segments.md
Normal file
222
docs/canons/segments.md
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
title: Сегменты
|
||||
description: Сегменты внутри модуля (ui/, parts/, hooks/ и др.), назначение и правила размещения файлов
|
||||
---
|
||||
|
||||
# Сегменты
|
||||
|
||||
Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит.
|
||||
|
||||
## Определение
|
||||
|
||||
**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.**
|
||||
|
||||
## Обзор
|
||||
|
||||
| Сегмент | Содержимое |
|
||||
|---------|------------|
|
||||
| `ui/` | Презентационные компоненты родительского модуля |
|
||||
| `parts/` | Вложенные модули со своими сегментами |
|
||||
| `providers/` | Провайдеры модуля |
|
||||
| `hooks/` | React-хуки |
|
||||
| `stores/` | Сторы состояния |
|
||||
| `services/` | Сценарии и операции владельца module |
|
||||
| `mappers/` | Трансформация данных между форматами |
|
||||
| `types/` | TypeScript-типы и интерфейсы |
|
||||
| `styles/` | Стили |
|
||||
| `lib/` | Утилиты и хелперы модуля |
|
||||
| `config/` | Константы и конфигурация |
|
||||
|
||||
Сегменты не являются обязательными. Например, `providers/` нужен только модулю, который владеет провайдерами. Если provider, store или guard относится к конкретной странице или маршруту, он размещается внутри соответствующего composition module, а не в `infra` или `shared`.
|
||||
|
||||
Business-модули не используют `ui/` для React-компонентов. Доменный logic API живёт в factory/services/hook wrappers/mappers. React tree, guards, layouts и visual fallbacks размещаются в consumer compositions; concrete domain source hooks/stores — в adapters `compositions/business/{domain}`.
|
||||
|
||||
## Сегмент ui/
|
||||
|
||||
Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля.
|
||||
|
||||
Компонент в `ui/`:
|
||||
|
||||
- Находится в собственной папке.
|
||||
- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`.
|
||||
- Не содержит вложенные компоненты и модули — папка компонента остаётся плоской.
|
||||
- Может рендерить соседние компоненты из `ui/` своего модуля и компоненты слоя `ui`, если правила слоёв разрешают родительскому модулю импортировать `ui`.
|
||||
- Не импортирует другой код проекта за пределами родительского модуля.
|
||||
- Не делает внешние запросы.
|
||||
- Не вызывает сценарные хуки.
|
||||
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
|
||||
- Не содержит бизнес-логику или сценарную логику.
|
||||
|
||||
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент).
|
||||
|
||||
Корневой файл модуля в `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
|
||||
compositions/pages/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.page.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — компонент родительского модуля без собственной архитектурной ответственности.
|
||||
|
||||
Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке.
|
||||
|
||||
Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше.
|
||||
|
||||
## Сегмент providers/
|
||||
|
||||
Провайдеры модуля: React Context providers, провайдеры scope-состояния, провайдеры композиции фабрик или другие обёртки, которые принадлежат модулю.
|
||||
|
||||
```text
|
||||
providers/
|
||||
├── profile-page.provider.tsx
|
||||
└── profile-business-composition.provider.tsx
|
||||
```
|
||||
|
||||
Provider размещается в том модуле, который владеет соответствующим состоянием или композицией. Page-level provider живёт в page composition module; application-level provider, завязанный на фреймворк, подключается в `app`, но реализуется в нижнем подходящем слое.
|
||||
|
||||
## Сегмент hooks/
|
||||
|
||||
React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты.
|
||||
|
||||
```text
|
||||
hooks/
|
||||
├── use-auth.hook.ts
|
||||
├── use-session.hook.ts
|
||||
└── use-permissions.hook.ts
|
||||
```
|
||||
|
||||
В business-модуле `hooks/` содержит только wrappers, созданные поверх dependency hooks, переданных фабрике. Business не импортирует React state/effect APIs, SWR, TanStack Query, Apollo или другой hook runtime напрямую.
|
||||
|
||||
Concrete source hook реализуется adapter-ом в `compositions/business/{domain}` и возвращает business-owned result type. Business wrapper нормализует данные и заменяет source error собственной domain error.
|
||||
|
||||
## Сегмент stores/
|
||||
|
||||
Сторы состояния composition/infra/UI module. Конкретная реализация зависит от выбранного state manager (Zustand, MobX, Redux и т.д.).
|
||||
|
||||
```text
|
||||
stores/
|
||||
├── auth.store.ts
|
||||
└── session.store.ts
|
||||
```
|
||||
|
||||
Если состояние нужно всей странице, concrete store живёт в page composition module. Если состояние относится к бизнес-домену, business владеет state model, transitions и state port. Concrete Zustand/Redux/MobX adapter factory реализуется в `compositions/business/{domain}`, передаётся через `deps`, принимает initial domain state от business-фабрики и возвращает concrete port.
|
||||
|
||||
Для каждого store определи creator, scope, количество instances и cleanup. Module singleton допустим только для явно доказанного application/process lifetime.
|
||||
|
||||
## Сегмент services/
|
||||
|
||||
Сценарии и операции module. Содержимое зависит от слоя и владельца.
|
||||
|
||||
```text
|
||||
services/
|
||||
├── auth.service.ts
|
||||
└── token.service.ts
|
||||
```
|
||||
|
||||
Правила по слоям:
|
||||
|
||||
- `business/services` реализует доменные сценарии только поверх `deps` фабрики;
|
||||
- `compositions/business/{domain}/adapters` реализует concrete product/runtime dependencies, а не `services/` обычной composition;
|
||||
- composition `services/` может оркестрировать готовые business API и технические infra-сервисы, но не обращаться к product source напрямую;
|
||||
- `infra/services` реализует технический сервис или transport;
|
||||
- `ui` и `shared` не выполняют product I/O.
|
||||
|
||||
Business service не импортирует SDK, generated API, HTTP-клиент, storage, env, browser API, React/SWR/query runtime или concrete store напрямую.
|
||||
|
||||
## Сегмент mappers/
|
||||
|
||||
Функции трансформации данных на границе ответственности module.
|
||||
|
||||
```text
|
||||
mappers/
|
||||
├── map-user.ts
|
||||
├── map-product.ts
|
||||
└── map-order-to-dto.ts
|
||||
```
|
||||
|
||||
В business-модулях mappers защищают public contract от ненадёжной runtime-границы: преобразуют `unknown` в доменную модель, отклоняют невалидные структуры и не импортируют concrete DTO SDK.
|
||||
|
||||
Dependency adapter может преобразовать доменные аргументы в transport payload, но не создаёт доменную модель из ответа, domain error или business fallback. Domain-to-ViewModel mapping принадлежит потребительской composition, если описывает только представление.
|
||||
|
||||
## Сегмент types/
|
||||
|
||||
TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов.
|
||||
|
||||
```text
|
||||
types/
|
||||
├── user.type.ts
|
||||
└── session.type.ts
|
||||
```
|
||||
|
||||
В business-модулях `types/` содержит собственные доменные типы, `{Domain}Api`, `{Domain}Deps`, `{Domain}Factory`, dependency hook/state result types и доменные error codes. Generated DTO, SDK-типы, `StoreApi`, query-library types и типы HTTP-клиента не входят в контракт business-модуля.
|
||||
|
||||
## Сегмент 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
|
||||
```
|
||||
214
docs/canons/validation.md
Normal file
214
docs/canons/validation.md
Normal file
@@ -0,0 +1,214 @@
|
||||
---
|
||||
title: Архитектурная проверка
|
||||
description: Блокирующие проверки SLM перед завершением создания, рефакторинга и архитектурного ревью
|
||||
---
|
||||
|
||||
# Архитектурная проверка
|
||||
|
||||
Не считай задачу завершённой только потому, что код компилируется или UI отображается. Проверь архитектурное решение, runtime-цепочку и обязательные тестовые границы.
|
||||
|
||||
## Проверка владельца
|
||||
|
||||
- У каждой ответственности один явный владелец.
|
||||
- Product state и сценарии принадлежат business-домену.
|
||||
- Page/route UI-state принадлежит соответствующему composition module.
|
||||
- Concrete technical service принадлежит infra-модулю.
|
||||
- Concrete business dependency adapter принадлежит `compositions/business/{domain}`.
|
||||
- Provider находится у владельца scope, а не в generic infra-модуле.
|
||||
- Код не поднят в общий слой или package без реального consumer.
|
||||
|
||||
## Проверка runtime-цепочки
|
||||
|
||||
Для каждого `{Domain}Api` проследи цепочку сборки:
|
||||
|
||||
```text
|
||||
graph owner
|
||||
→ per-domain builder
|
||||
→ dependency adapters + business factory
|
||||
→ {Domain}Api
|
||||
→ provider/entry
|
||||
```
|
||||
|
||||
И отдельно цепочку вызова:
|
||||
|
||||
```text
|
||||
consumer
|
||||
→ {Domain}Api
|
||||
→ business scenario
|
||||
→ {Domain}Deps
|
||||
→ dependency adapter
|
||||
→ source/runtime
|
||||
```
|
||||
|
||||
Если отсутствует хотя бы одно необходимое звено, домен не подключён. Тип будущего API, пустой provider или неиспользуемый builder не заменяет runtime-сборку.
|
||||
|
||||
Для каждого route/page entry проверь, что он достигает готового composition module. `app` не должен реализовывать screen/layout/product wiring самостоятельно.
|
||||
|
||||
## Проверка business imports
|
||||
|
||||
В production-коде `business/**` не должно быть прямых runtime или type-only imports из concrete runtimes:
|
||||
|
||||
- `infra`, `compositions`, `app`;
|
||||
- SDK/generated clients;
|
||||
- SWR/TanStack Query/Apollo;
|
||||
- Zustand/Redux/MobX/RxJS store runtime;
|
||||
- React state/effect runtime;
|
||||
- storage/browser API;
|
||||
- env/event bus/clock/random implementations.
|
||||
|
||||
Разрешены собственные файлы, type-only business contracts, `shared` без runtime-capabilities и чистые детерминированные библиотеки.
|
||||
|
||||
Проверь не только import paths, но и re-export/barrel/helper, который может скрывать запрещённую зависимость.
|
||||
|
||||
## Проверка deps
|
||||
|
||||
- Каждая runtime-capability присутствует в `{Domain}Deps`.
|
||||
- Контракт принадлежит business и назван доменным языком.
|
||||
- В `Deps` нет client/SDK/generated operation/StoreApi/query-library types.
|
||||
- Dependency hook возвращает business-owned source result.
|
||||
- State dependency использует business-owned state port.
|
||||
- External result имеет `unknown`, если требует runtime validation.
|
||||
- Subscription возвращает cleanup.
|
||||
- Cross-domain API сужен до реально используемых методов.
|
||||
|
||||
## Проверка adapters
|
||||
|
||||
- Для каждой runtime-capability есть явный adapter.
|
||||
- Adapter находится в `compositions/business/{domain}`.
|
||||
- Builder не содержит inline integration logic.
|
||||
- Adapter не формирует domain error.
|
||||
- Adapter не выполняет domain normalization.
|
||||
- Adapter не выбирает business fallback.
|
||||
- Private adapters отсутствуют в public `index.ts`.
|
||||
- Concrete transport imports не протекают в обычные consumer compositions.
|
||||
|
||||
## Проверка product data
|
||||
|
||||
- Page/layout/screen/widget получает product data через `{Domain}Api`.
|
||||
- Нет прямого вызова SDK/client/generated operation из потребительской composition.
|
||||
- Нет product storage access в UI/component/composition service.
|
||||
- DTO не используется как domain/view contract без business normalization.
|
||||
- Один источник не имеет параллельного прямого и business-пути.
|
||||
|
||||
## Проверка ошибок
|
||||
|
||||
Для каждого public runtime operation проверь:
|
||||
|
||||
- rejected dependency;
|
||||
- synchronous throw dependency;
|
||||
- `undefined`/`null`/empty body;
|
||||
- объект неправильной формы;
|
||||
- source hook error;
|
||||
- invalid state/storage value;
|
||||
- неизвестную runtime-ошибку.
|
||||
|
||||
Во всех случаях наружу выходит только domain error со стабильным `code`. Source error сохраняется в `cause`, но не становится consumer contract. Technical failure и malformed response нельзя превращать в fallback; fallback разрешён только для валидного доменного исхода.
|
||||
|
||||
Не оставляй формулировку «domain error, если контракт это обещает». Business public contract всегда обещает только domain errors.
|
||||
|
||||
## Проверка state и hooks
|
||||
|
||||
- Business владеет domain state model, но не concrete state manager.
|
||||
- Zustand/Redux/MobX store создаётся adapter-ом, не factory.
|
||||
- SWR/Query hook создаётся adapter-ом, не business-модулем.
|
||||
- Dependency hook является non-throwing/non-Suspense и передаёт technical error через business-owned result.
|
||||
- Business wrapper вызывает dependency hook и возвращает собственный result type.
|
||||
- Business wrapper преобразует error result и ошибки callbacks в domain errors.
|
||||
- Store/query library types отсутствуют в public API и `Deps`.
|
||||
- Определены creator, scope, количество instances и cleanup.
|
||||
- Module singleton используется только при явно доказанном application/process lifetime.
|
||||
- Provider не создаёт ложное впечатление владения instance, созданным на module scope.
|
||||
|
||||
## Проверка graph
|
||||
|
||||
- Per-domain builders собирают только свои фабрики.
|
||||
- Graph owner назван и соответствует lifecycle.
|
||||
- Домены создаются в топологическом порядке.
|
||||
- Runtime-циклы отсутствуют.
|
||||
- Один и тот же graph не копируется по нескольким providers без обоснования scope.
|
||||
- Screen/widget не собирает graph самостоятельно.
|
||||
- Provider value имеет точный тип.
|
||||
- Нет `Partial<Graph>` с unchecked cast к полному graph.
|
||||
- Subscription, timer, socket и другие resources имеют cleanup/dispose.
|
||||
- Pending operation не может записать stale state после invalidation/unmount без явно принятой политики.
|
||||
|
||||
## Проверка public API
|
||||
|
||||
- Межмодульные импорты идут через реальный public entrypoint.
|
||||
- Import alias/package export существует физически.
|
||||
- Group не имеет `index.ts`.
|
||||
- Business `index.ts` экспортирует runtime только factory, остальное через `export type`.
|
||||
- Integration module экспортирует builder и необходимые type-only integration input contracts.
|
||||
- Raw context, raw store, mutable singleton, adapter, generated operation и persistence key закрыты.
|
||||
- Каждый export имеет реального внешнего consumer.
|
||||
- Deep imports отсутствуют, включая tests уровня public contract.
|
||||
|
||||
## Проверка тестов
|
||||
|
||||
Business-модуль не завершён без factory-level tests.
|
||||
|
||||
Обязательный минимум:
|
||||
|
||||
1. Форма public API фабрики.
|
||||
2. Happy path каждого runtime method/hook.
|
||||
3. Invalid dependency response.
|
||||
4. Rejected dependency.
|
||||
5. Synchronous throw каждого обычного method/callback/state/lifecycle dependency. Dependency hooks проверяются отдельным non-throwing contract.
|
||||
6. Domain error `code` и `cause`.
|
||||
7. Порядок side effects и остановка после ошибки.
|
||||
8. State transitions и concurrent calls.
|
||||
|
||||
`compositions/business/{domain}` не завершён без assembly tests:
|
||||
|
||||
1. Factory получает правильные adapters.
|
||||
2. Каждый adapter вызывает нужный runtime source с правильным payload.
|
||||
3. Builder не делает I/O при создании API.
|
||||
4. Cross-domain API передан в правильном виде.
|
||||
5. Private adapters не раскрыты public API.
|
||||
6. Adapter пробрасывает source error без создания domain error.
|
||||
7. Dependency hook не бросает и не использует Suspense/throw-on-error mode.
|
||||
8. Lifecycle cleanup проверен, если есть subscriptions/resources.
|
||||
|
||||
Colocated tests обязательны для mappers, normalizers, type guards, domain errors и другой внутренней runtime-safe логики. Они дополняют, но не заменяют factory-level tests. Подробная матрица находится в [Тестировании business-модулей](../examples/business-testing.md).
|
||||
|
||||
Проверь наличие исполняемого test script и test runner именно в изменяемом workspace. Root task без локального script не является выполненной тестовой инфраструктурой.
|
||||
|
||||
## Проверка целостности репозитория
|
||||
|
||||
- Все imports разрешаются.
|
||||
- Все упомянутые modules и public entrypoints существуют.
|
||||
- Direct runtime packages объявлены в package текущего workspace.
|
||||
- Старый provider/store/source path удалён после миграции, если больше не используется.
|
||||
- Нет speculative scaffold с пустым graph, несуществующими доменами или placeholder contracts.
|
||||
- Template исправлен, если именно он системно создаёт нарушение.
|
||||
- Выполнены доступные typecheck, tests, lint/build и `git diff --check`.
|
||||
|
||||
## Формат архитектурного ревью
|
||||
|
||||
Для каждого нарушения укажи:
|
||||
|
||||
1. Путь и строку.
|
||||
2. Нарушенный invariant.
|
||||
3. Runtime или maintenance риск.
|
||||
4. Минимальную корректную границу.
|
||||
5. Необходимые tests.
|
||||
|
||||
Отделяй обязательное нарушение от необязательного улучшения. Не предлагай большую миграцию, если нарушение можно устранить локально без создания второй архитектуры.
|
||||
|
||||
## Финальный gate
|
||||
|
||||
Перед завершением ответь «да» на все вопросы:
|
||||
|
||||
- Архитектурная роль изменения определена?
|
||||
- Владелец ответственности и state определён?
|
||||
- Все runtime-capabilities проходят через правильную границу?
|
||||
- Product data проходит через business API?
|
||||
- Business вызывает только переданные deps и собственную детерминированную логику?
|
||||
- Наружу выходят только domain errors?
|
||||
- Adapters существуют и закрыты?
|
||||
- Graph и lifecycle определены?
|
||||
- Public API минимален и разрешим?
|
||||
- Обязательные tests созданы и запущены?
|
||||
- Изменение не оставило старый параллельный путь?
|
||||
|
||||
Если хотя бы один ответ «нет», задача не завершена.
|
||||
390
docs/examples/business-composition.md
Normal file
390
docs/examples/business-composition.md
Normal file
@@ -0,0 +1,390 @@
|
||||
---
|
||||
title: Business composition
|
||||
description: Пример runtime-сборки business-фабрик в compositions/business
|
||||
---
|
||||
|
||||
# Business composition
|
||||
|
||||
`compositions/business/{domain}` — composition module, который собирает конкретную business-фабрику с реальными runtime-зависимостями приложения.
|
||||
|
||||
Этот модуль не является бизнес-доменом. Он находится на слое `compositions`, потому что связывает `business`, `infra`, SDK, storage, browser API и другие внешние runtime-источники.
|
||||
|
||||
Это единственная integration-зона concrete product dependencies. Page, layout, screen и widget не импортируют SDK/client/storage напрямую и получают product data только через готовый `{Domain}Api`.
|
||||
|
||||
## Структура
|
||||
|
||||
```text
|
||||
src/compositions/business/
|
||||
├── auth/
|
||||
│ ├── create-auth-business.ts
|
||||
│ ├── create-auth-business.test.ts
|
||||
│ ├── adapters/
|
||||
│ │ ├── phone-auth.adapter.ts
|
||||
│ │ ├── session.adapter.ts
|
||||
│ │ ├── auth-session-events.adapter.ts
|
||||
│ │ └── zustand-auth-state.adapter.ts
|
||||
│ └── index.ts
|
||||
├── user/
|
||||
│ ├── create-user-business.ts
|
||||
│ ├── create-user-business.test.ts
|
||||
│ ├── adapters/
|
||||
│ │ ├── user-profile.adapter.ts
|
||||
│ │ └── user-storage.adapter.ts
|
||||
│ ├── types/
|
||||
│ │ └── create-user-business-deps.type.ts
|
||||
│ └── index.ts
|
||||
└── content/
|
||||
├── create-content-business.ts
|
||||
├── create-content-business.test.ts
|
||||
├── adapters/
|
||||
│ └── content-api.adapter.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Если business-домены сгруппированы, `compositions/business` повторяет тот же относительный путь. Например: `business/app/auth` соответствует `compositions/business/app/auth`, `business/cms/content` соответствует `compositions/business/cms/content`.
|
||||
|
||||
Сегменты добавляются только по необходимости, но каждая concrete business dependency всегда оформляется отдельным файлом в `adapters/`. Не оставляй короткий adapter inline внутри builder.
|
||||
|
||||
## Ответственность
|
||||
|
||||
`compositions/business/{domain}` отвечает за adapter composition:
|
||||
|
||||
- создаёт или получает внешние клиенты из `infra`;
|
||||
- отдельными adapters адаптирует SDK, API, storage, source/query hooks, state managers, events и browser API к `deps` business-модуля;
|
||||
- вызывает business-фабрику;
|
||||
- принимает API других business-модулей, если текущий домен зависит от них;
|
||||
- экспортирует готовый `{Domain}Api` через builder-функцию;
|
||||
- тестирует сборку и корректность адаптеров.
|
||||
|
||||
`compositions/business/{domain}` не должен содержать доменную логику. Если код описывает бизнес-правило, маппинг доменной модели, доменную ошибку или сценарий, он должен жить в соответствующем `business`-модуле.
|
||||
|
||||
`compositions/business/{domain}` не должен содержать React-компоненты, layouts, guards, providers и page-level wrappers. Применение logic API в React tree выполняется в обычных composition modules страниц, layouts, screens или widgets.
|
||||
|
||||
Builder не реализует dependencies inline. Он явно создаёт scoped runtime instances без I/O, создаёт adapters поверх них и передаёт adapters фабрике.
|
||||
|
||||
## Business-контракт
|
||||
|
||||
Business-модуль объявляет dependency contract.
|
||||
|
||||
```ts
|
||||
// business/auth/types/auth-deps.type.ts
|
||||
import type { AuthState } from './auth-state.type'
|
||||
import type { VerifyPhoneCodeData } from './verify-phone-code-data.type'
|
||||
|
||||
export type AuthDeps = {
|
||||
phoneAuth: {
|
||||
requestCode: (phone: string) => Promise<unknown>
|
||||
resendCode: (challengeId: string) => Promise<unknown>
|
||||
verifyCode: (data: VerifyPhoneCodeData) => Promise<unknown>
|
||||
}
|
||||
session: {
|
||||
setToken: (token?: string | null) => void
|
||||
useToken: () => string | null | undefined
|
||||
}
|
||||
sessionEvents: {
|
||||
onInvalidated: (listener: () => void) => () => void
|
||||
}
|
||||
state: {
|
||||
create: (initialState: AuthState) => {
|
||||
get: () => AuthState
|
||||
set: (state: AuthState) => void
|
||||
useState: () => AuthState
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Business-модуль не знает, через какой SDK, backend или storage реализованы эти возможности.
|
||||
|
||||
## Adapter composition
|
||||
|
||||
Composition-адаптер знает про конкретный runtime и приводит его к business-контракту.
|
||||
|
||||
```ts
|
||||
// compositions/business/auth/adapters/phone-auth.adapter.ts
|
||||
import type { AuthDeps } from '@/business/auth'
|
||||
import type { AuthApiClient } from '@/infra/backend-api'
|
||||
|
||||
export const createPhoneAuthAdapter = (authApiClient: AuthApiClient): AuthDeps['phoneAuth'] => ({
|
||||
requestCode: (phone) => {
|
||||
return authApiClient.authOtp.phoneStart({ body: { phone } })
|
||||
},
|
||||
resendCode: (challengeId) => {
|
||||
return authApiClient.authOtp.phoneResend({ body: { challengeId } })
|
||||
},
|
||||
verifyCode: (data) => {
|
||||
return authApiClient.authOtp.phoneVerify({ body: data })
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Адаптер не формирует доменные ошибки и не выбирает доменный `code`. Он может вернуть результат внешнего вызова или пробросить ошибку dependency. Решение о доменном коде принимает business-модуль.
|
||||
|
||||
Плохо:
|
||||
|
||||
```ts
|
||||
export const createVerifyPhoneCode = (
|
||||
authApiClient: AuthApiClient,
|
||||
): AuthDeps['phoneAuth']['verifyCode'] => async (data) => {
|
||||
try {
|
||||
return await authApiClient.authOtp.phoneVerify({ body: data })
|
||||
} catch (error) {
|
||||
throw new AuthBusinessError('AUTH_PHONE_CODE_VERIFY_FAILED', error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Проблема: composition-адаптер начал владеть доменной ошибкой.
|
||||
|
||||
Хорошо:
|
||||
|
||||
```ts
|
||||
export const createVerifyPhoneCode = (
|
||||
authApiClient: AuthApiClient,
|
||||
): AuthDeps['phoneAuth']['verifyCode'] => (data) => {
|
||||
return authApiClient.authOtp.phoneVerify({ body: data })
|
||||
}
|
||||
```
|
||||
|
||||
## State adapter
|
||||
|
||||
Доменное состояние принадлежит business-контракту, но concrete state manager остаётся снаружи business.
|
||||
|
||||
```ts
|
||||
// compositions/business/auth/adapters/zustand-auth-state.adapter.ts
|
||||
import { useStore } from 'zustand'
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import type { AuthDeps, AuthState } from '@/business/auth'
|
||||
|
||||
export const authStateAdapter: AuthDeps['state'] = {
|
||||
create: (initialState) => {
|
||||
const store = createStore<AuthState>()(() => initialState)
|
||||
|
||||
return {
|
||||
get: store.getState,
|
||||
set: (state) => store.setState(state),
|
||||
useState: () => useStore(store),
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`authFactory` выбирает initial domain state и вызывает `deps.state.create(initialState)`. Он не импортирует Zustand и не раскрывает `StoreApi` через public contract. Adapter только создаёт concrete store с переданным состоянием и не выбирает доменную политику.
|
||||
|
||||
Для SWR, TanStack Query и других source hooks действует то же правило: adapter реализует business-owned hook contract, business wrapper нормализует `data`, заменяет source `error` собственной domain error и возвращает собственный result type.
|
||||
|
||||
## Lifecycle adapter
|
||||
|
||||
External event также передаётся через business-owned contract.
|
||||
|
||||
```ts
|
||||
// compositions/business/auth/adapters/auth-session-events.adapter.ts
|
||||
import type { AuthDeps } from '@/business/auth'
|
||||
import { onAuthSessionInvalidated } from '@/infra/backend-api'
|
||||
|
||||
export const authSessionEventsAdapter: AuthDeps['sessionEvents'] = {
|
||||
onInvalidated: onAuthSessionInvalidated,
|
||||
}
|
||||
```
|
||||
|
||||
Business API предоставляет domain-level operation `startSessionInvalidationTracking()`. Внутри business она вызывает `deps.sessionEvents.onInvalidated`, выполняет доменный state transition и возвращает cleanup wrapper. Ошибки регистрации, callback и cleanup заменяются `AuthBusinessError`.
|
||||
|
||||
Graph owner запускает operation после commit и вызывает возвращённый cleanup при unmount, как показано в полном provider ниже. Provider не импортирует raw infra event и не связывает его с business command самостоятельно.
|
||||
|
||||
## Builder одного домена
|
||||
|
||||
Builder собирает одну business-фабрику.
|
||||
|
||||
```ts
|
||||
// compositions/business/auth/create-auth-business.ts
|
||||
import { authFactory } from '@/business/auth'
|
||||
import { createBackendApiClient } from '@/infra/backend-api'
|
||||
import { authSessionEventsAdapter } from './adapters/auth-session-events.adapter'
|
||||
import { authStateAdapter } from './adapters/zustand-auth-state.adapter'
|
||||
import { createPhoneAuthAdapter } from './adapters/phone-auth.adapter'
|
||||
import { createSessionAdapter } from './adapters/session.adapter'
|
||||
|
||||
export const createAuthBusiness = () => {
|
||||
const authApiClient = createBackendApiClient()
|
||||
|
||||
return authFactory({
|
||||
phoneAuth: createPhoneAuthAdapter(authApiClient),
|
||||
session: createSessionAdapter(),
|
||||
sessionEvents: authSessionEventsAdapter,
|
||||
state: authStateAdapter,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Browser/application builder без cross-domain зависимостей вызывается без аргументов. Он явно создаёт runtime instances и передаёт их private adapter factories. Client/adapter constructors не выполняют I/O, не читают storage/env неявно и не запускают subscriptions; lifecycle каждого instance соответствует lifecycle builder result.
|
||||
|
||||
Request-scoped builder принимает отдельный `requestScopeInput` только с request data, а concrete client factory импортирует сам. Не используй application singleton для request credentials, cookies или tenant context.
|
||||
|
||||
## Cross-domain зависимости
|
||||
|
||||
Если один business-модуль зависит от API другого business-модуля, builder принимает уже собранный API.
|
||||
|
||||
```ts
|
||||
// compositions/business/user/types/create-user-business-deps.type.ts
|
||||
import type { AuthApi } from '@/business/auth'
|
||||
|
||||
export type CreateUserBusinessDeps = {
|
||||
authApi: Pick<AuthApi, 'useAuth'>
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// compositions/business/user/create-user-business.ts
|
||||
import { userFactory } from '@/business/user'
|
||||
import { createBackendApiClient } from '@/infra/backend-api'
|
||||
import { createUserProfileAdapter } from './adapters/user-profile.adapter'
|
||||
import { createUserStorageAdapter } from './adapters/user-storage.adapter'
|
||||
import type { CreateUserBusinessDeps } from './types/create-user-business-deps.type'
|
||||
|
||||
export const createUserBusiness = (deps: CreateUserBusinessDeps) => {
|
||||
const apiClient = createBackendApiClient()
|
||||
|
||||
return userFactory({
|
||||
authApi: deps.authApi,
|
||||
profile: createUserProfileAdapter(apiClient),
|
||||
storage: createUserStorageAdapter(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- сначала создаются независимые домены;
|
||||
- затем создаются домены, которым нужны API уже созданных доменов;
|
||||
- browser/application builder deps содержат только API других business-фабрик;
|
||||
- request-scoped builder отделяет cross-domain API от `requestScopeInput` с request data;
|
||||
- зависимость сужается через `Pick`, если нужен один метод;
|
||||
- циклические runtime-зависимости между business API запрещены;
|
||||
- если появляется цикл, нужно пересмотреть границы доменов или вынести общий сценарий в отдельный домен.
|
||||
|
||||
## Сборка графа в месте использования
|
||||
|
||||
Конечный граф создаётся там, где понятен lifecycle: page provider, route composition, application-lifetime composition provider, request scope или test setup. Слой `app` только подключает готовую composition.
|
||||
|
||||
```tsx
|
||||
// compositions/routes/profile/providers/profile-business.provider.tsx
|
||||
'use client'
|
||||
|
||||
import { createContext, useEffect, useState, type ReactNode } from 'react'
|
||||
import { createAuthBusiness } from '@/compositions/business/auth'
|
||||
import { createUserBusiness } from '@/compositions/business/user'
|
||||
|
||||
type ProfileBusiness = {
|
||||
authApi: ReturnType<typeof createAuthBusiness>
|
||||
userApi: ReturnType<typeof createUserBusiness>
|
||||
}
|
||||
|
||||
export const ProfileBusinessContext = createContext<ProfileBusiness | null>(null)
|
||||
|
||||
const createProfileBusiness = (): ProfileBusiness => {
|
||||
const authApi = createAuthBusiness()
|
||||
const userApi = createUserBusiness({ authApi })
|
||||
|
||||
return { authApi, userApi }
|
||||
}
|
||||
|
||||
export const ProfileBusinessProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [business] = useState(createProfileBusiness)
|
||||
|
||||
useEffect(() => {
|
||||
return business.authApi.startSessionInvalidationTracking()
|
||||
}, [business.authApi])
|
||||
|
||||
return (
|
||||
<ProfileBusinessContext.Provider value={business}>
|
||||
{children}
|
||||
</ProfileBusinessContext.Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Route-level `ProfileBusinessProvider` владеет lifecycle graph. `compositions/business/*` только предоставляет чистые функции сборки. React Strict Mode может повторно вызвать lazy initializer в development, поэтому factory, builder и adapter constructors не выполняют I/O и не запускают subscriptions.
|
||||
|
||||
Graph owner импортирует builders, но не raw SDK/client/event bus для «досборки» конкретного домена. Если external event влияет на domain state, event subscription является частью `{Domain}Deps`; business API предоставляет domain-level lifecycle operation, которую provider запускает в effect и cleanup которой вызывает при unmount. Registration и cleanup errors преобразуются business-модулем в domain errors.
|
||||
|
||||
## Public API
|
||||
|
||||
`index.ts` composition-модуля экспортирует builder и type-only deps, если builder зависит от других business API.
|
||||
|
||||
```ts
|
||||
// compositions/business/auth/index.ts
|
||||
export { createAuthBusiness } from './create-auth-business'
|
||||
```
|
||||
|
||||
```ts
|
||||
// compositions/business/user/index.ts
|
||||
export { createUserBusiness } from './create-user-business'
|
||||
|
||||
export type { CreateUserBusinessDeps } from './types/create-user-business-deps.type'
|
||||
```
|
||||
|
||||
Не экспортируй из public API:
|
||||
|
||||
- внутренние SDK-клиенты;
|
||||
- generated operation trees;
|
||||
- private adapters;
|
||||
- test mocks;
|
||||
- helpers, которые нужны только для сборки.
|
||||
|
||||
Если адаптер нужен нескольким composition-модулям, сначала проверь, не является ли это infra-сервисом. Не поднимай адаптер в `shared` только ради удобного импорта.
|
||||
|
||||
## Как не превратить сборку в кашу
|
||||
|
||||
Признаки плохой сборки:
|
||||
|
||||
- один файл создаёт все API-клиенты, все dependency-адаптеры и все фабрики;
|
||||
- рядом лежат unrelated helpers для разных доменов;
|
||||
- dependency-адаптеры смешаны с domain mappers;
|
||||
- Zustand/SWR/SDK logic написана прямо внутри builder;
|
||||
- graph owner напрямую связывает raw infra event с business command;
|
||||
- business-правила реализованы в `compositions/business`;
|
||||
- public API экспортирует внутренние адаптеры;
|
||||
- невозможно протестировать сборку одного домена отдельно.
|
||||
|
||||
Что делать вместо этого:
|
||||
|
||||
- один домен runtime-сборки — один composition module;
|
||||
- dependency-адаптеры держать рядом с конкретной сборкой домена;
|
||||
- большие dependency-адаптеры выносить в `adapters/`;
|
||||
- типы сборщика выносить в `types/`, если они перестали быть локальными;
|
||||
- доменные mappers оставлять в `business/{domain}/mappers`;
|
||||
- тестировать сборку домена отдельно от полной сборки приложения.
|
||||
|
||||
## Тестирование сборки
|
||||
|
||||
Тесты `compositions/business/{domain}` не заменяют factory-level тесты business-модуля.
|
||||
|
||||
Они проверяют только composition-риск:
|
||||
|
||||
- правильные dependency-адаптеры переданы в фабрику;
|
||||
- API другой фабрики передан в нужном виде;
|
||||
- SDK operation вызывается с ожидаемым payload;
|
||||
- storage/browser adapter соответствует dependency-контракту;
|
||||
- state/query adapter соответствует business-owned contract и не раскрывает library types;
|
||||
- сборка не делает запросы во время создания business API;
|
||||
- client/adapter constructors не выполняют import-time I/O, storage access или subscriptions;
|
||||
- lifecycle operation запускается владельцем scope и вызывает cleanup;
|
||||
- минимальный API-клиент не тянет лишние generated-операции.
|
||||
|
||||
Factory-level поведение самого домена тестируется в `business/{domain}/tests/{domain}-factory`.
|
||||
|
||||
## Чеклист
|
||||
|
||||
- Runtime-сборка находится в `compositions/business/{domain}`.
|
||||
- Business-модуль не импортирует реальные SDK, API или storage.
|
||||
- Dependency-адаптер реализован на composition-слое.
|
||||
- State/query runtime реализован adapter-ом, а не импортирован business-модулем.
|
||||
- Файлы внутри модуля сборки разнесены по ответственности.
|
||||
- Runtime-зависимости между доменами передаются через builder deps.
|
||||
- Builder deps содержат только API других собранных business-фабрик.
|
||||
- Request-scoped builder отделяет cross-domain API от `requestScopeInput`.
|
||||
- Public API composition-модуля не раскрывает internal adapters.
|
||||
- Builder не содержит inline integration logic.
|
||||
- Lifecycle operation запускается после commit и имеет cleanup.
|
||||
- Сборка покрыта тестами на корректность связки deps и адаптеров.
|
||||
- Business-поведение покрыто factory-level тестами в business-модуле.
|
||||
364
docs/examples/business-testing.md
Normal file
364
docs/examples/business-testing.md
Normal file
@@ -0,0 +1,364 @@
|
||||
---
|
||||
title: Тестирование business-модулей
|
||||
description: Factory-level и colocated unit-тесты для business-фабрик SLM
|
||||
---
|
||||
|
||||
# Тестирование business-модулей
|
||||
|
||||
Business-модуль тестируется как доменный контракт приложения. Главный контракт business-модуля — фабрика и API, который она возвращает.
|
||||
|
||||
Factory-level тесты обязательны для каждого business-модуля. Assembly tests обязательны для `compositions/business/{domain}`. Внутренние colocated unit-тесты добавляются для runtime-safe логики и не заменяют проверку public API фабрики.
|
||||
|
||||
## Уровни тестов
|
||||
|
||||
Полное изменение домена проверяется на трёх границах:
|
||||
|
||||
1. Factory-level тесты.
|
||||
2. Assembly tests dependency adapters и builder.
|
||||
3. Colocated unit-тесты внутренней runtime-safe логики.
|
||||
|
||||
Factory-level тесты отвечают на вопрос: работает ли домен снаружи через публичный API фабрики.
|
||||
|
||||
Assembly tests отвечают на вопрос: правильно ли concrete runtime реализует `Deps` и передан фабрике.
|
||||
|
||||
Colocated unit-тесты отвечают на вопрос: надёжна ли внутренняя runtime-safe механика, на которой держится публичный контракт.
|
||||
|
||||
## Factory-level тесты
|
||||
|
||||
Размещение:
|
||||
|
||||
```text
|
||||
business/{domain}/tests/{domain}-factory/
|
||||
```
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
business/user/tests/user-factory/
|
||||
├── public-api.test.tsx
|
||||
├── use-current-user.test.tsx
|
||||
├── update-current-user-profile.test.ts
|
||||
├── get-stored-user-agreements.test.ts
|
||||
└── testing/
|
||||
└── create-user-deps.mock.ts
|
||||
```
|
||||
|
||||
Factory-level тесты импортируют модуль только через public API.
|
||||
|
||||
```ts
|
||||
import { userFactory } from '@/business/user'
|
||||
```
|
||||
|
||||
Factory-level тесты не импортируют:
|
||||
|
||||
- `services/*`;
|
||||
- `hooks/*`;
|
||||
- `mappers/*`;
|
||||
- `lib/*`;
|
||||
- `errors/*`;
|
||||
- любые deep imports business-модуля.
|
||||
|
||||
## Что покрывать на factory-level
|
||||
|
||||
Каждый runtime-метод, который возвращает фабрика, должен иметь factory-level тесты.
|
||||
|
||||
Обязательно проверяются:
|
||||
|
||||
- полный публичный runtime API фабрики;
|
||||
- happy path каждого метода;
|
||||
- edge cases публичного контракта;
|
||||
- корректные ответы DI-зависимостей;
|
||||
- пустые ответы DI-зависимостей;
|
||||
- невалидные ответы DI-зависимостей;
|
||||
- rejected promise от dependency;
|
||||
- синхронное исключение dependency;
|
||||
- преобразование внешних ошибок в доменные ошибки;
|
||||
- преобразование ошибок source hooks, stores и subscriptions в доменные ошибки;
|
||||
- стабильный доменный `code` для каждой ошибки public contract;
|
||||
- сохранение исходной ошибки в `cause`;
|
||||
- отсутствие зависимости public contract от `status`, `message`, `response` и других внешних полей ошибки;
|
||||
- порядок side effects;
|
||||
- отсутствие следующих side effects после ошибки;
|
||||
- hooks, если фабрика возвращает hooks.
|
||||
|
||||
Пример:
|
||||
|
||||
```ts
|
||||
const deps = createUserDepsMock({
|
||||
profile: {
|
||||
useCurrent: createCurrentUserSourceHookMock({ data: sourceUser }),
|
||||
},
|
||||
})
|
||||
const userApi = userFactory(deps)
|
||||
|
||||
await userApi.updateCurrentUserProfile(data)
|
||||
|
||||
const result = renderHook(() => userApi.useCurrentUser())
|
||||
```
|
||||
|
||||
Тест проверяет поведение `userApi`, а не внутреннее устройство `createUpdateCurrentUserProfile`.
|
||||
|
||||
## Public API тест
|
||||
|
||||
У каждого business-модуля должен быть тест, который фиксирует публичный runtime API фабрики.
|
||||
|
||||
```ts
|
||||
it('returns stable user business API', () => {
|
||||
const userApi = userFactory(createUserDepsMock())
|
||||
|
||||
expect(Object.keys(userApi).sort()).toEqual([
|
||||
'getStoredUserAgreements',
|
||||
'updateCurrentUserProfile',
|
||||
'useCurrentUser',
|
||||
])
|
||||
})
|
||||
```
|
||||
|
||||
Такой тест не заменяет сценарные тесты методов. Он только фиксирует форму API и защищает от случайного удаления или переименования методов.
|
||||
|
||||
## DI-границы
|
||||
|
||||
Любая dependency фабрики считается ненадёжной runtime-границей.
|
||||
|
||||
Для каждой dependency нужно проверить минимум:
|
||||
|
||||
- корректный успешный ответ;
|
||||
- `undefined`;
|
||||
- `null`;
|
||||
- пустой объект;
|
||||
- объект неправильной формы;
|
||||
- rejected promise;
|
||||
- синхронный throw обычного method/callback/state/lifecycle dependency;
|
||||
- повторные вызовы;
|
||||
- смену результата dependency hook или domain state.
|
||||
|
||||
Если dependency является callback'ом, проверяется порядок вызовов и payload.
|
||||
|
||||
Если dependency работает с storage, проверяются битые, устаревшие и отсутствующие данные.
|
||||
|
||||
## Hooks через фабрику
|
||||
|
||||
Hooks, которые возвращает фабрика, тестируются через factory API.
|
||||
|
||||
Проверяй:
|
||||
|
||||
- hook не делает запрос без готовых входных данных;
|
||||
- dependency hook получает ожидаемые доменные аргументы;
|
||||
- hook корректно обрабатывает смену dependency result;
|
||||
- `data` имеет доменную модель;
|
||||
- `error` имеет доменный контракт;
|
||||
- loading/refresh state соответствует собственному API модуля;
|
||||
- невалидный dependency response не попадает наружу как валидная доменная модель.
|
||||
|
||||
```ts
|
||||
const useCurrent = createCurrentUserSourceHookMock({ data: sourceUser })
|
||||
const deps = createUserDepsMock({ profile: { useCurrent } })
|
||||
const userApi = userFactory(deps)
|
||||
|
||||
const { result } = renderHook(() => userApi.useCurrentUser())
|
||||
```
|
||||
|
||||
Не тестируй hook business-модуля как отдельную публичную сущность, если он не является public API фабрики.
|
||||
|
||||
SWR/Query cache keys, provider wrapper и library-specific revalidation тестируются в assembly tests dependency adapter, а не в business factory tests.
|
||||
|
||||
## Command-сценарии
|
||||
|
||||
Для command-методов вроде `save`, `update`, `change`, `request`, `verify` проверяются:
|
||||
|
||||
- payload передаётся во внешнюю dependency в ожидаемой форме;
|
||||
- входной payload не мутируется;
|
||||
- пустой успешный ответ считается успехом, если body не нужен;
|
||||
- ошибка dependency превращается в доменную ошибку;
|
||||
- потребитель может принять решение по доменному `code`;
|
||||
- side effects выполняются в правильном порядке;
|
||||
- при ошибке одного шага следующие side effects не выполняются;
|
||||
- повторный вызов не использует устаревшее состояние, если это важно для сценария.
|
||||
|
||||
Если сценарий использует несколько зависимостей, тест должен явно фиксировать порядок.
|
||||
|
||||
## Colocated unit-тесты
|
||||
|
||||
Colocated unit-тесты размещаются рядом с файлом, который владеет runtime-логикой.
|
||||
|
||||
```text
|
||||
business/{domain}/
|
||||
├── errors/
|
||||
│ ├── {domain}-business.error.ts
|
||||
│ └── {domain}-business.error.test.ts
|
||||
├── lib/
|
||||
│ ├── normalize-{entity}.ts
|
||||
│ └── normalize-{entity}.test.ts
|
||||
├── mappers/
|
||||
│ ├── map-{entity}.ts
|
||||
│ └── map-{entity}.test.ts
|
||||
├── services/
|
||||
│ ├── update-{entity}.service.ts
|
||||
│ └── update-{entity}.service.test.ts
|
||||
└── hooks/
|
||||
├── use-{scenario}.hook.ts
|
||||
└── use-{scenario}.hook.test.tsx
|
||||
```
|
||||
|
||||
Colocated unit-тесты нужны для:
|
||||
|
||||
- mappers;
|
||||
- normalizers;
|
||||
- type guards;
|
||||
- runtime-safe helpers;
|
||||
- domain errors;
|
||||
- сложных services;
|
||||
- hook wrappers со сложной нормализацией domain result/error;
|
||||
- storage parsers;
|
||||
- fallback-логики.
|
||||
|
||||
Colocated unit-тесты не нужны для:
|
||||
|
||||
- type-only файлов;
|
||||
- `index.ts` без runtime-логики;
|
||||
- простых re-export файлов;
|
||||
- статических config-файлов без branching;
|
||||
- типов, которые проверяются typecheck'ом.
|
||||
|
||||
## Почему colocated тесты не заменяют factory-level
|
||||
|
||||
Colocated тест может доказать, что mapper работает правильно, но он не доказывает, что фабрика использует этот mapper в публичном сценарии.
|
||||
|
||||
Colocated тест может доказать, что service обрабатывает ошибку, но он не доказывает, что service реально попал в public API фабрики.
|
||||
|
||||
Factory-level тест проверяет интеграцию внутренних частей business-модуля как чёрный ящик.
|
||||
|
||||
Правило:
|
||||
|
||||
- сначала покрывай public API фабрики;
|
||||
- затем усиливай покрытие colocated тестами там, где есть runtime-safe логика.
|
||||
|
||||
## Маппинг и runtime safety
|
||||
|
||||
Если business-модуль получает данные с любой dependency boundary, тестируй не только happy path. Boundary включает методы, source hooks, stores, events и browser capabilities.
|
||||
|
||||
Проверяй:
|
||||
|
||||
- отсутствующие обязательные поля;
|
||||
- nullable-поля;
|
||||
- поля неправильного runtime-типа;
|
||||
- пустые строки;
|
||||
- пробельные строки;
|
||||
- странные идентификаторы;
|
||||
- пустые массивы;
|
||||
- не-массив вместо массива;
|
||||
- частично валидные объекты;
|
||||
- дефолтные значения;
|
||||
- domain error для malformed response, если модель не может быть безопасно построена.
|
||||
|
||||
Fallback допустим только для валидного доменного исхода, например корректно представленного отсутствия данных. Rejection, synchronous throw, source error и malformed response всегда дают domain error.
|
||||
|
||||
## Доменные ошибки
|
||||
|
||||
Business-модуль никогда не отдаёт наружу сырые ошибки SDK, HTTP-клиента, source hook, store, storage или browser API. Public contract всегда содержит только собственные domain errors.
|
||||
|
||||
Factory-level тесты должны доказывать, что потребитель может работать только с доменным `code` и не знает форму внешней ошибки.
|
||||
|
||||
Проверяй:
|
||||
|
||||
- `error.name`;
|
||||
- стабильный `error.code`;
|
||||
- сохранение `cause`;
|
||||
- отсутствие утечки DTO/HTTP-specific деталей в public contract;
|
||||
- rejected promise от разных dependencies маппится в ожидаемый доменный код;
|
||||
- синхронный throw dependency маппится в ожидаемый доменный код;
|
||||
- невалидный успешный ответ превращается в доменный код ошибки;
|
||||
- разные технические ошибки дают один код, если для потребителя это один бизнес-сценарий;
|
||||
- разные пользовательские сценарии дают разные коды, если UI должен реагировать по-разному;
|
||||
- safe fallback только для валидного доменного исхода, явно представленного dependency contract.
|
||||
|
||||
UI и i18n должны ориентироваться на `code`, а не на `message` внешней ошибки.
|
||||
|
||||
Пример factory-level проверки:
|
||||
|
||||
```ts
|
||||
it('throws domain error code when phone code verification fails', async () => {
|
||||
const externalError = new Error('Request failed with status code 500')
|
||||
const authApi = authFactory({
|
||||
phoneAuth: {
|
||||
requestCode: vi.fn(),
|
||||
resendCode: vi.fn(),
|
||||
verifyCode: vi.fn().mockRejectedValue(externalError),
|
||||
},
|
||||
session,
|
||||
sessionEvents: createAuthSessionEventsMock(),
|
||||
state: createAuthStateAdapterMock(),
|
||||
})
|
||||
|
||||
await expect(authApi.verifyPhoneCode(data)).rejects.toMatchObject({
|
||||
name: 'AuthBusinessError',
|
||||
code: 'AUTH_PHONE_CODE_VERIFY_FAILED',
|
||||
cause: externalError,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Не проверяй в потребительских сценариях `externalError.message`, HTTP status или тип ошибки SDK как ожидаемое поведение business API.
|
||||
|
||||
## Тестирование compositions/business
|
||||
|
||||
Тесты `compositions/business/{domain}` проверяют сборку, а не бизнес-поведение.
|
||||
|
||||
Проверяй:
|
||||
|
||||
- builder вызывает нужную business-фабрику;
|
||||
- adapter вызывает SDK operation с ожидаемым payload;
|
||||
- storage/browser adapter соответствует dependency contract;
|
||||
- API другого домена передаётся в нужном виде;
|
||||
- builder deps содержат только API других собранных business-фабрик;
|
||||
- builder/client/adapter constructors не выполняют I/O, storage/env reads или subscriptions во время создания API;
|
||||
- state/query runtime находится в adapter и не импортируется business-модулем;
|
||||
- dependency hook работает без Suspense/throw-on-error и возвращает technical error через result;
|
||||
- adapter пробрасывает source error без создания domain error;
|
||||
- lifecycle subscription возвращает и вызывает cleanup;
|
||||
- public API composition-модуля не экспортирует внутренние adapters.
|
||||
|
||||
Не проверяй здесь domain errors, fallback'и и маппинг доменной модели. Это ответственность factory-level и colocated тестов в `business/{domain}`.
|
||||
|
||||
## Что не тестировать unit-тестами business-модуля
|
||||
|
||||
Unit-тесты business-модуля не проверяют:
|
||||
|
||||
- реальные REST-запросы;
|
||||
- generated-клиенты;
|
||||
- настоящий backend;
|
||||
- Next.js routing;
|
||||
- визуальную вёрстку;
|
||||
- интеграцию с production storage;
|
||||
- реальные внешние сервисы;
|
||||
- e2e-поток целого приложения.
|
||||
|
||||
Эти проверки относятся к `infra`, `compositions`, integration или e2e уровням.
|
||||
|
||||
## Архитектурные импорты
|
||||
|
||||
Проверяй production import graph business-модуля. В `business/**` не должно быть runtime или type-only imports из concrete runtimes:
|
||||
|
||||
- SDK/client/infra;
|
||||
- SWR/TanStack Query/Apollo;
|
||||
- Zustand/Redux/MobX;
|
||||
- React state/effect APIs;
|
||||
- storage/browser/event implementations.
|
||||
|
||||
Factory-level test обязан импортировать фабрику через public API business-модуля. Deep import `../../{domain}.factory` не доказывает корректность public boundary.
|
||||
|
||||
## Чеклист
|
||||
|
||||
- Каждый runtime-метод фабрики имеет factory-level тесты.
|
||||
- Factory-level тесты импортируют модуль только через public API.
|
||||
- Public API фабрики зафиксирован отдельным тестом.
|
||||
- DI-зависимости проверены на корректные, пустые, невалидные и ошибочные ответы.
|
||||
- Hooks тестируются через API, который вернула фабрика.
|
||||
- Command-сценарии проверяют порядок side effects.
|
||||
- После ошибки не выполняются лишние side effects.
|
||||
- Runtime-safe mappers, normalizers и guards покрыты colocated unit-тестами.
|
||||
- Type-only файлы не покрываются бессмысленными unit-тестами.
|
||||
- Доменные ошибки имеют стабильный `code`, сохраняют `cause` и не раскрывают внешнюю ошибку как public contract.
|
||||
- Тесты `compositions/business/{domain}` проверяют сборку, а не бизнес-логику.
|
||||
- Business production imports не содержат concrete state/query/source runtime.
|
||||
- Тесты не требуют backend, network, env и долгоживущих процессов.
|
||||
346
docs/examples/react/composition-provider.md
Normal file
346
docs/examples/react/composition-provider.md
Normal file
@@ -0,0 +1,346 @@
|
||||
---
|
||||
title: Композиция через Provider
|
||||
description: Пример page-level Provider для composition modules в React-проекте
|
||||
---
|
||||
|
||||
# Композиция через Provider
|
||||
|
||||
Раздел показывает, как page composition может владеть provider, store и business composition, которые нужны layout, screen и другим composition modules.
|
||||
|
||||
## Идея
|
||||
|
||||
Page composition хранит состояние и композицию бизнес-доменов на уровне страницы. Layout и screen не импортируют друг друга: они получают доступ к page-level данным через публичный API page composition.
|
||||
|
||||
В примере `ProfilePageState` — только локальное UI-state страницы. Это не domain state и не product data cache. Доменное состояние описывается business-модулем, а concrete store/query hook передаётся его фабрике через adapter в `compositions/business/{domain}`.
|
||||
|
||||
В примере page composition владеет scope-контрактом страницы, но не экспортирует готовый `ProfilePage`, потому что layout и screen импортируют hooks из `pages/profile`. Дерево страницы собирается в отдельном entry-point composition module, который слой `app` только подключает.
|
||||
|
||||
## Принципы
|
||||
|
||||
1. **Владение.** Page-level store, provider и business composition принадлежат page composition module.
|
||||
2. **Обычные сегменты.** Provider, hooks, stores и types лежат в обычных сегментах модуля: `providers/`, `hooks/`, `stores/`, `types/`.
|
||||
3. **Публичный контракт.** Page composition экспортирует только безопасные hooks, provider и типы, которые нужны другим composition modules.
|
||||
4. **Сборка снаружи business.** Business-модули не используют page-level providers. Page composition вызывает builders из `compositions/business/{domain}` и владеет lifecycle готового графа.
|
||||
5. **Без deep imports.** Layout и screen импортируют hooks только из public API page composition.
|
||||
|
||||
## Структура модулей
|
||||
|
||||
```text
|
||||
compositions/pages/profile/
|
||||
├── profile-business-composition.ts
|
||||
├── providers/
|
||||
│ └── profile-page.provider.tsx
|
||||
├── hooks/
|
||||
│ ├── use-profile-page-store.hook.ts
|
||||
│ └── use-profile-business-composition.hook.ts
|
||||
├── stores/
|
||||
│ └── profile-page.store.ts
|
||||
├── types/
|
||||
│ └── profile-page-state.type.ts
|
||||
└── index.ts
|
||||
|
||||
compositions/layouts/profile-main/
|
||||
├── profile-main.layout.tsx
|
||||
└── index.ts
|
||||
|
||||
compositions/screens/profile/
|
||||
├── profile.screen.tsx
|
||||
├── ui/
|
||||
│ ├── profile-error/
|
||||
│ ├── profile-summary/
|
||||
│ └── profile-summary-skeleton/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Тип состояния страницы
|
||||
|
||||
Файл: `compositions/pages/profile/types/profile-page-state.type.ts`.
|
||||
|
||||
```ts
|
||||
export type ProfilePageState = {
|
||||
title: string
|
||||
isSidebarOpen: boolean
|
||||
setSidebarOpen: (value: boolean) => void
|
||||
}
|
||||
```
|
||||
|
||||
## Store страницы
|
||||
|
||||
Файл: `compositions/pages/profile/stores/profile-page.store.ts`.
|
||||
|
||||
```ts
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import type { ProfilePageState } from '../types/profile-page-state.type'
|
||||
|
||||
export const createProfilePageStore = () =>
|
||||
createStore<ProfilePageState>((set) => ({
|
||||
title: 'Profile',
|
||||
isSidebarOpen: false,
|
||||
setSidebarOpen: (value) => set({ isSidebarOpen: value }),
|
||||
}))
|
||||
```
|
||||
|
||||
`createProfilePageStore` не экспортируется через public API модуля. Это внутренняя деталь создания состояния.
|
||||
|
||||
## Business composition страницы
|
||||
|
||||
Файл: `compositions/pages/profile/profile-business-composition.ts`.
|
||||
|
||||
```ts
|
||||
import { createAuthBusiness } from '@/compositions/business/auth'
|
||||
import { createProfileBusiness } from '@/compositions/business/profile'
|
||||
|
||||
export const createProfileBusinessComposition = () => {
|
||||
const authApi = createAuthBusiness()
|
||||
const profileApi = createProfileBusiness({ authApi })
|
||||
|
||||
return { authApi, profileApi }
|
||||
}
|
||||
```
|
||||
|
||||
Page composition собирает нужный для страницы граф из per-domain builders. Реальные runtime-зависимости остаются в `compositions/business/{domain}`, а не внутри `business`.
|
||||
|
||||
Page composition не импортирует SDK, product storage, source hook или raw infra event для дополнительной настройки домена. Такое wiring принадлежит соответствующему integration module.
|
||||
|
||||
## Provider страницы
|
||||
|
||||
Файл: `compositions/pages/profile/providers/profile-page.provider.tsx`.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { createContext, useEffect, useState, type ReactNode } from 'react'
|
||||
import type { StoreApi } from 'zustand/vanilla'
|
||||
import { createProfileBusinessComposition } from '../profile-business-composition'
|
||||
import { createProfilePageStore } from '../stores/profile-page.store'
|
||||
import type { ProfilePageState } from '../types/profile-page-state.type'
|
||||
|
||||
type ProfileBusinessComposition = ReturnType<typeof createProfileBusinessComposition>
|
||||
|
||||
type ProfilePageProviderValue = {
|
||||
store: StoreApi<ProfilePageState>
|
||||
business: ProfileBusinessComposition
|
||||
}
|
||||
|
||||
export const ProfilePageContext = createContext<ProfilePageProviderValue | null>(null)
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const createProfilePageProviderValue = (): ProfilePageProviderValue => ({
|
||||
store: createProfilePageStore(),
|
||||
business: createProfileBusinessComposition(),
|
||||
})
|
||||
|
||||
export const ProfilePageProvider = ({ children }: Props) => {
|
||||
const [value] = useState(createProfilePageProviderValue)
|
||||
|
||||
useEffect(() => {
|
||||
return value.business.authApi.startSessionInvalidationTracking()
|
||||
}, [value.business.authApi])
|
||||
|
||||
return (
|
||||
<ProfilePageContext.Provider value={value}>
|
||||
{children}
|
||||
</ProfilePageContext.Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Context object остаётся технической деталью provider и не должен использоваться внешними модулями напрямую. Наружу экспортируются hooks доступа.
|
||||
|
||||
Lazy initializer может быть повторно вызван React Strict Mode в development. Поэтому store/business constructors не выполняют I/O и не запускают subscriptions. Domain-level lifecycle operations запускаются отдельно в effect и возвращают cleanup.
|
||||
|
||||
## Hooks доступа
|
||||
|
||||
Файл: `compositions/pages/profile/hooks/use-profile-page-store.hook.ts`.
|
||||
|
||||
```ts
|
||||
'use client'
|
||||
|
||||
import { useContext } from 'react'
|
||||
import { useStore } from 'zustand'
|
||||
import { ProfilePageContext } from '../providers/profile-page.provider'
|
||||
import type { ProfilePageState } from '../types/profile-page-state.type'
|
||||
|
||||
export const useProfilePageStore = <T,>(selector: (state: ProfilePageState) => T) => {
|
||||
const ctx = useContext(ProfilePageContext)
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('useProfilePageStore must be used within ProfilePageProvider')
|
||||
}
|
||||
|
||||
return useStore(ctx.store, selector)
|
||||
}
|
||||
```
|
||||
|
||||
Файл: `compositions/pages/profile/hooks/use-profile-business-composition.hook.ts`.
|
||||
|
||||
```ts
|
||||
'use client'
|
||||
|
||||
import { useContext } from 'react'
|
||||
import { ProfilePageContext } from '../providers/profile-page.provider'
|
||||
|
||||
export const useProfileBusinessComposition = () => {
|
||||
const ctx = useContext(ProfilePageContext)
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('useProfileBusinessComposition must be used within ProfilePageProvider')
|
||||
}
|
||||
|
||||
return ctx.business
|
||||
}
|
||||
```
|
||||
|
||||
## Layout использует page-level store
|
||||
|
||||
Файл: `compositions/layouts/profile-main/profile-main.layout.tsx`.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { useProfilePageStore } from '@/compositions/pages/profile'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const ProfileMainLayout = ({ children }: Props) => {
|
||||
const title = useProfilePageStore((state) => state.title)
|
||||
const isSidebarOpen = useProfilePageStore((state) => state.isSidebarOpen)
|
||||
|
||||
return (
|
||||
<div data-sidebar-open={isSidebarOpen}>
|
||||
<header>{title}</header>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Layout импортирует hook из public API page composition. Он не импортирует screen и не лезет во внутренние файлы `pages/profile`.
|
||||
|
||||
## Screen использует business composition
|
||||
|
||||
Файл: `compositions/screens/profile/profile.screen.tsx`.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useProfileBusinessComposition } from '@/compositions/pages/profile'
|
||||
import { ProfileError } from './ui/profile-error'
|
||||
import { ProfileSummary } from './ui/profile-summary'
|
||||
import { ProfileSummarySkeleton } from './ui/profile-summary-skeleton'
|
||||
|
||||
export const ProfileScreen = () => {
|
||||
const { profileApi } = useProfileBusinessComposition()
|
||||
const currentProfile = profileApi.useCurrentProfile()
|
||||
|
||||
if (currentProfile.isLoading) {
|
||||
return <ProfileSummarySkeleton />
|
||||
}
|
||||
|
||||
if (currentProfile.error) {
|
||||
return <ProfileError code={currentProfile.error.code} />
|
||||
}
|
||||
|
||||
return currentProfile.data ? <ProfileSummary profile={currentProfile.data} /> : null
|
||||
}
|
||||
```
|
||||
|
||||
Screen получает готовые доменные API из page composition и не собирает граф фабрик самостоятельно. `ProfileSummary` — компонент screen composition, а не часть `business/profile`.
|
||||
|
||||
## Публичный API page composition
|
||||
|
||||
Файл: `compositions/pages/profile/index.ts`.
|
||||
|
||||
```ts
|
||||
export { ProfilePageProvider } from './providers/profile-page.provider'
|
||||
export { useProfilePageStore } from './hooks/use-profile-page-store.hook'
|
||||
export { useProfileBusinessComposition } from './hooks/use-profile-business-composition.hook'
|
||||
|
||||
export type { ProfilePageState } from './types/profile-page-state.type'
|
||||
```
|
||||
|
||||
Внутренние `createProfilePageStore`, `createProfileBusinessComposition` и `ProfilePageContext` не экспортируются через public API.
|
||||
|
||||
Готовое дерево собирай в отдельном entry-point composition module. Не смешивай в одном public API готовую page composition и hooks, которые импортируют её дочерние layout/screen modules: это может создать runtime-цикл.
|
||||
|
||||
## Подключение в app
|
||||
|
||||
Entry composition связывает provider, layout и screen:
|
||||
|
||||
```tsx
|
||||
// compositions/entries/profile/profile.entry.tsx
|
||||
'use client'
|
||||
|
||||
import { ProfilePageProvider } from '@/compositions/pages/profile'
|
||||
import { ProfileMainLayout } from '@/compositions/layouts/profile-main'
|
||||
import { ProfileScreen } from '@/compositions/screens/profile'
|
||||
|
||||
export const ProfileEntry = () => (
|
||||
<ProfilePageProvider>
|
||||
<ProfileMainLayout>
|
||||
<ProfileScreen />
|
||||
</ProfileMainLayout>
|
||||
</ProfilePageProvider>
|
||||
)
|
||||
```
|
||||
|
||||
React Router config только подключает готовый entry:
|
||||
|
||||
```tsx
|
||||
import { ProfileEntry } from '@/compositions/entries/profile'
|
||||
|
||||
export const profileRoute = {
|
||||
path: '/profile',
|
||||
element: <ProfileEntry />,
|
||||
}
|
||||
```
|
||||
|
||||
Для Next App Router создай готовые layout/page entries в `compositions`, а framework files только подключают их.
|
||||
|
||||
```tsx
|
||||
// compositions/entries/profile/profile-layout.entry.tsx
|
||||
'use client'
|
||||
|
||||
import { ProfilePageProvider } from '@/compositions/pages/profile'
|
||||
import { ProfileMainLayout } from '@/compositions/layouts/profile-main'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export const ProfileLayoutEntry = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<ProfilePageProvider>
|
||||
<ProfileMainLayout>{children}</ProfileMainLayout>
|
||||
</ProfilePageProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// compositions/entries/profile/profile-page.entry.tsx
|
||||
'use client'
|
||||
|
||||
import { ProfileScreen } from '@/compositions/screens/profile'
|
||||
|
||||
export const ProfilePageEntry = () => <ProfileScreen />
|
||||
```
|
||||
|
||||
```tsx
|
||||
// app/(profile)/layout.tsx
|
||||
import { ProfileLayoutEntry } from '@/compositions/entries/profile'
|
||||
|
||||
export default ProfileLayoutEntry
|
||||
```
|
||||
|
||||
```tsx
|
||||
// app/(profile)/page.tsx
|
||||
import { ProfilePageEntry } from '@/compositions/entries/profile'
|
||||
|
||||
export default ProfilePageEntry
|
||||
```
|
||||
|
||||
`app` размещает готовые entry composition modules по правилам фреймворка, но не реализует product tree внутри себя.
|
||||
83
docs/examples/react/composition-structures.md
Normal file
83
docs/examples/react/composition-structures.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Структуры compositions
|
||||
description: Примеры организации слоя compositions под разные способы сборки React-приложения
|
||||
---
|
||||
|
||||
# Структуры compositions
|
||||
|
||||
Раздел показывает, что SLM не фиксирует жёсткую структуру внутри `compositions`. Команда выбирает организацию под фреймворк, роутинг, CMS и продуктовую задачу.
|
||||
|
||||
## Базовая рекомендация
|
||||
|
||||
Подходит для большинства приложений, где есть явные страницы, layouts, screens и переиспользуемые композиционные блоки.
|
||||
|
||||
```text
|
||||
src/compositions/
|
||||
├── business/
|
||||
│ ├── auth/
|
||||
│ └── user/
|
||||
├── pages/
|
||||
│ ├── home/
|
||||
│ └── profile/
|
||||
├── layouts/
|
||||
│ ├── main/
|
||||
│ └── dashboard/
|
||||
├── screens/
|
||||
│ ├── home/
|
||||
│ └── profile/
|
||||
└── widgets/
|
||||
├── page-heading/
|
||||
└── promo-banner/
|
||||
```
|
||||
|
||||
`business`, `pages`, `layouts`, `screens` и `widgets` здесь не являются отдельными SLM-слоями. Это группы composition modules внутри одного слоя `compositions`.
|
||||
|
||||
`compositions/business/{domain}` используется для runtime-сборки business-фабрик. Он не заменяет `business/{domain}` и не содержит доменную логику.
|
||||
|
||||
Только эта группа integration modules знает одновременно business dependency contract и concrete product runtime. Остальные composition modules являются graph owners или consumers готовых business API.
|
||||
|
||||
## Entry-points и blocks
|
||||
|
||||
Подходит для проектов, где точка сборки не всегда является страницей: CMS registry, embedded UI, route entries, feature entries.
|
||||
|
||||
```text
|
||||
src/compositions/
|
||||
├── entry-points/
|
||||
│ ├── cms-profile/
|
||||
│ └── embedded-checkout/
|
||||
├── pages/
|
||||
│ └── profile/
|
||||
├── layouts/
|
||||
│ └── profile-main/
|
||||
├── screens/
|
||||
│ └── profile/
|
||||
└── blocks/
|
||||
├── profile-summary/
|
||||
└── recommended-products/
|
||||
```
|
||||
|
||||
## Группировка вокруг продукта
|
||||
|
||||
Подходит, когда удобнее держать все части одной крупной области рядом.
|
||||
|
||||
```text
|
||||
src/compositions/
|
||||
└── profile/
|
||||
├── page/
|
||||
├── layout/
|
||||
├── screen/
|
||||
└── blocks/
|
||||
```
|
||||
|
||||
## Главное правило
|
||||
|
||||
Любая структура допустима, если соблюдаются границы слоя:
|
||||
|
||||
- `app` подключает готовые composition modules к фреймворку.
|
||||
- `compositions` может импортировать `business`, `infra`, `ui`, `shared`.
|
||||
- `compositions/business/{domain}` отдельными adapters собирает конкретную business-фабрику с runtime-зависимостями.
|
||||
- Page/layout/screen/widget получают product data только через `{Domain}Api`.
|
||||
- Graph owner импортирует builders, но не raw product SDK/client/event для досборки домена.
|
||||
- `business`, `infra`, `ui`, `shared` не импортируют `compositions`.
|
||||
- Импорты между composition modules идут только через public API.
|
||||
- Deep imports внутрь composition modules запрещены.
|
||||
Reference in New Issue
Block a user