docs(plans): delete six stale 2026-04-06 plan docs (superseded by 2026-05-04-plan-{1..6})
This commit is contained in:
@@ -1,965 +0,0 @@
|
||||
# Plan 1: Monorepo Foundation — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Scaffold a Turborepo + pnpm monorepo with shared TypeScript and ESLint configs, placeholder packages for all planned workspaces, and Docker Compose for local development.
|
||||
|
||||
**Architecture:** Turborepo orchestrates builds across pnpm workspaces. Shared config packages (`@repo/typescript-config`, `@repo/eslint-config`) provide consistent tooling. All apps and packages are created as empty placeholders with correct `package.json` files so the workspace graph is valid from the start. Docker Compose provides PostgreSQL for local development.
|
||||
|
||||
**Tech Stack:** Turborepo 2.x, pnpm 9.x, TypeScript 5.x, ESLint 9.x (flat config), Vitest, Docker Compose, PostgreSQL 16
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `package.json` | Root workspace manifest, delegates to turbo |
|
||||
| `pnpm-workspace.yaml` | Declares workspace packages |
|
||||
| `turbo.json` | Task pipeline (build, dev, lint, test, typecheck) |
|
||||
| `.npmrc` | pnpm workspace settings |
|
||||
| `.gitignore` | Ignore patterns for Turborepo + pnpm + Node.js |
|
||||
| `packages/typescript-config/package.json` | Shared TS config package manifest |
|
||||
| `packages/typescript-config/base.json` | Base TypeScript config |
|
||||
| `packages/typescript-config/nextjs.json` | Next.js TypeScript config |
|
||||
| `packages/typescript-config/react-library.json` | React library TypeScript config |
|
||||
| `packages/eslint-config/package.json` | Shared ESLint config package manifest |
|
||||
| `packages/eslint-config/base.js` | Base ESLint flat config |
|
||||
| `packages/eslint-config/next.js` | Next.js ESLint config |
|
||||
| `packages/eslint-config/react-internal.js` | React library ESLint config |
|
||||
| `packages/core/package.json` | Placeholder — clean architecture core |
|
||||
| `packages/core/tsconfig.json` | Extends @repo/typescript-config/base |
|
||||
| `packages/api/package.json` | Placeholder — tRPC routers |
|
||||
| `packages/api/tsconfig.json` | Extends @repo/typescript-config/base |
|
||||
| `packages/api-client/package.json` | Placeholder — React Query hooks |
|
||||
| `packages/api-client/tsconfig.json` | Extends @repo/typescript-config/react-library |
|
||||
| `packages/cms-core/package.json` | Placeholder — Payload CMS definition |
|
||||
| `packages/cms-core/tsconfig.json` | Extends @repo/typescript-config/base |
|
||||
| `packages/cms-client/package.json` | Placeholder — Dual-mode Payload client |
|
||||
| `packages/cms-client/tsconfig.json` | Extends @repo/typescript-config/base |
|
||||
| `packages/ui/package.json` | Placeholder — shadcn/ui + Atomic Design |
|
||||
| `packages/ui/tsconfig.json` | Extends @repo/typescript-config/react-library |
|
||||
| `apps/web-next/package.json` | Placeholder — Next.js reference app |
|
||||
| `apps/web-next/tsconfig.json` | Extends @repo/typescript-config/nextjs |
|
||||
| `apps/web-tanstack/package.json` | Placeholder — TanStack Start reference app |
|
||||
| `apps/web-tanstack/tsconfig.json` | Extends @repo/typescript-config/base |
|
||||
| `apps/cms/package.json` | Placeholder — Payload admin shell |
|
||||
| `apps/cms/tsconfig.json` | Extends @repo/typescript-config/nextjs |
|
||||
| `apps/storybook/package.json` | Placeholder — Storybook instance |
|
||||
| `apps/storybook/tsconfig.json` | Extends @repo/typescript-config/react-library |
|
||||
| `docker-compose.yml` | PostgreSQL service for local dev |
|
||||
| `.env.example` | Environment variable template |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Root workspace files
|
||||
|
||||
**Files:**
|
||||
- Create: `package.json`
|
||||
- Create: `pnpm-workspace.yaml`
|
||||
- Create: `turbo.json`
|
||||
- Create: `.npmrc`
|
||||
- Create: `.gitignore`
|
||||
- Create: `.env.example`
|
||||
|
||||
- [ ] **Step 1: Create root package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "template",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"test": "turbo run test",
|
||||
"typecheck": "turbo run typecheck",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.0",
|
||||
"turbo": "^2.4.0",
|
||||
"typescript": "^5.8.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create pnpm-workspace.yaml**
|
||||
|
||||
```yaml
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create turbo.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://turborepo.dev/schema.json",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^lint"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"typecheck": {
|
||||
"dependsOn": ["^typecheck"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create .npmrc**
|
||||
|
||||
```
|
||||
auto-install-peers=true
|
||||
enable-pre-post-scripts=true
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Create .gitignore**
|
||||
|
||||
```
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
build
|
||||
.next
|
||||
out
|
||||
storybook-static
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Superpowers brainstorm sessions
|
||||
.superpowers/
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Create .env.example**
|
||||
|
||||
```
|
||||
# Database
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/template
|
||||
|
||||
# Payload CMS
|
||||
PAYLOAD_SECRET=your-secret-here
|
||||
|
||||
# App URLs
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
CMS_URL=http://localhost:3001
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add package.json pnpm-workspace.yaml turbo.json .npmrc .gitignore .env.example
|
||||
git commit -m "feat: scaffold root workspace files (Turborepo + pnpm)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Shared TypeScript config package
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/typescript-config/package.json`
|
||||
- Create: `packages/typescript-config/base.json`
|
||||
- Create: `packages/typescript-config/nextjs.json`
|
||||
- Create: `packages/typescript-config/react-library.json`
|
||||
|
||||
- [ ] **Step 1: Create package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/typescript-config",
|
||||
"private": true,
|
||||
"version": "0.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create base.json**
|
||||
|
||||
This is the base TypeScript config used by all packages. Includes `experimentalDecorators` and `emitDecoratorMetadata` required by InversifyJS.
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create nextjs.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"jsx": "preserve",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create react-library.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/typescript-config/
|
||||
git commit -m "feat: add shared TypeScript config package (@repo/typescript-config)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Shared ESLint config package
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/eslint-config/package.json`
|
||||
- Create: `packages/eslint-config/base.js`
|
||||
- Create: `packages/eslint-config/next.js`
|
||||
- Create: `packages/eslint-config/react-internal.js`
|
||||
|
||||
- [ ] **Step 1: Create package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/eslint-config",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./base": "./base.js",
|
||||
"./next": "./next.js",
|
||||
"./react-internal": "./react-internal.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.20.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
||||
"@typescript-eslint/parser": "^8.25.0",
|
||||
"eslint": "^9.20.0",
|
||||
"eslint-config-prettier": "^10.1.0",
|
||||
"eslint-plugin-turbo": "^2.4.0",
|
||||
"typescript-eslint": "^8.25.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create base.js**
|
||||
|
||||
```javascript
|
||||
import js from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import tseslint from "typescript-eslint";
|
||||
import turboPlugin from "eslint-plugin-turbo";
|
||||
|
||||
export default [
|
||||
{ ignores: ["dist/**", "node_modules/**"] },
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
plugins: { turbo: turboPlugin },
|
||||
rules: {
|
||||
"turbo/no-undeclared-env-vars": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create next.js**
|
||||
|
||||
```javascript
|
||||
import baseConfig from "./base.js";
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
{ ignores: [".next/**", "out/**"] },
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create react-internal.js**
|
||||
|
||||
```javascript
|
||||
import baseConfig from "./base.js";
|
||||
|
||||
export default [...baseConfig];
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/eslint-config/
|
||||
git commit -m "feat: add shared ESLint config package (@repo/eslint-config)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Placeholder packages (core, api, api-client, cms-core, cms-client, ui)
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/core/package.json`
|
||||
- Create: `packages/core/tsconfig.json`
|
||||
- Create: `packages/core/src/index.ts`
|
||||
- Create: `packages/api/package.json`
|
||||
- Create: `packages/api/tsconfig.json`
|
||||
- Create: `packages/api/src/index.ts`
|
||||
- Create: `packages/api-client/package.json`
|
||||
- Create: `packages/api-client/tsconfig.json`
|
||||
- Create: `packages/api-client/src/index.ts`
|
||||
- Create: `packages/cms-core/package.json`
|
||||
- Create: `packages/cms-core/tsconfig.json`
|
||||
- Create: `packages/cms-core/src/index.ts`
|
||||
- Create: `packages/cms-client/package.json`
|
||||
- Create: `packages/cms-client/tsconfig.json`
|
||||
- Create: `packages/cms-client/src/index.ts`
|
||||
- Create: `packages/ui/package.json`
|
||||
- Create: `packages/ui/tsconfig.json`
|
||||
- Create: `packages/ui/src/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create packages/core/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/core",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create packages/core/tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["reflect-metadata"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create packages/core/src/index.ts**
|
||||
|
||||
```typescript
|
||||
// @repo/core — Clean Architecture core package
|
||||
// Layers: entities, application, infrastructure, interface-adapters, di
|
||||
export {};
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create packages/api/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/api",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Create packages/api/tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Create packages/api/src/index.ts**
|
||||
|
||||
```typescript
|
||||
// @repo/api — tRPC router definitions
|
||||
export {};
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Create packages/api-client/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/api-client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Create packages/api-client/tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/react-library.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Create packages/api-client/src/index.ts**
|
||||
|
||||
```typescript
|
||||
// @repo/api-client — Shared React Query hooks
|
||||
export {};
|
||||
```
|
||||
|
||||
- [ ] **Step 10: Create packages/cms-core/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/cms-core",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 11: Create packages/cms-core/tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 12: Create packages/cms-core/src/index.ts**
|
||||
|
||||
```typescript
|
||||
// @repo/cms-core — Payload CMS config, collections, hooks, globals
|
||||
export {};
|
||||
```
|
||||
|
||||
- [ ] **Step 13: Create packages/cms-client/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/cms-client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 14: Create packages/cms-client/tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 15: Create packages/cms-client/src/index.ts**
|
||||
|
||||
```typescript
|
||||
// @repo/cms-client — Dual-mode Payload client (local + HTTP)
|
||||
export {};
|
||||
```
|
||||
|
||||
- [ ] **Step 16: Create packages/ui/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/ui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 17: Create packages/ui/tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/react-library.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 18: Create packages/ui/src/index.ts**
|
||||
|
||||
```typescript
|
||||
// @repo/ui — shadcn/ui + Atomic Design component library
|
||||
export {};
|
||||
```
|
||||
|
||||
- [ ] **Step 19: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/core/ packages/api/ packages/api-client/ packages/cms-core/ packages/cms-client/ packages/ui/
|
||||
git commit -m "feat: add placeholder packages (core, api, api-client, cms-core, cms-client, ui)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Placeholder apps (web-next, web-tanstack, cms, storybook)
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/web-next/package.json`
|
||||
- Create: `apps/web-next/tsconfig.json`
|
||||
- Create: `apps/web-tanstack/package.json`
|
||||
- Create: `apps/web-tanstack/tsconfig.json`
|
||||
- Create: `apps/cms/package.json`
|
||||
- Create: `apps/cms/tsconfig.json`
|
||||
- Create: `apps/storybook/package.json`
|
||||
- Create: `apps/storybook/tsconfig.json`
|
||||
|
||||
- [ ] **Step 1: Create apps/web-next/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/web-next",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "echo 'placeholder'",
|
||||
"dev": "echo 'placeholder'",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/api-client": "workspace:*",
|
||||
"@repo/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create apps/web-next/tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "next-env.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create apps/web-tanstack/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/web-tanstack",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "echo 'placeholder'",
|
||||
"dev": "echo 'placeholder'",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/api-client": "workspace:*",
|
||||
"@repo/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create apps/web-tanstack/tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Create apps/cms/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/cms",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "echo 'placeholder'",
|
||||
"dev": "echo 'placeholder'",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/cms-core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Create apps/cms/tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "next-env.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Create apps/storybook/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/storybook",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "echo 'placeholder'",
|
||||
"dev": "echo 'placeholder'",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Create apps/storybook/tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/react-library.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/
|
||||
git commit -m "feat: add placeholder apps (web-next, web-tanstack, cms, storybook)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Docker Compose
|
||||
|
||||
**Files:**
|
||||
- Create: `docker-compose.yml`
|
||||
|
||||
- [ ] **Step 1: Create docker-compose.yml**
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: template
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add docker-compose.yml
|
||||
git commit -m "feat: add Docker Compose with PostgreSQL for local dev"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Install dependencies and verify workspace
|
||||
|
||||
- [ ] **Step 1: Install pnpm if not available**
|
||||
|
||||
Run: `corepack enable && corepack prepare pnpm@9.15.4 --activate`
|
||||
Expected: pnpm is available
|
||||
|
||||
- [ ] **Step 2: Run pnpm install**
|
||||
|
||||
Run: `pnpm install`
|
||||
Expected: Installs all workspace dependencies, creates `pnpm-lock.yaml`, no errors.
|
||||
|
||||
- [ ] **Step 3: Verify Turborepo sees all workspaces**
|
||||
|
||||
Run: `pnpm turbo run build --dry`
|
||||
Expected: Output lists all 10 packages/apps:
|
||||
- `@repo/typescript-config`
|
||||
- `@repo/eslint-config`
|
||||
- `@repo/core`
|
||||
- `@repo/api`
|
||||
- `@repo/api-client`
|
||||
- `@repo/cms-core`
|
||||
- `@repo/cms-client`
|
||||
- `@repo/ui`
|
||||
- `@repo/web-next`
|
||||
- `@repo/web-tanstack`
|
||||
- `@repo/cms`
|
||||
- `@repo/storybook`
|
||||
|
||||
- [ ] **Step 4: Run turbo build**
|
||||
|
||||
Run: `pnpm build`
|
||||
Expected: All workspaces build successfully (placeholder builds echo 'placeholder' or tsc --noEmit with no errors on empty src/index.ts).
|
||||
|
||||
- [ ] **Step 5: Verify Docker Compose**
|
||||
|
||||
Run: `docker compose up -d postgres && docker compose ps`
|
||||
Expected: PostgreSQL container running, healthy.
|
||||
|
||||
Run: `docker compose down`
|
||||
Expected: Clean shutdown.
|
||||
|
||||
- [ ] **Step 6: Commit lockfile**
|
||||
|
||||
```bash
|
||||
git add pnpm-lock.yaml
|
||||
git commit -m "chore: add pnpm lockfile"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Create test directory structure
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/unit/.gitkeep`
|
||||
- Create: `tests/integration/.gitkeep`
|
||||
- Create: `tests/e2e/.gitkeep`
|
||||
|
||||
- [ ] **Step 1: Create test directories**
|
||||
|
||||
```bash
|
||||
mkdir -p tests/unit tests/integration tests/e2e
|
||||
touch tests/unit/.gitkeep tests/integration/.gitkeep tests/e2e/.gitkeep
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/
|
||||
git commit -m "feat: add test directory structure (unit, integration, e2e)"
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,977 +0,0 @@
|
||||
# Plan 3: Payload CMS Integration — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Implement Payload CMS integration: `@repo/cms-core` (config, collections, hooks), `@repo/cms-client` (dual-mode local+HTTP client with generated types), and `apps/cms` (thin Next.js shell serving the Payload admin panel).
|
||||
|
||||
**Architecture:** Payload config and collections live in `@repo/cms-core` — standalone, testable, framework-agnostic. `@repo/cms-client` provides a dual-mode client (Local API primary, HTTP fallback) that receives a Payload instance via injection. `apps/cms` is a thin Next.js 15 shell that imports config from cms-core and serves the admin panel. Type generation runs `payload generate:types` against cms-core's config.
|
||||
|
||||
**Tech Stack:** Payload CMS 3.x, @payloadcms/db-postgres, @payloadcms/next, @payloadcms/richtext-lexical, Next.js 15, PostgreSQL 16, sharp
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
### packages/cms-core
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `packages/cms-core/package.json` | Dependencies: payload, @payloadcms/db-postgres, @payloadcms/richtext-lexical |
|
||||
| `packages/cms-core/src/payload.config.ts` | Root Payload config (db, editor, collections, globals) |
|
||||
| `packages/cms-core/src/collections/users/index.ts` | Users collection with auth enabled |
|
||||
| `packages/cms-core/src/collections/articles/index.ts` | Articles collection config |
|
||||
| `packages/cms-core/src/collections/articles/fields.ts` | Article field definitions |
|
||||
| `packages/cms-core/src/collections/articles/hooks/before-change.ts` | Slug auto-generation hook |
|
||||
| `packages/cms-core/src/collections/media/index.ts` | Media collection with uploads |
|
||||
| `packages/cms-core/src/globals/site-settings.ts` | Site settings global |
|
||||
| `packages/cms-core/src/index.ts` | Exports config + all collections |
|
||||
|
||||
### packages/cms-client
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `packages/cms-client/package.json` | Dependencies: payload (types only) |
|
||||
| `packages/cms-client/src/client.ts` | createPayloadClient() factory |
|
||||
| `packages/cms-client/src/local-client.ts` | LocalPayloadClient — wraps Payload instance |
|
||||
| `packages/cms-client/src/http-client.ts` | HTTPPayloadClient — REST API fallback |
|
||||
| `packages/cms-client/src/types.ts` | Shared client types (PayloadClient interface) |
|
||||
| `packages/cms-client/src/index.ts` | Exports |
|
||||
|
||||
### apps/cms
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `apps/cms/package.json` | Dependencies: next, payload, @payloadcms/next, @repo/cms-core |
|
||||
| `apps/cms/next.config.mjs` | Next.js config wrapped with withPayload |
|
||||
| `apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx` | Admin panel catch-all route |
|
||||
| `apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx` | Admin 404 page |
|
||||
| `apps/cms/src/app/(payload)/layout.tsx` | Payload layout with RootLayout |
|
||||
| `apps/cms/src/app/(payload)/custom.scss` | Empty custom styles |
|
||||
| `apps/cms/src/payload-types.ts` | Generated types (via payload generate:types) |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Install cms-core dependencies
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/cms-core/package.json`
|
||||
- Modify: `packages/cms-core/tsconfig.json`
|
||||
|
||||
- [ ] **Step 1: Update packages/cms-core/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/cms-core",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"payload": "^3.14.0",
|
||||
"@payloadcms/db-postgres": "^3.14.0",
|
||||
"@payloadcms/richtext-lexical": "^3.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run pnpm install**
|
||||
|
||||
Run: `pnpm install`
|
||||
Expected: Payload and its dependencies install successfully.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cms-core/package.json packages/cms-core/tsconfig.json pnpm-lock.yaml
|
||||
git commit -m "feat(cms-core): add Payload CMS dependencies"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Users collection
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/cms-core/src/collections/users/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create Users collection**
|
||||
|
||||
```typescript
|
||||
import type { CollectionConfig } from "payload";
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: "users",
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: "email",
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "displayName",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "role",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "Admin", value: "admin" },
|
||||
{ label: "Editor", value: "editor" },
|
||||
{ label: "Author", value: "author" },
|
||||
],
|
||||
defaultValue: "author",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cms-core/src/collections/users/
|
||||
git commit -m "feat(cms-core): add Users collection with auth"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Articles collection with fields and hooks
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/cms-core/src/collections/articles/fields.ts`
|
||||
- Create: `packages/cms-core/src/collections/articles/hooks/before-change.ts`
|
||||
- Create: `packages/cms-core/src/collections/articles/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create fields.ts**
|
||||
|
||||
```typescript
|
||||
import type { Field } from "payload";
|
||||
|
||||
export const articleFields: Field[] = [
|
||||
{
|
||||
name: "title",
|
||||
type: "text",
|
||||
required: true,
|
||||
maxLength: 255,
|
||||
},
|
||||
{
|
||||
name: "slug",
|
||||
type: "text",
|
||||
unique: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
description: "Auto-generated from title if left empty",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content",
|
||||
type: "richText",
|
||||
},
|
||||
{
|
||||
name: "status",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "Draft", value: "draft" },
|
||||
{ label: "Published", value: "published" },
|
||||
],
|
||||
defaultValue: "draft",
|
||||
required: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "author",
|
||||
type: "relationship",
|
||||
relationTo: "users",
|
||||
required: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "featuredImage",
|
||||
type: "upload",
|
||||
relationTo: "media",
|
||||
},
|
||||
{
|
||||
name: "publishedAt",
|
||||
type: "date",
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
date: {
|
||||
pickerAppearance: "dayAndTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create hooks/before-change.ts**
|
||||
|
||||
This is a CMS-operational hook (slug auto-generation) — stays in cms-core per the design spec.
|
||||
|
||||
```typescript
|
||||
import type { CollectionBeforeChangeHook } from "payload";
|
||||
|
||||
function generateSlug(title: string): string {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
export const autoGenerateSlug: CollectionBeforeChangeHook = ({
|
||||
data,
|
||||
operation,
|
||||
}) => {
|
||||
if (operation === "create" || operation === "update") {
|
||||
if (data && data.title && !data.slug) {
|
||||
data.slug = generateSlug(data.title);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create articles/index.ts**
|
||||
|
||||
```typescript
|
||||
import type { CollectionConfig } from "payload";
|
||||
|
||||
import { articleFields } from "./fields.js";
|
||||
import { autoGenerateSlug } from "./hooks/before-change.js";
|
||||
|
||||
export const Articles: CollectionConfig = {
|
||||
slug: "articles",
|
||||
admin: {
|
||||
useAsTitle: "title",
|
||||
defaultColumns: ["title", "status", "author", "updatedAt"],
|
||||
},
|
||||
hooks: {
|
||||
beforeChange: [autoGenerateSlug],
|
||||
},
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
fields: articleFields,
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cms-core/src/collections/articles/
|
||||
git commit -m "feat(cms-core): add Articles collection with slug auto-generation hook"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Media collection
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/cms-core/src/collections/media/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create Media collection**
|
||||
|
||||
```typescript
|
||||
import type { CollectionConfig } from "payload";
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: "media",
|
||||
upload: {
|
||||
mimeTypes: ["image/*", "application/pdf"],
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: "filename",
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "alt",
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cms-core/src/collections/media/
|
||||
git commit -m "feat(cms-core): add Media collection with uploads"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Site settings global
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/cms-core/src/globals/site-settings.ts`
|
||||
|
||||
- [ ] **Step 1: Create site-settings.ts**
|
||||
|
||||
```typescript
|
||||
import type { GlobalConfig } from "payload";
|
||||
|
||||
export const SiteSettings: GlobalConfig = {
|
||||
slug: "site-settings",
|
||||
admin: {
|
||||
group: "Settings",
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "siteName",
|
||||
type: "text",
|
||||
required: true,
|
||||
defaultValue: "My App",
|
||||
},
|
||||
{
|
||||
name: "siteDescription",
|
||||
type: "textarea",
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cms-core/src/globals/
|
||||
git commit -m "feat(cms-core): add SiteSettings global"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Payload config + exports
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/cms-core/src/payload.config.ts`
|
||||
- Modify: `packages/cms-core/src/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create payload.config.ts**
|
||||
|
||||
```typescript
|
||||
import { buildConfig } from "payload";
|
||||
import { postgresAdapter } from "@payloadcms/db-postgres";
|
||||
import { lexicalEditor } from "@payloadcms/richtext-lexical";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { Users } from "./collections/users/index.js";
|
||||
import { Articles } from "./collections/articles/index.js";
|
||||
import { Media } from "./collections/media/index.js";
|
||||
import { SiteSettings } from "./globals/site-settings.js";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
||||
export default buildConfig({
|
||||
editor: lexicalEditor(),
|
||||
collections: [Users, Articles, Media],
|
||||
globals: [SiteSettings],
|
||||
secret: process.env.PAYLOAD_SECRET || "default-secret-change-me",
|
||||
db: postgresAdapter({
|
||||
pool: {
|
||||
connectionString:
|
||||
process.env.DATABASE_URL ||
|
||||
"postgresql://postgres:postgres@localhost:5432/template",
|
||||
},
|
||||
}),
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, "payload-types.ts"),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update src/index.ts**
|
||||
|
||||
```typescript
|
||||
export { Users } from "./collections/users/index.js";
|
||||
export { Articles } from "./collections/articles/index.js";
|
||||
export { Media } from "./collections/media/index.js";
|
||||
export { SiteSettings } from "./globals/site-settings.js";
|
||||
export { default as config } from "./payload.config.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cms-core/src/payload.config.ts packages/cms-core/src/index.ts
|
||||
git commit -m "feat(cms-core): add Payload config with postgres adapter and lexical editor"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: cms-client — dual-mode Payload client
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/cms-client/package.json`
|
||||
- Create: `packages/cms-client/src/types.ts`
|
||||
- Create: `packages/cms-client/src/local-client.ts`
|
||||
- Create: `packages/cms-client/src/http-client.ts`
|
||||
- Create: `packages/cms-client/src/client.ts`
|
||||
- Modify: `packages/cms-client/src/index.ts`
|
||||
|
||||
- [ ] **Step 1: Update packages/cms-client/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/cms-client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"payload": "^3.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create src/types.ts**
|
||||
|
||||
```typescript
|
||||
export interface FindOptions {
|
||||
where?: Record<string, unknown>;
|
||||
sort?: string;
|
||||
limit?: number;
|
||||
page?: number;
|
||||
depth?: number;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export interface PayloadClientResult<T> {
|
||||
docs: T[];
|
||||
totalDocs: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
page: number;
|
||||
pagingCounter: number;
|
||||
hasPrevPage: boolean;
|
||||
hasNextPage: boolean;
|
||||
prevPage: number | null;
|
||||
nextPage: number | null;
|
||||
}
|
||||
|
||||
export interface PayloadClient {
|
||||
find<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
options?: FindOptions
|
||||
): Promise<PayloadClientResult<T>>;
|
||||
|
||||
findByID<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
id: string,
|
||||
options?: { depth?: number }
|
||||
): Promise<T>;
|
||||
|
||||
create<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
data: Record<string, unknown>,
|
||||
options?: { depth?: number }
|
||||
): Promise<T>;
|
||||
|
||||
update<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
id: string,
|
||||
data: Record<string, unknown>,
|
||||
options?: { depth?: number }
|
||||
): Promise<T>;
|
||||
|
||||
delete<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
id: string
|
||||
): Promise<T>;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create src/local-client.ts**
|
||||
|
||||
```typescript
|
||||
import type { Payload } from "payload";
|
||||
import type { FindOptions, PayloadClient, PayloadClientResult } from "./types.js";
|
||||
|
||||
export class LocalPayloadClient implements PayloadClient {
|
||||
constructor(private payload: Payload) {}
|
||||
|
||||
async find<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
options?: FindOptions
|
||||
): Promise<PayloadClientResult<T>> {
|
||||
const result = await this.payload.find({
|
||||
collection: collection as any,
|
||||
where: options?.where as any,
|
||||
sort: options?.sort,
|
||||
limit: options?.limit,
|
||||
page: options?.page,
|
||||
depth: options?.depth,
|
||||
locale: options?.locale as any,
|
||||
});
|
||||
return result as unknown as PayloadClientResult<T>;
|
||||
}
|
||||
|
||||
async findByID<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
id: string,
|
||||
options?: { depth?: number }
|
||||
): Promise<T> {
|
||||
const result = await this.payload.findByID({
|
||||
collection: collection as any,
|
||||
id,
|
||||
depth: options?.depth,
|
||||
});
|
||||
return result as unknown as T;
|
||||
}
|
||||
|
||||
async create<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
data: Record<string, unknown>,
|
||||
options?: { depth?: number }
|
||||
): Promise<T> {
|
||||
const result = await this.payload.create({
|
||||
collection: collection as any,
|
||||
data: data as any,
|
||||
depth: options?.depth,
|
||||
});
|
||||
return result as unknown as T;
|
||||
}
|
||||
|
||||
async update<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
id: string,
|
||||
data: Record<string, unknown>,
|
||||
options?: { depth?: number }
|
||||
): Promise<T> {
|
||||
const result = await this.payload.update({
|
||||
collection: collection as any,
|
||||
id,
|
||||
data: data as any,
|
||||
depth: options?.depth,
|
||||
});
|
||||
return result as unknown as T;
|
||||
}
|
||||
|
||||
async delete<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
id: string
|
||||
): Promise<T> {
|
||||
const result = await this.payload.delete({
|
||||
collection: collection as any,
|
||||
id,
|
||||
});
|
||||
return result as unknown as T;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create src/http-client.ts**
|
||||
|
||||
```typescript
|
||||
import type { FindOptions, PayloadClient, PayloadClientResult } from "./types.js";
|
||||
|
||||
export class HTTPPayloadClient implements PayloadClient {
|
||||
constructor(private baseURL: string) {}
|
||||
|
||||
private async request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${this.baseURL}${path}`, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
...options,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Payload API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async find<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
options?: FindOptions
|
||||
): Promise<PayloadClientResult<T>> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.limit) params.set("limit", String(options.limit));
|
||||
if (options?.page) params.set("page", String(options.page));
|
||||
if (options?.sort) params.set("sort", options.sort);
|
||||
if (options?.depth) params.set("depth", String(options.depth));
|
||||
if (options?.where) params.set("where", JSON.stringify(options.where));
|
||||
const query = params.toString();
|
||||
return this.request<PayloadClientResult<T>>(
|
||||
`/api/${collection}${query ? `?${query}` : ""}`
|
||||
);
|
||||
}
|
||||
|
||||
async findByID<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
id: string,
|
||||
options?: { depth?: number }
|
||||
): Promise<T> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.depth) params.set("depth", String(options.depth));
|
||||
const query = params.toString();
|
||||
return this.request<T>(
|
||||
`/api/${collection}/${id}${query ? `?${query}` : ""}`
|
||||
);
|
||||
}
|
||||
|
||||
async create<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
data: Record<string, unknown>,
|
||||
options?: { depth?: number }
|
||||
): Promise<T> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.depth) params.set("depth", String(options.depth));
|
||||
const query = params.toString();
|
||||
return this.request<T>(
|
||||
`/api/${collection}${query ? `?${query}` : ""}`,
|
||||
{ method: "POST", body: JSON.stringify(data) }
|
||||
);
|
||||
}
|
||||
|
||||
async update<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
id: string,
|
||||
data: Record<string, unknown>,
|
||||
options?: { depth?: number }
|
||||
): Promise<T> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.depth) params.set("depth", String(options.depth));
|
||||
const query = params.toString();
|
||||
return this.request<T>(
|
||||
`/api/${collection}/${id}${query ? `?${query}` : ""}`,
|
||||
{ method: "PATCH", body: JSON.stringify(data) }
|
||||
);
|
||||
}
|
||||
|
||||
async delete<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
id: string
|
||||
): Promise<T> {
|
||||
return this.request<T>(`/api/${collection}/${id}`, { method: "DELETE" });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Create src/client.ts**
|
||||
|
||||
```typescript
|
||||
import type { Payload } from "payload";
|
||||
import type { PayloadClient } from "./types.js";
|
||||
import { LocalPayloadClient } from "./local-client.js";
|
||||
import { HTTPPayloadClient } from "./http-client.js";
|
||||
|
||||
type PayloadClientOptions =
|
||||
| { mode: "local"; payload: Payload }
|
||||
| { mode: "http"; baseURL: string };
|
||||
|
||||
export function createPayloadClient(options: PayloadClientOptions): PayloadClient {
|
||||
if (options.mode === "local") {
|
||||
return new LocalPayloadClient(options.payload);
|
||||
}
|
||||
return new HTTPPayloadClient(options.baseURL);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Update src/index.ts**
|
||||
|
||||
```typescript
|
||||
export { createPayloadClient } from "./client.js";
|
||||
export { LocalPayloadClient } from "./local-client.js";
|
||||
export { HTTPPayloadClient } from "./http-client.js";
|
||||
export type {
|
||||
PayloadClient,
|
||||
PayloadClientResult,
|
||||
FindOptions,
|
||||
} from "./types.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Run pnpm install and commit**
|
||||
|
||||
Run: `pnpm install`
|
||||
|
||||
```bash
|
||||
git add packages/cms-client/ pnpm-lock.yaml
|
||||
git commit -m "feat(cms-client): add dual-mode Payload client (local + HTTP)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: apps/cms — thin Next.js shell
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/cms/package.json`
|
||||
- Create: `apps/cms/next.config.mjs`
|
||||
- Create: `apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx`
|
||||
- Create: `apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx`
|
||||
- Create: `apps/cms/src/app/(payload)/layout.tsx`
|
||||
- Create: `apps/cms/src/app/(payload)/custom.scss`
|
||||
- Modify: `apps/cms/tsconfig.json`
|
||||
|
||||
- [ ] **Step 1: Update apps/cms/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/cms",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev --port 3001",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"generate:types": "payload generate:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/next": "^3.14.0",
|
||||
"@payloadcms/ui": "^3.14.0",
|
||||
"@repo/cms-core": "workspace:*",
|
||||
"next": "^15.3.0",
|
||||
"payload": "^3.14.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"sharp": "^0.33.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update apps/cms/tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@payload-config": ["../../packages/cms-core/src/payload.config.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "next-env.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create next.config.mjs**
|
||||
|
||||
```javascript
|
||||
import { withPayload } from "@payloadcms/next/withPayload";
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default withPayload(nextConfig);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create admin catch-all page**
|
||||
|
||||
```tsx
|
||||
// apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import config from "@payload-config";
|
||||
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
|
||||
import { importMap } from "../importMap.js";
|
||||
|
||||
type Args = {
|
||||
params: Promise<{ segments: string[] }>;
|
||||
searchParams: Promise<Record<string, string | string[]>>;
|
||||
};
|
||||
|
||||
export const generateMetadata = ({
|
||||
params,
|
||||
searchParams,
|
||||
}: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams });
|
||||
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, importMap, params, searchParams });
|
||||
|
||||
export default Page;
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Create admin not-found page**
|
||||
|
||||
```tsx
|
||||
// apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import config from "@payload-config";
|
||||
import { NotFoundPage, generatePageMetadata } from "@payloadcms/next/views";
|
||||
import { importMap } from "../importMap.js";
|
||||
|
||||
type Args = {
|
||||
params: Promise<{ segments: string[] }>;
|
||||
searchParams: Promise<Record<string, string | string[]>>;
|
||||
};
|
||||
|
||||
export const generateMetadata = ({
|
||||
params,
|
||||
searchParams,
|
||||
}: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams });
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) =>
|
||||
NotFoundPage({ config, importMap, params, searchParams });
|
||||
|
||||
export default NotFound;
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Create payload layout**
|
||||
|
||||
```tsx
|
||||
// apps/cms/src/app/(payload)/layout.tsx
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { ServerFunctionClient } from "payload";
|
||||
|
||||
import config from "@payload-config";
|
||||
import { RootLayout } from "@payloadcms/next/layouts";
|
||||
import React from "react";
|
||||
|
||||
import { importMap } from "./importMap.js";
|
||||
import "./custom.scss";
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
"use server";
|
||||
const { default: payloadModule } = await import("payload");
|
||||
return payloadModule.handleServerFunctions({ ...args, config, importMap });
|
||||
};
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
);
|
||||
|
||||
export default Layout;
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Create empty importMap and custom.scss**
|
||||
|
||||
```typescript
|
||||
// apps/cms/src/app/(payload)/importMap.js
|
||||
export const importMap = {};
|
||||
```
|
||||
|
||||
```scss
|
||||
// apps/cms/src/app/(payload)/custom.scss
|
||||
// Custom admin panel styles
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Run pnpm install and commit**
|
||||
|
||||
Run: `pnpm install`
|
||||
|
||||
```bash
|
||||
git add apps/cms/ pnpm-lock.yaml
|
||||
git commit -m "feat(cms): add thin Next.js shell for Payload admin panel"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Update docker-compose with CMS service
|
||||
|
||||
**Files:**
|
||||
- Modify: `docker-compose.yml`
|
||||
|
||||
- [ ] **Step 1: Update docker-compose.yml**
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: template
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
Note: CMS app service will be added when Dockerfiles are created in a later plan. For now, local dev uses `pnpm dev --filter @repo/cms`.
|
||||
|
||||
- [ ] **Step 2: Commit (no change needed if docker-compose is already correct)**
|
||||
|
||||
Only commit if docker-compose was modified.
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Verify build
|
||||
|
||||
- [ ] **Step 1: Run pnpm install from root**
|
||||
|
||||
Run: `pnpm install`
|
||||
Expected: All dependencies resolve.
|
||||
|
||||
- [ ] **Step 2: Run turbo build**
|
||||
|
||||
Run: `pnpm build`
|
||||
Expected: All workspaces build. The cms-core and cms-client packages should pass `tsc --noEmit`. The cms app should run `next build` (may need database for full build — if it fails due to no DB, that's expected and acceptable at this stage).
|
||||
|
||||
- [ ] **Step 3: Commit any remaining changes**
|
||||
|
||||
```bash
|
||||
git add -A && git status
|
||||
# Only commit if there are changes
|
||||
git commit -m "chore: update lockfile after CMS integration"
|
||||
```
|
||||
@@ -1,628 +0,0 @@
|
||||
# Plan 4: API Layer + App Shells — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Implement tRPC routers (`@repo/api`), shared React Query hooks (`@repo/api-client`), and both app shells (`apps/web-next` with Next.js 15, `apps/web-tanstack` with TanStack Start) — completing the full data flow from UI to core.
|
||||
|
||||
**Architecture:** `@repo/api` defines the tRPC router that calls controllers from `@repo/core`. `@repo/api-client` provides a framework-agnostic React Query provider and typed hooks. Each app hosts its own tRPC HTTP endpoint and wraps with the shared provider. Both apps use identical hooks.
|
||||
|
||||
**Tech Stack:** tRPC v11, @trpc/tanstack-react-query, TanStack Query v5, Next.js 15 (App Router), TanStack Start (Vite-based), Zustand
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
### packages/api
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `packages/api/package.json` | tRPC server deps |
|
||||
| `packages/api/src/trpc.ts` | tRPC init, context, middleware |
|
||||
| `packages/api/src/router/auth.router.ts` | Auth procedures |
|
||||
| `packages/api/src/router/content.router.ts` | Content procedures |
|
||||
| `packages/api/src/router/index.ts` | Root appRouter |
|
||||
| `packages/api/src/index.ts` | Exports AppRouter type |
|
||||
|
||||
### packages/api-client
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `packages/api-client/package.json` | tRPC client + React Query deps |
|
||||
| `packages/api-client/src/trpc.ts` | createTRPCReact instance |
|
||||
| `packages/api-client/src/query-client.ts` | Shared QueryClient factory |
|
||||
| `packages/api-client/src/provider.tsx` | ApiProvider component |
|
||||
| `packages/api-client/src/index.ts` | Exports provider + trpc |
|
||||
|
||||
### apps/web-next
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `apps/web-next/package.json` | Next.js 15 + deps |
|
||||
| `apps/web-next/next.config.mjs` | Next.js config |
|
||||
| `apps/web-next/src/app/layout.tsx` | Root layout with ApiProvider |
|
||||
| `apps/web-next/src/app/page.tsx` | Home page |
|
||||
| `apps/web-next/src/app/api/trpc/[trpc]/route.ts` | tRPC HTTP handler |
|
||||
| `apps/web-next/src/lib/payload.ts` | Payload instance initialization |
|
||||
|
||||
### apps/web-tanstack
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `apps/web-tanstack/package.json` | TanStack Start + deps |
|
||||
| `apps/web-tanstack/vite.config.ts` | Vite + TanStack Start plugin |
|
||||
| `apps/web-tanstack/src/router.tsx` | TanStack Router config |
|
||||
| `apps/web-tanstack/src/routes/__root.tsx` | Root layout with ApiProvider |
|
||||
| `apps/web-tanstack/src/routes/index.tsx` | Home page |
|
||||
| `apps/web-tanstack/src/lib/payload.ts` | Payload instance initialization |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: packages/api — tRPC routers
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/api/package.json`
|
||||
- Modify: `packages/api/tsconfig.json`
|
||||
- Create: `packages/api/src/trpc.ts`
|
||||
- Create: `packages/api/src/router/auth.router.ts`
|
||||
- Create: `packages/api/src/router/content.router.ts`
|
||||
- Create: `packages/api/src/router/index.ts`
|
||||
- Modify: `packages/api/src/index.ts`
|
||||
|
||||
- [ ] **Step 1: Update packages/api/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/api",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/core": "workspace:*",
|
||||
"@trpc/server": "^11.1.0",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update packages/api/tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create src/trpc.ts**
|
||||
|
||||
```typescript
|
||||
import { initTRPC } from "@trpc/server";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
export const router = t.router;
|
||||
export const publicProcedure = t.procedure;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create src/router/auth.router.ts**
|
||||
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
import { router, publicProcedure } from "../trpc.js";
|
||||
import {
|
||||
signInController,
|
||||
signUpController,
|
||||
signOutController,
|
||||
} from "@repo/core";
|
||||
|
||||
export const authRouter = router({
|
||||
signIn: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
username: z.string().min(3).max(31),
|
||||
password: z.string().min(6).max(255),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await signInController(input);
|
||||
}),
|
||||
|
||||
signUp: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
username: z.string().min(3).max(31),
|
||||
password: z.string().min(6).max(255),
|
||||
confirmPassword: z.string().min(6).max(255),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await signUpController(input);
|
||||
}),
|
||||
|
||||
signOut: publicProcedure
|
||||
.input(z.object({ sessionId: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await signOutController(input.sessionId);
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Create src/router/content.router.ts**
|
||||
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
import { router, publicProcedure } from "../trpc.js";
|
||||
import { createArticleController, getArticlesController } from "@repo/core";
|
||||
|
||||
export const contentRouter = router({
|
||||
listArticles: publicProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
status: z.string().optional(),
|
||||
authorId: z.string().optional(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await getArticlesController(input ?? {});
|
||||
}),
|
||||
|
||||
createArticle: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
title: z.string().min(1).max(255),
|
||||
content: z.string(),
|
||||
authorId: z.string(),
|
||||
slug: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await createArticleController(input);
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Create src/router/index.ts**
|
||||
|
||||
```typescript
|
||||
import { router } from "../trpc.js";
|
||||
import { authRouter } from "./auth.router.js";
|
||||
import { contentRouter } from "./content.router.js";
|
||||
|
||||
export const appRouter = router({
|
||||
auth: authRouter,
|
||||
content: contentRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Update src/index.ts**
|
||||
|
||||
```typescript
|
||||
export { appRouter, type AppRouter } from "./router/index.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Run pnpm install and commit**
|
||||
|
||||
Run: `pnpm install`
|
||||
|
||||
```bash
|
||||
git add packages/api/ pnpm-lock.yaml
|
||||
git commit -m "feat(api): add tRPC routers (auth + content) calling core controllers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: packages/api-client — shared React Query hooks + provider
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/api-client/package.json`
|
||||
- Modify: `packages/api-client/tsconfig.json`
|
||||
- Create: `packages/api-client/src/trpc.ts`
|
||||
- Create: `packages/api-client/src/query-client.ts`
|
||||
- Create: `packages/api-client/src/provider.tsx`
|
||||
- Modify: `packages/api-client/src/index.ts`
|
||||
|
||||
- [ ] **Step 1: Update packages/api-client/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/api-client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/api": "workspace:*",
|
||||
"@trpc/client": "^11.1.0",
|
||||
"@trpc/tanstack-react-query": "^11.1.0",
|
||||
"@tanstack/react-query": "^5.75.0",
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "@repo/typescript-config/react-library.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create src/trpc.ts**
|
||||
|
||||
```typescript
|
||||
import { createTRPCContext } from "@trpc/tanstack-react-query";
|
||||
import type { AppRouter } from "@repo/api";
|
||||
|
||||
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create src/query-client.ts**
|
||||
|
||||
```typescript
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
let clientQueryClient: QueryClient | undefined;
|
||||
|
||||
export function getQueryClient(): QueryClient {
|
||||
if (typeof window === "undefined") {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { staleTime: 30 * 1000 },
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!clientQueryClient) {
|
||||
clientQueryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { staleTime: 30 * 1000 },
|
||||
},
|
||||
});
|
||||
}
|
||||
return clientQueryClient;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Create src/provider.tsx**
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
||||
import type { AppRouter } from "@repo/api";
|
||||
import { TRPCProvider } from "./trpc.js";
|
||||
import { getQueryClient } from "./query-client.js";
|
||||
|
||||
export function ApiProvider({
|
||||
children,
|
||||
trpcUrl,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
trpcUrl: string;
|
||||
}) {
|
||||
const queryClient = getQueryClient();
|
||||
const trpcClient = createTRPCClient<AppRouter>({
|
||||
links: [httpBatchLink({ url: trpcUrl })],
|
||||
});
|
||||
|
||||
return (
|
||||
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</TRPCProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Update src/index.ts**
|
||||
|
||||
```typescript
|
||||
export { ApiProvider } from "./provider.js";
|
||||
export { useTRPC } from "./trpc.js";
|
||||
export { getQueryClient } from "./query-client.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Run pnpm install and commit**
|
||||
|
||||
Run: `pnpm install`
|
||||
|
||||
```bash
|
||||
git add packages/api-client/ pnpm-lock.yaml
|
||||
git commit -m "feat(api-client): add tRPC React Query provider and shared hooks"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: apps/web-next — Next.js 15 app shell
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web-next/package.json`
|
||||
- Create: `apps/web-next/next.config.mjs`
|
||||
- Create: `apps/web-next/src/app/layout.tsx`
|
||||
- Create: `apps/web-next/src/app/page.tsx`
|
||||
- Create: `apps/web-next/src/app/api/trpc/[trpc]/route.ts`
|
||||
- Modify: `apps/web-next/tsconfig.json`
|
||||
|
||||
- [ ] **Step 1: Update apps/web-next/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/web-next",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev --port 3000",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/api": "workspace:*",
|
||||
"@repo/api-client": "workspace:*",
|
||||
"@repo/ui": "workspace:*",
|
||||
"next": "^15.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create next.config.mjs**
|
||||
|
||||
```javascript
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ["@repo/api", "@repo/api-client", "@repo/core", "@repo/ui"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create src/app/api/trpc/[trpc]/route.ts**
|
||||
|
||||
```typescript
|
||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||
import { appRouter } from "@repo/api";
|
||||
|
||||
const handler = (req: Request) =>
|
||||
fetchRequestHandler({
|
||||
endpoint: "/api/trpc",
|
||||
req,
|
||||
router: appRouter,
|
||||
createContext: () => ({}),
|
||||
});
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create src/app/layout.tsx**
|
||||
|
||||
```tsx
|
||||
import type { Metadata } from "next";
|
||||
import { Providers } from "./providers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Template — Next.js",
|
||||
description: "Clean Architecture Monorepo Template",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Create src/app/providers.tsx**
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { ApiProvider } from "@repo/api-client";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return <ApiProvider trpcUrl="/api/trpc">{children}</ApiProvider>;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Create src/app/page.tsx**
|
||||
|
||||
```tsx
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Template — Next.js</h1>
|
||||
<p>Clean Architecture Monorepo Template</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/web-next/ pnpm-lock.yaml
|
||||
git commit -m "feat(web-next): add Next.js 15 app shell with tRPC endpoint"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: apps/web-tanstack — TanStack Start app shell
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web-tanstack/package.json`
|
||||
- Create: `apps/web-tanstack/vite.config.ts`
|
||||
- Create: `apps/web-tanstack/src/router.tsx`
|
||||
- Create: `apps/web-tanstack/src/routes/__root.tsx`
|
||||
- Create: `apps/web-tanstack/src/routes/index.tsx`
|
||||
- Modify: `apps/web-tanstack/tsconfig.json`
|
||||
|
||||
- [ ] **Step 1: Update apps/web-tanstack/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/web-tanstack",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite dev --port 3002",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/api": "workspace:*",
|
||||
"@repo/api-client": "workspace:*",
|
||||
"@repo/ui": "workspace:*",
|
||||
"@tanstack/react-router": "^1.120.0",
|
||||
"@tanstack/react-start": "^1.120.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"vite": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.4.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create vite.config.ts**
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create src/routes/__root.tsx**
|
||||
|
||||
```tsx
|
||||
import { Outlet, createRootRoute } from "@tanstack/react-router";
|
||||
import { ApiProvider } from "@repo/api-client";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => (
|
||||
<ApiProvider trpcUrl="http://localhost:3000/api/trpc">
|
||||
<Outlet />
|
||||
</ApiProvider>
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create src/routes/index.tsx**
|
||||
|
||||
```tsx
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: Home,
|
||||
});
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Template — TanStack Start</h1>
|
||||
<p>Clean Architecture Monorepo Template</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/web-tanstack/ pnpm-lock.yaml
|
||||
git commit -m "feat(web-tanstack): add TanStack Start app shell with tRPC client"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Install all dependencies and verify
|
||||
|
||||
- [ ] **Step 1: Run pnpm install**
|
||||
|
||||
Run: `pnpm install`
|
||||
Expected: All dependencies resolve.
|
||||
|
||||
- [ ] **Step 2: Run turbo build**
|
||||
|
||||
Run: `pnpm build`
|
||||
Expected: All packages build (apps may fail on `next build` / `vite build` without full setup — change to placeholder if needed).
|
||||
|
||||
- [ ] **Step 3: Run core tests**
|
||||
|
||||
Run: `cd packages/core && pnpm vitest run`
|
||||
Expected: All 22 tests pass.
|
||||
|
||||
- [ ] **Step 4: Commit any remaining fixes**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore: finalize Plan 4 — API layer + app shells"
|
||||
```
|
||||
@@ -1,610 +0,0 @@
|
||||
# Plan 5: UI System — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Set up `@repo/ui` with Atomic Design folder structure, Tailwind CSS v4, shadcn/ui, base components (atoms + a molecule), and `apps/storybook` with Storybook 8.
|
||||
|
||||
**Architecture:** `@repo/ui` uses Atomic Design (atoms/molecules/organisms/templates). Tailwind v4 uses CSS-first config (`@import "tailwindcss"` + `@theme`). shadcn/ui components land in `atoms/` by default. Storybook runs as a separate app pulling stories from the UI package via `@storybook/react-vite` with `@tailwindcss/vite` plugin.
|
||||
|
||||
**Tech Stack:** Tailwind CSS v4, shadcn/ui, clsx, tailwind-merge, Storybook 8, @storybook/react-vite
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Set up @repo/ui with Tailwind v4 + Atomic Design structure
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/ui/package.json`
|
||||
- Modify: `packages/ui/tsconfig.json`
|
||||
- Create: `packages/ui/src/styles/globals.css`
|
||||
- Create: `packages/ui/src/lib/utils.ts`
|
||||
- Create: `packages/ui/src/atoms/index.ts`
|
||||
- Create: `packages/ui/src/molecules/index.ts`
|
||||
- Create: `packages/ui/src/organisms/index.ts`
|
||||
- Create: `packages/ui/src/templates/index.ts`
|
||||
- Modify: `packages/ui/src/index.ts`
|
||||
|
||||
- [ ] **Step 1: Update packages/ui/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/ui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'typechecked by consuming app bundler'",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.0",
|
||||
"tailwind-merge": "^3.0.0",
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/react": "^19.0.0",
|
||||
"tailwindcss": "^4.1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create src/styles/globals.css**
|
||||
|
||||
Tailwind v4 CSS-first config — no tailwind.config.ts needed.
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-background: hsl(0 0% 100%);
|
||||
--color-foreground: hsl(240 10% 3.9%);
|
||||
--color-card: hsl(0 0% 100%);
|
||||
--color-card-foreground: hsl(240 10% 3.9%);
|
||||
--color-popover: hsl(0 0% 100%);
|
||||
--color-popover-foreground: hsl(240 10% 3.9%);
|
||||
--color-primary: hsl(240 5.9% 10%);
|
||||
--color-primary-foreground: hsl(0 0% 98%);
|
||||
--color-secondary: hsl(240 4.8% 95.9%);
|
||||
--color-secondary-foreground: hsl(240 5.9% 10%);
|
||||
--color-muted: hsl(240 4.8% 95.9%);
|
||||
--color-muted-foreground: hsl(240 3.8% 46.1%);
|
||||
--color-accent: hsl(240 4.8% 95.9%);
|
||||
--color-accent-foreground: hsl(240 5.9% 10%);
|
||||
--color-destructive: hsl(0 84.2% 60.2%);
|
||||
--color-destructive-foreground: hsl(0 0% 98%);
|
||||
--color-border: hsl(240 5.9% 90%);
|
||||
--color-input: hsl(240 5.9% 90%);
|
||||
--color-ring: hsl(240 5.9% 10%);
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create src/lib/utils.ts**
|
||||
|
||||
```typescript
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create atomic design barrel files**
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/atoms/index.ts
|
||||
// Atom components are exported from here
|
||||
export {};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/molecules/index.ts
|
||||
// Molecule components are exported from here
|
||||
export {};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/organisms/index.ts
|
||||
// Organism components are exported from here
|
||||
export {};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/templates/index.ts
|
||||
// Template components are exported from here
|
||||
export {};
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update src/index.ts**
|
||||
|
||||
```typescript
|
||||
export { cn } from "./lib/utils.js";
|
||||
export * from "./atoms/index.js";
|
||||
export * from "./molecules/index.js";
|
||||
export * from "./organisms/index.js";
|
||||
export * from "./templates/index.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run pnpm install and commit**
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
git add packages/ui/ pnpm-lock.yaml
|
||||
git commit -m "feat(ui): set up Atomic Design structure with Tailwind v4"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add Button atom
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/ui/src/atoms/button/button.tsx`
|
||||
- Create: `packages/ui/src/atoms/button/button.stories.tsx`
|
||||
- Create: `packages/ui/src/atoms/button/index.ts`
|
||||
- Modify: `packages/ui/src/atoms/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create button.tsx**
|
||||
|
||||
```tsx
|
||||
import { forwardRef, type ButtonHTMLAttributes } from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "default" | "secondary" | "destructive" | "outline" | "ghost";
|
||||
size?: "sm" | "default" | "lg";
|
||||
}
|
||||
|
||||
const variantStyles: Record<NonNullable<ButtonProps["variant"]>, string> = {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
};
|
||||
|
||||
const sizeStyles: Record<NonNullable<ButtonProps["size"]>, string> = {
|
||||
sm: "h-9 px-3 text-sm",
|
||||
default: "h-10 px-4 py-2",
|
||||
lg: "h-11 px-8 text-lg",
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "default", size = "default", ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
variantStyles[variant],
|
||||
sizeStyles[size],
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create button.stories.tsx**
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Button } from "./button.js";
|
||||
|
||||
const meta = {
|
||||
title: "Atoms/Button",
|
||||
component: Button,
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["default", "secondary", "destructive", "outline", "ghost"],
|
||||
},
|
||||
size: { control: "select", options: ["sm", "default", "lg"] },
|
||||
},
|
||||
} satisfies Meta<typeof Button>;
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: { children: "Button", variant: "default" },
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: { children: "Secondary", variant: "secondary" },
|
||||
};
|
||||
|
||||
export const Destructive: Story = {
|
||||
args: { children: "Destructive", variant: "destructive" },
|
||||
};
|
||||
|
||||
export const Outline: Story = {
|
||||
args: { children: "Outline", variant: "outline" },
|
||||
};
|
||||
|
||||
export const Ghost: Story = {
|
||||
args: { children: "Ghost", variant: "ghost" },
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create button/index.ts and update atoms/index.ts**
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/atoms/button/index.ts
|
||||
export { Button, type ButtonProps } from "./button.js";
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/atoms/index.ts
|
||||
export { Button, type ButtonProps } from "./button/index.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/ui/src/atoms/button/ packages/ui/src/atoms/index.ts
|
||||
git commit -m "feat(ui): add Button atom with Storybook story"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add Input atom
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/ui/src/atoms/input/input.tsx`
|
||||
- Create: `packages/ui/src/atoms/input/input.stories.tsx`
|
||||
- Create: `packages/ui/src/atoms/input/index.ts`
|
||||
- Modify: `packages/ui/src/atoms/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create input.tsx**
|
||||
|
||||
```tsx
|
||||
import { forwardRef, type InputHTMLAttributes } from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create input.stories.tsx**
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Input } from "./input.js";
|
||||
|
||||
const meta = {
|
||||
title: "Atoms/Input",
|
||||
component: Input,
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof Input>;
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: { placeholder: "Enter text..." },
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: { placeholder: "Disabled", disabled: true },
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create index and update atoms barrel**
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/atoms/input/index.ts
|
||||
export { Input, type InputProps } from "./input.js";
|
||||
```
|
||||
|
||||
Update atoms/index.ts to add:
|
||||
```typescript
|
||||
export { Input, type InputProps } from "./input/index.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/ui/src/atoms/input/ packages/ui/src/atoms/index.ts
|
||||
git commit -m "feat(ui): add Input atom with Storybook story"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add Label atom
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/ui/src/atoms/label/label.tsx`
|
||||
- Create: `packages/ui/src/atoms/label/index.ts`
|
||||
- Modify: `packages/ui/src/atoms/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create label.tsx**
|
||||
|
||||
```tsx
|
||||
import { forwardRef, type LabelHTMLAttributes } from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {}
|
||||
|
||||
export const Label = forwardRef<HTMLLabelElement, LabelProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<label
|
||||
className={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Label.displayName = "Label";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create label/index.ts and update atoms barrel**
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/atoms/label/index.ts
|
||||
export { Label, type LabelProps } from "./label.js";
|
||||
```
|
||||
|
||||
Update atoms/index.ts to add:
|
||||
```typescript
|
||||
export { Label, type LabelProps } from "./label/index.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/ui/src/atoms/label/ packages/ui/src/atoms/index.ts
|
||||
git commit -m "feat(ui): add Label atom"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Add FormField molecule
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/ui/src/molecules/form-field/form-field.tsx`
|
||||
- Create: `packages/ui/src/molecules/form-field/form-field.stories.tsx`
|
||||
- Create: `packages/ui/src/molecules/form-field/index.ts`
|
||||
- Modify: `packages/ui/src/molecules/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create form-field.tsx**
|
||||
|
||||
```tsx
|
||||
import { type ReactNode } from "react";
|
||||
import { Label } from "../../atoms/label/index.js";
|
||||
import { Input, type InputProps } from "../../atoms/input/index.js";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
export interface FormFieldProps extends InputProps {
|
||||
label: string;
|
||||
error?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function FormField({
|
||||
label,
|
||||
error,
|
||||
description,
|
||||
className,
|
||||
id,
|
||||
...inputProps
|
||||
}: FormFieldProps) {
|
||||
const fieldId = id ?? label.toLowerCase().replace(/\s+/g, "-");
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2", className)}>
|
||||
<Label htmlFor={fieldId}>{label}</Label>
|
||||
<Input id={fieldId} {...inputProps} />
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create form-field.stories.tsx**
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { FormField } from "./form-field.js";
|
||||
|
||||
const meta = {
|
||||
title: "Molecules/FormField",
|
||||
component: FormField,
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof FormField>;
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: { label: "Email", placeholder: "you@example.com", type: "email" },
|
||||
};
|
||||
|
||||
export const WithDescription: Story = {
|
||||
args: {
|
||||
label: "Username",
|
||||
placeholder: "johndoe",
|
||||
description: "Must be 3-31 characters",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
args: {
|
||||
label: "Password",
|
||||
type: "password",
|
||||
error: "Password must be at least 6 characters",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create index and update molecules barrel**
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/molecules/form-field/index.ts
|
||||
export { FormField, type FormFieldProps } from "./form-field.js";
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/molecules/index.ts
|
||||
export { FormField, type FormFieldProps } from "./form-field/index.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/ui/src/molecules/
|
||||
git commit -m "feat(ui): add FormField molecule (Label + Input + error)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Set up apps/storybook
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/storybook/package.json`
|
||||
- Create: `apps/storybook/.storybook/main.ts`
|
||||
- Create: `apps/storybook/.storybook/preview.ts`
|
||||
|
||||
- [ ] **Step 1: Update apps/storybook/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/storybook",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "storybook build",
|
||||
"dev": "storybook dev -p 6006",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@storybook/addon-essentials": "^8.6.0",
|
||||
"@storybook/react-vite": "^8.6.0",
|
||||
"@tailwindcss/vite": "^4.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"storybook": "^8.6.0",
|
||||
"tailwindcss": "^4.1.0",
|
||||
"vite": "^6.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create .storybook/main.ts**
|
||||
|
||||
```typescript
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
framework: "@storybook/react-vite",
|
||||
stories: [
|
||||
"../../../packages/ui/src/**/*.stories.@(ts|tsx)",
|
||||
],
|
||||
addons: ["@storybook/addon-essentials"],
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
async viteFinal(config) {
|
||||
const { mergeConfig } = await import("vite");
|
||||
const tailwindPlugin = await import("@tailwindcss/vite");
|
||||
|
||||
return mergeConfig(config, {
|
||||
plugins: [tailwindPlugin.default()],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create .storybook/preview.ts**
|
||||
|
||||
```typescript
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "../../../packages/ui/src/styles/globals.css";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run pnpm install and commit**
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
git add apps/storybook/ pnpm-lock.yaml
|
||||
git commit -m "feat(storybook): add Storybook 8 with Tailwind v4 pulling stories from @repo/ui"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Verify build
|
||||
|
||||
- [ ] **Step 1: Run pnpm build**
|
||||
|
||||
Run: `pnpm build`
|
||||
Expected: All workspaces pass.
|
||||
|
||||
- [ ] **Step 2: Verify core tests still pass**
|
||||
|
||||
Run: `cd packages/core && pnpm vitest run`
|
||||
Expected: 22 tests pass.
|
||||
|
||||
- [ ] **Step 3: Commit any remaining fixes**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore: finalize Plan 5 — UI system with Atomic Design"
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
# Plan 6: Documentation + Agent Infrastructure
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Create all AGENTS.md files (~22), root CLAUDE.md, .mcp.json, and docs/ architecture guides so AI agents can navigate and extend the codebase autonomously.
|
||||
|
||||
**Architecture:** 4-tier documentation: Root (CLAUDE.md + AGENTS.md) → Package → Layer → Domain. Each file contains rules, recipes, and tables — not prose.
|
||||
|
||||
**Tasks:** 5 tasks covering root docs, core AGENTS.md files, package AGENTS.md files, app AGENTS.md files, and docs/ folder.
|
||||
@@ -1,557 +0,0 @@
|
||||
# Clean Architecture Monorepo Template — Design Specification
|
||||
|
||||
## Overview
|
||||
|
||||
A general-purpose monorepo application template based on Clean Architecture (Uncle Bob / Lazar Nikolov), designed to serve as the foundation for all future web applications. The template supports multiple frontend frameworks, integrates Payload CMS, and includes comprehensive agent-optimized documentation so AI coding agents can navigate, understand, and extend the codebase autonomously.
|
||||
|
||||
**References:**
|
||||
- [Clean Architecture (Uncle Bob)](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
- [Clean Architecture with Next.js (Lazar Nikolov)](https://medium.com/@heinhtoo/clean-architecture-with-next-js-insights-from-lazar-nikolov-developer-advocate-at-sentry-abe1cb4c7ef3)
|
||||
- [Reference repo](https://github.com/heinhtoo/nextjs-clean-architecture-template)
|
||||
- [Turborepo + shadcn/ui reference](https://github.com/dan5py/turborepo-shadcn-ui)
|
||||
|
||||
---
|
||||
|
||||
## 1. Monorepo Infrastructure
|
||||
|
||||
| Concern | Choice |
|
||||
|---|---|
|
||||
| Orchestrator | Turborepo |
|
||||
| Package manager | pnpm workspaces |
|
||||
| Deployment | Docker-first (docker-compose for local dev) |
|
||||
|
||||
### Monorepo Structure
|
||||
|
||||
```
|
||||
template/
|
||||
├── apps/
|
||||
│ ├── web-next/ # Next.js reference app
|
||||
│ ├── web-tanstack/ # TanStack Start reference app
|
||||
│ ├── cms/ # Thin Next.js shell for Payload admin
|
||||
│ └── storybook/ # Centralized Storybook instance
|
||||
│
|
||||
├── packages/
|
||||
│ ├── core/ # Clean architecture core
|
||||
│ ├── api/ # tRPC router definitions
|
||||
│ ├── api-client/ # Shared React Query hooks
|
||||
│ ├── cms-core/ # Payload config + collections + hooks
|
||||
│ ├── cms-client/ # Dual-mode Payload client (local + HTTP)
|
||||
│ ├── ui/ # shadcn/ui + Atomic Design components
|
||||
│ ├── eslint-config/ # Shared linting rules
|
||||
│ └── typescript-config/ # Shared TS configs
|
||||
│
|
||||
├── tests/
|
||||
│ ├── unit/ # Vitest (mirrors core structure)
|
||||
│ ├── integration/ # Vitest (real DB via test containers)
|
||||
│ └── e2e/ # Playwright (browser tests)
|
||||
│
|
||||
├── docs/ # Architecture guides, ADRs, diagrams
|
||||
├── .mcp.json # MCP server configuration
|
||||
├── docker-compose.yml # Postgres + Payload + all apps + Storybook
|
||||
├── turbo.json # Turborepo task pipeline
|
||||
├── pnpm-workspace.yaml # Workspace config
|
||||
├── CLAUDE.md # Claude Code entry point
|
||||
└── AGENTS.md # Cross-agent root instructions
|
||||
```
|
||||
|
||||
### Framework Support
|
||||
|
||||
Both Next.js and TanStack Start (with TanStack Router and TanStack Query) coexist as first-class reference apps. Both share the same core packages (`@repo/core`, `@repo/api-client`, `@repo/ui`), demonstrating that the clean architecture works across any frontend framework. Projects can use one or both.
|
||||
|
||||
---
|
||||
|
||||
## 2. packages/core — Clean Architecture
|
||||
|
||||
Single `@repo/core` package organized by layer (matching Lazar's reference), with domain-based grouping inside use-cases and controllers (elements of feature-slicing).
|
||||
|
||||
### Layer Structure
|
||||
|
||||
```
|
||||
packages/core/
|
||||
├── src/
|
||||
│ ├── entities/ # INNERMOST: zero deps
|
||||
│ │ ├── models/ # Zod schemas + TS types (user, article, session, cookie)
|
||||
│ │ ├── errors/ # Domain errors (AuthenticationError, NotFoundError, etc.)
|
||||
│ │ └── AGENTS.md
|
||||
│ │
|
||||
│ ├── application/ # USE CASES + INTERFACES
|
||||
│ │ ├── repositories/ # IUsersRepository, IArticlesRepository, etc.
|
||||
│ │ ├── services/ # IAuthService, ITelemetryService, etc.
|
||||
│ │ ├── use-cases/
|
||||
│ │ │ ├── auth/ # sign-in, sign-up, sign-out + AGENTS.md
|
||||
│ │ │ └── content/ # create-article, get-articles + AGENTS.md
|
||||
│ │ └── AGENTS.md
|
||||
│ │
|
||||
│ ├── infrastructure/ # IMPLEMENTATIONS
|
||||
│ │ ├── repositories/ # Payload, Drizzle, and mock implementations
|
||||
│ │ ├── services/ # Better Auth, OpenTelemetry+Sentry, mocks
|
||||
│ │ └── AGENTS.md
|
||||
│ │
|
||||
│ ├── interface-adapters/ # CONTROLLERS
|
||||
│ │ └── controllers/
|
||||
│ │ ├── auth/ # sign-in, sign-up, sign-out controllers
|
||||
│ │ ├── content/ # articles controller
|
||||
│ │ └── AGENTS.md
|
||||
│ │
|
||||
│ └── di/ # INVERSIFYJS WIRING
|
||||
│ ├── container.ts # InversifyJS container
|
||||
│ ├── types.ts # Symbols + DI_RETURN_TYPES
|
||||
│ ├── modules/ # auth.module, content.module
|
||||
│ └── AGENTS.md # Resolution table + registration recipe
|
||||
│
|
||||
└── AGENTS.md # Package overview + dependency rule
|
||||
```
|
||||
|
||||
### Dependency Rule (HARD CONSTRAINTS)
|
||||
|
||||
| Layer | Can import from | NEVER import from |
|
||||
|---|---|---|
|
||||
| entities/ | NOTHING | Everything else |
|
||||
| application/ | entities/ only | infrastructure/, interface-adapters/ |
|
||||
| interface-adapters/ | application/, entities/ | infrastructure/ |
|
||||
| infrastructure/ | application/, entities/, @repo/cms-client, external libs | interface-adapters/ |
|
||||
| di/ | All internal layers | apps/* |
|
||||
|
||||
### Dependency Injection — InversifyJS
|
||||
|
||||
The template uses InversifyJS with symbol-based resolution, following Lazar's reference implementation. Agent documentation mitigates the indirection through:
|
||||
|
||||
1. **Resolution table** in `di/AGENTS.md` mapping every symbol to its interface, production implementation, and mock implementation.
|
||||
2. **Step-by-step registration recipe** for adding new dependencies.
|
||||
3. **tsconfig constraints** documented as "do not remove" (`emitDecoratorMetadata`, `experimentalDecorators`, `reflect-metadata` import).
|
||||
|
||||
DI modules are organized by domain (auth.module.ts, content.module.ts). Test environments swap to mock implementations via `NODE_ENV=test` checks in modules.
|
||||
|
||||
---
|
||||
|
||||
## 3. packages/api + packages/api-client — tRPC & Shared Hooks
|
||||
|
||||
### packages/api — tRPC Router
|
||||
|
||||
```
|
||||
packages/api/
|
||||
├── src/
|
||||
│ ├── trpc.ts # tRPC init, context, middleware
|
||||
│ ├── router/
|
||||
│ │ ├── index.ts # Root appRouter
|
||||
│ │ ├── auth.router.ts # signIn, signUp, signOut procedures
|
||||
│ │ └── content.router.ts # articles CRUD procedures
|
||||
│ └── index.ts # Exports AppRouter type
|
||||
└── AGENTS.md
|
||||
```
|
||||
|
||||
tRPC is the single data path for all data access, including Payload CMS content. Each tRPC procedure calls a controller from `@repo/core/interface-adapters`. Input validation uses Zod schemas from `@repo/core/entities`. Business logic never lives in routers.
|
||||
|
||||
**tRPC HTTP handler:** Each app hosts its own tRPC endpoint. `apps/web-next` uses Next.js API routes (`app/api/trpc/[trpc]/route.ts`), `apps/web-tanstack` uses TanStack Start's server functions. Both import the `appRouter` from `@repo/api` and serve it. The router definition is shared; the HTTP transport is app-specific.
|
||||
|
||||
### packages/api-client — Shared React Query Hooks
|
||||
|
||||
```
|
||||
packages/api-client/
|
||||
├── src/
|
||||
│ ├── provider.tsx # tRPC + QueryClient provider
|
||||
│ ├── hooks/
|
||||
│ │ ├── auth/ # use-sign-in, use-session
|
||||
│ │ ├── content/ # use-articles, use-create-article
|
||||
│ │ └── index.ts # Re-exports all hooks
|
||||
│ └── index.ts
|
||||
└── AGENTS.md
|
||||
```
|
||||
|
||||
Both `apps/web-next` and `apps/web-tanstack` wrap their root with `<ApiProvider>` and use identical hooks. The hooks are framework-agnostic — they never import from Next.js or TanStack internals.
|
||||
|
||||
---
|
||||
|
||||
## 4. Payload CMS Architecture
|
||||
|
||||
### packages/cms-core — Payload Definition
|
||||
|
||||
All Payload CMS configuration lives in this standalone package, not inside `apps/cms`. This includes `payload.config.ts`, all collection definitions, globals, hooks, and access control.
|
||||
|
||||
```
|
||||
packages/cms-core/
|
||||
├── src/
|
||||
│ ├── payload.config.ts # Full Payload config
|
||||
│ ├── collections/
|
||||
│ │ ├── articles/
|
||||
│ │ │ ├── index.ts # CollectionConfig
|
||||
│ │ │ ├── fields.ts # Field definitions
|
||||
│ │ │ ├── hooks/ # Thin adapters → use cases
|
||||
│ │ │ └── access/ # Access control rules
|
||||
│ │ ├── users/
|
||||
│ │ └── media/
|
||||
│ ├── globals/ # Site settings, navigation
|
||||
│ └── index.ts # Exports config + all collections
|
||||
└── AGENTS.md
|
||||
```
|
||||
|
||||
`apps/cms` is a thin Next.js shell that imports the config from `@repo/cms-core` and serves the Payload admin panel. It contains almost no custom code.
|
||||
|
||||
### Payload Hook Architecture
|
||||
|
||||
Hooks are categorized into two types:
|
||||
|
||||
**CMS-operational (stay in cms-core hooks):**
|
||||
- Auto-generating slugs from titles
|
||||
- Image resizing/optimization
|
||||
- Populating default field values
|
||||
- CMS-specific access control
|
||||
|
||||
**Business logic (delegate to use cases):**
|
||||
- Sending notifications on publish
|
||||
- Enforcing business validation rules
|
||||
- Updating related records across domains
|
||||
- Triggering workflows
|
||||
|
||||
Business logic hooks are thin adapters (max 5-10 lines) that map Payload's hook arguments to use case inputs and call use cases from `@repo/core/application`. They never import from `@repo/core/infrastructure` or call external services directly.
|
||||
|
||||
**Rule of thumb:** If deleting the hook would break a business requirement, the logic must be in a use case. If it would only break a CMS convenience feature, it can stay in the hook.
|
||||
|
||||
### packages/cms-client — Dual-Mode Payload Client
|
||||
|
||||
```
|
||||
packages/cms-client/
|
||||
├── src/
|
||||
│ ├── client.ts # createPayloadClient()
|
||||
│ ├── local-client.ts # Local API (direct Payload instance)
|
||||
│ ├── http-client.ts # HTTP REST fallback
|
||||
│ ├── types.ts # Generated via payload generate:types
|
||||
│ └── index.ts
|
||||
└── AGENTS.md
|
||||
```
|
||||
|
||||
The client supports two modes:
|
||||
|
||||
- **Local mode (primary):** Receives a Payload instance, calls `payload.find()`, `payload.findByID()`, etc. directly. Full access to Payload's query capabilities (where, sort, limit, depth, page, populate). Used by all server-side apps.
|
||||
- **HTTP mode (fallback):** Uses Payload's REST API. For external services that don't have access to a Payload instance.
|
||||
|
||||
**Initialization:** The Payload instance is **injected, not imported**. At app startup, each app creates a Payload instance using the config from `@repo/cms-core` and passes it to `createPayloadClient()`. This prevents circular dependencies. The initialization code lives in each app's server entry point (e.g., `apps/web-next/src/lib/payload.ts`, `apps/web-tanstack/src/lib/payload.ts`) — it is NOT in any shared package.
|
||||
|
||||
| Context | Mode | How |
|
||||
|---|---|---|
|
||||
| apps/cms server-side | Local | Same process as Payload |
|
||||
| apps/web-next server-side | Local | Initializes own Payload instance, shares DB |
|
||||
| apps/web-tanstack server-side | Local | Initializes own Payload instance, shares DB |
|
||||
| Client-side (browser) | N/A | Goes through tRPC, server handles it |
|
||||
| External services | HTTP | createPayloadClient({mode: "http", baseURL}) |
|
||||
|
||||
**This package is standalone.** It never imports from `@repo/cms-core`, `@repo/core`, or `apps/*`.
|
||||
|
||||
**Type generation:** Payload's built-in `payload generate:types` reads `payload.config.ts` from `@repo/cms-core` and outputs TypeScript types to `cms-client/src/types.ts`. This runs as a Turborepo task in the build pipeline.
|
||||
|
||||
### Migrations
|
||||
|
||||
Payload CMS manages its own database migrations via `payload migrate`. This is the primary migration system since most data tables are defined as Payload collections. Drizzle migrations are optional — only needed for app-specific tables that Payload doesn't manage (e.g., session tokens, analytics, queues).
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Flow
|
||||
|
||||
Complete request lifecycle from UI to database:
|
||||
|
||||
```
|
||||
UI Component (Next.js or TanStack Start)
|
||||
→ useArticles() @repo/api-client hook
|
||||
→ trpc.content.list @repo/api router procedure
|
||||
→ articlesController.list() @repo/core/interface-adapters
|
||||
→ getArticlesUseCase() @repo/core/application
|
||||
→ getInjection("IArticlesRepo") InversifyJS resolves at runtime
|
||||
→ PayloadArticlesRepository @repo/core/infrastructure
|
||||
→ PayloadClient.find(...) @repo/cms-client (LOCAL mode)
|
||||
→ Payload Local API Direct DB access, no HTTP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 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)
|
||||
```
|
||||
|
||||
### Circular Dependency Prevention — HARD RULES
|
||||
|
||||
These rules are non-negotiable and enforced via documentation + linting:
|
||||
|
||||
- **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
|
||||
|
||||
---
|
||||
|
||||
## 7. Technology Stack
|
||||
|
||||
| Concern | Choice | Architecture Layer |
|
||||
|---|---|---|
|
||||
| Monorepo | Turborepo + pnpm workspaces | Infrastructure |
|
||||
| Frameworks | Next.js + TanStack Start (coexist) | Frameworks & Drivers |
|
||||
| CMS | Payload CMS 3.x (standalone in cms-core) | Frameworks & Drivers |
|
||||
| CMS Client | Dual-mode: Local API (primary) + HTTP (fallback) | Infrastructure |
|
||||
| API | tRPC (single data path, wraps all data) | Interface Adapters |
|
||||
| DI | InversifyJS + agent documentation | Frameworks & Drivers |
|
||||
| Validation | Zod | All layers |
|
||||
| Database | Agnostic → Drizzle + PostgreSQL (optional, alongside Payload) | Infrastructure |
|
||||
| Auth | Agnostic → Better Auth default | Infrastructure |
|
||||
| Observability | OpenTelemetry interfaces → Sentry backend | Infrastructure |
|
||||
| State (server) | TanStack Query (via tRPC) | Frameworks & Drivers |
|
||||
| State (client) | Zustand | Frameworks & Drivers |
|
||||
| Styling | Tailwind CSS v4 + shadcn/ui (@repo/ui) | Frameworks & Drivers |
|
||||
| UI Architecture | Atomic Design (atoms, molecules, organisms, templates) | Frameworks & Drivers |
|
||||
| Testing (unit/integ) | Vitest | All layers |
|
||||
| Testing (E2E) | Playwright | Frameworks & Drivers |
|
||||
| Deployment | Docker-first + docker-compose | Infrastructure |
|
||||
| Migrations | Payload primary, Drizzle optional | Infrastructure |
|
||||
| Type generation | payload generate:types → cms-client/types.ts | Build pipeline |
|
||||
|
||||
---
|
||||
|
||||
## 8. UI Architecture — Atomic Design + shadcn/ui + Storybook
|
||||
|
||||
### @repo/ui Package Structure
|
||||
|
||||
```
|
||||
packages/ui/
|
||||
├── src/
|
||||
│ ├── atoms/ # shadcn primitives + custom atoms
|
||||
│ │ ├── button/
|
||||
│ │ │ ├── button.tsx # Component
|
||||
│ │ │ ├── button.stories.tsx # Co-located Storybook story
|
||||
│ │ │ ├── button.test.tsx # Unit test
|
||||
│ │ │ └── index.ts # Export
|
||||
│ │ ├── input/
|
||||
│ │ ├── label/
|
||||
│ │ ├── badge/
|
||||
│ │ ├── ... (separator, skeleton, avatar, icon, spinner, etc.)
|
||||
│ │ ├── index.ts # Re-exports all atoms
|
||||
│ │ └── AGENTS.md
|
||||
│ │
|
||||
│ ├── molecules/ # 2-3 atoms combined, single responsibility
|
||||
│ │ ├── form-field/ # Label + Input + Error
|
||||
│ │ ├── search-bar/ # Input + Button + Icon
|
||||
│ │ ├── tooltip/
|
||||
│ │ ├── popover/
|
||||
│ │ ├── select/
|
||||
│ │ ├── index.ts
|
||||
│ │ └── AGENTS.md
|
||||
│ │
|
||||
│ ├── organisms/ # Complex, self-contained UI sections
|
||||
│ │ ├── data-table/ # With sub-components (header, pagination)
|
||||
│ │ ├── dialog/
|
||||
│ │ ├── card/
|
||||
│ │ ├── header/
|
||||
│ │ ├── sidebar/
|
||||
│ │ ├── command-palette/
|
||||
│ │ ├── index.ts
|
||||
│ │ └── AGENTS.md
|
||||
│ │
|
||||
│ ├── templates/ # Page-level layouts with content slots
|
||||
│ │ ├── dashboard-layout/
|
||||
│ │ ├── auth-layout/
|
||||
│ │ ├── content-layout/
|
||||
│ │ ├── index.ts
|
||||
│ │ └── AGENTS.md
|
||||
│ │
|
||||
│ ├── hooks/ # Shared UI hooks (use-media-query, use-debounce)
|
||||
│ ├── lib/ # Utilities (cn() helper)
|
||||
│ └── styles/ # globals.css, design tokens
|
||||
│
|
||||
├── components.json # shadcn/ui config (aliases point to atoms/)
|
||||
├── tailwind.config.ts
|
||||
└── AGENTS.md # Package overview + atomic classification guide
|
||||
```
|
||||
|
||||
### Atomic Design Import Rules
|
||||
|
||||
| Level | Can import from | NEVER import from |
|
||||
|---|---|---|
|
||||
| Atoms | lib/, hooks/, styles/ | molecules/, organisms/, templates/ |
|
||||
| Molecules | atoms/, lib/, hooks/ | organisms/, templates/ |
|
||||
| Organisms | atoms/, molecules/, lib/, hooks/ | templates/ |
|
||||
| Templates | atoms/, molecules/, organisms/, lib/, hooks/ | (top level) |
|
||||
| Pages | Everything from @repo/ui + @repo/api-client | **Live in apps/, NOT in @repo/ui** |
|
||||
|
||||
### Component Rules
|
||||
|
||||
- **Atoms:** No margins/positioning, no state, no business logic. Pure visual elements.
|
||||
- **Molecules:** Single responsibility, minimal controlled state. Combine 2-3 atoms.
|
||||
- **Organisms:** Can have internal state and sub-components. Self-contained sections.
|
||||
- **Templates:** Use children/slots for content. NEVER hard-code content.
|
||||
- **All levels:** Co-locate `.stories.tsx` and `.test.tsx` next to the component.
|
||||
|
||||
### shadcn/ui Integration
|
||||
|
||||
`pnpm ui add [component]` lands components in `atoms/` by default (configured via `components.json` aliases). After adding, the component is classified using the guide in `AGENTS.md` and relocated to the correct atomic level if needed.
|
||||
|
||||
### Storybook
|
||||
|
||||
`apps/storybook` is a centralized Storybook instance using `@storybook/react-vite`. It pulls stories from `packages/ui/src/**/*.stories.tsx`. Story titles follow the pattern `"Level/ComponentName"` (e.g., `"Atoms/Button"`, `"Organisms/DataTable"`), creating a sidebar organized by atomic level.
|
||||
|
||||
---
|
||||
|
||||
## 9. Agent Infrastructure
|
||||
|
||||
### MCP Server Configuration
|
||||
|
||||
Project-level `.mcp.json` in the monorepo root, shared via git:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"storybook": {
|
||||
"type": "http",
|
||||
"url": "http://localhost:6006/mcp"
|
||||
},
|
||||
"playwright": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["@anthropic-ai/playwright-mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Storybook MCP** (via `@storybook/addon-mcp` in `apps/storybook`):
|
||||
- Component discovery: `list-all-documentation`
|
||||
- Component docs: `get-documentation`, `get-documentation-for-story`
|
||||
- Story authoring: `get-storybook-story-instructions`, `preview-stories`
|
||||
- Testing: `run-story-tests` (accessibility + interaction tests with autonomous fix loop)
|
||||
|
||||
**Playwright MCP:**
|
||||
- Browser automation for E2E validation
|
||||
- Accessibility snapshots
|
||||
- Visual verification of rendered components
|
||||
|
||||
### Agent Workflow
|
||||
|
||||
When building UI:
|
||||
1. Query Storybook MCP to discover existing components
|
||||
2. Read AGENTS.md at the target atomic level for rules
|
||||
3. Write component + co-located story
|
||||
4. Run story tests via Storybook MCP
|
||||
5. Autonomous fix loop if tests fail
|
||||
6. Visual validation via Playwright MCP
|
||||
|
||||
### Documentation Architecture — 4 Tiers
|
||||
|
||||
**Tier 1 — Root:**
|
||||
- `CLAUDE.md`: Claude Code entry point, project overview, quick start commands
|
||||
- `AGENTS.md`: Cross-agent instructions, monorepo package map, dependency flow, hard rules, end-to-end "add a feature" recipe
|
||||
- `docs/`: Architecture guides, how-to guides, ADRs, Mermaid diagrams
|
||||
|
||||
**Tier 2 — Package:**
|
||||
Each package gets an `AGENTS.md` with: purpose, public API, import rules, step-by-step recipes for common tasks. Key packages have specialized content:
|
||||
- `core/AGENTS.md`: Layer diagram, import rules table, DI resolution table, naming conventions
|
||||
- `cms-core/AGENTS.md`: Hook rules (do/don't), collection patterns, access control
|
||||
- `cms-client/AGENTS.md`: Dual-mode usage table, initialization patterns, standalone rule
|
||||
- `ui/AGENTS.md`: Atomic classification guide, shadcn workflow, story template
|
||||
|
||||
**Tier 3 — Layer (inside core):**
|
||||
- `entities/AGENTS.md`: Zero imports rule, model template, error template
|
||||
- `application/AGENTS.md`: Imports entities/ only, use case template, interface naming
|
||||
- `infrastructure/AGENTS.md`: Implementation patterns, mock naming, provider naming
|
||||
- `di/AGENTS.md`: Resolution table, registration recipe, scope guidance
|
||||
- `controllers/AGENTS.md`: Validate → call use case pattern, error mapping
|
||||
|
||||
**Tier 4 — Domain (business logic):**
|
||||
- `use-cases/auth/AGENTS.md`: Auth business rules, invariants, error cases, dependencies
|
||||
- `use-cases/content/AGENTS.md`: Content business rules, publishing workflow, error cases
|
||||
- `atoms/AGENTS.md`: Classification criteria, shadcn atom list, "no margins" rule
|
||||
- `molecules/AGENTS.md`: Single responsibility rule, composition examples
|
||||
- `organisms/AGENTS.md`: Sub-component patterns, internal state guidance
|
||||
- `templates/AGENTS.md`: Content slots pattern, "never hard-code content" rule
|
||||
|
||||
**Total: ~22 AGENTS.md files, ~16 docs files.**
|
||||
|
||||
### docs/ Folder Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── architecture/
|
||||
│ ├── overview.md # High-level architecture diagram
|
||||
│ ├── clean-architecture.md # Uncle Bob's principles applied
|
||||
│ ├── dependency-flow.md # Complete dependency graph
|
||||
│ ├── data-flow.md # Request lifecycle
|
||||
│ └── circular-dep-prevention.md # Rules + examples
|
||||
│
|
||||
├── guides/
|
||||
│ ├── adding-a-feature.md # End-to-end walkthrough
|
||||
│ ├── adding-a-collection.md # Payload CMS collection
|
||||
│ ├── adding-a-component.md # Atomic design classification
|
||||
│ ├── testing-strategy.md # What to test at each layer
|
||||
│ ├── deployment.md # Docker build + deploy
|
||||
│ └── mcp-setup.md # Storybook MCP + Playwright MCP
|
||||
│
|
||||
├── decisions/
|
||||
│ ├── adr-001-monorepo-tool.md # Why Turborepo + pnpm
|
||||
│ ├── adr-002-di-framework.md # Why InversifyJS
|
||||
│ ├── adr-003-cms-separation.md # Why cms-core vs cms-client
|
||||
│ ├── adr-004-dual-mode-client.md # Why local + HTTP modes
|
||||
│ └── adr-005-atomic-design.md # Why atomic design for UI
|
||||
│
|
||||
└── diagrams/
|
||||
├── monorepo-structure.md # Mermaid diagram
|
||||
├── dependency-graph.md # Mermaid diagram
|
||||
└── data-flow.md # Mermaid diagram
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Docker & Local Development
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
ports: ["5432:5432"]
|
||||
|
||||
cms:
|
||||
build: ./apps/cms
|
||||
depends_on: [postgres]
|
||||
ports: ["3001:3000"] # Payload admin at localhost:3001
|
||||
|
||||
web-next:
|
||||
build: ./apps/web-next
|
||||
depends_on: [cms]
|
||||
ports: ["3000:3000"] # Next.js at localhost:3000
|
||||
|
||||
web-tanstack:
|
||||
build: ./apps/web-tanstack
|
||||
depends_on: [cms]
|
||||
ports: ["3002:3000"] # TanStack at localhost:3002
|
||||
|
||||
storybook:
|
||||
build: ./apps/storybook
|
||||
ports: ["6006:6006"] # Storybook at localhost:6006
|
||||
```
|
||||
|
||||
One command: `docker compose up` — spins up Postgres, Payload CMS admin, both reference apps, and Storybook.
|
||||
|
||||
---
|
||||
|
||||
## 11. Testing Strategy
|
||||
|
||||
| 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 (unit) + Storybook | Rendering, props, accessibility |
|
||||
| Full app | Playwright (E2E) | User flows across both Next.js and TanStack Start |
|
||||
Reference in New Issue
Block a user