Skip to content

Commit 154c118

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

File tree

5 files changed

+269
-20
lines changed

5 files changed

+269
-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: 12 additions & 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';
@@ -1177,6 +1178,17 @@ export function create(rawOptions: CreateOptions = {}): Service {
11771178
// Create a simple TypeScript compiler proxy.
11781179
function compile(code: string, fileName: string, lineOffset = 0) {
11791180
const normalizedFileName = normalizeSlashes(fileName);
1181+
1182+
if (
1183+
/* ts-node flag or node --experimental-repl-await flag &&*/
1184+
code.includes('await')
1185+
) {
1186+
const wrappedCode = processTopLevelAwait(code) || code;
1187+
if (code !== wrappedCode) {
1188+
code = wrappedCode;
1189+
}
1190+
}
1191+
11801192
const [value, sourceMap] = getOutput(code, normalizedFileName);
11811193
const output = updateOutput(
11821194
value,

src/repl-top-level-await.ts

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

src/repl.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export function createRepl(options: CreateReplOptions = {}) {
109109
return _eval(service!, state, code);
110110
}
111111

112-
function nodeEval(
112+
async function nodeEval(
113113
code: string,
114114
_context: any,
115115
_filename: string,
@@ -125,7 +125,7 @@ export function createRepl(options: CreateReplOptions = {}) {
125125
}
126126

127127
try {
128-
result = evalCode(code);
128+
result = await evalCode(code);
129129
} catch (error) {
130130
if (error instanceof TSError) {
131131
// Support recoverable compilations using >= node 6.
@@ -208,7 +208,7 @@ export function createEvalAwarePartialHost(
208208
/**
209209
* Evaluate the code snippet.
210210
*/
211-
function _eval(service: Service, state: EvalState, input: string) {
211+
async function _eval(service: Service, state: EvalState, input: string) {
212212
const lines = state.lines;
213213
const isCompletion = !/\n$/.test(input);
214214
const undo = appendEval(state, input);

0 commit comments

Comments
 (0)