website refactor
This commit is contained in:
@@ -4,6 +4,12 @@ This document defines architecture guardrails that must be enforced via tests +
|
||||
|
||||
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
|
||||
|
||||
Purpose:
|
||||
|
||||
- Encode the architecture as *enforceable* rules.
|
||||
- Remove ambiguity and prevent drift.
|
||||
- Make it impossible for `page.tsx` and Templates to accumulate business logic.
|
||||
|
||||
## 1) RSC boundary guardrails
|
||||
|
||||
Fail CI if any `apps/website/app/**/page.tsx`:
|
||||
@@ -13,6 +19,29 @@ Fail CI if any `apps/website/app/**/page.tsx`:
|
||||
- calls `Intl.*` or `toLocale*`
|
||||
- performs sorting/filtering (`sort`, `filter`, `reduce`) beyond trivial null checks
|
||||
|
||||
Also fail CI if any `apps/website/app/**/page.tsx`:
|
||||
|
||||
- imports from `apps/website/lib/display-objects/**`
|
||||
- imports from `apps/website/lib/services/**` **that are not explicitly server-safe**
|
||||
- imports from `apps/website/lib/di/**` (server DI ban)
|
||||
- defines local helper functions other than trivial `assert*`/`invariant*` guards
|
||||
- contains `new SomeClass()` (object graph construction belongs in PageQueries)
|
||||
- contains any of these calls (directly or indirectly):
|
||||
- `ContainerManager.getInstance()`
|
||||
- `ContainerManager.getContainer()`
|
||||
|
||||
Filename rules (route module clarity):
|
||||
|
||||
- Only `page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`, `actions.ts` are allowed under `apps/website/app/**`.
|
||||
- Fail CI if any file under `apps/website/app/**` matches:
|
||||
- `*Template.tsx`
|
||||
- `*ViewModel.ts`
|
||||
- `*Presenter.ts`
|
||||
|
||||
Allowed exception:
|
||||
|
||||
- `apps/website/app/<route>/actions.ts` may call services and API clients (server-side), but it must not import ViewModels or Presenters.
|
||||
|
||||
## 2) Template purity guardrails
|
||||
|
||||
Fail CI if any `apps/website/templates/**`:
|
||||
@@ -22,12 +51,198 @@ Fail CI if any `apps/website/templates/**`:
|
||||
- imports from `apps/website/lib/display-objects/*`
|
||||
- calls `Intl.*` or `toLocale*`
|
||||
|
||||
Also fail CI if any Template:
|
||||
|
||||
- contains `useMemo`, `useEffect`, `useState`, `useReducer` (state belongs in `*PageClient.tsx` and components)
|
||||
- calls `.filter`, `.sort`, `.reduce` (derived computations must happen before ViewData reaches Templates)
|
||||
- imports from:
|
||||
- `apps/website/lib/page-queries/**`
|
||||
- `apps/website/lib/services/**`
|
||||
- `apps/website/lib/api/**`
|
||||
- `apps/website/lib/di/**`
|
||||
- `apps/website/lib/contracts/**`
|
||||
|
||||
Templates accept ViewData only.
|
||||
|
||||
Filename + signature rules:
|
||||
|
||||
- Template filenames must end with `Template.tsx`.
|
||||
- The first parameter type of a Template component must be `*ViewData` (or an object containing only `*ViewData` shapes).
|
||||
- Templates must not export helper functions.
|
||||
|
||||
## 3) Display Object guardrails
|
||||
|
||||
Fail CI if any `apps/website/lib/display-objects/**`:
|
||||
|
||||
- calls `Intl.*` or `toLocale*`
|
||||
|
||||
Also fail CI if any Display Object:
|
||||
|
||||
- imports from `apps/website/lib/api/**`, `apps/website/lib/services/**`, or `apps/website/lib/page-queries/**` (no IO)
|
||||
- imports from `apps/website/lib/view-models/**` (direction must be Presenter/ViewModel -> DisplayObject, not vice versa)
|
||||
- exports non-class members (Display Objects must be class-based)
|
||||
|
||||
Display Objects must be deterministic.
|
||||
|
||||
## 4) Page Query guardrails (server composition only)
|
||||
|
||||
Fail CI if any `apps/website/lib/page-queries/**`:
|
||||
|
||||
- imports from `apps/website/lib/view-models/**`
|
||||
- imports from `apps/website/lib/display-objects/**`
|
||||
- imports from `apps/website/lib/di/**` or references `ContainerManager`
|
||||
- calls `Intl.*` or `toLocale*`
|
||||
- calls `.sort`, `.filter`, `.reduce` (sorting/filtering belongs in API if canonical; otherwise client ViewModel)
|
||||
- returns `null` (must return `PageQueryResult` union)
|
||||
|
||||
Filename rules:
|
||||
|
||||
- PageQueries must be named `*PageQuery.ts`.
|
||||
- Page DTO types must be named `*PageDto` and live next to their PageQuery.
|
||||
|
||||
## 5) Services guardrails (DTO-only, server-safe)
|
||||
|
||||
Fail CI if any `apps/website/lib/services/**`:
|
||||
|
||||
- imports from `apps/website/lib/view-models/**` or `apps/website/templates/**`
|
||||
- imports from `apps/website/lib/display-objects/**`
|
||||
- stores state on `this` other than injected dependencies (services must be stateless)
|
||||
- uses blockers (blockers are client-only UX helpers)
|
||||
|
||||
Naming rules:
|
||||
|
||||
- Service methods returning API responses should use variable name `apiDto`.
|
||||
- Service methods returning Page DTO should use variable name `pageDto`.
|
||||
|
||||
## 6) Client-only guardrails (ViewModels, Presenters)
|
||||
|
||||
Fail CI if any file under `apps/website/lib/view-models/**`:
|
||||
|
||||
- lacks `'use client'` at top-level when it exports a ViewModel class intended for instantiation
|
||||
- imports from `apps/website/lib/page-queries/**` or `apps/website/app/**` (dependency direction violation)
|
||||
|
||||
Fail CI if any Presenter/ViewModel uses:
|
||||
|
||||
- HTTP calls (`fetch`, axios, API clients)
|
||||
|
||||
## 7) Write boundary guardrails (Server Actions only)
|
||||
|
||||
Fail CI if any client module (`'use client'` file or `apps/website/components/**`) performs HTTP writes:
|
||||
|
||||
- `fetch` with method `POST|PUT|PATCH|DELETE`
|
||||
|
||||
Fail CI if any server action (`apps/website/app/**/actions.ts`):
|
||||
|
||||
- imports from `apps/website/lib/view-models/**` or `apps/website/templates/**`
|
||||
- returns ViewModels (must return primitives / redirect / revalidate)
|
||||
|
||||
## 8) Model taxonomy guardrails (naming + type suffixes)
|
||||
|
||||
Problem being prevented:
|
||||
|
||||
- Calling everything “dto” collapses API Transport DTO, Page DTO, and ViewData.
|
||||
- This causes wrong-layer dependencies and makes reviews error-prone.
|
||||
|
||||
Fail CI if any file under `apps/website/**` contains a variable named exactly:
|
||||
|
||||
- `dto`
|
||||
|
||||
Allowed variable names (pick the right one):
|
||||
|
||||
- `apiDto` (API Transport DTO from OpenAPI / backend HTTP)
|
||||
- `pageDto` (Page DTO assembled by PageQueries)
|
||||
- `viewData` (Template input)
|
||||
- `commandDto` (write intent)
|
||||
|
||||
Type naming rules (CI should fail if violated):
|
||||
|
||||
1. Any PageQuery output type MUST end with `PageDto`.
|
||||
- Applies to types defined in `apps/website/lib/page-queries/**`.
|
||||
|
||||
2. Any Template prop type MUST end with `ViewData`.
|
||||
- Applies to types used by `apps/website/templates/**`.
|
||||
|
||||
3. API Transport DTO types may end with `DTO` (existing generated convention) or `ApiDto` (preferred for hand-written).
|
||||
|
||||
Module boundary reinforcement:
|
||||
|
||||
- `apps/website/templates/**` MUST NOT import API Transport DTO types directly.
|
||||
- Prefer: PageQuery emits `pageDto` → Presenter emits `viewData`.
|
||||
|
||||
## 9) Contracts enforcement (mandatory interfaces)
|
||||
|
||||
Purpose:
|
||||
|
||||
- Guardrails that rely on regex alone will always have loopholes.
|
||||
- Contracts make the compiler enforce architecture: code must implement the right shapes.
|
||||
|
||||
These contracts live under:
|
||||
|
||||
- `apps/website/lib/contracts/**`
|
||||
|
||||
### 9.1 Required contracts
|
||||
|
||||
Fail CI if any of these are missing:
|
||||
|
||||
1. PageQuery contract: `apps/website/lib/contracts/page-queries/PageQuery.ts`
|
||||
- Requires `execute(...) -> PageQueryResult<PageDto>`.
|
||||
|
||||
2. Service contract(s): `apps/website/lib/contracts/services/*`
|
||||
- Services return `ApiDto`/`PageDto` only.
|
||||
- No ViewModels.
|
||||
|
||||
3. Presenter contract: `apps/website/lib/contracts/presenters/Presenter.ts`
|
||||
- `present(input) -> output` (pure, deterministic).
|
||||
|
||||
4. ViewModel base: `apps/website/lib/contracts/view-models/ViewModel.ts`
|
||||
- ViewModels are client-only.
|
||||
- Must not expose a method that returns Page DTO or API DTO.
|
||||
|
||||
### 9.2 Enforcement rules
|
||||
|
||||
Fail CI if:
|
||||
|
||||
- Any file under `apps/website/lib/page-queries/**` defines a `class *PageQuery` that does NOT implement `PageQuery`.
|
||||
- Any file under `apps/website/lib/services/**` defines a `class *Service` that does NOT implement a Service contract.
|
||||
- Any file under `apps/website/lib/view-models/**` defines a `*Presenter` that does NOT implement `Presenter`.
|
||||
|
||||
Additionally:
|
||||
|
||||
- Fail if a PageQuery returns a shape that is not `PageQueryResult`.
|
||||
- Fail if a service method returns a `*ViewModel` type.
|
||||
|
||||
Note:
|
||||
|
||||
- Enforcement can be implemented as a boundary test that parses TypeScript files (or a regex-based approximation as a first step), but the source of truth is: contracts must exist and be implemented.
|
||||
|
||||
## 10) Generated DTO isolation (OpenAPI transport types do not reach UI)
|
||||
|
||||
Purpose:
|
||||
|
||||
- Generated OpenAPI DTOs are transport contracts.
|
||||
- UI must not depend on transport contracts directly.
|
||||
- Prevents “DTO soup” and forces the PageDto/ViewData boundary.
|
||||
|
||||
Fail CI if any of these import from `apps/website/lib/types/generated/**`:
|
||||
|
||||
- `apps/website/templates/**`
|
||||
- `apps/website/components/**`
|
||||
- `apps/website/hooks/**` and `apps/website/lib/hooks/**`
|
||||
|
||||
Fail CI if any Template imports from `apps/website/lib/types/**`.
|
||||
|
||||
Allowed locations for generated DTO imports:
|
||||
|
||||
- `apps/website/lib/api/**` (API clients)
|
||||
- `apps/website/lib/services/**` (transport orchestration)
|
||||
- `apps/website/lib/page-queries/**` (Page DTO assembly)
|
||||
|
||||
Enforced flow:
|
||||
|
||||
- Generated `*DTO` -> `apiDto` (API client/service)
|
||||
- `apiDto` -> `pageDto` (PageQuery)
|
||||
- `pageDto` -> `viewData` (Presenter)
|
||||
|
||||
Rationale:
|
||||
|
||||
- If the API contract changes, the blast radius stays in infrastructure + server composition, not in Templates.
|
||||
|
||||
Reference in New Issue
Block a user