@polymorphism-tech/morph-spec 9.4.0 → 9.6.0
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/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/bin/morph-spec.js +1 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +23 -33
- package/framework/agents.json +6 -7
- package/framework/docs/CLI.md +12 -8
- package/framework/hooks/README.md +2 -2
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/hooks/claude-code/core/comment-guard.js +1 -0
- package/framework/hooks/claude-code/core/denylist.json +63 -1
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +0 -8
- package/framework/hooks/claude-code/core/gate-guard.js +0 -8
- package/framework/hooks/claude-code/core/heavy-gate.js +5 -3
- package/framework/hooks/claude-code/core/loop-detect.js +0 -3
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +2 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +0 -7
- package/framework/hooks/claude-code/core/validate-completion.js +0 -4
- package/framework/hooks/claude-code/core/worktree-context.js +0 -2
- package/framework/hooks/claude-code/statusline.py +3 -44
- package/framework/hooks/shared/build-check.js +12 -19
- package/framework/hooks/shared/comment-rules.js +11 -1
- package/framework/hooks/shared/feature-locator.js +7 -5
- package/framework/hooks/shared/heavy-command.js +12 -0
- package/framework/hooks/shared/heavy-config.js +28 -1
- package/framework/hooks/shared/heavy-slots.js +41 -5
- package/framework/hooks/shared/hook-response.js +0 -4
- package/framework/hooks/shared/payload-utils.js +1 -35
- package/framework/hooks/shared/phase-utils.js +0 -12
- package/framework/hooks/shared/slice-progress.js +0 -2
- package/framework/hooks/shared/state-reader.js +0 -19
- package/framework/hooks/shared/telemetry-logger.js +1 -1
- package/framework/rules/frontend-standards.md +1 -2
- package/framework/rules/nextjs-standards.md +9 -8
- package/framework/schemas/config.schema.json +12 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +2 -2
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +32 -29
- package/framework/standards/STANDARDS.meta.json +13 -15
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/frontend/admin-app/data-table.md +1 -3
- package/framework/standards/frontend/admin-app/page-structure.md +8 -4
- package/framework/standards/frontend/nextjs/app-router.md +3 -4
- package/framework/standards/frontend/nextjs/components.md +2 -15
- package/framework/standards/frontend/nextjs/data-fetching.md +2 -3
- package/framework/standards/frontend/nextjs/lint.md +141 -0
- package/framework/standards/frontend/nextjs/motion-patterns.md +2 -7
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -2
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +5 -7
- package/framework/standards/frontend/nextjs/project-structure.md +3 -2
- package/framework/standards/frontend/nextjs/state-management.md +6 -7
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +1 -0
- package/framework/templates/README.md +4 -4
- package/framework/templates/REGISTRY.json +21 -309
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +54 -56
- package/package.json +3 -2
- package/src/commands/e2e/e2e.js +3 -0
- package/src/commands/project/env.js +3 -3
- package/src/commands/project/heavy.js +10 -3
- package/src/commands/project/worktree.js +2 -1
- package/src/commands/state/finish.js +17 -3
- package/src/commands/validation/mutate.js +2 -2
- package/src/commands/validation/verify.js +3 -0
- package/src/core/paths/output-schema.js +0 -9
- package/src/core/state/feature-identity.js +0 -2
- package/src/core/state/state-manager.js +1 -169
- package/src/core/templates/template-renderer.js +0 -23
- package/src/dashboard/app/lib/format.js +0 -7
- package/src/dashboard/app/lib/phases.js +0 -7
- package/src/dashboard/scanner/activity-scanner.js +0 -4
- package/src/lib/ai/ai-project-scan.js +1 -6
- package/src/lib/e2e/compose-env.js +26 -3
- package/src/lib/graph/graphify-runtime.js +0 -1
- package/src/lib/heavy/gate.js +23 -4
- package/src/lib/heavy/live-stacks.js +64 -0
- package/src/lib/mutation/snapshot.js +1 -5
- package/src/lib/risk-detector.js +9 -6
- package/src/lib/stack/stack-profile.js +0 -4
- package/src/lib/stack/stack-scan.js +15 -4
- package/src/lib/tasks/eval-node.js +1 -7
- package/src/lib/validators/validation-runner.js +0 -43
- package/src/lib/validators/verify-runner.js +18 -2
- package/src/lib/worktree/docker-audit.js +7 -3
- package/src/lib/worktree/env-allocator.js +27 -5
- package/src/lib/worktree/ignored-triage.js +8 -0
- package/src/utils/agents-installer.js +12 -19
- package/src/utils/claude-settings-manager.js +0 -28
- package/src/utils/file-copier.js +0 -53
- package/framework/hooks/shared/activity-logger.js +0 -131
- package/framework/standards/frontend/design-system/naming.md +0 -68
- package/src/commands/validation/index.js +0 -2
- package/src/lib/validators/code-budget/index.js +0 -2
- package/src/lib/validators/contracts/index.js +0 -1
- package/src/lib/validators/nextjs/index.js +0 -1
- package/src/lib/validators/packages/index.js +0 -1
- package/src/lib/validators/reuse/index.js +0 -2
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
Server Components for initial page load. TanStack Query for client mutations and interactive data.
|
|
12
|
+
Server Components for initial page load. TanStack Query for client mutations and interactive data.
|
|
13
13
|
|
|
14
14
|
## Core Rules
|
|
15
15
|
|
|
16
16
|
- ALWAYS use Server Components for initial data that doesn't require interactivity
|
|
17
17
|
- ALWAYS use TanStack Query (`useQuery`, `useMutation`) for client-side data needs
|
|
18
|
-
- NEVER use `useEffect(() => { fetch(...) }, [])` — this is the old pattern
|
|
19
18
|
- ALWAYS validate API responses with Zod before using them
|
|
20
19
|
- ALWAYS use query key factories for consistent cache invalidation
|
|
21
20
|
- NEVER fetch a whole collection to page, sort or filter it in the browser — a list is paged on the server, with the query in the URL (`admin-app/data-table.md`)
|
|
21
|
+
- Fetch inside `useEffect` and a DB driver in Next are lint errors (`frontend-nextjs-lint`)
|
|
22
22
|
|
|
23
23
|
## TanStack Query Setup
|
|
24
24
|
|
|
@@ -134,7 +134,6 @@ export function UserListClient({ initialData }: { initialData: User[] }) {
|
|
|
134
134
|
|
|
135
135
|
| Wrong | Right | Why |
|
|
136
136
|
|-------|-------|-----|
|
|
137
|
-
| `useEffect(() => { fetch(...) }, [])` | `useQuery(...)` | Two renders, no cache, no SSR |
|
|
138
137
|
| No Zod on API response | `schema.parse(await res.json())` | Type safety at runtime |
|
|
139
138
|
| Hardcoded query keys: `['users']` | Key factory: `userKeys.lists()` | Consistent invalidation |
|
|
140
139
|
| `queryClient.invalidateQueries(['users'])` | `{ queryKey: userKeys.lists() }` | Object syntax required in v5 |
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# ESLint Next.js — o lint cobra, a prosa não repete
|
|
2
|
+
|
|
3
|
+
> **Scope:** nextjs
|
|
4
|
+
> **Layer:** 0 (resumo injetado pela rule `nextjs-standards` (`**/*.ts`, `**/*.tsx`); o `stack-scan` acusa o projeto sem este arquivo)
|
|
5
|
+
> **Keywords:** eslint, lint, eslint.config.mjs, no-restricted-imports, no-restricted-paths, no-restricted-syntax, no-default-export, no-explicit-any, framer-motion, zustand
|
|
6
|
+
> **Load When:** projeto Next.js sem `eslint.config.mjs` com `no-restricted-imports`, ou o lint acusou uma destas regras
|
|
7
|
+
|
|
8
|
+
**Verified against:** eslint 9.39.5, eslint-config-next 16.4.2 (typescript-eslint 8.71.2, eslint-plugin-import 2.32.0), next 16.4.2, typescript 6.0.3 (sonda: um arquivo ruim por regra → erro da regra esperada, `no-console` → aviso, arquivos conformes e o scaffold `morph-spec scaffold nextjs` → 0 problemas). Last-verified: 2026-10-18.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Regra de código que tem motivo e que uma máquina enxerga vira **erro de lint**, não frase que o
|
|
13
|
+
agente relê a cada sessão. Um arquivo na raiz do app Next basta, e só usa o que o
|
|
14
|
+
`eslint-config-next` já instala (typescript-eslint e eslint-plugin-import): nenhum plugin novo.
|
|
15
|
+
|
|
16
|
+
## Arquivos
|
|
17
|
+
|
|
18
|
+
Dependências (`npm i -D eslint@9 eslint-config-next typescript`). ESLint 9 é a faixa que
|
|
19
|
+
`eslint-plugin-import` e `eslint-plugin-react` declaram como peer; o typescript-eslint do
|
|
20
|
+
`eslint-config-next` pede TypeScript `<6.1`.
|
|
21
|
+
|
|
22
|
+
No Next 16 o `next build` não roda mais o lint. O nó de build do `morph-spec verify` roda o
|
|
23
|
+
eslint (`--quiet`: só erro reprova) **só nos arquivos que a feature mudou**, na raiz do app, quando
|
|
24
|
+
acha `eslint.config.*` e o pacote `eslint` instalado: dívida de lint antiga nunca deixa a feature
|
|
25
|
+
vermelha. As zonas são relativas a essa raiz.
|
|
26
|
+
|
|
27
|
+
`eslint.config.mjs`:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
31
|
+
import nextVitals from 'eslint-config-next/core-web-vitals'
|
|
32
|
+
import nextTs from 'eslint-config-next/typescript'
|
|
33
|
+
|
|
34
|
+
const BANNED = [
|
|
35
|
+
[['framer-motion'], 'Motion entra por motion/react: um único caminho de import.'],
|
|
36
|
+
[['zustand', 'redux', '@reduxjs/toolkit'], 'Estado de servidor é do TanStack Query; página e filtro vão na URL.'],
|
|
37
|
+
[['pg', '@neondatabase/serverless', '@prisma/client', 'drizzle-orm', 'postgres'], 'O Next nunca fala com o banco: o dado vem da API .NET.'],
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const DEEP_FEATURE = {
|
|
41
|
+
group: ['@/features/*/*'],
|
|
42
|
+
message: 'Fora da feature, importe a API pública (@/features/x), nunca um caminho interno.',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const restrictedImports = (...extraPatterns) => ['error', {
|
|
46
|
+
paths: [
|
|
47
|
+
...BANNED.flatMap(([names, message]) => names.map((name) => ({ name, message }))),
|
|
48
|
+
{ name: '@/components/ui/form', message: 'Form legado do shadcn: Controller + Field (frontend-nextjs-forms).' },
|
|
49
|
+
],
|
|
50
|
+
patterns: [
|
|
51
|
+
...BANNED.map(([names, message]) => ({ group: names.map((n) => `${n}/*`), message })),
|
|
52
|
+
...extraPatterns,
|
|
53
|
+
],
|
|
54
|
+
}]
|
|
55
|
+
|
|
56
|
+
const FETCH_IN_EFFECT = {
|
|
57
|
+
selector:
|
|
58
|
+
"CallExpression[callee.name='useEffect'] CallExpression:matches([callee.name='fetch'], [callee.name='apiClient'], [callee.object.name='apiClient'])",
|
|
59
|
+
message: 'Dado do servidor vem de Server Component ou TanStack Query, nunca de fetch em useEffect.',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default defineConfig([
|
|
63
|
+
...nextVitals,
|
|
64
|
+
...nextTs,
|
|
65
|
+
globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']),
|
|
66
|
+
{
|
|
67
|
+
rules: {
|
|
68
|
+
'no-restricted-imports': restrictedImports(DEEP_FEATURE),
|
|
69
|
+
'import/no-restricted-paths': ['error', {
|
|
70
|
+
zones: [
|
|
71
|
+
{ target: './src/components', from: './src/features', message: 'components/ é apresentação: não conhece feature.' },
|
|
72
|
+
{ target: './src/features', from: './src/app', message: 'app/ importa de features/, nunca o contrário.' },
|
|
73
|
+
{ target: './src/hooks', from: ['./src/lib/api-client.ts', './src/lib/api'], message: 'Hook compartilhado não chama API: isso é hook de feature.' },
|
|
74
|
+
],
|
|
75
|
+
}],
|
|
76
|
+
'no-restricted-syntax': ['error', FETCH_IN_EFFECT],
|
|
77
|
+
'import/no-default-export': 'error',
|
|
78
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
79
|
+
'no-console': 'warn',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
files: ['src/features/**'],
|
|
84
|
+
rules: { 'no-restricted-imports': restrictedImports() },
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
files: ['app/layout.tsx', 'src/app/layout.tsx'],
|
|
88
|
+
rules: {
|
|
89
|
+
'no-restricted-syntax': ['error', FETCH_IN_EFFECT, {
|
|
90
|
+
selector: "Program > ExpressionStatement[directive='use client']",
|
|
91
|
+
message: "O layout raiz é Server Component (metadata): o 'use client' vai no provider que ele renderiza.",
|
|
92
|
+
}],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
files: [
|
|
97
|
+
'**/app/**/{page,layout,loading,error,global-error,not-found,forbidden,unauthorized,template,default,route}.{ts,tsx}',
|
|
98
|
+
'**/app/**/{sitemap,robots,manifest,icon,apple-icon,opengraph-image,twitter-image}.{ts,tsx}',
|
|
99
|
+
'**/*.config.{js,mjs,cjs,ts}',
|
|
100
|
+
],
|
|
101
|
+
rules: { 'import/no-default-export': 'off' },
|
|
102
|
+
},
|
|
103
|
+
])
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Cada override de `no-restricted-imports` e `no-restricted-syntax` substitui a lista inteira da
|
|
107
|
+
regra naquele arquivo, por isso o bloco do layout repete `FETCH_IN_EFFECT` e o de `features/`
|
|
108
|
+
repete os pacotes banidos.
|
|
109
|
+
|
|
110
|
+
## O que cada regra compra
|
|
111
|
+
|
|
112
|
+
| Regra | Bloqueia | Por quê |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `no-restricted-imports` `@/features/*/*` (fora de `src/features/`) | `app/`, `components/` e `lib/` lendo arquivo interno de uma feature | o `index.ts` é a API pública: refatorar dentro da feature não quebra quem está fora |
|
|
115
|
+
| `no-restricted-imports` `framer-motion` | o segundo caminho de import do Motion | `motion/react` e `framer-motion` juntos duplicam o runtime no bundle e dividem o código em duas APIs |
|
|
116
|
+
| `no-restricted-imports` `zustand`, `redux`, `@reduxjs/toolkit` | store global | cópia do estado do servidor fica velha sem o cache do TanStack Query; página e filtro na URL sobrevivem a refresh e link |
|
|
117
|
+
| `no-restricted-imports` `pg`, `@neondatabase/serverless`, `@prisma/client`, `drizzle-orm`, `postgres` | driver de banco no Next | regra de negócio e acesso a dados moram no .NET (EF Core); um segundo caminho ao banco pula a autorização dele |
|
|
118
|
+
| `no-restricted-imports` `@/components/ui/form` | o `Form` legado do shadcn | a documentação do shadcn é escrita em `Controller` + `Field`; a tela legada migra quando é tocada |
|
|
119
|
+
| `import/no-restricted-paths` `src/components` ↛ `src/features` | componente compartilhado que conhece domínio | ele deixa de ser compartilhável e cria ciclo feature → components → feature |
|
|
120
|
+
| `import/no-restricted-paths` `src/features` ↛ `src/app` | feature importando rota | a dependência é de mão única: a rota compõe a feature |
|
|
121
|
+
| `import/no-restricted-paths` `src/hooks` ↛ cliente da API | hook compartilhado chamando o backend | chamada à API é de feature, com a key factory dela; hook compartilhado é só de UI |
|
|
122
|
+
| `no-restricted-syntax` fetch/`apiClient` dentro de `useEffect` | busca de dado no efeito | sem cache, sem dedup, corrida entre requisições e cascata de render; Server Component ou TanStack Query resolvem |
|
|
123
|
+
| `no-restricted-syntax` `'use client'` em `app/layout.tsx` | layout raiz no cliente | perde `metadata` e manda a árvore inteira para o bundle do cliente |
|
|
124
|
+
| `import/no-default-export` (exceto arquivos de rota e `*.config.*`) | `export default` em componente e hook | nome único em todo import: busca e rename acham todos os usos; o Next exige `default` só nos arquivos de convenção |
|
|
125
|
+
| `@typescript-eslint/no-explicit-any` | `any` | desliga o tipo no ponto onde o dado da API entra; o certo é schema Zod + `z.infer` |
|
|
126
|
+
| `no-console` (aviso) | `console.log` esquecido | ruído no console do usuário e dado vazando; aviso, porque o diagnóstico local é legítimo |
|
|
127
|
+
|
|
128
|
+
Restrição nova entra como linha neste arquivo, com o porquê na `message`. Nunca uma regra nova
|
|
129
|
+
em prosa. Exceção pontual é `// eslint-disable-next-line <regra> -- <motivo>` na linha, nunca o
|
|
130
|
+
desligamento da regra no projeto.
|
|
131
|
+
|
|
132
|
+
## O que não é regra
|
|
133
|
+
|
|
134
|
+
Nome de classe CSS (BEM, prefixo de página, namespace `--app-`, sufixo de estado), catálogo de
|
|
135
|
+
classes `.animate-*`/`.stagger-*`, `className` opcional em toda prop, Zod em props, quantos
|
|
136
|
+
momentos de motion uma página tem, o deslocamento de uma transição, o skeleton com
|
|
137
|
+
`animate-pulse`, os defaults de `staleTime`/`retry` do `QueryClient` e quantos níveis de prop
|
|
138
|
+
drilling são aceitáveis são gosto: nem prosa nem lint. Siga o arquivo vizinho, e o formatter do
|
|
139
|
+
projeto decide a forma. Também saíram da prosa o `'use client'` no `error.tsx` (o `next build`
|
|
140
|
+
recusa sem ele) e o prefixo `use` do hook (o `react-hooks/rules-of-hooks` que vem no
|
|
141
|
+
`eslint-config-next` acusa hook chamado fora de função `useX`).
|
|
@@ -221,14 +221,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
221
221
|
|
|
222
222
|
- `mode="wait"` ensures the exit animation completes before the entering page renders.
|
|
223
223
|
- The `key` prop must change on navigation — use `usePathname()` from `next/navigation`.
|
|
224
|
-
- Keep page transitions subtle (`y: 8`, fast duration) — aggressive transitions hurt perceived performance.
|
|
225
224
|
|
|
226
225
|
---
|
|
227
226
|
|
|
228
227
|
## Section 6: Loading Patterns
|
|
229
228
|
|
|
230
|
-
Prefer CSS `animate-pulse` for skeleton loaders over Motion — CSS animations run off the main thread and have lower performance overhead.
|
|
231
|
-
|
|
232
229
|
```tsx
|
|
233
230
|
// components/skeleton.tsx
|
|
234
231
|
import { cn } from '@/lib/utils';
|
|
@@ -315,11 +312,8 @@ This CSS fallback catches animations outside Motion (Tailwind `animate-*`, third
|
|
|
315
312
|
| `duration: 0.8` for UI interactions | `duration: durations.fast` (0.15) for micro-interactions |
|
|
316
313
|
| Hardcoded easing `[0.4, 0, 0.2, 1]` | `ease: easings.standard` (from design-tokens) |
|
|
317
314
|
| No `useReducedMotion()` check | Always check or use CSS media query |
|
|
318
|
-
| Animating everything on the page | Reserve motion for 3-5 meaningful moments per page |
|
|
319
315
|
| Forgetting `animation-fill-mode: backwards` in stagger | Will cause a flash before the animation starts |
|
|
320
316
|
| `AnimatePresence` without `key` changes | Exit animations will never fire |
|
|
321
|
-
| Using `motion` for skeleton loaders | Use CSS `animate-pulse` — lower performance cost |
|
|
322
|
-
| `npm install framer-motion` / `import ... from 'framer-motion'` | `npm install motion` / `import ... from 'motion/react'` — same code underneath, but one import path per codebase |
|
|
323
317
|
| Need ScrollTrigger, SVG morph, or complex timelines | Use GSAP — see `frontend/gsap/gsap-core.md` |
|
|
324
318
|
|
|
325
319
|
---
|
|
@@ -333,7 +327,8 @@ npm install motion
|
|
|
333
327
|
> The package is `motion` and components import from `motion/react`. `framer-motion`
|
|
334
328
|
> is not deprecated: it is published with the same version and `motion` depends on
|
|
335
329
|
> it. New code still depends on `motion` and imports from `motion/react`, so the
|
|
336
|
-
> codebase has a single import path;
|
|
330
|
+
> codebase has a single import path; a `framer-motion` import is a lint error
|
|
331
|
+
> (`frontend-nextjs-lint`).
|
|
337
332
|
|
|
338
333
|
Verify the installed version supports the features used here (requires motion >= 11; current major on npm is 14):
|
|
339
334
|
|
|
@@ -15,9 +15,9 @@ Consistent naming prevents filesystem bugs (Linux case-sensitivity) and keeps th
|
|
|
15
15
|
|
|
16
16
|
- ALWAYS use kebab-case for file names — never PascalCase or camelCase
|
|
17
17
|
- ALWAYS use PascalCase for React component exports
|
|
18
|
-
- ALWAYS use camelCase starting with `use` for hook exports
|
|
19
18
|
- ALWAYS suffix schema files with `.schemas.ts` and type files with `.types.ts`
|
|
20
19
|
- NEVER mix cases in the same category — no `UserCard.tsx` alongside `user-profile.tsx`
|
|
20
|
+
- Named exports, the hook `use` prefix and `default` only in route files are lint errors (`frontend-nextjs-lint`), not rules to reread
|
|
21
21
|
|
|
22
22
|
## Complete Reference Table
|
|
23
23
|
|
|
@@ -63,7 +63,6 @@ export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
|
|
63
63
|
|-------|-------|-----|
|
|
64
64
|
| `UserCard.tsx` | `user-card.tsx` | Linux servers are case-sensitive |
|
|
65
65
|
| `useUsers.ts` | `use-users.ts` | Inconsistent with Next.js file conventions |
|
|
66
|
-
| `export default function UserCard` | `export function UserCard` | Named exports are more refactor-safe |
|
|
67
66
|
| `type User = { id: string }` | `type User = z.infer<typeof userSchema>` | Single source of truth |
|
|
68
67
|
|
|
69
68
|
---
|
|
@@ -16,7 +16,8 @@ Stack: Next.js 15 + Neon PostgreSQL + .NET Backend
|
|
|
16
16
|
- ALWAYS use App Router (not Pages Router)
|
|
17
17
|
- Default to Server Components -- add `'use client'` only when needed
|
|
18
18
|
- ALWAYS colocate loading.tsx and error.tsx with page.tsx
|
|
19
|
-
-
|
|
19
|
+
- Route Handlers are a BFF that delegates to the .NET API
|
|
20
|
+
- A DB driver, `any`, fetch inside `useEffect` and `'use client'` in the root layout are lint errors (`frontend-nextjs-lint`)
|
|
20
21
|
- ALWAYS validate inputs with Zod on both client and server
|
|
21
22
|
- Use TypeScript strict mode (`"strict": true`)
|
|
22
23
|
|
|
@@ -58,7 +59,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
58
59
|
// loading.tsx
|
|
59
60
|
export default function Loading() { return <div className="animate-pulse">Loading...</div>; }
|
|
60
61
|
|
|
61
|
-
// error.tsx
|
|
62
|
+
// error.tsx
|
|
62
63
|
'use client';
|
|
63
64
|
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
64
65
|
return <div><h2>Something went wrong</h2><button onClick={reset}>Try again</button></div>;
|
|
@@ -187,7 +188,7 @@ export function DocumentForm() {
|
|
|
187
188
|
|
|
188
189
|
## shadcn/ui
|
|
189
190
|
|
|
190
|
-
Install: `npx shadcn@latest init` then `npx shadcn@latest add button input card dialog
|
|
191
|
+
Install: `npx shadcn@latest init` then `npx shadcn@latest add button input card dialog field`.
|
|
191
192
|
Components are copied to `components/ui/` -- NOT an npm dependency, your code to customize.
|
|
192
193
|
|
|
193
194
|
## TypeScript Strict Patterns
|
|
@@ -195,7 +196,7 @@ Components are copied to `components/ui/` -- NOT an npm dependency, your code to
|
|
|
195
196
|
| Pattern | Approach |
|
|
196
197
|
|---------|----------|
|
|
197
198
|
| API responses | Zod schema + `z.infer<typeof schema>` |
|
|
198
|
-
| Props | Explicit interface
|
|
199
|
+
| Props | Explicit interface |
|
|
199
200
|
| Event handlers | `React.ChangeEvent<HTMLInputElement>` |
|
|
200
201
|
| Null safety | `?.` over type assertions, `if (!data) return null` |
|
|
201
202
|
|
|
@@ -204,10 +205,7 @@ Components are copied to `components/ui/` -- NOT an npm dependency, your code to
|
|
|
204
205
|
| Wrong | Right | Why |
|
|
205
206
|
|-------|-------|-----|
|
|
206
207
|
| `'use client'` on every component | Default to Server Components | Unnecessary JS bundle size |
|
|
207
|
-
| Direct DB from client | Route Handler `/api/*` as BFF | Exposes queries, harder to secure |
|
|
208
|
-
| `any` for API responses | Zod schema + infer | No runtime safety |
|
|
209
208
|
| Missing loading.tsx | Colocate with page.tsx | Blank page during load |
|
|
210
|
-
| `useEffect` for data fetching | React Query `useQuery` | No caching, race conditions |
|
|
211
209
|
| Form validation on submit only | Zod resolver + react-hook-form | Delayed error feedback |
|
|
212
210
|
| shadcn as npm package | `npx shadcn@latest add` | Copy-paste system, not a dependency |
|
|
213
211
|
|
|
@@ -14,10 +14,11 @@ Feature-based architecture with a shared core. Features are self-contained; shar
|
|
|
14
14
|
## Core Rules
|
|
15
15
|
|
|
16
16
|
- ALWAYS use feature-based folders: `features/{feature-name}/`
|
|
17
|
-
- ALWAYS keep `app/` for routing only —
|
|
17
|
+
- ALWAYS keep `app/` for routing only — the screen's logic lives in `features/`
|
|
18
18
|
- NEVER import from one feature into another — extract to `components/` or `lib/` if shared
|
|
19
19
|
- ALWAYS use `src/` directory (configured in `tsconfig.json` with `@/` alias)
|
|
20
20
|
- NEVER put business logic in `components/` — it is presentation only
|
|
21
|
+
- Deep imports into a feature from outside it, `components/` → `features/`, `features/` → `app/` and shared `hooks/` → the API client are lint errors (`frontend-nextjs-lint`)
|
|
21
22
|
|
|
22
23
|
## Canonical Folder Tree
|
|
23
24
|
|
|
@@ -82,7 +83,7 @@ export { useCreateUser } from './hooks/use-create-user';
|
|
|
82
83
|
export type { User, CreateUserInput } from './types/user.types';
|
|
83
84
|
```
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
Outside the feature, import from the index:
|
|
86
87
|
```ts
|
|
87
88
|
// GOOD
|
|
88
89
|
import { UserList, useUsers } from '@/features/users';
|
|
@@ -16,7 +16,7 @@ Server state (API data) lives in TanStack Query. UI state lives in React. No glo
|
|
|
16
16
|
- ALWAYS use TanStack Query for anything that comes from the .NET API
|
|
17
17
|
- ALWAYS use `useState` for local UI state (modal open, form step)
|
|
18
18
|
- ALWAYS keep a list's page, sort and filters, and a page's active tab, in the URL (`searchParams`) — never in `useState`: a refresh or a shared link must reopen the same view (`admin-app/data-table.md`, `admin-app/page-structure.md`)
|
|
19
|
-
-
|
|
19
|
+
- `zustand`, `redux` and `@reduxjs/toolkit` are lint errors (`frontend-nextjs-lint`); the only way past it is the exception below
|
|
20
20
|
- NEVER use React Context for server state — Context does not cache or deduplicate
|
|
21
21
|
- ALWAYS derive state from server data rather than syncing it to local state
|
|
22
22
|
|
|
@@ -30,9 +30,9 @@ Is this local UI state (open/closed, selected, current step)?
|
|
|
30
30
|
YES → useState in the component that needs it
|
|
31
31
|
|
|
32
32
|
Does multiple components need this UI state?
|
|
33
|
-
YES — is it parent-child? →
|
|
33
|
+
YES — is it parent-child? → props
|
|
34
34
|
YES — is it truly global? → React Context (theme, auth user, locale)
|
|
35
|
-
YES — complex with many actions? →
|
|
35
|
+
YES — complex with many actions? → global store (exception below)
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## Server State (TanStack Query)
|
|
@@ -55,9 +55,10 @@ function UserList() {
|
|
|
55
55
|
}
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
## When
|
|
58
|
+
## When a Global Store Is Justified
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
The lint blocks the import. Lift it only if ALL of these are true, with one
|
|
61
|
+
`// eslint-disable-next-line no-restricted-imports -- <reason>` in the single store file:
|
|
61
62
|
1. State is needed in 5+ unrelated components
|
|
62
63
|
2. State has complex update logic (not just toggle/set)
|
|
63
64
|
3. Context causes measurable re-render performance issues
|
|
@@ -67,10 +68,8 @@ Only add Zustand if ALL of these are true:
|
|
|
67
68
|
|
|
68
69
|
| Wrong | Right | Why |
|
|
69
70
|
|-------|-------|-----|
|
|
70
|
-
| `useEffect` + `useState` for API data | TanStack Query | Stale data, no cache, double-render |
|
|
71
71
|
| Global store for user list | `useUsers()` | TanStack Query handles sync/cache |
|
|
72
72
|
| Context for everything | useState + prop drilling first | Context causes all consumers to re-render |
|
|
73
|
-
| Zustand by default | Start with useState | YAGNI — most state is local |
|
|
74
73
|
|
|
75
74
|
---
|
|
76
75
|
|
|
@@ -96,5 +96,6 @@ volumes:
|
|
|
96
96
|
```bash
|
|
97
97
|
# inside the worktree, outside Claude Code
|
|
98
98
|
eval "$(morph-spec env)" # PowerShell: morph-spec env --powershell | Invoke-Expression
|
|
99
|
+
# --with-secrets also loads the e2e.auth.envFile credentials
|
|
99
100
|
docker compose up -d --wait # lands in project morph-{feature}, on the block's ports
|
|
100
101
|
```
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
nextjs) rather than by stack. A template for a .NET slice serves every stack with a .NET
|
|
9
9
|
backend, so a new stack reuses what exists instead of copying it.
|
|
10
10
|
|
|
11
|
-
`REGISTRY.json` is the source of truth: it maps each template `id` to its `
|
|
12
|
-
|
|
11
|
+
`REGISTRY.json` is the source of truth: it maps each template `id` to its `kind`, `path` and
|
|
12
|
+
`category` (the shape per kind is in its `kinds` block). Nothing resolves a template by hardcoded path — code goes
|
|
13
13
|
through the registry, and this README never repeats a path the registry already owns.
|
|
14
14
|
|
|
15
15
|
---
|
|
@@ -109,8 +109,8 @@ updates** — only override when the packaged template genuinely does not fit.
|
|
|
109
109
|
1. Create the file under the right technology directory:
|
|
110
110
|
`framework/templates/code/{technology}/...` (Handlebars syntax, `.hbs` when the extension
|
|
111
111
|
would otherwise confuse tooling).
|
|
112
|
-
2. Register it in `REGISTRY.json` with a unique `id`, its `
|
|
113
|
-
|
|
112
|
+
2. Register it in `REGISTRY.json` with a unique `id`, its `kind`, `path` and `category`, plus
|
|
113
|
+
the fields its kind requires (`kinds` block; `scripts/validate-framework.js` checks them).
|
|
114
114
|
3. If a generator should emit it, add the `id` to the relevant list in
|
|
115
115
|
`src/lib/generators/scaffold-generator.js`.
|
|
116
116
|
|