@balderdash/sails-edge
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
538 lines (476 loc) • 19.1 kB
JavaScript
module.exports = function(sails) {
var path = require('path');
var async = require('async');
var _ = require('lodash');
var buildDictionary = require('sails-build-dictionary');
var walk = require('walk');
/**
* Module loader
*
* Load a module into memory
*/
return {
defaults: function (config) {
var localConfig = {
// The path to the application
appPath: config.appPath ? path.resolve(config.appPath) : process.cwd(),
// Paths for application modules and key files
// If `paths.app` not specified, use process.cwd()
// (the directory where this Sails process is being initiated from)
paths: {
// Configuration
//
// For `userconfig` hook
config: path.resolve(config.appPath, 'config'),
// Server-Side Code
//
// For `controllers` hook
controllers: path.resolve(config.appPath, 'api/controllers'),
// For `policies` hook
policies: path.resolve(config.appPath, 'api/policies'),
// For `services` hook
services: path.resolve(config.appPath, 'api/services'),
// For `orm` hook
adapters: path.resolve(config.appPath, 'api/adapters'),
models: path.resolve(config.appPath, 'api/models'),
// For `userhooks` hook
hooks: path.resolve(config.appPath, 'api/hooks'),
// For `blueprints` hook
blueprints: path.resolve(config.appPath, 'api/blueprints'),
// For `responses` hook
responses: path.resolve(config.appPath, 'api/responses'),
// Server-Side HTML
//
// For `views` hook
views: path.resolve(config.appPath, 'views'),
layout: path.resolve(config.appPath, 'views/layout.ejs')
},
moduleloader: {
}
};
var conf = localConfig.moduleloader;
// Declare supported languages.
// To add another language use the format below.
// {
// extensions: An array of file extensions supported by this module
// module: the NPM module name
// require: the require statement
// }
var supportedLangs = [
{
extensions: ['iced','liticed'],
module: 'iced-coffee-script',
require: 'iced-coffee-script/register'
},
{
extensions: ['coffee','litcoffee'],
module: 'coffee-script',
require: 'coffee-script/register'
},
{
extensions: ['ls'],
module: 'LiveScript',
require: 'livescript'
}
];
var detectedLangs = [];
var detectedExtens = [];
// Function to run for every found file when we walk the directory tree
var walkFunction = {
listeners: {
file: function (root, fileStats, next) {
var fileName = fileStats.name;
var extens = path.extname(fileName).substring(1);
// Look for every file extension we support and flag the appropriate language
_.forEach(supportedLangs, function(lang){
// If we have already found a language, skip it.
if (!_.contains(detectedLangs, lang.module)) {
// If we find a new one, add it to the list.
if (_.contains(lang.extensions, extens)) {
detectedLangs.push(lang.module);
}
}
});
next();
},
errors: function (root, nodeStatsArray, next) {
next();
}
}
};
// Walk the /api and /config directories
walk.walkSync(localConfig.appPath+'/api', walkFunction);
walk.walkSync(localConfig.appPath+'/config', walkFunction);
// Check for which languages were found and load the necessary modules to compile them
_.forEach(detectedLangs, function(moduleName){
var lang = _.find(supportedLangs, {module: moduleName});
detectedExtens = detectedExtens.concat(lang.extensions);
try {
require(lang.require);
} catch(e0){
try {
require(path.join(localConfig.appPath, 'node_modules/'+lang.require));
}
catch (e1) {
sails.log.error('Please run `npm install '+lang.module+'` to use '+lang.module+'!');
sails.log.silly('Here\'s the require error(s): ',e0,e1);
}
}
});
conf.configExt = ['js','json'].concat(detectedExtens);
conf.sourceExt = ['js'].concat(detectedExtens);
return localConfig;
},
initialize: function(cb) {
// Expose self as `sails.modules` (for backwards compatibility)
sails.modules = sails.hooks.moduleloader;
return cb();
},
configure: function() {
if (sails.config.moduleLoaderOverride) {
var override = sails.config.moduleLoaderOverride(sails, this);
sails.util.extend(this, override);
if (override.configure) {
this.configure();
}
}
sails.config.appPath = sails.config.appPath ? path.resolve(sails.config.appPath) : process.cwd();
_.extend(sails.config.paths, {
// Configuration
//
// For `userconfig` hook
config: path.resolve(sails.config.appPath, sails.config.paths.config),
// Server-Side Code
//
// For `controllers` hook
controllers: path.resolve(sails.config.appPath, sails.config.paths.controllers),
// For `policies` hook
policies: path.resolve(sails.config.appPath, sails.config.paths.policies),
// For `services` hook
services: path.resolve(sails.config.appPath, sails.config.paths.services),
// For `orm` hook
adapters: path.resolve(sails.config.appPath, sails.config.paths.adapters),
models: path.resolve(sails.config.appPath, sails.config.paths.models),
// For `userhooks` hook
hooks: path.resolve(sails.config.appPath, sails.config.paths.hooks),
// For `blueprints` hook
blueprints: path.resolve(sails.config.appPath, sails.config.paths.blueprints),
// For `responses` hook
responses: path.resolve(sails.config.appPath, sails.config.paths.responses),
// Server-Side HTML
//
// For `views` hook
views: path.resolve(sails.config.appPath, sails.config.paths.views),
layout: path.resolve(sails.config.appPath, sails.config.paths.layout)
});
},
/**
* Load user config from app
*
* @param {Object} options
* @param {Function} cb
*/
loadUserConfig: function (cb) {
async.auto({
'config/*': function loadOtherConfigFiles (cb) {
buildDictionary.aggregate({
dirname : sails.config.paths.config || sails.config.appPath + '/config',
exclude : ['locales'].concat(_.map(sails.config.moduleloader.configExt, function(item){ return 'local.'+item; })),
excludeDirs: /(locales|env)$/,
filter : new RegExp("(.+)\\.(" + sails.config.moduleloader.configExt.join('|') + ")$"),
flattenDirectories: !(sails.config.dontFlattenConfig),
identity : false
}, cb);
},
'config/local' : function loadLocalOverrideFile (cb) {
buildDictionary.aggregate({
dirname : sails.config.paths.config || sails.config.appPath + '/config',
filter : new RegExp("local\\.(" + sails.config.moduleloader.configExt.join('|') + ")$"),
identity : false
}, cb);
},
// Load environment-specific config folder, e.g. config/env/development/*
'config/env/**': ['config/local', function loadEnvConfigFolder (cb, async_data) {
// If there's an environment already set in sails.config, then it came from the environment
// or the command line, so that takes precedence. Otherwise, check the config/local.js file
// for an environment setting. Lastly, default to development.
var env = sails.config.environment || async_data['config/local'].environment || 'development';
buildDictionary.aggregate({
dirname : (sails.config.paths.config || sails.config.appPath + '/config') + '/env/' + env,
filter : new RegExp("(.+)\\.(" + sails.config.moduleloader.configExt.join('|') + ")$"),
optional : true,
flattenDirectories: !(sails.config.dontFlattenConfig),
identity : false
}, cb);
}],
// Load environment-specific config file, e.g. config/env/development.js
'config/env/*' : ['config/local', function loadEnvConfigFile (cb, async_data) {
// If there's an environment already set in sails.config, then it came from the environment
// or the command line, so that takes precedence. Otherwise, check the config/local.js file
// for an environment setting. Lastly, default to development.
var env = sails.config.environment || async_data['config/local'].environment || 'development';
buildDictionary.aggregate({
dirname : (sails.config.paths.config || sails.config.appPath + '/config') + '/env',
filter : new RegExp("^" + env + "\\.(" + sails.config.moduleloader.configExt.join('|') + ")$"),
optional : true,
flattenDirectories: !(sails.config.dontFlattenConfig),
identity : false
}, cb);
}]
}, function (err, async_data) {
if (err) { return cb(err); }
// Save the environment override, if any.
var env = sails.config.environment;
// Merge the configs, with env/*.js files taking precedence over others, and local.js
// taking precedence over everything
var config = sails.util.merge(
async_data['config/*'],
async_data['config/env/**'],
async_data['config/env/*'],
async_data['config/local']
);
// Set the environment, but don't allow env/* files to change it; that'd be weird.
config.environment = env || async_data['config/local'].environment || 'development';
// Return the user config
cb(null, config);
});
},
/**
* Load app controllers
*
* @param {Object} options
* @param {Function} cb
*/
loadControllers: function (cb) {
buildDictionary.optional({
dirname: sails.config.paths.controllers,
filter: new RegExp("(.+)Controller\\.(" + sails.config.moduleloader.sourceExt.join('|') + ")$"),
flattenDirectories: true,
keepDirectoryPath: true
}, bindToSails(cb));
},
/**
* Load adapters
*
* @param {Object} options
* @param {Function} cb
*/
loadAdapters: function (cb) {
buildDictionary.optional({
dirname: sails.config.paths.adapters,
filter: new RegExp("(.+Adapter)\\.(" + sails.config.moduleloader.sourceExt.join('|') + ")$"),
replaceExpr: /Adapter/,
flattenDirectories: true
}, bindToSails(cb));
},
/**
* Load app's model definitions
*
* @param {Object} options
* @param {Function} cb
*/
loadModels: function (cb) {
// Get the main model files
buildDictionary.optional({
dirname : sails.config.paths.models,
filter : new RegExp("^([^.]+)\\.(" + sails.config.moduleloader.sourceExt.join('|') + ")$"),
replaceExpr : /^.*\//,
flattenDirectories: true
}, function(err, models) {
if (err) {return cb(err);}
// Get any supplemental files
buildDictionary.optional({
dirname : sails.config.paths.models,
filter : /(.+)\.attributes.json$/,
replaceExpr : /^.*\//,
flattenDirectories: true
}, bindToSails(function(err, supplements) {
if (err) {return cb(err);}
return cb(null, sails.util.merge(models, supplements));
}));
});
},
/**
* Load app services
*
* @param {Object} options
* @param {Function} cb
*/
loadServices: function (cb) {
buildDictionary.optional({
dirname : sails.config.paths.services,
filter : new RegExp("(.+)\\.(" + sails.config.moduleloader.sourceExt.join('|') + ")$"),
depth : 1,
caseSensitive : true
}, bindToSails(cb));
},
/**
* Check for the existence of views in the app
*
* @param {Object} options
* @param {Function} cb
*/
statViews: function (cb) {
buildDictionary.optional({
dirname: sails.config.paths.views,
filter: /(.+)\..+$/,
replaceExpr: null,
dontLoad: true
}, cb);
},
/**
* Load app policies
*
* @param {Object} options
* @param {Function} cb
*/
loadPolicies: function (cb) {
buildDictionary.optional({
dirname: sails.config.paths.policies,
filter: new RegExp("(.+)\\.(" + sails.config.moduleloader.sourceExt.join('|') + ")$"),
replaceExpr: null,
flattenDirectories: true,
keepDirectoryPath: true
}, bindToSails(cb));
},
/**
* Load app hooks
*
* @param {Object} options
* @param {Function} cb
*/
loadUserHooks: function (cb) {
async.auto({
// Load apps from the "api/hooks" folder
hooksFolder: function(cb) {
buildDictionary.optional({
dirname: sails.config.paths.hooks,
filter: new RegExp("^(.+)\\.(" + sails.config.moduleloader.sourceExt.join('|') + ")$"),
// Hooks should be defined as either single files as a function
// OR (better yet) a subfolder with an index.js file
// (like a standard node module)
depth: 2
}, cb);
},
// Load package.json files from node_modules to check for hooks
nodeModulesFolder: function(cb) {
buildDictionary.optional({
dirname: path.resolve(sails.config.appPath, "node_modules"),
filter: /^(package\.json)$/,
excludeDirs: /^\./,
depth: 3
}, cb);
}
}, function(err, results) {
if (err) {return cb(err);}
// Marshall the hooks by checking that they are valid. The ones from the
// api/hooks folder are assumed to be okay.
var hooks = results.hooksFolder;
try {
var modules = flattenNamespacedModules(results.nodeModulesFolder);
_.extend(hooks, _.reduce(modules, function(memo, module, identity) {
// Hooks loaded from "node_modules" need to have "sails.isHook: true" in order for us
// to know that they are a sails hook
if (module['package.json'] && module['package.json'].sails && module['package.json'].sails.isHook) {
var hookConfig = module['package.json'].sails;
// Determine the name the hook should be added as
var hookName;
if (!_.isEmpty(hookConfig.hookName)) {
hookName = hookConfig.hookName;
}
// If an identity was specified in sails.config.installedHooks, use that
else if (sails.config.installedHooks && sails.config.installedHooks[identity] && sails.config.installedHooks[identity].name) {
hookName = sails.config.installedHooks[identity].name;
}
// Otherwise use the module name, with initial "sails-hook" stripped off if it exists
else {
hookName = identity.match(/^sails-hook-/) ? identity.replace(/^sails-hook-/,'') : identity;
}
// Allow overriding core hooks
if (sails.hooks[hookName]) {
sails.log.verbose('Found hook: `'+hookName+'` in `node_modules/`. Overriding core hook w/ the same identity...');
}
// If we have a hook in api/hooks with this name, throw an error
if (hooks[hookName]) {
var err = (function (){
var msg =
'Found hook: `' + hookName + '`, in `node_modules/`, but a hook with that identity already exists in `api/hooks/`. '+
'The hook defined in your `api/hooks/` folder will take precedence.';
var err = new Error(msg);
err.code = 'E_INVALID_HOOK_NAME';
return err;
});
sails.log.warn(err);
return memo;
}
// Load the hook code
var hook = require(path.resolve(sails.config.appPath, "node_modules", identity));
// Set its config key (defaults to the hook name)
hook.configKey = (sails.config.installedHooks && sails.config.installedHooks[identity] && sails.config.installedHooks[identity].configKey) || hookName;
// Add this to the list of hooks to load
memo[hookName] = hook;
}
return memo;
}, {}));
return bindToSails(cb)(null, hooks);
} catch (e) {
return cb(e);
}
});
},
/**
* Load app blueprint middleware.
*
* @param {Object} options
* @param {Function} cb
*/
loadBlueprints: function (cb) {
buildDictionary.optional({
dirname: sails.config.paths.blueprints,
filter: new RegExp("(.+)\\.(" + sails.config.moduleloader.sourceExt.join('|') + ")$"),
useGlobalIdForKeyName: true
}, cb);
},
/**
* Load custom API responses.
*
* @param {Object} options
* @param {Function} cb
*/
loadResponses: function (cb) {
buildDictionary.optional({
dirname: sails.config.paths.responses,
filter: new RegExp("(.+)\\.(" + sails.config.moduleloader.sourceExt.join('|') + ")$"),
useGlobalIdForKeyName: true
}, bindToSails(cb));
},
optional: buildDictionary.optional,
required: buildDictionary.required,
aggregate: buildDictionary.aggregate,
exits: buildDictionary.exists
};
function bindToSails(cb) {
return function(err, modules) {
if (err) {return cb(err);}
_.each(modules, function(module) {
// Add a reference to the Sails app that loaded the module
module.sails = sails;
// Bind all methods to the module context
_.bindAll(module);
});
return cb(null, modules);
};
}
function flattenNamespacedModules (tree) {
return _.transform(tree, function (result, dir, dirName) {
if (/^@/.test(dirName)) {
_.extend(result, _.transform(_.omit(dir, 'identity', 'globalId'), function (result, subdir, subdirName) {
return result[dirName + '/' + subdirName] = subdir;
}));
}
else {
result[dirName] = dir;
}
});
}
};