initial commit
This commit is contained in:
343
docs/architecture/17-coding-standards.md
Normal file
343
docs/architecture/17-coding-standards.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user