pegjs-otf
Version:
On-The-Fly Compilation for PEG.js
167 lines (147 loc) • 6.79 kB
JavaScript
/*
** pegjs-otf -- On-The-Fly Compilation for PEG.js
** Copyright (c) 2014-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
**
** Permission is hereby granted, free of charge, to any person obtaining
** a copy of this software and associated documentation files (the
** "Software"), to deal in the Software without restriction, including
** without limitation the rights to use, copy, modify, merge, publish,
** distribute, sublicense, and/or sell copies of the Software, and to
** permit persons to whom the Software is furnished to do so, subject to
** the following conditions:
**
** The above copyright notice and this permission notice shall be included
** in all copies or substantial portions of the Software.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* global module: false */
/* global require: false */
/* global Buffer: false */
/* built-in requirements */
var fs = require("fs");
var path = require("path");
/* external requirements */
var peggy = require("peggy");
var acorn = require("acorn");
var MagicString = require("magic-string");
var staticEval = require("static-eval");
var through = require("through");
/* compile a single "generateFromFile(...)" call into parser source */
var compile = function (filename, options) {
/* read the grammar definition */
var source = fs.readFileSync(filename, { encoding: "utf8" });
/* enfore source code output */
options = options || {};
options.output = "source";
/* generate the parser with regular PEG.js API */
var parser = peggy.generate(source, options);
/* return the parser source, wrapped for use as an expression */
return "(" + parser + ")";
};
/* recursively walk an AST, invoking a callback for every node */
var walk = function (node, callback) {
if (node === null || typeof node !== "object")
return;
if (typeof node.type === "string")
callback(node);
for (var key in node) {
if (key === "type" || !Object.prototype.hasOwnProperty.call(node, key))
continue;
var child = node[key];
if (Array.isArray(child)) {
for (var i = 0; i < child.length; i++)
walk(child[i], callback);
}
else if (child !== null && typeof child === "object" && typeof child.type === "string")
walk(child, callback);
}
};
/* Browserify transform */
module.exports = function (file /*, options */) {
/* act on JavaScript files only */
if (path.extname(file) !== ".js")
return through();
/* buffer the entire file content, as we have to parse it as a whole */
var chunks = [];
return through(function (chunk) {
chunks.push(chunk);
}, function () {
var stream = this;
var code = Buffer.concat(chunks.map(function (chunk) {
return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
})).toString("utf8");
try {
/* parse the JavaScript source into an AST */
var ast = acorn.parse(code, {
ecmaVersion: "latest",
allowReturnOutsideFunction: true,
allowHashBang: true,
sourceType: "module"
});
/* determine the set of local bindings which reference "pegjs-otf",
i.e., the "xx" in "var xx = require(\"pegjs-otf\")" */
var bindings = {};
var requires = [];
walk(ast, function (node) {
if ( node.type === "VariableDeclarator"
&& node.id.type === "Identifier"
&& node.init
&& node.init.type === "CallExpression"
&& node.init.callee.type === "Identifier"
&& node.init.callee.name === "require"
&& node.init.arguments.length === 1
&& node.init.arguments[0].type === "Literal"
&& node.init.arguments[0].value === "pegjs-otf") {
bindings[node.id.name] = true;
requires.push(node.init);
}
});
/* bail out early if module "pegjs-otf" is not used at all */
if (Object.keys(bindings).length === 0) {
stream.queue(code);
stream.queue(null);
return;
}
/* provide the variables allowed inside the statically evaluated arguments */
var vars = {
/* allow "__dirname" to be used in "generateFromFile(...)" calls */
__dirname: path.dirname(file)
};
/* surgically replace all "xx.generateFromFile(...)" calls */
var magic = new MagicString(code);
walk(ast, function (node) {
if ( node.type === "CallExpression"
&& node.callee.type === "MemberExpression"
&& node.callee.object.type === "Identifier"
&& bindings[node.callee.object.name] === true
&& node.callee.property.type === "Identifier"
&& node.callee.property.name === "generateFromFile"
&& !node.callee.computed) {
/* statically evaluate the call arguments */
var filename = staticEval(node.arguments[0], vars);
var options = node.arguments.length > 1 ? staticEval(node.arguments[1], vars) : {};
if (filename === undefined)
throw new Error("pegjs-otf: cannot statically evaluate grammar filename argument");
/* replace the "generateFromFile(...)" call with the generated parser */
magic.overwrite(node.start, node.end, compile(filename, options));
}
});
/* neutralize the "require(\"pegjs-otf\")" calls, as the module is
no longer needed at run-time once all calls are inlined */
for (var j = 0; j < requires.length; j++)
magic.overwrite(requires[j].start, requires[j].end, "{}");
stream.queue(magic.toString());
stream.queue(null);
}
catch (err) {
stream.emit("error", err);
}
});
};