Una descripción general de ts-migrate: una herramienta para traducir proyectos a gran escala a TypeScript

Airbnb utiliza oficialmente TypeScript (TS) para el desarrollo front-end. Pero el proceso de implementar TypeScript y traducir un código base maduro de miles de archivos JavaScript al lenguaje no es cuestión de un día. Es decir, la implementación de TS se llevó a cabo en varias etapas. En un principio fue una propuesta, después de un tiempo el lenguaje comenzó a usarse en muchos equipos, luego la introducción de TS entró en la fase beta. Como resultado, TypeScript se convirtió en el lenguaje de desarrollo front-end oficial de Airbnb. Obtenga más información sobre el proceso de implementación de Airbnb TS aquí . Este artículo está dedicado a describir los procesos de traducción de grandes proyectos a TypeScript y una historia sobre una herramienta especializada, ts-migrate, desarrollada en Airbnb.











Estrategias migratorias



Traducir un proyecto a gran escala de JavaScript a TypeScript es un desafío. Antes de comenzar a resolverlo, estudiamos dos estrategias para cambiar de JS a TS.



▍1. Estrategia de migración híbrida



Con este enfoque, se lleva a cabo una traducción gradual, archivo por archivo, del proyecto a TypeScript. Durante este proceso, se editan los archivos, se corrigen los errores tipográficos y funcionan de esta manera hasta que todo el proyecto se traduce a TS. El parámetro allowJS le permite tener archivos TypeScript y JavaScript en su proyecto. Gracias a esto, este enfoque para traducir proyectos JS a TS es bastante viable.



Con una estrategia de migración híbrida, no tiene que pausar el proceso de desarrollo, puede gradualmente, archivo por archivo, traducir el proyecto a TypeScript. Pero, si hablamos de un proyecto a gran escala, este proceso puede llevar bastante tiempo. También requiere capacitación para programadores de toda la organización. Los programadores deberán conocer los detalles del proyecto.



▍2. Estrategia de migración integral



Este enfoque toma un proyecto escrito completamente en JavaScript, o una parte del cual está escrito en TypeScript, y lo transforma completamente en un proyecto de TypeScript. En este caso, deberá utilizar el tipo anyy los comentarios @ts-ignore, lo que permitirá que el proyecto se compile sin errores. Pero con el tiempo, el código se puede editar y pasar a utilizar tipos más adecuados.



La estrategia general de migración de TypeScript tiene varias ventajas significativas sobre la estrategia híbrida:



  • . , , . , TypeScript, , .
  • , . , , , . .


Teniendo en cuenta lo anterior, parecería que la migración generalizada es superior a la migración híbrida en todos los aspectos. Pero traducir un código base maduro a TypeScript de una manera que lo abarque todo es una tarea muy difícil. Para solucionarlo, decidimos recurrir a scripts para modificar el código, a los llamados "codemods" ( codemods ). Cuando comenzamos a traducir un proyecto a TypeScript, haciéndolo manualmente, notamos operaciones repetitivas que podrían automatizarse. Escribimos modificaciones de código para cada una de estas operaciones y las combinamos en una única tubería de migración.



La experiencia nos dice que no podemos estar 100% seguros de que después de la traducción automática de un proyecto a TypeScript, no habrá errores en él. Pero descubrimos que la combinación de pasos que se describen a continuación nos dio los mejores resultados y, al final, obtuvimos un proyecto de TypeScript sin errores. Usando mods de código, pudimos traducir a TypeScript un proyecto que contenía más de 50,000 líneas de código y representado por más de 1,000 archivos. Nos tomó un día hacer esto.



Basándonos en la canalización que se muestra en la siguiente figura, hemos creado la herramienta ts-migrate.





Ts-migrate codemods



Airbnb tiene una gran parte de su interfaz escrita usando React . Es por eso que algunas partes del mod de código están relacionadas con conceptos específicos de React. La herramienta ts-migrate se puede utilizar con otras bibliotecas o marcos, pero esto requerirá configuración y pruebas adicionales.



Descripción general del proceso de migración



