base.js 4.5 KB

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