habitjs
Version:
Modules and Dependency Injection for the browser and Node
716 lines (560 loc) • 23.8 kB
JavaScript
(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;
};
}
})();