W iziwork mieliśmy od dawna zadanie na koncie: migrację naszej bazy kodu Flow do TypeScript. Z przyjemnością informujemy, że po miesiącach przygotowań pomyślnie przeprowadziliśmy migrację naszego rdzenia LoC o pojemności 300 tys. do TypeScript bez zamrażania bazy kodu!

W tym poście na blogu zobaczymy, co zrobiliśmy, aby umożliwić tę migrację. Jest to pierwsza część dwuczęściowej serii.

Opuszczanie Flow

Kiedy wystartowaliśmy z iziwork, chcieliśmy skorzystać z narzędzi do sprawdzania typów. Wybraliśmy Flow. Idąc dalej, 3 lata później myślę, że można śmiało powiedzieć, że Flow stracił dynamikę. Nawet Facebook, który stoi za Flow, przeszedł na TypeScript. TypeScript jest obecnie szeroko stosowany w branży, ekosystem typów jest znacznie bogatszy, a narzędzia oparte na TypeScript ewoluowały.

Co więcej, większość naszych projektów w iziwork korzystała już z TypeScriptu. Nadszedł czas na migrację naszego rdzenia do TypeScriptu.

Cele

Migracja to ciężkie zadanie i mieliśmy jasne cele:

  • Przeprowadź migrację tak szybko, jak to możliwe, bez utraty jakości
  • Upewnij się, że nie będzie żadnych potencjalnych, istotnych zmian w kodzie, pracując jak najwięcej na poziomie typu. Dlatego rzeczywisty, skompilowany kod wykonawczy pozostanie taki sam
  • iziwork działa szybko, nie chcieliśmy zamrażać bazy kodu. Migracja powinna odbywać się równolegle
  • Zautomatyzuj to, co da się zautomatyzować, unikając w miarę możliwości pracy ręcznej (np. jscodeshift)

To były cele, które odpowiadały naszej ówczesnej sytuacji. W pozostałej części tego wpisu na blogu wyjaśniono, co zrobiliśmy, aby zachować zgodność z tymi celami.

Migracja przyrostowa a migracja Wielkiego Wybuchu

Możliwe są dwie ścieżki migracji:

1. Dokonuj stopniowej migracji z Flow do TypeScript, korzystając z dwóch typów plików w kodzie. Może to działać poprzez wykorzystanie mocy nadpisań Babel 7

  • Łatwo jest to przejrzeć
  • Proces migracji można z łatwością przeprowadzić równolegle i zająć tygodnie/miesiące bez konieczności zamrażania kodu
  • Ponowne użycie typów jest trudne. Nie możemy zaimportować typu Flow w TS i odwrotnie
  • Doświadczenia programistów IDE są okropne. Dwa narzędzia do uruchomienia w celu sprawdzenia typu kodu, zdecydowanie za dużo informacji w menu kontekstowych itp.

2. Przeprowadź migrację całego kodu w jednym przebiegu

  • Recenzja może być dość skomplikowana
  • Trudno jest to obejść bez zamrażania bazy kodu (uwaga spoiler – jest to wykonalne)
  • Doświadczenie programistyczne jest całkiem niezłe, IDE musi obsługiwać tylko TypeScript
  • Możemy całkowicie pozbyć się oprzyrządowania Flow

Opierając się na opiniach, jakie możemy znaleźć w Internecie, wszyscy raczej zgadzają się, że lepsza jest druga droga. Skomplikowanie przeglądu można złagodzić, stosując przejrzysty proces i automatyzując większość procesu. Dlatego wybraliśmy drugą ścieżkę: migrację 100% bazy kodu w jednym przebiegu.

Zapewnienie spójnego stylu kodu

Aby zapewnić płynną migrację, podczas migracji kodu chcemy mieć czyste różnice. Ułatwi to przeglądanie różnych zatwierdzeń.

Uruchomimy także narzędzia i skrypty do automatycznej migracji oraz wykonamy sporo zbiorczego wyszukiwania i zamiany. Często psują one styl naszego kodu i ostatecznie zanieczyszczają naszą historię Git.

