Ikhtisar ts-migrate - alat untuk menerjemahkan proyek skala besar ke TypeScript

Airbnb secara resmi menggunakan TypeScript (TS) untuk pengembangan front-end. Tetapi proses penerapan TypeScript dan menerjemahkan basis kode yang matang dari ribuan file JavaScript ke dalam bahasa bukanlah pekerjaan sehari-hari. Yakni, implementasi TS berlangsung dalam beberapa tahap. Awalnya berupa proposal, setelah beberapa saat bahasa mulai digunakan di banyak tim, kemudian pengenalan TS memasuki fase beta. Hasilnya, TypeScript menjadi bahasa pengembangan front-end resmi Airbnb. Pelajari lebih lanjut tentang proses implementasi TS Airbnb di sini . Artikel ini dikhususkan untuk menjelaskan proses menerjemahkan proyek besar ke dalam TypeScript dan cerita tentang alat khusus, ts-migrate, yang dikembangkan di Airbnb.











Strategi migrasi



Menerjemahkan proyek skala besar dari JavaScript ke TypeScript itu menantang. Sebelum kami mulai menyelesaikannya, kami mempelajari dua strategi untuk beralih dari JS ke TS.



▍1. Strategi migrasi hibrida



Dengan pendekatan ini, penerjemahan file-demi-file secara bertahap ke dalam TypeScript dilakukan. Dalam proses ini, mereka mengedit file, memperbaiki kesalahan pengetikan, dan bekerja dengan cara ini hingga seluruh proyek diterjemahkan ke TS. Parameter allowJS memungkinkan Anda memiliki file TypeScript dan file JavaScript dalam proyek Anda. Berkat ini, pendekatan untuk menerjemahkan proyek JS ke TS cukup layak.



Dengan strategi migrasi hybrid, Anda tidak perlu menjeda proses pengembangan, Anda dapat secara bertahap, file demi file, menerjemahkan proyek ke TypeScript. Tetapi, jika kita berbicara tentang proyek berskala besar, proses ini bisa memakan waktu lama. Ini juga membutuhkan pelatihan untuk programmer di seluruh organisasi. Pemrogram perlu diperkenalkan dengan spesifikasi proyek.



▍2. Strategi migrasi yang komprehensif



Pendekatan ini mengambil proyek yang seluruhnya ditulis dalam JavaScript, atau satu bagiannya ditulis dalam TypeScript, dan mengubahnya sepenuhnya menjadi proyek TypeScript. Dalam kasus ini, Anda perlu menggunakan type anydan comments @ts-ignore, yang akan memungkinkan proyek untuk dikompilasi tanpa kesalahan. Namun seiring waktu, kode dapat diedit dan beralih menggunakan jenis yang lebih sesuai.



Strategi migrasi TypeScript yang menyeluruh memiliki beberapa keunggulan signifikan dibandingkan strategi hibrid:



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


Mempertimbangkan hal di atas, tampaknya migrasi pervasif lebih unggul daripada migrasi hibrida dalam segala hal. Tetapi menerjemahkan basis kode yang matang ke TypeScript dengan cara yang mencakup semua adalah tugas yang sangat sulit. Untuk mengatasinya, kami memutuskan untuk menggunakan skrip untuk memodifikasi kode, ke apa yang disebut "codemods" ( codemods ). Ketika kami pertama kali mulai menerjemahkan proyek ke TypeScript, melakukannya secara manual, kami melihat operasi berulang yang dapat diotomatiskan. Kami menulis mod kode untuk setiap operasi ini dan menggabungkannya menjadi satu pipeline migrasi.



Pengalaman memberi tahu kami bahwa kami tidak dapat 100% yakin bahwa setelah terjemahan otomatis proyek ke TypeScript, tidak akan ada kesalahan di dalamnya. Tetapi kami menemukan bahwa kombinasi langkah-langkah yang dijelaskan di bawah ini memberi kami hasil terbaik dan, pada akhirnya, mendapatkan proyek TypeScript yang bebas dari kesalahan. Dengan menggunakan mod kode, kami dapat menerjemahkan ke dalam TypeScript sebuah proyek yang berisi lebih dari 50.000 baris kode dan diwakili oleh lebih dari 1.000 file. Kami butuh satu hari untuk melakukan ini.



