Skip to content

Commit 7f04251

Browse files
fix: handle skipped questions in when with display option
1 parent 87cb01e commit 7f04251

File tree

7 files changed

+205
-14
lines changed

7 files changed

+205
-14
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
packages/prompts/README.md
1+
packages/prompts/README.md

packages/inquirer/inquirer.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,62 @@ describe('inquirer.prompt(...)', () => {
738738
]);
739739
expect(answers).toEqual({ q: 'foo' });
740740
});
741+
742+
it('should display skipped question when `when` returns { ask: false, display: true }', async () => {
743+
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
744+
745+
const answers = await inquirer.prompt([
746+
{
747+
type: 'stub',
748+
name: 'q1',
749+
message: 'Question 1',
750+
},
751+
{
752+
type: 'stub',
753+
name: 'q2',
754+
message: 'Question 2',
755+
when() {
756+
return { ask: false, display: true };
757+
},
758+
},
759+
]);
760+
761+
const output = writeSpy.mock.calls.map(([text]) => text).join('');
762+
expect(output).toMatch(/Question 2/);
763+
expect(output.includes('\x1b[2m')).toBe(true);
764+
expect(answers).toEqual({ q1: 'bar' });
765+
writeSpy.mockRestore();
766+
});
767+
768+
it('should skip question entirely when `when` returns { ask: false, display: false }', async () => {
769+
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
770+
771+
const answers = await inquirer.prompt([
772+
{
773+
type: 'stub',
774+
name: 'q1',
775+
message: 'Question 1',
776+
},
777+
{
778+
type: 'stub',
779+
name: 'q2',
780+
message: 'Question 2',
781+
when() {
782+
return { ask: false, display: false };
783+
},
784+
},
785+
{
786+
type: 'stub',
787+
name: 'q3',
788+
message: 'Question 3',
789+
},
790+
]);
791+
792+
const output = writeSpy.mock.calls.map(([text]) => text).join('');
793+
expect(output).not.toMatch(/Question 2/);
794+
expect(answers).toEqual({ q1: 'bar', q3: 'bar' });
795+
writeSpy.mockRestore();
796+
});
741797
});
742798

