-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
load-action-modules.js
254 lines (206 loc) · 10.9 KB
/
load-action-modules.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/**
* 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>
};