Авторы Калеб Мередит и Эндрю Ван

Кодовая база Airtable следует идеалу непрерывного развития. Мы не боимся отказываться от старого кода, когда он больше не отвечает нашим потребностям, или внедрять новые технологии и шаблоны, отражающие текущее состояние дел. Сегодня мы собираемся сделать обзор некоторых этапных изменений в истории нашей кодовой базы и углубиться в самое крупное из этих изменений: нашу миграцию на TypeScript. Мы также открываем исходный код для некоторых инструментов, которые мы использовали для успешного выполнения этой миграции.

Да будет свет

Эта история начинается с самого начала, с генезисной фиксации монорепозитория Airtable:

commit 681429d64232f44c6af1a1d838b91fe39d52edb0
Author: Howie <[email protected]>
Date:   Sun Apr 22 15:55:06 2012 -0700
beginning state

В 2012 году идея серверного JavaScript была еще относительно новой. В 2012 году был выпущен iPhone 5, и Psy’s Gangnam Style стал первым видео в Интернете, которое собрало 1 миллиард просмотров. Наши основатели сделали ставку как на JavaScript, так и на экосистему JavaScript с первоначальным технологическим стеком Airtable.

Хотя некоторые из этих исходных решений давно устарели (Node.js, Express и сам JavaScript), другие варианты - нет (Backbone, Underscore.js, EJS и jQuery). Здесь мы возвращаемся к нашему духу непрерывной эволюции. Благодаря постоянным усилиям по рефакторингу в течение последних девяти лет мы добились того, что наша кодовая база написана на последовательном, относительно современном диалекте JavaScript, даже несмотря на то, что мы выросли до более сотни инженеров и более миллиона строк кода.

Основной образец нашей кодовой базы: Расширенная запись

Например, Airtable имеет функцию Расширенная запись, которая была введена в коммите Genesis от Howie с 2012 года. С тех пор основные функции остались в основном такими же, но структура кода значительно изменилась. Вот первые 50 строк функции расширенной записи (известной как подробный просмотр) за последние 9 лет:

Эти фрагменты дают представление о некоторых основных моментах кодовой базы Airtable:

  1. 2012: Первая фиксация в репозитории
  2. 2015: Все больше людей начинают работать над кодовой базой, установлены общие соглашения
  3. 2016: Представлен Browserify, добавлен явный импорт CommonJS
  4. 2018: класс стиля Backbone преобразован в класс стиля ES6
  5. 2019: Фреймворк пользовательских компонентов преобразован в компонент React
  6. 2019: переход с Flow на TypeScript, импорт CommonJS заменен импортом ES6
  7. 2021: Заменены createReactClass и миксины компонентами класса React.

История Expanded Record демонстрирует нашу готовность выполнять крупномасштабные рефакторы, затрагивающие даже самые старые части кодовой базы. Мы отвергаем идею неподдерживаемого кода как неизбежность. Вместо этого мы рассматриваем качество кода как ответственность всех инженеров. В качестве примера приведенные выше изменения в Expanded Record выполнялись разными инженерами из разных команд в течение многих лет.

В оставшейся части этого сообщения в блоге мы рассмотрим один из лучших примеров нашего стремления к непрерывной эволюции: наш переход на TypeScript. Это история о том, как один высокомотивированный инженер смог выполнить рефакторинг, затронувший почти каждый файл в нашей миллионной кодовой базе, и как инженерия в целом с тех пор взяла на себя мантию и продолжила поставлять дополнительные улучшения, связанные с TypeScript. .

Ставка не на ту лошадь

Кодовая база Airtable начиналась с ванильного JavaScript. Много чернил было пролито на преимущества статического набора текста; достаточно сказать, что мы согласны. Когда мы исследовали инкрементную типизацию для JavaScript в 2016 году, это была гонка двух лошадей между Flow и TypeScript. Мы выбрали Flow, потому что в то время он лучше поддерживал React.

К 2019 году стало ясно, что мы сделали ставку не на ту лошадь. Скорость разработки TypeScript намного опережала Flow, и TypeScript имел очевидные преимущества с точки зрения функций, поддержки IDE и ресурсов сообщества. Один из нас (Калеб), который ранее работал над Flow в Facebook, взял на себя проект по преобразованию нашей кодовой базы в TypeScript.

