@polymorphism-tech/morph-spec 4.8.1 → 4.8.4

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.
Files changed (44) hide show
  1. package/README.md +2 -2
  2. package/claude-plugin.json +1 -1
  3. package/docs/CHEATSHEET.md +1 -1
  4. package/docs/QUICKSTART.md +1 -1
  5. package/framework/hooks/dev/guard-version-numbers.js +1 -1
  6. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
  7. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
  8. package/framework/skills/level-1-workflows/phase-design/SKILL.md +1 -1
  9. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +1 -1
  10. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -1
  11. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +1 -1
  12. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
  13. package/package.json +4 -4
  14. package/.morph/analytics/threads-log.jsonl +0 -54
  15. package/.morph/state.json +0 -198
  16. package/docs/ARCHITECTURE.md +0 -328
  17. package/docs/COMMAND-FLOWS.md +0 -398
  18. package/docs/plans/2026-02-22-claude-docs-morph-alignment-analysis.md +0 -514
  19. package/docs/plans/2026-02-22-claude-settings.md +0 -517
  20. package/docs/plans/2026-02-22-morph-cc-alignment-impl.md +0 -730
  21. package/docs/plans/2026-02-22-morph-spec-next.md +0 -480
  22. package/docs/plans/2026-02-22-native-alignment-design.md +0 -201
  23. package/docs/plans/2026-02-22-native-alignment-impl.md +0 -927
  24. package/docs/plans/2026-02-22-native-enrichment-design.md +0 -246
  25. package/docs/plans/2026-02-22-native-enrichment.md +0 -737
  26. package/docs/plans/2026-02-23-ddd-architecture-refactor.md +0 -1155
  27. package/docs/plans/2026-02-23-ddd-nextsteps.md +0 -684
  28. package/docs/plans/2026-02-23-infra-architect-refactor.md +0 -439
  29. package/docs/plans/2026-02-23-nextjs-code-review-design.md +0 -157
  30. package/docs/plans/2026-02-23-nextjs-code-review-impl.md +0 -1256
  31. package/docs/plans/2026-02-23-nextjs-standards-design.md +0 -150
  32. package/docs/plans/2026-02-23-nextjs-standards-impl.md +0 -1848
  33. package/docs/plans/2026-02-24-cli-radical-simplification.md +0 -592
  34. package/docs/plans/2026-02-24-framework-failure-points.md +0 -125
  35. package/docs/plans/2026-02-24-morph-init-design.md +0 -337
  36. package/docs/plans/2026-02-24-morph-init-impl.md +0 -1269
  37. package/docs/plans/2026-02-24-tutorial-command-design.md +0 -71
  38. package/docs/plans/2026-02-24-tutorial-command.md +0 -298
  39. package/scripts/bump-version.js +0 -248
  40. package/scripts/generate-refs.js +0 -336
  41. package/scripts/generate-standards-registry.js +0 -44
  42. package/scripts/install-dev-hooks.js +0 -138
  43. package/scripts/scan-nextjs.mjs +0 -169
  44. package/scripts/validate-real.mjs +0 -255
