Ein Überblick über ts-migrate - ein Tool zum Übersetzen von Großprojekten in TypeScript

Airbnb verwendet TypeScript (TS) offiziell für die Front-End-Entwicklung. Der Prozess der Implementierung von TypeScript und der Übersetzung einer ausgereiften Codebasis von Tausenden von JavaScript-Dateien in die Sprache ist jedoch keine Frage eines Tages. Die TS-Implementierung erfolgte nämlich in mehreren Phasen. Zuerst war es ein Vorschlag, nach einer Weile begann die Sprache in vielen Teams verwendet zu werden, dann trat die Einführung von TS in die Beta-Phase ein. Infolgedessen wurde TypeScript zur offiziellen Front-End-Entwicklungssprache von Airbnb. Erfahren Sie mehr über die Airbnb TS Umsetzungsprozess hier . Dieser Artikel beschreibt die Prozesse zur Übersetzung großer Projekte in TypeScript und eine Geschichte über ein spezielles Tool, ts-migrate, das bei Airbnb entwickelt wurde.











Migrationsstrategien



Die Übersetzung eines Großprojekts von JavaScript nach TypeScript ist eine Herausforderung. Bevor wir mit der Lösung begannen, haben wir zwei Strategien für den Wechsel von JS zu TS untersucht.



▍1. Hybride Migrationsstrategie



Mit diesem Ansatz wird eine schrittweise, dateiweise Übersetzung des Projekts in TypeScript durchgeführt. Während dieses Vorgangs werden Dateien bearbeitet, Tippfehler korrigiert und funktionieren so lange, bis das gesamte Projekt in TS übersetzt ist. Mit dem Parameter allowJS können Sie sowohl TypeScript-Dateien als auch JavaScript-Dateien in Ihrem Projekt haben. Dank dessen ist dieser Ansatz zur Übersetzung von JS-Projekten in TS durchaus praktikabel.



Mit einer hybriden Migrationsstrategie müssen Sie den Entwicklungsprozess nicht unterbrechen. Sie können das Projekt schrittweise Datei für Datei in TypeScript übersetzen. Wenn wir jedoch über ein Großprojekt sprechen, kann dieser Prozess lange dauern. Es erfordert auch Schulungen für Programmierer im gesamten Unternehmen. Programmierer müssen in die Besonderheiten des Projekts eingeführt werden.



▍2. Umfassende Migrationsstrategie



Bei diesem Ansatz wird ein Projekt, das vollständig in JavaScript geschrieben ist oder von dem ein Teil in TypeScript geschrieben ist, vollständig in ein TypeScript-Projekt umgewandelt. In diesem Fall müssen Sie den Typ anyund die Kommentare verwenden @ts-ignore, damit das Projekt fehlerfrei kompiliert werden kann. Im Laufe der Zeit kann der Code jedoch bearbeitet und weiterentwickelt werden, um geeignetere Typen zu verwenden.



Die übergreifende TypeScript-Migrationsstrategie bietet gegenüber der Hybridstrategie mehrere wesentliche Vorteile:



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


In Anbetracht des Vorstehenden scheint die allgegenwärtige Migration der hybriden Migration in jeder Hinsicht überlegen zu sein. Die umfassende Übersetzung einer ausgereiften Codebasis in TypeScript ist jedoch eine sehr schwierige Aufgabe. Um dies zu lösen, haben wir uns entschlossen, auf Skripte zurückzugreifen, um den Code zu ändern, auf die sogenannten "Codemods" ( Codemods ). Als wir anfingen, ein Projekt manuell in TypeScript zu übersetzen, bemerkten wir sich wiederholende Vorgänge, die automatisiert werden konnten. Wir haben für jede dieser Operationen Code-Mods geschrieben und diese in einer einzigen Migrationspipeline zusammengefasst.



Die Erfahrung zeigt, dass wir nicht 100% sicher sein können, dass nach der automatischen Übersetzung eines Projekts in TypeScript keine Fehler auftreten. Wir haben jedoch herausgefunden, dass die unten beschriebene Kombination von Schritten die besten Ergebnisse liefert und am Ende ein fehlerfreies TypeScript-Projekt erhalten hat. Mit Code-Mods konnten wir ein Projekt in TypeScript übersetzen, das über 50.000 Codezeilen enthält und durch über 1000 Dateien dargestellt wird. Wir haben einen Tag dafür gebraucht.



Basierend auf der in der folgenden Abbildung gezeigten Pipeline haben wir das Tool ts-migrate erstellt.





Ts-migrate-Codemods



