UNPKG

kitchensink

Version:

Dispatch's awesome components and style guide

385 lines (326 loc) 13.6 kB
makeInstaller = function (options) { options = options || {}; // These file extensions will be appended to required module identifiers // if they do not exactly match an installed module. var extensions = options.extensions || [".js", ".json"]; // This constructor will be used to instantiate the module objects // passed to module factory functions (i.e. the third argument after // require and exports). var Module = options.Module || function Module(id, parent) { this.id = id; this.parent = parent; }; // If defined, the options.onInstall function will be called any time // new modules are installed. var onInstall = options.onInstall; // 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; // Whenever a new require function is created in the makeRequire // function below, any methods contained by options.requireMethods will // be bound and attached as methods to that function object. This option // is intended to support user-defined require.* extensions like // require.ensure and require.promise. var requireMethods = options.requireMethods; // Sentinel returned by fileEvaluate when module resolution fails. var MISSING = {}; // Nothing special about MISSING.hasOwnProperty, except that it's fewer // characters than Object.prototype.hasOwnProperty after minification. var hasOwn = MISSING.hasOwnProperty; // The file object representing the root directory of the installed // module tree. var root = new File({}); // 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) { if (isObject(tree)) { fileMergeContents(root, tree); if (isFunction(onInstall)) { onInstall(root.r); } } return root.r; } function getOwn(obj, key) { return hasOwn.call(obj, key) && obj[key]; } function isObject(value) { return value && typeof value === "object"; } function isFunction(value) { return typeof value === "function"; } function isString(value) { return typeof value === "string"; } function makeRequire(file) { function require(id) { var result = fileEvaluate(fileResolve(file, id)); if (result === MISSING) { var error = new Error("Cannot find module '" + id + "'"); if (isFunction(fallback)) { result = fallback( id, // The missing module identifier. file.m.id, // The path of the enclosing directory. error // The error we would have thrown. ); } else throw error; } return result; } // A function that immediately returns true iff all the transitive // dependencies of the module identified by id have been installed. // This function can be used with options.onInstall to implement // asynchronous module loading APIs like require.ensure. require.ready = function (id) { return fileReady(fileResolve(file, id)); }; if (requireMethods) { Object.keys(requireMethods).forEach(function (name) { if (isFunction(requireMethods[name])) { require[name] = requireMethods[name].bind(require); } }); } return require; } // File objects represent either directories or modules that have been // installed. When a `File` respresents a directory, its `.c` (contents) // property is an object containing the names of the files (or // directories) that it contains. When a `File` represents a module, its // `.c` property is a function that can be invoked with the appropriate // `(require, exports, module)` arguments to evaluate the module. If the // `.c` 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 `.p` (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 `.c` object. This is important for // implementing anonymous files, and preventing child modules from using // `../relative/identifier` syntax to examine unrelated modules. function File(contents, /*optional:*/ parent, name) { var file = this; // Link to the parent file. file.p = parent = parent || null; // The module object for this File, which will eventually boast an // .exports property when/if the file is evaluated. file.m = new Module( // If this file was created with `name`, join it with `parent.m.id` // to generate a module identifier. parent && name ? parent.m.id + "/" + name : "", parent && parent.m ); // Queue for tracking required modules with unmet dependencies, // inherited from the `parent`. file.q = parent && parent.q; // Each directory has its own bound version of the `require` function // that can resolve relative identifiers. Non-directory Files inherit // the require function of their parent directories, so we don't have // to create a new require function every time we evaluate a module. file.r = isObject(contents) ? makeRequire(file) : parent && parent.r; // Set the initial value of `file.c` (the "contents" of the File). fileMergeContents(file, contents); // When the file is a directory, `file.rc` is an object mapping module // identifiers to boolean ready statuses ("rc" is short for "ready // cache"). This information can be shared by all files in the // directory, because module resolution always has the same results // for all files in a given directory. file.rc = fileIsDirectory(file) && {}; } // A file is ready if all of its dependencies are installed and ready. function fileReady(file) { var result = !! file; var contents = file && file.c; if (contents && ! file.inReady) { file.inReady = true; if (isString(contents)) { // This file is aliased (or symbolically linked) to the file // obtained by resolving the contents string as a module // identifier, so regard it as ready iff the resolved file exists // and is ready. result = fileReady(fileResolve(file, contents)); } else if (isFunction(contents)) { var deps = contents.d; if (deps) { var parentReadyCache = file.p.rc; result = deps.every(function (dep) { // By storing the results of these lookups in `parentReadyCache`, // we benefit when any other file in the same directory resolves // the same identifier. return parentReadyCache[dep] = parentReadyCache[dep] || fileReady(fileResolve(file.p, dep)); }); } } file.inReady = false; } return result; } function fileEvaluate(file) { var contents = file && file.c; if (isFunction(contents)) { var module = file.m; if (! hasOwn.call(module, "exports")) { contents(file.r, module.exports = {}, module, file.m.id, file.p.m.id || "/"); } return module.exports; } return MISSING; } function fileIsDirectory(file) { return file && isObject(file.c); } function fileMergeContents(file, contents) { // If contents is an array of strings and functions, return the last // function with a `.d` property containing all the strings. if (Array.isArray(contents)) { var deps = []; contents.forEach(function (item) { if (isString(item)) { deps.push(item); } else if (isFunction(item)) { contents = item; } }); if (isFunction(contents)) { contents.d = deps; } else { // If the array did not contain a function, merge nothing. contents = null; } } else if (isFunction(contents)) { // If contents is already a function, make sure it has `.d`. contents.d = contents.d || []; } else if (! 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) { var fileContents = file.c = file.c || ( isObject(contents) ? {} : contents ); if (isObject(contents) && fileIsDirectory(file)) { Object.keys(contents).forEach(function (key) { var child = getOwn(fileContents, key); if (child) { fileMergeContents(child, contents[key]); } else { fileContents[key] = new File(contents[key], file, key); } }); } } } function fileAppendIdPart(file, part, isLastPart) { // Always append relative to a directory. while (file && ! fileIsDirectory(file)) { file = file.p; } if (! file || ! part || part === ".") { return file; } if (part === "..") { return file.p; } var exactChild = getOwn(file.c, 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 (isLastPart && (! exactChild || fileIsDirectory(exactChild))) { for (var e = 0; e < extensions.length; ++e) { var child = getOwn(file.c, part + extensions[e]); if (child) { return child; } } } return exactChild; } function fileAppendId(file, id) { 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 = fileAppendIdPart(file, part, i === parts.length - 1); }); return file; } function fileResolve(file, id, seenDirFiles) { 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) : // Relative module identifiers are interpreted relative to the // current file, naturally. id.charAt(0) === "." ? fileAppendId(file, id) : // Top-level module identifiers are interpreted as referring to // packages in `node_modules` directories. nodeModulesLookup(file, id); // 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); // If `package.json` does not exist, `fileEvaluate` will return // the `MISSING` object, which has no `.main` property. var pkg = fileEvaluate(fileAppendIdPart(file, "package.json")); if (pkg && isString(pkg.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. file = fileAppendId(file, pkg.main) || fileResolve(file, pkg.main, seenDirFiles); if (file) { // 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 the result of `fileAppendIdPart(file, "index.js")`. 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.js"); } if (file && isString(file.c)) { file = fileResolve(file, file.c, seenDirFiles); } return file; }; function nodeModulesLookup(file, id) { return fileIsDirectory(file) && fileAppendId(file, "node_modules/" + id) || (file.p && nodeModulesLookup(file.p, id)); } return install; }; if (typeof exports === "object") { exports.makeInstaller = makeInstaller; }