Module 8 · Le stack mobile

L'outillage du projet

Voici une vérité qui démystifie la moitié des erreurs que tu rencontreras : le code que tu écris n'est pas celui qui s'exécute. Entre ton fichier .tsx et ce qui tourne sur le téléphone, toute une chaîne d'outils traduit, vérifie et assemble ton travail. Ce module ouvre cette boîte noire. Une fois que tu sais quel outil fait quoi, une erreur n'est plus un mur : c'est juste un outil précis qui te parle.

💡 L'idée directrice du module

Tu écris du TypeScript moderne avec du JSX, des alias @/..., des décorateurs, des fonctions fléchées. Rien de tout ça n'existe pour le moteur JavaScript du téléphone. Des outils transforment ton code en quelque chose de plus simple, vérifient qu'il tient la route, et l'assemblent en un paquet livrable. Connaître ces outils, c'est comprendre d'où vient chaque message : compilation, lint, ou exécution. C'est le but de toute cette page.

1. La grande illusion : ton code n'est pas exécuté tel quel

Quand tu programmais du JavaScript « pur » dans un navigateur, l'illusion était presque vraie : tu écrivais un fichier, le navigateur le lisait, il le faisait tourner. Presque ligne pour ligne. Sur une app React Native moderne comme Halterofit, cette illusion s'effondre complètement, et c'est une bonne nouvelle — parce que ça t'explique enfin pourquoi certaines erreurs apparaissent à des moments si différents.

Pense à une cuisine de restaurant. Le client (le téléphone) reçoit une assiette finie. Mais avant l'assiette, il y a eu une recette écrite (ton code source), un chef qui vérifie que la recette a du sens et que les ingrédients existent (TypeScript), un commis qui traduit la recette dans un langage que les cuisiniers comprennent vraiment (Babel), et un serveur qui assemble tous les plats sur un plateau unique et l'apporte à table (Metro). Le client ne voit jamais la recette d'origine. Il mange le résultat.

Cette distinction est centrale pour lire le projet. Beaucoup de débutants paniquent parce qu'ils mélangent « le moment où j'écris », « le moment où le projet se construit » et « le moment où l'app tourne ». Ce sont trois mondes différents, gérés par trois familles d'outils différentes. On va les visiter un par un.

🧭 Bon à savoir : « temps de compilation » vs « temps d'exécution »

Retiens ces deux expressions, on les emploiera partout. Le temps de compilation (ou « build time »), c'est tout ce qui se passe avant que l'app démarre : vérification des types, traduction, assemblage. Le temps d'exécution (ou « runtime »), c'est quand l'app tourne réellement dans tes mains. Une règle d'or : TypeScript vit au temps de compilation et disparaît au temps d'exécution. On y revient juste en dessous.

2. La chaîne de build, étape par étape

Voici le voyage d'un fichier, dans l'ordre. Trois outils principaux se le passent comme un relais : TypeScript, puis Babel, puis Metro. Chacun a un rôle bien distinct, et confondre ces rôles est la source d'innombrables malentendus.

le parcours d'un fichier
  Ton fichier .tsx
        │
        ▼
  [ TypeScript ]  ── vérifie les types ── puis DISPARAÎT (n'émet rien à l'exécution)
        │
        ▼
  [ Babel ]       ── traduit JSX + TS + JS moderne ── en JS simple
        │
        ▼
  [ Metro ]       ── rassemble TOUS les fichiers ── en un seul paquet (bundle)
        │
        ▼
  Le moteur JavaScript du téléphone (Hermes) exécute le paquet

Garde ce schéma en tête : il explique pourquoi une erreur de type ne plante jamais l'app (TypeScript a déjà disparu), alors qu'une erreur d'exécution, elle, te saute au visage dans l'app.

2a. TypeScript — le vérificateur qui s'efface ensuite

TypeScript est d'abord un vérificateur de types. Quand tu écris function logSet(id: string), le : string n'est pas une instruction que le téléphone exécutera. C'est une promesse que TypeScript va contrôler : « partout où tu appelles logSet, tu lui donnes bien une chaîne de caractères ? ». S'il trouve un endroit où tu lui passes un nombre, il te le signale — avant même que l'app démarre. C'est un filet de sécurité posé au temps de compilation.

