Skip to content

Commit b411b98

Browse files
committed
refactor(@angular/cli): use latest inquirer prompt package
The `inquirer` package has been rewritten with a new set of packages. The rewrite had a focus on reduced package size and improved performance. The main prompt package is now `@inquirer/prompts`. The API is very similar but did require some refactoring to adapt to Angular's usage.
1 parent ee9d404 commit b411b98

File tree

8 files changed

+252
-125
lines changed

8 files changed

+252
-125
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"@bazel/esbuild": "5.8.1",
8484
"@bazel/jasmine": "5.8.1",
8585
"@discoveryjs/json-ext": "0.5.7",
86+
"@inquirer/prompts": "5.0.5",
8687
"@rollup/plugin-commonjs": "^26.0.0",
8788
"@rollup/plugin-node-resolve": "^13.0.5",
8889
"@types/babel__core": "7.20.5",

packages/angular/cli/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ ts_library(
5454
"//packages/angular_devkit/schematics/tasks",
5555
"//packages/angular_devkit/schematics/tools",
5656
"@npm//@angular/core",
57+
"@npm//@inquirer/prompts",
5758
"@npm//@types/ini",
58-
"@npm//@types/inquirer",
5959
"@npm//@types/node",
6060
"@npm//@types/npm-package-arg",
6161
"@npm//@types/pacote",

packages/angular/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@
2525
"@angular-devkit/architect": "0.0.0-EXPERIMENTAL-PLACEHOLDER",
2626
"@angular-devkit/core": "0.0.0-PLACEHOLDER",
2727
"@angular-devkit/schematics": "0.0.0-PLACEHOLDER",
28+
"@inquirer/prompts": "5.0.5",
2829
"@schematics/angular": "0.0.0-PLACEHOLDER",
2930
"@yarnpkg/lockfile": "1.1.0",
3031
"ansi-colors": "4.1.3",
3132
"ini": "4.1.3",
32-
"inquirer": "9.2.23",
3333
"jsonc-parser": "3.2.1",
3434
"npm-package-arg": "11.0.2",
3535
"npm-pick-manifest": "9.0.1",

packages/angular/cli/src/command-builder/schematics-command-module.ts

Lines changed: 90 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,19 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { normalize as devkitNormalize, schema } from '@angular-devkit/core';
9+
import { JsonValue, normalize as devkitNormalize, schema } from '@angular-devkit/core';
1010
import { Collection, UnsuccessfulWorkflowExecution, formats } from '@angular-devkit/schematics';
1111
import {
1212
FileSystemCollectionDescription,
1313
FileSystemSchematicDescription,
1414
NodeWorkflow,
1515
} from '@angular-devkit/schematics/tools';
16-
import type { CheckboxQuestion, Question } from 'inquirer';
1716
import { relative, resolve } from 'path';
1817
import { Argv } from 'yargs';
1918
import { isPackageNameSafeForAnalytics } from '../analytics/analytics';
2019
import { EventCustomDimension } from '../analytics/analytics-parameters';
2120
import { getProjectByCwd, getSchematicDefaults } from '../utilities/config';
2221
import { assertIsError } from '../utilities/error';
23-
import { loadEsmModule } from '../utilities/load-esm';
2422
import { memoize } from '../utilities/memoize';
2523
import { isTTY } from '../utilities/tty';
2624
import {
@@ -172,76 +170,98 @@ export abstract class SchematicsCommandModule
172170

173171
if (options.interactive !== false && isTTY()) {
174172
workflow.registry.usePromptProvider(async (definitions: Array<schema.PromptDefinition>) => {
175-
const questions = definitions
176-
.filter((definition) => !options.defaults || definition.default === undefined)
177-
.map((definition) => {
178-
const question: Question = {
179-
name: definition.id,
180-
message: definition.message,
181-
default: definition.default,
182-
};
183-
184-
const validator = definition.validator;
185-
if (validator) {
186-
question.validate = (input) => validator(input);
187-
188-
// Filter allows transformation of the value prior to validation
189-
question.filter = async (input) => {
190-
for (const type of definition.propertyTypes) {
191-
let value;
192-
switch (type) {
193-
case 'string':
194-
value = String(input);
195-
break;
196-
case 'integer':
197-
case 'number':
198-
value = Number(input);
199-
break;
200-
default:
201-
value = input;
202-
break;
173+
let prompts: typeof import('@inquirer/prompts') | undefined;
174+
const answers: Record<string, JsonValue> = {};
175+
176+
for (const definition of definitions) {
177+
if (options.defaults && definition.default !== undefined) {
178+
continue;
179+
}
180+
181+
// Only load prompt package if needed
182+
prompts ??= await import('@inquirer/prompts');
183+
184+
switch (definition.type) {
185+
case 'confirmation':
186+
answers[definition.id] = await prompts.confirm({
187+
message: definition.message,
188+
default: definition.default as boolean | undefined,
189+
});
190+
break;
191+
case 'list':
192+
if (!definition.items?.length) {
193+
continue;
194+
}
195+
196+
const choices = definition.items?.map((item) => {
197+
return typeof item == 'string'
198+
? {
199+
name: item,
200+
value: item,
201+
}
202+
: {
203+
name: item.label,
204+
value: item.value,
205+
};
206+
});
207+
208+
answers[definition.id] = await (
209+
definition.multiselect ? prompts.checkbox : prompts.select
210+
)({
211+
message: definition.message,
212+
default: definition.default,
213+
choices,
214+
});
215+
break;
216+
case 'input':
217+
let finalValue: JsonValue | undefined;
218+
answers[definition.id] = await prompts.input({
219+
message: definition.message,
220+
default: definition.default as string | undefined,
221+
async validate(value) {
222+
if (definition.validator === undefined) {
223+
return true;
203224
}
204-
// Can be a string if validation fails
205-
const isValid = (await validator(value)) === true;
206-
if (isValid) {
207-
return value;
225+
226+
let lastValidation: ReturnType<typeof definition.validator> = false;
227+
for (const type of definition.propertyTypes) {
228+
let potential;
229+
switch (type) {
230+
case 'string':
231+
potential = String(value);
232+
break;
233+
case 'integer':
234+
case 'number':
235+
potential = Number(value);
236+
break;
237+
default:
238+
potential = value;
239+
break;
240+
}
241+
lastValidation = await definition.validator(potential);
242+
243+
// Can be a string if validation fails
244+
if (lastValidation === true) {
245+
finalValue = potential;
246+
247+
return true;
248+
}
208249
}
209-
}
210-
211-
return input;
212-
};
213-
}
214-
215-
switch (definition.type) {
216-
case 'confirmation':
217-
question.type = 'confirm';
218-
break;
219-
case 'list':
220-
question.type = definition.multiselect ? 'checkbox' : 'list';
221-
(question as CheckboxQuestion).choices = definition.items?.map((item) => {
222-
return typeof item == 'string'
223-
? item
224-
: {
225-
name: item.label,
226-
value: item.value,
227-
};
228-
});
229-
break;
230-
default:
231-
question.type = definition.type;
232-
break;
233-
}
234-
235-
return question;
236-
});
237-
238-
if (questions.length) {
239-
const { default: inquirer } = await loadEsmModule<typeof import('inquirer')>('inquirer');
240-
241-
return inquirer.prompt(questions);
242-
} else {
243-
return {};
250+
251+
return lastValidation;
252+
},
253+
});
254+
255+
// Use validated value if present.
256+
// This ensures the correct type is inserted into the final schema options.
257+
if (finalValue !== undefined) {
258+
answers[definition.id] = finalValue;
259+
}
260+
break;
261+
}
244262
}
263+
264+
return answers;
245265
});
246266
}
247267

packages/angular/cli/src/utilities/prompt.ts

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,6 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import type {
10-
CheckboxChoiceOptions,
11-
CheckboxQuestion,
12-
ListChoiceOptions,
13-
ListQuestion,
14-
Question,
15-
} from 'inquirer';
16-
import { loadEsmModule } from './load-esm';
179
import { isTTY } from './tty';
1810

1911
export async function askConfirmation(
@@ -25,64 +17,58 @@ export async function askConfirmation(
2517
return noTTYResponse ?? defaultResponse;
2618
}
2719

28-
const question: Question = {
29-
type: 'confirm',
30-
name: 'confirmation',
31-
prefix: '',
20+
const { confirm } = await import('@inquirer/prompts');
21+
const answer = await confirm({
3222
message,
3323
default: defaultResponse,
34-
};
24+
theme: {
25+
prefix: '',
26+
},
27+
});
3528

36-
const { default: inquirer } = await loadEsmModule<typeof import('inquirer')>('inquirer');
37-
const answers = await inquirer.prompt([question]);
38-
39-
return answers['confirmation'];
29+
return answer;
4030
}
4131

4232
export async function askQuestion(
4333
message: string,
44-
choices: ListChoiceOptions[],
34+
choices: { name: string; value: string | null }[],
4535
defaultResponseIndex: number,
4636
noTTYResponse: null | string,
4737
): Promise<string | null> {
4838
if (!isTTY()) {
4939
return noTTYResponse;
5040
}
5141

52-
const question: ListQuestion = {
53-
type: 'list',
54-
name: 'answer',
55-
prefix: '',
42+
const { select } = await import('@inquirer/prompts');
43+
const answer = await select({
5644
message,
5745
choices,
5846
default: defaultResponseIndex,
59-
};
60-
61-
const { default: inquirer } = await loadEsmModule<typeof import('inquirer')>('inquirer');
62-
const answers = await inquirer.prompt([question]);
47+
theme: {
48+
prefix: '',
49+
},
50+
});
6351

64-
return answers['answer'];
52+
return answer;
6553
}
6654

6755
export async function askChoices(
6856
message: string,
69-
choices: CheckboxChoiceOptions[],
57+
choices: { name: string; value: string }[],
7058
noTTYResponse: string[] | null,
7159
): Promise<string[] | null> {
7260
if (!isTTY()) {
7361
return noTTYResponse;
7462
}
7563

76-
const question: CheckboxQuestion = {
77-
type: 'checkbox',
78-
name: 'answer',
79-
prefix: '',
64+
const { checkbox } = await import('@inquirer/prompts');
65+
const answers = await checkbox({
8066
message,
8167
choices,
82-
};
83-
84-
const { default: inquirer } = await loadEsmModule<typeof import('inquirer')>('inquirer');
85-
const answers = await inquirer.prompt([question]);
68+
theme: {
69+
prefix: '',
70+
},
71+
});
8672

87-
return answers['answer'];
73+
return answers;
8874
}

tests/legacy-cli/e2e/tests/commands/analytics/ask-analytics-command.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default async function () {
1414
NG_FORCE_TTY: '1',
1515
NG_FORCE_AUTOCOMPLETE: 'false',
1616
},
17-
'y' /* stdin */,
17+
'n\n' /* stdin */,
1818
);
1919

2020
if (!ANALYTICS_PROMPT.test(stdout)) {

tests/legacy-cli/e2e/tests/commands/completion/completion-prompt.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default async function () {
4848
SHELL: '/bin/bash',
4949
HOME: home,
5050
},
51-
'y' /* stdin: accept prompt */,
51+
'y\n' /* stdin: accept prompt */,
5252
);
5353

5454
if (!AUTOCOMPLETION_PROMPT.test(stdout)) {
@@ -80,7 +80,7 @@ export default async function () {
8080
SHELL: '/bin/bash',
8181
HOME: home,
8282
},
83-
'n' /* stdin: reject prompt */,
83+
'n\n' /* stdin: reject prompt */,
8484
);
8585

8686
if (!AUTOCOMPLETION_PROMPT.test(stdout)) {
@@ -118,7 +118,7 @@ export default async function () {
118118
SHELL: '/bin/bash',
119119
HOME: home,
120120
},
121-
'y' /* stdin: accept prompt */,
121+
'y\n' /* stdin: accept prompt */,
122122
);
123123

124124
if (!AUTOCOMPLETION_PROMPT.test(stdout1)) {
@@ -171,7 +171,7 @@ export default async function () {
171171
SHELL: '/bin/bash',
172172
HOME: home,
173173
},
174-
'n' /* stdin: reject prompt */,
174+
'n\n' /* stdin: reject prompt */,
175175
);
176176

177177
if (!AUTOCOMPLETION_PROMPT.test(stdout1)) {
@@ -217,7 +217,7 @@ export default async function () {
217217
SHELL: '/bin/bash',
218218
HOME: home,
219219
},
220-
'y' /* stdin: accept prompt */,
220+
'y\n' /* stdin: accept prompt */,
221221
);
222222

223223
if (!err.message.includes('Failed to append autocompletion setup')) {
@@ -237,7 +237,7 @@ export default async function () {
237237
SHELL: '/bin/bash',
238238
HOME: home,
239239
},
240-
'y' /* stdin: accept prompt */,
240+
'y\n' /* stdin: accept prompt */,
241241
);
242242

243243
if (!AUTOCOMPLETION_PROMPT.test(stdout2)) {

0 commit comments

Comments
 (0)