Façon de travailler

Vérifier avant d'enregistrer, enregistrer par petits pas, et rendre visibles les erreurs qui ne produisent aucun message.

Précédemment. Nous avons installé le projet avec une base, un stockage et une boîte mail locaux, et vérifié chaque service. L'application compile et tourne.

Les erreurs silencieuses#

Une erreur de compilation s'affiche, désigne une ligne et bloque. Certaines erreurs, elles, ne produisent aucun message : le code est correct, mais placé au mauvais endroit ou fondé sur une hypothèse fragile.

Un exemple : Next.js ne lit ses fichiers de convention (proxy.ts, instrumentation.ts…) qu'à côté du dossier app. Notre application vit dans apps/web/src/app ; un proxy.ts posé dans apps/web/ serait ignoré sans avertissement, et le filtre d'authentification ne s'exécuterait jamais.

Le projet traite ces erreurs de la même façon : chacune a une vérification automatique, lancée avant chaque commit.

pnpm check#

Toutes les vérifications tiennent dans une commande, lancée à la racine :

pnpm check

Elle enchaîne sept étapes et s'arrête à la première qui échoue :

Étape

Ce qu’elle vérifie

Ce qu’elle garantit

scripts/check-next-conventions.mjs
Les fichiers de convention de Next sont au bon endroit (apps/web/src), et middleware et proxy ne coexistent pas.
Le filtre d’authentification et l’initialisation au démarrage sont bien exécutés.
scripts/check-tour-targets.mjs
Chaque étape d’un tutoriel interactif vise un élément qui porte bien l’attribut data-tour correspondant ; chaque tuto est au registre ; chaque bouton de fiche désigne un tuto existant.
Aucun tutoriel ne se bloque chez l’utilisateur après un renommage, ni ne disparaît sans bruit.
scripts/build-procedure-map.mjs
La carte des procédures (ce que chaque procédure lit et écrit) correspond au code du backend.
Les parcours de la documentation montrent les effets réels du code.
scripts/check-doc-entities.mjs
Les entités, propriétés, procédures et sections citées dans l’en-tête des chapitres existent.
La carte des entités et les parcours d’un chapitre restent justes après une modification.
pnpm clean
Efface .next et le cache.
La suite repart d’un état propre, sans fichier compilé périmé.
pnpm typecheck
TypeScript sur apps/web, sur packages/backend, et sur la configuration de la racine.
Chaque procédure renvoie ce que le front attend, et chaque composant l’appelle avec les bons arguments.
pnpm lint
ESLint sur apps/web, puis sur le backend, les contrats et le moteur du Finder, règles de React et du React Compiler comprises, sans aucun avertissement toléré.
Les composants restent optimisables par le Compiler ; le code mort et les motifs fautifs ne s’accumulent pas (partie « Qualité »).

Les quatre scripts maison sont courts : chacun traque une seule erreur silencieuse et l'explique en français quand il la trouve (partie « Qualité », chapitre « Les gardes maison »). Quand une nouvelle erreur de ce genre apparaît, la réponse est une vérification de plus.

pnpm clean supprime le dossier .next que pnpm dev utilise. Après un pnpm check, si une modification n'apparaît pas dans le navigateur, arrêtons pnpm dev (Ctrl+C) et relançons-le. Si pnpm dev réécrit .next pendant la suppression, pnpm check peut aussi échouer sur « Directory not empty » : il suffit d'arrêter pnpm dev avant de vérifier.

Le lint#

TypeScript vérifie les types ; il ne connaît pas les règles de React. ESLint, avec les règles react-hooks, les connaît, et elles se sont durcies avec React 19 et son compilateur.

pnpm lint

Un rendu React doit être une fonction pure : React peut le lancer deux fois, l'interrompre ou le jeter. Le carrousel de la page d'accueil, par exemple, garde l'état « lecture ou pause » dans une ref que sa boucle d'animation lit à chaque image. La ref est mise à jour après le rendu, dans un effet :

