TS-Migrate:一种规模化文字迁移工具

2020-08-19 08:33:49

TypeScript是Airbnb前端Web开发的官方语言。然而,采用打字脚本并迁移包含数千个JavaScript文件的成熟代码库的过程不是一天完成的。打字稿的采用经历了最初的提案、多个团队的采用、测试版阶段,最后成为Airbnb前端开发的官方语言。在Brie Bunge的这次演讲中,您可以了解到更多关于我们如何大规模采用打字脚本的信息。

大规模迁移是一项复杂的任务,我们探索了从JavaScript迁移到TypeScript的几种选择:

1)混合迁移策略。逐个文件部分移植,修复类型错误,然后重复此过程,直到移植完整个项目。AllowJS配置选项允许我们在项目中同时存在TypeScript和JavaScript文件,这使得这种方法成为可能!

在混合迁移策略中,我们不需要暂停开发,可以逐个文件地逐步迁移。不过,在大范围内,这一过程可能需要很长时间。此外,还需要对来自组织不同部门的工程师进行培训和入职。

2)一体化迁移!使用JavaScript或部分TypeScript项目并将其完全转换。我们将需要添加一些任意类型和@ts忽略注释,以便项目编译时不会出错,但随着时间的推移,我们可以用更具描述性的类型替换它们。

整个项目的一致性:全包迁移将保证每个文件的状态是相同的,工程师不需要记住他们可以在哪里使用打字功能,以及编译器将在哪里防止基本错误。

只修复一种类型比修复文件容易得多:修复整个文件可能非常复杂,因为文件可能有多个依赖项。使用混合迁移时,更难跟踪迁移的实际进度和文件状态。

看起来全员移民显然是这里的赢家!但是,执行大型且成熟的代码库的全面迁移过程是一个沉重而复杂的问题。为了解决这个问题,我们决定使用代码修改脚本--codemods!通过手动迁移到打字脚本的初始过程,我们认识到可以自动执行的重复操作。我们对这些步骤中的每一个步骤都进行了编码,并将它们合并到最重要的迁移管道中。

根据我们的经验,不能100%保证自动迁移将产生一个完全没有错误的项目,但是我们发现下面列出的步骤组合在最终迁移到一个没有错误的TypeScript项目方面给了我们最好的结果。通过使用代码,我们能够在一天内将包含超过50,000行代码和1,000多个文件的项目从JavaScript转换为打字脚本!

在Airbnb,我们使用REACTION作为前端代码库的重要组成部分。这就是为什么编码代码的某些部分与基于反应的概念相关。通过额外的配置和测试,ts-Migrate可以潜在地与其他框架或库一起使用。

让我们演练将项目从JavaScript迁移到TypeScript所需的主要步骤,以及这些步骤是如何实现的:

1)每个TypeScript项目的第一部分是创建tsconfig.json文件,如果需要,ts-Migrate可以做到这一点。有一个默认的配置文件模板和一个验证检查,可以帮助我们确保所有项目配置一致。以下是基本级别配置的示例:

2)tsconfig.json文件就位后,下一步是将源代码文件的文件扩展名从.js/.jsx更改为.ts/.tsx。这一步的自动化相当容易,而且它还消除了大量的人工工作。

3)下一步是运行codemods!我们称它们为“插件”。用于TS-Migrate的插件是可以通过打字语言服务器访问附加信息的代码代码。插件接受字符串作为输入,并生成更新后的字符串作为输出。可以使用jcodeshift、typecript API、字符串替换或其他AST修改工具来支持代码转换。

在每个步骤之后,我们检查Git历史中是否有任何挂起的更改并提交它们。这有助于将迁移拉请求拆分成更易于理解的提交,并跟踪文件重命名。

通过这样做,我们能够将转换逻辑从核心运行器中分离出来,并为不同的目的创建多个配置。目前,我们主要有两种配置:迁移和重新忽略。

迁移配置的目标是从JavaScript迁移到TypeScript,而ReIgnore的目的是通过简单地忽略所有错误来使项目可编译。当用户具有较大的代码库并执行以下任务时,重新忽略非常有用:

这样,即使有一些我们不想立即处理的错误,我们也可以迁移项目。它使打字稿或库的更新变得容易得多。

TSServer:这一部分非常类似于VSCode编辑器为编辑器和语言服务器之间的通信所做的工作。TypeScript语言服务器的新实例作为单独的进程运行,开发工具使用语言协议与服务器通信。

迁移运行者:此部分运行并协调迁移过程。它需要以下参数:

将每个文件发送到打字语言服务器进行诊断。编译器为我们提供了三种类型的诊断:语义诊断、语法诊断和建议诊断。我们使用这些诊断来查找源代码中有问题的地方。根据唯一的诊断代码和行号,我们可以识别问题的潜在类型并应用必要的代码修改。

在每个文件上运行所有插件。如果文本因插件执行而更改,我们将更新原始文件的内容,并通知TypeScript语言服务器该文件已更改。

您可以在Examples包或主包中找到ts-Migrate-server用法的示例。Ts-Migrate-Example还包含插件的基本示例。它们主要分为三大类:

存储库中有一组示例来演示如何构建所有类型的简单插件,并将它们与ts-Migrate-server结合使用。以下是转换以下代码的迁移管道示例:

向函数声明函数tlum(tsrif,dnoces)->;函数tlum(tsrif:number,dnoces:number)添加类型:number。

