mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
feat: Добавить VitePress
This commit is contained in:
390
old-docs/examples/business-composition.md
Normal file
390
old-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
old-docs/examples/business-testing.md
Normal file
364
old-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
old-docs/examples/react/composition-provider.md
Normal file
346
old-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
old-docs/examples/react/composition-structures.md
Normal file
83
old-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