nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
256 lines (237 loc) • 8.54 kB
JavaScript
// 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 };