Le point qui surprend toujours : une fois la vérification faite, TypeScript s'efface complètement. Tous les : string, : number, les interface, les type — tout ça est retiré. Le moteur du téléphone ne voit jamais une seule annotation de type. C'est ce qu'on appelle le type erasure (« effacement des types »). TypeScript ne change pas comment ton code fonctionne ; il te dit juste, en amont, si ton code a du sens. C'est un correcteur d'orthographe pour la logique, pas un moteur.

💡 Conséquence pratique énorme

Comme les types disparaissent à l'exécution, tu ne peux pas « demander son type » à une variable pendant que l'app tourne de la même manière qu'au moment de l'écriture. Si tu as besoin de vérifier une forme de donnée à l'exécution (par exemple une réponse réseau), il faut le faire avec du vrai code (des if, des validations), pas avec les types. Les types protègent ton clavier ; ils ne protègent pas le téléphone. D'où l'existence d'outils de validation à l'exécution, vus ailleurs dans le guide.

2b. Babel — le traducteur

Le moteur JavaScript du téléphone (Hermes, chez React Native) ne comprend pas tout ce que tu écris. Le JSX — ces balises <View>...</View> au milieu de ton JavaScript — n'est pas du JavaScript valide pour un moteur. Le TypeScript doit être retiré. Et certaines tournures très récentes du JavaScript doivent parfois être réécrites en versions plus universelles. C'est le travail de Babel : un traducteur qui prend ton code « humain et moderne » et le réécrit en JavaScript simple que le moteur avale sans broncher.

Concrètement, <Text>Bonjour</Text> devient un appel de fonction du genre jsx(Text, null, 'Bonjour'). Tes const f = (x) => x + 1 peuvent rester tels quels ou être réécrits. Tes annotations de types sont arrachées. Tout ça, automatiquement, sans que tu t'en aperçoives. Babel travaille en silence au temps de compilation.

Dans Halterofit, Babel est configuré finement parce que le projet utilise des bibliothèques aux besoins particuliers. Le fichier de configuration le montre :

apps/mobile/babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: [['babel-preset-expo', { decorators: false }]],
    plugins: [
      // 1. Retire le TypeScript EN PREMIER (avant les décorateurs).
      ['@babel/plugin-transform-typescript', { isTSX: true, allExtensions: true }],
      // 2. Décorateurs « legacy » — exigés par WatermelonDB (la base locale).
      ['@babel/plugin-proposal-decorators', { legacy: true }],
      // Le plugin Reanimated doit passer EN DERNIER (animations).
      'react-native-reanimated/plugin',
    ],
    // ...
  };
};

Tu n'as pas à comprendre chaque plugin. Ce qu'il faut voir : Babel est une chaîne de transformations ordonnée. L'ordre compte (« le TS d'abord », « Reanimated en dernier »), exactement comme on enchaîne des étapes de recette. Les longs commentaires de ce fichier réel racontent pourquoi cet ordre précis a été choisi.

🧭 Bon à savoir : preset vs plugin

Un plugin Babel fait une transformation. Un preset est juste un paquet de plugins prêts à l'emploi. Ici, babel-preset-expo regroupe tout ce qu'il faut pour une app Expo standard ; le projet y ajoute ensuite quelques plugins « maison » pour ses cas particuliers (la base de données locale, les animations). Même logique que les presets d'un éditeur photo : un réglage groupé, qu'on peut compléter à la main.

2c. Metro — l'assembleur (le « bundler »)

Ton app n'est pas un fichier : c'est des centaines de fichiers qui s'importent les uns les autres, plus toutes les bibliothèques installées. Le moteur du téléphone, lui, ne sait pas aller chercher tout ça morceau par morceau. Il lui faut un seul gros paquet, prêt à avaler. Construire ce paquet, c'est le métier du bundler. En React Native, ce bundler s'appelle Metro.

