-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy path.cursorrules
More file actions
552 lines (468 loc) · 15.9 KB
/
.cursorrules
File metadata and controls
552 lines (468 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# Cursor Rules for MCP App Demo
You are an expert in TypeScript, React, TanStack Start, Vite, Tailwind CSS, and Shadcn/ui development.
## Technology Stack & Code Quality
- **TypeScript**: Strict mode enabled - never use `any` type, use proper generics or let TypeScript infer
- **TanStack Start**: SSR routing with file-based patterns
- **Tailwind CSS & Shadcn/ui**: UI styling and component system
- **Zod**: Runtime validation and TypeScript integration
- **React Query**: Server state management (avoid redundant fetching when using loaders)
- **Vite**: Build tooling
- Keep code self-documenting with clear names; add comments only for complex business logic
- Run `npm run lint:fix` after significant changes
## TanStack Start Routing Patterns
### Route File Structure and Conventions
Place routes in `src/routes/` using file-based routing:
- Static routes: `about.tsx`
- Dynamic routes: `[id].tsx`, `users/[userId].tsx`
- Layout routes: `_layout.tsx`
- API routes: `api/chat.ts` (server-side with `.ts` extension)
- Always export `Route` from `createFileRoute()` or `ServerRoute` from `createServerFileRoute()`
### Loader and Action Conventions
**Always explicitly type loader/action returns with interfaces:**
```typescript
// ✅ Good: Client route with properly typed loader
import { createFileRoute } from '@tanstack/react-router';
import { userSchema } from '@/lib/schemas';
import { z } from 'zod';
interface UserDetailParams {
id: string;
}
interface UserDetailLoaderData {
user: z.infer<typeof userSchema>;
}
export const Route = createFileRoute('/users/$id')({
loader: async ({ params }): Promise<UserDetailLoaderData> => {
const user = await fetchUser(params.id);
const validatedUser = userSchema.parse(user);
return { user: validatedUser };
},
component: UserDetail,
errorBoundary: ({ error }) => (
<div className="text-red-600 p-4">Error loading user: {error.message}</div>
),
pendingBoundary: () => (
<div className="flex items-center justify-center p-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" role="status" aria-label="Loading">
<span className="sr-only">Loading...</span>
</div>
</div>
),
});
function UserDetail() {
const { user } = Route.useLoaderData();
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
```
**Server routes with comprehensive validation:**
```typescript
// ✅ Good: Server route with validation
import { createServerFileRoute } from '@tanstack/react-start/server'
import { userCreateSchema } from '@/lib/schemas'
export const ServerRoute = createServerFileRoute('/api/users').methods({
async POST({ request }) {
try {
const body = await request.json()
const result = userCreateSchema.safeParse(body)
if (!result.success) {
return new Response(
JSON.stringify({
error: 'Validation failed',
details: result.error.flatten(),
}),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
},
)
}
const user = await createUser(result.data)
return new Response(JSON.stringify(userSchema.parse(user)))
} catch (error) {
console.error('Server error:', error)
return new Response(
JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
)
}
},
})
```
### Error and Loading Boundaries
**Always use errorBoundary and pendingBoundary in routes:**
```typescript
// ✅ Good: Consistent boundary patterns
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const users = await fetchUsers();
return { users: userListSchema.parse(users) };
},
errorBoundary: ({ error, reset }) => (
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200">
<h2 className="font-semibold">Failed to load users</h2>
<p className="mt-1 text-sm">{error.message}</p>
<Button onClick={reset} className="mt-2" variant="outline" size="sm">
Try Again
</Button>
</div>
),
pendingBoundary: () => (
<div className="flex items-center justify-center p-8" role="status" aria-label="Loading users">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary">
<span className="sr-only">Loading users...</span>
</div>
</div>
),
component: Dashboard,
});
```
## React Query vs Route Loaders
**Prefer Route Loaders for:**
- Initial page data that's required for rendering
- Server-side rendering (SSR) requirements
- Data that should be fetched before the component mounts
- Critical path data needed for SEO
```typescript
// ✅ Good: Use loader for initial page data
export const Route = createFileRoute('/users')({
loader: async (): Promise<{ users: User[] }> => {
const users = await fetchUsers()
return { users: userListSchema.parse(users) }
},
component: UserList,
})
```
**Prefer React Query for:**
- Data that updates frequently
- Optional/secondary data not critical for initial render
- Client-side mutations and optimistic updates
- Data that needs fine-grained cache control
```typescript
// ✅ Good: Use React Query for dynamic/optional data
function UserStats({ userId }: { userId: string }) {
const { data: stats, isLoading, refetch } = useQuery({
queryKey: ['user-stats', userId],
queryFn: () => fetchUserStats(userId),
refetchInterval: 30000, // Auto-refresh every 30s
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
});
if (isLoading) return <div>Loading stats...</div>;
return <div>{stats?.totalPosts} posts</div>;
}
```
## Validation with Zod
**Always validate external data** - from APIs, user input, environment variables, and any data crossing system boundaries.
### Schema Organization and Best Practices
Define schemas in `src/lib/schemas.ts` with proper documentation:
```typescript
// ✅ Good: Comprehensive schema definition
import { z } from 'zod'
export const userSchema = z.object({
id: z.string().describe('Unique user identifier'),
name: z.string().min(1, 'Name is required').max(100),
email: z.string().email('Invalid email format').optional(),
age: z.number().int().min(13, 'Must be at least 13 years old').optional(),
createdAt: z.string().datetime('Invalid date format'),
role: z.enum(['admin', 'user', 'moderator']).default('user'),
})
export type User = z.infer<typeof userSchema>
// ✅ Good: Input/output schemas for transformations
export const userCreateInputSchema = userSchema.omit({
id: true,
createdAt: true,
})
export type UserCreateInput = z.infer<typeof userCreateInputSchema>
```
### Advanced Validation Patterns
```typescript
// ✅ Good: Safe parsing with proper error handling
function validateUserData(data: unknown): User | null {
const result = userSchema.safeParse(data)
if (!result.success) {
console.error('User validation failed:', result.error.format())
return null
}
return result.data
}
// ✅ Good: Custom refinements and transformations
export const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
.refine(
(val) => /[A-Z]/.test(val),
'Password must contain at least one uppercase letter',
)
.refine(
(val) => /[0-9]/.test(val),
'Password must contain at least one number',
)
// ✅ Good: Conditional schemas
export const contactSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('email'),
email: z.string().email(),
}),
z.object({
type: z.literal('phone'),
phone: z.string().regex(/^\+?[\d\s-()]+$/, 'Invalid phone format'),
}),
])
```
## Import Aliasing & File Organization
**Always use `@/` alias for internal imports** to ensure consistency:
```typescript
// ✅ Good: Import order and grouping
// 1. External packages
import React from 'react'
import { z } from 'zod'
import { createFileRoute } from '@tanstack/react-router'
// 2. Internal modules (using @/ alias)
import { Button } from '@/components/ui/button'
import { userSchema } from '@/lib/schemas'
import { cn } from '@/lib/utils'
// 3. Types (if needed separately)
import type { User } from '@/lib/schemas'
// ❌ Bad: Relative imports for internal modules
import { Button } from '../components/ui/button'
import { userSchema } from '../../lib/schemas'
```
**File structure:**
```
src/
├── components/ # Shared UI components (prefer Shadcn)
│ └── ui/ # Shadcn/ui components
├── contexts/ # React Context providers
├── hooks/ # Custom React hooks
├── lib/ # Utilities and helpers
│ └── schemas.ts # Zod schemas
├── routes/ # TanStack Start routes
│ └── api/ # Server routes (.ts files)
└── styles.css # Global styles
```
## Accessibility Best Practices
### Semantic HTML and ARIA Attributes
Always use semantic HTML elements for structure and meaning. Only use ARIA attributes when there is no appropriate semantic HTML equivalent:
```typescript
// ✅ Good: Proper ARIA attributes and semantic elements
<button
aria-expanded={isOpen}
aria-controls="menu"
aria-label="Toggle navigation menu"
onClick={toggleMenu}
>
<MenuIcon aria-hidden="true" />
<span className="sr-only">Navigation Menu</span>
</button>
// ✅ Good: Accessible form elements
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email Address
</label>
<input
id="email"
type="email"
aria-describedby="email-error"
aria-invalid={errors.email ? 'true' : 'false'}
className="mt-1 block w-full rounded-md border border-gray-300"
/>
{errors.email && (
<p id="email-error" className="mt-1 text-sm text-red-600" role="alert">
{errors.email}
</p>
)}
</div>
```
### Keyboard Navigation
```typescript
// ✅ Good: Keyboard event handling
function SearchInput() {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
clearSearch();
}
if (e.key === 'Enter') {
performSearch();
}
};
return (
<input
type="search"
onKeyDown={handleKeyDown}
aria-label="Search users"
placeholder="Search users..."
/>
);
}
```
## Shadcn/ui Components & Styling
**Always prefer Shadcn components with proper accessibility:**
```typescript
// ✅ Good: Shadcn Dialog with proper accessibility
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
<Dialog>
<DialogTrigger asChild>
<Button>Open Settings</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>User Settings</DialogTitle>
<DialogDescription>
Manage your account settings and preferences.
</DialogDescription>
</DialogHeader>
{/* Dialog content */}
</DialogContent>
</Dialog>
```
**Add new components with:**
```bash
npx shadcn@latest add <component-name>
```
**Use Tailwind with responsive design:**
```typescript
// ✅ Good: Mobile-first responsive classes
<div className="flex flex-col gap-4 p-6 md:flex-row md:gap-6">
<Button className="w-full md:w-auto">Action</Button>
</div>
// ✅ Good: Conditional classes with cn utility
<div className={cn("base-classes", isActive && "active-classes")}>
```
## Component Storybook Stories
- All new components **must** have a colocated Storybook story for visual documentation, testing, and design review.
- The story file should be named `SomeComponent.stories.tsx` and placed in the same directory as `SomeComponent.tsx`.
- Example file structure:
```
src/components/
MyComponent.tsx
MyComponent.stories.tsx
```
- See [`src/components/ui/button.stories.tsx`](src/components/ui/button.stories.tsx) for a full example. Minimal example:
```typescript
// MyComponent.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { MyComponent } from './MyComponent'
const meta: Meta<typeof MyComponent> = {
title: 'UI/MyComponent',
component: MyComponent,
tags: ['autodocs'],
}
export default meta
type Story = StoryObj<typeof MyComponent>
export const Default: Story = {
args: {
/* props */
},
}
```
## State Management Patterns
**Loader-first approach - avoid redundant React Query:**
```typescript
// ✅ Good: Use loader data, React Query only for mutations/real-time
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const data = await fetchDashboardData();
return { dashboard: data };
},
component: Dashboard,
});
function Dashboard() {
const { dashboard } = Route.useLoaderData(); // Use loader data
// React Query only for mutations or real-time updates
const mutation = useMutation({
mutationFn: updateDashboard,
onSuccess: () => router.invalidate(),
});
}
```
**Context for global state:**
```typescript
// ✅ Good: Typed context
const { user, setUser } = useContext(UserContext);
```
## Testing Patterns (Optional)
**Component testing with Testing Library:**
```typescript
// ✅ Good: Component testing with proper setup
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { UserCard } from '@/components/UserCard';
import { userSchema } from '@/lib/schemas';
describe('UserCard', () => {
const mockUser = userSchema.parse({
id: '1',
name: 'John Doe',
email: 'john@example.com',
createdAt: new Date().toISOString(),
});
it('displays user information correctly', () => {
render(<UserCard user={mockUser} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
it('handles click events', () => {
const onEdit = vi.fn();
render(<UserCard user={mockUser} onEdit={onEdit} />);
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
expect(onEdit).toHaveBeenCalledWith(mockUser.id);
});
});
```
## Error Handling Patterns
### React Query Error Handling
```typescript
// ✅ Good: React Query with comprehensive error handling
const { data, error, isLoading, refetch } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
});
if (error) {
return (
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-red-800">
<h3 className="font-medium">Error loading data</h3>
<p className="mt-1 text-sm">{error.message}</p>
<Button onClick={() => refetch()} className="mt-2" size="sm">
Retry
</Button>
</div>
);
}
```
### Route-Level Error Handling
```typescript
// ✅ Good: Route with error and pending boundaries (see examples above)
```
## What NOT to do
- ❌ Don't use `any` type - always use proper TypeScript types
- ❌ Don't use class components - use function components
- ❌ Don't use external state libraries like Zustand or Redux
- ❌ Don't create custom UI components when Shadcn alternatives exist
- ❌ Don't write custom CSS when Tailwind classes work
- ❌ Don't put business logic directly in components
- ❌ Don't forget to validate external data with Zod schemas
- ❌ Don't use relative imports for internal modules - use `@/` alias
- ❌ Don't skip error boundaries and pending boundaries in routes
- ❌ Don't forget ARIA attributes and accessibility considerations
- ❌ Don't use React Query for data that should be loaded by route loaders
- ❌ Don't ignore TypeScript errors or use `@ts-ignore` without justification
## Key Principles
Focus on writing clean, type-safe, maintainable code that follows the established patterns in this project. Prioritize accessibility, proper error handling, and use TanStack Start's conventions for optimal performance and developer experience.