cli/new: update config loading tests + fixes

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-02-11 01:09:29 +01:00
parent 7fb1408bb2
commit cf25d713a3
7 changed files with 95 additions and 50 deletions
@@ -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}!`);
}
}
}
@@ -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',
@@ -30,8 +30,7 @@ describe('collectTemplateParams', () => {
packageNamePluginInfix: 'plugin-',
} satisfies PortableTemplateConfig,
template: {
id: 'test',
templatePath: '/test',
name: 'test',
role: 'frontend-plugin' as const,
files: [],
parameters: {},
@@ -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,
@@ -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}'`,
@@ -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 () => {
@@ -19,22 +19,22 @@ import { PortableTemplateConfig, PortableTemplatePointer } from '../types';
export async function selectTemplateInteractively(
config: PortableTemplateConfig,
preselectedTemplateId?: string,
preselectedTemplateName?: string,
): Promise<PortableTemplatePointer> {
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;
}