Skip to content

Commit 855f7dc

Browse files
committed
Refactor(@inquirer/core) Cleanup the exit logic & promise constructor
1 parent e17b0c7 commit 855f7dc

File tree

2 files changed

+79
-70
lines changed

2 files changed

+79
-70
lines changed

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

Lines changed: 68 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import * as readline from 'node:readline';
22
import { AsyncResource } from 'node:async_hooks';
3-
import { CancelablePromise, type Prompt, type Prettify } from '@inquirer/type';
3+
import { type Prompt, type Prettify } from '@inquirer/type';
44
import MuteStream from 'mute-stream';
55
import { onExit as onSignalExit } from 'signal-exit';
66
import ScreenManager from './screen-manager.mjs';
7-
import type { InquirerReadline } from '@inquirer/type';
7+
import { CancelablePromise, type InquirerReadline } from '@inquirer/type';
88
import { withHooks, effectScheduler } from './hook-engine.mjs';
99
import { CancelPromptError, ExitPromptError } from './errors.mjs';
1010

@@ -14,13 +14,13 @@ type ViewFunction<Value, Config> = (
1414
) => string | [string, string | undefined];
1515

1616
export function createPrompt<Value, Config>(view: ViewFunction<Value, Config>) {
17-
const prompt: Prompt<Value, Config> = (config, context) => {
17+
const prompt: Prompt<Value, Config> = (config, context = {}) => {
1818
// Default `input` to stdin
19-
const input = context?.input ?? process.stdin;
19+
const { input = process.stdin } = context;
2020

2121
// Add mute capabilities to the output
2222
const output = new MuteStream();
23-
output.pipe(context?.output ?? process.stdout);
23+
output.pipe(context.output ?? process.stdout);
2424

2525
const rl = readline.createInterface({
2626
terminal: true,
@@ -29,84 +29,82 @@ export function createPrompt<Value, Config>(view: ViewFunction<Value, Config>) {
2929
}) as InquirerReadline;
3030
const screen = new ScreenManager(rl);
3131

32-
let cancel: () => void = () => {};
33-
const answer = new CancelablePromise<Value>((resolve, reject) => {
34-
withHooks(rl, (cycle) => {
35-
function checkCursorPos() {
36-
screen.checkCursorPos();
37-
}
32+
const cleanups = new Set<() => void>();
33+
const { promise, resolve, reject } = CancelablePromise.withResolver<Value>();
3834

39-
const removeExitListener = onSignalExit((code, signal) => {
40-
onExit();
41-
reject(
42-
new ExitPromptError(`User force closed the prompt with ${code} ${signal}`),
43-
);
44-
});
35+
function onExit() {
36+
cleanups.forEach((cleanup) => cleanup());
4537

46-
const hooksCleanup = AsyncResource.bind(() => {
47-
try {
48-
effectScheduler.clearAll();
49-
} catch (error) {
50-
reject(error);
51-
}
52-
});
53-
54-
function onExit() {
55-
hooksCleanup();
38+
screen.done({ clearContent: Boolean(context?.clearPromptOnDone) });
39+
output.end();
40+
}
5641

57-
screen.done({ clearContent: Boolean(context?.clearPromptOnDone) });
42+
function fail(error: unknown) {
43+
onExit();
44+
reject(error);
45+
}
5846

59-
removeExitListener();
60-
rl.input.removeListener('keypress', checkCursorPos);
61-
rl.removeListener('close', hooksCleanup);
62-
output.end();
47+
withHooks(rl, (cycle) => {
48+
cleanups.add(
49+
onSignalExit((code, signal) => {
50+
fail(
51+
new ExitPromptError(`User force closed the prompt with ${code} ${signal}`),
52+
);
53+
}),
54+
);
55+
56+
const hooksCleanup = AsyncResource.bind(() => {
57+
try {
58+
effectScheduler.clearAll();
59+
} catch (error) {
60+
reject(error);
6361
}
64-
65-
cancel = () => {
62+
});
63+
cleanups.add(hooksCleanup);
64+
65+
// Re-renders only happen when the state change; but the readline cursor could change position
66+
// and that also requires a re-render (and a manual one because we mute the streams).
67+
// We set the listener after the initial workLoop to avoid a double render if render triggered
68+
// by a state change sets the cursor to the right position.
69+
const checkCursorPos = () => screen.checkCursorPos();
70+
rl.input.on('keypress', checkCursorPos);
71+
cleanups.add(() => rl.input.removeListener('keypress', checkCursorPos));
72+
73+
// The close event triggers immediately when the user press ctrl+c. SignalExit on the other hand
74+
// triggers after the process is done (which happens after timeouts are done triggering.)
75+
// We triggers the hooks cleanup phase on rl `close` so active timeouts can be cleared.
76+
rl.on('close', hooksCleanup);
77+
cleanups.add(() => rl.removeListener('close', hooksCleanup));
78+
79+
function done(value: Value) {
80+
// Delay execution to let time to the hookCleanup functions to registers.
81+
setImmediate(() => {
6682
onExit();
67-
reject(new CancelPromptError());
68-
};
69-
70-
function done(value: Value) {
71-
// Delay execution to let time to the hookCleanup functions to registers.
72-
setImmediate(() => {
73-
onExit();
74-
75-
// Finally we resolve our promise
76-
resolve(value);
77-
});
78-
}
7983

80-
cycle(() => {
81-
try {
82-
const nextView = view(config, done);
83-
84-
const [content, bottomContent] =
85-
typeof nextView === 'string' ? [nextView] : nextView;
86-
screen.render(content, bottomContent);
87-
88-
effectScheduler.run();
89-
} catch (error) {
90-
onExit();
91-
reject(error);
92-
}
84+
// Finally we resolve our promise
85+
resolve(value);
9386
});
87+
}
9488

95-
// Re-renders only happen when the state change; but the readline cursor could change position
96-
// and that also requires a re-render (and a manual one because we mute the streams).
97-
// We set the listener after the initial workLoop to avoid a double render if render triggered
98-
// by a state change sets the cursor to the right position.
99-
rl.input.on('keypress', checkCursorPos);
89+
cycle(() => {
90+
try {
91+
const nextView = view(config, done);
10092

101-
// The close event triggers immediately when the user press ctrl+c. SignalExit on the other hand
102-
// triggers after the process is done (which happens after timeouts are done triggering.)
103-
// We triggers the hooks cleanup phase on rl `close` so active timeouts can be cleared.
104-
rl.on('close', hooksCleanup);
93+
const [content, bottomContent] =
94+
typeof nextView === 'string' ? [nextView] : nextView;
95+
screen.render(content, bottomContent);
96+
97+
effectScheduler.run();
98+
} catch (error: unknown) {
99+
fail(error);
100+
}
105101
});
106102
});
107103

108-
answer.cancel = cancel;
109-
return answer;
104+
promise.cancel = () => {
105+
fail(new CancelPromptError());
106+
};
107+
return promise;
110108
};
111109

112110
return prompt;

packages/type/src/inquirer.mts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ import MuteStream from 'mute-stream';
33

44
export class CancelablePromise<T> extends Promise<T> {
55
public cancel: () => void = () => {};
6+
7+
static withResolver<T>() {
8+
let resolve: (value: T) => void;
9+
let reject: (error: unknown) => void;
10+
const promise = new CancelablePromise<T>((res, rej) => {
11+
resolve = res;
12+
reject = rej;
13+
});
14+
15+
return { promise, resolve: resolve!, reject: reject! };
16+
}
617
}
718

819
export type InquirerReadline = readline.ReadLine & {

0 commit comments

Comments
 (0)