Z tego powodu ważne jest, aby użyć narzędzia, które formatuje kod w sposób deterministyczny. Wybraliśmy Prettier, które jest w zasadzie wiodącym narzędziem do formatowania kodu w ekosystemie JS.

Możesz wybrać żądane narzędzie, ale przed rozpoczęciem migracji upewnij się, że Twój kod Flow jest „zgodny ze stylem”.

Wdrażanie siatki bezpieczeństwa

Podczas migracji przejdziemy przez wiele zmian w kodzie.

Z tego powodu potrzebujemy siatki bezpieczeństwa. Sposób, w jaki możemy sprawdzić, czy nawet jeśli zmienimy dużo kodu, niczego nie zepsujemy. W idealnym świecie zapewnia to zestaw testów. Mamy zestaw testów, ale z pewnością istnieją ścieżki kodu, które nie są testowane.

Jak stwierdzono w naszych celach, będziemy pracować tak dużo, jak to możliwe, na poziomie typu. Dlatego w idealnym przypadku skompilowany kod JS, pozbawiony wszelkich adnotacji TypeScript, powinien pozostać taki sam jak kod Flow. Mając to na uwadze, wpadliśmy na pomysł zrobienia migawki skompilowanego kodu JS naszej bazy kodów Flow. W ten sposób podczas migracji kodu do TypeScriptu możemy wykonać migawkę skompilowanego kodu, porównać go z naszym punktem odniesienia (migawką Flow) i upewnić się, że niczego nie zepsujemy.

Aby to zrobić, musimy użyć tego samego narzędzia do tworzenia.

W przeciwnym razie wyemitowany kod będzie nieco inny i trudno będzie to sprawdzić na tysiącu plików. Prawdopodobnie używasz Babel z wtyczką @babel/plugin-transform-flow-strip-types. Na szczęście dla nas Babel obsługuje zarówno Flow, jak i TypeScript. Oznacza to, że będziemy mogli używać Babel jako narzędzia do budowania TypeScriptu, a nie kompilatora TypeScript. Użyjemy tsc z opcją --noEmit, aby przeprowadzić jedynie sprawdzanie typu.

⚠️ Jeśli nie używasz Babel do transpilacji kodu Flow (na przykład, może używasz flow-remove-types), powinieneś i możesz łatwo przeprowadzić migrację do Babel.

Jest ma świetną funkcję, która pozwala nam robić dokładnie to, co chcemy: wykonać migawkę kodu i porównać ją z migawką referencyjną. Oto stworzony przez nas test Jest:

Powyższe testy sprawdzają dwie rzeczy:

  1. Struktura pliku skompilowanego kodu powinna pozostać taka sama, przechowując wynik funkcji glob
  2. Zawartość każdego pliku powinna pozostać taka sama

Uzbrojeni w ten test, będziesz teraz chciał zbudować swój kod i uruchomić test, aby wykonać migawkę struktury pliku i zawartości każdego pliku. Spowoduje to wygenerowanie dużego pliku .snap, który będzie migawką referencyjną używaną przez Jest.

💡 Wskazówka: przed wykonaniem migawki kodu ustaw comments: false w .babelrc.

Wtyczka Flow Babel może formatować komentarze w kodzie inaczej niż wtyczka TypeScript. Wyłączenie komentarzy pomoże Ci przejrzeć rzeczywiste zmiany w kodzie, zamiast zbędnych różnych formatów komentarzy.

⚠️ Teraz, gdy nasza migawka jest już gotowa, nie bazuj swojej gałęzi migracyjnej na gałęzi develop. Spowodowałoby to uszkodzenie migawki. Zmiany z wersji develop będziesz mógł pobrać później, gdy przeprowadzisz migrację całego kodu w gałęzi migracji.

Teraz za każdym razem, gdy będziesz zmieniać kod w ramach migracji, będziesz przeprowadzać następujący proces:

  1. Zbuduj kod za pomocą Babel
  2. Uruchom test sieci zabezpieczającej Jest

