186 lines
7.0 KiB
Plaintext
186 lines
7.0 KiB
Plaintext
"use strict";
|
|
const util_1 = require("util");
|
|
const escodegen_1 = require("escodegen");
|
|
const esprima_1 = require("esprima");
|
|
const ast_types_1 = require("ast-types");
|
|
const vm2_1 = require("vm2");
|
|
/**
|
|
* Compiles sync JavaScript code into JavaScript with async Functions.
|
|
*
|
|
* @param {String} code JavaScript string to convert
|
|
* @param {Array} names Array of function names to add `await` operators to
|
|
* @return {String} Converted JavaScript string with async/await injected
|
|
* @api public
|
|
*/
|
|
function degenerator(code, _names) {
|
|
if (!Array.isArray(_names)) {
|
|
throw new TypeError('an array of async function "names" is required');
|
|
}
|
|
// Duplicate the `names` array since it's rude to augment the user args
|
|
const names = _names.slice(0);
|
|
const ast = esprima_1.parseScript(code);
|
|
// First pass is to find the `function` nodes and turn them into async or
|
|
// generator functions only if their body includes `CallExpressions` to
|
|
// function in `names`. We also add the names of the functions to the `names`
|
|
// array. We'll iterate several time, as every iteration might add new items
|
|
// to the `names` array, until no new names were added in the iteration.
|
|
let lastNamesLength = 0;
|
|
do {
|
|
lastNamesLength = names.length;
|
|
ast_types_1.visit(ast, {
|
|
visitVariableDeclaration(path) {
|
|
if (path.node.declarations) {
|
|
for (let i = 0; i < path.node.declarations.length; i++) {
|
|
const declaration = path.node.declarations[i];
|
|
if (ast_types_1.namedTypes.VariableDeclarator.check(declaration) &&
|
|
ast_types_1.namedTypes.Identifier.check(declaration.init) &&
|
|
ast_types_1.namedTypes.Identifier.check(declaration.id) &&
|
|
checkName(declaration.init.name, names) &&
|
|
!checkName(declaration.id.name, names)) {
|
|
names.push(declaration.id.name);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
visitAssignmentExpression(path) {
|
|
if (ast_types_1.namedTypes.Identifier.check(path.node.left) &&
|
|
ast_types_1.namedTypes.Identifier.check(path.node.right) &&
|
|
checkName(path.node.right.name, names) &&
|
|
!checkName(path.node.left.name, names)) {
|
|
names.push(path.node.left.name);
|
|
}
|
|
return false;
|
|
},
|
|
visitFunction(path) {
|
|
if (path.node.id) {
|
|
let shouldDegenerate = false;
|
|
ast_types_1.visit(path.node, {
|
|
visitCallExpression(path) {
|
|
if (checkNames(path.node, names)) {
|
|
shouldDegenerate = true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
if (!shouldDegenerate) {
|
|
return false;
|
|
}
|
|
// Got a "function" expression/statement,
|
|
// convert it into an async function
|
|
path.node.async = true;
|
|
// Add function name to `names` array
|
|
if (!checkName(path.node.id.name, names)) {
|
|
names.push(path.node.id.name);
|
|
}
|
|
}
|
|
this.traverse(path);
|
|
},
|
|
});
|
|
} while (lastNamesLength !== names.length);
|
|
// Second pass is for adding `await`/`yield` statements to any function
|
|
// invocations that match the given `names` array.
|
|
ast_types_1.visit(ast, {
|
|
visitCallExpression(path) {
|
|
if (checkNames(path.node, names)) {
|
|
// A "function invocation" expression,
|
|
// we need to inject a `AwaitExpression`/`YieldExpression`
|
|
const delegate = false;
|
|
const { name, parent: { node: pNode }, } = path;
|
|
const expr = ast_types_1.builders.awaitExpression(path.node, delegate);
|
|
if (ast_types_1.namedTypes.CallExpression.check(pNode)) {
|
|
pNode.arguments[name] = expr;
|
|
}
|
|
else {
|
|
pNode[name] = expr;
|
|
}
|
|
}
|
|
this.traverse(path);
|
|
},
|
|
});
|
|
return escodegen_1.generate(ast);
|
|
}
|
|
(function (degenerator) {
|
|
function compile(code, returnName, names, options = {}) {
|
|
const compiled = degenerator(code, names);
|
|
const vm = new vm2_1.VM(options);
|
|
const script = new vm2_1.VMScript(`${compiled};${returnName}`, {
|
|
filename: options.filename,
|
|
});
|
|
const fn = vm.run(script);
|
|
if (typeof fn !== 'function') {
|
|
throw new Error(`Expected a "function" to be returned for \`${returnName}\`, but got "${typeof fn}"`);
|
|
}
|
|
const r = function (...args) {
|
|
try {
|
|
const p = fn.apply(this, args);
|
|
if (typeof (p === null || p === void 0 ? void 0 : p.then) === 'function') {
|
|
return p;
|
|
}
|
|
return Promise.resolve(p);
|
|
}
|
|
catch (err) {
|
|
return Promise.reject(err);
|
|
}
|
|
};
|
|
Object.defineProperty(r, 'toString', {
|
|
value: fn.toString.bind(fn),
|
|
enumerable: false,
|
|
});
|
|
return r;
|
|
}
|
|
degenerator.compile = compile;
|
|
})(degenerator || (degenerator = {}));
|
|
/**
|
|
* Returns `true` if `node` has a matching name to one of the entries in the
|
|
* `names` array.
|
|
*
|
|
* @param {types.Node} node
|
|
* @param {Array} names Array of function names to return true for
|
|
* @return {Boolean}
|
|
* @api private
|
|
*/
|
|
function checkNames({ callee }, names) {
|
|
let name;
|
|
if (ast_types_1.namedTypes.Identifier.check(callee)) {
|
|
name = callee.name;
|
|
}
|
|
else if (ast_types_1.namedTypes.MemberExpression.check(callee)) {
|
|
if (ast_types_1.namedTypes.Identifier.check(callee.object) &&
|
|
ast_types_1.namedTypes.Identifier.check(callee.property)) {
|
|
name = `${callee.object.name}.${callee.property.name}`;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
else if (ast_types_1.namedTypes.FunctionExpression.check(callee)) {
|
|
if (callee.id) {
|
|
name = callee.id.name;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(`Don't know how to get name for: ${callee.type}`);
|
|
}
|
|
return checkName(name, names);
|
|
}
|
|
function checkName(name, names) {
|
|
// now that we have the `name`, check if any entries match in the `names` array
|
|
for (let i = 0; i < names.length; i++) {
|
|
const n = names[i];
|
|
if (util_1.isRegExp(n)) {
|
|
if (n.test(name)) {
|
|
return true;
|
|
}
|
|
}
|
|
else if (name === n) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
module.exports = degenerator;
|
|
//# sourceMappingURL=index.js.map |