Skip to content

Commit 0f2994e

Browse files
authored
Merge pull request #156 from TopCli/enhance-error-handling
Enhance error handling & remove new Promise
2 parents 263bf0a + 74cf102 commit 0f2994e

11 files changed

+332
-273
lines changed

src/index.ts

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Import Node.js Dependencies
2+
import { once } from "node:events";
3+
14
// Import Internal Dependencies
25
import {
36
required,
@@ -24,41 +27,116 @@ import {
2427
type ConfirmOptions,
2528
type MultiselectOptions
2629
} from "./prompts/index.js";
30+
import type { AbortError } from "./errors/abort.js";
2731

28-
export function question(
32+
export async function question(
2933
message: string,
3034
options: Omit<QuestionOptions, "message"> = {}
31-
) {
32-
return new QuestionPrompt(
35+
): Promise<string> {
36+
const prompt = new QuestionPrompt(
3337
{ ...options, message }
34-
).question();
38+
);
39+
40+
const onErrorSignal = new AbortController();
41+
const onError = once(
42+
prompt, "error", { signal: onErrorSignal.signal }
43+
) as Promise<[AbortError]>;
44+
const result = await Promise.race([
45+
prompt.listen(),
46+
onError
47+
]);
48+
if (isAbortError(result)) {
49+
prompt.destroy();
50+
51+
throw result[0];
52+
}
53+
onErrorSignal.abort();
54+
55+
return result;
3556
}
3657

37-
export function select<T extends string>(
58+
export async function select<T extends string>(
3859
message: string,
3960
options: Omit<SelectOptions<T>, "message">
40-
) {
41-
return new SelectPrompt<T>(
61+
): Promise<T> {
62+
const prompt = new SelectPrompt<T>(
4263
{ ...options, message }
43-
).select();
64+
);
65+
66+
const onErrorSignal = new AbortController();
67+
const onError = once(
68+
prompt, "error", { signal: onErrorSignal.signal }
69+
) as Promise<[AbortError]>;
70+
const result = await Promise.race([
71+
prompt.listen(),
72+
onError
73+
]);
74+
if (isAbortError(result)) {
75+
prompt.destroy();
76+
77+
throw result[0];
78+
}
79+
onErrorSignal.abort();
80+
81+
return result;
4482
}
4583

46-
export function confirm(
84+
export async function confirm(
4785
message: string,
4886
options: Omit<ConfirmOptions, "message"> = {}
49-
) {
50-
return new ConfirmPrompt(
87+
): Promise<boolean> {
88+
const prompt = new ConfirmPrompt(
5189
{ ...options, message }
52-
).confirm();
90+
);
91+
92+
const onErrorSignal = new AbortController();
93+
const onError = once(
94+
prompt, "error", { signal: onErrorSignal.signal }
95+
) as Promise<[AbortError]>;
96+
const result = await Promise.race([
97+
prompt.listen(),
98+
onError
99+
]);
100+
if (isAbortError(result)) {
101+
prompt.destroy();
102+
103+
throw result[0];
104+
}
105+
onErrorSignal.abort();
106+
107+
return result;
53108
}
54109

55-
export function multiselect<T extends string>(
110+
export async function multiselect<T extends string>(
56111
message: string,
57112
options: Omit<MultiselectOptions<T>, "message">
58-
) {
59-
return new MultiselectPrompt<T>(
113+
): Promise<T[]> {
114+
const prompt = new MultiselectPrompt<T>(
60115
{ ...options, message }
61-
).multiselect();
116+
);
117+
118+
const onErrorSignal = new AbortController();
119+
const onError = once(
120+
prompt, "error", { signal: onErrorSignal.signal }
121+
) as Promise<[AbortError]>;
122+
const result = await Promise.race([
123+
prompt.listen(),
124+
onError
125+
]);
126+
if (isAbortError(result)) {
127+
prompt.destroy();
128+
129+
throw result[0];
130+
}
131+
onErrorSignal.abort();
132+
133+
return result;
134+
}
135+
136+
function isAbortError(
137+
error: unknown
138+
): error is [AbortError] {
139+
return Array.isArray(error) && error.length > 0 && error[0] instanceof Error;
62140
}
63141

64142
export type {

src/prompts/confirm.ts

Lines changed: 30 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { styleText } from "node:util";
55

66
// Import Internal Dependencies
77
import { AbstractPrompt, type AbstractPromptOptions } from "./abstract.js";
8-
import { stringLength } from "../utils.js";
8+
import { stringLength, withResolvers } from "../utils.js";
99
import { SYMBOLS } from "../constants.js";
1010

1111
export interface ConfirmOptions extends AbstractPromptOptions {
@@ -56,21 +56,7 @@ export class ConfirmPrompt extends AbstractPrompt<boolean> {
5656
this.write(this.#getQuestionQuery());
5757
}
5858

59-
#question() {
60-
return new Promise((resolve) => {
61-
const questionQuery = this.#getQuestionQuery();
62-
63-
this.write(questionQuery);
64-
65-
this.#boundKeyPressEvent = this.#onKeypress.bind(this, resolve);
66-
this.stdin.on("keypress", this.#boundKeyPressEvent);
67-
68-
this.#boundExitEvent = this.#onProcessExit.bind(this);
69-
process.once("exit", this.#boundExitEvent);
70-
});
71-
}
72-
73-
#onKeypress(resolve: (value: unknown) => void, _value: any, key: Key) {
59+
#onKeypress(resolve: (value: boolean) => void, _value: any, key: Key) {
7460
this.stdout.moveCursor(
7561
-this.stdout.columns,
7662
-Math.floor(stringLength(this.#getQuestionQuery()) / this.stdout.columns)
@@ -123,46 +109,47 @@ export class ConfirmPrompt extends AbstractPrompt<boolean> {
123109
this.write(`${this.selectedValue ? SYMBOLS.Tick : SYMBOLS.Cross} ${styleText("bold", this.message)}${EOL}`);
124110
}
125111

126-
async confirm(): Promise<boolean> {
112+
async listen(): Promise<boolean> {
127113
if (this.skip) {
128114
this.destroy();
129115

130116
return this.initial;
131117
}
132118

133-
// eslint-disable-next-line no-async-promise-executor
134-
return new Promise(async(resolve, reject) => {
135-
const answer = this.agent.nextAnswers.shift();
136-
if (answer !== undefined) {
137-
this.selectedValue = answer;
138-
this.#onQuestionAnswer();
139-
this.destroy();
119+
const answer = this.agent.nextAnswers.shift();
120+
if (answer !== undefined) {
121+
this.selectedValue = answer;
122+
this.#onQuestionAnswer();
123+
this.destroy();
124+
125+
return answer;
126+
}
127+
128+
this.write(SYMBOLS.HideCursor);
140129

141-
resolve(answer);
130+
try {
131+
const { resolve, promise } = withResolvers<boolean>();
132+
const questionQuery = this.#getQuestionQuery();
142133

143-
return;
144-
}
134+
this.write(questionQuery);
145135

146-
this.once("error", (error) => {
147-
reject(error);
148-
});
136+
this.#boundKeyPressEvent = this.#onKeypress.bind(this, resolve);
137+
this.stdin.on("keypress", this.#boundKeyPressEvent);
149138

150-
this.write(SYMBOLS.HideCursor);
139+
this.#boundExitEvent = this.#onProcessExit.bind(this);
140+
process.once("exit", this.#boundExitEvent);
151141

152-
try {
153-
await this.#question();
154-
this.#onQuestionAnswer();
142+
this.#onQuestionAnswer();
155143

156-
resolve(this.selectedValue);
157-
}
158-
finally {
159-
this.write(SYMBOLS.ShowCursor);
144+
return promise;
145+
}
146+
finally {
147+
this.write(SYMBOLS.ShowCursor);
160148

161-
this.#onProcessExit();
162-
process.off("exit", this.#boundExitEvent);
149+
this.#onProcessExit();
150+
process.off("exit", this.#boundExitEvent);
163151

164-
this.destroy();
165-
}
166-
});
152+
this.destroy();
153+
}
167154
}
168155
}

0 commit comments

Comments
 (0)