123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556 |
- 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;
- }
|