Compare commits
2 Commits
main
...
1af27795ed
| Author | SHA1 | Date | |
|---|---|---|---|
| 1af27795ed | |||
| d69fca16fe |
@@ -2,37 +2,12 @@ name: CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Установка зависимостей
|
||||
run: npm ci
|
||||
|
||||
- name: Генерация артефактов
|
||||
run: npm run generate
|
||||
|
||||
- name: Сборка документации
|
||||
run: npm run docs:build
|
||||
|
||||
- name: Сборка лендинга
|
||||
run: npm run build
|
||||
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
@@ -41,6 +16,11 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Версия из package.json
|
||||
id: version
|
||||
run: |
|
||||
@@ -48,6 +28,23 @@ jobs:
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Версия: $VERSION"
|
||||
|
||||
- name: Генерация docs
|
||||
run: |
|
||||
npm ci
|
||||
npm run docs
|
||||
|
||||
- name: Коммит generated/
|
||||
run: |
|
||||
git config user.name "CI Bot"
|
||||
git config user.email "ci@gromlab.ru"
|
||||
git add generated/ README_RU.md
|
||||
if git diff --cached --quiet; then
|
||||
echo "Нет изменений, пропуск"
|
||||
else
|
||||
git commit -m "docs: обновить generated (${{ steps.version.outputs.version }}) [skip ci]"
|
||||
git push origin main
|
||||
fi
|
||||
|
||||
- name: Создать тег
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
@@ -61,7 +58,7 @@ jobs:
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: version
|
||||
needs: docs
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -95,7 +92,7 @@ jobs:
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=raw,value=${{ needs.version.outputs.version }}
|
||||
type=raw,value=${{ needs.docs.outputs.version }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
@@ -107,7 +104,7 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
VERSION_TAG=${{ needs.version.outputs.version }}
|
||||
VERSION_TAG=${{ needs.docs.outputs.version }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -135,7 +135,6 @@ dist
|
||||
.vitepress/cache
|
||||
.vitepress/dist
|
||||
docs/.vitepress
|
||||
docs/public/
|
||||
|
||||
# Generated artifacts
|
||||
public/docs/
|
||||
@@ -150,3 +149,4 @@ dist
|
||||
# Рабочие заметки
|
||||
notes
|
||||
|
||||
|
||||
|
||||
@@ -8,33 +8,16 @@ const sidebar = [
|
||||
{ text: 'Слои', link: '/architecture/layers' },
|
||||
{ text: 'Модули', link: '/architecture/modules' },
|
||||
{ text: 'Сегменты', link: '/architecture/segments' },
|
||||
{ text: 'Монорепозитории', link: '/architecture/monorepo' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Примеры React',
|
||||
items: [
|
||||
{ text: 'Создание фабрики', link: '/examples/react/factory' },
|
||||
{ text: 'Композиция фабрик', link: '/examples/react/factory-composition' },
|
||||
{ text: 'Композиция через Provider', link: '/examples/react/composition-provider' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: 'docs',
|
||||
srcExclude: ['public/**'],
|
||||
outDir: 'public/docs',
|
||||
title: 'SLM Design',
|
||||
description: 'Правила и стандарты архитектуры проекта',
|
||||
base: '/docs/',
|
||||
cleanUrls: true,
|
||||
head: [
|
||||
['meta', { name: 'llms', content: '/llms.txt' }],
|
||||
['link', { rel: 'alternate llms', type: 'text/plain', href: '/llms.txt', title: 'llms.txt' }],
|
||||
['link', { rel: 'alternate', type: 'text/plain', href: '/llms-full.txt', title: 'llms-full.txt' }],
|
||||
['link', { rel: 'alternate', type: 'text/markdown', href: '/ARCHITECTURE.md', title: 'ARCHITECTURE.md' }],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
sidebar,
|
||||
|
||||
198
CONTRIBUTING.md
198
CONTRIBUTING.md
@@ -4,79 +4,182 @@
|
||||
|
||||
## О проекте
|
||||
|
||||
Сайт-документация архитектуры SLM Design с лендингом.
|
||||
Документационный сайт с правилами и стандартами фронтенд-разработки на Next.js + TypeScript.
|
||||
|
||||
- Лендинг: React + Vite
|
||||
- Документация: VitePress
|
||||
- Язык: русский
|
||||
- Документация: `docs/architecture/`
|
||||
- Движок: VitePress
|
||||
- Языки: русский (основной), английский
|
||||
- Русская версия: `docs/ru/`
|
||||
- Английская версия: `docs/en/`
|
||||
|
||||
## Команды
|
||||
|
||||
| Команда | Что делает |
|
||||
|---------|-----------|
|
||||
| `npm run dev` | Локальный сервер лендинга |
|
||||
| `npm run build` | Сборка лендинга |
|
||||
| `npm run docs:dev` | Локальный сервер документации |
|
||||
| `npm run docs:build` | Сборка документации |
|
||||
| `npm run generate` | Генерация артефактов (llms.txt, llms-full.txt, ARCHITECTURE.md, ZIP, README) |
|
||||
| `npm run dev` | Локальный сервер разработки |
|
||||
| `npm run build` | Сборка статического сайта |
|
||||
| `npm run docs` | Генерация `generated/{lang}/RULES.md` — единый файл для AI-ассистентов |
|
||||
|
||||
## Структура файлов
|
||||
|
||||
```
|
||||
docs/
|
||||
├── index.md # Страница навигации по документации
|
||||
└── architecture/ # Разделы архитектуры
|
||||
├── index.md # Обзор SLM Design
|
||||
├── layers.md # Слои
|
||||
├── modules.md # Модули
|
||||
└── segments.md # Сегменты
|
||||
├── ru/ # Русская версия (основная)
|
||||
│ ├── index.md # Главная страница
|
||||
│ ├── basics/ # Базовые правила
|
||||
│ │ ├── tech-stack.md
|
||||
│ │ ├── architecture.md
|
||||
│ │ ├── code-style.md
|
||||
│ │ ├── naming.md
|
||||
│ │ ├── documentation.md
|
||||
│ │ └── typing.md
|
||||
│ └── applied/ # Прикладные разделы
|
||||
│ ├── vscode.md
|
||||
│ ├── project-structure.md
|
||||
│ ├── components.md
|
||||
│ ├── page-level.md
|
||||
│ ├── templates-generation.md
|
||||
│ ├── styles.md
|
||||
│ ├── images-sprites.md
|
||||
│ ├── svg-sprites.md
|
||||
│ ├── video.md
|
||||
│ ├── api.md
|
||||
│ ├── stores.md
|
||||
│ ├── hooks.md
|
||||
│ ├── fonts.md
|
||||
│ └── localization.md
|
||||
├── en/ # Английская версия (зеркало ru/)
|
||||
.vitepress/
|
||||
├── config.ts # Конфигурация VitePress, сайдбар
|
||||
public/
|
||||
├── llms.txt # Карта документации для LLM
|
||||
├── llms-full.txt # Полная документация в одном файле
|
||||
└── ARCHITECTURE.md # Единый файл архитектуры
|
||||
generate.ts # Скрипт генерации артефактов
|
||||
src/ # Исходники лендинга (React + Vite)
|
||||
├── config.ts # Конфигурация VitePress, сайдбары, локали
|
||||
generated/
|
||||
├── ru/RULES.md # Сгенерированный единый файл (ru)
|
||||
└── en/RULES.md # Сгенерированный единый файл (en)
|
||||
concat-md.js # Скрипт генерации RULES.md
|
||||
```
|
||||
|
||||
### Добавление нового раздела
|
||||
|
||||
1. Создать `.md`-файл в `docs/architecture/`.
|
||||
2. Добавить пункт в сайдбар — `.vitepress/config.ts`.
|
||||
3. Добавить `description` в frontmatter файла — используется для `llms.txt`.
|
||||
4. Запустить `npm run generate` для обновления артефактов.
|
||||
1. Создать `.md`-файл в нужной папке (`basics/` или `applied/`).
|
||||
2. Добавить пункт в сайдбар — `.vitepress/config.ts` (оба языка, если есть перевод).
|
||||
3. Добавить файл в массив `fileOrder` — `concat-md.js` (для генерации RULES.md).
|
||||
|
||||
## Frontmatter
|
||||
## Два типа документации
|
||||
|
||||
### Базовые правила
|
||||
|
||||
**Отвечает на вопрос:** «Каким должен быть любой код?»
|
||||
|
||||
Универсальные стандарты, **не привязанные к конкретной области**.
|
||||
Правило базовое, если оно применимо ко всему коду одинаково: именование переменных, оформление импортов, когда использовать `type` vs `interface`.
|
||||
|
||||
Примеры в базовых правилах допускаются, но служат иллюстрацией принципа, а не инструкцией по конкретной области.
|
||||
|
||||
**Граница:** если правило касается только одной области (только стили, только компоненты, только API) — оно живёт в прикладном разделе, не в базовых.
|
||||
|
||||
### Прикладные разделы
|
||||
|
||||
**Отвечает на вопрос:** «Как работать с X?»
|
||||
|
||||
Полное описание конкретной области: структура файлов, правила, именование, типизация, примеры.
|
||||
|
||||
**Граница:** прикладной раздел не дублирует базовые правила.
|
||||
Если правило уже описано в базовых — прикладной раздел ссылается на него, но не повторяет.
|
||||
|
||||
## Структура прикладного раздела
|
||||
|
||||
Шаблон ниже описывает все допустимые секции. Раздел включает только те секции, которые для него релевантны — пустые секции не создаются.
|
||||
|
||||
```markdown
|
||||
# {Название}
|
||||
|
||||
Краткое описание: о чём раздел и какие аспекты работы с областью он охватывает.
|
||||
|
||||
## Что нужно знать
|
||||
|
||||
Неочевидная информация, которую читатель должен знать перед чтением раздела.
|
||||
Если для раздела нет такой вводной — секция не создаётся.
|
||||
|
||||
## Структура
|
||||
|
||||
Файловая организация: какие файлы создавать и куда класть.
|
||||
Обязательно — дерево файлов через code-block.
|
||||
|
||||
## Правила
|
||||
|
||||
Конкретные требования, специфичные для области. Делятся на две подсекции:
|
||||
|
||||
### Реализация
|
||||
|
||||
Как написан код внутри файла: синтаксис, паттерны, API.
|
||||
Отвечает на вопрос: «Как писать код?»
|
||||
|
||||
Примеры: объявление через `const`, деструктуризация пропсов, формат вызова `cl()`, способ подключения стилей, структура хука.
|
||||
|
||||
### Организация
|
||||
|
||||
Как компонент/модуль встроен в проект: файловые границы, зоны ответственности, экспорт.
|
||||
Отвечает на вопрос: «Где что лежит и за что отвечает?»
|
||||
|
||||
Примеры: один компонент — один файл, вложенные компоненты в `ui/`, логика выносится в `model/`.
|
||||
|
||||
Формат обеих подсекций — маркированный список.
|
||||
Для неочевидных случаев — блоки «Хорошо / Плохо».
|
||||
Если в области нет правил одной из категорий — подсекция не создаётся.
|
||||
|
||||
## Именование
|
||||
|
||||
Соглашения по именам, специфичные для этой области.
|
||||
Только то, что НЕ покрыто в базовом разделе «Именование».
|
||||
|
||||
## Типизация
|
||||
|
||||
Правила типизации, специфичные для этой области.
|
||||
Только то, что НЕ покрыто в базовом разделе «Типизация».
|
||||
|
||||
## Документирование
|
||||
|
||||
Что и как документировать в этой области.
|
||||
Только то, что НЕ покрыто в базовом разделе «Документирование».
|
||||
|
||||
## Примеры
|
||||
|
||||
Полноценные примеры кода.
|
||||
Каждый пример с путём к файлу и пояснениями.
|
||||
|
||||
```
|
||||
|
||||
### Порядок секций
|
||||
|
||||
Порядок фиксированный: контекст → структура → правила → специализации базовых правил → примеры.
|
||||
|
||||
Логика: читатель сначала понимает «что это», потом «где это лежит», потом «как это делать», и в конце видит полный пример.
|
||||
|
||||
### Секции-расширения базовых правил
|
||||
|
||||
«Именование», «Типизация», «Документирование» в прикладном разделе — это **точки расширения** базовых правил.
|
||||
|
||||
- В базовых описано общее: `camelCase` для переменных, `type` vs `interface`, формат JSDoc.
|
||||
- В прикладном разделе описано специфичное: как именовать CSS-классы (стили), как типизировать пропсы компонентов (компоненты), как документировать хуки (хуки).
|
||||
|
||||
Если для области нет специфики по именованию, типизации или документированию — секция не создаётся.
|
||||
|
||||
## Конвенции оформления
|
||||
|
||||
### Frontmatter
|
||||
|
||||
Каждый `.md`-файл начинается с YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Название раздела
|
||||
description: Краткое описание для llms.txt
|
||||
---
|
||||
```
|
||||
|
||||
- `title` — совпадает с `h1`-заголовком в файле.
|
||||
- `description` — кратное описание содержимого страницы, используется при генерации `llms.txt`.
|
||||
|
||||
## Структура страницы документации
|
||||
|
||||
Каждая страница начинается одинаково:
|
||||
|
||||
1. **Заголовок** (`h1`) — совпадает с `title` из frontmatter.
|
||||
2. **Описание раздела** — 1–2 строки сразу после заголовка. Говорит, что это за раздел, какую информацию он описывает и что читатель в нём получит.
|
||||
3. **Определение** (`## Определение`) — для справочных страниц, посвящённых одному термину. Короткая формулировка жирным: что это за сущность и какую роль она играет.
|
||||
4. **Контент** — остальные `h2`-подразделы.
|
||||
|
||||
## Конвенции оформления
|
||||
Значение `title` совпадает с текстом `h1`-заголовка в файле.
|
||||
|
||||
### Заголовки
|
||||
|
||||
- Один `h1` на файл — совпадает с `title` из frontmatter.
|
||||
- Сразу после `h1` — описание раздела (1–2 предложения).
|
||||
- Сразу после `h1` — вводный абзац (одно-два предложения).
|
||||
- Основные секции — `h2`.
|
||||
- Подсекции внутри `h2` — `h3`.
|
||||
- `h4` не используется.
|
||||
@@ -91,6 +194,8 @@ description: Краткое описание для llms.txt
|
||||
|
||||
Используются для контрастного сравнения правильного и неправильного подхода.
|
||||
|
||||
Формат:
|
||||
|
||||
```markdown
|
||||
**Хорошо:**
|
||||
|
||||
@@ -112,10 +217,11 @@ description: Краткое описание для llms.txt
|
||||
|
||||
### Ссылки между разделами
|
||||
|
||||
Раздел может ссылаться на другие разделы, но не дублирует их содержимое.
|
||||
Прикладной раздел может ссылаться на другие разделы, но не дублирует их содержимое.
|
||||
|
||||
## Принципы
|
||||
|
||||
1. **Не дублировать.** Одна мысль живёт в одном месте. Остальные ссылаются.
|
||||
2. **Пустые секции не создавать.** Если для раздела нет специфики — секция не создаётся.
|
||||
3. **Примеры обязательны.** Раздел без примеров кода — незавершён.
|
||||
2. **Базовое vs прикладное.** Если правило применимо ко всему коду — оно базовое. Если только к одной области — прикладное.
|
||||
3. **Пустые секции не создавать.** Если для раздела нет специфики по именованию — секции «Именование» в нём нет.
|
||||
4. **Примеры обязательны.** Прикладной раздел без примеров кода — незавершён.
|
||||
|
||||
40
Caddyfile
40
Caddyfile
@@ -1,40 +1,10 @@
|
||||
:8082 {
|
||||
root * /srv
|
||||
|
||||
# Устаревшие пути llms.txt в подпапках ведём к корневым артефактам.
|
||||
redir /docs/llms.txt /llms.txt 301
|
||||
redir /docs/llms-full.txt /llms-full.txt 301
|
||||
|
||||
# Чистые URL: запросы вида `/docs/foo.html` редиректим на `/docs/foo`.
|
||||
@legacyHtml {
|
||||
path_regexp legacyHtml ^(/.+)\.html$
|
||||
not path /index.html
|
||||
}
|
||||
redir @legacyHtml {re.legacyHtml.1} 301
|
||||
|
||||
header Link "</llms.txt>; rel=\"llms\""
|
||||
|
||||
@existingText {
|
||||
path *.txt
|
||||
file
|
||||
}
|
||||
header @existingText Content-Type "text/plain; charset=utf-8"
|
||||
|
||||
@existingMarkdown {
|
||||
path *.md
|
||||
file
|
||||
}
|
||||
header @existingMarkdown Content-Type "text/markdown; charset=utf-8"
|
||||
|
||||
@architecture path /ARCHITECTURE.md
|
||||
header @architecture Cache-Control "no-cache, no-store, must-revalidate"
|
||||
|
||||
@missingText {
|
||||
path *.txt *.md
|
||||
not file
|
||||
}
|
||||
respond @missingText 404
|
||||
|
||||
@plainText path /llms.txt /llms-full.txt
|
||||
header @plainText Content-Type "text/plain; charset=utf-8"
|
||||
@markdown path /ARCHITECTURE.md
|
||||
header @markdown Content-Type "text/markdown; charset=utf-8"
|
||||
file_server
|
||||
header Link "</llms.txt>; rel=\"llms\""
|
||||
try_files {path} {path}.html {path}/index.html /index.html
|
||||
}
|
||||
|
||||
15
README.md
15
README.md
@@ -1,17 +1,6 @@
|
||||
# SLM Design
|
||||
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
||||
|
||||
## Разделы спецификации
|
||||
|
||||
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
|
||||
|
||||
- [Слои](docs/architecture/layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
|
||||
- [Модули](docs/architecture/modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
|
||||
- [Сегменты](docs/architecture/segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
|
||||
- [Монорепозитории](docs/architecture/monorepo.md) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business.
|
||||
|
||||
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории.
|
||||
|
||||
## Преимущества
|
||||
|
||||
### Вертикальная организация домена
|
||||
@@ -42,10 +31,6 @@ Cross-domain зависимости в бизнес-слое реализуют
|
||||
|
||||
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции).
|
||||
|
||||
### Адаптация к монорепозиториям
|
||||
|
||||
SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы.
|
||||
|
||||
## Происхождение
|
||||
|
||||
SLM Design вырос на основе:
|
||||
|
||||
@@ -6,17 +6,6 @@ description: Назначение архитектуры, ключевые пр
|
||||
# SLM Design
|
||||
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
||||
|
||||
## Разделы спецификации
|
||||
|
||||
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
|
||||
|
||||
- [Слои](/architecture/layers) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
|
||||
- [Модули](/architecture/modules) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
|
||||
- [Сегменты](/architecture/segments) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
|
||||
- [Монорепозитории](/architecture/monorepo) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business.
|
||||
|
||||
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории.
|
||||
|
||||
## Преимущества
|
||||
|
||||
### Вертикальная организация домена
|
||||
@@ -47,10 +36,6 @@ Cross-domain зависимости в бизнес-слое реализуют
|
||||
|
||||
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции).
|
||||
|
||||
### Адаптация к монорепозиториям
|
||||
|
||||
SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы.
|
||||
|
||||
## Происхождение
|
||||
|
||||
SLM Design вырос на основе:
|
||||
|
||||
@@ -194,11 +194,87 @@ Business-модуль всегда экспортирует фабрику. Фа
|
||||
|
||||
Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция».
|
||||
|
||||
### Примеры
|
||||
### Структура business-модуля
|
||||
|
||||
Пример реализации фабрики в React см. в [Создание фабрики](/examples/react/factory).
|
||||
```text
|
||||
business/customer/
|
||||
├── customer.factory.ts
|
||||
├── index.ts
|
||||
└── types/
|
||||
├── customer.type.ts
|
||||
├── customer-api.type.ts
|
||||
├── customer-deps.type.ts
|
||||
└── customer-factory.type.ts
|
||||
```
|
||||
|
||||
Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](/examples/react/factory-composition).
|
||||
### Типы
|
||||
|
||||
```ts
|
||||
// business/customer/types/customer-api.type.ts
|
||||
export type CustomerApi = {
|
||||
useCustomer: () => Customer
|
||||
CustomerCard: (props: CustomerCardProps) => ReactNode
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/order/types/order-deps.type.ts
|
||||
export type OrderDeps = {
|
||||
customer: Pick<CustomerApi, 'useCustomer'>
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/order/types/order-factory.type.ts
|
||||
export type OrderFactory = (deps: OrderDeps) => OrderApi
|
||||
```
|
||||
|
||||
### Фабрика без зависимостей
|
||||
|
||||
```ts
|
||||
// business/customer/customer.factory.ts
|
||||
import type { CustomerFactory } from './types/customer-factory.type'
|
||||
|
||||
export const customerFactory: CustomerFactory = () => {
|
||||
return {
|
||||
useCustomer,
|
||||
CustomerCard,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Фабрика с зависимостями
|
||||
|
||||
```ts
|
||||
// business/order/order.factory.ts
|
||||
import type { OrderFactory } from './types/order-factory.type'
|
||||
|
||||
export const orderFactory: OrderFactory = (deps) => {
|
||||
return {
|
||||
useOrder,
|
||||
OrderCard,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Композиция на уровне screen
|
||||
|
||||
```tsx
|
||||
// screens/home/home.screen.tsx
|
||||
import { customerFactory } from '@/business/customer'
|
||||
import { orderFactory } from '@/business/order'
|
||||
|
||||
const customer = customerFactory()
|
||||
const order = orderFactory({ customer })
|
||||
|
||||
const { useOrder, OrderCard } = order
|
||||
|
||||
export const HomeScreen = () => {
|
||||
const currentOrder = useOrder()
|
||||
|
||||
return <OrderCard order={currentOrder} />
|
||||
}
|
||||
```
|
||||
|
||||
## Жизненный цикл
|
||||
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
---
|
||||
title: Монорепозитории
|
||||
description: Правила применения SLM Design для frontend-проектов, находящихся в монорепозитории
|
||||
---
|
||||
|
||||
# Монорепозитории
|
||||
|
||||
Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов.
|
||||
|
||||
## Определение
|
||||
|
||||
**Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв `ui`, `infra` и `shared`.**
|
||||
|
||||
## Базовая структура
|
||||
|
||||
Каждое приложение внутри `apps/` сохраняет собственную SLM-структуру в `src/`.
|
||||
|
||||
```text
|
||||
repo/
|
||||
├── apps/
|
||||
│ ├── web/
|
||||
│ │ └── src/
|
||||
│ │ ├── app/
|
||||
│ │ ├── layouts/
|
||||
│ │ ├── screens/
|
||||
│ │ ├── widgets/
|
||||
│ │ ├── business/
|
||||
│ │ ├── infra/
|
||||
│ │ ├── ui/
|
||||
│ │ └── shared/
|
||||
│ └── admin/
|
||||
│ └── src/
|
||||
│ └── ...
|
||||
└── packages/
|
||||
├── ui/
|
||||
│ ├── button/ # самостоятельный пакет UI-модуля
|
||||
│ ├── input/ # самостоятельный пакет UI-модуля
|
||||
│ └── modal/ # самостоятельный пакет UI-модуля
|
||||
├── infra/
|
||||
│ ├── theme/ # самостоятельный пакет infra-модуля
|
||||
│ ├── backend-api/ # самостоятельный пакет infra-модуля
|
||||
│ └── logger/ # самостоятельный пакет infra-модуля
|
||||
└── shared/ # единый shared-пакет
|
||||
├── package.json
|
||||
└── src/
|
||||
├── lib/ # переиспользуемые утилиты
|
||||
├── helpers/ # переиспользуемые helpers
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`apps/{app}/src` — граница SLM-приложения. `packages/*` находятся выше SLM и не добавляют новые архитектурные слои.
|
||||
|
||||
## Группировка frontend-пакетов
|
||||
|
||||
Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: `ui`, `infra`, `shared`.
|
||||
|
||||
```text
|
||||
packages/ui/* # пакеты UI-модулей
|
||||
packages/infra/* # пакеты infra-модулей
|
||||
packages/shared # единый shared-пакет
|
||||
```
|
||||
|
||||
Эта группировка повторяет названия SLM-слоёв для навигации, но сама не является слоистой архитектурой внутри `packages/`. Монорепозиторий может содержать другие пакеты: tooling, конфиги, SDK, схемы, e2e и другие технические пакеты вне SLM.
|
||||
|
||||
## Пакет и модуль
|
||||
|
||||
Пакет не равен SLM-модулю: модуль — архитектурная единица внутри слоя приложения, package — единица монорепозитория для переиспользования, владения, сборки и публикации.
|
||||
|
||||
В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. `packages/shared` устроен иначе: это единый пакет для переиспользуемых утилит, helpers и другого фундаментального кода без привязки к конкретному приложению.
|
||||
|
||||
```text
|
||||
packages/ui/button/
|
||||
packages/ui/modal/
|
||||
packages/infra/theme/
|
||||
packages/infra/backend-api/
|
||||
packages/shared/
|
||||
```
|
||||
|
||||
## Что остаётся в приложении
|
||||
|
||||
Слои `app`, `layouts`, `screens`, `widgets` и `business` остаются внутри конкретного приложения.
|
||||
|
||||
```text
|
||||
apps/web/src/app/
|
||||
apps/web/src/layouts/
|
||||
apps/web/src/screens/
|
||||
apps/web/src/widgets/
|
||||
apps/web/src/business/
|
||||
```
|
||||
|
||||
`app`, `layouts` и `screens` привязаны к роутингу, каркасу и страницам конкретного приложения. `widgets` не выносятся в пакеты, потому что это слой композиции интерфейса приложения.
|
||||
|
||||
`business` не выносится в `packages/*`. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой.
|
||||
|
||||
## Что можно выносить
|
||||
|
||||
В пакеты выносится только код из `ui`, `infra` и `shared`, который потенциально будет использоваться в двух и более фронтенд-приложениях монорепозитория.
|
||||
|
||||
| Группа | Что выносить | Пример |
|
||||
|--------|--------------|--------|
|
||||
| `packages/ui/*` | Самостоятельные UI-модули без бизнес-логики | `packages/ui/button` |
|
||||
| `packages/infra/*` | Самостоятельные технические сервисы | `packages/infra/backend-api` |
|
||||
| `packages/shared` | Общие утилиты, helpers и фундаментальный код | `packages/shared` |
|
||||
|
||||
Пакет можно создавать сразу, если модуль имеет общую природу и ожидается его переиспользование между приложениями. App-specific код остаётся внутри приложения.
|
||||
|
||||
## UI-пакеты
|
||||
|
||||
В `packages/ui/*` размещаются переиспользуемые UI-модули.
|
||||
|
||||
```text
|
||||
packages/ui/button/
|
||||
├── package.json
|
||||
└── src/
|
||||
├── button.tsx
|
||||
├── styles/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
UI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц.
|
||||
|
||||
## Infra-пакеты
|
||||
|
||||
В `packages/infra/*` размещаются переиспользуемые инфраструктурные модули.
|
||||
|
||||
```text
|
||||
packages/infra/backend-api/
|
||||
├── package.json
|
||||
└── src/
|
||||
├── clients/
|
||||
├── config/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Привязанные к конкретному приложению сервисы остаются в `apps/{app}/src/infra`. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок.
|
||||
|
||||
## Shared-пакет
|
||||
|
||||
`packages/shared` является единым пакетом.
|
||||
|
||||
```text
|
||||
packages/shared/
|
||||
├── package.json
|
||||
└── src/
|
||||
├── lib/
|
||||
├── helpers/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
В `packages/shared` сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте.
|
||||
|
||||
Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий `shared` не выносятся.
|
||||
|
||||
## Имена пакетов и импорты
|
||||
|
||||
Путь импорта задаётся `name` в `package.json`, а не расположением директории.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/theme"
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
packages/infra/theme/package.json
|
||||
```
|
||||
|
||||
```ts
|
||||
import { ThemeProvider } from '@repo/theme'
|
||||
```
|
||||
|
||||
Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены.
|
||||
|
||||
```ts
|
||||
// Хорошо
|
||||
import { Button } from '@repo/button'
|
||||
|
||||
// Плохо
|
||||
import { Button } from '@repo/button/src/button'
|
||||
```
|
||||
|
||||
## Зависимости
|
||||
|
||||
На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений.
|
||||
|
||||
```text
|
||||
apps → packages
|
||||
packages -/→ apps
|
||||
```
|
||||
|
||||
Внутри приложения продолжает действовать обычное направление зависимостей SLM.
|
||||
|
||||
```text
|
||||
app → [ layouts | screens ] → widgets → business → infra → ui → shared
|
||||
```
|
||||
|
||||
Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях.
|
||||
|
||||
## Когда не выносить
|
||||
|
||||
Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API.
|
||||
|
||||
Фактическое использование в одном приложении не запрещает пакет, если модуль имеет общую природу и потенциально нужен нескольким приложениям.
|
||||
|
||||
```text
|
||||
# Плохо
|
||||
apps/web/src/screens/home/parts/promo-section/
|
||||
packages/ui/promo-section/
|
||||
```
|
||||
|
||||
Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным `parts/`-модулем.
|
||||
|
||||
## Конфигурационные пакеты
|
||||
|
||||
Конфигурационные пакеты не относятся к SLM-архитектуре.
|
||||
|
||||
Если в монорепозитории есть общие настройки TypeScript, ESLint, сборки или форматирования, они относятся к tooling-инфраструктуре репозитория. Такие пакеты могут находиться в `packages/`, но их структура зависит от выбранного инструментария и не участвует в правилах слоёв внутри `src/`.
|
||||
|
||||
## Правила
|
||||
|
||||
- SLM применяется внутри каждого `apps/{app}/src`.
|
||||
- Frontend-пакеты, вынесенные из SLM-приложений, группируются в `packages/ui`, `packages/infra`, `packages/shared`.
|
||||
- Группы `packages/ui`, `packages/infra`, `packages/shared` не являются SLM-слоями.
|
||||
- В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей.
|
||||
- В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей.
|
||||
- `packages/shared` является единым пакетом для переиспользуемых утилит и helpers.
|
||||
- Модуль можно размещать в пакете, если он потенциально будет использоваться в двух и более фронтенд-приложениях.
|
||||
- `business`, `app`, `layouts`, `screens`, `widgets` не выносятся в пакеты.
|
||||
- Проектные стили, типы приложения и продуктовые конфиги не выносятся в `packages/shared`.
|
||||
- Пакеты не импортируют приложения.
|
||||
- Межпакетные импорты идут только через публичный API.
|
||||
- Deep imports внутрь пакетов запрещены.
|
||||
- Локальная колокация важнее преждевременного выноса в `packages/*`.
|
||||
@@ -1,249 +0,0 @@
|
||||
---
|
||||
title: Композиция через Provider
|
||||
description: Пример композиции бизнес-фабрик screen-модуля через React Provider
|
||||
---
|
||||
|
||||
# Композиция через Provider
|
||||
|
||||
Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя.
|
||||
|
||||
## Идея
|
||||
|
||||
Screen получает готовый API бизнес-доменов через React Context. Граф фабрик собирается снаружи, например в роутере, а внутренние `parts/` достают нужные домены через хук без пропс-дриллинга.
|
||||
|
||||
## Принципы
|
||||
|
||||
1. **Принадлежность.** Provider, Context и хук принадлежат конкретному screen-модулю и лежат в его сегментах.
|
||||
2. **Внутренний тип.** Тип композиции не экспортируется наружу — это закрывает доступ из бизнес-модулей.
|
||||
3. **Внутренний хук.** Хук доступа не экспортируется — доступен только внутри screen и его `parts/`.
|
||||
4. **Публичный Provider.** Только Provider экспортируется через `index.ts`, чтобы роутер мог обернуть screen.
|
||||
5. **Сборка снаружи.** Граф фабрик собирается в роутере или другом композиторе, screen фабрики не вызывает.
|
||||
6. **Запрет для бизнеса.** Бизнес-модули не используют провайдеры композиции. Cross-domain зависимости передаются только через аргументы фабрики.
|
||||
|
||||
## Структура модуля
|
||||
|
||||
```text
|
||||
screens/main/
|
||||
├── main.screen.tsx
|
||||
├── providers/
|
||||
│ └── main-composition.provider.tsx
|
||||
├── hooks/
|
||||
│ └── use-main-composition.hook.ts
|
||||
├── types/
|
||||
│ └── main-composition.type.ts
|
||||
├── parts/
|
||||
│ └── featured-products/
|
||||
│ ├── featured-products.tsx
|
||||
│ └── index.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Сегмент `providers/` — расширение стандартного набора SLM. Спецификация это разрешает: команда сама определяет, какие сегменты используются.
|
||||
|
||||
## Распределение по сегментам
|
||||
|
||||
| Файл | Сегмент | Назначение |
|
||||
|------|---------|------------|
|
||||
| `main-composition.type.ts` | `types/` | TypeScript-тип композиции |
|
||||
| `main-composition.provider.tsx` | `providers/` | Context и Provider-компонент |
|
||||
| `use-main-composition.hook.ts` | `hooks/` | React-хук доступа |
|
||||
| `main.screen.tsx` | корень | Корневой компонент screen-модуля |
|
||||
| `featured-products/` | `parts/` | Вложенный модуль со своим публичным API |
|
||||
|
||||
## Тип композиции
|
||||
|
||||
Файл: `screens/main/types/main-composition.type.ts`.
|
||||
|
||||
Тип описывает, какие бизнес-домены доступны на этой странице. Он не экспортируется через `index.ts`, чтобы другие модули не зависели от внутренней формы композиции screen.
|
||||
|
||||
```ts
|
||||
import type { CatalogApi } from '@/business/catalog'
|
||||
import type { CartApi } from '@/business/cart'
|
||||
|
||||
export type MainComposition = {
|
||||
catalog: CatalogApi
|
||||
cart: CartApi
|
||||
}
|
||||
```
|
||||
|
||||
## Context и Provider
|
||||
|
||||
Файл: `screens/main/providers/main-composition.provider.tsx`.
|
||||
|
||||
Context — внутренняя деталь Provider, наружу он не экспортируется. Значение `null` по умолчанию нужно, чтобы хук мог проверить отсутствие Provider в дереве.
|
||||
|
||||
Provider-компонент экспортируется через `index.ts`. Роутер передаёт в `value` уже собранный граф фабрик со стабильной ссылкой.
|
||||
|
||||
```tsx
|
||||
import { createContext, type ReactNode } from 'react'
|
||||
import type { MainComposition } from '../types/main-composition.type'
|
||||
|
||||
export const MainCompositionContext = createContext<MainComposition | null>(null)
|
||||
|
||||
type Props = {
|
||||
value: MainComposition
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const MainCompositionProvider = ({ value, children }: Props) => (
|
||||
<MainCompositionContext.Provider value={value}>
|
||||
{children}
|
||||
</MainCompositionContext.Provider>
|
||||
)
|
||||
```
|
||||
|
||||
## Хук доступа
|
||||
|
||||
Файл: `screens/main/hooks/use-main-composition.hook.ts`.
|
||||
|
||||
Хук остаётся внутренним и не экспортируется через `index.ts` модуля. Он доступен только внутри screen и его `parts/`.
|
||||
|
||||
Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева.
|
||||
|
||||
```ts
|
||||
import { useContext } from 'react'
|
||||
import { MainCompositionContext } from '../providers/main-composition.provider'
|
||||
|
||||
export const useMainComposition = () => {
|
||||
const ctx = useContext(MainCompositionContext)
|
||||
if (!ctx) {
|
||||
throw new Error('useMainComposition must be used within MainCompositionProvider')
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
```
|
||||
|
||||
## Сборка графа в роутере
|
||||
|
||||
Файл: `app/router.tsx`.
|
||||
|
||||
Роутер или другой композитор собирает граф фабрик в точке использования screen. Каждый домен получает свои зависимости через аргументы фабрики.
|
||||
|
||||
Фабрики вызываются вне React-компонента, если не зависят от runtime-параметров. Так API доменов не пересоздаётся на каждый рендер route-компонента.
|
||||
|
||||
```tsx
|
||||
import { MainScreen, MainCompositionProvider } from '@/screens/main'
|
||||
import { catalogFactory } from '@/business/catalog'
|
||||
import { cartFactory } from '@/business/cart'
|
||||
import { authFactory } from '@/business/auth'
|
||||
|
||||
const auth = authFactory()
|
||||
const catalog = catalogFactory()
|
||||
const cart = cartFactory({ auth })
|
||||
|
||||
const MainRoute = () => (
|
||||
<MainCompositionProvider value={{ catalog, cart }}>
|
||||
<MainScreen />
|
||||
</MainCompositionProvider>
|
||||
)
|
||||
```
|
||||
|
||||
## Корневой компонент screen
|
||||
|
||||
Файл: `screens/main/main.screen.tsx`.
|
||||
|
||||
Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные `useCategories` и `CategoryList`, а не обращение к фабричному API через точку.
|
||||
|
||||
```tsx
|
||||
import { useMainComposition } from './hooks/use-main-composition.hook'
|
||||
import { FeaturedProducts } from './parts/featured-products'
|
||||
|
||||
export const MainScreen = () => {
|
||||
const { catalog } = useMainComposition()
|
||||
const { useCategories, CategoryList } = catalog
|
||||
const categories = useCategories()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CategoryList categories={categories} />
|
||||
<FeaturedProducts />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Вложенный part
|
||||
|
||||
Файл: `screens/main/parts/featured-products/featured-products.tsx`.
|
||||
|
||||
Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props.
|
||||
|
||||
Из API доменов достаются готовые сущности: `useFeatured`, `ProductCard` и `addItem`. Компонент работает с ними напрямую.
|
||||
|
||||
```tsx
|
||||
import { useMainComposition } from '../../hooks/use-main-composition.hook'
|
||||
|
||||
export const FeaturedProducts = () => {
|
||||
const { catalog, cart } = useMainComposition()
|
||||
const { useFeatured, ProductCard } = catalog
|
||||
const { addItem } = cart
|
||||
const products = useFeatured()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{products.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
onAdd={() => addItem(product.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Файл: `screens/main/parts/featured-products/index.ts`.
|
||||
|
||||
```ts
|
||||
export { FeaturedProducts } from './featured-products'
|
||||
```
|
||||
|
||||
## Публичный API screen-модуля
|
||||
|
||||
Файл: `screens/main/index.ts`.
|
||||
|
||||
Наружу экспортируются только screen и его Provider. `MainComposition`, `MainCompositionContext` и `useMainComposition` остаются деталями реализации.
|
||||
|
||||
```ts
|
||||
export { MainScreen } from './main.screen'
|
||||
export { MainCompositionProvider } from './providers/main-composition.provider'
|
||||
```
|
||||
|
||||
## Почему тип композиции не экспортируется
|
||||
|
||||
Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen.
|
||||
|
||||
Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen.
|
||||
|
||||
```ts
|
||||
import type { MainComposition } from '@/screens/main'
|
||||
```
|
||||
|
||||
Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля.
|
||||
|
||||
## Почему хук не экспортируется
|
||||
|
||||
Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его `parts/`.
|
||||
|
||||
## Почему Provider экспортируется
|
||||
|
||||
Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева.
|
||||
|
||||
## Стабильность value
|
||||
|
||||
Фабрики создаются на уровне модуля, поэтому `catalog` и `cart` сохраняют ссылки между рендерами `MainRoute`.
|
||||
|
||||
Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию.
|
||||
|
||||
## Расширение на другие screen-модули
|
||||
|
||||
Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов.
|
||||
|
||||
```text
|
||||
screens/checkout/providers/checkout-composition.provider.tsx
|
||||
screens/checkout/hooks/use-checkout-composition.hook.ts
|
||||
screens/checkout/types/checkout-composition.type.ts
|
||||
```
|
||||
|
||||
Имена включают имя screen-модуля. Не используйте универсальные названия вроде `useComposition` или `useScope`: по имени файла должно быть понятно, к какой странице привязан Context.
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
title: Композиция фабрик
|
||||
description: Пример композиции business-фабрик на уровне screen-модуля в React-проекте
|
||||
---
|
||||
|
||||
# Композиция фабрик
|
||||
|
||||
Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов.
|
||||
|
||||
## Идея
|
||||
|
||||
Композиция фабрик выполняется в модуле-потребителе: screen, layout или другом модуле группы «Композиция». Business-модули не импортируют runtime-код друг друга напрямую, а cross-domain зависимости получают только через аргументы фабрик.
|
||||
|
||||
## Структура screen-модуля
|
||||
|
||||
```text
|
||||
screens/home/
|
||||
├── home.screen.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Сборка фабрик
|
||||
|
||||
Файл: `screens/home/home.screen.tsx`.
|
||||
|
||||
```tsx
|
||||
import { customerFactory } from '@/business/customer'
|
||||
import { orderFactory } from '@/business/order'
|
||||
|
||||
const customer = customerFactory()
|
||||
const order = orderFactory({ customer })
|
||||
|
||||
const { useOrder, OrderCard } = order
|
||||
|
||||
export const HomeScreen = () => {
|
||||
const currentOrder = useOrder()
|
||||
|
||||
return <OrderCard order={currentOrder} />
|
||||
}
|
||||
```
|
||||
|
||||
`customerFactory` создаётся первой, потому что `orderFactory` зависит от части API домена `customer`. Модуль `order` не импортирует `customer` в runtime — зависимость передаётся снаружи.
|
||||
|
||||
## Публичный API screen-модуля
|
||||
|
||||
Файл: `screens/home/index.ts`.
|
||||
|
||||
```ts
|
||||
export { HomeScreen } from './home.screen'
|
||||
```
|
||||
|
||||
Screen экспортирует только собственный публичный API. Собранные экземпляры business API остаются деталями реализации screen-модуля.
|
||||
@@ -1,114 +0,0 @@
|
||||
---
|
||||
title: Создание фабрики
|
||||
description: Пример создания фабрики business-модуля в React-проекте
|
||||
---
|
||||
|
||||
# Создание фабрики
|
||||
|
||||
Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API.
|
||||
|
||||
## Структура business-модуля
|
||||
|
||||
Фабрика лежит в корне business-модуля. Типы публичного API и зависимостей размещаются в `types/`.
|
||||
|
||||
```text
|
||||
business/customer/
|
||||
├── customer.factory.ts
|
||||
├── hooks/
|
||||
├── types/
|
||||
│ ├── customer.type.ts
|
||||
│ ├── customer-api.type.ts
|
||||
│ └── customer-factory.type.ts
|
||||
├── ui/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Тип публичного API
|
||||
|
||||
Публичный API описывает runtime-возможности, которые модуль отдаёт потребителям: хуки, компоненты и сценарные методы.
|
||||
|
||||
```ts
|
||||
// business/customer/types/customer-api.type.ts
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Customer } from './customer.type'
|
||||
|
||||
export type CustomerCardProps = {
|
||||
customer: Customer
|
||||
}
|
||||
|
||||
export type CustomerApi = {
|
||||
useCustomer: () => Customer | null
|
||||
CustomerCard: (props: CustomerCardProps) => ReactNode
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/customer/types/customer-factory.type.ts
|
||||
import type { CustomerApi } from './customer-api.type'
|
||||
|
||||
export type CustomerFactory = () => CustomerApi
|
||||
```
|
||||
|
||||
## Фабрика без зависимостей
|
||||
|
||||
Если модулю не нужны другие домены в runtime, фабрика создаётся без аргументов.
|
||||
|
||||
```ts
|
||||
// business/customer/customer.factory.ts
|
||||
import { useCustomer } from './hooks/use-customer.hook'
|
||||
import { CustomerCard } from './ui/customer-card'
|
||||
import type { CustomerFactory } from './types/customer-factory.type'
|
||||
|
||||
export const customerFactory: CustomerFactory = () => {
|
||||
return {
|
||||
useCustomer,
|
||||
CustomerCard,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/customer/index.ts
|
||||
export { customerFactory } from './customer.factory'
|
||||
|
||||
export type { Customer } from './types/customer.type'
|
||||
export type { CustomerApi } from './types/customer-api.type'
|
||||
export type { CustomerFactory } from './types/customer-factory.type'
|
||||
```
|
||||
|
||||
## Фабрика с зависимостями
|
||||
|
||||
Если модулю нужен другой домен в runtime, зависимость передаётся аргументом фабрики. Тип зависимости описывает только нужную часть API.
|
||||
|
||||
```ts
|
||||
// business/order/types/order-deps.type.ts
|
||||
import type { CustomerApi } from '@/business/customer'
|
||||
|
||||
export type OrderDeps = {
|
||||
customer: Pick<CustomerApi, 'useCustomer'>
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/order/types/order-factory.type.ts
|
||||
import type { OrderApi } from './order-api.type'
|
||||
import type { OrderDeps } from './order-deps.type'
|
||||
|
||||
export type OrderFactory = (deps: OrderDeps) => OrderApi
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/order/order.factory.ts
|
||||
import { createUseOrder } from './hooks/use-order.hook'
|
||||
import { OrderCard } from './ui/order-card'
|
||||
import type { OrderFactory } from './types/order-factory.type'
|
||||
|
||||
export const orderFactory: OrderFactory = (deps) => {
|
||||
const useOrder = createUseOrder(deps)
|
||||
|
||||
return {
|
||||
useOrder,
|
||||
OrderCard,
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -12,7 +12,6 @@ title: Документация
|
||||
- Если проектируете структуру `src/` — откройте [Слои](/architecture/layers).
|
||||
- Если создаёте новый домен или блок интерфейса — используйте [Модули](/architecture/modules).
|
||||
- Если выбираете папку внутри модуля — смотрите [Сегменты](/architecture/segments).
|
||||
- Если адаптируете SLM к монорепозиторию — откройте [Монорепозитории](/architecture/monorepo).
|
||||
|
||||
## Разделы
|
||||
|
||||
@@ -22,7 +21,6 @@ title: Документация
|
||||
| [Слои](/architecture/layers) | Нужно определить, где должен жить код и какие зависимости допустимы. |
|
||||
| [Модули](/architecture/modules) | Нужно оформить границы модуля, публичный API или фабрику. |
|
||||
| [Сегменты](/architecture/segments) | Нужно выбрать внутреннюю папку для компонента, хука, стиля, типа или конфига. |
|
||||
| [Монорепозитории](/architecture/monorepo) | Нужно понять, что можно выносить в `packages/*` и какие слои остаются внутри приложения. |
|
||||
|
||||
## Для ассистентов
|
||||
|
||||
|
||||
117
generate.ts
117
generate.ts
@@ -5,12 +5,7 @@ import { execFileSync } from "child_process";
|
||||
|
||||
const SRC_DIR = "./docs";
|
||||
const PUBLIC_DIR = "./public";
|
||||
const DOCS_PUBLIC_DIR = path.join(SRC_DIR, "public");
|
||||
const DOC_ROUTE_PREFIX = "/docs";
|
||||
const PUBLIC_ARCHITECTURE_FILE = "ARCHITECTURE.md";
|
||||
const PUBLIC_ARCHITECTURE_NOTICE = `> Локальная копия канонической спецификации SLM Design.
|
||||
> Источник: https://slm-design.gromlab.ru/ARCHITECTURE.md
|
||||
> Не редактировать вручную в этом проекте.`;
|
||||
|
||||
interface SidebarItem {
|
||||
text: string;
|
||||
@@ -48,59 +43,15 @@ function parseSidebar(): SidebarGroup[] {
|
||||
|
||||
const SIDEBAR = parseSidebar();
|
||||
|
||||
function linkToFileRel(link: string): string {
|
||||
const rel = link.replace(/^\//, "");
|
||||
if (rel === "" || rel.endsWith("/")) return `${rel}index.md`;
|
||||
return `${rel}.md`;
|
||||
}
|
||||
|
||||
function fileRelToRoute(file: string): string {
|
||||
const route = file.endsWith("/index.md")
|
||||
? file.replace(/index\.md$/, "")
|
||||
: file.replace(/\.md$/, "");
|
||||
return `${DOC_ROUTE_PREFIX}/${route}`;
|
||||
}
|
||||
|
||||
function fileRelToMdUrl(file: string): string {
|
||||
return `${DOC_ROUTE_PREFIX}/${file}`;
|
||||
}
|
||||
|
||||
const DOC_LINK_RE = /\]\((\/(?:architecture|examples)(?:\/[^)\s#]*)?)(#[^)\s]*)?\)/g;
|
||||
|
||||
function docRouteToFileRel(route: string): string {
|
||||
if (route.replace(/\/$/, "") === "/architecture") return "architecture/index.md";
|
||||
if (route.replace(/\/$/, "") === "/examples") return "examples/index.md";
|
||||
return linkToFileRel(route);
|
||||
}
|
||||
|
||||
function transformDocLinks(
|
||||
content: string,
|
||||
toHref: (route: string, hash: string) => string,
|
||||
): string {
|
||||
return content.replace(DOC_LINK_RE, (_match, route: string, hash = "") => {
|
||||
return `](${toHref(route, hash)})`;
|
||||
});
|
||||
}
|
||||
|
||||
function formatRelativeMarkdownPath(fromFile: string, toFile: string): string {
|
||||
const relative = path.relative(path.dirname(fromFile), toFile).split(path.sep).join("/");
|
||||
return relative.startsWith(".") ? relative : `./${relative}`;
|
||||
}
|
||||
|
||||
function transformArchiveLinks(content: string, fromFile: string): string {
|
||||
return transformDocLinks(content, (route, hash) => {
|
||||
return `${formatRelativeMarkdownPath(fromFile, docRouteToFileRel(route))}${hash}`;
|
||||
});
|
||||
}
|
||||
|
||||
function transformSiteMarkdownLinks(content: string): string {
|
||||
return transformDocLinks(content, (route, hash) => {
|
||||
return `${fileRelToMdUrl(docRouteToFileRel(route))}${hash}`;
|
||||
});
|
||||
}
|
||||
|
||||
function getAllFiles(): string[] {
|
||||
return SIDEBAR.flatMap((g) => g.items.map((item) => linkToFileRel(item.link)));
|
||||
return SIDEBAR.flatMap((g) =>
|
||||
g.items.map((item) => {
|
||||
const rel = item.link.replace(/^\//, "") + ".md";
|
||||
const indexPath = rel.replace(/\.md$/, "/index.md");
|
||||
const filePath = path.join(SRC_DIR, indexPath);
|
||||
return fs.existsSync(filePath) ? indexPath : rel;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const stripFrontmatter = (content: string) =>
|
||||
@@ -109,18 +60,6 @@ const stripFrontmatter = (content: string) =>
|
||||
const stripRulesLink = (content: string) =>
|
||||
content.replace(/<!-- rules-link -->[\s\S]*?<!-- \/rules-link -->\n*/g, "");
|
||||
|
||||
function transformSingleFileLinks(content: string): string {
|
||||
return transformDocLinks(content, (route, hash) => {
|
||||
return `${fileRelToMdUrl(docRouteToFileRel(route))}${hash}`;
|
||||
});
|
||||
}
|
||||
|
||||
function transformReadmeLinks(content: string): string {
|
||||
return transformDocLinks(content, (route, hash) => {
|
||||
return `docs/${docRouteToFileRel(route)}${hash}`;
|
||||
});
|
||||
}
|
||||
|
||||
const shiftHeadings = (content: string) => {
|
||||
const lines = content.split("\n");
|
||||
let inCodeBlock = false;
|
||||
@@ -145,9 +84,8 @@ const buildArchitectureMarkdown = (routePrefix: string) => {
|
||||
const content = stripRulesLink(stripFrontmatter(raw)).trim();
|
||||
if (!content) continue;
|
||||
|
||||
const route = routePrefix + fileRelToRoute(file).replace(DOC_ROUTE_PREFIX, "");
|
||||
const shifted = file.endsWith("index.md") ? content : shiftHeadings(content);
|
||||
const processed = transformSingleFileLinks(shifted);
|
||||
const route = routePrefix + "/" + file.replace(/\.md$/, "");
|
||||
const processed = file.endsWith("index.md") ? content : shiftHeadings(content);
|
||||
parts.push(`<!-- ${route} -->\n${processed}`);
|
||||
}
|
||||
|
||||
@@ -161,7 +99,9 @@ function buildLlms() {
|
||||
for (const group of SIDEBAR) {
|
||||
parts.push(`## ${group.text}`);
|
||||
for (const item of group.items) {
|
||||
const fileRel = linkToFileRel(item.link);
|
||||
const rel = item.link.replace(/^\//, "") + ".md";
|
||||
const indexPath = rel.replace(/\.md$/, "/index.md");
|
||||
const fileRel = fs.existsSync(path.join(SRC_DIR, indexPath)) ? indexPath : rel;
|
||||
const filePath = path.join(SRC_DIR, fileRel);
|
||||
let desc = "";
|
||||
if (fs.existsSync(filePath)) {
|
||||
@@ -169,7 +109,7 @@ function buildLlms() {
|
||||
const fm = raw.match(/^---[\s\S]*?---\n*/m);
|
||||
desc = fm ? fm[0].match(/description:\s*(.+)/)?.[1] || "" : "";
|
||||
}
|
||||
const route = fileRelToMdUrl(fileRel);
|
||||
const route = "/docs" + item.link;
|
||||
const line = desc
|
||||
? `- [${item.text}](${route}): ${desc}`
|
||||
: `- [${item.text}](${route})`;
|
||||
@@ -190,29 +130,10 @@ function buildLlmsFull() {
|
||||
console.log(`llms-full.txt создан: ${outPath}`);
|
||||
}
|
||||
|
||||
function copyMarkdownFiles() {
|
||||
fs.rmSync(DOCS_PUBLIC_DIR, { recursive: true, force: true });
|
||||
|
||||
let copied = 0;
|
||||
for (const file of getAllFiles()) {
|
||||
const src = path.join(SRC_DIR, file);
|
||||
if (!fs.existsSync(src)) continue;
|
||||
|
||||
const content = transformSiteMarkdownLinks(fs.readFileSync(src, "utf8"));
|
||||
const dest = path.join(DOCS_PUBLIC_DIR, file);
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(dest, content, "utf8");
|
||||
copied++;
|
||||
}
|
||||
|
||||
console.log(`скопировано ${copied} .md-файлов в ${DOCS_PUBLIC_DIR}`);
|
||||
}
|
||||
|
||||
function buildPublicArchitecture() {
|
||||
const outPath = path.join(PUBLIC_DIR, PUBLIC_ARCHITECTURE_FILE);
|
||||
const content = `${PUBLIC_ARCHITECTURE_NOTICE}\n\n${buildArchitectureMarkdown("/docs")}`;
|
||||
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
|
||||
fs.writeFileSync(outPath, content, "utf8");
|
||||
fs.writeFileSync(outPath, buildArchitectureMarkdown("/docs"), "utf8");
|
||||
console.log(`${PUBLIC_ARCHITECTURE_FILE} создан: ${outPath}`);
|
||||
}
|
||||
|
||||
@@ -227,10 +148,8 @@ function buildZip() {
|
||||
if (!fs.existsSync(src)) continue;
|
||||
let content = fs.readFileSync(src, "utf8");
|
||||
content = stripRulesLink(stripFrontmatter(content)).trim();
|
||||
content = transformArchiveLinks(content, file);
|
||||
const dest = path.join(tmpDir, file);
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(dest, content, "utf8");
|
||||
const destName = path.basename(file);
|
||||
fs.writeFileSync(path.join(tmpDir, destName), content, "utf8");
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8"));
|
||||
@@ -255,14 +174,12 @@ function buildReadme() {
|
||||
|
||||
let content = stripFrontmatter(fs.readFileSync(indexPath, "utf8"));
|
||||
content = content.replace(/<!-- rules-link -->[\s\S]*?<!-- \/rules-link -->\n*/g, "");
|
||||
content = transformReadmeLinks(content);
|
||||
fs.writeFileSync("./README.md", content, "utf8");
|
||||
console.log("README.md создан");
|
||||
}
|
||||
|
||||
buildLlms();
|
||||
buildLlmsFull();
|
||||
copyMarkdownFiles();
|
||||
buildPublicArchitecture();
|
||||
buildZip();
|
||||
buildReadme();
|
||||
|
||||
20
index.html
20
index.html
@@ -5,28 +5,10 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SLM Design</title>
|
||||
<meta name="description" content="Scoped Layered Module Design — модульная архитектура фронтенд-приложений" />
|
||||
<meta name="llms" content="/llms.txt" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="alternate llms" type="text/plain" href="/llms.txt" title="llms.txt" />
|
||||
<link rel="alternate" type="text/plain" href="/llms-full.txt" title="llms-full.txt" />
|
||||
<link rel="alternate" type="text/markdown" href="/ARCHITECTURE.md" title="ARCHITECTURE.md" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<main>
|
||||
<h1>SLM Design</h1>
|
||||
<p>Scoped Layered Module Design — модульная архитектура фронтенд-приложений.</p>
|
||||
<nav aria-label="Карта сайта и AI-артефакты">
|
||||
<ul>
|
||||
<li><a href="/docs/">Документация</a></li>
|
||||
<li><a href="/llms.txt" rel="alternate" type="text/plain">llms.txt</a></li>
|
||||
<li><a href="/llms-full.txt" rel="alternate" type="text/plain">llms-full.txt</a></li>
|
||||
<li><a href="/ARCHITECTURE.md" rel="alternate" type="text/markdown">ARCHITECTURE.md</a></li>
|
||||
<li><a href="/slm-design.zip" download>slm-design.zip</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</main>
|
||||
</div>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
> Локальная копия канонической спецификации SLM Design.
|
||||
> Источник: https://slm-design.gromlab.ru/ARCHITECTURE.md
|
||||
> Не редактировать вручную в этом проекте.
|
||||
|
||||
<!-- /docs/architecture/ -->
|
||||
<!-- /docs/architecture//index -->
|
||||
# SLM Design
|
||||
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
||||
|
||||
## Разделы спецификации
|
||||
|
||||
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
|
||||
|
||||
- [Слои](/docs/architecture/layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
|
||||
- [Модули](/docs/architecture/modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
|
||||
- [Сегменты](/docs/architecture/segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
|
||||
- [Монорепозитории](/docs/architecture/monorepo.md) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business.
|
||||
|
||||
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории.
|
||||
|
||||
## Преимущества
|
||||
|
||||
### Вертикальная организация домена
|
||||
@@ -47,10 +32,6 @@ Cross-domain зависимости в бизнес-слое реализуют
|
||||
|
||||
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции).
|
||||
|
||||
### Адаптация к монорепозиториям
|
||||
|
||||
SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы.
|
||||
|
||||
## Происхождение
|
||||
|
||||
SLM Design вырос на основе:
|
||||
@@ -513,7 +494,7 @@ backend-api/
|
||||
└── index.ts # публичный API
|
||||
```
|
||||
|
||||
Подробное описание сегментов — в разделе [Сегменты](/docs/architecture/segments.md).
|
||||
Подробное описание сегментов — в разделе [Сегменты](/architecture/segments).
|
||||
|
||||
### Публичный API
|
||||
|
||||
@@ -556,11 +537,87 @@ Business-модуль всегда экспортирует фабрику. Фа
|
||||
|
||||
Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция».
|
||||
|
||||
#### Примеры
|
||||
#### Структура business-модуля
|
||||
|
||||
Пример реализации фабрики в React см. в [Создание фабрики](/docs/examples/react/factory.md).
|
||||
```text
|
||||
business/customer/
|
||||
├── customer.factory.ts
|
||||
├── index.ts
|
||||
└── types/
|
||||
├── customer.type.ts
|
||||
├── customer-api.type.ts
|
||||
├── customer-deps.type.ts
|
||||
└── customer-factory.type.ts
|
||||
```
|
||||
|
||||
Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](/docs/examples/react/factory-composition.md).
|
||||
#### Типы
|
||||
|
||||
```ts
|
||||
// business/customer/types/customer-api.type.ts
|
||||
export type CustomerApi = {
|
||||
useCustomer: () => Customer
|
||||
CustomerCard: (props: CustomerCardProps) => ReactNode
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/order/types/order-deps.type.ts
|
||||
export type OrderDeps = {
|
||||
customer: Pick<CustomerApi, 'useCustomer'>
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/order/types/order-factory.type.ts
|
||||
export type OrderFactory = (deps: OrderDeps) => OrderApi
|
||||
```
|
||||
|
||||
#### Фабрика без зависимостей
|
||||
|
||||
```ts
|
||||
// business/customer/customer.factory.ts
|
||||
import type { CustomerFactory } from './types/customer-factory.type'
|
||||
|
||||
export const customerFactory: CustomerFactory = () => {
|
||||
return {
|
||||
useCustomer,
|
||||
CustomerCard,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Фабрика с зависимостями
|
||||
|
||||
```ts
|
||||
// business/order/order.factory.ts
|
||||
import type { OrderFactory } from './types/order-factory.type'
|
||||
|
||||
export const orderFactory: OrderFactory = (deps) => {
|
||||
return {
|
||||
useOrder,
|
||||
OrderCard,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Композиция на уровне screen
|
||||
|
||||
```tsx
|
||||
// screens/home/home.screen.tsx
|
||||
import { customerFactory } from '@/business/customer'
|
||||
import { orderFactory } from '@/business/order'
|
||||
|
||||
const customer = customerFactory()
|
||||
const order = orderFactory({ customer })
|
||||
|
||||
const { useOrder, OrderCard } = order
|
||||
|
||||
export const HomeScreen = () => {
|
||||
const currentOrder = useOrder()
|
||||
|
||||
return <OrderCard order={currentOrder} />
|
||||
}
|
||||
```
|
||||
|
||||
### Жизненный цикл
|
||||
|
||||
@@ -614,7 +671,7 @@ Business-модуль всегда экспортирует фабрику. Фа
|
||||
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
|
||||
- Не содержит бизнес-логику или сценарную логику.
|
||||
|
||||
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](/docs/architecture/modules.md#компонент).
|
||||
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](/architecture/modules#компонент).
|
||||
|
||||
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.
|
||||
|
||||
@@ -750,642 +807,4 @@ lib/
|
||||
config/
|
||||
├── routes.ts
|
||||
└── constants.ts
|
||||
```
|
||||
|
||||
<!-- /docs/architecture/monorepo -->
|
||||
## Монорепозитории
|
||||
|
||||
Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов.
|
||||
|
||||
### Определение
|
||||
|
||||
**Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв `ui`, `infra` и `shared`.**
|
||||
|
||||
### Базовая структура
|
||||
|
||||
Каждое приложение внутри `apps/` сохраняет собственную SLM-структуру в `src/`.
|
||||
|
||||
```text
|
||||
repo/
|
||||
├── apps/
|
||||
│ ├── web/
|
||||
│ │ └── src/
|
||||
│ │ ├── app/
|
||||
│ │ ├── layouts/
|
||||
│ │ ├── screens/
|
||||
│ │ ├── widgets/
|
||||
│ │ ├── business/
|
||||
│ │ ├── infra/
|
||||
│ │ ├── ui/
|
||||
│ │ └── shared/
|
||||
│ └── admin/
|
||||
│ └── src/
|
||||
│ └── ...
|
||||
└── packages/
|
||||
├── ui/
|
||||
│ ├── button/ # самостоятельный пакет UI-модуля
|
||||
│ ├── input/ # самостоятельный пакет UI-модуля
|
||||
│ └── modal/ # самостоятельный пакет UI-модуля
|
||||
├── infra/
|
||||
│ ├── theme/ # самостоятельный пакет infra-модуля
|
||||
│ ├── backend-api/ # самостоятельный пакет infra-модуля
|
||||
│ └── logger/ # самостоятельный пакет infra-модуля
|
||||
└── shared/ # единый shared-пакет
|
||||
├── package.json
|
||||
└── src/
|
||||
├── lib/ # переиспользуемые утилиты
|
||||
├── helpers/ # переиспользуемые helpers
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`apps/{app}/src` — граница SLM-приложения. `packages/*` находятся выше SLM и не добавляют новые архитектурные слои.
|
||||
|
||||
### Группировка frontend-пакетов
|
||||
|
||||
Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: `ui`, `infra`, `shared`.
|
||||
|
||||
```text
|
||||
packages/ui/* # пакеты UI-модулей
|
||||
packages/infra/* # пакеты infra-модулей
|
||||
packages/shared # единый shared-пакет
|
||||
```
|
||||
|
||||
Эта группировка повторяет названия SLM-слоёв для навигации, но сама не является слоистой архитектурой внутри `packages/`. Монорепозиторий может содержать другие пакеты: tooling, конфиги, SDK, схемы, e2e и другие технические пакеты вне SLM.
|
||||
|
||||
### Пакет и модуль
|
||||
|
||||
Пакет не равен SLM-модулю: модуль — архитектурная единица внутри слоя приложения, package — единица монорепозитория для переиспользования, владения, сборки и публикации.
|
||||
|
||||
В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. `packages/shared` устроен иначе: это единый пакет для переиспользуемых утилит, helpers и другого фундаментального кода без привязки к конкретному приложению.
|
||||
|
||||
```text
|
||||
packages/ui/button/
|
||||
packages/ui/modal/
|
||||
packages/infra/theme/
|
||||
packages/infra/backend-api/
|
||||
packages/shared/
|
||||
```
|
||||
|
||||
### Что остаётся в приложении
|
||||
|
||||
Слои `app`, `layouts`, `screens`, `widgets` и `business` остаются внутри конкретного приложения.
|
||||
|
||||
```text
|
||||
apps/web/src/app/
|
||||
apps/web/src/layouts/
|
||||
apps/web/src/screens/
|
||||
apps/web/src/widgets/
|
||||
apps/web/src/business/
|
||||
```
|
||||
|
||||
`app`, `layouts` и `screens` привязаны к роутингу, каркасу и страницам конкретного приложения. `widgets` не выносятся в пакеты, потому что это слой композиции интерфейса приложения.
|
||||
|
||||
`business` не выносится в `packages/*`. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой.
|
||||
|
||||
### Что можно выносить
|
||||
|
||||
В пакеты выносится только код из `ui`, `infra` и `shared`, который потенциально будет использоваться в двух и более фронтенд-приложениях монорепозитория.
|
||||
|
||||
| Группа | Что выносить | Пример |
|
||||
|--------|--------------|--------|
|
||||
| `packages/ui/*` | Самостоятельные UI-модули без бизнес-логики | `packages/ui/button` |
|
||||
| `packages/infra/*` | Самостоятельные технические сервисы | `packages/infra/backend-api` |
|
||||
| `packages/shared` | Общие утилиты, helpers и фундаментальный код | `packages/shared` |
|
||||
|
||||
Пакет можно создавать сразу, если модуль имеет общую природу и ожидается его переиспользование между приложениями. App-specific код остаётся внутри приложения.
|
||||
|
||||
### UI-пакеты
|
||||
|
||||
В `packages/ui/*` размещаются переиспользуемые UI-модули.
|
||||
|
||||
```text
|
||||
packages/ui/button/
|
||||
├── package.json
|
||||
└── src/
|
||||
├── button.tsx
|
||||
├── styles/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
UI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц.
|
||||
|
||||
### Infra-пакеты
|
||||
|
||||
В `packages/infra/*` размещаются переиспользуемые инфраструктурные модули.
|
||||
|
||||
```text
|
||||
packages/infra/backend-api/
|
||||
├── package.json
|
||||
└── src/
|
||||
├── clients/
|
||||
├── config/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Привязанные к конкретному приложению сервисы остаются в `apps/{app}/src/infra`. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок.
|
||||
|
||||
### Shared-пакет
|
||||
|
||||
`packages/shared` является единым пакетом.
|
||||
|
||||
```text
|
||||
packages/shared/
|
||||
├── package.json
|
||||
└── src/
|
||||
├── lib/
|
||||
├── helpers/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
В `packages/shared` сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте.
|
||||
|
||||
Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий `shared` не выносятся.
|
||||
|
||||
### Имена пакетов и импорты
|
||||
|
||||
Путь импорта задаётся `name` в `package.json`, а не расположением директории.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/theme"
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
packages/infra/theme/package.json
|
||||
```
|
||||
|
||||
```ts
|
||||
import { ThemeProvider } from '@repo/theme'
|
||||
```
|
||||
|
||||
Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены.
|
||||
|
||||
```ts
|
||||
// Хорошо
|
||||
import { Button } from '@repo/button'
|
||||
|
||||
// Плохо
|
||||
import { Button } from '@repo/button/src/button'
|
||||
```
|
||||
|
||||
### Зависимости
|
||||
|
||||
На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений.
|
||||
|
||||
```text
|
||||
apps → packages
|
||||
packages -/→ apps
|
||||
```
|
||||
|
||||
Внутри приложения продолжает действовать обычное направление зависимостей SLM.
|
||||
|
||||
```text
|
||||
app → [ layouts | screens ] → widgets → business → infra → ui → shared
|
||||
```
|
||||
|
||||
Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях.
|
||||
|
||||
### Когда не выносить
|
||||
|
||||
Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API.
|
||||
|
||||
Фактическое использование в одном приложении не запрещает пакет, если модуль имеет общую природу и потенциально нужен нескольким приложениям.
|
||||
|
||||
```text
|
||||
# Плохо
|
||||
apps/web/src/screens/home/parts/promo-section/
|
||||
packages/ui/promo-section/
|
||||
```
|
||||
|
||||
Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным `parts/`-модулем.
|
||||
|
||||
### Конфигурационные пакеты
|
||||
|
||||
Конфигурационные пакеты не относятся к SLM-архитектуре.
|
||||
|
||||
Если в монорепозитории есть общие настройки TypeScript, ESLint, сборки или форматирования, они относятся к tooling-инфраструктуре репозитория. Такие пакеты могут находиться в `packages/`, но их структура зависит от выбранного инструментария и не участвует в правилах слоёв внутри `src/`.
|
||||
|
||||
### Правила
|
||||
|
||||
- SLM применяется внутри каждого `apps/{app}/src`.
|
||||
- Frontend-пакеты, вынесенные из SLM-приложений, группируются в `packages/ui`, `packages/infra`, `packages/shared`.
|
||||
- Группы `packages/ui`, `packages/infra`, `packages/shared` не являются SLM-слоями.
|
||||
- В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей.
|
||||
- В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей.
|
||||
- `packages/shared` является единым пакетом для переиспользуемых утилит и helpers.
|
||||
- Модуль можно размещать в пакете, если он потенциально будет использоваться в двух и более фронтенд-приложениях.
|
||||
- `business`, `app`, `layouts`, `screens`, `widgets` не выносятся в пакеты.
|
||||
- Проектные стили, типы приложения и продуктовые конфиги не выносятся в `packages/shared`.
|
||||
- Пакеты не импортируют приложения.
|
||||
- Межпакетные импорты идут только через публичный API.
|
||||
- Deep imports внутрь пакетов запрещены.
|
||||
- Локальная колокация важнее преждевременного выноса в `packages/*`.
|
||||
|
||||
<!-- /docs/examples/react/factory -->
|
||||
## Создание фабрики
|
||||
|
||||
Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API.
|
||||
|
||||
### Структура business-модуля
|
||||
|
||||
Фабрика лежит в корне business-модуля. Типы публичного API и зависимостей размещаются в `types/`.
|
||||
|
||||
```text
|
||||
business/customer/
|
||||
├── customer.factory.ts
|
||||
├── hooks/
|
||||
├── types/
|
||||
│ ├── customer.type.ts
|
||||
│ ├── customer-api.type.ts
|
||||
│ └── customer-factory.type.ts
|
||||
├── ui/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### Тип публичного API
|
||||
|
||||
Публичный API описывает runtime-возможности, которые модуль отдаёт потребителям: хуки, компоненты и сценарные методы.
|
||||
|
||||
```ts
|
||||
// business/customer/types/customer-api.type.ts
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Customer } from './customer.type'
|
||||
|
||||
export type CustomerCardProps = {
|
||||
customer: Customer
|
||||
}
|
||||
|
||||
export type CustomerApi = {
|
||||
useCustomer: () => Customer | null
|
||||
CustomerCard: (props: CustomerCardProps) => ReactNode
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/customer/types/customer-factory.type.ts
|
||||
import type { CustomerApi } from './customer-api.type'
|
||||
|
||||
export type CustomerFactory = () => CustomerApi
|
||||
```
|
||||
|
||||
### Фабрика без зависимостей
|
||||
|
||||
Если модулю не нужны другие домены в runtime, фабрика создаётся без аргументов.
|
||||
|
||||
```ts
|
||||
// business/customer/customer.factory.ts
|
||||
import { useCustomer } from './hooks/use-customer.hook'
|
||||
import { CustomerCard } from './ui/customer-card'
|
||||
import type { CustomerFactory } from './types/customer-factory.type'
|
||||
|
||||
export const customerFactory: CustomerFactory = () => {
|
||||
return {
|
||||
useCustomer,
|
||||
CustomerCard,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/customer/index.ts
|
||||
export { customerFactory } from './customer.factory'
|
||||
|
||||
export type { Customer } from './types/customer.type'
|
||||
export type { CustomerApi } from './types/customer-api.type'
|
||||
export type { CustomerFactory } from './types/customer-factory.type'
|
||||
```
|
||||
|
||||
### Фабрика с зависимостями
|
||||
|
||||
Если модулю нужен другой домен в runtime, зависимость передаётся аргументом фабрики. Тип зависимости описывает только нужную часть API.
|
||||
|
||||
```ts
|
||||
// business/order/types/order-deps.type.ts
|
||||
import type { CustomerApi } from '@/business/customer'
|
||||
|
||||
export type OrderDeps = {
|
||||
customer: Pick<CustomerApi, 'useCustomer'>
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/order/types/order-factory.type.ts
|
||||
import type { OrderApi } from './order-api.type'
|
||||
import type { OrderDeps } from './order-deps.type'
|
||||
|
||||
export type OrderFactory = (deps: OrderDeps) => OrderApi
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/order/order.factory.ts
|
||||
import { createUseOrder } from './hooks/use-order.hook'
|
||||
import { OrderCard } from './ui/order-card'
|
||||
import type { OrderFactory } from './types/order-factory.type'
|
||||
|
||||
export const orderFactory: OrderFactory = (deps) => {
|
||||
const useOrder = createUseOrder(deps)
|
||||
|
||||
return {
|
||||
useOrder,
|
||||
OrderCard,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- /docs/examples/react/factory-composition -->
|
||||
## Композиция фабрик
|
||||
|
||||
Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов.
|
||||
|
||||
### Идея
|
||||
|
||||
Композиция фабрик выполняется в модуле-потребителе: screen, layout или другом модуле группы «Композиция». Business-модули не импортируют runtime-код друг друга напрямую, а cross-domain зависимости получают только через аргументы фабрик.
|
||||
|
||||
### Структура screen-модуля
|
||||
|
||||
```text
|
||||
screens/home/
|
||||
├── home.screen.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### Сборка фабрик
|
||||
|
||||
Файл: `screens/home/home.screen.tsx`.
|
||||
|
||||
```tsx
|
||||
import { customerFactory } from '@/business/customer'
|
||||
import { orderFactory } from '@/business/order'
|
||||
|
||||
const customer = customerFactory()
|
||||
const order = orderFactory({ customer })
|
||||
|
||||
const { useOrder, OrderCard } = order
|
||||
|
||||
export const HomeScreen = () => {
|
||||
const currentOrder = useOrder()
|
||||
|
||||
return <OrderCard order={currentOrder} />
|
||||
}
|
||||
```
|
||||
|
||||
`customerFactory` создаётся первой, потому что `orderFactory` зависит от части API домена `customer`. Модуль `order` не импортирует `customer` в runtime — зависимость передаётся снаружи.
|
||||
|
||||
### Публичный API screen-модуля
|
||||
|
||||
Файл: `screens/home/index.ts`.
|
||||
|
||||
```ts
|
||||
export { HomeScreen } from './home.screen'
|
||||
```
|
||||
|
||||
Screen экспортирует только собственный публичный API. Собранные экземпляры business API остаются деталями реализации screen-модуля.
|
||||
|
||||
<!-- /docs/examples/react/composition-provider -->
|
||||
## Композиция через Provider
|
||||
|
||||
Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя.
|
||||
|
||||
### Идея
|
||||
|
||||
Screen получает готовый API бизнес-доменов через React Context. Граф фабрик собирается снаружи, например в роутере, а внутренние `parts/` достают нужные домены через хук без пропс-дриллинга.
|
||||
|
||||
### Принципы
|
||||
|
||||
1. **Принадлежность.** Provider, Context и хук принадлежат конкретному screen-модулю и лежат в его сегментах.
|
||||
2. **Внутренний тип.** Тип композиции не экспортируется наружу — это закрывает доступ из бизнес-модулей.
|
||||
3. **Внутренний хук.** Хук доступа не экспортируется — доступен только внутри screen и его `parts/`.
|
||||
4. **Публичный Provider.** Только Provider экспортируется через `index.ts`, чтобы роутер мог обернуть screen.
|
||||
5. **Сборка снаружи.** Граф фабрик собирается в роутере или другом композиторе, screen фабрики не вызывает.
|
||||
6. **Запрет для бизнеса.** Бизнес-модули не используют провайдеры композиции. Cross-domain зависимости передаются только через аргументы фабрики.
|
||||
|
||||
### Структура модуля
|
||||
|
||||
```text
|
||||
screens/main/
|
||||
├── main.screen.tsx
|
||||
├── providers/
|
||||
│ └── main-composition.provider.tsx
|
||||
├── hooks/
|
||||
│ └── use-main-composition.hook.ts
|
||||
├── types/
|
||||
│ └── main-composition.type.ts
|
||||
├── parts/
|
||||
│ └── featured-products/
|
||||
│ ├── featured-products.tsx
|
||||
│ └── index.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Сегмент `providers/` — расширение стандартного набора SLM. Спецификация это разрешает: команда сама определяет, какие сегменты используются.
|
||||
|
||||
### Распределение по сегментам
|
||||
|
||||
| Файл | Сегмент | Назначение |
|
||||
|------|---------|------------|
|
||||
| `main-composition.type.ts` | `types/` | TypeScript-тип композиции |
|
||||
| `main-composition.provider.tsx` | `providers/` | Context и Provider-компонент |
|
||||
| `use-main-composition.hook.ts` | `hooks/` | React-хук доступа |
|
||||
| `main.screen.tsx` | корень | Корневой компонент screen-модуля |
|
||||
| `featured-products/` | `parts/` | Вложенный модуль со своим публичным API |
|
||||
|
||||
### Тип композиции
|
||||
|
||||
Файл: `screens/main/types/main-composition.type.ts`.
|
||||
|
||||
Тип описывает, какие бизнес-домены доступны на этой странице. Он не экспортируется через `index.ts`, чтобы другие модули не зависели от внутренней формы композиции screen.
|
||||
|
||||
```ts
|
||||
import type { CatalogApi } from '@/business/catalog'
|
||||
import type { CartApi } from '@/business/cart'
|
||||
|
||||
export type MainComposition = {
|
||||
catalog: CatalogApi
|
||||
cart: CartApi
|
||||
}
|
||||
```
|
||||
|
||||
### Context и Provider
|
||||
|
||||
Файл: `screens/main/providers/main-composition.provider.tsx`.
|
||||
|
||||
Context — внутренняя деталь Provider, наружу он не экспортируется. Значение `null` по умолчанию нужно, чтобы хук мог проверить отсутствие Provider в дереве.
|
||||
|
||||
Provider-компонент экспортируется через `index.ts`. Роутер передаёт в `value` уже собранный граф фабрик со стабильной ссылкой.
|
||||
|
||||
```tsx
|
||||
import { createContext, type ReactNode } from 'react'
|
||||
import type { MainComposition } from '../types/main-composition.type'
|
||||
|
||||
export const MainCompositionContext = createContext<MainComposition | null>(null)
|
||||
|
||||
type Props = {
|
||||
value: MainComposition
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const MainCompositionProvider = ({ value, children }: Props) => (
|
||||
<MainCompositionContext.Provider value={value}>
|
||||
{children}
|
||||
</MainCompositionContext.Provider>
|
||||
)
|
||||
```
|
||||
|
||||
### Хук доступа
|
||||
|
||||
Файл: `screens/main/hooks/use-main-composition.hook.ts`.
|
||||
|
||||
Хук остаётся внутренним и не экспортируется через `index.ts` модуля. Он доступен только внутри screen и его `parts/`.
|
||||
|
||||
Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева.
|
||||
|
||||
```ts
|
||||
import { useContext } from 'react'
|
||||
import { MainCompositionContext } from '../providers/main-composition.provider'
|
||||
|
||||
export const useMainComposition = () => {
|
||||
const ctx = useContext(MainCompositionContext)
|
||||
if (!ctx) {
|
||||
throw new Error('useMainComposition must be used within MainCompositionProvider')
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
```
|
||||
|
||||
### Сборка графа в роутере
|
||||
|
||||
Файл: `app/router.tsx`.
|
||||
|
||||
Роутер или другой композитор собирает граф фабрик в точке использования screen. Каждый домен получает свои зависимости через аргументы фабрики.
|
||||
|
||||
Фабрики вызываются вне React-компонента, если не зависят от runtime-параметров. Так API доменов не пересоздаётся на каждый рендер route-компонента.
|
||||
|
||||
```tsx
|
||||
import { MainScreen, MainCompositionProvider } from '@/screens/main'
|
||||
import { catalogFactory } from '@/business/catalog'
|
||||
import { cartFactory } from '@/business/cart'
|
||||
import { authFactory } from '@/business/auth'
|
||||
|
||||
const auth = authFactory()
|
||||
const catalog = catalogFactory()
|
||||
const cart = cartFactory({ auth })
|
||||
|
||||
const MainRoute = () => (
|
||||
<MainCompositionProvider value={{ catalog, cart }}>
|
||||
<MainScreen />
|
||||
</MainCompositionProvider>
|
||||
)
|
||||
```
|
||||
|
||||
### Корневой компонент screen
|
||||
|
||||
Файл: `screens/main/main.screen.tsx`.
|
||||
|
||||
Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные `useCategories` и `CategoryList`, а не обращение к фабричному API через точку.
|
||||
|
||||
```tsx
|
||||
import { useMainComposition } from './hooks/use-main-composition.hook'
|
||||
import { FeaturedProducts } from './parts/featured-products'
|
||||
|
||||
export const MainScreen = () => {
|
||||
const { catalog } = useMainComposition()
|
||||
const { useCategories, CategoryList } = catalog
|
||||
const categories = useCategories()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CategoryList categories={categories} />
|
||||
<FeaturedProducts />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Вложенный part
|
||||
|
||||
Файл: `screens/main/parts/featured-products/featured-products.tsx`.
|
||||
|
||||
Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props.
|
||||
|
||||
Из API доменов достаются готовые сущности: `useFeatured`, `ProductCard` и `addItem`. Компонент работает с ними напрямую.
|
||||
|
||||
```tsx
|
||||
import { useMainComposition } from '../../hooks/use-main-composition.hook'
|
||||
|
||||
export const FeaturedProducts = () => {
|
||||
const { catalog, cart } = useMainComposition()
|
||||
const { useFeatured, ProductCard } = catalog
|
||||
const { addItem } = cart
|
||||
const products = useFeatured()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{products.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
onAdd={() => addItem(product.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Файл: `screens/main/parts/featured-products/index.ts`.
|
||||
|
||||
```ts
|
||||
export { FeaturedProducts } from './featured-products'
|
||||
```
|
||||
|
||||
### Публичный API screen-модуля
|
||||
|
||||
Файл: `screens/main/index.ts`.
|
||||
|
||||
Наружу экспортируются только screen и его Provider. `MainComposition`, `MainCompositionContext` и `useMainComposition` остаются деталями реализации.
|
||||
|
||||
```ts
|
||||
export { MainScreen } from './main.screen'
|
||||
export { MainCompositionProvider } from './providers/main-composition.provider'
|
||||
```
|
||||
|
||||
### Почему тип композиции не экспортируется
|
||||
|
||||
Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen.
|
||||
|
||||
Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen.
|
||||
|
||||
```ts
|
||||
import type { MainComposition } from '@/screens/main'
|
||||
```
|
||||
|
||||
Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля.
|
||||
|
||||
### Почему хук не экспортируется
|
||||
|
||||
Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его `parts/`.
|
||||
|
||||
### Почему Provider экспортируется
|
||||
|
||||
Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева.
|
||||
|
||||
### Стабильность value
|
||||
|
||||
Фабрики создаются на уровне модуля, поэтому `catalog` и `cart` сохраняют ссылки между рендерами `MainRoute`.
|
||||
|
||||
Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию.
|
||||
|
||||
### Расширение на другие screen-модули
|
||||
|
||||
Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов.
|
||||
|
||||
```text
|
||||
screens/checkout/providers/checkout-composition.provider.tsx
|
||||
screens/checkout/hooks/use-checkout-composition.hook.ts
|
||||
screens/checkout/types/checkout-composition.type.ts
|
||||
```
|
||||
|
||||
Имена включают имя screen-модуля. Не используйте универсальные названия вроде `useComposition` или `useScope`: по имени файла должно быть понятно, к какой странице привязан Context.
|
||||
```
|
||||
@@ -1,3 +1,5 @@
|
||||
const version = __BUILD_VERSION__
|
||||
|
||||
export const repositoryUrl = 'https://gromlab.ru/gromov/slm-design'
|
||||
|
||||
export const homeCards = [
|
||||
@@ -8,19 +10,10 @@ export const homeCards = [
|
||||
cta: 'Открыть →',
|
||||
},
|
||||
{
|
||||
title: 'Скачать',
|
||||
description: 'Локальная копия спецификации и архив документации.',
|
||||
actions: [
|
||||
{
|
||||
href: '/ARCHITECTURE.md',
|
||||
label: 'ARCHITECTURE.md',
|
||||
},
|
||||
{
|
||||
href: '/slm-design.zip',
|
||||
label: 'slm-design.zip',
|
||||
download: true,
|
||||
},
|
||||
],
|
||||
title: 'ARCHITECTURE.md',
|
||||
description: 'Полная версия архитектуры в одном файле',
|
||||
href: `/ARCHITECTURE.md?v=${version}`,
|
||||
cta: 'Открыть →',
|
||||
},
|
||||
{
|
||||
title: 'Ассистенту',
|
||||
|
||||
@@ -48,12 +48,7 @@ export function HomeScreen() {
|
||||
<p>{card.description}</p>
|
||||
<div className={styles.cardActions}>
|
||||
{card.actions.map((action) => (
|
||||
<a
|
||||
className={styles.cardAction}
|
||||
download={'download' in action ? action.download : undefined}
|
||||
href={action.href}
|
||||
key={action.href}
|
||||
>
|
||||
<a className={styles.cardAction} href={action.href} key={action.href}>
|
||||
{action.label}
|
||||
</a>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user