@codihaus/claude-skills 1.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/README.md +167 -0
- package/bin/cli.js +58 -0
- package/package.json +46 -0
- package/skills/_quality-attributes.md +392 -0
- package/skills/_registry.md +189 -0
- package/skills/debrief/SKILL.md +647 -0
- package/skills/debrief/references/change-request-template.md +124 -0
- package/skills/debrief/references/file-patterns.md +173 -0
- package/skills/debrief/references/group-codes.md +72 -0
- package/skills/debrief/references/research-queries.md +106 -0
- package/skills/debrief/references/use-case-template.md +141 -0
- package/skills/debrief/scripts/generate_questionnaire.py +195 -0
- package/skills/dev-arch/SKILL.md +747 -0
- package/skills/dev-changelog/SKILL.md +378 -0
- package/skills/dev-coding/SKILL.md +470 -0
- package/skills/dev-coding-backend/SKILL.md +361 -0
- package/skills/dev-coding-frontend/SKILL.md +534 -0
- package/skills/dev-coding-frontend/references/nextjs.md +477 -0
- package/skills/dev-review/SKILL.md +548 -0
- package/skills/dev-scout/SKILL.md +723 -0
- package/skills/dev-scout/references/feature-patterns.md +210 -0
- package/skills/dev-scout/references/file-patterns.md +252 -0
- package/skills/dev-scout/references/tech-detection.md +211 -0
- package/skills/dev-scout/scripts/scout-analyze.sh +280 -0
- package/skills/dev-specs/SKILL.md +577 -0
- package/skills/dev-specs/references/checklist.md +176 -0
- package/skills/dev-specs/references/spec-templates.md +460 -0
- package/skills/dev-test/SKILL.md +364 -0
- package/skills/utils/diagram/SKILL.md +205 -0
- package/skills/utils/diagram/references/common-errors.md +305 -0
- package/skills/utils/diagram/references/diagram-types.md +636 -0
- package/skills/utils/docs-graph/SKILL.md +204 -0
- package/skills/utils/gemini/SKILL.md +292 -0
- package/skills/utils/gemini/scripts/gemini-scan.py +340 -0
- package/skills/utils/gemini/scripts/setup.sh +169 -0
- package/src/commands/add.js +64 -0
- package/src/commands/doctor.js +179 -0
- package/src/commands/init.js +251 -0
- package/src/commands/list.js +88 -0
- package/src/commands/remove.js +60 -0
- package/src/commands/update.js +72 -0
- package/src/index.js +26 -0
- package/src/utils/config.js +272 -0
- package/src/utils/deps.js +599 -0
- package/src/utils/skills.js +253 -0
- package/templates/CLAUDE.md.template +58 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# Next.js Best Practices
|
|
2
|
+
|
|
3
|
+
Performance optimization patterns for Next.js App Router applications.
|
|
4
|
+
|
|
5
|
+
## Rule Categories by Priority
|
|
6
|
+
|
|
7
|
+
| Priority | Category | Impact |
|
|
8
|
+
|----------|----------|--------|
|
|
9
|
+
| 1 | Eliminating Waterfalls | CRITICAL |
|
|
10
|
+
| 2 | Bundle Optimization | CRITICAL |
|
|
11
|
+
| 3 | Server Components | HIGH |
|
|
12
|
+
| 4 | Client Components | MEDIUM-HIGH |
|
|
13
|
+
| 5 | Re-render Optimization | MEDIUM |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Eliminating Waterfalls (CRITICAL)
|
|
18
|
+
|
|
19
|
+
### Parallel Data Fetching
|
|
20
|
+
|
|
21
|
+
**Bad (sequential, 3 round trips):**
|
|
22
|
+
```typescript
|
|
23
|
+
const user = await fetchUser()
|
|
24
|
+
const posts = await fetchPosts()
|
|
25
|
+
const comments = await fetchComments()
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Good (parallel, 1 round trip):**
|
|
29
|
+
```typescript
|
|
30
|
+
const [user, posts, comments] = await Promise.all([
|
|
31
|
+
fetchUser(),
|
|
32
|
+
fetchPosts(),
|
|
33
|
+
fetchComments()
|
|
34
|
+
])
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Move Await Into Branches
|
|
38
|
+
|
|
39
|
+
**Bad (always waits):**
|
|
40
|
+
```typescript
|
|
41
|
+
async function getUser(id: string, options?: { includeProfile: boolean }) {
|
|
42
|
+
const user = await db.user.findUnique({ where: { id } })
|
|
43
|
+
const profile = await db.profile.findUnique({ where: { userId: id } })
|
|
44
|
+
|
|
45
|
+
if (options?.includeProfile) {
|
|
46
|
+
return { ...user, profile }
|
|
47
|
+
}
|
|
48
|
+
return user
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Good (only waits when needed):**
|
|
53
|
+
```typescript
|
|
54
|
+
async function getUser(id: string, options?: { includeProfile: boolean }) {
|
|
55
|
+
const user = await db.user.findUnique({ where: { id } })
|
|
56
|
+
|
|
57
|
+
if (options?.includeProfile) {
|
|
58
|
+
const profile = await db.profile.findUnique({ where: { userId: id } })
|
|
59
|
+
return { ...user, profile }
|
|
60
|
+
}
|
|
61
|
+
return user
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Use Suspense Boundaries
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
// Stream content as it loads
|
|
69
|
+
export default function Page() {
|
|
70
|
+
return (
|
|
71
|
+
<div>
|
|
72
|
+
<Header />
|
|
73
|
+
<Suspense fallback={<PostsSkeleton />}>
|
|
74
|
+
<Posts />
|
|
75
|
+
</Suspense>
|
|
76
|
+
<Suspense fallback={<CommentsSkeleton />}>
|
|
77
|
+
<Comments />
|
|
78
|
+
</Suspense>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 2. Bundle Optimization (CRITICAL)
|
|
87
|
+
|
|
88
|
+
### Avoid Barrel File Imports
|
|
89
|
+
|
|
90
|
+
Barrel files (index.js re-exports) load thousands of unused modules.
|
|
91
|
+
|
|
92
|
+
**Bad (imports entire library):**
|
|
93
|
+
```tsx
|
|
94
|
+
import { Check, X, Menu } from 'lucide-react'
|
|
95
|
+
// Loads 1,583 modules, 200-800ms runtime cost
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Good (direct imports):**
|
|
99
|
+
```tsx
|
|
100
|
+
import Check from 'lucide-react/dist/esm/icons/check'
|
|
101
|
+
import X from 'lucide-react/dist/esm/icons/x'
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Alternative (Next.js 13.5+):**
|
|
105
|
+
```js
|
|
106
|
+
// next.config.js
|
|
107
|
+
module.exports = {
|
|
108
|
+
experimental: {
|
|
109
|
+
optimizePackageImports: ['lucide-react', '@mui/material']
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Dynamic Imports for Heavy Components
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import dynamic from 'next/dynamic'
|
|
118
|
+
|
|
119
|
+
// Only load when needed
|
|
120
|
+
const HeavyChart = dynamic(() => import('@/components/Chart'), {
|
|
121
|
+
loading: () => <ChartSkeleton />,
|
|
122
|
+
ssr: false // If client-only
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Defer Third-Party Scripts
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
// Load after hydration
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
import('analytics').then(({ init }) => init())
|
|
132
|
+
}, [])
|
|
133
|
+
|
|
134
|
+
// Or use next/script
|
|
135
|
+
<Script src="analytics.js" strategy="afterInteractive" />
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 3. Server Components (HIGH)
|
|
141
|
+
|
|
142
|
+
### Default to Server Components
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// app/posts/page.tsx - Server Component (default)
|
|
146
|
+
export default async function PostsPage() {
|
|
147
|
+
const posts = await getPosts() // Direct DB access
|
|
148
|
+
return <PostList posts={posts} />
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Use React.cache() for Deduplication
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { cache } from 'react'
|
|
156
|
+
|
|
157
|
+
export const getCurrentUser = cache(async () => {
|
|
158
|
+
const session = await auth()
|
|
159
|
+
if (!session?.user?.id) return null
|
|
160
|
+
return await db.user.findUnique({
|
|
161
|
+
where: { id: session.user.id }
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Multiple components can call getCurrentUser()
|
|
166
|
+
// Only runs once per request
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Important:** Use primitives as arguments, not objects:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// Bad - always cache miss (new object each call)
|
|
173
|
+
const getUser = cache(async (params: { uid: number }) => {...})
|
|
174
|
+
|
|
175
|
+
// Good - cache hit on same value
|
|
176
|
+
const getUser = cache(async (uid: number) => {...})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Minimize Client Component Data
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
// Bad - sends entire user object to client
|
|
183
|
+
<ClientComponent user={user} />
|
|
184
|
+
|
|
185
|
+
// Good - send only needed fields
|
|
186
|
+
<ClientComponent userName={user.name} userAvatar={user.avatar} />
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Parallel Server Fetching
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
// Bad - sequential in parent
|
|
193
|
+
async function Page() {
|
|
194
|
+
const user = await getUser()
|
|
195
|
+
const posts = await getPosts()
|
|
196
|
+
return <Content user={user} posts={posts} />
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Good - parallel in children
|
|
200
|
+
async function Page() {
|
|
201
|
+
return (
|
|
202
|
+
<>
|
|
203
|
+
<UserSection /> {/* fetches user */}
|
|
204
|
+
<PostsSection /> {/* fetches posts in parallel */}
|
|
205
|
+
</>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 4. Client Components (MEDIUM-HIGH)
|
|
213
|
+
|
|
214
|
+
### Mark Client Components Explicitly
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
'use client'
|
|
218
|
+
|
|
219
|
+
import { useState } from 'react'
|
|
220
|
+
|
|
221
|
+
export function Counter() {
|
|
222
|
+
const [count, setCount] = useState(0)
|
|
223
|
+
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Use SWR for Client Data Fetching
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
'use client'
|
|
231
|
+
|
|
232
|
+
import useSWR from 'swr'
|
|
233
|
+
|
|
234
|
+
export function UserProfile({ userId }: { userId: string }) {
|
|
235
|
+
const { data, error, isLoading } = useSWR(
|
|
236
|
+
`/api/users/${userId}`,
|
|
237
|
+
fetcher
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if (isLoading) return <Skeleton />
|
|
241
|
+
if (error) return <Error message={error.message} />
|
|
242
|
+
return <Profile user={data} />
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Deduplicate Event Listeners
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
// Bad - each component adds listener
|
|
250
|
+
function Component() {
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
window.addEventListener('resize', handleResize)
|
|
253
|
+
return () => window.removeEventListener('resize', handleResize)
|
|
254
|
+
}, [])
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Good - shared hook with single listener
|
|
258
|
+
const useWindowSize = create(() => {
|
|
259
|
+
// Single listener shared across all components
|
|
260
|
+
})
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 5. Re-render Optimization (MEDIUM)
|
|
266
|
+
|
|
267
|
+
### Don't Subscribe to Unused State
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
// Bad - re-renders on every position change
|
|
271
|
+
function ClickHandler() {
|
|
272
|
+
const position = useMousePosition()
|
|
273
|
+
return <button onClick={() => console.log(position)}>Log</button>
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Good - no subscription, read on demand
|
|
277
|
+
function ClickHandler() {
|
|
278
|
+
const getPosition = useMousePositionRef()
|
|
279
|
+
return <button onClick={() => console.log(getPosition())}>Log</button>
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Use Primitive Dependencies
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
// Bad - effect runs on every render (new object)
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
fetch(`/api/user/${user.id}`)
|
|
289
|
+
}, [user])
|
|
290
|
+
|
|
291
|
+
// Good - effect runs only when id changes
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
fetch(`/api/user/${userId}`)
|
|
294
|
+
}, [userId])
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Derived State as Booleans
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
// Bad - re-renders on any cart change
|
|
301
|
+
function CheckoutButton() {
|
|
302
|
+
const cart = useCart()
|
|
303
|
+
if (cart.items.length === 0) return null
|
|
304
|
+
return <Button>Checkout</Button>
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Good - only re-renders when emptiness changes
|
|
308
|
+
function CheckoutButton() {
|
|
309
|
+
const hasItems = useCartHasItems()
|
|
310
|
+
if (!hasItems) return null
|
|
311
|
+
return <Button>Checkout</Button>
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Functional setState for Stable Callbacks
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
// Bad - new callback each render
|
|
319
|
+
<button onClick={() => setCount(count + 1)}>+1</button>
|
|
320
|
+
|
|
321
|
+
// Good - stable callback
|
|
322
|
+
<button onClick={() => setCount(c => c + 1)}>+1</button>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Lazy State Initialization
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
// Bad - runs expensive function every render
|
|
329
|
+
const [data] = useState(expensiveComputation())
|
|
330
|
+
|
|
331
|
+
// Good - only runs once
|
|
332
|
+
const [data] = useState(() => expensiveComputation())
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## 6. Rendering Patterns
|
|
338
|
+
|
|
339
|
+
### Conditional Rendering
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
// Bad - can render 0
|
|
343
|
+
{items.length && <List items={items} />}
|
|
344
|
+
|
|
345
|
+
// Good - explicit boolean
|
|
346
|
+
{items.length > 0 && <List items={items} />}
|
|
347
|
+
|
|
348
|
+
// Better - ternary
|
|
349
|
+
{items.length > 0 ? <List items={items} /> : null}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Hoist Static JSX
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
// Bad - creates new object each render
|
|
356
|
+
function Component() {
|
|
357
|
+
const icon = <Icon />
|
|
358
|
+
return <div>{icon}</div>
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Good - stable reference
|
|
362
|
+
const icon = <Icon />
|
|
363
|
+
function Component() {
|
|
364
|
+
return <div>{icon}</div>
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Content Visibility for Long Lists
|
|
369
|
+
|
|
370
|
+
```css
|
|
371
|
+
.list-item {
|
|
372
|
+
content-visibility: auto;
|
|
373
|
+
contain-intrinsic-size: 0 50px;
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## 7. Next.js Specific
|
|
380
|
+
|
|
381
|
+
### Route Handlers
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// app/api/users/route.ts
|
|
385
|
+
export async function GET() {
|
|
386
|
+
const users = await getUsers()
|
|
387
|
+
return Response.json(users)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function POST(request: Request) {
|
|
391
|
+
const body = await request.json()
|
|
392
|
+
const user = await createUser(body)
|
|
393
|
+
return Response.json(user, { status: 201 })
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Metadata
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// Static
|
|
401
|
+
export const metadata = {
|
|
402
|
+
title: 'My Page',
|
|
403
|
+
description: 'Page description'
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Dynamic
|
|
407
|
+
export async function generateMetadata({ params }) {
|
|
408
|
+
const post = await getPost(params.id)
|
|
409
|
+
return { title: post.title }
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Loading & Error States
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
// app/posts/loading.tsx
|
|
417
|
+
export default function Loading() {
|
|
418
|
+
return <PostsSkeleton />
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// app/posts/error.tsx
|
|
422
|
+
'use client'
|
|
423
|
+
export default function Error({ error, reset }) {
|
|
424
|
+
return (
|
|
425
|
+
<div>
|
|
426
|
+
<p>Something went wrong</p>
|
|
427
|
+
<button onClick={reset}>Try again</button>
|
|
428
|
+
</div>
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Image Optimization
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
import Image from 'next/image'
|
|
437
|
+
|
|
438
|
+
<Image
|
|
439
|
+
src="/hero.jpg"
|
|
440
|
+
alt="Hero"
|
|
441
|
+
width={1200}
|
|
442
|
+
height={600}
|
|
443
|
+
priority // For above-fold images
|
|
444
|
+
/>
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Link Prefetching
|
|
448
|
+
|
|
449
|
+
```tsx
|
|
450
|
+
import Link from 'next/link'
|
|
451
|
+
|
|
452
|
+
<Link href="/about" prefetch={true}>About</Link>
|
|
453
|
+
|
|
454
|
+
// Prefetch on hover for dynamic routes
|
|
455
|
+
<Link href={`/posts/${id}`} prefetch={false}>
|
|
456
|
+
Post
|
|
457
|
+
</Link>
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Common Gotchas
|
|
463
|
+
|
|
464
|
+
1. **Server vs Client confusion** - Default is Server, add 'use client' only when needed
|
|
465
|
+
2. **Fetching in useEffect** - Prefer Server Components or SWR
|
|
466
|
+
3. **Barrel imports** - Use optimizePackageImports or direct imports
|
|
467
|
+
4. **Object dependencies** - Use primitives in useEffect/useMemo deps
|
|
468
|
+
5. **Missing Suspense** - Wrap async components for streaming
|
|
469
|
+
6. **Hydration mismatch** - Ensure server/client render same content
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## References
|
|
474
|
+
|
|
475
|
+
- [Next.js App Router Docs](https://nextjs.org/docs/app)
|
|
476
|
+
- [React Server Components](https://react.dev/reference/react/use-server)
|
|
477
|
+
- [Vercel React Best Practices](https://github.com/vercel/react-best-practices)
|