sails-generate
Version:
Runner script for sails generators
351 lines (287 loc) • 15.6 kB
JavaScript
/**
* Module dependencies
*/
var path = require('path');
var _ = require('@sailshq/lodash');
var builtinTemplate = require('../../builtins/template');
var inventDescription = require('../../invent-description');
var IS_CURRENT_NODE_VERSION_CAPABLE_OF_AWAIT = require('../../IS_CURRENT_NODE_VERSION_CAPABLE_OF_AWAIT');
/**
* sails-generate-action
*
* Usage:
* `sails generate action <name>`
*
* @type {Dictionary}
*/
module.exports = {
// ╔╗ ╔═╗╔═╗╔═╗╦═╗╔═╗
// ╠╩╗║╣ ╠╣ ║ ║╠╦╝║╣
// ╚═╝╚═╝╚ ╚═╝╩╚═╚═╝ooo
before: function (scope, done){
var roughName;
// Use actions2 by default, since actions are being generated individually.
if (scope.actions2 === undefined) {
scope.actions2 = true;
}
// The default template to use.
scope.templateName = 'actions2.template';
// If `--no-actions2` was used, switch the default template and easter-egg template suffix.
if (!scope.actions2) {
scope.templateName = 'action.template';
}
// Accept --name
if (scope.name) {
roughName = scope.name;
}
// Or otherwise the first serial arg is the "roughName" of the action we want to create.
else if (_.isArray(scope.args)) {
// If more than one serial args were provided, they'll constitute the action friendly name
// and we'll combine them to form the base name.
if (scope.args.length >= 2) {
// However, note that we first check to be sure no dots or slashes were used
// (if so, this would throw things off)
if (scope.args[0].match(/[\/\.\\]/)) {
return done(new Error(
'Too many serial arguments: A "." or "/" was specified in the name for this new action, but extra words were provided too.\n' +
'(should be provided like either `foo-bar/baz/view-profile` or `View profile`).'
));
}
scope.friendlyName = scope.args.join(' ');
roughName = _.camelCase(scope.friendlyName);
}
// Otherwise, we'll infer an appropriate friendly name further on down, and just use the
// first serial arg as our "roughName".
else if (scope.args[0] && _.isString(scope.args[0])) {
roughName = scope.args[0];
}
}//>-
if (!roughName) {
return done(new Error(
'Missing argument: Please provide a name for the new action.\n' +
'(should be "verby" and use the imperative mood; e.g. `view-profile` or `sign-up`).'
));
}
// Replace backslashes with proper slashes.
// (This is crucial for Windows compatibility.)
roughName = roughName.replace(/\\/g, '/');
// Now get our actionName.
// Be kind -- transform slashes to dots when creating the actionName.
scope.actionName = roughName.replace(/\/+/g, '.');
// Then split on dots and make sure each segment is using camel-case before recombining.
// (We'll unwind this later for most of the things.)
scope.actionName = _.map(scope.actionName.split('.'), function (segment){
return _.camelCase(segment);
}).join('.');
// After transformation, the actionName must contain only letters, numbers, and dots--
// and start with a lowercased letter.
if (!scope.actionName.match(/^[a-z][a-zA-Z0-9\.]*$/)) {
return done( new Error('The name `' + scope.actionName + '` is invalid. '+
'Action names must start with a lower-cased letter and contain only '+
'letters, numbers, and dots.'));
}
// If the action name contains something like "FooController", then simplify that.
scope.actionName = scope.actionName.replace(/(\.?)(.+)controller\./i, '$1$2.');
// Then get our `relPath` (filename / path).
// When building the relPath, we convert any dots to slashes.
scope.relPath = scope.actionName.replace(/\.+/g, '/');
// Then split on slashes and make sure each segment is using kebab-case before recombining.
scope.relPath = _.map(scope.relPath.split('/'), function (segment){
// One exception: Don't kebab-case words like "v1" within this segment, if any.
return _.map(_.words(segment), function(word){
if (word.match(/[a-z]+[0-9]+/)) { return word; }
return _.kebabCase(word);
}).join('-');
}).join('/');
// (And of course, finally, we tack on `.js` at the end-- we're not barbarians after all.)
scope.relPath += '.js';
// Grab the filename for potential use in our template.
scope.filename = path.basename(scope.relPath);
// Identify the action "stem" for use below.
// (e.g. "view-stuff-and-things" from "v1.pages.view-stuff-and-things")
var stem = _.last(scope.actionName.split('.'));
var sentenceCaseStem = _.map(_.words(stem), function (word, i){ if (i===0) { return _.capitalize(word); } else { return word[0].toLowerCase() + word.slice(1); } }).join(' ');
// Attempt to invent a description, if there isn't one already.
if (_.isUndefined(scope.description)) {
scope.description = inventDescription(scope.actionName, true);
}//>-
// Infer other defaults.
_.defaults(scope, {
friendlyName: scope.friendlyName || sentenceCaseStem,
description: scope.description || '',
inferredViewTemplatePath: '',
verbose: false,
IS_CURRENT_NODE_VERSION_CAPABLE_OF_AWAIT: IS_CURRENT_NODE_VERSION_CAPABLE_OF_AWAIT,
// getAlternativeTemplate: null << FUTURE: a configurable function
});
// ┌┐ ┬ ┬┬┬ ┌┬┐ ┬┌┐┌ ┌─┐┌─┐┌┬┐┬┌─┐┌┐┌┌─┐ ┌─┐┬─┐┌─┐┌┬┐ ┬ ┬┌─┐┌─┐┬┌─┌─┐
// ├┴┐│ │││ │───││││ ├─┤│ │ ││ ││││└─┐ ├┤ ├┬┘│ ││││ ├─┤│ ││ │├┴┐└─┐
// └─┘└─┘┴┴─┘┴ ┴┘└┘ ┴ ┴└─┘ ┴ ┴└─┘┘└┘└─┘ └ ┴└─└─┘┴ ┴ ┴ ┴└─┘└─┘┴ ┴└─┘
// SECURITY HOOK
// ================================================
// FUTURE: If the specified relative path is "security/grant-csrf-token", then make an assumption and go
// ahead and generate a simple exampl override of the corresponding built-in action from the `security` hook.
// BLUEPRINTS HOOK
// ================================================
// FUTURE: If action stem is "find", and the name of a resource can be inferred from the specified relative
// path, then make an assumption and go ahead and generate a reified version of the corresponding blueprint action.
// FUTURE: If action stem is "findOne", and the name of a resource can be inferred from the specified relative
// path, then make an assumption and go ahead and generate a reified version of the corresponding blueprint action.
// FUTURE: If action stem is "create", and the name of a resource can be inferred from the specified relative
// path, then make an assumption and go ahead and generate a reified version of the corresponding blueprint action.
// FUTURE: If action stem is "update", and the name of a resource can be inferred from the specified relative
// path, then make an assumption and go ahead and generate a reified version of the corresponding blueprint action.
// FUTURE: If action stem is "destroy", and the name of a resource can be inferred from the specified relative
// path, then make an assumption and go ahead and generate a reified version of the corresponding blueprint action.
// ┌─┐┌┬┐┬ ┬┌─┐┬─┐ ┌─┐┬┌┬┐┌─┐┬ ┌─┐ ┌┬┐┬┌┬┐┌─┐ ┌─┐┌─┐┬ ┬┌─┐┬─┐┌─┐
// │ │ │ ├─┤├┤ ├┬┘ └─┐││││├─┘│ ├┤ │ ││││├┤───└─┐├─┤└┐┌┘├┤ ├┬┘└─┐
// └─┘ ┴ ┴ ┴└─┘┴└─ └─┘┴┴ ┴┴ ┴─┘└─┘ ┴ ┴┴ ┴└─┘ └─┘┴ ┴ └┘ └─┘┴└─└─┘
// ACTION THAT SERVES A WEB PAGE
// ================================================
// If action stem begins with "view" (and if it's not just... "view" by itself--
// which would be weird, but whatever), then make an assumption about what to generate.
// (set up the success exit with `viewTemplatePath` ready to go)
if (stem.match(/^view/i) && stem !== 'view') {
// console.log('stem!',_.words(stem).slice(1));
var inferredViewTemplateBasename = _.reduce(_.words(stem).slice(1), function (memo, segment){
// One exception: Don't kebab-case stuff like "v1".
if (segment.match(/[a-z]+[0-9]+/)) {
memo += segment;
}
else {
memo += _.kebabCase(segment);
}
// Attach a hyphen at the end, if there isn't one already.
memo = memo.replace(/\-*$/, '-');
return memo;
}, '');
// Trim the last hyphen off the end.
inferredViewTemplateBasename = inferredViewTemplateBasename.replace(/\-*$/, '');
// console.log('inferredViewTemplateBasename!',inferredViewTemplateBasename);
// If this action is in a subdirectory, apply that subdirectory to our inferred
// view template path:
var intermediateSubdirs = scope.actionName.split('.');
intermediateSubdirs.pop();
// console.log('intermediateSubdirs', intermediateSubdirs);
intermediateSubdirs = intermediateSubdirs.map(function(subdirName){
// One exception: Don't kebab-case words like "v1" within this subdir name, if any.
return _.map(_.words(subdirName), function(word){
if (word.match(/[a-z]+[0-9]+/)) { return word; }
return _.kebabCase(word);
}).join('-');
});//∞
scope.inferredViewTemplatePath = (
path.join('pages/', intermediateSubdirs.join('/'), inferredViewTemplateBasename)
.replace(/\\/g,'/')//« because Windows
);
// Adjust description:
scope.description = (function _gettingAdjustedDescription(){
var words = _.words(sentenceCaseStem).slice(1);
if (_.last(words).match(/page/i)){
words = words.slice(0, -1);
}
var friendlyPageName = words.join(' ');
if (friendlyPageName.length > 1) {
friendlyPageName = friendlyPageName[0].toUpperCase() + friendlyPageName.slice(1);
}
return 'Display "'+friendlyPageName+'" page.';
})();//= (†)
}//>-
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Note: For an example of how to swap out the template dynamically
// (e.g. to inject smarter defaults based on context, or fun easter eggs)
// take a look at the approach here:
// https://github.com/balderdashy/sails-generate/commit/9b8cb6e85bdcc8b19f51976cd3a395d4b2512161
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
return done();
},
// ╔═╗╔═╗╔╦╗╔═╗╦═╗
// ╠═╣╠╣ ║ ║╣ ╠╦╝
// ╩ ╩╚ ╩ ╚═╝╩╚═ooo
after: function (scope, done) {
if (!scope.suppressFinalLog) {
// Disable the "Created a new …!" output so we can use our own instead.
scope.suppressFinalLog = true;
if (scope.actions2) {
var stem = _.last(scope.actionName.split('.'));
// Make some guesses about how to build an appropriate route for this action:
var httpMethodGuess;
var pathPrefixGuess;
var isProbablyOptimizedForSocketRequests;
if (stem.match(/^(find|search|count|check|determine|validate|fetch|navigate|get)/i)) {
httpMethodGuess = 'GET';
pathPrefixGuess = '/api/v1';
} else if (stem.match(/^(subscribe|listen)/i) || stem.match(/and-(subscribe|listen)$/i)) {
httpMethodGuess = 'GET';
pathPrefixGuess = '/api/v1';
isProbablyOptimizedForSocketRequests = true;
} else if (stem.match(/^(download|stream)/i)) {
httpMethodGuess = 'GET';
pathPrefixGuess = '';
} else if (stem.match(/^(view|show)/i)) {
httpMethodGuess = 'GET';
pathPrefixGuess = '';
} else if (stem.match(/^redirect/i) || stem.match(/or-redirect$/i) || stem.match(/and-redirect$/i)) {
httpMethodGuess = 'GET';
pathPrefixGuess = '';
} else if (stem.match(/^(destroy|remove|archive|detach|unlink|delete)/i)) {
httpMethodGuess = 'DELETE';
pathPrefixGuess = '/api/v1';
} else if (stem.match(/^(update|modify|edit|adjust|tweak|patch)/i)) {
httpMethodGuess = 'PATCH';
pathPrefixGuess = '/api/v1';
} else {
httpMethodGuess = 'POST';
pathPrefixGuess = '/api/v1';
}
console.log();
console.log('Successfully generated:');
console.log(' •-','api/controllers/'+scope.relPath);
console.log();
console.log('A few reminders:');
console.log(' (1) For most projects, you\'ll need to manually configure an explicit route');
console.log(' in your `config/routes.js` file; e.g.');
console.log(' \''+httpMethodGuess+' '+pathPrefixGuess+'/'+scope.relPath.replace(/\.js$/,'')+'\': { action: \''+(
scope.relPath.replace(/\\/g,'/')//« because Windows
.replace(/\.js$/,'')
)+'\''+(isProbablyOptimizedForSocketRequests ? ', isSocket: true' : '')+' },');
console.log();
console.log(' (2) If you are using the built-in JavaScript SDK ("Cloud") for AJAX requests');
console.log(' from client-side code, then after configuring a new route, you\'ll want to');
console.log(' regenerate the SDK setup file using:');
console.log(' sails run rebuild-cloud-sdk');
console.log();
console.log(' (3) This new action was generated in the "actions2" format.');
console.log(' [?] https://sailsjs.com/docs/concepts/actions');
console.log();
console.log(' (4) Last but not least, since adding an action or route is a backend change,');
console.log(' don\'t forget to re-lift the server before testing!');
console.log();
}
else {
done.log.info('Created a traditional (req,res) controller action, but as a standalone file.');
}
}
return done();
},
// ╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗
// ║ ╠═╣╠╦╝║ ╦║╣ ║ ╚═╗
// ╩ ╩ ╩╩╚═╚═╝╚═╝ ╩ ╚═╝
targets: {
'./api/controllers/:relPath': {
exec: function(scope, done) {
scope.templatePath = './' + scope.templateName;
builtinTemplate(scope, function(err) {
if (err) {
return done(err);
}
return done();
});
}
}
},
// ╔╦╗╔═╗╔╦╗╔═╗╦ ╔═╗╔╦╗╔═╗╔═╗ ╔╦╗╦╦═╗╔═╗╔═╗╔╦╗╔═╗╦═╗╦ ╦
// ║ ║╣ ║║║╠═╝║ ╠═╣ ║ ║╣ ╚═╗ ║║║╠╦╝║╣ ║ ║ ║ ║╠╦╝╚╦╝
// ╩ ╚═╝╩ ╩╩ ╩═╝╩ ╩ ╩ ╚═╝╚═╝ ═╩╝╩╩╚═╚═╝╚═╝ ╩ ╚═╝╩╚═ ╩
templatesDirectory: path.resolve(__dirname,'./templates'),
};