Skip to content

Commit aff7ad8

Browse files
committed
Add basic experimental support for top level await when the topLevelAwait option is set to "simple" or "return"
1 parent 3360b75 commit aff7ad8

File tree

17 files changed

+157
-30
lines changed

17 files changed

+157
-30
lines changed

async-to-promises.test.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ function readTest(name) {
173173
let output;
174174
let inlined;
175175
let hoisted;
176-
let options;
176+
let optionsJSON;
177177
const cases = Object.create(null);
178178
for (const fileName of fs.readdirSync(`tests/${name}`)) {
179179
const content = fs.readFileSync(`tests/${name}/${fileName}`).toString();
@@ -187,7 +187,7 @@ function readTest(name) {
187187
hoisted = content;
188188
} else if (fileName === "options.json") {
189189
try {
190-
options = JSON.parse(content);
190+
optionsJSON = JSON.parse(content);
191191
} catch (e) {
192192
throw new Error(`Failed to parse tests/${name}/options.json`);
193193
}
@@ -205,7 +205,8 @@ function readTest(name) {
205205
plugins = [],
206206
supportedBabels = Object.keys(environments),
207207
presets = [],
208-
} = options || {};
208+
options = {},
209+
} = optionsJSON || {};
209210
return {
210211
error,
211212
checkSyntax,
@@ -218,6 +219,7 @@ function readTest(name) {
218219
plugins,
219220
supportedBabels,
220221
presets,
222+
options,
221223
};
222224
}
223225

@@ -243,6 +245,7 @@ for (const name of fs.readdirSync("tests").sort()) {
243245
plugins,
244246
presets,
245247
supportedBabels,
248+
options,
246249
} = readTest(name);
247250
for (const babelName of supportedBabels) {
248251
if (!(babelName in environments)) {
@@ -268,7 +271,7 @@ for (const name of fs.readdirSync("tests").sort()) {
268271
try {
269272
babel.transformFromAst(ast, parseInput, {
270273
presets,
271-
plugins: [[pluginUnderTest, {}]],
274+
plugins: [[pluginUnderTest, options]],
272275
compact: true,
273276
});
274277
throw new Error("Expected error: " + error.toString());
@@ -281,15 +284,17 @@ for (const name of fs.readdirSync("tests").sort()) {
281284
const extractFunction = module ? extractOnlyUserCode : extractJustFunction;
282285
const result = babel.transformFromAst(types.cloneDeep(ast), parseInput, {
283286
presets,
284-
plugins: mappedPlugins.concat([[pluginUnderTest, { target: "es6" }]]),
287+
plugins: mappedPlugins.concat([[pluginUnderTest, Object.assign({ target: "es6" }, options)]]),
285288
compact: true,
286289
ast: true,
287290
});
288291
const strippedResult = extractFunction(babel, result);
289292
if (runInlined) {
290293
var inlinedResult = babel.transformFromAst(types.cloneDeep(ast), parseInput, {
291294
presets,
292-
plugins: mappedPlugins.concat([[pluginUnderTest, { inlineHelpers: true }]]),
295+
plugins: mappedPlugins.concat([
296+
[pluginUnderTest, Object.assign({ inlineHelpers: true }, options)],
297+
]),
293298
compact: true,
294299
ast: true,
295300
});
@@ -298,7 +303,9 @@ for (const name of fs.readdirSync("tests").sort()) {
298303
if (runHoisted) {
299304
var hoistedResult = babel.transformFromAst(types.cloneDeep(ast), parseInput, {
300305
presets,
301-
plugins: mappedPlugins.concat([[pluginUnderTest, { hoist: true, minify: true }]]),
306+
plugins: mappedPlugins.concat([
307+
[pluginUnderTest, Object.assign({ hoist: true, minify: true }, options)],
308+
]),
302309
compact: true,
303310
ast: true,
304311
});

async-to-promises.ts

Lines changed: 99 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ interface AsyncToPromisesConfiguration {
5252
inlineHelpers: boolean;
5353
minify: boolean;
5454
target: "es5" | "es6";
55+
topLevelAwait: "disabled" | "simple" | "return" | "ignore";
5556
}
5657

5758
const defaultConfigValues: AsyncToPromisesConfiguration = {
@@ -60,6 +61,7 @@ const defaultConfigValues: AsyncToPromisesConfiguration = {
6061
inlineHelpers: false,
6162
minify: false,
6263
target: "es5",
64+
topLevelAwait: "disabled",
6365
} as const;
6466

6567
function readConfigKey<K extends keyof AsyncToPromisesConfiguration>(
@@ -284,8 +286,12 @@ declare module "@babel/traverse" {
284286
}
285287
}
286288

289+
type UsedHelpers = { [key in HelperName]: true };
290+
287291
interface PluginState {
288292
readonly opts: Partial<Readonly<AsyncToPromisesConfiguration>>;
293+
processedTopLevelAwait?: true;
294+
usedHelpers?: UsedHelpers;
289295
}
290296

291297
interface GeneratorState {
@@ -343,6 +349,16 @@ const numberNames = ["zero", "one", "two", "three", "four", "five", "six", "seve
343349

344350
type CompatibleSubset<New, Old> = Partial<New> & Pick<New, keyof New & keyof Old>;
345351

352+
// function generate(node?: Node): string {
353+
// if (node === undefined) {
354+
// return "";
355+
// }
356+
// return require("@babel/generator").default(node, {
357+
// "compact": true,
358+
// "minified": true,
359+
// }).code;
360+
// }
361+
346362
// Main function, called by babel with module implementations for types, template, traverse, transformFromAST and its version information
347363
export default function ({
348364
types,
@@ -1568,7 +1584,8 @@ export default function ({
15681584
additionalConstantNames: string[],
15691585
temporary?: Identifier | Pattern,
15701586
exitCheck?: Expression,
1571-
directExpression?: Expression
1587+
directExpression?: Expression,
1588+
skipReturns?: boolean
15721589
) {
15731590
// Find the tail continuation
15741591
checkPathValidity(target);
@@ -1637,6 +1654,8 @@ export default function ({
16371654
// Insert a call to return the awaited expression
16381655
if (target.isExpression() && target.parentPath.isArrowFunctionExpression()) {
16391656
target.replaceWith(replacement.expression);
1657+
} else if (skipReturns) {
1658+
target.replaceWith(replacement.expression);
16401659
} else if (target.isBlockStatement() && target.parentPath.isFunctionExpression()) {
16411660
target.replaceWith(types.blockStatement([returnStatement(replacement.expression, originalNode)]));
16421661
} else {
@@ -3179,6 +3198,7 @@ export default function ({
31793198
path: NodePath;
31803199
additionalConstantNames: string[];
31813200
exitIdentifier?: Identifier;
3201+
skipReturns?: boolean;
31823202
}
31833203

31843204
// Calls the _yield helper on an expression
@@ -3366,7 +3386,8 @@ export default function ({
33663386
undefined,
33673387
targetPath.isYieldExpression()
33683388
? undefined
3369-
: booleanLiteral(false, readConfigKey(pluginState.opts, "minify"))
3389+
: booleanLiteral(false, readConfigKey(pluginState.opts, "minify")),
3390+
state.skipReturns
33703391
);
33713392
} else if (parent.isIfStatement()) {
33723393
const test = parent.get("test");
@@ -3391,7 +3412,9 @@ export default function ({
33913412
parent,
33923413
additionalConstantNames,
33933414
resultIdentifier,
3394-
exitIdentifier
3415+
exitIdentifier,
3416+
undefined,
3417+
state.skipReturns
33953418
);
33963419
processExpressions = false;
33973420
}
@@ -3505,7 +3528,9 @@ export default function ({
35053528
parent,
35063529
additionalConstantNames,
35073530
temporary,
3508-
exitCheck
3531+
exitCheck,
3532+
undefined,
3533+
state.skipReturns
35093534
);
35103535
processExpressions = false;
35113536
} else if (
@@ -3580,7 +3605,9 @@ export default function ({
35803605
: (parent as NodePath<Statement>),
35813606
additionalConstantNames,
35823607
resultIdentifier,
3583-
exitIdentifier
3608+
exitIdentifier,
3609+
undefined,
3610+
state.skipReturns
35843611
);
35853612
processExpressions = false;
35863613
} else {
@@ -3701,7 +3728,9 @@ export default function ({
37013728
parent,
37023729
additionalConstantNames,
37033730
resultIdentifier,
3704-
exitIdentifier
3731+
exitIdentifier,
3732+
undefined,
3733+
state.skipReturns
37053734
);
37063735
processExpressions = false;
37073736
}
@@ -3791,7 +3820,9 @@ export default function ({
37913820
label && parent.parentPath.isStatement() ? parent.parentPath : parent,
37923821
additionalConstantNames,
37933822
resultIdentifier,
3794-
exitIdentifier
3823+
exitIdentifier,
3824+
undefined,
3825+
state.skipReturns
37953826
);
37963827
processExpressions = false;
37973828
}
@@ -3825,7 +3856,9 @@ export default function ({
38253856
parent,
38263857
additionalConstantNames,
38273858
resultIdentifier,
3828-
exitCheck
3859+
exitCheck,
3860+
undefined,
3861+
state.skipReturns
38293862
);
38303863
processExpressions = false;
38313864
}
@@ -3892,7 +3925,8 @@ export default function ({
38923925
additionalConstantNames,
38933926
resultIdentifier,
38943927
undefined,
3895-
awaitPath.isYieldExpression() ? undefined : directExpression
3928+
awaitPath.isYieldExpression() ? undefined : directExpression,
3929+
state.skipReturns
38963930
);
38973931
}
38983932
}
@@ -4020,9 +4054,16 @@ export default function ({
40204054
path: NodePath,
40214055
additionalConstantNames: string[],
40224056
exitIdentifier?: Identifier,
4023-
shouldUnpromisify?: boolean
4057+
shouldUnpromisify?: boolean,
4058+
skipReturns?: boolean
40244059
) {
4025-
path.traverse(rewriteAsyncBlockVisitor, { generatorState, path, additionalConstantNames, exitIdentifier });
4060+
path.traverse(rewriteAsyncBlockVisitor, {
4061+
generatorState,
4062+
path,
4063+
additionalConstantNames,
4064+
exitIdentifier,
4065+
skipReturns,
4066+
});
40264067
if (shouldUnpromisify) {
40274068
// Rewrite values that potentially could be promises to booleans so that they aren't awaited
40284069
if (path.isArrowFunctionExpression()) {
@@ -4240,18 +4281,8 @@ export default function ({
42404281
for (const dependency of helper.dependencies) {
42414282
helperReference(state, path, dependency);
42424283
}
4243-
// Insert the new node
4244-
const value = cloneNode(helper.value) as typeof helper.value;
4245-
const newPath = insertHelper(file.path, value);
4246-
// Rename references to other helpers due to name conflicts
4247-
newPath.traverse({
4248-
Identifier(path) {
4249-
const name = path.node.name;
4250-
if (Object.hasOwnProperty.call(helpers, name)) {
4251-
path.replaceWith(file.declarations[name]);
4252-
}
4253-
},
4254-
} as Visitor);
4284+
const usedHelpers = state.usedHelpers || (state.usedHelpers = {} as UsedHelpers);
4285+
usedHelpers[name] = true;
42554286
}
42564287
}
42574288
return result;
@@ -4766,6 +4797,51 @@ export default function ({
47664797
parserOptions.plugins.push("asyncGenerators");
47674798
},
47684799
visitor: {
4800+
AwaitExpression(path) {
4801+
if (!path.getFunctionParent() && !this.processedTopLevelAwait) {
4802+
this.processedTopLevelAwait = true;
4803+
switch (readConfigKey(this.opts, "topLevelAwait")) {
4804+
case "simple": {
4805+
const programPath = path.scope.getProgramParent().path;
4806+
rewriteAsyncBlock({ state: this }, programPath, [], undefined, false, true);
4807+
break;
4808+
}
4809+
case "return": {
4810+
const programPath = path.scope.getProgramParent().path;
4811+
rewriteAsyncBlock({ state: this }, programPath, [], undefined, false, false);
4812+
break;
4813+
}
4814+
case "ignore":
4815+
break;
4816+
default:
4817+
throw path.buildCodeFrameError(
4818+
`Top level await is not supported unless experimental topLevelAwait: "simple" or topLevelAwait: "return" options are specified!`,
4819+
TypeError
4820+
);
4821+
}
4822+
}
4823+
},
4824+
Program: {
4825+
exit(path) {
4826+
const usedHelpers = this.usedHelpers;
4827+
if (usedHelpers !== undefined) {
4828+
const file = getFile(path);
4829+
for (const helperName of Object.keys(usedHelpers)) {
4830+
const helper = helpers![helperName];
4831+
const value = cloneNode(helper.value) as typeof helper.value;
4832+
const newPath = insertHelper(file.path, value);
4833+
newPath.traverse({
4834+
Identifier(identifierPath) {
4835+
const name = identifierPath.node.name;
4836+
if (Object.hasOwnProperty.call(helpers, name)) {
4837+
identifierPath.replaceWith(file.declarations[name]);
4838+
}
4839+
},
4840+
} as Visitor);
4841+
}
4842+
}
4843+
},
4844+
},
47694845
FunctionDeclaration(path) {
47704846
const node = path.node;
47714847
if (node.async) {

tests/break switch/hoisted.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/break switch/inlined.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/break switch/input.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
async () => {
2+
const r = await test1();
3+
switch (r) {
4+
case "1":
5+
console.log("1111");
6+
break;
7+
case "2":
8+
console.log("2222");
9+
break;
10+
}
11+
console.log("33333333333");
12+
}

tests/break switch/options.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"supportedBabels": ["babel 7"]
3+
}

tests/break switch/output.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/top level await basic/hoisted.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/top level await basic/inlined.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/top level await basic/input.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const response = await fetch("https://www.example.com/");
2+
const json = await response.json();
3+
console.log(json);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"supportedBabels": ["babel 7"],
3+
"module": true,
4+
"options": {
5+
"topLevelAwait": "simple"
6+
}
7+
}

tests/top level await basic/output.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
_await(fetch("https://www.example.com/"),response=>_await(response.json(),json=>{console.log(json);}));

tests/top level await return/hoisted.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/top level await return/inlined.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/top level await return/input.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const response = await fetch("https://www.example.com/");
2+
const json = await response.json();
3+
console.log(json);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"supportedBabels": ["babel 7"],
3+
"module": true,
4+
"options": {
5+
"topLevelAwait": "return"
6+
}
7+
}

0 commit comments

Comments
 (0)