UNPKG

sails

Version:

API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)

255 lines (206 loc) 11.1 kB
/** * Module dependencies. */ var path = require('path'); var _ = require('@sailshq/lodash'); var includeAll = require('include-all'); var flaverr = require('flaverr'); var helpRegisterAction = require('./help-register-action'); /** * loadActionModules() * * @param {SailsApp} sails * @param {Function} cb */ module.exports = function loadActionModules (sails, cb) { sails.config.paths = sails.config.paths || {}; sails.config.paths.controllers = sails.config.paths.controllers || 'api/controllers'; // Keep track of actions loaded from disk, so we can detect conflicts. var actionsLoadedFromDisk = {}; // Load all files under the controllers folder. includeAll.optional({ dirname: sails.config.paths.controllers, filter: /(^[^.]+\.(?:(?!md|txt).)+$)/, flatten: true, keepDirectoryPath: true }, function(err, files) { if (err) { return cb(err); } try { // Set up a var to hold a list of invalid files. var garbage = []; // Traditional controllers are PascalCased and end with the word "Controller". var traditionalRegex = new RegExp('^((?:(?:.*)/)*([0-9A-Z][0-9a-zA-Z_]*))Controller\\..+$'); // Actions are kebab-cased. var actionRegex = new RegExp('^((?:(?:.*)/)*([a-z][a-z0-9-]*))\\..+$'); // Loop through all of the files returned from include-all. _.each(files, function(moduleDef) { // Get the original filepath of the action or controller. var filePath = moduleDef.globalId; // If the filepath starts with a dot, ignore it. if (filePath[0] === '.') {return;} // If the file is in a subdirectory, transform any dots in the subdirectory // path into slashes. if (path.dirname(filePath) !== '.') { filePath = path.dirname(filePath).replace(/\./g, '/') + '/' + path.basename(filePath); } // Declare a var to hold the eventual action identity. var identity = ''; // Attempt to match the file path to the pattern of a traditional controller file. var match = traditionalRegex.exec(filePath); // Is it a traditional controller? if (match) { // If it looks like a traditional controller, but it's not a dictionary, // throw it in the can. if (!_.isObject(moduleDef) || _.isArray(moduleDef) || _.isFunction(moduleDef)) { return garbage.push(filePath); } // Get the controller identity (e.g. /somefolder/somecontroller) identity = match[1]; // Loop through each action in the controller file's dictionary. _.each(moduleDef, function(action, actionName) { // Ignore strings (this could be the "identity" property of a module). if (_.isString(action)) {return;} // Give the action name `_config` special treatement: just merge it into the blueprint // config instead of trying to load it as an action. if (actionName === '_config') { if (sails.config.blueprints) { sails.config.blueprints._controllers[identity.toLowerCase()] = action; } return; } // The action identity is the controller identity + the action name, // with path separators transformed to dots. // e.g. somefolder.somecontroller.dostuff var actionIdentity = (identity + '/' + actionName).toLowerCase(); // If the action identity matches one we've already loaded from disk, bail. if (actionsLoadedFromDisk[actionIdentity]) { throw flaverr({ name: 'userError', code: 'E_CONFLICT', identity: actionIdentity}, new Error('The action `' + actionName + '` in `' + filePath + '` conflicts with a previously-loaded action.')); } // Attempt to load the action into our set of actions. // Since the following code might throw E_CONFLICT errors, we'll inject a `try` block here // to intercept them and wrap the Error. try { helpRegisterAction(sails, action, actionIdentity, true); } catch (e) { switch (e.code) { case 'E_CONFLICT': // Improve error message with addtl contextual information about where this action came from. // (plus a slightly better stack trace) throw flaverr({ name: 'userError', code: 'E_CONFLICT', identity: actionIdentity }, new Error('Failed to register `' + actionName + '`, an action in the controller loaded from `'+filePath+'` because it conflicts with a previously-registered action.') ); default: throw e; } }//</catch> // Flag that an action with the given identity was successfully loaded from disk. actionsLoadedFromDisk[actionIdentity] = true; }); } // </ is it a traditional controller? > // Okay, it's not a traditional controller. Is it an action? // Attempt to match the file path to the pattern of an action file, // and make sure it is either a function OR a dictionary containing // a function as its `fn` property. else if ((match = actionRegex.exec(filePath)) && (_.isFunction(moduleDef) || !_.isUndefined(moduleDef.machine) || !_.isUndefined(moduleDef.friendlyName) || _.isFunction(moduleDef.fn))) { // The action identity is the same as the module identity // e.g. somefolder/dostuff var actionIdentity = match[1].toLowerCase(); if (actionsLoadedFromDisk[actionIdentity]) { throw flaverr({ name: 'userError', code: 'E_CONFLICT', identity: actionIdentity }, new Error('The action `' + _.last(actionIdentity.split('/')) + '` in `' + filePath + '` conflicts with a previously-loaded action.')); } // Attempt to load the action into our set of actions. // This may throw an error, which will be caught below. try { helpRegisterAction(sails, moduleDef, actionIdentity, true); } catch (e) { switch (e.code) { // Improve Error with addtl contextual information about where this action came from. case 'E_CONFLICT': throw flaverr({ name: 'userError', code: 'E_CONFLICT', identity: actionIdentity }, new Error( 'Failed to register `' + _.last(actionIdentity.split('/')) + '`, an action loaded from `'+filePath+'` because it conflicts with a previously-registered action.' )); default: throw e; } }//</catch> // Flag that an action with the given identity was successfully loaded from disk. actionsLoadedFromDisk[actionIdentity] = true; } // </ is it an action?> // Otherwise give up on this file, it's GARBAGE. // No, no, it's probably a very nice file but it's // no controller as far as we're concerned. else { garbage.push(filePath); } // </ it is garbage> }); // </each(file from includeAll)> // Complain about garbage. if (garbage.length) { sails.log.warn('---------------------------------------------------------------------------'); sails.log.warn('Files in the `controllers` directory may be traditional controllers or \n' + 'action files. Traditional controllers are dictionaries of actions, with \n' + 'pascal-cased filenames ending in "Controller" (e.g. MyGreatController.js).\n' + 'Action files are kebab-cased (e.g. do-stuff.js) and contain a single action.\n'+ 'The following file'+(garbage.length > 1 ? 's were' : ' was')+' ignored for not meeting those criteria:'); _.each(garbage, function(filePath){sails.log.warn('- '+filePath);}); sails.log.warn('----------------------------------------------------------------------------\n'); } // (Shallow) merge stuff from sails.config.controllers.moduleDefinitions on top of any loaded files. // Note that the third argument (force) to `helpRegisterAction` is `true`, so there's no danger // of identity conflicts. Actions defined in `moduleDefinitions` will override anything else. _.each(_.get(sails, 'config.controllers.moduleDefinitions') || {}, function(action, actionIdentity) { helpRegisterAction(sails, action, actionIdentity, true); }); } catch (e) { return cb(e); } // Get a list of the action identities. var actionIdentities = _.keys(sails._actions); // Flag indicating that warnings were raised (for formatting purposes). var raisedWarnings = false; // Now that we have all the actions loaded, loop through the registered action middleware // and raise a warning about any that don't correspond to a registered action. _.each(sails._actionMiddleware, function(fns, target) { // Iterate over the list of action globs (e.g. 'foo', 'foo/bar', 'foo/bar/*', '!baz/boop') that a middleware is targeting. _.each(_.map(target.split(','), _.trim), function(actionGlob) { // Ignore * (it matches everything) and anything starting with '!' // (doesn't matter if a middleware is NOT applied to a non-existent action). if (actionGlob === '*' || actionGlob[0] === '!') { return; } // If the glob doesn't contain a wildcard, and it exactly matches a known action identity, it's ok. if (actionGlob.indexOf('*') === -1) { if (actionIdentities.indexOf(actionGlob) > -1) { return; } } // Otherwise, if one of the known action identities would match against the glob, it's okay. else { var actionGlobWithoutWildcard = actionGlob.replace(/\/\*$/, ''); if (_.find(actionIdentities, function(actionIdentity) { return actionIdentity.indexOf(actionGlobWithoutWildcard) === 0; })) { return; } } // Otherwise, construct a warning using the _middlewareType properties (if available) of the middleware functions // that were mapped to this action glob. var warning = 'Action middleware '; warning += (function(){ var fnDescs = _.reduce(fns, function(memo, fn) { if (fn._middlewareType) { memo.push(fn._middlewareType); } return memo; }, []); if (fnDescs.length) { return '(' + fnDescs.join(', ') + ') '; } return ''; })();//† warning += 'was bound to a target `' + actionGlob + '` that doesn\'t match any registered actions.'; sails.log.warn(warning); raisedWarnings = true; });//∞ });//∞ // If we raised any warnings, add an extra line break afterwards. if (raisedWarnings) { console.log(); } // All done. return cb(); }); // </includeAll> };