const playingRef = useRef(playing);useEffect(() => {  playingRef.current = playing; // écrit APRÈS le rendu}, [playing]);

Écrire playingRef.current = playing directement dans le corps du composant fonctionnerait aujourd'hui, mais ESLint le refuse (« Cannot update ref during render ») : ce code dépendrait de la façon dont React ordonne ses rendus. Dans un effet, l'écriture reste correcte quoi que fasse React.

Une règle de lint qui se déclenche sur du code « qui marche » n'est pas une fausse alerte : elle signale presque toujours une dépendance à un comportement de React qui n'est pas garanti.

Enregistrer par petits pas#

Un commit est une étape qui tient debout seule : pnpm check passe avant et après. Une fonctionnalité qui touche le backend et le front est découpée : d'abord le backend (le modèle, la procédure, ses vérifications), puis le front qui l'utilise. Chaque moitié se vérifie séparément, et un problème se localise immédiatement.

Les messages suivent la forme type(portée): résumé, en français, avec un corps qui explique pourquoi :

feat(rgpd): formulaire de droit à l'image — téléchargement, dépôt admin, aperçu membrefix(carousel): erreurs react-hooks/refs et react-hooks/immutabilitystyle(header): logo encore agrandi, barre de même hauteur

Type

Quand

feat
Une fonctionnalité nouvelle, visible ou non.
fix
La correction d’un comportement erroné.
style
Un changement d’apparence, sans effet sur la logique.
refactor
Une réorganisation du code, sans changement de comportement.
docs
La documentation, celle-ci comprise.
chore
L’outillage : dépendances, scripts, configuration.

Le corps du message est ce que retrouvera, des mois plus tard, la personne qui lance git blame sur une ligne inattendue.

Les tutoriels interactifs : un contrat dans le code#

Le site embarque des tutoriels qui guident un utilisateur pas à pas sur la vraie page, en ne laissant actif que l'élément de l'étape en cours. Chaque étape désigne son élément par un nom :

<input  value={title}  onChange={(e) => setTitle(e.target.value)}  data-tour="posts.form.title"/>

Cet attribut ne change rien à l'affichage, mais c'est un contrat : le composant garantit que l'élément existe sous ce nom, et un tutoriel (dans apps/web/src/features/tours/definitions/) s'appuie dessus. pnpm check vérifie ce contrat : si un data-tour utilisé disparaît, il échoue et nomme le tutoriel concerné. Nous mettons alors à jour l'un ou l'autre, dans le même commit.

Avant de supprimer un data-tour, cherchons son nom dans features/tours/definitions/ : s'il y apparaît, un tutoriel en dépend.

Écrire la documentation#

La documentation vit dans apps/web/content/docs, en trois dossiers qui sont aussi les trois espaces du site : guide pour les visiteurs et les membres, admin pour les administrateurs (réservé côté serveur), dev pour nous. Le premier dossier d'une page décide de son espace ; son en-tête (title, description, section, order) décide de sa place dans la barre latérale.

Dans l'espace développeur, nous écrivons avec « nous ». Chaque chapitre part d'un besoin, présente le choix retenu et ce qu'il permet, puis le code. Il s'ouvre sur un rappel du précédent et se termine en annonçant le suivant. Les points à retenir vont dans un Callout ; les listes ne servent qu'aux énumérations courtes.

Le compilateur MDX supprime sans prévenir les expressions JavaScript dans les attributs et les chaînes d'attribut sur plusieurs lignes. Le composant DocsTable reçoit donc ses colonnes et ses lignes en JSON, entre apostrophes simples, sur une seule ligne, et toute apostrophe du contenu devient une apostrophe typographique (’) :

<DocsTable columnsJson='["Colonne A","Colonne B"]' rowsJson='[["L’exemple","oui"],["Autre","non"]]' />

La carte des entités#

Un chapitre qui s'appuie sur la base déclare dans son en-tête les entités qu'il mobilise, et pour chacune les propriétés dont il parle :

entities:  Ticket: [number, status, dueAt]  TicketCategory: [label, replyTo, groupId]

La page affiche alors un bouton « Entités » : une carte des entités du chapitre, lue dans prisma/schema.prisma, avec leurs relations, leurs propriétés et leurs commentaires ///. Les propriétés déclarées y sont mises en évidence. Dans le texte, un nom d'entité ou de propriété écrit en code (`Ticket`, `dueAt`, `Ticket.dueAt`) devient un lien vers la carte. Le schéma est la seule source : une migration qui ajoute un champ l'ajoute à la carte, et scripts/check-doc-entities.mjs signale une entité ou une propriété déclarée qui n'existe plus.

Un chapitre peut aussi déclarer des parcours : un cas d'utilisation raconté en étapes, que la carte rejoue pas à pas. Chaque étape cite ce qui s'exécute : une procédure tRPC (ticket.take), une fonction du serveur (purgeExpiredData), une server action, une page qui lit la base elle-même (page /agenda) ou une route d'API (GET /api/me/export). Elle cite aussi la section du chapitre qui l'explique :

stories:  - title: Le club traite une demande    actor: Membre d'un groupe qui traite la catégorie    steps:      - label: Un membre la prend en charge        call: ticket.take        section: "Le délai : fixé une fois pour toutes"

Les effets d'une étape (entités créées, modifiées, lues, supprimées, propriétés écrites, e-mails envoyés) ne sont pas écrits à la main : scripts/build-procedure-map.mjs les relève dans le code du backend, en suivant les appels jusqu'aux appels Prisma. Après une modification du backend, pnpm check demande de régénérer cette carte avec pnpm docs:procedures.

Chaque effet garde aussi sa piste : les appels traversés depuis la procédure jusqu'à l'appel qui touche la base, avec le fichier et la ligne où lire chacun. Une étape propose ce « chemin dans le code », replié par défaut :

storage.registerR2Upload           packages/backend/src/modules/storage/router.ts:… └ adapter.registerUploadedAsset    packages/backend/src/modules/storage/adapters/r2/r2StorageAdapter.ts:…    └ prisma.mediaAsset.upsert      MediaAsset créée, modifiée

Pour suivre les appels, l'analyse charge le backend comme le fait pnpm typecheck et interroge le vérificateur de types, comme « Aller à la définition » dans l'éditeur. Elle part des procédures, des server actions du front (fichiers "use server"), des pages, des gabarits (layout) et des routes d'API qui touchent la base. Elle suit les fonctions importées, les méthodes d'objets ou de classes, et les composants qu'un composant serveur affiche (<Footer /> s'exécute sur le serveur et peut lire la base). Quand une méthode est déclarée dans une interface (adapter.move, déclarée par StorageAdapter), elle retrouve les objets qui la réalisent ; s'il y en a plusieurs, chaque piste est montrée. Les requêtes SQL écrites à la main ($executeRaw) sont lues aussi, et marquées « SQL direct ». Une lecture qui ramène des entités liées (select ou include d'une relation), ou qui filtre sur elles (where: { pageReferences: { some: {} } }), les compte comme lues. Une suppression entraîne celles que la base fait seule, déclarées dans le schéma (onDelete: Cascade, ou SetNull qui vide un champ) : elles sont marquées « en cascade », et leur piste mène à la ligne du schéma. L'accès d'une procédure (public, membre, admin) se lit dans sa chaîne (protectedProcedure, .use(isAdmin), adminProcedure). Ce qu'elle ne peut pas voir : une fonction reçue en paramètre et appelée plus loin, ou une requête construite morceau par morceau. Un doute sur une étape se lève en ouvrant sa piste et en lisant le code.

Le fil de cette partie : rendre visibles les erreurs silencieuses avant qu'elles n'atteignent la production. pnpm check en est l'outil, le petit commit la discipline, le contrat data-tour un exemple.

À suivre#

Les fondations du travail sont posées : un environnement jetable, des vérifications automatiques, une discipline de commit. Avant d'entrer dans le code, un détour par les fichiers qui le configurent : on les ouvre rarement, mais chacun décide de quelque chose que tout le reste suppose.

Partie suivante : la configuration, en commençant par package.json et le monorepo. Comment pnpm relie les paquets entre eux, d'où viennent packageManager et les scripts, et ce que chaque package.json déclare.

Sur cette page