lib/classes/Module.js
var injector = require('injector')
, Class = injector.getInstance('Class')
, Exceptions = require('exceptions')
, path = require('path')
, async = require('async')
, fs = require('fs')
, i = require('i')()
, moduleDebug = require('debug')('Modules')
, config = injector.getInstance('config')
, moduleLdr = injector.getInstance('moduleLoader')
, modules = {};
/**
* @classdesc CleverStack Module Class
* @class Module
* @extends Class
*/
module.exports = Class.extend(
/**
* @lends Module
*/
{
/**
* The default module folders to load resources from
* @type {Array}
*/
moduleFolders: [
'exceptions',
'classes',
'models',
'services',
'controllers'
],
/**
* Which of the Module.moduleFolders can use injector.inject() for loading
* @type {Array}
*/
injectableFolders: [
'models',
'controllers',
'services'
],
/**
* Which of the files inside of Module.moduleFolders should be explicitly ignore
* @type {Array}
*/
excludedFiles: [
'index.js',
'module.js',
'Gruntfile.js',
'package.json'
],
/**
* Extends the Module class with new static and prototype functions, there are a variety of ways to use extend.
* // with static and prototype
* Module.extend({ STATIC },{ PROTOTYPE })
*
* // with just classname and prototype functions
* Module.extend({ PROTOTYPE })
*
* @override
* @param {Object} Static the new Modules static properties/functions
* @param {Object} Proto the new Modules prototype properties/functions
* @return {Module}
*/
extend: function() {
var Reg = new RegExp('.*\\(([^\\)]+)\\:.*\\:.*\\)', 'ig')
, stack = new Error().stack.split('\n')
, extendingArgs = [].slice.call(arguments)
, Static = (extendingArgs.length === 2) ? extendingArgs.shift() : {}
, Proto = extendingArgs.shift()
, modulePath
, moduleName
, pkg;
// Get rid of the Error at the start
stack.shift();
if (Reg.test(stack[ 2 ])) {
modulePath = RegExp.$1.split(path.sep);
modulePath = modulePath.splice(0, modulePath.length - 1).join(path.sep);
pkg = path.resolve(path.join(modulePath, 'package.json'));
moduleName = path.basename(modulePath);
} else {
throw new Error('Error loading module, unable to determine modules location and name.');
}
if (modules[ moduleName ] !== undefined) {
moduleDebug('Returning previously defined module ' + moduleName + '...');
return modules[ moduleName ];
}
moduleDebug('Setting up ' + moduleName + ' module from path ' + modulePath + '...');
if (Static.extend) {
moduleDebug('You cannot override the extend() function provided by the CleverStack Module Class!');
delete Static.extend;
}
if (fs.existsSync(pkg)) {
moduleDebug('Loading ' + pkg + '...');
pkg = require(pkg);
} else {
pkg = false;
}
Proto._camelName = i.camelize(moduleName.replace(/\-/ig, '_'), false);
moduleDebug('Creating debugger with name ' + Proto._camelName + '...');
Proto.debug = require('debug')('cleverstack:' + Proto._camelName);
moduleDebug('Creating module class...');
/**
* extend event.
*
* @event Module.extend
* @type {Module}
*/
var Klass = this._super.apply(this, [Static, Proto])
, instance = Klass.callback('newInstance')(moduleName, modulePath, pkg);
modules[moduleName] = instance;
return instance;
}
},
/**
* @lends Module#
*/
{
/**
* The name of this service
* @type {String}
*/
name: null,
/**
* The configuration of this module, as loaded via require('config')[moduleName]
* @type {Object}
*/
config: null,
/**
* The resolved path to the module
* @type {String}
*/
path: null,
/**
* The contents of the JSON inside this modules/module/package.json
* @type {Object}
*/
pkg: null,
/**
* A list of existing paths that are build in Module#setup
* @type {Array}
*/
paths: null,
/**
* An override setup function for Module
*
* @override
* @param {String} _name The name of the service
* @param {String} _path The path of this module
* @param {Object} _pkg The contents of this modules package.json file
* @return {Arary}
*/
setup: function(_name, _path, _pkg) {
// Set our module name
this.name = _name;
// Set our config if there is any
this.config = typeof config[ _name ] === 'object' ? config[ _name ] : {};
// Set the modules location
this.path = _path;
// Set the modules package.json
this.pkg = _pkg;
// Ensure its dependencies are enabled and will be loaded
if (this.pkg && this.pkg.peerDependencies) {
Object.keys(_pkg.peerDependencies).forEach(this.proxy(function(dependency) {
if (!moduleLdr.moduleIsEnabled(dependency)) {
throw new Exceptions.ModuleDependencyNotMet(this.name + ' requires ' + dependency + '@' + this.pkg.peerDependencies[dependency]);
}
}));
}
/**
* preSetup event.
*
* @event Module.preSetup
* @type {Module}
*/
this.hook('preSetup');
// Add the modules path to our list of paths
this.paths = [ _path ];
// Check to see if clever-background-tasks is installed
if (moduleLdr.moduleIsEnabled('clever-background-tasks')) {
this.Class.moduleFolders.push('tasks');
}
// Add our moduleFolders to the list of paths, and our injector paths
this.Class.moduleFolders.forEach(this.proxy('addFolderToPath', injector));
/**
* preInit event.
*
* @event Module.preInit
* @type {Module}
*/
this.hook('preInit');
// Call the Class constructor
this._super.call(this);
// Return no arguments to init
return [];
},
/**
* Used to load the resources (files, classes, configuration, etc...) for this module
* @return {undefined}
*/
loadResources: function() {
async.forEach(
this.paths,
this.proxy('inspectPathForResources'),
this.proxy('resourcesLoaded')
);
},
/**
* Helper function that inspects the given path for resources to load
*
* @param {String} pathToInspect the path to inspect
* @param {Function} callback callback for async
* @return {undefined}
*/
inspectPathForResources: function(pathToInspect, callback) {
var that = this;
if (fs.existsSync(pathToInspect + path.sep)) {
fs.readdir(pathToInspect + path.sep, function(err, files) {
async.forEach(files, that.proxy('addResource', pathToInspect), callback);
});
} else {
callback(null);
}
},
/**
* Helper function to load a module resource and add it to this module.
*
* @param {String} pathToInspect the path to the resource
* @param {String} file the filename of the resource
* @param {Function} callback callback for async
* @return {undefined}
*/
addResource: function(pathToInspect, file, callback) {
if ((file.match(/.+\.js$/g) !== null || file.match(/.+\.es6$/g) !== null) && this.Class.excludedFiles.indexOf(file) === -1) {
var folders = pathToInspect.split(path.sep)
, name = file.replace('.js', '').replace('.es6', '')
, currentFolder = null
, insideModule = false
, rootFolder = null
, lastFolder = this
, that = this
, traceError = new Error('Load Timeout for ' + file)
, loadTimeout = setTimeout(function() {
that.debug(traceError);
process.exit(1);
}, 10000)
, resource;
while (folders.length > 0) {
currentFolder = folders.shift();
if (insideModule === false && currentFolder === this.name) {
// Make sure that this is the LAST instance of the name
if (folders.indexOf(this.name) === -1) {
insideModule = true;
}
} else if (insideModule === true) {
if (rootFolder === null) {
rootFolder = currentFolder;
if (this[ rootFolder ] !== undefined ) {
lastFolder = this[ rootFolder ];
}
} else {
lastFolder = lastFolder[ currentFolder ];
}
}
}
// Load the resource
resource = require([ pathToInspect, path.sep, file ].join(''));
// Do not load dependencies that can be injected
if (this.Class.injectableFolders.indexOf(rootFolder) === -1) {
if (this.Class.injectableFolders.indexOf(name) === -1) {
// Add the resource to the injector
if (name !== 'routes') {
this.debug('Adding ' + name + ' to the injector');
injector.instance(name, resource);
}
// Add the resource to the last object we found
lastFolder[ name ] = resource;
clearTimeout(loadTimeout);
callback(null);
} else {
clearTimeout(loadTimeout);
callback(null);
}
} else {
this.debug('Loading ' + name + ' using the injector...');
injector.inject(resource, function(resource) {
injector.instance(name, resource);
lastFolder[ name ] = resource;
clearTimeout(loadTimeout);
callback(null);
});
}
} else {
callback(null);
}
},
/**
* Fires the resourcesLoaded event with any errors raised during loading
* @return {undefined}
*/
resourcesLoaded: function(err) {
this.debug('Resources Loaded');
/**
* resourcesLoaded event.
*
* @event Module.resourcesLoaded
* @type {Module}
*/
this.emit('resourcesLoaded', err || null);
},
/**
* A helper function to add a folder to this.paths
*
* @param {String} hookName the name of the hook function to run
* @return {undefined}
*/
addFolderToPath: function(injector, folder) {
var folderPath = path.join(this.path, folder)
, folders = folder.split('/')
, currentFolder = null
, rootFolder = null
, obj = {}
, lastFolder = obj;
while (folders.length > 0) {
currentFolder = folders.shift();
if (rootFolder === null) {
rootFolder = currentFolder;
if (this[ rootFolder ] !== undefined ) {
lastFolder = obj = this[ rootFolder ];
}
} else {
if (lastFolder[ currentFolder ] === undefined) {
lastFolder[ currentFolder ] = {};
}
lastFolder = lastFolder[ currentFolder ];
}
}
this[ rootFolder ] = obj;
this.paths.push(folderPath);
injector._inherited.factoriesDirs.push(folderPath);
},
/**
* A helper function to run a hook on a module, it that hook function exists
*
* @param {String} hookName the name of the hook function to run
* @return {undefined}
*/
hook: function(hookName) {
if (typeof this[ hookName ] === 'function') {
this.debug('calling ' + hookName + '() hook...');
injector.inject(this[hookName].bind(this));
}
},
/**
* Default implementation of the initRoutes hook for this module, you can override this if you need
* @return {undefined}
*/
initRoutes: function() {
if (typeof this.routes === 'function') {
this.debug('calling initRoutes() hook...');
injector.inject(this.routes);
}
}
});