Airbnb hat einen großen Teil seines Frontends mit React geschrieben . Aus diesem Grund beziehen sich einige Teile des Code-Mods auf reaktionsspezifische Konzepte. Das ts-migrate-Tool kann mit anderen Bibliotheken oder Frameworks verwendet werden, dies erfordert jedoch zusätzliche Konfiguration und Tests.



Übersicht über den Migrationsprozess



Lassen Sie uns die grundlegenden Schritte durchgehen, die Sie ausführen müssen, um ein Projekt von JavaScript in TypeScript zu übersetzen. Lassen Sie uns darüber sprechen, wie diese Schritte implementiert werden.



▍Schritt 1



Das erste, was jedes TypeScript-Projekt erstellt, ist a tsconfig.json. Ts-migrate kann dies bei Bedarf selbst tun. Für diese Datei gibt es eine Standardvorlage. Darüber hinaus ist ein Verifizierungssystem vorhanden, um sicherzustellen, dass alle Projekte konsistent konfiguriert sind. Hier ist ein Beispiel für eine Grundkonfiguration:



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


▍Schritt 2



Sobald die Datei dort tsconfig.jsonist, wo sie sein sollte, werden die Quelldateien umbenannt. Die Erweiterungen .js / .jsx ändern sich nämlich in .ts / .tsx. Dieser Schritt ist sehr einfach zu automatisieren. Dies ermöglicht es Ihnen, eine große Menge an Handarbeit loszuwerden.



▍Schritt 3



Jetzt ist es Zeit, die Code-Mods auszuführen! Wir nennen sie Plugins. Plugins für ts-migrate sind Code-Mods, die über den TypeScript-Sprachserver Zugriff auf zusätzliche Informationen haben. Plugins akzeptieren Strings als Eingabe und geben modifizierte Strings zurück. Die jscodeshift-Toolbox , die TypeScript-API, Zeichenfolgenverarbeitungswerkzeuge oder andere AST-Modifikationstools können zum Ausführen von Codetransformationen verwendet werden .



Nachdem Sie die oben genannten Schritte ausgeführt haben, überprüfen wir, ob Änderungen im Git-Verlauf ausstehen, und nehmen sie in das Projekt auf. Auf diese Weise können Sie Migrations-PRs in Commits aufteilen. Dies erleichtert das Verständnis der Vorgänge und hilft, Änderungen an Dateinamen zu verfolgen.



Übersicht der Pakete, aus denen ts-migrate besteht



Wir teilen ts-migrate in 3 Pakete auf:





Auf diese Weise konnten wir die Codetransformationslogik vom Kern des Systems trennen und viele Konfigurationen erstellen, um verschiedene Probleme zu lösen. Wir haben jetzt zwei Hauptkonfigurationen: Migration und Regoreore .



Der Zweck der Anwendung der Konfiguration migrationbesteht darin, das Projekt von JavaScript in TypeScript zu übersetzen. Die Konfiguration reignorewird verwendet, um das Kompilieren des Projekts durch einfaches Ignorieren von Fehlern zu ermöglichen. Diese Konfiguration ist nützlich, wenn Sie eine große Codebasis haben und verschiedene Dinge damit tun, wie die folgenden:



  • Update der TypeScript-Version.
  • Nehmen Sie größere Änderungen am Code vor oder überarbeiten Sie die Codebasis.
  • Verbesserte Typen einiger häufig verwendeter Bibliotheken.


Mit diesem Ansatz können wir das Projekt auch dann in TypeScript übersetzen, wenn Kompilierungsfehler generiert werden, die wir nicht sofort behandeln möchten. Es erleichtert auch das Aktualisieren von TypeScript oder der in Ihrem Code verwendeten Bibliotheken.



Beide Konfigurationen werden auf einem Server ausgeführt ts-migrate-server, der aus zwei Teilen besteht:



  • TSServer : Dieser Teil des Servers ist sehr ähnlich zu dem, was VSCode verwendet zwischen dem Editor und dem Sprachserver zu kommunizieren. Die neue Instanz des TypeScript-Sprachservers wird in einem separaten Prozess gestartet. Entwicklungswerkzeuge interagieren mit ihm über ein Sprachprotokoll .
  • Migrationstool : Dies ist der Code, der den Migrationsprozess ausführt und diesen Prozess koordiniert. Dieses Tool akzeptiert die folgenden Parameter:


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


