- CLAUDE.md Key Conventions: 'App bootstrap' rule rewritten as 'Three binding modes per feature' — describes USE_DEV_SEED + NODE_ENV resolution order and the new ./di/bind-dev-seed export. - AGENTS.md (root): exports list now mentions ./ui + ./di/bind-dev-seed; Per-feature public-API surface table gains a row; Apps section shows the bindAll() dispatcher with three-rule logic. - docs/architecture/vertical-feature-spec.md §6: file shape now includes bind-dev-seed.ts, bind-dev-seed.test.ts, __seeds__/dev.ts; package.json exports list updated to include ./di/bind-dev-seed. - docs/architecture/data-flow-explainer.html: anatomy tree gains __seeds__/ row; LAYERS.di description updated with new binders + cross-link to di-explainer.html; new LAYERS.seeds entry; public- surface card expanded to six subpaths. - docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md §7: new 'Post-Plan-9: dev-seed binders' entry summarizing the rollout (commits, per-feature additions, app wiring, tests, turbo, docs). - bind-production.test.ts: dispatcher tests use vi.stubEnv (typesafe way to test process.env in TypeScript 5+ with @types/node read-only process.env types). 4 dispatcher tests + 2 bindAllProduction tests = 7 tests total.
69 lines
5.7 KiB
Markdown
69 lines
5.7 KiB
Markdown
# Clean Architecture Monorepo Template
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
pnpm install # Install all dependencies
|
|
pnpm dev # Start all dev servers
|
|
pnpm build # Build all packages
|
|
pnpm test # Run all tests
|
|
pnpm turbo boundaries # Validate workspace dependency graph
|
|
docker compose up -d # Start PostgreSQL
|
|
```
|
|
|
|
## TDD
|
|
|
|
```bash
|
|
pnpm test --watch --filter @repo/<feature> # watch one feature
|
|
pnpm test -- --coverage # full run with coverage
|
|
pnpm test:stories # Storybook smoke tests
|
|
pnpm test:e2e # Playwright e2e
|
|
```
|
|
|
|
See `docs/guides/tdd-workflow.md` for the full cycle.
|
|
|
|
## Project Overview
|
|
|
|
Turborepo + pnpm monorepo organized by vertical features. Each feature (`auth`, `blog`, `media`, `marketing-pages`, `navigation`) owns its Clean Architecture layers. Core packages (`core-shared`, `core-cms`, `core-api`, `core-trpc`, `core-ui`) provide foundation. Two tooling packages (`core-eslint`, `core-typescript`) provide shared configs. Workspace boundaries are enforced by ESLint (lint-time) and Turborepo (build-graph time). Supports Next.js and TanStack Start as frontend frameworks, Payload CMS for content management, and comprehensive agent-optimized documentation.
|
|
|
|
## Read First
|
|
|
|
- `AGENTS.md` — Package map, boundary rules, per-package conventions
|
|
- `docs/architecture/overview.md` — High-level architecture and package responsibilities
|
|
- `docs/architecture/vertical-feature-spec.md` — Design spec with rationale and decision log
|
|
- `docs/guides/adding-a-feature.md` — End-to-end new feature walkthrough
|
|
|
|
## Key Conventions
|
|
|
|
- **Relative imports in `src/`** — Source files use relative paths (`../repositories/...`), not `@/` alias
|
|
- **`@/` alias in tests** — Test files (`*.test.ts`) use `@/` to import from `src/`
|
|
- **`vitest.config.ts`** — Every package must define `resolve.alias: { "@": path.resolve(__dirname, "./src") }`
|
|
- **`tsconfig.json` rootDir** — Set `"rootDir": "."` so TypeScript finds both `src/` and test files
|
|
- **Lazar-conformant file layout** — Entities live at `entities/models/<x>.ts`; errors at `entities/errors/<domain>.ts` + `entities/errors/common.ts`; mock siblings use the `.mock.ts` suffix (`<x>.repository.mock.ts`); real repository impls drop the `payload-` prefix (`<x>.repository.ts`); interface filenames are dot-separated (`<x>.repository.interface.ts`)
|
|
- **Factory-function use cases & controllers** — Every use case and controller is `(deps) => async (input) => result`; each exports `export type I*UseCase = ReturnType<typeof xUseCase>` (and the analogous `I*Controller`); one controller per use case (no multi-method controllers)
|
|
- **DI uses `.toDynamicValue()` for factories** — `bind<IXUseCase>(SYMBOL).toDynamicValue((ctx) => xUseCase(ctx.container.get(...)))`; mocks remain the default binding
|
|
- **Tests inject mocks directly** — Construct `MockXRepository` and pass into the factory: `signInUseCase(mockUsers, mockAuth)(input)`. No container rebinding in unit tests
|
|
- **Schemas in the use-case file** — Every use case exports `xInputSchema` (a `z.ZodObject` with `.strict()`; `z.object({}).strict()` for void inputs) and, for non-void use cases, `xOutputSchema`. Types: `XInput = z.infer<typeof xInputSchema>` and `XOutput`. Use case body ends with `xOutputSchema.parse(result)` before returning (runtime guarantee against malformed repository data)
|
|
- **Controllers receive `unknown` + presenter** — Controllers `safeParse(xInputSchema)` from the use-case file and throw `InputParseError` on failure. Non-void controllers define a top-level `function presenter(value: XOutput)` and return `Promise<ReturnType<typeof presenter>>` (identity is fine — `return value`); void controllers return `Promise<void>` with no presenter
|
|
- **Feature-scoped tRPC error mapping** — Each feature has `integrations/api/procedures.ts` exporting `xProcedure = t.procedure.use(defineErrorMiddleware([[Ctor, "TRPC_CODE"], ...]))` from `@repo/core-shared/trpc/define-error-middleware`. Routers use `xProcedure.input(xInputSchema)` — schemas are imported from the use-case file, never redefined inline. `core-shared` never enumerates feature error classes
|
|
- **Public surface split** — Feature root (`.`) exports contracts only: types, errors, schemas, IUseCase / IController aliases, router type, constants. UI artifacts (query builders, components) live behind `./ui` (`src/ui/index.ts`). Apps import queries from `@repo/<feature>/ui`, schemas/types from `@repo/<feature>`
|
|
- **Payload repositories via constructor** — Feature packages receive Payload config at constructor time, not as a direct dependency
|
|
- **Three binding modes per feature** — Each feature exports two binders: `./di/bind-production` (real Payload) and `./di/bind-dev-seed` (populated mock). The app's `bindAll()` dispatcher in `apps/web-next/src/server/bind-production.ts` picks one by env: `USE_DEV_SEED="true"` → dev seed; `NODE_ENV="production"` → production; otherwise → dev seed (developer default so `pnpm dev` boots without Payload). Dev seed lives in `src/__seeds__/dev.ts` as a lazy `buildDev<Entities>()` function that uses the feature's existing factory
|
|
- **App bootstrap** — Each app calls `bindAll()` from a server entry point (page server component, route handler) before resolving any feature controller. The dispatcher is idempotent
|
|
|
|
## MCP Servers
|
|
|
|
Start Storybook before UI work: `pnpm dev --filter @repo/storybook`
|
|
|
|
Storybook MCP available at `http://localhost:6006/mcp` — use `list-all-documentation` to discover existing components before creating new ones.
|
|
|
|
## Key Ports
|
|
|
|
| Service | Port |
|
|
|---|---|
|
|
| Next.js | 3000 |
|
|
| Payload CMS | 3001 |
|
|
| TanStack Start | 3002 |
|
|
| PostgreSQL | 5432 |
|
|
| Storybook | 6006 |
|