Merge pull request #32700 from backstage/rugvip/new-frontend-system-default
Make the new frontend system the default for new apps
This commit is contained in:
@@ -16,6 +16,8 @@
|
||||
|
||||
export const defaultTemplates = [
|
||||
'@backstage/cli-module-new/templates/frontend-plugin',
|
||||
'@backstage/cli-module-new/templates/frontend-plugin-module',
|
||||
'@backstage/cli-module-new/templates/legacy-frontend-plugin',
|
||||
'@backstage/cli-module-new/templates/backend-plugin',
|
||||
'@backstage/cli-module-new/templates/backend-plugin-module',
|
||||
'@backstage/cli-module-new/templates/plugin-web-library',
|
||||
|
||||
@@ -360,6 +360,141 @@ describe('loadPortableTemplateConfig', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter out legacy frontend template for new frontend system apps', async () => {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({}),
|
||||
packages: {
|
||||
app: {
|
||||
'package.json': JSON.stringify({
|
||||
dependencies: {
|
||||
'@backstage/frontend-defaults': '^0.1.0',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
node_modules: Object.fromEntries(
|
||||
defaultTemplates.map(t => {
|
||||
// Match the real behavior: both frontend-plugin and legacy-frontend-plugin
|
||||
// have the same template name "frontend-plugin"
|
||||
const name = t.endsWith('/legacy-frontend-plugin')
|
||||
? 'frontend-plugin'
|
||||
: basename(t);
|
||||
return [
|
||||
t,
|
||||
{ [TEMPLATE_FILE_NAME]: `name: ${name}\nrole: web-library\n` },
|
||||
];
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const config = await loadPortableTemplateConfig({
|
||||
packagePath: mockDir.resolve('package.json'),
|
||||
});
|
||||
|
||||
expect(config.isUsingDefaultTemplates).toBe(true);
|
||||
|
||||
const templateNames = config.templatePointers.map(t => t.name);
|
||||
expect(templateNames).toContain('frontend-plugin');
|
||||
expect(templateNames).toContain('frontend-plugin-module');
|
||||
expect(templateNames).toContain('backend-plugin');
|
||||
// Legacy template should be filtered out
|
||||
expect(templateNames).not.toContain('legacy-frontend-plugin');
|
||||
|
||||
// The frontend-plugin in the list should be from the new template, not legacy
|
||||
const frontendPlugin = config.templatePointers.find(
|
||||
t => t.name === 'frontend-plugin',
|
||||
);
|
||||
expect(frontendPlugin?.target).toContain('/frontend-plugin/');
|
||||
expect(frontendPlugin?.target).not.toContain('/legacy-frontend-plugin/');
|
||||
});
|
||||
|
||||
it('should filter out new frontend templates for legacy frontend system apps', async () => {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({}),
|
||||
packages: {
|
||||
app: {
|
||||
'package.json': JSON.stringify({
|
||||
dependencies: {
|
||||
'@backstage/app-defaults': '^0.1.0',
|
||||
'@backstage/core-app-api': '^0.1.0',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
node_modules: Object.fromEntries(
|
||||
defaultTemplates.map(t => {
|
||||
const name = t.endsWith('/legacy-frontend-plugin')
|
||||
? 'frontend-plugin'
|
||||
: basename(t);
|
||||
return [
|
||||
t,
|
||||
{ [TEMPLATE_FILE_NAME]: `name: ${name}\nrole: web-library\n` },
|
||||
];
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const config = await loadPortableTemplateConfig({
|
||||
packagePath: mockDir.resolve('package.json'),
|
||||
});
|
||||
|
||||
expect(config.isUsingDefaultTemplates).toBe(true);
|
||||
|
||||
const templateNames = config.templatePointers.map(t => t.name);
|
||||
// Legacy template should be present (shown as "frontend-plugin")
|
||||
expect(templateNames).toContain('frontend-plugin');
|
||||
expect(templateNames).toContain('backend-plugin');
|
||||
// New frontend templates should be filtered out
|
||||
expect(templateNames).not.toContain('frontend-plugin-module');
|
||||
|
||||
// The frontend-plugin in the list should be from the legacy template
|
||||
const frontendPlugin = config.templatePointers.find(
|
||||
t => t.name === 'frontend-plugin',
|
||||
);
|
||||
expect(frontendPlugin?.target).toContain('/legacy-frontend-plugin/');
|
||||
});
|
||||
|
||||
it('should not filter templates when using explicit configuration', async () => {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
backstage: {
|
||||
cli: {
|
||||
new: {
|
||||
templates: ['./my-frontend-plugin', './my-backend-plugin'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Even with a new frontend system app, explicit templates aren't filtered
|
||||
packages: {
|
||||
app: {
|
||||
'package.json': JSON.stringify({
|
||||
dependencies: {
|
||||
'@backstage/frontend-defaults': '^0.1.0',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
'my-frontend-plugin': {
|
||||
[TEMPLATE_FILE_NAME]: 'name: frontend-plugin\nrole: frontend-plugin\n',
|
||||
},
|
||||
'my-backend-plugin': {
|
||||
[TEMPLATE_FILE_NAME]: 'name: backend-plugin\nrole: backend-plugin\n',
|
||||
},
|
||||
});
|
||||
|
||||
const config = await loadPortableTemplateConfig({
|
||||
packagePath: mockDir.resolve('package.json'),
|
||||
});
|
||||
|
||||
expect(config.isUsingDefaultTemplates).toBe(false);
|
||||
expect(config.templatePointers).toHaveLength(2);
|
||||
expect(config.templatePointers.map(t => t.name)).toEqual([
|
||||
'frontend-plugin',
|
||||
'backend-plugin',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle missing backstage.new configuration', async () => {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({}),
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath, dirname, isAbsolute } from 'node:path';
|
||||
import { resolve as resolvePath, dirname, isAbsolute, join } from 'node:path';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
|
||||
import { defaultTemplates } from '../defaultTemplates';
|
||||
import {
|
||||
PortableTemplateConfig,
|
||||
@@ -29,6 +28,60 @@ import { z } from 'zod/v3';
|
||||
import { fromZodError } from 'zod-validation-error/v3';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
|
||||
type FrontendSystem = 'new' | 'legacy' | 'unknown';
|
||||
|
||||
async function detectFrontendSystem(basePath: string): Promise<FrontendSystem> {
|
||||
const appPkgPath = join(basePath, 'packages', 'app', 'package.json');
|
||||
|
||||
try {
|
||||
const appPkgJson = await fs.readJson(appPkgPath);
|
||||
const deps = {
|
||||
...appPkgJson.dependencies,
|
||||
...appPkgJson.devDependencies,
|
||||
};
|
||||
|
||||
if (
|
||||
deps['@backstage/frontend-defaults'] ||
|
||||
deps['@backstage/frontend-app-api']
|
||||
) {
|
||||
return 'new';
|
||||
}
|
||||
if (deps['@backstage/app-defaults'] || deps['@backstage/core-app-api']) {
|
||||
return 'legacy';
|
||||
}
|
||||
} catch {
|
||||
// App package doesn't exist or can't be read
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// Templates to exclude based on frontend system detection (by path, not name)
|
||||
const newFrontendTemplates = [
|
||||
'@backstage/cli-module-new/templates/frontend-plugin',
|
||||
'@backstage/cli-module-new/templates/frontend-plugin-module',
|
||||
];
|
||||
const legacyFrontendTemplates = [
|
||||
'@backstage/cli-module-new/templates/legacy-frontend-plugin',
|
||||
];
|
||||
|
||||
function filterTemplateEntriesForFrontendSystem(
|
||||
entries: Array<{ pointer: PortableTemplatePointer; rawPointer: string }>,
|
||||
frontendSystem: FrontendSystem,
|
||||
): Array<{ pointer: PortableTemplatePointer; rawPointer: string }> {
|
||||
if (frontendSystem === 'unknown') {
|
||||
return entries;
|
||||
}
|
||||
|
||||
if (frontendSystem === 'new') {
|
||||
// Filter out legacy frontend templates
|
||||
return entries.filter(e => !legacyFrontendTemplates.includes(e.rawPointer));
|
||||
}
|
||||
|
||||
// Legacy system - filter out new frontend templates
|
||||
return entries.filter(e => !newFrontendTemplates.includes(e.rawPointer));
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
license: 'Apache-2.0',
|
||||
version: '0.1.0',
|
||||
@@ -105,7 +158,9 @@ export async function loadPortableTemplateConfig(
|
||||
const config = parsed.data.backstage?.cli?.new;
|
||||
|
||||
const basePath = dirname(pkgPath);
|
||||
const templatePointerEntries = await Promise.all(
|
||||
const isUsingDefaultTemplates = !config?.templates;
|
||||
|
||||
let templatePointerEntries = await Promise.all(
|
||||
(config?.templates ?? defaultTemplates).map(async rawPointer => {
|
||||
try {
|
||||
const templatePath = resolveLocalTemplatePath(rawPointer, basePath);
|
||||
@@ -121,6 +176,17 @@ export async function loadPortableTemplateConfig(
|
||||
}),
|
||||
);
|
||||
|
||||
// Auto-filter frontend templates based on detected frontend system.
|
||||
// This must happen before the conflict check since both the new and legacy
|
||||
// frontend plugin templates have the same name, but only one will be shown.
|
||||
if (isUsingDefaultTemplates) {
|
||||
const frontendSystem = await detectFrontendSystem(basePath);
|
||||
templatePointerEntries = filterTemplateEntriesForFrontendSystem(
|
||||
templatePointerEntries,
|
||||
frontendSystem,
|
||||
);
|
||||
}
|
||||
|
||||
const templateNameConflicts = new Map<string, string>();
|
||||
for (const { pointer, rawPointer } of templatePointerEntries) {
|
||||
const conflict = templateNameConflicts.get(pointer.name);
|
||||
@@ -143,7 +209,7 @@ export async function loadPortableTemplateConfig(
|
||||
);
|
||||
|
||||
return {
|
||||
isUsingDefaultTemplates: !config?.templates,
|
||||
isUsingDefaultTemplates,
|
||||
templatePointers: templatePointerEntries.map(({ pointer }) => pointer),
|
||||
license: overrides.license ?? config?.globals?.license ?? defaults.license,
|
||||
version: overrides.version ?? config?.globals?.version ?? defaults.version,
|
||||
|
||||
Reference in New Issue
Block a user