探索 TypeScript 编程的利器:ts-morph 入门与实践

news/2024/11/29 16:41:38/文章来源:https://www.cnblogs.com/dtux/p/18577051

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:贝儿

背景

在开发 web IDE 中生成代码大纲的功能时, 发现自己对 TypeScript 的了解知之甚少,以至于针对该功能的实现没有明确的思路。究其原因,平时的工作只停留在 TypeScript 使用类型定义的阶段,导致缺乏对 TypeScript 更深的了解, 所以本次通过 ts-morph 的学习,对 TypeScript 相关内容初步深入;

基础

TypeScript 如何转译成 JavaScript ?

// typescript -> javascript
// 执行 tsc greet.ts
function greet(name: string) {return "Hello," + name;
}const user = "TypeScript";console.log(greet(user));// 定义一个箭头函数
const welcome = (name: string) => {console.log(`Welcome ${name}`);
};welcome(user);
// typescript -> javascript
function greet(name) {// 类型擦除return "Hello," + name;
}
var user = "TypeScript";
console.log(greet(user));
// 定义一个箭头函数
var welcome = function (name) {// 箭头函数转普通函数// ts --traget 没有指定版本则转译成字符串拼接console.log("Welcome ".concat(name)); // 字符串拼接
};
welcome(user);

大致的流程:
file

tsconfig.json 的作用?

如果一个目录下存在 tsconfig.json 文件,那么它意味着这个目录是 TypeScript 项目的根目录。 tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项。

// 例如执行: tsc --init, 生成默认 tsconfig.json 文件, 其中包含主要配置
{"compilerOptions": {"target": "es2016","module": "commonjs","outDir": "./dist","esModuleInterop": true,"strict": true,"skipLibCheck": true}// 自行配置例如:"includes": ["src/**/*"]"exclude": ["node_modules", "dist", "src/public/**/*"],
}

什么是 AST?

