使用TypeScript AST API修改TS代码
最近我遇到一个情况,需要根据另一个项目暴露的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。
根节点的node声明在statements下。目前各个node类型结构是没有官方文档的,基本只能靠生成ast来reverse engineer。
继续往下展开,我们可以看到定义属性的列表在第一个declaration的type的members里,其中有两个已经定义的方法
MethodSignature的内部结构,name为Identifier,其中escapedText为方法的名称
此时如果我们想要获得所有定义方法的名称,可以:
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。
这里推荐一个非常好用的库,可以完整地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写入文件。
转载自:https://juejin.cn/post/7049563485583179812