From 89ae0b9f5d0f4f60eb89f1bf586c78b0420f3fac Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 3 Mar 2018 18:32:38 -0800 Subject: [PATCH 1/8] consolidate all prompts into single helper --- lib/prompts.js | 216 ++++++++++++++++++++++--------------------------- 1 file changed, 97 insertions(+), 119 deletions(-) diff --git a/lib/prompts.js b/lib/prompts.js index fd209b22..c7ac1475 100644 --- a/lib/prompts.js +++ b/lib/prompts.js @@ -1,186 +1,164 @@ 'use strict'; const el = require('./elements'); -const noop = () => {}; +const noop = v => v; + +function toPrompt(type, args, opts={}) { + if (typeof args.message !== 'string') { + throw new Error('message is required'); + } + + return new Promise((res, rej) => { + const p = new el[type](args); + const onAbort = opts.onAbort || noop; + const onSubmit = opts.onSubmit || noop; + p.on('state', args.onState || noop); + p.on('submit', x => res(onSubmit(x))); + p.on('abort', x => rej(onAbort(x))); + }); +} /** * Text prompt - * @param {string} message Prompt message to display - * @param {string} [initial] Default string value - * @param {string} [style="default"] Render style ('default', 'password', 'invisible') - * @param {function} [onState] On state change callback + * @param {string} args.message Prompt message to display + * @param {string} [args.initial] Default string value + * @param {string} [args.style="default"] Render style ('default', 'password', 'invisible') + * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function text({ message, initial, style, onState = noop }) { - if (typeof message !== 'string') throw new Error('message is required'); - return new Promise((resolve, reject) => { - const p = new el.TextPrompt({ message, initial, style }); - p.on('submit', resolve); - p.on('abort', reject); - p.on('state', onState) - }); +function text(args) { + return toPrompt('TextPrompt', args); } /** * Password prompt with masked input - * @param {string} message Prompt message to display - * @param {string} [initial] Default string value - * @param {function} [onState] On state change callback + * @param {string} args.message Prompt message to display + * @param {string} [args.initial] Default string value + * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input * */ -const password = ({ message, initial, onState }) => text({ message, initial, onState, style: 'password' }); +function password(args) { + args.style = 'password'; + return text(args); +} /** * Prompt where input is invisible, like sudo - * @param {string} message Prompt message to display - * @param {string} [initial] Default string value - * @param {function} [onState] On state change callback + * @param {string} opts.message Prompt message to display + * @param {string} [opts.initial] Default string value + * @param {function} [opts.onState] On state change callback * @returns {Promise} Promise with user input */ -const invisible = ({ message, initial, onState }) => text({ message, initial, onState, style: 'invisible' }); +function invisible(opts) { + opts.style = 'invisible'; + return text(opts); +} /** * Number prompt - * @param {string} message Prompt message to display - * @param {number} initial Default number value - * @param {number} [max] Max value - * @param {number} [min] Min value - * @param {string} [style="default"] Render style ('default', 'password', 'invisible') - * @param {function} [onState] On state change callback + * @param {string} args.message Prompt message to display + * @param {number} args.initial Default number value + * @param {number} [args.max] Max value + * @param {number} [args.min] Min value + * @param {string} [args.style="default"] Render style ('default', 'password', 'invisible') + * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function number({ message, initial, max, min, style, onState = noop }) { - if (typeof message !== 'string') throw new Error('message is required'); - return new Promise((resolve, reject) => { - const p = new el.NumberPrompt({ message, initial, max, min, style }); - p.on('submit', resolve); - p.on('abort', reject); - p.on('state', onState) - }); +function number(args) { + return toPrompt('NumberPrompt', args); } /** * Classic yes/no prompt - * @param {string} message Prompt message to display - * @param {boolean} [initial=false] Default value - * @param {function} [onState] On state change callback + * @param {string} args.message Prompt message to display + * @param {boolean} [args.initial=false] Default value + * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function confirm({ message, initial, onState = noop }) { - if (typeof message !== 'string') throw new Error('message is required'); - return new Promise((resolve, reject) => { - const p = new el.ConfirmPrompt({ message, initial }); - p.on('submit', resolve); - p.on('abort', reject); - p.on('state', onState); - }); +function confirm(args) { + return toPrompt('ConfirmPrompt', args); } /** * List prompt, split intput string by `seperator` - * @param {string} message Prompt message to display - * @param {string} [initial] Default string value - * @param {string} [style="default"] Render style ('default', 'password', 'invisible') - * @param {string} [separator] String separator - * @param {function} [onState] On state change callback + * @param {string} args.message Prompt message to display + * @param {string} [args.initial] Default string value + * @param {string} [args.style="default"] Render style ('default', 'password', 'invisible') + * @param {string} [args.separator] String separator + * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input, in form of an `Array` */ -function list({ message, initial, style, separator = ',', onState = noop }) { - if (typeof message !== 'string') throw new Error('message is required'); - return new Promise((resolve, reject) => { - const p = new el.TextPrompt({ message, initial, style }); - p.on('submit', str => resolve(str.split(separator).map(s => s.trim()))); - p.on('abort', reject); - p.on('state', onState); +function list(args) { + const sep = args.separator || ','; + return toPrompt('TextPrompt', args, { + onSubmit: str => str.split(sep).map(s => s.trim()) }); } /** * Toggle/switch prompt - * @param {string} message Prompt message to display - * @param {boolean} [initial=false] Default value - * @param {string} [active="on"] Text for `active` state - * @param {string} [inactive="off"] Text for `inactive` state - * @param {function} [onState] On state change callback + * @param {string} args.message Prompt message to display + * @param {boolean} [args.initial=false] Default value + * @param {string} [args.active="on"] Text for `active` state + * @param {string} [args.inactive="off"] Text for `inactive` state + * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function toggle({ message, initial, active, inactive, onState = noop }) { - if (typeof message !== 'string') throw new Error('message is required'); - return new Promise((resolve, reject) => { - const p = new el.TogglePrompt({ message, initial, active, inactive }); - p.on('submit', resolve); - p.on('abort', reject); - p.on('state', onState); - }); +function toggle(args) { + return toPrompt('TogglePrompt', args); } /** * Interactive select prompt - * @param {string} message Prompt message to display - * @param {Array} choices Array of choices objects `[{ title, value }, ...]` - * @param {number} [initial] Index of default value - * @param {function} [onState] On state change callback + * @param {string} arr.message Prompt message to display + * @param {Array} arr.choices Array of choices objects `[{ title, value }, ...]` + * @param {number} [arr.initial] Index of default value + * @param {function} [arr.onState] On state change callback * @returns {Promise} Promise with user input */ -function select({ message, choices, initial, onState = noop }) { - if (typeof message !== 'string') throw new Error('message is required'); - return new Promise((resolve, reject) => { - const p = new el.SelectPrompt({ message, choices, initial }); - p.on('submit', resolve); - p.on('abort', reject); - p.on('state', onState); - }); +function select(args) { + return toPrompt('SelectPrompt', args); } /** * Interactive multi-select prompt - * @param {string} message Prompt message to display - * @param {Array} choices Array of choices objects `[{ title, value, [selected] }, ...]` - * @param {number} [max] Max select - * @param {string} [hint] Hint to display user - * @param {function} [onState] On state change callback + * @param {string} args.message Prompt message to display + * @param {Array} args.choices Array of choices objects `[{ title, value, [selected] }, ...]` + * @param {number} [args.max] Max select + * @param {string} [args.hint] Hint to display user + * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function multiselect({ message, choices, max, hint, onState = noop }) { - if (typeof message !== 'string') throw new Error('message is required'); - if (!Array.isArray(choices)) throw new Error('choices array is required'); - - return new Promise((resolve, reject) => { - const p = new el.MultiselectPrompt({ message, choices, max, hint }); - const selected = items => items.filter(item => item.selected).map(item => item.value); - p.on('submit', items => resolve(selected(items))); - p.on('abort', items => reject(selected(items))); - p.on('state', onState); +function multiselect(args) { + if (!Array.isArray(args.choices)) throw new Error('choices array is required'); + const toSelected = items => items.filter(item => item.selected).map(item => item.value); + return toPrompt('MultiselectPrompt', args, { + onAbort: toSelected, + onSubmit: toSelected }); } /** * Interactive multi-select prompt - * @param {string} message Prompt message to display - * @param {Array} choices Array of auto-complete choices objects `[{ title, value }, ...]` - * @param {Function} [suggest] Function to filter results based on user input. Defaults to stort by `title` - * @param {number} [limit=10] Max number of results to show - * @param {string} [style="default"] Render style ('default', 'password', 'invisible') - * @param {function} [onState] On state change callback + * @param {string} args.message Prompt message to display + * @param {Array} args.choices Array of auto-complete choices objects `[{ title, value }, ...]` + * @param {Function} [args.suggest] Function to filter results based on user input. Defaults to sort by `title` + * @param {number} [args.limit=10] Max number of results to show + * @param {string} [args.style="default"] Render style ('default', 'password', 'invisible') + * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ function autocomplete({ message, choices, suggest, limit, style, onState = noop }) { - if (typeof message !== 'string') throw new Error('message is required'); - if (!Array.isArray(choices)) throw new Error('choices array is required'); - const suggestByTitle = (input, choices) => - Promise.resolve( - choices.filter( - item => item.title.slice(0, input.length).toLowerCase() === input.toLowerCase() - ) - ); - suggest = suggest ? suggest : suggestByTitle; - return new Promise((resolve, reject) => { - const p = new el.AutocompletePrompt({ message, choices, suggest, limit, style }); - p.on('submit', resolve); - p.on('abort', reject); - p.on('state', onState); - }); + if (!Array.isArray(args.choices)) throw new Error('choices array is required'); + args.suggest = args.suggest || byTitle; + return toPrompt('AutocompletePrompt', args); +} + +function byTitle(input, choices) { + return Promise.resolve( + choices.filter(item => item.title.slice(0, input.length).toLowerCase() === input.toLowerCase()) + ); } module.exports = { From aa6a1925dbdfeef8f9c6cd9781b82fd3e45271b0 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 3 Mar 2018 18:35:14 -0800 Subject: [PATCH 2/8] replace `toArray` with concatenation --- lib/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index 5e4bea83..a6bb9254 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,7 +2,6 @@ const prompts = require('./prompts'); -const toArray = val => (Array.isArray(val) ? val : val == null ? [] : [val]); const ignore = ['suggest', 'format', 'onState']; const noop = () => {}; @@ -13,7 +12,7 @@ const noop = () => {}; */ async function prompt(questions=[], { onSubmit=noop, onCancel=noop }={}) { const answers = {}; - questions = toArray(questions); + questions = [].concat(questions); let answer, question, quit, name, key; let MAP = prompt._map || {}; From 8c65cc3e174e69a4fc32b3e06ab58ca23c6357e5 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 3 Mar 2018 18:37:31 -0800 Subject: [PATCH 3/8] save `question.type` to variable; update existence check --- lib/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/index.js b/lib/index.js index a6bb9254..de503972 100644 --- a/lib/index.js +++ b/lib/index.js @@ -13,7 +13,7 @@ const noop = () => {}; async function prompt(questions=[], { onSubmit=noop, onCancel=noop }={}) { const answers = {}; questions = [].concat(questions); - let answer, question, quit, name, key; + let answer, question, quit, name, type, key; let MAP = prompt._map || {}; for (question of questions) { @@ -33,14 +33,14 @@ async function prompt(questions=[], { onSubmit=noop, onCancel=noop }={}) { } // skip if type is a falsy value - if (!question.type) continue; + if (!(type=question.type)) continue; - if (!prompts.hasOwnProperty(question.type)) { - throw new Error(`prompt type ${question.type} not defined`); + if (prompts[type] === void 0) { + throw new Error(`prompt type (${type}) is not defined`); } try { - answer = await prompts[question.type](question); + answer = await prompts[type](question); answers[name] = answer = question.format ? question.format(answer, answers) : answer; quit = onSubmit(question, answer); } catch (err) { From 6457a1586a255175e3de840840e235804c5fdb26 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 3 Mar 2018 18:50:20 -0800 Subject: [PATCH 4/8] fix `args` param rename on `autocomplete` --- lib/prompts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/prompts.js b/lib/prompts.js index c7ac1475..9fed9be3 100644 --- a/lib/prompts.js +++ b/lib/prompts.js @@ -149,7 +149,7 @@ function multiselect(args) { * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function autocomplete({ message, choices, suggest, limit, style, onState = noop }) { +function autocomplete(args) { if (!Array.isArray(args.choices)) throw new Error('choices array is required'); args.suggest = args.suggest || byTitle; return toPrompt('AutocompletePrompt', args); From b42cfc0d449b60cebb58c78534ac540f68270e64 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 3 Mar 2018 18:56:06 -0800 Subject: [PATCH 5/8] assign type value via shared destruct --- lib/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index de503972..4677673d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -17,7 +17,7 @@ async function prompt(questions=[], { onSubmit=noop, onCancel=noop }={}) { let MAP = prompt._map || {}; for (question of questions) { - name = question.name; + ({ name, type } = question); if (MAP[name] !== void 0) { answers[name] = MAP[name]; @@ -33,7 +33,7 @@ async function prompt(questions=[], { onSubmit=noop, onCancel=noop }={}) { } // skip if type is a falsy value - if (!(type=question.type)) continue; + if (!type) continue; if (prompts[type] === void 0) { throw new Error(`prompt type (${type}) is not defined`); From 6e43a5f4a04e6bd8cbef3b62b47c867b97a10f22 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 3 Mar 2018 19:04:11 -0800 Subject: [PATCH 6/8] move `o.message` throw into main loop --- lib/index.js | 4 ++++ lib/prompts.js | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/index.js b/lib/index.js index 4677673d..8fe5d640 100644 --- a/lib/index.js +++ b/lib/index.js @@ -25,6 +25,10 @@ async function prompt(questions=[], { onSubmit=noop, onCancel=noop }={}) { continue; // take val & run } + if (typeof question.message !== 'string') { + throw new Error('prompt message is required'); + } + // if property is a function, invoke it unless it's ignored for (key in question) { if (ignore.includes(key)) continue; diff --git a/lib/prompts.js b/lib/prompts.js index 9fed9be3..99b534b4 100644 --- a/lib/prompts.js +++ b/lib/prompts.js @@ -4,10 +4,6 @@ const el = require('./elements'); const noop = v => v; function toPrompt(type, args, opts={}) { - if (typeof args.message !== 'string') { - throw new Error('message is required'); - } - return new Promise((res, rej) => { const p = new el[type](args); const onAbort = opts.onAbort || noop; From 3f5fd9d3686d1b30899e8b1250cdabd354bef465 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 3 Mar 2018 19:06:13 -0800 Subject: [PATCH 7/8] replace silent `choices` throws; ensure array instead --- lib/prompts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/prompts.js b/lib/prompts.js index 99b534b4..a8f793ea 100644 --- a/lib/prompts.js +++ b/lib/prompts.js @@ -127,7 +127,7 @@ function select(args) { * @returns {Promise} Promise with user input */ function multiselect(args) { - if (!Array.isArray(args.choices)) throw new Error('choices array is required'); + args.choices = [].concat(args.choices || []); const toSelected = items => items.filter(item => item.selected).map(item => item.value); return toPrompt('MultiselectPrompt', args, { onAbort: toSelected, @@ -146,8 +146,8 @@ function multiselect(args) { * @returns {Promise} Promise with user input */ function autocomplete(args) { - if (!Array.isArray(args.choices)) throw new Error('choices array is required'); args.suggest = args.suggest || byTitle; + args.choices = [].concat(args.choices || []); return toPrompt('AutocompletePrompt', args); } From 46f77871b3816cf8bb262fa8dec4e3d09d44c77d Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 3 Mar 2018 19:13:17 -0800 Subject: [PATCH 8/8] rewrite prompts file exports; immediate vs bottom --- lib/prompts.js | 75 +++++++++++++++++--------------------------------- 1 file changed, 25 insertions(+), 50 deletions(-) diff --git a/lib/prompts.js b/lib/prompts.js index a8f793ea..1bbbb24e 100644 --- a/lib/prompts.js +++ b/lib/prompts.js @@ -1,5 +1,5 @@ 'use strict'; - +const $ = exports; const el = require('./elements'); const noop = v => v; @@ -22,9 +22,7 @@ function toPrompt(type, args, opts={}) { * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function text(args) { - return toPrompt('TextPrompt', args); -} +$.text = args => toPrompt('TextPrompt', args); /** * Password prompt with masked input @@ -34,22 +32,22 @@ function text(args) { * @returns {Promise} Promise with user input * */ -function password(args) { +$.password = args => { args.style = 'password'; - return text(args); -} + return $.text(args); +}; /** * Prompt where input is invisible, like sudo - * @param {string} opts.message Prompt message to display - * @param {string} [opts.initial] Default string value - * @param {function} [opts.onState] On state change callback + * @param {string} args.message Prompt message to display + * @param {string} [args.initial] Default string value + * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function invisible(opts) { - opts.style = 'invisible'; - return text(opts); -} +$.invisible = args => { + args.style = 'invisible'; + return $.text(args); +}; /** * Number prompt @@ -61,9 +59,7 @@ function invisible(opts) { * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function number(args) { - return toPrompt('NumberPrompt', args); -} +$.number = args => toPrompt('NumberPrompt', args); /** * Classic yes/no prompt @@ -72,9 +68,7 @@ function number(args) { * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function confirm(args) { - return toPrompt('ConfirmPrompt', args); -} +$.confirm = args => toPrompt('ConfirmPrompt', args); /** * List prompt, split intput string by `seperator` @@ -85,12 +79,12 @@ function confirm(args) { * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input, in form of an `Array` */ -function list(args) { +$.list = args => { const sep = args.separator || ','; return toPrompt('TextPrompt', args, { onSubmit: str => str.split(sep).map(s => s.trim()) }); -} +}; /** * Toggle/switch prompt @@ -101,9 +95,7 @@ function list(args) { * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function toggle(args) { - return toPrompt('TogglePrompt', args); -} +$.toggle = args => toPrompt('TogglePrompt', args); /** * Interactive select prompt @@ -113,9 +105,7 @@ function toggle(args) { * @param {function} [arr.onState] On state change callback * @returns {Promise} Promise with user input */ -function select(args) { - return toPrompt('SelectPrompt', args); -} +$.select = args => toPrompt('SelectPrompt', args); /** * Interactive multi-select prompt @@ -126,14 +116,18 @@ function select(args) { * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function multiselect(args) { +$.multiselect = args => { args.choices = [].concat(args.choices || []); const toSelected = items => items.filter(item => item.selected).map(item => item.value); return toPrompt('MultiselectPrompt', args, { onAbort: toSelected, onSubmit: toSelected }); -} +}; + +const byTitle = (input, choices) => Promise.resolve( + choices.filter(item => item.title.slice(0, input.length).toLowerCase() === input.toLowerCase()) +); /** * Interactive multi-select prompt @@ -145,27 +139,8 @@ function multiselect(args) { * @param {function} [args.onState] On state change callback * @returns {Promise} Promise with user input */ -function autocomplete(args) { +$.autocomplete = args => { args.suggest = args.suggest || byTitle; args.choices = [].concat(args.choices || []); return toPrompt('AutocompletePrompt', args); -} - -function byTitle(input, choices) { - return Promise.resolve( - choices.filter(item => item.title.slice(0, input.length).toLowerCase() === input.toLowerCase()) - ); -} - -module.exports = { - text, - password, - invisible, - number, - confirm, - list, - toggle, - select, - multiselect, - autocomplete };