diff --git a/packages/cli/src/lib/new/execution/executePortableTemplate.ts b/packages/cli/src/lib/new/execution/executePortableTemplate.ts index 435e909278..1dc34a71c9 100644 --- a/packages/cli/src/lib/new/execution/executePortableTemplate.ts +++ b/packages/cli/src/lib/new/execution/executePortableTemplate.ts @@ -58,7 +58,7 @@ export async function executePortableTemplate( }); Task.log(); - Task.log(`🎉 Successfully created ${template.id}`); + Task.log(`🎉 Successfully created ${template.name}`); Task.log(); } catch (error) { assertError(error); @@ -74,7 +74,7 @@ export async function executePortableTemplate( 'continue manually, but you can also revert the changes and try again.', ); - Task.error(`🔥 Failed to create ${template.id}!`); + Task.error(`🔥 Failed to create ${template.name}!`); } } } diff --git a/packages/cli/src/lib/new/execution/writeTemplateContents.test.ts b/packages/cli/src/lib/new/execution/writeTemplateContents.test.ts index 86d5e5f4f7..2d5804f4a6 100644 --- a/packages/cli/src/lib/new/execution/writeTemplateContents.test.ts +++ b/packages/cli/src/lib/new/execution/writeTemplateContents.test.ts @@ -39,7 +39,7 @@ describe('writeTemplateContents', () => { it('should write an empty template', async () => { const { targetDir } = await writeTemplateContents( { - id: 'test', + name: 'test', files: [], role: 'frontend-plugin', parameters: {}, @@ -60,7 +60,7 @@ describe('writeTemplateContents', () => { it('should write template with various files', async () => { await writeTemplateContents( { - id: 'test', + name: 'test', files: [ { content: 'test', diff --git a/packages/cli/src/lib/new/preparation/collectPortableTemplateInput.test.ts b/packages/cli/src/lib/new/preparation/collectPortableTemplateInput.test.ts index 28231976e8..d19bdc5778 100644 --- a/packages/cli/src/lib/new/preparation/collectPortableTemplateInput.test.ts +++ b/packages/cli/src/lib/new/preparation/collectPortableTemplateInput.test.ts @@ -30,8 +30,7 @@ describe('collectTemplateParams', () => { packageNamePluginInfix: 'plugin-', } satisfies PortableTemplateConfig, template: { - id: 'test', - templatePath: '/test', + name: 'test', role: 'frontend-plugin' as const, files: [], parameters: {}, diff --git a/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.test.ts b/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.test.ts index 1fc1a8915c..3ce3cbd76b 100644 --- a/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.test.ts +++ b/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { realpathSync } from 'node:fs'; import { loadPortableTemplateConfig } from './loadPortableTemplateConfig'; import { defaultTemplates } from '../defaultTemplates'; import { createMockDirectory } from '@backstage/backend-test-utils'; @@ -31,10 +32,7 @@ describe('loadPortableTemplateConfig', () => { backstage: { cli: { new: { - templates: [ - { id: 'template1', target: 'path/to/template1' }, - { id: 'template2', target: 'path/to/template2' }, - ], + templates: ['./path/to/template1', './path/to/template2.yaml'], globals: { license: 'MIT', private: true, @@ -45,6 +43,9 @@ describe('loadPortableTemplateConfig', () => { }, }, }), + 'path/to/template1/template.yaml': + 'name: template1\nrole: frontend-plugin\n', + 'path/to/template2.yaml': 'name: template2\nrole: frontend-plugin\n', }); await expect( @@ -54,8 +55,14 @@ describe('loadPortableTemplateConfig', () => { ).resolves.toEqual({ isUsingDefaultTemplates: false, templatePointers: [ - { id: 'template1', target: 'path/to/template1' }, - { id: 'template2', target: 'path/to/template2' }, + { + name: 'template1', + target: mockDir.resolve('path/to/template1/template.yaml'), + }, + { + name: 'template2', + target: mockDir.resolve('path/to/template2.yaml'), + }, ], license: 'MIT', private: true, @@ -75,8 +82,14 @@ describe('loadPortableTemplateConfig', () => { ).resolves.toEqual({ isUsingDefaultTemplates: false, templatePointers: [ - { id: 'template1', target: 'path/to/template1' }, - { id: 'template2', target: 'path/to/template2' }, + { + name: 'template1', + target: mockDir.resolve('path/to/template1/template.yaml'), + }, + { + name: 'template2', + target: mockDir.resolve('path/to/template2.yaml'), + }, ], license: 'nope', version: '0.1.0', @@ -92,11 +105,7 @@ describe('loadPortableTemplateConfig', () => { backstage: { cli: { new: { - templates: [ - 'default-backend-plugin', - { id: 'template1', target: 'path/to/template1' }, - 'default-frontend-plugin', - ], + templates: ['@my/package/templates/plugin', 'my-package'], globals: { license: 'MIT', private: true, @@ -107,6 +116,21 @@ describe('loadPortableTemplateConfig', () => { }, }, }), + node_modules: { + '@my': { + package: { + templates: { + plugin: { + 'template.yaml': + 'name: frontend-plugin\nrole: frontend-plugin\n', + }, + }, + }, + }, + 'my-package': { + 'template.yaml': 'name: backend-plugin\nrole: backend-plugin\n', + }, + }, }); await expect( @@ -117,15 +141,18 @@ describe('loadPortableTemplateConfig', () => { isUsingDefaultTemplates: false, templatePointers: [ { - id: 'backend-plugin', - description: 'A new backend plugin', - target: expect.stringMatching(/default-backend-plugin.yaml$/), + name: 'frontend-plugin', + target: realpathSync( + mockDir.resolve( + 'node_modules/@my/package/templates/plugin/template.yaml', + ), + ), }, - { id: 'template1', target: 'path/to/template1' }, { - id: 'frontend-plugin', - description: 'A new frontend plugin', - target: expect.stringMatching(/default-plugin.yaml$/), + name: 'backend-plugin', + target: realpathSync( + mockDir.resolve('node_modules/my-package/template.yaml'), + ), }, ], license: 'MIT', @@ -150,6 +177,12 @@ describe('loadPortableTemplateConfig', () => { }, }, }), + node_modules: Object.fromEntries( + defaultTemplates.map(t => [ + t, + { 'template.yaml': `name: x\nrole: web-library\n` }, + ]), + ), }); await expect( @@ -158,7 +191,12 @@ describe('loadPortableTemplateConfig', () => { }), ).resolves.toEqual({ isUsingDefaultTemplates: true, - templatePointers: defaultTemplates, + templatePointers: defaultTemplates.map(t => ({ + name: 'x', + target: realpathSync( + mockDir.resolve(`node_modules/${t}/template.yaml`), + ), + })), license: 'MIT', private: true, version: '0.1.0', @@ -195,7 +233,7 @@ describe('loadPortableTemplateConfig', () => { backstage: { cli: { new: { - templates: ['invalid'], + templates: ['./invalid'], }, }, }, @@ -207,13 +245,19 @@ describe('loadPortableTemplateConfig', () => { packagePath: mockDir.resolve('package.json'), }), ).rejects.toThrow( - /^Failed to load templating configuration from '.*'; caused by Validation error: Invalid enum value/, + `Failed to load template definition '.\/invalid'; caused by Error: ENOENT`, ); }); it('should handle missing backstage.new configuration', async () => { mockDir.setContent({ 'package.json': JSON.stringify({}), + node_modules: Object.fromEntries( + defaultTemplates.map(t => [ + t, + { 'template.yaml': `name: x\nrole: web-library\n` }, + ]), + ), }); await expect( @@ -222,7 +266,7 @@ describe('loadPortableTemplateConfig', () => { }), ).resolves.toEqual({ isUsingDefaultTemplates: true, - templatePointers: defaultTemplates, + templatePointers: expect.any(Array), license: 'Apache-2.0', version: '0.1.0', private: true, @@ -239,7 +283,7 @@ describe('loadPortableTemplateConfig', () => { }), ).resolves.toEqual({ isUsingDefaultTemplates: true, - templatePointers: defaultTemplates, + templatePointers: expect.any(Array), license: 'nope', version: '0.1.0', private: true, diff --git a/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.ts b/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.ts index a84956e121..35f24799a7 100644 --- a/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.ts +++ b/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.ts @@ -86,11 +86,11 @@ export async function loadPortableTemplateConfig( const basePath = dirname(pkgPath); const templatePointers = await Promise.all( - (config?.templates ?? defaultTemplates).map(pointer => { + (config?.templates ?? defaultTemplates).map(async pointer => { try { const templatePath = resolveLocalTemplatePath(pointer, basePath); - return peekLocalTemplateDefinition(templatePath); + return await peekLocalTemplateDefinition(templatePath); } catch (error) { throw new ForwardedError( `Failed to load template definition '${pointer}'`, diff --git a/packages/cli/src/lib/new/preparation/selectTemplateInteractively.test.ts b/packages/cli/src/lib/new/preparation/selectTemplateInteractively.test.ts index 6ff7acb5fe..adf815e7a6 100644 --- a/packages/cli/src/lib/new/preparation/selectTemplateInteractively.test.ts +++ b/packages/cli/src/lib/new/preparation/selectTemplateInteractively.test.ts @@ -23,8 +23,8 @@ describe('selectTemplateInteractively', () => { const mockConfig = { isUsingDefaultTemplates: false, templatePointers: [ - { id: 'template1', target: 'path/to/template1' }, - { id: 'template2', target: 'path/to/template2' }, + { name: 'template1', target: '/path/to/template1' }, + { name: 'template2', target: '/path/to/template2' }, ], } as PortableTemplateConfig; @@ -33,25 +33,27 @@ describe('selectTemplateInteractively', () => { }); it('should select a template interactively', async () => { - jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ id: 'template1' }); + jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ name: 'template1' }); const result = await selectTemplateInteractively(mockConfig); - expect(result).toEqual({ id: 'template1', target: 'path/to/template1' }); + expect(result).toEqual({ name: 'template1', target: '/path/to/template1' }); }); it('should error if interactive selections is not found', async () => { - jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ id: 'nonexistent' }); + jest + .spyOn(inquirer, 'prompt') + .mockResolvedValueOnce({ name: 'nonexistent' }); await expect(selectTemplateInteractively(mockConfig)).rejects.toThrow( "Template 'nonexistent' not found", ); }); - it('should use preselected template id', async () => { + it('should use preselected template name', async () => { const result = await selectTemplateInteractively(mockConfig, 'template2'); - expect(result).toEqual({ id: 'template2', target: 'path/to/template2' }); + expect(result).toEqual({ name: 'template2', target: '/path/to/template2' }); }); it('should throw an error if template is not found', async () => { diff --git a/packages/cli/src/lib/new/preparation/selectTemplateInteractively.ts b/packages/cli/src/lib/new/preparation/selectTemplateInteractively.ts index 808efffc0a..154e8af56f 100644 --- a/packages/cli/src/lib/new/preparation/selectTemplateInteractively.ts +++ b/packages/cli/src/lib/new/preparation/selectTemplateInteractively.ts @@ -19,22 +19,22 @@ import { PortableTemplateConfig, PortableTemplatePointer } from '../types'; export async function selectTemplateInteractively( config: PortableTemplateConfig, - preselectedTemplateId?: string, + preselectedTemplateName?: string, ): Promise { - let selectedId = preselectedTemplateId; + let selectedName = preselectedTemplateName; - if (config.isUsingDefaultTemplates && selectedId === 'plugin') { + if (config.isUsingDefaultTemplates && selectedName === 'plugin') { console.warn( `DEPRECATION WARNING: The 'plugin' template is deprecated, use 'frontend-plugin' instead`, ); - selectedId = 'frontend-plugin'; + selectedName = 'frontend-plugin'; } - if (!selectedId) { - const answers = await inquirer.prompt<{ id: string }>([ + if (!selectedName) { + const answers = await inquirer.prompt<{ name: string }>([ { type: 'list', - name: 'id', + name: 'name', message: 'What do you want to create?', choices: config.templatePointers.map(t => t.description @@ -43,12 +43,12 @@ export async function selectTemplateInteractively( ), }, ]); - selectedId = answers.id; + selectedName = answers.name; } - const template = config.templatePointers.find(t => t.name === selectedId); + const template = config.templatePointers.find(t => t.name === selectedName); if (!template) { - throw new Error(`Template '${selectedId}' not found`); + throw new Error(`Template '${selectedName}' not found`); } return template; }