cli/new: move built-in prompts to be derived from role instead

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-02-07 15:56:49 +01:00
parent d6996fcb66
commit 64f90cbea0
15 changed files with 110 additions and 183 deletions
@@ -29,20 +29,23 @@ describe('collectTemplateParams', () => {
id: 'test',
templatePath: '/test',
targetPath: '/example',
role: 'frontend-plugin' as const,
},
prefilledParams: {
pluginId: 'test',
owner: '',
},
prefilledParams: {},
};
it('should return default values if not provided', async () => {
await expect(collectTemplateParams(baseOptions)).resolves.toEqual({
id: '',
pluginId: 'test',
private: true,
baseVersion: '0.1.0',
owner: '',
license: 'Apache-2.0',
targetPath: '/example',
scope: '',
moduleId: '',
});
});
@@ -53,61 +56,33 @@ describe('collectTemplateParams', () => {
config: { ...baseOptions.config, globals: { foo: 'bar' } },
}),
).resolves.toEqual({
id: '',
pluginId: 'test',
private: true,
baseVersion: '0.1.0',
owner: '',
license: 'Apache-2.0',
targetPath: '/example',
scope: '',
moduleId: '',
foo: 'bar',
});
});
it('should use prefilled parameters', async () => {
await expect(
collectTemplateParams({
...baseOptions,
template: {
...baseOptions.template,
prompts: ['id'],
},
prefilledParams: { id: 'test' },
}),
).resolves.toEqual({
id: 'test',
private: true,
baseVersion: '0.1.0',
owner: '',
license: 'Apache-2.0',
targetPath: '/example',
scope: '',
moduleId: '',
});
});
it('should prompt for parameters', async () => {
jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ id: 'test' });
it('should prompt for missing parameters', async () => {
jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ pluginId: 'other' });
await expect(
collectTemplateParams({
...baseOptions,
template: {
...baseOptions.template,
prompts: ['id'],
},
prefilledParams: {},
}),
).resolves.toEqual({
id: 'test',
pluginId: 'other',
private: true,
baseVersion: '0.1.0',
owner: '',
license: 'Apache-2.0',
targetPath: '/example',
scope: '',
moduleId: '',
});
});
});
@@ -14,10 +14,11 @@
* limitations under the License.
*/
import inquirer from 'inquirer';
import { getCodeownersFilePath } from '../../codeowners';
import { paths } from '../../paths';
import { NewConfig } from '../types';
import { promptOptions } from './prompts';
import { buildCustomPrompt, getPromptsForRole, ownerPrompt } from './prompts';
import { NewTemplate } from '../types';
import { Options } from '../execution/utils';
@@ -28,11 +29,9 @@ type CollectTemplateParamsOptions = {
};
const defaultParams = {
id: '',
owner: '',
license: 'Apache-2.0',
scope: '',
moduleId: '',
baseVersion: '0.1.0',
private: true,
};
@@ -44,27 +43,28 @@ export async function collectTemplateParams(
const codeOwnersFilePath = await getCodeownersFilePath(paths.targetRoot);
const prompts = template.prompts ?? [];
const prompts = getPromptsForRole(template.role);
const prefilledAnswers = Object.fromEntries(
prompts.flatMap(prompt => {
const id = typeof prompt === 'string' ? prompt : prompt.id;
const answer = prefilledParams[id];
return answer ? [[id, answer]] : [];
}),
);
if (codeOwnersFilePath) {
prompts.push(ownerPrompt());
}
if (template.prompts) {
prompts.push(...template.prompts.map(buildCustomPrompt));
}
const promptAnswers = await promptOptions({
prompts:
prompts.filter(
prompt =>
!Object.hasOwn(
prefilledAnswers,
typeof prompt === 'string' ? prompt : prompt.id,
),
) ?? [],
codeOwnersFilePath,
});
const needsAnswer = [];
const prefilledAnswers = {} as Record<string, string | number | boolean>;
for (const prompt of prompts) {
if (prefilledParams[prompt.name] !== undefined) {
prefilledAnswers[prompt.name] = prefilledParams[prompt.name];
} else {
needsAnswer.push(prompt);
}
}
const promptAnswers = await inquirer.prompt<
Record<string, string | number | boolean>
>(needsAnswer);
return {
...defaultParams,
+64 -75
View File
@@ -14,18 +14,31 @@
* limitations under the License.
*/
import inquirer, { Answers, DistinctQuestion } from 'inquirer';
import { NewTemplatePrompt } from '../types';
import { Answers, DistinctQuestion } from 'inquirer';
import { NewTemplatePrompt, TemplateRole } from '../types';
import { parseOwnerIds } from '../../codeowners';
export type Prompt<TOptions extends Answers> = DistinctQuestion<TOptions> & {
name: string;
};
export function pluginIdPrompt(): Prompt<{ id: string }> {
export type Prompt<TOptions extends Answers> = DistinctQuestion<TOptions>;
export function namePrompt(): Prompt<{ name: string }> {
return {
type: 'input',
name: 'id',
name: 'name',
message: 'Enter the name of the package, without scope [required]',
validate: (value: string) => {
if (!value) {
return 'Please enter the name of the package';
} else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {
return 'Package names must be lowercase and contain only letters, digits, and dashes.';
}
return true;
},
};
}
export function pluginIdPrompt(): Prompt<{ pluginId: string }> {
return {
type: 'input',
name: 'pluginId',
message: 'Enter the ID of the plugin [required]',
validate: (value: string) => {
if (!value) {
@@ -54,30 +67,31 @@ export function moduleIdIdPrompt(): Prompt<{ moduleId: string }> {
};
}
export function npmRegistryPrompt(): Prompt<{ npmRegistry: string }> {
return {
type: 'input',
name: 'npmRegistry',
message: 'Please specify your NPM registry [optional]',
validate: (value: string) => {
if (!value) {
return 'Please enter the URL of your NPM registry';
} else if (!/^http*$/.test(value)) {
return 'Invalid URL.';
}
return true;
},
};
export function getPromptsForRole(role: TemplateRole) {
switch (role) {
case 'web-library':
case 'node-library':
case 'common-library':
return [namePrompt()];
case 'plugin-web-library':
case 'plugin-node-library':
case 'plugin-common-library':
case 'frontend-plugin':
case 'backend-plugin':
return [pluginIdPrompt()];
case 'frontend-plugin-module':
case 'backend-plugin-module':
return [pluginIdPrompt(), moduleIdIdPrompt()];
default:
return [];
}
}
export function ownerPrompt(
codeOwnersPath: string | undefined,
): Prompt<{ owner?: string }> {
export function ownerPrompt(): Prompt<{ owner?: string }> {
return {
type: 'input',
name: 'owner',
message: 'Enter an owner to add to CODEOWNERS [optional]',
when: Boolean(codeOwnersPath),
validate: (value: string) => {
if (!value) {
return true;
@@ -93,58 +107,33 @@ export function ownerPrompt(
};
}
export async function promptOptions({
prompts,
codeOwnersFilePath,
}: {
prompts: NewTemplatePrompt[];
codeOwnersFilePath: string | undefined;
}): Promise<Record<string, string>> {
const answers = await inquirer.prompt(
prompts.map((prompt: NewTemplatePrompt) => {
if (typeof prompt === 'string') {
switch (prompt) {
case 'id':
return pluginIdPrompt();
case 'moduleId':
return moduleIdIdPrompt();
case 'npmRegistry':
return npmRegistryPrompt();
case 'owner':
return ownerPrompt(codeOwnersFilePath);
export function buildCustomPrompt(
prompt: NewTemplatePrompt,
): Prompt<{ [key: string]: string }> {
return {
type: 'input',
name: prompt.id,
message: prompt.prompt,
validate: (value: string) => {
if (!value) {
return `Please provide a value for ${prompt.id}`;
} else if (prompt.validate) {
let valid: boolean;
let message: string;
switch (prompt.validate) {
case 'backstage-id':
valid = /^[a-z0-9]+(-[a-z0-9]+)*$/.test(value);
message =
'Value must be lowercase and contain only letters, digits, and dashes.';
break;
default:
throw new Error(
`There is no built-in prompt with the following id: ${prompt}`,
`There is no built-in validator with the following id: ${prompt.validate}`,
);
}
return valid || message;
}
return {
type: 'input',
name: prompt.id,
message: prompt.prompt,
validate: (value: string) => {
if (!value) {
return `Please provide a value for ${prompt.id}`;
} else if (prompt.validate) {
let valid: boolean;
let message: string;
switch (prompt.validate) {
case 'backstage-id':
valid = /^[a-z0-9]+(-[a-z0-9]+)*$/.test(value);
message =
'Value must be lowercase and contain only letters, digits, and dashes.';
break;
default:
throw new Error(
`There is no built-in validator with the following id: ${prompt.validate}`,
);
}
return valid || message;
}
return true;
},
};
}),
);
return { ...answers };
return true;
},
};
}
@@ -16,14 +16,12 @@
import { NewTemplate } from '../types';
export interface Options extends Record<string, string | boolean> {
id: string;
private: boolean;
baseVersion: string;
license: string;
targetPath: string;
owner: string;
scope: string;
moduleId: string;
}
export const resolvePackageName = (options: {
@@ -33,15 +33,12 @@ const templateDefinitionSchema = z
role: z.enum(TEMPLATE_ROLES),
prompts: z
.array(
z.union([
z.string(),
z.object({
id: z.string(),
prompt: z.string(),
validate: z.string().optional(),
default: z.union([z.string(), z.boolean(), z.number()]).optional(),
}),
]),
z.object({
id: z.string(),
prompt: z.string(),
validate: z.string().optional(),
default: z.union([z.string(), z.boolean(), z.number()]).optional(),
}),
)
.optional(),
additionalActions: z.array(z.string()).optional(),
+6 -8
View File
@@ -38,14 +38,12 @@ export type NewTemplatePointer = {
target: string;
};
export type NewTemplatePrompt =
| {
id: string;
prompt: string;
validate?: string;
default?: string | boolean | number;
}
| string;
export type NewTemplatePrompt = {
id: string;
prompt: string;
validate?: string;
default?: string | boolean | number;
};
export const TEMPLATE_ROLES = [
'web-library',
@@ -2,10 +2,6 @@ description: A new backend module that extends an existing backend plugin with a
template: ./default-backend-module
targetPath: plugins
role: backend-plugin-module
prompts:
- id
- moduleId
- owner
additionalActions:
- install-backend
- add-backend
@@ -2,9 +2,6 @@ description: A new backend plugin
template: ./default-backend-plugin
targetPath: plugins
role: backend-plugin
prompts:
- id
- owner
additionalActions:
- install-backend
- add-backend
@@ -2,6 +2,3 @@ description: A new isomorphic common plugin package
template: ./default-common-plugin-package
targetPath: plugins
role: plugin-common-library
prompts:
- id
- owner
@@ -2,6 +2,3 @@ description: A new Node.js library plugin package
template: ./default-node-plugin-package
targetPath: plugins
role: plugin-node-library
prompts:
- id
- owner
@@ -2,9 +2,6 @@ description: A new frontend plugin
template: ./default-plugin
targetPath: plugins
role: frontend-plugin
prompts:
- id
- owner
additionalActions:
- install-frontend
- add-frontend-legacy
@@ -2,6 +2,3 @@ description: A new web library plugin package
template: ./default-react-plugin-package
targetPath: plugins
role: plugin-web-library
prompts:
- id
- owner
@@ -2,6 +2,3 @@ description: A new node-library package, exporting shared functionality for back
template: ./node-library-package
targetPath: packages
role: node-library
prompts:
- id
- owner
@@ -2,13 +2,8 @@ description: A module exporting custom actions for @backstage/plugin-scaffolder-
template: ./scaffolder-module
targetPath: plugins
role: backend-plugin-module
prompts:
- id: id
prompt: Enter the ID of the plugin [required]
default: scaffolder
validate: backstage-id
- moduleId
- owner
params:
pluginId: scaffolder
additionalActions:
- install-backend
- add-backend
@@ -2,6 +2,3 @@ description: A new web-library package, exporting shared functionality for front
template: ./web-library-package
targetPath: packages
role: web-library
prompts:
- id
- owner