背景
相信很多小伙伴与我一样,可能会碰到这样的问题,公司有统一使用一个通用的工具库或者框架,并且已经迭代了多个大版本,但是大版本之间有破坏性更新,现在为了降低维护成本与使用难度,决定只维护最新的一个大版本,但是有很多项目一直还在使用低版本,没有跟随工具库或者框架迭代升级,这个时候如果强行推动让使用方自己升级,会发现有很大的困难,使用方会找各种理由来推脱不进行升级,那么作为库或者框架的维护者,我们一般会这么做
- 不升就不升,出了问题不要再来找我
- 手动帮助使用方升级
- 提供一个工具,使用方通过工具来完成自动升级
第一种不可取,第二种适用于项目较少的情况,第三种则是适用项目较多的情况,我们公司目前恰好属于第三种,为了降低升级成本,决定通过开发一个cli工具来进行项目自动升级,通过工具来自动增、删、改项目中有变化的代码
工具处理过程中碰到最有挑战的问题如下所示
低版本中方法调用示例
typescript
// 这部分是grpc方法,由脚本自动生成
GetAllGrey(
request: types.backstage.GetAllGreyRequest,
metadata: MetadataMap,
options?: { timeout?: number; flags?: number; host?: string; }
): Promise<types.backstage.GetAllGreyResponse>;
// 然后在service中调用改方法
const { data, code } = await this.xxxService.GetAllGrey({id: 1}, {token: 'xxxx'})
高版本中方法调用示例
typescript
// 这部分是grpc方法,由脚本自动生成
GetAllGrey(option: {
request: types.backstage.GetAllGreyRequest,
metadata: MetadataMap,
options?: { timeout?: number; flags?: number; host?: string; }
}): Promise<{ response:types.backstage.GetAllGreyResponse, metadata: Metadata }>;
// 然后在service中调用改方法
const { response: { data, code } } = await this.xxxService.GetAllGrey({
request: {id: 1},
metadata: {token: 'xxxx'}
})
高版本相对于低版本,主要的变化在于传参方式发生了改变 ,及返回值包裹了一层response,至于为什么发生这些变化不做讨论,工具主要要做的就是怎么自动实现这个变化,将低版本的写法转换成高版本的写法
最开始想到的思路是通过replace + 正则的方式来匹配与替换,但是实际上项目内的写法有多种,replace + 正则的方式不够用,随后想到ast操作,由于项目代码都是使用typescript编写,找了一圈之后,发现ts-morph这个工具库,这个工具库在typescript compiler api的基础上做了一层封装,更利于我们操作typescript中的ast,所以决定使用ts-morph来完成我们的目标
在实现具体的功能之前,先一起了解下ts-morph是怎样操作ast的
ts-morph
ts-morph是一个针对 Typescrpit/Javascript的AST处理库,可用于浏览、修改TS/JS的AST。 在 TypeScript 中引入了 TS Compiler API,开发人员可以通过编程的方式访问和操作 TypeScript 抽象语法树(AST)。TS Compiler API 提供了一系列的 API 接口,可以帮助我们分析、修改和生成 TypeScript 代码。不过 TS Compiler API 的使用门槛较高,需要比较深厚的 TypeScript 知识储备。 为了简化这个过程,社区出现了一些基于 TS Compiler API 的工具, ts-morph 就是其中之一,它提供了更加友好的 API 接口,并且可以轻松地访问 AST,完成各种类型的代码操作,例如重构、生成、检查和分析等。
安装依赖
typescript
pnpm install --save-dev ts-morph
本文书写时,使用的版本为ts-morph@20.0.0
项目初始化
通过Project初始化一个typescript项目
typescript
import { Project } from 'ts-morph';
const project = new Project({
compilerOptions: {
target: ScriptTarget.ES3,
},
});
通过tsConfigFilePath指定tsconfig.json,根据tsconfig.json获取源文件
typescript
// 可以通过compilerOptions覆盖tsconfig.json参数
const project = new Project({
11 tsConfigFilePath: '<替换成tsconfig.json文件路径>'
12 })
也可以通过tsConfigFilePath指定tsconfig.json
typescript
const project = new Project({
tsConfigFilePath: "path/to/tsconfig.json",
skipAddingFilesFromTsConfig: true, // 可以通过这个参数跳过解析tsconfig中指定的源文件
});
// 也可以通过addSourceFilesFromTsConfig方法指定tsconfig.json
project.addSourceFilesFromTsConfig("path/to/tsconfig.json");
还可以通过project.compilerOptions设置ts配置参数
typescript
const oldCompilerOptions = project.getCompilerOptions()
project.compilerOptions.set({
...oldCompilerOptions,
noImplicitReturns: true // 在内存中打开 noImplicitReturns 选项,使得所有 no-implicit-returns 的情况都会有一个TS错误,错误代号为 TS7030
})
也可以不指定tsconfig.json文件,通过addSourceFilesAtPaths、addSourceFileAtPath方法添加源文件
typescript
import { Project } from 'ts-morph'
const project = new Project()
// glob语法添加多个源文件
project.addSourceFilesAtPaths('src/**/*{.js,.jsx}');
// 添加单个文件
project.addSourceFileAtPath("path/to/file.ts")
通过ast的方式创建源文件
typescript
const sourceFile = project.createSourceFile("path/to/myStructureFile.ts", {
statements: [{
kind: StructureKind.Enum,
name: "MyEnum",
members: [{
name: "member",
}],
}, {
kind: StructureKind.Class,
name: "MyClass",
// etc...
}],
// etc...
});
通过字符串的方式创建源文件
typescript
const fileText = "enum MyEnum {\n}\n";
const sourceFile = project.createSourceFile("path/to/myNewFile.ts", fileText);
获取源文件
当我们完成项目初始化之后,那么就是要进一步操作,比如获取源文件列表,获取类型错误等
typescript
// 获取项目内所有文件的编译错误
const diagnostics = project.getPreEmitDiagnostics()
// 获取某个文件的编译错误
const sourceFileDiagnostics = sourceFile.getPreEmitDiagnostics();
// 格式化输出编译错误
console.log(project.formatDiagnosticsWithColorAndContext(diagnostics));
// 获取源文件列表
const files = project.getSourceFiles()
files.forEach(file => {
// TODO
})
每个编译信息包含的内容
typescript
// 获取错误信息
const message = diagnostic.getMessageText();
// 获取错误码
const code = diagnostic.getCode(); // returns: number
// 获取错误文件
const sourceFile = diagnostic.getSourceFile(); // returns: SourceFile | undefined
// 获取文件中的位置、行号、长度
const start = diagnostic.getStart(); // returns: number
const lineNumber = diagnostic.getLineNumber(); // returns: number
const length = diagnostic.getLength(); // returns: number
// 获取信息类别
const category = diagnostic.getCategory(); // returns: DiagnosticCategory
遍历错误信息
typescript
for (const item of diagnostics) {
const file = item.getSourceFile()
if (file !== undefined) {
const start = item.getStart() ?? 0
const length = item.getLength() ?? 0
if (start > 0 && length > 0) {
// 先根据 start 位置定位到具体的Node,再获取Parent得到所属 Function 的Node
const node = file.getDescendantAtPos(start)?.getParent()
if (
(node instanceof FunctionExpression) ||
(node instanceof MethodDeclaration) ||
(node instanceof ArrowFunction) ||
(node instanceof FunctionDeclaration)) {
toModify.push(node)
}
}
}
}
操作源文件
接下来怎么操作具体的ast,对内容如何进行增、删、改呢?这里以文件为例 首先获取项目中的某个源文件
typescript
// 通过getSourceFile获取指定名称的源文件
const sourceFile = project.getSourceFile(filename);
针对语句进行操作
typescript
// 新增语句
const statements = sourceFile.addStatements("console.log(5);\nconsole.log(6);");
// 插入语句
const statements = sourceFile.insertStatements(3, "console.log(5);\nconsole.log(6);");
// 删除语句
sourceFile.removeStatements([1, 3]); // removes statements from index 1 to 3
sourceFile.removeStatement(1); // removes statement at index 1
针对文本进行操作
typescript
// 插入文本内容
sourceFile.insertText(0, writer => writer.writeLine("// some comment")); // or provide a string
// 替换文本内容
sourceFile.replaceText([3, 7], "a"); // "// a comment\n"
// 删除文本内容
sourceFile.removeText(sourceFile.getPos(), sourceFile.getEnd());
针对枚举的操作
typescript
enum MyEnum {
myMember,
}
const myVar = MyEnum.myMember;
const myEnum = sourceFile.getEnum("MyEnum")!;
// 重命名
myEnum.rename("NewEnum");
const member = sourceFile.getEnum("MyEnum")!.getMember("myMember")!;
// 删除枚举
member.remove();
针对类的操作
typescript
export class MyClass {
myProp = 5;
}
const classStructure = classDeclaration.getStructure(); // returns: ClassDeclarationStructure
{
isAbstract: false,
isExported: true,
name: "MyClass",
typeParameters: [],
constructors: [],
properties: [{
name: "myProp",
initializer: "5",
type: undefined,
isReadonly: false,
isStatic: false
}],
methods: []
}
classDeclaration.set({ name: "NewName" });
classDeclaration.set({ properties: [{ name: "newProperty" }] });
// 往源文件中添加类
sourceFile.addClass({ name: "NewClass", ...classDeclaration.getStructure() });
格式化源文件
typescript
var myVariable : string | number;
function myFunction(param : string){
return "";
}
sourceFile.formatText();
sourceFile.formatText({
placeOpenBraceOnNewLineForFunctions: true,
});
源文件本身的增删改查操作
typescript
// 删除源文件
await sourceFile.deleteImmediately();
sourceFile.deleteImmediatelySync();
// 复制源文件
const newSourceFile = sourceFile.copy("newFileName.ts");
const otherSourceFile = sourceFile.copy("other.ts", { overwrite: true });
sourceFile.copyToDirectory("/some/dir");
sourceFile.copyToDirectory(someDirectoryObject);
await sourceFile.copyImmediately("NewFile.ts");
sourceFile.copyImmediatelySync("NewFile2.ts");
// 移动源文件
sourceFile.move("newFileName.ts");
sourceFile.move("other.ts", { overwrite: true });
sourceFile.moveToDirectory("/some/dir");
sourceFile.moveToDirectory(someDirectoryObject);
await sourceFile.moveImmediately("NewFile.ts");
sourceFile.moveImmediatelySync("NewFile2.ts");
// 移除文件,与删除有区别,这个不会真正的删除文件
sourceFile.forget();
project.removeSourceFile(sourceFile);
操作ast节点
上面包含了对源文件中部分操作,下面是针对源文件中本来就有的Class、Docorators、Enums、Functions、Imports、Params、Variable等操作,到这里我们需要借助辅助工具TypeScript AST Viewer,帮助我们查找节点的信息,如下图所示
选择数的展示模式
forEachChild:只包含真实子节点
getChildren:除了真实子节点,还包括一些token信息
通过getChildren、forEachChild方法操作一级子节点
typescript
const allChildren = node.getChildren();
// 遍历当前节点的一级子节点,常用的方法
node.forEachChild(node => {
console.log(node.getText());
});
const classDec = node.forEachChild(node => {
if (Node.isClassDeclaration(node))
return node;
return undefined;
});
通过forEachDescendant遍历所有一级子节点
typescript
node.forEachDescendant(node => console.log(node.getText()));
// 通过forEachDescendant遍历所有一级子节点
const result = node.forEachDescendant((node, traversal) => {
switch (node.getKind()) {
case SyntaxKind.ClassDeclaration:
// skips traversal of the current node's descendants
traversal.skip();
break;
case SyntaxKind.Parameter:
// skips traversal of the current node's descendants and its siblings and all their descendants
traversal.up();
break;
case SyntaxKind.FunctionDeclaration:
// stops traversal completely
traversal.stop();
break;
case SyntaxKind.InterfaceDeclaration:
// stops traversal completely and returns this value
return node;
}
return undefined;
});
即forEachDescendant会比forEachChild包含更多的子节点,在实际使用中,我们使用forEachChild即可
针对源文件中存在的函数进行增删改操作
typescript
// 获取某个文件的所有函数
const functions = sourceFile.getFunctions();
const functions = sourceFile?.getFunctions();
functions?.forEach((node) => {
// 获取函数名称
console.log('node', node.getName())
// 添加参数
node.addParameter({
name: 'name'
})
})
// 获取函数名为Function1的函数
const function1 = sourceFile.getFunction("Function1");
// 获取嵌套的第一个函数
const firstFunctionWithChildFunction = sourceFile.getFunction(f => f.getFunctions().length > 0);
// 添加函数
const functionDeclaration = sourceFile.addFunction({
name: "FunctionName",
});
// 删除函数
functionDeclaration.remove();
// 设置函数内容
functionDeclaration.setBodyText("const myNumber = 5;");
// 获取函数内容
console.log(functionDeclaration.getBodyText());
针对源文件中存在的变量进行增删改操作
typescript
// 获取所有变量声明语句
const variableStatements = sourceFile.getVariableStatements();
variableStatements?.forEach((node) => {
// 获取声明语句的内容
console.log(node.getText());// 返回 const name = 'jack';
})
// 获取第一个export的的变量声明语句
const firstExportedVariableStatement = sourceFile.getVariableStatement(s => s.hasExportKeyword());
// 插入变量
const variableStatement = sourceFile.addVariableStatement({
declarationKind: VariableDeclarationKind.Const, // defaults to "let"
declarations: [{
name: "myNumber",
initializer: "5",
}, {
name: "myString",
type: "string",
initializer: `'my string'`,
}],
});
// 删除变量
variableStatement.remove();
// 获取
const variableDeclarations = variableStatement.getDeclarations();
variableDeclarations?.forEach((node) => {
// 获取声明语句的内容
console.log(node.getText());// 返回 name = 'jack'; 不包含声明关键词
})
sourceFile?.getVariableDeclarations().forEach((n) => {
console.log('variableDeclarations', n.getText()) // 返回 name = 'jack';
})
// 查找指定变量
const variableDeclaration = sourceFile.getVariableDeclaration("myVar");
// 获取string类型的变量
const firstStringTypedVariableDeclaration = sourceFile.getVariableDeclaration(v => v.getType().getText() === "string");
// 添加变量
const declaration = variableStatement.addDeclaration({ name: "num", type: "number" });
// 删除变量声明语句
variableDeclaration.remove();
针对源文件中的表达式进行操作
typescript
// 获取所有的函数调用表达式
const descendantCallExpressions = sourceFile?.getDescendantsOfKind(SyntaxKind.CallExpression);
descendantCallExpressions?.forEach((callExpression) => {
const value = callExpression.getText()
// 替换表达式内容
callExpression.replaceWithText(`${value}.then((res) => res.response || res)`);
// 获取传入的参数
const args = callExpression.getArguments();
args.forEach((node) => {
// 删除参数
callExpression.removeArgument(node);
})
// 添加参数
callExpression.addArgument(`{
request: '{}',
}`);
const returnType = callExpression.getReturnType();
})
// 获取某个节点下面的函数调用表达式,获取其它表达式也是类似的方法
const childCallExpressions = node.getChildrenOfKind(SyntaxKind.CallExpression);
针对源文件中的属性表达式进行操作
typescript
// 获取到所有的字面量对象表达式
const objectLiteralExpressions = sourceFile?.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)
objectLiteralExpressions?.forEach((objectLiteralExpression) => {
// 获取字面量表达式的内容
const content = objectLiteralExpression.getText()
// 遍历字面量表示的每个属性
objectLiteralExpression.getProperties().forEach((p) => {
// console.log('p', p.getText())
})
// 根据属性名,获取对象字面量中的对应的属性key与value
const property = objectLiteralExpression.getProperty("approver_uuid");
console.log('property', property?.getText());
// 给字面量对象添加属性
const propertyAssignment = objectLiteralExpression.addPropertyAssignment({
name: "propertyAssignment",
initializer: "5",
});
// 给字面量对象添加简写属性
const shorthandPropertyAssignment = objectLiteralExpression.addShorthandPropertyAssignment({
name: "shorthandPropertyAssignment",
});
// 给字面量对象添加解构属性
const spreadAssignment = objectLiteralExpression.addSpreadAssignment({ expression: "spreadAssignment" });
// 给字面量对象添加get、set属性
const getAccessor = objectLiteralExpression.addGetAccessor({
name: "someNumber",
returnType: "number",
statements: ["return someNumber;"],
});
const setAccessor = objectLiteralExpression.addSetAccessor({
name: "someNumber",
parameters: [{ name: "value", type: "number" }],
statements: ["someNumber = value;"],
});
// 给字面量对象添加方法
const method = objectLiteralExpression.addMethod({
name: "method",
statements: [`return "some string";`],
});
// 根据属性名移除属性
objectLiteralExpression.getPropertyOrThrow("prop1").remove();
})
针对标识符的操作
typescript
// 获取某个源文件中的所有标识符
const descendantIdentifiers = sourceFile?.getDescendantsOfKind(SyntaxKind.Identifier);
// 获取某个节点下的子节点中的标识符
const childIdentifiers = node.getChildrenOfKind(SyntaxKind.Identifier);
// 获取某个节点下的所有子节点中的标识符
const descendantIdentifiers = node.getDescendantsOfKind(SyntaxKind.Identifier);
descendantIdentifiers?.forEach((identifier) => {
// 获取标识符内容
const text = identifier.getText();
// 标识符改名
identifier.rename("someNewName");
// 获取标识符引用节点
const references = identifier.findReferences();
// 获取查找变量的定义
const definitions = identifier.getDefinitions();
// 查找变量的定义节点
const nodes = identifier.getDefinitionNodes();
})
到这里我们已经了解应该怎么去增删改源文件的内容,总结起来就是
- 首先获取到对应的源文件
- 其次根据源文件获取源文件内的函数、类、函数调用、对象等
- 最后就是根据获取到的函数、类进行增删改
项目实践
已经学会了怎么去增删改源文件中内容,下面要做的就是穷举样例、分析样例,最后通过ts-morph修改代码
分析样例
样例1:
typescript
const ret = await this.xzxCommonCstService.CommRecommendedCsts({
proj_id,
pager: { page, page_size } },
metadata);
// 传参与返回值修改
const { response: ret } = await this.xzxCommonCstService.CommRecommendedCsts({
request: {
pager: { page, page_size }
},
metadata: metadata
});
样例2:
typescript
const { comm_cst_item: list = [], pager } = await this.xzxCommonCstService.AssignCommonCsts({
cst_uuids,
user_ids,
operator_id: user_id,
operator_type: 'admin',
}, metadata, { timeout: API_TIMEOUT });
// 传参与返回值修改
const { response: { comm_cst_item: list = [], pager } } = await this.xzxCommonCstService.AssignCommonCsts({
request: {
cst_uuids,
user_ids,
operator_id: user_id,
operator_type: 'admin',
},
metadata,
options: { timeout: API_TIMEOUT }
});
样例3:
typescript
const result = await Promise.all([
memberUuIds.length && this.memberService.GetMembersByUUIDs({ member_uuids: memberUuIds, with_team: true }, metadata) as any,
clueRelatedIds.length && this.saleService.GetSalesByClueRelatedIDs({ clue_related_ids: _.uniq(clueRelatedIds) }, metadata) as any,
this.yxxExtApiService.getRecommendSourceTexts(metadata.orgcode, recomSourceList),
]).then((res:any) => {
const memberInfoList = memberUuIds.length ? res[0].members || [] : [];
const clueRelatedRet = clueRelatedIds.length ? res[1]?.items || [] : [];
const recomSourceRet = res[2] || [];
return { xxxx };
});
// 传参修改
const result = await Promise.all([
memberUuIds.length && this.memberService.GetMembersByUUIDs({
request: { member_uuids: memberUuIds, with_team: true },
metadata
}).then((res) => res.response || res) as any,
clueRelatedIds.length && this.saleService.GetSalesByClueRelatedIDs({
request: { clue_related_ids: _.uniq(clueRelatedIds) },
metadata
}).then((res) => res.response || res) as any,
this.yxxExtApiService.getRecommendSourceTexts(metadata.orgcode, recomSourceList).then((res) => res.response || res),
]).then((res) => {
const memberInfoMap:any = {};
const clueRelatedMap:any = {};
const recomSourceMap:any = {};
// 参数取值修改
const memberInfoList = memberUuIds.length ? res[0].reponse.members || [] : [];
const clueRelatedRet = clueRelatedIds.length ? res[1]?.reponse.items || [] : [];
const recomSourceRet = res[2] || [];
return { list, pager };
});
从样例可以看到,修改主要围绕在
- 函数调用时传入参数的变化,之前是多个参数,之后是一个对象参数
- 返回值的取值变化,多了一层response
在修改之前,可以借助ast面板,先查看下对应的节点信息,如下图所示
变量节点如下图所示
代码实现
这里有两个思路
- 思路1: 通过sourceFIle找到所有的CallExpression,然后过滤出需要修改的CallExpression,在修改传入的参数,与返回值
- 思路2: 通过ts类型错误,根据错误信息找到对应的CallExpression,然后在修改传入的参数与返回值
这里采用第二种思路
通过 const diagnostics = project.getPreEmitDiagnostics(); 获取项目内所有的类型错误信息,如下所示
typescript
src/controller/TeamController.ts:69:151 - error TS2554: Expected 1 arguments, but got 2.
69 await this.unimemberService.UpdateMemberInfo({ member_id: id, name, mobile_tel, id_card_number, job_number, identity_value, empty_job_number }, metadata);
~~~~~~~~
src/controller/TeamController.ts:96:9 - error TS2554: Expected 1 arguments, but got 2.
96 metadata,
~~~~~~~~
src/controller/TeamController.ts:207:9 - error TS2554: Expected 1 arguments, but got 2.
207 metadata,
~~~~~~~~
src/controller/TeamController.ts:210:14 - error TS2339: Property 'member_id' does not exist on type '{ response: CheckTelExistedResponse; metadata: Metadata; }'.
210 if(ret.member_id) {
~~~~~~~~~
~~~~~~~~
然后遍历所有的错误节点信息,在根据对应的错误编码TS2554过滤错误信息,找到对应的CallExpression,然后在修改对应的传参与返回值,示例代码如下所示
typescript
const codeMap = {
2554: 'modifyCallFunParamsBy2554',
2345: 'modifyCallFunParamsBy2345',
} as const;
class AST {
varActions: Function[];
paramsActions: Function[];
constructor() {
this.varActions = [];
this.paramsActions = [];
}
init({
diagnostics,
indent,
project,
}: {
diagnostics: Diagnostic<ts.Diagnostic>[];
indent: number;
project?: Project;
}) {
this.traverseAst({ diagnostics, indent });
}
traverseAst({ diagnostics, indent }: { diagnostics: Diagnostic<ts.Diagnostic>[]; indent: number }) {
// 每次遍历ast之前先清空数组
this.varActions.length = 0;
this.paramsActions.length = 0;
for (const item of diagnostics) {
const result = item.getMessageText();
const lastText = typeof result !== 'string' ? (result as DiagnosticMessageChain).getMessageText() : result;
const info = {
code: item.getCode(),
text: lastText,
source: item.getSource(),
start: item.getStart() || 0,
length: item.getLength() || 0,
sourceFile: item.getSourceFile(),
};
this[codeMap[info.code as 2554]] && this[codeMap[info.code as 2554]](info, indent);
}
// 注意变量修改要在前,传参修改要在后,避免await this.memberService.GetMembersInfo({ member_ids: [ret.member_id], with_team: false }, metadata); 这种直接在传参的时候,使用上一个请求的返回值来作为参数的场景
this.varActions.forEach((act) => act()); // 遍历执行
this.paramsActions.forEach((act) => act()); // 遍历执行
}
modifyCallFunParamsBy2554(info: Info, indent?: number) {
this.modifyCallFunParams(info, indent);
}
modifyCallFunParamsBy2345(info: Info, indent?: number) {
if (!/request:.*metadata/.test(info.text)) return;
this.modifyCallFunParams(info, indent);
}
modifyCallFunParams(info: Info, indent?: number) {
// console.log(info, typeof info.text !== 'string' ? info.text.getMessageText() : info.text)
// 处理error TS2554: Expected 1 arguments, but got 2.
// 处理error TS2345: Argument of type '{ scene_code: any; type: string; file_name: any; file_type: string; file_content: any; }' is not assignable to parameter of type '{ request: UploadFileRequest; metadata?: MetadataMap | undefined; options?: { timeout?: number | undefined; flags?: number | undefined; host?: string | undefined; type?: string | undefined; } | undefined; }'.Object literal may only specify known properties, and 'scene_code' does not exist in type '{ request: UploadFileRequest; metadata?: MetadataMap | undefined; options?: { timeout?: number | undefined; flags?: number | undefined; host?: string | undefined; type?: string | undefined; } | undefined; }'.
const currentNode = info.sourceFile!.getDescendantAtPos(info.start);
const callExpression =
currentNode?.getParentIfKind(SyntaxKind.CallExpression) ||
currentNode?.getFirstAncestorByKind(SyntaxKind.CallExpression);
if (callExpression) {
// await this.xzxMemberService.SearchUserIDsByName({ name: keyword }, metadata);
// await this.teamService.GetProjTeamList({ proj_ids }, ctx.state.metadata);
const args = callExpression.getArguments();
// const returnStatement = callExpression.getParentIfKind(SyntaxKind.ReturnStatement);
const parentNode = callExpression.getParent();
switch (parentNode?.getKind()) {
case SyntaxKind.AwaitExpression:
const pp = parentNode?.getParent();
if (pp?.isKind(SyntaxKind.VariableDeclaration)) {
const leftVars = pp.getFirstChildByKind(SyntaxKind.Identifier);
if (leftVars) {
// const ret = await this.xzxCommonCstService.AssignCommonCsts({})
const oldText = leftVars?.getText() || '';
this.varActions.unshift(() => {
if (!leftVars.wasForgotten()) {
// 不能使用rename,使用rename会将 const { pager } = ret; 这里的ret也修改了
// realVarNode?.rename(`{response: ${oldText}}`);
leftVars?.replaceWithText(`{response: ${oldText}}`);
}
});
} else {
// const { user_ids = [] } = await this.xzxMemberService.SearchUserIDsByName({ name: keyword }, metadata);
// const { items } = await this.xzxDownlineService.GetMemberBindDownlineCountList({})
const objectBindingPattern = pp.getFirstChildByKind(SyntaxKind.ObjectBindingPattern);
const oldText = objectBindingPattern?.getText();
this.varActions.unshift(() => {
objectBindingPattern?.replaceWithText(`{response: ${oldText}}`);
});
}
} else if (pp?.isKind(SyntaxKind.BinaryExpression)) {
// let result: customTypes.commonResponse
// result = await this.memberInfo.EditStatus(params)
this.paramsActions.unshift(() => {
const newValue = callExpression.getText();
callExpression.replaceWithText(`${newValue}.then((res) => res.response || res)`);
callExpression.formatText({
indentSize: indent,
});
});
}
break;
case SyntaxKind.ArrayLiteralExpression:
case SyntaxKind.PropertyAccessExpression:
// 不带then场景
// Promise.all([
// bProduct.GetProductList({
// ...params,
// issue_project_id: authProject,
// }),
// 带then场景
// bProduct.GetProductList({
// ...params,
// issue_project_id: authProject,
// }).then((res) => res),
this.paramsActions.unshift(() => {
const newValue = callExpression.getText();
callExpression.replaceWithText(`${newValue}.then((res) => res.response || res)`);
callExpression.formatText({
indentSize: indent,
});
});
break;
}
this.paramsActions.unshift(() => {
const values = args.reduce((prev, node, idx) => {
prev.push(node.getText());
callExpression.removeArgument(node);
return prev;
}, [] as string[]);
// console.log('values', values);
callExpression.addArgument(
(() => {
if (values.length === 1) {
return `{
request: ${values[0] || '{}'},
}`;
} else if (values.length === 2) {
return `{
request: ${values[0] || '{}'},
metadata: ${values[1] || '{}'},
}`;
}
return `{
request: ${values[0] || '{}'},
metadata: ${values[1] || '{}'},
options: ${values[2] || '{}'},
}`;
})(),
);
// 处理controller直接调用rpc服务,且没有处理response的场景,需要解析一次response才行
// @Get('/')
// getTag(@QueryParams() params: TagListDto) {
// return this.tagService.memberService.GetLabelList(params)
// }
// if (parentNode?.isKind(SyntaxKind.ReturnStatement) && /{\srequest:.*metadata.*options/.test(info.text)) {
if (parentNode?.isKind(SyntaxKind.ReturnStatement)) {
const newValue = callExpression.getText();
callExpression.replaceWithText(`${newValue}.then((res) => res.response || res)`);
}
callExpression.formatText({
indentSize: indent,
});
});
}
}
}
export async function modifyGrpcParams({ tsConfigFilePath, indent }: { tsConfigFilePath: string; indent: number }) {
const project = new Project({
tsConfigFilePath,
});
// 获取编译错误
const diagnostics = project.getPreEmitDiagnostics();
const ast = new AST();
await ast.init({ diagnostics, indent, project });
await project.save();
}
最终处理结果如下图所示
总结
ts-morph操作ast的步骤主要是
- 初始化项目
- 获取源文件
- 操作源文件
- 操作ast节点
- 保存项目or源文件
借助ts-morph完成ts代码的批量转换,帮助我们加速cli工具的发布,截止到现在已经完成10多个项目的无缝升级。
本篇只是描述了其中一个场景,更多的是给大家提供一个思路,希望对各位小伙伴所有帮助
参考链接