Skip to content

Commit d36c53b

Browse files
committed
refactor(@angular-devkit/schematics-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 d6aa216 commit d36c53b

File tree

3 files changed

+83
-88
lines changed

3 files changed

+83
-88
lines changed

‎packages/angular_devkit/schematics_cli/BUILD.bazel

+1-2
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,10 @@ ts_library(
5151
"//packages/angular_devkit/schematics",
5252
"//packages/angular_devkit/schematics/tasks",
5353
"//packages/angular_devkit/schematics/tools",
54-
"@npm//@types/inquirer",
54+
"@npm//@inquirer/prompts",
5555
"@npm//@types/node",
5656
"@npm//@types/yargs-parser",
5757
"@npm//ansi-colors",
58-
"@npm//inquirer",
5958
"@npm//symbol-observable",
6059
"@npm//yargs-parser",
6160
],

‎packages/angular_devkit/schematics_cli/bin/schematics.ts

+81-85
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@
99

1010
// symbol polyfill must go first
1111
import 'symbol-observable';
12-
import type { logging, schema } from '@angular-devkit/core';
12+
import type { JsonValue, logging, schema } from '@angular-devkit/core';
1313
import { ProcessOutput, createConsoleLogger } from '@angular-devkit/core/node';
1414
import { UnsuccessfulWorkflowExecution } from '@angular-devkit/schematics';
1515
import { NodeWorkflow } from '@angular-devkit/schematics/tools';
1616
import ansiColors from 'ansi-colors';
17-
import type { Question, QuestionCollection } from 'inquirer';
1817
import { existsSync } from 'node:fs';
1918
import * as path from 'node:path';
2019
import yargsParser, { camelCase, decamelize } from 'yargs-parser';
@@ -69,71 +68,94 @@ function _listSchematics(workflow: NodeWorkflow, collectionName: string, logger:
6968

7069
function _createPromptProvider(): schema.PromptProvider {
7170
return async (definitions) => {
72-
const questions: QuestionCollection = definitions.map((definition) => {
73-
const question: Question = {
74-
name: definition.id,
75-
message: definition.message,
76-
default: definition.default,
77-
};
78-
79-
const validator = definition.validator;
80-
if (validator) {
81-
question.validate = (input) => validator(input);
82-
83-
// Filter allows transformation of the value prior to validation
84-
question.filter = async (input) => {
85-
for (const type of definition.propertyTypes) {
86-
let value;
87-
switch (type) {
88-
case 'string':
89-
value = String(input);
90-
break;
91-
case 'integer':
92-
case 'number':
93-
value = Number(input);
94-
break;
95-
default:
96-
value = input;
97-
break;
98-
}
99-
// Can be a string if validation fails
100-
const isValid = (await validator(value)) === true;
101-
if (isValid) {
102-
return value;
103-
}
104-
}
71+
let prompts: typeof import('@inquirer/prompts') | undefined;
72+
const answers: Record<string, JsonValue> = {};
10573

106-
return input;
107-
};
108-
}
74+
for (const definition of definitions) {
75+
// Only load prompt package if needed
76+
prompts ??= await import('@inquirer/prompts');
10977

11078
switch (definition.type) {
11179
case 'confirmation':
112-
return { ...question, type: 'confirm' };
80+
answers[definition.id] = await prompts.confirm({
81+
message: definition.message,
82+
default: definition.default as boolean | undefined,
83+
});
84+
break;
11385
case 'list':
114-
return {
115-
...question,
116-
type: definition.multiselect ? 'checkbox' : 'list',
117-
choices:
118-
definition.items &&
119-
definition.items.map((item) => {
120-
if (typeof item == 'string') {
121-
return item;
122-
} else {
123-
return {
124-
name: item.label,
125-
value: item.value,
126-
};
86+
if (!definition.items?.length) {
87+
continue;
88+
}
89+
90+
const choices = definition.items?.map((item) => {
91+
return typeof item == 'string'
92+
? {
93+
name: item,
94+
value: item,
95+
}
96+
: {
97+
name: item.label,
98+
value: item.value,
99+
};
100+
});
101+
102+
answers[definition.id] = await (
103+
definition.multiselect ? prompts.checkbox : prompts.select
104+
)({
105+
message: definition.message,
106+
default: definition.default,
107+
choices,
108+
});
109+
break;
110+
case 'input':
111+
let finalValue: JsonValue | undefined;
112+
answers[definition.id] = await prompts.input({
113+
message: definition.message,
114+
default: definition.default as string | undefined,
115+
async validate(value) {
116+
if (definition.validator === undefined) {
117+
return true;
118+
}
119+
120+
let lastValidation: ReturnType<typeof definition.validator> = false;
121+
for (const type of definition.propertyTypes) {
122+
let potential;
123+
switch (type) {
124+
case 'string':
125+
potential = String(value);
126+
break;
127+
case 'integer':
128+
case 'number':
129+
potential = Number(value);
130+
break;
131+
default:
132+
potential = value;
133+
break;
127134
}
128-
}),
129-
};
130-
default:
131-
return { ...question, type: definition.type };
135+
lastValidation = await definition.validator(potential);
136+
137+
// Can be a string if validation fails
138+
if (lastValidation === true) {
139+
finalValue = potential;
140+
141+
return true;
142+
}
143+
}
144+
145+
return lastValidation;
146+
},
147+
});
148+
149+
// Use validated value if present.
150+
// This ensures the correct type is inserted into the final schema options.
151+
if (finalValue !== undefined) {
152+
answers[definition.id] = finalValue;
153+
}
154+
break;
132155
}
133-
});
134-
const { default: inquirer } = await loadEsmModule<typeof import('inquirer')>('inquirer');
156+
}
135157

136-
return inquirer.prompt(questions);
158+
return answers;
137159
};
138160
}
139161

@@ -487,29 +509,3 @@ if (require.main === module) {
487509
throw e;
488510
});
489511
}
490-
491-
/**
492-
* Lazily compiled dynamic import loader function.
493-
*/
494-
let load: (<T>(modulePath: string | URL) => Promise<T>) | undefined;
495-
496-
/**
497-
* This uses a dynamic import to load a module which may be ESM.
498-
* CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
499-
* will currently, unconditionally downlevel dynamic import into a require call.
500-
* require calls cannot load ESM code and will result in a runtime error. To workaround
501-
* this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
502-
* Once TypeScript provides support for keeping the dynamic import this workaround can
503-
* be dropped.
504-
*
505-
* @param modulePath The path of the module to load.
506-
* @returns A Promise that resolves to the dynamically imported module.
507-
*/
508-
export function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
509-
load ??= new Function('modulePath', `return import(modulePath);`) as Exclude<
510-
typeof load,
511-
undefined
512-
>;
513-
514-
return load(modulePath);
515-
}

‎packages/angular_devkit/schematics_cli/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
"dependencies": {
1919
"@angular-devkit/core": "0.0.0-PLACEHOLDER",
2020
"@angular-devkit/schematics": "0.0.0-PLACEHOLDER",
21+
"@inquirer/prompts": "5.0.5",
2122
"ansi-colors": "4.1.3",
22-
"inquirer": "9.2.23",
2323
"symbol-observable": "4.0.0",
2424
"yargs-parser": "21.1.1"
2525
}

0 commit comments

Comments
 (0)