Repasemos los pasos básicos que debe seguir para traducir un proyecto de JavaScript a TypeScript. Hablemos de cómo se implementan estos pasos.



▍Paso 1



Lo primero que crea cada proyecto de TypeScript es un tsconfig.json. Ts-migrate puede hacerlo solo si es necesario. Hay una plantilla estándar para este archivo. Además, existe un sistema de verificación para garantizar que todos los proyectos se configuren de manera coherente. A continuación, se muestra un ejemplo de una configuración básica:



{
  "extends": "../typescript/tsconfig.base.json",
  "include": [".", "../typescript/types"]
}


▍Paso 2



Una vez que el archivo tsconfig.jsonestá donde debería estar, se cambia el nombre de los archivos de origen. Es decir, las extensiones .js / .jsx cambian a .ts / .tsx. Este paso es muy fácil de automatizar. Esto le permite deshacerse de una gran cantidad de trabajo manual.



▍Paso 3



¡Y ahora es el momento de iniciar las modificaciones de código! Los llamamos complementos. Los complementos para ts-migrate son modificaciones de código que tienen acceso a información adicional a través del servidor de lenguaje TypeScript. Los complementos aceptan cadenas como entrada y devuelven cadenas modificadas. La caja de herramientas jscodeshift , la API de TypeScript, las herramientas de procesamiento de cadenas u otras herramientas de modificación de AST se pueden utilizar para realizar transformaciones de código .



Después de completar cada uno de los pasos anteriores, verificamos si hay cambios pendientes en el historial de Git y los incluimos en el proyecto. Esto le permite dividir los RP de migración en confirmaciones, lo que facilita la comprensión de lo que está sucediendo y ayuda a rastrear los cambios en los nombres de archivos.



Descripción general de los paquetes que componen ts-migrate



Dividimos ts-migrate en 3 paquetes:





Al hacer esto, pudimos separar la lógica de transformación de código del núcleo del sistema y pudimos crear muchas configuraciones diseñadas para resolver diferentes problemas. Ahora tenemos dos configuraciones principales: migración y re - ignorar .



El propósito de aplicar la configuración migrationes traducir el proyecto de JavaScript a TypeScript. Y la configuración reignorese usa para hacer posible compilar el proyecto simplemente ignorando cualquier error. Esta configuración es útil cuando tienes una base de código grande y haces diferentes cosas con ella, como las siguientes:



  • Actualización de la versión de TypeScript.
  • Realizar cambios importantes en el código o refactorizar la base de código.
  • Tipos mejorados de algunas bibliotecas de uso común.


Con este enfoque, podemos traducir el proyecto a TypeScript incluso si, al compilar, se generan errores que no planeamos abordar de inmediato. También facilita la actualización de TypeScript o las bibliotecas utilizadas en su código.



Ambas configuraciones se ejecutan en un servidor ts-migrate-serverque tiene dos partes:



  • TSServer : esta parte del servidor es muy similar a la que usa VSCode para comunicarse entre el editor y el servidor de idiomas. La nueva instancia del servidor de lenguaje TypeScript comienza en un proceso separado. Las herramientas de desarrollo interactúan con él mediante un protocolo de lenguaje .
  • Herramienta de migración : es el código que realiza y coordina el proceso de migración. Esta herramienta toma los siguientes parámetros:


interface MigrateParams {
  rootDir: string;          //    .
  config: MigrateConfig;    //  ,   
                            // .
  server: TSServer;         //   TSServer.
}


Esta herramienta hace lo siguiente:



  1. Analizando el archivo tsconfig.json.
  2. Creación de archivos .ts con código fuente.
  3. Envíe cada archivo al servidor de lenguaje TypeScript para diagnosticar este archivo. Hay tres tipos de diagnósticos, lo que nos da el compilador: semanticDiagnostics, syntacticDiagnosticsy suggestionDiagnostics. Usamos estas comprobaciones para encontrar áreas problemáticas en el código fuente. Basándonos en el código de diagnóstico único y el número de línea en el archivo, podemos identificar el posible tipo de problema y aplicar las modificaciones de código necesarias.
  4. Procesando cada archivo por todos los complementos. Si el texto en el archivo ha cambiado por iniciativa del complemento, actualizamos el contenido del archivo original y notificamos al servidor de idioma que el archivo ha sido cambiado.


