lasso-modules-client
Version:
The client-side CommonJS runtime for Lasso.js
524 lines (425 loc) • 17.4 kB
JavaScript
/*
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;
}
})();