diff --git a/npm-packages/meteor-babel/options.js b/npm-packages/meteor-babel/options.js index 7f3db27945..7ff86ac842 100644 --- a/npm-packages/meteor-babel/options.js +++ b/npm-packages/meteor-babel/options.js @@ -188,16 +188,15 @@ function getDefaultsForNode8(features) { ); // TODO [fibers]: instead of removing the code below, consider this comment: // https://github.com/meteor/meteor/pull/12471/files#r1089610144 - const isFiberDisabled = process.env.DISABLE_FIBERS === '1'; const ignoreAsyncPlugin = process.env.IGNORE_ASYNC_PLUGIN === '1'; - if (!ignoreAsyncPlugin) { + if (!features.useNativeAsyncAwait && !ignoreAsyncPlugin) { combined.plugins.push([ require('./plugins/async-await.js'), { - // Do not transform `await x` to `Promise.await(x)`, since Node - // 8 has native support for await expressions. - useNativeAsyncAwait: isFiberDisabled, + // Even though Node 8 supports native async/await, it is not + // compatible with fibers. + useNativeAsyncAwait: false, }, ]); } diff --git a/npm-packages/meteor-babel/plugins/async-await.js b/npm-packages/meteor-babel/plugins/async-await.js new file mode 100644 index 0000000000..ffd97d28af --- /dev/null +++ b/npm-packages/meteor-babel/plugins/async-await.js @@ -0,0 +1,74 @@ +"use strict"; + +module.exports = function (babel) { + const t = babel.types; + + return { + name: "transform-meteor-async-await", + visitor: { + Function: { + exit: function (path) { + const node = path.node; + if (!node.async) { + return; + } + + // The original function becomes a non-async function that + // returns a Promise. + node.async = false; + + // The inner function should inherit lexical environment items + // like `this`, `super`, and `arguments` from the outer + // function, and arrow functions provide exactly that behavior. + const innerFn = t.arrowFunctionExpression( + // The inner function has no parameters of its own, but can + // refer to the outer parameters of the original function. + [], + node.body, + // The inner function called by Promise.asyncApply should be + // async if we have native async/await support. + !!this.opts.useNativeAsyncAwait + ); + + const promiseResultExpression = t.callExpression( + t.memberExpression( + t.identifier("Promise"), + t.identifier("asyncApply"), + false + ), [innerFn] + ); + + // Calling the async function with Promise.asyncApply is + // important to ensure that the part before the first await + // expression runs synchronously in its own Fiber, even when + // there is native support for async/await. + if (node.type === "ArrowFunctionExpression") { + node.body = promiseResultExpression; + } else { + node.body = t.blockStatement([ + t.returnStatement(promiseResultExpression) + ]); + } + } + }, + + AwaitExpression: function (path) { + if (this.opts.useNativeAsyncAwait) { + // No need to transform await expressions if we have native + // support for them. + return; + } + + const node = path.node; + path.replaceWith(t.callExpression( + t.memberExpression( + t.identifier("Promise"), + t.identifier(node.all ? "awaitAll" : "await"), + false + ), + [node.argument] + )); + } + } + }; +}; diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 879b6b302e..2b7e2e8301 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -96,6 +96,8 @@ BCp.processOneFileForTarget = function (inputFile, source) { features.topLevelAwait = arch.startsWith('os.') || enableClientTLA + features.useNativeAsyncAwait = Meteor.isFibersDisabled; + if (! features.hasOwnProperty("jscript")) { // Perform some additional transformations to improve compatibility // in older browsers (e.g. wrapping named function expressions, per diff --git a/packages/meteor/asl-helpers-client.js b/packages/meteor/asl-helpers-client.js index 688ae22205..4780867f3a 100644 --- a/packages/meteor/asl-helpers-client.js +++ b/packages/meteor/asl-helpers-client.js @@ -1,3 +1,5 @@ +Meteor.isFibersDisabled = true; + Meteor._isPromise = (r) => { return r && typeof r.then === 'function'; }; diff --git a/packages/meteor/asl-helpers.js b/packages/meteor/asl-helpers.js index f140cc9a3c..bd9271b7cb 100644 --- a/packages/meteor/asl-helpers.js +++ b/packages/meteor/asl-helpers.js @@ -2,7 +2,9 @@ const getAslStore = () => (Meteor.isServer && global?.asyncLocalStorage?.getStor const getValueFromAslStore = key => getAslStore()[key]; const updateAslStore = (key, value) => getAslStore()[key] = value; -Meteor._isFibersEnabled = !process.env.DISABLE_FIBERS && Meteor.isServer; +Meteor.isFibersDisabled = !!__meteor_bootstrap__.isFibersDisabled; +Meteor._isFibersEnabled = !Meteor.isFibersDisabled; + Meteor._getAslStore = getAslStore; Meteor._getValueFromAslStore = getValueFromAslStore; Meteor._updateAslStore = updateAslStore; diff --git a/tools/isobuild/builder.js b/tools/isobuild/builder.js index 98f74a1a19..563d47bf01 100644 --- a/tools/isobuild/builder.js +++ b/tools/isobuild/builder.js @@ -369,7 +369,8 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}` const babel = require("@meteorjs/babel"); const commonBabelOptions = babel.getDefaultOptions({ nodeMajorVersion: parseInt(process.versions.node), - typescript: true + typescript: true, + useNativeAsyncAwait: true }); commonBabelOptions.sourceMaps = true; diff --git a/tools/isobuild/isopack.js b/tools/isobuild/isopack.js index b1925feaf4..31be03ed33 100644 --- a/tools/isobuild/isopack.js +++ b/tools/isobuild/isopack.js @@ -472,8 +472,19 @@ Object.assign(Isopack.prototype, { // case right.) }, async function () { // Make a new Plugin API object for this plugin. - var Plugin = self._makePluginApi(name); - await plugin.load({ Plugin: Plugin, Profile: Profile }); + const Plugin = self._makePluginApi(name); + const __meteor_bootstrap__ = { + isFibersDisabled: true, + // Set to null to tell Meteor.startup to call hooks immediately + // XXX: should we fully support startup hooks in build plugins? + startupHooks: null + }; + + await plugin.load({ + Plugin, + Profile, + __meteor_bootstrap__ + }); }); } diff --git a/tools/static-assets/server/boot.js b/tools/static-assets/server/boot.js index 8f768807b1..a62bfd592d 100644 --- a/tools/static-assets/server/boot.js +++ b/tools/static-assets/server/boot.js @@ -33,7 +33,8 @@ var starJson = JSON.parse(fs.readFileSync(path.join(buildDir, "star.json"))); __meteor_bootstrap__ = { startupHooks: [], serverDir: serverDir, - configJson: configJson + configJson: configJson, + isFibersDisabled: true }; __meteor_runtime_config__ = { @@ -508,3 +509,4 @@ var runMain = Profile("Run main()", async function () { process.exit(1) }); + diff --git a/tools/tool-env/install-babel.js b/tools/tool-env/install-babel.js index fb79789846..a49bcb6a7b 100644 --- a/tools/tool-env/install-babel.js +++ b/tools/tool-env/install-babel.js @@ -11,7 +11,8 @@ function babelRegister() { const cacheDir = path.join(meteorPath, ".babel-cache"); const babelOptions = meteorBabel.getDefaultOptions({ nodeMajorVersion: parseInt(process.versions.node), - typescript: true + typescript: true, + useNativeAsyncAwait: true }); // Make sure that source maps are included in the generated code for diff --git a/tools/tool-env/isopackets.js b/tools/tool-env/isopackets.js index d2c21afd6d..14f4700791 100644 --- a/tools/tool-env/isopackets.js +++ b/tools/tool-env/isopackets.js @@ -303,7 +303,10 @@ var loadIsopacketFromDisk = async function (isopacketName) { // An incredibly minimalist version of the environment from // tools/server/boot.js. Kind of a hack. var env = { - __meteor_bootstrap__: { startupHooks: [] }, + __meteor_bootstrap__: { + startupHooks: [], + isFibersDisabled: true + }, __meteor_runtime_config__: { meteorRelease: "ISOPACKET" } }; env.Profile = Profile;