Inicio / Artículos

Por qué Codegen hace obligatorio el enfoque basado en especificaciones para React Native TurboModules

Vea cómo React Native Codegen convierte la especificación TypeScript de TurboModule en un contrato en tiempo de compilación, qué falla al omitirlo y dónde deja de aplicarse la regla.

Editor: REACTAPP.TOP8 min de lectura1819 palabras

Fuentes: Spec First Isn’t a House Rule — the New Architecture Toolchain Enforces It

Muchos equipos que mantienen bibliotecas de React Native incluyen una regla en su guía para contribuidores: cambiar la especificación de TurboModule antes de tocar cualquier código nativo. Parece una preferencia estilística, ese tipo de convención que uno espera que los contribuidores respeten bajo presión de plazos. En la Nueva Arquitectura no es nada por el estilo. Codegen deriva el puente nativo a partir de la especificación, por lo que la propia compilación decide el orden de los trabajos. Este artículo explica cómo se aplica ese contrato, qué ocurre realmente en iOS y Android cuando alguien intenta tomar el atajo, y qué tipos de módulos quedan fuera de él.

Una convención que resulta ser una restricción

Imaginemos a un mantenedor que decide ahorrar tiempo: agrega primero el método nativo y luego vuelve atrás para actualizar la especificación. En un módulo respaldado por Codegen, ese plan fracasa antes incluso de ser útil. No es necesario que ningún analizador ni revisor se oponga; el compilador y el código generado se encargan de ello. La conclusión que surge es la parte importante: esa regla nunca fue un hábito del equipo al que casualmente toleraba la cadena de herramientas. Es una propiedad de dicha cadena de herramientas que el equipo decidió anotar.

La diferencia va más allá de la redacción. Una preferencia puede cederse ante un plazo límite. Un contrato en tiempo de compilación no puede hacerlo, y saber con cuál se está lidiando cambia la forma en que se planifican los cambios en una biblioteca.

El archivo de especificación en el centro de un TurboModule

Cada TurboModule de nueva arquitectura cuenta con un único archivo en TypeScript, convencionalmente src/Native<Nombre>.ts, que declara cada método que cruza la frontera entre JavaScript y el código nativo. Un wrapper alrededor de un SDK de inicio de sesión social podría denominarse src/NativeNaverLogin.ts; un wrapper para reseñas en tiendas integradas podría usar src/NativeInAppReview.ts. Una entrada codegenConfig en el package.json del paquete indica a Codegen dónde encontrar ese archivo y qué tipo de artefacto generar a partir de él.

Como regla general, el orden es sencillo: cualquier método que JavaScript deba poder llamar se declara primero en la especificación y solo luego se implementa de forma nativa. Primero la especificación, luego el código nativo; nunca al revés. Lo que hace que ese orden sea ineludible es el mecanismo encargado de procesar la especificación.

Cómo Codegen convierte la especificación en obligaciones nativas

Codegen es una transformación que se ejecuta en el momento de la compilación. Analiza la especificación de TypeScript y genera la interfaz de conexión para cada plataforma:

  • Android: una clase abstracta, por ejemplo NativeNaverLoginSpec. Tu módulo en Kotlin o Java extiende esta clase generada en lugar de extender directamente ReactContextBaseJavaModule, y cada método de la especificación se convierte en un método abstracto que debes implementar.
  • iOS: un enlace JSI de Objective-C++, que incluye una unidad de traducción NativeNaverLoginSpecJSI y un protocolo al que tu implementación en .mm debe ajustarse.

El detalle clave es la dirección de la dependencia. El código nativo no describe el puente; hereda un puente que fue generado a partir de la especificación. JavaScript, a su vez, accede al código nativo únicamente a través de esa superficie generada. Ese único hecho determina lo que ocurre en cada una de las dos formas en que las partes pueden separarse.

Un método nativo que la especificación no declara

Si agrega un método al lado nativo sin incluirlo en la especificación, Codegen no ha generado nada para él. No existe un método abstracto que pueda sobrescribirse en Android ni una entrada de protocolo en iOS.

