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
| Lignes | Niveau | Action |
|---|---|---|
| 0 - 200 | Normal | Rien |
| 200 - 300 | Surveillé | Vérifier la cohérence du fichier en review |
| 300 - 500 | Alerte | Splitter sauf justification claire |
| 500 - 800 | Refus de PR | Interdit, sauf CONVENTION-EXCEPTION documentée |
| > 800 | Bloquant | Refus 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
| Type | Cible idéale | Plafond strict |
|---|---|---|
| Service (logique métier) | < 250 | 400 |
| Repository (Drizzle) | < 200 | 350 |
| Routes Elysia d'un module | < 200 | 400 |
| Composant React métier | < 150 | 300 |
Composant UI partagé (packages/ui) | < 200 | 400 |
| Hook React | < 100 | 200 |
| Schéma Drizzle d'une table | < 100 | 200 |
| Test file | < 400 | 600 (peut être plus permissif) |
Type definitions (types.ts) | < 150 | 250 |
Utility (lib/*.ts) | < 100 | 200 |
Page showcase / démo (/components/*) | n/a | Exclue, fichiers de démo, pas du code métier |
Composant UI complexe généré (sidebar.tsx, chart.tsx) | n/a | CONVENTION-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 deCONVENTION-EXCEPTIONmais un commentaire en tête est bienvenu (// Composant UI composite, voir conventions/file-thresholds) - > 700 lignes :
CONVENTION-EXCEPTIONobligatoire 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'updatemembers.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.
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.ts4. 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.tsAnti-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 :
- name: Check file thresholds
run: bun run scripts/check-thresholds.tsLe 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
- Architecture modulaire, où placer les fichiers extraits
- Factorisation, quand factoriser, quand pas