project-mcp-server 2.0.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/LICENSE +21 -0
- package/README.md +335 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/scanner/actions.d.ts +9 -0
- package/dist/scanner/actions.js +77 -0
- package/dist/scanner/actions.js.map +1 -0
- package/dist/scanner/cache.d.ts +4 -0
- package/dist/scanner/cache.js +34 -0
- package/dist/scanner/cache.js.map +1 -0
- package/dist/scanner/components.d.ts +17 -0
- package/dist/scanner/components.js +74 -0
- package/dist/scanner/components.js.map +1 -0
- package/dist/scanner/prisma.d.ts +17 -0
- package/dist/scanner/prisma.js +75 -0
- package/dist/scanner/prisma.js.map +1 -0
- package/dist/scanner/routes.d.ts +11 -0
- package/dist/scanner/routes.js +98 -0
- package/dist/scanner/routes.js.map +1 -0
- package/dist/setup.d.ts +10 -0
- package/dist/setup.js +289 -0
- package/dist/setup.js.map +1 -0
- package/dist/tools/env/index.d.ts +2 -0
- package/dist/tools/env/index.js +152 -0
- package/dist/tools/env/index.js.map +1 -0
- package/dist/tools/generate/index.d.ts +2 -0
- package/dist/tools/generate/index.js +320 -0
- package/dist/tools/generate/index.js.map +1 -0
- package/dist/tools/project/index.d.ts +2 -0
- package/dist/tools/project/index.js +193 -0
- package/dist/tools/project/index.js.map +1 -0
- package/package.json +35 -0
- package/skills/commit.md +109 -0
- package/skills/infra.md +218 -0
- package/skills/nextauth.md +256 -0
- package/skills/nextjs.md +262 -0
- package/skills/prisma.md +281 -0
- package/skills/project-intelligence.SKILL.md +141 -0
- package/skills/security.md +353 -0
- package/skills/shadcn.md +299 -0
- package/skills/testing.md +188 -0
- package/skills/zod.md +253 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nextauth
|
|
3
|
+
description: "Activar cuando se configura o modifica autenticacion, se protegen rutas o layouts, o se trabaja con sesiones y roles"
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill: NextAuth.js v5 (beta 30)
|
|
10
|
+
|
|
11
|
+
## Cuándo cargar esta skill
|
|
12
|
+
- Configurar o modificar autenticación
|
|
13
|
+
- Proteger rutas o layouts
|
|
14
|
+
- Acceder a la sesión del usuario
|
|
15
|
+
- Implementar middleware de auth
|
|
16
|
+
- Manejar roles y permisos en rutas
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Configuración base (v5 beta 30)
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// lib/auth/config.ts
|
|
24
|
+
import type { NextAuthConfig } from "next-auth"
|
|
25
|
+
import Credentials from "next-auth/providers/credentials"
|
|
26
|
+
import bcrypt from "bcrypt"
|
|
27
|
+
import { prismaPostgres } from "@/lib/db/postgres"
|
|
28
|
+
import { LoginSchema } from "@/types/schemas"
|
|
29
|
+
|
|
30
|
+
export const authConfig: NextAuthConfig = {
|
|
31
|
+
providers: [
|
|
32
|
+
Credentials({
|
|
33
|
+
async authorize(credentials) {
|
|
34
|
+
const parsed = LoginSchema.safeParse(credentials)
|
|
35
|
+
if (!parsed.success) return null
|
|
36
|
+
|
|
37
|
+
const user = await prismaPostgres.user.findUnique({
|
|
38
|
+
where: { email: parsed.data.email },
|
|
39
|
+
select: { id: true, email: true, name: true, password: true, roleId: true }
|
|
40
|
+
})
|
|
41
|
+
if (!user) return null
|
|
42
|
+
|
|
43
|
+
const valid = await bcrypt.compare(parsed.data.password, user.password)
|
|
44
|
+
if (!valid) return null
|
|
45
|
+
|
|
46
|
+
// No retornar el password al cliente
|
|
47
|
+
const { password: _, ...safeUser } = user
|
|
48
|
+
return safeUser
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
],
|
|
52
|
+
|
|
53
|
+
callbacks: {
|
|
54
|
+
// Extender el token JWT con datos del usuario
|
|
55
|
+
async jwt({ token, user }) {
|
|
56
|
+
if (user) {
|
|
57
|
+
token.id = user.id
|
|
58
|
+
token.roleId = user.roleId
|
|
59
|
+
}
|
|
60
|
+
return token
|
|
61
|
+
},
|
|
62
|
+
// Extender la sesión con datos del token
|
|
63
|
+
async session({ session, token }) {
|
|
64
|
+
if (token && session.user) {
|
|
65
|
+
session.user.id = token.id as string
|
|
66
|
+
session.user.roleId = token.roleId as string
|
|
67
|
+
}
|
|
68
|
+
return session
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
pages: {
|
|
73
|
+
signIn: "/login",
|
|
74
|
+
error: "/login" // Redirigir errores al login
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
session: { strategy: "jwt" }
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// lib/auth/index.ts — exportar handlers y helper auth()
|
|
83
|
+
import NextAuth from "next-auth"
|
|
84
|
+
import { authConfig } from "./config"
|
|
85
|
+
|
|
86
|
+
export const { handlers, auth, signIn, signOut } = NextAuth(authConfig)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// app/api/auth/[...nextauth]/route.ts
|
|
91
|
+
import { handlers } from "@/lib/auth"
|
|
92
|
+
export const { GET, POST } = handlers
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Acceder a la sesión
|
|
98
|
+
|
|
99
|
+
### En RSC o Server Actions
|
|
100
|
+
```typescript
|
|
101
|
+
import { auth } from "@/lib/auth"
|
|
102
|
+
|
|
103
|
+
// En un RSC
|
|
104
|
+
export default async function Page() {
|
|
105
|
+
const session = await auth()
|
|
106
|
+
if (!session) redirect("/login")
|
|
107
|
+
|
|
108
|
+
return <div>Hola {session.user.name}</div>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// En una Server Action
|
|
112
|
+
export async function deletePost(id: string) {
|
|
113
|
+
"use server"
|
|
114
|
+
const session = await auth()
|
|
115
|
+
if (!session) throw new Error("Unauthorized")
|
|
116
|
+
if (session.user.roleId !== "admin") throw new Error("Forbidden")
|
|
117
|
+
// ...
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### En Client Components
|
|
122
|
+
```typescript
|
|
123
|
+
"use client"
|
|
124
|
+
import { useSession } from "next-auth/react"
|
|
125
|
+
|
|
126
|
+
export function UserMenu() {
|
|
127
|
+
const { data: session, status } = useSession()
|
|
128
|
+
|
|
129
|
+
if (status === "loading") return <Skeleton />
|
|
130
|
+
if (!session) return <LoginButton />
|
|
131
|
+
|
|
132
|
+
return <div>{session.user.name}</div>
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Middleware — protección de rutas
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// middleware.ts (raíz del proyecto)
|
|
142
|
+
import { auth } from "@/lib/auth"
|
|
143
|
+
import { NextResponse } from "next/server"
|
|
144
|
+
|
|
145
|
+
export default auth((req) => {
|
|
146
|
+
const isLoggedIn = !!req.auth
|
|
147
|
+
const isOnDashboard = req.nextUrl.pathname.startsWith("/dashboard")
|
|
148
|
+
const isOnAdmin = req.nextUrl.pathname.startsWith("/admin")
|
|
149
|
+
const isOnLogin = req.nextUrl.pathname === "/login"
|
|
150
|
+
|
|
151
|
+
// Si intenta entrar al dashboard sin sesión → login
|
|
152
|
+
if (isOnDashboard && !isLoggedIn) {
|
|
153
|
+
return NextResponse.redirect(new URL("/login", req.url))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Si intenta admin sin rol admin → 403
|
|
157
|
+
if (isOnAdmin) {
|
|
158
|
+
if (!isLoggedIn) return NextResponse.redirect(new URL("/login", req.url))
|
|
159
|
+
if (req.auth?.user?.roleId !== "admin") {
|
|
160
|
+
return NextResponse.redirect(new URL("/403", req.url))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Si ya está logueado e intenta ir al login → dashboard
|
|
165
|
+
if (isOnLogin && isLoggedIn) {
|
|
166
|
+
return NextResponse.redirect(new URL("/dashboard", req.url))
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
export const config = {
|
|
171
|
+
matcher: ["/dashboard/:path*", "/admin/:path*", "/login"]
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Extender tipos de sesión (TypeScript)
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// types/next-auth.d.ts
|
|
181
|
+
import "next-auth"
|
|
182
|
+
import "next-auth/jwt"
|
|
183
|
+
|
|
184
|
+
declare module "next-auth" {
|
|
185
|
+
interface Session {
|
|
186
|
+
user: {
|
|
187
|
+
id: string
|
|
188
|
+
name: string
|
|
189
|
+
email: string
|
|
190
|
+
roleId: string
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface User {
|
|
195
|
+
roleId: string
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
declare module "next-auth/jwt" {
|
|
200
|
+
interface JWT {
|
|
201
|
+
id: string
|
|
202
|
+
roleId: string
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Login y logout desde Client Components
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
"use client"
|
|
213
|
+
import { signIn, signOut } from "next-auth/react"
|
|
214
|
+
|
|
215
|
+
// Login con redirect
|
|
216
|
+
<button onClick={() => signIn("credentials", { redirect: true, callbackUrl: "/dashboard" })}>
|
|
217
|
+
Iniciar sesión
|
|
218
|
+
</button>
|
|
219
|
+
|
|
220
|
+
// Logout
|
|
221
|
+
<button onClick={() => signOut({ callbackUrl: "/login" })}>
|
|
222
|
+
Cerrar sesión
|
|
223
|
+
</button>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Helper de permisos (RBAC)
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// lib/auth/permissions.ts
|
|
232
|
+
import { auth } from "@/lib/auth"
|
|
233
|
+
import { prismaPostgres } from "@/lib/db/postgres"
|
|
234
|
+
|
|
235
|
+
export async function hasPermission(action: string): Promise<boolean> {
|
|
236
|
+
const session = await auth()
|
|
237
|
+
if (!session?.user?.roleId) return false
|
|
238
|
+
|
|
239
|
+
const permission = await prismaPostgres.permission.findFirst({
|
|
240
|
+
where: {
|
|
241
|
+
roleId: session.user.roleId,
|
|
242
|
+
action: { in: [action, `${action.split(":")[0]}:*`] }
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
return !!permission
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Uso en Server Action
|
|
250
|
+
export async function createPost(data: unknown) {
|
|
251
|
+
"use server"
|
|
252
|
+
const allowed = await hasPermission("posts:create")
|
|
253
|
+
if (!allowed) throw new Error("Forbidden")
|
|
254
|
+
// ...
|
|
255
|
+
}
|
|
256
|
+
```
|
package/skills/nextjs.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nextjs
|
|
3
|
+
description: "Activar cuando se crean o modifican paginas, layouts, componentes React, Server Actions o Route Handlers en Next.js"
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill: Next.js 16 + React 19
|
|
10
|
+
|
|
11
|
+
## Cuándo cargar esta skill
|
|
12
|
+
- Crear o modificar páginas, layouts, o componentes React
|
|
13
|
+
- Implementar Server Actions o Route Handlers
|
|
14
|
+
- Trabajar con fetching de datos en RSC
|
|
15
|
+
- Configurar metadata, loading, error boundaries
|
|
16
|
+
- Cualquier tarea que involucre el App Router
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Decisión RSC vs Client Component
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
¿Necesita hooks (useState, useEffect, useRef)? → "use client"
|
|
24
|
+
¿Maneja eventos del DOM (onClick, onChange)? → "use client"
|
|
25
|
+
¿Usa Context de React? → "use client"
|
|
26
|
+
¿Solo muestra datos / layout / estructura? → RSC (default)
|
|
27
|
+
¿Hace fetch de datos desde DB o API externa? → RSC
|
|
28
|
+
¿Necesita acceso a cookies/headers en runtime? → RSC con cookies()/headers()
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Regla práctica: empezar como RSC, agregar `"use client"` solo cuando el compilador
|
|
32
|
+
o la lógica lo exija. Los Client Components deben ser hojas del árbol, no raíces.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Patrones de data fetching (Next.js 16)
|
|
37
|
+
|
|
38
|
+
### En RSC — fetch directo o query de DB
|
|
39
|
+
```typescript
|
|
40
|
+
// app/(dashboard)/users/page.tsx
|
|
41
|
+
import { prismaPostgres } from "@/lib/db/postgres"
|
|
42
|
+
|
|
43
|
+
export default async function UsersPage() {
|
|
44
|
+
// Directo — no hace falta useEffect ni SWR
|
|
45
|
+
const users = await prismaPostgres.user.findMany({
|
|
46
|
+
where: { active: true },
|
|
47
|
+
select: { id: true, name: true, email: true }
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return <UserList users={users} />
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Cache en RSC — usar `use cache` (Next 16, NO unstable_cache)
|
|
55
|
+
```typescript
|
|
56
|
+
import { unstable_cache as useCache } from "next/cache"
|
|
57
|
+
|
|
58
|
+
// Revalidar cada hora, tag para invalidación manual
|
|
59
|
+
const getProducts = useCache(
|
|
60
|
+
async () => prismaPostgres.product.findMany(),
|
|
61
|
+
["products"],
|
|
62
|
+
{ revalidate: 3600, tags: ["products"] }
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// Invalidar desde Server Action
|
|
66
|
+
import { revalidateTag } from "next/cache"
|
|
67
|
+
revalidateTag("products")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Server Actions
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// server/actions/users.ts
|
|
76
|
+
"use server"
|
|
77
|
+
|
|
78
|
+
import { z } from "zod"
|
|
79
|
+
import { prismaPostgres } from "@/lib/db/postgres"
|
|
80
|
+
import { auth } from "@/lib/auth"
|
|
81
|
+
|
|
82
|
+
const CreateUserSchema = z.object({
|
|
83
|
+
name: z.string().min(2),
|
|
84
|
+
email: z.string().email(),
|
|
85
|
+
role: z.enum(["admin", "user"])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
export async function createUser(formData: FormData) {
|
|
89
|
+
// 1. Autenticar
|
|
90
|
+
const session = await auth()
|
|
91
|
+
if (!session) throw new Error("Unauthorized")
|
|
92
|
+
|
|
93
|
+
// 2. Validar con Zod
|
|
94
|
+
const parsed = CreateUserSchema.safeParse({
|
|
95
|
+
name: formData.get("name"),
|
|
96
|
+
email: formData.get("email"),
|
|
97
|
+
role: formData.get("role")
|
|
98
|
+
})
|
|
99
|
+
if (!parsed.success) {
|
|
100
|
+
return { error: parsed.error.flatten().fieldErrors }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3. Ejecutar
|
|
104
|
+
const user = await prismaPostgres.user.create({ data: parsed.data })
|
|
105
|
+
|
|
106
|
+
// 4. Revalidar cache si aplica
|
|
107
|
+
revalidateTag("users")
|
|
108
|
+
|
|
109
|
+
return { success: true, user }
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Consumir Server Action desde Client Component
|
|
114
|
+
```typescript
|
|
115
|
+
"use client"
|
|
116
|
+
|
|
117
|
+
import { useActionState } from "react" // React 19 — reemplaza useFormState
|
|
118
|
+
import { createUser } from "@/server/actions/users"
|
|
119
|
+
|
|
120
|
+
export function CreateUserForm() {
|
|
121
|
+
const [state, action, isPending] = useActionState(createUser, null)
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<form action={action}>
|
|
125
|
+
<input name="name" />
|
|
126
|
+
<input name="email" type="email" />
|
|
127
|
+
<button type="submit" disabled={isPending}>
|
|
128
|
+
{isPending ? "Creando..." : "Crear usuario"}
|
|
129
|
+
</button>
|
|
130
|
+
{state?.error && <p>{JSON.stringify(state.error)}</p>}
|
|
131
|
+
</form>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Route Handlers (API)
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// app/api/users/route.ts
|
|
142
|
+
import { NextRequest, NextResponse } from "next/server"
|
|
143
|
+
import { auth } from "@/lib/auth"
|
|
144
|
+
import { prismaPostgres } from "@/lib/db/postgres"
|
|
145
|
+
|
|
146
|
+
export async function GET(request: NextRequest) {
|
|
147
|
+
const session = await auth()
|
|
148
|
+
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
|
149
|
+
|
|
150
|
+
const { searchParams } = new URL(request.url)
|
|
151
|
+
const page = Number(searchParams.get("page") ?? 1)
|
|
152
|
+
|
|
153
|
+
const users = await prismaPostgres.user.findMany({
|
|
154
|
+
skip: (page - 1) * 20,
|
|
155
|
+
take: 20
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
return NextResponse.json({ users, page })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function POST(request: NextRequest) {
|
|
162
|
+
const body = await request.json()
|
|
163
|
+
// validar con Zod, crear, retornar
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Layouts y metadata
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// app/(dashboard)/layout.tsx
|
|
173
|
+
import { auth } from "@/lib/auth"
|
|
174
|
+
import { redirect } from "next/navigation"
|
|
175
|
+
|
|
176
|
+
export default async function DashboardLayout({
|
|
177
|
+
children
|
|
178
|
+
}: {
|
|
179
|
+
children: React.ReactNode
|
|
180
|
+
}) {
|
|
181
|
+
const session = await auth()
|
|
182
|
+
if (!session) redirect("/login")
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div className="flex min-h-screen">
|
|
186
|
+
<aside>...</aside>
|
|
187
|
+
<main className="flex-1">{children}</main>
|
|
188
|
+
</div>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Metadata dinámica
|
|
193
|
+
export async function generateMetadata({ params }: { params: { id: string } }) {
|
|
194
|
+
const user = await prismaPostgres.user.findUnique({ where: { id: params.id } })
|
|
195
|
+
return { title: user?.name ?? "Usuario" }
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## React 19 — novedades a usar
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// use() — leer promesas y context en render
|
|
205
|
+
import { use } from "react"
|
|
206
|
+
|
|
207
|
+
function UserDetails({ promise }: { promise: Promise<User> }) {
|
|
208
|
+
const user = use(promise) // Suspense-compatible
|
|
209
|
+
return <div>{user.name}</div>
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// useOptimistic — updates optimistas
|
|
213
|
+
import { useOptimistic } from "react"
|
|
214
|
+
|
|
215
|
+
function LikeButton({ post }) {
|
|
216
|
+
const [optimisticLikes, addOptimisticLike] = useOptimistic(
|
|
217
|
+
post.likes,
|
|
218
|
+
(current, increment: number) => current + increment
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
async function handleLike() {
|
|
222
|
+
addOptimisticLike(1)
|
|
223
|
+
await likePost(post.id)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// useActionState — reemplaza useFormState de React DOM
|
|
228
|
+
import { useActionState } from "react"
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Anti-patrones a evitar
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// ❌ No mezclar lógica de servidor en Client Components
|
|
237
|
+
"use client"
|
|
238
|
+
const data = await fetch("/api/users") // Error: await en cliente sin Suspense
|
|
239
|
+
|
|
240
|
+
// ❌ No usar cookies()/headers() fuera de RSC o Route Handlers
|
|
241
|
+
// (en Client Components no están disponibles)
|
|
242
|
+
|
|
243
|
+
// ❌ No importar código de servidor en Client Components
|
|
244
|
+
import { prismaPostgres } from "@/lib/db/postgres" // Expone DB al cliente
|
|
245
|
+
|
|
246
|
+
// ✓ Pasar datos como props desde RSC padre
|
|
247
|
+
export default async function Page() {
|
|
248
|
+
const data = await getData()
|
|
249
|
+
return <ClientComponent initialData={data} />
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Comandos frecuentes
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
pnpm dev # dev con Turbopack
|
|
259
|
+
pnpm build # build de producción
|
|
260
|
+
pnpm lint # ESLint 9
|
|
261
|
+
pnpm type-check # tsc --noEmit
|
|
262
|
+
```
|