Skip to content

Commit 76437f0

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

File tree

5 files changed

+344
-20
lines changed

5 files changed

+344
-20
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/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from './util';
1919
import { readConfig } from './configuration';
2020
import type { TSCommon, TSInternal } from './ts-compiler-types';
21+
import { processTopLevelAwait } from './repl-top-level-await';
2122

2223
export { TSCommon };
2324
export { createRepl, CreateReplOptions, ReplService } from './repl';

src/repl-top-level-await.ts

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

0 commit comments

Comments
 (0)