Руководящие принципы

На данный момент кодовая база Airtable насчитывала более миллиона строк JavaScript. Учитывая масштаб, предстояло разгадать множество сложностей. Мы ориентировали усилия по миграции на три руководящих принципа:

  • Не нарушайте продукт. Всегда старайтесь сохранять семантику существующего кода, чтобы не создавать проблем, с которыми сталкиваются клиенты.
  • Не снижайте безопасность типов. Каждое отдельное изменение в процессе миграции должно повышать безопасность типов по сравнению с Flow. По-прежнему может быть много небезопасного кода, но любое изменение должно сохранить или повысить безопасность типов, которую мы имели ранее.
  • Будьте проще. Для миграции требуется много изменений. Каждое отдельное изменение должно быть простым, и его можно рассматривать в контексте отдельного файла. Мы всегда можем выполнить небольшие миграции для более сложных преобразований.

Миграция большого взрыва

Большинство миграций TypeScript выполняется постепенно: тип за типом и файл за файлом. Поскольку большая часть нашей кодовой базы уже была напечатана в Flow, мы выбрали другой подход: мигрировали всю кодовую базу на TypeScript сразу, как одну миграцию большого взрыва.

Первым шагом было написание codemod для выполнения чисто механических преобразований. Существовали существующие кодовые модификации для преобразования Flow в TypeScript (пример 1, пример 2), но мы написали наш собственный код-мод с некоторыми дополнительными функциями для удовлетворения наших конкретных потребностей:

  • Существующие codemods не изменили синтаксис модуля. Сначала нам нужно было преобразовать синтаксис модуля CommonJS (require() и module.exports) в синтаксис модуля ES (import и export).
  • Некоторые существующие кодмоды неверно истолковали функции Flow. Например, они преобразовали выражение приведения Flow (x: T) (которое является ковариантным) в выражение приведения типов TypeScript x as T (которое является двувариантным), что небезопасно! Вместо этого наш codemod использует специальную служебную функцию, например cast<T>(x), реализованную как function cast<T>(x: T): T { return x }.
  • Мы также хотели особой обработки некоторых идиом, специфичных для Airtable. Например, мы часто использовали такие типы, как {[key: UserId]: string}, хотя TypeScript не поддерживает настраиваемые индексированные типы доступа. Поэтому мы преобразовали эти типы в Record<UserId, string> вместо {[key: string]: string}.

Одна из наиболее технически интересных (и уникальных) особенностей нашего codemod - это то, как он обрабатывает неаннотированные параметры функции. Например, рассмотрим следующий пример, в котором не указан тип параметра x:

function f(x) {
  return x * 2;
}

В этом случае Flow может сделать вывод, что x - это число на основе контекста. Однако TypeScript этого не сделает, а в строгом режиме выдаст ошибку.

Часть кода Airtable использовала эту возможность Flow. Поскольку одним из наших руководящих принципов было «Не снижайте безопасность типов», аннотирование всех этих параметров функции с помощью any было недопустимо. Итак, codemod выполняет flow type-at-pos, чтобы использовать выведенный тип Flow для каждого неаннотированного параметра функции. Как оказалось, большую часть времени Flow все равно делал выводы any!

В рамках этого сообщения в блоге мы также рады сообщить, что открываем исходный код для нашего codemod! Вы можете найти его по адресу github.com/Airtable/typescript-migration-codemod. Если вас интересуют более подробные технические подробности, мы также включили отредактированную версию нашей внутренней документации для всех изменений. Мы надеемся, что это полезный справочник для всех, кто выполняет миграцию TypeScript, особенно если он исходит из Flow.

Закатывая рукава

Codemod выполнил основную часть необходимых изменений, изменив 3300 файлов. Однако при этом остались все изменения, которые нельзя было обработать автоматически. На данный момент запуск tsc показал более 15 000 ошибок TypeScript, разбросанных по 1600 файлам, что потребовало некоторой степени ручного вмешательства.

