diff --git a/__tests__/reporters/base-reporter.js b/__tests__/reporters/base-reporter.js index 636e27f954..5573530889 100644 --- a/__tests__/reporters/base-reporter.js +++ b/__tests__/reporters/base-reporter.js @@ -119,3 +119,15 @@ test('BaseReporter.termstrings', () => { const expected = '"\u001b[2mjsprim#\u001b[22mjson-schema" not installed'; expect(reporter.lang('packageNotInstalled', '\u001b[2mjsprim#\u001b[22mjson-schema')).toEqual(expected); }); + +test('BaseReporter.prompt', async () => { + const reporter = new BaseReporter(); + let error; + try { + await reporter.prompt('', []); + } catch (e) { + error = e; + } + expect(error).not.toBeUndefined(); + reporter.close(); +}); diff --git a/src/cli/commands/upgrade-interactive.js b/src/cli/commands/upgrade-interactive.js index 63a3c70813..96848ab6a3 100644 --- a/src/cli/commands/upgrade-interactive.js +++ b/src/cli/commands/upgrade-interactive.js @@ -9,8 +9,6 @@ import {Add} from './add.js'; import {Install} from './install.js'; import Lockfile from '../../lockfile/wrapper.js'; -const tty = require('tty'); - export const requireLockfile = true; export function setFlags(commander: Object) { @@ -23,25 +21,6 @@ export function hasWrapper(): boolean { return true; } -type InquirerResponses = {[key: K]: Array}; - -// Prompt user with Inquirer -async function prompt(choices): Promise> { - let pageSize; - if (process.stdout instanceof tty.WriteStream) { - pageSize = process.stdout.rows - 2; - } - const answers: InquirerResponses<'packages', Dependency> = await inquirer.prompt([{ - name: 'packages', - type: 'checkbox', - message: 'Choose which packages to update.', - choices, - pageSize, - validate: (answer) => !!answer.length || 'You must choose at least one package.', - }]); - return answers.packages; -} - export async function run( config: Config, reporter: Reporter, @@ -109,31 +88,43 @@ export async function run( (ys, y) => ys.concat(Array.isArray(y) ? flatten(y) : y), [], ); - const choices = Object.keys(groupedDeps).map((key) => [ + const choices = flatten(Object.keys(groupedDeps).map((key) => [ new inquirer.Separator(reporter.format.bold.underline.green(key)), groupedDeps[key], new inquirer.Separator(' '), - ]); - - const answers = await prompt(flatten(choices)); - - const getName = ({name}) => name; - const isHint = (x) => ({hint}) => hint === x; - - await [null, 'dev', 'optional', 'peer'].reduce(async (promise, hint) => { - // Wait for previous promise to resolve - await promise; - // Reset dependency flags - flags.dev = hint === 'dev'; - flags.peer = hint === 'peer'; - flags.optional = hint === 'optional'; - - const deps = answers.filter(isHint(hint)).map(getName); - if (deps.length) { - reporter.info(reporter.lang('updateInstalling', getNameFromHint(hint))); - const add = new Add(deps, flags, config, reporter, lockfile); - return await add.init(); - } - return Promise.resolve(); - }, Promise.resolve()); + ])); + + try { + const answers: Array = await reporter.prompt( + 'Choose which packages to update.', + choices, + { + name: 'packages', + type: 'checkbox', + validate: (answer) => !!answer.length || 'You must choose at least one package.', + }, + ); + + const getName = ({name}) => name; + const isHint = (x) => ({hint}) => hint === x; + + await [null, 'dev', 'optional', 'peer'].reduce(async (promise, hint) => { + // Wait for previous promise to resolve + await promise; + // Reset dependency flags + flags.dev = hint === 'dev'; + flags.peer = hint === 'peer'; + flags.optional = hint === 'optional'; + + const deps = answers.filter(isHint(hint)).map(getName); + if (deps.length) { + reporter.info(reporter.lang('updateInstalling', getNameFromHint(hint))); + const add = new Add(deps, flags, config, reporter, lockfile); + return await add.init(); + } + return Promise.resolve(); + }, Promise.resolve()); + } catch (e) { + Promise.reject(e); + } } diff --git a/src/reporters/base-reporter.js b/src/reporters/base-reporter.js index 65589f97fb..92ce06a0c2 100644 --- a/src/reporters/base-reporter.js +++ b/src/reporters/base-reporter.js @@ -10,6 +10,7 @@ import type { Package, ReporterSpinner, QuestionOptions, + PromptOptions, } from './types.js'; import type {LanguageKeys} from './lang/en.js'; import type {Formatter} from './format.js'; @@ -259,4 +260,11 @@ export default class BaseReporter { disableProgress() { this.noProgress = true; } + + // + prompt( + message: string, choices: Array<*>, options?: PromptOptions = {}, + ): Promise> { + return Promise.reject(new Error('Not implemented')); + } } diff --git a/src/reporters/console/console-reporter.js b/src/reporters/console/console-reporter.js index 99a086794b..0042e10904 100644 --- a/src/reporters/console/console-reporter.js +++ b/src/reporters/console/console-reporter.js @@ -7,6 +7,7 @@ import type { ReporterSpinner, ReporterSelectOption, QuestionOptions, + PromptOptions, } from '../types.js'; import type {FormatKeys} from '../format.js'; import BaseReporter from '../base-reporter.js'; @@ -15,13 +16,16 @@ import Spinner from './spinner-progress.js'; import {clearLine} from './util.js'; import {removeSuffix} from '../../util/misc.js'; import {sortTrees, recurseTree, getFormattedOutput} from './helpers/tree-helper.js'; +import inquirer from 'inquirer'; const {inspect} = require('util'); const readline = require('readline'); const chalk = require('chalk'); const read = require('read'); +const tty = require('tty'); type Row = Array; +type InquirerResponses = {[key: K]: Array}; export default class ConsoleReporter extends BaseReporter { constructor(opts: Object) { @@ -380,4 +384,51 @@ export default class ConsoleReporter extends BaseReporter { bar.tick(); }; } + + async prompt( + message: string, choices: Array<*>, options?: PromptOptions = {}, + ): Promise> { + if (!process.stdout.isTTY) { + return Promise.reject(new Error("Can't answer a question unless a user TTY")); + } + + let pageSize; + if (process.stdout instanceof tty.WriteStream) { + pageSize = process.stdout.rows - 2; + } + + const rl = readline.createInterface({ + input: this.stdin, + output: this.stdout, + terminal: true, + }); + + // $FlowFixMe: Need to update the type of Inquirer + const prompt = inquirer.createPromptModule({ + input: this.stdin, + output: this.stdout, + }); + + let rejectRef = () => {}; + const killListener = () => { + rejectRef(); + }; + + const handleKillFromInquirer = new Promise((resolve, reject) => { + rejectRef = reject; + }); + + rl.addListener('SIGINT', killListener); + + const {name = 'prompt', type = 'input', validate} = options; + const answers: InquirerResponses = await Promise.race([ + prompt([{name, type, message, choices, pageSize, validate}]), + handleKillFromInquirer, + ]); + + rl.removeListener('SIGINT', killListener); + rl.close(); + + return answers[name]; + } } diff --git a/src/reporters/noop-reporter.js b/src/reporters/noop-reporter.js index 8dd918c029..b5e4a8ced9 100644 --- a/src/reporters/noop-reporter.js +++ b/src/reporters/noop-reporter.js @@ -10,6 +10,7 @@ import type { Package, ReporterSpinner, QuestionOptions, + PromptOptions, } from './types.js'; import type {LanguageKeys} from './lang/en.js'; import type {Formatter} from './format.js'; @@ -79,4 +80,10 @@ export default class NoopReporter extends BaseReporter { disableProgress() { this.noProgress = true; } + + prompt( + message: string, choices: Array<*>, options?: PromptOptions = {}, + ): Promise> { + return Promise.reject(new Error('Not implemented')); + } } diff --git a/src/reporters/types.js b/src/reporters/types.js index 3a722e5853..2e674164eb 100644 --- a/src/reporters/types.js +++ b/src/reporters/types.js @@ -47,3 +47,12 @@ export type QuestionOptions = { password?: boolean, required?: boolean, }; + +export type InquirerPromptTypes = 'list' | 'rawlist' | 'expand' | + 'checkbox' | 'confirm' | 'input' | 'password' | 'editor'; + +export type PromptOptions = { + name?: string, + type?: InquirerPromptTypes, + validate?: (input: string | Array) => (boolean | string), +};