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.
This commit is contained in:
Charles Lyding 2024-06-06 14:59:59 -04:00 committed by Charles
parent d6aa216553
commit d36c53bc82
3 changed files with 85 additions and 90 deletions

View File

@ -51,11 +51,10 @@ ts_library(
"//packages/angular_devkit/schematics",
"//packages/angular_devkit/schematics/tasks",
"//packages/angular_devkit/schematics/tools",
"@npm//@types/inquirer",
"@npm//@inquirer/prompts",
"@npm//@types/node",
"@npm//@types/yargs-parser",
"@npm//ansi-colors",
"@npm//inquirer",
"@npm//symbol-observable",
"@npm//yargs-parser",
],

View File

@ -9,12 +9,11 @@
// symbol polyfill must go first
import 'symbol-observable';
import type { logging, schema } from '@angular-devkit/core';
import type { JsonValue, logging, schema } from '@angular-devkit/core';
import { ProcessOutput, createConsoleLogger } from '@angular-devkit/core/node';
import { UnsuccessfulWorkflowExecution } from '@angular-devkit/schematics';
import { NodeWorkflow } from '@angular-devkit/schematics/tools';
import ansiColors from 'ansi-colors';
import type { Question, QuestionCollection } from 'inquirer';
import { existsSync } from 'node:fs';
import * as path from 'node:path';
import yargsParser, { camelCase, decamelize } from 'yargs-parser';
@ -69,71 +68,94 @@ function _listSchematics(workflow: NodeWorkflow, collectionName: string, logger:
function _createPromptProvider(): schema.PromptProvider {
return async (definitions) => {
const questions: QuestionCollection = definitions.map((definition) => {
const question: Question = {
name: definition.id,
message: definition.message,
default: definition.default,
};
let prompts: typeof import('@inquirer/prompts') | undefined;
const answers: Record<string, JsonValue> = {};
const validator = definition.validator;
if (validator) {
question.validate = (input) => validator(input);
// Filter allows transformation of the value prior to validation
question.filter = async (input) => {
for (const type of definition.propertyTypes) {
let value;
switch (type) {
case 'string':
value = String(input);
break;
case 'integer':
case 'number':
value = Number(input);
break;
default:
value = input;
break;
}
// Can be a string if validation fails
const isValid = (await validator(value)) === true;
if (isValid) {
return value;
}
}
return input;
};
}
for (const definition of definitions) {
// Only load prompt package if needed
prompts ??= await import('@inquirer/prompts');
switch (definition.type) {
case 'confirmation':
return { ...question, type: 'confirm' };
answers[definition.id] = await prompts.confirm({
message: definition.message,
default: definition.default as boolean | undefined,
});
break;
case 'list':
return {
...question,
type: definition.multiselect ? 'checkbox' : 'list',
choices:
definition.items &&
definition.items.map((item) => {
if (typeof item == 'string') {
return item;
} else {
return {
name: item.label,
value: item.value,
};
}
}),
};
default:
return { ...question, type: definition.type };
}
});
const { default: inquirer } = await loadEsmModule<typeof import('inquirer')>('inquirer');
if (!definition.items?.length) {
continue;
}
return inquirer.prompt(questions);
const choices = definition.items?.map((item) => {
return typeof item == 'string'
? {
name: item,
value: item,
}
: {
name: item.label,
value: item.value,
};
});
answers[definition.id] = await (
definition.multiselect ? prompts.checkbox : prompts.select
)({
message: definition.message,
default: definition.default,
choices,
});
break;
case 'input':
let finalValue: JsonValue | undefined;
answers[definition.id] = await prompts.input({
message: definition.message,
default: definition.default as string | undefined,
async validate(value) {
if (definition.validator === undefined) {
return true;
}
let lastValidation: ReturnType<typeof definition.validator> = false;
for (const type of definition.propertyTypes) {
let potential;
switch (type) {
case 'string':
potential = String(value);
break;
case 'integer':
case 'number':
potential = Number(value);
break;
default:
potential = value;
break;
}
lastValidation = await definition.validator(potential);
// Can be a string if validation fails
if (lastValidation === true) {
finalValue = potential;
return true;
}
}
return lastValidation;
},
});
// Use validated value if present.
// This ensures the correct type is inserted into the final schema options.
if (finalValue !== undefined) {
answers[definition.id] = finalValue;
}
break;
}
}
return answers;
};
}
@ -487,29 +509,3 @@ if (require.main === module) {
throw e;
});
}
/**
* Lazily compiled dynamic import loader function.
*/
let load: (<T>(modulePath: string | URL) => Promise<T>) | undefined;
/**
* This uses a dynamic import to load a module which may be ESM.
* CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
* will currently, unconditionally downlevel dynamic import into a require call.
* require calls cannot load ESM code and will result in a runtime error. To workaround
* this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
* Once TypeScript provides support for keeping the dynamic import this workaround can
* be dropped.
*
* @param modulePath The path of the module to load.
* @returns A Promise that resolves to the dynamically imported module.
*/
export function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
load ??= new Function('modulePath', `return import(modulePath);`) as Exclude<
typeof load,
undefined
>;
return load(modulePath);
}

View File

@ -18,8 +18,8 @@
"dependencies": {
"@angular-devkit/core": "0.0.0-PLACEHOLDER",
"@angular-devkit/schematics": "0.0.0-PLACEHOLDER",
"@inquirer/prompts": "5.0.5",
"ansi-colors": "4.1.3",
"inquirer": "9.2.23",
"symbol-observable": "4.0.0",
"yargs-parser": "21.1.1"
}