profoundjs
Version:
Profound.js Framework and Server
414 lines (388 loc) • 13.5 kB
JavaScript
"use strict";
const acorn = require("acorn");
const estraverse = require("estraverse");
const fs = require("fs");
const moment = require("moment");
require("profoundjs");
module.exports = function(file) {
let content = fs.readFileSync(file, "utf8");
const ast = acorn.parse(content, {
ecmaVersion: "latest",
sourceType: "script",
allowReturnOutsideFunction: true,
allowHashBang: true
});
let callAPIs = [];
const scopeStack = [];
const fnStack = [];
const classStack = [];
const scopeMap = new Map();
const fnMap = new Map();
const callMap = new Map();
const needsAwait = new Map();
const needsAsync = new Map();
const insertions = [];
let pjsPrefix;
// First pass:
// Sets up variable scopes/names.
// * This requires a first pass because variables in an outer scope can be declared after contained scopes.
// * The JavaScript runtime 'hoists' these definitions to the top of their scope, so they are visible to any contained scopes.
// * To deal with this, we'll copy any names down to contained scopes when exiting each scope.
// Sets up function map.
// Gets Profound.js prefix from require() call.
estraverse.traverse(ast, {
enter: (node, parent) => {
if (node.type === "Program" || isFunction(node) || (node.type === "BlockStatement" && !isFunction(parent))) {
// Set up new scope.
const scope = {
node,
objects: new Map(),
scopes: new Map()
};
// Keep track of contained scopes.
const containingScope = scopeMap.get(scopeStack[scopeStack.length - 1]);
if (containingScope) {
containingScope.scopes.set(node, scope);
}
// Add function parameter names to scope.
if (isFunction(node)) {
node.params.forEach(param => {
if (param.type === "AssignmentPattern") {
param = param.left;
}
if (param.type === "Identifier") {
scope.objects.set(param.name, { scope });
}
});
}
scopeMap.set(node, scope);
scopeStack.push(node);
// Setup function map.
if (node.type !== "BlockStatement") {
let useStrict;
if (node.type === "Program") {
useStrict = node.body.length >= 1 && node.body[0].directive === "use strict";
}
else {
const containingFn = fnStack[fnStack.length - 1];
const containingFnInfo = fnMap.get(containingFn);
if (classStack.length > 0) {
useStrict = true;
}
else {
useStrict = containingFnInfo.useStrict;
if (node.body.type === "BlockStatement" && node.body.body.length > 0 && node.body.body[0].directive === "use strict") {
useStrict = true;
}
}
}
fnMap.set(node, {
useStrict,
parent
});
fnStack.push(node);
}
}
else if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
classStack.push(node);
}
else if (node.type === "VariableDeclaration") {
// Add variable names to scope.
const fn = fnStack[fnStack.length - 1];
node.declarations.forEach(variableDeclarator => {
if (variableDeclarator.id.type === "Identifier") {
const scope = node.kind === "var" ? scopeMap.get(fn) : scopeMap.get(scopeStack[scopeStack.length - 1]);
scope.objects.set(variableDeclarator.id.name, { scope });
}
});
}
else if (node.type === "CallExpression") {
let calleeName = getQualifiedName(node.callee);
if (calleeName) {
calleeName = getCallName(calleeName);
if (calleeName === "require") {
const arg = node.arguments[0];
if (arg && arg.type === "Literal" && arg.value === "profoundjs") {
let name;
if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") {
name = parent.id.name;
}
else if (parent.type === "AssignmentExpression" && parent.right === node) {
name = getQualifiedName(parent.left);
}
if (name) {
pjsPrefix = name;
}
}
}
}
}
},
leave: (node, parent) => {
if (node.type === "Program" || isFunction(node) || (node.type === "BlockStatement" && !isFunction(parent))) {
const scope = scopeMap.get(scopeStack.pop());
copyScopeObjects(scope); // Copy any names down to contained scopes.
if (node.type !== "BlockStatement") {
fnStack.pop();
}
}
else if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
classStack.pop();
}
}
});
if (!pjsPrefix) {
throw new Error("Unable to locate require(\"profoundjs\")");
}
callAPIs = [pjsPrefix + ".call", pjsPrefix + ".fiber.call"];
estraverse.traverse(ast, {
enter: (node, parent) => {
if (node.type === "Program" || isFunction(node) || (node.type === "BlockStatement" && !isFunction(parent))) {
scopeStack.push(node);
if (node.type !== "BlockStatement") {
fnStack.push(node);
}
}
const fn = fnStack[fnStack.length - 1];
if (isFunction(node)) {
// Update function map entries with function name and name scope.
const fnInfo = fnMap.get(node);
const containingScope = scopeMap.get(scopeStack[scopeStack.length - 2]);
const containingFn = fnStack[fnStack.length - 2];
if (node.type === "FunctionDeclaration") {
if (node.id.name) {
fnInfo.name = node.id.name;
}
const containingFnInfo = fnMap.get(containingFn);
fnInfo.scope = (containingFnInfo.useStrict && containingScope.node.type === "BlockStatement") ? containingScope.node : containingFn;
}
else {
let name;
if (parent.type === "VariableDeclarator") {
name = getQualifiedName(parent.id);
}
else if (parent.type === "AssignmentExpression" && parent.right === node) {
name = getQualifiedName(parent.left);
}
if (name) {
fnInfo.name = name;
const baseName = name.split(".")[0];
const entry = containingScope.objects.get(baseName);
fnInfo.scope = entry ? entry.scope.node : ast;
}
else {
fnInfo.scope = containingFn;
}
}
}
else if (node.type === "CallExpression") {
callMap.set(node, {
containingFn: fn,
parent
});
const callee = node.callee;
const calleeName = getQualifiedName(callee);
if (calleeName && calleeName.startsWith(pjsPrefix + ".")) {
const internalName = "profound" + getCallName(calleeName.substring(pjsPrefix.length));
let fn;
try {
// eslint-disable-next-line no-eval
eval("fn = " + internalName);
}
catch (error) {}
if (fn && typeof (fn) === "function" && fn.constructor.name === "AsyncFunction") {
needsAwait.set(node, true);
}
}
}
},
leave: (node, parent) => {
if (node.type === "Program" || isFunction(node) || (node.type === "BlockStatement" && !isFunction(parent))) {
scopeStack.pop();
if (node.type !== "BlockStatement") {
fnStack.pop();
}
}
}
});
findAsyncCalls(needsAwait, needsAsync, callMap, fnMap);
let changed = false;
needsAwait.forEach((value, callExpression) => {
const callInfo = callMap.get(callExpression);
if (callInfo.parent.type !== "AwaitExpression") {
insertions.push(
{
pos: callExpression.start,
code: "(await"
},
{
pos: callExpression.end,
code: ")"
}
);
}
});
let topLevelAwait = false;
needsAsync.forEach((value, fn) => {
if (fn.type === "Program") {
topLevelAwait = true;
}
else if (!fn.async) {
const fnInfo = fnMap.get(fn);
const parent = fnInfo.parent;
if (parent && parent.type === "MethodDefinition" && (parent.kind !== "method" || !parent.key)) {
return;
}
const start = parent.type === "MethodDefinition" ? parent.key.start : fn.start;
insertions.push({
pos: start,
code: "async"
});
}
});
insertions.sort(function(a, b) {
if (a.pos === b.pos) return (a.atEnd || 0) - (b.atEnd || 0);
return a.pos - b.pos;
});
const newCode = [];
if (insertions.length) {
// For performance -- Instead of string concat use an array to hold the parts and pieces -- then join them together
let lastPos = 0;
for (let i = 0; i < insertions.length; i++) {
const insertion = insertions[i];
if (lastPos < insertion.pos) newCode.push(content.substring(lastPos, insertion.pos));
newCode.push(insertion.code);
lastPos = insertion.pos;
}
if (lastPos < content.length) newCode.push(content.substring(lastPos));
content = newCode.join(" ");
changed = true;
}
if (topLevelAwait) {
// Detect EOL by counting CRLFs and LFs.
let EOL = "\n";
const match = content.match(/\r\n|\n/g);
if (match) {
const lfs = match.filter(el => el === "\n").length;
const crlfs = match.length - lfs;
if (lfs > crlfs) {
EOL = "\n";
}
else if (crlfs > lfs) {
EOL = "\r\n";
}
}
const lines = content.split(EOL);
let index = 0;
if (lines[0].startsWith("#!")) {
index = 1;
}
lines.splice(index, 0, "async function startPJS() { ");
for (let i = index + 1; i < lines.length; i++) {
lines[i] = " " + lines[i];
}
lines.push("}", "startPJS();");
content = lines.join(EOL);
changed = true;
}
if (changed) {
const backupFile = file + ".orig." + moment(new Date()).format("YYYYMMDDHHmmss");
fs.copyFileSync(file, backupFile);
fs.writeFileSync(file, content, "utf8");
}
return changed;
function getCallName(name) {
if (callAPIs.includes(name)) {
return name;
}
else {
return name.replace(/\.(call|apply)$/, "");
}
}
// Given a list of CallExpressions needing await (needsAwait):
// Find all functions that need to be made async (needsAsync) and all functions/calls that need async/await as a result.
// Functions that contain await calls need to be made async, and any caller of the containing function needs to await the result.
// So then the caller of the containing function needs to be made aysnc, and so on...
function findAsyncCalls(needsAwait, needsAsync, callMap, fnMap) {
find(new Map(needsAwait));
function find(calls) {
calls.forEach((value, callExpression) => {
const callInfo = callMap.get(callExpression);
const containingFn = callInfo.containingFn;
needsAsync.set(containingFn, true);
if (containingFn.type === "Program") {
return;
}
const containingFnInfo = fnMap.get(containingFn);
if (containingFnInfo.scope && containingFnInfo.name && containingFnInfo.name !== "startPJS") {
const newCalls = new Map();
estraverse.traverse(containingFnInfo.scope, {
enter: (node, parent) => {
if (node.type === "CallExpression") {
let name = getQualifiedName(node.callee);
if (name) {
name = getCallName(name);
if (name === containingFnInfo.name) {
newCalls.set(node, true);
needsAwait.set(node, true);
}
}
}
}
});
find(newCalls);
}
});
}
}
};
// Given an Identifier or MemberExpression, tries to return a fully qualified name.
// Returns undefined if the name can't be determined due to dynamic parts.
function getQualifiedName(node) {
if (node.type !== "Identifier" && node.type !== "MemberExpression") {
return;
}
const parts = process(node);
if (parts.indexOf(undefined) === -1) {
return parts.join(".");
}
function process(node) {
const parts = [];
if (node.type === "Identifier") {
parts.push(node.name);
}
else if (node.type === "Literal" && typeof node.value === "string") {
parts.push(node.value);
}
else if (node.type === "MemberExpression") {
parts.push(...process(node.object));
if (node.computed === true && node.property.type !== "Literal") {
parts.push("__COMPUTED__");
}
else {
parts.push(...process(node.property));
}
}
else {
parts.push(undefined);
}
return parts;
}
}
function copyScopeObjects(scope, objects) {
if (!objects) {
objects = Object.fromEntries(scope.objects);
}
scope.scopes.forEach(scope => {
for (const name in objects) {
const object = objects[name];
if (!scope.objects.has(name)) {
scope.objects.set(name, object);
}
}
copyScopeObjects(scope, objects);
});
}
function isFunction(node) {
return node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
}