fire-up
Version:
Fire Up! is a dependency injection container designed specifically for node.js with a powerful but sleek API.
527 lines (396 loc) • 19.5 kB
JavaScript
;
var _ = require('lodash');
var BPromise = require('bluebird');
var fs = require('fs');
var glob = require("simple-glob");
var path = require('path');
var Registry = require('./registry.js');
var descriptor = require('./descriptor.js');
var moduleHelpers = require('./module.js');
function newLoader(fireUp) {
function registerModules() {
fireUp._internal.registry = Registry.new(fireUp);
var modulePaths = _.filter(fireUp._internal.options.modules, function (elem) {
return _.isString(elem);
});
registerModulesFromPaths(modulePaths);
var customModules = _.filter(fireUp._internal.options.modules, function (elem) {
return _.isPlainObject(elem);
});
registerCustomModules(customModules);
validateModuleDependencies();
}
function registerModulesFromPaths(modulePaths) {
// Find module files
var files = glob({ cwd: fireUp._internal.options.basePath }, modulePaths);
for (var i = 0; i < files.length; i += 1) {
var filePathAbsolute = path.join(fireUp._internal.options.basePath, files[i]);
var filePathRelativeToCwd = path.relative(fireUp._internal.registry.basePathForFileRefs, filePathAbsolute);
fireUp._internal.registry.modules[filePathRelativeToCwd] = Registry.newModuleEntry();
// Check if they shall be used with fireUp
var contents = fs.readFileSync(filePathAbsolute, 'utf8');
if (contents.match(/^\s*\/\/\s*Fire\s*me\s*up\s*!\s*$/im) !== null) {
fireUp._internal.registry.modules[filePathRelativeToCwd].status = fireUp.constants.MODULE_STATUS_TO_LOAD;
} else {
fireUp._internal.registry.modules[filePathRelativeToCwd].status = fireUp.constants.MODULE_STATUS_TO_IGNORE;
continue;
}
// Load the module
var module;
try {
if (fireUp._internal.options.bustRequireCache === true) {
delete require.cache[filePathAbsolute];
}
module = require(filePathAbsolute);
module.__filename = filePathAbsolute;
module.__dirname = path.dirname(filePathAbsolute);
} catch (e) {
fireUp._internal.registry.modules[filePathRelativeToCwd].status = fireUp.constants.MODULE_STATUS_LOAD_FAILED;
var requireErr = new fireUp.errors.ModuleLoadingError(filePathAbsolute, e);
fireUp.log.error({ err: requireErr }, requireErr.message + '\n' + e.stack);
throw requireErr;
}
validateAndRegisterLoadedModule(module, filePathRelativeToCwd);
}
}
function registerCustomModules(customModules) {
for ( var i = 0; i < customModules.length; i+=1 ) {
var registryId = 'Custom Module No. ' + (i+1) + ' passed to fireUpLib.newInjector(...)';
fireUp._internal.registry.modules[registryId] = Registry.newModuleEntry();
fireUp._internal.registry.modules[registryId].status = fireUp.constants.MODULE_STATUS_TO_LOAD;
customModules[i].__filename = undefined;
customModules[i].__dirname = fireUp._internal.options.basePath;
validateAndRegisterLoadedModule(customModules[i], registryId);
}
}
function validateAndRegisterLoadedModule(loadedModule, registryId) {
try {
moduleHelpers.validateModule(fireUp, loadedModule, registryId);
} catch (e) {
fireUp._internal.registry.modules[registryId].status = fireUp.constants.MODULE_STATUS_LOAD_FAILED;
throw e;
}
loadedModule = moduleHelpers.addFactoryAdapterIfNeeded(loadedModule);
fireUp._internal.registry.modules[registryId].cache.module = loadedModule;
if (_.isString(loadedModule.implements)) {
fireUp._internal.registry.modules[registryId].implements = [loadedModule.implements];
} else {
fireUp._internal.registry.modules[registryId].implements = loadedModule.implements;
}
if (_.isUndefined(loadedModule.inject) === false) {
if (_.isString(loadedModule.inject)) {
fireUp._internal.registry.modules[registryId].inject = [loadedModule.inject];
} else {
fireUp._internal.registry.modules[registryId].inject = loadedModule.inject;
}
}
if (_.isString(loadedModule.type)) {
fireUp._internal.registry.modules[registryId].type = loadedModule.type;
}
fireUp._internal.registry.modules[registryId].status = fireUp.constants.MODULE_STATUS_LOADED;
// Step 4: Register the module
var interfaces = loadedModule.implements;
if (_.isString(interfaces)) {
interfaces = [interfaces];
}
for (var k = 0; k < interfaces.length; k += 1) {
fireUp._internal.registry.registerInterface(registryId, interfaces[k]);
}
fireUp._internal.registry.modules[registryId].status = fireUp.constants.MODULE_STATUS_REGISTERED;
}
function validateModuleDependencies() {
// Validate that all interfaces are available for the required injections
fireUp.log.info({}, 'Start validating all inject references...');
_.forOwn(fireUp._internal.registry.modules, function (registryEntry, modulePath) {
if (registryEntry.status !== fireUp.constants.MODULE_STATUS_REGISTERED) {
return;
}
for (var r = 0; r < registryEntry.inject.length; r += 1) {
var internalDependencies = ['fireUp/currentInjector', 'fireUp/injectionRequest', 'fireUp/options'];
if (_.includes(internalDependencies, registryEntry.inject[r])) {
continue;
}
if (descriptor.usesStarSelector(registryEntry.inject[r])) {
continue;
}
fireUp._internal.registry.findInterface(
descriptor.convertModuleReferenceToInterfaceName(registryEntry.inject[r])
); // Logs warnings...
}
});
fireUp.log.info({}, 'Validating all inject references done.');
}
function loadDependency(moduleReference, options, parentInjectionRequest) {
var injectionRequest = {
moduleReference: moduleReference,
parsedModuleReference: descriptor.parseModuleReference(moduleReference),
usedInterface: undefined,
implementedByVirtualModule: undefined,
cachedModule: undefined,
_internal: {
interfaceRecord: undefined,
moduleRecord: undefined
},
parent: parentInjectionRequest,
nestingLevel: (_.isUndefined(parentInjectionRequest) ? 0 : parentInjectionRequest.nestingLevel+1)
};
if (descriptor.usesStarSelector(injectionRequest.moduleReference)) {
return loadAllModules(injectionRequest, options);
}
return loadModule(injectionRequest, options);
}
function loadAllModules(injectionRequest, options) {
var err;
logInstantiationInfo(injectionRequest);
injectionRequest.implementedByVirtualModule = true;
var baseInterfaceReference = descriptor.formatInterfaceName(_.initial(injectionRequest.parsedModuleReference.segments));
var baseInterface = fireUp._internal.registry.getInterface(baseInterfaceReference);
injectionRequest._internal.interfaceRecord = baseInterface;
var interfaceKeys = [];
if (baseInterface && interfaceKeys.length === 0) {
interfaceKeys = _.keys(baseInterface.interfaces);
}
throwIfInjectionMovesInCircles(injectionRequest);
return new BPromise(function (resolve) {
function loadDependencyAndIgnoreNoImplementationError(interfaceName, options, injectionRequest) {
try {
return loadDependency(interfaceName, options, injectionRequest);
} catch (e) {
if (e instanceof fireUp.errors.NoImplementationError) {
return undefined;
} else {
throw e;
}
}
}
var promisesForMatchingModules = [];
for ( var i = 0; i < interfaceKeys.length; i+= 1 ) {
promisesForMatchingModules.push(
loadDependencyAndIgnoreNoImplementationError(baseInterface.interfaces[interfaceKeys[i]].interfaceName, options, injectionRequest)
);
}
resolve(BPromise.all(promisesForMatchingModules)
.then(function (modules) {
var loadedModules = {};
for ( var i = 0; i < modules.length; i+=1 ) {
if (_.isUndefined(modules[i])) {
continue;
}
loadedModules[modules[i].interfaceName] = modules[i].instance;
}
return boxLoadedDependency(injectionRequest.moduleReference, loadedModules);
}));
});
}
function loadModule(injectionRequest, options) {
var err;
applyUseOption(options, injectionRequest);
if (injectionRequest.usedInterface.match(/^fireUp\//m)) {
if (injectionRequest.usedInterface === 'fireUp/currentInjector') {
return BPromise.resolve(boxLoadedDependency('fireUp/currentInjector', fireUp));
} else if (injectionRequest.usedInterface === 'fireUp/injectionRequest') {
return BPromise.resolve(boxLoadedDependency('fireUp/injectionRequest', injectionRequest.parent));
} else if (injectionRequest.usedInterface === 'fireUp/options') {
return BPromise.resolve(boxLoadedDependency('fireUp/options', options.toObject()));
} else {
var parsedUsedInterface = descriptor.parseInterfaceName(injectionRequest.usedInterface);
if (parsedUsedInterface.length === 1 ||
(parsedUsedInterface[0] !== 'fireUp/currentInjector' &&
parsedUsedInterface[0] !== 'fireUp/injectionRequest' &&
parsedUsedInterface[0] !== 'fireUp/options')) {
fireUp.log.debug({}, 'The module reference \'%s\' does not match any interface provided by Fire Up!', injectionRequest.usedInterface);
fireUp.log.debug({}, 'Currently Fire Up! provides: \'fireUp/currentInjector\', \'fireUp/injectionRequest\', and \'fireUp/options\'.');
}
}
}
var interfaceRecord = fireUp._internal.registry.findInterface(injectionRequest.usedInterface);
injectionRequest._internal.interfaceRecord = interfaceRecord;
logInstantiationInfo(injectionRequest);
if (_.isUndefined(interfaceRecord)) {
throwNoImplementationError(injectionRequest);
}
var moduleRecord = fireUp._internal.registry.modules[interfaceRecord.file];
injectionRequest._internal.moduleRecord = moduleRecord;
injectionRequest.cachedModule = moduleRecord.cache.module;
injectionRequest.implementedByVirtualModule = false;
if (injectionRequest.parsedModuleReference.args.length > 0) {
if (moduleRecord.type !== fireUp.constants.MODULE_TYPE_MULTIPLE_INSTANCES) {
err = new fireUp.errors.ConfigError('The module reference \'%s\' contains static args but the following implementation is not of type \'%s\': %s', injectionRequest.moduleReference, fireUp.constants.MODULE_TYPE_MULTIPLE_INSTANCES, interfaceRecord.file);
fireUp.log.error(err);
throw err;
} else if (_.isUndefined(moduleRecord.cache.module.instance) === false) {
err = new fireUp.errors.ConfigError("The module reference '%s' contains static args but the following implementation cannot process them because it defines the exports.instance property: %s", injectionRequest.moduleReference, interfaceRecord.file);
fireUp.log.error(err);
fireUp.log.debug({}, "Use a factory instead to process static args.");
throw err;
}
}
throwIfInjectionMovesInCircles(injectionRequest);
if (moduleRecord.type === fireUp.constants.MODULE_TYPE_SINGLETON) {
var fill = ''; for ( var i = 0; i < injectionRequest.nestingLevel; i+= 1 ) { fill += ' '; }
if (moduleRecord.cache.singletonInstance !== null) {
fireUp.log.info({}, fill + "Returning cached module instance for interface '%s'.", injectionRequest._internal.interfaceRecord.interfaceName);
return BPromise.resolve(boxLoadedDependency(interfaceRecord.interfaceName, moduleRecord.cache.singletonInstance));
} else if (moduleRecord.cache.instantiationPromise !== null) {
fireUp.log.info({}, fill + "Will return already requested module instance for interface '%s'.", injectionRequest._internal.interfaceRecord.interfaceName);
return moduleRecord.cache.instantiationPromise;
}
}
var promise = new BPromise(function (resolve) {
var promisesForInjectedModules = [];
for ( var i = 0; i < moduleRecord.inject.length; i+= 1 ) {
promisesForInjectedModules.push(
loadDependency(moduleRecord.inject[i], options, injectionRequest)
.then(unboxLoadedDependency)
);
}
resolve(BPromise.all(promisesForInjectedModules)
.then(function (modules) {
return new BPromise(function (resolve) {
for ( var k = 0; k < injectionRequest.parsedModuleReference.args.length; k+=1 ) {
modules.push(injectionRequest.parsedModuleReference.args[k]);
}
resolve(moduleRecord.cache.module.factory.apply(moduleRecord.cache.module, modules));
})
.catch(function (e) {
var initErr = new fireUp.errors.InstanceInitializationError(injectionRequest, interfaceRecord.file, e);
fireUp.log.error({ err: initErr }, initErr.message + '\n' + e.stack);
fireUp.log.debug({}, 'All dependencies were initialized and injected. Hence the error occurred in the factory method of \'%s\'.', interfaceRecord.file);
throw initErr;
})
.then(function (instance) {
if (moduleRecord.type === fireUp.constants.MODULE_TYPE_SINGLETON) {
moduleRecord.cache.singletonInstance = instance;
}
return boxLoadedDependency(interfaceRecord.interfaceName, instance);
});
}));
}).finally(function () {
moduleRecord.cache.instantiationPromise = null;
});
if (moduleRecord.type === fireUp.constants.MODULE_TYPE_SINGLETON) {
moduleRecord.cache.instantiationPromise = promise;
}
return promise;
}
function logInstantiationInfo(injectionRequest) {
var info = '';
for ( var i = 0; i < injectionRequest.nestingLevel; i+=1 ) {
info += (i < injectionRequest.nestingLevel-1 ? ' ' : '|-- ');
}
info += 'Requested: ' + injectionRequest.moduleReference;
if (_.isUndefined(injectionRequest.usedInterface) === false &&
descriptor.convertModuleReferenceToInterfaceName(injectionRequest.moduleReference) !== injectionRequest.usedInterface) {
info += ', but using: ' + injectionRequest.usedInterface;
}
if (_.isUndefined(injectionRequest._internal.interfaceRecord) === false) {
if (injectionRequest.usedInterface !== injectionRequest._internal.interfaceRecord.interfaceName) {
info += ', but only available: ' + injectionRequest._internal.interfaceRecord.interfaceName;
}
if (_.indexOf(['require', 'require:mock'], injectionRequest._internal.interfaceRecord.interfaceName) === -1) {
info += ', implemented in: ' + injectionRequest._internal.interfaceRecord.file;
}
}
fireUp.log.info({}, info);
}
function throwNoImplementationError(injectionRequest) {
var err = new fireUp.errors.NoImplementationError(injectionRequest);
fireUp.log.error(err);
if (_.isUndefined(injectionRequest.parent)) {
fireUp.log.debug({}, 'The implementation was directly requested by a fireUp call.');
} else {
fireUp.log.debug({}, 'The implementation was requested to be injected into the module with the source file at \'%s\'.', injectionRequest.parent._internal.interfaceRecord.file);
}
throw err;
}
function throwIfInjectionMovesInCircles(injectionRequest) {
if (injectionMovesInCircles(injectionRequest)) {
var err = new fireUp.errors.CircularDependencyError(injectionRequest);
fireUp.log.error(err);
throw err;
}
}
function injectionMovesInCircles(injectionRequest) {
if (injectionRequest.implementedByVirtualModule === true) {
return false;
}
if (injectionRequest._internal.moduleRecord.type === fireUp.constants.MODULE_TYPE_SINGLETON) {
var tempInjectionRequest = injectionRequest.parent;
while (_.isUndefined(tempInjectionRequest) === false) {
if (tempInjectionRequest.implementedByVirtualModule === false &&
tempInjectionRequest._internal.interfaceRecord.file === injectionRequest._internal.interfaceRecord.file) {
return true;
}
tempInjectionRequest = tempInjectionRequest.parent;
}
return false;
} else {
// TODO: Algorithm for modules of multiple instances type needed.
return false;
}
}
function applyUseOption(options, injectionRequest) {
var requestedInterface = descriptor.convertModuleReferenceToInterfaceName(injectionRequest.moduleReference);
var searchStringForExtendedInterfaces = requestedInterface + ':';
if (_.isUndefined(injectionRequest.parent) === false &&
injectionRequest.parent.implementedByVirtualModule === false) {
for ( var i = 0; i < injectionRequest.parent._internal.moduleRecord.implements.length; i+=1 ) {
if (injectionRequest.parent._internal.moduleRecord.implements[i].substr(0, searchStringForExtendedInterfaces.length) === searchStringForExtendedInterfaces) {
// Requesting module implements an extended interface of the requested one. E.g. the module is a wrapper.
// Use options do not apply.
injectionRequest.usedInterface = requestedInterface;
return;
}
}
}
var extendedInterfaces = _.filter(options.use, function (entry) {
return entry.substr(0, searchStringForExtendedInterfaces.length) === searchStringForExtendedInterfaces;
});
if (extendedInterfaces.length === 0) {
injectionRequest.usedInterface = requestedInterface;
return;
} else if (extendedInterfaces.length === 1) {
injectionRequest.usedInterface = extendedInterfaces[0];
return;
}
var parsedExtendedInterfaces = _.map(extendedInterfaces, descriptor.parseInterfaceName);
var maxNesting = _.max(parsedExtendedInterfaces, 'length').length;
var extendedInterfacesWithDeepestNesting = _.filter(parsedExtendedInterfaces, function (entry) {
return entry.length === maxNesting;
});
if (extendedInterfacesWithDeepestNesting.length === 1) {
var allInterfacesAreCompatible = true;
for ( var k = 0; k < parsedExtendedInterfaces.length; k+=1 ) {
if (_.isEqual(parsedExtendedInterfaces[k], _.slice(extendedInterfacesWithDeepestNesting[0], 0, parsedExtendedInterfaces[k].length)) === false) {
allInterfacesAreCompatible = false;
break;
}
}
if (allInterfacesAreCompatible === true) {
injectionRequest.usedInterface = descriptor.formatInterfaceName(extendedInterfacesWithDeepestNesting[0]);
return;
}
}
var err = new fireUp.errors.UseOptionConflictError(requestedInterface, extendedInterfaces, injectionRequest);
logInstantiationInfo(injectionRequest);
fireUp.log.error(err);
throw err;
}
function boxLoadedDependency(interfaceName, instance) {
return {
interfaceName: interfaceName,
instance: instance
};
}
function unboxLoadedDependency(boxedModule) {
return boxedModule.instance;
}
return {
registerModules: registerModules,
loadDependency: loadDependency,
unboxLoadedDependency: unboxLoadedDependency
};
}
module.exports = {
newLoader: newLoader
};