parser 的功能是把源码转成 AST,支持各种语法的 parse。
babel 的 parser 并不是从零自己实现的,而是基于 acron 做了扩展。在 《js parser 的历史》那一节大部分讲过 js parser 都是 estree 标准的,acorn 也是 estree 标准的实现,支持插件,
babel 就是基于 acorn,然后实现了 jsx、typescript、flow 等语法插件的扩展,并且修改了一些 AST,比如 Literal 扩展为了 StringLitreal、NumericLiteral 等。
所以,我们也不会从零实现 parser,也会采用基于 acron 扩下扩展的方式。
思路分析
acorn 插件的实现方式是继承之前的 Parser 返回新的 Parser,重写一些方法来做 AST 修改和扩充。
比如:
module.exports = function(Parser) {
return class extends Parser {
parseLiteral (...args) {
const node = super.parseLiteral(...args);
switch(typeof node.value) {
case 'number':
node.type = 'NumericLiteral';
break;
case 'string':
node.type = 'StringLiteral';
break;
}
return node;
}
}
}
这是我们之前实现过的,把 Literal 扩展为 StringLiteral、NumericLiteral 等的一个插件。
之前还实现过扩展一个 guang 的关键字的插件。
我们希望提供这种 api:
const ast = parser.parse(sourceCode, {
plugins: ['literal', 'guangKeyword']
});
也就是根据传入的 plugins 来确定使用什么插件,然后返回扩展以后的 parser。实现方式就是保存一个插件的 map,按照传入的插件名使用就行。
代码实现
我们把插件放到不同的模块中,然后通过 map 来维护:
const syntaxPlugins = {
'literal': require('./plugins/literal'),
'guangKeyword': require('./plugins/guangKeyword')
}
之后实现 parse 的时候,先把 options 做合并,之后根据 plugin 来依此启用不同的插件。
const defaultOptions = {
plugins: []
}
function parse(code, options) {
const resolvedOptions = Object.assign({}, defaultOptions, options);
const newParser = resolvedOptions.plugins.reduce((Parser, pluginName) => {
let plugin = syntaxPlugins[pluginName]
return plugin ? Parser.extend(plugin) : Parser;
}, acorn.Parser);
return newParser.parse(code, {
locations: true
});
}
这里要指定 locations 为 true,也就是保留 AST 在源码中的位置信息,这个在生成 sourcemap 的时候会用的。
这样就实现了 parse 和语法插件功能。
总结
parser 负责把源码转成 AST,js parser 大多是符合 estree 的标准的,acorn 也是对它的实现。
acorn 支持插件,可以扩展语法,babel parser 就是 fork 了 acorn 做了扩展,我们也通过类似的方式,实现了两个语法插件,然后通过 options 启用。
当然,我们没有实现类似 jsx、typescript 这种复杂语法插件。我们的目的只是理清 babel 实现思路,而不是做一个完善的 babel。
(代码在这里,建议 git clone 下来通过 node 跑一下)