UNPKG

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