Metro fait trois choses essentielles. Premièrement, il suit tous les imports : il part de ton point d'entrée, voit que ce fichier importe celui-ci, qui importe celui-là, et ainsi de suite, jusqu'à avoir cartographié tout l'arbre des dépendances. Deuxièmement, il passe chaque fichier par Babel au passage (c'est Metro qui orchestre la traduction) et rassemble le tout en un bundle unique. Troisièmement, pendant le développement, il sert ce bundle au téléphone et gère le rechargement à chaud (« Fast Refresh ») : quand tu sauvegardes un fichier, Metro renvoie juste la partie modifiée et tu vois le changement à l'écran en une seconde, sans tout relancer.

C'est exactement Metro que tu démarres quand tu lances expo start (le script start du projet). La fenêtre qui reste ouverte dans ton terminal, qui affiche un QR code et recompile à chaque sauvegarde : c'est le serveur Metro qui tourne.

apps/mobile/metro.config.js (extrait)
const config = getSentryExpoConfig(__dirname);
const monorepoRoot = path.resolve(__dirname, '../..');

// Metro doit aussi SURVEILLER la racine du monorepo, car des fichiers
// partagés vivent au-dessus du dossier apps/mobile.
config.watchFolders = [...(config.watchFolders ?? []), monorepoRoot];

// Forcer la résolution CJS pour des paquets qui exposent deux formats.
// Hermes ne gère pas certains imports dynamiques ESM.
config.resolver.unstable_conditionNames = ['require', 'react-native', 'default'];

Lecture : ce fichier configure Metro. Il lui dit où chercher les fichiers (y compris à la racine du monorepo) et comment trancher quand une bibliothèque propose plusieurs versions d'elle-même. Le détail technique importe peu ; l'idée à retenir, c'est que Metro est configurable et qu'on l'enveloppe ici dans Sentry (surveillance) et NativeWind (les styles).

🏋️ Dans Halterofit

Le metro.config.js du projet est enveloppé par getSentryExpoConfig et par withNativewind. Traduction humaine : on prend la config Metro de base d'Expo, on lui ajoute la capacité de Sentry de retrouver l'origine exacte d'un crash en production (les fameuses « source maps »), et on lui branche NativeWind pour que tes classes de style à la Tailwind fonctionnent. Trois couches empilées sur le même assembleur.

3. La gestion des paquets : pnpm, le monorepo et le package.json

Aucune app moderne ne réinvente tout. Halterofit s'appuie sur des dizaines de bibliothèques externes (React, Expo, la base de données locale, les animations…). Il faut les installer, suivre quelle version de chacune, et faire en sorte que ton ordinateur, celui de ton collègue et le serveur de build utilisent exactement les mêmes. C'est le rôle du gestionnaire de paquets. Ici, c'est pnpm.

Le contrat de chaque app vit dans son package.json. C'est la carte d'identité du projet : son nom, ses scripts, et surtout la liste de ce dont il dépend. Et cette liste est coupée en deux catégories qu'il est essentiel de distinguer.

3a. dependencies vs devDependencies

Les dependencies sont les bibliothèques nécessaires à l'app elle-même, celles qui finiront dans le paquet livré sur le téléphone. Sans elles, l'app ne marche pas. Les devDependencies sont les outils de développement : ils servent à construire, vérifier et tester le projet, mais ils ne partent pas dans l'app finale. L'utilisateur n'en a jamais besoin.

Analogie : pour construire une maison, le bois et les fenêtres font partie de la maison (ce sont les dependencies) ; la perceuse et l'échafaudage servent à la bâtir mais ne restent pas dedans (ce sont les devDependencies). Regarde le vrai partage dans Halterofit :

CatégorieExemples réels du projetRôle
dependencies react, react-native, expo, @nozbe/watermelondb, zustand, @supabase/supabase-js, nativewind Tournent dans l'app, sur le téléphone.
devDependencies typescript, eslint, prettier, jest, babel-preset-expo, babel-plugin-react-compiler, msw Servent avant l'exécution : vérifier, traduire, formater, tester.