Berdasarkan pipeline yang ditunjukkan pada gambar berikut, kami telah membuat alat ts-migrate.





Codemods-migrate



Di Airbnb, sebagian besar front-end ditulis menggunakan React . Inilah sebabnya mengapa beberapa bagian dari mod kode terkait dengan konsep khusus React. Alat ts-migrate dapat digunakan dengan pustaka atau kerangka kerja lain, tetapi ini akan membutuhkan konfigurasi dan pengujian tambahan.



Sekilas tentang proses migrasi



Mari kita telusuri langkah-langkah utama yang perlu Anda ikuti untuk menerjemahkan proyek dari JavaScript ke TypeScript. Mari kita bicara tentang bagaimana langkah-langkah ini diterapkan.



▍Langkah 1



Hal pertama yang dibuat oleh setiap proyek TypeScript adalah file tsconfig.json. Ts-migrate dapat melakukannya sendiri jika diperlukan. Ada template standar untuk file ini. Selain itu, sistem verifikasi tersedia untuk memastikan bahwa semua proyek dikonfigurasi secara konsisten. Berikut contoh konfigurasi dasar:



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


▍Langkah 2



Setelah file tsconfig.jsonberada di tempatnya, file sumber diganti namanya. Yaitu, ekstensi .js / .jsx berubah menjadi .ts / .tsx. Langkah ini sangat mudah untuk diotomatisasi. Ini memungkinkan Anda untuk menghilangkan banyak pekerjaan manual.



▍Langkah 3



Sekarang saatnya menjalankan mod kode! Kami menyebutnya plugin. Plugin untuk ts-migrate adalah mod kode yang memiliki akses ke informasi tambahan melalui server bahasa TypeScript. Plugin menerima string sebagai input dan mengembalikan string yang dimodifikasi. Kotak alat jscodeshift , API TypeScript, alat pemrosesan string, atau alat modifikasi AST lainnya dapat digunakan untuk melakukan transformasi kode .



Setelah menyelesaikan setiap langkah di atas, kami memeriksa untuk melihat apakah ada perubahan yang tertunda dalam sejarah Git dan memasukkannya ke dalam proyek. Ini memungkinkan Anda untuk membagi PR migrasi menjadi komit, yang membuatnya lebih mudah untuk memahami apa yang terjadi dan membantu melacak perubahan dalam nama file.



Gambaran umum paket yang membentuk ts-migrate



Kami membagi ts-migrate menjadi 3 paket:





Dengan melakukan ini, kami dapat memisahkan logika transformasi kode dari inti sistem dan dapat membuat banyak konfigurasi yang dirancang untuk memecahkan masalah yang berbeda. Kami sekarang memiliki dua konfigurasi utama: migrasi dan pengaturan kembali .



Tujuan penerapan konfigurasi migrationadalah untuk menerjemahkan proyek dari JavaScript ke TypeScript. Dan konfigurasi reignoretersebut digunakan untuk memungkinkan dilakukannya kompilasi proyek hanya dengan mengabaikan semua kesalahan. Konfigurasi ini berguna jika Anda memiliki basis kode yang besar dan melakukan berbagai hal dengannya, seperti berikut ini:



  • Pembaruan versi TypeScript.
  • Membuat perubahan besar pada kode atau memfaktorkan ulang basis kode.
  • Jenis yang ditingkatkan dari beberapa pustaka yang umum digunakan.


Dengan pendekatan ini, kita dapat menerjemahkan proyek ke TypeScript bahkan jika, saat kompilasi, terjadi kesalahan yang tidak segera kita tangani. Ini juga memudahkan untuk memperbarui TypeScript atau pustaka yang digunakan dalam kode Anda.



Kedua konfigurasi tersebut berjalan di server ts-migrate-serveryang memiliki dua bagian:



  • TSServer : Bagian server ini sangat mirip dengan yang digunakan VSCode untuk berkomunikasi antara editor dan server bahasa. Contoh baru dari server bahasa TypeScript dimulai dalam proses terpisah. Alat pengembangan berinteraksi dengannya menggunakan protokol bahasa .
  • Alat migrasi : Ini adalah kode yang melakukan proses migrasi dan mengoordinasikan proses ini. Alat ini mengambil parameter berikut:


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


