sails-generate
Version:
Runner script for sails generators
313 lines (243 loc) • 12.1 kB
JavaScript
/**
* Module dependencies
*/
var util = require('util');
var _ = require('@sailshq/lodash');
var reportback = require('reportback')();
var loadGeneratorDef = require('./load-generator-def');
var generateFolder = require('./builtins/folder');
var generateFileFromTemplate = require('./builtins/template');
var generateJsonFile = require('./builtins/jsonfile');
var generateFileCopy = require('./builtins/copy');
/**
* Known builtins.
* @type {Array}
*/
var KNOWN_BUILTINS = ['exec', 'folder', 'template', 'jsonfile', 'file', 'copy'];
/**
* generateTarget()
*
* Generate the specified target using the provided scope.
* > This module is called from exactly one place: `help-generate.js`
*
* @param {Dictionary} options
* @property {String|Dictionary} target
* @property {Dictionary} scope
* @property {Dictionary} parentGenerator
* @property {String} templatesDirectory
* @property {Function} recursiveGenerate
* a recursive reference to `helpGenerate()`.
*
* @param {Function} done
*/
module.exports = function generateTarget(options, done) {
// Ensure `done` is a reportback, for consistency.
// (This will likely be removed in a future version Sails.)
done = reportback.extend(done);
// Options
var scope = options.scope;
var parentGenerator = options.parent;
var helpGenerateRecursive = options.recursiveGenerate;
// This local var (`target`) will be used to hold our final, resolved target.
var target = options.target;
try {
// Inherit the `templatesDirectory` from the parent Generator's template directory.
// (But only if there is one... otherwise just keep whatever's already on the scope.)
//
// > Notes:
// > + This is always an abs path.
// > + This can still be overridden by this target's definition below.
// > `scope` is being mutated here!
if (!_.isUndefined(parentGenerator.templatesDirectory)) {
scope.templatesDirectory = parentGenerator.templatesDirectory;
}
// A special variable we'll use just in case the logic below inside the `while` loop
// decides it doesn't want us to generate anything at all.
var dontActuallyGenerateThisOne;
// Prevent ∞ iterations.
var MAX_RESOLVES = 5;
var _resolves = 0;
while (true) {
// Try to parse the target.
// If the target is a string, interpret it as the name of an overridable generator,
// so expand it into a dictionary accordingly.
if (_.isString(target)) {
target = {
overridable: target,
generator: target
};
}//>-
// Before moving on, check if this target has a matching `onlyIf`.
// If so, set a variable so we know not to generate it, then break out of the loop.
if (target.onlyIf !== undefined) {
// Check that `onlyIf` syntax is valid.
if (!_.isString(target.onlyIf)) {
throw new Error('Unrecognized generator syntax in targets: ' + util.inspect(target, {depth: null}) + ' (If specified, `onlyIf` must be a string: the name of the scope property to check. If falsey, this target will be skipped.)' );
}
if (!scope[target.onlyIf]) {
dontActuallyGenerateThisOne = true;
break;
}//•
}//fi
// ╦ ╔═╗╔═╗╔╦╗ ╔═╗╦ ╦╔═╗╦═╗╦═╗╦╔╦╗╔═╗
// ║ ║ ║╠═╣ ║║ ║ ║╚╗╔╝║╣ ╠╦╝╠╦╝║ ║║║╣
// ╩═╝╚═╝╩ ╩═╩╝ ╚═╝ ╚╝ ╚═╝╩╚═╩╚═╩═╩╝╚═╝
// ┌─ ┬┌─┐ ┌─┐┬ ┬┌─┐┬─┐┬─┐┬┌┬┐┌─┐┌┐ ┬ ┌─┐ ─┐
// │─── │├┤ │ │└┐┌┘├┤ ├┬┘├┬┘│ ││├─┤├┴┐│ ├┤ ───│
// └─ ┴└ └─┘ └┘ └─┘┴└─┴└─┴─┴┘┴ ┴└─┘┴─┘└─┘ ─┘
// If this target declares itself overridable, then try to load the package for it.
// (If we find one, we'll use that instead of whatever was configured.)
if (target.overridable) {
// Check that `overridable` syntax is valid.
if (!_.isString(target.overridable)) {
throw new Error('Unrecognized generator syntax in targets: ' + util.inspect(target, {depth: null}) + ' (If specified, `overridable` must be a string: the "generator type" to allow this target to be overridden as.)' );
}
// Try to require the appropriate generator of this type.
try {
// If it works, point `target.generator` straight at the generator def.
target = {
generator: loadGeneratorDef({
generatorType: target.overridable,
topLvlRootPath: scope.topLvlRootPath,
modules: scope.modules
})
};
} catch (e) {
switch (e.code) {
// If it could not be loaded, that's ok.
// (We'll still continue below anyway and just use whatever we have in the target otherwise.)
case 'generatorNotFound':
break;
// If an unexpected fatal error occurred, handle it.
default:
throw e;
}
}//</catch>
}//</if :: target.overridable>
//>-•
//
// At this point, if this target could be overridden and we were able
// to load an override, then we've done so already (and updated our target
// accordingly.) And if not, then we haven't.
// Aside:
// The word "overridable" is kind of hard to spell.
// So just in case we see any other likely spelling, trigger
// a fatal error.
//
// > Note that this could eventually be changed over to be a generic
// > "unrecognized property" check. But for now, just cutting to the
// > chase and solving the thing that ate like an hour of my time.
if (!_.isUndefined(target.overidable) || !_.isUndefined(target.overriddable) || !_.isUndefined(target.overiddable) || !_.isUndefined(target.overideable) || !_.isUndefined(target.overrideable) || !_.isUndefined(target.overiddeable)) {
throw new Error('Unrecognized target syntax: Please check your spelling. Did you mean "overridable"?');
}
// Now check if this target is valid.
// If target is pointed at a builtin, then bail now.
// (we can use the target exactly as-is)
var isTargetPointedAtBuiltin = _.isObject(target) && _.intersection(_.keys(target), KNOWN_BUILTINS).length > 0;
if (isTargetPointedAtBuiltin) {
break;
}//-•
var isTargetAGeneratorPrbly = _.isObject(target) && _.isObject(target.generator);
if (isTargetAGeneratorPrbly) {
// IWMIH, then we can assume that this target is referencing a generator (or at least trying to).
// That means we can expect `target.generator` to be present, and to be a valid inline generator
// definition.
// Verify that the `generator` of our target is now a dictionary, like it should be.
if (!_.isPlainObject(target.generator)) {
throw new Error('Generator error: Invalid sub-generator configuration for target "' + scope.rootPath + '": '+util.inspect(target.generator, {depth:null}));
}
// Otherwise, the target's `generator` is a dictionary, so we'll treat it as an inline
// sub-generator definition and finish up.
target = target.generator;
break;
}//-•
// But if it's not, then increment the counter.
// (And check if we've reached MAX_RESOLVES-- if so, then we'll throw and stop.)
_resolves++;
if (_resolves > MAX_RESOLVES) {
throw new Error('Generator error: Could not resolve target "' + scope.rootPath + '" due to a recursive loop. (Exceeded maximum of '+MAX_RESOLVES+' tries.)');
}//-•
}//∞ </while>
// If we decided to straight-up cancel this target, do that now.
if (dontActuallyGenerateThisOne) {
return done();
}//•
// Now interpret generator definition:
// (note that `scope` is being mutated along the way!)
// ╔═╗═╗ ╦╔═╗╔═╗
// ║╣ ╔╩╦╝║╣ ║
// ╚═╝╩ ╚═╚═╝╚═╝
if (_.isFunction(target.exec)) {
target.exec(scope, done);
return;
}//-•
if (!_.isUndefined(target.exec)){ throw new Error('Invalid target syntax: `exec` must be specified as a function.'); }
// ╔═╗╔═╗╔═╗╦ ╦
// ║ ║ ║╠═╝╚╦╝
// ╚═╝╚═╝╩ ╩
if (_.isString(target.copy)) {
_.extend(scope, { templatePath: target.copy });
generateFileCopy(scope, done);
return;
}//-•
if (_.isPlainObject(target.copy)) {
_.merge(scope, target.copy);
generateFileCopy(scope, done);
return;
}//-•
if (!_.isUndefined(target.copy)) { throw new Error('Invalid target syntax: `copy` must be specified as a dictionary or a string.'); }//-•
// ╔═╗╔═╗╦ ╔╦╗╔═╗╦═╗
// ╠╣ ║ ║║ ║║║╣ ╠╦╝
// ╚ ╚═╝╩═╝═╩╝╚═╝╩╚═
if (target.folder) {
_.merge(scope, target.folder);
generateFolder(scope, done);
return;
}//-•
if (!_.isUndefined(target.folder)) { throw new Error('Invalid target syntax: `folder` must be specified as a dictionary.'); }//-•
// ╔╦╗╔═╗╔╦╗╔═╗╦ ╔═╗╔╦╗╔═╗
// ║ ║╣ ║║║╠═╝║ ╠═╣ ║ ║╣
// ╩ ╚═╝╩ ╩╩ ╩═╝╩ ╩ ╩ ╚═╝
if (_.isString(target.template)) {
_.extend(scope, { templatePath: target.template });
generateFileFromTemplate(scope, done);
return;
}//-•
if (_.isPlainObject(target.template)) {
_.merge(scope, target.template);
generateFileFromTemplate(scope, done);
return;
}//-•
if (!_.isUndefined(target.template)) { throw new Error('Invalid target syntax: `template` must be specified as a dictionary or a string.'); }//-•
// ╦╔═╗╔═╗╔╗╔╔═╗╦╦ ╔═╗
// ║╚═╗║ ║║║║╠╣ ║║ ║╣
// ╚╝╚═╝╚═╝╝╚╝╚ ╩╩═╝╚═╝
if (_.isFunction(target.jsonfile)) {
var jsonDataToUse = target.jsonfile(scope);
_.extend(scope, { data: jsonDataToUse });
generateJsonFile(scope, done);
return;
}//-•
if (_.isPlainObject(target.jsonfile)) {
_.merge(scope, target.jsonfile);
generateJsonFile(scope, done);
return;
}//-•
if (!_.isUndefined(target.jsonfile)) { throw new Error('Invalid target syntax: `jsonfile` must be specified as a dictionary or a function.'); }//-•
// ╔═╗╔═╗╔╦╗╔═╗ ╔═╗╔╦╗╦ ╦╔═╗╦═╗ ╔═╗╔═╗╔╗╔╔═╗╦═╗╔═╗╔╦╗╔═╗╦═╗
// ╚═╗║ ║║║║║╣ ║ ║ ║ ╠═╣║╣ ╠╦╝ ║ ╦║╣ ║║║║╣ ╠╦╝╠═╣ ║ ║ ║╠╦╝
// ╚═╝╚═╝╩ ╩╚═╝ ╚═╝ ╩ ╩ ╩╚═╝╩╚═ ╚═╝╚═╝╝╚╝╚═╝╩╚═╩ ╩ ╩ ╚═╝╩╚═
// If we made it here, this must be a recursive generator.
// So real quick, check to make sure maxHops has not been exceeded.
if (++scope._depth > scope.maxHops) {
throw new Error('`maxHops` (' + scope.maxHops + ' exceeded! There is probably a recursive loop in one of your generators.');
}//-•
} catch (e) { return done(e); }
// And now that the generator target has been resolved,
// call this method recursively on it, passing along our
// callback:
helpGenerateRecursive(target, scope, function (err) {
if (err) { return done(err); }
return done();
});
};