feat(dev-tools): adiciona plugin dev-tools (agents, skills, knowledge e metadados do projeto)
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
---
|
||||
name: nextjs
|
||||
description: Desenvolvimento Next.js moderno (13+) com App Router, Server Components, rotas API e deployment seguindo padroes Descomplicar.
|
||||
layer: wiki
|
||||
---
|
||||
|
||||
# /nextjs - Next.js Development
|
||||
|
||||
Desenvolvimento Next.js moderno (13+) com App Router e Server Components.
|
||||
|
||||
---
|
||||
|
||||
## Contexto NotebookLM
|
||||
|
||||
```
|
||||
mcp__notebooklm__notebook_query({
|
||||
notebook_id: "24947ffa-0019-448a-a340-2f4a275d2eb1",
|
||||
query: "<adaptar ao contexto do pedido>"
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Regra #48 - Dev Container (OBRIGATÓRIO)
|
||||
|
||||
**TODOS os projectos Next.js devem ser desenvolvidos no container dev.**
|
||||
|
||||
```
|
||||
SSH: server="dev" (mcp__ssh-unified__ssh_execute)
|
||||
Path: /root/Dev/<projecto>
|
||||
Sync: auto -> /media/ealmeida/Dados/Dev/<projecto> (Syncthing)
|
||||
```
|
||||
|
||||
**Workflow:**
|
||||
1. `mcp__ssh-unified__ssh_execute server:"dev" command:"mkdir -p /root/Dev/<projecto>"`
|
||||
2. Desenvolver e testar no container
|
||||
3. `npm run build` no container antes de deploy
|
||||
4. Deploy via EasyPanel
|
||||
5. Syncthing propaga automaticamente
|
||||
|
||||
**NUNCA:** `npx create-next-app` directamente no PC local para projectos colaborativos.
|
||||
|
||||
---
|
||||
|
||||
## App Router Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── layout.tsx # Root layout (obrigatório)
|
||||
├── page.tsx # Home page
|
||||
├── loading.tsx # Loading UI (Suspense)
|
||||
├── error.tsx # Error boundary
|
||||
├── not-found.tsx # 404 page
|
||||
├── (auth)/ # Route group (não afecta URL)
|
||||
│ ├── login/page.tsx
|
||||
│ └── register/page.tsx
|
||||
├── dashboard/
|
||||
│ ├── layout.tsx
|
||||
│ ├── page.tsx
|
||||
│ └── [id]/page.tsx # Dynamic route
|
||||
└── api/route.ts # API route
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server vs Client Components
|
||||
|
||||
### Server Component (default)
|
||||
|
||||
```tsx
|
||||
async function ProductsPage() {
|
||||
const products = await db.product.findMany();
|
||||
return <div>{products.map(p => <ProductCard key={p.id} product={p} />)}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Client Component
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Fetching Patterns
|
||||
|
||||
```tsx
|
||||
// Static (SSG) - default
|
||||
const data = await fetch('https://api.example.com/data', { cache: 'force-cache' });
|
||||
|
||||
// Dynamic (SSR)
|
||||
const data = await fetch('https://api.example.com/data', { cache: 'no-store' });
|
||||
|
||||
// ISR - revalidação automática
|
||||
const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 } });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server Actions
|
||||
|
||||
```tsx
|
||||
// app/actions.ts
|
||||
'use server';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const title = formData.get('title') as string;
|
||||
await db.post.create({ data: { title } });
|
||||
revalidatePath('/posts');
|
||||
}
|
||||
|
||||
// Uso no form:
|
||||
<form action={createPost}>
|
||||
<input name="title" required />
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Routing Avançado
|
||||
|
||||
### Middleware
|
||||
|
||||
```tsx
|
||||
// middleware.ts
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const token = request.cookies.get('token');
|
||||
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
||||
return NextResponse.redirect(new URL('/login', request.url));
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = { matcher: ['/dashboard/:path*'] };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optimizações
|
||||
|
||||
### Images
|
||||
```tsx
|
||||
import Image from 'next/image';
|
||||
<Image src="/photo.jpg" width={500} height={300} alt="Photo" priority />
|
||||
```
|
||||
|
||||
### Metadata (SEO)
|
||||
```tsx
|
||||
export const metadata = {
|
||||
title: 'Home',
|
||||
description: 'Home page',
|
||||
openGraph: { title: 'Home', images: ['/og-image.jpg'] },
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance (Prioridades)
|
||||
|
||||
| Prioridade | Categoria | Ganho |
|
||||
|---|---|---|
|
||||
| CRITICAL | Eliminar Waterfalls (Promise.all) | 2-10x latência |
|
||||
| CRITICAL | Bundle Size (dynamic imports) | 200-800ms load |
|
||||
| HIGH | Server-Side (auth actions, React.cache) | RSC payload |
|
||||
| MEDIUM | Re-renders (derived state, functional setState) | Responsividade |
|
||||
|
||||
Para todas as 57 regras com exemplos de código: ver `references/performance-rules.md`
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker
|
||||
|
||||
```dockerfile
|
||||
FROM node:18-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package*.json ./
|
||||
RUN npm ci --production
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Datasets Dify
|
||||
|
||||
| Dataset | ID |
|
||||
|---------|-----|
|
||||
| Desenvolvimento de Software | `e7c7decc-0ded-4351-ab14-b110b3c38ec9` |
|
||||
| TI | `7f63ec0c-6321-488c-b107-980140199850` |
|
||||
|
||||
---
|
||||
|
||||
## Referências
|
||||
|
||||
- `references/performance-rules.md` - 57 regras de performance com exemplos completos
|
||||
|
||||
---
|
||||
|
||||
*Versão 1.0.0 | Descomplicar®*
|
||||
|
||||
---
|
||||
|
||||
## Healing Log
|
||||
|
||||
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
|
||||
|
||||
```jsonl
|
||||
{"date":"","issue":"","fix":"","source":"user|auto"}
|
||||
```
|
||||
|
||||
*Adicionar nova linha após cada erro corrigido.*
|
||||
@@ -0,0 +1,245 @@
|
||||
# /nextjs - Performance Rules (Vercel Engineering)
|
||||
|
||||
57 regras priorizadas por impacto. Fonte: Vercel Engineering react-best-practices.
|
||||
|
||||
## 1. Eliminating Waterfalls (CRITICAL)
|
||||
|
||||
```tsx
|
||||
// ❌ ERRADO: Awaits sequenciais (waterfall)
|
||||
const session = await getSession()
|
||||
const config = await getConfig()
|
||||
const user = await getUser(session.userId)
|
||||
|
||||
// ✅ CORRECTO: Paralelo com Promise.all()
|
||||
const [session, config] = await Promise.all([
|
||||
getSession(),
|
||||
getConfig()
|
||||
])
|
||||
const user = await getUser(session.userId) // depende de session
|
||||
|
||||
// ✅ CORRECTO: Suspense boundaries estratégicos
|
||||
async function Page() {
|
||||
return (
|
||||
<Layout>
|
||||
<Suspense fallback={<HeaderSkeleton />}>
|
||||
<Header />
|
||||
</Suspense>
|
||||
<Suspense fallback={<ContentSkeleton />}>
|
||||
<SlowContent />
|
||||
</Suspense>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ CORRECTO: Defer await até necessário
|
||||
async function handler(req) {
|
||||
const dataPromise = fetchData() // inicia imediatamente
|
||||
if (req.method === 'HEAD') return new Response() // early return
|
||||
const data = await dataPromise // só espera quando precisa
|
||||
return Response.json(data)
|
||||
}
|
||||
```
|
||||
|
||||
**Regras:**
|
||||
- `async-defer-await`: Mover await para onde é necessário
|
||||
- `async-dependency-parallel`: Usar `better-all` para dependências parciais
|
||||
- `async-prevent-waterfall`: Iniciar promises antes de awaitar
|
||||
- `async-promise-all`: `Promise.all()` para operações independentes
|
||||
- `async-suspense-boundaries`: Suspense para UI progressiva
|
||||
|
||||
---
|
||||
|
||||
## 2. Bundle Size (CRITICAL)
|
||||
|
||||
```tsx
|
||||
// ❌ ERRADO: Barrel file imports
|
||||
import { Button, Icon } from '@mui/material' // carrega tudo
|
||||
|
||||
// ✅ CORRECTO: Import directo
|
||||
import Button from '@mui/material/Button'
|
||||
|
||||
// ✅ CORRECTO: optimizePackageImports (Next.js 13.5+)
|
||||
module.exports = {
|
||||
experimental: {
|
||||
optimizePackageImports: ['lucide-react', '@mui/material', 'lodash']
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CORRECTO: Dynamic imports para componentes pesados
|
||||
const MonacoEditor = dynamic(() => import('./MonacoEditor'), {
|
||||
ssr: false,
|
||||
loading: () => <EditorSkeleton />
|
||||
})
|
||||
|
||||
// ✅ CORRECTO: Preload em hover/focus
|
||||
function FeatureButton() {
|
||||
return (
|
||||
<button
|
||||
onMouseEnter={() => import('./HeavyFeature')}
|
||||
onClick={openFeature}
|
||||
>
|
||||
Open Feature
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Server-Side Performance (HIGH)
|
||||
|
||||
```tsx
|
||||
// ✅ Server Actions: autenticar SEMPRE
|
||||
'use server'
|
||||
async function updateProfile(formData: FormData) {
|
||||
const session = await auth() // 1. Autenticar
|
||||
if (!session) throw new Error('Unauthorized')
|
||||
const data = schema.parse(formData) // 2. Validar
|
||||
await db.user.update({ where: { id: session.userId }, data })
|
||||
revalidatePath('/profile')
|
||||
}
|
||||
|
||||
// ✅ React.cache() para deduplicação per-request
|
||||
const getUser = cache(async (id: string) => {
|
||||
return db.user.findUnique({ where: { id } })
|
||||
})
|
||||
|
||||
// ✅ after() para operações não-bloqueantes
|
||||
import { after } from 'next/server'
|
||||
async function submitForm(data: FormData) {
|
||||
const result = await processForm(data)
|
||||
after(async () => {
|
||||
await logAnalytics(result) // executa APÓS resposta enviada
|
||||
})
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Client-Side Data Fetching (MEDIUM-HIGH)
|
||||
|
||||
```tsx
|
||||
// ✅ SWR para deduplicação automática
|
||||
const { data, isLoading } = useSWR('/api/user', fetcher)
|
||||
|
||||
// ✅ Passive event listeners para scroll performance
|
||||
element.addEventListener('scroll', handler, { passive: true })
|
||||
|
||||
// ✅ localStorage versionado com try-catch
|
||||
const key = 'userConfig:v2'
|
||||
try {
|
||||
const data = JSON.parse(localStorage.getItem(key) ?? '{}')
|
||||
} catch { /* incognito mode, quota exceeded */ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Re-render Optimization (MEDIUM)
|
||||
|
||||
```tsx
|
||||
// ✅ Derived state: calcular durante render, NÃO em useEffect
|
||||
function FilteredList({ items, query }: Props) {
|
||||
const filtered = items.filter(i => i.name.includes(query)) // derive!
|
||||
return <List items={filtered} />
|
||||
}
|
||||
|
||||
// ✅ Functional setState para evitar stale closures
|
||||
setItems(curr => [...curr, ...newItems]) // ✅
|
||||
// setItems([...items, ...newItems]) // ❌ stale closure
|
||||
|
||||
// ✅ Lazy state initialization
|
||||
const [index] = useState(() => buildSearchIndex(items)) // computa 1x
|
||||
|
||||
// ✅ useTransition para updates não-urgentes
|
||||
const [isPending, startTransition] = useTransition()
|
||||
startTransition(() => setSearchResults(results))
|
||||
|
||||
// ✅ Extrair defaults não-primitivos para module scope
|
||||
const EMPTY_ARRAY: string[] = []
|
||||
const NOOP = () => {}
|
||||
function Component({ items = EMPTY_ARRAY, onClick = NOOP }) { ... }
|
||||
|
||||
// ✅ Dependências de effect: primitivos, não objectos
|
||||
useEffect(() => { fetchUser(userId) }, [userId]) // ✅ primitivo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Rendering Performance (MEDIUM)
|
||||
|
||||
```tsx
|
||||
// ✅ Hoist static JSX fora do componente
|
||||
const skeleton = <div className="skeleton h-4 w-full" /> // module scope
|
||||
|
||||
// ✅ Conditional rendering explícito (evitar && com falsy)
|
||||
{count > 0 ? <Badge>{count}</Badge> : null} // ✅
|
||||
// {count && <Badge>{count}</Badge>} // ❌ renderiza "0"
|
||||
|
||||
// ✅ useTransition em vez de useState manual para loading
|
||||
const [isPending, startTransition] = useTransition()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. JavaScript Performance (LOW-MEDIUM)
|
||||
|
||||
```tsx
|
||||
// ✅ Index Maps para lookups repetidos O(1) vs O(n)
|
||||
const userMap = new Map(users.map(u => [u.id, u]))
|
||||
orders.map(o => ({ ...o, user: userMap.get(o.userId) }))
|
||||
|
||||
// ✅ Set para membership checks
|
||||
const allowedIds = new Set(['a', 'b', 'c'])
|
||||
if (allowedIds.has(id)) { ... } // O(1)
|
||||
|
||||
// ✅ toSorted() para imutabilidade (React-safe)
|
||||
const sorted = items.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
// items.sort() // ❌ muta o array original
|
||||
|
||||
// ✅ Combinar iterações de array
|
||||
const { active, expired } = items.reduce((acc, item) => {
|
||||
if (item.active) acc.active.push(item)
|
||||
else acc.expired.push(item)
|
||||
return acc
|
||||
}, { active: [], expired: [] })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Advanced Patterns (LOW)
|
||||
|
||||
```tsx
|
||||
// ✅ Init uma vez, não por mount (Strict Mode remonta)
|
||||
let didInit = false
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
if (didInit) return
|
||||
didInit = true
|
||||
initializeAnalytics()
|
||||
}, [])
|
||||
}
|
||||
|
||||
// ✅ useEffectEvent para callbacks estáveis em effects
|
||||
const onTick = useEffectEvent((value) => {
|
||||
console.log(value) // sempre valor mais recente
|
||||
})
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => onTick(count), 1000)
|
||||
return () => clearInterval(id)
|
||||
}, []) // sem dependência de count!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resumo de Impacto
|
||||
|
||||
| Prioridade | Categoria | Ganho |
|
||||
|---|---|---|
|
||||
| CRITICAL | Waterfalls | 2-10x latência |
|
||||
| CRITICAL | Bundle Size | 200-800ms load |
|
||||
| HIGH | Server-Side | RSC payload, TTI |
|
||||
| MEDIUM-HIGH | Client Fetch | Request dedup |
|
||||
| MEDIUM | Re-renders | UI responsiveness |
|
||||
| MEDIUM | Rendering | Paint performance |
|
||||
| LOW-MEDIUM | JS Perf | CPU cycles |
|
||||
Reference in New Issue
Block a user