Dieses Tool führt Folgendes aus:



  1. Analysieren der Datei tsconfig.json.
  2. Generieren von .ts-Dateien mit Quellcode.
  3. Senden Sie jede Datei an den TypeScript-Sprachserver, um die Datei zu diagnostizieren. Es gibt drei Arten der Diagnostik, der uns den Compiler gibt: semanticDiagnostics, syntacticDiagnosticsund suggestionDiagnostics. Wir verwenden diese Überprüfungen, um Problembereiche im Quellcode zu finden. Anhand des eindeutigen Diagnosecodes und der Zeilennummer in der Datei können wir die mögliche Art des Problems identifizieren und die erforderlichen Codeänderungen anwenden.
  4. Verarbeitung jeder Datei durch alle Plugins. Wenn sich der Text in der Datei auf Initiative des Plugins geändert hat, aktualisieren wir den Inhalt der Originaldatei und benachrichtigen den Sprachserver, dass die Datei geändert wurde.


Anwendungsbeispiele ts-migrate-serverfinden Sie in dem gefunden werden Beispiele Paket oder in dem Haupt- Paket . Es ts-migrate-exampleenthält auch grundlegende Plugin-Beispiele . Sie fallen in 3 Hauptkategorien:



  • Plugins basierend auf jscodeshift.
  • Plugins basierend auf dem TypeScript des Abstract Syntax Tree (AST).
  • Textverarbeitungs- Plugins .


Das Repository enthält eine Reihe von Beispielen, die den Prozess der Erstellung einfacher Plugins dieser Art demonstrieren sollen. Es zeigt auch ihre Verwendung in Kombination c ts-migrate-server. Hier ist ein Beispiel für eine Migrationspipeline , die Code transformiert. Der folgende Code wird an seiner Eingabe empfangen:



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


Und er gibt folgendes:



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


In diesem Beispiel hat ts-migrate drei Transformationen durchgeführt:



  1. Es kehrt die Reihenfolge der Zeichen in allen Bezeichnern um : first -> tsrif.
  2. Informationen zu den Typen in der Funktionsdeklaration hinzugefügt : function tlum(tsrif, dnoces) -> function tlum(tsrif: number, dnoces: number): number.
  3. Die Zeile wurde zum Code hinzugefügt console.log(‘args:${arguments}’);


Allzweck-Plugins



Die echten Plugins befinden sich in einem separaten Paket - ts-migrate-plugins . Schauen wir uns einige davon an. Wir haben zwei Plugins basierend auf jscodeshift: explicitAnyPluginund declareMissingClassPropertiesPlugin. Mit dem jscodeshift-Toolkit können Sie ASTs mithilfe des neu zusammengestellten Pakets in regulären Code konvertieren . Mit dieser Funktion können wir den toSource()in unseren Dateien enthaltenen Quellcode direkt aktualisieren.



Das expliziteAnyPlugin- Plugin ruft vom TypeScript-Sprachserver Informationen zu allen Fehlern semanticDiagnosticsund den Zeilen ab, in denen diese Fehler erkannt wurden. Anschließend wird diesen Zeilen die Typanmerkung hinzugefügt any. Mit diesem Ansatz können Sie Fehler beheben, da Sie den Typ verwendenanyermöglicht es Ihnen, Kompilierungsfehler zu beseitigen.



Hier ist ein Beispielcode vor der Verarbeitung:



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


Hier ist der gleiche Code, der vom Plugin verarbeitet wird:



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


Das declareMissingClassPropertiesPlugin nimmt alle Diagnosemeldungen mit einem Fehlercode auf 2339(können Sie erraten, was dieser Code bedeutet ?). Wenn Klassendeklarationen mit fehlenden Bezeichnern gefunden werden, werden sie dem Hauptteil der mit Anmerkungen versehenen Klasse hinzugefügt any. Aus dem Namen des Plugins können wir schließen, dass es nur für ES6-Klassen gilt .



Die nächste Kategorie von Plugins basiert auf AST TypeScript. Durch die Verarbeitung des AST können wir eine Reihe von Aktualisierungen generieren, die an der Quelldatei vorgenommen werden sollen. Beschreibungen dieser Updates sehen folgendermaßen aus:



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 };


Nach dem Generieren von Informationen zu den erforderlichen Aktualisierungen müssen diese nur in umgekehrter Reihenfolge in die Datei eingegeben werden. Wenn wir nach dieser Operation neuen Programmcode erhalten, aktualisieren wir die Quellcodedatei entsprechend.



Werfen wir einen Blick auf die nächsten AST-basierten Plugins. Das ist stripTSIgnorePluginund hoistClassStaticsPlugin.



