Reciprok Docs

Seuils de fichiers

Lignes max par fichier, signaux d'alarme, quand splitter

Pas de fichier monstre. Un fichier > 300 lignes est un signal, au-delà de 500, c'est un refus de PR systématique sauf justification écrite.

Pourquoi des seuils

Un fichier court c'est :

  • Lisible en une seule fois sans scroll interminable
  • Plus facile à tester (moins de chemins, moins de mocks)
  • Plus facile à reviewer en PR
  • Plus facile à refactor (on peut le déplacer/renommer sans casser 30 imports)
  • Plus facile à comprendre pour l'IA (Claude perd en précision sur les très longs fichiers)

Les seuils ne sont pas arbitraires : ce sont des alertes de complexité. Quand un fichier dépasse, ce n'est pas "tu as écrit trop", c'est "ton code fait trop de choses".

Les seuils

LignesNiveauAction
0 - 200NormalRien
200 - 300SurveilléVérifier la cohérence du fichier en review
300 - 500AlerteSplitter sauf justification claire
500 - 800Refus de PRInterdit, sauf CONVENTION-EXCEPTION documentée
> 800BloquantRefus systématique, refacto obligatoire

Comptage : lignes non vides, hors imports et hors commentaires de licence/documentation longue. Les fichiers types ou schémas générés sont exclus.

Par type de fichier

