cli/new: refactor prompts

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-02-07 14:19:19 +01:00
parent a2ad89b50c
commit 35f1e32e3b
7 changed files with 204 additions and 106 deletions
+3 -3
View File
@@ -33,16 +33,16 @@ export default async (opts: ArgOptions) => {
...globals
} = opts;
const argOptions = parseOptions(rawArgOptions);
const prefilledParams = parseParams(rawArgOptions);
await createNewPackage({
argOptions,
prefilledParams,
preselectedTemplateId,
globals,
});
};
function parseOptions(optionStrings: string[]): Record<string, string> {
function parseParams(optionStrings: string[]): Record<string, string> {
const options: Record<string, string> = {};
for (const str of optionStrings) {
@@ -0,0 +1,111 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import inquirer from 'inquirer';
import { NewConfig } from '../config/types';
import { collectTemplateParams } from './collectTemplateParams';
describe('collectTemplateParams', () => {
const baseOptions = {
config: {
isUsingDefaultTemplates: false,
templatePointers: [],
globals: {},
} satisfies NewConfig,
template: {
id: 'test',
templatePath: '/test',
targetPath: '/example',
},
globals: {},
prefilledParams: {},
};
it('should return default values if not provided', async () => {
await expect(collectTemplateParams(baseOptions)).resolves.toEqual({
id: '',
private: true,
baseVersion: '0.1.0',
owner: '',
license: 'Apache-2.0',
targetPath: '/example',
scope: '',
moduleId: '',
});
});
it('should include all non-standard global and prompt values', async () => {
await expect(
collectTemplateParams({ ...baseOptions, globals: { foo: 'bar' } }),
).resolves.toEqual({
id: '',
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' });
await expect(
collectTemplateParams({
...baseOptions,
template: {
...baseOptions.template,
prompts: ['id'],
},
prefilledParams: {},
}),
).resolves.toEqual({
id: 'test',
private: true,
baseVersion: '0.1.0',
owner: '',
license: 'Apache-2.0',
targetPath: '/example',
scope: '',
moduleId: '',
});
});
});
@@ -0,0 +1,78 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getCodeownersFilePath } from '../../codeowners';
import { paths } from '../../paths';
import { NewConfig } from '../config/types';
import { promptOptions } from './prompts';
import { Template } from '../types';
import { Options } from '../utils';
type CollectTemplateParamsOptions = {
config: NewConfig;
template: Template;
globals: Record<string, string | number | boolean>;
prefilledParams: Record<string, string | number | boolean>;
};
const defaultParams = {
id: '',
owner: '',
license: 'Apache-2.0',
scope: '',
moduleId: '',
baseVersion: '0.1.0',
private: true,
};
export async function collectTemplateParams(
options: CollectTemplateParamsOptions,
): Promise<Options> {
const { config, template, globals, prefilledParams } = options;
const codeOwnersFilePath = await getCodeownersFilePath(paths.targetRoot);
const prompts = template.prompts ?? [];
const prefilledAnswers = Object.fromEntries(
prompts.flatMap(prompt => {
const id = typeof prompt === 'string' ? prompt : prompt.id;
const answer = prefilledParams[id];
return answer ? [[id, answer]] : [];
}),
);
const promptAnswers = await promptOptions({
prompts:
prompts.filter(
prompt =>
!Object.hasOwn(
prefilledAnswers,
typeof prompt === 'string' ? prompt : prompt.id,
),
) ?? [],
globals: config.globals,
codeOwnersFilePath,
});
return {
...defaultParams,
...globals,
...prefilledAnswers,
...promptAnswers,
targetPath: template.targetPath,
};
}
@@ -15,8 +15,8 @@
*/
import inquirer from 'inquirer';
import { Prompt, ConfigurablePrompt } from './types';
import { parseOwnerIds } from '../codeowners';
import { Prompt, ConfigurablePrompt } from '../types';
import { parseOwnerIds } from '../../codeowners';
export function pluginIdPrompt(): Prompt<{ id: string }> {
return {
+9 -26
View File
@@ -21,15 +21,15 @@ import { assertError } from '@backstage/errors';
import { paths } from '../paths';
import { Task } from '../tasks';
import { addCodeownersEntry, getCodeownersFilePath } from '../codeowners';
import { addCodeownersEntry } from '../codeowners';
import { promptOptions } from './prompts';
import { populateOptions, createDirName, resolvePackageName } from './utils';
import { createDirName, resolvePackageName } from './utils';
import { runAdditionalActions } from './additionalActions';
import { executePluginPackageTemplate } from './executeTemplate';
import { TemporaryDirectoryManager } from './TemporaryDirectoryManager';
import { loadNewConfig } from './config/loadNewConfig';
import { NewTemplateLoader } from './loader/NewTemplateLoader';
import { collectTemplateParams } from './collection/collectTemplateParams';
export type CreateNewPackageOptions = {
preselectedTemplateId?: string;
@@ -40,7 +40,7 @@ export type CreateNewPackageOptions = {
license?: string;
baseVersion?: string;
};
argOptions: { [name in string]?: string };
prefilledParams: Record<string, string>;
};
export async function createNewPackage(options: CreateNewPackageOptions) {
@@ -52,29 +52,12 @@ export async function createNewPackage(options: CreateNewPackageOptions) {
);
const template = await NewTemplateLoader.loadTemplate(selectedTemplate);
const codeOwnersFilePath = await getCodeownersFilePath(paths.targetRoot);
const prefilledAnswers = Object.fromEntries(
(template.prompts ?? []).flatMap(prompt => {
const id = typeof prompt === 'string' ? prompt : prompt.id;
const answer = options.argOptions[id];
return answer ? [[id, answer]] : [];
}),
);
const promptAnswers = await promptOptions({
prompts:
template.prompts?.filter(
prompt =>
!Object.hasOwn(
prefilledAnswers,
typeof prompt === 'string' ? prompt : prompt.id,
),
) ?? [],
globals: newConfig.globals,
codeOwnersFilePath,
const params = await collectTemplateParams({
config: newConfig,
template,
globals: options.globals,
prefilledParams: options.prefilledParams,
});
const answers = { ...prefilledAnswers, ...promptAnswers };
const params = populateOptions({ ...answers, ...options.globals }, template);
const tmpDirManager = TemporaryDirectoryManager.create();
+1 -58
View File
@@ -13,12 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
resolvePackageName,
populateOptions,
createDirName,
Options,
} from './utils';
import { resolvePackageName, createDirName, Options } from './utils';
import { Template } from './types';
describe('resolvePackageName', () => {
@@ -83,58 +78,6 @@ describe('resolvePackageName', () => {
});
});
describe('populateOptions', () => {
it('should return default values if not provided', () => {
expect(populateOptions({}, { targetPath: '/example' } as Template)).toEqual(
{
id: '',
private: true,
baseVersion: '0.1.0',
owner: '',
license: 'Apache-2.0',
targetPath: '/example',
scope: '',
moduleId: '',
},
);
});
it('should include all non-standard global and prompt values', () => {
expect(
populateOptions({ foo: 'bar' }, {
targetPath: '/example',
} as Template),
).toEqual({
id: '',
private: true,
baseVersion: '0.1.0',
owner: '',
license: 'Apache-2.0',
targetPath: '/example',
scope: '',
moduleId: '',
foo: 'bar',
});
});
it('should priority global targetPath over the targetPath specified in template', () => {
expect(
populateOptions({ targetPath: '/global' }, {
targetPath: '/example',
} as Template),
).toEqual({
id: '',
private: true,
baseVersion: '0.1.0',
owner: '',
license: 'Apache-2.0',
targetPath: '/global',
scope: '',
moduleId: '',
});
});
});
describe('createDirName', () => {
it('should return name in the backend-module format if backendModulePrefix is set to true', () => {
expect(
-17
View File
@@ -49,23 +49,6 @@ export const resolvePackageName = (options: {
return plugin ? `backstage-plugin-${baseName}` : baseName;
};
export function populateOptions(
options: { [name in string]?: string | boolean },
template: Template,
): Options {
return {
id: '',
owner: '',
license: 'Apache-2.0',
scope: '',
moduleId: '',
baseVersion: '0.1.0',
private: true,
targetPath: template.targetPath,
...options,
};
}
export function createDirName(template: Template, options: Options) {
if (!options.id) {
throw new Error(`id prompt is mandatory for all cli templates`);