base.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. 'use strict';
  2. /**
  3. * Base prompt implementation
  4. * Should be extended by prompt types.
  5. */
  6. const _ = {
  7. defaults: require('lodash/defaults'),
  8. clone: require('lodash/clone'),
  9. };
  10. const chalk = require('chalk');
  11. const runAsync = require('run-async');
  12. const { filter, flatMap, share, take, takeUntil } = require('rxjs/operators');
  13. const Choices = require('../objects/choices');
  14. const ScreenManager = require('../utils/screen-manager');
  15. class Prompt {
  16. constructor(question, rl, answers) {
  17. // Setup instance defaults property
  18. Object.assign(this, {
  19. answers,
  20. status: 'pending',
  21. });
  22. // Set defaults prompt options
  23. this.opt = _.defaults(_.clone(question), {
  24. validate: () => true,
  25. validatingText: '',
  26. filter: (val) => val,
  27. filteringText: '',
  28. when: () => true,
  29. suffix: '',
  30. prefix: chalk.green('?'),
  31. });
  32. // Make sure name is present
  33. if (!this.opt.name) {
  34. this.throwParamError('name');
  35. }
  36. // Set default message if no message defined
  37. if (!this.opt.message) {
  38. this.opt.message = this.opt.name + ':';
  39. }
  40. // Normalize choices
  41. if (Array.isArray(this.opt.choices)) {
  42. this.opt.choices = new Choices(this.opt.choices, answers);
  43. }
  44. this.rl = rl;
  45. this.screen = new ScreenManager(this.rl);
  46. }
  47. /**
  48. * Start the Inquiry session and manage output value filtering
  49. * @return {Promise}
  50. */
  51. run() {
  52. return new Promise((resolve, reject) => {
  53. this._run(
  54. (value) => resolve(value),
  55. (error) => reject(error)
  56. );
  57. });
  58. }
  59. // Default noop (this one should be overwritten in prompts)
  60. _run(cb) {
  61. cb();
  62. }
  63. /**
  64. * Throw an error telling a required parameter is missing
  65. * @param {String} name Name of the missing param
  66. * @return {Throw Error}
  67. */
  68. throwParamError(name) {
  69. throw new Error('You must provide a `' + name + '` parameter');
  70. }
  71. /**
  72. * Called when the UI closes. Override to do any specific cleanup necessary
  73. */
  74. close() {
  75. this.screen.releaseCursor();
  76. }
  77. /**
  78. * Run the provided validation method each time a submit event occur.
  79. * @param {Rx.Observable} submit - submit event flow
  80. * @return {Object} Object containing two observables: `success` and `error`
  81. */
  82. handleSubmitEvents(submit) {
  83. const self = this;
  84. const validate = runAsync(this.opt.validate);
  85. const asyncFilter = runAsync(this.opt.filter);
  86. const validation = submit.pipe(
  87. flatMap((value) => {
  88. this.startSpinner(value, this.opt.filteringText);
  89. return asyncFilter(value, self.answers).then(
  90. (filteredValue) => {
  91. this.startSpinner(filteredValue, this.opt.validatingText);
  92. return validate(filteredValue, self.answers).then(
  93. (isValid) => ({ isValid, value: filteredValue }),
  94. (err) => ({ isValid: err, value: filteredValue })
  95. );
  96. },
  97. (err) => ({ isValid: err })
  98. );
  99. }),
  100. share()
  101. );
  102. const success = validation.pipe(
  103. filter((state) => state.isValid === true),
  104. take(1)
  105. );
  106. const error = validation.pipe(
  107. filter((state) => state.isValid !== true),
  108. takeUntil(success)
  109. );
  110. return {
  111. success,
  112. error,
  113. };
  114. }
  115. startSpinner(value, bottomContent) {
  116. value = this.getSpinningValue(value);
  117. // If the question will spin, cut off the prefix (for layout purposes)
  118. const content = bottomContent
  119. ? this.getQuestion() + value
  120. : this.getQuestion().slice(this.opt.prefix.length + 1) + value;
  121. this.screen.renderWithSpinner(content, bottomContent);
  122. }
  123. /**
  124. * Allow override, e.g. for password prompts
  125. * See: https://github.com/SBoudrias/Inquirer.js/issues/1022
  126. *
  127. * @return {String} value to display while spinning
  128. */
  129. getSpinningValue(value) {
  130. return value;
  131. }
  132. /**
  133. * Generate the prompt question string
  134. * @return {String} prompt question string
  135. */
  136. getQuestion() {
  137. let message =
  138. (this.opt.prefix ? this.opt.prefix + ' ' : '') +
  139. chalk.bold(this.opt.message) +
  140. this.opt.suffix +
  141. chalk.reset(' ');
  142. // Append the default if available, and if question isn't touched/answered
  143. if (
  144. this.opt.default != null &&
  145. this.status !== 'touched' &&
  146. this.status !== 'answered'
  147. ) {
  148. // If default password is supplied, hide it
  149. if (this.opt.type === 'password') {
  150. message += chalk.italic.dim('[hidden] ');
  151. } else {
  152. message += chalk.dim('(' + this.opt.default + ') ');
  153. }
  154. }
  155. return message;
  156. }
  157. }
  158. module.exports = Prompt;