Alat ini melakukan hal berikut:



  1. Mengurai file tsconfig.json.
  2. Membuat file .ts dengan kode sumber.
  3. Kirim setiap file ke server bahasa TypeScript untuk mendiagnosis file ini. Ada tiga jenis diagnosa, yang memberi kami compiler: semanticDiagnostics, syntacticDiagnosticsdan suggestionDiagnostics. Kami menggunakan pemeriksaan ini untuk menemukan area masalah dalam kode sumber. Berdasarkan kode diagnostik unik dan nomor baris dalam file, kami dapat mengidentifikasi kemungkinan jenis masalah dan menerapkan modifikasi kode yang diperlukan.
  4. Memproses setiap file dengan semua plugin. Jika teks dalam file telah berubah atas inisiatif plugin, kami memperbarui konten file asli dan memberi tahu server bahasa bahwa file tersebut telah diubah.


Contoh penggunaan ts-migrate-serverdapat ditemukan di paket contoh atau di paket utama . Ini ts-migrate-examplejuga berisi contoh plugin dasar . Mereka terbagi dalam 3 kategori utama:



  • Plugin berdasarkan jscodeshift.
  • Plugin berdasarkan pada TypeScript Pohon Sintaks Abstrak (AST).
  • Plugin pemrosesan teks.


Repositori berisi sekumpulan contoh yang ditujukan untuk mendemonstrasikan proses pembuatan plugin sederhana dari semua jenis ini. Ini juga menunjukkan penggunaannya dalam kombinasi c ts-migrate-server. Berikut adalah contoh pipeline migrasi yang mengubah kode. Kode berikut diterima pada masukannya:



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


Dan dia memberikan yang berikut ini:



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


Dalam contoh ini, ts-migrate telah melakukan 3 transformasi:



  1. Membalikkan urutan karakter dalam semua pengidentifikasi: first -> tsrif.
  2. Ditambahkan informasi tentang jenis dalam deklarasi fungsi: function tlum(tsrif, dnoces) -> function tlum(tsrif: number, dnoces: number): number.
  3. Menambahkan baris ke kode console.log(‘args:${arguments}’);


Plugin tujuan umum



Plugin sebenarnya berada dalam paket terpisah - ts-migrate-plugins . Mari kita lihat beberapa di antaranya. Kami memiliki dua plugin berdasarkan jscodeshift: explicitAnyPlugindan declareMissingClassPropertiesPlugin. The jscodeshift toolkit memungkinkan Anda untuk mengkonversi AST untuk kode biasa menggunakan perombakan paket . Kita dapat menggunakan fungsi tersebut toSource()untuk secara langsung memperbarui kode sumber yang terdapat dalam file kita.



Plugin eksplisitAnyPlugin mengambil informasi dari server bahasa TypeScript tentang semua kesalahan semanticDiagnosticsdan baris di mana kesalahan itu terdeteksi. Kemudian jenis anotasi ditambahkan ke baris ini any. Pendekatan ini memungkinkan Anda untuk memperbaiki kesalahan, karena menggunakan tipeanymemungkinkan Anda untuk menghilangkan kesalahan kompilasi.



Berikut beberapa contoh kode sebelum diproses:



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


Berikut adalah kode yang sama yang diproses oleh plugin:



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


The declareMissingClassPropertiesPlugin mengambil semua pesan diagnostik dengan kode kesalahan 2339(Anda bisa menebak apa kode ini berarti ?) Dan, jika dapat menemukan deklarasi kelas dengan pengidentifikasi yang hilang, menambahkan mereka ke tubuh kelas dijelaskan any. Dari nama pluginnya, kita dapat menyimpulkan bahwa itu hanya berlaku untuk kelas ES6 .



Kategori plugin berikutnya didasarkan pada AST TypeScript. Dengan memproses AST, kita dapat menghasilkan serangkaian pembaruan yang akan dibuat ke file sumber. Deskripsi pembaruan ini terlihat seperti ini:



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


Setelah menghasilkan informasi tentang pembaruan yang diperlukan, tinggal memasukkannya ke dalam file dalam urutan terbalik. Jika, setelah melakukan operasi ini, kami menerima kode program baru, kami akan memperbarui file kode sumber yang sesuai.



Mari kita lihat beberapa plugin berbasis AST berikutnya. Ini stripTSIgnorePlugindan hoistClassStaticsPlugin.



