From 5477aeac3fc1b2fd67226dc271c006895d4eedfe Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Mon, 24 Apr 2017 15:49:47 -0400 Subject: [PATCH] Support module.prefetch(id) to fetch but not evaluate dynamic modules. Generally, module.prefetch(id) will not throw even if the fetched module is missing. If you need to know whether module.prefetch(id) succeeded, simply await the result of the promise, which will be null on success, or an Error object if the module could not be imported. --- packages/dynamic-import/client.js | 93 ++++++++++++------- packages/modules-runtime/meteor-install.js | 12 ++- .../dynamic-import/imports/prefetch-child.js | 1 + .../apps/dynamic-import/imports/prefetch.js | 3 + tools/tests/apps/dynamic-import/tests.js | 22 +++++ 5 files changed, 94 insertions(+), 37 deletions(-) create mode 100644 tools/tests/apps/dynamic-import/imports/prefetch-child.js create mode 100644 tools/tests/apps/dynamic-import/imports/prefetch.js 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() {