mapAwaitExpression.js (6323B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 import generate from "@babel/generator"; 6 import * as t from "@babel/types"; 7 8 import { replaceNode } from "./utils/ast"; 9 import { isTopLevel } from "./utils/helpers"; 10 11 // translates new bindings `var a = 3` into `a = 3`. 12 function translateDeclarationIntoAssignment(node) { 13 return node.declarations.reduce((acc, declaration) => { 14 // Don't translate declaration without initial assignment (e.g. `var a;`) 15 if (!declaration.init) { 16 return acc; 17 } 18 acc.push( 19 t.expressionStatement( 20 t.assignmentExpression("=", declaration.id, declaration.init) 21 ) 22 ); 23 return acc; 24 }, []); 25 } 26 27 /** 28 * Given an AST, modify it to return the last evaluated statement's expression value if possible. 29 * This is to preserve existing console behavior of displaying the last executed expression value. 30 */ 31 function addReturnNode(ast) { 32 const statements = ast.program.body; 33 const lastStatement = statements.pop(); 34 35 // if the last expression is an awaitExpression, strip the `await` part and directly 36 // return the argument to avoid calling the argument's `then` function twice when the 37 // mapped expression gets evaluated (See Bug 1771428) 38 if (t.isAwaitExpression(lastStatement.expression)) { 39 lastStatement.expression = lastStatement.expression.argument; 40 } 41 42 // NOTE: For more complicated cases such as an if/for statement, the last evaluated 43 // expression value probably can not be displayed, unless doing hacky workarounds such 44 // as returning the `eval` of the final statement (won't always work due to CSP issues?) 45 // or SpiderMonkey support (See Bug 1839588) at which point this entire module can be removed. 46 statements.push( 47 t.isExpressionStatement(lastStatement) 48 ? t.returnStatement(lastStatement.expression) 49 : lastStatement 50 ); 51 return statements; 52 } 53 54 function getDeclarations(node) { 55 const { kind, declarations } = node; 56 const declaratorNodes = declarations.reduce((acc, d) => { 57 const declarators = getVariableDeclarators(d.id); 58 return acc.concat(declarators); 59 }, []); 60 61 // We can't declare const variables outside of the async iife because we 62 // wouldn't be able to re-assign them. As a workaround, we transform them 63 // to `let` which should be good enough for those case. 64 return t.variableDeclaration( 65 kind === "const" ? "let" : kind, 66 declaratorNodes 67 ); 68 } 69 70 function getVariableDeclarators(node) { 71 if (t.isIdentifier(node)) { 72 return t.variableDeclarator(t.identifier(node.name)); 73 } 74 75 if (t.isObjectProperty(node)) { 76 return getVariableDeclarators(node.value); 77 } 78 if (t.isRestElement(node)) { 79 return getVariableDeclarators(node.argument); 80 } 81 82 if (t.isAssignmentPattern(node)) { 83 return getVariableDeclarators(node.left); 84 } 85 86 if (t.isArrayPattern(node)) { 87 return node.elements.reduce( 88 (acc, element) => acc.concat(getVariableDeclarators(element)), 89 [] 90 ); 91 } 92 if (t.isObjectPattern(node)) { 93 return node.properties.reduce( 94 (acc, property) => acc.concat(getVariableDeclarators(property)), 95 [] 96 ); 97 } 98 return []; 99 } 100 101 /** 102 * Given an AST and an array of variableDeclaration nodes, return a new AST with 103 * all the declarations at the top of the AST. 104 */ 105 function addTopDeclarationNodes(ast, declarationNodes) { 106 const statements = []; 107 declarationNodes.forEach(declarationNode => { 108 statements.push(getDeclarations(declarationNode)); 109 }); 110 statements.push(ast); 111 return t.program(statements); 112 } 113 114 /** 115 * Given an AST, return an object of the following shape: 116 * - newAst: {AST} the AST where variable declarations were transformed into 117 * variable assignments 118 * - declarations: {Array<Node>} An array of all the declaration nodes needed 119 * outside of the async iife. 120 */ 121 function translateDeclarationsIntoAssignment(ast) { 122 const declarations = []; 123 t.traverse(ast, (node, ancestors) => { 124 const parent = ancestors[ancestors.length - 1]; 125 126 if ( 127 t.isWithStatement(node) || 128 !isTopLevel(ancestors) || 129 t.isAssignmentExpression(node) || 130 !t.isVariableDeclaration(node) || 131 t.isForStatement(parent.node) || 132 t.isForXStatement(parent.node) || 133 !Array.isArray(node.declarations) || 134 node.declarations.length === 0 135 ) { 136 return; 137 } 138 139 const newNodes = translateDeclarationIntoAssignment(node); 140 replaceNode(ancestors, newNodes); 141 declarations.push(node); 142 }); 143 144 return { 145 newAst: ast, 146 declarations, 147 }; 148 } 149 150 /** 151 * Given an AST, wrap its body in an async iife, transform variable declarations 152 * in assignments and move the variable declarations outside of the async iife. 153 * Example: With the AST for the following expression: `let a = await 123`, the 154 * function will return: 155 * let a; 156 * (async => { 157 * return a = await 123; 158 * })(); 159 */ 160 function wrapExpressionFromAst(ast) { 161 // Transform let and var declarations into assignments, and get back an array 162 // of variable declarations. 163 let { newAst, declarations } = translateDeclarationsIntoAssignment(ast); 164 const body = addReturnNode(newAst); 165 166 // Create the async iife. 167 newAst = t.expressionStatement( 168 t.callExpression( 169 t.arrowFunctionExpression([], t.blockStatement(body), true), 170 [] 171 ) 172 ); 173 174 // Now let's put all the variable declarations at the top of the async iife. 175 newAst = addTopDeclarationNodes(newAst, declarations); 176 177 return generate(newAst).code; 178 } 179 180 export default function mapTopLevelAwait(expression, ast) { 181 if (!ast) { 182 // If there's no ast this means the expression is malformed. And if the 183 // expression contains the await keyword, we still want to wrap it in an 184 // async iife in order to get a meaningful message (without this, the 185 // engine will throw an Error stating that await keywords are only valid 186 // in async functions and generators). 187 if (expression.includes("await ")) { 188 return `(async () => { ${expression} })();`; 189 } 190 191 return expression; 192 } 193 194 // Does it have top-level-await? 195 if (!ast.program.extra.topLevelAwait) { 196 return expression; 197 } 198 199 return wrapExpressionFromAst(ast); 200 }