Remarque cohérente avec tout ce module : typescript, eslint et prettier sont des devDependencies. Logique — ce sont des outils du temps de compilation et du temps d'écriture, pas du temps d'exécution. jest et msw (pour simuler le réseau dans les tests) aussi : on teste avant de livrer, pas sur le téléphone du client.

🧭 Bon à savoir : lire les versions

Dans le package.json, tu verras "react": "19.2.3" (version exacte, figée) et "zustand": "^5.0.14" (le accent circonflexe ^ autorise les petites mises à jour compatibles). Le ~ est encore plus restrictif. Ces symboles disent « jusqu'où j'accepte que cette bibliothèque évolue ». Pour les paquets Expo, les versions sont volontairement épinglées serré (~56.0.x) pour rester alignées avec le SDK.

3b. Le lockfile : la photo exacte des versions

Le package.json décrit des intervalles (« accepte react-native 0.85.x »). Mais au moment de l'installation, pnpm choisit une version précise pour chaque paquet, et pour les paquets dont dépendent tes paquets, et ainsi de suite sur des centaines de niveaux. Il fige ce choix complet dans un fichier : le lockfile (pnpm-lock.yaml). C'est la photo exacte de l'arbre installé.

Pourquoi c'est crucial ? Parce que sans lui, deux personnes installant « les mêmes intervalles » à deux semaines d'écart pourraient se retrouver avec des versions légèrement différentes — et donc des bugs qui apparaissent chez l'un, pas chez l'autre. Le lockfile garantit que tout le monde installe rigoureusement le même arbre. C'est lui qui rend la phrase « ça marche sur ma machine » fiable. On ne le modifie jamais à la main : c'est un fichier généré, qu'on versionne et qu'on laisse pnpm gérer.

3c. Le monorepo et les workspaces

Tu as sans doute remarqué le chemin apps/mobile. Le mobile est l'app téléphone. À côté vit apps/web, le site web. Les deux habitent dans un seul et même dépôt : c'est un monorepo (« dépôt unique »). pnpm gère ça avec des workspaces : plusieurs paquets cohabitent, partagent une installation commune et peuvent se référencer entre eux. C'est ce qui permet, par exemple, de partager des types ou des fonctions utilitaires entre l'app mobile et le site sans copier-coller.

Le nom du paquet mobile, d'ailleurs, le trahit : dans son package.json, il s'appelle @halterofit/mobile. Le préfixe @halterofit/ est la « marque » du monorepo ; chaque sous-projet en porte une déclinaison. On en arrive naturellement à l'outil qui orchestre tout ce petit monde.

4. L'orchestration du monorepo : Turborepo

Avec plusieurs projets dans un même dépôt, une question se pose vite : comment lancer « vérifie les types partout », « passe le lint partout », « construis tout » sans taper dix commandes à la main et sans tout recalculer pour rien ? C'est le rôle de Turborepo (la commande turbo). C'est un chef d'orchestre : il connaît les tâches de chaque projet, sait dans quel ordre les enchaîner, et — détail décisif — met en cache les résultats.

turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "ui": "tui",
  "tasks": {
    "build":      { "dependsOn": ["^build"], "outputs": [".next/**", "dist/**"] },
    "dev":        { "persistent": true, "cache": false },
    "lint":       {},
    "type-check": {},
    "test":       {},
    "test:ci":    {}
  }
}

Chaque clé de "tasks" est une tâche que Turborepo sait lancer dans les projets du monorepo : construire, lint, vérifier les types, tester. Court mais dense — décodons-le.

Trois détails à savoir lire dans ce fichier. Le "dependsOn": ["^build"] de la tâche build signifie « avant de construire ce projet, construis d'abord les projets dont il dépend » (le accent circonflexe ^ veut dire « les dépendances en amont »). Le "persistent": true de dev indique une tâche qui ne s'arrête pas (le serveur de développement reste allumé). Et "cache": false sur dev précise qu'on ne met pas en cache un serveur vivant — ça n'aurait aucun sens.

