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.
This commit is contained in:
Ben Newman
2017-04-24 15:49:47 -04:00
parent f4fc2a904d
commit 5477aeac3f
5 changed files with 94 additions and 37 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -0,0 +1 @@
export const shared = Object.create(null);

View File

@@ -0,0 +1,3 @@
import { shared } from "./prefetch-child.js";
export const name = module.id;
shared[name] = true;

View File

@@ -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() {