Jeśli się powiedzie, OK, kod wykonawczy nie został zmieniony.
Jeśli tak się nie stanie, będziesz mógł sprawdzić różnicę. Jeśli zmiany Ci odpowiadają, możesz uruchomić jest --updateSnapshot, aby zaktualizować migawki

Jak wspomniano wcześniej, przez większość czasu będziesz pracować na poziomie typu. Są jednak chwile, w których będziesz musiał lub chciał zmienić czas działania. Na przykład, dzięki rygorystyczności TypeScriptu, prawdopodobnie zauważysz kilka błędów, których Flow nie wyłapał.

Uruchamianie automatycznej konwersji przepływu do TS

Początkową pracę polegającą na konwersji kodu na TypeScript i mapowaniu kilku wbudowanych typów Flow na wbudowane typy TypeScript (np. $Keys<T> do keyof T) można wykonać automatycznie. Istnieje wiele narzędzi, które mogą to zrobić, w szczególności:

Ten pierwszy jest bardziej „znany”, ale po uruchomieniu naszego kodu uległ awarii bez żadnych logów.

Ten ostatni jest mniej znany, ale oferuje większe bezpieczeństwo:

  • Ma więcej automatycznych mapowań typów
  • Automatycznie uruchamia Prettier na wynikowym kodzie TypeScript
  • Sprawdza, czy wynikowy JS AST jest dokładnie taki sam przed i po, wyświetlając jasne, szczegółowe ostrzeżenia, jeśli tak nie jest

Dlatego zdecydowaliśmy się na flowts. Możesz wypróbować oba te narzędzia, przebieg może się różnić.

Możesz teraz uruchomić npx flowts .. Twój kod zostanie teraz przekonwertowany na TypeScript. Może pojawić się kilka ostrzeżeń i potencjalnie błąd: weryfikacja nie powiodła się, różnica po usunięciu adnotacji typu. Po prostu sprawdź różnicę i napraw problemy, jeśli takie istnieją. W naszej bazie kodu liczącej 300 000 kB mieliśmy jeden problem: ciąg dosłowny szablonu wielowierszowego zawierał 2 niepotrzebne spacje. Naprawienie tego było banalne!

Nasz kod jest teraz w TypeScript. Narzędzie, sprawdzając AST, gwarantuje, że kod wyjściowy jest taki sam, a różnica po usunięciu adnotacji typu jest taka sama, co oznacza, że ​​skompilowany .js jest dokładnie taki sam. Innymi słowy, nasz kod można uruchomić tak jak poprzednio.

Konfigurowanie narzędzi do obsługi TypeScript

Nasz kod jest teraz napisany w TypeScript! Czas skonfigurować nasze narzędzia, aby to wspierać.

Ulepszanie .babelrc tak, aby obsługiwało TypeScript, jest całkiem proste i polega jedynie na dodaniu ustawienia wstępnego @babel/preset-typescript. Musimy także zaktualizować wywołania Babel CLI, aby obsługiwały rozszerzenie .ts.

💡 Wskazówka: ustaw onlyRemoveTypeImports: true w opcjach ustawienia wstępnego @babel/preset-typescript.

Wtyczka Flow Babel usuwa tylko import type importy z wyemitowanego kodu. Wtyczka TypeScript jest nieco mądrzejsza: usuwa również wszystkie importy, które są używane tylko jako typ, nawet jeśli nie są jawnie importowane ze składnią import type. Aby ułatwić przeglądanie migawek, wyłączenie tego zachowania za pomocą powyższej opcji zapewni zachowanie takich samych importów jak poprzednio. Możesz oczywiście (i powinieneś) powrócić do domyślnego zachowania po migracji.

Musisz także skonfigurować bibliotekę linterową i testową do obsługi TypeScript. Jeśli używasz ESLint, postępuj zgodnie ze wskazówkami zawartymi w README Typescript-eslint: https://github.com/typescript-eslint/typescript-eslint

