base.js 4.54 KB
'use strict';
/**
 * Base prompt implementation
 * Should be extended by prompt types.
 */
const _ = {
  defaults: require('lodash/defaults'),
  clone: require('lodash/clone'),
};
const chalk = require('chalk');
const runAsync = require('run-async');
const { filter, flatMap, share, take, takeUntil } = require('rxjs/operators');
const Choices = require('../objects/choices');
const ScreenManager = require('../utils/screen-manager');

class Prompt {
  constructor(question, rl, answers) {
    // Setup instance defaults property
    Object.assign(this, {
      answers,
      status: 'pending',
    });

    // Set defaults prompt options
    this.opt = _.defaults(_.clone(question), {
      validate: () => true,
      validatingText: '',
      filter: (val) => val,
      filteringText: '',
      when: () => true,
      suffix: '',
      prefix: chalk.green('?'),
    });

    // Make sure name is present
    if (!this.opt.name) {
      this.throwParamError('name');
    }

    // Set default message if no message defined
    if (!this.opt.message) {
      this.opt.message = this.opt.name + ':';
    }

    // Normalize choices
    if (Array.isArray(this.opt.choices)) {
      this.opt.choices = new Choices(this.opt.choices, answers);
    }

    this.rl = rl;
    this.screen = new ScreenManager(this.rl);
  }

  /**
   * Start the Inquiry session and manage output value filtering
   * @return {Promise}
   */

  run() {
    return new Promise((resolve, reject) => {
      this._run(
        (value) => resolve(value),
        (error) => reject(error)
      );
    });
  }

  // Default noop (this one should be overwritten in prompts)
  _run(cb) {
    cb();
  }

  /**
   * Throw an error telling a required parameter is missing
   * @param  {String} name Name of the missing param
   * @return {Throw Error}
   */

  throwParamError(name) {
    throw new Error('You must provide a `' + name + '` parameter');
  }

  /**
   * Called when the UI closes. Override to do any specific cleanup necessary
   */
  close() {
    this.screen.releaseCursor();
  }

  /**
   * Run the provided validation method each time a submit event occur.
   * @param  {Rx.Observable} submit - submit event flow
   * @return {Object}        Object containing two observables: `success` and `error`
   */
  handleSubmitEvents(submit) {
    const self = this;
    const validate = runAsync(this.opt.validate);
    const asyncFilter = runAsync(this.opt.filter);
    const validation = submit.pipe(
      flatMap((value) => {
        this.startSpinner(value, this.opt.filteringText);
        return asyncFilter(value, self.answers).then(
          (filteredValue) => {
            this.startSpinner(filteredValue, this.opt.validatingText);
            return validate(filteredValue, self.answers).then(
              (isValid) => ({ isValid, value: filteredValue }),
              (err) => ({ isValid: err, value: filteredValue })
            );
          },
          (err) => ({ isValid: err })
        );
      }),
      share()
    );

    const success = validation.pipe(
      filter((state) => state.isValid === true),
      take(1)
    );
    const error = validation.pipe(
      filter((state) => state.isValid !== true),
      takeUntil(success)
    );

    return {
      success,
      error,
    };
  }

  startSpinner(value, bottomContent) {
    value = this.getSpinningValue(value);
    // If the question will spin, cut off the prefix (for layout purposes)
    const content = bottomContent
      ? this.getQuestion() + value
      : this.getQuestion().slice(this.opt.prefix.length + 1) + value;

    this.screen.renderWithSpinner(content, bottomContent);
  }

  /**
   * Allow override, e.g. for password prompts
   * See: https://github.com/SBoudrias/Inquirer.js/issues/1022
   *
   * @return {String} value to display while spinning
   */
  getSpinningValue(value) {
    return value;
  }

  /**
   * Generate the prompt question string
   * @return {String} prompt question string
   */
  getQuestion() {
    let message =
      (this.opt.prefix ? this.opt.prefix + ' ' : '') +
      chalk.bold(this.opt.message) +
      this.opt.suffix +
      chalk.reset(' ');

    // Append the default if available, and if question isn't touched/answered
    if (
      this.opt.default != null &&
      this.status !== 'touched' &&
      this.status !== 'answered'
    ) {
      // If default password is supplied, hide it
      if (this.opt.type === 'password') {
        message += chalk.italic.dim('[hidden] ');
      } else {
        message += chalk.dim('(' + this.opt.default + ') ');
      }
    }

    return message;
  }
}

module.exports = Prompt;