我们是袋鼠云数栈 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);

大致的流程:

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;
} // ---函数声明

Statement

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

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

Expression

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

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
);

诊断

// 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 classes
if (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 中的结构如下:

而源代码中查找 MyEnum 的调用方法是获取 getEnum("MyEnum"),通过 ts-morph 源码实现可以看到, getEnum 方法通过判断是否为 EnumDeclaration 节点进行过滤。



据此可以得出下面语句为 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 代码大纲

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

// 调用获取 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()}`))?.length
const 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;
})
);

利器 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

探索 TypeScript 编程的利器:ts-morph 入门与实践的更多相关文章

  1. Spring Reactor 入门与实践

    适合阅读的人群:本文适合对 Spring.Netty 等框架,以及 Java 8 的 Lambda.Stream 等特性有基本认识,希望了解 Spring 5 的反应式编程特性的技术人员阅读. 一.前 ...

  2. Python编程:从入门到实践(选记)

    本文参考< Python 编程:从入门到实践>一书,作者: [ 美 ] Eric Matthes 第1章 起步 1.1     搭建python环境 在不同的操作系统中, Python 存 ...

  3. 三、Android NDK编程预备之Java jni入门创建C/C++共享库

    转自: http://www.eoeandroid.com/thread-264971-1-1.html 应网友回复,答应在两天前要出一篇创建C/C++共享库的,但由于清明节假期,跟朋友出去游玩,丢手 ...

  4. 二、Android NDK编程预备之Java jni入门Hello World

    转自:  http://www.eoeandroid.com/forum.php?mod=viewthread&tid=264543&fromuid=588695 昨天已经简要介绍了J ...

  5. Python编程从入门到实践笔记——异常和存储数据

    Python编程从入门到实践笔记——异常和存储数据 #coding=gbk #Python编程从入门到实践笔记——异常和存储数据 #10.3异常 #Python使用被称为异常的特殊对象来管理程序执行期 ...

  6. Python编程从入门到实践笔记——文件

    Python编程从入门到实践笔记——文件 #coding=gbk #Python编程从入门到实践笔记——文件 #10.1从文件中读取数据 #1.读取整个文件 file_name = 'pi_digit ...

  7. Python编程从入门到实践笔记——类

    Python编程从入门到实践笔记——类 #coding=gbk #Python编程从入门到实践笔记——类 #9.1创建和使用类 #1.创建Dog类 class Dog():#类名首字母大写 " ...

  8. Python编程从入门到实践笔记——函数

    Python编程从入门到实践笔记——函数 #coding=gbk #Python编程从入门到实践笔记——函数 #8.1定义函数 def 函数名(形参): # [缩进]注释+函数体 #1.向函数传递信息 ...

  9. Python编程从入门到实践笔记——用户输入和while循环

    Python编程从入门到实践笔记——用户输入和while循环 #coding=utf-8 #函数input()让程序暂停运行,等待用户输入一些文本.得到用户的输入以后将其存储在一个变量中,方便后续使用 ...

  10. Python编程从入门到实践笔记——字典

    Python编程从入门到实践笔记——字典 #coding=utf-8 #字典--放在{}中的键值对:跟json很像 #键和值之间用:分隔:键值对之间用,分隔 alien_0 = {'color':'g ...

随机推荐

  1. Pytorch数据加载与使用

    前言 在训练的时候通常使用Dataset来处理数据集. Dataset的作用 提供一个方式获取数据内容和标签(label). 实战 from torch.utils.data import Datas ...

  2. GitHub Star 数量前 13 的自托管项目清单

    一个多月前,我们撰写并发布了这篇文章<终极自托管解决方案指南>.在那篇文章里我们深入探讨了云端服务与自托管方案的对比.自托管的潜在挑战.如何选择适合自托管解决方案,并深入介绍了五款涵盖不同 ...

  3. 论文阅读翻译之Deep reinforcement learning from human preferences

    论文阅读翻译之Deep reinforcement learning from human preferences 关于 首次发表日期:2024-09-11 论文原文链接:https://arxiv. ...

  4. ASP.NET Core – Work with X509

    前言 这篇主要是说如何用 ASP.NET Core 读写系统里的证书 Store 和创建一个证书, 还有使用证书做加密, 解密, 签名. 主要参考: C#数字证书编程总结 (读写证书 Store) E ...

  5. Advanced .Net Debugging 11:完结篇

    一.介绍 这是我的<Advanced .Net Debugging>这个系列的第十一篇文章,也是这个系列的最后一篇了.我已经把原书的前八章内容全部写完了,本来打算继续写第九章和第十章的内容 ...

  6. ajax发送 put和delete请求

    AJAX发送 PUT和DELETE请求(转载)    转自:https://blog.csdn.net/liuyuanjiang109/article/details/78972644 ajax使用r ...

  7. 好文分享 | 记一次Oracle12c数据库SQL短暂缓慢问题分析

    本文为墨天轮社区作者 张sir 原创作品,记录了日常运维Oracle数据库过程中遇到的一个慢SQL问题的解决.优化过程,文章内容全面具体.分析到位,且含有经验总结,分享给各位. 问题现象 这次出问题的 ...

  8. 7-11 leetcode 2619

    请你编写一段代码实现一个数组方法,使任何数组都可以调用 array.last() 方法,这个方法将返回数组最后一个元素.如果数组中没有元素,则返回 -1 . ps:this 环境变量的使用 ,this ...

  9. 神经网络之卷积篇:详解经典网络(Classic networks)

    详解经典网络 首先看看LeNet-5的网络结构,假设有一张32×32×1的图片,LeNet-5可以识别图中的手写数字,比如像这样手写数字7.LeNet-5是针对灰度图片训练的,所以图片的大小只有32× ...

  10. CSP-J2/S2 2023 游记

    可能早就应该发出来的游记. 2023-10-07 16:32. 前一天睡得比较晚,所以迟到了一点点. 上来先敲了个对拍,拍了一个 if a % 1000 = 0 then a++ 的 A + B,拍出 ...