JavaScript 代码读取、修改和重写(基于 AST)

12.31'20

介绍

对源代码处理有两种思路。一是直接作为文本处理,通过fs模块读取文件后做字符串转换,再重新写入文件。

另一种则是读取文件后,对字符串提取 AST(Abstract Syntax Tree)-抽象语法树。AST 包含了代码的元信息。对 AST 进行修改,再重新生成源码。这比直接处理字符要准确,更适合复杂的情况。

AST 处理方案

esprima + ast-types + escodegen

esprima: 根据代码提取 AST。

escodegen: 根据 AST 生成代码。

ast-types: 语法树的节点都是属于特定类型的对象。借助 ast-types 可以创建 AST 类型节点,进而用于修改 AST。

组合使用,即可从编程层面实现代码的转换。

esprima.parseModule适用于import/export命令的代码, esprima.parseScript适用于普通代码。

const esprima = require('esprima');
const escodegen = require('escodegen');
const b = require('ast-types').builders;

const program = `export default [
  require('@/pages/page1/model').default,
]`;
const tree = esprima.parseModule(program);
const elements = tree.body[0].declaration.elements

const newNode = b.memberExpression(
  b.callExpression(
    b.identifier('require'), [
      b.literal('@/pages/page2/model'),
    ],
  ),
  b.identifier('default'),
)
elements.push(newNode);

const newCode = escodegen.generate(tree);
console.log(newCode);

// export default [
//     require('@/pages/page1/model').default,
//     require('@/pages/page2/model').default
// ];

acorn + ast-types + escodegen

acorn: 根据代码提取 AST。使用的类型与 esprima 一致,支持更多的配置和插件。相比 esprima 能处理更多的语法。

以下是一个 yoeman generator 帮助函数的例子,根据传入参数对特定文件做修改。

const updatePageRoute = (file, config) => {
  const target = file;
  const { title, page } = config;
  fs.readFile(target, 'utf8', (err, content) => {
    if (!err) {
      const tree = acorn.Parser.extend(dynamicImport).parse(content, {
        sourceType: 'module',
        allowImportExportEverywhere: true,
      });
      const elements = tree.body[0].declaration.elements

      const newNode = b.objectExpression(
        [
          b.property(
            'init',
            b.identifier('title'),
            b.identifier(`'${title}'`),
          ),
          b.property(
            'init',
            b.identifier('path'),
            b.identifier(`'/${page}'`),
          ),
          b.property(
            'init',
            b.identifier('component'),
            b.identifier(`() => import('@/pages/${page}')`)),
        ]
      )
      elements.push(newNode);

      const newCode = escodegen.generate(tree);
      fs.writeFile(target, newCode, (err) => {
        this.log(err);
      });
    } else {
      this.log(err);
    }
  })
}

支持 dynamic import

读取时,acorn 需要配置插件

acorn.Parser.extend(dynamicImport).parse(...)

目前(2018.12)escodegen尚未支持 dynamic import 对应的节点,需要稍微修改源码

escodegen/escodegen.js(line: 2438)

    ModuleSpecifier: function (expr, precedence, flags) {
        return this.Literal(expr, precedence, flags);
    },

+   Import: function (expr, precedence, flag) {
+       expr.name = 'import';
+
+       return generateIdentifier(expr);
+   },

};

recast

recast: 对 AST 进行转换。可以看成上述几个库的集成。

recast.parse用于提取 AST

var recast = require("recast");

// Let's turn this function declaration into a variable declaration.
var code = [
    "function add(a, b) {",
    "  return a +",
    "    // Weird formatting, huh?",
    "    b;",
    "}"
].join("\n");

// Parse the code using an interface similar to require("esprima").parse.
var ast = recast.parse(code);

借助recast.types.builders处理语法树中的节点

// Grab a reference to the function declaration we just parsed.
var add = ast.program.body[0];

// Make sure it's a FunctionDeclaration (optional).
var n = recast.types.namedTypes;
n.FunctionDeclaration.assert(add);

// If you choose to use recast.builders to construct new AST nodes, all builder
// arguments will be dynamically type-checked against the Mozilla Parser API.
var b = recast.types.builders;

// This kind of manipulation should seem familiar if you've used Esprima or the
// Mozilla Parser API before.
ast.program.body[0] = b.variableDeclaration("var", [
    b.variableDeclarator(add.id, b.functionExpression(
        null, // Anonymize the function expression.
        add.params,
        add.body
    ))
]);

// Just for fun, because addition is commutative:
add.params.push(add.params.shift());

recast.print获取结果

var output = recast.print(ast).code;

output内容

var add = function(b, a) {
  return a +
    // Weird formatting, huh?
    b;
}
📖