344 lines
10 KiB
Markdown
344 lines
10 KiB
Markdown
# 17. Coding Standards
|
|
|
|
Cette section définit les standards de code minimaux mais critiques pour le développement. Ces règles sont conçues pour les agents IA et les développeurs.
|
|
|
|
## Critical Fullstack Rules
|
|
|
|
| Rule | Description |
|
|
|------|-------------|
|
|
| **Type Sharing** | Toujours définir les types partagés dans `src/types/` et les importer dans les composants et server functions. Ne jamais dupliquer les interfaces. |
|
|
| **Server Functions Only** | Ne jamais faire d'appels HTTP directs (fetch, axios). Toujours utiliser les server functions importées depuis `@/server/*`. |
|
|
| **Zod Validation** | Toute entrée utilisateur doit être validée avec un schéma Zod. Le schéma doit être colocalisé avec la server function. |
|
|
| **Prisma Transactions** | Les opérations multi-tables doivent utiliser `prisma.$transaction()` pour garantir l'atomicité. |
|
|
| **Cache Invalidation** | Après chaque mutation, invalider les caches affectés via `cacheService.invalidate()` et `queryClient.invalidateQueries()`. |
|
|
| **Error Boundaries** | Chaque page doit être wrappée dans un ErrorBoundary. Les erreurs serveur doivent utiliser les classes de `@/lib/errors.ts`. |
|
|
| **No Direct State Mutation** | Ne jamais muter l'état directement. Utiliser les setters de useState ou les mutations TanStack Query. |
|
|
| **URL State for Filters** | Les filtres et la pagination doivent être stockés dans l'URL via TanStack Router, pas dans le state local. |
|
|
| **Optimistic Updates Pattern** | Pour les actions fréquentes (toggle, update), utiliser les optimistic updates de TanStack Query. |
|
|
| **Component Colocation** | Les hooks, types et helpers spécifiques à un composant doivent être dans le même dossier. |
|
|
|
|
## Naming Conventions
|
|
|
|
| Element | Frontend | Backend | Example |
|
|
|---------|----------|---------|---------|
|
|
| Components | PascalCase | — | `CharacterCard.tsx` |
|
|
| Component files | kebab-case ou PascalCase | — | `character-card.tsx` |
|
|
| Hooks | camelCase + use prefix | — | `useCharacters.ts` |
|
|
| Server Functions | camelCase | camelCase | `getCharacters`, `createTeam` |
|
|
| Server Function files | kebab-case | kebab-case | `characters.ts` |
|
|
| Types/Interfaces | PascalCase | PascalCase | `Character`, `CreateCharacterInput` |
|
|
| Enums | PascalCase | PascalCase | `CharacterClass`, `TeamType` |
|
|
| Constants | SCREAMING_SNAKE | SCREAMING_SNAKE | `MAX_TEAM_SIZE` |
|
|
| Database tables | — | snake_case | `character_progressions` |
|
|
| Database columns | — | snake_case | `created_at`, `account_id` |
|
|
| URL routes | kebab-case | kebab-case | `/characters`, `/bulk-progressions` |
|
|
| Query keys | camelCase arrays | — | `['characters', 'list', filters]` |
|
|
| CSS classes | Tailwind utilities | — | `bg-card text-foreground` |
|
|
|
|
## File Structure Conventions
|
|
|
|
### Component File Structure
|
|
|
|
```typescript
|
|
// 1. Imports (grouped)
|
|
import { useState } from 'react'; // React
|
|
import { useQuery } from '@tanstack/react-query'; // External libs
|
|
import { Button } from '@/components/ui/button'; // Internal UI
|
|
import { getCharacters } from '@/server/characters'; // Server functions
|
|
import type { Character } from '@/types'; // Types (last)
|
|
|
|
// 2. Types (if not in separate file)
|
|
interface CharacterListProps {
|
|
serverId?: string;
|
|
}
|
|
|
|
// 3. Constants (if any)
|
|
const PAGE_SIZE = 20;
|
|
|
|
// 4. Component
|
|
export function CharacterList({ serverId }: CharacterListProps) {
|
|
// 4a. Hooks (in order: router, query, state, effects, callbacks)
|
|
const navigate = useNavigate();
|
|
const { data, isLoading } = useQuery({ ... });
|
|
const [selected, setSelected] = useState<string[]>([]);
|
|
|
|
// 4b. Early returns
|
|
if (isLoading) return <Loading />;
|
|
if (!data) return <EmptyState />;
|
|
|
|
// 4c. Render
|
|
return (
|
|
<div>
|
|
{/* JSX */}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 5. Sub-components (if small and only used here)
|
|
function CharacterRow({ character }: { character: Character }) {
|
|
return <tr>...</tr>;
|
|
}
|
|
```
|
|
|
|
### Server Function File Structure
|
|
|
|
```typescript
|
|
// 1. Imports
|
|
import { createServerFn } from '@tanstack/start';
|
|
import { z } from 'zod';
|
|
import { prisma } from '@/lib/prisma';
|
|
import { cacheService } from '@/lib/cache';
|
|
import { NotFoundError, ValidationError } from '@/lib/errors';
|
|
|
|
// 2. Schemas (colocated with functions)
|
|
const createCharacterSchema = z.object({
|
|
name: z.string().min(1).max(50),
|
|
class: z.nativeEnum(CharacterClass),
|
|
level: z.number().int().min(1).max(200),
|
|
serverId: z.string().uuid(),
|
|
accountId: z.string().uuid(),
|
|
});
|
|
|
|
// 3. Server Functions (exported)
|
|
export const getCharacters = createServerFn('GET', async (filters) => {
|
|
// Implementation
|
|
});
|
|
|
|
export const createCharacter = createServerFn('POST', async (input) => {
|
|
const data = createCharacterSchema.parse(input);
|
|
// Implementation
|
|
});
|
|
|
|
// 4. Helper functions (private, at bottom)
|
|
function buildWhereClause(filters: Filters): Prisma.CharacterWhereInput {
|
|
// Implementation
|
|
}
|
|
```
|
|
|
|
## Code Quality Rules
|
|
|
|
### TypeScript
|
|
|
|
```typescript
|
|
// ✅ DO: Explicit return types for exported functions
|
|
export function calculateTotal(items: Item[]): number {
|
|
return items.reduce((sum, item) => sum + item.amount, 0);
|
|
}
|
|
|
|
// ❌ DON'T: Implicit any
|
|
function processData(data) { ... }
|
|
|
|
// ✅ DO: Use type imports
|
|
import type { Character } from '@/types';
|
|
|
|
// ❌ DON'T: Import types as values
|
|
import { Character } from '@/types';
|
|
|
|
// ✅ DO: Discriminated unions for state
|
|
type State =
|
|
| { status: 'loading' }
|
|
| { status: 'error'; error: Error }
|
|
| { status: 'success'; data: Character[] };
|
|
|
|
// ❌ DON'T: Optional properties for state
|
|
type State = {
|
|
loading?: boolean;
|
|
error?: Error;
|
|
data?: Character[];
|
|
};
|
|
```
|
|
|
|
### React
|
|
|
|
```typescript
|
|
// ✅ DO: Named exports for components
|
|
export function CharacterCard({ character }: Props) { ... }
|
|
|
|
// ❌ DON'T: Default exports (except for routes)
|
|
export default function CharacterCard() { ... }
|
|
|
|
// ✅ DO: Destructure props
|
|
function CharacterCard({ character, onSelect }: Props) { ... }
|
|
|
|
// ❌ DON'T: Props object
|
|
function CharacterCard(props: Props) {
|
|
const character = props.character;
|
|
}
|
|
|
|
// ✅ DO: useCallback for handlers passed to children
|
|
const handleSelect = useCallback((id: string) => {
|
|
setSelected(prev => [...prev, id]);
|
|
}, []);
|
|
|
|
// ✅ DO: useMemo for expensive computations
|
|
const filteredCharacters = useMemo(
|
|
() => characters.filter(c => c.level >= 180),
|
|
[characters]
|
|
);
|
|
|
|
// ❌ DON'T: Inline objects/arrays in props (causes re-renders)
|
|
<Table columns={[col1, col2]} /> // Creates new array each render
|
|
```
|
|
|
|
### Prisma
|
|
|
|
```typescript
|
|
// ✅ DO: Select only needed fields
|
|
const characters = await prisma.character.findMany({
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
level: true,
|
|
server: { select: { name: true } },
|
|
},
|
|
});
|
|
|
|
// ❌ DON'T: Fetch everything
|
|
const characters = await prisma.character.findMany({
|
|
include: {
|
|
server: true,
|
|
account: true,
|
|
progressions: true,
|
|
currencies: true,
|
|
teamMemberships: true,
|
|
},
|
|
});
|
|
|
|
// ✅ DO: Use transactions for related operations
|
|
await prisma.$transaction([
|
|
prisma.teamMember.deleteMany({ where: { teamId } }),
|
|
prisma.team.delete({ where: { id: teamId } }),
|
|
]);
|
|
|
|
// ✅ DO: Count separately from data
|
|
const [data, total] = await Promise.all([
|
|
prisma.character.findMany({ take: 20 }),
|
|
prisma.character.count(),
|
|
]);
|
|
```
|
|
|
|
## Import Order
|
|
|
|
ESLint avec `eslint-plugin-import` applique cet ordre :
|
|
|
|
```typescript
|
|
// 1. React
|
|
import { useState, useCallback } from 'react';
|
|
|
|
// 2. External packages (alphabetical)
|
|
import { useQuery, useMutation } from '@tanstack/react-query';
|
|
import { useNavigate } from '@tanstack/react-router';
|
|
import { z } from 'zod';
|
|
|
|
// 3. Internal absolute imports (alphabetical by path)
|
|
import { Button } from '@/components/ui/button';
|
|
import { DataTable } from '@/components/data-table/data-table';
|
|
import { useCharacters } from '@/hooks/use-characters';
|
|
import { prisma } from '@/lib/prisma';
|
|
import { getCharacters } from '@/server/characters';
|
|
|
|
// 4. Relative imports
|
|
import { CharacterRow } from './character-row';
|
|
|
|
// 5. Type imports (at the end)
|
|
import type { Character, CharacterFilters } from '@/types';
|
|
```
|
|
|
|
## ESLint & Prettier Config
|
|
|
|
### .eslintrc.cjs
|
|
|
|
```javascript
|
|
module.exports = {
|
|
root: true,
|
|
env: { browser: true, es2022: true, node: true },
|
|
extends: [
|
|
'eslint:recommended',
|
|
'plugin:@typescript-eslint/recommended',
|
|
'plugin:react-hooks/recommended',
|
|
'prettier',
|
|
],
|
|
parser: '@typescript-eslint/parser',
|
|
parserOptions: {
|
|
ecmaVersion: 'latest',
|
|
sourceType: 'module',
|
|
project: './tsconfig.json',
|
|
},
|
|
plugins: ['@typescript-eslint', 'import'],
|
|
rules: {
|
|
// TypeScript
|
|
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
'@typescript-eslint/consistent-type-imports': 'error',
|
|
'@typescript-eslint/no-explicit-any': 'warn',
|
|
|
|
// Import order
|
|
'import/order': [
|
|
'error',
|
|
{
|
|
groups: [
|
|
'builtin',
|
|
'external',
|
|
'internal',
|
|
'parent',
|
|
'sibling',
|
|
'index',
|
|
'type',
|
|
],
|
|
'newlines-between': 'always',
|
|
alphabetize: { order: 'asc' },
|
|
},
|
|
],
|
|
|
|
// React
|
|
'react-hooks/rules-of-hooks': 'error',
|
|
'react-hooks/exhaustive-deps': 'warn',
|
|
|
|
// General
|
|
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
|
},
|
|
ignorePatterns: ['node_modules', '.output', 'dist'],
|
|
};
|
|
```
|
|
|
|
### .prettierrc
|
|
|
|
```json
|
|
{
|
|
"semi": true,
|
|
"singleQuote": true,
|
|
"tabWidth": 2,
|
|
"trailingComma": "es5",
|
|
"printWidth": 100,
|
|
"plugins": ["prettier-plugin-tailwindcss"]
|
|
}
|
|
```
|
|
|
|
## Git Commit Standards
|
|
|
|
```bash
|
|
# Format
|
|
<type>(<scope>): <subject>
|
|
|
|
# Types
|
|
feat # New feature
|
|
fix # Bug fix
|
|
docs # Documentation
|
|
style # Formatting (no code change)
|
|
refactor # Refactoring
|
|
test # Adding tests
|
|
chore # Build, deps, tooling
|
|
|
|
# Scopes (optional)
|
|
characters, teams, accounts, progressions, currencies
|
|
ui, api, db, auth, dofusdb
|
|
|
|
# Examples
|
|
feat(characters): add bulk progression update
|
|
fix(teams): validate account constraint on member add
|
|
refactor(api): extract prisma helpers
|
|
test(teams): add integration tests for constraints
|
|
chore(deps): upgrade TanStack Query to v5.17
|
|
```
|
|
|
|
---
|