跳到主要内容

使用 ts-morph 库编辑TS文件

· 阅读需 6 分钟

时间如梭,停更快三个月了,时间过得太快了!

今天分享一个库:ts-morph。使用它可以读取和操作TS文件,并实时保存文件内容。这个库包装了 TypeScript 编译器 API,可以让我们方便的操作TS AST。

起因

接触到这个库的起因是这样的:不久前我下载了stable diffusion webui,玩了一段一时间的AI文生图。然后想把这些图片分享出来,比如创建一个网站。但是图片比较大,原版的png格式文件基本都在5MB以上,转成jpeg格式基本也要1MB以上。

如果网页上图片很多,而且还都是尺寸很大的图片,不仅加载慢,而且占内存。大部分网站都会把原始的图片压缩或者剪切生成空间小、尺寸小的图片,当用户要预览的时候再动态加载原始图片。

只是做一个很简单的分享网站,我采取的方案是使用JamStack相关框架,比如:DocusaurusVitePressAstro等。然后在public或者static文件夹下存放图片资源。

使用 sharp 这样的库读取、压缩图片。然后我需要生成一个配置文件,比如json文件,他内部存放的都是图片的信息,比如:hashwidthheightpathkeywordscategory等信息。

然后就是需要一个脚本,两种思路:

  1. 我需要读取这个json文件,然后看哪些图片还没处理,就去读取图片,生成完整的图片信息;
  2. 我去先读取文件,对比json文件中还没有的图片,然后给这些新的图片增加信息;

最后我采用了第一种办法,第一种办法更可控一些,但是无需要先把要使用的图片的基础信息编辑到配置文件里,比如它的文件路径、它的关键词、它的分类等等,不过这些还好(后续获取可以利用AI工具自动帮忙生成关键词)。

But,我没有使用json文件,现在想想获取使用json文件最省事🤔,我最终选用了TS文件,使用TS文件,我可以在前端直接引入(当然,用json也不是不行~)。

TS文件可不是想json一样,他不算是数据文件。在脚本里,json文件直接引入即可,处理完数据直接从新写入新的数据即可。

TS文件你怎么读取里面的数据变量?并且只是改这个数据变量的值?好像只有AST了(想想工作量就上来了😔),先读取文件,然后语法解析,更改值、生成新的语法树、转换成原始code,最后保存文件...

有没有可以简化的方案?当然有的,比如ts-morph。他使用起来就简单多了。只需要读取文件,然后操作AST、调用save,文件内容直接就变了(😊)。前提是我们需要了解一点AST的知识,比如这个网站:https://ts-ast-viewer.com/

Demo

举个例子,summary.ts我们待处理的文件,他的内容如下:

interface Person {
name: string,
age: number,
}

const members = [
{ name: 'John', age: 18 },
{ name: 'Mary', age: 20 }
];

我们想修改John的年龄改成24,需要怎么做?代码如下:

import { Project, SyntaxKind } from 'ts-morph';

const project = new Project();
const fileSource = project.addSourceFileAtPath('./summary.ts');
const variableDeclaration = fileSource.getVariableDeclarationOrThrow('members');
const children = variableDeclaration.getChildren();
const arrayLiteralExpressionNode = children.filter(child => child.getKind() === SyntaxKind.ArrayLiteralExpression);

for (let node of arrayLiteralExpressionNode) {
const arrayLiteralExpression = node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);

const expressionList = arrayLiteralExpression.getElements();

for (let expression of expressionList) {
const objectLiteralExpression = expression.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
const nameProperty = objectLiteralExpression.getPropertyOrThrow("name");
const ageProperty = objectLiteralExpression.getPropertyOrThrow("age");
const nameLiteral = nameProperty.getFirstChildByKind(SyntaxKind.StringLiteral);
const ageLiteral = ageProperty.getFirstChildByKind(SyntaxKind.NumericLiteral);

if (nameLiteral?.getLiteralValue() === 'John') {
if (ageLiteral) ageLiteral.setLiteralValue(24);
else objectLiteralExpression.addPropertyAssignment({
name: 'age',
initializer: `${24}`,
});

project.saveSync(); // 同步保存修改内容
}
}
}

我们把summary.ts的代码粘贴进去,就可以得到语法树了。

TS AST Viewer

解释一下上面代码的意思,首先使用ts-morph时,第一步要先创建一个项目(new Project),然后让项目里添加文件,就是addSourceFileAtPath,他会返回一个当前文件的SourceFile对象,这个对象提供了很多用于操作AST的API,其中getVariableDeclarationOrThrow就可以获取到文件中的某个变量声明,这个方法后带有OrThrow,表明如果获取不到就会停止执行代码,直接抛错。当然如果不叫OrThrow,则会返回VariableDeclaration | undefined类型,代码会接着往下走。

variableDeclaration 对应到语法树,就是这个位置:

接下来的认为就是沿着树结构往下找,一直找到StringLiteralNumericLiteral两个节点。

代码第五行getChildren,可以获取VariableDeclaration节点的子节点们,即:IdentifierArrayLiteralExpression,显然我们想要ArrayLiteralExpression。因此做一下过滤,把节点类型是ArrayLiteralExpression的给过滤出来。或者我们使用下面的方法也可以得到arrayLiteralExpression,这样就不用再做遍历了。

const arrayLiteralExpression = variableDeclaration.getFirstChildByKindOrThrow(SyntaxKind.ArrayLiteralExpression);

接着就是获取数组的元素列表,使用getElements()就可以拿到了,然后遍历列表,把元素类型定义为ObjectLiteralExpression,即是对象类型。

得到对象类型后,就是获取对象中的属性,使用getPropertyOrThrow拿到nameage属性结点:

然后还是使用getFirstChildByKind获取属性值节点即可,这个方法会返回各种类型的Literal对象。

使用getLiteralValue可以获取值,使用setLiteralValue设置值。当然如果没有Literal这个节点,或者没有Property这个节点,我们也可以使用addPropertyAssignment,添加一个新的属性。需要注意的是initializer参数需要是一个字符串,如果属性值是Number类型,我们需要传"4"即可,而如果是String类型,则需要多包一层字符串:"'Tom'"

了解了ts-morph的原理和使用,接下来就好办了,无非就是增删改查。再比如,我想往数组里添加新的一项,该怎么做?代码如下:

arrayLiteralExpression.addElement("{ name: 'Tom', age: 16 }");

直接调用ArrayLiteralExpression对象的addElement方法即可,而且是传字符串即可,它会自己解析。

项目配置

因为都是使用ts,用node跑程序,我需要下面几个包辅助:

  • ts-node、@types/node、typescript

然后还需要配置一下tsconfig.json

{
"compilerOptions": {
"target": "es2016",

/* Modules */
"module": "ESNext",
"moduleResolution": "node10",
"forceConsistentCasingInFileNames": true,

/* Type Checking */
"strict": true,
"skipLibCheck": true
}
}

然后就是package.json的配置,需要添加"type": "module",因为我们用的是ES Module模块。然后还需要配置脚本:

{
"type": "module",
"scripts": {
"parse": "node --loader ts-node/esm --no-warnings=ExperimentalWarning ./scripts.ts"
}
}