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
@@ -0,0 +1,16 @@
---
'@backstage/cli': minor
---
**BREAKING**: The CLI templates for frontend plugins have been renamed:
- `new-frontend-plugin``frontend-plugin`
- `new-frontend-plugin-module``frontend-plugin-module`
- `frontend-plugin` (legacy) → `legacy-frontend-plugin`
To smooth out this breaking change, the CLI now auto-detects which frontend system your app uses based on the dependencies in `packages/app/package.json`. When using the default templates (no explicit `templates` configuration):
- Apps using `@backstage/frontend-defaults` will see the new frontend system templates (`frontend-plugin`, `frontend-plugin-module`)
- Apps using `@backstage/app-defaults` will see the legacy template (displayed as `frontend-plugin`)
This means existing projects that haven't migrated to the new frontend system will continue to create legacy plugins by default, while new projects will get the new frontend system templates. If you have explicit template configuration in your `package.json`, it will be used as-is without any auto-detection.
@@ -899,39 +899,7 @@ It's encouraged that once you switch over to using the new frontend system, that
This practice is also pretty important early on, as it's going to help you get familiar with the practices of the new frontend system.
When creating a new Backstage app with `create-app` you'll automatically get these choices in the `yarn new` command, but if you want to bring these templates to an older app, you can add the following to your root `package.json`:
```json
{
...
"scripts": {
...
"new": "backstage-cli new"
},
"backstage": {
"cli": {
"new": {
"globals": {
"license": "UNLICENSED"
},
"templates": [
"@backstage/cli-module-new/templates/new-frontend-plugin",
"@backstage/cli-module-new/templates/new-frontend-plugin-module",
"@backstage/cli-module-new/templates/backend-plugin",
"@backstage/cli-module-new/templates/backend-plugin-module",
"@backstage/cli-module-new/templates/plugin-web-library",
"@backstage/cli-module-new/templates/plugin-node-library",
"@backstage/cli-module-new/templates/plugin-common-library",
"@backstage/cli-module-new/templates/web-library",
"@backstage/cli-module-new/templates/node-library",
"@backstage/cli-module-new/templates/catalog-provider-module",
"@backstage/cli-module-new/templates/scaffolder-backend-module"
]
}
}
}
}
```
The `yarn new` command now defaults to the new frontend system templates for frontend plugins. If you have an older app that was created before this change, you can simply update the `@backstage/cli-module-new` package to get access to the new templates.
## Troubleshooting
+3
View File
@@ -83,6 +83,8 @@ When defining the `templates` array it will override the default set of template
"new": {
"templates": [
"@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",
@@ -90,6 +92,7 @@ When defining the `templates` array it will override the default set of template
"@backstage/cli-module-new/templates/plugin-common-library",
"@backstage/cli-module-new/templates/web-library",
"@backstage/cli-module-new/templates/node-library",
"@backstage/cli-module-new/templates/cli-module",
"@backstage/cli-module-new/templates/catalog-provider-module",
"@backstage/cli-module-new/templates/scaffolder-backend-module"
]
@@ -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,
@@ -6,7 +6,14 @@ _This plugin was created through the Backstage CLI_
## Getting started
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/{{pluginId}}](http://localhost:3000/{{pluginId}}).
Your plugin has been added to the app in this repository, meaning you'll be able
to access it by running `yarn start` in the root directory, and then navigating
to [/{{pluginId}}](http://localhost:3000/{{pluginId}}).
This plugin is built with Backstage's [new frontend
system](https://backstage.io/docs/frontend-system/architecture/index), and you
can find more information about building plugins in the [plugin builder
documentation](https://backstage.io/docs/frontend-system/building-plugins/index).
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
@@ -23,7 +23,7 @@
},
"dependencies": {
"@backstage/core-components": "{{versionQuery '@backstage/core-components'}}",
"@backstage/core-plugin-api": "{{versionQuery '@backstage/core-plugin-api'}}",
"@backstage/frontend-plugin-api": "{{versionQuery '@backstage/frontend-plugin-api'}}",
"@backstage/theme": "{{versionQuery '@backstage/theme'}}",
"@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}",
"@material-ui/icons": "{{versionQuery '@material-ui/icons' '4.9.1'}}",
@@ -31,22 +31,17 @@
"react-use": "{{versionQuery 'react-use' '17.2.4'}}"
},
"peerDependencies": {
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}",
"react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}",
"react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}"
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}"
},
"devDependencies": {
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
"@backstage/core-app-api": "{{versionQuery '@backstage/core-app-api'}}",
"@backstage/dev-utils": "{{versionQuery '@backstage/dev-utils'}}",
"@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}",
"@backstage/frontend-dev-utils": "{{versionQuery '@backstage/frontend-dev-utils'}}",
"@backstage/frontend-test-utils": "{{versionQuery '@backstage/frontend-test-utils'}}",
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}",
"@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}",
"@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}",
"msw": "{{versionQuery 'msw' '1.0.0'}}",
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}",
"react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}",
"react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}"
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}"
},
"files": [
"dist"
@@ -3,4 +3,3 @@ role: frontend-plugin
description: A new frontend plugin
values:
pluginVar: '{{ camelCase pluginId }}Plugin'
extensionName: '{{ upperFirst ( camelCase pluginId ) }}Page'
@@ -5,7 +5,7 @@ import { screen } from '@testing-library/react';
import {
registerMswTestHooks,
renderInTestApp,
} from '@backstage/test-utils';
} from '@backstage/frontend-test-utils';
describe('ExampleComponent', () => {
const server = setupServer();
@@ -1,4 +1,4 @@
import { renderInTestApp } from '@backstage/test-utils';
import { renderInTestApp } from '@backstage/frontend-test-utils';
import { ExampleFetchComponent } from './ExampleFetchComponent';
describe('ExampleFetchComponent', () => {
@@ -1 +1 @@
export { {{ pluginVar }}, {{ extensionName }} } from './plugin';
export { {{ pluginVar }} as default } from './plugin';
@@ -0,0 +1,13 @@
# {{pluginId}}
Welcome to the {{pluginId}} plugin!
_This plugin was created through the Backstage CLI_
## Getting started
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/{{pluginId}}](http://localhost:3000/{{pluginId}}).
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
@@ -23,7 +23,7 @@
},
"dependencies": {
"@backstage/core-components": "{{versionQuery '@backstage/core-components'}}",
"@backstage/frontend-plugin-api": "{{versionQuery '@backstage/frontend-plugin-api'}}",
"@backstage/core-plugin-api": "{{versionQuery '@backstage/core-plugin-api'}}",
"@backstage/theme": "{{versionQuery '@backstage/theme'}}",
"@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}",
"@material-ui/icons": "{{versionQuery '@material-ui/icons' '4.9.1'}}",
@@ -31,17 +31,22 @@
"react-use": "{{versionQuery 'react-use' '17.2.4'}}"
},
"peerDependencies": {
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}"
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}",
"react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}",
"react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}"
},
"devDependencies": {
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
"@backstage/frontend-dev-utils": "{{versionQuery '@backstage/frontend-dev-utils'}}",
"@backstage/frontend-test-utils": "{{versionQuery '@backstage/frontend-test-utils'}}",
"@backstage/core-app-api": "{{versionQuery '@backstage/core-app-api'}}",
"@backstage/dev-utils": "{{versionQuery '@backstage/dev-utils'}}",
"@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}",
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}",
"@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}",
"@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}",
"msw": "{{versionQuery 'msw' '1.0.0'}}",
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}"
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}",
"react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}",
"react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}"
},
"files": [
"dist"
@@ -0,0 +1,6 @@
name: frontend-plugin
role: frontend-plugin
description: A new frontend plugin (legacy system)
values:
pluginVar: '{{ camelCase pluginId }}Plugin'
extensionName: '{{ upperFirst ( camelCase pluginId ) }}Page'
@@ -5,7 +5,7 @@ import { screen } from '@testing-library/react';
import {
registerMswTestHooks,
renderInTestApp,
} from '@backstage/frontend-test-utils';
} from '@backstage/test-utils';
describe('ExampleComponent', () => {
const server = setupServer();
@@ -1,4 +1,4 @@
import { renderInTestApp } from '@backstage/frontend-test-utils';
import { renderInTestApp } from '@backstage/test-utils';
import { ExampleFetchComponent } from './ExampleFetchComponent';
describe('ExampleFetchComponent', () => {
@@ -0,0 +1 @@
export { {{ pluginVar }}, {{ extensionName }} } from './plugin';
@@ -1,20 +0,0 @@
# {{pluginId}}
Welcome to the {{pluginId}} plugin!
_This plugin was created through the Backstage CLI_
## Getting started
Your plugin has been added to the app in this repository, meaning you'll be able
to access it by running `yarn start` in the root directory, and then navigating
to [/{{pluginId}}](http://localhost:3000/{{pluginId}}).
This plugin is built with Backstage's [new frontend
system](https://backstage.io/docs/frontend-system/architecture/index), and you
can find more information about building plugins in the [plugin builder
documentation](https://backstage.io/docs/frontend-system/building-plugins/index).
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
@@ -1,5 +0,0 @@
name: frontend-plugin
role: frontend-plugin
description: A new frontend plugin
values:
pluginVar: '{{ camelCase pluginId }}Plugin'
@@ -1 +0,0 @@
export { {{ pluginVar }} as default } from './plugin';
@@ -22,6 +22,28 @@
"prettier:check": "prettier --check .",
"new": "backstage-cli new"
},
"backstage": {
"cli": {
"new": {
"globals": {
"license": "UNLICENSED"
},
"templates": [
"@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",
"@backstage/cli-module-new/templates/plugin-node-library",
"@backstage/cli-module-new/templates/plugin-common-library",
"@backstage/cli-module-new/templates/web-library",
"@backstage/cli-module-new/templates/node-library",
"@backstage/cli-module-new/templates/cli-module",
"@backstage/cli-module-new/templates/catalog-provider-module",
"@backstage/cli-module-new/templates/scaffolder-backend-module"
]
}
}
},
"workspaces": [
"packages/*",
"plugins/*"
@@ -27,20 +27,7 @@
"new": {
"globals": {
"license": "UNLICENSED"
},
"templates": [
"@backstage/cli-module-new/templates/new-frontend-plugin",
"@backstage/cli-module-new/templates/new-frontend-plugin-module",
"@backstage/cli-module-new/templates/backend-plugin",
"@backstage/cli-module-new/templates/backend-plugin-module",
"@backstage/cli-module-new/templates/plugin-web-library",
"@backstage/cli-module-new/templates/plugin-node-library",
"@backstage/cli-module-new/templates/plugin-common-library",
"@backstage/cli-module-new/templates/web-library",
"@backstage/cli-module-new/templates/node-library",
"@backstage/cli-module-new/templates/catalog-provider-module",
"@backstage/cli-module-new/templates/scaffolder-backend-module"
]
}
}
}
},