En Android, la forma más común de escribir tal método es con override, lo cual falla al compilar porque la clase padre generada no cuenta con un miembro correspondiente. Incluso si elimina el modificador y el archivo se compila, el método no forma parte de la superficie generada, por lo que JavaScript no tiene forma de acceder a él.

En iOS, el fallo es más silencioso. El método existe en tu archivo .mm y se compila sin problemas, pero no está incluido en el protocolo que utiliza el entorno de ejecución para exponer métodos, por lo que JavaScript nunca puede acceder a él. Un método inactivo e invisible es, sin duda, peor que un error de compilación: nada indica que esté roto hasta que alguien lo llama desde JavaScript y descubre que falta.

Un método de especificación sin implementación nativa

También falla el caso opuesto. Si la especificación declara un método pero el lado nativo no lo implementa, Codegen sigue generando el puente correspondiente, y el proxy de JavaScript expone felizmente ese método.

En Android, el compilador de Kotlin o Java rechaza a una clase que extiende la especificación abstracta generada sin implementar ninguno de sus métodos abstractos, lo cual es lo mejor ya que el error se detecta temprano. En iOS, la falta de un método de protocolo requerido suele generar solo una advertencia del compilador; el verdadero error aparece en tiempo de ejecución como un fallo por selector no reconocido cuando JavaScript llama al método.

De cualquier manera, las dos partes se vinculan entre sí cuando se genera el código. No puedes darle a JavaScript una nueva capacidad editando solo un lado. Lo que te lo impide no es un comentario de revisión, sino el sistema de tipos y el código generador que se niegan a coincidir.

Por qué la regla aplica a todas las bibliotecas Codegen

Si el enfoque basado en las especificaciones fuera simplemente un estilo propio de algún proyecto, solo se aplicaría en los repositorios cuyos mantenedores lo eligieran. Pero la aplicación de estas reglas proviene de Codegen, y Codegen pertenece a React Native. Toda biblioteca que habilite Codegen mediante un bloque codegenConfig en la nueva arquitectura está sujeta a las mismas normas, independientemente de que sus creadores hayan pensado alguna vez en términos de “especificaciones primero” o no.

La documentación oficial de React Native refleja esto en su redacción. Al describir la carpeta de salida en la que escribe Codegen, presenta la clase abstracta generada como algo que un módulo Turbo Native debe implementar. “Requerido” es la palabra clave: se trata de una obligación que se verifica al compilar el código, no de una recomendación. Las guías de la comunidad describen el mismo comportamiento desde el exterior: una vez que se configura codegenConfig, aparece un archivo Native...Spec generado, cuyo nombre proviene de lo que codegenConfig especifica en package.json. Diferentes autores, diferentes repositorios, contrato idéntico.

También existe una confirmación útil desde la dirección opuesta. Si se omite por completo el campo codegenConfig en package.json, al ejecutar una instalación de CocoaPods en iOS se informará sobre esta falta:

[Codegen] The "codegenConfig" field is not defined in package.json

Codegen entonces omite por completo ese paquete. La cadena de procesamiento está diseñada para detenerse en lugar de intentar encontrar una solución intermedia. La especificación no es un documento opcional que pueda quedar obsoleto; es la entrada en torno a la cual se organiza todo el proceso de generación.

En conjunto, el enfoque basado primero en la especificación es una consecuencia estructural de cómo la cadena de procesamiento lee sus datos de entrada. Las bibliotecas lo siguen porque adoptan la estructura de Codegen. Una biblioteca que parece ignorarlo está o bien aún utilizando la arquitectura heredada o bien no ha configurado codegenConfig en absoluto.

Dónde deja de aplicarse el contrato

