UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

256 lines (237 loc) 8.54 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/repl/await.js import { Parser as parser } from "nstdlib/stub/internal/deps/acorn/acorn/dist/acorn"; import * as walk from "nstdlib/stub/internal/deps/acorn/acorn-walk/dist/walk"; import { Recoverable } from "nstdlib/stub/internal/repl"; function isTopLevelDeclaration(state) { return state.ancestors[state.ancestors.length - 2] === state.body; } const noop = Function.prototype; const visitorsWithoutAncestors = { ClassDeclaration(node, state, c) { if (isTopLevelDeclaration(state)) { state.prepend(node, `${node.id.name}=`); Array.prototype.push.call( state.hoistedDeclarationStatements, `let ${node.id.name}; `, ); } walk.base.ClassDeclaration(node, state, c); }, ForOfStatement(node, state, c) { if (node.await === true) { state.containsAwait = true; } walk.base.ForOfStatement(node, state, c); }, FunctionDeclaration(node, state, c) { state.prepend(node, `this.${node.id.name} = ${node.id.name}; `); Array.prototype.push.call( state.hoistedDeclarationStatements, `var ${node.id.name}; `, ); }, FunctionExpression: noop, ArrowFunctionExpression: noop, MethodDefinition: noop, AwaitExpression(node, state, c) { state.containsAwait = true; walk.base.AwaitExpression(node, state, c); }, ReturnStatement(node, state, c) { state.containsReturn = true; walk.base.ReturnStatement(node, state, c); }, VariableDeclaration(node, state, c) { const variableKind = node.kind; const isIterableForDeclaration = Array.prototype.includes.call( ["ForOfStatement", "ForInStatement"], state.ancestors[state.ancestors.length - 2].type, ); if (variableKind === "var" || isTopLevelDeclaration(state)) { state.replace( node.start, node.start + variableKind.length + (isIterableForDeclaration ? 1 : 0), variableKind === "var" && isIterableForDeclaration ? "" : "void" + (node.declarations.length === 1 ? "" : " ("), ); if (!isIterableForDeclaration) { Array.prototype.forEach.call(node.declarations, (decl) => { state.prepend(decl, "("); state.append(decl, decl.init ? ")" : "=undefined)"); }); if (node.declarations.length !== 1) { state.append(node.declarations[node.declarations.length - 1], ")"); } } const variableIdentifiersToHoist = [ ["var", []], ["let", []], ]; function registerVariableDeclarationIdentifiers(node) { switch (node.type) { case "Identifier": Array.prototype.push.call( variableIdentifiersToHoist[variableKind === "var" ? 0 : 1][1], node.name, ); break; case "ObjectPattern": Array.prototype.forEach.call(node.properties, (property) => { registerVariableDeclarationIdentifiers( property.value || property.argument, ); }); break; case "ArrayPattern": Array.prototype.forEach.call(node.elements, (element) => { registerVariableDeclarationIdentifiers(element); }); break; } } Array.prototype.forEach.call(node.declarations, (decl) => { registerVariableDeclarationIdentifiers(decl.id); }); Array.prototype.forEach.call( variableIdentifiersToHoist, ({ 0: kind, 1: identifiers }) => { if (identifiers.length > 0) { Array.prototype.push.call( state.hoistedDeclarationStatements, `${kind} ${Array.prototype.join.call(identifiers, ", ")}; `, ); } }, ); } walk.base.VariableDeclaration(node, state, c); }, }; const visitors = {}; for (const nodeType of Object.keys(walk.base)) { const callback = visitorsWithoutAncestors[nodeType] || walk.base[nodeType]; visitors[nodeType] = (node, state, c) => { const isNew = node !== state.ancestors[state.ancestors.length - 1]; if (isNew) { Array.prototype.push.call(state.ancestors, node); } callback(node, state, c); if (isNew) { Array.prototype.pop.call(state.ancestors); } }; } function processTopLevelAwait(src) { const wrapPrefix = "(async () => { "; const wrapped = `${wrapPrefix}${src} })()`; const wrappedArray = String.prototype.split.call(wrapped, ""); let root; try { root = parser.parse(wrapped, { ecmaVersion: "latest" }); } catch (e) { if (String.prototype.startsWith.call(e.message, "Unterminated ")) throw new Recoverable(e); // If the parse error is before the first "await", then use the execution // error. Otherwise we must emit this parse error, making it look like a // proper syntax error. const awaitPos = String.prototype.indexOf.call(src, "await"); const errPos = e.pos - wrapPrefix.length; if (awaitPos > errPos) return null; // Convert keyword parse errors on await into their original errors when // possible. if ( errPos === awaitPos + 6 && String.prototype.includes.call( e.message, "Expecting Unicode escape sequence", ) ) return null; if ( errPos === awaitPos + 7 && String.prototype.includes.call(e.message, "Unexpected token") ) return null; const line = e.loc.line; const column = line === 1 ? e.loc.column - wrapPrefix.length : e.loc.column; let message = "\n" + String.prototype.split.call(src, "\n")[line - 1] + "\n" + String.prototype.repeat.call(" ", column) + "^\n\n" + RegExp.prototype[Symbol.replace].call(/ \([^)]+\)/, e.message, ""); // V8 unexpected token errors include the token string. if (String.prototype.endsWith.call(message, "Unexpected token")) message += " '" + // Wrapper end may cause acorn to report error position after the source (src[e.pos - wrapPrefix.length] ?? src[src.length - 1]) + "'"; // eslint-disable-next-line no-restricted-syntax throw new SyntaxError(message); } const body = root.body[0].expression.callee.body; const state = { body, ancestors: [], hoistedDeclarationStatements: [], replace(from, to, str) { for (let i = from; i < to; i++) { wrappedArray[i] = ""; } if (from === to) str += wrappedArray[from]; wrappedArray[from] = str; }, prepend(node, str) { wrappedArray[node.start] = str + wrappedArray[node.start]; }, append(node, str) { wrappedArray[node.end - 1] += str; }, containsAwait: false, containsReturn: false, }; walk.recursive(body, state, visitors); // Do not transform if // 1. False alarm: there isn't actually an await expression. // 2. There is a top-level return, which is not allowed. if (!state.containsAwait || state.containsReturn) { return null; } for (let i = body.body.length - 1; i >= 0; i--) { const node = body.body[i]; if (node.type === "EmptyStatement") continue; if (node.type === "ExpressionStatement") { // For an expression statement of the form // ( expr ) ; // ^^^^^^^^^^ // node // ^^^^ // node.expression // // We do not want the left parenthesis before the `return` keyword; // therefore we prepend the `return (` to `node`. // // On the other hand, we do not want the right parenthesis after the // semicolon. Since there can only be more right parentheses between // node.expression.end and the semicolon, appending one more to // node.expression should be fine. // // We also create a wrapper object around the result of the expression. // Consider an expression of the form `(await x).y`. If we just return // this expression from an async function, the caller will await `y`, too, // if it evaluates to a Promise. Instead, we return // `{ value: ((await x).y) }`, which allows the caller to retrieve the // awaited value correctly. state.prepend(node.expression, "{ value: ("); state.prepend(node, "return "); state.append(node.expression, ") }"); } break; } return ( Array.prototype.join.call(state.hoistedDeclarationStatements, "") + Array.prototype.join.call(wrappedArray, "") ); } export { processTopLevelAwait };