# Модули Модуль является владельцем самостоятельной ответственности. Публичный API, зависимости, состояние, жизненный цикл и внутренняя реализация ответственности относятся к модулю независимо от того, в каком файле или механизме выполняется код. ## Ответственность и владелец Перед созданием модуля ответственность формулируется без упоминания желаемой папки, файла или библиотеки. Самостоятельность ответственности определяется вопросами: - какой один результат или поведение она обеспечивает; - что модуль должен делать сам для получения этого результата; - какие возможности ему нужны от других модулей; - нужен ли внешним потребителям собственный контракт; - требуют ли зависимости отдельного архитектурного владения; - владеет ли она смыслом данных или изменяемого состояния; - нужна ли ей собственная область жизни; - можно ли назвать её независимо от внутренней реализации. Props, импорты, Context, локальное состояние, lifecycle-код или количество файлов сами по себе не доказывают самостоятельность ответственности. Если ответственность самостоятельна, она получает ровно один модуль-владелец. Размер не определяет модуль. Модуль может состоять из одного файла реализации, а большая папка может оставаться частью другого владельца. ## Ближайшая граница Каждый файл принадлежит ближайшей модульной границе и реализует ответственность этого модуля. Вид кода не меняет владельца: framework-компонент, Provider, Guard, hook, store, service или utility остаются внутренней реализацией ближайшего модуля. Вложенный модуль начинает новую границу. Его содержимое реализует выделенную подответственность, а сам вложенный модуль как единица участвует в реализации общего результата родителя: ```text checkout/ # Владеет ответственностью checkout ├── checkout.tsx # Реализует checkout ├── components/ # Реализуют checkout └── modules/ └── form-session/ # Владеет подответственностью form session ├── form-session.provider.tsx └── hooks/ # Реализуют form session ``` Родитель и вложенный модуль не владеют одной ответственностью одновременно. Родитель определяет общий результат, а вложенный модуль — отдельно сформулированную связную часть этого результата. ## Граница владения Модуль определяет: - публичные возможности ответственности; - допустимые внешние зависимости; - модели и правила, принадлежащие ответственности; - состояние и источник истины; - создание и очистку долгоживущих ресурсов; - устройство внутренней реализации. Внутренний файл может технически выполнять часть или всю эту работу. Архитектурное владение остаётся у модуля и не переносится в компонент, Provider, store или другой механизм реализации. ## Публичный API У модуля один логический публичный API. Он является контрактом между владельцем и внешними потребителями, а не перечнем всех внутренних файлов. Публичный API: - открывает только возможности, необходимые реальным внешним потребителям; - скрывает детали реализации и изменяемые внутренние механизмы; - не раскрывает внутренние сегменты; - представлен объявленными публичными фасетами; - является единственным способом доступа к модулю извне. Внутри своей границы модуль может обращаться к собственным файлам напрямую. Требование публичного API действует при пересечении модульной границы. ### Фасеты Фасет — публичная точка входа, открывающая часть единого API для определённой среды выполнения. | Фасет | Назначение | |---|---| | `index` | Универсальные типы и исполняемый код, совместимый с серверным рендерингом, включая RSC, и клиентским выполнением | | `client` | Клиентская граница фреймворка, участвующая в серверной предварительной отрисовке и выполняющаяся при гидратации и в браузере | | `browser` | Browser-only и динамически подключаемый клиентский код; фасет всегда импортируется динамически с отключённым 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' ``` Направления между слоями, same-layer импорты, роль групп и требования к lint-проверке описаны в разделе [Зависимости](./dependencies.md). ## Корень модуля Корень модуля не используется как плоский каталог реализации. В нём находятся: - объявленные публичные фасеты; - не более одного опционального главного implementation- или assembly-файла. Главный файл непосредственно реализует или собирает ответственность модуля и однозначно соответствует ей по имени и роли. Возможные примеры: ```text header/header.tsx footer/footer.tsx auth-guard/auth-guard.provider.tsx ``` Архитектурным владельцем остаётся модуль, а главный файл является основной реализацией или точкой её сборки. Если проблемно доказать, что файл является главным, его не выносят в корень. Вся остальная реализация размещается в [сегментах](./segments.md). Главный framework-файл не обязан экспортироваться через `index`. Модуль открывает его через минимально подходящий фасет среды выполнения: ```text theme/ ├── index.ts # Универсальные публичные типы ├── client.ts # Экспортирует ThemeProvider и useTheme ├── theme.provider.tsx # Главная framework-реализация ├── context/ │ └── theme-context.ts ├── hooks/ │ └── use-theme.ts ├── types/ └── styles/ ``` `ThemeProvider` может хранить состояние, синхронизироваться с платформой, предоставлять Context и очищать ресурсы при размонтировании. Он технически реализует ответственность, но её смысл, контракт, зависимости и область жизни определяет модуль `theme`. ## Framework-компоненты SLM не вводит собственный вид компонента. Модуль может содержать любые framework-компоненты: визуальные элементы, Providers, Guards, Error Boundaries и другие сущности, которые используемый фреймворк считает компонентами. Помимо опционального главного framework-файла в корне, остальные framework-компоненты организуются на одном внутреннем уровне относительно модуля. Они могут быть одиночными файлами или каталогами с локальными стилями, типами, hooks, тестами и внутренним `index.ts`, но каталог компонентной единицы не содержит другие компонентные единицы или вложенные модули. ```text main-layout/ # Модуль ├── index.ts # Публичный фасет ├── main-layout.tsx # Главная реализация ├── components/ # Сегмент │ ├── header/ │ │ ├── index.ts # Локальная точка входа │ │ ├── header.tsx │ │ ├── styles/ │ │ └── types/ │ ├── navigation-item.tsx │ └── footer.tsx └── providers/ # Сегмент └── layout-state/ ├── layout-state.provider.tsx ├── hooks/ └── types/ ``` `Header` может рендерить `NavigationItem`, но их файловые области остаются соседними относительно `main-layout`. Ограничение относится к организации файлов, а не к runtime-дереву фреймворка. Локальный `index.ts` компонентной единицы упрощает импорты внутри модуля, но не создаёт публичный API, владельца или узел графа зависимостей. Если компонент нужен внешнему потребителю, фасет модуля явно реэкспортирует его, а внешний код по-прежнему импортирует модуль. Названия `components`, `providers`, `styles`, `types` и `hooks` являются примерами локального стайлгайда, а не обязательными путями SLM. ## Вложенные модули Вложенный модуль — обычный модуль, физически размещённый внутри родительского. Он владеет отдельно сформулированной связной подответственностью, имеет публичный API, собственные зависимости и область жизни и подчиняется всем правилам модулей. ```text checkout/ # Родительский модуль ├── index.ts ├── checkout.tsx # Главная реализация checkout ├── components/ │ ├── order-summary.tsx │ └── submit-order.tsx └── modules/ └── form-session/ # Вложенный модуль ├── index.ts # Универсальные публичные типы ├── client.ts # Экспортирует Provider и hook ├── form-session.provider.tsx # Главная реализация form session ├── hooks/ │ └── use-form-session.ts └── types/ ``` Framework-компонент не превращается в архитектурную сущность. Если окружающему его коду требуется самостоятельная ответственность, вокруг кода создаётся вложенный модуль, а компонент остаётся его обычной framework-реализацией. Код родительского модуля использует вложенный модуль через его публичный API. Код за пределами родительской границы не импортирует вложенный модуль напрямую и получает необходимые возможности через API родителя. Вложенный модуль может рекурсивно содержать другие вложенные модули. Именно модульная граница, а не каталог компонента, создаёт каждый следующий структурный уровень. Если вложенный модуль становится нужен нескольким внешним владельцам, его переносят в минимальную общую область. ## Колокация и рост Код создаётся в самой узкой допустимой области внутри своего архитектурного владельца: 1. Вспомогательный код, нужный одной компонентной единице, колоцируется в её файле или каталоге. 2. Каждая выделенная framework-компонентная единица размещается на общем внутреннем уровне модуля, даже если используется только одним другим компонентом. 3. Код, нужный нескольким внутренним единицам, поднимается в их ближайший общий сегмент. 4. При появлении самостоятельной подответственности вокруг кода создаётся вложенный модуль. 5. Вложенный модуль переносится в общую область, когда прямой доступ к нему требуется внешним владельцам. Расширение числа потребителей меняет колокацию внутри владельца. Появление самостоятельной ответственности меняет модульный граф. ## Состояние и жизненный цикл Состояние принадлежит модулю, ответственность которого определяет его смысл, допустимые изменения и область жизни. Место хранения состояния не переносит владение в store, Provider или Context. Для каждого долгоживущего ресурса модуль-владелец определяет: - место создания; - момент запуска; - область жизни; - допустимое число экземпляров; - способ остановки, отмены или освобождения. Подписка, обработчик событий, таймер, наблюдатель, запрос или соединение не остаются активными после завершения своей области жизни. Очистку может технически выполнить framework-компонент или сам фреймворк, но ответственность за корректную границу остаётся у модуля. Размещение экземпляра на уровне файла не доказывает область жизни всего приложения. Точка входа `app` может запустить ресурс через публичный API модуля, но не становится его владельцем. ## Связанные правила - [`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-MODULE-R020`](../rules/registry.md#slm-module-r020) - [`SLM-MODULE-R021`](../rules/registry.md#slm-module-r021) - [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005) - [`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)