Skip to content

Commit c2fcf6e

Browse files
authored
Fix(@inquirer/core): Add SIGINT handler to make sure the prompt throw an exit error. Fixes #1741 (#1744)
1 parent 8378f64 commit c2fcf6e

File tree

2 files changed

+30
-27
lines changed

2 files changed

+30
-27
lines changed

packages/core/core.test.ts

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -492,32 +492,26 @@ describe('createPrompt()', () => {
492492

493493
it('clear timeout when force closing', { timeout: 1000 }, async () => {
494494
const exitSpy = vi.fn();
495-
const prompt = createPrompt(
496-
(config: { message: string }, done: (value: string) => void) => {
497-
const timeout = useRef<NodeJS.Timeout | undefined>();
498-
const cleaned = useRef(false);
499-
useKeypress(() => {
500-
if (cleaned.current) {
501-
expect.unreachable('once cleaned up keypress should not be called');
502-
}
503-
clearTimeout(timeout.current);
504-
timeout.current = setTimeout(() => {}, 1000);
505-
});
506-
507-
exitSpy.mockImplementation(() => {
508-
clearTimeout(timeout.current);
509-
cleaned.current = true;
510-
// We call done explicitly, as onSignalExit is not triggered in this case
511-
// But, CTRL+C will trigger rl.close, which should call this effect
512-
// This way we can have the promise resolve
513-
done('closed');
514-
});
515-
516-
useEffect(() => exitSpy, []);
517-
518-
return config.message;
519-
},
520-
);
495+
const prompt = createPrompt((config: { message: string }) => {
496+
const timeout = useRef<NodeJS.Timeout | undefined>();
497+
const cleaned = useRef(false);
498+
useKeypress(() => {
499+
if (cleaned.current) {
500+
expect.unreachable('once cleaned up keypress should not be called');
501+
}
502+
clearTimeout(timeout.current);
503+
timeout.current = setTimeout(() => {}, 1000);
504+
});
505+
506+
exitSpy.mockImplementation(() => {
507+
clearTimeout(timeout.current);
508+
cleaned.current = true;
509+
});
510+
511+
useEffect(() => exitSpy, []);
512+
513+
return config.message;
514+
});
521515

522516
const { answer, events } = await render(prompt, { message: 'Question' });
523517

@@ -526,7 +520,8 @@ describe('createPrompt()', () => {
526520
// This closes the readline
527521
events.keypress({ ctrl: true, name: 'c' });
528522

529-
await expect(answer).resolves.toBe('closed');
523+
await expect(answer).rejects.toThrow('User force closed the prompt with SIGINT');
524+
530525
expect(exitSpy).toHaveBeenCalledTimes(1);
531526
});
532527

packages/core/src/lib/create-prompt.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ export function createPrompt<Value, Config>(
7676
}),
7777
);
7878

79+
// SIGINT must be explicitly handled by the prompt so the ExitPromptError can be handled.
80+
// Otherwise, the prompt will stop and in some scenarios never resolve.
81+
// Ref issue #1741
82+
const sigint = () =>
83+
reject(new ExitPromptError(`User force closed the prompt with SIGINT`));
84+
rl.on('SIGINT', sigint);
85+
cleanups.add(() => rl.removeListener('SIGINT', sigint));
86+
7987
// Re-renders only happen when the state change; but the readline cursor could change position
8088
// and that also requires a re-render (and a manual one because we mute the streams).
8189
// We set the listener after the initial workLoop to avoid a double render if render triggered

0 commit comments

Comments
 (0)