557 lines
19 KiB
JavaScript
557 lines
19 KiB
JavaScript
makeInstaller = function (options) {
|
|
"use strict";
|
|
|
|
options = options || {};
|
|
|
|
// These file extensions will be appended to required module identifiers
|
|
// if they do not exactly match an installed module.
|
|
var defaultExtensions = options.extensions || [".js", ".json"];
|
|
|
|
// If defined, the options.fallback function will be called when no
|
|
// installed module is found for a required module identifier. Often
|
|
// options.fallback will be implemented in terms of the native Node
|
|
// require function, which has the ability to load binary modules.
|
|
var fallback = options.fallback;
|
|
|
|
// List of fields to look for in package.json files to determine the
|
|
// main entry module of the package. The first field listed here whose
|
|
// value is a string will be used to resolve the entry module.
|
|
var mainFields = options.mainFields ||
|
|
// If options.mainFields is absent and options.browser is truthy,
|
|
// package resolution will prefer the "browser" field of package.json
|
|
// files to the "main" field. Note that this only supports
|
|
// string-valued "browser" fields for now, though in the future it
|
|
// might make sense to support the object version, a la browserify.
|
|
(options.browser ? ["browser", "main"] : ["main"]);
|
|
|
|
var hasOwn = {}.hasOwnProperty;
|
|
function strictHasOwn(obj, key) {
|
|
return isObject(obj) && isString(key) && hasOwn.call(obj, key);
|
|
}
|
|
|
|
// Cache for looking up File objects given absolute module identifiers.
|
|
// Invariants:
|
|
// filesByModuleId[module.id] === fileAppendId(root, module.id)
|
|
// filesByModuleId[module.id].module === module
|
|
var filesByModuleId = {};
|
|
|
|
// The file object representing the root directory of the installed
|
|
// module tree.
|
|
var root = new File("/", new File("/.."));
|
|
var rootRequire = makeRequire(root);
|
|
|
|
// Merges the given tree of directories and module factory functions
|
|
// into the tree of installed modules and returns a require function
|
|
// that behaves as if called from a module in the root directory.
|
|
function install(tree, options) {
|
|
if (isObject(tree)) {
|
|
fileMergeContents(root, tree, options);
|
|
}
|
|
return rootRequire;
|
|
}
|
|
|
|
// Replace this function to enable Module.prototype.prefetch.
|
|
install.fetch = function (ids) {
|
|
throw new Error("fetch not implemented");
|
|
};
|
|
|
|
// This constructor will be used to instantiate the module objects
|
|
// passed to module factory functions (i.e. the third argument after
|
|
// require and exports), and is exposed as install.Module in case the
|
|
// caller of makeInstaller wishes to modify Module.prototype.
|
|
function Module(id) {
|
|
this.id = id;
|
|
|
|
// The Node implementation of module.children unfortunately includes
|
|
// only those child modules that were imported for the first time by
|
|
// this parent module (i.e., child.parent === this).
|
|
this.children = [];
|
|
|
|
// This object is an install.js extension that includes all child
|
|
// modules imported by this module, even if this module is not the
|
|
// first to import them.
|
|
this.childrenById = {};
|
|
}
|
|
|
|
// Used to keep module.prefetch promise resolutions well-ordered.
|
|
var lastPrefetchPromise;
|
|
|
|
// May be shared by multiple sequential calls to module.prefetch.
|
|
// Initialized to {} only when necessary.
|
|
var missing;
|
|
|
|
Module.prototype.prefetch = function (id) {
|
|
var module = this;
|
|
var parentFile = getOwn(filesByModuleId, module.id);
|
|
|
|
lastPrefetchPromise = lastPrefetchPromise || Promise.resolve();
|
|
var previousPromise = lastPrefetchPromise;
|
|
|
|
function walk(module) {
|
|
var file = getOwn(filesByModuleId, module.id);
|
|
if (fileIsDynamic(file) && ! file.pending) {
|
|
file.pending = true;
|
|
missing = missing || {};
|
|
|
|
// These are the data that will be exposed to the install.fetch
|
|
// callback, so it's worth documenting each item with a comment.
|
|
missing[module.id] = {
|
|
// The CommonJS module object that will be exposed to this
|
|
// dynamic module when it is evaluated. Note that install.fetch
|
|
// could decide to populate module.exports directly, instead of
|
|
// fetching anything. In that case, install.fetch should omit
|
|
// this module from the tree that it produces.
|
|
module: file.module,
|
|
// List of module identifier strings imported by this module.
|
|
// Note that the missing object already contains all available
|
|
// dependencies (including transitive dependencies), so
|
|
// install.fetch should not need to traverse these dependencies
|
|
// in most cases; however, they may be useful for other reasons.
|
|
// Though the strings are unique, note that two different
|
|
// strings could resolve to the same module.
|
|
deps: Object.keys(file.deps),
|
|
// The options (if any) that were passed as the second argument
|
|
// to the install(tree, options) function when this stub was
|
|
// first registered. Typically contains options.extensions, but
|
|
// could contain any information appropriate for the entire tree
|
|
// as originally installed. These options will be automatically
|
|
// inherited by the newly fetched modules, so install.fetch
|
|
// should not need to modify them.
|
|
options: file.options,
|
|
// Any stub data included in the array notation from the
|
|
// original entry for this dynamic module. Typically contains
|
|
// "main" and/or "browser" fields for package.json files, and is
|
|
// otherwise undefined.
|
|
stub: file.stub
|
|
};
|
|
|
|
each(file.deps, function (parentId, id) {
|
|
fileResolve(file, id);
|
|
});
|
|
|
|
each(module.childrenById, walk);
|
|
}
|
|
}
|
|
|
|
return lastPrefetchPromise = new Promise(function (resolve) {
|
|
var absChildId = module.resolve(id);
|
|
each(module.childrenById, walk);
|
|
resolve(absChildId);
|
|
|
|
}).then(function (absChildId) {
|
|
// Grab the current missing object and fetch its contents.
|
|
var toBeFetched = missing;
|
|
missing = null;
|
|
|
|
function clearPending() {
|
|
if (toBeFetched) {
|
|
Object.keys(toBeFetched).forEach(function (id) {
|
|
getOwn(filesByModuleId, id).pending = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
return new Promise(function (resolve) {
|
|
// The install.fetch function takes an object mapping missing
|
|
// dynamic module identifiers to options objects, and should
|
|
// return a Promise that resolves to a module tree that can be
|
|
// installed. As an optimization, if there were no missing dynamic
|
|
// modules, then we can skip calling install.fetch entirely.
|
|
resolve(toBeFetched && install.fetch(toBeFetched));
|
|
|
|
}).then(function (tree) {
|
|
function both() {
|
|
install(tree);
|
|
clearPending();
|
|
return absChildId;
|
|
}
|
|
|
|
// Although we want multiple install.fetch calls to run in
|
|
// parallel, it is important that the promises returned by
|
|
// module.prefetch are resolved in the same order as the original
|
|
// calls to module.prefetch, because previous fetches may include
|
|
// modules assumed to exist by more recent module.prefetch calls.
|
|
// Whether previousPromise was resolved or rejected, carry on with
|
|
// the installation regardless.
|
|
return previousPromise.then(both, both);
|
|
|
|
}, function (error) {
|
|
// Fixes https://github.com/meteor/meteor/issues/10182.
|
|
clearPending();
|
|
throw error;
|
|
});
|
|
});
|
|
};
|
|
|
|
install.Module = Module;
|
|
|
|
function getOwn(obj, key) {
|
|
return strictHasOwn(obj, key) && obj[key];
|
|
}
|
|
|
|
function isObject(value) {
|
|
return value !== null && typeof value === "object";
|
|
}
|
|
|
|
function isFunction(value) {
|
|
return typeof value === "function";
|
|
}
|
|
|
|
function isString(value) {
|
|
return typeof value === "string";
|
|
}
|
|
|
|
function makeMissingError(id) {
|
|
return new Error("Cannot find module '" + id + "'");
|
|
}
|
|
|
|
Module.prototype.resolve = function (id) {
|
|
var file = fileResolve(filesByModuleId[this.id], id);
|
|
if (file) return file.module.id;
|
|
var error = makeMissingError(id);
|
|
if (fallback && isFunction(fallback.resolve)) {
|
|
return fallback.resolve(id, this.id, error);
|
|
}
|
|
throw error;
|
|
};
|
|
|
|
Module.prototype.require = function require(id) {
|
|
var result = fileResolve(filesByModuleId[this.id], id);
|
|
if (result) {
|
|
return fileEvaluate(result, this);
|
|
}
|
|
|
|
var error = makeMissingError(id);
|
|
|
|
if (isFunction(fallback)) {
|
|
return fallback(
|
|
id, // The missing module identifier.
|
|
this.id, // ID of the parent module.
|
|
error // The error we would have thrown.
|
|
);
|
|
}
|
|
|
|
throw error;
|
|
};
|
|
|
|
function makeRequire(file) {
|
|
var module = file.module;
|
|
|
|
function require(id) {
|
|
return module.require(id);
|
|
}
|
|
|
|
require.extensions = fileGetExtensions(file).slice(0);
|
|
|
|
require.resolve = function resolve(id) {
|
|
return module.resolve(id);
|
|
};
|
|
|
|
return require;
|
|
}
|
|
|
|
// File objects represent either directories or modules that have been
|
|
// installed. When a `File` respresents a directory, its `.contents`
|
|
// property is an object containing the names of the files (or
|
|
// directories) that it contains. When a `File` represents a module, its
|
|
// `.contents` property is a function that can be invoked with the
|
|
// appropriate `(require, exports, module)` arguments to evaluate the
|
|
// module. If the `.contents` property is a string, that string will be
|
|
// resolved as a module identifier, and the exports of the resulting
|
|
// module will provide the exports of the original file. The `.parent`
|
|
// property of a File is either a directory `File` or `null`. Note that
|
|
// a child may claim another `File` as its parent even if the parent
|
|
// does not have an entry for that child in its `.contents` object.
|
|
// This is important for implementing anonymous files, and preventing
|
|
// child modules from using `../relative/identifier` syntax to examine
|
|
// unrelated modules.
|
|
function File(moduleId, parent) {
|
|
var file = this;
|
|
|
|
// Link to the parent file.
|
|
file.parent = parent = parent || null;
|
|
|
|
// The module object for this File, which will eventually boast an
|
|
// .exports property when/if the file is evaluated.
|
|
file.module = new Module(moduleId);
|
|
filesByModuleId[moduleId] = file;
|
|
|
|
// The .contents of the file can be either (1) an object, if the file
|
|
// represents a directory containing other files; (2) a factory
|
|
// function, if the file represents a module that can be imported; (3)
|
|
// a string, if the file is an alias for another file; or (4) null, if
|
|
// the file's contents are not (yet) available.
|
|
file.contents = null;
|
|
|
|
// Set of module identifiers imported by this module. Note that this
|
|
// set is not necessarily complete, so don't rely on it unless you
|
|
// know what you're doing.
|
|
file.deps = {};
|
|
}
|
|
|
|
function fileEvaluate(file, parentModule) {
|
|
var module = file.module;
|
|
if (! strictHasOwn(module, "exports")) {
|
|
var contents = file.contents;
|
|
if (! contents) {
|
|
// If this file was installed with array notation, and the array
|
|
// contained one or more objects but no functions, then the combined
|
|
// properties of the objects are treated as a temporary stub for
|
|
// file.module.exports. This is particularly important for partial
|
|
// package.json modules, so that the resolution logic can know the
|
|
// value of the "main" and/or "browser" fields, at least, even if
|
|
// the rest of the package.json file is not (yet) available.
|
|
if (file.stub) {
|
|
return file.stub;
|
|
}
|
|
|
|
throw makeMissingError(module.id);
|
|
}
|
|
|
|
if (parentModule) {
|
|
module.parent = parentModule;
|
|
var children = parentModule.children;
|
|
if (Array.isArray(children)) {
|
|
children.push(module);
|
|
}
|
|
}
|
|
|
|
contents(
|
|
makeRequire(file),
|
|
// If the file had a .stub, reuse the same object for exports.
|
|
module.exports = file.stub || {},
|
|
module,
|
|
file.module.id,
|
|
file.parent.module.id
|
|
);
|
|
|
|
module.loaded = true;
|
|
}
|
|
|
|
// The module.runModuleSetters method will be deprecated in favor of
|
|
// just module.runSetters: https://github.com/benjamn/reify/pull/160
|
|
var runSetters = module.runSetters || module.runModuleSetters;
|
|
if (isFunction(runSetters)) {
|
|
runSetters.call(module);
|
|
}
|
|
|
|
return module.exports;
|
|
}
|
|
|
|
function fileIsDirectory(file) {
|
|
return file && isObject(file.contents);
|
|
}
|
|
|
|
function fileIsDynamic(file) {
|
|
return file && file.contents === null;
|
|
}
|
|
|
|
function fileMergeContents(file, contents, options) {
|
|
if (Array.isArray(contents)) {
|
|
contents.forEach(function (item) {
|
|
if (isString(item)) {
|
|
file.deps[item] = file.module.id;
|
|
} else if (isFunction(item)) {
|
|
contents = item;
|
|
} else if (isObject(item)) {
|
|
file.stub = file.stub || {};
|
|
each(item, function (value, key) {
|
|
file.stub[key] = value;
|
|
});
|
|
}
|
|
});
|
|
|
|
if (! isFunction(contents)) {
|
|
// If the array did not contain a function, merge nothing.
|
|
contents = null;
|
|
}
|
|
|
|
} else if (! isFunction(contents) &&
|
|
! isString(contents) &&
|
|
! isObject(contents)) {
|
|
// If contents is neither an array nor a function nor a string nor
|
|
// an object, just give up and merge nothing.
|
|
contents = null;
|
|
}
|
|
|
|
if (contents) {
|
|
file.contents = file.contents || (isObject(contents) ? {} : contents);
|
|
if (isObject(contents) && fileIsDirectory(file)) {
|
|
each(contents, function (value, key) {
|
|
if (key === "..") {
|
|
child = file.parent;
|
|
|
|
} else {
|
|
var child = getOwn(file.contents, key);
|
|
|
|
if (! child) {
|
|
child = file.contents[key] = new File(
|
|
file.module.id.replace(/\/*$/, "/") + key,
|
|
file
|
|
);
|
|
|
|
child.options = options;
|
|
}
|
|
}
|
|
|
|
fileMergeContents(child, value, options);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function each(obj, callback, context) {
|
|
Object.keys(obj).forEach(function (key) {
|
|
callback.call(this, obj[key], key);
|
|
}, context);
|
|
}
|
|
|
|
function fileGetExtensions(file) {
|
|
return file.options
|
|
&& file.options.extensions
|
|
|| defaultExtensions;
|
|
}
|
|
|
|
function fileAppendIdPart(file, part, extensions) {
|
|
// Always append relative to a directory.
|
|
while (file && ! fileIsDirectory(file)) {
|
|
file = file.parent;
|
|
}
|
|
|
|
if (! file || ! part || part === ".") {
|
|
return file;
|
|
}
|
|
|
|
if (part === "..") {
|
|
return file.parent;
|
|
}
|
|
|
|
var exactChild = getOwn(file.contents, part);
|
|
|
|
// Only consider multiple file extensions if this part is the last
|
|
// part of a module identifier and not equal to `.` or `..`, and there
|
|
// was no exact match or the exact match was a directory.
|
|
if (extensions && (! exactChild || fileIsDirectory(exactChild))) {
|
|
for (var e = 0; e < extensions.length; ++e) {
|
|
var child = getOwn(file.contents, part + extensions[e]);
|
|
if (child && ! fileIsDirectory(child)) {
|
|
return child;
|
|
}
|
|
}
|
|
}
|
|
|
|
return exactChild;
|
|
}
|
|
|
|
function fileAppendId(file, id, extensions) {
|
|
var parts = id.split("/");
|
|
|
|
// Use `Array.prototype.every` to terminate iteration early if
|
|
// `fileAppendIdPart` returns a falsy value.
|
|
parts.every(function (part, i) {
|
|
return file = i < parts.length - 1
|
|
? fileAppendIdPart(file, part)
|
|
: fileAppendIdPart(file, part, extensions);
|
|
});
|
|
|
|
return file;
|
|
}
|
|
|
|
function recordChild(parentModule, childFile) {
|
|
var childModule = childFile && childFile.module;
|
|
if (parentModule && childModule) {
|
|
parentModule.childrenById[childModule.id] = childModule;
|
|
}
|
|
}
|
|
|
|
function fileResolve(file, id, parentModule, seenDirFiles) {
|
|
var parentModule = parentModule || file.module;
|
|
var extensions = fileGetExtensions(file);
|
|
|
|
file =
|
|
// Absolute module identifiers (i.e. those that begin with a `/`
|
|
// character) are interpreted relative to the root directory, which
|
|
// is a slight deviation from Node, which has access to the entire
|
|
// file system.
|
|
id.charAt(0) === "/" ? fileAppendId(root, id, extensions) :
|
|
// Relative module identifiers are interpreted relative to the
|
|
// current file, naturally.
|
|
id.charAt(0) === "." ? fileAppendId(file, id, extensions) :
|
|
// Top-level module identifiers are interpreted as referring to
|
|
// packages in `node_modules` directories.
|
|
nodeModulesLookup(file, id, extensions);
|
|
|
|
// If the identifier resolves to a directory, we use the same logic as
|
|
// Node to find an `index.js` or `package.json` file to evaluate.
|
|
while (fileIsDirectory(file)) {
|
|
seenDirFiles = seenDirFiles || [];
|
|
|
|
// If the "main" field of a `package.json` file resolves to a
|
|
// directory we've already considered, then we should not attempt to
|
|
// read the same `package.json` file again. Using an array as a set
|
|
// is acceptable here because the number of directories to consider
|
|
// is rarely greater than 1 or 2. Also, using indexOf allows us to
|
|
// store File objects instead of strings.
|
|
if (seenDirFiles.indexOf(file) < 0) {
|
|
seenDirFiles.push(file);
|
|
|
|
var pkgJsonFile = fileAppendIdPart(file, "package.json");
|
|
var pkg = pkgJsonFile && fileEvaluate(pkgJsonFile, parentModule);
|
|
var mainFile, resolved = pkg && mainFields.some(function (name) {
|
|
var main = pkg[name];
|
|
if (isString(main)) {
|
|
// The "main" field of package.json does not have to begin
|
|
// with ./ to be considered relative, so first we try
|
|
// simply appending it to the directory path before
|
|
// falling back to a full fileResolve, which might return
|
|
// a package from a node_modules directory.
|
|
return mainFile = fileAppendId(file, main, extensions) ||
|
|
fileResolve(file, main, parentModule, seenDirFiles);
|
|
}
|
|
});
|
|
|
|
if (resolved && mainFile) {
|
|
file = mainFile;
|
|
recordChild(parentModule, pkgJsonFile);
|
|
// The fileAppendId call above may have returned a directory,
|
|
// so continue the loop to make sure we resolve it to a
|
|
// non-directory file.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// If we didn't find a `package.json` file, or it didn't have a
|
|
// resolvable `.main` property, the only possibility left to
|
|
// consider is that this directory contains an `index.js` module.
|
|
// This assignment almost always terminates the while loop, because
|
|
// there's very little chance `fileIsDirectory(file)` will be true
|
|
// for `fileAppendIdPart(file, "index", extensions)`. However, in
|
|
// principle it is remotely possible that a file called `index.js`
|
|
// could be a directory instead of a file.
|
|
file = fileAppendIdPart(file, "index", extensions);
|
|
}
|
|
|
|
if (file && isString(file.contents)) {
|
|
file = fileResolve(file, file.contents, parentModule, seenDirFiles);
|
|
}
|
|
|
|
recordChild(parentModule, file);
|
|
|
|
return file;
|
|
};
|
|
|
|
function nodeModulesLookup(file, id, extensions) {
|
|
for (var resolved; file && ! resolved; file = file.parent) {
|
|
resolved = fileIsDirectory(file) &&
|
|
fileAppendId(file, "node_modules/" + id, extensions);
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
return install;
|
|
};
|
|
|
|
if (typeof exports === "object") {
|
|
exports.makeInstaller = makeInstaller;
|
|
}
|