darkmagic
Version:
A dependency injection framework
433 lines (330 loc) • 11.3 kB
JavaScript
var debug = require('debug')('darkmagic_Injector')
var Waterfall = require('./Waterfall.js')
var Dependency = require('./Dependency.js')
var path = require('path')
var fs = require('fs')
var esprima = require('esprima')
var util = require('util')
var assert = require('assert')
var EventEmitter = require('events').EventEmitter
var util = require('util')
module.exports = Injector
/* words that cannot be used as parameters */
var ILLEGAL = [ 'toString' ]
util.inherits(Injector, EventEmitter)
function Injector(options) {
EventEmitter.call(this)
options = options || {}
this._customCache = {}
this.explicitRealModule = options.explicitRealModule
var realModule = this._getRealModule()
this._initSearchPaths(path.dirname(realModule.filename))
var injectorDependency = this.newDependencyObject('$injector')
injectorDependency.object = this
this._cacheDependency(injectorDependency, this)
this.autoInjectLocalFactories = options.autoInjectLocalFactories === undefined ? true : options.autoInjectLocalFactories
this.autoInjectExternalFactories = options.autoInjectExternalFactories === undefined ? false: options.autoInjectExternalFactories
}
Injector.prototype.inject = function(target, overrides, callback) {
if (typeof (overrides) === 'function') {
callback = overrides
overrides = undefined
}
if (typeof (overrides) === 'object') {
try {
this.addOverrides(overrides)
} catch (e) {
if (callback) return callback(e)
throw e
}
}
var realModule = this._getRealModule()
var params
// actuation instead of inject, in this scenario we dont finish by calling target
if (util.isArray(target)) {
debug('target is an array')
params = []
for (var i = 0; i < target.length; i++) {
params.push({ name: target[i] })
}
target = actuation
}
if (typeof target !== 'function') {
throw new Error('can only inject functions')
}
params = params || this._getFunctionParameters(target)
var dependency = new Dependency(target.name || 'anonymous')
this._inject(target, params, realModule, dependency, [], this._callbackOrThrow(callback))
}
Injector.prototype._inject = function(target, params, realModule, targetDependency, ancestors, callback) {
debug('_inject "%s" (%s)', targetDependency.name, typeof target)
debug('"%s" has %d params', targetDependency.name, params.length)
if (typeof target !== 'function')
throw new Error('can only inject functions')
// if there are no parameters take the short path
if (params.length === 0) {
invokeTarget(this, target, targetDependency, false, callback)()
return
}
var _hasCallbackParam = hasCallbackParam(params)
if (_hasCallbackParam)
params.pop()
var args = []
var order = []
var waterfall = new Waterfall(args)
// resolve params
for (var i = 0; i < params.length; i++) {
var dependencyName = params[i].name
if (ILLEGAL.indexOf(dependencyName) > -1)
throw new Error('illegal parameter name ' + dependency)
var dependency = this.getDependencyByName(dependencyName)
var exists = dependency !== undefined
if (exists) {
var artifact
try {
artifact = dependency.load(realModule, targetDependency)
} catch(e) {
return callback(e)
}
args.push(artifact)
debug('dependency "%s" exists', dependencyName)
} else {
debug('dependency "%s" is new', dependencyName)
dependency = this.newDependencyObject(dependencyName)
order.push(i)
args.push(injectFunctor(this, realModule, ancestors, dependency, targetDependency))
}
}
debug('running waterfall [%s]', order)
// after all the dependencies have been resolved, invoke the current dependency
waterfall.run(order, invokeTarget(this, target, targetDependency, _hasCallbackParam, callback))
}
function injectFunctor(injector, realModule, ancestors, dependency, parentDependency) {
return function (callback) {
var dependencyName = dependency.name
debug('injectFunctor("%s" => "%s")', dependencyName, parentDependency.name)
var artifact
try {
artifact = dependency.load(realModule, parentDependency)
} catch (e) {
return callback(e)
}
// missing dependency?
if (!artifact) {
if (dependency.isOptional) callback()
else callback(
new Error(
util.format('"%s%s" is Missing dependency "%s"',
parentDependency.name,
parentDependency.isFactory ? '(...)' : '', dependencyName)))
return
}
if (dependency.isInjectable()) {
debug('dependency "%s" is injectable', dependencyName)
// circular dependencyName
debug('"%s" ancestors: [%s]', dependencyName, ancestors)
if (ancestors && ancestors.indexOf(dependencyName) > -1) {
callback(new Error(
util.format('circular dependency detected between "%s" and "%s", dependency chain was: "%s"',
dependencyName, parentDependency.name, util.inspect(ancestors))))
} else {
ancestors = ancestors || []
ancestors.push(dependencyName)
}
var params = injector._getFunctionParameters(artifact)
injector._inject(artifact, params, realModule, dependency, ancestors, callback)
} else {
debug('dependency "%s" not injectable', dependencyName)
injector._cacheDependency(dependency, artifact)
callback(null, artifact)
}
}
}
function invokeTarget(injector, target, dependency, hasCallbackParam, callback) {
return function invokeTargetFunctor(err, results) {
debug('invoking "%s", hasCallbackParam: %s', dependency.name, hasCallbackParam)
var resolve = resolveDependencyCallback(injector, dependency, callback)
if (err)
return resolve(err);
// invoke the artifact
if (hasCallbackParam) {
results.push(resolve)
target.apply(null, results)
} else {
try {
var returnValue = target.apply(null, results)
resolve(null, returnValue)
} catch (e) {
resolve(e)
}
}
}
}
function resolveDependencyCallback(injector, dependency, next) {
return function resolveFunctor(err, result) {
debug('resolved "%s"', dependency.name)
// not sure this is the right thing to do ...
if (err) {
return next(err)
}
if (dependency.isInjectable() && result) {
// this dependency is a factory that resolved successfully,
// save the results of the invocation for next time.
dependency.isFactory = false
injector._cacheDependency(dependency, result)
}
next(null, result)
}
}
function hasCallbackParam(params) {
return params[params.length - 1].name === 'callback'
}
Injector.prototype.addDependency = function (dependency) {
if (dependency.name === '$injector') {
throw new Error('cannot add an injector dependency')
}
debug('addDependency() "%s"', dependency.name)
this._cacheDependency(dependency, dependency.load(this._getRealModule(), null))
}
Injector.prototype._cacheDependency = function (dependency, artifact) {
if (!this.getDependencyByName(dependency.name)) {
this.emit('new dependency', dependency, artifact)
}
// TOD: consider IoC here, then have two types of dependencies instead of having all these ifs laying around
if (dependency.object) {
debug('caching dependency "%s" in custom cache', dependency.name)
this._customCache[dependency.name] = dependency
} else {
debug('caching "%s" with requireId: "%s"', dependency.name, dependency.requireId)
this._customCache[dependency.requireId] = require.cache[dependency.requireId] = { exports: artifact, darkmagic: dependency }
}
}
Injector.prototype.removeDependency = function (dependency) {
if (typeof dependency === 'string') {
dependency = this.getDependencyByName(dependency)
if (!dependency) {
// TODO should I throw an exception here ?
debug('trying to remove a non existent dependency')
return
}
}
debug('removeDependency() "%s"', dependency.name)
delete require.cache[dependency.requireId]
delete this._customCache[dependency.name]
}
Injector.prototype.getDependencyByName = function(name) {
var cache = require.cache
for (var requireId in cache) {
if (!cache.hasOwnProperty(requireId)) {
continue
}
var dependency = cache[requireId].darkmagic
if (dependency && dependency.name === name) {
debug('dependency %s found in require cache', name)
return dependency
}
}
if (this._customCache.hasOwnProperty(name)) {
var customCacheDependency = this._customCache[name]
if (customCacheDependency) {
debug('dependency %s found in custom cache', name)
return customCacheDependency
}
}
// return nothing otherwise
return
}
Injector.prototype.addSearchPath = function (p) {
debug('adding search path "%s"', p)
this._searchPaths.unshift(p)
}
Injector.prototype.newDependencyObject = function (name) {
var dependency = new Dependency(name)
dependency.autoInjectLocalFactories = this.autoInjectLocalFactories
dependency.autoInjectExternalFactories = this.autoInjectExternalFactories
dependency.searchPaths(this._searchPaths)
return dependency
}
Injector.prototype._getFunctionParameters = function (f) {
var parsed = esprima.parse('__f__f(' + f.toString() + ')')
var parsedFunction = parsed.body[0].expression.arguments[0]
if (parsedFunction && parsedFunction.params && parsedFunction.params.length > 0)
return parsedFunction.params
return []
}
Injector.prototype._initSearchPaths = function (rootDir) {
this._searchPaths = []
var lib1 = path.resolve(rootDir, 'lib')
// TODO: dont remember why I did this, looks redundant or otherwise obsolete...
var lib2 = path.resolve(rootDir, '..', 'lib')
if (this._isDirectory(lib1)) {
this.addSearchPath(lib1)
}
if (this._isDirectory(lib2)) {
this.addSearchPath(lib2)
}
debug('injector initial search paths: %s', util.inspect( this._searchPaths))
}
Injector.prototype._isDirectory = function(dir) {
return fs.existsSync(dir) && fs.statSync(dir).isDirectory()
}
Injector.prototype._getRealModule = function () {
// use the thing that required darkmagic
if (this.explicitRealModule) {
debug('using explicitRealModule')
return this.explicitRealModule
}
if (module.parent && module.parent.parent) {
debug('using module.parent.parent')
return module.parent.parent
}
debug('using require.main')
return require.main
}
Injector.prototype._callbackOrThrow = function (userCallback) {
var injector = this
return function handler(err) {
if (typeof userCallback === 'function') {
return userCallback(err)
}
if (err) {
debug('throwing error because no callback is supplied by the user')
throw err
}
}
}
/*
* override with custom dependencies
*/
Injector.prototype.addOverrides = function(overrides) {
for (var name in overrides) {
debug('overriding dependency %s', name)
var existing = this.getDependencyByName(name)
if (existing) {
debug('removing existing dependency %s', name)
this.removeDependency(existing)
}
var dep = new Dependency(name)
if (typeof overrides[name] === 'string') {
dep.requireId = overrides[name]
} else {
dep.object = overrides[name]
}
this.addDependency(dep)
}
}
Injector.prototype.clearCache = function () {
debug('clearing cache')
var cache = require.cache
var customCache = this._customCache
for (var name in customCache) {
if (name === '$injector') continue
if (customCache.hasOwnProperty(name)) {
delete customCache[name]
}
if (cache.hasOwnProperty(name)) {
delete cache[name]
}
}
}
function actuation() {}