guardian-framework 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +333 -29
- package/dist/exports.js +333 -29
- package/package.json +1 -1
- package/templates/pi/architecture/modules/module-template.md +90 -57
- package/templates/pi/extensions/domain-explorer.ts +402 -108
- package/templates/pi/preflight_report.json +1 -1
- package/templates/pi/skills/nextjs-enterprise-codegen.md +303 -293
|
@@ -1,383 +1,393 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nextjs-enterprise-codegen
|
|
3
|
-
description: Full reference for Next.js enterprise code generation with DDD + Clean Architecture.
|
|
3
|
+
description: "Full reference for Next.js enterprise code generation with DDD + Clean Architecture. Every module uses 4 DDD layers: domain/, application/, infrastructure/, interfaces/. Covers User Intents, App Shell, design system, navigation philosophy, offline strategy, server/client boundary, and testing. Loaded on-demand via agents/nextjs-codegen.md skill."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Next.js Enterprise Code Generation — DDD + Clean Architecture
|
|
7
7
|
|
|
8
8
|
> Canonical skill for generating production-grade Next.js code with DDD patterns.
|
|
9
|
-
>
|
|
9
|
+
> **Every module MUST follow the 4 DDD layers below.**
|
|
10
|
+
> Validators enforce compliance.
|
|
10
11
|
>
|
|
11
|
-
> For design system and styling patterns, see: `.pi/skills/design-system-enterprise-codegen.md`
|
|
12
|
+
> For design system and styling patterns, see: `.pi/skills/design-system-enterprise-codegen.md`
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
15
|
-
## 1.
|
|
16
|
+
## 1. DDD Module Structure — The 4 Layers
|
|
17
|
+
|
|
18
|
+
Every feature module uses exactly this structure:
|
|
16
19
|
|
|
17
20
|
```
|
|
18
21
|
src/
|
|
19
|
-
|
|
20
|
-
domain/ # Pure business logic
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
auth.ts # Auth provider SDK wrapper
|
|
33
|
-
analytics.ts # Analytics SDK wrapper
|
|
34
|
-
interfaces/ # UI layer
|
|
35
|
-
page.tsx # App Router page (default: Server Component)
|
|
36
|
-
component.client.tsx # Client Component with 'use client'
|
|
37
|
-
layout.tsx # Layout wrapper
|
|
38
|
-
shared/ # Cross-module shared code
|
|
39
|
-
ui/ # Design system components
|
|
40
|
-
button/
|
|
41
|
-
card/
|
|
42
|
-
modal/
|
|
43
|
-
lib/ # Shared utilities
|
|
44
|
-
hooks/ # Shared React hooks
|
|
22
|
+
collaboration/ # One bounded context / feature
|
|
23
|
+
domain/ # Layer 1: Pure business logic
|
|
24
|
+
types.ts # Types, interfaces, enums (zero React)
|
|
25
|
+
errors.ts # Typed error classes
|
|
26
|
+
notification.ts # Domain value objects
|
|
27
|
+
application/ # Layer 2: State + use cases
|
|
28
|
+
store.ts # Zustand store
|
|
29
|
+
actions.ts # Server Actions
|
|
30
|
+
infrastructure/ # Layer 3: External adapters
|
|
31
|
+
api.ts # API client (fetch)
|
|
32
|
+
interfaces/ # Layer 4: UI components
|
|
33
|
+
thread-panel.tsx # React components
|
|
34
|
+
comment-list.tsx
|
|
45
35
|
```
|
|
46
36
|
|
|
47
|
-
### Dependency Rule
|
|
37
|
+
### Dependency Rule (Inward)
|
|
38
|
+
|
|
48
39
|
```
|
|
49
40
|
domain → application → infrastructure → interfaces
|
|
50
|
-
↕
|
|
51
|
-
shared/ (no framework deps)
|
|
52
41
|
```
|
|
53
42
|
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
43
|
+
- `domain/` — **ZERO React/Next.js imports.** Pure TypeScript. No `'use client'`.
|
|
44
|
+
- `application/` — imports from `domain/`. Stores, actions, server actions.
|
|
45
|
+
- `infrastructure/` — imports from `domain/` and `application/`. API clients, SDK wrappers.
|
|
46
|
+
- `interfaces/` — imports from `application/`. **Only this layer has React components.**
|
|
47
|
+
|
|
48
|
+
### What goes where — specific rules
|
|
49
|
+
|
|
50
|
+
| DO put in `domain/` | DO NOT put in `domain/` |
|
|
51
|
+
|--------------------|------------------------|
|
|
52
|
+
| TypeScript interfaces and types | React components |
|
|
53
|
+
| Error classes (`class NotFoundError`) | Zustand stores |
|
|
54
|
+
| Value objects (`class Money`) | `useState`, `useEffect` |
|
|
55
|
+
| Enum definitions (`enum Status`) | Server Actions (`'use server'`) |
|
|
56
|
+
| Pure functions (no side effects) | API fetch calls |
|
|
57
|
+
| Constants and configuration | CSS, Tailwind classes |
|
|
58
|
+
|
|
59
|
+
| DO put in `application/` | DO NOT put in `application/` |
|
|
60
|
+
|-------------------------|------------------------------|
|
|
61
|
+
| Zustand stores | React components |
|
|
62
|
+
| Server Actions (`'use server'`) | API fetch implementation |
|
|
63
|
+
| React Query hooks | UI state like `isOpen`, `isVisible` |
|
|
64
|
+
| Command/Query DTOs | JSX markup |
|
|
65
|
+
|
|
66
|
+
| DO put in `infrastructure/` | DO NOT put in `infrastructure/` |
|
|
67
|
+
|----------------------------|--------------------------------|
|
|
68
|
+
| API client functions (`fetch`, `axios`) | React components |
|
|
69
|
+
| localStorage/cookie wrappers | Store logic |
|
|
70
|
+
| Auth provider SDK wrappers | Domain types (import them) |
|
|
71
|
+
| Analytics SDK wrappers | UI state |
|
|
72
|
+
|
|
73
|
+
| DO put in `interfaces/` | DO NOT put in `interfaces/` |
|
|
74
|
+
|------------------------|-----------------------------|
|
|
75
|
+
| Page components (`page.tsx`) | API fetch calls |
|
|
76
|
+
| Client components (`*.client.tsx`) | Domain types (import them) |
|
|
77
|
+
| Layout components | Business logic |
|
|
78
|
+
| UI state (`useState` for modals) | Server Actions (call them, don't define) |
|
|
58
79
|
|
|
59
80
|
---
|
|
60
81
|
|
|
61
|
-
## 2.
|
|
62
|
-
|
|
63
|
-
This is the most critical architectural decision in Next.js. Get it wrong and you lose the benefits of both paradigms.
|
|
82
|
+
## 2. Domain Layer — Pure Business Logic
|
|
64
83
|
|
|
65
|
-
|
|
84
|
+
The domain layer has zero framework dependencies. It defines the types, errors, and value objects that the rest of the module uses.
|
|
66
85
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
```typescript
|
|
87
|
+
// collaboration/domain/types.ts
|
|
88
|
+
export interface Thread {
|
|
89
|
+
id: string;
|
|
90
|
+
knowledgeNodeId: string;
|
|
91
|
+
title: string;
|
|
92
|
+
createdAt: string;
|
|
93
|
+
createdBy: User;
|
|
94
|
+
replyCount: number;
|
|
95
|
+
lastActivityAt: string;
|
|
96
|
+
}
|
|
77
97
|
|
|
98
|
+
export interface Comment {
|
|
99
|
+
id: string;
|
|
100
|
+
threadId: string;
|
|
101
|
+
body: string;
|
|
102
|
+
mentions: string[];
|
|
103
|
+
createdAt: string;
|
|
104
|
+
createdBy: User;
|
|
105
|
+
}
|
|
78
106
|
```
|
|
79
|
-
┌─────────────────────────────────────────┐
|
|
80
|
-
│ page.tsx (Server Component) │
|
|
81
|
-
│ <header>Static header</header> │
|
|
82
|
-
│ <ProductList> │
|
|
83
|
-
│ {products.map(p => <ProductCard/>)} │ ← Server-rendered
|
|
84
|
-
│ </ProductList> │
|
|
85
|
-
│ <CheckoutButton.client.tsx> │ ← Client island
|
|
86
|
-
│ <AddToCartButton/> │
|
|
87
|
-
│ <PriceSummary/> │
|
|
88
|
-
│ </CheckoutButton.client.tsx> │
|
|
89
|
-
└─────────────────────────────────────────┘
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Rules
|
|
93
|
-
- ✅ Default to Server Components. Add `'use client'` only when you need interactivity.
|
|
94
|
-
- ✅ Push client state DOWN to the leaf components that need it.
|
|
95
|
-
- ✅ Use Server Actions for mutations, client handlers for optimistic updates.
|
|
96
|
-
- ❌ Never put `'use client'` on a page wrapper — only on interactive islands.
|
|
97
|
-
- ❌ Never import server-only code (actions, DB queries) in client components.
|
|
98
|
-
|
|
99
|
-
---
|
|
100
|
-
|
|
101
|
-
## 3. Data Fetching Patterns
|
|
102
|
-
|
|
103
|
-
### Server Component Fetch (Default)
|
|
104
107
|
|
|
105
108
|
```typescript
|
|
106
|
-
//
|
|
107
|
-
|
|
109
|
+
// collaboration/domain/errors.ts
|
|
110
|
+
export class CommentError extends Error {
|
|
111
|
+
constructor(
|
|
112
|
+
public readonly code: string,
|
|
113
|
+
message: string,
|
|
114
|
+
public readonly retriable: boolean = false,
|
|
115
|
+
) {
|
|
116
|
+
super(message);
|
|
117
|
+
this.name = 'CommentError';
|
|
118
|
+
}
|
|
108
119
|
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
static saveFailed(): CommentError {
|
|
121
|
+
return new CommentError('SAVE_FAILED', 'Failed to save comment', true);
|
|
122
|
+
}
|
|
111
123
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<ProductCard key={p.id} product={p} /> // Server-rendered
|
|
116
|
-
))}
|
|
117
|
-
</div>
|
|
118
|
-
);
|
|
124
|
+
static notFound(id: string): CommentError {
|
|
125
|
+
return new CommentError('NOT_FOUND', `Comment ${id} not found`);
|
|
126
|
+
}
|
|
119
127
|
}
|
|
120
128
|
```
|
|
121
129
|
|
|
122
|
-
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 3. Application Layer — Stores and Actions
|
|
123
133
|
|
|
124
134
|
```typescript
|
|
125
|
-
// application/
|
|
126
|
-
import {
|
|
127
|
-
import {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
// collaboration/application/store.ts
|
|
136
|
+
import { create } from 'zustand';
|
|
137
|
+
import type { Thread, Comment } from '../domain/types';
|
|
138
|
+
|
|
139
|
+
interface CollaborationState {
|
|
140
|
+
threads: Thread[];
|
|
141
|
+
activeThreadId: string | null;
|
|
142
|
+
isLoading: boolean;
|
|
143
|
+
error: string | null;
|
|
144
|
+
|
|
145
|
+
setThreads: (threads: Thread[]) => void;
|
|
146
|
+
setActiveThread: (id: string) => void;
|
|
147
|
+
optimisticallyAddComment: (comment: Comment) => void;
|
|
148
|
+
rollbackComment: (commentId: string) => void;
|
|
136
149
|
}
|
|
137
150
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
151
|
+
export const useCollaborationStore = create<CollaborationState>((set) => ({
|
|
152
|
+
threads: [],
|
|
153
|
+
activeThreadId: null,
|
|
154
|
+
isLoading: false,
|
|
155
|
+
error: null,
|
|
156
|
+
|
|
157
|
+
setThreads: (threads) => set({ threads }),
|
|
158
|
+
setActiveThread: (id) => set({ activeThreadId: id }),
|
|
159
|
+
optimisticallyAddComment: (comment) => set((state) => ({
|
|
160
|
+
threads: state.threads.map(t =>
|
|
161
|
+
t.id === comment.threadId
|
|
162
|
+
? { ...t, replyCount: t.replyCount + 1 }
|
|
163
|
+
: t
|
|
164
|
+
),
|
|
165
|
+
})),
|
|
166
|
+
rollbackComment: (commentId) => set((state) => ({
|
|
167
|
+
threads: state.threads.map(t =>
|
|
168
|
+
t.id === commentId
|
|
169
|
+
? { ...t, replyCount: t.replyCount - 1 }
|
|
170
|
+
: t
|
|
171
|
+
),
|
|
172
|
+
})),
|
|
173
|
+
}));
|
|
145
174
|
```
|
|
146
175
|
|
|
147
|
-
### Server Actions (Mutations)
|
|
148
|
-
|
|
149
176
|
```typescript
|
|
150
|
-
// application/actions.ts
|
|
177
|
+
// collaboration/application/actions.ts
|
|
151
178
|
'use server';
|
|
152
179
|
import { revalidatePath } from 'next/cache';
|
|
153
|
-
import {
|
|
154
|
-
|
|
155
|
-
export async function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
180
|
+
import type { Comment } from '../domain/types';
|
|
181
|
+
|
|
182
|
+
export async function createComment(
|
|
183
|
+
nodeId: string,
|
|
184
|
+
body: string,
|
|
185
|
+
mentions: string[],
|
|
186
|
+
): Promise<Comment> {
|
|
187
|
+
// Server Action — calls backend API
|
|
188
|
+
const res = await fetch(`/api/nodes/${nodeId}/comments`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
body: JSON.stringify({ body, mentions }),
|
|
191
|
+
});
|
|
192
|
+
if (!res.ok) throw new Error('Failed to create comment');
|
|
193
|
+
const comment = await res.json();
|
|
194
|
+
revalidatePath(`/nodes/${nodeId}`);
|
|
195
|
+
return comment;
|
|
161
196
|
}
|
|
162
197
|
```
|
|
163
198
|
|
|
164
|
-
### Rules
|
|
165
|
-
- ✅ Server Components for initial data — zero client JS for the first render
|
|
166
|
-
- ✅ React Query for client-side data that needs realtime/stale-while-revalidate
|
|
167
|
-
- ✅ Server Actions for mutations — no API route boilerplate
|
|
168
|
-
- ❌ Never fetch in Server Components for data that needs realtime updates
|
|
169
|
-
- ❌ Never use `useEffect` for data fetching — use React Query or Server Actions
|
|
170
|
-
|
|
171
199
|
---
|
|
172
200
|
|
|
173
|
-
## 4.
|
|
174
|
-
|
|
175
|
-
| State type | Tool | Location |
|
|
176
|
-
|-----------|------|----------|
|
|
177
|
-
| Server state (data) | React Query / SWR | `application/queries.ts` |
|
|
178
|
-
| Client state (UI) | Zustand / Jotai | `application/store.ts` |
|
|
179
|
-
| URL state (filters, page) | `useSearchParams` | `interfaces/*.client.tsx` |
|
|
180
|
-
| Form state | React Hook Form + Zod | `interfaces/*.client.tsx` |
|
|
181
|
-
| Server mutations | Server Actions | `application/actions.ts` |
|
|
201
|
+
## 4. Infrastructure Layer — API Clients
|
|
182
202
|
|
|
183
203
|
```typescript
|
|
184
|
-
//
|
|
185
|
-
import {
|
|
186
|
-
import {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
addItem: (productId: string, qty: number) => Promise<void>;
|
|
193
|
-
toggleCart: () => void;
|
|
204
|
+
// collaboration/infrastructure/api.ts
|
|
205
|
+
import type { Thread, Comment } from '../domain/types';
|
|
206
|
+
import { CommentError } from '../domain/errors';
|
|
207
|
+
|
|
208
|
+
export async function fetchThreads(nodeId: string): Promise<Thread[]> {
|
|
209
|
+
const res = await fetch(`/api/nodes/${nodeId}/threads`);
|
|
210
|
+
if (!res.ok) throw new CommentError('FETCH_FAILED', 'Failed to load threads', true);
|
|
211
|
+
return res.json();
|
|
194
212
|
}
|
|
195
213
|
|
|
196
|
-
export
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
);
|
|
214
|
+
export async function saveComment(
|
|
215
|
+
nodeId: string,
|
|
216
|
+
threadId: string,
|
|
217
|
+
body: string,
|
|
218
|
+
): Promise<Comment> {
|
|
219
|
+
const res = await fetch(`/api/nodes/${nodeId}/threads/${threadId}/comments`, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
body: JSON.stringify({ body }),
|
|
222
|
+
});
|
|
223
|
+
if (!res.ok) throw CommentError.saveFailed();
|
|
224
|
+
return res.json();
|
|
225
|
+
}
|
|
209
226
|
```
|
|
210
227
|
|
|
211
|
-
### Rules
|
|
212
|
-
- ✅ Separate server state (React Query) from client state (Zustand)
|
|
213
|
-
- ✅ Keep UI-only state (modals, toggles) local with `useState` when possible
|
|
214
|
-
- ✅ Use Zustand slices for complex domains
|
|
215
|
-
- ❌ Don't put server data in Zustand — that's what React Query is for
|
|
216
|
-
|
|
217
228
|
---
|
|
218
229
|
|
|
219
|
-
## 5.
|
|
220
|
-
|
|
221
|
-
### Structure
|
|
230
|
+
## 5. Interfaces Layer — React Components
|
|
222
231
|
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
spacing.ts
|
|
230
|
-
button/
|
|
231
|
-
button.tsx # Component
|
|
232
|
-
button.module.css # Styles
|
|
233
|
-
button.test.tsx # Tests
|
|
234
|
-
card/
|
|
235
|
-
modal/
|
|
236
|
-
form/
|
|
237
|
-
input.tsx
|
|
238
|
-
select.tsx
|
|
239
|
-
```
|
|
232
|
+
```typescript
|
|
233
|
+
// collaboration/interfaces/thread-panel.tsx
|
|
234
|
+
'use client';
|
|
235
|
+
import { useCollaborationStore } from '../application/store';
|
|
236
|
+
import { fetchThreads } from '../infrastructure/api';
|
|
237
|
+
import { CommentList } from './comment-list';
|
|
240
238
|
|
|
241
|
-
|
|
239
|
+
export function ThreadPanel({ nodeId }: { nodeId: string }) {
|
|
240
|
+
const { threads, activeThreadId, setActiveThread, setThreads } = useCollaborationStore();
|
|
242
241
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
| Tailwind utility classes | Layout, spacing, one-off adjustments |
|
|
247
|
-
| CSS Variables (`var(--color-primary)`) | Design tokens, theming |
|
|
248
|
-
| `clsx` / `tailwind-merge` | Conditional class composition |
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
fetchThreads(nodeId).then(setThreads);
|
|
244
|
+
}, [nodeId]);
|
|
249
245
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
261
|
-
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
262
|
-
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
263
|
-
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
264
|
-
},
|
|
265
|
-
size: {
|
|
266
|
-
sm: 'h-9 px-3',
|
|
267
|
-
md: 'h-10 px-4 py-2',
|
|
268
|
-
lg: 'h-11 px-8',
|
|
269
|
-
},
|
|
270
|
-
},
|
|
271
|
-
defaultVariants: { variant: 'primary', size: 'md' },
|
|
272
|
-
},
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
interface ButtonProps
|
|
276
|
-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
277
|
-
VariantProps<typeof buttonVariants> {}
|
|
278
|
-
|
|
279
|
-
export function Button({ className, variant, size, ...props }: ButtonProps) {
|
|
280
|
-
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />;
|
|
246
|
+
return (
|
|
247
|
+
<div>
|
|
248
|
+
{threads.map(thread => (
|
|
249
|
+
<button key={thread.id} onClick={() => setActiveThread(thread.id)}>
|
|
250
|
+
{thread.title} ({thread.replyCount})
|
|
251
|
+
</button>
|
|
252
|
+
))}
|
|
253
|
+
{activeThreadId && <CommentList threadId={activeThreadId} />}
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
281
256
|
}
|
|
282
257
|
```
|
|
283
258
|
|
|
284
|
-
### Design System Rules
|
|
285
|
-
- ✅ Design tokens in `shared/ui/tokens/` — single source of truth for colors, spacing, typography
|
|
286
|
-
- ✅ Components use `cva` (class-variance-authority) for variant props
|
|
287
|
-
- ✅ CSS Modules for component styles, Tailwind for layout
|
|
288
|
-
- ✅ Dark mode via `class` strategy on `<html>` element
|
|
289
|
-
- ❌ No inline styles (`style={{}}`) except for dynamic values (animations, transforms)
|
|
290
|
-
- ❌ No CSS-in-JS libraries (styled-components, emotion) — they break Server Components
|
|
291
|
-
|
|
292
259
|
---
|
|
293
260
|
|
|
294
|
-
## 6.
|
|
261
|
+
## 6. Server/Client Boundary
|
|
295
262
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
public readonly status: number = 400,
|
|
303
|
-
) {
|
|
304
|
-
super(message);
|
|
305
|
-
this.name = 'DomainError';
|
|
306
|
-
}
|
|
263
|
+
| Layer | 'use client'? | 'use server'? | Can use hooks? |
|
|
264
|
+
|-------|--------------|---------------|----------------|
|
|
265
|
+
| `domain/` | No | No | No |
|
|
266
|
+
| `application/` | No (except stores) | Yes (actions) | No |
|
|
267
|
+
| `infrastructure/` | No | No | No |
|
|
268
|
+
| `interfaces/` | Yes | No | Yes |
|
|
307
269
|
|
|
308
|
-
|
|
309
|
-
return new DomainError('NOT_FOUND', `Resource ${id} not found`, 404);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
270
|
+
---
|
|
312
271
|
|
|
313
|
-
|
|
314
|
-
'use client';
|
|
315
|
-
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
316
|
-
return (
|
|
317
|
-
<div role="alert">
|
|
318
|
-
<h2>Something went wrong</h2>
|
|
319
|
-
<button onClick={() => reset()}>Try again</button>
|
|
320
|
-
</div>
|
|
321
|
-
);
|
|
322
|
-
}
|
|
272
|
+
## 7. Shared Code — When Something Spans Multiple Modules
|
|
323
273
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
274
|
+
Not everything belongs inside a module. The `shared/` directory holds cross-cutting code that multiple modules need. But it has strict rules.
|
|
275
|
+
|
|
276
|
+
### What goes in `shared/` vs `module/`
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
src/
|
|
280
|
+
shared/ # Cross-cutting, NOT a bounded context
|
|
281
|
+
domain/ # Shared domain concepts (Anti-Corruption Layer)
|
|
282
|
+
user.ts # User type used by multiple contexts
|
|
283
|
+
team.ts # Team type used by multiple contexts
|
|
284
|
+
pagination.ts # Generic pagination types
|
|
285
|
+
hooks/ # Generic React hooks (zero business logic)
|
|
286
|
+
use-debounce.ts
|
|
287
|
+
use-media-query.ts
|
|
288
|
+
use-intersection-observer.ts
|
|
289
|
+
lib/ # Pure utilities
|
|
290
|
+
date-utils.ts # Date formatting
|
|
291
|
+
string-utils.ts # Slug generation
|
|
292
|
+
api-client.ts # Base fetch wrapper (not domain-specific)
|
|
293
|
+
ui/ # Design system (framework-agnostic)
|
|
294
|
+
button/
|
|
295
|
+
card/
|
|
296
|
+
modal/
|
|
297
|
+
collaboration/ # Bounded context
|
|
298
|
+
domain/
|
|
299
|
+
thread.ts # Business-specific concept
|
|
300
|
+
comment.ts
|
|
301
|
+
application/
|
|
302
|
+
store.ts
|
|
303
|
+
infrastructure/
|
|
304
|
+
api.ts
|
|
305
|
+
interfaces/
|
|
306
|
+
thread-panel.tsx
|
|
328
307
|
```
|
|
329
308
|
|
|
330
|
-
|
|
309
|
+
### The rules
|
|
331
310
|
|
|
332
|
-
|
|
311
|
+
| Goes in `shared/` | Goes in `module/` |
|
|
312
|
+
|------------------|-------------------|
|
|
313
|
+
| `useDebounce` (generic hook) | `useCollaborationStore` (domain-specific) |
|
|
314
|
+
| `apiClient.fetch()` (generic HTTP) | `collaborationApi.fetchThreads()` (domain-specific) |
|
|
315
|
+
| `formatDate()` (utility) | `calculateThreadActivity()` (domain logic) |
|
|
316
|
+
| `Button`, `Card` (design system) | `ThreadPanel`, `CommentList` (domain components) |
|
|
317
|
+
| `User`, `Team` (shared kernel) | `Thread`, `Comment` (domain entities) |
|
|
333
318
|
|
|
334
|
-
|
|
335
|
-
|-----------|------|-------------|
|
|
336
|
-
| Unit | Vitest | `domain/` entities, value objects |
|
|
337
|
-
| Component | Vitest + Testing Library | `interfaces/` components |
|
|
338
|
-
| Integration | Vitest + MSW | API client with mocked server |
|
|
339
|
-
| E2E | Playwright | Full user flows |
|
|
319
|
+
### The critical rule: `shared/` must NOT contain business logic
|
|
340
320
|
|
|
341
321
|
```typescript
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
expect(cart.total).toBe(200);
|
|
350
|
-
});
|
|
351
|
-
});
|
|
322
|
+
// ❌ BAD — business logic in shared/
|
|
323
|
+
shared/lib/thread-utils.ts
|
|
324
|
+
export function calculateThreadPriority(thread: Thread): number { }
|
|
325
|
+
|
|
326
|
+
// ✅ GOOD — business logic in the module
|
|
327
|
+
collaboration/domain/thread.ts
|
|
328
|
+
export function calculateThreadPriority(thread: Thread): number { }
|
|
352
329
|
```
|
|
353
330
|
|
|
354
|
-
|
|
331
|
+
```typescript
|
|
332
|
+
// ✅ GOOD — generic infrastructure in shared/
|
|
333
|
+
shared/lib/api-client.ts
|
|
334
|
+
export class ApiClient {
|
|
335
|
+
async get<T>(path: string): Promise<T> { }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ✅ GOOD — domain-specific API in module infrastructure
|
|
339
|
+
collaboration/infrastructure/api.ts
|
|
340
|
+
import { ApiClient } from '@/shared/lib/api-client';
|
|
341
|
+
const api = new ApiClient();
|
|
342
|
+
export async function fetchThreads(nodeId: string): Promise<Thread[]> {
|
|
343
|
+
return api.get(`/nodes/${nodeId}/threads`);
|
|
344
|
+
}
|
|
345
|
+
```
|
|
355
346
|
|
|
356
|
-
|
|
347
|
+
### When the same concept appears in multiple domains (Shared Kernel)
|
|
357
348
|
|
|
358
349
|
```typescript
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
350
|
+
// shared/domain/user.ts — User is a shared concept
|
|
351
|
+
// Shared kernel: multiple domains reference the same User
|
|
352
|
+
export interface User {
|
|
353
|
+
id: string;
|
|
354
|
+
name: string;
|
|
355
|
+
email: string;
|
|
362
356
|
}
|
|
363
357
|
|
|
364
|
-
//
|
|
365
|
-
|
|
358
|
+
// collaboration/domain/thread.ts — imports shared User, doesn't redefine it
|
|
359
|
+
import { User } from '@/shared/domain/user';
|
|
360
|
+
export interface Thread {
|
|
361
|
+
id: string;
|
|
362
|
+
title: string;
|
|
363
|
+
createdBy: User; // References shared type, not duplicated
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
366
|
|
|
367
|
-
|
|
368
|
-
'use client'; // BAD — only add to interactive islands
|
|
367
|
+
### File naming convention
|
|
369
368
|
|
|
370
|
-
|
|
371
|
-
<div style={{ marginLeft: '16px' }}> // BAD — use Tailwind or CSS Modules
|
|
369
|
+
Files are named by their **concept**, not prefixed with the module name:
|
|
372
370
|
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
| Wrong | Right |
|
|
372
|
+
|-------|-------|
|
|
373
|
+
| `collaboration-types.ts` | `types.ts` (or `thread.ts`, `comment.ts`) |
|
|
374
|
+
| `collaboration-errors.ts` | `errors.ts` |
|
|
375
|
+
| `collaboration-store.ts` | `store.ts` |
|
|
375
376
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
377
|
+
The directory path `collaboration/domain/thread.ts` already tells you which module it belongs to.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## 8. Testing by Layer
|
|
382
|
+
|
|
383
|
+
| Layer | Test type | Framework | What to test |
|
|
384
|
+
|-------|-----------|-----------|-------------|
|
|
385
|
+
| `domain/` | Unit | Vitest | Types, errors, pure functions |
|
|
386
|
+
| `application/` | Unit | Vitest | Store logic, action validation |
|
|
387
|
+
| `infrastructure/` | Integration | Vitest + MSW | API calls with mocked server |
|
|
388
|
+
| `interfaces/` | Component | Vitest + Testing Library | Render, user events, accessibility |
|
|
379
389
|
|
|
380
390
|
---
|
|
381
391
|
|
|
382
|
-
*Version: 1.
|
|
392
|
+
*Version: 1.2.0*
|
|
383
393
|
*Last updated: 2026-07-03*
|