prompt.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. 'use strict';
  2. const _ = {
  3. isPlainObject: require('lodash/isPlainObject'),
  4. get: require('lodash/get'),
  5. set: require('lodash/set'),
  6. };
  7. const { defer, empty, from, of } = require('rxjs');
  8. const { concatMap, filter, publish, reduce } = require('rxjs/operators');
  9. const runAsync = require('run-async');
  10. const utils = require('../utils/utils');
  11. const Base = require('./baseUI');
  12. /**
  13. * Base interface class other can inherits from
  14. */
  15. class PromptUI extends Base {
  16. constructor(prompts, opt) {
  17. super(opt);
  18. this.prompts = prompts;
  19. }
  20. run(questions, answers) {
  21. // Keep global reference to the answers
  22. if (_.isPlainObject(answers)) {
  23. this.answers = { ...answers };
  24. } else {
  25. this.answers = {};
  26. }
  27. // Make sure questions is an array.
  28. if (_.isPlainObject(questions)) {
  29. // It's either an object of questions or a single question
  30. questions = Object.values(questions).every(
  31. (v) => _.isPlainObject(v) && v.name === undefined
  32. )
  33. ? Object.entries(questions).map(([name, question]) => ({ name, ...question }))
  34. : [questions];
  35. }
  36. // Create an observable, unless we received one as parameter.
  37. // Note: As this is a public interface, we cannot do an instanceof check as we won't
  38. // be using the exact same object in memory.
  39. const obs = Array.isArray(questions) ? from(questions) : questions;
  40. this.process = obs.pipe(
  41. concatMap(this.processQuestion.bind(this)),
  42. publish() // Creates a hot Observable. It prevents duplicating prompts.
  43. );
  44. this.process.connect();
  45. return this.process
  46. .pipe(
  47. reduce((answers, answer) => {
  48. _.set(answers, answer.name, answer.answer);
  49. return answers;
  50. }, this.answers)
  51. )
  52. .toPromise(Promise)
  53. .then(this.onCompletion.bind(this), this.onError.bind(this));
  54. }
  55. /**
  56. * Once all prompt are over
  57. */
  58. onCompletion() {
  59. this.close();
  60. return this.answers;
  61. }
  62. onError(error) {
  63. this.close();
  64. return Promise.reject(error);
  65. }
  66. processQuestion(question) {
  67. question = { ...question };
  68. return defer(() => {
  69. const obs = of(question);
  70. return obs.pipe(
  71. concatMap(this.setDefaultType.bind(this)),
  72. concatMap(this.filterIfRunnable.bind(this)),
  73. concatMap(() =>
  74. utils.fetchAsyncQuestionProperty(question, 'message', this.answers)
  75. ),
  76. concatMap(() =>
  77. utils.fetchAsyncQuestionProperty(question, 'default', this.answers)
  78. ),
  79. concatMap(() =>
  80. utils.fetchAsyncQuestionProperty(question, 'choices', this.answers)
  81. ),
  82. concatMap(this.fetchAnswer.bind(this))
  83. );
  84. });
  85. }
  86. fetchAnswer(question) {
  87. const Prompt = this.prompts[question.type];
  88. this.activePrompt = new Prompt(question, this.rl, this.answers);
  89. return defer(() =>
  90. from(this.activePrompt.run().then((answer) => ({ name: question.name, answer })))
  91. );
  92. }
  93. setDefaultType(question) {
  94. // Default type to input
  95. if (!this.prompts[question.type]) {
  96. question.type = 'input';
  97. }
  98. return defer(() => of(question));
  99. }
  100. filterIfRunnable(question) {
  101. if (
  102. question.askAnswered !== true &&
  103. _.get(this.answers, question.name) !== undefined
  104. ) {
  105. return empty();
  106. }
  107. if (question.when === false) {
  108. return empty();
  109. }
  110. if (typeof question.when !== 'function') {
  111. return of(question);
  112. }
  113. const { answers } = this;
  114. return defer(() =>
  115. from(
  116. runAsync(question.when)(answers).then((shouldRun) => {
  117. if (shouldRun) {
  118. return question;
  119. }
  120. })
  121. ).pipe(filter((val) => val != null))
  122. );
  123. }
  124. }
  125. module.exports = PromptUI;