Skip to content

Commit c0c979e

Browse files
committed
feat: add repl top level await support
1 parent 5643ad6 commit c0c979e

File tree

4 files changed

+368
-19
lines changed

4 files changed

+368
-19
lines changed

package-lock.json

Lines changed: 14 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@
162162
"@tsconfig/node12": "^1.0.7",
163163
"@tsconfig/node14": "^1.0.0",
164164
"@tsconfig/node16": "^1.0.1",
165+
"acorn": "^8.4.1",
166+
"acorn-walk": "^8.1.1",
165167
"arg": "^4.1.0",
166168
"create-require": "^1.1.0",
167169
"diff": "^4.0.1",

src/repl-top-level-await.ts

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
// Based on https://github.com/nodejs/node/blob/975bbbc443c47df777d460fadaada3c6d265b321/lib/internal/repl/await.js
2+
import { Node, Parser } from 'acorn';
3+
import { base, recursive, RecursiveWalkerFn, WalkerCallback } from 'acorn-walk';
4+
import { Recoverable } from 'repl';
5+
6+
const walk = {
7+
base,
8+
recursive,
9+
};
10+
11+
function isTopLevelDeclaration(state: State) {
12+
return state.ancestors[state.ancestors.length - 2] === state.body;
13+
}
14+
15+
const noop: NOOP = () => {};
16+
const visitorsWithoutAncestors: VisitorsWithoutAncestors = {
17+
ClassDeclaration(node, state, c) {
18+
if (isTopLevelDeclaration(state)) {
19+
state.prepend(node, `${node.id.name}=`);
20+
state.hoistedDeclarationStatements.push(`let ${node.id.name}; `);
21+
}
22+
23+
walk.base.ClassDeclaration(node, state, c);
24+
},
25+
ForOfStatement(node, state, c) {
26+
if (node.await === true) {
27+
state.containsAwait = true;
28+
}
29+
walk.base.ForOfStatement(node, state, c);
30+
},
31+
FunctionDeclaration(node, state, c) {
32+
state.prepend(node, `${node.id.name}=`);
33+
state.hoistedDeclarationStatements.push(`var ${node.id.name}; `);
34+
},
35+
FunctionExpression: noop,
36+
ArrowFunctionExpression: noop,
37+
MethodDefinition: noop,
38+
AwaitExpression(node, state, c) {
39+
state.containsAwait = true;
40+
walk.base.AwaitExpression(node, state, c);
41+
},
42+
ReturnStatement(node, state, c) {
43+
state.containsReturn = true;
44+
walk.base.ReturnStatement(node, state, c);
45+
},
46+
VariableDeclaration(node, state, c) {
47+
const variableKind = node.kind;
48+
const isIterableForDeclaration = [
49+
'ForOfStatement',
50+
'ForInStatement',
51+
].includes(state.ancestors[state.ancestors.length - 2].type);
52+
53+
if (variableKind === 'var' || isTopLevelDeclaration(state)) {
54+
state.replace(
55+
node.start,
56+
node.start + variableKind.length + (isIterableForDeclaration ? 1 : 0),
57+
variableKind === 'var' && isIterableForDeclaration
58+
? ''
59+
: 'void' + (node.declarations.length === 1 ? '' : ' (')
60+
);
61+
62+
if (!isIterableForDeclaration) {
63+
node.declarations.forEach((decl) => {
64+
state.prepend(decl, '(');
65+
state.append(decl, decl.init ? ')' : '=undefined)');
66+
});
67+
68+
if (node.declarations.length !== 1) {
69+
state.append(node.declarations[node.declarations.length - 1], ')');
70+
}
71+
}
72+
73+
const variableIdentifiersToHoist: [
74+
['var', string[]],
75+
['let', string[]]
76+
] = [
77+
['var', []],
78+
['let', []],
79+
];
80+
81+
function registerVariableDeclarationIdentifiers(node: BaseNode) {
82+
switch (node.type) {
83+
case 'Identifier':
84+
variableIdentifiersToHoist[variableKind === 'var' ? 0 : 1][1].push(
85+
node.name
86+
);
87+
break;
88+
case 'ObjectPattern':
89+
node.properties.forEach((property) => {
90+
registerVariableDeclarationIdentifiers(property.value);
91+
});
92+
break;
93+
case 'ArrayPattern':
94+
node.elements.forEach((element) => {
95+
registerVariableDeclarationIdentifiers(element);
96+
});
97+
break;
98+
}
99+
}
100+
101+
node.declarations.forEach((decl) => {
102+
registerVariableDeclarationIdentifiers(decl.id);
103+
});
104+
105+
variableIdentifiersToHoist.forEach(({ 0: kind, 1: identifiers }) => {
106+
if (identifiers.length > 0) {
107+
state.hoistedDeclarationStatements.push(
108+
`${kind} ${identifiers.join(', ')}; `
109+
);
110+
}
111+
});
112+
}
113+
114+
walk.base.VariableDeclaration(node, state, c);
115+
},
116+
};
117+
118+
const visitors: Record<string, RecursiveWalkerFn<State>> = {};
119+
for (const nodeType of Object.keys(walk.base)) {
120+
const callback =
121+
(visitorsWithoutAncestors[nodeType as keyof VisitorsWithoutAncestors] as
122+
| VisitorsWithoutAncestorsFunction
123+
| undefined) || walk.base[nodeType];
124+
125+
visitors[nodeType] = (node, state, c) => {
126+
const isNew = node !== state.ancestors[state.ancestors.length - 1];
127+
if (isNew) {
128+
state.ancestors.push(node);
129+
}
130+
callback(node as CustomRecursiveWalkerNode, state, c);
131+
if (isNew) {
132+
state.ancestors.pop();
133+
}
134+
};
135+
}
136+
137+
export function processTopLevelAwait(src: string) {
138+
const wrapPrefix = '(async () => { ';
139+
const wrapped = `${wrapPrefix}${src} })()`;
140+
const wrappedArray = Array.from(wrapped);
141+
let root;
142+
try {
143+
root = Parser.parse(wrapped, { ecmaVersion: 'latest' }) as RootNode;
144+
} catch (e) {
145+
if (e.message.startsWith('Unterminated ')) throw new Recoverable(e);
146+
// If the parse error is before the first "await", then use the execution
147+
// error. Otherwise we must emit this parse error, making it look like a
148+
// proper syntax error.
149+
const awaitPos = src.indexOf('await');
150+
const errPos = e.pos - wrapPrefix.length;
151+
if (awaitPos > errPos) return null;
152+
// Convert keyword parse errors on await into their original errors when
153+
// possible.
154+
if (
155+
errPos === awaitPos + 6 &&
156+
e.message.includes('Expecting Unicode escape sequence')
157+
)
158+
return null;
159+
if (errPos === awaitPos + 7 && e.message.includes('Unexpected token'))
160+
return null;
161+
const line = e.loc.line;
162+
const column = line === 1 ? e.loc.column - wrapPrefix.length : e.loc.column;
163+
let message =
164+
'\n' +
165+
src.split('\n')[line - 1] +
166+
'\n' +
167+
' '.repeat(column) +
168+
'^\n\n' +
169+
e.message.replace(/ \([^)]+\)/, '');
170+
// V8 unexpected token errors include the token string.
171+
if (message.endsWith('Unexpected token'))
172+
message += " '" + src[e.pos - wrapPrefix.length] + "'";
173+
// eslint-disable-next-line no-restricted-syntax
174+
throw new SyntaxError(message);
175+
}
176+
const body = root.body[0].expression.callee.body;
177+
const state: State = {
178+
body,
179+
ancestors: [],
180+
hoistedDeclarationStatements: [],
181+
replace(from, to, str) {
182+
for (let i = from; i < to; i++) {
183+
wrappedArray[i] = '';
184+
}
185+
if (from === to) str += wrappedArray[from];
186+
wrappedArray[from] = str;
187+
},
188+
prepend(node, str) {
189+
wrappedArray[node.start] = str + wrappedArray[node.start];
190+
},
191+
append(node, str) {
192+
wrappedArray[node.end - 1] += str;
193+
},
194+
containsAwait: false,
195+
containsReturn: false,
196+
};
197+
198+
walk.recursive(body, state, visitors);
199+
200+
// Do not transform if
201+
// 1. False alarm: there isn't actually an await expression.
202+
// 2. There is a top-level return, which is not allowed.
203+
if (!state.containsAwait || state.containsReturn) {
204+
return null;
205+
}
206+
207+
const last = body.body[body.body.length - 1];
208+
if (last.type === 'ExpressionStatement') {
209+
// For an expression statement of the form
210+
// ( expr ) ;
211+
// ^^^^^^^^^^ // last
212+
// ^^^^ // last.expression
213+
//
214+
// We do not want the left parenthesis before the `return` keyword;
215+
// therefore we prepend the `return (` to `last`.
216+
//
217+
// On the other hand, we do not want the right parenthesis after the
218+
// semicolon. Since there can only be more right parentheses between
219+
// last.expression.end and the semicolon, appending one more to
220+
// last.expression should be fine.
221+
state.prepend(last, 'return (');
222+
state.append(last.expression, ')');
223+
}
224+
225+
return state.hoistedDeclarationStatements.join('') + wrappedArray.join('');
226+
}
227+
228+
type CustomNode<T> = Node & T;
229+
type RootNode = CustomNode<{
230+
body: Array<
231+
CustomNode<{
232+
expression: CustomNode<{
233+
callee: CustomNode<{
234+
body: CustomNode<{
235+
body: Array<CustomNode<{ expression: Node }>>;
236+
}>;
237+
}>;
238+
}>;
239+
}>
240+
>;
241+
}>;
242+
type CommonVisitorMethodNode = CustomNode<{ id: CustomNode<{ name: string }> }>;
243+
type ForOfStatementNode = CustomNode<{ await: boolean }>;
244+
type VariableDeclarationNode = CustomNode<{
245+
kind: 'var' | 'let' | 'const';
246+
declarations: VariableDeclaratorNode[];
247+
}>;
248+
249+
type IdentifierNode = CustomNode<{ type: 'Identifier'; name: string }>;
250+
type ObjectPatternNode = CustomNode<{
251+
type: 'ObjectPattern';
252+
properties: Array<PropertyNode>;
253+
}>;
254+
type ArrayPatternNode = CustomNode<{
255+
type: 'ArrayPattern';
256+
elements: Array<BaseNode>;
257+
}>;
258+
type PropertyNode = CustomNode<{
259+
type: 'Property';
260+
method: boolean;
261+
shorthand: boolean;
262+
computed: boolean;
263+
key: BaseNode;
264+
kind: string;
265+
value: BaseNode;
266+
}>;
267+
type BaseNode = IdentifierNode | ObjectPatternNode | ArrayPatternNode;
268+
type VariableDeclaratorNode = CustomNode<{ id: BaseNode; init: Node }>;
269+
270+
interface State {
271+
body: Node;
272+
ancestors: Node[];
273+
hoistedDeclarationStatements: string[];
274+
replace: (from: number, to: number, str: string) => void;
275+
prepend: (node: Node, str: string) => void;
276+
append: (from: Node, str: string) => void;
277+
containsAwait: boolean;
278+
containsReturn: boolean;
279+
}
280+
281+
type NOOP = () => void;
282+
283+
type VisitorsWithoutAncestors = {
284+
ClassDeclaration: CustomRecursiveWalkerFn<CommonVisitorMethodNode>;
285+
ForOfStatement: CustomRecursiveWalkerFn<ForOfStatementNode>;
286+
FunctionDeclaration: CustomRecursiveWalkerFn<CommonVisitorMethodNode>;
287+
FunctionExpression: NOOP;
288+
ArrowFunctionExpression: NOOP;
289+
MethodDefinition: NOOP;
290+
AwaitExpression: CustomRecursiveWalkerFn<CommonVisitorMethodNode>;
291+
ReturnStatement: CustomRecursiveWalkerFn<CommonVisitorMethodNode>;
292+
VariableDeclaration: CustomRecursiveWalkerFn<VariableDeclarationNode>;
293+
};
294+
295+
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
296+
k: infer I
297+
) => void
298+
? I
299+
: never;
300+
type VisitorsWithoutAncestorsFunction = VisitorsWithoutAncestors[keyof VisitorsWithoutAncestors];
301+
type CustomRecursiveWalkerNode = UnionToIntersection<
302+
Exclude<Parameters<VisitorsWithoutAncestorsFunction>[0], undefined>
303+
>;
304+
305+
type CustomRecursiveWalkerFn<N extends Node> = (
306+
node: N,
307+
state: State,
308+
c: WalkerCallback<State>
309+
) => void;

0 commit comments

Comments
 (0)