💡 Le cache, le vrai super-pouvoir

L'argument massue de Turborepo, c'est le cache. Si tu lances lint et que rien n'a changé depuis la dernière fois, Turborepo ne relance pas le lint : il ressort le résultat précédent instantanément. Pareil pour les types, les tests. Sur un gros projet, ça transforme des minutes d'attente en quelques secondes. Le "outputs" de la tâche build dit à Turborepo quels fichiers constituent le résultat à mémoriser, pour pouvoir le restituer tel quel plus tard.

🧭 Pourquoi un monorepo, au fond ?

On aurait pu mettre l'app mobile et le site web dans deux dépôts séparés. Le choix du monorepo a un but : partager du code et rester cohérent. Le même type « un Exercice ressemble à ça », la même logique métier, les mêmes règles de style peuvent vivre une seule fois et servir aux deux applications. Une seule installation, un seul lockfile, un seul chef d'orchestre. Le coût, c'est un peu plus de configuration au départ ; le gain, c'est qu'on ne maintient pas deux fois la même chose.

5. Les outils de qualité : ESLint, Prettier, Husky

Vérifier que le code fonctionne ne suffit pas. Une équipe veut aussi un code cohérent et lisible, où chacun écrit dans le même style et évite les pièges connus. Trois outils s'en chargent, et il faut bien les distinguer car ils ne font pas la même chose.

5a. ESLint — le gardien des règles

ESLint est un linter : il lit ton code et te prévient quand tu enfreins une règle. Ces règles vont du basique (« tu déclares une variable que tu n'utilises jamais ») au subtil (« ton useEffect oublie une dépendance », via eslint-plugin-react-hooks, présent dans le projet). ESLint ne vérifie pas les types (ça, c'est TypeScript) et ne juge pas si le code marche (ça, c'est l'exécution). Il juge des pratiques.

Le projet expose deux scripts : lint (qui corrige automatiquement ce qui peut l'être, via eslint . --fix) et lint:check (qui vérifie sans rien modifier, avec un cache pour aller vite). Cette séparation « répare » / « vérifie seulement » est typique : on répare en local, on vérifie sur le serveur d'intégration.

🏋️ Dans Halterofit : des règles MAISON

Détail savoureux et très instructif : Halterofit a des règles ESLint sur mesure, écrites pour ce projet. La plus parlante interdit les couleurs « en dur » dans le code (genre color: '#FF0000'). Pourquoi se priver d'écrire une couleur ? Pour forcer tout le monde à passer par le design system : on n'écrit pas un rouge au hasard, on utilise le « jeton » de couleur officiel défini une seule fois. Si quelqu'un colle un code couleur en dur, ESLint refuse. La règle de lint devient ici un garde-fou qui protège la cohérence visuelle de toute l'app. Une règle de style, mais avec une vraie intention de design derrière.

5b. Prettier — le formateur

Prettier ne se soucie pas du sens de ton code, seulement de son apparence : indentation, guillemets, points-virgules, retours à la ligne. Tu sauvegardes, il reformate, tout le monde a la même mise en forme — fin des débats stériles « simples ou doubles guillemets ? ». Le projet l'invoque via prettier --write . (reformate) et prettier --check . (vérifie sans toucher).

La division du travail entre les deux est nette, et c'est une confusion classique de débutant : ESLint = qualité et pratiques ; Prettier = présentation. On les fait même cohabiter proprement grâce à eslint-config-prettier (dans les devDependencies), qui désactive les règles ESLint de pur formatage pour laisser ce terrain à Prettier. Chacun son métier.

5c. Husky + lint-staged — le filet avant le commit

Tout ça ne sert à rien si on oublie de le lancer. D'où une astuce : faire en sorte que les vérifications se déclenchent automatiquement, juste avant chaque commit Git. C'est le rôle des hooks Git, et Husky est l'outil qui les installe facilement. Couplé à lint-staged, il ne vérifie que les fichiers que tu t'apprêtes vraiment à committer (« staged » = mis en scène pour le commit), pas tout le projet — c'est plus rapide.

Le scénario concret : tu tapes git commit. Avant que le commit ne se fasse, le hook pre-commit se réveille, passe Prettier et ESLint sur tes fichiers modifiés. Si tout est propre, le commit passe. Si une règle est violée, le commit est bloqué et on te dit quoi corriger. Résultat : du code non conforme n'entre quasiment jamais dans l'historique. Le filet de sécurité est tendu avant que le problème se propage à l'équipe.

6. Lire les fichiers de config : tsconfig et les alias de chemin

Maintenant qu'on connaît les acteurs, apprenons à lire leur configuration. Le plus utile au quotidien est le tsconfig.json, qui pilote TypeScript. Voici l'essentiel du fichier réel de Halterofit :

apps/mobile/tsconfig.json
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,                  // mode sévère : TS traque le maximum d'erreurs
    "noUncheckedIndexedAccess": true,// accéder à tableau[i] peut être undefined → à gérer
    "paths": {
      "@/*": ["./src/*"],            // l'alias : @/x veut dire ./src/x
      "@tests/*": ["./__tests__/*"]
    },
    "incremental": true              // ne re-vérifie que ce qui a changé (rapidité CI)
  }
  // ...
}

