Typescript 在 AST 解析上的妙用

背景

最近在业务开发过程中,发现有一些框架支持以配置的方式,强制将所有样式文件视为 css module 文件。且不谈它会带来什么影响,假设我们现在需要关闭这个特性,矫正存量文件,并设置一个卡点,禁止这种行为,你会怎么做?

如果是单次矫正,使用编辑器的搜索替换功能就可以完成。即使想设置卡点,也可以编写正则匹配脚本来实现。

话虽如此,如果我们更进一步,需要打印出具体出错的位置,行号列号,像这样

又或者其他高级定制需求,此时再使用正则就有点力不从心了,而且也不利于长期维护。

好的,下面进入正题,带大家重新认识一下 typescript 这个 npm 包。

AST 解析

也许你会觉得但凡涉及到抽象语法树(AST),必定是高深莫测的。需要使用 babel,各种 parser,一堆插件,先这样,再那样...想想就觉得望而却步。殊不知,我们每天都在打交道的 typescript,本身就是一个非常便捷的解析器

我们正好可以借此机会重新认识下 typescript 这个包,使用它进行一些简单的信息收集工作还是非常方便的。

如下所示,我们只使用 typescript 这个包,30 行就实现了一个可以遍历语法树的程序。

ts 复制代码
import ts from 'typescript';

/**
 * 遍历节点树 - 利用自身的 forEach 方法轻松实现递归遍历
 * @param {ts.Node} node
 * @param {(node: ts.Node) => void} cb
 */
export function walkTsNode(node, cb) {
  cb(node);
  node.forEachChild((child) => walkTsNode(child, cb));
}

/**
 * Script AST traverse
 * @param {string} source - script file content
 * @param {(node: ts.Node) => void} - callback
 */
export function walkFile(source, callback) {
  walkTsNode(
    ts.createSourceFile(
      // 随便起个名字就行,需要注意以 tsx 结尾,这样能同时兼容 jsx 和 js 语法
      '.virtual-filename.tsx',
      // 文件内容,例如使用 fs.readFileSync 读取的文本内容
      source,
      // 语法标准,使用最新即可
      ts.ScriptTarget.Latest,
    ),
    callback,
  );
}

虽然使用正则解析也能做一个模糊的检查,但不可否认,使用语法树精准定位的过程并没有想象中那么复杂。我们只需要做两件事,遍历和判断

上面的示例代码告诉了大家如何遍历,下面👇和大家介绍一个工具 astexplorer.net,可以辅助我们编写逻辑,在遍历的同时提取节点,收集信息。

我们需要点击右上角,选择 typescript 作为解析器,然后在左侧输入代码,即可在右侧查看解析好的语法树。

通过上面的示例,我们得知,需要的节点类型为 ImportDeclaration,于是有以下伪代码

ts 复制代码
// 处理 ImportDeclaration 节点
function handleImportDeclaration(node) {
  if (ts.isImportDeclaration(node)) {
    // logic
  }
}

在我们的场景中:

  1. 判断 import 声明,找出文件中的样式引用路径(css,scss)

    1. 若使用了样式文件的 default 导出,需确保文件名称需以 .module.css 结尾

      1. 不符合要求则校验失败,终止 CI

在上面☝️的逻辑中,我们同时记录下问题出现的文件名称和行列位置,就可以在检查结束之后进行自动修复。

自动修复

信息提取容易,要如何修改呢?在上面的逻辑中,我们提前记录下了问题节点在文件中的起始和结束位置

ts 复制代码
// test.ts
import styles from './style.css';
                   ^           ^
                 start        end

那么我们只需要将区间内的文本替换成新的文件路径即可,再将对应的文件重新命名,矫正过程便完成了。

ts 复制代码
newVal = oldVal.slice(0, start) + replacement + oldVal.slice(end);

很快你察觉异样,如果一个文件中出现多处问题,前一处修改势必造成后方索引位置的更新,还要计算偏移 😮💨。

但是不要怕,其实我们可以换个思路,对区间进行排序,从后往前进行替换,就可以避免这种情况。

ts 复制代码
function replaceSubstrings(str: string, ranges: ReplaceRange[]) {
  // 按照开始索引进行降序排序,这样靠前的修改不会影响到后面
  ranges.sort((a, b) => b.start - a.start);

  for (const range of ranges) {
    const start = range.start;
    const end = range.end;
    const replacement = range.replacement;

    // 替换指定区间的内容
    str = str.substring(0, start) + replacement + str.substring(end);
  }

  return str;
}

其实这些琐碎的问题,早有社区方案可用,顺便和大家推荐一个工具(magic-string),生态的力量开始展现 🎉!

ts 复制代码
import MagicString from 'magic-string';

const ms = new MagicString(fileContent);

// 进行若干处修改
ms.update(11, 16, './xxx.module.css');
ms.update(25, 30, '../styles/xxx.module.scss');
// ...
const newFileContent = ms.toString();
// 然后写入磁盘

实际上,除了这些基础能力外,它还能生成 sourcemap!也因此被广泛应用在各种构建工具链中,包括我们熟知的 webpack/rollup/vite。

当然,思路永远是最重要的,基于这一套丝滑连招,可以衍生出非常多的花样和玩法,在此抛砖引玉,仅供参考。

相关推荐
木木爱研究7 小时前
elpis 全栈里程碑一总结
node.js
夏暖冬凉7 小时前
npm发布流程(记录遇到的问题)
前端·npm·node.js
张小五3158 小时前
node服务器是什么
node.js
张小五3158 小时前
什么是node.js 小白也能看明白
node.js
Ava的硅谷新视界10 小时前
TypeScript 中用判别联合类型替代 instanceof 检查
前端·javascript·typescript
落魄江湖行11 小时前
基础篇六 Nuxt4 状态管理:useState 的正确用法
前端·vue.js·typescript·nuxt4
军军君0111 小时前
数字孪生监控大屏实战模板:智慧城市大屏
前端·vue.js·typescript·前端框架·echarts·智慧城市·大屏展示
软弹13 小时前
快速了解前端中的跨域问题
前端·javascript·vue.js·react.js·node.js·跨域
MacroZheng15 小时前
全面升级!看看人家的后台管理系统,确实清新优雅!
前端·vue.js·typescript
禅思院15 小时前
一个轻量级 Vue3 轮播组件:支持多视图、滑动距离决定切换数量,核心原理与 Swiper 对比
前端·vue.js·typescript