src/services/common/prompt.js
const promptTool = require('prompt');
const { provider } = require('jimple');
/**
* This services works as an abstraction of the `prompt` package in order to add support for
* Promises, fix some quirks regarding boolean options, customize the interface just once and,
* finally, integrate it withe Jimple.
*/
class Prompt {
/**
* Class constructor.
* @param {string} [messagesPrefix=''] A prefix text that will be shown before each message.
*/
constructor(messagesPrefix = '') {
// Overwrite the default prefix.
promptTool.message = messagesPrefix;
/**
* Set a single space as a delimiter between the messages components (prefix, question and
* default value).
*/
promptTool.delimiter = ' ';
// Disable colors because `prompt` has a hardcoded gray on the texts.
promptTool.colors = false;
}
/**
* Invoke the `prompt` package and ask the user for input.
* @param {Object} schema The input data schema. For more information on how to build it, you
* should check the `prompt` package documentation, it's pretty complete.
* IMPORTANT: On the `prompt` documentation, this object would be the
* `properties` inside their `schema` object.
* @return {Promise<Object,Error>} If everything goes well, the resolved value is an object with
* the values the user entered; and if the user cancels the input,
* you'll get an error with the message `canceled`.
*/
ask(schema) {
// Copy the schema into a new object in order to modify it.
const newSchema = Object.assign({}, schema);
// Loop all the properties.
Object.keys(newSchema).forEach((name) => {
const property = newSchema[name];
// If the property type is `boolean`, use the helper method to add the validation properties.
if (property.type === 'boolean') {
newSchema[name] = this._booleanHelper(property);
}
});
// Return a _"promisified"_ implementation of `prompt`.
return new Promise((resolve, reject) => {
promptTool.get({ properties: newSchema }, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
/**
* Access the history of the prompt.
* @param {string} property The name of the property you want to look on the history.
* @return {?Object} If the property is present, it will return an object with the name as
* `property` and its `value`, otherwise it will return `undefined`.
*/
history(property) {
return promptTool.history(property);
}
/**
* Get a property value from the history.
* @param {string} property The name of the property.
* @return {?string} If the property is on the history, it will return its value, otherwise it
* will return `undefined`.
*/
getValue(property) {
const saved = this.history(property);
let result;
if (saved && typeof saved.value !== 'undefined') {
result = saved.value;
}
return result;
}
/**
* The default implementation of boolean properties is not very friendly and it only accepts
* `true`, `t`, `false` or `f`, which is not _"human friendly"_, so this method changes boolean
* properties into string properties and add validations for `yes`, `y`, `no` and `n`. It also
* _"booleanizes"_ the input so when the results are resolved, the value will be a real `boolean`.
* @param {Object} property The property to format.
* @return {Object} The updated property.
* @ignore
* @access protected
*/
_booleanHelper(property) {
return Object.assign({}, property, {
type: 'string',
message: 'You can only answer with \'yes\' or \'no\'',
conform: (value) => ['yes', 'y', 'no', 'n'].includes(value.toLowerCase()),
before: (value) => ['yes', 'y'].includes(value.toLowerCase()),
});
}
}
/**
* Generates a `Provider` with an already defined message prefix.
* @example
* // Generate the provider
* const provider = promptWithOptions('my-prefix');
* // Register it on the container
* container.register(provider);
* // Getting access to the service instance
* const prompt = container.get('prompt');
* @param {string} [messagesPrefix] A prefix to include in front of all the messages.
* @return {Provider}
*/
const promptWithOptions = (messagesPrefix) => provider((app) => {
app.set('prompt', () => new Prompt(messagesPrefix));
});
/**
* The service provider that once registered on the app container will set an instance of
* `Prompt` as the `prompt` service.
* @example
* // Register it on the container
* container.register(prompt);
* // Getting access to the service instance
* const prompt = container.get('prompt');
* @type {Provider}
*/
const prompt = promptWithOptions();
/**
* The service provider that once registered on the app container will set an instance of
* `Prompt` as the `appPrompt` service. The difference with the regular `prompt` is that this one
* uses the `packageInfo` service in order to retrieve the name of the project and use it as
* messages prefix.
* @example
* // Register it on the container
* container.register(appPrompt);
* // Getting access to the service instance
* const appPrompt = container.get('appPrompt');
* @type {Provider}
*/
const appPrompt = provider((app) => {
app.set('appPrompt', () => {
const packageInfo = app.get('packageInfo');
const prefix = packageInfo.nameForCLI || packageInfo.name;
return new Prompt(prefix);
});
});
module.exports = {
Prompt,
promptWithOptions,
prompt,
appPrompt,
};