feat: add docs/ folder (architecture, guides, ADRs)
This commit is contained in:
50
docs/architecture/dependency-flow.md
Normal file
50
docs/architecture/dependency-flow.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Dependency Flow
|
||||||
|
|
||||||
|
## Package Dependencies (one direction only)
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/web-next → @repo/api-client, @repo/ui
|
||||||
|
Startup: @repo/cms-core (config) + @repo/cms-client (init local)
|
||||||
|
apps/web-tanstack → @repo/api-client, @repo/ui
|
||||||
|
Startup: @repo/cms-core (config) + @repo/cms-client (init local)
|
||||||
|
apps/cms → @repo/cms-core, payload, next
|
||||||
|
apps/storybook → @repo/ui
|
||||||
|
|
||||||
|
@repo/api-client → @repo/api (router types only)
|
||||||
|
@repo/api → @repo/core/interface-adapters (controllers)
|
||||||
|
@repo/cms-core → @repo/core/application (use cases for hooks), payload (types)
|
||||||
|
@repo/cms-client → (standalone — receives Payload instance, doesn't import it)
|
||||||
|
@repo/ui → (standalone — tailwind, shadcn)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Internal Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
core/entities → (standalone — zero deps)
|
||||||
|
core/application → core/entities only
|
||||||
|
core/interface-adapters → core/application, core/entities
|
||||||
|
core/infrastructure → core/application, core/entities, @repo/cms-client, external libs
|
||||||
|
core/di → all internal layers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Circular Dependency Prevention — HARD RULES
|
||||||
|
|
||||||
|
- **NEVER:** packages/core → apps/*
|
||||||
|
- **NEVER:** apps/cms → packages/core/infrastructure
|
||||||
|
- **NEVER:** packages/cms-client → apps/cms or packages/core or packages/cms-core
|
||||||
|
- **NEVER:** packages/cms-core → packages/cms-client
|
||||||
|
- **NEVER:** core/entities → anything
|
||||||
|
- **NEVER:** core/application → core/infrastructure
|
||||||
|
|
||||||
|
## Why These Rules Exist
|
||||||
|
|
||||||
|
The Payload CMS integration creates a potential circular dependency:
|
||||||
|
```
|
||||||
|
apps/cms hooks → @repo/core/application (use cases)
|
||||||
|
@repo/core/infrastructure → @repo/cms-client → Payload API
|
||||||
|
```
|
||||||
|
|
||||||
|
This is resolved by:
|
||||||
|
1. `cms-client` is standalone — receives Payload instance via injection, never imports it
|
||||||
|
2. `cms-core` hooks only import from `core/application`, never `core/infrastructure`
|
||||||
|
3. App startup code (not a shared package) wires Payload instance into cms-client
|
||||||
55
docs/architecture/overview.md
Normal file
55
docs/architecture/overview.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Architecture Overview
|
||||||
|
|
||||||
|
## Clean Architecture Monorepo
|
||||||
|
|
||||||
|
This template implements Uncle Bob's Clean Architecture in a Turborepo + pnpm monorepo. The core principle: **dependencies point inward only**.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ Frameworks & Drivers (outermost) │
|
||||||
|
│ Next.js, TanStack Start, Payload CMS, │
|
||||||
|
│ Storybook, PostgreSQL, Docker │
|
||||||
|
│ ┌─────────────────────────────────────────┐ │
|
||||||
|
│ │ Interface Adapters │ │
|
||||||
|
│ │ tRPC routers, Controllers, Presenters │ │
|
||||||
|
│ │ ┌─────────────────────────────────┐ │ │
|
||||||
|
│ │ │ Application (Use Cases) │ │ │
|
||||||
|
│ │ │ Business logic, interfaces │ │ │
|
||||||
|
│ │ │ ┌─────────────────────────┐ │ │ │
|
||||||
|
│ │ │ │ Entities (innermost) │ │ │ │
|
||||||
|
│ │ │ │ Models, Errors, Zod │ │ │ │
|
||||||
|
│ │ │ └─────────────────────────┘ │ │ │
|
||||||
|
│ │ └─────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Map
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/core → Clean architecture (entities, use cases, infra, DI)
|
||||||
|
packages/api → tRPC routers (calls core controllers)
|
||||||
|
packages/api-client → Shared React Query hooks + provider
|
||||||
|
packages/cms-core → Payload CMS config, collections, hooks
|
||||||
|
packages/cms-client → Dual-mode Payload client (local + HTTP)
|
||||||
|
packages/ui → Atomic Design + shadcn/ui + Tailwind v4
|
||||||
|
|
||||||
|
apps/web-next → Next.js 15 reference app
|
||||||
|
apps/web-tanstack → TanStack Start reference app
|
||||||
|
apps/cms → Thin Next.js shell for Payload admin
|
||||||
|
apps/storybook → Centralized Storybook instance
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
UI → useQuery(trpc.content.list) → tRPC Router → Controller → Use Case → Repository → Payload Local API → PostgreSQL
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
- **InversifyJS DI** — symbol-based resolution, documented in di/AGENTS.md
|
||||||
|
- **Dual-mode CMS client** — Local API for server-side (no HTTP overhead), HTTP for external
|
||||||
|
- **Atomic Design** — atoms/molecules/organisms/templates, co-located stories
|
||||||
|
- **tRPC single data path** — all data (including CMS content) flows through tRPC
|
||||||
|
- **Agent-optimized docs** — AGENTS.md at every level with rules, recipes, tables
|
||||||
19
docs/decisions/adr-001-monorepo-tool.md
Normal file
19
docs/decisions/adr-001-monorepo-tool.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# ADR-001: Turborepo + pnpm for Monorepo
|
||||||
|
|
||||||
|
## Status: Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Need a monorepo tool for a clean architecture template with multiple apps and shared packages.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Turborepo + pnpm workspaces.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
- Clean architecture already provides organizational structure — Nx's opinions would compete
|
||||||
|
- Minimal config (turbo.json + pnpm-workspace.yaml) means agents understand the setup quickly
|
||||||
|
- Excellent caching — shared packages build once and are reused
|
||||||
|
- Battle-tested pairing, both reference repos use it
|
||||||
|
- Works naturally with Vite for non-Next packages
|
||||||
26
docs/decisions/adr-002-di-framework.md
Normal file
26
docs/decisions/adr-002-di-framework.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# ADR-002: InversifyJS for Dependency Injection
|
||||||
|
|
||||||
|
## Status: Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Need DI for clean architecture. Options: InversifyJS, tsyringe, manual composition root.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
InversifyJS with symbol-based resolution + targeted agent documentation.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
- Scales to 20+ services with automatic dependency chain resolution
|
||||||
|
- Built-in singleton/transient/request scopes
|
||||||
|
- Middleware support for logging/tracing (Sentry integration)
|
||||||
|
- Matches the reference implementation by Lazar Nikolov
|
||||||
|
- Agent readability gap (4/10 → 7/10) mitigated by resolution tables and step-by-step recipes in di/AGENTS.md
|
||||||
|
- Familiar to developers from Java/C# backgrounds
|
||||||
|
|
||||||
|
## Trade-offs
|
||||||
|
|
||||||
|
- Requires reflect-metadata + decorator config in tsconfig
|
||||||
|
- Symbol indirection harder to trace than plain functions
|
||||||
|
- Extra dependency (inversify + reflect-metadata)
|
||||||
27
docs/decisions/adr-003-cms-separation.md
Normal file
27
docs/decisions/adr-003-cms-separation.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# ADR-003: CMS Core + CMS Client Separation
|
||||||
|
|
||||||
|
## Status: Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Payload CMS needs to integrate with clean architecture without creating circular dependencies.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Three packages: `@repo/cms-core` (config + collections), `@repo/cms-client` (standalone typed client), `apps/cms` (thin shell).
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
- `cms-core` is testable independently — collections are just config objects
|
||||||
|
- `cms-client` is standalone (no internal imports) — prevents circular deps
|
||||||
|
- `apps/cms` is almost empty — just boots Next.js with cms-core's config
|
||||||
|
- Payload instance injected into cms-client, not imported — app startup code wires them
|
||||||
|
|
||||||
|
## Circular Dependency Prevention
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/cms → @repo/cms-core → @repo/core/application (hooks)
|
||||||
|
@repo/core/infrastructure → @repo/cms-client (standalone)
|
||||||
|
```
|
||||||
|
|
||||||
|
No cycles because cms-client never imports from cms-core or core.
|
||||||
18
docs/decisions/adr-004-dual-mode-client.md
Normal file
18
docs/decisions/adr-004-dual-mode-client.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# ADR-004: Dual-Mode Payload Client (Local + HTTP)
|
||||||
|
|
||||||
|
## Status: Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Apps need to access Payload CMS data. Payload 3.x offers both Local API (direct) and REST API (HTTP).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
`@repo/cms-client` supports both modes via `createPayloadClient()`.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
- **Local mode (primary):** Direct Payload instance access — no HTTP overhead, full query capabilities (where, sort, limit, depth, page, populate). All server-side apps use this.
|
||||||
|
- **HTTP mode (fallback):** REST API for external consumers without access to a Payload process.
|
||||||
|
- Payload instance is injected at app startup, not imported — keeps cms-client standalone.
|
||||||
|
- Both modes share the same `PayloadClient` interface — consumers don't know which mode is active.
|
||||||
20
docs/decisions/adr-005-atomic-design.md
Normal file
20
docs/decisions/adr-005-atomic-design.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# ADR-005: Atomic Design for UI Components
|
||||||
|
|
||||||
|
## Status: Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Need a scalable component architecture for the shared UI package.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Atomic Design (atoms/molecules/organisms/templates) + shadcn/ui + Storybook.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
- Clear hierarchy makes agents know exactly where to place new components
|
||||||
|
- Import rules enforce composition direction (atoms never import molecules)
|
||||||
|
- Co-located stories make components self-documenting
|
||||||
|
- shadcn/ui provides excellent base atoms that map naturally to atomic levels
|
||||||
|
- Storybook sidebar mirrors the hierarchy via story titles
|
||||||
|
- Pages live in apps (not UI package) — they connect to real data
|
||||||
70
docs/guides/adding-a-feature.md
Normal file
70
docs/guides/adding-a-feature.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Adding a New Feature — End-to-End Guide
|
||||||
|
|
||||||
|
This guide walks through adding a complete feature from entity to UI.
|
||||||
|
|
||||||
|
## Example: Adding a "Comments" feature
|
||||||
|
|
||||||
|
### 1. Define Entity
|
||||||
|
|
||||||
|
Create `packages/core/src/entities/models/comment.ts`:
|
||||||
|
```typescript
|
||||||
|
import { z } from "zod";
|
||||||
|
export const commentSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
content: z.string().min(1),
|
||||||
|
articleId: z.string(),
|
||||||
|
authorId: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
});
|
||||||
|
export type Comment = z.infer<typeof commentSchema>;
|
||||||
|
```
|
||||||
|
Export from `entities/models/index.ts`.
|
||||||
|
|
||||||
|
### 2. Define Repository Interface
|
||||||
|
|
||||||
|
Create `packages/core/src/application/repositories/comments.repository.interface.ts`:
|
||||||
|
```typescript
|
||||||
|
import type { Comment } from "@/entities/models/comment.js";
|
||||||
|
export interface ICommentsRepository {
|
||||||
|
getCommentsByArticle(articleId: string): Promise<Comment[]>;
|
||||||
|
createComment(input: Comment): Promise<Comment>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Export from `application/repositories/index.ts`.
|
||||||
|
|
||||||
|
### 3. Write Use Case (TDD)
|
||||||
|
|
||||||
|
Write test first in `packages/core/tests/unit/use-cases/content/create-comment.use-case.test.ts`, then implement in `packages/core/src/application/use-cases/content/create-comment.use-case.ts`.
|
||||||
|
|
||||||
|
### 4. Write Controller
|
||||||
|
|
||||||
|
Create `packages/core/src/interface-adapters/controllers/content/comments.controller.ts` — validates input with Zod, calls use case.
|
||||||
|
|
||||||
|
### 5. Write Mock Implementation
|
||||||
|
|
||||||
|
Create `packages/core/src/infrastructure/repositories/mock-comments.repository.ts` with `@injectable()`.
|
||||||
|
|
||||||
|
### 6. Register in DI
|
||||||
|
|
||||||
|
Add `ICommentsRepository` symbol to `di/types.ts`, create binding in `di/modules/content.module.ts`, load in `di/container.ts`.
|
||||||
|
|
||||||
|
### 7. Add tRPC Router
|
||||||
|
|
||||||
|
Create `packages/api/src/router/comments.router.ts`, add to `appRouter` in `router/index.ts`.
|
||||||
|
|
||||||
|
### 8. (Optional) Add Payload Collection
|
||||||
|
|
||||||
|
If comments are stored in Payload CMS, create `packages/cms-core/src/collections/comments/`.
|
||||||
|
|
||||||
|
### 9. Build UI
|
||||||
|
|
||||||
|
Create component in `packages/ui/src/organisms/comment-list/` with co-located `.stories.tsx`.
|
||||||
|
|
||||||
|
### 10. Wire in App
|
||||||
|
|
||||||
|
Use `useTRPC()` in app pages to fetch and display comments.
|
||||||
|
|
||||||
|
### 11. Write Tests
|
||||||
|
|
||||||
|
- Unit: use case + controller tests in `packages/core/tests/`
|
||||||
|
- E2E: Playwright test in `tests/e2e/`
|
||||||
41
docs/guides/testing-strategy.md
Normal file
41
docs/guides/testing-strategy.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Testing Strategy
|
||||||
|
|
||||||
|
## Test Layers
|
||||||
|
|
||||||
|
| Layer | Tool | What to Test |
|
||||||
|
|---|---|---|
|
||||||
|
| Entities | Vitest (unit) | Zod schema validation, error classes |
|
||||||
|
| Use Cases | Vitest (unit) | Business logic with mock implementations via DI |
|
||||||
|
| Controllers | Vitest (unit) | Input validation, use case delegation, error mapping |
|
||||||
|
| Infrastructure | Vitest (integration) | Real DB via test containers, Payload API calls |
|
||||||
|
| UI Components | Vitest + Storybook | Rendering, props, accessibility |
|
||||||
|
| Full App | Playwright (E2E) | User flows across both Next.js and TanStack Start |
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test # All tests via Turborepo
|
||||||
|
cd packages/core && pnpm vitest run # Core unit tests only
|
||||||
|
cd packages/core && pnpm vitest watch # Core tests in watch mode
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Pattern (DI Container)
|
||||||
|
|
||||||
|
All tests that use the DI container must initialize and destroy it:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import "reflect-metadata";
|
||||||
|
import { beforeEach, afterEach } from "vitest";
|
||||||
|
import { initializeContainer, destroyContainer } from "@/di/container.js";
|
||||||
|
|
||||||
|
beforeEach(() => { initializeContainer(); });
|
||||||
|
afterEach(() => { destroyContainer(); });
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures each test gets fresh mock instances (singleton scope resets).
|
||||||
|
|
||||||
|
## Test File Location
|
||||||
|
|
||||||
|
- Core tests: `packages/core/tests/unit/{use-cases,controllers}/{domain}/`
|
||||||
|
- UI tests: co-located next to component (`*.test.tsx`)
|
||||||
|
- E2E tests: `tests/e2e/`
|
||||||
Reference in New Issue
Block a user