Aligns tooling packages with the core-* naming convention used by all other foundation packages (core-shared, core-cms, core-api, core-trpc, core-ui). Updates ~50 files: package.json names, devDependencies, tsconfig extends, eslint.config imports, vitest.config imports, AGENTS.md references, and the boundaries plugin patterns to match the new paths. The tooling-specific patterns in boundaries/elements are now ordered BEFORE the broader core-* pattern to ensure correct first-match-wins behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1545 lines
40 KiB
Markdown
1545 lines
40 KiB
Markdown
# Vertical Refactor — Plan 1: Foundation
|
|
|
|
> **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 the five `core-*` packages, populate `core-shared` with generic primitives + tRPC plumbing, lift the Payload config into a stub `core-cms` package, and repoint `apps/cms` at it — leaving `pnpm install`, `typecheck`, and the CMS dev server all green at the end.
|
|
|
|
**Architecture:** Three sequential phases. Phase 1 creates empty package skeletons (no code yet) and updates path aliases. Phase 2 fills `core-shared` with reusable Payload primitives (slug field, SEO fields, CTA block, access helpers, hooks) and tRPC init/context. Phase 3 moves the Payload config into `core-cms` (with empty collection/global arrays), updates `apps/cms` to import from the new package, and verifies the admin UI still boots.
|
|
|
|
**Tech Stack:** pnpm workspaces, Turborepo, TypeScript 5.8, Vitest 3.x, Payload 3.14, tRPC 11, Zod 3.
|
|
|
|
**Plan position:** This is plan 1 of 4 in the vertical-refactor sequence:
|
|
- Plan 1 (this doc): Foundation — Phases 1-3
|
|
- Plan 2: Feature migrations — Phases 4-5 (blog canonical, then auth + marketing-pages + navigation + media)
|
|
- Plan 3: App + UI integration — Phases 6-7 (core-trpc, route handlers, example pages, core-ui migration)
|
|
- Plan 4: Cleanup, enforcement, e2e, docs — Phases 8-11
|
|
|
|
**Spec reference:** `docs/superpowers/specs/2026-04-21-vertical-monorepo-refactor-design.md`
|
|
|
|
---
|
|
|
|
## File Structure (this plan creates/modifies)
|
|
|
|
**Create:**
|
|
- `tsconfig.base.json` (root — does not exist yet)
|
|
- `packages/core-shared/{package.json,tsconfig.json,turbo.json,vitest.config.ts}`
|
|
- `packages/core-shared/src/index.ts`
|
|
- `packages/core-shared/src/lib/{env.ts,date.ts}` + tests
|
|
- `packages/core-shared/src/payload/index.ts`
|
|
- `packages/core-shared/src/payload/access/is-admin.ts` + test
|
|
- `packages/core-shared/src/payload/fields/{slug-field.ts,seo-fields.ts}` + tests
|
|
- `packages/core-shared/src/payload/blocks/cta.ts` + test
|
|
- `packages/core-shared/src/payload/hooks/{set-published-at.ts,slugify-if-missing.ts}` + tests
|
|
- `packages/core-shared/src/trpc/{init.ts,context.ts}`
|
|
- `packages/core-cms/{package.json,tsconfig.json,turbo.json}`
|
|
- `packages/core-cms/src/{index.ts,payload.config.ts,generated-types.ts}` (generated-types is an empty stub initially)
|
|
- `packages/core-api/{package.json,tsconfig.json,turbo.json}`
|
|
- `packages/core-api/src/{index.ts,root.ts}`
|
|
- `packages/core-trpc/{package.json,tsconfig.json,turbo.json}`
|
|
- `packages/core-trpc/src/index.ts`
|
|
- `packages/core-ui/{package.json,tsconfig.json,turbo.json}`
|
|
- `packages/core-ui/src/index.ts`
|
|
- `packages/typescript-config/vitest.base.ts` (shared vitest config)
|
|
|
|
**Modify:**
|
|
- `apps/cms/src/payload.config.ts` (change re-export from `@repo/cms-core` → `@repo/core-cms`)
|
|
- `apps/cms/package.json` (add `@repo/core-cms`, leave `@repo/cms-core` for now — removed in Plan 4)
|
|
|
|
**Do NOT touch in this plan:**
|
|
- `packages/core/`, `packages/api/`, `packages/api-client/`, `packages/cms-core/`, `packages/cms-client/`, `packages/ui/` — these continue to exist; deleted in Plan 4.
|
|
|
|
---
|
|
|
|
## Phase 1: Scaffold core-* packages
|
|
|
|
### Task 1.1: Add root tsconfig.base.json with path aliases
|
|
|
|
**Files:**
|
|
- Create: `tsconfig.base.json`
|
|
|
|
- [ ] **Step 1: Create the file**
|
|
|
|
```json
|
|
{
|
|
"$schema": "https://json.schemastore.org/tsconfig",
|
|
"compilerOptions": {
|
|
"baseUrl": ".",
|
|
"paths": {
|
|
"@repo/core-shared": ["packages/core-shared/src/index.ts"],
|
|
"@repo/core-shared/payload": ["packages/core-shared/src/payload/index.ts"],
|
|
"@repo/core-shared/trpc/init": ["packages/core-shared/src/trpc/init.ts"],
|
|
"@repo/core-shared/trpc/context": ["packages/core-shared/src/trpc/context.ts"],
|
|
"@repo/core-cms": ["packages/core-cms/src/index.ts"],
|
|
"@repo/core-cms/generated-types": ["packages/core-cms/src/generated-types.ts"],
|
|
"@repo/core-api": ["packages/core-api/src/index.ts"],
|
|
"@repo/core-trpc": ["packages/core-trpc/src/index.ts"],
|
|
"@repo/core-ui": ["packages/core-ui/src/index.ts"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add tsconfig.base.json
|
|
git commit -m "build: add root tsconfig.base.json with core-* path aliases"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.2: Scaffold @repo/core-shared package
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/package.json`
|
|
- Create: `packages/core-shared/tsconfig.json`
|
|
- Create: `packages/core-shared/turbo.json`
|
|
- Create: `packages/core-shared/src/index.ts`
|
|
|
|
- [ ] **Step 1: Create package.json**
|
|
|
|
```json
|
|
{
|
|
"name": "@repo/core-shared",
|
|
"private": true,
|
|
"version": "0.0.0",
|
|
"type": "module",
|
|
"exports": {
|
|
".": "./src/index.ts",
|
|
"./payload": "./src/payload/index.ts",
|
|
"./trpc/init": "./src/trpc/init.ts",
|
|
"./trpc/context": "./src/trpc/context.ts"
|
|
},
|
|
"scripts": {
|
|
"build": "tsc --noEmit",
|
|
"lint": "eslint .",
|
|
"test": "vitest run",
|
|
"typecheck": "tsc --noEmit"
|
|
},
|
|
"dependencies": {
|
|
"@trpc/server": "^11.0.0",
|
|
"payload": "^3.14.0",
|
|
"superjson": "^2.2.1",
|
|
"zod": "^3.24.0"
|
|
},
|
|
"devDependencies": {
|
|
"@repo/core-eslint": "workspace:*",
|
|
"@repo/core-typescript": "workspace:*",
|
|
"@types/node": "^22.0.0",
|
|
"vitest": "^3.1.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Create tsconfig.json**
|
|
|
|
```json
|
|
{
|
|
"extends": "@repo/core-typescript/base.json",
|
|
"compilerOptions": {
|
|
"outDir": "dist",
|
|
"rootDir": "src"
|
|
},
|
|
"include": ["src/**/*"],
|
|
"exclude": ["node_modules", "dist"]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Create turbo.json (with tag)**
|
|
|
|
```json
|
|
{
|
|
"extends": ["//"],
|
|
"tags": ["core"]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Create empty index.ts**
|
|
|
|
```typescript
|
|
export {};
|
|
```
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared
|
|
git commit -m "feat(core-shared): scaffold empty package with exports + tags"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.3: Scaffold @repo/core-cms package
|
|
|
|
**Files:**
|
|
- Create: `packages/core-cms/package.json`
|
|
- Create: `packages/core-cms/tsconfig.json`
|
|
- Create: `packages/core-cms/turbo.json`
|
|
- Create: `packages/core-cms/src/index.ts`
|
|
- Create: `packages/core-cms/src/generated-types.ts`
|
|
|
|
- [ ] **Step 1: Create package.json**
|
|
|
|
```json
|
|
{
|
|
"name": "@repo/core-cms",
|
|
"private": true,
|
|
"version": "0.0.0",
|
|
"type": "module",
|
|
"exports": {
|
|
".": "./src/index.ts",
|
|
"./generated-types": "./src/generated-types.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/core-eslint": "workspace:*",
|
|
"@repo/core-typescript": "workspace:*",
|
|
"@types/node": "^22.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Create tsconfig.json**
|
|
|
|
```json
|
|
{
|
|
"extends": "@repo/core-typescript/base.json",
|
|
"compilerOptions": {
|
|
"outDir": "dist",
|
|
"rootDir": "src",
|
|
"lib": ["ES2022", "DOM"]
|
|
},
|
|
"include": ["src/**/*"],
|
|
"exclude": ["node_modules", "dist"]
|
|
}
|
|
```
|
|
|
|
> Note: `lib: ["ES2022", "DOM"]` is needed because Payload's types reference DOM globals (e.g., `URL`).
|
|
|
|
- [ ] **Step 3: Create turbo.json**
|
|
|
|
```json
|
|
{
|
|
"extends": ["//"],
|
|
"tags": ["core"]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Create empty index.ts** (will export the config in Phase 3)
|
|
|
|
```typescript
|
|
export {};
|
|
```
|
|
|
|
- [ ] **Step 5: Create empty generated-types.ts placeholder**
|
|
|
|
```typescript
|
|
// Generated by Payload — do not edit by hand.
|
|
// This file is regenerated by `pnpm generate:types` in apps/cms.
|
|
export {};
|
|
```
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add packages/core-cms
|
|
git commit -m "feat(core-cms): scaffold empty package with exports + tags"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.4: Scaffold @repo/core-api package
|
|
|
|
**Files:**
|
|
- Create: `packages/core-api/package.json`
|
|
- Create: `packages/core-api/tsconfig.json`
|
|
- Create: `packages/core-api/turbo.json`
|
|
- Create: `packages/core-api/src/root.ts`
|
|
- Create: `packages/core-api/src/index.ts`
|
|
|
|
- [ ] **Step 1: Create package.json**
|
|
|
|
```json
|
|
{
|
|
"name": "@repo/core-api",
|
|
"private": true,
|
|
"version": "0.0.0",
|
|
"type": "module",
|
|
"exports": {
|
|
".": "./src/index.ts"
|
|
},
|
|
"scripts": {
|
|
"build": "tsc --noEmit",
|
|
"lint": "eslint .",
|
|
"typecheck": "tsc --noEmit"
|
|
},
|
|
"dependencies": {
|
|
"@repo/core-shared": "workspace:*",
|
|
"@trpc/server": "^11.0.0"
|
|
},
|
|
"devDependencies": {
|
|
"@repo/core-eslint": "workspace:*",
|
|
"@repo/core-typescript": "workspace:*",
|
|
"@types/node": "^22.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Create tsconfig.json**
|
|
|
|
```json
|
|
{
|
|
"extends": "@repo/core-typescript/base.json",
|
|
"compilerOptions": {
|
|
"outDir": "dist",
|
|
"rootDir": "src"
|
|
},
|
|
"include": ["src/**/*"],
|
|
"exclude": ["node_modules", "dist"]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Create turbo.json**
|
|
|
|
```json
|
|
{
|
|
"extends": ["//"],
|
|
"tags": ["core"]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Create root.ts (empty appRouter that compiles)**
|
|
|
|
```typescript
|
|
import { router } from "@repo/core-shared/trpc/init";
|
|
|
|
export const appRouter = router({});
|
|
|
|
export type AppRouter = typeof appRouter;
|
|
```
|
|
|
|
> Note: This depends on `@repo/core-shared/trpc/init` which will exist after Task 2.13. To make typecheck pass in isolation now, this file imports a not-yet-existing module — but Phase 1 verification (Task 1.7) only runs `pnpm install`, not `typecheck`. Typecheck runs after Phase 2 completes.
|
|
|
|
- [ ] **Step 5: Create index.ts**
|
|
|
|
```typescript
|
|
export { appRouter, type AppRouter } from "./root";
|
|
```
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add packages/core-api
|
|
git commit -m "feat(core-api): scaffold empty appRouter aggregator"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.5: Scaffold @repo/core-trpc package
|
|
|
|
**Files:**
|
|
- Create: `packages/core-trpc/package.json`
|
|
- Create: `packages/core-trpc/tsconfig.json`
|
|
- Create: `packages/core-trpc/turbo.json`
|
|
- Create: `packages/core-trpc/src/index.ts`
|
|
|
|
- [ ] **Step 1: Create package.json**
|
|
|
|
```json
|
|
{
|
|
"name": "@repo/core-trpc",
|
|
"private": true,
|
|
"version": "0.0.0",
|
|
"type": "module",
|
|
"exports": {
|
|
".": "./src/index.ts"
|
|
},
|
|
"scripts": {
|
|
"build": "tsc --noEmit",
|
|
"lint": "eslint .",
|
|
"typecheck": "tsc --noEmit"
|
|
},
|
|
"dependencies": {
|
|
"@repo/core-api": "workspace:*",
|
|
"@trpc/client": "^11.0.0",
|
|
"@trpc/react-query": "^11.0.0",
|
|
"@trpc/server": "^11.0.0",
|
|
"@tanstack/react-query": "^5.66.0",
|
|
"react": "^19.0.0",
|
|
"superjson": "^2.2.1"
|
|
},
|
|
"devDependencies": {
|
|
"@repo/core-eslint": "workspace:*",
|
|
"@repo/core-typescript": "workspace:*",
|
|
"@types/react": "^19.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Create tsconfig.json (jsx for React provider files added in Plan 3)**
|
|
|
|
```json
|
|
{
|
|
"extends": "@repo/core-typescript/base.json",
|
|
"compilerOptions": {
|
|
"outDir": "dist",
|
|
"rootDir": "src",
|
|
"lib": ["ES2022", "DOM"],
|
|
"jsx": "preserve"
|
|
},
|
|
"include": ["src/**/*"],
|
|
"exclude": ["node_modules", "dist"]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Create turbo.json**
|
|
|
|
```json
|
|
{
|
|
"extends": ["//"],
|
|
"tags": ["core"]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Create empty index.ts (client + providers added in Plan 3)**
|
|
|
|
```typescript
|
|
export {};
|
|
```
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/core-trpc
|
|
git commit -m "feat(core-trpc): scaffold empty package (client + providers in Plan 3)"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.6: Scaffold @repo/core-ui package
|
|
|
|
**Files:**
|
|
- Create: `packages/core-ui/package.json`
|
|
- Create: `packages/core-ui/tsconfig.json`
|
|
- Create: `packages/core-ui/turbo.json`
|
|
- Create: `packages/core-ui/src/index.ts`
|
|
|
|
- [ ] **Step 1: Create package.json**
|
|
|
|
```json
|
|
{
|
|
"name": "@repo/core-ui",
|
|
"private": true,
|
|
"version": "0.0.0",
|
|
"type": "module",
|
|
"exports": {
|
|
".": "./src/index.ts"
|
|
},
|
|
"scripts": {
|
|
"build": "tsc --noEmit",
|
|
"lint": "eslint .",
|
|
"typecheck": "tsc --noEmit"
|
|
},
|
|
"dependencies": {
|
|
"clsx": "^2.1.1",
|
|
"react": "^19.0.0",
|
|
"tailwind-merge": "^3.0.0"
|
|
},
|
|
"devDependencies": {
|
|
"@repo/core-eslint": "workspace:*",
|
|
"@repo/core-typescript": "workspace:*",
|
|
"@types/react": "^19.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Create tsconfig.json**
|
|
|
|
```json
|
|
{
|
|
"extends": "@repo/core-typescript/base.json",
|
|
"compilerOptions": {
|
|
"outDir": "dist",
|
|
"rootDir": "src",
|
|
"lib": ["ES2022", "DOM"],
|
|
"jsx": "preserve"
|
|
},
|
|
"include": ["src/**/*"],
|
|
"exclude": ["node_modules", "dist"]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Create turbo.json**
|
|
|
|
```json
|
|
{
|
|
"extends": ["//"],
|
|
"tags": ["core"]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Create empty index.ts (contents migrated from packages/ui in Plan 3)**
|
|
|
|
```typescript
|
|
export {};
|
|
```
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/core-ui
|
|
git commit -m "feat(core-ui): scaffold empty package (contents migrated in Plan 3)"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.7: Install + verify Phase 1
|
|
|
|
- [ ] **Step 1: Install deps (picks up new workspace packages)**
|
|
|
|
Run: `pnpm install`
|
|
|
|
Expected: install completes; new packages registered in workspace; lockfile updated.
|
|
|
|
- [ ] **Step 2: Verify all five packages registered**
|
|
|
|
Run: `pnpm list --recursive --depth=-1 | grep -E "core-(shared|cms|api|trpc|ui)"`
|
|
|
|
Expected: all five names appear.
|
|
|
|
- [ ] **Step 3: Commit lockfile**
|
|
|
|
```bash
|
|
git add pnpm-lock.yaml
|
|
git commit -m "build: update lockfile for new core-* packages"
|
|
```
|
|
|
|
> **Phase 1 Gate:** `pnpm install` green. (Typecheck/test deferred until Phase 2 fills `core-shared` — `core-api/src/root.ts` imports a not-yet-existing `core-shared/trpc/init` module.)
|
|
|
|
---
|
|
|
|
## Phase 2: Populate core-shared
|
|
|
|
### Task 2.1: Add shared vitest base config
|
|
|
|
**Files:**
|
|
- Create: `packages/typescript-config/vitest.base.ts`
|
|
- Modify: `packages/typescript-config/package.json`
|
|
|
|
- [ ] **Step 1: Replace packages/typescript-config/package.json with the version that exposes both `base.json` and `vitest.base.ts`**
|
|
|
|
Current contents (verified):
|
|
```json
|
|
{
|
|
"name": "@repo/core-typescript",
|
|
"private": true,
|
|
"version": "0.0.0"
|
|
}
|
|
```
|
|
|
|
Replace with:
|
|
```json
|
|
{
|
|
"name": "@repo/core-typescript",
|
|
"private": true,
|
|
"version": "0.0.0",
|
|
"type": "module",
|
|
"exports": {
|
|
"./base.json": "./base.json",
|
|
"./vitest.base": "./vitest.base.ts"
|
|
},
|
|
"devDependencies": {
|
|
"vitest": "^3.1.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Create vitest.base.ts**
|
|
|
|
```typescript
|
|
import { defineConfig } from "vitest/config";
|
|
|
|
export const baseVitestConfig = defineConfig({
|
|
test: {
|
|
globals: true,
|
|
environment: "node",
|
|
include: ["src/**/*.test.ts", "src/**/*.test.tsx", "tests/**/*.test.ts"],
|
|
coverage: {
|
|
provider: "v8",
|
|
reporter: ["text", "html"],
|
|
include: ["src/**"],
|
|
exclude: ["src/**/*.test.{ts,tsx}", "src/**/index.ts"],
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 3: Install vitest into typescript-config**
|
|
|
|
Run: `pnpm install`
|
|
Expected: vitest devDep installed; lockfile updated.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add packages/typescript-config pnpm-lock.yaml
|
|
git commit -m "build(typescript-config): add shared vitest base config"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.2: Add vitest.config.ts to core-shared
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/vitest.config.ts`
|
|
- Modify: `packages/core-shared/package.json` (add typescript-config to devDeps if not already)
|
|
|
|
- [ ] **Step 1: Create vitest.config.ts**
|
|
|
|
```typescript
|
|
import { baseVitestConfig } from "@repo/core-typescript/vitest.base";
|
|
|
|
export default baseVitestConfig;
|
|
```
|
|
|
|
- [ ] **Step 2: Run vitest to confirm config loads (no tests yet — should report 0 tests)**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run`
|
|
|
|
Expected: "No test files found, exiting with code 1" — that's fine; the config loads.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/vitest.config.ts
|
|
git commit -m "test(core-shared): wire shared vitest base"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.3: Implement is-admin access helper
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/src/payload/access/is-admin.ts`
|
|
- Create: `packages/core-shared/src/payload/access/is-admin.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/access/is-admin.test.ts
|
|
import { describe, expect, it } from "vitest";
|
|
import { isAdmin } from "./is-admin";
|
|
|
|
describe("isAdmin", () => {
|
|
it("returns true when user role is 'admin'", () => {
|
|
expect(isAdmin({ req: { user: { role: "admin" } } })).toBe(true);
|
|
});
|
|
|
|
it("returns false when user role is not 'admin'", () => {
|
|
expect(isAdmin({ req: { user: { role: "editor" } } })).toBe(false);
|
|
});
|
|
|
|
it("returns false when user has no role", () => {
|
|
expect(isAdmin({ req: { user: {} } })).toBe(false);
|
|
});
|
|
|
|
it("returns false when there is no user", () => {
|
|
expect(isAdmin({ req: {} })).toBe(false);
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run test — expect failure**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/payload/access/is-admin.test.ts`
|
|
Expected: FAIL — "Cannot find module './is-admin'"
|
|
|
|
- [ ] **Step 3: Implement**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/access/is-admin.ts
|
|
export function isAdmin({
|
|
req,
|
|
}: {
|
|
req: { user?: { role?: string } };
|
|
}): boolean {
|
|
return req.user?.role === "admin";
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run test — expect pass**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/payload/access/is-admin.test.ts`
|
|
Expected: PASS — 4 tests.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/src/payload/access
|
|
git commit -m "feat(core-shared): add isAdmin access helper"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.4: Implement slug-field
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/src/payload/fields/slug-field.ts`
|
|
- Create: `packages/core-shared/src/payload/fields/slug-field.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/fields/slug-field.test.ts
|
|
import { describe, expect, it } from "vitest";
|
|
import { slugField } from "./slug-field";
|
|
|
|
describe("slugField", () => {
|
|
it("returns a Payload Field with default name 'slug'", () => {
|
|
const field = slugField();
|
|
expect(field.name).toBe("slug");
|
|
expect(field.type).toBe("text");
|
|
expect(field.required).toBe(true);
|
|
expect(field.unique).toBe(true);
|
|
expect(field.index).toBe(true);
|
|
});
|
|
|
|
it("accepts a custom field name", () => {
|
|
const field = slugField("permalink");
|
|
expect(field.name).toBe("permalink");
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run — expect failure**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/payload/fields/slug-field.test.ts`
|
|
Expected: FAIL — "Cannot find module './slug-field'"
|
|
|
|
- [ ] **Step 3: Implement**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/fields/slug-field.ts
|
|
import type { Field } from "payload";
|
|
|
|
export function slugField(name = "slug"): Field {
|
|
return {
|
|
name,
|
|
type: "text",
|
|
required: true,
|
|
unique: true,
|
|
index: true,
|
|
};
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run — expect pass**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/payload/fields/slug-field.test.ts`
|
|
Expected: PASS — 2 tests.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/src/payload/fields/slug-field.ts packages/core-shared/src/payload/fields/slug-field.test.ts
|
|
git commit -m "feat(core-shared): add slugField helper"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.5: Implement seo-fields
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/src/payload/fields/seo-fields.ts`
|
|
- Create: `packages/core-shared/src/payload/fields/seo-fields.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/fields/seo-fields.test.ts
|
|
import { describe, expect, it } from "vitest";
|
|
import { seoFields } from "./seo-fields";
|
|
|
|
describe("seoFields", () => {
|
|
it("is a group field named 'seo'", () => {
|
|
expect(seoFields.name).toBe("seo");
|
|
expect(seoFields.type).toBe("group");
|
|
});
|
|
|
|
it("contains required title and optional description", () => {
|
|
if (seoFields.type !== "group") {
|
|
throw new Error("seoFields must be a group");
|
|
}
|
|
const fieldNames = seoFields.fields.map((f) =>
|
|
"name" in f ? f.name : null,
|
|
);
|
|
expect(fieldNames).toContain("title");
|
|
expect(fieldNames).toContain("description");
|
|
|
|
const titleField = seoFields.fields.find(
|
|
(f) => "name" in f && f.name === "title",
|
|
);
|
|
expect(titleField && "required" in titleField && titleField.required).toBe(
|
|
true,
|
|
);
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run — expect failure**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/payload/fields/seo-fields.test.ts`
|
|
Expected: FAIL — "Cannot find module './seo-fields'"
|
|
|
|
- [ ] **Step 3: Implement**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/fields/seo-fields.ts
|
|
import type { Field } from "payload";
|
|
|
|
export const seoFields: Field = {
|
|
name: "seo",
|
|
type: "group",
|
|
fields: [
|
|
{ name: "title", type: "text", required: true },
|
|
{ name: "description", type: "textarea" },
|
|
],
|
|
};
|
|
```
|
|
|
|
- [ ] **Step 4: Run — expect pass**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/payload/fields/seo-fields.test.ts`
|
|
Expected: PASS — 2 tests.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/src/payload/fields/seo-fields.ts packages/core-shared/src/payload/fields/seo-fields.test.ts
|
|
git commit -m "feat(core-shared): add seoFields group"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.6: Implement cta block
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/src/payload/blocks/cta.ts`
|
|
- Create: `packages/core-shared/src/payload/blocks/cta.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/blocks/cta.test.ts
|
|
import { describe, expect, it } from "vitest";
|
|
import { cta } from "./cta";
|
|
|
|
describe("cta block", () => {
|
|
it("has slug 'cta'", () => {
|
|
expect(cta.slug).toBe("cta");
|
|
});
|
|
|
|
it("requires title, buttonLabel, and href", () => {
|
|
const fieldNames = cta.fields.map((f) => ("name" in f ? f.name : null));
|
|
expect(fieldNames).toEqual(["title", "buttonLabel", "href"]);
|
|
cta.fields.forEach((f) => {
|
|
if ("required" in f) expect(f.required).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run — expect failure**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/payload/blocks/cta.test.ts`
|
|
Expected: FAIL — "Cannot find module './cta'"
|
|
|
|
- [ ] **Step 3: Implement**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/blocks/cta.ts
|
|
import type { Block } from "payload";
|
|
|
|
export const cta: Block = {
|
|
slug: "cta",
|
|
fields: [
|
|
{ name: "title", type: "text", required: true },
|
|
{ name: "buttonLabel", type: "text", required: true },
|
|
{ name: "href", type: "text", required: true },
|
|
],
|
|
};
|
|
```
|
|
|
|
- [ ] **Step 4: Run — expect pass**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/payload/blocks/cta.test.ts`
|
|
Expected: PASS — 2 tests.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/src/payload/blocks
|
|
git commit -m "feat(core-shared): add cta block"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.7: Implement set-published-at hook
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/src/payload/hooks/set-published-at.ts`
|
|
- Create: `packages/core-shared/src/payload/hooks/set-published-at.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/hooks/set-published-at.test.ts
|
|
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
|
|
import { setPublishedAt } from "./set-published-at";
|
|
|
|
describe("setPublishedAt", () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-05-04T12:00:00.000Z"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("sets publishedAt to now when status is published and publishedAt is missing", () => {
|
|
const result = setPublishedAt({ data: { status: "published" } });
|
|
expect(result?.publishedAt).toBe("2026-05-04T12:00:00.000Z");
|
|
});
|
|
|
|
it("does not overwrite an existing publishedAt", () => {
|
|
const result = setPublishedAt({
|
|
data: { status: "published", publishedAt: "2025-01-01T00:00:00.000Z" },
|
|
});
|
|
expect(result?.publishedAt).toBe("2025-01-01T00:00:00.000Z");
|
|
});
|
|
|
|
it("does not set publishedAt when status is not published", () => {
|
|
const result = setPublishedAt({ data: { status: "draft" } });
|
|
expect(result?.publishedAt).toBeUndefined();
|
|
});
|
|
|
|
it("returns data unchanged when data is missing", () => {
|
|
expect(setPublishedAt({})).toBeUndefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run — expect failure**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/payload/hooks/set-published-at.test.ts`
|
|
Expected: FAIL — "Cannot find module './set-published-at'"
|
|
|
|
- [ ] **Step 3: Implement**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/hooks/set-published-at.ts
|
|
export function setPublishedAt({
|
|
data,
|
|
}: {
|
|
data?: { status?: string; publishedAt?: string | null };
|
|
}) {
|
|
if (!data) return data;
|
|
|
|
if (data.status === "published" && !data.publishedAt) {
|
|
data.publishedAt = new Date().toISOString();
|
|
}
|
|
|
|
return data;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run — expect pass**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/payload/hooks/set-published-at.test.ts`
|
|
Expected: PASS — 4 tests.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/src/payload/hooks/set-published-at.ts packages/core-shared/src/payload/hooks/set-published-at.test.ts
|
|
git commit -m "feat(core-shared): add setPublishedAt hook"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.8: Implement slugify-if-missing hook
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/src/payload/hooks/slugify-if-missing.ts`
|
|
- Create: `packages/core-shared/src/payload/hooks/slugify-if-missing.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/hooks/slugify-if-missing.test.ts
|
|
import { describe, expect, it } from "vitest";
|
|
import { slugifyIfMissing } from "./slugify-if-missing";
|
|
|
|
describe("slugifyIfMissing", () => {
|
|
it("derives slug from title on create when slug is empty", () => {
|
|
const result = slugifyIfMissing({
|
|
data: { title: "Hello World" },
|
|
operation: "create",
|
|
});
|
|
expect(result?.slug).toBe("hello-world");
|
|
});
|
|
|
|
it("does not overwrite an existing slug", () => {
|
|
const result = slugifyIfMissing({
|
|
data: { title: "New Title", slug: "kept-slug" },
|
|
operation: "create",
|
|
});
|
|
expect(result?.slug).toBe("kept-slug");
|
|
});
|
|
|
|
it("does nothing on update", () => {
|
|
const result = slugifyIfMissing({
|
|
data: { title: "Hello World" },
|
|
operation: "update",
|
|
});
|
|
expect(result?.slug).toBeUndefined();
|
|
});
|
|
|
|
it("strips non-alphanumerics and trims hyphens", () => {
|
|
const result = slugifyIfMissing({
|
|
data: { title: " Hello, World!! 2026 " },
|
|
operation: "create",
|
|
});
|
|
expect(result?.slug).toBe("hello-world-2026");
|
|
});
|
|
|
|
it("returns data unchanged when title is missing", () => {
|
|
const result = slugifyIfMissing({
|
|
data: {},
|
|
operation: "create",
|
|
});
|
|
expect(result?.slug).toBeUndefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run — expect failure**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/payload/hooks/slugify-if-missing.test.ts`
|
|
Expected: FAIL — "Cannot find module './slugify-if-missing'"
|
|
|
|
- [ ] **Step 3: Implement**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/hooks/slugify-if-missing.ts
|
|
export function slugifyIfMissing({
|
|
data,
|
|
operation,
|
|
}: {
|
|
data?: { title?: string; slug?: string };
|
|
operation?: string;
|
|
}) {
|
|
if (!data) return data;
|
|
if (operation !== "create") return data;
|
|
if (data.slug) return data;
|
|
if (!data.title) return data;
|
|
|
|
data.slug = data.title
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
|
|
return data;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run — expect pass**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/payload/hooks/slugify-if-missing.test.ts`
|
|
Expected: PASS — 5 tests.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/src/payload/hooks/slugify-if-missing.ts packages/core-shared/src/payload/hooks/slugify-if-missing.test.ts
|
|
git commit -m "feat(core-shared): add slugifyIfMissing hook"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.9: Implement env helper
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/src/lib/env.ts`
|
|
- Create: `packages/core-shared/src/lib/env.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/lib/env.test.ts
|
|
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
|
import { requireEnv } from "./env";
|
|
|
|
describe("requireEnv", () => {
|
|
const originalEnv = process.env.SOME_KEY;
|
|
|
|
afterEach(() => {
|
|
if (originalEnv === undefined) delete process.env.SOME_KEY;
|
|
else process.env.SOME_KEY = originalEnv;
|
|
});
|
|
|
|
it("returns the value when set", () => {
|
|
process.env.SOME_KEY = "value";
|
|
expect(requireEnv("SOME_KEY")).toBe("value");
|
|
});
|
|
|
|
it("throws when missing", () => {
|
|
delete process.env.SOME_KEY;
|
|
expect(() => requireEnv("SOME_KEY")).toThrow(
|
|
/Missing required env var: SOME_KEY/,
|
|
);
|
|
});
|
|
|
|
it("throws when empty string", () => {
|
|
process.env.SOME_KEY = "";
|
|
expect(() => requireEnv("SOME_KEY")).toThrow();
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run — expect failure**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/lib/env.test.ts`
|
|
Expected: FAIL — "Cannot find module './env'"
|
|
|
|
- [ ] **Step 3: Implement**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/lib/env.ts
|
|
export function requireEnv(name: string): string {
|
|
const value = process.env[name];
|
|
if (!value) {
|
|
throw new Error(`Missing required env var: ${name}`);
|
|
}
|
|
return value;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run — expect pass**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/lib/env.test.ts`
|
|
Expected: PASS — 3 tests.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/src/lib/env.ts packages/core-shared/src/lib/env.test.ts
|
|
git commit -m "feat(core-shared): add requireEnv helper"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.10: Implement date helper
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/src/lib/date.ts`
|
|
- Create: `packages/core-shared/src/lib/date.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/lib/date.test.ts
|
|
import { describe, expect, it } from "vitest";
|
|
import { toIsoString } from "./date";
|
|
|
|
describe("toIsoString", () => {
|
|
it("converts a Date to ISO string", () => {
|
|
const d = new Date("2026-05-04T12:00:00.000Z");
|
|
expect(toIsoString(d)).toBe("2026-05-04T12:00:00.000Z");
|
|
});
|
|
|
|
it("passes through an existing ISO string", () => {
|
|
expect(toIsoString("2026-05-04T12:00:00.000Z")).toBe(
|
|
"2026-05-04T12:00:00.000Z",
|
|
);
|
|
});
|
|
|
|
it("returns null for null input", () => {
|
|
expect(toIsoString(null)).toBeNull();
|
|
});
|
|
|
|
it("returns null for undefined input", () => {
|
|
expect(toIsoString(undefined)).toBeNull();
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run — expect failure**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/lib/date.test.ts`
|
|
Expected: FAIL — "Cannot find module './date'"
|
|
|
|
- [ ] **Step 3: Implement**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/lib/date.ts
|
|
export function toIsoString(input: Date | string | null | undefined): string | null {
|
|
if (input === null || input === undefined) return null;
|
|
if (input instanceof Date) return input.toISOString();
|
|
return input;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run — expect pass**
|
|
|
|
Run: `cd packages/core-shared && pnpm vitest run src/lib/date.test.ts`
|
|
Expected: PASS — 4 tests.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/src/lib/date.ts packages/core-shared/src/lib/date.test.ts
|
|
git commit -m "feat(core-shared): add toIsoString helper"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.11: Create payload barrel export
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/src/payload/index.ts`
|
|
|
|
- [ ] **Step 1: Create barrel**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/payload/index.ts
|
|
export { isAdmin } from "./access/is-admin";
|
|
export { slugField } from "./fields/slug-field";
|
|
export { seoFields } from "./fields/seo-fields";
|
|
export { cta } from "./blocks/cta";
|
|
export { setPublishedAt } from "./hooks/set-published-at";
|
|
export { slugifyIfMissing } from "./hooks/slugify-if-missing";
|
|
```
|
|
|
|
- [ ] **Step 2: Verify import path resolves**
|
|
|
|
Run: `cd packages/core-shared && pnpm typecheck`
|
|
Expected: PASS — no errors.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/src/payload/index.ts
|
|
git commit -m "feat(core-shared): add payload barrel export"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.12: Implement trpc init
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/src/trpc/init.ts`
|
|
|
|
- [ ] **Step 1: Implement** (no test — exercised indirectly through routers in Plan 2)
|
|
|
|
```typescript
|
|
// packages/core-shared/src/trpc/init.ts
|
|
import { initTRPC } from "@trpc/server";
|
|
import superjson from "superjson";
|
|
|
|
const t = initTRPC.create({
|
|
transformer: superjson,
|
|
});
|
|
|
|
export const router = t.router;
|
|
export const publicProcedure = t.procedure;
|
|
export const middleware = t.middleware;
|
|
```
|
|
|
|
- [ ] **Step 2: Verify it compiles**
|
|
|
|
Run: `cd packages/core-shared && pnpm typecheck`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/src/trpc/init.ts
|
|
git commit -m "feat(core-shared): add tRPC init with superjson"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.13: Implement trpc context
|
|
|
|
**Files:**
|
|
- Create: `packages/core-shared/src/trpc/context.ts`
|
|
|
|
- [ ] **Step 1: Implement**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/trpc/context.ts
|
|
export async function createTrpcContext() {
|
|
return {};
|
|
}
|
|
|
|
export type TrpcContext = Awaited<ReturnType<typeof createTrpcContext>>;
|
|
```
|
|
|
|
- [ ] **Step 2: Verify it compiles**
|
|
|
|
Run: `cd packages/core-shared && pnpm typecheck`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/src/trpc/context.ts
|
|
git commit -m "feat(core-shared): add tRPC context factory"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.14: Wire root index.ts and verify all tests
|
|
|
|
**Files:**
|
|
- Modify: `packages/core-shared/src/index.ts`
|
|
|
|
- [ ] **Step 1: Replace empty index with re-exports**
|
|
|
|
```typescript
|
|
// packages/core-shared/src/index.ts
|
|
export { requireEnv } from "./lib/env";
|
|
export { toIsoString } from "./lib/date";
|
|
```
|
|
|
|
> Note: Payload primitives are accessed via `@repo/core-shared/payload` subpath; tRPC via `/trpc/init` and `/trpc/context`. The root export is just the lib helpers.
|
|
|
|
- [ ] **Step 2: Run all core-shared tests**
|
|
|
|
Run: `cd packages/core-shared && pnpm test`
|
|
Expected: PASS — 26 tests across 8 test files: is-admin (4) + slug-field (2) + seo-fields (2) + cta (2) + set-published-at (4) + slugify-if-missing (5) + env (3) + date (4). Use `pnpm vitest run --reporter=verbose` for a full list.
|
|
|
|
- [ ] **Step 3: Run typecheck across the whole repo**
|
|
|
|
Run: `pnpm typecheck`
|
|
Expected: PASS (now that `core-shared/trpc/init` exists, `core-api/src/root.ts` from Task 1.4 resolves).
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add packages/core-shared/src/index.ts
|
|
git commit -m "feat(core-shared): wire root index.ts barrel"
|
|
```
|
|
|
|
> **Phase 2 Gate:** `pnpm test --filter @repo/core-shared` green; `pnpm typecheck` green across the whole repo.
|
|
|
|
---
|
|
|
|
## Phase 3: Populate core-cms stub + repoint apps/cms
|
|
|
|
### Task 3.1: Create core-cms payload.config.ts (stub — empty arrays)
|
|
|
|
**Files:**
|
|
- Create: `packages/core-cms/src/payload.config.ts`
|
|
|
|
- [ ] **Step 1: Implement**
|
|
|
|
```typescript
|
|
// packages/core-cms/src/payload.config.ts
|
|
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";
|
|
|
|
const filename = fileURLToPath(import.meta.url);
|
|
const dirname = path.dirname(filename);
|
|
|
|
export default buildConfig({
|
|
editor: lexicalEditor(),
|
|
collections: [],
|
|
globals: [],
|
|
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, "generated-types.ts"),
|
|
},
|
|
});
|
|
```
|
|
|
|
> Note: `collections: []` and `globals: []` initially. Plan 2 wires in feature-owned `@repo/<feature>/cms` exports here.
|
|
|
|
- [ ] **Step 2: Update index.ts to re-export the config**
|
|
|
|
```typescript
|
|
// packages/core-cms/src/index.ts
|
|
export { default } from "./payload.config";
|
|
```
|
|
|
|
- [ ] **Step 3: Verify it compiles**
|
|
|
|
Run: `cd packages/core-cms && pnpm typecheck`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add packages/core-cms/src
|
|
git commit -m "feat(core-cms): add stub payload.config (empty collections/globals)"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3.2: Repoint apps/cms to @repo/core-cms
|
|
|
|
**Files:**
|
|
- Modify: `apps/cms/src/payload.config.ts`
|
|
- Modify: `apps/cms/package.json`
|
|
|
|
- [ ] **Step 1: Update the re-export in apps/cms/src/payload.config.ts**
|
|
|
|
```typescript
|
|
// apps/cms/src/payload.config.ts
|
|
// Re-export Payload config from @repo/core-cms.
|
|
// This file exists so @payload-config resolves correctly in the CMS app.
|
|
export { default } from "@repo/core-cms";
|
|
```
|
|
|
|
- [ ] **Step 2: Update apps/cms/package.json — add @repo/core-cms dependency**
|
|
|
|
Add to `dependencies` block (keep `@repo/cms-core` for now — removed in Plan 4):
|
|
|
|
```json
|
|
"@repo/core-cms": "workspace:*",
|
|
```
|
|
|
|
- [ ] **Step 3: Install**
|
|
|
|
Run: `pnpm install`
|
|
Expected: install completes; new dep added.
|
|
|
|
- [ ] **Step 4: Verify cms typecheck**
|
|
|
|
Run: `pnpm typecheck --filter @repo/cms`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add apps/cms/src/payload.config.ts apps/cms/package.json pnpm-lock.yaml
|
|
git commit -m "feat(cms): repoint @payload-config from cms-core to core-cms"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3.3: Boot apps/cms against new core-cms config (smoke test)
|
|
|
|
- [ ] **Step 1: Ensure Postgres is running**
|
|
|
|
Run: `docker compose ps postgres`
|
|
Expected: postgres container healthy. If not: `docker compose up -d postgres`.
|
|
|
|
- [ ] **Step 2: Start the cms dev server in the background**
|
|
|
|
Run: `pnpm dev --filter @repo/cms`
|
|
|
|
This starts Next.js on port 3001. Wait ~10s for "Ready" / "compiled successfully".
|
|
|
|
- [ ] **Step 3: Verify the admin UI responds**
|
|
|
|
Run: `curl -sf http://localhost:3001/admin -o /dev/null && echo OK`
|
|
Expected: `OK` (the admin UI responds with HTML — even if the empty-collections config means there's nothing to manage yet, the admin shell loads).
|
|
|
|
- [ ] **Step 4: Stop the dev server**
|
|
|
|
Stop the `pnpm dev` process (Ctrl+C if foreground, or kill the background job).
|
|
|
|
- [ ] **Step 5: No commit needed for the smoke test (no file changes)**
|
|
|
|
---
|
|
|
|
### Task 3.4: Verify Payload type generation against the stub
|
|
|
|
- [ ] **Step 1: Run type generation**
|
|
|
|
Run: `cd apps/cms && pnpm generate:types`
|
|
Expected: writes a new `generated-types.ts` somewhere (per the config's `outputFile: path.resolve(dirname, "generated-types.ts")` — this puts the file in `packages/core-cms/src/generated-types.ts`).
|
|
|
|
- [ ] **Step 2: Inspect the generated file**
|
|
|
|
Run: `head -30 packages/core-cms/src/generated-types.ts`
|
|
Expected: a valid TypeScript file (probably mostly empty or with just a `Config` interface and Auth types since Users isn't in the empty arrays — Payload may include built-in user/preference types).
|
|
|
|
- [ ] **Step 3: Verify the file typechecks**
|
|
|
|
Run: `pnpm typecheck --filter @repo/core-cms`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 4: Commit the regenerated types**
|
|
|
|
```bash
|
|
git add packages/core-cms/src/generated-types.ts
|
|
git commit -m "feat(core-cms): generate initial (empty) Payload types"
|
|
```
|
|
|
|
> **Phase 3 Gate:** `pnpm dev --filter @repo/cms` boots and serves `/admin`; `pnpm generate:types` succeeds; `pnpm typecheck && pnpm test --filter @repo/core-shared` green.
|
|
|
|
---
|
|
|
|
## Final Verification
|
|
|
|
- [ ] **Step 1: Repo-wide typecheck**
|
|
|
|
Run: `pnpm typecheck`
|
|
Expected: PASS across all packages (old `@repo/core`, `@repo/api`, etc. still exist and still typecheck — they're untouched).
|
|
|
|
- [ ] **Step 2: Repo-wide tests**
|
|
|
|
Run: `pnpm test`
|
|
Expected: PASS — `core-shared` tests pass; old `@repo/core` tests still pass (unchanged).
|
|
|
|
- [ ] **Step 3: Lint**
|
|
|
|
Run: `pnpm lint`
|
|
Expected: PASS (no boundary rules added yet — those land in Plan 4).
|
|
|
|
- [ ] **Step 4: Build**
|
|
|
|
Run: `pnpm build`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 5: Final summary commit (optional — only if any cleanup edits were needed)**
|
|
|
|
If everything is already committed phase-by-phase, no extra commit needed.
|
|
|
|
---
|
|
|
|
## Plan 1 Done Criteria
|
|
|
|
- [ ] All 26 tests in `core-shared` pass (8 test files)
|
|
- [ ] `pnpm typecheck` green across whole repo
|
|
- [ ] `pnpm dev --filter @repo/cms` serves `/admin` against new `@repo/core-cms` config
|
|
- [ ] `pnpm generate:types` succeeds
|
|
- [ ] Five new packages (`core-shared`, `core-cms`, `core-api`, `core-trpc`, `core-ui`) registered in workspace
|
|
- [ ] `tsconfig.base.json` exists at repo root with all five `@repo/core-*` aliases
|
|
- [ ] No deletions yet (old packages still intact for Plan 2-4 to drain)
|
|
|
|
**Next plan:** Plan 2 — Feature migrations. Migrate the `blog` feature end-to-end as the canonical example, then `auth`, `marketing-pages`, `navigation`, `media`. Each feature follows the same template (entities → application → infrastructure → di → interface-adapters/controllers → integrations/cms + integrations/api → ui), composes its `/cms` export into `core-cms` and its `/api` export into `core-api`.
|