From 35f1e32e3b5325ea9c69d75ea0c3bfe53ba67924 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 7 Feb 2025 14:19:19 +0100 Subject: [PATCH] cli/new: refactor prompts Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/new/new.ts | 6 +- .../collection/collectTemplateParams.test.ts | 111 ++++++++++++++++++ .../new/collection/collectTemplateParams.ts | 78 ++++++++++++ .../src/lib/new/{ => collection}/prompts.ts | 4 +- packages/cli/src/lib/new/createNewPackage.ts | 35 ++---- packages/cli/src/lib/new/utils.test.ts | 59 +--------- packages/cli/src/lib/new/utils.ts | 17 --- 7 files changed, 204 insertions(+), 106 deletions(-) create mode 100644 packages/cli/src/lib/new/collection/collectTemplateParams.test.ts create mode 100644 packages/cli/src/lib/new/collection/collectTemplateParams.ts rename packages/cli/src/lib/new/{ => collection}/prompts.ts (97%) diff --git a/packages/cli/src/commands/new/new.ts b/packages/cli/src/commands/new/new.ts index 273aba8957..0ab5cf027f 100644 --- a/packages/cli/src/commands/new/new.ts +++ b/packages/cli/src/commands/new/new.ts @@ -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 { +function parseParams(optionStrings: string[]): Record { const options: Record = {}; for (const str of optionStrings) { diff --git a/packages/cli/src/lib/new/collection/collectTemplateParams.test.ts b/packages/cli/src/lib/new/collection/collectTemplateParams.test.ts new file mode 100644 index 0000000000..5668273ff6 --- /dev/null +++ b/packages/cli/src/lib/new/collection/collectTemplateParams.test.ts @@ -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: '', + }); + }); +}); diff --git a/packages/cli/src/lib/new/collection/collectTemplateParams.ts b/packages/cli/src/lib/new/collection/collectTemplateParams.ts new file mode 100644 index 0000000000..39d7c0635a --- /dev/null +++ b/packages/cli/src/lib/new/collection/collectTemplateParams.ts @@ -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; + prefilledParams: Record; +}; + +const defaultParams = { + id: '', + owner: '', + license: 'Apache-2.0', + scope: '', + moduleId: '', + baseVersion: '0.1.0', + private: true, +}; + +export async function collectTemplateParams( + options: CollectTemplateParamsOptions, +): Promise { + 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, + }; +} diff --git a/packages/cli/src/lib/new/prompts.ts b/packages/cli/src/lib/new/collection/prompts.ts similarity index 97% rename from packages/cli/src/lib/new/prompts.ts rename to packages/cli/src/lib/new/collection/prompts.ts index c7ba0fcaba..246c899a28 100644 --- a/packages/cli/src/lib/new/prompts.ts +++ b/packages/cli/src/lib/new/collection/prompts.ts @@ -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 { diff --git a/packages/cli/src/lib/new/createNewPackage.ts b/packages/cli/src/lib/new/createNewPackage.ts index 19208f4c6b..cb1d85908e 100644 --- a/packages/cli/src/lib/new/createNewPackage.ts +++ b/packages/cli/src/lib/new/createNewPackage.ts @@ -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; }; 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(); diff --git a/packages/cli/src/lib/new/utils.test.ts b/packages/cli/src/lib/new/utils.test.ts index b2ec958a69..5e15812077 100644 --- a/packages/cli/src/lib/new/utils.test.ts +++ b/packages/cli/src/lib/new/utils.test.ts @@ -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( diff --git a/packages/cli/src/lib/new/utils.ts b/packages/cli/src/lib/new/utils.ts index 02f885ca20..436151405d 100644 --- a/packages/cli/src/lib/new/utils.ts +++ b/packages/cli/src/lib/new/utils.ts @@ -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`);