Концепции
Куда положить файл
Алгоритм размещения кода: от корня композиции до promotion и shared.
Ответ на вопрос «куда положить этот файл» — не вкус, а последовательность предикатов. Пройдите шаги сверху вниз и остановитесь на первом совпадении.
Алгоритм
- Роут, layout, связка, провайдеры? → корень композиции:
app/,pages/илиroutes/. Только монтирование*/public/*, без бизнес-логики. - Другой модуль должен импортировать этот код (продуктовый сценарий)? →
services/<name>/public/. Promotion: перенос + пересмотр public API. Никогдаfeature → feature. - Переносимый хелпер, UI или тип уже нужен 2+ модулям? →
shared/{ui,lib,api,model,domain}. Не выносите при первом использовании. - Единственный потребитель — один модуль? → разместите рядом внутри этого модуля (внутренний файл или сегмент
ui/,model/). - Новый пользовательский сценарий, монтируемый только из корня композиции? →
features/<name>(стадия 0 — один файл, стадия 1+ — папка сpublic/). - Ребро дало бы цикл или шаг вверх? → порт в
public/ports.ts+ привязка в корень композиции. Не используйте порт, чтобы обойти законную promotion.
Быстрые предикаты
| Путь | Предикат |
|---|---|
app/ / pages/ / routes/ | Собирает приложение; модули его не импортируют |
features/ | Нет входящих рёбер от других модулей |
services/ | Есть входящие рёбра от модулей; папку создают при первой promotion |
shared/ | Переносимый код без продуктового сценария |
Входящие рёбра для promotion — только от features/* и services/*. Монтирование из корня композиции promotion не вызывает.
Таблица: типичный файл → папка
| У вас… | Куда | Пример |
|---|---|---|
page.tsx, +page.svelte, index.astro | корень композиции | app/page.tsx |
| Провайдеры, связка событий | корень композиции | app/providers.tsx |
| Экран / flow для роута | features/<name>/public/ или стадия-0 файл | catalog-page.tsx |
| Store одного feature | разместите рядом в feature | checkout.store.ts |
| Composable одного feature (Vue) | разместите рядом в feature | use-catalog-search.ts |
| Логика для catalog и checkout | services/<name>/public/ | cart.ts |
| Кнопка без продуктовой логики | shared/ui/ | button.tsx |
formatCurrency, даты, i18n | shared/lib/ | format-currency.ts |
| Базовый HTTP-клиент, interceptors | shared/api/ | http.ts |
| Эндпоинты одной фичи | разместите рядом в feature | catalog.api.ts |
Product, injection key | shared/model/ или shared/domain/ | product.ts |
| Unit-тест store | рядом с store | checkout.store.test.ts |
| E2E | корень app | e2e/ рядом с конфигом |
services vs shared (последний ручной шаг)
Вопрос: «Мог бы этот код жить в другом продукте без изменений смысла?»
- Да →
shared/(форматирование, UI-примитив, тип) - Нет →
services/(корзина, сессия, каталог API как продуктовый сценарий)
При сомнении оставьте размещение рядом в feature до второго потребителя. Подробнее — Services vs Shared.
Склейка модулей
Feature не импортирует feature. Варианты:
| Задача | Механизм |
|---|---|
| Callback / props | корень композиции передаёт между public/ entry |
| Событие без подписчиков | public/*.events.ts; подписка в app/ |
| Цикл / внешние начальные данные | public/ports.ts + привязка в app/ |
Разбор — Склейка модулей.
Что проверяют инструменты
| Ситуация | Правило |
|---|---|
| Импорт вверх по слоям | layer-direction |
features/a → features/b | feature-to-feature |
Импорт мимо public/ | public-api |
| Feature с входящими рёбрами | feature-has-inbound |
| Кандидат в shared/services | shared-candidate (doctor) — в help шаги Placement #2–3 по слою |
Полный разбор ошибок — dma check: справочник нарушений.