UNPKG

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