Jednym z najważniejszych elementów konfiguracji jest plik tsconfig.json. Ponieważ używamy Babel jako narzędzia do kompilacji, będziesz chciał skonfigurować TypeScript tak, aby sprawdzał tylko typy. Oto na przykład konfiguracja, której używamy dla naszego backendu w iziwork:

Zauważ, że nie włączyliśmy żadnych strict flag. Ścisłe flagi mogą z łatwością podwoić czas migracji niezbędny do zaliczenia tsc. Sugerujemy stopniowe włączanie ścisłych flag po migracji.

Po skonfigurowaniu wszystkich narzędzi nie zapomnij zaktualizować skryptów (package.json, potoki CI), aby uruchamiały tsc zamiast flow check. Nie zapomnij także skonfigurować IDE dla TypeScript.

Możesz także zbudować swój kod za pomocą Babel i uruchomić test sieci bezpieczeństwa Jest. Zauważysz, że, jak gwarantuje flowts, transpilowany kod naszej wersji TypeScript jest dokładnie taki sam, jak transpilowany kod początkowego kodu Flow.

Do tej pory Twoje testy jednostkowe i CI powinny przejść pomyślnie, z wyjątkiem kontroli tsc. Powodzenie! Twój kod i narzędzia obsługują teraz TypeScript! 🎉

Jednak przed nami jeszcze długa droga...

Bieganie tsc bez ucieczki

Kiedy uruchomisz tsc, prawdopodobnie będziesz mieć wiele błędów. Wiemy, jakie to uczucie: popełniliśmy 20 000 błędów.

TypeScript jest mądrzejszy i bardziej rygorystyczny niż Flow w wielu aspektach. Ale nie zniechęcaj się, wiele z tego początkowego przepływu błędów można bardzo łatwo naprawić:

  • Prawdopodobnie masz wiele brakujących @types/ pakietów
  • Być może masz w swoim kodzie jakieś aliasy, których nie ma w tsconfig.json
  • Niektóre składnie są dozwolone w Flow, ale nie w TypeScript. Na przykład TypeScript zawiera błąd TS1095: A 'set' accessor cannot have a return type annotation, podczas gdy jest to dozwolone w Flow (z typem zwracanym void). Tego typu błędy są proste do naprawienia i można je naprawić za pomocą codemodów, więcej o tym później

Chociaż wiele z tych błędów jest łatwych do naprawienia, trudno jest określić, które dokładnie, patrząc na tajemniczy wynik tsc. Naprawmy to!

Uzyskiwanie bardziej kompleksowych raportów o błędach z tsc

Aby uzyskać lepszy przegląd błędów zgłaszanych przez TypeScript, ważna jest możliwość syntezy raportu. TypeScript nie zapewnia takiej opcji, ale możesz łatwo napisać własny skrypt, który sobie z tym poradzi. Oto jak wygląda nasz niestandardowy raport (zrzut ekranu zrobiony pod koniec naszej migracji — na początku mieliśmy znacznie więcej błędów):

Raport ten pokazuje dwa ważne dane:

  1. Zagregowane typy błędów TS. Dzięki temu będziesz mógł rzucić okiem na sytuację, a także będziesz mógł zapoznać się z dokumentacją TS i określić, czy niektóre błędy są trudne do naprawienia
  2. Liczba błędów na plik, która pozwoli Ci zobaczyć, które pliki sprawiają najwięcej problemów. Pomoże Ci to ustalić priorytety prac związanych z migracją i podzielić zadania pomiędzy wielu programistów

Szybki i brudny skrypt, który napisaliśmy w celu wygenerowania powyższego raportu, jest „dostępny jako podsumowanie tutaj”.

Uzbrojeni w to możesz zacząć analizować wszystkie swoje błędy. Prawdopodobnie zobaczysz błędy, które można naprawić automatycznie…

Stosowanie codemodów

Analizując własne błędy, znaleźliśmy wiele z nich:

const foo = {}
foo.bar = true // TS2339: Property 'bar' does not exist on type '{}'

