guardian-framework 0.1.23 → 0.1.25
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 +1 -1
- package/dist/exports.js +1 -1
- package/package.json +1 -1
- package/templates/pi/preflight_report.json +1 -1
- package/templates/pi/skills/agents/design-system-codegen.md +41 -0
- package/templates/pi/skills/angular-enterprise-codegen.md +330 -78
- package/templates/pi/skills/design-system-enterprise-codegen.md +351 -0
- package/templates/pi/skills/nextjs-enterprise-codegen.md +319 -74
|
@@ -1,138 +1,383 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nextjs-enterprise-codegen
|
|
3
|
-
description: Full reference for Next.js enterprise code generation with DDD + Clean Architecture. Covers module structure, server
|
|
3
|
+
description: Full reference for Next.js enterprise code generation with DDD + Clean Architecture. Covers module structure, server/client boundary, data fetching, state management, design system integration, error handling, testing, security, performance, and accessibility. 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
|
> All code MUST follow these patterns. Validators enforce compliance.
|
|
10
|
+
>
|
|
11
|
+
> For design system and styling patterns, see: `.pi/skills/design-system-enterprise-codegen.md` — tokens, CSS adapters, theming, component API contracts
|
|
10
12
|
|
|
11
13
|
---
|
|
12
14
|
|
|
13
|
-
## 1. Project Structure
|
|
15
|
+
## 1. Project Structure — DDD with Next.js App Router
|
|
14
16
|
|
|
15
17
|
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
src/
|
|
19
|
+
module/ # One bounded context
|
|
20
|
+
domain/ # Pure business logic, zero framework imports
|
|
21
|
+
entity.ts # Domain entities with encapsulated state
|
|
22
|
+
value.ts # Immutable value objects
|
|
23
|
+
events.ts # Domain events
|
|
24
|
+
errors.ts # Typed error classes
|
|
25
|
+
application/ # Application logic
|
|
26
|
+
store.ts # Zustand / Jotai state (client-only)
|
|
27
|
+
actions.ts # Server Actions (server-only)
|
|
28
|
+
queries.ts # React Query hooks for data fetching
|
|
29
|
+
dto.ts # Command/Query DTOs
|
|
30
|
+
infrastructure/ # External adapters
|
|
31
|
+
api-client.ts # fetch wrapper, error handling, retry
|
|
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
|
|
32
45
|
```
|
|
33
46
|
|
|
34
47
|
### Dependency Rule
|
|
35
48
|
```
|
|
36
49
|
domain → application → infrastructure → interfaces
|
|
50
|
+
↕
|
|
51
|
+
shared/ (no framework deps)
|
|
37
52
|
```
|
|
38
53
|
|
|
54
|
+
- **domain/** — zero React/Next.js imports. Pure TypeScript.
|
|
55
|
+
- **application/** — imports from domain. May import from `shared/lib/`.
|
|
56
|
+
- **infrastructure/** — imports from domain + application. Wraps external SDKs.
|
|
57
|
+
- **interfaces/** — imports from application. Contains all React/Next.js code.
|
|
58
|
+
|
|
39
59
|
---
|
|
40
60
|
|
|
41
|
-
## 2.
|
|
61
|
+
## 2. Server/Client Boundary
|
|
62
|
+
|
|
63
|
+
This is the most critical architectural decision in Next.js. Get it wrong and you lose the benefits of both paradigms.
|
|
64
|
+
|
|
65
|
+
### What goes where
|
|
66
|
+
|
|
67
|
+
| Layer | Renders on | 'use client'? | Can use |
|
|
68
|
+
|-------|-----------|---------------|---------|
|
|
69
|
+
| `interfaces/page.tsx` | Server | No | `async`, `cookies()`, `headers()`, direct DB |
|
|
70
|
+
| `interfaces/*.client.tsx` | Client | Yes | `useState`, `useEffect`, `onClick`, browser APIs |
|
|
71
|
+
| `application/store.ts` | Client | Depends | Zustand, Jotai |
|
|
72
|
+
| `application/actions.ts` | Server | No | `'use server'`, `revalidatePath()` |
|
|
73
|
+
| `application/queries.ts` | Client | Yes | React Query, SWR |
|
|
74
|
+
| `domain/`, `infrastructure/` | Both | No | Pure TS, fetch |
|
|
75
|
+
|
|
76
|
+
### Pattern: Island Architecture
|
|
77
|
+
|
|
78
|
+
```
|
|
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)
|
|
42
104
|
|
|
43
105
|
```typescript
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
private readonly _id: string;
|
|
47
|
-
private _items: CartItem[] = [];
|
|
106
|
+
// interfaces/products/page.tsx — Server Component, async
|
|
107
|
+
import { ProductService } from '../../application/actions';
|
|
48
108
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
109
|
+
export default async function ProductsPage() {
|
|
110
|
+
const products = await ProductService.list(); // Direct call, no API route
|
|
52
111
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
existing.increaseQuantity(quantity);
|
|
61
|
-
} else {
|
|
62
|
-
this._items.push(new CartItem(product, quantity));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
112
|
+
return (
|
|
113
|
+
<div>
|
|
114
|
+
{products.map(p => (
|
|
115
|
+
<ProductCard key={p.id} product={p} /> // Server-rendered
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
65
119
|
}
|
|
66
120
|
```
|
|
67
121
|
|
|
122
|
+
### Client Data Fetching (React Query)
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// application/queries.ts
|
|
126
|
+
import { useQuery } from '@tanstack/react-query';
|
|
127
|
+
import { apiClient } from '../infrastructure/api-client';
|
|
128
|
+
|
|
129
|
+
export function useProducts() {
|
|
130
|
+
return useQuery({
|
|
131
|
+
queryKey: ['products'],
|
|
132
|
+
queryFn: () => apiClient.get('/api/products'),
|
|
133
|
+
staleTime: 30_000, // 30s before refetch
|
|
134
|
+
gcTime: 5 * 60 * 1000, // 5min cache
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// interfaces/product-list.client.tsx
|
|
139
|
+
'use client';
|
|
140
|
+
export function ProductList() {
|
|
141
|
+
const { data, isLoading } = useProducts();
|
|
142
|
+
if (isLoading) return <Skeleton />;
|
|
143
|
+
return data.map(p => <ProductCard key={p.id} product={p} />);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Server Actions (Mutations)
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// application/actions.ts
|
|
151
|
+
'use server';
|
|
152
|
+
import { revalidatePath } from 'next/cache';
|
|
153
|
+
import { Cart } from '../domain/entity';
|
|
154
|
+
|
|
155
|
+
export async function addToCart(productId: string, quantity: number) {
|
|
156
|
+
const cart = await loadCart();
|
|
157
|
+
cart.addItem(productId, quantity);
|
|
158
|
+
await saveCart(cart);
|
|
159
|
+
revalidatePath('/cart'); // Revalidate without full page reload
|
|
160
|
+
return { success: true };
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
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
|
+
|
|
68
171
|
---
|
|
69
172
|
|
|
70
|
-
##
|
|
173
|
+
## 4. State Management
|
|
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` |
|
|
71
182
|
|
|
72
183
|
```typescript
|
|
73
184
|
// application/store.ts — Zustand store
|
|
74
185
|
import { create } from 'zustand';
|
|
186
|
+
import { devtools } from 'zustand/middleware';
|
|
75
187
|
import { Cart } from '../domain/entity';
|
|
76
188
|
|
|
77
|
-
interface
|
|
189
|
+
interface CartState {
|
|
78
190
|
cart: Cart | null;
|
|
79
|
-
|
|
80
|
-
addItem: (productId: string,
|
|
191
|
+
isOpen: boolean; // UI state — cart drawer toggle
|
|
192
|
+
addItem: (productId: string, qty: number) => Promise<void>;
|
|
193
|
+
toggleCart: () => void;
|
|
81
194
|
}
|
|
82
195
|
|
|
83
|
-
export const useCartStore = create<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}))
|
|
196
|
+
export const useCartStore = create<CartState>()(
|
|
197
|
+
devtools(
|
|
198
|
+
(set) => ({
|
|
199
|
+
cart: null,
|
|
200
|
+
isOpen: false,
|
|
201
|
+
addItem: async (productId, qty) => {
|
|
202
|
+
// Call Server Action, update local state optimistically
|
|
203
|
+
},
|
|
204
|
+
toggleCart: () => set(s => ({ isOpen: !s.isOpen })),
|
|
205
|
+
}),
|
|
206
|
+
{ name: 'cart-store' },
|
|
207
|
+
),
|
|
208
|
+
);
|
|
92
209
|
```
|
|
93
210
|
|
|
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
|
+
|
|
94
217
|
---
|
|
95
218
|
|
|
96
|
-
##
|
|
219
|
+
## 5. Design System & CSS Architecture
|
|
220
|
+
|
|
221
|
+
### Structure
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
shared/
|
|
225
|
+
ui/ # Design system components
|
|
226
|
+
tokens/ # Design tokens
|
|
227
|
+
colors.ts
|
|
228
|
+
typography.ts
|
|
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
|
+
```
|
|
240
|
+
|
|
241
|
+
### Styling Strategy
|
|
242
|
+
|
|
243
|
+
| Approach | When to use |
|
|
244
|
+
|----------|-------------|
|
|
245
|
+
| CSS Modules (`*.module.css`) | Component-scoped styles in design system |
|
|
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 |
|
|
97
249
|
|
|
98
250
|
```typescript
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
251
|
+
// shared/ui/button/button.tsx
|
|
252
|
+
import { type VariantProps, cva } from 'class-variance-authority';
|
|
253
|
+
import { cn } from '../../lib/utils';
|
|
254
|
+
|
|
255
|
+
const buttonVariants = cva(
|
|
256
|
+
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors',
|
|
257
|
+
{
|
|
258
|
+
variants: {
|
|
259
|
+
variant: {
|
|
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} />;
|
|
106
281
|
}
|
|
107
282
|
```
|
|
108
283
|
|
|
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
|
+
|
|
109
292
|
---
|
|
110
293
|
|
|
111
|
-
##
|
|
294
|
+
## 6. Error Handling
|
|
112
295
|
|
|
113
296
|
```typescript
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
297
|
+
// domain/errors.ts — Typed domain errors
|
|
298
|
+
export class DomainError extends Error {
|
|
299
|
+
constructor(
|
|
300
|
+
public readonly code: string,
|
|
301
|
+
message: string,
|
|
302
|
+
public readonly status: number = 400,
|
|
303
|
+
) {
|
|
304
|
+
super(message);
|
|
305
|
+
this.name = 'DomainError';
|
|
306
|
+
}
|
|
120
307
|
|
|
121
|
-
|
|
122
|
-
|
|
308
|
+
static notFound(id: string) {
|
|
309
|
+
return new DomainError('NOT_FOUND', `Resource ${id} not found`, 404);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
123
312
|
|
|
313
|
+
// interfaces/error.tsx — Next.js error boundary
|
|
314
|
+
'use client';
|
|
315
|
+
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
124
316
|
return (
|
|
125
|
-
<div>
|
|
126
|
-
<
|
|
127
|
-
{
|
|
128
|
-
<CartItem key={item.id} item={item} />
|
|
129
|
-
))}
|
|
317
|
+
<div role="alert">
|
|
318
|
+
<h2>Something went wrong</h2>
|
|
319
|
+
<button onClick={() => reset()}>Try again</button>
|
|
130
320
|
</div>
|
|
131
321
|
);
|
|
132
322
|
}
|
|
323
|
+
|
|
324
|
+
// interfaces/not-found.tsx — Next.js 404 boundary
|
|
325
|
+
export default function NotFound() {
|
|
326
|
+
return <div>Page not found</div>;
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## 7. Testing Patterns
|
|
333
|
+
|
|
334
|
+
| Test type | Tool | What to test |
|
|
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 |
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
// domain/entity.test.ts — Pure unit test, no React
|
|
343
|
+
import { Cart } from './entity';
|
|
344
|
+
|
|
345
|
+
describe('Cart', () => {
|
|
346
|
+
it('adds item', () => {
|
|
347
|
+
const cart = new Cart();
|
|
348
|
+
cart.addItem({ id: '1', name: 'Test', price: 100 }, 2);
|
|
349
|
+
expect(cart.total).toBe(200);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## 8. Anti-Patterns — NEVER DO
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
// ❌ Business logic in components
|
|
360
|
+
export default function Page() {
|
|
361
|
+
const [cart, setCart] = useState([]); // BAD — belongs in domain/
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ❌ Data fetching in client components without React Query
|
|
365
|
+
useEffect(() => { fetch('/api/data').then(setData); }, []); // BAD
|
|
366
|
+
|
|
367
|
+
// ❌ 'use client' on the entire page
|
|
368
|
+
'use client'; // BAD — only add to interactive islands
|
|
369
|
+
|
|
370
|
+
// ❌ Inline styles
|
|
371
|
+
<div style={{ marginLeft: '16px' }}> // BAD — use Tailwind or CSS Modules
|
|
372
|
+
|
|
373
|
+
// ❌ Mixing server/client concerns
|
|
374
|
+
import { cookies } from 'next/headers'; // BAD in client component
|
|
375
|
+
|
|
376
|
+
// ❌ Global CSS for component styles
|
|
377
|
+
h1 { color: blue; } // BAD — use CSS Modules or Tailwind
|
|
133
378
|
```
|
|
134
379
|
|
|
135
380
|
---
|
|
136
381
|
|
|
137
|
-
*Version: 1.
|
|
382
|
+
*Version: 1.1.0*
|
|
138
383
|
*Last updated: 2026-07-03*
|