diff --git a/packages/dynamic-import/client.js b/packages/dynamic-import/client.js index 9668f32bdb..b6d444f31b 100644 --- a/packages/dynamic-import/client.js +++ b/packages/dynamic-import/client.js @@ -2,8 +2,13 @@ var Module = module.constructor; var delayPromise = Promise.resolve(); var requireMeta = meteorInstall._requireMeta; var cache = require("./cache.js"); +var Mp = Module.prototype; -Module.prototype.dynamicImport = function (id) { +// Call module.dynamicImport(id) to fetch a module and any/all of its +// dependencies that have not already been fetched, and evaluate them as +// soon as they arrive. This runtime API makes it very easy to implement +// ECMAScript dynamic import(...) syntax. +Mp.dynamicImport = function (id) { // The real (not meta) parent module. var module = this; @@ -18,45 +23,59 @@ Module.prototype.dynamicImport = function (id) { throw error; } - // Require the parent module from the complete meta graph. - var meta = requireMeta(module.id); - var versions = Object.create(null); - var dynamicVersions = require("./dynamic-versions.js"); + return module.prefetch(id).then(get); + }); +}; - function walk(meta) { - if (meta.dynamic && ! meta.pending) { - meta.pending = true; - var id = meta.module.id; - versions[id] = getFromTree(dynamicVersions, id); - meta.eachChild(walkChild); +// Call module.prefetch(id) to fetch modules without evaluating them. +// Returns a Promise that resolves to an Error object if importing the +// given id failed, and null otherwise. +Mp.prefetch = function (id) { + // Require the parent module from the complete meta graph. + var meta = requireMeta(this.id); + var versions = Object.create(null); + var dynamicVersions = require("./dynamic-versions.js"); + + function walk(meta) { + if (meta.dynamic && ! meta.pending) { + meta.pending = true; + var id = meta.module.id; + versions[id] = getFromTree(dynamicVersions, id); + meta.eachChild(walkChild); + } + } + + function walkChild(childModule) { + return walk(childModule.exports); + } + + meta.eachChild(walkChild, [id]); + + var error = meta.errors && meta.errors[id]; + if (error) { + // If there was an error resolving the top-level id, let that error be + // the final result of the module.prefetch(id) promise. + return Promise.resolve(error); + } + + return cache.checkMany(versions).then(function (sources) { + var localTree = null; + var missingTree = null; + + Object.keys(sources).forEach(function (id) { + var source = sources[id]; + if (source) { + addToTree(localTree = localTree || Object.create(null), id, source); + } else { + addToTree(missingTree = missingTree || Object.create(null), id, 1); } + }); + + if (localTree) { + installResults(localTree, true); } - function walkChild(childModule) { - return walk(childModule.exports); - } - - meta.eachChild(walkChild, [id]); - - var localTree; - var missingTree; - - return cache.checkMany(versions).then(function (sources) { - Object.keys(sources).forEach(function (id) { - var source = sources[id]; - if (source) { - addToTree(localTree = localTree || Object.create(null), id, source); - } else { - addToTree(missingTree = missingTree || Object.create(null), id, 1); - } - }); - - if (localTree) { - installResults(localTree, true); - } - - return missingTree && fetchMissing(missingTree); - }).then(get); + return missingTree && fetchMissing(missingTree); }); }; @@ -162,6 +181,8 @@ function installResults(resultsTree, doNotCache) { if (! doNotCache) { cache.setMany(versionsAndSourcesById); } + + return null; } function getFromTree(tree, id) { diff --git a/packages/modules-runtime/meteor-install.js b/packages/modules-runtime/meteor-install.js index ae9f6ca33c..57c40ec4d1 100644 --- a/packages/modules-runtime/meteor-install.js +++ b/packages/modules-runtime/meteor-install.js @@ -124,6 +124,16 @@ function makeMetaFunc(value, dynamic, options) { exports.dynamic = !! dynamic; exports.options = options; + function quietRequire(id) { + try { + return require(id); + } catch (error) { + (exports.errors = + exports.errors || Object.create(null) + )[id] = error; + } + } + // One of the purposes of the meta graph is to support traversing // module dependencies without evaluating any actual module code. // The eachChild function is essential to that traversal. @@ -134,7 +144,7 @@ function makeMetaFunc(value, dynamic, options) { idsToRequire = idsToRequire || (value && value.deps); if (Array.isArray(idsToRequire)) { - idsToRequire.forEach(require); + idsToRequire.forEach(quietRequire); } // After requiring any/all dependencies of this module, iterate over diff --git a/tools/tests/apps/dynamic-import/imports/prefetch-child.js b/tools/tests/apps/dynamic-import/imports/prefetch-child.js new file mode 100644 index 0000000000..dba4092260 --- /dev/null +++ b/tools/tests/apps/dynamic-import/imports/prefetch-child.js @@ -0,0 +1 @@ +export const shared = Object.create(null); diff --git a/tools/tests/apps/dynamic-import/imports/prefetch.js b/tools/tests/apps/dynamic-import/imports/prefetch.js new file mode 100644 index 0000000000..25dcdc0e57 --- /dev/null +++ b/tools/tests/apps/dynamic-import/imports/prefetch.js @@ -0,0 +1,3 @@ +import { shared } from "./prefetch-child.js"; +export const name = module.id; +shared[name] = true; diff --git a/tools/tests/apps/dynamic-import/tests.js b/tools/tests/apps/dynamic-import/tests.js index fd38dab720..9109d5ebec 100644 --- a/tools/tests/apps/dynamic-import/tests.js +++ b/tools/tests/apps/dynamic-import/tests.js @@ -148,6 +148,28 @@ describe("dynamic import(...)", function () { global.Helper ); }); + + it("works with module.prefetch(id)", async function () { + import { shared } from "./imports/prefetch-child"; + assert.deepEqual(shared, {}); + + const error = await module.prefetch("./imports/nonexistent.js"); + assert.ok(error instanceof Error); + assert.ok(error.message.startsWith("Cannot find module")); + + assert.strictEqual( + await module.prefetch("./tests"), + null // Indicates no error. + ); + + return module.prefetch("./imports/prefetch").then(() => { + assert.deepEqual(shared, {}); + }).then(() => { + import { name } from "./imports/prefetch.js"; + assert.strictEqual(name, "/imports/prefetch.js"); + assert.deepEqual(shared, { [name]: true }); + }); + }); }); function maybeClearDynamicImportCache() {