feat: добавить документацию NextJS Style Guide
- добавлен отдельный VitePress-сайт для NextJS Style Guide - удалены дубли SLM-канонов из style-guide - обновлены ссылки, сборочные скрипты, CI, Docker и README - разблокирована карточка NextJS Style Guide на главной
This commit is contained in:
@@ -23,6 +23,9 @@ jobs:
|
||||
- name: Сборка документации SLM Design
|
||||
run: npm run docs:build:slm-design
|
||||
|
||||
- name: Сборка документации NextJS Style Guide
|
||||
run: npm run docs:build:nextjs-style-guide
|
||||
|
||||
- name: Сборка документации Figma Adaptive Standards
|
||||
run: npm run docs:build:figma-adaptive-standards
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ docs/*/content/
|
||||
docs/*/.vitepress/cache/
|
||||
public/llms.txt
|
||||
public/slm-design/
|
||||
public/nextjs-style-guide/
|
||||
public/figma-adaptive-standards/
|
||||
|
||||
# Editor directories and files
|
||||
|
||||
@@ -6,7 +6,7 @@ COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run docs:build:slm-design && npm run docs:build:figma-adaptive-standards && npm run build
|
||||
RUN npm run docs:build:slm-design && npm run docs:build:nextjs-style-guide && npm run docs:build:figma-adaptive-standards && npm run build
|
||||
|
||||
FROM caddy:2-alpine
|
||||
|
||||
|
||||
18
README.md
18
README.md
@@ -8,6 +8,8 @@
|
||||
|
||||
- React/Vite-лендинг со списком документаций.
|
||||
- VitePress-сборка для `SLM Design`.
|
||||
- VitePress-сборка для `NextJS Style Guide`.
|
||||
- VitePress-сборка для `Figma Adaptive Standards`.
|
||||
- Корневой `llms.txt` как карта всех документаций.
|
||||
- Собственные `llms.txt` и `llms-full.txt` внутри каждой документации.
|
||||
- Docker/Caddy-конфигурация для публикации статической сборки.
|
||||
@@ -16,15 +18,17 @@
|
||||
## Документации
|
||||
|
||||
- `SLM Design` — архитектура frontend-приложений через слои, модули, публичные API и DI через фабрики.
|
||||
- `NextJS Style Guide` — будущие правила организации Next.js-приложений.
|
||||
- `NextJS Style Guide` — практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript.
|
||||
- `React Style Guide` — будущие правила написания React-кода.
|
||||
- `Figma Adaptive Standards` — будущие стандарты подготовки адаптивных макетов в Figma.
|
||||
- `Figma Adaptive Standards` — стандарты подготовки адаптивных макетов в Figma.
|
||||
|
||||
## Структура
|
||||
|
||||
```text
|
||||
canons/ исходные материалы и черновики
|
||||
docs/slm-design/ VitePress-сайт SLM Design
|
||||
docs/nextjs-style-guide/ VitePress-сайт NextJS Style Guide
|
||||
docs/figma-adaptive-standards/ VitePress-сайт Figma Adaptive Standards
|
||||
scripts/docs/ подготовка контента для документаций
|
||||
scripts/site/ генерация корневых артефактов сайта
|
||||
src/ React-лендинг
|
||||
@@ -40,6 +44,8 @@ npm run dev
|
||||
|
||||
```bash
|
||||
npm run docs:build:slm-design
|
||||
npm run docs:build:nextjs-style-guide
|
||||
npm run docs:build:figma-adaptive-standards
|
||||
npm run site:generate
|
||||
npm run build
|
||||
```
|
||||
@@ -48,6 +54,8 @@ npm run build
|
||||
|
||||
- `npm run dev` — запускает Vite dev server.
|
||||
- `npm run docs:build:slm-design` — подготавливает и собирает VitePress-документацию SLM Design.
|
||||
- `npm run docs:build:nextjs-style-guide` — подготавливает и собирает VitePress-документацию NextJS Style Guide.
|
||||
- `npm run docs:build:figma-adaptive-standards` — подготавливает и собирает VitePress-документацию Figma Adaptive Standards.
|
||||
- `npm run site:generate` — генерирует корневой `public/llms.txt` из `src/config/docs.config.ts` и хардкод-секций.
|
||||
- `npm run build` — генерирует корневые артефакты и собирает лендинг.
|
||||
- `npm run lint` — запускает ESLint.
|
||||
@@ -67,6 +75,10 @@ npm run build
|
||||
```text
|
||||
/slm-design/llms.txt
|
||||
/slm-design/llms-full.txt
|
||||
/nextjs-style-guide/llms.txt
|
||||
/nextjs-style-guide/llms-full.txt
|
||||
/figma-adaptive-standards/llms.txt
|
||||
/figma-adaptive-standards/llms-full.txt
|
||||
```
|
||||
|
||||
Корневой `llms-full.txt` намеренно не создаётся. Полные bundles остаются внутри конкретных документаций.
|
||||
@@ -95,6 +107,8 @@ Docker-сборка выполняет:
|
||||
|
||||
```bash
|
||||
npm run docs:build:slm-design
|
||||
npm run docs:build:nextjs-style-guide
|
||||
npm run docs:build:figma-adaptive-standards
|
||||
npm run build
|
||||
```
|
||||
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
---
|
||||
title: SLM Design
|
||||
description: Назначение архитектуры, ключевые принципы и карта разделов документации
|
||||
---
|
||||
|
||||
# SLM Design
|
||||
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
||||
|
||||
## Разделы спецификации
|
||||
|
||||
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
|
||||
|
||||
- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
|
||||
- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
|
||||
- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
|
||||
|
||||
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.
|
||||
|
||||
## Преимущества
|
||||
|
||||
### Вертикальная организация домена
|
||||
|
||||
Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы.
|
||||
|
||||
### Dependency Injection без фреймворков
|
||||
|
||||
Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий.
|
||||
|
||||
### Разделение ответственности без перегрузки слоёв
|
||||
|
||||
Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода.
|
||||
|
||||
### Горизонтальная инкапсуляция
|
||||
|
||||
Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга.
|
||||
|
||||
### Колокация по умолчанию
|
||||
|
||||
Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями.
|
||||
|
||||
### Явное разделение каркаса и контента
|
||||
|
||||
Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью.
|
||||
|
||||
### Масштабирование через группировку
|
||||
|
||||
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции).
|
||||
|
||||
## Происхождение
|
||||
|
||||
SLM Design вырос на основе:
|
||||
|
||||
- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей
|
||||
- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое
|
||||
- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию
|
||||
- **Colocation Principle** — код живёт рядом с местом использования
|
||||
|
||||
## Пример структуры проекта
|
||||
|
||||
```text
|
||||
src/
|
||||
├── app/
|
||||
│
|
||||
├── layouts/
|
||||
│ ├── main/
|
||||
│ └── dashboard/
|
||||
│
|
||||
├── screens/
|
||||
│ ├── home/
|
||||
│ ├── products/
|
||||
│ ├── product-detail/
|
||||
│ └── about/
|
||||
│
|
||||
├── widgets/
|
||||
│ ├── page-heading/
|
||||
│ ├── hero-section/
|
||||
│ └── promo-banner/
|
||||
│
|
||||
├── business/
|
||||
│ ├── auth/
|
||||
│ ├── catalog/
|
||||
│ ├── orders/
|
||||
│ └── chat/
|
||||
│
|
||||
├── infra/
|
||||
│ ├── theme/
|
||||
│ ├── i18n/
|
||||
│ ├── backend-api/
|
||||
│ └── logger/
|
||||
│
|
||||
├── ui/
|
||||
│ ├── button/
|
||||
│ ├── input/
|
||||
│ ├── modal/
|
||||
│ ├── toast/
|
||||
│ └── dropdown/
|
||||
│
|
||||
└── shared/
|
||||
├── lib/
|
||||
├── types/
|
||||
└── styles/
|
||||
```
|
||||
|
||||
## Принципы
|
||||
|
||||
- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле.
|
||||
- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости.
|
||||
- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API.
|
||||
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.
|
||||
@@ -1,254 +0,0 @@
|
||||
---
|
||||
title: Слои
|
||||
description: Иерархия слоёв от app до shared, правила зависимостей и зона ответственности каждого слоя
|
||||
---
|
||||
|
||||
# Слои
|
||||
|
||||
Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом.
|
||||
|
||||
## Определение
|
||||
|
||||
**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.**
|
||||
|
||||
## Группы слоёв
|
||||
|
||||
Слои делятся на три группы:
|
||||
|
||||
| Группа | Слои | Описание |
|
||||
|--------|------|----------|
|
||||
| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы |
|
||||
| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит |
|
||||
| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги |
|
||||
|
||||
## Направление зависимостей
|
||||
|
||||
Любой импорт между модулями — только через публичный API.
|
||||
|
||||
```
|
||||
app → [ layouts | screens ] → widgets → business → infra → ui → shared
|
||||
```
|
||||
|
||||
- `layouts` и `screens` — параллельные слои, не импортируют друг друга
|
||||
- Модули одного слоя в группе «Композиция» изолированы друг от друга
|
||||
- Модули одного слоя `infra` и `ui` могут импортировать друг друга через публичный API
|
||||
- Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую
|
||||
- Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях
|
||||
|
||||
|
||||
## Слой App
|
||||
|
||||
Точка входа приложения. Отвечает за запуск, роутинг и композицию маршрутов из layout и screen.
|
||||
|
||||
В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации.
|
||||
|
||||
### Требования
|
||||
|
||||
- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация
|
||||
- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов
|
||||
- Провайдеры и гарды — только подключает готовые из нижних слоёв, не реализует
|
||||
- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы
|
||||
- Никем не импортируется
|
||||
|
||||
## Слой Layouts
|
||||
|
||||
Каркас страницы: общие элементы, одинаковые для группы маршрутов (header, footer, sidebar).
|
||||
|
||||
```text
|
||||
src/layouts/
|
||||
├── main/
|
||||
├── dashboard/
|
||||
└── auth/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Содержит только модули
|
||||
- Не содержит бизнес-логику
|
||||
- Контекстно-зависимые блоки принимает через пропсы от `app`, не импортирует напрямую
|
||||
|
||||
## Слой Screens
|
||||
|
||||
Контент конкретной страницы: собирает её из модулей нижних слоёв.
|
||||
|
||||
```text
|
||||
src/screens/
|
||||
├── home/
|
||||
├── products/
|
||||
├── product-detail/
|
||||
├── about/
|
||||
└── contacts/
|
||||
```
|
||||
|
||||
Когда количество страниц затрудняет навигацию — вводится группировка по разделам. Группа — папка для организации, не модуль (без `index.ts`).
|
||||
|
||||
```text
|
||||
src/screens/
|
||||
├── shop/
|
||||
│ ├── home/
|
||||
│ ├── products/
|
||||
│ ├── product-detail/
|
||||
│ └── cart/
|
||||
├── account/
|
||||
│ ├── profile/
|
||||
│ ├── settings/
|
||||
│ └── order-history/
|
||||
└── info/
|
||||
├── about/
|
||||
├── contacts/
|
||||
└── faq/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Содержит только модули
|
||||
- Не содержит бизнес-логику
|
||||
- Локальные одноразовые секции живут внутри screen-модуля, не выносятся в `widgets`/`business`
|
||||
|
||||
## Слой Widgets
|
||||
|
||||
Составной блок интерфейса, который компонует модули ядра, но не принадлежит конкретному бизнес-домену. Widget появляется когда блок используется в нескольких screens или layouts.
|
||||
|
||||
Если блок принадлежит домену — он живёт в `business/{area}/`, даже если переиспользуется. Если блок нужен только в одном месте — это `screens/{name}/parts/` или `layouts/{name}/parts/`, а не widget.
|
||||
|
||||
```text
|
||||
src/widgets/
|
||||
├── page-heading/
|
||||
├── hero-section/
|
||||
├── onboarding-checklist/
|
||||
├── promo-banner/
|
||||
└── error-boundary/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Не принадлежит конкретному бизнес-домену. Если блок доменный — он живёт в `business/`
|
||||
- Используется в нескольких screens или layouts
|
||||
|
||||
## Слой Business
|
||||
|
||||
Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами.
|
||||
|
||||
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `import type`.
|
||||
|
||||
Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки.
|
||||
|
||||
```text
|
||||
src/business/
|
||||
├── auth/
|
||||
├── catalog/
|
||||
├── orders/
|
||||
├── checkout/
|
||||
└── chat/
|
||||
```
|
||||
|
||||
Когда количество доменов затрудняет навигацию — вводится группировка по субдоменам. Группа — папка для организации, не модуль (без `index.ts`).
|
||||
|
||||
```text
|
||||
src/business/
|
||||
├── commerce/
|
||||
│ ├── catalog/
|
||||
│ ├── cart/
|
||||
│ ├── orders/
|
||||
│ └── checkout/
|
||||
└── communication/
|
||||
├── chat/
|
||||
└── notifications/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Один модуль = один бизнес-домен
|
||||
- Циклические зависимости между доменами запрещены
|
||||
- Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты
|
||||
- Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую
|
||||
- Доменные типы (`User`, `Product`) живут здесь, не в `shared/`
|
||||
|
||||
## Слой infra
|
||||
|
||||
Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль.
|
||||
|
||||
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`.
|
||||
|
||||
Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
|
||||
|
||||
```text
|
||||
src/infra/
|
||||
├── theme/
|
||||
├── i18n/
|
||||
├── backend-api/
|
||||
├── maps-api/
|
||||
├── logger/
|
||||
├── feature-flags/
|
||||
└── realtime/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Один модуль = один техсервис
|
||||
- Импортирует `infra/`, `ui/`, `shared/`
|
||||
|
||||
## Слой UI
|
||||
|
||||
UI-кит без бизнес-логики: button, carousel, toast, modal.
|
||||
|
||||
Слой входит в группу «Ядро». Импортирует `ui/` и `shared/`.
|
||||
|
||||
Компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`.
|
||||
|
||||
```text
|
||||
src/ui/
|
||||
├── button/
|
||||
├── input/
|
||||
├── icon/
|
||||
├── carousel/
|
||||
├── modal/
|
||||
├── toast/
|
||||
├── dropdown/
|
||||
├── tabs/
|
||||
└── tooltip/
|
||||
```
|
||||
|
||||
Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (`button`, `icon`, `input`) не импортируют композиции. Композиции (`carousel`, `modal`, `dropdown`) строятся на примитивах.
|
||||
|
||||
```text
|
||||
src/ui/
|
||||
├── primitives/
|
||||
│ ├── button/
|
||||
│ ├── input/
|
||||
│ ├── icon/
|
||||
│ └── badge/
|
||||
└── composites/
|
||||
├── carousel/
|
||||
├── modal/
|
||||
├── dropdown/
|
||||
├── tabs/
|
||||
└── tooltip/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Не содержит бизнес-логику
|
||||
- Импортирует только `ui/` и `shared/`
|
||||
|
||||
## Слой Shared
|
||||
|
||||
Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене.
|
||||
|
||||
Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует.
|
||||
|
||||
Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
|
||||
|
||||
Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь.
|
||||
|
||||
```text
|
||||
src/shared/
|
||||
├── lib/
|
||||
├── types/
|
||||
├── styles/
|
||||
└── sprites/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Не имеет runtime-состояния
|
||||
@@ -1,289 +0,0 @@
|
||||
---
|
||||
title: Модули
|
||||
description: Структура модуля, типы (UI, бизнес, инфра), публичный API, отличие модуля от компонента
|
||||
---
|
||||
|
||||
# Модули
|
||||
|
||||
Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом.
|
||||
|
||||
## Определение
|
||||
|
||||
**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.**
|
||||
|
||||
Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно.
|
||||
|
||||
Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность.
|
||||
|
||||
Главная граница модуля — не папка, а ответственность.
|
||||
|
||||
## Компонент
|
||||
|
||||
**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.**
|
||||
|
||||
Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля.
|
||||
|
||||
> Компонент отображает. Модуль организует.
|
||||
|
||||
Компонент не может:
|
||||
|
||||
- Импортировать код проекта за пределами родительского модуля.
|
||||
- Владеть архитектурными зависимостями.
|
||||
- Содержать любые компоненты.
|
||||
- Содержать любые модули.
|
||||
- Делать внешние запросы.
|
||||
- Самостоятельно получать данные.
|
||||
- Выбирать источник данных.
|
||||
- Композировать данные.
|
||||
- Вызывать сценарные хуки.
|
||||
- Оркестрировать сценарий.
|
||||
- Композировать модули.
|
||||
- Решать, как устроен процесс.
|
||||
- Содержать бизнес-логику.
|
||||
- Содержать сценарную логику.
|
||||
|
||||
Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль.
|
||||
|
||||
```text
|
||||
auth/
|
||||
├── ui/
|
||||
│ └── logout-button/
|
||||
│ ├── logout-button.tsx
|
||||
│ ├── styles/
|
||||
│ │ └── logout-button.module.css
|
||||
│ ├── types/
|
||||
│ │ └── logout-button-props.type.ts
|
||||
│ └── index.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Что считается модулем
|
||||
|
||||
Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу.
|
||||
|
||||
Примеры модулей:
|
||||
|
||||
- `screens/home/` — модуль страницы.
|
||||
- `widgets/page-heading/` — модуль виджета.
|
||||
- `business/auth/` — модуль бизнес-домена.
|
||||
- `infra/theme/` — модуль инфраструктурного сервиса.
|
||||
- `ui/button/` — модуль UI-kit сущности.
|
||||
- `screens/home/parts/hero-section/` — вложенный модуль страницы.
|
||||
|
||||
Не считаются модулями:
|
||||
|
||||
- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты.
|
||||
- `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`.
|
||||
- `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента.
|
||||
|
||||
## Типы модулей
|
||||
|
||||
Тип модуля определяет обязательный корневой файл и стартовую структуру.
|
||||
|
||||
### UI-модуль
|
||||
|
||||
Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне:
|
||||
|
||||
```text
|
||||
header/
|
||||
├── header.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу.
|
||||
|
||||
### Бизнес-модуль
|
||||
|
||||
Бизнес-модуль — модуль, который строится вокруг публичного runtime API.
|
||||
|
||||
Бизнес-модуль обязан иметь фабрику в корне:
|
||||
|
||||
```text
|
||||
auth/
|
||||
├── auth.factory.ts
|
||||
├── index.ts
|
||||
└── types/
|
||||
```
|
||||
|
||||
Фабрика возвращает публичный runtime API модуля.
|
||||
|
||||
### Инфраструктурный модуль
|
||||
|
||||
Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции.
|
||||
|
||||
Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса.
|
||||
|
||||
```text
|
||||
theme/
|
||||
├── index.ts
|
||||
├── config/
|
||||
├── hooks/
|
||||
├── styles/
|
||||
└── ui/
|
||||
```
|
||||
|
||||
```text
|
||||
backend-api/
|
||||
├── backend-api.client.ts
|
||||
├── config/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Структура
|
||||
|
||||
Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности.
|
||||
|
||||
```text
|
||||
{module-name}/
|
||||
├── {module-name}.factory.ts # фабрика (для business-модулей)
|
||||
├── {module-name}.tsx # корневой файл модуля (опционален)
|
||||
├── ui/ # компоненты модуля
|
||||
├── parts/ # вложенные модули
|
||||
├── hooks/ # хуки
|
||||
├── stores/ # сторы состояния
|
||||
├── services/ # внешние источники данных
|
||||
├── mappers/ # трансформация данных между форматами
|
||||
├── types/ # типы
|
||||
├── styles/ # стили
|
||||
├── lib/ # утилиты модуля
|
||||
├── config/ # константы и конфигурация
|
||||
└── index.ts # публичный API
|
||||
```
|
||||
|
||||
Подробное описание сегментов — в разделе [Сегменты](./segments.md).
|
||||
|
||||
## Публичный API
|
||||
|
||||
Внешний код импортирует модуль только через публичный API.
|
||||
|
||||
```ts
|
||||
// Хорошо
|
||||
import { customerFactory } from '@/business/customer'
|
||||
import type { Customer } from '@/business/customer'
|
||||
```
|
||||
|
||||
```ts
|
||||
// Плохо
|
||||
import { validateToken } from '@/business/auth/lib/tokens'
|
||||
```
|
||||
|
||||
`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи.
|
||||
|
||||
Внутренние сегменты модуля остаются деталями реализации.
|
||||
|
||||
Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика.
|
||||
|
||||
```ts
|
||||
// business/customer/index.ts
|
||||
export { customerFactory } from './customer.factory'
|
||||
|
||||
export type { Customer } from './types/customer.type'
|
||||
export type { CustomerApi } from './types/customer-api.type'
|
||||
export type { CustomerDeps } from './types/customer-deps.type'
|
||||
export type { CustomerFactory } from './types/customer-factory.type'
|
||||
```
|
||||
|
||||
## Фабрика
|
||||
|
||||
Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля.
|
||||
|
||||
Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика.
|
||||
|
||||
Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type` — `import type` не является runtime-зависимостью.
|
||||
|
||||
Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция».
|
||||
|
||||
### Структура business-модуля
|
||||
|
||||
```text
|
||||
business/customer/
|
||||
├── customer.factory.ts
|
||||
├── index.ts
|
||||
└── types/
|
||||
├── customer.type.ts
|
||||
├── customer-api.type.ts
|
||||
├── customer-deps.type.ts
|
||||
└── customer-factory.type.ts
|
||||
```
|
||||
|
||||
### Типы
|
||||
|
||||
```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} />
|
||||
}
|
||||
```
|
||||
|
||||
## Жизненный цикл
|
||||
|
||||
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
|
||||
|
||||
- Нужен на одной странице → `screens/{name}/parts/`
|
||||
- Появился в 2+ местах → поднимается по природе:
|
||||
- абстрактный UI → `ui/`
|
||||
- блок с данными/логикой → `widgets/`
|
||||
- представление бизнес-домена → `business/{area}/parts/`
|
||||
|
||||
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||
@@ -1,181 +0,0 @@
|
||||
---
|
||||
title: Сегменты
|
||||
description: Сегменты внутри модуля (ui/, model/, lib/ и др.), назначение и правила размещения файлов
|
||||
---
|
||||
|
||||
# Сегменты
|
||||
|
||||
Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит.
|
||||
|
||||
## Определение
|
||||
|
||||
**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.**
|
||||
|
||||
## Обзор
|
||||
|
||||
| Сегмент | Содержимое |
|
||||
|---------|------------|
|
||||
| `ui/` | Презентационные компоненты родительского модуля |
|
||||
| `parts/` | Вложенные модули со своими сегментами |
|
||||
| `hooks/` | React-хуки |
|
||||
| `stores/` | Сторы состояния |
|
||||
| `services/` | Работа с внешними источниками данных |
|
||||
| `mappers/` | Трансформация данных между форматами |
|
||||
| `types/` | TypeScript-типы и интерфейсы |
|
||||
| `styles/` | Стили |
|
||||
| `lib/` | Утилиты и хелперы модуля |
|
||||
| `config/` | Константы и конфигурация |
|
||||
|
||||
## Сегмент ui/
|
||||
|
||||
Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля.
|
||||
|
||||
Компонент в `ui/`:
|
||||
|
||||
- Находится в собственной папке.
|
||||
- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`.
|
||||
- Не содержит любые компоненты.
|
||||
- Не содержит любые модули.
|
||||
- Не импортирует код проекта за пределами родительского модуля.
|
||||
- Не делает внешние запросы.
|
||||
- Не вызывает сценарные хуки.
|
||||
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
|
||||
- Не содержит бизнес-логику или сценарную логику.
|
||||
|
||||
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент).
|
||||
|
||||
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.
|
||||
|
||||
```text
|
||||
user/
|
||||
├── ui/
|
||||
│ ├── user-avatar/
|
||||
│ │ ├── user-avatar.tsx
|
||||
│ │ ├── styles/
|
||||
│ │ │ └── user-avatar.module.css
|
||||
│ │ ├── types/
|
||||
│ │ │ └── user-avatar-props.type.ts
|
||||
│ │ └── index.ts
|
||||
│ └── user-status/
|
||||
│ ├── user-status.tsx
|
||||
│ └── index.ts
|
||||
├── types/
|
||||
├── hooks/
|
||||
├── user.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `ui/`, а модуль в `parts/`.
|
||||
|
||||
## Сегмент parts/
|
||||
|
||||
Вложенные модули со своими сегментами. `parts/` содержит только модули: каждый элемент `parts/` — папка полноценного модуля с собственным публичным API. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не размещаются.
|
||||
|
||||
```text
|
||||
home/
|
||||
├── parts/
|
||||
│ ├── hero-section/
|
||||
│ │ ├── hero-section.tsx
|
||||
│ │ ├── styles/
|
||||
│ │ ├── parts/
|
||||
│ │ │ └── top-banner/
|
||||
│ │ │ ├── top-banner.tsx
|
||||
│ │ │ └── index.ts
|
||||
│ │ └── index.ts
|
||||
│ └── features-section/
|
||||
│ ├── features-section.tsx
|
||||
│ ├── hooks/
|
||||
│ └── index.ts
|
||||
├── home.screen.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — компонент родительского модуля без собственной архитектурной ответственности.
|
||||
|
||||
Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке.
|
||||
|
||||
Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше.
|
||||
|
||||
## Сегмент hooks/
|
||||
|
||||
React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты.
|
||||
|
||||
```text
|
||||
hooks/
|
||||
├── use-auth.hook.ts
|
||||
├── use-session.hook.ts
|
||||
└── use-permissions.hook.ts
|
||||
```
|
||||
|
||||
## Сегмент stores/
|
||||
|
||||
Сторы состояния модуля. Конкретная реализация зависит от выбранного стейт-менеджера (Zustand, MobX, Redux и т.д.).
|
||||
|
||||
```text
|
||||
stores/
|
||||
├── auth.store.ts
|
||||
└── session.store.ts
|
||||
```
|
||||
|
||||
## Сегмент services/
|
||||
|
||||
Работа с внешними источниками данных: API-вызовы, запросы, подписки.
|
||||
|
||||
```text
|
||||
services/
|
||||
├── auth.service.ts
|
||||
└── token.service.ts
|
||||
```
|
||||
|
||||
## Сегмент mappers/
|
||||
|
||||
Функции трансформации данных из одного формата в другой: DTO в доменный тип, доменный тип в DTO, доменный тип в ViewModel.
|
||||
|
||||
```text
|
||||
mappers/
|
||||
├── map-user.ts
|
||||
├── map-product.ts
|
||||
└── map-order-to-dto.ts
|
||||
```
|
||||
|
||||
## Сегмент types/
|
||||
|
||||
TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов.
|
||||
|
||||
```text
|
||||
types/
|
||||
├── user.type.ts
|
||||
└── session.type.ts
|
||||
```
|
||||
|
||||
## Сегмент styles/
|
||||
|
||||
Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.).
|
||||
|
||||
```text
|
||||
styles/
|
||||
├── auth.module.css
|
||||
└── login-form.module.css
|
||||
```
|
||||
|
||||
## Сегмент lib/
|
||||
|
||||
Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов.
|
||||
|
||||
```text
|
||||
lib/
|
||||
├── validate-email.ts
|
||||
└── format-phone.ts
|
||||
```
|
||||
|
||||
Отличие от `shared/lib/`: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в `shared/lib/`.
|
||||
|
||||
## Сегмент config/
|
||||
|
||||
Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения.
|
||||
|
||||
```text
|
||||
config/
|
||||
├── routes.ts
|
||||
└── constants.ts
|
||||
```
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: NextJS Style Guide
|
||||
description: Стандарты разработки фронтенд-приложений на Next.js и TypeScript.
|
||||
description: Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript.
|
||||
---
|
||||
|
||||
# NextJS Style Guide
|
||||
|
||||
Стандарты разработки фронтенд-приложений на Next.js и TypeScript.
|
||||
Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript.
|
||||
|
||||
## Использование
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
v0.1.5
|
||||
2026-05-11T18:29:26.821Z
|
||||
@@ -1,114 +0,0 @@
|
||||
---
|
||||
title: SLM Design
|
||||
description: "Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили."
|
||||
---
|
||||
|
||||
# SLM Design
|
||||
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
||||
|
||||
## Разделы спецификации
|
||||
|
||||
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
|
||||
|
||||
- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
|
||||
- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
|
||||
- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
|
||||
- [Монорепозитории](./monorepo.md) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business.
|
||||
|
||||
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории.
|
||||
|
||||
## Преимущества
|
||||
|
||||
### Вертикальная организация домена
|
||||
|
||||
Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы.
|
||||
|
||||
### Dependency Injection без фреймворков
|
||||
|
||||
Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий.
|
||||
|
||||
### Разделение ответственности без перегрузки слоёв
|
||||
|
||||
Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода.
|
||||
|
||||
### Горизонтальная инкапсуляция
|
||||
|
||||
Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга.
|
||||
|
||||
### Колокация по умолчанию
|
||||
|
||||
Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями.
|
||||
|
||||
### Явное разделение каркаса и контента
|
||||
|
||||
Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью.
|
||||
|
||||
### Масштабирование через группировку
|
||||
|
||||
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции).
|
||||
|
||||
### Адаптация к монорепозиториям
|
||||
|
||||
SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы.
|
||||
|
||||
## Происхождение
|
||||
|
||||
SLM Design вырос на основе:
|
||||
|
||||
- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей
|
||||
- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое
|
||||
- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию
|
||||
- **Colocation Principle** — код живёт рядом с местом использования
|
||||
|
||||
## Пример структуры проекта
|
||||
|
||||
```text
|
||||
src/
|
||||
├── app/
|
||||
│
|
||||
├── layouts/
|
||||
│ ├── main/
|
||||
│ └── dashboard/
|
||||
│
|
||||
├── screens/
|
||||
│ ├── home/
|
||||
│ ├── products/
|
||||
│ ├── product-detail/
|
||||
│ └── about/
|
||||
│
|
||||
├── widgets/
|
||||
│ ├── page-heading/
|
||||
│ ├── hero-section/
|
||||
│ └── promo-banner/
|
||||
│
|
||||
├── business/
|
||||
│ ├── auth/
|
||||
│ ├── catalog/
|
||||
│ ├── orders/
|
||||
│ └── chat/
|
||||
│
|
||||
├── infra/
|
||||
│ ├── theme/
|
||||
│ ├── i18n/
|
||||
│ ├── backend-api/
|
||||
│ └── logger/
|
||||
│
|
||||
├── ui/
|
||||
│ ├── button/
|
||||
│ ├── input/
|
||||
│ ├── modal/
|
||||
│ ├── toast/
|
||||
│ └── dropdown/
|
||||
│
|
||||
└── shared/
|
||||
├── lib/
|
||||
├── types/
|
||||
└── styles/
|
||||
```
|
||||
|
||||
## Принципы
|
||||
|
||||
- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле.
|
||||
- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости.
|
||||
- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API.
|
||||
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.
|
||||
@@ -1,254 +0,0 @@
|
||||
---
|
||||
title: Слои
|
||||
description: "Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом."
|
||||
---
|
||||
|
||||
# Слои
|
||||
|
||||
Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом.
|
||||
|
||||
## Определение
|
||||
|
||||
**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.**
|
||||
|
||||
## Группы слоёв
|
||||
|
||||
Слои делятся на три группы:
|
||||
|
||||
| Группа | Слои | Описание |
|
||||
|--------|------|----------|
|
||||
| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы |
|
||||
| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит |
|
||||
| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги |
|
||||
|
||||
## Направление зависимостей
|
||||
|
||||
Любой импорт между модулями — только через публичный API.
|
||||
|
||||
```
|
||||
app → [ layouts | screens ] → widgets → business → infra → ui → shared
|
||||
```
|
||||
|
||||
- `layouts` и `screens` — параллельные слои, не импортируют друг друга
|
||||
- Модули одного слоя в группе «Композиция» изолированы друг от друга
|
||||
- Модули одного слоя `infra` и `ui` могут импортировать друг друга через публичный API
|
||||
- Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую
|
||||
- Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях
|
||||
|
||||
|
||||
## Слой App
|
||||
|
||||
Точка входа приложения. Отвечает за запуск, роутинг и композицию маршрутов из layout и screen.
|
||||
|
||||
В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации.
|
||||
|
||||
### Требования
|
||||
|
||||
- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация
|
||||
- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов
|
||||
- Провайдеры и гарды — только подключает готовые из нижних слоёв, не реализует
|
||||
- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы
|
||||
- Никем не импортируется
|
||||
|
||||
## Слой Layouts
|
||||
|
||||
Каркас страницы: общие элементы, одинаковые для группы маршрутов (header, footer, sidebar).
|
||||
|
||||
```text
|
||||
src/layouts/
|
||||
├── main/
|
||||
├── dashboard/
|
||||
└── auth/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Содержит только модули
|
||||
- Не содержит бизнес-логику
|
||||
- Контекстно-зависимые блоки принимает через пропсы от `app`, не импортирует напрямую
|
||||
|
||||
## Слой Screens
|
||||
|
||||
Контент конкретной страницы: собирает её из модулей нижних слоёв.
|
||||
|
||||
```text
|
||||
src/screens/
|
||||
├── home/
|
||||
├── products/
|
||||
├── product-detail/
|
||||
├── about/
|
||||
└── contacts/
|
||||
```
|
||||
|
||||
Когда количество страниц затрудняет навигацию — вводится группировка по разделам. Группа — папка для организации, не модуль (без `index.ts`).
|
||||
|
||||
```text
|
||||
src/screens/
|
||||
├── shop/
|
||||
│ ├── home/
|
||||
│ ├── products/
|
||||
│ ├── product-detail/
|
||||
│ └── cart/
|
||||
├── account/
|
||||
│ ├── profile/
|
||||
│ ├── settings/
|
||||
│ └── order-history/
|
||||
└── info/
|
||||
├── about/
|
||||
├── contacts/
|
||||
└── faq/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Содержит только модули
|
||||
- Не содержит бизнес-логику
|
||||
- Локальные одноразовые секции живут внутри screen-модуля, не выносятся в `widgets`/`business`
|
||||
|
||||
## Слой Widgets
|
||||
|
||||
Составной блок интерфейса, который компонует модули ядра, но не принадлежит конкретному бизнес-домену. Widget появляется когда блок используется в нескольких screens или layouts.
|
||||
|
||||
Если блок принадлежит домену — он живёт в `business/{area}/`, даже если переиспользуется. Если блок нужен только в одном месте — это `screens/{name}/parts/` или `layouts/{name}/parts/`, а не widget.
|
||||
|
||||
```text
|
||||
src/widgets/
|
||||
├── page-heading/
|
||||
├── hero-section/
|
||||
├── onboarding-checklist/
|
||||
├── promo-banner/
|
||||
└── error-boundary/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Не принадлежит конкретному бизнес-домену. Если блок доменный — он живёт в `business/`
|
||||
- Используется в нескольких screens или layouts
|
||||
|
||||
## Слой Business
|
||||
|
||||
Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами.
|
||||
|
||||
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `import type`.
|
||||
|
||||
Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки.
|
||||
|
||||
```text
|
||||
src/business/
|
||||
├── auth/
|
||||
├── catalog/
|
||||
├── orders/
|
||||
├── checkout/
|
||||
└── chat/
|
||||
```
|
||||
|
||||
Когда количество доменов затрудняет навигацию — вводится группировка по субдоменам. Группа — папка для организации, не модуль (без `index.ts`).
|
||||
|
||||
```text
|
||||
src/business/
|
||||
├── commerce/
|
||||
│ ├── catalog/
|
||||
│ ├── cart/
|
||||
│ ├── orders/
|
||||
│ └── checkout/
|
||||
└── communication/
|
||||
├── chat/
|
||||
└── notifications/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Один модуль = один бизнес-домен
|
||||
- Циклические зависимости между доменами запрещены
|
||||
- Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты
|
||||
- Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую
|
||||
- Доменные типы (`User`, `Product`) живут здесь, не в `shared/`
|
||||
|
||||
## Слой infra
|
||||
|
||||
Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль.
|
||||
|
||||
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`.
|
||||
|
||||
Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
|
||||
|
||||
```text
|
||||
src/infra/
|
||||
├── theme/
|
||||
├── i18n/
|
||||
├── backend-api/
|
||||
├── maps-api/
|
||||
├── logger/
|
||||
├── feature-flags/
|
||||
└── realtime/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Один модуль = один техсервис
|
||||
- Импортирует `infra/`, `ui/`, `shared/`
|
||||
|
||||
## Слой UI
|
||||
|
||||
UI-кит без бизнес-логики: button, carousel, toast, modal.
|
||||
|
||||
Слой входит в группу «Ядро». Импортирует `ui/` и `shared/`.
|
||||
|
||||
Компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`.
|
||||
|
||||
```text
|
||||
src/ui/
|
||||
├── button/
|
||||
├── input/
|
||||
├── icon/
|
||||
├── carousel/
|
||||
├── modal/
|
||||
├── toast/
|
||||
├── dropdown/
|
||||
├── tabs/
|
||||
└── tooltip/
|
||||
```
|
||||
|
||||
Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (`button`, `icon`, `input`) не импортируют композиции. Композиции (`carousel`, `modal`, `dropdown`) строятся на примитивах.
|
||||
|
||||
```text
|
||||
src/ui/
|
||||
├── primitives/
|
||||
│ ├── button/
|
||||
│ ├── input/
|
||||
│ ├── icon/
|
||||
│ └── badge/
|
||||
└── composites/
|
||||
├── carousel/
|
||||
├── modal/
|
||||
├── dropdown/
|
||||
├── tabs/
|
||||
└── tooltip/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Не содержит бизнес-логику
|
||||
- Импортирует только `ui/` и `shared/`
|
||||
|
||||
## Слой Shared
|
||||
|
||||
Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене.
|
||||
|
||||
Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует.
|
||||
|
||||
Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
|
||||
|
||||
Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь.
|
||||
|
||||
```text
|
||||
src/shared/
|
||||
├── lib/
|
||||
├── types/
|
||||
├── styles/
|
||||
└── sprites/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Не имеет runtime-состояния
|
||||
@@ -1,213 +0,0 @@
|
||||
---
|
||||
title: Модули
|
||||
description: "Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом."
|
||||
---
|
||||
|
||||
# Модули
|
||||
|
||||
Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом.
|
||||
|
||||
## Определение
|
||||
|
||||
**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.**
|
||||
|
||||
Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно.
|
||||
|
||||
Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность.
|
||||
|
||||
Главная граница модуля — не папка, а ответственность.
|
||||
|
||||
## Компонент
|
||||
|
||||
**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.**
|
||||
|
||||
Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля.
|
||||
|
||||
> Компонент отображает. Модуль организует.
|
||||
|
||||
Компонент не может:
|
||||
|
||||
- Импортировать код проекта за пределами родительского модуля.
|
||||
- Владеть архитектурными зависимостями.
|
||||
- Содержать любые компоненты.
|
||||
- Содержать любые модули.
|
||||
- Делать внешние запросы.
|
||||
- Самостоятельно получать данные.
|
||||
- Выбирать источник данных.
|
||||
- Композировать данные.
|
||||
- Вызывать сценарные хуки.
|
||||
- Оркестрировать сценарий.
|
||||
- Композировать модули.
|
||||
- Решать, как устроен процесс.
|
||||
- Содержать бизнес-логику.
|
||||
- Содержать сценарную логику.
|
||||
|
||||
Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль.
|
||||
|
||||
```text
|
||||
auth/
|
||||
├── ui/
|
||||
│ └── logout-button/
|
||||
│ ├── logout-button.tsx
|
||||
│ ├── styles/
|
||||
│ │ └── logout-button.module.css
|
||||
│ ├── types/
|
||||
│ │ └── logout-button-props.type.ts
|
||||
│ └── index.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Что считается модулем
|
||||
|
||||
Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу.
|
||||
|
||||
Примеры модулей:
|
||||
|
||||
- `screens/home/` — модуль страницы.
|
||||
- `widgets/page-heading/` — модуль виджета.
|
||||
- `business/auth/` — модуль бизнес-домена.
|
||||
- `infra/theme/` — модуль инфраструктурного сервиса.
|
||||
- `ui/button/` — модуль UI-kit сущности.
|
||||
- `screens/home/parts/hero-section/` — вложенный модуль страницы.
|
||||
|
||||
Не считаются модулями:
|
||||
|
||||
- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты.
|
||||
- `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`.
|
||||
- `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента.
|
||||
|
||||
## Типы модулей
|
||||
|
||||
Тип модуля определяет обязательный корневой файл и стартовую структуру.
|
||||
|
||||
### UI-модуль
|
||||
|
||||
Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне:
|
||||
|
||||
```text
|
||||
header/
|
||||
├── header.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу.
|
||||
|
||||
### Бизнес-модуль
|
||||
|
||||
Бизнес-модуль — модуль, который строится вокруг публичного runtime API.
|
||||
|
||||
Бизнес-модуль обязан иметь фабрику в корне:
|
||||
|
||||
```text
|
||||
auth/
|
||||
├── auth.factory.ts
|
||||
├── index.ts
|
||||
└── types/
|
||||
```
|
||||
|
||||
Фабрика возвращает публичный runtime API модуля.
|
||||
|
||||
### Инфраструктурный модуль
|
||||
|
||||
Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции.
|
||||
|
||||
Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса.
|
||||
|
||||
```text
|
||||
theme/
|
||||
├── index.ts
|
||||
├── config/
|
||||
├── hooks/
|
||||
├── styles/
|
||||
└── ui/
|
||||
```
|
||||
|
||||
```text
|
||||
backend-api/
|
||||
├── backend-api.client.ts
|
||||
├── config/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Структура
|
||||
|
||||
Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности.
|
||||
|
||||
```text
|
||||
{module-name}/
|
||||
├── {module-name}.factory.ts # фабрика (для business-модулей)
|
||||
├── {module-name}.tsx # корневой файл модуля (опционален)
|
||||
├── ui/ # компоненты модуля
|
||||
├── parts/ # вложенные модули
|
||||
├── hooks/ # хуки
|
||||
├── stores/ # сторы состояния
|
||||
├── services/ # внешние источники данных
|
||||
├── mappers/ # трансформация данных между форматами
|
||||
├── types/ # типы
|
||||
├── styles/ # стили
|
||||
├── lib/ # утилиты модуля
|
||||
├── config/ # константы и конфигурация
|
||||
└── index.ts # публичный API
|
||||
```
|
||||
|
||||
Подробное описание сегментов — в разделе [Сегменты](./segments.md).
|
||||
|
||||
## Публичный API
|
||||
|
||||
Внешний код импортирует модуль только через публичный API.
|
||||
|
||||
```ts
|
||||
// Хорошо
|
||||
import { customerFactory } from '@/business/customer'
|
||||
import type { Customer } from '@/business/customer'
|
||||
```
|
||||
|
||||
```ts
|
||||
// Плохо
|
||||
import { validateToken } from '@/business/auth/lib/tokens'
|
||||
```
|
||||
|
||||
`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи.
|
||||
|
||||
Внутренние сегменты модуля остаются деталями реализации.
|
||||
|
||||
Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика.
|
||||
|
||||
```ts
|
||||
// business/customer/index.ts
|
||||
export { customerFactory } from './customer.factory'
|
||||
|
||||
export type { Customer } from './types/customer.type'
|
||||
export type { CustomerApi } from './types/customer-api.type'
|
||||
export type { CustomerDeps } from './types/customer-deps.type'
|
||||
export type { CustomerFactory } from './types/customer-factory.type'
|
||||
```
|
||||
|
||||
## Фабрика
|
||||
|
||||
Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля.
|
||||
|
||||
Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика.
|
||||
|
||||
Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type` — `import type` не является runtime-зависимостью.
|
||||
|
||||
Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция».
|
||||
|
||||
### Примеры
|
||||
|
||||
Пример реализации фабрики в React см. в [Создание фабрики](../examples/react/factory.md).
|
||||
|
||||
Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](../examples/react/factory-composition.md).
|
||||
|
||||
## Жизненный цикл
|
||||
|
||||
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
|
||||
|
||||
- Нужен на одной странице → `screens/{name}/parts/`
|
||||
- Появился в 2+ местах → поднимается по природе:
|
||||
- абстрактный UI → `ui/`
|
||||
- блок с данными/логикой → `widgets/`
|
||||
- представление бизнес-домена → `business/{area}/parts/`
|
||||
|
||||
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||
@@ -1,235 +0,0 @@
|
||||
---
|
||||
title: Монорепозитории
|
||||
description: "Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов."
|
||||
---
|
||||
|
||||
# Монорепозитории
|
||||
|
||||
Раздел описывает, как применять 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,181 +0,0 @@
|
||||
---
|
||||
title: Сегменты
|
||||
description: "Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит."
|
||||
---
|
||||
|
||||
# Сегменты
|
||||
|
||||
Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит.
|
||||
|
||||
## Определение
|
||||
|
||||
**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.**
|
||||
|
||||
## Обзор
|
||||
|
||||
| Сегмент | Содержимое |
|
||||
|---------|------------|
|
||||
| `ui/` | Презентационные компоненты родительского модуля |
|
||||
| `parts/` | Вложенные модули со своими сегментами |
|
||||
| `hooks/` | React-хуки |
|
||||
| `stores/` | Сторы состояния |
|
||||
| `services/` | Работа с внешними источниками данных |
|
||||
| `mappers/` | Трансформация данных между форматами |
|
||||
| `types/` | TypeScript-типы и интерфейсы |
|
||||
| `styles/` | Стили |
|
||||
| `lib/` | Утилиты и хелперы модуля |
|
||||
| `config/` | Константы и конфигурация |
|
||||
|
||||
## Сегмент ui/
|
||||
|
||||
Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля.
|
||||
|
||||
Компонент в `ui/`:
|
||||
|
||||
- Находится в собственной папке.
|
||||
- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`.
|
||||
- Не содержит любые компоненты.
|
||||
- Не содержит любые модули.
|
||||
- Не импортирует код проекта за пределами родительского модуля.
|
||||
- Не делает внешние запросы.
|
||||
- Не вызывает сценарные хуки.
|
||||
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
|
||||
- Не содержит бизнес-логику или сценарную логику.
|
||||
|
||||
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент).
|
||||
|
||||
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.
|
||||
|
||||
```text
|
||||
user/
|
||||
├── ui/
|
||||
│ ├── user-avatar/
|
||||
│ │ ├── user-avatar.tsx
|
||||
│ │ ├── styles/
|
||||
│ │ │ └── user-avatar.module.css
|
||||
│ │ ├── types/
|
||||
│ │ │ └── user-avatar-props.type.ts
|
||||
│ │ └── index.ts
|
||||
│ └── user-status/
|
||||
│ ├── user-status.tsx
|
||||
│ └── index.ts
|
||||
├── types/
|
||||
├── hooks/
|
||||
├── user.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `ui/`, а модуль в `parts/`.
|
||||
|
||||
## Сегмент parts/
|
||||
|
||||
Вложенные модули со своими сегментами. `parts/` содержит только модули: каждый элемент `parts/` — папка полноценного модуля с собственным публичным API. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не размещаются.
|
||||
|
||||
```text
|
||||
home/
|
||||
├── parts/
|
||||
│ ├── hero-section/
|
||||
│ │ ├── hero-section.tsx
|
||||
│ │ ├── styles/
|
||||
│ │ ├── parts/
|
||||
│ │ │ └── top-banner/
|
||||
│ │ │ ├── top-banner.tsx
|
||||
│ │ │ └── index.ts
|
||||
│ │ └── index.ts
|
||||
│ └── features-section/
|
||||
│ ├── features-section.tsx
|
||||
│ ├── hooks/
|
||||
│ └── index.ts
|
||||
├── home.screen.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — компонент родительского модуля без собственной архитектурной ответственности.
|
||||
|
||||
Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке.
|
||||
|
||||
Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше.
|
||||
|
||||
## Сегмент hooks/
|
||||
|
||||
React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты.
|
||||
|
||||
```text
|
||||
hooks/
|
||||
├── use-auth.hook.ts
|
||||
├── use-session.hook.ts
|
||||
└── use-permissions.hook.ts
|
||||
```
|
||||
|
||||
## Сегмент stores/
|
||||
|
||||
Сторы состояния модуля. Конкретная реализация зависит от выбранного стейт-менеджера (Zustand, MobX, Redux и т.д.).
|
||||
|
||||
```text
|
||||
stores/
|
||||
├── auth.store.ts
|
||||
└── session.store.ts
|
||||
```
|
||||
|
||||
## Сегмент services/
|
||||
|
||||
Работа с внешними источниками данных: API-вызовы, запросы, подписки.
|
||||
|
||||
```text
|
||||
services/
|
||||
├── auth.service.ts
|
||||
└── token.service.ts
|
||||
```
|
||||
|
||||
## Сегмент mappers/
|
||||
|
||||
Функции трансформации данных из одного формата в другой: DTO в доменный тип, доменный тип в DTO, доменный тип в ViewModel.
|
||||
|
||||
```text
|
||||
mappers/
|
||||
├── map-user.ts
|
||||
├── map-product.ts
|
||||
└── map-order-to-dto.ts
|
||||
```
|
||||
|
||||
## Сегмент types/
|
||||
|
||||
TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов.
|
||||
|
||||
```text
|
||||
types/
|
||||
├── user.type.ts
|
||||
└── session.type.ts
|
||||
```
|
||||
|
||||
## Сегмент styles/
|
||||
|
||||
Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.).
|
||||
|
||||
```text
|
||||
styles/
|
||||
├── auth.module.css
|
||||
└── login-form.module.css
|
||||
```
|
||||
|
||||
## Сегмент lib/
|
||||
|
||||
Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов.
|
||||
|
||||
```text
|
||||
lib/
|
||||
├── validate-email.ts
|
||||
└── format-phone.ts
|
||||
```
|
||||
|
||||
Отличие от `shared/lib/`: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в `shared/lib/`.
|
||||
|
||||
## Сегмент config/
|
||||
|
||||
Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения.
|
||||
|
||||
```text
|
||||
config/
|
||||
├── routes.ts
|
||||
└── constants.ts
|
||||
```
|
||||
@@ -1,249 +0,0 @@
|
||||
---
|
||||
title: Композиция через Provider
|
||||
description: "Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя."
|
||||
---
|
||||
|
||||
# Композиция через 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: "Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов."
|
||||
---
|
||||
|
||||
# Композиция фабрик
|
||||
|
||||
Раздел показывает, как собрать 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-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API."
|
||||
---
|
||||
|
||||
# Создание фабрики
|
||||
|
||||
Раздел показывает, как оформить фабрику 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,
|
||||
}
|
||||
}
|
||||
```
|
||||
27
docs/nextjs-style-guide/.vitepress/config.ts
Normal file
27
docs/nextjs-style-guide/.vitepress/config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import taskLists from 'markdown-it-task-lists';
|
||||
import llmstxt from 'vitepress-plugin-llms';
|
||||
import { themeSyncHead } from '../../shared/vitepress/themeHead';
|
||||
import { sidebar, site } from '../docs.config';
|
||||
|
||||
export default defineConfig({
|
||||
title: site.title,
|
||||
description: site.description,
|
||||
base: site.base,
|
||||
outDir: site.outDir,
|
||||
srcDir: 'content',
|
||||
cleanUrls: true,
|
||||
head: [...themeSyncHead],
|
||||
vite: {
|
||||
plugins: [llmstxt()],
|
||||
},
|
||||
markdown: {
|
||||
config(md) {
|
||||
md.use(taskLists);
|
||||
},
|
||||
},
|
||||
themeConfig: {
|
||||
sidebar,
|
||||
socialLinks: [],
|
||||
},
|
||||
});
|
||||
1
docs/nextjs-style-guide/.vitepress/theme/index.ts
Normal file
1
docs/nextjs-style-guide/.vitepress/theme/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '../../../shared/vitepress/theme';
|
||||
201
docs/nextjs-style-guide/docs.config.ts
Normal file
201
docs/nextjs-style-guide/docs.config.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
export const site = {
|
||||
title: 'NextJS Style Guide',
|
||||
description: 'Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript',
|
||||
base: '/nextjs-style-guide/',
|
||||
outDir: '../../public/nextjs-style-guide',
|
||||
};
|
||||
|
||||
/**
|
||||
* Карта монтирования исходных канонов в VitePress-документацию.
|
||||
*
|
||||
* SLM-разделы берутся только из корневого `canons/slm-design/`, чтобы
|
||||
* NextJS-гайд не содержал собственных дублей архитектурного канона.
|
||||
*/
|
||||
export const mounts = [
|
||||
{ target: 'index.md', source: 'style-guide/index.md' },
|
||||
{ target: 'workflow.md', source: 'style-guide/workflow.md' },
|
||||
|
||||
{ target: 'slm-design/architecture/index.md', source: 'slm-design/architecture/index.md' },
|
||||
{ target: 'slm-design/architecture/layers.md', source: 'slm-design/architecture/layers.md' },
|
||||
{ target: 'slm-design/architecture/modules.md', source: 'slm-design/architecture/modules.md' },
|
||||
{ target: 'slm-design/architecture/segments.md', source: 'slm-design/architecture/segments.md' },
|
||||
{ target: 'slm-design/architecture/monorepo.md', source: 'slm-design/architecture/monorepo.md' },
|
||||
{ target: 'slm-design/examples/react/factory.md', source: 'slm-design/examples/react/factory.md' },
|
||||
{ target: 'slm-design/examples/react/factory-composition.md', source: 'slm-design/examples/react/factory-composition.md' },
|
||||
{ target: 'slm-design/examples/react/composition-provider.md', source: 'slm-design/examples/react/composition-provider.md' },
|
||||
|
||||
{ target: 'basics/tech-stack.md', source: 'style-guide/basics/tech-stack.md' },
|
||||
{ target: 'basics/naming.md', source: 'style-guide/basics/naming.md' },
|
||||
{ target: 'basics/code-style.md', source: 'style-guide/basics/code-style.md' },
|
||||
{ target: 'basics/documentation.md', source: 'style-guide/basics/documentation.md' },
|
||||
{ target: 'basics/typing.md', source: 'style-guide/basics/typing.md' },
|
||||
|
||||
{ target: 'applied/creating-project/from-template.md', source: 'style-guide/applied/creating-project/from-template.md' },
|
||||
{ target: 'applied/creating-project/manual.md', source: 'style-guide/applied/creating-project/manual.md' },
|
||||
{ target: 'applied/creating-project/nextjs.md', source: 'style-guide/applied/creating-project/nextjs.md' },
|
||||
{ target: 'applied/project-structure.md', source: 'style-guide/applied/project-structure.md' },
|
||||
{ target: 'applied/page-level.md', source: 'style-guide/applied/page-level.md' },
|
||||
{ target: 'applied/component.md', source: 'style-guide/applied/component.md' },
|
||||
{ target: 'applied/module.md', source: 'style-guide/applied/module.md' },
|
||||
{ target: 'applied/rest-client/index.md', source: 'style-guide/applied/rest-client/index.md' },
|
||||
{ target: 'applied/rest-client/setup/index.md', source: 'style-guide/applied/rest-client/setup/index.md' },
|
||||
{ target: 'applied/rest-client/setup/auto.md', source: 'style-guide/applied/rest-client/setup/auto.md' },
|
||||
{ target: 'applied/rest-client/setup/manual.md', source: 'style-guide/applied/rest-client/setup/manual.md' },
|
||||
{ target: 'applied/rest-client/setup/hooks.md', source: 'style-guide/applied/rest-client/setup/hooks.md' },
|
||||
{ target: 'applied/rest-client/usage.md', source: 'style-guide/applied/rest-client/usage.md' },
|
||||
{ target: 'applied/data-fetch/index.md', source: 'style-guide/applied/data-fetch/index.md' },
|
||||
{ target: 'applied/data-fetch/server-await.md', source: 'style-guide/applied/data-fetch/server-await.md' },
|
||||
{ target: 'applied/data-fetch/parallel-server-requests.md', source: 'style-guide/applied/data-fetch/parallel-server-requests.md' },
|
||||
{ target: 'applied/data-fetch/pass-promise-down.md', source: 'style-guide/applied/data-fetch/pass-promise-down.md' },
|
||||
{ target: 'applied/data-fetch/client-hooks-initial-data.md', source: 'style-guide/applied/data-fetch/client-hooks-initial-data.md' },
|
||||
{ target: 'applied/data-fetch/client-get-hook.md', source: 'style-guide/applied/data-fetch/client-get-hook.md' },
|
||||
{ target: 'applied/data-fetch/business-composition.md', source: 'style-guide/applied/data-fetch/business-composition.md' },
|
||||
{ target: 'applied/styles/styles-setup.md', source: 'style-guide/applied/styles/styles-setup.md' },
|
||||
{ target: 'applied/styles/styles-usage.md', source: 'style-guide/applied/styles/styles-usage.md' },
|
||||
{ target: 'applied/svg-sprites/svg-sprites-intro.md', source: 'style-guide/applied/svg-sprites/svg-sprites-intro.md' },
|
||||
{ target: 'applied/svg-sprites/svg-sprites-setup.md', source: 'style-guide/applied/svg-sprites/svg-sprites-setup.md' },
|
||||
{ target: 'applied/svg-sprites/svg-sprites-usage.md', source: 'style-guide/applied/svg-sprites/svg-sprites-usage.md' },
|
||||
{ target: 'applied/images.md', source: 'style-guide/applied/images.md' },
|
||||
{ target: 'applied/fonts.md', source: 'style-guide/applied/fonts.md' },
|
||||
{ target: 'applied/aliases.md', source: 'style-guide/applied/aliases.md' },
|
||||
{ target: 'applied/templates/templates-intro.md', source: 'style-guide/applied/templates/templates-intro.md' },
|
||||
{ target: 'applied/templates/templates-setup.md', source: 'style-guide/applied/templates/templates-setup.md' },
|
||||
{ target: 'applied/templates/templates-create.md', source: 'style-guide/applied/templates/templates-create.md' },
|
||||
{ target: 'applied/templates/templates-usage.md', source: 'style-guide/applied/templates/templates-usage.md' },
|
||||
{ target: 'applied/biome.md', source: 'style-guide/applied/biome.md' },
|
||||
{ target: 'applied/postcss.md', source: 'style-guide/applied/postcss.md' },
|
||||
{ target: 'applied/vscode.md', source: 'style-guide/applied/vscode.md' },
|
||||
{ target: 'applied/localization.md', source: 'style-guide/applied/localization.md' },
|
||||
{ target: 'applied/stores.md', source: 'style-guide/applied/stores.md' },
|
||||
];
|
||||
|
||||
export const routeRewrites = [
|
||||
{ from: '/docs/basics/architecture', to: '/slm-design/architecture' },
|
||||
{ from: '/architecture', to: '/slm-design/architecture' },
|
||||
{ from: '/examples', to: '/slm-design/examples' },
|
||||
];
|
||||
|
||||
export const sidebar = [
|
||||
{
|
||||
text: 'Подсказки',
|
||||
link: '/workflow',
|
||||
},
|
||||
{
|
||||
text: 'Архитектура',
|
||||
items: [
|
||||
{
|
||||
text: 'Спецификация SLM',
|
||||
items: [
|
||||
{ text: 'Обзор', link: '/slm-design/architecture/' },
|
||||
{ text: 'Слои', link: '/slm-design/architecture/layers' },
|
||||
{ text: 'Модули', link: '/slm-design/architecture/modules' },
|
||||
{ text: 'Сегменты', link: '/slm-design/architecture/segments' },
|
||||
{ text: 'Монорепозитории', link: '/slm-design/architecture/monorepo' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Примеры',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Создание фабрики', link: '/slm-design/examples/react/factory' },
|
||||
{ text: 'Композиция фабрик', link: '/slm-design/examples/react/factory-composition' },
|
||||
{ text: 'Композиция через Provider', link: '/slm-design/examples/react/composition-provider' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Базовые правила',
|
||||
items: [
|
||||
{ text: 'Технологии и библиотеки', link: '/basics/tech-stack' },
|
||||
{ text: 'Именование', link: '/basics/naming' },
|
||||
{ text: 'Стиль кода', link: '/basics/code-style' },
|
||||
{ text: 'Документирование', link: '/basics/documentation' },
|
||||
{ text: 'Типизация', link: '/basics/typing' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Прикладные разделы',
|
||||
items: [
|
||||
{
|
||||
text: 'Создание проекта',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Из шаблона', link: '/applied/creating-project/from-template' },
|
||||
{ text: 'По гайду вручную', link: '/applied/creating-project/manual' },
|
||||
{ text: 'Чистый Next.js', link: '/applied/creating-project/nextjs' },
|
||||
],
|
||||
},
|
||||
{ text: 'Структура проекта', link: '/applied/project-structure' },
|
||||
{ text: 'Страницы', link: '/applied/page-level' },
|
||||
{ text: 'Компонент', link: '/applied/component' },
|
||||
{ text: 'Модуль', link: '/applied/module' },
|
||||
{
|
||||
text: 'REST-клиент',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Введение', link: '/applied/rest-client/' },
|
||||
{
|
||||
text: 'Настройка',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Обзор', link: '/applied/rest-client/setup/' },
|
||||
{ text: 'Автогенерация из OpenAPI', link: '/applied/rest-client/setup/auto' },
|
||||
{ text: 'Ручное создание', link: '/applied/rest-client/setup/manual' },
|
||||
{ text: 'GET-хуки REST-клиента', link: '/applied/rest-client/setup/hooks' },
|
||||
],
|
||||
},
|
||||
{ text: 'Использование', link: '/applied/rest-client/usage' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Получение данных',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Обзор', link: '/applied/data-fetch/' },
|
||||
{ text: 'Серверный await', link: '/applied/data-fetch/server-await' },
|
||||
{ text: 'Параллельные серверные запросы', link: '/applied/data-fetch/parallel-server-requests' },
|
||||
{ text: 'Передача промиса ниже', link: '/applied/data-fetch/pass-promise-down' },
|
||||
{ text: 'Начальные данные для клиентских хуков', link: '/applied/data-fetch/client-hooks-initial-data' },
|
||||
{ text: 'Клиентский GET-хук', link: '/applied/data-fetch/client-get-hook' },
|
||||
{ text: 'Business-композиция', link: '/applied/data-fetch/business-composition' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Стили',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Настройка', link: '/applied/styles/styles-setup' },
|
||||
{ text: 'Использование', link: '/applied/styles/styles-usage' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'SVG-спрайты',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Введение', link: '/applied/svg-sprites/svg-sprites-intro' },
|
||||
{ text: 'Настройка', link: '/applied/svg-sprites/svg-sprites-setup' },
|
||||
{ text: 'Использование', link: '/applied/svg-sprites/svg-sprites-usage' },
|
||||
],
|
||||
},
|
||||
{ text: 'Изображения', link: '/applied/images' },
|
||||
{ text: 'Шрифты', link: '/applied/fonts' },
|
||||
{ text: 'Алиасы импортов', link: '/applied/aliases' },
|
||||
{
|
||||
text: 'Шаблоны генерации',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Введение', link: '/applied/templates/templates-intro' },
|
||||
{ text: 'Настройка', link: '/applied/templates/templates-setup' },
|
||||
{ text: 'Создание шаблонов', link: '/applied/templates/templates-create' },
|
||||
{ text: 'Использование', link: '/applied/templates/templates-usage' },
|
||||
],
|
||||
},
|
||||
{ text: 'Biome', link: '/applied/biome' },
|
||||
{ text: 'PostCSS', link: '/applied/postcss' },
|
||||
{ text: 'VS Code', link: '/applied/vscode' },
|
||||
{ text: 'Локализация', link: '/applied/localization' },
|
||||
{ text: 'Stores', link: '/applied/stores' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -6,7 +6,12 @@ import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
globalIgnores([
|
||||
'dist',
|
||||
'docs/*/content',
|
||||
'docs/*/.vitepress/cache',
|
||||
'public',
|
||||
]),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"build": "npm run site:generate && tsc -b && vite build",
|
||||
"docs:prepare:slm-design": "tsx scripts/docs/prepare.ts slm-design",
|
||||
"docs:build:slm-design": "npm run docs:prepare:slm-design && vitepress build docs/slm-design",
|
||||
"docs:prepare:nextjs-style-guide": "tsx scripts/docs/prepare.ts nextjs-style-guide",
|
||||
"docs:build:nextjs-style-guide": "npm run docs:prepare:nextjs-style-guide && vitepress build docs/nextjs-style-guide",
|
||||
"docs:prepare:figma-adaptive-standards": "tsx scripts/docs/prepare.ts figma-adaptive-standards",
|
||||
"docs:build:figma-adaptive-standards": "npm run docs:prepare:figma-adaptive-standards && vitepress build docs/figma-adaptive-standards",
|
||||
"site:generate": "tsx scripts/site/generate-artifacts.ts",
|
||||
|
||||
@@ -7,11 +7,17 @@ type Page = {
|
||||
target: string;
|
||||
};
|
||||
|
||||
type DocsConfig = {
|
||||
mounts: Page[];
|
||||
type RouteRewrite = {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
const MD_LINK_RE = /\]\((?!#|[a-z][a-z0-9+.-]*:)([^)\s]+\.md)(#[^)]*)?\)/gi;
|
||||
type DocsConfig = {
|
||||
mounts: Page[];
|
||||
routeRewrites?: RouteRewrite[];
|
||||
};
|
||||
|
||||
const MD_LINK_RE = /\]\((?!#|[a-z][a-z0-9+.-]*:|\/\/)([^)\s]+)(#[^)]*)?\)/gi;
|
||||
|
||||
const siteName = process.argv[2];
|
||||
|
||||
@@ -33,6 +39,7 @@ const config = (await import(pathToFileURL(configPath).href)) as DocsConfig;
|
||||
const targetBySource = new Map(
|
||||
config.mounts.map((page) => [normalizePath(page.source), normalizePath(page.target)]),
|
||||
);
|
||||
const routeRewrites = [...(config.routeRewrites ?? [])].sort((a, b) => b.from.length - a.from.length);
|
||||
|
||||
function normalizePath(value: string) {
|
||||
return value.split(path.sep).join('/').replace(/^\.\//, '');
|
||||
@@ -47,17 +54,71 @@ function formatRelativeMarkdownPath(fromTarget: string, toTarget: string) {
|
||||
return relative.startsWith('.') ? relative : `./${relative}`;
|
||||
}
|
||||
|
||||
function applyRouteRewrites(route: string) {
|
||||
for (const rewrite of routeRewrites) {
|
||||
if (route === rewrite.from || route.startsWith(`${rewrite.from}/`) || route.startsWith(`${rewrite.from}#`)) {
|
||||
return `${rewrite.to}${route.slice(rewrite.from.length)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatDocsRoute(route: string) {
|
||||
const rewritten = applyRouteRewrites(route);
|
||||
if (rewritten) return rewritten;
|
||||
|
||||
if (route === '/docs') return '/';
|
||||
if (route.startsWith('/docs/')) return route.slice('/docs'.length);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatRelativeRoute(hrefPath: string, sourceDir: string) {
|
||||
const sourcePath = normalizePath(path.posix.normalize(path.posix.join(sourceDir, hrefPath)));
|
||||
|
||||
if (sourcePath.startsWith('style-guide/')) {
|
||||
const route = `/docs/${sourcePath.slice('style-guide/'.length)}`;
|
||||
|
||||
return applyRouteRewrites(route);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function transformMarkdownLinks(content: string, page: Page) {
|
||||
const sourceDir = path.posix.dirname(normalizePath(page.source));
|
||||
|
||||
return content.replace(MD_LINK_RE, (match, href: string, hash = '') => {
|
||||
const [hrefPath, query = ''] = href.split('?');
|
||||
const queryPart = query ? `?${query}` : '';
|
||||
|
||||
if (hrefPath.startsWith('/')) {
|
||||
const route = formatDocsRoute(hrefPath);
|
||||
|
||||
if (route) return `](${route}${queryPart}${hash})`;
|
||||
|
||||
const rewritten = applyRouteRewrites(hrefPath);
|
||||
|
||||
if (rewritten) return `](${rewritten}${queryPart}${hash})`;
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
if (!hrefPath.endsWith('.md')) {
|
||||
const route = formatRelativeRoute(hrefPath, sourceDir);
|
||||
|
||||
if (route) return `](${route}${queryPart}${hash})`;
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
const sourcePath = normalizePath(path.posix.normalize(path.posix.join(sourceDir, hrefPath)));
|
||||
const target = targetBySource.get(sourcePath);
|
||||
|
||||
if (!target) return match;
|
||||
|
||||
const nextHref = formatRelativeMarkdownPath(page.target, `${target}${query ? `?${query}` : ''}`);
|
||||
const nextHref = formatRelativeMarkdownPath(page.target, `${target}${queryPart}`);
|
||||
|
||||
return `](${nextHref}${hash})`;
|
||||
});
|
||||
|
||||
11
src/App.tsx
11
src/App.tsx
@@ -98,11 +98,11 @@ const applyTheme = (theme: ThemeMode) => {
|
||||
|
||||
function useTheme() {
|
||||
const [theme, setTheme] = useState<ThemeMode>(getStoredTheme)
|
||||
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => resolveTheme(getStoredTheme()))
|
||||
const [systemTheme, setSystemTheme] = useState<ResolvedTheme>(getSystemTheme)
|
||||
const resolvedTheme = theme === 'auto' ? systemTheme : theme
|
||||
|
||||
useLayoutEffect(() => {
|
||||
applyTheme(theme)
|
||||
setResolvedTheme(resolveTheme(theme))
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -110,13 +110,14 @@ function useTheme() {
|
||||
|
||||
const mediaQuery = window.matchMedia(DARK_THEME_QUERY)
|
||||
const handleSystemThemeChange = () => {
|
||||
if (theme !== 'auto') return
|
||||
const nextResolvedTheme = getSystemTheme()
|
||||
|
||||
const nextResolvedTheme = resolveTheme(theme)
|
||||
setSystemTheme(nextResolvedTheme)
|
||||
|
||||
if (theme !== 'auto') return
|
||||
|
||||
document.documentElement.style.colorScheme = nextResolvedTheme
|
||||
document.documentElement.classList.toggle('dark', nextResolvedTheme === 'dark')
|
||||
setResolvedTheme(nextResolvedTheme)
|
||||
}
|
||||
|
||||
mediaQuery.addEventListener('change', handleSystemThemeChange)
|
||||
|
||||
@@ -30,10 +30,11 @@ export const docs: DocCard[] = [
|
||||
},
|
||||
{
|
||||
title: 'NextJS Style Guide',
|
||||
label: 'Стиль проекта',
|
||||
label: 'Стайлгайд',
|
||||
mark: 'NX',
|
||||
description: 'Правила организации Next.js-приложений, роутинга, серверных границ и проектных соглашений.',
|
||||
status: 'Скоро',
|
||||
description: 'Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript.',
|
||||
href: '/nextjs-style-guide/',
|
||||
status: 'Доступно',
|
||||
accent: 'blue',
|
||||
links: [
|
||||
{ label: 'llms.txt', href: '/nextjs-style-guide/llms.txt' },
|
||||
@@ -42,7 +43,7 @@ export const docs: DocCard[] = [
|
||||
},
|
||||
{
|
||||
title: 'React Style Guide',
|
||||
label: 'Стиль кода',
|
||||
label: 'Стайлгайд',
|
||||
mark: 'RE',
|
||||
description: 'Практики написания React-компонентов, хуков, состояния и клиентского UI-кода.',
|
||||
status: 'Скоро',
|
||||
|
||||
Reference in New Issue
Block a user