Das stripTSIgnorePlugin- Plugin ist das erste Plugin, das in der Migrationspipeline verwendet wird. Es werden alle Kommentare aus der Datei entfernt.@ts-ignore(Mit diesen Kommentaren können wir den Compiler anweisen, Fehler in der nächsten Zeile zu ignorieren.) Wenn wir ein in JavaScript geschriebenes Projekt in TypeScript übersetzen, führt dieses Plugin keine Aktion aus. Wenn es sich jedoch um ein Projekt handelt, das teilweise in JS und teilweise in TS geschrieben ist (einige unserer Projekte befanden sich in einem ähnlichen Zustand), ist dies der erste Migrationsschritt, auf den nicht verzichtet werden kann. Erst nachdem die Kommentare entfernt wurden @ts-ignore, generiert der TypeScript-Compiler Diagnosefehlermeldungen, die behoben werden müssen.



Hier ist der Code, der in die Eingabe dieses Plugins eingeht:



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


Hier ist die Ausgabe:



const str3 = foo
  ? bar
  : baz;


Nachdem @ts-ignorewir die Kommentare entfernt haben, führen wir das hoistClassStaticsPlugin- Plugin aus . Es werden alle Klassendeklarationen durchlaufen. Das Plugin erkennt die Möglichkeit, Bezeichner oder Ausdrücke zu erhöhen, und stellt fest, ob eine bestimmte Zuweisungsoperation bereits auf Klassenebene angehoben wurde.



Um eine hohe Entwicklungsgeschwindigkeit zu gewährleisten und erzwungene Downgrades auf frühere Versionen des Projekts zu vermeiden, haben wir jedem Plugin und jeder ts-migrate eine Reihe von Komponententests zur Verfügung gestellt.



Reaktionsbezogene Plugins



Das ReactPropsPlugin- Plugin , das auf diesem großartigen Tool basiert , konvertiert Typinformationen von PropTypes in TypeScript-Typdeklarationen. Mit diesem Plugin müssen Sie nur .tsx-Dateien verarbeiten, die mindestens eine React-Komponente enthalten. Dieses Plugin sucht nach allen PropTypes-Deklarationen und versucht, sie mit ASTs und einfachen regulären Ausdrücken wie /number/oder mit komplexeren regulären Ausdrücken wie / objectOf $ / zu analysieren . Wenn es erkannt wird React-Komponente (Funktion oder basierend auf Klasse), wird es in eine Komponente umgewandelt, in der ein neuer Typ zur Eingabe von Parametern (Requisiten) verwendet wird : type Props = {…};. ReactDefaultPropsPlugin



Pluginist für die Implementierung des defaultProps- Musters in React-Komponenten verantwortlich . Wir verwenden einen speziellen Typ, um Eingabeparameter darzustellen, denen Standardwerte zugewiesen werden:



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];
};


Wir versuchen, die Requisiten zu finden, denen Standardwerte zugewiesen wurden, und kombinieren sie dann mit dem Typ, der die Requisiten für die Komponente beschreibt, die wir im vorherigen Schritt erstellt haben.



Das React-Ökosystem nutzt in großem Umfang die Konzepte des Zustands- und Komponentenlebenszyklus. Wir beschäftigen uns mit den Herausforderungen im Zusammenhang mit diesen Konzepten in den nächsten Plugins. Wenn die Komponente den Status hat, generiert das ReactClassStatePlugin- Plugin einen neuen Typ ( type State = any;), und das ReactClassLifecycleMethodsPlugin- Plugin kommentiert die Komponentenlebenszyklusmethoden mit den entsprechenden Typen. Die Funktionalität dieser Plugins kann erweitert werden, indem sie auch mit anypräziseren Typen ersetzt werden.



Diese Plugins können insbesondere verbessert werden, indem die Typunterstützung für Status und Eigenschaften erweitert wird. Wie sich herausstellte, sind ihre vorhandenen Funktionen jedoch ein guter Ausgangspunkt für die Implementierung der von uns benötigten Funktionen. Wir arbeiten hier auch nicht mit React-Hooks , da unsere Codebasis zu Beginn der Migration eine alte Version von React verwendet hat, die keine Hooks unterstützt.



Überprüfen Sie, ob das Projekt korrekt kompiliert wurde



Unser Ziel ist es, ein mit Basistypen ausgestattetes TypeScript-Projekt zu kompilieren, ohne das Verhalten des Programms zu ändern.



Nach all den Transformationen und Modifikationen kann sich herausstellen, dass unser Code ungleichmäßig formatiert ist, was dazu führen kann, dass einige Codeprüfungen mit dem Linter Fehler aufdecken. Unsere Frontend-Codebasis verwendet ein System, das auf Prettier und ESLint basiert. Prettier wird nämlich für die automatische Code-Formatierung verwendet, und ESLint hilft dabei, den Code auf Übereinstimmung mit den empfohlenen Entwicklungsansätzen zu überprüfen. All dies ermöglicht es uns, Probleme mit der Codeformatierung, die sich aus früheren Aktionen ergeben, schnell zu lösen, indem wir einfach das entsprechende Plugin verwenden.- eslintFixPlugin.