Le "extends" en haut dit « pars de la config Expo et ajoute mes réglages par-dessus ». Comme pour Babel et Metro : on hérite d'une base, on la personnalise.

6a. strict: true — le mode sévère

Le réglage le plus important est "strict": true. Il active d'un coup tout un paquet de contrôles exigeants : interdiction d'utiliser une variable peut-être undefined sans la vérifier, obligation de typer ce qui doit l'être, refus des conversions dangereuses. C'est plus contraignant à l'écriture, mais ça attrape une foule de bugs avant qu'ils n'existent. Halterofit pousse même un cran plus loin avec noUncheckedIndexedAccess, qui te force à envisager qu'un accès comme liste[3] pourrait ne rien renvoyer. Le projet a choisi la rigueur maximale, et c'est un choix de qualité.

6b. Les alias de chemin — finis les ../../../

Voici un confort que tu vas adorer. Sans alias, pour importer un composant situé loin dans l'arborescence, tu écrirais une suite fragile de ../ pour remonter les dossiers :

Sans alias (pénible et fragile)Avec l'alias @/*
import { Button } from '../../../components/ui/button' import { Button } from '@/components/ui/button'

Grâce à la ligne "@/*": ["./src/*"], le préfixe @/ est un raccourci qui veut toujours dire « depuis le dossier src ». Donc @/components/ui/button pointe sur src/components/ui/button, peu importe d'où tu écris l'import. Deux avantages concrets : c'est lisible (on voit tout de suite d'où vient la chose), et c'est solide — si tu déplaces ton fichier dans un autre dossier, l'import @/... reste valable, alors qu'une cascade de ../../ aurait cassé. Le @ n'a rien de magique : c'est juste une convention que ce projet a choisie pour dire « racine du code source ».

⚠️ Subtilité : l'alias doit être connu de plusieurs outils