Powyższy kod jest całkowicie poprawny w Flow, ale nie jest w TypeScript z powodu nadmiernego sprawdzania właściwości. Gdybyśmy mieli tylko kilka takich błędów, moglibyśmy je naprawić ręcznie, ale mieliśmy ich kilka tysięcy. Aby dostosować się do zachowania Flow, musielibyśmy zadeklarować foo jako any. Musi istnieć sposób, aby to zautomatyzować, prawda?

Rzeczywiście istnieje wiele narzędzi, które mogą to osiągnąć. Ten, który wybraliśmy to:

jscodeshift to zestaw narzędzi, który pozwala nam budować modyfikacje. Codemod to po prostu skrypt, który pobiera AST jako dane wejściowe i można je edytować. Następnie jscodeshift zapisuje wynikowy kod z powrotem do pliku początkowego. Jest o wiele potężniejszy niż wyszukiwanie/zamiana wyrażeń regularnych.

Oto codemod, który rozwiązuje powyższy problem:

Możesz go uruchomić za pomocą npx jscodeshift --parser ts --extensions ts -t ./codemod.js src.

Aby to krótko wyjaśnić:

  • Znajduje wszystkie deklaratory zmiennych w AST
  • Dla każdego z nich, jeśli nie posiada jeszcze adnotacji typu, jeżeli wartość jest wyrażeniem obiektowym i jeżeli obiekt ten nie posiada żadnych właściwości, oznaczamy adnotację typu jako any

Sugerujemy używanie https://astexplorer.net, to naprawdę pomaga w pisaniu codemodów.

Kodody pisz tylko wtedy, gdy zauważysz błędy, które są systematycznie naprawiane w ten sam sposób, a których ręczne naprawienie byłoby zbyt długie. Nie ma sensu pisać codemodu, jeśli zajmuje to więcej czasu niż ręczne naprawienie problemu.

Miejmy nadzieję, że pozostanie tylko kilkaset lub tysiące błędów. Kiedy myślisz, że skończyłeś z modami, nadszedł czas, aby ręcznie naprawić pozostałe błędy!

Ręczna naprawa pozostałych błędów

Spójrzmy prawdzie w oczy, jest to najnudniejszy etap migracji, ale najbardziej satysfakcjonujący, ponieważ na końcu zobaczysz, że tsc przeszło.

Do tej pory powinieneś mieć już dokładne pojęcie o pozostałych błędach. Przeglądając swój niestandardowy tsc raport, powinieneś sporządzić listę błędów, którymi musisz się zająć w pierwszej kolejności. Posiadanie 2000 błędów nie oznacza, że ​​będziesz musiał naprawić błędy w 2000 miejscach; naprawiając gdzieś błąd, możesz naprawić 100 błędów gdzie indziej. Dlatego ważne jest, aby mieć jasny plan działania.

Od tej chwili sensowne jest, aby nie być jedyną osobą pracującą nad migracją, ponieważ możesz rozpocząć migrację rzeczy jednocześnie.

W pewnym momencie przekonasz się, że wszystkie pozostałe błędy nie są ze sobą powiązane i nie można już przypisać im priorytetów. Jeśli nad migracją pracuje dwóch programistów, jeden może naprawiać błędy, zaczynając od góry listy plików, a drugi od dołu. W ten sposób będziesz pracować efektywnie, a jednocześnie nie będziesz dublować się ze swoimi współpracownikami.

Dla porównania, w iziwork ukończenie tego etapu u 2 programistów zajęło nam 2 dni, a ręczne naprawienie około 2000 błędów.

Wszystko dobrze, masz teraz działającą gałąź TypeScript.

Jeszcze nie skończyłeś! W następnej części zobaczymy, jak zaktualizować gałąź TypeScript za pomocą oprogramowania developerskiego, które prawdopodobnie ewoluowało od początku migracji, oraz jak przeprowadzić migrację wszystkich równoległych gałęzi, nad którymi pracowali Twoi współpracownicy, zanim wreszcie połączenie całego kodu TypeScript.

Czekać na dalsze informacje!