Derived Modular Arch
Гайды

Next.js

DMA на App Router: Server Components, Client boundary, providers.

Пример показывает, как DMA сочетается с RSC boundary Next.js App Router. Не полный запускаемый app в v1 — достаточно исходников для dma check и ESLint.

Исходники: examples/next-app.

Дерево

src/
├── app/
│   ├── layout.tsx       # Server — shell + <Providers>
│   ├── page.tsx         # Server — тонкий route
│   ├── providers.tsx    # Client — SessionProvider
│   └── globals.scss
├── features/
│   ├── catalog/         # catalog.store + public/catalog-page
│   ├── checkout/
│   ├── search/
│   └── profile.tsx      # stage 0
├── services/
│   ├── cart/public/
│   └── session/public/
└── shared/
    ├── domain/
    ├── lib/
    └── ui/

Направление: app/pages/routes → features → services → shared. Features не импортируют друг друга.

Server vs Client

Next.js по умолчанию — Server Components. DMA держит split на корень композиции vs feature public entries:

ФайлBoundaryПочему
layout.tsxServerHTML shell, без hooks
page.tsxServerМонтирует только */public/*
providers.tsxClient ("use client")Context требует client boundary
catalog-page.tsxClientonClick, store
button.tsxServer-safeБез "use client" — попадает в client bundle через feature
app/layout.tsx          (Server)
  └── app/providers.tsx (Client)
        └── app/page.tsx        (Server)
              ├── features/catalog/public/catalog-page.tsx   (Client)
              ├── features/search/public/search-page.tsx     (Client)
              └── features/profile.tsx                       (Client)

Паттерн: page.tsx остаётся тонким тонкий server-корень композиции. Интерактивность — в feature public/ entries. Не импортируйте *.store.ts из page.tsx.

Тонкий page.tsx

import { CatalogPage } from "@/features/catalog/public/catalog-page";
import { CheckoutPage } from "@/features/checkout/public/checkout-page";
import { Profile } from "@/features/profile";
import { SearchPage } from "@/features/search/public/search-page";
// ❌ никогда в app/
import { catalogStore } from "@/features/catalog/catalog.store";

Providers в app

Cross-cutting context (session) — в app/providers.tsx:

"use client";
import { SessionProvider } from "@/services/session/public/session-provider";

export function Providers({ children }) {
  return <SessionProvider>{children}</SessionProvider>;
}

profile.tsx читает useSession() — provider смонтирован в app, не в feature.

Services

МодульПотребителиРоль
cartcatalog, checkoutaddToCart, cartTotal
sessionprofile (через provider)SessionProvider

services/ появился, потому что два модуля импортируют cart — promotion из размещение рядом.

Стили

ВидГдеПример
Globalapp/globals.scssreset, shell
SCSS moduleрядом с public/ pagecatalog-page.module.scss
CSS moduleрядом с public/ pagecheckout-page.module.css

Глобальные стили — только из layout.tsx. Features не импортируют globals.

Инструменты

bun run dma-check   # dma check .
bun run lint        # eslint + compositionRoots: ["app", "pages", "routes"]
bun run test

ESLint ловит file-scoped ошибки (store из page.tsx). Полный граф — только dma check.

Что дальше

On this page