prompt.js 3.7 KB

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