TypeCible idéalePlafond strict
Service (logique métier)< 250400
Repository (Drizzle)< 200350
Routes Elysia d'un module< 200400
Composant React métier< 150300
Composant UI partagé (packages/ui)< 200400
Hook React< 100200
Schéma Drizzle d'une table< 100200
Test file< 400600 (peut être plus permissif)
Type definitions (types.ts)< 150250
Utility (lib/*.ts)< 100200
Page showcase / démo (/components/*)n/aExclue, fichiers de démo, pas du code métier
Composant UI complexe généré (sidebar.tsx, chart.tsx)n/aCONVENTION-EXCEPTION requise

Cas particulier : composants UI packages/ui/

Les composants du package packages/ui (shadcn/Coss UI) sont souvent générés ou composés depuis une lib externe. Ils ont des caractéristiques différentes du code métier :

  • Beaucoup de variantes (tailles, couleurs, états)
  • Beaucoup de sous-composants exportés depuis le même fichier (ex: Sidebar, SidebarHeader, SidebarFooter, SidebarTrigger…)
  • Splitter ces fichiers casserait l'import groupé (ex: import { Sidebar, SidebarHeader, SidebarContent } from '@reciprok/ui/components/sidebar')

Règle pour packages/ui :

  • < 400 lignes : pas de question
  • 400-700 lignes : toléré si le composant expose beaucoup de sous-exports (comme sidebar.tsx, chart.tsx). Pas besoin de CONVENTION-EXCEPTION mais un commentaire en tête est bienvenu (// Composant UI composite, voir conventions/file-thresholds)
  • > 700 lignes : CONVENTION-EXCEPTION obligatoire avec justification

Cas particulier : pages showcase

Les fichiers apps/web/src/app/components/*/page.tsx sont des pages de démo qui présentent les composants UI. Elles servent de vitrine, pas de code métier. Elles sont exclues des seuils et du check CI car :

  • Ce sont des galeries de composants assemblés à la main
  • Elles n'ont pas d'impact métier ni de maintenance critique
  • Elles sont rarement modifiées après création

Le script CI les exclut via le path apps/web/src/app/components/*/page.tsx.

Tous les autres fichiers

Un test file peut être plus long parce qu'il enchaîne souvent beaucoup de cases similaires. Mais s'il dépasse 600 lignes, c'est qu'on devrait splitter par "groupe de scénarios" (ex: members.service.create.test.ts, members.service.search.test.ts).

Comment splitter

1. Splitter par sous-responsabilité

Si un service gère plusieurs aspects, on extrait dans des fichiers nommés.

modules/members/
├── members.service.ts            # ← devenu trop gros

modules/members/
├── members.service.ts            # entrypoint, orchestration
├── members.service.create.ts     # logique de création
├── members.service.search.ts     # logique de recherche
└── members.service.update.ts     # logique d'update

members.service.ts devient fin et délègue.

2. Extraire les helpers privés

Si une fonction utilitaire prend 80 lignes au milieu d'un service, on l'extrait dans un fichier interne.

modules/requests/requests.service.ts
import { computeBudgetRange } from './internal/compute-budget-range';

Convention : les fichiers internes vont dans un sous-dossier internal/ qui n'est jamais exporté par l'index.ts.

3. Splitter un composant React

Un composant > 200 lignes est presque toujours en fait plusieurs composants enchevêtrés. On extrait les sous-blocs visuels en sous-composants colocalisés.

components/member-card/
├── member-card.tsx               # 120 lignes, composition haut niveau
├── member-card.header.tsx        # 40 lignes
├── member-card.body.tsx          # 60 lignes
├── member-card.footer.tsx        # 30 lignes
└── index.ts

4. Schémas Drizzle

Si un fichier de schéma a 8 tables, on splitte par domaine :

packages/db/src/schema/
├── members.ts          # member, member_tag, member_photo
├── rooms.ts            # room, room_capacity
├── requests.ts         # request, request_date, request_result
└── index.ts

Anti-patterns à éviter en splittant

Le "split de surface"

Couper un fichier en deux moitiés arbitraires juste pour passer sous le seuil. Aucune valeur ajoutée, on a juste rendu la navigation plus pénible. Le split doit suivre une logique sémantique.

Le "split par couche d'abstraction"

Sortir 12 micro-fonctions d'une ligne dans 12 fichiers séparés au nom de la "single responsibility". On obtient un labyrinthe illisible. Préférer un fichier de 250 lignes cohérent à 12 fichiers de 20 lignes éparpillés.

Le sous-dossier vide

Créer un sous-dossier internal/ avec un seul fichier dedans. Si tu n'as qu'un fichier, mets-le au niveau parent et préfixe-le _helper.ts ou similaire.

Mesure

Pour scripter un check des fichiers qui dépassent :

find apps packages -type f \( -name "*.ts" -o -name "*.tsx" \) \
  -not -path "*/node_modules/*" \
  -not -path "*/dist/*" \
  -not -path "*/.next/*" \
  | while read f; do
    lines=$(wc -l < "$f")
    if [ "$lines" -gt 300 ]; then
      echo "$lines  $f"
    fi
  done | sort -rn

À intégrer en CI plus tard, en mode warning puis error :

.github/workflows/check-thresholds.yml (extrait)
- name: Check file thresholds
  run: bun run scripts/check-thresholds.ts

Le script échoue le build si un fichier dépasse 500 lignes sans CONVENTION-EXCEPTION dans son entête.

Exception documentée

Si un fichier doit légitimement dépasser, mettre un commentaire en tête :

// CONVENTION-EXCEPTION: file-thresholds (562 lignes)
// Ce fichier expose tous les schémas TypeBox partagés via Elysia.model.
// Les splitter casserait l'inférence Eden côté client.
// Validé en review : PR #142

import { Elysia, t } from 'elysia';

export const models = new Elysia({ name: 'models' })
  .model({ /* ... */ });

Sans cet entête, le check CI échoue.

Pourquoi 300/500 et pas autre chose ?

Ce sont des seuils empiriques observés dans des codebases TS de taille moyenne :

  • À 300 lignes, un fichier reste navigable d'une seule lecture sur un écran 27''
  • À 500 lignes, on commence à perdre le contexte mental, on devient moins efficace
  • Au-delà de 800, la lecture demande des allers-retours qui font perdre 5-10 min à chaque fois

Tu peux trouver d'autres chiffres dans d'autres styleguides (Google a 1000, Airbnb n'en parle pas). Les nôtres sont volontairement bas pour forcer l'extraction tôt et éviter la dette qui s'accumule.

Voir aussi

On this page