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。

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

相关推荐
~甲壳虫3 小时前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
~甲壳虫3 小时前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫3 小时前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
熊的猫4 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
前端青山14 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
GDAL16 小时前
npm入门教程1:npm简介
前端·npm·node.js
王解1 天前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
郑小憨1 天前
Node.js简介以及安装部署 (基础介绍 一)
java·javascript·node.js
鸿蒙开天组●1 天前
鸿蒙进阶篇-网格布局 Grid/GridItem(二)
前端·华为·typescript·harmonyos·grid·mate70
zhizhiqiuya1 天前
第二章 TypeScript 函数详解
前端·javascript·typescript