1'use strict'; 2 3const { 4 ArrayFrom, 5 ArrayPrototypeForEach, 6 ArrayPrototypeIncludes, 7 ArrayPrototypeJoin, 8 ArrayPrototypePop, 9 ArrayPrototypePush, 10 FunctionPrototype, 11 ObjectKeys, 12 RegExpPrototypeSymbolReplace, 13 StringPrototypeEndsWith, 14 StringPrototypeIncludes, 15 StringPrototypeIndexOf, 16 StringPrototypeRepeat, 17 StringPrototypeSplit, 18 StringPrototypeStartsWith, 19 SyntaxError, 20} = primordials; 21 22const parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; 23const walk = require('internal/deps/acorn/acorn-walk/dist/walk'); 24const { Recoverable } = require('internal/repl'); 25 26function isTopLevelDeclaration(state) { 27 return state.ancestors[state.ancestors.length - 2] === state.body; 28} 29 30const noop = FunctionPrototype; 31const visitorsWithoutAncestors = { 32 ClassDeclaration(node, state, c) { 33 if (isTopLevelDeclaration(state)) { 34 state.prepend(node, `${node.id.name}=`); 35 ArrayPrototypePush( 36 state.hoistedDeclarationStatements, 37 `let ${node.id.name}; ` 38 ); 39 } 40 41 walk.base.ClassDeclaration(node, state, c); 42 }, 43 ForOfStatement(node, state, c) { 44 if (node.await === true) { 45 state.containsAwait = true; 46 } 47 walk.base.ForOfStatement(node, state, c); 48 }, 49 FunctionDeclaration(node, state, c) { 50 state.prepend(node, `this.${node.id.name} = ${node.id.name}; `); 51 ArrayPrototypePush( 52 state.hoistedDeclarationStatements, 53 `var ${node.id.name}; ` 54 ); 55 }, 56 FunctionExpression: noop, 57 ArrowFunctionExpression: noop, 58 MethodDefinition: noop, 59 AwaitExpression(node, state, c) { 60 state.containsAwait = true; 61 walk.base.AwaitExpression(node, state, c); 62 }, 63 ReturnStatement(node, state, c) { 64 state.containsReturn = true; 65 walk.base.ReturnStatement(node, state, c); 66 }, 67 VariableDeclaration(node, state, c) { 68 const variableKind = node.kind; 69 const isIterableForDeclaration = ArrayPrototypeIncludes( 70 ['ForOfStatement', 'ForInStatement'], 71 state.ancestors[state.ancestors.length - 2].type 72 ); 73 74 if (variableKind === 'var' || isTopLevelDeclaration(state)) { 75 state.replace( 76 node.start, 77 node.start + variableKind.length + (isIterableForDeclaration ? 1 : 0), 78 variableKind === 'var' && isIterableForDeclaration ? 79 '' : 80 'void' + (node.declarations.length === 1 ? '' : ' (') 81 ); 82 83 if (!isIterableForDeclaration) { 84 ArrayPrototypeForEach(node.declarations, (decl) => { 85 state.prepend(decl, '('); 86 state.append(decl, decl.init ? ')' : '=undefined)'); 87 }); 88 89 if (node.declarations.length !== 1) { 90 state.append(node.declarations[node.declarations.length - 1], ')'); 91 } 92 } 93 94 const variableIdentifiersToHoist = [ 95 ['var', []], 96 ['let', []], 97 ]; 98 function registerVariableDeclarationIdentifiers(node) { 99 switch (node.type) { 100 case 'Identifier': 101 ArrayPrototypePush( 102 variableIdentifiersToHoist[variableKind === 'var' ? 0 : 1][1], 103 node.name 104 ); 105 break; 106 case 'ObjectPattern': 107 ArrayPrototypeForEach(node.properties, (property) => { 108 registerVariableDeclarationIdentifiers(property.value); 109 }); 110 break; 111 case 'ArrayPattern': 112 ArrayPrototypeForEach(node.elements, (element) => { 113 registerVariableDeclarationIdentifiers(element); 114 }); 115 break; 116 } 117 } 118 119 ArrayPrototypeForEach(node.declarations, (decl) => { 120 registerVariableDeclarationIdentifiers(decl.id); 121 }); 122 123 ArrayPrototypeForEach( 124 variableIdentifiersToHoist, 125 ({ 0: kind, 1: identifiers }) => { 126 if (identifiers.length > 0) { 127 ArrayPrototypePush( 128 state.hoistedDeclarationStatements, 129 `${kind} ${ArrayPrototypeJoin(identifiers, ', ')}; ` 130 ); 131 } 132 } 133 ); 134 } 135 136 walk.base.VariableDeclaration(node, state, c); 137 } 138}; 139 140const visitors = {}; 141for (const nodeType of ObjectKeys(walk.base)) { 142 const callback = visitorsWithoutAncestors[nodeType] || walk.base[nodeType]; 143 visitors[nodeType] = (node, state, c) => { 144 const isNew = node !== state.ancestors[state.ancestors.length - 1]; 145 if (isNew) { 146 ArrayPrototypePush(state.ancestors, node); 147 } 148 callback(node, state, c); 149 if (isNew) { 150 ArrayPrototypePop(state.ancestors); 151 } 152 }; 153} 154 155function processTopLevelAwait(src) { 156 const wrapPrefix = '(async () => { '; 157 const wrapped = `${wrapPrefix}${src} })()`; 158 const wrappedArray = ArrayFrom(wrapped); 159 let root; 160 try { 161 root = parser.parse(wrapped, { ecmaVersion: 'latest' }); 162 } catch (e) { 163 if (StringPrototypeStartsWith(e.message, 'Unterminated ')) 164 throw new Recoverable(e); 165 // If the parse error is before the first "await", then use the execution 166 // error. Otherwise we must emit this parse error, making it look like a 167 // proper syntax error. 168 const awaitPos = StringPrototypeIndexOf(src, 'await'); 169 const errPos = e.pos - wrapPrefix.length; 170 if (awaitPos > errPos) 171 return null; 172 // Convert keyword parse errors on await into their original errors when 173 // possible. 174 if (errPos === awaitPos + 6 && 175 StringPrototypeIncludes(e.message, 'Expecting Unicode escape sequence')) 176 return null; 177 if (errPos === awaitPos + 7 && 178 StringPrototypeIncludes(e.message, 'Unexpected token')) 179 return null; 180 const line = e.loc.line; 181 const column = line === 1 ? e.loc.column - wrapPrefix.length : e.loc.column; 182 let message = '\n' + StringPrototypeSplit(src, '\n')[line - 1] + '\n' + 183 StringPrototypeRepeat(' ', column) + 184 '^\n\n' + RegExpPrototypeSymbolReplace(/ \([^)]+\)/, e.message, ''); 185 // V8 unexpected token errors include the token string. 186 if (StringPrototypeEndsWith(message, 'Unexpected token')) 187 message += " '" + 188 // Wrapper end may cause acorn to report error position after the source 189 (src[e.pos - wrapPrefix.length] ?? src[src.length - 1]) + 190 "'"; 191 // eslint-disable-next-line no-restricted-syntax 192 throw new SyntaxError(message); 193 } 194 const body = root.body[0].expression.callee.body; 195 const state = { 196 body, 197 ancestors: [], 198 hoistedDeclarationStatements: [], 199 replace(from, to, str) { 200 for (let i = from; i < to; i++) { 201 wrappedArray[i] = ''; 202 } 203 if (from === to) str += wrappedArray[from]; 204 wrappedArray[from] = str; 205 }, 206 prepend(node, str) { 207 wrappedArray[node.start] = str + wrappedArray[node.start]; 208 }, 209 append(node, str) { 210 wrappedArray[node.end - 1] += str; 211 }, 212 containsAwait: false, 213 containsReturn: false 214 }; 215 216 walk.recursive(body, state, visitors); 217 218 // Do not transform if 219 // 1. False alarm: there isn't actually an await expression. 220 // 2. There is a top-level return, which is not allowed. 221 if (!state.containsAwait || state.containsReturn) { 222 return null; 223 } 224 225 const last = body.body[body.body.length - 1]; 226 if (last.type === 'ExpressionStatement') { 227 // For an expression statement of the form 228 // ( expr ) ; 229 // ^^^^^^^^^^ // last 230 // ^^^^ // last.expression 231 // 232 // We do not want the left parenthesis before the `return` keyword; 233 // therefore we prepend the `return (` to `last`. 234 // 235 // On the other hand, we do not want the right parenthesis after the 236 // semicolon. Since there can only be more right parentheses between 237 // last.expression.end and the semicolon, appending one more to 238 // last.expression should be fine. 239 state.prepend(last, 'return ('); 240 state.append(last.expression, ')'); 241 } 242 243 return ( 244 ArrayPrototypeJoin(state.hoistedDeclarationStatements, '') + 245 ArrayPrototypeJoin(wrappedArray, '') 246 ); 247} 248 249module.exports = { 250 processTopLevelAwait 251}; 252