Open xingbofeng opened 1 year ago
在 TypeScript 中引入了 TS Compiler API,使得开发人员可以通过编程的方式访问和操作 TypeScript 抽象语法树(AST)。TS Compiler API 提供了一系列的 API 接口,可以帮助我们分析、修改和生成 TypeScript 代码。不过 TS Compiler API 的使用门槛较高,需要比较深厚的 TypeScript 知识储备。
为了简化这个过程,社区出现了一些基于 TS Compiler API 的工具,如 ts-simple-ast、dprint、ttypescript 等,而 ts-morph 就是其中之一,它提供了更加友好的 API 接口,并且可以轻松地访问 AST,完成各种类型的代码操作,例如重构、生成、检查和分析等。
除此之外,还有在线 TS AST 工具:AST Viewer 可以用来快速查看 TypeScript 代码的 AST 结构。
本文将介绍如何使用ts-morph进行TypeScript AST操作,包括以下几个方面:
安装ts-morph非常简单,只需要执行以下命令即可:
npm install ts-morph --save-dev
在使用ts-morph之前,我们首先需要创建一个TypeScript项目,并将所有的源文件添加到项目中。假设我们已经有了一个tsconfig.json文件,其中包含了项目中所有的源文件路径,我们可以使用以下代码将这些源文件加载到ts-morph项目中:
import { Project } from "ts-morph"; // 创建一个TypeScript项目对象 const project = new Project(); // 从文件系统加载tsconfig.json文件,并将其中的所有源文件添加到项目中 project.addSourceFilesAtPaths("./tsconfig.json"); // 获取项目中的所有源文件 const sourceFiles = project.getSourceFiles();
现在我们就可以通过sourceFiles变量来访问项目中的所有源文件。
在访问TypeScript代码中的基本元素时,ts-morph提供了很多方便的API接口,例如getSourceFiles()、getClasses()、getFunctions()等方法都可以帮助我们快速定位到目标元素的位置,并获取其具体属性和信息。
以下是一些常见的代码示例:
import { Project, SyntaxKind } from "ts-morph"; const project = new Project(); project.addSourceFilesAtPaths("./src/**/*.ts"); const sourceFiles = project.getSourceFiles(); // 获取所有类名 const classNames = sourceFiles.flatMap((sourceFile) => sourceFile.getClasses().map((classDecl) => classDecl.getName()) ); console.log(classNames); // 获取所有函数名 const functionNames = sourceFiles.flatMap((sourceFile) => sourceFile.getFunctions().map((funcDecl) => funcDecl.getName()) ); console.log(functionNames); // 获取所有import语句 const imports = sourceFiles.flatMap((sourceFile) => sourceFile.getImportDeclarations()); imports.forEach((importDecl) => { console.log(importDecl.getModuleSpecifierValue()); }); // 获取所有变量声明 const variables = sourceFiles.flatMap((sourceFile) => sourceFile.getVariableDeclarations()); variables.forEach((variable) => { console.log(variable.getName()); });
以上代码演示了如何使用ts-morph访问TypeScript代码中的基本元素,包括类、函数、import语句和变量声明等。
在分析TypeScript项目时,了解源文件之间的依赖关系和调用链非常重要。ts-morph提供了getDependencyGraph()和getCallGraph()两个方法,可以帮助我们分析项目中的依赖关系和调用链信息。
以下是一个分析依赖关系和调用链的代码示例:
import { Project } from "ts-morph"; const project = new Project(); project.addSourceFilesAtPaths("./src/**/*.ts"); // 获取依赖关系 const dependencyGraph = project.getDependencyGraph(); console.log(dependencyGraph.toString()); // 获取调用链 const callGraph = project.getCallGraph(); callGraph.forEach((value, key) => { console.log(`Function ${key} is called by:`); value.forEach((caller) => { console.log(` ${caller}`); }); });
以上代码演示了如何使用ts-morph分析TypeScript项目中的依赖关系和调用链。在这个示例中,我们首先获取了项目的依赖关系图,并将其转化为一个字符串进行输出;接着,我们获取了项目的调用链信息,并按照函数名逐一输出其调用者。
在使用ts-morph进行TypeScript AST操作时,最常见的需求之一就是修改已有的TypeScript代码。ts-morph提供了各种API接口,可以帮助我们定位到目标元素,修改其属性或内容,并将修改后的结果保存到文件系统中。
以下是一个修改TypeScript代码的代码示例:
import { Project } from "ts-morph"; const project = new Project(); project.addSourceFilesAtPaths("./src/**/*.ts"); const sourceFiles = project.getSourceFiles(); sourceFiles.forEach((sourceFile) => { sourceFile.getClasses().forEach((classDecl) => { classDecl.getMethods().forEach((methodDecl) => { if (methodDecl.getName() === "doSomething") { const block = methodDecl.getBody(); const statements = block.getStatements(); const firstStatement = statements[0]; const secondStatement = statements[1]; // 将原来的两行代码合并成一行,并添加注释 firstStatement.replaceWithText(`console.log("Hello, world!"); // Modified by ts-morph`); secondStatement.remove(); } }); }); // 将修改后的源文件保存到文件系统中 sourceFile.saveSync(); });
以上代码演示了如何使用ts-morph修改一个TypeScript源文件中的方法体内容。在这个示例中,我们首先遍历所有类和方法,找到包含名为“doSomething”的方法,并将其第一行和第二行代码修改为一行代码和一个注释;接着,我们将修改后的源文件保存到文件系统中。
除了修改已有的TypeScript代码之外,有时候我们还需要生成全新的TypeScript代码。ts-morph也提供了非常方便的API接口,可以帮助我们快速生成任意类型的TypeScript代码片段,并将其保存到文件系统中。
以下是一个生成TypeScript代码的代码示例:
import { Project } from "ts-morph"; const project = new Project(); project.createSourceFile( "./src/generated/HelloWorld.ts", `export function helloWorld(): void { console.log("Hello, world!"); }` ); // 将新生成的源文件保存到文件系统中 project.saveSync();
以上代码演示了如何使用ts-morph生成一个新的TypeScript源文件。在这个示例中,我们通过createSourceFile()方法创建了一个包含打印“Hello, world!”函数定义的TypeScript源文件,并将其保存到文件系统中。
使用 ts-morph 可以自动生成文档。例如,可以使用 ts-morph 分析 TypeScript 中的 JSDoc,最终生成包含函数名、描述和参数信息的 Markdown 或者 HTML 文档。
下面我们将演示如何使用 ts-morph 自动生成文档。首先,我们需要有一段包含了用户定义的接口和类的 TypeScript 代码,并在其中添加上注释。例如,我们写了一个 Person 接口和一个 Greeter 类,并给它们添加了 JSDoc 注释:
// src/example.ts /** * 这是一个用于演示的类 */ class ExampleClass { /** * 这是一个用于演示的方法 * @param name - 姓名 * @param age - 年龄 * @returns 返回一个字符串,表示问候语和年龄 */ sayHello(name: string, age: number): string { return `Hello, ${name}! You are ${age} years old.`; } } /** * 这是一个用于演示的接口 */ interface ExampleInterface { /** * 这是一个用于演示的属性 */ readonly id: number; /** * 这是一个用于演示的方法 * @param x - 第一个参数 * @param y - 第二个参数 * @returns 返回两个参数的和 */ add(x: number, y: number): number; }
把上述代码存放在src目录下,并执行下面的脚本来解析此文件,能够输出相应的API文档:
src
import * as fs from "fs"; import { Project } from "ts-morph"; const project = new Project({ tsConfigFilePath: "./tsconfig.json", }); project.addSourceFilesAtPaths("./src/example.ts"); const data = project.getSourceFiles().map((file) => { const classes = file.getClasses(); const classList = classes.map((cls) => { const doc = cls.getJsDocs()[0]?.getDescription().trim() || ""; // Get methods const methodList = cls.getMethods().map((method) => { const signature = `### ${method.getName()}(${method.getParameters().map((p) => `${p.getName()}: ${p.getType().getText()}`).join(", ")})`; const description = method.getJsDocs()[0]?.getDescription().trim() || ""; const parameters = method.getParameters().map((p) => { const paramStructure = p.getStructure(); const paramName = paramStructure.name; const paramTags = method.getJsDocs()[0]?.getTags() .filter(tag => tag.getTagName() === "param" && tag.getComment()); const paramJSDoc = paramTags?.map(tag => { const parts = (tag.getComment() as string).split(/\s+/) ?? []; const type = parts[0]; const description = parts.slice(1).join(" "); return `${type}: ${description}`; })[0] ?? ''; return `\`${paramName}\`: ${paramJSDoc}`; }).join('\n'); const returnType = method.getReturnTypeNode() ? `\n\n**Return Type:** \`${method.getReturnTypeNode()?.getText()}\`` : ""; return [signature, description, parameters, returnType].filter(Boolean).join("\n"); }); // Get properties const propertyList = cls.getProperties().map((property) => { const signature = `### ${property.getName()}`; const description = property.getJsDocs()[0]?.getDescription().trim() || ""; const type = property.getTypeNode() ? `\n\n**Type:** \`${property.getTypeNode()?.getText()}\`` : ""; return [signature, description, type].filter(Boolean).join("\n"); }); // Combine methods and properties const memberList = [...methodList, ...propertyList].join("\n\n"); return [`## ${cls.getName()} \n\n${doc}`, memberList].join("\n\n"); }); const interfaces = file.getInterfaces(); const interfaceList = interfaces.map((intf) => { const doc = intf.getJsDocs()[0]?.getDescription().trim() || ""; // Get methods const methodList = intf.getMethods().map((method) => { const signature = `### ${method.getName()}(${method.getParameters().map((p) => `${p.getName()}: ${p.getType().getText()}`).join(", ")})`; const description = method.getJsDocs()[0]?.getDescription().trim() || ""; const parameters = method.getParameters().map((p) => { const paramStructure = p.getStructure(); const paramName = paramStructure.name; const paramTags = method.getJsDocs()[0]?.getTags() .filter(tag => tag.getTagName() === "param" && tag.getComment()); const paramJSDoc = paramTags?.map(tag => { const parts = (tag.getComment() as string).split(/\s+/) ?? []; const type = parts[0]; const description = parts.slice(1).join(" "); return `${type}: ${description}`; })[0] ?? ''; return `\`${paramName}\`: ${paramJSDoc}`; }).join('\n'); const returnType = method.getReturnTypeNode() ? `\n\n**Return Type:** \`${method.getReturnTypeNode()?.getText()}\`` : ""; return [signature, description, parameters, returnType].filter(Boolean).join("\n"); }); // Get properties const propertyList = intf.getProperties().map((property) => { const signature = `### ${property.getName()}`; const description = property.getJsDocs()[0]?.getDescription().trim() || ""; const type = property.getTypeNode() ? `\n\n**Type:** \`${property.getTypeNode()?.getText()}\`` : ""; const readonly = property.isReadonly() ? "\n\n**Readonly**" : ""; return [signature, description, type, readonly].filter(Boolean).join("\n"); }); // Combine methods and properties const memberList = [...methodList, ...propertyList].join("\n\n"); return [`## ${intf.getName()} \n\n${doc}`, memberList].join("\n\n"); }); return [...classList, ...interfaceList].join("\n\n"); }); fs.writeFileSync("output.md", data.join("\n"));
在这个示例中,我们首先创建了一个项目对象,并添加了 TypeScript 文件。然后,通过调用 getSourceFiles() 方法获取所有源文件,并使用 flatMap() 方法来遍历每个类和接口定义,解析其中的 JSDoc 信息,并格式化成一个通用的 API 文档格式。
getSourceFiles()
flatMap()
然后,我们将 apiDocs 数组转换为 Markdown 格式的文档。对于每个类或接口,我们使用其名称、描述、方法和属性等信息生成 Markdown 文档的各个部分。具体地,我们按照如下格式生成 Markdown 文档:
apiDocs
## [类/接口名] [类/接口描述] [方法列表] [属性列表]
其中,方法列表和属性列表会根据不同的类型生成不同的格式:
### [方法名]
-
最后,我们将生成的 Markdown 文档保存到文件系统中,以便于查看和分享。您可以使用其他工具将 Markdown 转换成 HTML 或者其他格式的文档。
ts-morph是一个非常有用的TypeScript库,它提供了一个简单且直观的API,用于分析、生成和转换TypeScript代码。使用ts-morph,您可以轻松地创建自定义代码生成器、重构工具或其他自动化任务。该库还提供了丰富的类型信息,包括类型检查、符号解析和语法分析等功能,这些都可以帮助您更好地理解和操作TypeScript代码。如果你需要对TypeScript进行重构、格式化、分析、自动生成API文档等操作,ts-morph是一个非常有用的工具。它提供了一组功能强大的API,可以让您轻松地执行这些任务,并且不需要手动处理代码。总之,ts-morph是一个功能强大的TypeScript库,可以帮助您更轻松地管理和操作代码。
简介
在 TypeScript 中引入了 TS Compiler API,使得开发人员可以通过编程的方式访问和操作 TypeScript 抽象语法树(AST)。TS Compiler API 提供了一系列的 API 接口,可以帮助我们分析、修改和生成 TypeScript 代码。不过 TS Compiler API 的使用门槛较高,需要比较深厚的 TypeScript 知识储备。
为了简化这个过程,社区出现了一些基于 TS Compiler API 的工具,如 ts-simple-ast、dprint、ttypescript 等,而 ts-morph 就是其中之一,它提供了更加友好的 API 接口,并且可以轻松地访问 AST,完成各种类型的代码操作,例如重构、生成、检查和分析等。
除此之外,还有在线 TS AST 工具:AST Viewer 可以用来快速查看 TypeScript 代码的 AST 结构。
本文将介绍如何使用ts-morph进行TypeScript AST操作,包括以下几个方面:
安装ts-morph
安装ts-morph非常简单,只需要执行以下命令即可:
常见的基本应用
创建TypeScript项目
在使用ts-morph之前,我们首先需要创建一个TypeScript项目,并将所有的源文件添加到项目中。假设我们已经有了一个tsconfig.json文件,其中包含了项目中所有的源文件路径,我们可以使用以下代码将这些源文件加载到ts-morph项目中:
现在我们就可以通过sourceFiles变量来访问项目中的所有源文件。
访问基本元素
在访问TypeScript代码中的基本元素时,ts-morph提供了很多方便的API接口,例如getSourceFiles()、getClasses()、getFunctions()等方法都可以帮助我们快速定位到目标元素的位置,并获取其具体属性和信息。
以下是一些常见的代码示例:
以上代码演示了如何使用ts-morph访问TypeScript代码中的基本元素,包括类、函数、import语句和变量声明等。
分析依赖关系和调用链
在分析TypeScript项目时,了解源文件之间的依赖关系和调用链非常重要。ts-morph提供了getDependencyGraph()和getCallGraph()两个方法,可以帮助我们分析项目中的依赖关系和调用链信息。
以下是一个分析依赖关系和调用链的代码示例:
以上代码演示了如何使用ts-morph分析TypeScript项目中的依赖关系和调用链。在这个示例中,我们首先获取了项目的依赖关系图,并将其转化为一个字符串进行输出;接着,我们获取了项目的调用链信息,并按照函数名逐一输出其调用者。
修改TypeScript代码
在使用ts-morph进行TypeScript AST操作时,最常见的需求之一就是修改已有的TypeScript代码。ts-morph提供了各种API接口,可以帮助我们定位到目标元素,修改其属性或内容,并将修改后的结果保存到文件系统中。
以下是一个修改TypeScript代码的代码示例:
以上代码演示了如何使用ts-morph修改一个TypeScript源文件中的方法体内容。在这个示例中,我们首先遍历所有类和方法,找到包含名为“doSomething”的方法,并将其第一行和第二行代码修改为一行代码和一个注释;接着,我们将修改后的源文件保存到文件系统中。
生成TypeScript代码
除了修改已有的TypeScript代码之外,有时候我们还需要生成全新的TypeScript代码。ts-morph也提供了非常方便的API接口,可以帮助我们快速生成任意类型的TypeScript代码片段,并将其保存到文件系统中。
以下是一个生成TypeScript代码的代码示例:
以上代码演示了如何使用ts-morph生成一个新的TypeScript源文件。在这个示例中,我们通过createSourceFile()方法创建了一个包含打印“Hello, world!”函数定义的TypeScript源文件,并将其保存到文件系统中。
自动生成文档
使用 ts-morph 可以自动生成文档。例如,可以使用 ts-morph 分析 TypeScript 中的 JSDoc,最终生成包含函数名、描述和参数信息的 Markdown 或者 HTML 文档。
下面我们将演示如何使用 ts-morph 自动生成文档。首先,我们需要有一段包含了用户定义的接口和类的 TypeScript 代码,并在其中添加上注释。例如,我们写了一个 Person 接口和一个 Greeter 类,并给它们添加了 JSDoc 注释:
把上述代码存放在
src
目录下,并执行下面的脚本来解析此文件,能够输出相应的API文档:在这个示例中,我们首先创建了一个项目对象,并添加了 TypeScript 文件。然后,通过调用
getSourceFiles()
方法获取所有源文件,并使用flatMap()
方法来遍历每个类和接口定义,解析其中的 JSDoc 信息,并格式化成一个通用的 API 文档格式。然后,我们将
apiDocs
数组转换为 Markdown 格式的文档。对于每个类或接口,我们使用其名称、描述、方法和属性等信息生成 Markdown 文档的各个部分。具体地,我们按照如下格式生成 Markdown 文档:其中,方法列表和属性列表会根据不同的类型生成不同的格式:
### [方法名]
和-
[属性名]: [类型][描述]`` 的格式,并按照顺序罗列出来。最后,我们将生成的 Markdown 文档保存到文件系统中,以便于查看和分享。您可以使用其他工具将 Markdown 转换成 HTML 或者其他格式的文档。
总结
ts-morph是一个非常有用的TypeScript库,它提供了一个简单且直观的API,用于分析、生成和转换TypeScript代码。使用ts-morph,您可以轻松地创建自定义代码生成器、重构工具或其他自动化任务。该库还提供了丰富的类型信息,包括类型检查、符号解析和语法分析等功能,这些都可以帮助您更好地理解和操作TypeScript代码。如果你需要对TypeScript进行重构、格式化、分析、自动生成API文档等操作,ts-morph是一个非常有用的工具。它提供了一组功能强大的API,可以让您轻松地执行这些任务,并且不需要手动处理代码。总之,ts-morph是一个功能强大的TypeScript库,可以帮助您更轻松地管理和操作代码。