UNPKG

habitjs

Version:

Modules and Dependency Injection for the browser and Node

716 lines (560 loc) 23.8 kB
(function () { 'use strict'; var _global = {}, requireRoot = '', hasRequire = false; if(typeof module === 'undefined') { _global = window; factory(_global, hasRequire, requireRoot); } else { _global = module.exports; hasRequire = true; requireRoot = require('path').dirname(require.main.filename); factory(_global, hasRequire, requireRoot); _global.mock = _global.Habit.mock.bind(_global.Habit); _global.unmock = _global.Habit.unmock.bind(_global.Habit); _global.unmockAll = _global.Habit.unmockAll.bind(_global.Habit); _global.disolve = _global.Habit.disolve.bind(_global.Habit); _global.disolveAll = _global.Habit.disolveAll.bind(_global.Habit); } function factory(_global, hasRequire, requireRoot) { //jshint ignore: line /** * If there is a prededfined configuration object for module data * then store it before overwriting it with the Habit library * @type {[type]} */ var confObject = _global.Habit || _global.require || {}; /** Fixjs root namespace; */ var Habit = _global.Habit = {}; /** * Assign module configuration data */ var configData = Habit.__configData = confObject.config || {}; /** * Habit name resolver. Given a module name resolves ./ and ../ * relative modile names. * For example in a module named 'my/awesome/module' a requirement named * './library' would resolve to the module 'my/awesome/library' * and a requirement named '../terribly/clever/class' would resolve to * 'my/terribly/clever/class' */ Habit.nameResolver = { resolveDependencyName: function (moduleName, dependencyName) { if (dependencyName[0] === '.') { return this.resolveName( this.splitName(moduleName), this.splitName(dependencyName)); } return dependencyName; }, splitName: function (name) { return name.split('/'); }, resolveName: function (moduleParts, dependencyParts) { moduleParts.pop(); while(dependencyParts[0] === '..') { if (moduleParts.length === 0 || dependencyParts.length === 0) { throw new HabitError('Illegal Relative Path'); } dependencyParts.shift(); moduleParts.pop(); } if(dependencyParts[0] === '.') { dependencyParts.shift(); } return this.completeName(moduleParts, dependencyParts); }, completeName: function(moduleParts, dependencyParts) { dependencyParts.forEach(function(part) { moduleParts.push(part); }); return moduleParts.join('/'); } }; /** * Class for errors thrown by Habit * @param {[type]} message [description] */ var HabitError = Habit.HabitError = function(message) { if (Error.captureStackTrace) { Error.captureStackTrace(this, Habit.HabitError); } this.message = message; }; HabitError.prototype = Object.create(Error.prototype) HabitError.prototype.constructor = HabitError; /** * Module. Manage state and resolution for individual modules * @param {String} name The name of the module * @param {[String]} dependencies An array of dependency names for the module * @param {Object/Number/String/Function} value The value of the module */ var Module = Habit.Module = function (name, dependencies, value) { this.__resolvedDependencyNames; this.__isResolved = false; this.__resolveDepth = 0; this.__dependentModules = []; this.__originalValue = value; Object.defineProperties(this, { name: { get: function () {return name;} }, dependencies: { get: function () {return this.__resolvedDependencyNames.slice();} }, dependentModules: { get: function () {return this.__dependentModules.slice();} }, originalValue: { get: function () {return this.__originalValue;} } }); this.__resolvedDependencyNames = this.resolveDependencyNames(dependencies); }; Module.prototype.resolveDependencyNames = function (dependencies) { return dependencies.map(function(dependency) { return Habit.nameResolver.resolveDependencyName(this.name, dependency); }.bind(this)); }; Module.prototype.resolve = function (context, dependent) { this.__addDependent(dependent); return this.__returnOrResolve(context); }; Module.prototype.__returnOrResolve = function (context) { if(this.__isMocked) { return this.__mockedValue; } if(!this.__isResolved) { this.__resolveValue(context); } return this.__resolvedValue; }; Module.prototype.__isObjectInstance = function () { // Limit this to instances constructed by Object because instances constructed by functions // should be returned directly return typeof this.originalValue === 'object' && this.originalValue.constructor === Object; }; Module.prototype.__isFactory = function () { return typeof this.originalValue === 'function'; }; /** * Resolves the modules supplied value to it's working value. * Numbers and Strings are returned directly. * Objects constructed with Object (and object literals) are shallow cloned * Objects constructed from other functions are returned directly * Functions are called with their dependencies as arguments and their * return value is set as the working value for the module * @param {Context} context The Context object used to resolve dependencies */ Module.prototype.__resolveValue = function (context) { if (this.__isFactory()) { this.__resolvedValue = this.__resolveFactory(context); } else if (this.__isObjectInstance() || Array.isArray(this.originalValue)) { this.__resolvedValue = this.__cloneValue(this.originalValue); } else { this.__resolvedValue = this.originalValue; } this.__isResolved = true; }; Module.prototype.__resolveFactory = function (context) { this.__resolveDepth += 1; if(this.__resolveDepth > 1) { throw new Habit.HabitError('Circular Dependency Detected in ' + this.name); } var resolved = this.originalValue.apply(undefined, this.__resolvedDependencyNames.map(function (dependency) { return context.resolveModule(dependency, this); }.bind(this))); this.__resolveDepth -= 1; return resolved; }; Module.prototype.__addDependent = function (dependent) { if(dependent) { this.__dependentModules.push(dependent); } }; /** * Remove the working value of this module AND all modules * which depend upon this module, forcing them to be re-resolved * next time they are required. */ Module.prototype.disolve = function () { this.__dependentModules.forEach(function (module) { module.disolve(); }); this.__isResolved = false; this.__resolvedValue = undefined; this.__dependentModules = []; }; /** * Disolve this module (see above) and then * replace the working value of this module with the value * provided. * @param {Any} mock The mock working value for this module */ Module.prototype.mock = function (mock) { this.disolve(); this.__isMocked = true; this.__mockedValue = mock; }; /** * Remove the mock working value for this module and then disolve it * (see above) * @return {[type]} [description] */ Module.prototype.unmock = function () { if(!this.__isMocked) { throw new Habit.HabitError('"' + this.name + '" is not mocked'); } this.__isMocked = false; this.__mockedValue = undefined; this.disolve(); }; /** * Create a clone of this module for use inside a dependency injection closure * @return {[type]} [description] */ Module.prototype.clone = function () { var clone = new Habit.Module(this.name, this.__resolvedDependencyNames, this.originalValue); clone.__isMocked = this.__isMocked; clone.__mockedValue = this.__mockedValue; return clone; }; /** * Disolve this module and then replace the * original value of this module with the override supplied * Differs from mocking in that it is the original value and * not the working value which is replaced, whch means it wil * be resolved next time the module is required, allowing factory * functions with module dependencies to be injected * @param {Any} override The injected value */ Module.prototype.inject = function (override) { this.disolve(); this.__originalValue = override; }; Module.prototype.__cloneValue = function (value) { if (Array.isArray(value)) { return this.__cloneArray(value); } if(typeof value === 'object') { return this.__cloneObject(value); } //value is primitive return value; }; Module.prototype.__cloneObject = function (obj) { var clone = {}; Object.keys(obj).forEach(function(key) { clone[key] = obj[key]; }); return clone; }; Module.prototype.__cloneArray = function (arr) { return arr.slice(); }; /** * The config module can be required by other habit modules * by declaring the 'module' dependency. Any configuration information * placed inside the configuration object will be made available to * the requiring module via the config() function */ var ConfigModule = Habit.ConfigModule = function () { Habit.Module.call(this, 'module', [], configData); }; ConfigModule.prototype = new Habit.Module('module', [], configData); ConfigModule.prototype.constructor = ConfigModule; ConfigModule.prototype.resolve = function (context, dependent) { if(!dependent) { return undefined; } if(configData[dependent.name]) { return { config: function () {return configData[dependent.name];} }; } return undefined; }; /** * Context. Manage the current module graph. */ var Context = Habit.Context = function () { this.__modules = {}; Object.defineProperty(this, 'modules', {get: function () {return this.__modules;}}); this.__addConfigModule(); }; /** * Add a module to the current module graph. * Called by the supply global function * @param {String} name The name of the module * @param {[String]} dependencies An array of dependency names for this module * @param {Any} callback The value for this module (usually a factory function) */ Context.prototype.addModule = function (name, dependencies, callback) { if (name === 'module') { throw new Habit.HabitError('The module name "module" is reserved'); } this.__addModule(name, dependencies, callback); }; Context.prototype.__addConfigModule = function () { this.__modules['module'] = new Habit.ConfigModule(); }; Context.prototype.__addModule = function (name, dependencies, callback) { if(this.__modules[name]) { this.__modules[name].disolve(); } this.__modules[name] = new Module(name, dependencies, callback); }; Context.prototype.__requireModule = function (path, name) { require(path); if(this.modules[name]) { this.modules[name].required = true; this.modules[name].filename = require.resolve(path); return true; } return false; }; Context.prototype.__tryNativeRequireToDefineModule = function (name) { if(hasRequire) { var path = requireRoot + '/' + name; try { return this.__requireModule(path, name); } catch (error) { return false; } } return false; }; Context.prototype.resolveModule = function (name, dependent) { if(!(!!this.__modules[name] || this.__tryNativeRequireToDefineModule(name))) { throw new Habit.HabitError('Module "' + name + '"is not defined'); } return this.__modules[name].resolve(this, dependent); }; Context.prototype.resolveAndCall = function(dependencies, callback) { callback.apply(undefined, dependencies.map(function(dependency) { return this.resolveModule(dependency); }.bind(this))); }; Context.prototype.__allModules = function () { return Object.keys(this.__modules) .map(function (key) { return this.__modules[key]; }.bind(this)); }; Context.prototype.disolve = function (name) { if(this.__modules[name]) { this.__modules[name].disolve(); } }; Context.prototype.disolveAll = function () { this.__allModules() .filter(function (module) { return module.__isResolved; }) .forEach(function (module) { module.disolve(); }); }; Context.prototype.mock = function (name, mock) { if(!(this.__modules[name] || this.__tryNativeRequireToDefineModule(name))) { throw new Habit.HabitError('Module "' + name + '" cannot be mocked because it does not exist'); } this.__modules[name].mock(mock); }; Context.prototype.inject = function (name, override) { if(!this.__modules[name]) { throw new Habit.HabitError('Module "' + name + '" cannot be injected because it does not exist'); } this.__modules[name].inject(override); }; Context.prototype.unmock = function (name) { if(!this.__modules[name]) { throw new Habit.HabitError('Module "' + name + '" cannot be unmocked because it does not exist'); } this.__modules[name].unmock(); }; Context.prototype.unmockAll = function () { this.__allModules() .filter(function (module) { return module.__isMocked; }) .forEach(function (module) { module.unmock(); }); }; // If these are loaded first in a DI closure they will never be available outside of it Context.prototype.__tryToEnsureDependenciesExist = function (dependencies) { dependencies.forEach(function (dependency) { if(dependency !== 'local') { this.resolveModule(dependency); } }.bind(this)); }; Context.prototype.cloneResolveAndCall = function (dependencies, callback, overrides) { this.__tryToEnsureDependenciesExist(dependencies); var clone = this.clone(); clone.injectOverrides(overrides); clone.resolveAndCall(dependencies, callback); }; Context.prototype.__addLocalModule = function (clone) { clone.addModule('local', [], function () { return new Habit.Local(clone); }); }; Context.prototype.clone = function () { var clone = new Habit.Context(); Object.keys(this.__modules).forEach(function (key) { if(key !== 'module') { clone.__modules[key] = this.__modules[key].clone(); } }.bind(this)); this.__addLocalModule(clone); return clone; }; Context.prototype.injectOverrides = function (overrides) { Object.keys(overrides).forEach(function (key) { this.inject(key, overrides[key]); }.bind(this)); }; /** * Local, facade for local context made available to injection closures * @param {Context} context The context for this closure */ var Local = Habit.Local = function (context) { this.__context = context; }; Local.prototype.need = function (dependencies, callback, overrides) { return Habit.__need.bind(this.__context)(dependencies, callback, overrides); }; Local.prototype.require = Local.prototype.need; Local.prototype.supply = function (name, dependencies, callback) { this.__context.addModule(name, dependencies, callback); }; Local.prototype.define = Local.prototype.supply; Local.prototype.mock = function (name, mock) { this.__context.mock(name, mock); }; Local.prototype.unmock = function (name) { this.__context.unmock(name); }; Local.prototype.disolve = function (name) { this.__context.disolve(name); }; Local.prototype.disolveAll = function () { this.__context.disolveAll(); }; Local.prototype.unmockAll = function () { this.__context.unmockAll(); }; /** * Create the default Habit context * @type {Context} */ Habit.__context = new Context(); /** * Supply is used to define modules * @param {String} name The name of the module * @param {[String]} dependencies An array of dependency names for the module * @param {Any} callback The value for the module (usually a factory function) */ Habit.supply = function (name, dependencies, callback) { if(arguments.length !== 3) { throw new Habit.HabitError('Supply requires three arguments'); } Habit.__context.addModule(name, dependencies, callback); }; /** * Export the supply function to the global namespace */ _global.supply = Habit.supply.bind(Habit); _global.define = Habit.supply.bind(Habit); /** * concrete implementation for both need and local need. * MUST be called bound to the correct context. i.e. * Fixjs.__need.bind(context)(dependencies, callback, overrides); * @param {[String]|String} dependencies An array of dependencies * or a single dependcy name if the value is to be returned directly * @param {Function} callback A callback function which * will be called with all the required dependencies * @param {Object} overrides A hash of overrides for * the closure callback. If this is provided then the * context will be cloned before resolution * @return {Any} If the function is called with a single string argument * then the named module will be returned */ Habit.__need = function(dependencies, callback, overrides) { //jshint ignore: line if(overrides) { this.cloneResolveAndCall(dependencies, callback, overrides); return; } if(!callback) { return this.resolveModule(dependencies); } this.resolveAndCall(dependencies, callback); }; Habit.need = function (dependencies, callback, overrides) { return Habit.__need.bind(Habit.__context)(dependencies, callback, overrides); }; _global.need = Habit.need.bind(Habit); _global.require = Habit.need.bind(Habit); /** * Reset the working value of an individual module * @param {String} name The name of the module to reset */ Habit.disolve = function (name) { this.__context.disolve(name); }; /** * Disolve all modules, forcing them to be re-resolved * next time they are required */ Habit.disolveAll = function () { this.__context.disolveAll(); }; /** * Completely reset habit. Only really useful for * isolating tests */ Habit.__clearRequireCache = function () { if(hasRequire) { Object.keys(this.__context.modules).forEach(function(key) { if(this.__context.modules[key].required) { delete require.cache[this.__context.modules[key].filename]; } }.bind(this)); } }; Habit.destroy = function () { this.__clearRequireCache(); this.__context = new Habit.Context(); }; /** * Mock a module. Also disolves all dependent modules * so that the mock will be pulled into any modules * depending on it * @param {String} name The module to mock * @param {Any} mock The value to mock it with */ Habit.mock = function (name, mock) { this.__context.mock(name, mock); }; /** * Unmock a mocked module * @param {String} name The name of the module to mock */ Habit.unmock = function (name) { this.__context.unmock(name); }; /** * Unmock all currently mocked modules * @return {[type]} [description] */ Habit.unmockAll = function () { this.__context.unmockAll(); }; Habit.setRequireRoot = function (path) { requireRoot = path; }; } })();