Files
dofus-manager/docs/architecture/17-coding-standards.md
2026-01-19 08:52:38 +01:00

10 KiB

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

// 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

// 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

// ✅ 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

// ✅ 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

// ✅ 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 :

// 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

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

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "plugins": ["prettier-plugin-tailwindcss"]
}

Git Commit Standards

# 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