Plugin stripTSIgnorePlugin adalah plugin pertama yang digunakan dalam pipeline migrasi. Ini menghapus semua komentar dari file.@ts-ignore(komentar ini memungkinkan kita untuk memberi tahu kompilator untuk mengabaikan kesalahan yang terjadi pada baris berikutnya). Jika kami menerjemahkan proyek yang ditulis dalam JavaScript ke dalam TypeScript, maka plugin ini tidak akan melakukan tindakan apa pun. Tetapi jika kita berbicara tentang proyek yang sebagian ditulis dalam JS, dan sebagian lagi di TS (beberapa proyek kita berada dalam keadaan yang sama), maka ini adalah langkah migrasi pertama yang tidak dapat dihindari. Hanya setelah komentar dihapus @ts-ignorekompiler TypeScript akan menghasilkan pesan kesalahan diagnostik yang perlu diperbaiki.



Berikut kode yang masuk ke input plugin ini:



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


Berikut hasilnya:



const str3 = foo
  ? bar
  : baz;


Setelah menghapus komentar, @ts-ignorekami menjalankan plugin hoistClassStaticsPlugin . Ini melewati semua deklarasi kelas. Plugin mendeteksi kemungkinan memunculkan pengenal atau ekspresi dan mencari tahu apakah operasi penugasan tertentu telah dinaikkan ke tingkat kelas.



Untuk memastikan kecepatan pengembangan yang tinggi dan menghindari penurunan paksa ke versi proyek sebelumnya, kami menyediakan setiap plugin dan ts-migrate dengan serangkaian pengujian unit.



Plugin terkait React



The reactPropsPlugin , yang dibangun pada ini mengagumkan alat, mengubah jenis informasi dari PropTypes ke naskah deklarasi tipe. Dengan plugin ini, Anda hanya perlu memproses file .tsx yang berisi setidaknya satu komponen React. Plugin ini mencari semua deklarasi PropTypes dan mencoba menguraikannya menggunakan AST dan ekspresi reguler sederhana seperti /number/, atau menggunakan ekspresi reguler yang lebih kompleks seperti / objectOf $ / . Ketika terdeteksi Bereaksi-komponen (fungsi atau berdasarkan kelas), itu berubah menjadi komponen yang tipe baru digunakan untuk parameter input (alat peraga): type Props = {…};. ReactDefaultPropsPlugin



pluginbertanggung jawab untuk mengimplementasikan pola defaultProps di komponen React . Kami menggunakan tipe khusus untuk mewakili parameter input yang diberi nilai default:



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


Kami mencoba menemukan props yang telah ditetapkan nilai defaultnya, lalu menggabungkannya dengan tipe yang mendeskripsikan props untuk komponen yang kami buat di langkah sebelumnya.



Ekosistem React memanfaatkan secara ekstensif konsep status dan siklus hidup komponen. Kami menangani tantangan yang terkait dengan konsep ini di beberapa plugin berikutnya. Jadi, jika komponen memiliki status, maka plugin reactClassStatePlugin menghasilkan tipe baru ( type State = any;), dan plugin reactClassLifecycleMethodsPlugin menganotasi metode siklus hidup komponen dengan tipe yang sesuai. Fungsionalitas plugin ini dapat diperluas, termasuk dengan melengkapinya dengan kemampuan untuk menggantinya dengan anytipe yang lebih presisi.



Plugin ini dapat ditingkatkan, khususnya, dengan memperluas dukungan tipe untuk status dan properti. Namun kapabilitas mereka yang ada, ternyata, merupakan titik awal yang baik untuk mengimplementasikan fungsionalitas yang kami butuhkan. Kami juga tidak bekerja dengan hook React di sini , karena pada awal migrasi, basis kode kami menggunakan React versi lama yang tidak mendukung hook.



Memeriksa apakah proyek telah dikompilasi dengan benar



Tujuan kami adalah untuk mengkompilasi proyek TypeScript yang dilengkapi dengan tipe dasar tanpa mengubah perilaku program.



