UNPKG

profoundjs

Version:

Profound.js Framework and Server

414 lines (388 loc) 13.5 kB
"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"; }