hence-inquisitor
Version:
A streamlined scaffolding installer utility, incorporating inquirer and glup-utils at it's core.
320 lines (281 loc) • 11.7 kB
JavaScript
// # Scaffold
//
// Scaffold is used to define your installer.
var _ = require('lodash');
var async = require('async');
var ascii = require('./ascii');
var beep = require('gulp-util').beep;
var multiInstall = require('./scaffold/multiInstall');
var env = require('gulp-util').env;
// ```Scaffold(config)```
// The Scaffold constructor builds your installer object, taking in an optional configuration argument.
// * config - Defines the setting and method to be used in processing an installation.
// * returns - The configured scaffold installer to be consumed.
/**
* @param {Object} config
* @returns {Object}
*/
var Scaffold = function (config) {
var scaffold = _.cloneDeep(config || {});
// ## Default Members
_.defaultsDeep(scaffold, {
// **Steps**
// These define the different steps, each with their own set of prompts and defaults, which are processed to
// culminate a pool of answers to be used for the installation.
steps: [],
// **Defaults**
// The default answers or globally shared attributes between all of the steps.
// * files - The glob file patterns to be used by the installer.
// * aborted - Toggling whether or not the process should be aborted.
// * skipToInstall - A special toggle used for multi-installers, which allows bypassing the steps prompts to assume and process their defaults and run the installer off the hop.
defaults: {
files: [],
aborted: false,
skipToInstall: false
},
// **Answers**
// The accumulated set of answers/responses from the defaults here, and results of the various steps in this
// installer.
answers: {},
// **Inquirer**
// A set of helpers and checks provided to the step prompts, allowing you to make custom when/validate checks
// while defining them globally on the install for all steps to have access to.
inquirer: require('./inquirer'),
// **Content**
// Messages to help control the flow of the installer, allowing you to provide an introduction, handle a custom
// message on user manually aborting the process, and any final messages to convey when the process has
// completed successfully
content: {
intro: '',
aborted: " We're sorry you decided to stop here, but hope to see you again soon!",
done: ''
},
// **cliArg(arg)**
// Process cli args to be used in a multi-install.
cliArg: function (arg) {
return {};
},
// **Debug**
// A flag to toggle whether or not to display some lower level processing details, helpful when first building
// your installers should run into issues.
debug: !!env.debug
});
// ## Placeholder Methods
// These methods ensure the scaffold will run effectively without any additional tweaking beyond providing an
// install method, however you are free to adjust and provide you own overrides to any of these as needed.
_.defaultsDeep(scaffold, {
// ```postInstall(answers, finalize)```
// This method is fired following the completion of the installation, if stream based once the end event is emitted.
// * answers - Serving the scaffold's answer object for ease of use, vs typing ```var answers = this.answers;```
// * finalize - A callback to finish up the installer, outputting any final messages and execute the done function.
postInstall: function (answers, finalize) {
finalize();
},
// ```finalize(err)```
// This method outs any final messages, or errors, and finally execute the done function.
finalize: function (err) {
if (err) {
return this.error(err);
} else if (this.content.done) {
ascii.spacer(true);
if (this.content.done !== ascii.spacer()) {
ascii.done(this.content.done, true);
}
}
this.done(err, this.answers);
beep();
},
// ```done(err, answers)```
// Should no done method be configured from a cli/gulp/slush, this ensures the flow is processed without issue.
done: function (err, answers) {
}
});
// ## Essential Methods
_.extend(scaffold, {
// ```addStep(step)```
// This method will add more steps to the installer.
/**
* @param {Object|Array} step
*/
addStep: function (step) {
var self = this;
if (step) {
if (!_.isArray(step)) {
step = _.compact([step]);
}
if (step.length) {
_.each(step, function (s) {
self.steps.push(s);
});
}
}
},
// ```processSteps(step)```
// This method will configure the steps for installer.
processSteps: function () {
var self = this;
var steps = self.steps;
var index = 0;
var err;
if (steps.length) {
_.each(steps, function (step) {
// Ensure that the step has the essential inquire function to preform the installer's waterfall
if (_.isFunction(step.inquire)) {
// Include a reference back to the scaffold in the step.
step.scaffold = self;
// Include a reference to the inquirer checks and validation to be readily accessible by the step's prompts.
step.inquirer = self.inquirer;
// The current index of the step against the whole. Since this is using in an async waterfall, this isn't
// available as a normal array index and must be explicit.
step.stepIndex = index++;
// This builds the needed waterfall callback method, leveraging the steps inquire function to be
// used by the scaffold when processing all of the steps to build their answers.
step.waterfall = function (answers, callback) {
if (self.debug) {
console.log('>> Starting waterfall for:', step);
}
step.inquire.call(step, answers, callback);
};
} else { // Flag this step for error diagnosis.
err = step;
}
});
}
if (err) {
// Flag the malformed step so the scaffold author can diagnose the issue.
self.error('Your ScaffoldStep is mis-configured. Please ensure it contains an inquire function for' +
' prompting.' + JSON.stringify(err));
}
return !err;
},
run: function (done, args) {
// Since we can trust how this method is called, this is unavailable, and we must use the original object,
// which is already modified and configured fully.
var self = scaffold;
// Because glush leverages gulp-util, the .env for cli args is available
// We must always drop the first non-flagged arg, as it's always your generator's name
// This doesn't count actual flags set '--flag', just normal args on the cli
var cliArgs = args || _.drop(env._);
/* istanbul ignore next */
if (self.debug) {
console.log('debug args', args, cliArgs, self);
}
if (cliArgs.length) {
self.startMultiInstall.call(self, _.map(cliArgs, self.cliArg, self), done);
}
else {
self.start.call(self, done);
}
},
// ```start(steps, overrides, done)```
// This method begins your installation, running through all of your steps, and finally on completion executing
// the done method provided.
//
// * steps - An array of ScaffoldSteps, or a single ScaffoldStep object, to provide prompts for your install to act upon
// * overrides - Any run time/CLI overrides that you'd like to pass in to affect your installers out coming or defaults.
// * done - The final callback to execute when the installation has finished, successfully or not, to tie up streams or asyncs.
start: function () {
var self = this;
// Arguments
var arg0 = arguments[0];
var arg1 = arguments[1];
var steps = _.isArray(arg0) ? arg0 : (_.isObject(arg0) && _.has(arg0, 'inquire') ? arg0 : undefined);
var overrides = !_.isArray(arg0) && _.isObject(arg0) && !_.has(arg0, 'inquire') ? arg0 : (_.isObject(arg1) ? arg1 : undefined);
var done = arguments[2] || (_.isFunction(arg1) ? arg1 : (_.isFunction(arg0) ? arg0 : undefined));
if (overrides) {
self = _.defaultsDeep(overrides, self);
}
if (_.isFunction(done)) {
self.done = done;
}
// Ensure that done only ever runs once. This can cause issues with streams should it run multiple times causing
// abnormal behaviour.
self.done = _.once(self.done);
/* istanbul ignore next */
if (self.debug) {
console.log('>> Start installer with defaults of: ', _.pick(self, 'content', 'defaults'));
}
self.renderIntro();
self.addStep.call(self, steps);
if (self.processSteps.call(self)) {
// Pull the inquire functions together from the various steps, and remove any undefined as not to disrupt async
if (self.steps.length && !self.answers.aborted) {
var prompters = _.chain(self.steps).pluck('waterfall').compact().value();
async.waterfall(prompters, function (err, answers) {
self.preInstall.call(self, err, answers);
});
} else if (_.isFunction(self.install)) {
self.install.call(self, self.answers, function (err, stream) {
self.installResolve.call(self, err, stream);
});
} else {
return self.finalize.call(self);
}
}
},
renderIntro: function () {
if (this.content.intro) {
console.log(this.content.intro);
beep();
}
},
startMultiInstall: function () {
// Arguments
var arg0 = arguments[0];
var arg1 = arguments[1];
var isArgZeroAnArray = _.isArray(arg0) && arg0.length && _.isObject(arg0[0]);
var steps = isArgZeroAnArray && _.has(arg0[0], 'inquire') ? arg0 : (_.isObject(arg0) && _.has(arg0, 'inquire') ? arg0 : undefined);
var installOptions = isArgZeroAnArray && !_.has(arg0[0], 'inquire') ? arg0 : (_.isArray(arg1) ? arg1 : undefined);
var done = arguments[2] || (_.isFunction(arg1) ? arg1 : (_.isFunction(arg0) ? arg0 : undefined));
multiInstall(this, steps, installOptions, done);
},
preInstall: function (err, answers) {
var self = this;
if (err) {
return self.error(err);
}
_.extend(self.answers, answers);
ascii.spacer(true);
if (self.answers.aborted) {
ascii.aborted(self.content.aborted, true);
return self.done(); // don't use finalize when aborting, end things here
}
else if (_.isFunction(self.install)) {
self.install.call(self, self.answers, function (err, stream) {
self.installResolve.call(self, err, stream);
});
} else {
return self.finalize.call(self);
}
},
installResolve: function (err, stream) {
var self = this;
if (err) {
return self.error(err);
}
// Let the user know we're all done, and provide info on how they can learn to use the gulp commands.
if (stream) {
return stream.on('end', function () {
self.postInstall(self.answers, function (err) {
self.finalize.call(self, err);
});
});
}
return self.postInstall(self.answers, function (err) {
self.finalize.call(self, err);
});
},
error: function (err) {
var errMsg = 'An error occurred: ';
if (_.isObject(err) && err.message) {
errMsg += err.message;
} else {
errMsg += err;
}
ascii.aborted(errMsg, true);
return this.done(err);
}
});
return scaffold;
};
module.exports = Scaffold;