Describir esto como un contrato del entorno de herramientas es preciso, pero es fácil exagerar esta afirmación. Vale la pena establecer claramente los límites:

  • Módulos de puente heredados. Módulos desarrollados sobre el puente anterior a JSI con RCTBridgeModule en iOS o ReactContextBaseJavaModule en Android, sin especificaciones definidas, que existían antes de Codegen. No cuentan con una fuente de verdad generada, por lo que nada impone mecánicamente el orden. La disciplina es lo único que existe en este caso.
  • Paquetes sin codegenConfig. Sin ese bloque, no se generan artefactos alguno, y la cadena que va desde las especificaciones hasta las obligaciones nativas nunca se inicia.
  • Componentes de vista de Fabric. Los componentes declarados con type: "components" también pasan por Codegen, por lo que es razonable esperar una disciplina similar. Sin embargo, el razonamiento anterior se basa en los módulos Turbo Native (type: "modules"), y los artefactos generados para los componentes son diferentes, así que considere este caso como probable y no como algo establecido, verificándolo en la documentación actual de su versión de React Native.
  • La afirmación que se mantiene firmemente es limitada: en el caso de la Nueva Arquitectura, los TurboModules generados por Codegen a partir de una especificación en JavaScript o TypeScript —que es la forma oficialmente documentada para crearlos— aplican el enfoque de especificación primero durante el proceso de compilación.

    También cabe señalar que los TurboModules basados en Codegen no son la única forma de crear módulos nativos hoy en día. Alternativas como Nitro Modules utilizan su propio modelo de generación de código y vinculación; el artículo sobre cómo se comparan los enlaces estáticos de Nitro con los de TurboModules aborda ese equilibrio.

    El flujo de trabajo que realmente impone el sistema de compilación

    Una vez que se comprende el mecanismo, el orden de los pasos deja de ser cuestión de juicio. Agregar un método a un TurboModule sigue una dirección específica:

    1. Editar la especificación en src/Native<Name>.ts para declarar el nuevo método y sus tipos.
    2. Ejecutar Codegen: pod install regenera los archivos de enlace para iOS, y una sincronización o compilación con Gradle regenera los artefactos para Android.
  • Implemente las firmas recién generadas en cada plataforma. Los pasos para Android e iOS son independientes y pueden realizarse en paralelo, pero ambos dependen del paso 2.
  • Vuelva a exportar el método desde la API JavaScript pública de la biblioteca.
  • A continuación se presentan algunos hábitos prácticos que surgen de esto:

    • Revise los cambios en las especificaciones al igual que los cambios en la API. Las especificaciones constituyen, de hecho, el contrato nativo público de la biblioteca, por lo que sus cambios merecen la misma atención que los cambios en los tipos JavaScript exportados.
    • Regenere antes de depurar. Cuando parece que no existe un método nativo, el código generado obsoleto proveniente de una ejecución omitida de pod install o de la sincronización de Gradle es una causa común.
    • Ejecute las compilaciones para ambas plataformas en CI. Dado que iOS puede fallar de forma silenciosa mientras Android lo hace de manera evidente, una compilación exitosa solo para Android no demuestra que el puente funcione correctamente.

    Conclusión

    Saber quién “posee” la regla de priorizar las especificaciones no es pedantería. Una vez que se comprende que se trata de un proceso mecánico, deja de considerarse como un consejo para sopesarlo frente a una fecha límite. No existe una versión de “solo esta vez” para añadir primero el método nativo y corregir las especificaciones más tarde, ya que este atajo no genera código funcional.

    • Codegen genera el puente nativo a partir de las especificaciones de TypeScript, por lo que el código nativo implementa dichas especificaciones en lugar de definir el puente.
    • Cualquier desviación en cualquiera de las direcciones fracasa: a veces en tiempo de compilación, otras veces silenciosamente en iOS y, en ocasiones, como un fallo en tiempo de ejecución.
    • El contrato abarca los TurboModules respaldados por Codegen; los módulos y paquetes de puente heredados que no cuentan con codegenConfig quedan fuera de su alcance, y los componentes Fabric necesitan su propia verificación.
  • Mantenga la filosofía de “primero las especificaciones” en su guía de contribuciones, pero exprésela como una descripción de cómo funciona el proceso de compilación en lugar de como una solicitud de buen comportamiento. La compilación la hará cumplir sin importar lo que diga el documento.