darkmagic
Version:
A dependency injection framework
243 lines (180 loc) • 6.32 kB
JavaScript
var debug = require('debug')('darkmagic_Dependency')
var path = require('path')
var Module = require('module')
var util = require('util')
module.exports = Dependency
function Dependency(name) {
this.name = name
this.requireId = undefined
this.isLocal = false
this.isFactory = false
this.isOptional = name.length > 0 && name[name.length - 1] === '_'
this.isClass = false
// TODO: potential bug here?
if (this.isOptional && name.length > 1)
this.name = name.substr(0, name.length - 1)
this.autoInject = false
this.autoInjectLocalFactories = true
this.autoInjectExternalFactories = true
// usually a reference to the array of the injector's searchPath
// this is slightly risky, but its private api
// on the other hand this is suppose to reduce memory footprint
// so i'm not really sure if its a good or bad thing
// just a reminder to myself that pushing to this array will
// push to searchPath of the injector
this._searchPaths = []
this.object = undefined
}
var path = require('path')
Dependency.prototype.load = function (realModule, parentDependency) {
debug('loading "%s"', this.name)
var artifact
if (this.object) {
debug('"%s" is a custom dependency', this.name)
return this.object
}
if (this.requireId) {
debug('"%s" is loaded with requireId "%s"', this.name, this.requireId)
artifact = realModule.require(this.requireId)
if (!artifact) {
throw new Error('this module has a requireId but it was not loaded properly')
}
return artifact
}
var searchResults = this._init(realModule)
debug('found %d possible dependencies', searchResults.length)
if (searchResults.length === 0) {
if (this.isOptional) {
return
} else {
throw new Error(util.format('"%s%s" is Missing dependency "%s"', parentDependency.name, parentDependency.isFactory ? '(...)' : '', this.name))
}
}
if (searchResults.length > 1) {
throw new Error(util.format('multiple dependencies found for %s: %s, name collision must be resolved', this._name, pluck(searchResults, 'name')))
}
artifact = searchResults[0].artifact
this.requireId = searchResults[0].name
this.isLocal = searchResults[0].isLocal
if (artifact && artifact.$darkmagic) {
artifact.$darkmagic.dependency(this, parentDependency);
} else {
this.isFactory = typeof artifact === 'function'
// TODO isInjectable refactor point
if (this.isFactory && artifact.name) {
if (require.cache.hasOwnProperty(this.requireId) && require.cache[this.requireId].darkmagic) {
this.isFactory = false
} else if (artifact.name.substr(0, 6) === 'inject') {
this.autoInject = true
} else if (artifact.name.substr(0, 10) === 'dontInject') {
this.isFactory = false
} else if (artifact.name[0] === artifact.name[0].toUpperCase() && isNaN(artifact.name[0])) {
debug('"%s" exports a class named "%s"', this.name, artifact.name)
this.isClass = true
}
}
}
return artifact
}
Dependency.prototype._init = function (realModule) {
var artifacts = []
var artifact = this._loadExternal(realModule)
if (artifact) {
artifacts.push(artifact)
}
var searchPaths = this._searchPaths
debug('trying %d local search paths', searchPaths.length)
// try search paths
for (var x = 0; x < searchPaths.length; x++) {
var dir = searchPaths[x]
var depPath = path.resolve(dir, this.name)
artifact = this._loadLocal(realModule, depPath)
if (artifact) {
artifacts.push(artifact)
}
}
return artifacts
}
Dependency.prototype._loadExternal = function (realModule) {
var name = this.name
var dashedName = this._dashify(name)
var label = dashedName === name ? name : dashedName + ' [' + name + ']'
try {
debug('try require(\'%s\')', label)
// first try naive require
var artifact = realModule.require(dashedName)
debug('OK require(\'%s\')', label)
return {
name: dashedName,
artifact: artifact,
isLocal: false
}
} catch (e) {
// catch module not found exceptions but only for the module we are try to load
if (e.code === 'MODULE_NOT_FOUND' && this._isModuleSpecificError(dashedName, e)) {
debug('fail require(\'%s\')', dashedName)
} else {
debug('an error has occurred while trying to load "%s"', dashedName)
throw e
}
}
}
Dependency.prototype._loadLocal = function (realModule, depPath) {
debug('try require(\'%s\')', depPath)
try {
var name = Module._resolveFilename(depPath, realModule)
var artifact = realModule.require(name)
debug('OK require(\'%s\')', name)
return {
artifact: artifact,
name: name,
isLocal: true
}
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND' && this._isModuleSpecificError(depPath, e)) {
debug('fail require(\'%s\')', depPath)
} else {
debug('an error has occurred while trying to load "%s"', depPath)
throw e
}
}
}
//TODO need to refactor this, its spread here and in injectFunctor.
// there is ambiguity as to who decides what: injector has a policy
// but it should be overridable at dependency level if injector
// permits such behavior, this is terrible!
Dependency.prototype.isInjectable = function () {
debug('isInjectable()')
if (this.autoInject) return true;
debug('this.isClass=%s this.isFactory=%s (this.isLocal && this.autoInjectLocalFactories)=%s (!this.isLocal && this.autoInjectExternalFactories)=%s'
, this.isClass, this.isFactory, this.isLocal && this.autoInjectLocalFactories, !this.isLocal && this.autoInjectExternalFactories)
return !this.isClass && this.isFactory &&
((this.isLocal && this.autoInjectLocalFactories) ||
(!this.isLocal && this.autoInjectExternalFactories))
}
Dependency.prototype.searchPaths = function(searchPaths) {
this._searchPaths = searchPaths
}
Dependency.prototype._dashify = function(name) {
if (name.length === 0) return name
var result = name[0]
for (var i = 1; i < name.length; i++) {
if (name[i] === name[i].toUpperCase() && isNaN(name[i]) && name[i] !== '_') {
result += '-' + name[i].toLowerCase()
} else {
result += name[i]
}
}
return result
}
//TODO: make this check more robust
Dependency.prototype._isModuleSpecificError = function (module, e) {
return e.toString().indexOf(module) > -1
}
function pluck (arr, property) {
var results = []
for (var x = 0; x < arr.length; x++) {
results.push (arr[x][property])
}
return results
}