likes
comments
collection
share

使用TypeScript AST API修改TS代码

作者站长头像
站长
· 阅读数 23

最近我遇到一个情况,需要根据另一个项目暴露的d.ts文件,对其中的一个interface添加一些属性的定义,并生成新的d.ts文件。

用非常离谱的方法来做,就是直接用正则表达式的方法强行把新定义的string插入进去。这种方法遇到一些更复杂的场景,如需要修改定义时,正则替换会复杂到无法使用,还容易出bug。

所以对ts文件进行操作时,一般希望从typescript的语义入手去进行ast层面的代码替换。

typescript的ast和很多其他语言的ast一样,是以node(节点)为单位组成的树形结构。

这里有一个非常好用的网站:AST Explorer,点进去后可以把语言切换为JavaScript,parser切换为TypeScript。

先看一段非常简单的声明代码:

declare const Program: {
    /**
   * 执行
   * @param id
   */
    execute(id: string): Promise<void>;
    /**
   * 同步执行
   * @param id 方块id
   */
    executeSync(id: string):void;
};

可以看到TS的最上层是SourceFile类型,其中包含 fileName,path,代码的原始string,还有一些暂时看不懂的flag。

使用TypeScript AST API修改TS代码

根节点的node声明在statements下。目前各个node类型结构是没有官方文档的,基本只能靠生成ast来reverse engineer。

使用TypeScript AST API修改TS代码

继续往下展开,我们可以看到定义属性的列表在第一个declaration的type的members里,其中有两个已经定义的方法

使用TypeScript AST API修改TS代码

MethodSignature的内部结构,name为Identifier,其中escapedText为方法的名称

使用TypeScript AST API修改TS代码

此时如果我们想要获得所有定义方法的名称,可以:

const fs = require('fs')
const ts = require('typescript')

// ts文件的路径
const tsFilePath = ...

// 使用parser生成最上层的SourceFile
const sourceFile = ts.createSourceFile(
  tsFilePath,
  fs.readFileSync(tsFilePath).toString(),
  ts.ScriptTarget.ES2015,
  /*setParentNodes */ true
);

// 获取MethodSignature的列表
const programProperties = sourceFile.statements[0].declarationList.declarations[0].type.members
// 提取出方法的名称
const programFunctionNames = programProperties.map(f => f.name.escapedText)

创建一个新的方法作为Program的属性

需要使用ts.factory里的新建ast的方法。这里使用ts自带的factory。不同node的创建方法可以在typescript的definition文件中查看,参数也需要自行领悟,基本上和ast explorer中的属性是一一对应的。

const newFuncitonSignature = ts.factory.createMethodSignature(
  undefined, // modifiers      (export 等
  'abc',     // name           (方法名称
  undefined, // questionToken  (暂时不知道是啥
  undefined, // typeParameters (暂时不知道是啥
  [],        // parameters     (方法的参数&类型
  ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword) // type (return类型
)

// 把新建的方法加入Program的属性列表
programProperties.push(newFuncitonSignature)

那如果我们想要复用一段其他代码呢?

比如现在我们想要merge两个interface,我们可以直接把一个interface中的Node直接放进另外一个interface的列表里吗?答案是不可以,因为node的数据结构中的pos(位置)和end(结束)指针是会直接影响到生成的ts文件中对应代码的位置的,而且不会在生成代码时重新validate位置的正确性。想要复用已有代码转换而来的node,需要把它自身和所有子节点的pos和end清空为-1。

使用TypeScript AST API修改TS代码

这里推荐一个非常好用的库,可以完整地clone一个node且将其所有pos和end重置:

const { cloneNode } = require("ts-clone-node")
const clonedNode = cloneNode(existingNode)
programProperties.push(clonedNode)

现在printer在生成代码时发现pos和end为-1,需要重新计算,生成的代码排版就正常了。

对于一些node,如果我们只想更新它的属性,不想重新clone一个的话,factory中也有一些update方法可以快速更改并重新计算坐标指针:

const newSignature = ts.factory.updateMethodSignature(...)

最后使用ast生成ts代码:

// 将ast转换为typescript代码
const printer = ts.createPrinter();
const result = printer.printFile(sourceFile)

result为一个string,可以用fs写入文件。