From 059461af748f4a3ca6948549ccf0c5d5b4190a37 Mon Sep 17 00:00:00 2001 From: Joram van den Boezem Date: Sat, 30 Jan 2021 19:02:00 +0100 Subject: [PATCH] fix: stop and restart repl to allow enquirer prompt (#188) --- jest.config.js | 2 +- src/command.ts | 6 +++--- src/program.ts | 3 +-- src/prompter.ts | 2 +- src/repl.ts | 25 ++++++++++++++++++++++--- tests/program.spec.ts | 35 ++++++++++++++++++++++++++++++++++- 6 files changed, 62 insertions(+), 11 deletions(-) diff --git a/jest.config.js b/jest.config.js index 88972cb3..20febba0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,7 @@ module.exports = { testMatch: ['**/*.spec.ts'], globals: { 'ts-jest': { - tsConfig: 'tsconfig.json' + tsconfig: 'tsconfig.json' } }, setupFiles: ['./jest.setup.js'] diff --git a/src/command.ts b/src/command.ts index d61cbbcb..b266aabc 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,7 +1,7 @@ -import { Argv, CommandModule, Arguments as BaseArguments } from 'yargs' +import { Arguments as BaseArguments, Argv, CommandModule } from 'yargs' import { Argument, ArgumentOptions } from './argument' -import { Option, OptionOptions } from './option' import { InferArgType } from './baseArg' +import { Option, OptionOptions } from './option' import { prompter } from './prompter' export type Arguments = T & @@ -279,7 +279,7 @@ export class Command { return this.handler(args, commandRunner) } - // Display help this command contains sub-commands + // Display help if this command contains sub-commands if (this.getCommands().length) { return commandRunner(`${this.getFqn()} --help`) } diff --git a/src/program.ts b/src/program.ts index f73f5cb1..06cf70d8 100644 --- a/src/program.ts +++ b/src/program.ts @@ -2,9 +2,8 @@ import { EventEmitter } from 'events' import TypedEventEmitter from 'typed-emitter' import { Argv } from 'yargs' import createYargs from 'yargs/yargs' -import { Command, command } from './command' +import { Arguments, Command, command } from './command' import { Repl, repl } from './repl' -import { Arguments } from './command' import { isPromise } from './utils' interface Events { diff --git a/src/prompter.ts b/src/prompter.ts index 7e7ff5a4..90003b5b 100644 --- a/src/prompter.ts +++ b/src/prompter.ts @@ -18,7 +18,7 @@ type PromptType = type Question = any /** - * Creates a new command, which can be added to a program. + * Creates a new prompter instance */ export function prompter(baseArgs: Array, args: T) { return new Prompter(baseArgs, args) diff --git a/src/repl.ts b/src/repl.ts index 204f6420..f61b513e 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -1,9 +1,10 @@ -import nodeRepl, { REPLServer } from 'repl' +import { Prompt } from 'enquirer' import { CompleterResult } from 'readline' -import { Context } from 'vm' +import nodeRepl, { REPLServer } from 'repl' import { parseArgsStringToArgv } from 'string-argv' -import { Program } from './program' +import { Context } from 'vm' import { autocompleter, Autocompleter } from './autocompleter' +import { Program } from './program' const DEFAULT_PROMPT = '> ' @@ -27,6 +28,11 @@ export class Repl { private prompt: string = DEFAULT_PROMPT ) { this.autocompleter = autocompleter(program) + + // Stop the server to avoid eval'ing stdin from prompts + this.program.on('run', () => { + this.stop() + }) } /** @@ -42,6 +48,14 @@ export class Repl { completer: this.completer.bind(this), ignoreUndefined: true, }) + + // Fixes bug with hidden cursor after enquirer prompt + // @ts-ignore + new Prompt().cursorShow() + } + + public stop() { + this.server?.close() } /** @@ -101,6 +115,11 @@ export class Repl { this.errorHandler(error) } + // Since we stop the server when a command is executed (by listening to the + // 'run' event in the constructor), we need to start a new instance when the + // command is finished. + this.start() + // The result passed to this function is printed by the Node REPL server, // but we don't want to use that, so we pass undefined instead. cb(null, undefined) diff --git a/tests/program.spec.ts b/tests/program.spec.ts index fd3146c0..cdc26e8a 100644 --- a/tests/program.spec.ts +++ b/tests/program.spec.ts @@ -1,6 +1,33 @@ // @ts-ignore import mockArgv from 'mock-argv' -import { program, Program, command } from '../src' +import { mocked } from 'ts-jest/utils' +import { command, program, Program, Repl } from '../src' + +jest.mock('../src/repl', () => { + return { + repl: jest.fn().mockImplementation(() => { + return new MockedRepl() + }), + Repl: jest.fn().mockImplementation(() => { + return MockedRepl + }), + } +}) + +// Repl mock +const replStartFn = jest.fn() +const replPauseFn = jest.fn() +const replResumeFn = jest.fn() +class MockedRepl { + start = replStartFn + pause = replPauseFn + resume = replResumeFn +} + +beforeEach(() => { + const MockedRepl = mocked(Repl, true) + MockedRepl.mockClear() +}) test('program should return new Program object', () => { expect(program()).toBeInstanceOf(Program) @@ -25,3 +52,9 @@ test('program executes argv', async () => { await expect(app.run()).resolves.toBe('foo') }) }) + +test('program starts repl', async () => { + const app = program() + expect(app.repl()).toBeInstanceOf(MockedRepl) + expect(replStartFn).toHaveBeenCalled() +})