UNPKG

lasso-modules-client

Version:
524 lines (425 loc) 17.4 kB
/* GOAL: This module should mirror the NodeJS module system according the documented behavior. The module transport will send down code that registers module definitions by an assigned path. In addition, the module transport will send down code that registers additional metadata to allow the module resolver to resolve modules in the browser. Additional metadata includes the following: - "mains": The mapping of module directory paths to a fully resolved module path - "remaps": The remapping of one fully resolved module path to another fully resolved module path (used for browser overrides) - "run": A list of entry point modules that should be executed when ready Inspired by: https://github.com/joyent/node/blob/master/lib/module.js */ (function() { var win; if (typeof window !== 'undefined') { win = window; // This lasso modules client has already been loaded on the page. Do nothing; if (win.$_mod) { return; } win.global = win; } /** the module runtime */ var $_mod; // this object stores the module factories with the keys being module paths and // values being a factory function or object (e.g. "/baz$3.0.0/lib/index" --> Function) var definitions = Object.create(null); // Search path that will be checked when looking for modules var searchPaths = []; // The _ready flag is used to determine if "run" modules can // be executed or if they should be deferred until all dependencies // have been loaded var _ready = false; // If $_mod.run() is called when the page is not ready then // we queue up the run modules to be executed later var runQueue = []; // this object stores the Module instance cache with the keys being paths of modules (e.g., "/foo$1.0.0/bar" --> Module) var instanceCache = Object.create(null); // This object maps installed dependencies to specific versions // // For example: // { // // The package "foo" with version 1.0.0 has an installed package named "bar" (foo/node_modules/bar") and // // the version of "bar" is 3.0.0 // "/foo$1.0.0/bar": "3.0.0" // } var installed = Object.create(null); // Maps builtin modules such as "path", "buffer" to their fully resolved paths var builtins = Object.create(null); // this object maps a directory to the fully resolved module path // // For example: // var mains = Object.create(null); // used to remap a one fully resolved module path to another fully resolved module path var remapped = Object.create(null); function moduleNotFoundError(target, from) { var err = new Error('Cannot find module "' + target + '"' + (from ? ' from "' + from + '"' : '')); err.code = 'MODULE_NOT_FOUND'; return err; } function Module(filename) { /* A Node module has these properties: - filename: The path of the module - id: The path of the module (same as filename) - exports: The exports provided during load - loaded: Has module been fully loaded (set to false until factory function returns) NOT SUPPORTED: - parent: parent Module - paths: The search path used by this module (NOTE: not documented in Node.js module system so we don't need support) - children: The modules that were required by this module */ this.id = this.filename = filename; this.loaded = false; this.exports = undefined; } Module.cache = instanceCache; // temporary variable for referencing the Module prototype var Module_prototype = Module.prototype; Module_prototype.load = function(factoryOrObject) { var filename = this.id; if (typeof factoryOrObject === "function") { // find the value for the __dirname parameter to factory var dirname = filename.slice(0, filename.lastIndexOf('/')); // this is the require used by the module var instanceRequire = function(target) { return require(target, dirname); }; // The require method should have a resolve method that will return the resolved // path but not actually instantiate the module. // This resolve function will make sure a definition exists for the corresponding // path of the target but it will not instantiate a new instance of the target. instanceRequire.resolve = function(target) { if (!target) { throw moduleNotFoundError(''); } var resolved = resolve(target, dirname); if (resolved === undefined) { throw moduleNotFoundError(target, dirname); } return resolved; }; // NodeJS provides access to the cache as a property of the "require" function instanceRequire.cache = instanceCache; // Expose the module system runtime via the `runtime` property // TODO: We should deprecate this in favor of `Module.prototype.__runtime` // @deprecated instanceRequire.runtime = $_mod; // $_mod.def("/foo$1.0.0/lib/index", function(require, exports, module, __filename, __dirname) { this.exports = {}; // call the factory function factoryOrObject(instanceRequire, this.exports, this, filename, dirname); } else { // factoryOrObject is not a function so have exports reference factoryOrObject this.exports = factoryOrObject; } this.loaded = true; }; /** * Defines a packages whose metadata is used by raptor-loader to load the package. */ function define(path, factoryOrObject, options) { /* $_mod.def('/baz$3.0.0/lib/index', function(require, exports, module, __filename, __dirname) { // module source code goes here }); */ var globals = options && options.globals; definitions[path] = factoryOrObject; if (globals) { var target = win || global; var globalMod = require(path, "/"); for (var i=0;i<globals.length; i++) { target[globals[i]] = globalMod; } } } function registerMain(path, relativePath) { mains[path] = relativePath; } function remap(fromPath, toPath) { remapped[fromPath] = toPath; } function builtin(name, target) { builtins[name] = target; } function registerInstalledDependency(parentPath, packageName, packageVersion) { // Example: // dependencies['/my-package$1.0.0/$/my-installed-package'] = '2.0.0' installed[parentPath + '/' + packageName] = packageVersion; } function join(from, target) { var fromLen = from.length; var fromLastIndex = fromLen; var targetStartIndex = 0; var char; while ((char = target[targetStartIndex]) === ".") { targetStartIndex++; if ((char = target[targetStartIndex]) === ".") { targetStartIndex++; if (fromLastIndex) { fromLastIndex = from.lastIndexOf("/", fromLastIndex - 1); if (fromLastIndex === -1) { fromLastIndex = 0; } } } if ((char = target[targetStartIndex]) === "/") { targetStartIndex++; } else { break; } } if (char) { if (fromLastIndex) { return from.slice(0, fromLastIndex) + "/" + target.slice(targetStartIndex); } return target.slice(targetStartIndex); } if (fromLastIndex) { return fromLastIndex === fromLen ? from : from.slice(0, fromLastIndex); } return from[0] === "/" ? "/" : "."; } function withoutExtension(path) { var lastDotPos = path.lastIndexOf('.'); var lastSlashPos; /* jshint laxbreak:true */ return ((lastDotPos === -1) || ((lastSlashPos = path.lastIndexOf('/')) !== -1) && (lastSlashPos > lastDotPos)) ? undefined // use undefined to indicate that returned path is same as given path : path.substring(0, lastDotPos); } function splitPackageIdAndSubpath(path) { path = path.substring(1); /* Skip past the first slash */ // Examples: // '/my-package$1.0.0/foo/bar' --> ['my-package$1.0.0', '/foo/bar'] // '/my-package$1.0.0' --> ['my-package$1.0.0', ''] // '/my-package$1.0.0/' --> ['my-package$1.0.0', '/'] // '/@my-scoped-package/foo/$1.0.0/' --> ['@my-scoped-package/foo$1.0.0', '/'] var slashPos = path.indexOf('/'); if (path[1] === '@') { // path is something like "/@my-user-name/my-scoped-package/subpath" // For scoped packages, the package name is two parts. We need to skip // past the second slash to get the full package name slashPos = path.indexOf('/', slashPos+1); } var packageIdEnd = slashPos === -1 ? path.length : slashPos; return [ path.substring(0, packageIdEnd), // Everything up to the slash path.substring(packageIdEnd) // Everything after the package ID ]; } function resolveInstalledModule(target, from) { // Examples: // target='foo', from='/my-package$1.0.0/hello/world' if (target[target.length-1] === '/') { // This is a hack because I found require('util/') in the wild and // it did not work because of the trailing slash target = target.slice(0, -1); } // Check to see if the target module is a builtin module. // For example: // builtins['path'] = '/path-browserify$0.0.0/index' var builtinPath = builtins[target]; if (builtinPath) { return builtinPath; } var fromParts = splitPackageIdAndSubpath(from); var fromPackageId = fromParts[0]; var targetSlashPos = target.indexOf('/'); var targetPackageName; var targetSubpath; if (targetSlashPos < 0) { targetPackageName = target; targetSubpath = ''; } else { if (target[0] === '@') { // target is something like "@my-user-name/my-scoped-package/subpath" // For scoped packages, the package name is two parts. We need to skip // past the first slash to get the full package name targetSlashPos = target.indexOf('/', targetSlashPos + 1); } targetPackageName = target.substring(0, targetSlashPos); targetSubpath = target.substring(targetSlashPos); } var targetPackageVersion = installed[fromPackageId + '/' + targetPackageName]; if (targetPackageVersion) { var resolvedPath = '/' + targetPackageName + '$' + targetPackageVersion; if (targetSubpath) { resolvedPath += targetSubpath; } return resolvedPath; } } function resolve(target, from) { var resolvedPath; if (target[0] === '/') { // handle targets such as "/my/file" or "/$/foo/$/baz" resolvedPath = target; } else if (target[0] === '.') { // turn relative path into absolute path resolvedPath = join(from, target); } else { var len = searchPaths.length; for (var i = 0; i < len; i++) { // search path entries always end in "/"; var candidate = searchPaths[i] + target; var resolved = resolve(candidate, from); if (resolved) { return resolved; } } resolvedPath = resolveInstalledModule(target, from); } if (!resolvedPath) { return undefined; } // target is something like "/foo/baz" // There is no installed module in the path var relativePath = mains[resolvedPath]; // check to see if "target" is a "directory" which has a registered main file if (relativePath !== undefined) { // there is a main file corresponding to the given target so add the relative path resolvedPath = join(resolvedPath, relativePath || 'index'); } var remappedPath = remapped[resolvedPath]; if (remappedPath) { resolvedPath = remappedPath; } if (definitions[resolvedPath] === undefined) { // check for definition for given path but without extension resolvedPath = withoutExtension(resolvedPath); if (resolvedPath !== undefined && definitions[resolvedPath] === undefined) { resolvedPath = undefined; } } return resolvedPath; } function requireModule(target, from) { if (!target) { throw moduleNotFoundError(''); } var resolvedPath = resolve(target, from); if (resolvedPath === undefined) { throw moduleNotFoundError(target, from); } var module = instanceCache[resolvedPath]; if (module === undefined) { // cache the instance before loading (allows support for circular dependency with partial loading) module = instanceCache[resolvedPath] = new Module(resolvedPath); module.load(definitions[resolvedPath]); } return module; } function require(target, from) { return requireModule(target, from).exports; } /* $_mod.run('/$/installed-module', '/src/foo'); */ function run(path, options) { var wait = !options || (options.wait !== false); if (wait && !_ready) { return runQueue.push([path, options]); } requireModule(path, '/'); } /* * Mark the page as being ready and execute any of the * run modules that were deferred */ function ready() { _ready = true; var len; while((len = runQueue.length)) { // store a reference to the queue before we reset it var queue = runQueue; // clear out the queue runQueue = []; // run all of the current jobs for (var i = 0; i < len; i++) { var args = queue[i]; run(args[0], args[1]); } // stop running jobs in the queue if we change to not ready if (!_ready) { break; } } } function addSearchPath(prefix) { searchPaths.push(prefix); } var pendingCount = 0; var onPendingComplete = function() { pendingCount--; if (!pendingCount) { // Trigger any "require-run" modules in the queue to run ready(); } }; /* * $_mod is the short-hand version that that the transport layer expects * to be in the browser window object */ Module_prototype.__runtime = $_mod = { /** * Used to register a module factory/object (*internal*) */ def: define, /** * Used to register an installed dependency (e.g. "/$/foo" depends on "baz") (*internal*) */ installed: registerInstalledDependency, run: run, main: registerMain, remap: remap, builtin: builtin, require: require, resolve: function (target, from) { var resolved = resolve(target, from); if (resolved !== undefined) { return [resolved, definitions[resolved]]; } }, join: join, ready: ready, /** * Add a search path entry (internal) */ searchPath: addSearchPath, /** * Sets the loader metadata for this build. * * @param asyncPackageName {String} name of asynchronous package * @param contentType {String} content type ("js" or "css") * @param bundleUrl {String} URL of bundle that belongs to package */ loaderMetadata: function(data) { // We store loader metadata in the prototype of Module // so that `lasso-loader` can read it from // `module.__loaderMetadata`. Module_prototype.__loaderMetadata = data; }, /** * Asynchronous bundle loaders should call `pending()` to instantiate * a new job. The object we return here has a `done` method that * should be called when the job completes. When the number of * pending jobs drops to 0, we invoke any of the require-run modules * that have been declared. */ pending: function() { _ready = false; pendingCount++; return { done: onPendingComplete }; } }; if (win) { win.$_mod = $_mod; } else { module.exports = $_mod; } })();