Setelah semua transformasi dan modifikasi, kode kita mungkin berubah menjadi format yang tidak seragam, yang dapat mengarah pada fakta bahwa beberapa kode memeriksa dengan kesalahan pengungkapan linter. Basis kode frontend kami menggunakan sistem yang didasarkan pada Prettier dan ESLint. Yaitu, Prettier digunakan untuk pemformatan kode otomatis, dan ESLint membantu memeriksa kode untuk kepatuhan dengan pendekatan pengembangan yang direkomendasikan. Semua ini memungkinkan kita untuk dengan cepat menangani masalah pemformatan kode yang timbul dari tindakan sebelumnya, hanya dengan menggunakan plugin yang sesuai .- eslintFixPlugin.



Langkah terakhir dalam pipeline migrasi adalah memverifikasi bahwa semua masalah kompilasi TypeScript telah diselesaikan. Untuk menemukan dan memperbaiki potensi kesalahan, plugin tsIgnorePlugin mengambil diagnosa semantik dari kode dan nomor baris, dan kemudian menambahkan komentar ke kode @ts-ignoredengan penjelasan kesalahan. Misalnya, mungkin terlihat seperti ini:



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


Kami telah melengkapi sistem dengan dukungan sintaks 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()}
/>


Memiliki pesan kesalahan yang berarti yang kami inginkan memudahkan untuk memperbaiki kesalahan dan menemukan cuplikan kode untuk diwaspadai. Komentar yang relevan, dikombinasikan dengan $TSFixMe, memungkinkan kami mengumpulkan data berharga tentang kualitas kode dan menemukan fragmen kode yang berpotensi bermasalah. $TSFixMeAdalah tipe alias yang kita buat any. Dan untuk fungsi, ini $TSFixMeFunction = (…args: any[]) => any;. Sebaiknya hindari menggunakan tipe any, tetapi menggunakannya membantu kami menyederhanakan proses migrasi. Penggunaan jenis ini membantu kami mengetahui dengan tepat fragmen kode mana yang perlu ditingkatkan.



Perlu dicatat bahwa plugin ini eslintFixPluginberjalan dua kali. Pertama kali sebelum digunakantsIgnorePluginkarena pemformatan dapat memengaruhi pesan tentang di mana kesalahan kompilasi terjadi. Kedua kalinya adalah setelah aplikasi tsIgnorePlugin, karena menambahkan komentar ke kode @ts-ignoredapat menyebabkan kesalahan format.



catatan tambahan



Kami ingin menarik perhatian Anda pada beberapa fitur migrasi yang kami perhatikan selama pekerjaan ini. Mungkin mengetahui tentang fitur-fitur ini akan berguna saat bekerja dengan proyek Anda.



  • TypeScript 3.7 @ts-nocheck, TypeScript- . , .js-, .ts/.tsx-. , .
  • TypeScript 3.9 memperkenalkan dukungan untuk komentar @ ts-expect-error . Jika sebaris kode diawali dengan komentar seperti itu, TypeScript tidak akan melaporkan kesalahan yang sesuai. Jika tidak ada kesalahan dalam baris seperti itu, TypeScript akan memberi tahu @ts-expect-errorAnda bahwa tidak perlu memberikan komentar . Basis kode Airbnb telah berpindah dari komentar @ts-ignoreke komentar @ts-expect-error.


Hasil



Migrasi basis kode Airbnb dari JavaScript ke TypeScript masih berlangsung. Kami memiliki beberapa proyek lama yang masih diwakili oleh kode JavaScript. $TSFixMeKomentar masih umum di basis kode kami @ts-ignore.





JavaScript dan TypeScript di Airbnb



Namun perlu dicatat bahwa menggunakan ts-migrate sangat mempercepat proses penerjemahan proyek kami dari JS ke TS dan sangat meningkatkan produktivitas pekerjaan kami. Dengan ts-migrate, programmer dapat fokus pada peningkatan pengetikan daripada memproses setiap file secara manual. Saat ini, sekitar 86% dari mono-repositori front-end kami, yang memiliki sekitar 6 juta baris kode, diterjemahkan ke TypeScript. Kami berharap dapat mencapai 95% pada akhir tahun ini.



Di sini, di halaman beranda repositori proyek, Anda dapat mempelajari cara menginstal dan menjalankan ts-migrate. Jika Anda menemukan masalah dalam ts-migrate, atau jika Anda memiliki ide untuk menyempurnakan alat ini, kami mengundang Anda untuk bergabung.untuk mengerjakannya!



Pernahkah Anda menerjemahkan proyek besar dari JavaScript ke TypeScript?






All Articles