start-vibing-stacks 1.5.1 → 1.7.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/dist/setup.js +2 -2
- package/package.json +1 -1
- package/stacks/_shared/skills/hook-development/SKILL.md +88 -0
- package/stacks/_shared/skills/playwright-automation/SKILL.md +90 -0
- package/stacks/_shared/skills/test-coverage/SKILL.md +67 -0
- package/stacks/frontend/react/skills/react-patterns/SKILL.md +113 -0
- package/stacks/frontend/react/skills/shadcn-ui/SKILL.md +92 -0
- package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +94 -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/stacks/nodejs/skills/typescript-strict/SKILL.md +90 -0
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,90 @@
|
|
|
1
|
+
# Playwright Automation — E2E Testing
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing E2E tests or browser automation.**
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
tests/e2e/
|
|
9
|
+
├── fixtures/ # Auth, DB cleanup, custom fixtures
|
|
10
|
+
├── pages/ # Page Object Model
|
|
11
|
+
├── flows/ # User journey tests
|
|
12
|
+
├── api/ # API-only tests
|
|
13
|
+
└── playwright.config.ts
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Page Object Model
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// tests/e2e/pages/base.page.ts
|
|
20
|
+
import { type Page, type Locator, expect } from '@playwright/test';
|
|
21
|
+
|
|
22
|
+
export abstract class BasePage {
|
|
23
|
+
protected readonly page: Page;
|
|
24
|
+
constructor(page: Page) { this.page = page; }
|
|
25
|
+
|
|
26
|
+
get loadingSpinner(): Locator { return this.page.getByTestId('loading-spinner'); }
|
|
27
|
+
get errorMessage(): Locator { return this.page.getByTestId('error-message'); }
|
|
28
|
+
|
|
29
|
+
async waitForLoad(): Promise<void> {
|
|
30
|
+
await this.loadingSpinner.waitFor({ state: 'hidden' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async goto(path: string): Promise<void> {
|
|
34
|
+
await this.page.goto(path);
|
|
35
|
+
await this.waitForLoad();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Auth Fixture
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
export function generateTestUser(): TestUser {
|
|
44
|
+
const ts = Date.now();
|
|
45
|
+
const rand = Math.random().toString(36).substring(7);
|
|
46
|
+
return {
|
|
47
|
+
name: `Test User ${ts}`,
|
|
48
|
+
email: `testuser_${ts}_${rand}@test.com`,
|
|
49
|
+
password: 'TestPassword123!',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Multi-Viewport Testing
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const viewports = [
|
|
58
|
+
{ name: 'mobile', width: 375, height: 667 },
|
|
59
|
+
{ name: 'tablet', width: 768, height: 1024 },
|
|
60
|
+
{ name: 'desktop', width: 1280, height: 800 },
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
for (const viewport of viewports) {
|
|
64
|
+
test.describe(`Responsive - ${viewport.name}`, () => {
|
|
65
|
+
test.use({ viewport: { width: viewport.width, height: viewport.height } });
|
|
66
|
+
test('navigation adapts', async ({ page }) => { /* ... */ });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Required data-testid
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<input data-testid="email-input" />
|
|
75
|
+
<input data-testid="password-input" />
|
|
76
|
+
<button data-testid="submit-button" />
|
|
77
|
+
<div data-testid="error-message" />
|
|
78
|
+
<div data-testid="success-message" />
|
|
79
|
+
<div data-testid="loading-spinner" />
|
|
80
|
+
<nav data-testid="sidebar" />
|
|
81
|
+
<button data-testid="hamburger-menu" />
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## FORBIDDEN
|
|
85
|
+
|
|
86
|
+
1. **Hardcoded test data** — generate unique data with timestamps
|
|
87
|
+
2. **`.skip()` or `.only()`** — never in committed code
|
|
88
|
+
3. **No cleanup** — always track + clean created data
|
|
89
|
+
4. **Mocked auth** — use real authentication flows
|
|
90
|
+
5. **Single viewport** — test mobile + tablet + desktop
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Test Coverage — Testing Management
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke AFTER implementing any feature. Do NOT skip.**
|
|
4
|
+
|
|
5
|
+
## Critical Rules
|
|
6
|
+
|
|
7
|
+
1. **CLEANUP ALL TEST DATA** — fixture-based tracking
|
|
8
|
+
2. **VERIFY IN DATABASE** — check DB state after UI actions
|
|
9
|
+
3. **TEST ALL VIEWPORTS** — desktop, tablet, mobile minimum
|
|
10
|
+
4. **REAL AUTH ONLY** — never mock authentication
|
|
11
|
+
5. **UNIQUE DATA** — timestamps in emails/names
|
|
12
|
+
6. **NO `.skip()`** — never in committed code
|
|
13
|
+
|
|
14
|
+
## Files That NEED Tests
|
|
15
|
+
|
|
16
|
+
| Type | Test Expected | Required |
|
|
17
|
+
|------|--------------|----------|
|
|
18
|
+
| API Route | Unit + E2E | **YES** |
|
|
19
|
+
| Model/Entity | Unit | **YES** |
|
|
20
|
+
| Page/View | E2E flow | **YES** |
|
|
21
|
+
| Component (interactive) | E2E | YES |
|
|
22
|
+
| Hook/Service | Unit | YES |
|
|
23
|
+
| Utility (exported) | Unit | YES |
|
|
24
|
+
|
|
25
|
+
## Required E2E Flows
|
|
26
|
+
|
|
27
|
+
- [ ] Registration — create user, verify in DB
|
|
28
|
+
- [ ] Login/Logout — auth state changes
|
|
29
|
+
- [ ] CRUD Create — item created, visible, in DB
|
|
30
|
+
- [ ] CRUD Read — item displayed correctly
|
|
31
|
+
- [ ] CRUD Update — changes reflected in DB
|
|
32
|
+
- [ ] CRUD Delete — removed from DB
|
|
33
|
+
- [ ] Permissions — forbidden requests blocked
|
|
34
|
+
- [ ] Responsive — works on all viewports
|
|
35
|
+
|
|
36
|
+
## Stack-Specific Commands
|
|
37
|
+
|
|
38
|
+
### PHP (PHPUnit + Pest)
|
|
39
|
+
```bash
|
|
40
|
+
./vendor/bin/phpunit
|
|
41
|
+
./vendor/bin/pest --coverage --min=70
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Node.js (Vitest + Playwright)
|
|
45
|
+
```bash
|
|
46
|
+
npx vitest run --coverage
|
|
47
|
+
npx playwright test
|
|
48
|
+
npx playwright test --ui
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Before Commit Checklist
|
|
52
|
+
|
|
53
|
+
- [ ] All new features have tests?
|
|
54
|
+
- [ ] Tests use fixtures for cleanup?
|
|
55
|
+
- [ ] Database state verified after UI actions?
|
|
56
|
+
- [ ] Tests run on all viewports?
|
|
57
|
+
- [ ] Coverage threshold met (≥70%)?
|
|
58
|
+
- [ ] No `.skip()` in tests?
|
|
59
|
+
- [ ] All tests passing?
|
|
60
|
+
|
|
61
|
+
## FORBIDDEN
|
|
62
|
+
|
|
63
|
+
1. **Skipping tests** — `test.skip()`, `.only()`
|
|
64
|
+
2. **Mocking auth** — use real authentication
|
|
65
|
+
3. **Fixed test users** — `test@test.com` → generate unique
|
|
66
|
+
4. **No cleanup** — orphaned test data
|
|
67
|
+
5. **No DB validation** — trusting UI without checking DB
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# React Patterns — Modern Component Architecture
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing React components, hooks, or state management.**
|
|
4
|
+
|
|
5
|
+
## Component Patterns
|
|
6
|
+
|
|
7
|
+
### Compound Components
|
|
8
|
+
```tsx
|
|
9
|
+
const TabsContext = createContext<TabsContextValue | null>(null);
|
|
10
|
+
|
|
11
|
+
function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
|
|
12
|
+
const [activeTab, setActiveTab] = useState(defaultTab);
|
|
13
|
+
return (
|
|
14
|
+
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
|
15
|
+
<div className="tabs">{children}</div>
|
|
16
|
+
</TabsContext.Provider>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
// + TabList, Tab, TabPanel components using useContext(TabsContext)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Generic List Component
|
|
23
|
+
```tsx
|
|
24
|
+
interface ListProps<T> {
|
|
25
|
+
items: T[];
|
|
26
|
+
renderItem: (item: T) => ReactNode;
|
|
27
|
+
keyExtractor: (item: T) => string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
|
|
31
|
+
return <ul>{items.map(item => <li key={keyExtractor(item)}>{renderItem(item)}</li>)}</ul>;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Custom Hooks
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// useLocalStorage
|
|
39
|
+
function useLocalStorage<T>(key: string, initial: T) {
|
|
40
|
+
const [value, setValue] = useState<T>(() => {
|
|
41
|
+
if (typeof window === 'undefined') return initial;
|
|
42
|
+
try { return JSON.parse(window.localStorage.getItem(key) ?? '') } catch { return initial }
|
|
43
|
+
});
|
|
44
|
+
const set = useCallback((v: T | ((val: T) => T)) => {
|
|
45
|
+
setValue(prev => {
|
|
46
|
+
const next = v instanceof Function ? v(prev) : v;
|
|
47
|
+
window.localStorage.setItem(key, JSON.stringify(next));
|
|
48
|
+
return next;
|
|
49
|
+
});
|
|
50
|
+
}, [key]);
|
|
51
|
+
return [value, set] as const;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// useDebounce
|
|
55
|
+
function useDebounce<T>(value: T, delay: number): T {
|
|
56
|
+
const [debounced, setDebounced] = useState(value);
|
|
57
|
+
useEffect(() => { const t = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(t); }, [value, delay]);
|
|
58
|
+
return debounced;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## State Management
|
|
63
|
+
|
|
64
|
+
### useReducer for Complex State
|
|
65
|
+
```tsx
|
|
66
|
+
type Action =
|
|
67
|
+
| { type: 'FETCH_START' }
|
|
68
|
+
| { type: 'FETCH_SUCCESS'; payload: Item[] }
|
|
69
|
+
| { type: 'FETCH_ERROR'; payload: string };
|
|
70
|
+
|
|
71
|
+
function reducer(state: State, action: Action): State {
|
|
72
|
+
switch (action.type) {
|
|
73
|
+
case 'FETCH_START': return { ...state, loading: true, error: null };
|
|
74
|
+
case 'FETCH_SUCCESS': return { ...state, loading: false, items: action.payload };
|
|
75
|
+
case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Context (avoid prop drilling)
|
|
81
|
+
```tsx
|
|
82
|
+
const AppContext = createContext<AppContextValue | undefined>(undefined);
|
|
83
|
+
export function useApp() {
|
|
84
|
+
const ctx = useContext(AppContext);
|
|
85
|
+
if (!ctx) throw new Error('useApp must be used within AppProvider');
|
|
86
|
+
return ctx;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Performance
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// memo — prevent re-renders
|
|
94
|
+
const ExpensiveList = memo(function ExpensiveList({ items }: { items: Item[] }) { ... });
|
|
95
|
+
|
|
96
|
+
// useMemo — expensive calculations
|
|
97
|
+
const processed = useMemo(() => data.map(d => expensiveCalc(d)), [data]);
|
|
98
|
+
|
|
99
|
+
// useCallback — stable refs
|
|
100
|
+
const handleClick = useCallback(() => setCount(c => c + 1), []);
|
|
101
|
+
|
|
102
|
+
// Code splitting
|
|
103
|
+
const Heavy = lazy(() => import('./HeavyComponent'));
|
|
104
|
+
<Suspense fallback={<Loading />}><Heavy /></Suspense>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## FORBIDDEN
|
|
108
|
+
|
|
109
|
+
1. **Class components** — function components only
|
|
110
|
+
2. **Prop drilling** — use context or composition
|
|
111
|
+
3. **Inline objects/functions in JSX** — causes re-renders
|
|
112
|
+
4. **useEffect for derived state** — use useMemo
|
|
113
|
+
5. **Mutating state directly** — always setState/dispatch
|
|
@@ -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,94 @@
|
|
|
1
|
+
# Tailwind Patterns — Utility-First CSS
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing Tailwind CSS classes.**
|
|
4
|
+
|
|
5
|
+
## Mobile-First Breakpoints
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
sm: 640px → md: 768px → lg: 1024px → xl: 1280px → 2xl: 1536px
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<div className="w-full md:w-1/2 lg:w-1/3 xl:w-1/4">
|
|
13
|
+
{/* Mobile: full → Tablet: half → Desktop: third → Large: quarter */}
|
|
14
|
+
</div>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Layout Patterns
|
|
18
|
+
|
|
19
|
+
### Flexbox
|
|
20
|
+
```tsx
|
|
21
|
+
// Center
|
|
22
|
+
<div className="flex items-center justify-center h-screen" />
|
|
23
|
+
|
|
24
|
+
// Responsive direction
|
|
25
|
+
<div className="flex flex-col md:flex-row gap-4" />
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Grid
|
|
29
|
+
```tsx
|
|
30
|
+
// Responsive grid
|
|
31
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" />
|
|
32
|
+
|
|
33
|
+
// Auto-fit
|
|
34
|
+
<div className="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-4" />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Dark Mode
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
|
41
|
+
<p className="text-gray-600 dark:text-gray-300">Adapts to theme</p>
|
|
42
|
+
</div>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Component Patterns
|
|
46
|
+
|
|
47
|
+
### Card
|
|
48
|
+
```tsx
|
|
49
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow">
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Button
|
|
53
|
+
```tsx
|
|
54
|
+
<button className="bg-primary text-primary-foreground px-4 py-2 rounded-md font-medium hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Input
|
|
58
|
+
```tsx
|
|
59
|
+
<input className="w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-md text-gray-900 dark:text-white placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Animations
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<Loader className="h-5 w-5 animate-spin" /> // Spinner
|
|
66
|
+
<div className="h-4 w-24 bg-gray-200 animate-pulse" /> // Skeleton
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Custom (tailwind.config.js)
|
|
70
|
+
```js
|
|
71
|
+
animation: {
|
|
72
|
+
'fade-in': 'fadeIn 0.3s ease-in-out',
|
|
73
|
+
'slide-up': 'slideUp 0.3s ease-out',
|
|
74
|
+
},
|
|
75
|
+
keyframes: {
|
|
76
|
+
fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } },
|
|
77
|
+
slideUp: { '0%': { transform: 'translateY(10px)', opacity: '0' }, '100%': { transform: 'translateY(0)', opacity: '1' } },
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Show/Hide Responsive
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<div className="hidden lg:block">Desktop only</div>
|
|
85
|
+
<div className="block lg:hidden">Mobile only</div>
|
|
86
|
+
<span className="sr-only">Screen reader only</span>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## FORBIDDEN
|
|
90
|
+
|
|
91
|
+
1. **`!important`** — fix specificity properly
|
|
92
|
+
2. **Arbitrary values when utilities exist** — use the scale
|
|
93
|
+
3. **Mixing raw CSS with Tailwind** — stick to utilities
|
|
94
|
+
4. **Inconsistent spacing** — follow the 4px scale (p-1=4px, p-2=8px, p-4=16px)
|
|
@@ -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
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# TypeScript Strict — Type Safety Patterns
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing .ts/.tsx files.**
|
|
4
|
+
|
|
5
|
+
## Required tsconfig.json
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"compilerOptions": {
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"noImplicitAny": true,
|
|
13
|
+
"strictNullChecks": true,
|
|
14
|
+
"noImplicitReturns": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"exactOptionalPropertyTypes": true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Index Access
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// WRONG
|
|
25
|
+
const port = process.env.PORT;
|
|
26
|
+
|
|
27
|
+
// CORRECT — bracket notation
|
|
28
|
+
const port = process.env['PORT'];
|
|
29
|
+
const host = process.env['HOST'] ?? 'localhost';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Null Handling
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// Optional chaining + nullish coalescing
|
|
36
|
+
const avatar = user.profile?.avatar ?? '/default.png';
|
|
37
|
+
|
|
38
|
+
// WRONG — || treats 0, '', false as falsy
|
|
39
|
+
const count = input || 10;
|
|
40
|
+
// CORRECT — ?? only null/undefined
|
|
41
|
+
const count = input ?? 10;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Type Guards
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
function isUser(value: unknown): value is User {
|
|
48
|
+
return typeof value === 'object' && value !== null && 'id' in value && 'email' in value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Assertion function
|
|
52
|
+
function assertDefined<T>(value: T | undefined | null, msg: string): asserts value is T {
|
|
53
|
+
if (value == null) throw new Error(msg);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Const Assertions
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Exact types (not widened)
|
|
61
|
+
const endpoints = {
|
|
62
|
+
users: '/api/users',
|
|
63
|
+
posts: '/api/posts',
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
// Discriminated unions
|
|
67
|
+
type Result<T> =
|
|
68
|
+
| { success: true; data: T }
|
|
69
|
+
| { success: false; error: string };
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Generic Patterns
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// Constrained
|
|
76
|
+
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
|
|
77
|
+
return items.find(item => item.id === id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// RequireFields utility
|
|
81
|
+
type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## FORBIDDEN
|
|
85
|
+
|
|
86
|
+
1. **`any`** — use `unknown` instead
|
|
87
|
+
2. **Non-null assertion without certainty** — check first
|
|
88
|
+
3. **Type assertion to hide errors** — fix the actual issue
|
|
89
|
+
4. **`@ts-ignore`** — use `@ts-expect-error` with comment
|
|
90
|
+
5. **Ignoring strict errors** — always fix properly
|