@@ -1,1848 +0,0 @@
1
- # Next.js Standards Package Implementation Plan
2
-
3
- **Status:** COMPLETE
4
-
5
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
6
-
7
- **Goal:** Add a comprehensive Next.js standards package to morph-spec-framework covering standards files, a rule file, a validator, templates, and an enhanced agent for the stack: Next.js App Router + shadcn/ui + TanStack Query + react-hook-form/Zod + EasyPanel/Docker.
8
-
9
- **Architecture:** 8 standards files in `framework/standards/frontend/nextjs/`, 1 Claude Code rule file in `framework/rules/`, 1 validator module in `src/lib/validators/nextjs/`, 6 Handlebars templates in `framework/templates/frontend/nextjs/`, an updated `nextjs-expert` agent, and a regenerated STANDARDS.json. All content is opinionated for the locked stack — no generic fallbacks.
10
-
11
- **Tech Stack:** Node.js/ESM, Handlebars v2.0, Markdown, existing validator pattern from `src/lib/validators/blazor/blazor-validator.js`
12
-
13
- ---
14
-
15
- ## Locked Conventions (reference throughout)
16
-
17
- - **Files:** kebab-case (`user-card.tsx`)
18
- - **Exports:** PascalCase components (`UserCard`), camelCase hooks (`useCreateUser`)
19
- - **Hooks:** file `use-{action}.ts`, export `use{Action}()`
20
- - **Schemas:** `{feature}.schemas.ts` with `z.infer<>` for types
21
- - **Folders:** feature-based with `features/{name}/components|hooks|types/`
22
- - **Shared components:** `components/ui/` (shadcn, never edit) → `components/` (composed, no business logic) → `features/*/components/` (feature-scoped)
23
- - **Data:** Server Components for initial fetch, TanStack Query for mutations/client state
24
- - **Forms:** react-hook-form + Zod + shadcn `<Form>`
25
- - **Deployment:** EasyPanel → Docker multi-stage
26
-
27
- ---
28
-
29
- ### Task 1: Create 8 standards files
30
-
31
- **Files:**
32
- - Create: `framework/standards/frontend/nextjs/app-router.md`
33
- - Create: `framework/standards/frontend/nextjs/project-structure.md`
34
- - Create: `framework/standards/frontend/nextjs/naming-conventions.md`
35
- - Create: `framework/standards/frontend/nextjs/components.md`
36
- - Create: `framework/standards/frontend/nextjs/data-fetching.md`
37
- - Create: `framework/standards/frontend/nextjs/forms.md`
38
- - Create: `framework/standards/frontend/nextjs/state-management.md`
39
- - Create: `framework/standards/frontend/nextjs/testing.md`
40
-
41
- **Note:** No tests for content files. Write directly.
42
-
43
- **Step 1: Create `framework/standards/frontend/nextjs/app-router.md`**
44
-
45
- ```markdown
46
- # Next.js App Router Standard
47
-
48
- > **Scope:** frontend/nextjs/app-router
49
- > **Layer:** 2 (on keyword)
50
- > **Keywords:** next.js, app router, server component, client component, page, layout, route
51
- > **Load When:** editing files in `app/` or `features/` directories
52
-
53
- Next.js App Router with TypeScript strict mode. Server Components by default, Client Components only when interactivity is required.
54
-
55
- ## Core Rules
56
-
57
- - ALWAYS default to Server Components — add `'use client'` only when needed
58
- - ALWAYS keep `app/` directory for routing only — no business logic in page files
59
- - NEVER put data fetching in Client Components when a Server Component can do it
60
- - NEVER use `useEffect` to fetch data — use Server Components or TanStack Query
61
- - ALWAYS co-locate loading/error UI: `loading.tsx`, `error.tsx` next to `page.tsx`
62
- - ALWAYS use TypeScript — no `.js` or `.jsx` files in the project
63
-
64
- ## Server vs Client Components
65
-
66
- | Use Server Component | Use Client Component |
67
- |---------------------|---------------------|
68
- | Fetching from .NET API on load | onClick, onChange, form submit |
69
- | Rendering static or user-specific data | useState, useEffect, useRef |
70
- | Accessing backend environment variables | Browser APIs (localStorage, geolocation) |
71
- | SEO-critical content | TanStack Query hooks |
72
- | No interactivity needed | shadcn/ui interactive components |
73
-
74
- ## Decision Tree
75
-
76
- ```
77
- Does this component need user interaction (click, input, hover state)?
78
- YES → 'use client'
79
- NO → Does it fetch data?
80
- YES → Server Component (fetch directly)
81
- NO → Server Component (static)
82
- ```
83
-
84
- ## App Directory Structure
85
-
86
- ```
87
- src/app/
88
- ├── (auth)/
89
- │ ├── login/
90
- │ │ └── page.tsx # Server Component — renders login form
91
- │ └── register/
92
- │ └── page.tsx
93
- ├── (dashboard)/
94
- │ ├── layout.tsx # Shared layout for dashboard routes
95
- │ ├── page.tsx # Dashboard home
96
- │ └── users/
97
- │ ├── page.tsx # User list (Server Component)
98
- │ ├── [id]/
99
- │ │ └── page.tsx # User detail
100
- │ ├── loading.tsx # Suspense fallback
101
- │ └── error.tsx # Error boundary
102
- └── layout.tsx # Root layout — providers, fonts, metadata
103
- ```
104
-
105
- ## File Conventions
106
-
107
- | File | Purpose | Type |
108
- |------|---------|------|
109
- | `page.tsx` | Route segment UI | Server Component (default) |
110
- | `layout.tsx` | Shared UI wrapper | Server Component |
111
- | `loading.tsx` | Suspense fallback | Server Component |
112
- | `error.tsx` | Error boundary | **Must be Client Component** |
113
- | `not-found.tsx` | 404 page | Server Component |
114
- | `route.ts` | API route (avoid — use .NET API) | — |
115
-
116
- ## Server Component Data Fetch Pattern
117
-
118
- ```tsx
119
- // app/(dashboard)/users/page.tsx
120
- import { UserList } from '@/features/users/components/user-list';
121
-
122
- async function getUsers(): Promise<User[]> {
123
- const res = await fetch(`${process.env.API_URL}/api/users`, {
124
- headers: { Authorization: `Bearer ${await getServerToken()}` },
125
- next: { revalidate: 60 }, // ISR: revalidate every 60s
126
- });
127
- if (!res.ok) throw new Error('Failed to fetch users');
128
- return res.json();
129
- }
130
-
131
- export default async function UsersPage() {
132
- const users = await getUsers();
133
- return <UserList initialUsers={users} />;
134
- }
135
- ```
136
-
137
- ## Root Layout — Required Providers
138
-
139
- ```tsx
140
- // app/layout.tsx
141
- import { QueryProvider } from '@/lib/query-client';
142
- import { Toaster } from '@/components/ui/sonner';
143
-
144
- export default function RootLayout({ children }: { children: React.ReactNode }) {
145
- return (
146
- <html lang="pt-BR">
147
- <body>
148
- <QueryProvider>
149
- {children}
150
- <Toaster />
151
- </QueryProvider>
152
- </body>
153
- </html>
154
- );
155
- }
156
- ```
157
-
158
- ## Common Mistakes
159
-
160
- | Wrong | Right | Why |
161
- |-------|-------|-----|
162
- | `'use client'` on every component | Only add when needed | Kills SSR benefits, bloats JS bundle |
163
- | `useEffect(() => fetch(...), [])` | Server Component fetch or TanStack Query | Two renders, no caching, no SSR |
164
- | Business logic in `page.tsx` | Move to `features/` | pages are routes, not controllers |
165
- | `fetch` without error handling | Always check `res.ok` | Silent failures in production |
166
-
167
- ---
168
-
169
- *MORPH-SPEC by Polymorphism Tech*
170
- ```
171
-
172
- **Step 2: Create `framework/standards/frontend/nextjs/project-structure.md`**
173
-
174
- ```markdown
175
- # Next.js Project Structure Standard
176
-
177
- > **Scope:** frontend/nextjs/project-structure
178
- > **Layer:** 2 (on keyword)
179
- > **Keywords:** project structure, folder structure, feature folder, src layout
180
- > **Load When:** creating new files or features
181
-
182
- Feature-based architecture with a shared core. Features are self-contained; shared code is explicit.
183
-
184
- ## Core Rules
185
-
186
- - ALWAYS use feature-based folders: `features/{feature-name}/`
187
- - ALWAYS keep `app/` for routing only — import from `features/`, not the other way
188
- - NEVER import from one feature into another — extract to `components/` or `lib/` if shared
189
- - ALWAYS use `src/` directory (configured in `tsconfig.json` with `@/` alias)
190
- - NEVER put business logic in `components/` — it is presentation only
191
-
192
- ## Canonical Folder Tree
193
-
194
- ```
195
- src/
196
- ├── app/ # Next.js App Router — routes only
197
- │ ├── layout.tsx # Root layout + providers
198
- │ ├── page.tsx # Home page
199
- │ └── (dashboard)/
200
- │ ├── layout.tsx
201
- │ ├── users/
202
- │ │ ├── page.tsx # Renders UserPage from features/users
203
- │ │ ├── loading.tsx
204
- │ │ └── error.tsx
205
- │ └── billing/
206
- │ └── page.tsx
207
-
208
- ├── features/ # Domain features
209
- │ └── {feature-name}/
210
- │ ├── components/ # Feature-specific UI components
211
- │ │ ├── user-list.tsx
212
- │ │ └── user-card.tsx
213
- │ ├── hooks/ # TanStack Query hooks for this feature
214
- │ │ ├── use-users.ts
215
- │ │ └── use-create-user.ts
216
- │ ├── types/
217
- │ │ ├── user.types.ts # TypeScript types
218
- │ │ └── user.schemas.ts # Zod schemas + z.infer<> types
219
- │ └── index.ts # Public API — only export what's needed
220
-
221
- ├── components/ # Shared UI — no business logic
222
- │ ├── ui/ # shadcn/ui — never edit directly
223
- │ │ ├── button.tsx
224
- │ │ └── card.tsx
225
- │ ├── data-table.tsx # Composed: TanStack Table + shadcn Table
226
- │ ├── page-header.tsx
227
- │ └── empty-state.tsx
228
-
229
- ├── hooks/ # Shared utility hooks — no API calls
230
- │ ├── use-debounce.ts
231
- │ └── use-media-query.ts
232
-
233
- ├── lib/
234
- │ ├── api-client.ts # Typed fetch wrapper for .NET API
235
- │ └── query-client.tsx # TanStack Query client + provider
236
-
237
- ├── types/
238
- │ ├── api.ts # Shared API shapes: PaginatedResponse<T>, ApiError
239
- │ └── env.d.ts # Environment variable types
240
-
241
- └── env.mjs # Zod-validated env vars — crashes at startup if missing
242
- ```
243
-
244
- ## Feature Index Pattern
245
-
246
- ```ts
247
- // features/users/index.ts — explicit public API
248
- export { UserList } from './components/user-list';
249
- export { UserCard } from './components/user-card';
250
- export { useUsers } from './hooks/use-users';
251
- export { useCreateUser } from './hooks/use-create-user';
252
- export type { User, CreateUserInput } from './types/user.types';
253
- ```
254
-
255
- Import from the index, not deep paths:
256
- ```ts
257
- // GOOD
258
- import { UserList, useUsers } from '@/features/users';
259
-
260
- // BAD
261
- import { UserList } from '@/features/users/components/user-list';
262
- ```
263
-
264
- ## Feature Boundary Rule
265
-
266
- ```
267
- app/ → imports from features/ ✓
268
- features/users/ → imports from components/ and lib/ ✓
269
- features/users/ → imports from features/billing/ ✗ (extract shared code instead)
270
- components/ → imports from components/ui/ ✓
271
- components/ → imports from features/ ✗
272
- ```
273
-
274
- ---
275
-
276
- *MORPH-SPEC by Polymorphism Tech*
277
- ```
278
-
279
- **Step 3: Create `framework/standards/frontend/nextjs/naming-conventions.md`**
280
-
281
- ```markdown
282
- # Next.js Naming Conventions Standard
283
-
284
- > **Scope:** frontend/nextjs/naming-conventions
285
- > **Layer:** 1 (always)
286
- > **Keywords:** naming, conventions, file names, component names, hooks, typescript
287
- > **Load When:** creating any new file in a Next.js project
288
-
289
- Consistent naming prevents filesystem bugs (Linux case-sensitivity) and keeps the codebase predictable.
290
-
291
- ## Core Rules
292
-
293
- - ALWAYS use kebab-case for file names — never PascalCase or camelCase
294
- - ALWAYS use PascalCase for React component exports
295
- - ALWAYS use camelCase starting with `use` for hook exports
296
- - ALWAYS suffix schema files with `.schemas.ts` and type files with `.types.ts`
297
- - NEVER mix cases in the same category — no `UserCard.tsx` alongside `user-profile.tsx`
298
-
299
- ## Complete Reference Table
300
-
301
- | Artifact | File Name | Export Name | Example |
302
- |----------|-----------|-------------|---------|
303
- | React Component | `user-card.tsx` | `UserCard` | `export function UserCard()` |
304
- | Client Component | `user-form.tsx` | `UserForm` | `export function UserForm()` (+ `'use client'`) |
305
- | TanStack Query hook | `use-users.ts` | `useUsers` | `export function useUsers()` |
306
- | Mutation hook | `use-create-user.ts` | `useCreateUser` | `export function useCreateUser()` |
307
- | Utility hook | `use-debounce.ts` | `useDebounce` | `export function useDebounce()` |
308
- | Zod schema file | `user.schemas.ts` | `createUserSchema` | `export const createUserSchema = z.object(...)` |
309
- | TypeScript types | `user.types.ts` | `User`, `CreateUserInput` | `export type User = z.infer<typeof userSchema>` |
310
- | API utilities | `users-api.ts` | `fetchUsers` | `export async function fetchUsers()` |
311
- | Feature index | `index.ts` | (re-exports) | `export { UserCard } from './components/user-card'` |
312
- | Page file | `page.tsx` | `default` | `export default function UsersPage()` |
313
-
314
- ## Component Naming Rules
315
-
316
- ```
317
- Feature name: users
318
- List component: user-list.tsx → UserList
319
- Card component: user-card.tsx → UserCard
320
- Form component: user-form.tsx → UserForm
321
- Dialog: create-user-dialog.tsx → CreateUserDialog
322
- Table: users-table.tsx → UsersTable
323
- ```
324
-
325
- ## Hook Naming Rules
326
-
327
- ```
328
- GET list: use-users.ts → useUsers()
329
- GET single: use-user.ts → useUser(id: string)
330
- POST: use-create-user.ts → useCreateUser()
331
- PUT/PATCH: use-update-user.ts → useUpdateUser()
332
- DELETE: use-delete-user.ts → useDeleteUser()
333
- ```
334
-
335
- ## Schema and Type Naming Rules
336
-
337
- ```typescript
338
- // user.schemas.ts
339
- export const userSchema = z.object({ id: z.string(), name: z.string() });
340
- export const createUserSchema = z.object({ name: z.string().min(2), email: z.string().email() });
341
- export const updateUserSchema = createUserSchema.partial();
342
-
343
- // user.types.ts — derive from schemas, don't duplicate
344
- export type User = z.infer<typeof userSchema>;
345
- export type CreateUserInput = z.infer<typeof createUserSchema>;
346
- export type UpdateUserInput = z.infer<typeof updateUserSchema>;
347
- ```
348
-
349
- ## Zod Schema Conventions
350
-
351
- | Operation | Schema Name | Derived Type |
352
- |-----------|-------------|--------------|
353
- | Response shape | `userSchema` | `User` |
354
- | Create request | `createUserSchema` | `CreateUserInput` |
355
- | Update request | `updateUserSchema` | `UpdateUserInput` |
356
- | Form values | `userFormSchema` | `UserFormValues` |
357
- | API params | `getUsersParamsSchema` | `GetUsersParams` |
358
-
359
- ## Common Mistakes
360
-
361
- | Wrong | Right | Why |
362
- |-------|-------|-----|
363
- | `UserCard.tsx` | `user-card.tsx` | Linux servers are case-sensitive |
364
- | `useUsers.ts` | `use-users.ts` | Inconsistent with Next.js file conventions |
365
- | `export default function UserCard` | `export function UserCard` | Named exports are more refactor-safe |
366
- | `type User = { id: string; name: string }` | `type User = z.infer<typeof userSchema>` | Single source of truth |
367
-
368
- ---
369
-
370
- *MORPH-SPEC by Polymorphism Tech*
371
- ```
372
-
373
- **Step 4: Create `framework/standards/frontend/nextjs/components.md`**
374
-
375
- ```markdown
376
- # Next.js Component Standards
377
-
378
- > **Scope:** frontend/nextjs/components
379
- > **Layer:** 2 (on keyword)
380
- > **Keywords:** component, shadcn, reusable, shared, ui, three-tier
381
- > **Load When:** creating or editing React components
382
-
383
- Three-tier component hierarchy. `components/ui/` is shadcn primitives (never edit). `components/` is composed shared (no business logic). `features/*/components/` is feature-scoped.
384
-
385
- ## Core Rules
386
-
387
- - NEVER edit files in `components/ui/` — they are regenerated by shadcn CLI
388
- - NEVER import from `features/` inside `components/` — components know nothing about domain
389
- - ALWAYS compose shadcn primitives in `components/` instead of editing them
390
- - ALWAYS add `'use client'` only if the component uses hooks, events, or browser APIs
391
- - NEVER pass raw API data directly to a component — transform to props first
392
-
393
- ## Three-Tier Hierarchy
394
-
395
- ```
396
- Tier 1: components/ui/ ← shadcn/ui CLI output (DO NOT EDIT)
397
- ↓ composed into
398
- Tier 2: components/ ← shared project components (no business logic)
399
- ↓ used by
400
- Tier 3: features/*/components/ ← feature-scoped (knows about users, billing, etc.)
401
- ```
402
-
403
- ## Tier 1 — shadcn/ui Primitives
404
-
405
- ```bash
406
- # Add shadcn components via CLI — never write them manually
407
- npx shadcn@latest add button card dialog form input table
408
-
409
- # Available at:
410
- src/components/ui/button.tsx
411
- src/components/ui/card.tsx
412
- src/components/ui/dialog.tsx
413
- ```
414
-
415
- If you need to change a shadcn component's behavior, wrap it — do not edit the source file.
416
-
417
- ## Tier 2 — Shared Composed Components
418
-
419
- ```tsx
420
- // components/data-table.tsx — composes shadcn Table + TanStack Table
421
- 'use client'; // TanStack Table requires client
422
-
423
- import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
424
- import { type ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
425
-
426
- interface DataTableProps<TData> {
427
- columns: ColumnDef<TData>[];
428
- data: TData[];
429
- }
430
-
431
- export function DataTable<TData>({ columns, data }: DataTableProps<TData>) {
432
- const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
433
- return (
434
- <Table>
435
- <TableHeader>
436
- {table.getHeaderGroups().map((headerGroup) => (
437
- <TableRow key={headerGroup.id}>
438
- {headerGroup.headers.map((header) => (
439
- <TableHead key={header.id}>
440
- {flexRender(header.column.columnDef.header, header.getContext())}
441
- </TableHead>
442
- ))}
443
- </TableRow>
444
- ))}
445
- </TableHeader>
446
- <TableBody>
447
- {table.getRowModel().rows.map((row) => (
448
- <TableRow key={row.id}>
449
- {row.getVisibleCells().map((cell) => (
450
- <TableCell key={cell.id}>
451
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
452
- </TableCell>
453
- ))}
454
- </TableRow>
455
- ))}
456
- </TableBody>
457
- </Table>
458
- );
459
- }
460
- ```
461
-
462
- ## Tier 3 — Feature Components
463
-
464
- ```tsx
465
- // features/users/components/user-list.tsx
466
- // Uses DataTable (Tier 2) and UserCard (Tier 3)
467
- 'use client';
468
-
469
- import { DataTable } from '@/components/data-table';
470
- import { useUsers } from '@/features/users/hooks/use-users';
471
- import { type ColumnDef } from '@tanstack/react-table';
472
- import type { User } from '@/features/users/types/user.types';
473
-
474
- const columns: ColumnDef<User>[] = [
475
- { accessorKey: 'name', header: 'Name' },
476
- { accessorKey: 'email', header: 'Email' },
477
- ];
478
-
479
- export function UserList() {
480
- const { data: users = [], isLoading } = useUsers();
481
- if (isLoading) return <div>Loading...</div>;
482
- return <DataTable columns={columns} data={users} />;
483
- }
484
- ```
485
-
486
- ## Component Props Conventions
487
-
488
- ```tsx
489
- // GOOD — explicit, typed props
490
- interface UserCardProps {
491
- user: User;
492
- onEdit?: (id: string) => void;
493
- className?: string; // Always allow className for Tailwind override
494
- }
495
-
496
- export function UserCard({ user, onEdit, className }: UserCardProps) {}
497
-
498
- // BAD — spreading unknown props, no types
499
- export function UserCard({ ...props }) {}
500
- ```
501
-
502
- ## Common Mistakes
503
-
504
- | Wrong | Right | Why |
505
- |-------|-------|-----|
506
- | Edit `components/ui/button.tsx` | Wrap it in `components/action-button.tsx` | shadcn CLI overwrites it |
507
- | `import { useUsers } from '@/features/users'` inside `components/` | Move to a feature component | Breaks tier isolation |
508
- | `'use client'` on all components | Only on interactive ones | Unnecessary client JS |
509
- | Pass raw fetch response as prop | Type and validate with Zod first | Runtime type safety |
510
-
511
- ---
512
-
513
- *MORPH-SPEC by Polymorphism Tech*
514
- ```
515
-
516
- **Step 5: Create `framework/standards/frontend/nextjs/data-fetching.md`**
517
-
518
- ```markdown
519
- # Next.js Data Fetching Standard
520
-
521
- > **Scope:** frontend/nextjs/data-fetching
522
- > **Layer:** 2 (on keyword)
523
- > **Keywords:** data fetching, tanstack query, react query, server component, fetch, api
524
- > **Load When:** fetching data from .NET API
525
-
526
- Server Components for initial page load. TanStack Query for client mutations and interactive data. Never `useEffect` for fetching.
527
-
528
- ## Core Rules
529
-
530
- - ALWAYS use Server Components for initial data that doesn't require interactivity
531
- - ALWAYS use TanStack Query (`useQuery`, `useMutation`) for client-side data needs
532
- - NEVER use `useEffect(() => { fetch(...) }, [])` — this is the old pattern
533
- - ALWAYS validate API responses with Zod before using them
534
- - ALWAYS use query key factories for consistent cache invalidation
535
-
536
- ## TanStack Query Setup
537
-
538
- ```tsx
539
- // lib/query-client.tsx
540
- 'use client';
541
-
542
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
543
- import { useState } from 'react';
544
-
545
- export function QueryProvider({ children }: { children: React.ReactNode }) {
546
- const [queryClient] = useState(() => new QueryClient({
547
- defaultOptions: {
548
- queries: { staleTime: 60 * 1000, retry: 1 },
549
- },
550
- }));
551
- return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
552
- }
553
- ```
554
-
555
- ## Query Key Factory Pattern
556
-
557
- ```ts
558
- // features/users/hooks/query-keys.ts
559
- export const userKeys = {
560
- all: ['users'] as const,
561
- lists: () => [...userKeys.all, 'list'] as const,
562
- list: (filters: Record<string, unknown>) => [...userKeys.lists(), filters] as const,
563
- details: () => [...userKeys.all, 'detail'] as const,
564
- detail: (id: string) => [...userKeys.details(), id] as const,
565
- };
566
- ```
567
-
568
- ## useQuery Hook Pattern
569
-
570
- ```ts
571
- // features/users/hooks/use-users.ts
572
- import { useQuery } from '@tanstack/react-query';
573
- import { userKeys } from './query-keys';
574
- import { userSchema } from '@/features/users/types/user.schemas';
575
- import { z } from 'zod';
576
-
577
- const usersResponseSchema = z.array(userSchema);
578
-
579
- export function useUsers() {
580
- return useQuery({
581
- queryKey: userKeys.lists(),
582
- queryFn: async () => {
583
- const res = await fetch('/api/users'); // proxy to .NET API
584
- if (!res.ok) throw new Error('Failed to fetch users');
585
- return usersResponseSchema.parse(await res.json()); // Zod validation
586
- },
587
- });
588
- }
589
- ```
590
-
591
- ## useMutation Hook Pattern
592
-
593
- ```ts
594
- // features/users/hooks/use-create-user.ts
595
- import { useMutation, useQueryClient } from '@tanstack/react-query';
596
- import { userKeys } from './query-keys';
597
- import { type CreateUserInput } from '@/features/users/types/user.types';
598
-
599
- export function useCreateUser() {
600
- const queryClient = useQueryClient();
601
- return useMutation({
602
- mutationFn: async (input: CreateUserInput) => {
603
- const res = await fetch('/api/users', {
604
- method: 'POST',
605
- headers: { 'Content-Type': 'application/json' },
606
- body: JSON.stringify(input),
607
- });
608
- if (!res.ok) throw new Error('Failed to create user');
609
- return res.json();
610
- },
611
- onSuccess: () => {
612
- queryClient.invalidateQueries({ queryKey: userKeys.lists() });
613
- },
614
- });
615
- }
616
- ```
617
-
618
- ## Server Component + TanStack Query Handoff
619
-
620
- ```tsx
621
- // app/(dashboard)/users/page.tsx — Server Component: initial fetch
622
- async function getInitialUsers() {
623
- const res = await fetch(`${process.env.API_URL}/api/users`, { next: { revalidate: 30 } });
624
- return res.json();
625
- }
626
-
627
- export default async function UsersPage() {
628
- const initialUsers = await getInitialUsers();
629
- return <UserListClient initialData={initialUsers} />;
630
- }
631
-
632
- // features/users/components/user-list-client.tsx — Client Component: mutations
633
- 'use client';
634
- import { useUsers } from '@/features/users/hooks/use-users';
635
-
636
- export function UserListClient({ initialData }: { initialData: User[] }) {
637
- const { data: users = initialData } = useUsers(); // initialData hydrates cache
638
- // ...mutations, filtering, etc.
639
- }
640
- ```
641
-
642
- ## Common Mistakes
643
-
644
- | Wrong | Right | Why |
645
- |-------|-------|-----|
646
- | `useEffect(() => { fetch(...) }, [])` | `useQuery(...)` | Two renders, no cache, no SSR |
647
- | No Zod on API response | `schema.parse(await res.json())` | Type safety at runtime |
648
- | Hardcoded query keys: `['users']` | Key factory: `userKeys.lists()` | Consistent invalidation |
649
- | `queryClient.invalidateQueries(['users'])` | `queryClient.invalidateQueries({ queryKey: userKeys.lists() })` | Object syntax is required in v5 |
650
-
651
- ---
652
-
653
- *MORPH-SPEC by Polymorphism Tech*
654
- ```
655
-
656
- **Step 6: Create `framework/standards/frontend/nextjs/forms.md`**
657
-
658
- ```markdown
659
- # Next.js Forms Standard
660
-
661
- > **Scope:** frontend/nextjs/forms
662
- > **Layer:** 2 (on keyword)
663
- > **Keywords:** form, react-hook-form, zod, validation, input, submit
664
- > **Load When:** implementing any form in Next.js
665
-
666
- react-hook-form + Zod + shadcn Form components. Schema defines both validation rules and TypeScript types.
667
-
668
- ## Core Rules
669
-
670
- - ALWAYS define the Zod schema first — derive the TypeScript type from it
671
- - ALWAYS use `zodResolver` to connect schema to react-hook-form
672
- - ALWAYS use shadcn `<Form>`, `<FormField>`, `<FormItem>`, `<FormMessage>` for consistent UI
673
- - NEVER use `useState` for form field values — react-hook-form handles this
674
- - ALWAYS use `useMutation` from TanStack Query for form submission
675
-
676
- ## Complete Form Pattern
677
-
678
- ```tsx
679
- // features/users/types/user.schemas.ts
680
- import { z } from 'zod';
681
-
682
- export const createUserSchema = z.object({
683
- name: z.string().min(2, 'Name must be at least 2 characters'),
684
- email: z.string().email('Invalid email address'),
685
- role: z.enum(['admin', 'user'], { required_error: 'Role is required' }),
686
- });
687
-
688
- export type CreateUserInput = z.infer<typeof createUserSchema>;
689
- ```
690
-
691
- ```tsx
692
- // features/users/components/create-user-form.tsx
693
- 'use client';
694
-
695
- import { useForm } from 'react-hook-form';
696
- import { zodResolver } from '@hookform/resolvers/zod';
697
- import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
698
- import { Input } from '@/components/ui/input';
699
- import { Button } from '@/components/ui/button';
700
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
701
- import { useCreateUser } from '@/features/users/hooks/use-create-user';
702
- import { createUserSchema, type CreateUserInput } from '@/features/users/types/user.schemas';
703
-
704
- export function CreateUserForm({ onSuccess }: { onSuccess?: () => void }) {
705
- const form = useForm<CreateUserInput>({
706
- resolver: zodResolver(createUserSchema),
707
- defaultValues: { name: '', email: '', role: 'user' },
708
- });
709
-
710
- const { mutate: createUser, isPending } = useCreateUser();
711
-
712
- function onSubmit(values: CreateUserInput) {
713
- createUser(values, {
714
- onSuccess: () => { form.reset(); onSuccess?.(); },
715
- onError: (error) => { form.setError('root', { message: error.message }); },
716
- });
717
- }
718
-
719
- return (
720
- <Form {...form}>
721
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
722
- <FormField
723
- control={form.control}
724
- name="name"
725
- render={({ field }) => (
726
- <FormItem>
727
- <FormLabel>Name</FormLabel>
728
- <FormControl><Input placeholder="John Doe" {...field} /></FormControl>
729
- <FormMessage />
730
- </FormItem>
731
- )}
732
- />
733
- <FormField
734
- control={form.control}
735
- name="email"
736
- render={({ field }) => (
737
- <FormItem>
738
- <FormLabel>Email</FormLabel>
739
- <FormControl><Input type="email" placeholder="john@example.com" {...field} /></FormControl>
740
- <FormMessage />
741
- </FormItem>
742
- )}
743
- />
744
- <FormField
745
- control={form.control}
746
- name="role"
747
- render={({ field }) => (
748
- <FormItem>
749
- <FormLabel>Role</FormLabel>
750
- <Select onValueChange={field.onChange} defaultValue={field.value}>
751
- <FormControl>
752
- <SelectTrigger><SelectValue placeholder="Select role" /></SelectTrigger>
753
- </FormControl>
754
- <SelectContent>
755
- <SelectItem value="admin">Admin</SelectItem>
756
- <SelectItem value="user">User</SelectItem>
757
- </SelectContent>
758
- </Select>
759
- <FormMessage />
760
- </FormItem>
761
- )}
762
- />
763
- {form.formState.errors.root && (
764
- <p className="text-sm text-destructive">{form.formState.errors.root.message}</p>
765
- )}
766
- <Button type="submit" disabled={isPending}>
767
- {isPending ? 'Creating...' : 'Create User'}
768
- </Button>
769
- </form>
770
- </Form>
771
- );
772
- }
773
- ```
774
-
775
- ## Edit Form Pattern (pre-populated)
776
-
777
- ```tsx
778
- // Edit forms use the same schema but with defaultValues from existing data
779
- const form = useForm<UpdateUserInput>({
780
- resolver: zodResolver(updateUserSchema), // .partial() version
781
- defaultValues: { name: user.name, email: user.email, role: user.role },
782
- });
783
- ```
784
-
785
- ## Schema Composition
786
-
787
- ```ts
788
- // Reuse schemas — don't duplicate
789
- export const userSchema = z.object({ id: z.string(), name: z.string(), email: z.string() });
790
- export const createUserSchema = userSchema.omit({ id: true });
791
- export const updateUserSchema = createUserSchema.partial();
792
- ```
793
-
794
- ## Common Mistakes
795
-
796
- | Wrong | Right | Why |
797
- |-------|-------|-----|
798
- | `useState` for each field | `useForm` | Unnecessary re-renders on every keystroke |
799
- | Manual error display | `<FormMessage />` | Consistent UI, auto-connects to field errors |
800
- | Submit in `useEffect` | `form.handleSubmit(onSubmit)` | Handles validation before calling onSubmit |
801
- | `fetch` in submit handler | `useMutation` | Loading state, error handling, cache invalidation |
802
-
803
- ---
804
-
805
- *MORPH-SPEC by Polymorphism Tech*
806
- ```
807
-
808
- **Step 7: Create `framework/standards/frontend/nextjs/state-management.md`**
809
-
810
- ```markdown
811
- # Next.js State Management Standard
812
-
813
- > **Scope:** frontend/nextjs/state-management
814
- > **Layer:** 2 (on keyword)
815
- > **Keywords:** state, zustand, context, redux, client state, server state
816
- > **Load When:** deciding how to manage state in Next.js
817
-
818
- Server state (API data) lives in TanStack Query. UI state lives in React. No global state library unless genuinely needed.
819
-
820
- ## Core Rules
821
-
822
- - ALWAYS use TanStack Query for anything that comes from the .NET API
823
- - ALWAYS use `useState` for local UI state (modal open, selected tab, form step)
824
- - NEVER install Zustand or Redux without first exhausting Server Components + TanStack Query
825
- - NEVER use React Context for server state — Context does not cache or deduplicate
826
- - ALWAYS derive state from server data rather than syncing it to local state
827
-
828
- ## State Decision Tree
829
-
830
- ```
831
- Is this data from the .NET API?
832
- YES → TanStack Query (useQuery / useMutation)
833
-
834
- Is this local UI state (open/closed, selected, current step)?
835
- YES → useState in the component that needs it
836
-
837
- Does multiple components need this UI state?
838
- YES — is it parent-child? → prop drilling (2-3 levels is fine)
839
- YES — is it truly global? → React Context (theme, auth user, locale)
840
- YES — is it complex with many actions? → Consider Zustand (last resort)
841
- ```
842
-
843
- ## Server State (TanStack Query)
844
-
845
- ```ts
846
- // GOOD — server state in TanStack Query
847
- const { data: users } = useUsers(); // cached, deduplicated, background-refetched
848
-
849
- // BAD — copying server state to useState
850
- const [users, setUsers] = useState([]);
851
- useEffect(() => { fetchUsers().then(setUsers); }, []);
852
- ```
853
-
854
- ## Local UI State (useState)
855
-
856
- ```tsx
857
- // GOOD — local UI state in useState
858
- function UserList() {
859
- const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
860
- const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
861
- // ...
862
- }
863
- ```
864
-
865
- ## Shared UI State (React Context)
866
-
867
- Use Context only for truly global UI concerns:
868
-
869
- ```tsx
870
- // lib/auth-context.tsx — OK for Context (session-level, read-only)
871
- const AuthContext = createContext<{ user: User | null }>({ user: null });
872
-
873
- // NOT OK for Context
874
- // - List of users (use TanStack Query)
875
- // - Form values (use react-hook-form)
876
- // - Modal state only used in one feature (use useState)
877
- ```
878
-
879
- ## When Zustand Is Justified
880
-
881
- Only add Zustand if ALL of these are true:
882
- 1. State is needed in 5+ unrelated components
883
- 2. State has complex update logic (not just toggle/set)
884
- 3. Context causes measurable re-render performance issues
885
- 4. State is not from the server
886
-
887
- ## Common Mistakes
888
-
889
- | Wrong | Right | Why |
890
- |-------|-------|-----|
891
- | `useEffect` + `useState` for API data | TanStack Query | Stale data, no cache, double-render |
892
- | Global store for user list | `useUsers()` | TanStack Query handles sync/cache |
893
- | Context for everything | useState + prop drilling first | Context causes all consumers to re-render |
894
- | Zustand by default | Start with useState | YAGNI — most state is local |
895
-
896
- ---
897
-
898
- *MORPH-SPEC by Polymorphism Tech*
899
- ```
900
-
901
- **Step 8: Create `framework/standards/frontend/nextjs/testing.md`**
902
-
903
- ```markdown
904
- # Next.js Testing Standard
905
-
906
- > **Scope:** frontend/nextjs/testing
907
- > **Layer:** 2 (on keyword)
908
- > **Keywords:** testing, jest, vitest, testing-library, msw, component test
909
- > **Load When:** writing tests for Next.js components or hooks
910
-
911
- Jest + React Testing Library for component tests. MSW for API mocking. Co-locate tests with the code they test.
912
-
913
- ## Core Rules
914
-
915
- - ALWAYS co-locate test files: `user-card.test.tsx` next to `user-card.tsx`
916
- - ALWAYS use `@testing-library/user-event` for interactions — not `fireEvent`
917
- - ALWAYS mock the API layer with MSW — never mock `fetch` directly
918
- - NEVER test implementation details — test what the user sees
919
- - ALWAYS test the happy path + one error path per component
920
-
921
- ## Test File Co-location
922
-
923
- ```
924
- features/users/
925
- ├── components/
926
- │ ├── user-card.tsx
927
- │ ├── user-card.test.tsx ← co-located
928
- │ ├── user-list.tsx
929
- │ └── user-list.test.tsx
930
- ├── hooks/
931
- │ ├── use-users.ts
932
- │ └── use-users.test.ts ← co-located
933
- ```
934
-
935
- ## Component Test Pattern
936
-
937
- ```tsx
938
- // features/users/components/user-card.test.tsx
939
- import { render, screen } from '@testing-library/react';
940
- import userEvent from '@testing-library/user-event';
941
- import { UserCard } from './user-card';
942
-
943
- const mockUser = { id: '1', name: 'João Silva', email: 'joao@example.com', role: 'user' };
944
-
945
- describe('UserCard', () => {
946
- it('renders user name and email', () => {
947
- render(<UserCard user={mockUser} />);
948
- expect(screen.getByText('João Silva')).toBeInTheDocument();
949
- expect(screen.getByText('joao@example.com')).toBeInTheDocument();
950
- });
951
-
952
- it('calls onEdit with user id when edit button is clicked', async () => {
953
- const onEdit = vi.fn();
954
- render(<UserCard user={mockUser} onEdit={onEdit} />);
955
- await userEvent.click(screen.getByRole('button', { name: /edit/i }));
956
- expect(onEdit).toHaveBeenCalledWith('1');
957
- });
958
- });
959
- ```
960
-
961
- ## Hook Test with MSW
962
-
963
- ```ts
964
- // features/users/hooks/use-users.test.ts
965
- import { renderHook, waitFor } from '@testing-library/react';
966
- import { QueryClientWrapper } from '@/test/helpers';
967
- import { http, HttpResponse } from 'msw';
968
- import { server } from '@/test/msw-server';
969
- import { useUsers } from './use-users';
970
-
971
- describe('useUsers', () => {
972
- it('returns users from API', async () => {
973
- server.use(
974
- http.get('/api/users', () =>
975
- HttpResponse.json([{ id: '1', name: 'João', email: 'j@ex.com' }])
976
- )
977
- );
978
- const { result } = renderHook(() => useUsers(), { wrapper: QueryClientWrapper });
979
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
980
- expect(result.current.data).toHaveLength(1);
981
- });
982
-
983
- it('returns error state when API fails', async () => {
984
- server.use(http.get('/api/users', () => new HttpResponse(null, { status: 500 })));
985
- const { result } = renderHook(() => useUsers(), { wrapper: QueryClientWrapper });
986
- await waitFor(() => expect(result.current.isError).toBe(true));
987
- });
988
- });
989
- ```
990
-
991
- ## Test Helpers Setup
992
-
993
- ```tsx
994
- // test/helpers.tsx
995
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
996
-
997
- export function QueryClientWrapper({ children }: { children: React.ReactNode }) {
998
- const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
999
- return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
1000
- }
1001
- ```
1002
-
1003
- ## Common Mistakes
1004
-
1005
- | Wrong | Right | Why |
1006
- |-------|-------|-----|
1007
- | `fireEvent.click(button)` | `await userEvent.click(button)` | userEvent simulates real browser events |
1008
- | Mock `global.fetch` | Mock with MSW | MSW intercepts at network level, more realistic |
1009
- | `expect(component).toMatchSnapshot()` | `expect(screen.getByText(...))` | Snapshots break on any change |
1010
- | Test file in `__tests__/` folder | Co-locate with source | Easier to find, deleted with the component |
1011
-
1012
- ---
1013
-
1014
- *MORPH-SPEC by Polymorphism Tech*
1015
- ```
1016
-
1017
- **Step 9: Verify all 8 files exist**
1018
-
1019
- Run:
1020
- ```bash
1021
- ls "R:/Polymorphism Tech/repos/morph-spec-framework/framework/standards/frontend/nextjs/"
1022
- ```
1023
- Expected: 8 new files + the existing `nextjs-patterns.md` = 9 files total.
1024
-
1025
- **Step 10: Commit**
1026
-
1027
- ```bash
1028
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && git add framework/standards/frontend/nextjs/ && git commit -m "feat(standards): add 8 Next.js standards files — app-router, components, data-fetching, forms, state-management, naming-conventions, project-structure, testing"
1029
- ```
1030
-
1031
- ---
1032
-
1033
- ### Task 2: Create nextjs-standards.md rule file
1034
-
1035
- **Files:**
1036
- - Create: `framework/rules/nextjs-standards.md`
1037
- - Modify: `framework/rules/frontend-standards.md` (add nextjs-standards reference)
1038
-
1039
- **Step 1: Create `framework/rules/nextjs-standards.md`**
1040
-
1041
- ```markdown
1042
- ---
1043
- paths:
1044
- - "**/*.tsx"
1045
- - "**/*.ts"
1046
- - "!**/*.cs"
1047
- - "!**/*.csproj"
1048
- ---
1049
-
1050
- # Next.js Standards
1051
-
1052
- @.morph/framework/standards/frontend/nextjs/naming-conventions.md
1053
- @.morph/framework/standards/frontend/nextjs/project-structure.md
1054
- @.morph/framework/standards/frontend/nextjs/app-router.md
1055
- @.morph/framework/standards/frontend/nextjs/components.md
1056
- @.morph/framework/standards/frontend/nextjs/data-fetching.md
1057
- @.morph/framework/standards/frontend/nextjs/forms.md
1058
- @.morph/framework/standards/frontend/nextjs/state-management.md
1059
- @.morph/framework/standards/frontend/nextjs/testing.md
1060
- @.morph/framework/standards/frontend/nextjs/nextjs-patterns.md
1061
- ```
1062
-
1063
- **Step 2: Read `framework/rules/frontend-standards.md` to check current content**
1064
-
1065
- It currently imports `nextjs-patterns.md`. Since `nextjs-standards.md` is the dedicated rule for `.tsx`/`.ts` files, the `frontend-standards.md` can remove its nextjs reference to avoid duplication (it should remain focused on Blazor + shared CSS). Update it to only cover Blazor/CSS paths.
1066
-
1067
- Read the file first, then remove the nextjs-patterns.md line if present.
1068
-
1069
- **Step 3: Verify**
1070
-
1071
- ```bash
1072
- cat "R:/Polymorphism Tech/repos/morph-spec-framework/framework/rules/nextjs-standards.md"
1073
- ```
1074
-
1075
- **Step 4: Commit**
1076
-
1077
- ```bash
1078
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && git add framework/rules/nextjs-standards.md framework/rules/frontend-standards.md && git commit -m "feat(rules): add nextjs-standards.md rule file scoped to tsx/ts files"
1079
- ```
1080
-
1081
- ---
1082
-
1083
- ### Task 3: Create next-component-validator (TDD)
1084
-
1085
- **Files:**
1086
- - Create: `src/lib/validators/nextjs/next-component-validator.js`
1087
- - Create: `src/lib/validators/nextjs/index.js`
1088
- - Create: `test/validators/nextjs/next-component-validator.test.js`
1089
-
1090
- **Step 1: Write the failing tests first**
1091
-
1092
- Create `test/validators/nextjs/next-component-validator.test.js`:
1093
-
1094
- ```js
1095
- import { test, describe } from 'node:test';
1096
- import assert from 'node:assert/strict';
1097
- import { validateNextComponent } from '../../../src/lib/validators/nextjs/next-component-validator.js';
1098
-
1099
- describe('validateNextComponent', () => {
1100
-
1101
- describe('use client directive checks', () => {
1102
- test('warns when use client is present but no interactivity detected', () => {
1103
- const content = `'use client';\n\nexport function StaticCard() {\n return <div>Hello</div>;\n}`;
1104
- const issues = validateNextComponent(content, 'components/static-card.tsx');
1105
- const warnings = issues.filter(i => i.type === 'warning' && i.message.includes('use client'));
1106
- assert.ok(warnings.length > 0, 'Should warn about unnecessary use client');
1107
- });
1108
-
1109
- test('no warning when use client has useState', () => {
1110
- const content = `'use client';\nimport { useState } from 'react';\nexport function Counter() {\n const [count, setCount] = useState(0);\n return <button onClick={() => setCount(c => c+1)}>{count}</button>;\n}`;
1111
- const issues = validateNextComponent(content, 'components/counter.tsx');
1112
- const useClientWarnings = issues.filter(i => i.message.includes('use client'));
1113
- assert.equal(useClientWarnings.length, 0, 'useState justifies use client');
1114
- });
1115
-
1116
- test('no warning when use client has useEffect', () => {
1117
- const content = `'use client';\nimport { useEffect } from 'react';\nexport function Tracker() {\n useEffect(() => {}, []);\n return <div/>;\n}`;
1118
- const issues = validateNextComponent(content, 'components/tracker.tsx');
1119
- const useClientWarnings = issues.filter(i => i.message.includes('use client'));
1120
- assert.equal(useClientWarnings.length, 0, 'useEffect justifies use client');
1121
- });
1122
-
1123
- test('no warning when use client has onClick handler', () => {
1124
- const content = `'use client';\nexport function Btn() {\n return <button onClick={() => alert('hi')}>Click</button>;\n}`;
1125
- const issues = validateNextComponent(content, 'components/btn.tsx');
1126
- const useClientWarnings = issues.filter(i => i.message.includes('use client'));
1127
- assert.equal(useClientWarnings.length, 0, 'onClick justifies use client');
1128
- });
1129
- });
1130
-
1131
- describe('missing use client checks', () => {
1132
- test('errors when useState used without use client', () => {
1133
- const content = `import { useState } from 'react';\nexport function Counter() {\n const [c, setC] = useState(0);\n return <div>{c}</div>;\n}`;
1134
- const issues = validateNextComponent(content, 'components/counter.tsx');
1135
- const errors = issues.filter(i => i.type === 'error' && i.message.includes('use client'));
1136
- assert.ok(errors.length > 0, 'Should error when useState missing use client');
1137
- });
1138
-
1139
- test('errors when useEffect used without use client', () => {
1140
- const content = `import { useEffect } from 'react';\nexport function Effect() {\n useEffect(() => {}, []);\n return <div/>;\n}`;
1141
- const issues = validateNextComponent(content, 'components/effect.tsx');
1142
- const errors = issues.filter(i => i.type === 'error' && i.message.includes('use client'));
1143
- assert.ok(errors.length > 0, 'Should error when useEffect missing use client');
1144
- });
1145
-
1146
- test('no error for server component with no hooks', () => {
1147
- const content = `export default async function Page() {\n return <div>Hello</div>;\n}`;
1148
- const issues = validateNextComponent(content, 'app/page.tsx');
1149
- assert.equal(issues.length, 0, 'Clean server component should have no issues');
1150
- });
1151
- });
1152
-
1153
- describe('file naming checks', () => {
1154
- test('warns on PascalCase component file', () => {
1155
- const content = `export function UserCard() { return <div/>; }`;
1156
- const issues = validateNextComponent(content, 'components/UserCard.tsx');
1157
- const warnings = issues.filter(i => i.message.includes('kebab-case'));
1158
- assert.ok(warnings.length > 0, 'Should warn on PascalCase file name');
1159
- });
1160
-
1161
- test('no warning on kebab-case file', () => {
1162
- const content = `export function UserCard() { return <div/>; }`;
1163
- const issues = validateNextComponent(content, 'components/user-card.tsx');
1164
- const namingWarnings = issues.filter(i => i.message.includes('kebab-case'));
1165
- assert.equal(namingWarnings.length, 0, 'kebab-case file name is correct');
1166
- });
1167
-
1168
- test('no warning on Next.js special files', () => {
1169
- const content = `export default function Page() { return <div/>; }`;
1170
- ['page.tsx', 'layout.tsx', 'loading.tsx', 'error.tsx', 'not-found.tsx'].forEach(file => {
1171
- const issues = validateNextComponent(content, `app/${file}`);
1172
- const namingWarnings = issues.filter(i => i.message.includes('kebab-case'));
1173
- assert.equal(namingWarnings.length, 0, `${file} should not warn about kebab-case`);
1174
- });
1175
- });
1176
- });
1177
-
1178
- describe('returns empty array for non-component files', () => {
1179
- test('no issues for .ts utility files without JSX', () => {
1180
- const content = `export function formatDate(date: Date) { return date.toISOString(); }`;
1181
- const issues = validateNextComponent(content, 'lib/utils.ts');
1182
- assert.equal(issues.length, 0);
1183
- });
1184
- });
1185
-
1186
- });
1187
- ```
1188
-
1189
- **Step 2: Run tests to confirm they fail**
1190
-
1191
- ```bash
1192
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && node --test test/validators/nextjs/next-component-validator.test.js 2>&1 | head -20
1193
- ```
1194
-
1195
- Expected: FAIL with "Cannot find module" or similar.
1196
-
1197
- **Step 3: Create `src/lib/validators/nextjs/next-component-validator.js`**
1198
-
1199
- ```js
1200
- /**
1201
- * Next.js Component Validator
1202
- *
1203
- * Validates .tsx files for:
1204
- * 1. Unnecessary 'use client' directives (no interactivity)
1205
- * 2. Missing 'use client' when React hooks are used
1206
- * 3. File naming convention (kebab-case)
1207
- *
1208
- * @module next-component-validator
1209
- */
1210
-
1211
- // ============================================
1212
- // CONSTANTS
1213
- // ============================================
1214
-
1215
- /** Hooks/patterns that require 'use client' */
1216
- const CLIENT_HOOKS = ['useState', 'useEffect', 'useRef', 'useReducer', 'useCallback', 'useMemo', 'useContext'];
1217
-
1218
- /** Patterns that indicate interactivity requiring 'use client' */
1219
- const INTERACTIVITY_PATTERNS = [
1220
- /\bonClick\b/,
1221
- /\bonChange\b/,
1222
- /\bonSubmit\b/,
1223
- /\bonFocus\b/,
1224
- /\bonBlur\b/,
1225
- /\bonKeyDown\b/,
1226
- /\buseState\b/,
1227
- /\buseEffect\b/,
1228
- /\buseRef\b/,
1229
- /\buseReducer\b/,
1230
- /\buseCallback\b/,
1231
- /\buseMemo\b/,
1232
- /\buseContext\b/,
1233
- ];
1234
-
1235
- /** Next.js special files exempt from naming checks */
1236
- const NEXTJS_SPECIAL_FILES = ['page.tsx', 'layout.tsx', 'loading.tsx', 'error.tsx', 'not-found.tsx', 'route.ts', 'middleware.ts'];
1237
-
1238
- // ============================================
1239
- // MAIN VALIDATION FUNCTION
1240
- // ============================================
1241
-
1242
- /**
1243
- * @typedef {Object} ValidationIssue
1244
- * @property {'error' | 'warning' | 'info'} type
1245
- * @property {string} message
1246
- * @property {string} [suggestion]
1247
- * @property {string} file
1248
- * @property {number} [line]
1249
- */
1250
-
1251
- /**
1252
- * Validates a Next.js component file.
1253
- *
1254
- * @param {string} content - File content
1255
- * @param {string} filePath - Relative file path
1256
- * @returns {ValidationIssue[]}
1257
- */
1258
- export function validateNextComponent(content, filePath) {
1259
- const issues = [];
1260
-
1261
- // Only validate .tsx and .ts files
1262
- if (!filePath.endsWith('.tsx') && !filePath.endsWith('.ts')) {
1263
- return issues;
1264
- }
1265
-
1266
- // Skip non-component utility files (no JSX)
1267
- const hasJsx = /<[A-Z][a-zA-Z]*|<[a-z]+[\s>]/.test(content);
1268
- const hasHooks = CLIENT_HOOKS.some(hook => content.includes(hook));
1269
-
1270
- if (!hasJsx && !hasHooks) {
1271
- return issues;
1272
- }
1273
-
1274
- const hasUseClient = /^['"]use client['"]/.test(content.trimStart());
1275
-
1276
- // Check 1: use client present but no interactivity
1277
- if (hasUseClient) {
1278
- const hasInteractivity = INTERACTIVITY_PATTERNS.some(pattern => pattern.test(content));
1279
- if (!hasInteractivity) {
1280
- issues.push({
1281
- type: 'warning',
1282
- message: "'use client' directive present but no interactivity detected (no hooks, no event handlers)",
1283
- suggestion: "Remove 'use client' to make this a Server Component, or add interactive behavior",
1284
- file: filePath,
1285
- });
1286
- }
1287
- }
1288
-
1289
- // Check 2: hooks used without use client
1290
- if (!hasUseClient) {
1291
- const usedHooks = CLIENT_HOOKS.filter(hook => new RegExp(`\\b${hook}\\b`).test(content));
1292
- if (usedHooks.length > 0) {
1293
- issues.push({
1294
- type: 'error',
1295
- message: `React hook(s) used (${usedHooks.join(', ')}) without 'use client' directive`,
1296
- suggestion: "Add 'use client' as the first line of the file",
1297
- file: filePath,
1298
- line: 1,
1299
- });
1300
- }
1301
- }
1302
-
1303
- // Check 3: file naming convention (kebab-case)
1304
- const fileName = filePath.split('/').pop() ?? '';
1305
- const isSpecialFile = NEXTJS_SPECIAL_FILES.includes(fileName);
1306
-
1307
- if (!isSpecialFile && fileName.endsWith('.tsx')) {
1308
- const baseName = fileName.replace('.tsx', '');
1309
- const hasUpperCase = /[A-Z]/.test(baseName);
1310
- if (hasUpperCase) {
1311
- issues.push({
1312
- type: 'warning',
1313
- message: `Component file '${fileName}' should use kebab-case naming`,
1314
- suggestion: `Rename to '${toKebabCase(baseName)}.tsx'`,
1315
- file: filePath,
1316
- });
1317
- }
1318
- }
1319
-
1320
- return issues;
1321
- }
1322
-
1323
- // ============================================
1324
- // HELPERS
1325
- // ============================================
1326
-
1327
- function toKebabCase(str) {
1328
- return str
1329
- .replace(/([A-Z])/g, '-$1')
1330
- .toLowerCase()
1331
- .replace(/^-/, '');
1332
- }
1333
- ```
1334
-
1335
- **Step 4: Create `src/lib/validators/nextjs/index.js`**
1336
-
1337
- ```js
1338
- export { validateNextComponent } from './next-component-validator.js';
1339
- ```
1340
-
1341
- **Step 5: Run tests — must all pass**
1342
-
1343
- ```bash
1344
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && node --test test/validators/nextjs/next-component-validator.test.js 2>&1
1345
- ```
1346
-
1347
- Expected: All tests pass, 0 failures.
1348
-
1349
- **Step 6: Run full test suite**
1350
-
1351
- ```bash
1352
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && npm test 2>&1 | tail -10
1353
- ```
1354
-
1355
- Expected: Same pass count + new tests passing, 0 failures.
1356
-
1357
- **Step 7: Commit**
1358
-
1359
- ```bash
1360
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && git add src/lib/validators/nextjs/ test/validators/nextjs/ && git commit -m "feat(validators): add next-component-validator — use client, hooks, kebab-case checks"
1361
- ```
1362
-
1363
- ---
1364
-
1365
- ### Task 4: Create 6 Next.js templates
1366
-
1367
- **Files:**
1368
- - Create: `framework/templates/frontend/nextjs/page.tsx.hbs`
1369
- - Create: `framework/templates/frontend/nextjs/client-component.tsx.hbs`
1370
- - Create: `framework/templates/frontend/nextjs/feature-form.tsx.hbs`
1371
- - Create: `framework/templates/frontend/nextjs/env.mjs.hbs`
1372
- - Create: `framework/templates/frontend/nextjs/tsconfig.json.hbs`
1373
- - Create: `framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs`
1374
-
1375
- **Step 1: Create `framework/templates/frontend/nextjs/page.tsx.hbs`**
1376
-
1377
- ```handlebars
1378
- // app/(dashboard)/{{kebabCase featureName}}/page.tsx
1379
- // Server Component — fetches initial data, renders feature component
1380
- import { {{pascalCase featureName}}List } from '@/features/{{kebabCase featureName}}';
1381
-
1382
- async function get{{pascalCase featureName}}List() {
1383
- const res = await fetch(`${process.env.API_URL}/api/{{kebabCase featureName}}`, {
1384
- next: { revalidate: 30 },
1385
- headers: { 'Content-Type': 'application/json' },
1386
- });
1387
- if (!res.ok) throw new Error('Failed to fetch {{featureName}}');
1388
- return res.json();
1389
- }
1390
-
1391
- export default async function {{pascalCase featureName}}Page() {
1392
- const data = await get{{pascalCase featureName}}List();
1393
- return (
1394
- <div className="container py-6">
1395
- <h1 className="text-2xl font-bold mb-6">{{pascalCase featureName}}</h1>
1396
- <{{pascalCase featureName}}List initialData={data} />
1397
- </div>
1398
- );
1399
- }
1400
- ```
1401
-
1402
- **Step 2: Create `framework/templates/frontend/nextjs/client-component.tsx.hbs`**
1403
-
1404
- ```handlebars
1405
- // features/{{kebabCase featureName}}/components/{{kebabCase featureName}}-list.tsx
1406
- 'use client';
1407
-
1408
- import { use{{pascalCase featureName}}s } from '@/features/{{kebabCase featureName}}/hooks/use-{{kebabCase featureName}}s';
1409
- import { DataTable } from '@/components/data-table';
1410
- import type { {{pascalCase featureName}} } from '@/features/{{kebabCase featureName}}/types/{{kebabCase featureName}}.types';
1411
- import type { ColumnDef } from '@tanstack/react-table';
1412
-
1413
- const columns: ColumnDef<{{pascalCase featureName}}>[] = [
1414
- { accessorKey: 'id', header: 'ID' },
1415
- { accessorKey: 'name', header: 'Name' },
1416
- ];
1417
-
1418
- interface {{pascalCase featureName}}ListProps {
1419
- initialData?: {{pascalCase featureName}}[];
1420
- }
1421
-
1422
- export function {{pascalCase featureName}}List({ initialData }: {{pascalCase featureName}}ListProps) {
1423
- const { data = initialData ?? [], isLoading } = use{{pascalCase featureName}}s();
1424
-
1425
- if (isLoading) {
1426
- return <div className="flex items-center justify-center h-32">Loading...</div>;
1427
- }
1428
-
1429
- return <DataTable columns={columns} data={data} />;
1430
- }
1431
- ```
1432
-
1433
- **Step 3: Create `framework/templates/frontend/nextjs/feature-form.tsx.hbs`**
1434
-
1435
- ```handlebars
1436
- // features/{{kebabCase featureName}}/components/create-{{kebabCase featureName}}-form.tsx
1437
- 'use client';
1438
-
1439
- import { useForm } from 'react-hook-form';
1440
- import { zodResolver } from '@hookform/resolvers/zod';
1441
- import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
1442
- import { Input } from '@/components/ui/input';
1443
- import { Button } from '@/components/ui/button';
1444
- import { useCreate{{pascalCase featureName}} } from '@/features/{{kebabCase featureName}}/hooks/use-create-{{kebabCase featureName}}';
1445
- import { create{{pascalCase featureName}}Schema, type Create{{pascalCase featureName}}Input } from '@/features/{{kebabCase featureName}}/types/{{kebabCase featureName}}.schemas';
1446
-
1447
- interface Create{{pascalCase featureName}}FormProps {
1448
- onSuccess?: () => void;
1449
- }
1450
-
1451
- export function Create{{pascalCase featureName}}Form({ onSuccess }: Create{{pascalCase featureName}}FormProps) {
1452
- const form = useForm<Create{{pascalCase featureName}}Input>({
1453
- resolver: zodResolver(create{{pascalCase featureName}}Schema),
1454
- defaultValues: { name: '' },
1455
- });
1456
-
1457
- const { mutate: create{{pascalCase featureName}}, isPending } = useCreate{{pascalCase featureName}}();
1458
-
1459
- function onSubmit(values: Create{{pascalCase featureName}}Input) {
1460
- create{{pascalCase featureName}}(values, {
1461
- onSuccess: () => { form.reset(); onSuccess?.(); },
1462
- onError: (error) => { form.setError('root', { message: error.message }); },
1463
- });
1464
- }
1465
-
1466
- return (
1467
- <Form {...form}>
1468
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
1469
- <FormField
1470
- control={form.control}
1471
- name="name"
1472
- render={({ field }) => (
1473
- <FormItem>
1474
- <FormLabel>Name</FormLabel>
1475
- <FormControl>
1476
- <Input placeholder="Enter name" {...field} />
1477
- </FormControl>
1478
- <FormMessage />
1479
- </FormItem>
1480
- )}
1481
- />
1482
- {form.formState.errors.root && (
1483
- <p className="text-sm text-destructive">{form.formState.errors.root.message}</p>
1484
- )}
1485
- <Button type="submit" disabled={isPending}>
1486
- {isPending ? 'Creating...' : 'Create {{pascalCase featureName}}'}
1487
- </Button>
1488
- </form>
1489
- </Form>
1490
- );
1491
- }
1492
- ```
1493
-
1494
- **Step 4: Create `framework/templates/frontend/nextjs/env.mjs.hbs`**
1495
-
1496
- ```handlebars
1497
- // env.mjs — Zod-validated environment variables
1498
- // Crashes at startup if required vars are missing — no silent undefined
1499
- import { z } from 'zod';
1500
-
1501
- const serverSchema = z.object({
1502
- API_URL: z.string().url('API_URL must be a valid URL'),
1503
- NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
1504
- });
1505
-
1506
- const clientSchema = z.object({
1507
- NEXT_PUBLIC_APP_URL: z.string().url().optional(),
1508
- });
1509
-
1510
- // Validate server-side env (only available on server)
1511
- const serverEnv = serverSchema.safeParse(process.env);
1512
- if (!serverEnv.success) {
1513
- console.error('❌ Invalid server environment variables:');
1514
- console.error(serverEnv.error.flatten().fieldErrors);
1515
- throw new Error('Invalid environment variables');
1516
- }
1517
-
1518
- // Validate client-side env
1519
- const clientEnv = clientSchema.safeParse({
1520
- NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
1521
- });
1522
- if (!clientEnv.success) {
1523
- console.error('❌ Invalid client environment variables:');
1524
- console.error(clientEnv.error.flatten().fieldErrors);
1525
- throw new Error('Invalid client environment variables');
1526
- }
1527
-
1528
- export const env = { ...serverEnv.data, ...clientEnv.data };
1529
- ```
1530
-
1531
- **Step 5: Create `framework/templates/frontend/nextjs/tsconfig.json.hbs`**
1532
-
1533
- ```handlebars
1534
- {
1535
- "compilerOptions": {
1536
- "target": "ES2022",
1537
- "lib": ["dom", "dom.iterable", "esnext"],
1538
- "allowJs": false,
1539
- "skipLibCheck": true,
1540
- "strict": true,
1541
- "noUncheckedIndexedAccess": true,
1542
- "noImplicitOverride": true,
1543
- "forceConsistentCasingInFileNames": true,
1544
- "noEmit": true,
1545
- "esModuleInterop": true,
1546
- "module": "esnext",
1547
- "moduleResolution": "bundler",
1548
- "resolveJsonModule": true,
1549
- "isolatedModules": true,
1550
- "jsx": "preserve",
1551
- "incremental": true,
1552
- "plugins": [{ "name": "next" }],
1553
- "paths": {
1554
- "@/*": ["./src/*"]
1555
- }
1556
- },
1557
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
1558
- "exclude": ["node_modules"]
1559
- }
1560
- ```
1561
-
1562
- **Step 6: Create `framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs`**
1563
-
1564
- ```handlebars
1565
- # Multi-stage Next.js production build for EasyPanel
1566
- # Stage 1: Dependencies
1567
- FROM node:20-alpine AS deps
1568
- RUN apk add --no-cache libc6-compat
1569
- WORKDIR /app
1570
-
1571
- COPY package.json package-lock.json* ./
1572
- RUN npm ci --only=production && npm cache clean --force
1573
-
1574
- # Stage 2: Build
1575
- FROM node:20-alpine AS builder
1576
- WORKDIR /app
1577
-
1578
- COPY --from=deps /app/node_modules ./node_modules
1579
- COPY . .
1580
-
1581
- # Build args for NEXT_PUBLIC_ variables (must be available at build time)
1582
- ARG NEXT_PUBLIC_APP_URL
1583
- ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL
1584
-
1585
- RUN npm run build
1586
-
1587
- # Stage 3: Production runner
1588
- FROM node:20-alpine AS runner
1589
- WORKDIR /app
1590
-
1591
- ENV NODE_ENV=production
1592
- ENV NEXT_TELEMETRY_DISABLED=1
1593
-
1594
- RUN addgroup --system --gid 1001 nodejs
1595
- RUN adduser --system --uid 1001 nextjs
1596
-
1597
- COPY --from=builder /app/public ./public
1598
- COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
1599
- COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
1600
-
1601
- USER nextjs
1602
-
1603
- EXPOSE 3000
1604
- ENV PORT=3000
1605
- ENV HOSTNAME="0.0.0.0"
1606
-
1607
- CMD ["node", "server.js"]
1608
- ```
1609
-
1610
- **Step 7: Verify all 6 template files exist**
1611
-
1612
- ```bash
1613
- ls "R:/Polymorphism Tech/repos/morph-spec-framework/framework/templates/frontend/nextjs/"
1614
- ```
1615
-
1616
- Expected: 6 files.
1617
-
1618
- **Step 8: Commit**
1619
-
1620
- ```bash
1621
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && git add framework/templates/frontend/nextjs/ && git commit -m "feat(templates): add 6 Next.js templates — page, client-component, feature-form, env.mjs, tsconfig, Dockerfile"
1622
- ```
1623
-
1624
- ---
1625
-
1626
- ### Task 5: Update nextjs-expert agent
1627
-
1628
- **Files:**
1629
- - Modify: `framework/agents/frontend/nextjs-expert.md`
1630
- - Modify: `framework/agents.json`
1631
-
1632
- **Step 1: Read the current `framework/agents/frontend/nextjs-expert.md`**
1633
-
1634
- Read the file fully to understand current content.
1635
-
1636
- **Step 2: Rewrite `framework/agents/frontend/nextjs-expert.md`**
1637
-
1638
- Replace the full content with:
1639
-
1640
- ```markdown
1641
- ---
1642
- name: nextjs-expert
1643
- description: Next.js specialist for App Router, shadcn/ui, TanStack Query, react-hook-form/Zod, and EasyPanel deployment. Use when building Next.js pages or components, implementing forms, setting up data fetching from a .NET API, or configuring TypeScript/Docker for Next.js.
1644
- allowed-tools: Read, Write, Edit, Bash, Glob, Grep
1645
- ---
1646
-
1647
- # Next.js Expert
1648
-
1649
- Specialist for the locked stack: **Next.js App Router + shadcn/ui + TanStack Query + react-hook-form/Zod + EasyPanel/Docker**.
1650
-
1651
- ## Stack
1652
-
1653
- | Layer | Technology |
1654
- |-------|-----------|
1655
- | Framework | Next.js App Router (TypeScript strict) |
1656
- | UI Components | shadcn/ui — `npx shadcn@latest add` |
1657
- | Styling | Tailwind CSS |
1658
- | Server state | TanStack Query v5 |
1659
- | Forms | react-hook-form + Zod + zodResolver |
1660
- | Deployment | Docker multi-stage → EasyPanel (VPS) |
1661
-
1662
- ## Critical Standards (Read FIRST)
1663
-
1664
- | Standard | What |
1665
- |----------|------|
1666
- | `frontend/nextjs/naming-conventions.md` | **kebab-case files, PascalCase exports, use- hooks** |
1667
- | `frontend/nextjs/project-structure.md` | **Feature-based folders, component tiers** |
1668
- | `frontend/nextjs/app-router.md` | **Server vs Client components decision tree** |
1669
- | `frontend/nextjs/components.md` | **Three-tier hierarchy, never edit components/ui/** |
1670
- | `frontend/nextjs/data-fetching.md` | **TanStack Query patterns, query key factories** |
1671
- | `frontend/nextjs/forms.md` | **react-hook-form + Zod + shadcn Form** |
1672
- | `frontend/nextjs/state-management.md` | **No Zustand by default** |
1673
- | `frontend/nextjs/testing.md` | **Jest + Testing Library + MSW** |
1674
-
1675
- ## Quick Checklist
1676
-
1677
- - [ ] File names are kebab-case (`user-card.tsx` not `UserCard.tsx`)
1678
- - [ ] `'use client'` only on components with hooks or event handlers
1679
- - [ ] No `useEffect` for data fetching — use Server Components or TanStack Query
1680
- - [ ] Zod schema defined before TypeScript type (`type X = z.infer<typeof xSchema>`)
1681
- - [ ] Query keys use factory pattern (`userKeys.lists()` not `['users']`)
1682
- - [ ] Forms use `zodResolver` + shadcn `<Form>` + `<FormMessage>`
1683
- - [ ] `components/ui/` files never edited directly
1684
- - [ ] New features go in `features/{name}/components|hooks|types/`
1685
-
1686
- ## Naming Conventions (Non-Negotiable)
1687
-
1688
- ```
1689
- Component file: user-card.tsx → export function UserCard()
1690
- Hook file: use-create-user.ts → export function useCreateUser()
1691
- Schema file: user.schemas.ts → export const createUserSchema = z.object(...)
1692
- Type file: user.types.ts → export type User = z.infer<typeof userSchema>
1693
- Feature folder: features/user-mgmt/
1694
- ```
1695
-
1696
- ## Component Tier Rules
1697
-
1698
- ```
1699
- components/ui/ ← shadcn primitives. NEVER edit.
1700
- components/ ← Composed shared components. No API calls. No feature imports.
1701
- features/*/components ← Feature-scoped. May use components/ but not other features/.
1702
- ```
1703
-
1704
- ## Data Fetching Patterns
1705
-
1706
- ```tsx
1707
- // Server Component (initial load)
1708
- export default async function Page() {
1709
- const data = await fetch(`${process.env.API_URL}/api/users`).then(r => r.json());
1710
- return <UserList initialData={data} />;
1711
- }
1712
-
1713
- // Client Component (mutations/interactive)
1714
- 'use client';
1715
- const { data } = useUsers(); // TanStack Query
1716
- const { mutate } = useCreateUser(); // useMutation
1717
- ```
1718
-
1719
- ## Form Pattern
1720
-
1721
- ```tsx
1722
- // Schema first, type derived
1723
- const schema = z.object({ name: z.string().min(2) });
1724
- type Input = z.infer<typeof schema>;
1725
-
1726
- // Form wired to schema
1727
- const form = useForm<Input>({ resolver: zodResolver(schema) });
1728
- ```
1729
-
1730
- ## EasyPanel Deployment
1731
-
1732
- - Use `framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs` as base
1733
- - Requires `output: 'standalone'` in `next.config.js`
1734
- - `NEXT_PUBLIC_*` vars must be available at **build time** (Docker build args)
1735
- - Server-only vars (API_URL, secrets) go in EasyPanel environment at **runtime**
1736
-
1737
- ## Project Structure
1738
-
1739
- ```
1740
- src/
1741
- ├── app/ # Routes only
1742
- ├── features/{name}/
1743
- │ ├── components/ # Feature UI
1744
- │ ├── hooks/ # TanStack Query hooks
1745
- │ └── types/ # Zod schemas + inferred types
1746
- ├── components/
1747
- │ ├── ui/ # shadcn — do not edit
1748
- │ └── {shared}.tsx # Composed shared components
1749
- ├── hooks/ # Utility hooks (no API calls)
1750
- ├── lib/
1751
- │ ├── api-client.ts
1752
- │ └── query-client.tsx
1753
- └── env.mjs # Zod env validation
1754
- ```
1755
-
1756
- ---
1757
-
1758
- *MORPH-SPEC by Polymorphism Tech*
1759
- ```
1760
-
1761
- **Step 3: Update `framework/agents.json` — nextjs-expert standards array**
1762
-
1763
- Find the `nextjs-expert` entry and update its `standards` array to include all 8 new standards files:
1764
-
1765
- ```json
1766
- "standards": [
1767
- "core/coding.md",
1768
- "core/architecture.md",
1769
- "frontend/nextjs/naming-conventions.md",
1770
- "frontend/nextjs/project-structure.md",
1771
- "frontend/nextjs/app-router.md",
1772
- "frontend/nextjs/components.md",
1773
- "frontend/nextjs/data-fetching.md",
1774
- "frontend/nextjs/forms.md",
1775
- "frontend/nextjs/state-management.md",
1776
- "frontend/nextjs/testing.md",
1777
- "frontend/nextjs/nextjs-patterns.md"
1778
- ]
1779
- ```
1780
-
1781
- Also update `nextjs-expert.keywords` to include the new stack terms:
1782
- ```json
1783
- "keywords": ["next.js", "nextjs", "react", "tsx", "shadcn", "tailwind", "tanstack", "react-query", "zod", "react-hook-form", "app router", "server component", "client component"]
1784
- ```
1785
-
1786
- **Step 4: Validate JSON**
1787
-
1788
- ```bash
1789
- node -e "JSON.parse(require('fs').readFileSync('framework/agents.json','utf8')); console.log('OK')"
1790
- ```
1791
-
1792
- **Step 5: Commit**
1793
-
1794
- ```bash
1795
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && git add framework/agents/frontend/nextjs-expert.md framework/agents.json && git commit -m "feat(agents): enhance nextjs-expert with full stack standards, naming rules, EasyPanel deployment"
1796
- ```
1797
-
1798
- ---
1799
-
1800
- ### Task 6: Regenerate STANDARDS.json and final verification
1801
-
1802
- **Files:**
1803
- - Modify: `framework/standards/STANDARDS.json` (regenerated)
1804
-
1805
- **Step 1: Run the standards registry generator**
1806
-
1807
- ```bash
1808
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && node scripts/generate-standards-registry.js
1809
- ```
1810
-
1811
- **Step 2: Verify new entries appear in STANDARDS.json**
1812
-
1813
- ```bash
1814
- node -e "
1815
- const s = JSON.parse(require('fs').readFileSync('framework/standards/STANDARDS.json','utf8'));
1816
- const nextjs = s.standards.filter(x => x.path.includes('nextjs'));
1817
- console.log('Next.js standards in registry:', nextjs.length, '(expected 9)');
1818
- nextjs.forEach(x => console.log(' -', x.id));
1819
- "
1820
- ```
1821
-
1822
- Expected: 9 Next.js standards entries (8 new + 1 existing nextjs-patterns).
1823
-
1824
- **Step 3: Run full test suite**
1825
-
1826
- ```bash
1827
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && npm test 2>&1 | tail -10
1828
- ```
1829
-
1830
- Expected: All previously passing tests still pass, new validator tests pass, 0 failures.
1831
-
1832
- **Step 4: Commit**
1833
-
1834
- ```bash
1835
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && git add framework/standards/STANDARDS.json && git commit -m "feat(standards): regenerate STANDARDS.json — 9 Next.js entries added (82 total)"
1836
- ```
1837
-
1838
- **Step 5: Final audit**
1839
-
1840
- ```bash
1841
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && node -e "
1842
- const s = JSON.parse(require('fs').readFileSync('framework/standards/STANDARDS.json','utf8'));
1843
- console.log('Total standards:', s.standards.length);
1844
- const byCategory = {};
1845
- s.standards.forEach(x => { byCategory[x.category] = (byCategory[x.category] || 0) + 1; });
1846
- Object.entries(byCategory).sort((a,b) => b[1]-a[1]).forEach(([k,v]) => console.log(' ', k + ':', v));
1847
- "
1848
- ```