К счастью, мы смогли положиться на Калеба, нашего эксперта по локальным системам типов. Примерно неделю он приходил на работу, садился и исправлял ошибки TypeScript. Это было скучно и утомительно, но лучше, чем засорять кодовую базу // @ts-ignore. Вот почему наши руководящие принципы имели значение. Мы отказались от регресса в отношении безопасности типов для существующего Flow-кода (помните: Не снижайте безопасность типов), но более агрессивные рефакторы для повышения безопасности типов также могут быть опасными (помните: Не сломать товар). Поскольку не допускать поломки продукта было превыше всего, добавление // @ts-ignore иногда было лучшим решением.

Вся эта работа выполнялась в отдельной ветке. Как только у нас появилась ветка, которая прошла проверку типов и автоматизированные тесты, пришло время внести изменения в нашу основную ветку разработки.

Поскольку невозможно вручную просмотреть изменение, которое затрагивает более 1600 файлов (и более 48 000 измененных строк), вместо этого мы использовали комбинацию методов:

  • Задокументировал 14 автоматических преобразований и 17 классов преобразований вручную и попросил инженеров компании просмотреть этот документ.
  • Назначенные фрагменты до 10 файлов для проверки кода экспертами в затронутых областях.
  • Код рассмотрел различия скомпилированных пакетов JavaScript до и после изменения. Мы не меняли наш стек компиляции (Webpack и Babel), поэтому в скомпилированные пакеты было внесено лишь несколько тривиальных изменений.

30 октября 2019 года мы заблокировали нашу основную ветвь, повторно запустили автоматические модификации кода и объединили ветку TypeScript. С тех пор мы используем TypeScript.

Теперь, когда пыль осела

Поскольку одним из принципов миграции было «Сохраняйте простоту», многие идеи по дополнительному улучшению были отложены до работы в будущем. В то время Калеб написал документ, содержащий около двадцати таких идей. Мы гордимся тем, что за последние два года более половины этих идей были независимо реализованы инженерами компании. Поделюсь несколькими примерами:

  • В фрагментах кода, которыми мы поделились выше, вы можете увидеть, как в начале 2021 года мы наконец преобразовали наши createReactClass компоненты в классы React ES6. Это было трудное мероприятие из-за того, что мы широко использовали createReactClass миксины. Это было сделано одним из членов группы разработчиков, когда они обнаружили, что пользовательские типы для createReactClass значительно замедляют время сборки TypeScript.
  • Член нашей группы автоматизации написал вспомогательный метод для создания типа TypeScript с учетом определения схемы для нашей внутренней инфраструктуры проверки схемы объекта. Раньше мы поддерживали и схему, и тип TypeScript для объекта, что могло приводить к несогласованности.
  • Член нашей корпоративной команды преобразовал все расширения файлов в .tsx. Как команда, мы рассудили, что .ts и .tsx представляют два диалекта языка TypeScript, и решили, что хотим писать код только на одном диалекте.
  • Один из наших основателей улучшил уровень доступа к базе данных MySQL, чтобы он возвращал результаты типизированных запросов. Они также обновили все наши // @ts-ignore комментарии до // @ts-expect-error после выпуска TypeScript 3.9.
  • Член нашей команды Ecosystems использует codemod для подготовки базы кода для включения --noUncheckedIndexAccess, новой функции в TypeScript 4.1.

Заключение

Платформы разработки приложений с низким кодом / без кода - это светская тенденция, и мы верим, что Airtable продолжит вводить новшества в этой сфере в ближайшие десятилетия. Это означает, что большинство инженеров Airtable еще предстоит нанять, и большая часть кода Airtable еще не написана, во многих случаях. Эти убеждения являются причиной того, почему мы серьезно относимся к качеству кода и постоянно развиваем кодовую базу Airtable.

Миграция TypeScript была одним из крупнейших рефакторов в истории нашей кодовой базы, и уж точно не последним. Инженеры Airtable уполномочены (и поощряются!) Реализовывать подобные проекты, и мы рассматриваем долгосрочное здоровье нашей кодовой базы как ответственность, которую несут все инженеры.

Если вы присоединитесь к Airtable, мы рекомендуем вам ознакомиться с историей git для компонента Expanded Record, о котором мы рассказали выше. Хотя некоторые устаревшие ссылки на jQuery все еще остаются, работа с этим компонентом и остальной частью нашей кодовой базы сегодня намного лучше, чем если бы мы постоянно не выбирали прогресс.