Skip to content

Commit

Permalink
Pass Promise as a parameter to regeneratorRuntime.async() (#383)
Browse files Browse the repository at this point in the history
This makes it possible to run @babel/plugin-transform-runtime on
regenerator's output to provide a Promise polyfill in old browsers.
  • Loading branch information
nicolo-ribaudo committed Feb 20, 2020
1 parent 6e9e8d7 commit 491714f
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 8 deletions.
15 changes: 9 additions & 6 deletions packages/regenerator-runtime/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ var runtime = (function (exports) {
return { __await: arg };
};

function AsyncIterator(generator) {
function AsyncIterator(generator, PromiseImpl) {
function invoke(method, arg, resolve, reject) {
var record = tryCatch(generator[method], generator, arg);
if (record.type === "throw") {
Expand All @@ -141,14 +141,14 @@ var runtime = (function (exports) {
if (value &&
typeof value === "object" &&
hasOwn.call(value, "__await")) {
return Promise.resolve(value.__await).then(function(value) {
return PromiseImpl.resolve(value.__await).then(function(value) {
invoke("next", value, resolve, reject);
}, function(err) {
invoke("throw", err, resolve, reject);
});
}

return Promise.resolve(value).then(function(unwrapped) {
return PromiseImpl.resolve(value).then(function(unwrapped) {
// When a yielded Promise is resolved, its final value becomes
// the .value of the Promise<{value,done}> result for the
// current iteration.
Expand All @@ -166,7 +166,7 @@ var runtime = (function (exports) {

function enqueue(method, arg) {
function callInvokeWithMethodAndArg() {
return new Promise(function(resolve, reject) {
return new PromiseImpl(function(resolve, reject) {
invoke(method, arg, resolve, reject);
});
}
Expand Down Expand Up @@ -206,9 +206,12 @@ var runtime = (function (exports) {
// Note that simple async functions are implemented on top of
// AsyncIterator objects; they just return a Promise for the value of
// the final result produced by the iterator.
exports.async = function(innerFn, outerFn, self, tryLocsList) {
exports.async = function(innerFn, outerFn, self, tryLocsList, PromiseImpl) {
if (PromiseImpl === void 0) PromiseImpl = Promise;

var iter = new AsyncIterator(
wrap(innerFn, outerFn, self, tryLocsList)
wrap(innerFn, outerFn, self, tryLocsList),
PromiseImpl
);

return exports.isGeneratorFunction(outerFn)
Expand Down
17 changes: 15 additions & 2 deletions packages/regenerator-transform/src/visit.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,19 +134,32 @@ exports.getVisitor = ({ types: t }) => ({

if (node.generator) {
wrapArgs.push(outerFnExpr);
} else if (context.usesThis || tryLocsList) {
} else if (context.usesThis || tryLocsList || node.async) {
// Async functions that are not generators don't care about the
// outer function because they don't need it to be marked and don't
// inherit from its .prototype.
wrapArgs.push(t.nullLiteral());
}
if (context.usesThis) {
wrapArgs.push(t.thisExpression());
} else if (tryLocsList) {
} else if (tryLocsList || node.async) {
wrapArgs.push(t.nullLiteral());
}
if (tryLocsList) {
wrapArgs.push(tryLocsList);
} else if (node.async) {
wrapArgs.push(t.nullLiteral());
}

if (node.async) {
// Rename any locally declared "Promise" variable,
// to use the global one.
let currentScope = path.scope;
do {
if (currentScope.hasOwnBinding("Promise")) currentScope.rename("Promise");
} while (currentScope = currentScope.parent);

wrapArgs.push(t.identifier("Promise"));
}

let wrapCall = t.callExpression(
Expand Down
97 changes: 97 additions & 0 deletions test/async-custom-promise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

var assert = require("assert");

function SyncPromise(run) {
this.resolved = null;
this.value = null;
try {
run(
val => { this.resolved = true; this.value = val; },
val => { this.resolved = false; this.value = val; }
);
} catch (e) {
this.resolved = false;
this.value = e;
}
if (this.resolved === null) {
throw new Error("SyncPromise must be run synchronously");
}
if (this.value instanceof SyncPromise) this.value = this.value.value;
}
SyncPromise.prototype.then = function(onRes, onRej) {
try {
if (this.resolved) return SyncPromise.resolve(onRes(this.value));
if (onRej) return SyncPromise.resolve(onRej(this.value));
return this;
} catch (e) {
return SyncPromise.reject(e);
}
};
SyncPromise.prototype.catch = function(onRej) {
try {
if (this.resolved) return this;
return SyncPromise.resolve(onRej(this.value));
} catch (e) {
return SyncPromise.reject(e);
}
};
SyncPromise.resolve = val => new SyncPromise(res => res(val));
SyncPromise.reject = val => new SyncPromise((_, rej) => rej(val));

describe("custom Promise polyfills", function() {
it("should work with async functions", function() {
babelInjectPromise: SyncPromise;

async function fn() {
var first = await SyncPromise.resolve(2);
var second = await 3;
return 4 * first * second;
}

assert.ok(fn() instanceof SyncPromise);
assert.ok(fn().resolved);
assert.strictEqual(fn().value, 24);
});

it("should correctly handle rejections", function() {
babelInjectPromise: SyncPromise;

async function fn() {
throw 2;
}

assert.ok(fn() instanceof SyncPromise);
assert.strictEqual(fn().resolved, false);
assert.strictEqual(fn().value, 2);
});

it("should work with async generators", function() {
babelInjectPromise: SyncPromise;

async function* fn() {
await 1;
var input = yield 2;
await 3;
return input;
}

var it = fn();
var val = it.next();

assert.ok(val instanceof SyncPromise);
assert.ok(val.resolved);
assert.deepStrictEqual(val.value, { done: false, value: 2 });

val = it.next(7);

assert.ok(val instanceof SyncPromise);
assert.ok(val.resolved);
assert.deepStrictEqual(val.value, { done: true, value: 7 });
});
});
57 changes: 57 additions & 0 deletions test/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,62 @@ enqueue(convertWithSpread, [
"./test/regression.es5.js"
]);

function convertWithCustomPromiseReplacer(es6File, es5File, callback) {
var transformOptions = {
presets: [require("regenerator-preset")],
plugins: [function(babel) {
return {
visitor: {
FunctionExpression: {
exit(path) {
const stmt = path.get("body.body").find(function (stmt) {
return stmt.isLabeledStatement() &&
stmt.get("label").isIdentifier({ name: "babelInjectPromise" });
});
if (!stmt) return;

path.traverse({
ReferencedIdentifier(path) {
if (path.node.name === "Promise") {
path.replaceWith(
babel.types.cloneNode(stmt.node.body.expression)
);
}
}
});
}
}
}
};
}],
parserOpts: {
strictMode: false,
},
ast: true
};

fs.readFile(es6File, "utf-8", function(err, es6) {
if (err) {
return callback(err);
}

var { code: es5, ast } = babel.transformSync(es6, transformOptions);
fs.writeFileSync(es5File, es5);
try {
checkDuplicatedNodes(babel, ast);
} catch (err) {
err.message = "Occured while transforming: " + es6File + "\n" + err.message;
callback(err);
}
callback();
});
}

enqueue(convertWithCustomPromiseReplacer, [
"./test/async-custom-promise.js",
"./test/async-custom-promise.es5.js"
])

enqueue(makeMochaCopyFunction("mocha.js"));
enqueue(makeMochaCopyFunction("mocha.css"));

Expand Down Expand Up @@ -236,6 +292,7 @@ enqueue("mocha", [
"./test/tests-node4.es5.js",
"./test/non-native.es5.js",
"./test/async.es5.js",
"./test/async-custom-promise.es5.js",
"./test/regression.es5.js",
"./test/tests.transform.js"
]);
Expand Down

0 comments on commit 491714f

Please sign in to comment.