docs: обновить RULES.md после переработки раздела архитектуры
All checks were successful
CI/CD Pipeline / docker (push) Successful in 40s
CI/CD Pipeline / deploy (push) Successful in 5s

This commit is contained in:
2026-04-04 08:10:41 +03:00
parent be3d86f198
commit 436c87a986

View File

@@ -323,70 +323,471 @@ const users = {} as Record<string, User>;
<!-- /basics/architecture -->
## Архитектура
Этот раздел описывает архитектуру проекта: из каких слоёв состоит приложение,
Раздел описывает архитектуру проекта: из каких слоёв состоит приложение,
как организован код внутри слоёв и какие правила управляют зависимостями.
### Что важно знать
### Что нужно знать
Проект использует [FSD (Feature-Sliced Design)](https://feature-sliced.design/docs/get-started/overview)
как базовую архитектурную методологию. Если вы не знакомы с FSD — начните с официальной документации.
SLM Design (Scoped Layered Module Design) — архитектурный подход
к проектированию фронтенд-приложений, предложенный Громовым Сергеем в 2026 г.
Данная архитектура является **надстройкой над FSD**, а не заменой. Все правила FSD действуют
по умолчанию — если правило явно не переопределено в этом документе, применяется стандарт FSD.
Единственное отклонение: вместо слайсов используются **компоненты**.
Вырос на основе:
### Слои
- [Feature-Sliced Design](https://feature-sliced.design) — слои и направление зависимостей
- Screaming Architecture — структура говорит сама за себя
- Colocation Principle — код рядом с местом использования
| Слой | Назначение |
|------|-----------|
| `app` | Инициализация: провайдеры, стили, роутинг Next.js |
| `screens` | Сборка страницы из виджетов и фич |
| `layouts` | Каркасы и шаблоны страниц |
| `widgets` | Крупные блоки интерфейса |
| `features` | Пользовательские сценарии и действия |
| `entities` | Бизнес-сущности |
| `shared` | Утилиты, UI-кит, инфраструктура |
Переосмыслив эти подходы, SLM Design отличается от FSD в трёх аспектах:
где живёт код (колокация), как он организован (модули)
и как масштабируется (подъём при переиспользовании).
Слой `pages` не используется — конфликтует с Next.js. Вместо него: `screens` и `layouts`.
### Терминология
Зависимости идут строго сверху вниз: `app → screens → layouts → widgets → features → entities → shared`.
Архитектура оперирует четырьмя ключевыми понятиями:
### Компоненты
- **Слой** — содержит модули
- **Модуль** — содержит сегменты
- **Компонент** — содержит сегменты
- **Сегмент** — папка внутри модуля или компонента, группирующая код по назначению: UI-элементы (`ui/`), хуки (`hooks/`), типы (`types/`), стили (`styles/`) и другие
Компонент — стандартная UI-единица, такая же как в любом React-проекте. Содержит корневой `.tsx`,
публичный API (`index.ts`) и сегменты.
Модуль и компонент устроены одинаково — оба имеют сегменты. Разница в том, где они живут и обязателен ли UI.
Компоненты располагаются в:
- `shared/ui/` — переиспользуемые компоненты без бизнес-контекста
- `ui/` внутри master component'а — дочерние компоненты *(подробнее в разделе [Master component](#master-component))*
```text
Слой
└── Модуль
├── Сегменты (hooks/, stores/, types/, styles/, lib/...)
└── ui/
└── Компонент
├── Сегменты (hooks/, stores/, types/, styles/, lib/...)
└── ui/
└── Компонент → ...
```
### Сегменты
#### Слой
Сегмент — папка внутри компонента, группирующая код по техническому назначению. Набор не фиксирован.
Архитектурный уровень. Содержит только модули. Определяет назначение кода и правила зависимостей.
#### Модуль
Единица первого уровня слоя, объединённая по смыслу. Может содержать компонент, логику, типы, стили — или любую комбинацию. Имеет публичный API (`index.ts`) и внутреннюю структуру из сегментов.
Модуль — не обязательно UI. Feature `analytics` может быть только стором и сервисом. Entity `session` может быть только типами и хуком.
Модуль не может содержать вложенных модулей. Вложенные единицы с UI размещаются в сегменте `ui/` как компоненты.
#### Компонент
Вложенная единица внутри сегмента `ui/` модуля (или другого компонента). Публичный `.tsx` файл обязателен. Именуется без суффикса слоя.
Компонент может иметь собственные сегменты (`hooks/`, `styles/`, `types/` и т.д.), `index.ts` и свой `ui/` с ещё более вложенными компонентами.
Отличия от модуля:
| | Модуль | Компонент |
|--|--------|-----------|
| Где живёт | В корне слоя | В `ui/` модуля или другого компонента |
| Публичный `.tsx` | С суффиксом слоя, опционален | Без суффикса, обязателен |
| Может не иметь UI | Да | Нет |
Пример:
```text
features/auth-by-email/ # модуль
├── auth-by-email.feature.tsx # публичный .tsx модуля (с суффиксом, опционален)
├── ui/ # сегмент: компоненты
│ ├── login-form/ # компонент
│ │ ├── login-form.tsx # публичный .tsx компонента (без суффикса, обязателен)
│ │ ├── ui/ # вложенные компоненты
│ │ │ └── password-field/
│ │ │ └── password-field.tsx
│ │ ├── hooks/
│ │ │ └── use-validation.hook.ts
│ │ ├── styles/
│ │ │ └── login-form.module.css
│ │ └── index.ts
│ └── reset-password/ # компонент
│ ├── reset-password.tsx
│ └── index.ts
├── hooks/
│ └── use-auth.hook.ts
├── stores/
│ └── auth.store.ts
└── index.ts
```
#### Сегмент
Техническая папка внутри модуля или компонента, группирующая код по назначению. Набор не фиксирован — включаются только те сегменты, которые нужны.
| Сегмент | Назначение |
|---------|-----------|
| `styles/` | Стили |
| `types/` | Интерфейсы, типы, enums, DTO |
| `ui/` | Компоненты, провайдеры и любые другие элементы интерфейса |
| `stores/` | Сторы состояния |
| `ui/` | Вложенные компоненты |
| `hooks/` | React-хуки |
| `services/` | Внешние источники данных |
| `stores/` | Сторы состояния |
| `types/` | Интерфейсы, типы, enums, DTO |
| `styles/` | Стили |
| `lib/` | Утилиты |
| `services/` | Внешние источники данных |
| `helpers/` | Вспомогательные функции |
| `config/` | Константы, конфигурация |
### Master component
### Ключевой принцип
Master component — это обычный компонент, на который наложен ряд дополнительных правил.
Эти правила определяют его место в архитектуре и границы зависимостей.
> Модуль живёт на самом низком уровне, где он используется.
> Поднимается выше только при переиспользовании в 2+ местах.
- Может располагаться только в слоях: `screens`, `layouts`, `widgets`, `features`, `entities`
- Импортирует master component'ы только из слоёв ниже по иерархии
- Корневой `.tsx` именуется с суффиксом слоя: `header.widget.tsx`, `auth.feature.tsx`
- Корневой `.tsx` необязателен — `index.ts` может экспортировать несколько сущностей напрямую
- Дочерние компоненты в `ui/` доступны снаружи только через `index.ts`
- Компоненты внутри одного `ui/` могут импортировать друг друга
Если модуль используется только в одном месте — он остаётся на текущем уровне.
Как только он начинает использоваться в 2+ местах — выносится на уровень выше.
В крайнем случае — в `shared/`, где он доступен всем.
### Слои
Каждый нижний слой не знает о существовании верхних. Импорты идут строго сверху вниз.
```
app → layouts → screens → widgets → features → entities → shared
```
| Слой | Что лежит | Импортирует |
|------|-----------|-------------|
| **App** | Роутинг, провайдеры, глобальные стили. Композиция layout + screen для маршрута. | Все слои ниже |
| **Layouts** | Каркас страницы, общий для группы маршрутов. | widgets, features, entities, shared |
| **Screens** | Контент конкретной страницы. | widgets, features, entities, shared |
| **Widgets** | Составные блоки с данными/логикой, переиспользуемые в 2+ местах. | features, entities, shared |
| **Features** | Пользовательское действие или интерактивный сценарий. | entities, shared |
| **Entities** | Бизнес-сущность с отображением и типами. | shared |
| **Shared** | Переиспользуемые компоненты, утилиты, стили без бизнес-логики. | ничего |
Принципы:
- Импорты строго сверху вниз
- Модули одного слоя не знают друг о друге
- Layout получает контекстно-зависимые блоки через пропсы от app, а не импортирует их сам
- `entities/` и `features/` создаются осознанно — это не результат «вынесения» компонента из screen
#### App
Точка входа приложения: роутинг (Next.js App Router), провайдеры, глобальные стили.
Находится на самом высоком уровне абстракции — может импортировать любой слой ниже.
Никакой бизнес-логики — только композиция.
```text
app/
├── layout.tsx # RootLayout: провайдеры, глобальные стили
├── page.tsx # Главная: MainLayout + HomeScreen
├── knv-new/
│ └── page.tsx # КНВ: MainLayout + KnvScreen
└── catalog/
└── page.tsx # Каталог: MainLayout + CatalogScreen
```
```tsx
// app/knv-new/page.tsx
import { MainLayout } from '@/layouts/main'
import { KnvScreen } from '@/screens/knv'
export default function KnvNewPage() {
return (
<MainLayout>
<KnvScreen />
</MainLayout>
)
}
```
Если layout требует разный контент в зависимости от страницы — app передаёт его через пропсы:
```tsx
// app/knv-new/page.tsx
import { MainLayout } from '@/layouts/main'
import { KnvScreen } from '@/screens/knv'
import { KnvHeader } from '@/widgets/knv-header'
export default function KnvNewPage() {
return (
<MainLayout header={<KnvHeader />}>
<KnvScreen />
</MainLayout>
)
}
```
#### Layouts
Каркас страницы — общие элементы, одинаковые для группы маршрутов (header, footer, sidebar).
Если компонент внутри layout начинает использоваться в 2+ местах — он выносится в `widgets/` или `shared/ui/`.
```text
src/layouts/
└── main/
├── main.layout.tsx
├── ui/
│ ├── header/
│ │ └── header.tsx
│ └── footer/
│ └── footer.tsx
└── index.ts
```
#### Screens
Контент конкретной страницы. Собирает локальные секции и переиспользуемые модули из нижних слоёв.
Если компонент внутри screen начинает использоваться в 2+ местах — он выносится в `widgets/` или `shared/ui/`.
```text
src/screens/
└── knv/
├── knv.screen.tsx
├── ui/
│ ├── hero-section/
│ │ └── hero-section.tsx
│ ├── products-section/
│ │ └── products-section.tsx
│ └── diseases-section/
│ └── diseases-section.tsx
└── index.ts
```
#### Widgets
Составные блоки с данными и логикой, переиспользуемые в 2+ местах.
Если блок с логикой нужен только в одном месте — это компонент внутри `screens/{name}/ui/` или `layouts/{name}/ui/`, а не widget.
```text
src/widgets/
└── popular-products-slider/
├── popular-products-slider.widget.tsx
├── ui/
│ └── slider-card/
│ └── slider-card.tsx
├── hooks/
│ └── use-products.hook.ts
└── index.ts
```
#### Features
Пользовательское действие или интерактивный сценарий: авторизация, заказ, добавление в корзину.
Feature создаётся осознанно, когда есть действие пользователя с бизнес-логикой. Компонент опционален — feature может экспортировать хуки, сторы, компоненты или всё вместе.
```text
src/features/
└── auth-by-email/
├── auth-by-email.feature.tsx
├── ui/
│ ├── login-form/
│ │ └── login-form.tsx
│ └── reset-password/
│ └── reset-password.tsx
├── hooks/
│ └── use-auth.hook.ts
├── stores/
│ └── auth.store.ts
└── index.ts
```
#### Entities
Бизнес-сущность с отображением и типами: препарат, заболевание, врач, пользователь.
Entity создаётся осознанно, когда появляется бизнес-сущность. Компонент опционален — entity может быть только типами и хуком.
Отличие от `shared/ui/`: entity-компонент знает о бизнес-домене (принимает `Product`, а не абстрактные пропсы).
```text
src/entities/
├── product/
│ ├── product.entity.tsx
│ ├── ui/
│ │ └── product-card/
│ │ └── product-card.tsx
│ ├── types/
│ │ └── product.type.ts
│ └── index.ts
├── session/
│ ├── types/
│ │ └── session.type.ts
│ ├── hooks/
│ │ └── use-session.hook.ts
│ └── index.ts
```
#### Shared
Переиспользуемые компоненты, утилиты, стили без бизнес-логики. Не знает о бизнес-домене — работает с абстрактными данными.
Структурирован как набор сегментов:
```text
src/shared/
├── ui/
│ ├── icon/
│ │ └── icon.tsx
│ ├── carousel/
│ │ ├── carousel.tsx
│ │ ├── ui/
│ │ │ ├── carousel-slide/
│ │ │ │ └── carousel-slide.tsx
│ │ │ └── carousel-dots/
│ │ │ └── carousel-dots.tsx
│ │ └── index.ts
│ ├── container/
│ └── button/
├── lib/
│ ├── format-date.ts
│ └── cn.ts
├── styles/
│ ├── variables.css
│ └── media.css
└── sprites/
```
### Модуль
#### Структура
```text
{name}/
├── {name}.{суффикс}.tsx # компонент (опционален)
├── ui/ # вложенные компоненты
├── hooks/ # хуки
├── stores/ # сторы
├── types/ # типы, интерфейсы, enums
├── styles/ # стили
├── lib/ # утилиты
├── services/ # внешние источники данных
├── helpers/ # вспомогательные функции
├── config/ # константы, конфигурация
└── index.ts # публичный API
```
#### Именование компонента
Суффикс слоя получают **только модули первого уровня слоя** — те, что лежат непосредственно в корне слоя. Все компоненты (в `ui/`, любой глубины) именуются без суффикса. Без исключений.
| Слой | Суффикс | Пример |
|------|---------|--------|
| Layouts | `.layout.tsx` | `main.layout.tsx` |
| Screens | `.screen.tsx` | `knv.screen.tsx` |
| Widgets | `.widget.tsx` | `popular-products-slider.widget.tsx` |
| Features | `.feature.tsx` | `auth-by-email.feature.tsx` |
| Entities | `.entity.tsx` | `product.entity.tsx` |
Примеры:
```text
features/auth-by-email/auth-by-email.feature.tsx # модуль первого уровня → суффикс
features/auth-by-email/ui/login-form/login-form.tsx # компонент в ui/ → без суффикса
shared/ui/carousel/carousel.tsx # компонент в shared → без суффикса
```
#### Правила импорта
Три уровня правил:
**Между слоями** — импорты строго сверху вниз:
```
app → layouts → screens → widgets → features → entities → shared
```
**Внутри модуля (не shared)** — сегменты доступны друг другу и компонентам. Компоненты внутри одного `ui/` не импортируют друг друга:
```text
features/auth-by-email/
├── ui/
│ ├── login-form/ # НЕ может импортировать reset-password
│ └── reset-password/ # НЕ может импортировать login-form
```
Если двум компонентам нужен общий код — он поднимается на уровень выше:
```text
features/auth/ui/login-form/ui/email-input/ # нужен соседу
→ features/auth/ui/email-input/ # поднимаем на уровень
→ shared/ui/email-input/ # если нужен за пределами фичи
```
Компоненты наследуют правила зависимостей **родительского слоя**:
- Компонент внутри `features/auth/ui/login-form/` может импортировать `entities/` и `shared/` — как и сам feature
- Компонент внутри `widgets/hero/ui/hero-stats/` может импортировать `features/`, `entities/`, `shared/` — как и сам widget
**Shared** — без ограничений на внутренние импорты. Компоненты в `shared/ui/` могут импортировать друг друга (`button` использует `icon`), `ui/` может использовать `lib/` и другие сегменты. Shared — фундамент, его компоненты строятся друг на друге.
Правила импорта между слоями enforceable через ESLint — настройка границ слоёв и запрет обратных зависимостей.
### Жизненный цикл модуля
Модуль не проектируется «на вырост». Он рождается на самом низком уровне
и поднимается только когда появляется реальная потребность.
Пример пути компонента `product-card`:
1. **Начало:** `screens/catalog/ui/product-card/` — нужен только на странице каталога.
2. **Переиспользование:** появился на странице поиска — выносим выше.
Куда именно зависит от природы:
- Составной блок с данными и логикой → `widgets/product-card/`
- Представление бизнес-сущности → `entities/product/ui/product-card/`
- Абстрактный UI без бизнес-логики → `shared/ui/product-card/`
Основной триггер подъёма — переиспользование в 2+ местах.
Но если очевидно что модуль будет переиспользоваться — разумно разместить его на нужном уровне сразу.
Как происходит подъём на практике:
1. Разработчик обнаруживает что компонент нужен в другом месте
2. Определяет целевой слой по природе компонента (widget / entity / shared)
3. Перемещает папку, обновляет импорты, добавляет суффикс если это модуль первого уровня слоя
4. Ревью подтверждает что подъём обоснован
Подъём — это обычный рефакторинг в рамках задачи, а не отдельная активность.
### Граничные случаи
| Ситуация | Решение | Почему |
|----------|---------|--------|
| Фильтр каталога только на одной странице, но с хуками и стором | Модуль в `screens/catalog/` с сегментами `hooks/`, `stores/` | Не feature — feature это действие пользователя с бизнес-логикой (авторизация, заказ), а не UI с состоянием |
| Компонент используется в 2 местах на одной странице | Остаётся в `screens/{name}/ui/` | Переиспользование внутри одного screen — не повод выносить в widget |
| Entity без UI (только типы и хук) | Нормально | Модуль не обязан иметь UI. `entities/session/` с типами и хуком — валидный модуль |
| Компонент нужен и в layout, и в screen | Выносить в `widgets/` или `shared/ui/` | Два разных слоя используют один компонент → переиспользование → подъём |
| Модуль screen содержит бизнес-логику (хуки, стор, сервисы) | Нормально | Это не значит что он должен стать feature. Модуль с логикой внутри screen — обычное дело, пока он не переиспользуется |
| Компонент в `shared/ui/button` хочет использовать `shared/ui/icon` | Разрешено | Shared — исключение: внутренние импорты без ограничений. Это фундамент, его компоненты строятся друг на друге |
### Запрещено
- **Не создавать feature без бизнес-логики** — кнопка без состояния и сайд-эффектов это компонент в `ui/`, а не feature
- **Не класть доменные типы в shared** — если тип знает о Product, Disease, User — он живёт в `entities/`, не в `shared/`
- **Не создавать entity или feature как результат «вынесения»** — они создаются осознанно: появилась бизнес-сущность → entity, появилось действие пользователя → feature
- **Не импортировать соседние компоненты в `ui/` (кроме shared)** — если двум компонентам нужен общий код, он поднимается на уровень выше
- **Не хранить в shared «помойку для всего»** — shared содержит переиспользуемые компоненты и утилиты без бизнес-логики, а не код который «непонятно куда положить»
### Почему так, а не иначе
#### Почему `ui/` а не `modules/`
Внутри сегмента `ui/` всегда лежат единицы с обязательным UI (компоненты). Название точно отражает содержимое. `modules/` был нейтральнее, но скрывал природу вложенных единиц и создавал путаницу — модуль внутри модуля размывал понятие «модуль как единица слоя».
#### Почему модуль и компонент — разные понятия
Модуль — единица первого уровня слоя, может не иметь UI. Компонент — вложенная единица с обязательным UI. Разделение снимает вопрос «а если нет `.tsx` — это всё ещё компонент?» и делает название сегмента `ui/` честным.
#### Почему shared без ограничений на внутренние импорты
Shared — фундамент. Его компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`. Запрещать это — бороться с реальностью. Поднимать некуда — shared уже нижний слой.
#### Почему нет слоя pages
Роутинг живёт в `app/` (Next.js App Router). Отдельный слой `pages` конфликтовал бы с файловой структурой Next.js и дублировал ответственность `app/`.
#### Почему компоненты в одном `ui/` не импортируют друг друга (кроме shared)
Независимые компоненты легко выносить в другой слой при переиспользовании. Если компонент A зависит от соседа B — при подъёме A придётся тянуть B. Это усложняет рефакторинг и нарушает принцип колокации.
<!-- /basics/code-style -->
## Стиль кода