Los ejemplos de uso ts-migrate-serverse pueden encontrar en el paquete de ejemplos o en el paquete principal . Es ts-migrate-exampletambién contiene básicas ejemplos de plugin . Se dividen en 3 categorías principales:



  • Complementos basados ​​en jscodeshift.
  • Complementos basados ​​en TypeScript de árbol de sintaxis abstracta (AST).
  • De procesamiento de texto plugins .


El repositorio contiene un conjunto de ejemplos destinados a demostrar el proceso de creación de complementos simples de todo este tipo. También muestra su uso en combinación c ts-migrate-server. A continuación, se muestra un ejemplo de una canalización de migración que transforma el código. El siguiente código se recibe en su entrada:



function mult(first, second) {
  return first * second;
}


Y da lo siguiente:



function tlum(tsrif: number, dnoces: number): number {
  console.log(`args: ${arguments}`);
  return tsrif * dnoces;
}


En este ejemplo, ts-migrate ha realizado 3 transformaciones:



  1. Se invierte el orden de los caracteres en todos los identificadores: first -> tsrif.
  2. Se agregó información acerca de los tipos en la declaración de función: function tlum(tsrif, dnoces) -> function tlum(tsrif: number, dnoces: number): number.
  3. Añadida la línea al código console.log(‘args:${arguments}’);


Complementos de propósito general



Los complementos reales se encuentran en un paquete separado: ts-migrate-plugins . Echemos un vistazo a algunos de ellos. Tenemos dos complementos basados ​​en jscodeshift: explicitAnyPluginy declareMissingClassPropertiesPlugin. La caja de herramientas jscodeshift le permite convertir AST a código normal usando el paquete refundido . Podemos usar la función toSource()para actualizar directamente el código fuente contenido en nuestros archivos.



El complemento explicitAnyPlugin extrae información del servidor de lenguaje TypeScript sobre todos los errores semanticDiagnosticsy las líneas en las que se detectaron esos errores. Luego, se agrega la anotación de tipo a estas líneas any. Este enfoque le permite corregir errores, ya que usar el tipoanyle permite deshacerse de los errores de compilación.



Aquí hay un código de muestra antes de procesarlo:



const fn2 = function(p3, p4) {}
const var1 = [];


Aquí está el mismo código procesado por el complemento:



const fn2 = function(p3: any, p4: any) {}
const var1: any = [];


El declareMissingClassPropertiesPlugin toma todos los mensajes de diagnóstico con un código de error 2339(¿puedes adivinar qué significa este código?) Y, si puede encontrar declaraciones de clase con identificadores faltantes, los agrega al cuerpo de la clase anotada any. Por el nombre del complemento, podemos concluir que es aplicable solo a las clases de ES6 .



La siguiente categoría de complementos se basa en AST TypeScript. Al procesar el AST, podemos generar una serie de actualizaciones que se realizarán en el archivo fuente. Las descripciones de estas actualizaciones tienen este aspecto:



type Insert = { kind: 'insert'; index: number; text: string };
type Replace = { kind: 'replace'; index: number; length: number; text: string };
type Delete = { kind: 'delete'; index: number; length: number };


Después de generar información sobre las actualizaciones necesarias, solo queda ingresarlas en el archivo en orden inverso. Si, después de realizar esta operación, recibimos un nuevo código de programa, actualizaremos el archivo de código fuente en consecuencia.



Echemos un vistazo a los siguientes complementos basados ​​en AST. Esto es stripTSIgnorePluginy hoistClassStaticsPlugin.



