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.
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.
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.
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.
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 :
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.
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.
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).
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égorie | Exemples réels du projet | Rô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.
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.
{
"$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.
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.
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.
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 :
{
"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 ».
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'erreur | Qui parle | Quand | À 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.
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.
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.
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.
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.
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.
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.
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.