Pourquoi Codegen rend obligatoire une approche basée sur les spécifications pour React Native TurboModules
Voyez comment React Native Codegen transforme la spécification TypeScript de TurboModule en un contrat à l’heure de la compilation, ce qui se passe lorsqu’on l’omet, et où s’arrête l’application de cette règle.
Sources: Spec First Isn’t a House Rule — the New Architecture Toolchain Enforces It
De nombreuses équipes qui gèrent des bibliothèques React Native incluent une règle dans leur guide de contribution : modifier la spécification TurboModule avant d’alterer tout code natif. Cela ressemble à une préférence stylistique, le genre de convention que l’on espère voir respectée par les contributeurs sous pression de délai. Avec la nouvelle architecture, il n’en est rien. Codegen dérive le pont natif à partir de la spécification, ce qui fait que c’est le processus de compilation lui-même qui détermine l’ordre des tâches. Cet article explique comment ce contrat est appliqué, ce qui se passe réellement sur iOS et Android lorsque quelqu’un tente de prendre ce raccourci, ainsi quels types de modules en sont exclus.
Une convention qui se révèle être une contrainte
Imaginons un mainteneur qui décide d’économiser du temps : il ajoute d’abord la méthode native, puis revient pour mettre à jour la spécification. Dans le cas d’un module géré par Codegen, ce plan échoue avant même de pouvoir être utile. Aucun outil de vérification ni aucun réviseur n’a besoin d’objecter ; c’est le compilateur et le code généré qui s’en chargent. La réflexion qui en découle est la partie importante : cette règle n’a jamais été une habitude d’équipe que l’outilchain tolère par hasard. C’est plutôt une propriété de l’outilchain que l’équipe a eu la chance d’documenter.
La différence dépasse le simple choix des mots. Une préférence peut être compromise au profit d’une échéance. Un contrat en temps de compilation ne le permet pas, et savoir avec lequel on a affaire change la manière dont on planifie les modifications d’une bibliothèque.
Le fichier de spécification au cœur d’un TurboModule
Chaque TurboModule d’architecture nouvelle dispose d’un seul fichier TypeScript, généralement src/Native<Nom>.ts, qui déclare chaque méthode franchissant la frontière entre JavaScript et le code natif. Un wrapper autour d’un SDK de connexion sociale pourrait l’appeler src/NativeNaverLogin.ts ; un wrapper pour les avis dans l’application pourrait utiliser src/NativeInAppReview.ts. Une entrée codegenConfig dans le package.json du paquet indique à Codegen où trouver ce fichier et quel type d’artefact en générer.
En règle générale, l’ordre est simple : toute méthode que JavaScript doit pouvoir appeler est d’abord déclarée dans la spécification, puis mise en œuvre de manière native. D’abord la spécification, ensuite le code natif, jamais l’inverse. Ce qui rend cet ordre incontournable, c’est le mécanisme chargé de traiter la spécification.
Comment Codegen transforme la spécification en obligations natives
Codegen est une transformation qui s’exécute au moment de la compilation. Il analyse la spécification TypeScript et génère une interface d’interconnexion pour chaque plateforme :
- Android : une classe abstraite, par exemple
NativeNaverLoginSpec. Votre module Kotlin ou Java étend cette classe générée plutôt que d’étendre directementReactContextBaseJavaModule, et chaque méthode de la spécification devient une méthode abstraite que vous devez implémenter. - iOS : un mécanisme d’interconnexion JSI en Objective-C++, comprenant une unité de traduction
NativeNaverLoginSpecJSIainsi qu’un protocole auquel votre implémentation en.mmdoit se conformer.
Le détail crucial est la direction de la dépendance. Le code natif ne décrit pas le pont ; il hérite d’un pont généré à partir de la spécification. JavaScript, quant à lui, n’a accès au code natif que par cette interface générée. Ce seul fait détermine ce qui se passe dans chacun des deux cas où les deux parties peuvent s’éloigner l’une de l’autre.
Une méthode native non déclarée dans la spécification
Si vous ajoutez une méthode du côté natif sans la mentionner dans la spécification, Codegen n’a rien généré pour elle. Il n’existe aucune méthode abstraite à surcharger sur Android, ni d’entrée de protocole sur iOS.
Sur Android, la manière la plus courante d’écrire une telle méthode est d’utiliser override, ce qui échoue à la compilation car la classe parente générée ne possède pas de membre correspondant. Même en supprimant le modificateur et en permettant à le fichier de se compiler, la méthode ne fait pas partie de l’interface générée, ce qui empêche JavaScript d’y accéder.
Sur iOS, l’échec est plus silencieux. La méthode existe dans votre fichier .mm et se compile correctement, mais elle n’est pas présente dans le protocole que le moteur d’exécution utilise pour exposer les méthodes, de sorte que JavaScript ne peut jamais y accéder. Une méthode inutilisable sans erreur visible est probablement pire qu’une erreur de compilation : rien ne vous indique qu’elle ne fonctionne pas tant que quelqu’un ne l’appelle pas depuis JavaScript et ne la trouve pas.
Une méthode de spécification sans implémentation native
L’approche inverse échoue également. Si la spécification déclare une méthode mais que le côté natif ne l’implémente pas, Codegen génère quand même le pont correspondant, et le proxy JavaScript expose volontiers cette méthode.
Au niveau d’Android, une classe qui étend la spécification abstraite générée sans implémenter l’une de ses méthodes abstraites est rejetée par le compilateur Kotlin ou Java, ce qui constitue le meilleur scénario car l’erreur est détectée tôt. Sur iOS, l’absence d’une méthode de protocole obligatoire ne provoque généralement qu’un avertissement du compilateur ; l’erreur réelle apparaît en temps de exécution sous forme d’une panne due à un sélecteur non reconnu lorsque JavaScript appelle cette méthode.
Quoi qu’il en soit, les deux parties sont liées entre elles lors de la génération du code. Vous ne pouvez pas conférer une nouvelle capacité à JavaScript en modifiant uniquement un des côtés. Ce qui vous empêche de le faire n’est pas un commentaire d’évaluation, mais bien le système de types ainsi que le code de liaison généré qui refuse de s’aligner.
Pourquoi cette règle s’applique à toutes les bibliothèques Codegen
Si la méthode « spécification en premier » n’était qu’une approche propre à certains projets, elle ne s’appliquerait que dans les dépôts dont les mainteneurs l’ont choisie. Mais son application est assurée par Codegen, qui fait partie de React Native. Toute bibliothèque permettant l’utilisation de Codegen grâce à un bloc codegenConfig dans la nouvelle architecture est soumise aux mêmes règles, que ses auteurs aient ou non envisagé les choses en termes de « spécification en premier ».
La documentation officielle de React Native reflète cela dans son libellé. En décrivant le dossier de sortie dans lequel Codegen écrit, elle présente la classe abstraite générée comme quelque chose que tout module Turbo Native doit implémenter. Le mot « requis » est clé : il s’agit d’une obligation vérifiée lors de la compilation du code, et non d’une recommandation. Les guides communautaires décrivent le même comportement du point de vue externe : une fois codegenConfig configuré, un fichier Native...Spec généré apparaît, nommé selon ce que codegenConfig spécifie dans package.json. Différents auteurs, différents répertoires, contrat identique.
Il existe également une confirmation utile en partant d’une direction opposée. En omettant complètement le champ codegenConfig de package.json, une installation via CocoaPods sur iOS révèle cette absence :
[Codegen] The "codegenConfig" field is not defined in package.json
Codegen passe alors complètement à côté de ce package. Le pipeline est conçu pour s’arrêter plutôt que d’essayer de trouver une solution de remplacement. La spécification n’est pas un document optionnel qui peut devenir obsolète ; c’est l’entrée autour de laquelle toute l’étape de génération est organisée.
En somme, l’approche « spécification en premier » est une conséquence structurelle de la manière dont le pipeline lit ses données d’entrée. Les bibliothèques suivent cette approche car elles respectent la structure de Codegen. Une bibliothèque qui semble l’ignorer est soit encore basée sur l’architecture ancienne, soit n’a pas du tout configuré codegenConfig.
Où le contrat cesse d’être applicable
Décrire cela comme un contrat de chaîne d’outils est exact, mais il est facile d’exagérer cette affirmation. Il convient de définir clairement les limites :
- Modules de pont hérités. Modules conçus sur le pont pré-JSI avec
RCTBridgeModulesur iOS ouReactContextBaseJavaModulesur Android, sans spécification définie, antérieurs à Codegen. Ils ne disposent d’aucune source de vérité générée, ce qui signifie qu’aucun mécanisme ne garantit l’ordre de traitement. La discipline est la seule solution en l’occurrence. - Pakets sans
codegenConfig. En l’absence de ce bloc, aucun artefact n’est généré, et la chaîne allant de la spécification à l’exécution native ne débute jamais.
type: "components" passent également par Codegen, il est donc raisonnable d’attendre une discipline similaire. Cependant, le raisonnement ci-dessus est basé sur les Turbo Native Modules (type: "modules"), et les artefacts générés pour les composants diffèrent ; par conséquent, considérez ce cas comme probable plutôt que comme établi, et vérifiez-le en vous référant à la documentation actuelle de votre version React Native.L’affirmation qui tient fermement est restreinte : pour les TurboModules de la Nouvelle Architecture générés par Codegen à partir d’une spécification JavaScript ou TypeScript, qui constitue la méthode officiellement documentée pour les créer, l’approche basée d’abord sur la spécification est imposée par le processus de compilation.
Il convient également de noter que les TurboModules basés sur Codegen ne sont pas la seule façon d’écrire des modules natifs aujourd’hui. Des alternatives comme Nitro Modules utilisent leur propre modèle de génération de code et de liaison ; l’article sur la manière dont les liaisons statiques de Nitro se comparent aux TurboModules aborde ce compromis.
Le flux de travail réellement imposé par la compilation
Lorsque le mécanisme est clair, l’ordre des étapes n’est plus une question de jugement. Ajouter une méthode à un TurboModule suit une direction bien précise :
- Modifier la spécification dans
src/Native<Name>.tspour déclarer la nouvelle méthode et ses types. - Lancer Codegen :
pod installgénère à nouveau les fichiers d’intégration pour iOS, et une synchronisation ou compilation Gradle génère les artefacts pour Android.
Quelques habitudes pratiques en découlent :
- Vérifier les modifications des spécifications au même titre que les changements d’API. La spécification constitue en fait le contrat natif public de la bibliothèque, donc ses modifications méritent la même attention qu’un changement dans les types JavaScript exportés.
- Régénérer avant de déboguer. Lorsqu’une méthode native semble absente, un code généré obsolète provenant d’un
pod installmanqué ou d’une synchronisation Gradle est une cause fréquente. - Exécuter les builds des deux plateformes dans le CI. Comme iOS peut échouer silencieusement tandis qu’Android affiche des erreurs visibles, un build Android réussi ne prouve pas à lui seul que le pont de communication est complet.
Conclusion
Connaître celui qui « possède » la règle priorisant les spécifications n’est pas de l’obsession pour les détails. Une fois que vous comprenez qu’il s’agit d’une question technique, vous cessez de le considérer comme un conseil à prendre en compte face à une échéance. Il n’existe pas de version « juste cette fois » permettant d’ajouter d’abord la méthode native et de corriger les spécifications ensuite, car cette astuce ne produit pas de code fonctionnel.
- Codegen génère le pont natif à partir des spécifications TypeScript, de sorte que le code natif met en œuvre les spécifications plutôt que de définir ce pont.
- Toute déviation, dans un sens ou dans l’autre, échoue : parfois au moment de la compilation, parfois silencieusement sur iOS, et parfois en provoquant une panne en temps de exécution.
- Le contrat couvre les TurboModules pris en charge par Codegen ; les modules et paquets de ponts anciens sans
codegenConfigen sont exclus, et les composants Fabric nécessitent leur propre vérification.