Un piège qui déroute : un alias comme @/* doit être déclaré à la fois à TypeScript (le tsconfig, pour qu'il comprenne tes imports) et au bundler (Metro/Babel, pour qu'il les résolve à l'exécution). Si un seul des deux le connaît, tu peux avoir un code « vert » dans l'éditeur qui plante au lancement, ou l'inverse. Quand un import @/... échoue mystérieusement, c'est souvent qu'un des deux mondes n'est pas au courant de l'alias. Encore une illustration de « ce que tu écris n'est pas directement ce qui tourne ».

7. Les trois types d'erreurs à ne jamais confondre

C'est sans doute la section la plus utile de tout le module. Une fois la chaîne d'outils comprise, il devient évident qu'une « erreur » n'est pas une chose unique : selon qui te parle, elle n'arrive pas au même moment et ne se règle pas pareil. Distinguer ces trois familles te fera gagner un temps fou, parce que tu sauras où chercher.

Type d'erreurQui parleQuandÀ quoi ça ressemble
Erreur de TYPE TypeScript Temps de compilation (avant que l'app tourne) « Type string is not assignable to type number ». Souvent soulignée en rouge dans l'éditeur.
Erreur de LINT ESLint Au lint (souvent au commit, via Husky) « 'x' is defined but never used », ou une règle maison : « couleur en dur interdite ». Ça ne casse pas l'app, ça refuse le commit.
Erreur d'EXÉCUTION Le moteur (runtime) Temps d'exécution (l'app tourne, dans tes mains) « undefined is not an object (evaluating 'x.name') ». Écran rouge, l'app plante en marche.

Le réflexe à acquérir : devant un message, demande-toi « à quel moment suis-je ? ». Si l'erreur apparaît dans l'éditeur ou au tsc --noEmit (le script type-check du projet) sans même lancer l'app, c'est un type. Si elle surgit quand tu tentes de committer, ou via le script lint:check, c'est du lint. Si elle te tombe dessus avec l'app ouverte, en touchant un bouton, c'est de l'exécution.

💡 Pourquoi cette distinction change tout

Une erreur de type ou de lint est ton amie : elle te prévient avant que quoi que ce soit ne tourne, gratuitement, sans risque pour l'utilisateur. Une erreur d'exécution est plus sérieuse : elle est arrivée pendant l'usage. Tout l'enjeu des outils de ce module est de transformer un maximum d'erreurs d'exécution en erreurs de type ou de lint — c'est-à-dire de les attraper tôt. Plus tu attrapes haut dans la chaîne, moins ça coûte cher.

8. Deux invités à connaître : Sentry et le React Compiler

Deux outils méritent une mention, même si on ne les détaille pas ici. Ils complètent le tableau de la chaîne.

Sentry (@sentry/react-native, dans les dependencies) est la surveillance des erreurs en production. Malgré tous les filets en amont, certaines erreurs d'exécution n'arrivent que chez de vrais utilisateurs, sur de vrais téléphones, dans des situations imprévues. Sentry capture ces crashes, les renvoie à l'équipe avec la pile d'appels exacte, et — grâce aux « source maps » qu'on a vues dans la config Metro — pointe la ligne de ton code source responsable, même dans un bundle traduit et compressé. C'est le rétroviseur de l'app une fois livrée.

Le React Compiler (le plugin babel-plugin-react-compiler dans les devDependencies) est un nouvel outil qui optimise automatiquement les re-renders. Souviens-toi du module sur useMemo, useCallback et memo : on y mémorisait des calculs et des fonctions à la main pour éviter les re-renders inutiles. Le React Compiler fait une bonne partie de ce travail tout seul, au temps de compilation. C'est une raison de plus de ne pas couvrir ton code de memo par réflexe : l'outil s'en charge déjà en grande partie.

🧭 Et les tests ?

Le projet embarque Jest (le lanceur de tests), MSW (pour simuler les réponses réseau sans appeler le vrai serveur) et Maestro (pour tester l'app de bout en bout, comme un utilisateur). Ce sont des outils du temps de développement, eux aussi absents du paquet livré. On les détaille dans la section « Pour aller plus loin » du guide — ici, il suffit de savoir qu'ils existent et qu'ils vivent dans les devDependencies.

✍️ Exercice de lecture

Tu ouvres le package.json et tu vois ces deux lignes, dans deux sections différentes. Sans regarder ailleurs, réponds : laquelle part dans l'app livrée sur le téléphone, et pourquoi ?

// section A
"zustand": "^5.0.14"

// section B
"prettier": "^3.8.3"

Questions : (1) Dans quelle catégorie est chacune ? (2) Laquelle s'exécute sur le téléphone ? (3) Quel outil du module se sert de l'autre, et à quel moment ?

Voir le corrigé

(1) zustand est dans dependencies (c'est la bibliothèque de gestion d'état de l'app) ; prettier est dans devDependencies (c'est un outil de développement).

(2) Seul zustand s'exécute sur le téléphone : il fait partie de la logique de l'app, donc Metro l'inclut dans le bundle livré. prettier n'y est jamais — l'utilisateur n'a aucune raison de transporter un formateur de code.

(3) prettier est utilisé avant l'exécution : pendant le développement (quand tu sauvegardes) et surtout au moment du commit, déclenché par le hook pre-commit de Husky + lint-staged. Il agit au temps d'écriture, pas au temps d'exécution. La distinction « ça tourne sur le téléphone ? oui/non » est exactement celle qui sépare dependencies de devDependencies.

🧠 Quiz éclair

1. Dans quel ordre passent les trois outils principaux de la chaîne de build, et que fait chacun ?

TypeScript (vérifie les types, puis disparaît) → Babel (traduit JSX + TS + JS moderne en JS simple) → Metro (rassemble tous les fichiers en un bundle, le sert au téléphone, gère le rechargement à chaud). Vérifier, traduire, assembler.

2. Que devient une annotation de type TypeScript au temps d'exécution ?

Elle disparaît. TypeScript vérifie au temps de compilation, puis tous les types sont effacés (« type erasure ») : le moteur du téléphone n'en voit aucun. Les types protègent ton clavier, pas le runtime.

3. Quelle est la différence entre dependencies et devDependencies ?

Les dependencies sont nécessaires à l'app et partent dans le paquet livré (react, expo, zustand…). Les devDependencies sont des outils de développement (typescript, eslint, prettier, jest) qui servent avant l'exécution et ne partent pas dans l'app.

4. À quoi sert le lockfile, et faut-il l'éditer à la main ?

Il fige la version exacte de chaque paquet (et de leurs dépendances) pour que tout le monde installe rigoureusement le même arbre — fin du « ça marche sur ma machine ». On ne le modifie jamais à la main : c'est un fichier généré par pnpm, qu'on versionne.

5. Tu vois en plein usage de l'app un écran rouge : « undefined is not an object ». Type, lint ou exécution ?

Exécution (runtime) : l'erreur survient pendant que l'app tourne, pas avant. Une erreur de type ou de lint serait apparue plus tôt — dans l'éditeur, au type-check, ou au moment du commit — sans même lancer l'app.

À retenir

Le code que tu écris n'est pas celui qui s'exécute. TypeScript vérifie les types puis s'efface ; Babel traduit ton code moderne en JS simple ; Metro rassemble tout en un bundle et le sert au téléphone. pnpm installe les paquets (avec un lockfile qui fige les versions), dans un monorepo orchestré par Turborepo (tâches + cache). ESLint garde les règles (y compris des règles maison comme l'interdiction des couleurs en dur), Prettier formate, Husky vérifie avant chaque commit. Le tsconfig active le mode strict et l'alias @/* → src/*. Et surtout : sache reconnaître une erreur de type, de lint ou d'exécution — chacune arrive à un moment différent et te dit où chercher.

⚠️ Piège fréquent : tout mettre dans le même sac « erreur »

Le réflexe de débutant le plus coûteux est de traiter une erreur de type, de lint et d'exécution comme une seule et même chose, et de chercher au mauvais endroit. Si tu pars déboguer l'app en marche alors que TypeScript te parlait avant le démarrage, tu perds ton temps. Inversement, si tu attends que TypeScript signale un crash qui n'arrive qu'à l'exécution, tu attends pour rien — il a déjà disparu à ce moment-là. Localise d'abord le moment (compilation ? lint ? exécution ?), tu sauras instantanément quel outil interroger.

🔄 Transférable

Cette architecture — un compilateur/vérificateur, un traducteur, un assembleur (bundler), un gestionnaire de paquets avec lockfile, un linter, un formateur, des hooks de commit — n'est pas propre à React Native. Tu la retrouveras, sous d'autres noms, dans presque tout projet logiciel moderne : web, backend, applications de bureau. Les outils changent (Webpack ou Vite au lieu de Metro, npm ou yarn au lieu de pnpm), mais les rôles restent. Comprendre une fois « qui vérifie, qui traduit, qui assemble, qui surveille » te rend lisible n'importe quel projet inconnu.