Der letzte Schritt in der Migrationspipeline besteht darin, zu überprüfen, ob alle TypeScript-Kompilierungsprobleme behoben wurden. Um mögliche Fehler zu finden und zu beheben, verwendet das tsIgnorePlugin- Plugin Informationen aus der semantischen Diagnose des Codes und der Zeilennummern und fügt dem Code Kommentare @ts-ignoremit Erläuterungen zu den Fehlern hinzu. Zum Beispiel könnte es so aussehen:



// @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;


Wir haben das System mit JSX-Syntaxunterstützung ausgestattet:



{*
// @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()}
/>


Wenn Sie über aussagekräftige Fehlermeldungen verfügen, können Sie Fehler leichter beheben und Codefragmente finden, auf die Sie achten müssen. Relevante Kommentare in Kombination mit $TSFixMeermöglichen es uns, wertvolle Daten über die Qualität des Codes zu sammeln und potenziell problematische Codefragmente zu finden. $TSFixMeIst der Typalias, den wir erstellt haben any. Und für Funktionen ist dies $TSFixMeFunction = (…args: any[]) => any;. Es wird empfohlen, die Verwendung eines Typs zu vermeiden. Die Verwendung dieses Typs anyhat uns jedoch dabei geholfen, den Migrationsprozess zu vereinfachen. Durch die Verwendung dieses Typs konnten wir genau wissen, welche Codefragmente verbessert werden mussten.



Es ist erwähnenswert, dass das Plugin eslintFixPluginzweimal ausgeführt wird. Zum ersten Mal vor GebrauchtsIgnorePluginda die Formatierung Nachrichten darüber beeinflussen kann, wo Kompilierungsfehler auftreten. Das zweite Mal erfolgt nach der Anwendung tsIgnorePlugin, da das Hinzufügen von Kommentaren zum Code @ts-ignorezu Formatierungsfehlern führen kann.



Zusätzliche Bemerkungen



Wir möchten Ihre Aufmerksamkeit auf einige Migrationsfunktionen lenken, die wir während der Arbeit bemerkt haben. Vielleicht ist es hilfreich, diese Funktionen zu kennen, wenn Sie mit Ihren Projekten arbeiten.



  • TypeScript 3.7 @ts-nocheck, TypeScript- . , .js-, .ts/.tsx-. , .
  • TypeScript 3.9 bietet Unterstützung für @ ts-expected-error- Kommentare . Wenn einer Codezeile ein solcher Kommentar vorangestellt wird, meldet TypeScript den entsprechenden Fehler nicht. Wenn in einer solchen Zeile kein Fehler vorliegt, werden @ts-expect-errorSie von TypeScript darüber informiert , dass kein Kommentar erforderlich ist. Die Airbnb-Codebasis wurde von Kommentaren @ts-ignorezu Kommentaren verschoben @ts-expect-error.


Ergebnis



Die Migration der Airbnb-Codebasis von JavaScript zu TypeScript ist noch nicht abgeschlossen. Wir haben einige alte Projekte, die immer noch durch JavaScript-Code dargestellt werden. $TSFixMeKommentare sind in unserer Codebasis immer noch häufig @ts-ignore.





JavaScript und TypeScript in Airbnb



Es sollte jedoch beachtet werden, dass die Verwendung von ts-migrate den Prozess der Übersetzung unserer Projekte von JS nach TS erheblich beschleunigte und die Produktivität unserer Arbeit erheblich verbesserte. Mit ts-migrate konnten sich Programmierer darauf konzentrieren, die Eingabe zu verbessern, anstatt jede Datei manuell zu verarbeiten. Derzeit werden ungefähr 86% unseres Front-End-Mono-Repositorys mit ungefähr 6 Millionen Codezeilen in TypeScript übersetzt. Wir erwarten, bis Ende dieses Jahres 95% zu erreichen.



Hier auf der Homepage des Projekt-Repositorys erfahren Sie, wie Sie ts-migrate installieren und ausführen. Wenn Sie Probleme mit ts-migratehaben oder Ideen zur Verbesserung dieses Tools haben, laden wir Sie ein, sich anzumelden.daran arbeiten!



Haben Sie jemals große Projekte von JavaScript nach TypeScript übersetzt?






All Articles