第八章 - 编写 TypeScript 转换器

第八章 - 编写 TypeScript 转换器

学习如何重写AST来创建类似Babel的转换器

在上一章中,我们学习了如何使用TypeScript编译器API的工厂函数从零生成代码。本章将继续基于这个新知识,学习如何选择性地替换现有代码库中的部分内容!

这种选择性替换代码的策略被广泛应用于Babel、ESBuild、SWC等转译器和打包工具中。这些工具通常通过将代码转译为更多浏览器或Node引擎支持的语法,让开发者能更容易地编写现代JavaScript/TypeScript。

一个典型的降级转换例子是将箭头函数转为普通函数声明。曾经有段时间箭头函数刚被纳入ECMAScript规范,但浏览器还不支持该语法。许多开发者就使用工具转译代码,这样既能立即使用箭头函数编写代码,又能确保在浏览器中正常运行。

转换器实现

本章我们将实现把箭头函数转换为普通函数声明的例子。为了简化,我们不会递归查找所有箭头函数实例,只检查源文件中的根节点并进行转换。

我们继续使用fun.ts作为目标转换文件,内容如下:

typescript 复制代码
export const hello = (text?: string) => {  console.log(`Hello, ${text ?? "World!"}`)}
let world = <T>(t: T): T => {    return t  },  another = () => {    console.log("another")  },  a = 5,  b = "string",  c = true

这段代码展示了多种箭头函数:带export的const声明、使用泛型的函数、以及变量声明列表中的多个函数。我们的转换器需要处理所有这些情况。

index.mjs中,我们先清空之前的内容,然后添加以下基础代码:

typescript 复制代码
import ts from "typescript"
const program = ts.createProgram(["fun.ts"], {
  module: ts.ModuleKind.ESNext
})

const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed })

for (const rootFileName of program.getRootFileNames()) {
  const sourceFile = program.getSourceFile(rootFileName)
  
  if (sourceFile && !sourceFile.isDeclarationFile) {
    transformSourceFile(sourceFile)
  }

  const str = printer.printNode(ts.EmitHint.Unspecified, sourceFile, sourceFile)
  console.log(str)
}

和之前一样,我们创建TypeScript程序来解析fun.ts,并用printer输出修改后的代码。遍历程序中的根文件时,如果不是声明文件就传给transformSourceFile函数处理。这个函数将包含我们的转换逻辑,处理完成后用printer打印结果。

现在来看transformSourceFile的实现。我们需要遍历源文件的根节点,如果发现变量声明语句且其初始化器是箭头函数,就执行转换:

typescript 复制代码
/**
 * @param {ts.SourceFile} sourceFile
 */
function transformSourceFile(sourceFile) {
  sourceFile.forEachChild((node) => {
    if (ts.isVariableStatement(node)) {
      node.declarationList.declarations.forEach((declaration) => {
        if (declaration.initializer && ts.isArrowFunction(declaration.initializer)) {
          declaration.initializer = transformArrowFunction(declaration.initializer)
        }
      })
    }
  })
}

找到箭头函数初始化器后,我们直接修改声明节点的初始化器,将其替换为新的函数表达式。转换工作由transformArrowFunction函数完成:

typescript 复制代码
/**
 * 将箭头函数转换为函数表达式
 * @param {ts.ArrowFunction} arrowFunction
 * @returns {ts.FunctionExpression}
 */
function transformArrowFunction(arrowFunction) {
  return ts.factory.createFunctionExpression(
    arrowFunction.modifiers,
    arrowFunction.asteriskToken,
    arrowFunction.name,
    arrowFunction.typeParameters,
    arrowFunction.parameters,
    arrowFunction.type,
    arrowFunction.body
  )
}

转换过程非常简单,因为箭头函数和普通函数共享大部分属性。我们只需将所有属性传递给新创建的节点,就能生成有效的AST。

运行脚本后,控制台将输出:

typescript 复制代码
export const hello = function (text?: string) {
  console.log(`Hello, ${text ?? "World!"}`)
}
let world = function <T>(t: T): T {
    return t
  },
  another = function () {
    console.log("another")
  },
  a = 5,
  b = "string",
  c = true

成功!所有根级箭头函数都替换成了函数表达式,且参数、类型、修饰符等所有信息都完整保留。如果需要,我们可以将这个输出字符串写入实际文件。

需要说明的是,当前实现只转换根节点。如果要转换文件中的所有箭头函数,需要像前几章那样递归遍历AST。

本章总结

本章我们通过将箭头函数转为普通函数的实例,学习了如何修改AST来自动重写代码。这是许多流行工具使用的强大功能,能为开发者提供更好的开发体验。

下一章我们将学习如何为转换逻辑编写单元测试,确保转换结果符合预期!

相关推荐
LYFlied2 小时前
【算法解题模板】-【回溯】----“试错式”问题解决利器
前端·数据结构·算法·leetcode·面试·职场和发展
composurext2 小时前
录音切片上传
前端·javascript·css
程序员小寒2 小时前
前端高频面试题:深拷贝和浅拷贝的区别?
前端·javascript·面试
狮子座的男孩2 小时前
html+css基础:07、css2的复合选择器_伪类选择器(概念、动态伪类、结构伪类(核心)、否定伪类、UI伪类、目标伪类、语言伪类)及伪元素选择器
前端·css·经验分享·html·伪类选择器·伪元素选择器·结构伪类
zhougl9962 小时前
Vue 中的 `render` 函数
前端·javascript·vue.js
听风吟丶2 小时前
Spring Boot 自动配置深度解析:原理、实战与源码追踪
前端·bootstrap·html
跟着珅聪学java2 小时前
HTML中设置<select>下拉框默认值的详细教程
开发语言·前端·javascript
IT_陈寒2 小时前
JavaScript 性能优化:5个被低估的V8引擎技巧让你的代码提速50%
前端·人工智能·后端
想睡好2 小时前
setup
前端·javascript·html
光影少年2 小时前
react navite相比较传统开发有啥优势?
前端·react.js·前端框架