This commit is contained in:
S. Gromov
2026-08-10 09:12:22 +03:00
parent b5db9e5158
commit 691069af8e
55 changed files with 1519 additions and 3383 deletions

View File

@@ -0,0 +1,176 @@
# Модули
Модуль является владельцем самостоятельной ответственности. Публичный API, зависимости, состояние, жизненный цикл и внутренняя реализация ответственности относятся к модулю независимо от того, в каком файле или сущности фреймворка выполняется код.
## Ответственность и владелец
Перед созданием модуля ответственность формулируется без упоминания желаемой папки, файла или библиотеки.
Самостоятельность ответственности определяется вопросами:
- есть ли у неё отдельная причина изменяться;
- нужен ли внешним потребителям собственный контракт;
- есть ли у неё архитектурные зависимости;
- владеет ли она данными или изменяемым состоянием;
- нужна ли ей собственная область жизни;
- можно ли назвать её независимо от внутренней реализации.
Если ответственность самостоятельна, она получает ровно один модуль-владелец. Если несколько частей изменяются по несвязанным причинам, им нужны разные владельцы.
Размер не определяет модуль. Модуль может состоять из одного файла реализации, а большая папка может оставаться частью другого владельца.
## Граница владения
Модуль определяет:
- публичные возможности ответственности;
- допустимые внешние зависимости;
- модели и правила, принадлежащие ответственности;
- состояние и источник истины;
- создание и очистку долгоживущих ресурсов;
- устройство внутренней реализации.
Компонент, хук, провайдер, хранилище, сервис или маршрут могут выполнять часть этой работы технически. Владение остаётся у ближайшего модуля, пока часть кода не получает самостоятельную ответственность и собственную модульную границу.
## Публичный API
У модуля один логический публичный API. Он является контрактом между владельцем и внешними потребителями, а не перечнем всех внутренних файлов.
Публичный API:
- открывает только возможности, необходимые реальным внешним потребителям;
- скрывает детали реализации и изменяемые внутренние механизмы;
- не раскрывает внутренние сегменты;
- представлен объявленными публичными фасетами;
- является единственным способом доступа к модулю извне.
Внутри своей границы модуль может обращаться к собственным файлам напрямую. Требование публичного API действует при пересечении модульной границы.
### Фасеты
Фасет — публичная точка входа, открывающая часть единого API для определённой среды выполнения.
| Фасет | Назначение |
|---|---|
| `index` | Универсальные типы и исполняемый код, совместимый с серверным рендерингом, включая RSC, и клиентским выполнением |
| `client` | Клиентская граница фреймворка, участвующая в серверной предварительной отрисовке и выполняющаяся при гидратации и в браузере |
| `browser` | Код только для браузера, подключаемый динамически через границу с отключённым SSR |
| `server` | Код только для сервера, недоступный универсальной и клиентской среде выполнения |
`index` обязателен. Остальные фасеты создаются только при наличии реального потребителя и несовместимых требований к среде.
Ограничение среды распространяется на все исполняемые импорты и реэкспорты фасета, включая транзитивные. Имя файла, директива `use client`, tree shaking или проверка `typeof window` сами по себе не доказывают совместимость.
Один исполняемый экспорт размещается в минимально подходящем фасете и не дублируется между ними. Универсальный фасет не реэкспортирует специализированные фасеты.
```text
auth/
├── index.ts # Обязательный универсальный фасет
├── client.ts # При необходимости
├── browser.ts # При необходимости
├── server.ts # При необходимости
└── ... # Внутренняя реализация
```
Эта файловая форма представляет уже определённую публичную границу. Наличие `index.ts` само по себе не создаёт модуль.
## Зависимости между модулями
Зависимость связывает владельца исходного кода с владельцем импортируемого кода. Импорт внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей.
При пересечении модульной границы код использует только публичный фасет целевого модуля:
```ts
// Допустимо
import { Button } from '@/ui/button'
// Недопустимый глубокий импорт
import { Button } from '@/ui/button/button'
```
Для каждой связи выполняются три условия:
1. Направление разрешено [матрицей слоёв](./layers.md#направление-зависимостей).
2. Целевой модуль используется только через публичный API.
3. Общий граф модулей остаётся ацикличным.
Модули одного слоя могут зависеть друг от друга, если соблюдены публичные границы и не возникает цикл. SLM не требует обязательного посредника или отдельного механизма dependency injection для любой межмодульной связи.
## Компоненты
Компонент является сущностью фреймворка, реализующей часть интерфейса родительского модуля. Он не образует самостоятельного владельца только из-за наличия отдельного файла, каталога, props, локального состояния, доступа к данным или кода жизненного цикла.
Зависимости, состояние и ресурсы компонента относятся к родительскому модулю. Если UI-часть получает самостоятельную ответственность, публичный API, собственные зависимости или область жизни, ей требуется модульная граница.
Модуль может состоять из одного корневого компонента. В этом случае компонент реализует интерфейс, а модуль остаётся владельцем ответственности.
Компонент может быть оформлен отдельным каталогом и иметь локальный `index.ts`:
```text
button-submit/
├── button-submit.tsx
├── styles/
│ └── button-submit.module.css
├── types/
│ └── button-submit.types.ts
└── index.ts
```
Такой `index.ts` является внутренней точкой входа компонента. Он упрощает импорты внутри модуля, но не создаёт SLM public API, нового владельца или узел графа зависимостей.
| `index.ts` компонента | Публичный фасет модуля |
|---|---|
| Действует внутри ближайшего модуля | Действует при пересечении модульной границы |
| Экспортирует локальную реализацию и типы | Экспортирует контракт владельца |
| Не создаёт архитектурную границу | Представляет архитектурную границу |
| Не делает компонент модулем | Принадлежит уже определённому модулю |
Если компонент входит в публичный контракт владельца, корневой фасет модуля явно реэкспортирует его локальную точку входа. Внешний код по-прежнему импортирует модуль, а не внутренний путь компонента.
## Вложенные модули
Вложенный модуль — самостоятельный владелец, физически размещённый внутри родительского модуля. Он имеет собственные ответственность, публичный API и границу зависимостей и подчиняется общим правилам модулей.
Код родительского модуля использует вложенный модуль через его публичный API. Код за пределами родительской границы не импортирует вложенный модуль напрямую и получает необходимые возможности через API родителя.
Если вложенный модуль становится нужен нескольким внешним владельцам, его можно перенести в минимальную общую область. Сам факт повторного использования внутри родителя переноса не требует.
## Состояние и жизненный цикл
Состояние принадлежит модулю, ответственность которого определяет его смысл, допустимые изменения и область жизни. Место хранения состояния не переносит владение в файл хранилища, провайдер или контекст фреймворка.
Для каждого долгоживущего ресурса модуль-владелец определяет:
- место создания;
- момент запуска;
- область жизни;
- допустимое число экземпляров;
- способ остановки, отмены или освобождения.
Подписка, обработчик событий, таймер, наблюдатель, запрос или соединение не остаются активными после завершения своей области жизни. Очистку может технически выполнить компонент, провайдер или фреймворк, но ответственность за корректную границу остаётся у модуля.
Размещение экземпляра на уровне файла не доказывает область жизни всего приложения. Точка входа `app` может запустить ресурс через публичный API модуля, но не становится его владельцем.
## Внутренняя организация
После определения ответственности и публичной границы модуль может организовать реализацию [сегментами](./segments.md), компонентами и вложенными модулями. SLM не требует полного каркаса и не задаёт обязательный набор внутренних папок.
Каждый модуль физически размещается в отдельной папке. Это делает логическую границу наблюдаемой для потребителей и автоматических проверок, но не заменяет решение о владельце.
Наличие `index.ts` не используется как единственный признак модуля. Модульную границу определяют ответственность, владелец, публичные потребители и сопоставление путей, принятое проектом.
## Связанные правила
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-MODULE-A014`](../rules/registry.md#slm-module-a014)
- [`SLM-MODULE-R006`](../rules/registry.md#slm-module-r006)
- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
- [`SLM-MODULE-R012`](../rules/registry.md#slm-module-r012)
- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005)
- [`SLM-COMPONENT-R009`](../rules/registry.md#slm-component-r009)
- [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010)
- [`SLM-LIFECYCLE-R013`](../rules/registry.md#slm-lifecycle-r013)
- [`SLM-ENVIRONMENT-R016`](../rules/registry.md#slm-environment-r016)
- [`SLM-ENVIRONMENT-R017`](../rules/registry.md#slm-environment-r017)
- [`SLM-ENVIRONMENT-R018`](../rules/registry.md#slm-environment-r018)
- [`SLM-ENVIRONMENT-R019`](../rules/registry.md#slm-environment-r019)