743799
describe('Prefilling answers', () => {

packages/inquirer/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@
8080
"@inquirer/type": "^3.0.9",
8181
"mute-stream": "^2.0.0",
8282
"run-async": "^4.0.5",
83-
"rxjs": "^7.8.2"
83+
"rxjs": "^7.8.2",
84+
"yoctocolors-cjs": "^2.1.3"
8485
},
8586
"devDependencies": {
8687
"@arethetypeswrong/cli": "^0.18.2",

packages/inquirer/src/types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ export interface QuestionMap {
4747
type KeyValueOrAsyncGetterFunction<T, k extends string, A extends Answers> =
4848
T extends Record<string, any> ? T[k] | AsyncGetterFunction<T[k], A> : never;
4949

50+
export type CustomWhenResult = {
51+
display: boolean;
52+
ask: boolean;
53+
};
54+
5055
export type Question<A extends Answers = Answers, Type extends string = string> = {
5156
type: Type;
5257
name: string;
@@ -55,7 +60,7 @@ export type Question<A extends Answers = Answers, Type extends string = string>
5560
choices?: any;
5661
filter?: (answer: any, answers: Partial<A>) => any;
5762
askAnswered?: boolean;
58-
when?: boolean | AsyncGetterFunction<boolean, A>;
63+
when?: boolean | CustomWhenResult | AsyncGetterFunction<boolean | CustomWhenResult, A>;
5964
};
6065

6166
type QuestionWithGetters<
@@ -67,7 +72,7 @@ type QuestionWithGetters<
6772
{
6873
type: Type;
6974
askAnswered?: boolean;
70-
when?: boolean | AsyncGetterFunction<boolean, A>;
75+
when?: boolean | AsyncGetterFunction<boolean | CustomWhenResult, A>;
7176
filter?(input: any, answers: A): any;
7277
message: KeyValueOrAsyncGetterFunction<Q, 'message', A>;
7378
default?: KeyValueOrAsyncGetterFunction<Q, 'default', A>;

packages/inquirer/src/ui/prompt.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
from,
77
of,
88
concatMap,
9-
filter,
109
reduce,
1110
isObservable,
1211
Observable,
@@ -23,7 +22,9 @@ import type {
2322
AsyncGetterFunction,
2423
PromptSession,
2524
StreamOptions,
25+
CustomWhenResult,
2626
} from '../types.ts';
27+
import SkippedRenderer from './skipped-renderer.ts';
2728

2829
export const _ = {
2930
set: (obj: Record<string, unknown>, path: string = '', value: unknown): void => {
@@ -230,16 +231,30 @@ export default class PromptsRunner<A extends Answers> {
230231
concatMap((question) =>
231232
of(question).pipe(
232233
concatMap((question) =>
233-
from(
234-
this.shouldRun(question).then((shouldRun: boolean | void) => {
235-
if (shouldRun) {
236-
return question;
234+
from(this.shouldRun(question)).pipe(
235+
concatMap((result) => {
236+
let ask = true;
237+
let display = true;
238+
if (typeof result === 'object') {
239+
display = result.display;
240+
ask = result.ask;
241+
} else {
242+
ask = result;
237243
}
238-
return;
244+
if (ask || display) {
245+
return of({ question, ask });
246+
}
247+
return EMPTY;
239248
}),
240-
).pipe(filter((val) => val != null)),
249+
),
241250
),
242-
concatMap((question) => defer(() => from(this.fetchAnswer(question)))),
251+
concatMap(({ question, ask }) => {
252+
if (ask) {
253+
return defer(() => from(this.fetchAnswer(question)));
254+
}
255+
this.displaySkippedQuestion(question);
256+
return EMPTY;
257+
}),
243258
),
244259
),
245260
);
@@ -391,14 +406,23 @@ export default class PromptsRunner<A extends Answers> {
391406
});
392407
};
393408

409+
private displaySkippedQuestion(question: Question<A>) {
410+
const renderer = SkippedRenderer[question.type] || SkippedRenderer.default;
411+
type RendererFunc<A extends Answers> = (question: Question<A>) => string;
412+
const outputText = (renderer as RendererFunc<A>)(question);
413+
(this.opt.output || process.stdout).write(`${outputText}\n`);
414+
}
415+
394416
/**
395417
* Close the interface and cleanup listeners
396418
*/
397419
close = () => {
398420
this.abortController.abort();
399421
};
400422

401-
private shouldRun = async (question: Question<A>): Promise<boolean> => {
423+
private shouldRun = async (
424+
question: Question<A>,
425+
): Promise<boolean | CustomWhenResult> => {
402426
if (
403427
question.askAnswered !== true &&
404428
_.get(this.answers, question.name) !== undefined
@@ -409,6 +433,11 @@ export default class PromptsRunner<A extends Answers> {
409433
const { when } = question;
410434
if (typeof when === 'function') {
411435
const shouldRun = await runAsync(when)(this.answers);
436+
if (typeof shouldRun === 'object') {
437+
const display = shouldRun.display;
438+
const ask = shouldRun.ask;
439+
return { display, ask };
440+
}
412441
return Boolean(shouldRun);
413442
}
414443

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import colors from 'yoctocolors-cjs';
2+
import { Answers, Question } from '../types.ts';
3+
4+
type RendererFunction<A extends Answers = Answers> = (question: Question<A>) => string;
5+
6+
type Choice = { name: string; value: string | number };
7+
8+
type TypedQuestion<A extends Answers = Answers, T = string | boolean | number> = Omit<
9+
Question<A>,
10+
'choices' | 'default'
11+
> & {
12+
choices?: Choice[];
13+
default?: T; // now default is typed
14+
};
15+
16+
type SkippedRendererType<A extends Answers = Answers> = {
17+
[key: string]: RendererFunction<A>;
18+
confirm: RendererFunction<A>;
19+
select: RendererFunction<A>;
20+
checkbox: RendererFunction<A>;
21+
editor: RendererFunction<A>;
22+
password: RendererFunction<A>;
23+
default: RendererFunction<A>;
24+
};
25+
26+
const SkippedRenderer: SkippedRendererType = {
27+
confirm: (question: TypedQuestion) => {
28+
const defaultVal = question.default;
29+
const answerText = defaultVal === true ? 'Yes' : defaultVal === false ? 'No' : '';
30+
const prefix = '?';
31+
const line = `${prefix} ${question.message} ${answerText}`;
32+
return colors.dim(line);
33+
},
34+
35+
select: (question: TypedQuestion) => {
36+
const defaultVal = question.default;
37+
const prefix = '?';
38+
let answerText = String(defaultVal);
39+
40+
if (question.choices && defaultVal !== undefined) {
41+
const selectedChoice = question.choices.find((c) => c.value === defaultVal);
42+
answerText = selectedChoice ? selectedChoice.name : String(defaultVal);
43+
}
44+
45+
const line = `${prefix} ${question.message} ${answerText}`;
46+
return colors.dim(line);
47+
},
48+
49+
checkbox: (question: TypedQuestion) => {
50+
const defaultVal = question.default;
51+
const prefix = '?';
52+
let answerText = '';
53+
54+
if (Array.isArray(defaultVal) && question.choices) {
55+
const selectedNames = question.choices
56+
.filter((c) => defaultVal.includes(c.value))
57+
.map((c) => c.name);
58+
answerText = selectedNames.join(', ');
59+
} else if (defaultVal !== undefined) {
60+
answerText = String(defaultVal);
61+
}
62+
63+
const line = `${prefix} ${question.message} ${answerText}`;
64+
return colors.dim(line);
65+
},
66+
67+
editor: (question: TypedQuestion) => {
68+
const prefix = '?';
69+
const answerText = question.default !== undefined ? '[Default Content]' : '';
70+
const line = `${prefix} ${question.message} ${answerText}`;
71+
return colors.dim(line);
72+
},
73+
74+
password: (question: TypedQuestion) => {
75+
const defaultVal = question.default;
76+
const prefix = '?';
77+
let answerText = '';
78+
79+
if (defaultVal !== undefined) {
80+
answerText = '[PASSWORD SET]';
81+
}
82+
83+
const line = `${prefix} ${question.message} ${answerText}`;
84+
return colors.dim(line);
85+
},
86+
87+
default: (question: TypedQuestion) => {
88+
const prefix = '?';
89+
const answer = question.default !== undefined ? String(question.default) : '';
90+
const line = `${prefix} ${question.message} ${answer}`;
91+
return colors.dim(line);
92+
},
93+
list: (question: TypedQuestion) => SkippedRenderer.select(question),
94+
rawlist: (question: TypedQuestion) => SkippedRenderer.select(question),
95+
input: (question: TypedQuestion) => SkippedRenderer.default(question),
96+
number: (question: TypedQuestion) => SkippedRenderer.default(question),
97+
};
98+
99+
export default SkippedRenderer;

yarn.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4720,6 +4720,7 @@ __metadata:
47204720
run-async: "npm:^4.0.5"
47214721
rxjs: "npm:^7.8.2"
47224722
tshy: "npm:^3.0.2"
4723+
yoctocolors-cjs: "npm:^2.1.3"
47234724
peerDependencies:
47244725
"@types/node": ">=18"
47254726
peerDependenciesMeta:
@@ -8663,7 +8664,7 @@ __metadata:
86638664
languageName: node
86648665
linkType: hard
86658666

8666-
"yoctocolors-cjs@npm:^2.1.2":
8667+
"yoctocolors-cjs@npm:^2.1.2, yoctocolors-cjs@npm:^2.1.3":
86678668
version: 2.1.3
86688669
resolution: "yoctocolors-cjs@npm:2.1.3"
86698670
checksum: 10/b2144b38807673a4254dae06fe1a212729550609e606289c305e45c585b36fab1dbba44fe6cde90db9b28be465ec63f4c2a50867aeec6672f6bc36b6c9a361a0

0 commit comments

Comments
 (0)