El complemento stripTSIgnorePlugin es el primer complemento que se utiliza en el proceso de migración. Elimina todos los comentarios del archivo.@ts-ignore(estos comentarios nos permiten decirle al compilador que ignore los errores que ocurren en la siguiente línea). Si estamos traduciendo un proyecto escrito en JavaScript a TypeScript, este complemento no realizará ninguna acción. Pero si estamos hablando de un proyecto que está escrito en parte en JS y en parte en TS (varios de nuestros proyectos estaban en un estado similar), entonces este es el primer paso de migración que no se puede evitar. Solo después de eliminar los comentarios @ts-ignore, el compilador de TypeScript podrá emitir mensajes de error de diagnóstico que deben corregirse.



Aquí está el código que entra en la entrada de este complemento:



const str3 = foo
  ? // @ts-ignore
    // @ts-ignore comment
    bar
  : baz;


Aquí está el resultado:



const str3 = foo
  ? bar
  : baz;


Después de deshacernos de los comentarios, @ts-ignoreejecutamos el complemento hoistClassStaticsPlugin . Pasa por todas las declaraciones de clase. El complemento detecta la posibilidad de generar identificadores o expresiones y descubre si una determinada operación de asignación ya se ha elevado al nivel de clase.



Para garantizar una alta velocidad de desarrollo y evitar degradaciones forzadas a versiones anteriores del proyecto, proporcionamos a cada complemento y ts-migrate un conjunto de pruebas unitarias.



Complementos relacionados con React



El reactPropsPlugin , que se basa en esta increíble herramienta, convierte la información de tipos de PropTypes a declaraciones de tipos de TypeScript. Con este complemento, solo necesita procesar archivos .tsx que contengan al menos un componente React. Este complemento busca todas las declaraciones PropTypes e intenta analizarlas usando AST y expresiones regulares simples como /number/, o usando expresiones regulares más complejas como / objectOf $ / . Cuando se detecta Reaccionar-componente (función o basado en clase), se transforma en un componente en el que se utiliza un nuevo tipo de parámetros de entrada (apoyos): type Props = {…};. ReactDefaultPropsPlugin



plug-ines responsable de implementar el patrón defaultProps en los componentes de React . Usamos un tipo especial para representar parámetros de entrada a los que se les dan valores predeterminados:



type Defined<T> = T extends undefined ? never : T;
type WithDefaultProps<P, DP extends Partial<P>> = Omit<P, keyof DP> & {
  [K in Extract<keyof DP, keyof P>]:
    DP[K] extends Defined<P[K]>
      ? Defined<P[K]>
      : Defined<P[K]> | DP[K];
};


Intentamos encontrar los accesorios a los que se les han asignado valores predeterminados y luego los combinamos con el tipo que describe los accesorios para el componente que creamos en el paso anterior.



El ecosistema React hace un uso extensivo de los conceptos de estado y ciclo de vida de los componentes. Abordamos los desafíos relacionados con estos conceptos en los próximos dos complementos. Entonces, si el componente tiene un estado, entonces el complemento reactClassStatePlugin genera un nuevo tipo ( type State = any;), y el complemento reactClassLifecycleMethodsPlugin anota los métodos del ciclo de vida del componente con los tipos correspondientes. La funcionalidad de estos complementos se puede ampliar, incluso equipándolos con la capacidad de reemplazarlos con anytipos más precisos.



Estos complementos se pueden mejorar, en particular, ampliando el soporte de tipo para el estado y las propiedades. Pero sus capacidades existentes, como resultó, son un buen punto de partida para implementar la funcionalidad que necesitamos. Además, no trabajamos con React hooks aquí , ya que al comienzo de la migración, nuestro código base usó una versión antigua de React que no admite hooks.



Comprobando que el proyecto está compilado correctamente



Nuestro objetivo es compilar un proyecto TypeScript equipado con tipos base sin cambiar el comportamiento del programa.



Después de todas las transformaciones y modificaciones, nuestro código puede llegar a tener un formato no uniforme, lo que puede llevar al hecho de que algunas verificaciones de código con un linter revelan errores. Nuestro código base de frontend utiliza un sistema basado en Prettier y ESLint. Es decir, Prettier se utiliza para el formateo automático de código, y ESLint ayuda a verificar el código para verificar que cumpla con los enfoques de desarrollo recomendados. Todo esto nos permite abordar rápidamente los problemas de formato de código que surgen de acciones anteriores, simplemente utilizando el complemento adecuado .- eslintFixPlugin.



