Модули и public API
Стадии роста модуля, папка public/, сегменты и правила импорта между модулями.
Модуль в DMA — это папка (или один файл) внутри features/ или services/. Правила одинаковые для обоих слоёв: отличается только предикат слоя, а не структура внутри.
Главная идея: модуль растёт постепенно, без миграций «переписать всё с нуля». Каждая стадия — надстройка над предыдущей.
Стадии роста
Стадия 0 — файл-модуль
Пока в модуле один файл — весь файл и есть публичная поверхность:
features/
└── checkout.tsxИмпортируйте файл напрямую, без папки и без index.ts.
Стадия 1 — плоская папка
Появились соседние файлы с тем же basename-префиксом, что у file-модуля (например checkout.tsx и checkout.store.ts в features/) — заводите папку и public/:
features/checkout/
├── public/
│ └── checkout-page.tsx
├── use-cart-total.ts
└── cart-row.tsxВсё, что нужно снаружи, лежит в public/. Внутренние файлы — рядом, но импортировать их из других модулей нельзя.
Стадия 2 — сегменты
Когда внутри накопилось много файлов (ориентир — около 8+, сигнал stage-growth в dma doctor), делите по ролям:
features/checkout/
├── public/ # точки входа — плоская папка
├── ui/
├── model/
├── api/
└── lib/ # хелперы, которые нужны 2+ сегментам этого модуля| Сегмент | Что внутри |
|---|---|
public/ | Entrypoints, события, порты, внешние типы |
ui/ | Компоненты презентации |
model/ | Состояние и доменная логика модуля |
api/ | Транспорт, маппинг DTO |
lib/ | Чистые хелперы для нескольких сегментов |
Рекомендуемое направление внутри модуля (включайте, когда начинает мешать, не с первого дня):
public → ui, model, api, lib
ui → model, lib
model → api, lib
api → libИмена совпадают с группами в shared/, но права другие: features/checkout/ui может импортировать model того же модуля, а shared/ui — не может тянуть shared/model.
Стадия 3 — разделение
Если в public/ слишком много реализаций точка входа'ов (ориентир ~8+) — делите модуль, а не вкладывайте public/ друг в друга. Пока это решение на ревью, отдельного сигнала в doctor нет.
Стадия 4 — пакет
Несколько приложений потребляют модуль → выносите в пакет монорепо. Правила те же, enforcement через exports workspace-пакета.
Публичный API
Межмодульный импорт — только в */public/*, напрямую в файл:
// ✓
import { CheckoutPage } from "@/features/checkout/public/checkout-page";
// ✗ глубокий импорт во внутренности
import { CartRow } from "@/features/checkout/ui/cart-row";
// ✗ barrel
import { CheckoutPage } from "@/features/checkout";public/ остаётся плоским. Реализация точка входа'а живёт в public/ и может импортировать внутренние сегменты. Реэкспорт 1:1 из внутреннего файла допустим, но по умолчанию пишите код прямо в public/.
На стадии 0 весь файл — публичный, папка public/ не нужна.
Barrel-файлы запрещены
index.ts с реэкспортами внутри модуля — нельзя. Такой файл прячет граф зависимостей. Подробнее — Почему нет barrel-файлов.
Колокация внутри модуля
- Тесты
*.test.ts— рядом с тестируемым файлом - Стили — рядом с компонентом
- Типы — рядом с потребителем; межмодульные типы —
public/*.types.tsилиshared/domainпосле второго использования - Имена файлов — kebab-case, смысл в имени:
use-cart-total.ts, неhook.ts
Связь между модулями
| Задача | Как |
|---|---|
| Использовать код ниже по слоям | Прямой импорт services/*/public/* или shared/ |
| Событие без знания подписчиков | Событие в public/ эмиттера; подписка вниз или связка в app/ |
| Прямой импорт дал бы цикл или шаг вверх | Порт в public/ports.ts + привязка в корень композиции |
| Только визуальная композиция | Слоты/props в корень композиции |
Порты — зависимость во время выполнения, невидимая для статического графа. Держите их в public/ports.ts и не используйте для обхода promotion.
Что проверяют инструменты
| Правило | Что ловит |
|---|---|
public-api | Импорт мимо public/ |
no-barrel | Barrel index с реэкспортами |
stage-growth (doctor) | Структура отстаёт от размера модуля |