现实世界中的插件位于一个单独的包-ts-Migration-plugins中。让我们来看看其中的一些。我们有两个基于jcodeshift的插件:explitAnyPlugin和decreMissingClassPropertiesPlugin。Jcodeshift是一个可以使用重铸包将AST转换回字符串的工具。通过使用函数toSource(),我们可以直接更新文件的源代码。

ExplitAnyPlugin背后的主要思想在于从打字脚本语言服务器中提取所有语义诊断错误以及行号。然后,我们需要在诊断中指定的行上添加任何类型。此方法允许我们解决错误,因为添加Any类型会修复编译错误。

DecreMissingClassPropertiesPlugin接受代码2339的所有诊断(您能猜出此代码的意思吗?)。如果可以找到缺少标识符的类声明,插件会将它们添加到带有任何类型注释的类体中。从名称可以看出,此编码模式仅适用于ES6类。

下一类插件是基于TypeScript AST的插件。通过解析AST,我们可以在源文件中生成以下类型的更新数组:

生成更新后,剩下的唯一事情就是以相反的顺序应用更改。如果通过这些操作的结果,我们接收到新的文本,我们将更新源文件。让我们看看这些基于AST的插件:stripTSIgnorePlugin和hoistClassStaticsPlugin。

StripTSIgnorePlugin是迁移管道中的第一个插件。它会从文件中删除所有@ts-Ignore?实例。如果我们要将JavaScript项目转换为TypeScript,则此插件不会执行任何操作。然而,如果它是一个部分打字项目(在Airbnb,我们在这个州有几个项目),这是必不可少的第一步。只有在删除@ts-Ignore注释之后,TypeScript编译器才会发出所有需要解决的诊断错误。

在删除@ts-Ignore注释之后,我们运行hoistClassStaticsPlugin。此插件遍历文件中的所有类声明。它确定我们是否可以提升标识符或表达式,并确定赋值是否已经提升到类。

为了能够快速迭代并防止回归,我们为每个插件和ts-Migrate添加了一系列单元测试。

ReactPropsPlugin将类型信息从PropTypes转换为TypeScript Props类型定义。它是基于Mohsen Azimi编写的令人敬畏的工具。我们只需要在包含至少一个Reaction组件的.tsx文件上运行此插件。ReactPropsPlugin查找所有PropTypes声明,并尝试使用AST和简单的正则表达式(如/number/)或更复杂的情况(如/objectOf$/)来解析它们。当检测到Reaction组件(功能组件或类组件)时,会将其转换为具有新属性类型的组件:类型属性={…。};。

ReactDefaultPropsPlugin涵盖Reaction组件的defaultProps模式。我们使用一种特殊类型来表示具有默认值的道具:

我们尝试查找默认的道具声明,并将它们与上一步生成的组件道具类型合并。

状态和生命周期的概念在REACT生态系统中非常常见。我们在两个插件中解决了这些问题。如果组件是有状态的,那么reactClassStatePlugin会生成一个新类型State=any;并且reactClassLifecycleMethodsPlugin会用适当的类型注释组件生命周期方法。这些插件的功能可以扩展,包括用更具描述性的类型替换任何插件的能力。

对于状态和道具,还有更多改进和更好的类型支持的空间。然而,作为一个起点,事实证明这个功能就足够了。我们也不讨论钩子,因为在迁移开始时,我们的代码库使用的是较旧版本的Reaction。

我们的目标是获得一个具有基本类型覆盖且不会导致应用程序运行时行为更改的编译TypeScript项目。

在所有转换和代码修改之后,我们的代码可能具有不一致的格式,并且某些LINT检查可能会失败。我们的前端代码库依赖于更漂亮的eslint设置--更漂亮的是用于自动套用格式的代码,而ESLint则确保代码遵循最佳实践。因此,我们可以通过从我们的插件运行eslint-puterer来快速修复前面步骤可能引入的任何格式化问题。

迁移流水线的最后一部分确保所有排版脚本编译违规都得到解决。为了检测和修复潜在的错误,tsIgnorePlugin使用行号进行语义诊断,并使用有用的解释插入@ts-忽略注释,例如:

在注释中包含有意义的错误消息可以更容易地修复问题和重新访问需要注意的代码。这些注释与$TSFixMe²相结合,允许我们收集有关代码质量的有用数据,并识别潜在的代码问题区域。

最后但并非最不重要的一点是,我们需要运行eslint-fix插件两次。在tsIgnorePlugin之前,给定的格式可能会影响我们在哪里得到编译器错误。在tsIgnorePlugin之后,由于插入@ts-Ignore注释可能会引入新的格式化错误。

我们的迁移故事正在进行中:我们有一些遗留项目仍在使用JavaScript,我们还有大量的$TSFixMe和@ts-忽略代码库中的注释。

然而,使用ts-Migrate极大地加快了我们的迁移过程和生产力。工程师可以专注于输入改进,而不是手动进行逐个文件的迁移。目前,我们的6M前台单订单中约86%已经转换为打字稿,我们有望在今年年底前实现95%的目标。

您可以在Github存储库的主包中查看ts-Migrate并找到有关如何安装和运行ts-Migrate的说明。如果您发现任何问题或有改进意见,我们欢迎您的贡献!

向Brie Bunge欢呼吧,她是Airbnb Tyescript背后的推动力,也是ts-Migrate的创建者。感谢Joe Lencioni帮助我们在Airbnb采用了Typescript,并改进了我们的Typescript基础设施和工具。特别感谢埃利奥特·萨克斯(Elliot Sachs)和约翰·海特科(John Haytko)为移民做出的贡献。感谢在此过程中提供反馈和帮助的每一个人!