El último paso en el proceso de migración es verificar que se hayan resuelto todos los problemas de compilación de TypeScript. Para encontrar y corregir errores potenciales, el complemento tsIgnorePlugin toma el diagnóstico semántico del código y los números de línea, y luego agrega comentarios al código @ts-ignorecon explicaciones de los errores. Por ejemplo, podría verse así:



// @ts-ignore ts-migrate(7053) FIXME: No index signature with a parameter of type 'string...
const { field1, field2, field3 } = DATA[prop];
// @ts-ignore ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const field2 = object.some_property;


Hemos equipado el sistema con soporte de sintaxis JSX:



{*
// @ts-ignore ts-migrate(2339) FIXME: Property 'NORMAL' does not exist on type 'typeof W... */}
<Text weight={WEIGHT.NORMAL}>
  some text
</Text>
<input
  id="input"
  // @ts-ignore ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'string'.
  name={getName()}
/>


Tener mensajes de error significativos a nuestra disposición facilita la corrección de errores y la búsqueda de fragmentos de código para estar atento. Los comentarios relevantes, en combinación con $TSFixMe, nos permiten recopilar datos valiosos sobre la calidad del código y encontrar fragmentos de código potencialmente problemáticos. $TSFixMeEs el alias de tipo que creamos any. Y para funciones, esto es $TSFixMeFunction = (…args: any[]) => any;. Se recomienda evitar el uso de un tipo any, pero su uso nos ayudó a simplificar el proceso de migración. El uso de este tipo nos ayudó a saber exactamente qué fragmentos de código debían mejorarse.



Vale la pena señalar que el complemento se eslintFixPluginejecuta dos veces. Primera vez antes de usartsIgnorePluginya que el formateo puede afectar los mensajes sobre dónde ocurren los errores de compilación. La segunda vez es después de la aplicación tsIgnorePlugin, ya que agregar comentarios al código @ts-ignorepuede provocar errores de formato.



Notas adicionales



Nos gustaría llamar su atención sobre un par de características de migración que notamos durante el trabajo. Quizás conocer estas características sea útil cuando trabaje con sus proyectos.



  • TypeScript 3.7 @ts-nocheck, TypeScript- . , .js-, .ts/.tsx-. , .
  • TypeScript 3.9 introduce soporte para comentarios de @ ts-esperan-error . Si una línea de código tiene como prefijo dicho comentario, TypeScript no informará el error correspondiente. Si no hay ningún error en dicha línea, TypeScript le informará @ts-expect-errorque no es necesario un comentario . El código base de Airbnb ha pasado de comentarios @ts-ignorea comentarios @ts-expect-error.


Salir



La migración del código base de Airbnb de JavaScript a TypeScript aún está en curso. Tenemos algunos proyectos antiguos que todavía están representados por código JavaScript. $TSFixMeLos comentarios siguen siendo comunes en nuestro código base @ts-ignore.





JavaScript y TypeScript en Airbnb



Pero debe tenerse en cuenta que el uso de ts-migrate aceleró enormemente el proceso de traducción de nuestros proyectos de JS a TS y mejoró enormemente la productividad de nuestro trabajo. Con ts-migrate, los programadores pudieron concentrarse en mejorar la escritura en lugar de procesar manualmente cada archivo. Actualmente, aproximadamente el 86% de nuestro mono-repositorio front-end, que tiene alrededor de 6 millones de líneas de código, se traduce a TypeScript. Esperamos llegar al 95% a fines de este año.



Aquí, en la página de inicio del repositorio de proyectos, puede aprender a instalar y ejecutar ts-migrate. Si encuentra algún problema en ts-migrate, o si tiene ideas para mejorar esta herramienta, lo invitamos a unirse.para trabajar en ello!



¿Alguna vez ha traducido grandes proyectos de JavaScript a TypeScript?






All Articles