cli/new: reject configurations with duplicate template names

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-02-11 14:23:56 +01:00
parent c0d11068d5
commit 65f913de22
2 changed files with 50 additions and 9 deletions
@@ -19,6 +19,7 @@ import { loadPortableTemplateConfig } from './loadPortableTemplateConfig';
import { defaultTemplates } from '../defaultTemplates';
import { createMockDirectory } from '@backstage/backend-test-utils';
import { TEMPLATE_FILE_NAME } from '../types';
import { basename } from 'node:path';
describe('loadPortableTemplateConfig', () => {
const mockDir = createMockDirectory();
@@ -174,7 +175,7 @@ describe('loadPortableTemplateConfig', () => {
node_modules: Object.fromEntries(
defaultTemplates.map(t => [
t,
{ [TEMPLATE_FILE_NAME]: `name: x\nrole: web-library\n` },
{ [TEMPLATE_FILE_NAME]: `name: ${basename(t)}\nrole: web-library\n` },
]),
),
});
@@ -186,7 +187,7 @@ describe('loadPortableTemplateConfig', () => {
).resolves.toEqual({
isUsingDefaultTemplates: true,
templatePointers: defaultTemplates.map(t => ({
name: 'x',
name: basename(t),
target: realpathSync(
mockDir.resolve(`node_modules/${t}`, TEMPLATE_FILE_NAME),
),
@@ -199,6 +200,34 @@ describe('loadPortableTemplateConfig', () => {
});
});
it('should reject templates with conflicting names', async () => {
mockDir.setContent({
'package.json': JSON.stringify({
backstage: {
cli: {
new: {
templates: ['./template1', './template2'],
},
},
},
}),
template1: {
[TEMPLATE_FILE_NAME]: 'name: test\nrole: frontend-plugin\n',
},
template2: {
[TEMPLATE_FILE_NAME]: 'name: test\nrole: backend-plugin\n',
},
});
await expect(
loadPortableTemplateConfig({
packagePath: mockDir.resolve('package.json'),
}),
).rejects.toThrow(
`Invalid template configuration, received conflicting template name 'test' from './template1' and './template2'`,
);
});
it('should throw an error if package.json is invalid', async () => {
mockDir.setContent({
'package.json': JSON.stringify({
@@ -271,7 +300,7 @@ describe('loadPortableTemplateConfig', () => {
node_modules: Object.fromEntries(
defaultTemplates.map(t => [
t,
{ [TEMPLATE_FILE_NAME]: `name: x\nrole: web-library\n` },
{ [TEMPLATE_FILE_NAME]: `name: ${basename(t)}\nrole: web-library\n` },
]),
),
});
@@ -89,24 +89,36 @@ export async function loadPortableTemplateConfig(
const config = parsed.data.backstage?.cli?.new;
const basePath = dirname(pkgPath);
const templatePointers = await Promise.all(
(config?.templates ?? defaultTemplates).map(async pointer => {
const templatePointerEntries = await Promise.all(
(config?.templates ?? defaultTemplates).map(async rawPointer => {
try {
const templatePath = resolveLocalTemplatePath(pointer, basePath);
const templatePath = resolveLocalTemplatePath(rawPointer, basePath);
return await peekLocalTemplateDefinition(templatePath);
const pointer = await peekLocalTemplateDefinition(templatePath);
return { pointer, rawPointer };
} catch (error) {
throw new ForwardedError(
`Failed to load template definition '${pointer}'`,
`Failed to load template definition '${rawPointer}'`,
error,
);
}
}),
);
const templateNameConflicts = new Map<string, string>();
for (const { pointer, rawPointer } of templatePointerEntries) {
const conflict = templateNameConflicts.get(pointer.name);
if (conflict) {
throw new Error(
`Invalid template configuration, received conflicting template name '${pointer.name}' from '${conflict}' and '${rawPointer}'`,
);
}
templateNameConflicts.set(pointer.name, rawPointer);
}
return {
isUsingDefaultTemplates: !config?.templates,
templatePointers,
templatePointers: templatePointerEntries.map(({ pointer }) => pointer),
license: overrides.license ?? config?.globals?.license ?? defaults.license,
version: overrides.version ?? config?.globals?.version ?? defaults.version,
private: overrides.private ?? config?.globals?.private ?? defaults.private,