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;
}
📖