start-vibing-stacks 1.6.0 → 1.7.1
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/dist/index.js +50 -1
- package/dist/setup.js +2 -2
- package/package.json +1 -1
- package/stacks/_shared/skills/hook-development/SKILL.md +88 -0
- package/stacks/frontend/react/skills/shadcn-ui/SKILL.md +92 -0
- package/stacks/nodejs/skills/bun-runtime/SKILL.md +86 -0
- package/stacks/nodejs/skills/mongoose-patterns/SKILL.md +77 -0
- package/stacks/nodejs/skills/nextjs-app-router/SKILL.md +123 -0
- package/stacks/nodejs/skills/trpc-api/SKILL.md +87 -0
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Multi-stack AI-powered development workflow for Claude Code.
|
|
6
6
|
* Detects project stack, validates requirements, and configures agents.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { existsSync, readFileSync } from 'fs';
|
|
9
|
+
import { join, basename } from 'path';
|
|
9
10
|
import inquirer from 'inquirer';
|
|
10
11
|
import chalk from 'chalk';
|
|
11
12
|
import * as ui from './ui.js';
|
|
@@ -66,6 +67,54 @@ async function main() {
|
|
|
66
67
|
console.log(ui.LOGO);
|
|
67
68
|
const projectDir = process.cwd();
|
|
68
69
|
const projectName = basename(projectDir);
|
|
70
|
+
// ─── Quick Resume: Skip setup if already configured ─────────────────────
|
|
71
|
+
const activeProjectPath = join(projectDir, '.claude', 'config', 'active-project.json');
|
|
72
|
+
if (existsSync(activeProjectPath) && !FLAGS.force) {
|
|
73
|
+
try {
|
|
74
|
+
const existing = JSON.parse(readFileSync(activeProjectPath, 'utf8'));
|
|
75
|
+
ui.success(`Project already configured: ${existing.name || projectName}`);
|
|
76
|
+
ui.info(`Stack: ${existing.stack} | Framework: ${existing.framework} | DB: ${existing.database}`);
|
|
77
|
+
console.log('');
|
|
78
|
+
const { action } = await inquirer.prompt([
|
|
79
|
+
{
|
|
80
|
+
type: 'list',
|
|
81
|
+
name: 'action',
|
|
82
|
+
message: 'What do you want to do?',
|
|
83
|
+
choices: [
|
|
84
|
+
{ name: '🚀 Launch Claude Code', value: 'launch' },
|
|
85
|
+
{ name: '🔄 Reconfigure project (fresh setup)', value: 'reconfig' },
|
|
86
|
+
{ name: '❌ Exit', value: 'exit' },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
]);
|
|
90
|
+
if (action === 'launch') {
|
|
91
|
+
if (!FLAGS.noClaude) {
|
|
92
|
+
console.log(chalk.dim('\n Launching Claude Code...\n'));
|
|
93
|
+
const { execSync: run } = await import('child_process');
|
|
94
|
+
try {
|
|
95
|
+
run('claude --dangerously-skip-permissions', {
|
|
96
|
+
cwd: projectDir,
|
|
97
|
+
stdio: 'inherit',
|
|
98
|
+
env: { ...process.env },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
ui.info('Claude Code session ended.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
if (action === 'exit') {
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
// action === 'reconfig' → continue with full setup below
|
|
111
|
+
ui.info('Reconfiguring project...');
|
|
112
|
+
console.log('');
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Corrupted config, continue with fresh setup
|
|
116
|
+
}
|
|
117
|
+
}
|
|
69
118
|
// ─── Step 1: Detect Project ─────────────────────────────────────────────
|
|
70
119
|
ui.header('🔍 Scanning project directory...');
|
|
71
120
|
const detection = detectProject(projectDir);
|
package/dist/setup.js
CHANGED
|
@@ -176,7 +176,7 @@ export async function setupProject(projectDir, config, options = {}) {
|
|
|
176
176
|
hooks: [
|
|
177
177
|
{
|
|
178
178
|
type: 'command',
|
|
179
|
-
command: '
|
|
179
|
+
command: 'bash .claude/hooks/run-hook.sh user-prompt-submit',
|
|
180
180
|
timeout: 10,
|
|
181
181
|
},
|
|
182
182
|
],
|
|
@@ -187,7 +187,7 @@ export async function setupProject(projectDir, config, options = {}) {
|
|
|
187
187
|
hooks: [
|
|
188
188
|
{
|
|
189
189
|
type: 'command',
|
|
190
|
-
command: '
|
|
190
|
+
command: 'bash .claude/hooks/run-hook.sh stop-validator',
|
|
191
191
|
timeout: 30,
|
|
192
192
|
},
|
|
193
193
|
],
|
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Hook Development — Claude Code Hooks
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when creating or modifying Claude Code hooks.**
|
|
4
|
+
|
|
5
|
+
## Hook Types
|
|
6
|
+
|
|
7
|
+
| Event | When | Use Case |
|
|
8
|
+
|-------|------|----------|
|
|
9
|
+
| `UserPromptSubmit` | Before prompt is sent | Inject workflow, validate input |
|
|
10
|
+
| `Stop` | Before task completion | Validate state, block if dirty |
|
|
11
|
+
| `PreToolUse` | Before tool execution | Approve/block dangerous tools |
|
|
12
|
+
| `PostToolUse` | After tool execution | Log, validate output |
|
|
13
|
+
|
|
14
|
+
## File Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
.claude/hooks/
|
|
18
|
+
├── run-hook.sh # Entry point (bash → bun/tsx fallback)
|
|
19
|
+
├── user-prompt-submit.ts # Prompt injection
|
|
20
|
+
└── stop-validator.ts # Task completion gate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Hook Input (stdin JSON)
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// UserPromptSubmit
|
|
27
|
+
interface PromptInput {
|
|
28
|
+
user_prompt: string;
|
|
29
|
+
session_id: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Stop
|
|
33
|
+
interface StopInput {
|
|
34
|
+
stop_hook_active?: boolean; // Cycle detection
|
|
35
|
+
transcript?: string;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Hook Output (stdout JSON)
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// UserPromptSubmit — inject system message
|
|
43
|
+
{ "continue": true, "systemMessage": "WORKFLOW: ..." }
|
|
44
|
+
|
|
45
|
+
// Stop — approve or block
|
|
46
|
+
{ "continue": false, "decision": "approve", "reason": "All checks passed" }
|
|
47
|
+
{ "continue": true, "decision": "block", "reason": "Uncommitted files" }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Template: Stop Validator
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
#!/usr/bin/env node
|
|
54
|
+
import { execSync } from 'child_process';
|
|
55
|
+
|
|
56
|
+
function cmd(c: string): string {
|
|
57
|
+
try { return execSync(c, { encoding: 'utf8' }).trim(); } catch { return ''; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const branch = cmd('git rev-parse --abbrev-ref HEAD');
|
|
61
|
+
const dirty = cmd('git status --porcelain');
|
|
62
|
+
|
|
63
|
+
const result = (!dirty && (branch === 'main' || branch === 'master'))
|
|
64
|
+
? { continue: false, decision: 'approve', reason: 'Clean main branch' }
|
|
65
|
+
: { continue: true, decision: 'block', reason: `Branch: ${branch}, dirty: ${!!dirty}` };
|
|
66
|
+
|
|
67
|
+
console.log(JSON.stringify(result));
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## settings.json Registration
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"hooks": {
|
|
75
|
+
"Stop": [{ "hooks": [{ "type": "command", "command": "bash .claude/hooks/run-hook.sh stop-validator", "timeout": 30 }] }],
|
|
76
|
+
"UserPromptSubmit": [{ "matcher": "", "hooks": [{ "type": "command", "command": "bash .claude/hooks/run-hook.sh user-prompt-submit", "timeout": 10 }] }]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Rules
|
|
82
|
+
|
|
83
|
+
1. **Always use `run-hook.sh` as entry** — handles bun/tsx fallback
|
|
84
|
+
2. **Read stdin with timeout** — hooks must not hang
|
|
85
|
+
3. **Exit 0 always** — non-zero kills the session
|
|
86
|
+
4. **Output valid JSON to stdout** — Claude Code parses it
|
|
87
|
+
5. **Cycle detection** — check `stop_hook_active` flag in Stop hooks
|
|
88
|
+
6. **Keep hooks fast** — timeout applies (10s for prompt, 30s for stop)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# shadcn/ui — Component Library Patterns
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when adding or modifying shadcn/ui components.**
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx shadcn@latest init
|
|
9
|
+
npx shadcn@latest add button card dialog input
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Theming
|
|
13
|
+
|
|
14
|
+
```css
|
|
15
|
+
/* globals.css — CSS variables for theming */
|
|
16
|
+
@layer base {
|
|
17
|
+
:root {
|
|
18
|
+
--background: 0 0% 100%;
|
|
19
|
+
--foreground: 222.2 84% 4.9%;
|
|
20
|
+
--primary: 222.2 47.4% 11.2%;
|
|
21
|
+
--primary-foreground: 210 40% 98%;
|
|
22
|
+
--muted: 210 40% 96.1%;
|
|
23
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
24
|
+
--border: 214.3 31.8% 91.4%;
|
|
25
|
+
--ring: 222.2 84% 4.9%;
|
|
26
|
+
--radius: 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
.dark {
|
|
29
|
+
--background: 222.2 84% 4.9%;
|
|
30
|
+
--foreground: 210 40% 98%;
|
|
31
|
+
--primary: 210 40% 98%;
|
|
32
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Component Customization
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// CORRECT — extend via className + variants
|
|
41
|
+
import { Button } from '@/components/ui/button';
|
|
42
|
+
<Button variant="outline" size="lg" className="rounded-full">Custom</Button>
|
|
43
|
+
|
|
44
|
+
// WRONG — modify component source for one-off styles
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Composition Pattern
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
51
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
|
52
|
+
|
|
53
|
+
// Compose primitives, don't create monolithic components
|
|
54
|
+
<Dialog>
|
|
55
|
+
<DialogTrigger asChild>
|
|
56
|
+
<Button variant="outline">Open</Button>
|
|
57
|
+
</DialogTrigger>
|
|
58
|
+
<DialogContent>
|
|
59
|
+
<DialogHeader>
|
|
60
|
+
<DialogTitle>Title</DialogTitle>
|
|
61
|
+
</DialogHeader>
|
|
62
|
+
<Card><CardContent>...</CardContent></Card>
|
|
63
|
+
</DialogContent>
|
|
64
|
+
</Dialog>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Form Pattern (with Zod)
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
|
71
|
+
import { useForm } from 'react-hook-form';
|
|
72
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
73
|
+
|
|
74
|
+
const form = useForm<z.infer<typeof schema>>({ resolver: zodResolver(schema) });
|
|
75
|
+
|
|
76
|
+
<Form {...form}>
|
|
77
|
+
<FormField control={form.control} name="email" render={({ field }) => (
|
|
78
|
+
<FormItem>
|
|
79
|
+
<FormLabel>Email</FormLabel>
|
|
80
|
+
<FormControl><Input {...field} /></FormControl>
|
|
81
|
+
<FormMessage />
|
|
82
|
+
</FormItem>
|
|
83
|
+
)} />
|
|
84
|
+
</Form>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## FORBIDDEN
|
|
88
|
+
|
|
89
|
+
1. **Modifying component source for one-off needs** — use className/variants
|
|
90
|
+
2. **Installing without init** — always `npx shadcn@latest init` first
|
|
91
|
+
3. **Ignoring dark mode** — all components must work in both themes
|
|
92
|
+
4. **Skipping accessibility** — shadcn components have built-in a11y, don't break it
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Bun Runtime — Fast JavaScript Runtime
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when using Bun for scripts, packages, bundling, or testing.**
|
|
4
|
+
|
|
5
|
+
## Package Management
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install # Install deps (replaces npm install)
|
|
9
|
+
bun add zod # Add dependency
|
|
10
|
+
bun add -D vitest # Add dev dependency
|
|
11
|
+
bun remove lodash # Remove
|
|
12
|
+
bun update # Update all
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Scripts
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun run dev # Run script from package.json
|
|
19
|
+
bun run build
|
|
20
|
+
bun --watch src/index.ts # Watch mode
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## TypeScript (native, no config needed)
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// Bun runs .ts files directly — no tsc/tsx needed
|
|
27
|
+
// bun src/index.ts
|
|
28
|
+
|
|
29
|
+
import { serve } from 'bun';
|
|
30
|
+
|
|
31
|
+
serve({
|
|
32
|
+
port: 3000,
|
|
33
|
+
fetch(req) {
|
|
34
|
+
const url = new URL(req.url);
|
|
35
|
+
if (url.pathname === '/api/health') {
|
|
36
|
+
return Response.json({ status: 'ok' });
|
|
37
|
+
}
|
|
38
|
+
return new Response('Not Found', { status: 404 });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## File I/O (Bun APIs)
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Fast file operations
|
|
47
|
+
const content = await Bun.file('data.json').text();
|
|
48
|
+
const parsed = await Bun.file('data.json').json();
|
|
49
|
+
await Bun.write('output.txt', 'Hello');
|
|
50
|
+
|
|
51
|
+
// Glob
|
|
52
|
+
const glob = new Bun.Glob('**/*.ts');
|
|
53
|
+
for await (const file of glob.scan('.')) { console.log(file); }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Testing (built-in)
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// *.test.ts — bun test
|
|
60
|
+
import { describe, it, expect } from 'bun:test';
|
|
61
|
+
|
|
62
|
+
describe('math', () => {
|
|
63
|
+
it('adds', () => expect(1 + 1).toBe(2));
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
bun test # Run all tests
|
|
69
|
+
bun test --coverage # With coverage
|
|
70
|
+
bun test --watch # Watch mode
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Environment Variables
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// .env loaded automatically
|
|
77
|
+
const apiKey = Bun.env['API_KEY']; // Bun.env (recommended)
|
|
78
|
+
const dbUrl = process.env['DATABASE_URL']; // Also works
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## FORBIDDEN
|
|
82
|
+
|
|
83
|
+
1. **`node` command when `bun` works** — prefer bun for speed
|
|
84
|
+
2. **`npx` when `bunx` works** — `bunx` is faster
|
|
85
|
+
3. **Manual .env loading** — Bun loads `.env` automatically
|
|
86
|
+
4. **CommonJS `require()`** — use ESM `import`
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Mongoose Patterns — MongoDB ODM
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing Mongoose schemas, queries, or aggregations.**
|
|
4
|
+
|
|
5
|
+
## Schema Pattern
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { Schema, model, type InferSchemaType } from 'mongoose';
|
|
9
|
+
|
|
10
|
+
const userSchema = new Schema({
|
|
11
|
+
name: { type: String, required: true, trim: true, minlength: 2 },
|
|
12
|
+
email: { type: String, required: true, unique: true, lowercase: true, index: true },
|
|
13
|
+
role: { type: String, enum: ['admin', 'user', 'moderator'] as const, default: 'user' },
|
|
14
|
+
profile: {
|
|
15
|
+
avatar: String,
|
|
16
|
+
bio: { type: String, maxlength: 500 },
|
|
17
|
+
},
|
|
18
|
+
tags: [{ type: String, index: true }],
|
|
19
|
+
isActive: { type: Boolean, default: true, index: true },
|
|
20
|
+
}, {
|
|
21
|
+
timestamps: true, // createdAt, updatedAt
|
|
22
|
+
toJSON: { virtuals: true, transform: (_, ret) => { delete ret.__v; return ret; } },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Compound index
|
|
26
|
+
userSchema.index({ email: 1, isActive: 1 });
|
|
27
|
+
// Text index for search
|
|
28
|
+
userSchema.index({ name: 'text', 'profile.bio': 'text' });
|
|
29
|
+
|
|
30
|
+
type IUser = InferSchemaType<typeof userSchema>;
|
|
31
|
+
export const User = model('User', userSchema);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Query Patterns
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Pagination
|
|
38
|
+
async function paginate(page: number, limit: number) {
|
|
39
|
+
const [items, total] = await Promise.all([
|
|
40
|
+
User.find({ isActive: true }).skip((page - 1) * limit).limit(limit).lean(),
|
|
41
|
+
User.countDocuments({ isActive: true }),
|
|
42
|
+
]);
|
|
43
|
+
return { items, total, pages: Math.ceil(total / limit) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Aggregation
|
|
47
|
+
const stats = await User.aggregate([
|
|
48
|
+
{ $match: { isActive: true } },
|
|
49
|
+
{ $group: { _id: '$role', count: { $sum: 1 }, avgAge: { $avg: '$age' } } },
|
|
50
|
+
{ $sort: { count: -1 } },
|
|
51
|
+
]);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Middleware
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// Pre-save: hash password
|
|
58
|
+
userSchema.pre('save', async function (next) {
|
|
59
|
+
if (!this.isModified('password')) return next();
|
|
60
|
+
this.password = await bcrypt.hash(this.password, 12);
|
|
61
|
+
next();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Pre-find: exclude inactive by default
|
|
65
|
+
userSchema.pre(/^find/, function (next) {
|
|
66
|
+
this.where({ isActive: { $ne: false } });
|
|
67
|
+
next();
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## FORBIDDEN
|
|
72
|
+
|
|
73
|
+
1. **No indexes on queried fields** — always index filter/sort fields
|
|
74
|
+
2. **`find()` without `.lean()`** for read-only — wastes memory
|
|
75
|
+
3. **Unbounded queries** — always `.limit()`
|
|
76
|
+
4. **N+1 queries** — use `.populate()` or aggregation `$lookup`
|
|
77
|
+
5. **String IDs without casting** — use `new Types.ObjectId(id)`
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Next.js App Router — Modern Patterns
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing Next.js pages, layouts, or server components.**
|
|
4
|
+
|
|
5
|
+
## File Conventions
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
app/
|
|
9
|
+
├── layout.tsx # Root layout (required)
|
|
10
|
+
├── page.tsx # Home page
|
|
11
|
+
├── loading.tsx # Loading UI (Suspense boundary)
|
|
12
|
+
├── error.tsx # Error boundary ('use client')
|
|
13
|
+
├── not-found.tsx # 404 page
|
|
14
|
+
├── (auth)/ # Route group (no URL segment)
|
|
15
|
+
│ ├── login/page.tsx
|
|
16
|
+
│ └── register/page.tsx
|
|
17
|
+
├── dashboard/
|
|
18
|
+
│ ├── layout.tsx # Nested layout
|
|
19
|
+
│ ├── page.tsx
|
|
20
|
+
│ └── [id]/page.tsx # Dynamic route
|
|
21
|
+
└── api/
|
|
22
|
+
└── route.ts # API route handler
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Server vs Client Components
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// DEFAULT: Server Component (no directive needed)
|
|
29
|
+
async function UserList() {
|
|
30
|
+
const users = await db.user.findMany(); // Direct DB access
|
|
31
|
+
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// CLIENT: Only when needed (interactivity, hooks, browser APIs)
|
|
35
|
+
'use client';
|
|
36
|
+
function Counter() {
|
|
37
|
+
const [count, setCount] = useState(0);
|
|
38
|
+
return <button onClick={() => setCount(c + 1)}>{count}</button>;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Data Fetching
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// Server Component — fetch with caching
|
|
46
|
+
async function Page() {
|
|
47
|
+
const data = await fetch('https://api.example.com/data', {
|
|
48
|
+
next: { revalidate: 3600 }, // ISR: revalidate every hour
|
|
49
|
+
});
|
|
50
|
+
return <div>{data}</div>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Dynamic data (no cache)
|
|
54
|
+
async function Page() {
|
|
55
|
+
const data = await fetch('https://api.example.com/data', {
|
|
56
|
+
cache: 'no-store',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Server Actions
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// app/actions.ts
|
|
65
|
+
'use server';
|
|
66
|
+
|
|
67
|
+
import { revalidatePath } from 'next/cache';
|
|
68
|
+
|
|
69
|
+
export async function createUser(formData: FormData) {
|
|
70
|
+
const name = formData.get('name') as string;
|
|
71
|
+
await db.user.create({ data: { name } });
|
|
72
|
+
revalidatePath('/users');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// In component
|
|
76
|
+
<form action={createUser}>
|
|
77
|
+
<input name="name" />
|
|
78
|
+
<button type="submit">Create</button>
|
|
79
|
+
</form>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Route Handlers (API)
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// app/api/users/route.ts
|
|
86
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
87
|
+
|
|
88
|
+
export async function GET(request: NextRequest) {
|
|
89
|
+
const { searchParams } = new URL(request.url);
|
|
90
|
+
const page = Number(searchParams.get('page') ?? '1');
|
|
91
|
+
const users = await db.user.findMany({ skip: (page - 1) * 20, take: 20 });
|
|
92
|
+
return NextResponse.json(users);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function POST(request: NextRequest) {
|
|
96
|
+
const body = await request.json();
|
|
97
|
+
const result = schema.safeParse(body);
|
|
98
|
+
if (!result.success) return NextResponse.json(result.error, { status: 400 });
|
|
99
|
+
const user = await db.user.create({ data: result.data });
|
|
100
|
+
return NextResponse.json(user, { status: 201 });
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Metadata
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
// Static
|
|
108
|
+
export const metadata = { title: 'Dashboard', description: 'User dashboard' };
|
|
109
|
+
|
|
110
|
+
// Dynamic
|
|
111
|
+
export async function generateMetadata({ params }: { params: { id: string } }) {
|
|
112
|
+
const user = await getUser(params.id);
|
|
113
|
+
return { title: user.name };
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## FORBIDDEN
|
|
118
|
+
|
|
119
|
+
1. **`'use client'` on server-capable components** — default to server
|
|
120
|
+
2. **Fetching in client when server fetch works** — use server components
|
|
121
|
+
3. **`getServerSideProps` / `getStaticProps`** — App Router uses async components
|
|
122
|
+
4. **API routes for server-only data** — use server components directly
|
|
123
|
+
5. **Prop drilling through layouts** — use parallel routes or context
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# tRPC API — End-to-End Type Safety
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when building type-safe API routes with tRPC.**
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
server/
|
|
9
|
+
├── trpc.ts # tRPC init + context
|
|
10
|
+
├── routers/
|
|
11
|
+
│ ├── _app.ts # Root router (merges all)
|
|
12
|
+
│ ├── user.router.ts
|
|
13
|
+
│ └── post.router.ts
|
|
14
|
+
└── middleware/
|
|
15
|
+
└── auth.ts # Auth middleware
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// server/trpc.ts
|
|
22
|
+
import { initTRPC, TRPCError } from '@trpc/server';
|
|
23
|
+
import superjson from 'superjson';
|
|
24
|
+
|
|
25
|
+
const t = initTRPC.context<Context>().create({ transformer: superjson });
|
|
26
|
+
|
|
27
|
+
export const router = t.router;
|
|
28
|
+
export const publicProcedure = t.procedure;
|
|
29
|
+
|
|
30
|
+
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
|
|
31
|
+
if (!ctx.session?.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
32
|
+
return next({ ctx: { ...ctx, user: ctx.session.user } });
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Router Pattern
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// server/routers/user.router.ts
|
|
40
|
+
import { z } from 'zod';
|
|
41
|
+
import { router, protectedProcedure } from '../trpc';
|
|
42
|
+
|
|
43
|
+
export const userRouter = router({
|
|
44
|
+
me: protectedProcedure.query(async ({ ctx }) => {
|
|
45
|
+
return ctx.db.user.findUnique({ where: { id: ctx.user.id } });
|
|
46
|
+
}),
|
|
47
|
+
|
|
48
|
+
update: protectedProcedure
|
|
49
|
+
.input(z.object({ name: z.string().min(2) }))
|
|
50
|
+
.mutation(async ({ ctx, input }) => {
|
|
51
|
+
return ctx.db.user.update({
|
|
52
|
+
where: { id: ctx.user.id },
|
|
53
|
+
data: input,
|
|
54
|
+
});
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
list: protectedProcedure
|
|
58
|
+
.input(z.object({ page: z.number().min(1).default(1), limit: z.number().max(100).default(20) }))
|
|
59
|
+
.query(async ({ ctx, input }) => {
|
|
60
|
+
const { page, limit } = input;
|
|
61
|
+
return ctx.db.user.findMany({ skip: (page - 1) * limit, take: limit });
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Client Usage (React)
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { trpc } from '@/utils/trpc';
|
|
70
|
+
|
|
71
|
+
function Profile() {
|
|
72
|
+
const { data: user, isLoading } = trpc.user.me.useQuery();
|
|
73
|
+
const updateMutation = trpc.user.update.useMutation({
|
|
74
|
+
onSuccess: () => utils.user.me.invalidate(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (isLoading) return <Skeleton />;
|
|
78
|
+
return <div>{user?.name}</div>;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## FORBIDDEN
|
|
83
|
+
|
|
84
|
+
1. **REST endpoints when tRPC covers it** — use tRPC procedures
|
|
85
|
+
2. **Unvalidated input** — always use Zod `.input()`
|
|
86
|
+
3. **Public procedures for auth-required data** — use `protectedProcedure`
|
|
87
|
+
4. **Direct DB access in client** — always through tRPC procedures
|