在计算机科学中,抽象语法树 (Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

Declaration

声明节点,是特定类型的节点,在程序中具有语义作用, 用来引入新的标识。

function IAmFunction() {return 1;
} // ---函数声明

file

Statement

语句节点, 语句时执行某些操作的一段代码。

const a = IAmFunction(); // 执行语句

file

Expression

const a = function IAmFunction(a: number, b: number) {return a + b;
}; // -- 函数表达式

file

TypeScript Compiler API 中几乎提供了所有编译相关的 API, 可以进行了类似 tsc 的行为,但是 API 较为底层, 上手成本比较困难, 这个时候就要引出我们的利器: ts-morph , 让 AST 操作更加简单一些。

介绍

ts-morph 是一个功能强大的 TypeScript 工具库,它对 TypeScript 编译器的 API 进行了封装,提供更加友好的 API 接口。可以轻松地访问 AST,完成各种类型的代码操作,例如重构、生成、检查和分析等。

源文件

源文件(SourceFile):一棵抽象语法树的根节点。

import { Project } from "ts-morph";const project = new Project({});
// 创建 ts 文件
const myClassFile = project.createSourceFile("./sourceFiles/MyClass.ts","export class MyClass {}"
);
// 保存在本地
myClassFile.save();// 获取源文件
const sourceFiles = project.getSourceFiles();
// 提供 filePath 获取源文件
const personFile = project.getSourceFile("Models/Person.ts");
// 根据条件 获取满足条件的源文件
const fileWithFiveClasses = project.getSourceFile((f) => f.getClasses().length === 5
);

诊断

file

// 1.添加源文件到 Project 对象中
const myBaseFile = project.addSourceFileAtPathIfExists("./sourceFiles/base.ts");
// 调用诊断方法
const sourceFileDiagnostics = myBaseFile?.getPreEmitDiagnostics();
// 优化诊断
const diagnostics =sourceFileDiagnostics &&project.formatDiagnosticsWithColorAndContext(sourceFileDiagnostics);
// 获取诊断 message
const message = sourceFileDiagnostics?.[0]?.getMessageText();
// 获取报错文件类
const sourceFile = sourceFileDiagnostics?.[0]?.getSourceFile();
//...

操作

// 源文件操作
// 重命名
const project = new Project();
project.addSourceFilesAtPaths("./sourceFiles/compiler.ts");
const sourceFile = project.getSourceFile("./sourceFiles/compiler.ts");
const myEnum = sourceFile?.getEnum("MyEnum");
myEnum?.rename("NewEnum");
sourceFile?.save();
// 移除
const member = sourceFile?.getEnum("NewEnum")!.getMember("myMember")!;
member?.remove();
sourceFile?.save();// 结构
const classDe = sourceFile?.getClass("Test");
const classStructure = classDe?.getStructure();
console.log("classStructure", classStructure);// 顺序
const interfaceDeclaration = sourcefile?.getInterfaceOrThrow("MyInterface");
interfaceDeclaration?.setOrder(1);
sourcefile?.save();// 代码书写
const funcDe = sourceFile?.forEachChild((node) => {if (Node.isFunctionDeclaration(node)) {return node;}return undefined;
});
console.log("funcDe", funcDe);
funcDe?.setBodyText((writer) =>writer.writeLine("let myNumber = 5;").write("if (myNumber === 5)").block(() => {writer.writeLine("console.log('yes')");})
);
sourceFile?.save();// 操作 AST 转化
const sourceFile2 = project.createSourceFile("Example.ts",`class C1 {myMethod() {function nestedFunction() {}}}class C2 {prop1: string;}function f1() {console.log("1");function nestedFunction() {}}`
);sourceFile2.transform((traversal) => {// this will skip visiting the children of the classesif (ts.isClassDeclaration(traversal.currentNode))return traversal.currentNode;const node = traversal.visitChildren();if (ts.isFunctionDeclaration(node)) {return traversal.factory.updateFunctionDeclaration(node,[],undefined,traversal.factory.createIdentifier("newName"),[],[],undefined,traversal.factory.createBlock([]));}return node;
});sourceFile2.save();

提出问题: 引用后重命名是否获取的到? 例如: 通过操作 enum 类型, 如果变量是别名的话,是否也可以进行替换操作?

源文件如下:

// 引用后重命名是否获取的到?
// 操作 AST 文件
import { Project, Node, ts } from "ts-morph";
// 操作
// 设置
// 重命名
const project = new Project();
project.addSourceFilesAtPaths("./sourceFiles/compiler.ts");
const sourceFile = project.getSourceFile("./sourceFiles/compiler.ts");
const myEnum = sourceFile?.getEnum("MyEnum");
console.log("myEnum", myEnum); // 返回 undefined
// -------------------------
// compier.ts 文件
import { a as MyEnum } from "../src/";
interface IText {}
export default class Test {constructor() {const a: IText = {};}
}const a = new Test();enum NewEnum {myMember,
}const myVar = NewEnum.myMember;function getText() {let myNumber = 5;if (myNumber === 5) {console.log("yes");}
}
// src/index.ts 文件
export enum a {}

分析原因:
compile.ts 在 ts-ast-viewer 中的结构如下:
file

而源代码中查找 MyEnum 的调用方法是获取 getEnum("MyEnum"),通过 ts-morph 源码实现可以看到, getEnum 方法通过判断是否为 EnumDeclaration 节点进行过滤。
file
据此可以得出下面语句为 importDeclaration 类型,所以是获取不到的。

import { a as MyEnum } from "../src/"; 

同时,针对是否会先将 src/index.ts 中 a 的代码导入,再进行查找?
这就涉及到代码执行的全流程:

  1. 静态解析阶段;
  2. 编译阶段;

ts-ast-viewer 获取的 ast 实际上是静态解析阶段, 是不涉及代码的运行, 其实是通过 import a from b 创建了 模块之间的联系, 从而构建 AST, 所以更本不会在静态解析的阶段上获取 index 文件中的 a 变量;

而实际上将 a 中的枚举 真正的导入的流程, 在于

  1. 编译阶段: 识别 import , 创建模块依赖图;
  2. 加载阶段: 加载模块内容;
  3. 链接阶段: 加载模块后,编译器会链接模块,这意味着解析模块导出和导入之间的关系,确保每个导入都能正确地关联到其对应的导出;
  4. 执行阶段: 最后执行, 以为折模块世纪需要的时候会被执行;

实践

利器 1: Outline 代码大纲

file

从 vscode 代码大纲的展示入手, 实现步骤如下:

file

// 调用获取 treeData
export function getASTNode(fileName: string, sourceFileText: string): IDataSource {const project = new Project({ useInMemoryFileSystem: true });const sourceFile = project.createSourceFile('./test.tsx', sourceFileText);let tree: IDataSource = {id: -1,type: 'root',name: fileName,children: [],canExpended: true,};sourceFile.forEachChild(node => {getNodeItem(node, tree)})return tree;
}// getNodeItem 针对 AST 操作不同的语法类型,获取想要展示的数据
function getNodeItem(node: Node, tree: IDataSource) {const type = node.getKind();switch (type) {case SyntaxKind.ImportDeclaration:break;case SyntaxKind.FunctionDeclaration:{const name = (node as DeclarationNode).getName();const icon = `symbol-${AST_TYPE_ICON[type]}`;const start = node.getStartLineNumber();const end = node.getEndLineNumber();const statements = (node as FunctionDeclaration).getStatements();if (statements?.length) {const canExpended = !!statements.filter(sts => Object.keys(AST_TYPE_ICON)?.includes(`${sts?.getKind()}`))?.lengthconst node = { id: count++, name, type: icon, start, end, canExpended, children: [] };tree.children && tree.children.push(node);statements?.forEach((item) => getNodeItem(item, node));}break;}... // 其他语法类型的节点进行处理}
}

利器 2: 检查代码

举例: 检查源文件中不能包含函数表达式,目前的应用场景可能比较极端。

const project = new Project();const sourceFiles = project.addSourceFilesAtPaths("./sourceFiles/*.ts");const errList: string[] = [];sourceFiles?.forEach((file) =>file.transform((traversal) => {const node = traversal.visitChildren(); // return type is `ts.Node`if (ts.isVariableDeclaration(node)) {if (node.initializer && ts.isFunctionExpression(node.initializer)) {const filePath = file.getFilePath();console.log(`No function expression allowed.Found function expression: ${node.name.getText()}File: ${filePath}`);errList.push(filePath);}}return node;})
);

file

利器 3: jsDoc 生成

举例: 通过接口定义生成 props 传参的注释文档。

可以尝试一下api 进行组合使用/** 举个例子* Gets the name.* @param person - Person to get the name from.*/
function getName(person: Person) {// ...
}// 获取所有
functionDeclaration.getJsDocs(); // returns: JSDoc[]// 创建 注释
classDeclaration.addJsDoc({description: "Some description...",tags: [{tagName: "param",text: "value - My value.",}],
});// 获取描述
const jsDoc = functionDeclaration.getJsDocs()[0];
jsDoc.getDescription(); // returns string: "Gets the name."// 获取 tags
const tags = jsDoc.getTags();
tags[0].getText(); // "@param person - Person to get the name from."// 获取 jsDoc 内容
sDoc.getInnerText(); // "Gets the name.\n@param person - Person to get the name from."

参考

  1. ts-morph 官网
  2. TypeScript AST Viewer
  3. typeScript 官网
  4. typescript 编译 API
  5. TypeScript / How the compiler compiles

最后

欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

  • 大数据分布式任务调度系统——Taier
  • 轻量级的 Web IDE UI 框架——Molecule
  • 针对大数据领域的 SQL Parser 项目——dt-sql-parser
  • 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
  • 一个速度更快、配置更灵活、使用更简单的模块打包器——ko
  • 一个针对 antd 的组件测试工具库——ant-design-testing

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/843506.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RK3562J正式支持NPU,性价比再提升!

RK3562J是瑞芯微最新推出的一款超高性价比工业处理器,四核Cortex-A53@1.8GHz + Cortex-M0@200MHz异构多核架构,并支持十路UART、两路CAN、两路网口、三种显示、双路Camera等,外设接口资源十分丰富,是RK3568J处理器降成本、降功耗的首选平台,在工商业储能EMS、通讯管理机、…

Movie相关

IDA-VLM: Towards Movie Understanding via ID-Aware Large Vision-Language Model 故事:现在的LVLM只能处理单场景,跨场景中关联实体的能力不行。比如电影中同一个角色在不同场景中出现,现有的LVLM不能把相同角色合并。所以本文提出了一个benchmark衡量跨场景角色对齐能力,…

PHY6236超低成本低功耗蓝牙芯片智能灯控家居

超低成本SOC蓝牙芯片PHY6236PHY6236 是一款用于低功耗蓝牙和专有 2.4G 应用的片上系统 (SoC)。它具有高性能低功耗 32 位 RISC-V MCU,具有 8KB 保留 SRAM、80KB ROM、8KB OTP 和超低功耗、高性能、多模式无线电。此外,PHY6236 还可以支持具有安全性和应用的 BLE。串行外设 IO…

20222411 2024-2025-1 《网络与系统攻防技术》实验五实验报告

1.实验内容 1.1 实践内容 (1)从www.besti.edu.cn、baidu.com、sina.com.cn中选择一个DNS域名进行查询,获取如下信息:DNS注册人及联系方式、该域名对应IP地址、IP地址注册人及联系方式、IP地址所在国家、城市和具体地理位置 PS:使用whois、dig、nslookup、traceroute、以及…

如何通过精准管理,推动产品按时交付

作为产品经理,项目管理是一项必须具备的核心能力。无论是从产品规划、开发到最终交付,项目管理贯穿了整个产品生命周期,涉及团队协调、进度控制、资源分配、质量保障等多个方面。有效的项目管理不仅能帮助团队按时交付高质量的产品,还能提升效率、降低成本并确保客户满意。…

js和CSS3媒体查询制作简单的响应式导航菜单

这是一款使用纯js和css3媒体查询制作的简单的响应式导航菜单效果。该导航菜单类似bootstrap导航菜单,它通过media query制作760像素断点,当视口小于760像素时,菜单会收缩为隐藏的汉堡包菜单。在线演示 下载使用方法HTML结构 该导航菜单使用<nav>元素最为包裹容器,di…

hhdb数据库介绍(10-4)

实例管理 该功能用来查看和管理所有计算节点集群中存储节点所在实例的主从关系。实例信息可以通过主机名、端口号、和存储节点版本号进行筛选。 实例管理信息 功能入口: 登录管理用户界面->实例管理实例管理信息以一个存储节点为单位显示一条记录,可对具体的存储节点进行“…

PE文件结构解析 Part3 NT Headers

文章来源:https://0xrick.github.io/win-internals/pe4/ 目录简介NT Headers(IMAGE_NT_HEADERS)签名 SignatureFile Header(IMAGE_FILE_HEADER)Optional Header (IMAGE_OPTIONAL_HEADER)总结 简介 在前面的文章中,我们看过了DOS Header的结构以及逆向了DOS stub。 这篇文章我…

人员背夹佩戴识别智慧矿山一体机斜井人员进出识别:安防设备时间显示错乱原因及具体解决办法

在现代矿山行业中,安全和效率是两个核心议题。随着智能化技术的不断进步,智慧矿山的概念逐渐成为现实,特别是在非煤矿山的管理与运营中,智能化手段的应用已经成为提升安全性、效率和经济性的关键。本文将深入探讨人员背夹佩戴识别智慧矿山一体机在配电室无人巡检识别算法中…

windows下python批量更新软件包

前言 相信很多小伙伴都遇到过python有些软件包版本过低导致无法安装一些模块的问题,刚好我前两天也遇到了,这里写个文章记录一下 一、更新pip版本 打开命令控制面板,输入: python -m pip install --upgrade pip二、查看过期的软件包 pip list可以看到有很多已经过期的包也可…

配电室长期无人巡检识别智慧矿山一体机非煤矿山算法:配电室监控与安全管理

随着科技的进步和智能化技术的迅速发展,矿山行业正在经历一场深刻的变革。智慧矿山的概念应运而生,尤其是非煤矿山的管理与运营中,利用智能化手段提升安全性、效率和经济性已成为热门趋势。长期无人巡检作为智能矿山运营的重要组成部分,其应用不仅降低人力成本,还能实时监…

Web小案例

web小案例web小案例 HTML + CSS3dTab<style> *{margin: 0;padding: 0;box-sizing: border-box;font-family: Oswald, sans-serif; } body {display: flex;justify-content: center;align-items: center;height: 100vh;background: #434750; } ul {position: relative;/*整…