cli: Rename frontend plugin templates and add auto-detection

Renamed the CLI templates for frontend plugins:
- new-frontend-plugin → frontend-plugin
- new-frontend-plugin-module → frontend-plugin-module
- frontend-plugin (legacy) → legacy-frontend-plugin

Added auto-detection logic that checks packages/app/package.json to
determine which frontend system the app uses. When using default
templates, only the appropriate frontend plugin template is shown:
- Apps with @backstage/frontend-defaults see the new system templates
- Apps with @backstage/app-defaults see the legacy template

Both templates display as "frontend-plugin" to users, so existing
workflows are preserved while automatically using the correct template.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-02-05 13:55:06 +01:00
parent a6735c33b1
commit 08d9770715
42 changed files with 168 additions and 99 deletions
@@ -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',
@@ -15,9 +15,13 @@
*/
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 +33,60 @@ import { z } from 'zod';
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 +163,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 +181,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 +214,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,