Merge pull request #32527 from backstage/rugvip/pluginId

frontend-plugin-api: plugins now have pluginId
This commit is contained in:
Patrik Oldsberg
2026-01-27 13:10:59 +01:00
committed by GitHub
12 changed files with 51 additions and 15 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/frontend-plugin-api': patch
'@backstage/frontend-app-api': patch
'@backstage/frontend-defaults': patch
'@backstage/core-plugin-api': patch
'@backstage/core-compat-api': patch
---
Plugins in the new frontend system now have a `pluginId` field rather than `id` to better align with naming conventions used throughout the frontend and backend systems. The old field is still present but marked as deprecated. All internal code has been updated to prefer `pluginId` while maintaining backward compatibility by falling back to `id` when needed.
@@ -66,7 +66,7 @@ export function toLegacyPlugin(
legacy = {
getId(): string {
return plugin.id;
return plugin.pluginId ?? plugin.id;
},
get routes() {
return {};
@@ -43,6 +43,7 @@ describe('convertLegacyPlugin', () => {
"id": "test",
"info": [Function],
"infoOptions": undefined,
"pluginId": "test",
"routes": {},
"toString": [Function],
"version": "v1",
+1 -1
View File
@@ -53,7 +53,7 @@ function toLegacyPlugin(plugin: FrontendPlugin): BackstagePlugin {
legacy = {
getId(): string {
return plugin.id;
return plugin.pluginId ?? plugin.id;
},
get routes() {
return {};
@@ -252,11 +252,11 @@ function resolveV2Inputs(
return mapValues(inputMap, (input, inputName) => {
const allAttachedNodes = attachments.get(inputName) ?? [];
const collector = parentCollector.child({ inputName });
const inputPluginId = node.spec.plugin.id;
const inputPluginId = node.spec.plugin.pluginId;
const attachedNodes = input.config.internal
? allAttachedNodes.filter(attachment => {
const attachmentPluginId = attachment.spec.plugin.id;
const attachmentPluginId = attachment.spec.plugin.pluginId;
if (attachmentPluginId !== inputPluginId) {
collector.report({
code: 'EXTENSION_INPUT_INTERNAL_IGNORED',
@@ -32,6 +32,14 @@ import {
import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
import { ErrorCollector } from '../wiring/createErrorCollector';
function normalizePlugin(plugin: FrontendPlugin): FrontendPlugin {
// Ensure pluginId is always set for plugins in the app
if (!plugin.pluginId && 'id' in plugin && typeof plugin.id === 'string') {
(plugin as any).pluginId = plugin.id;
}
return plugin;
}
/** @internal */
export function resolveAppNodeSpecs(options: {
features?: FrontendFeature[];
@@ -48,7 +56,9 @@ export function resolveAppNodeSpecs(options: {
collector,
} = options;
const plugins = features.filter(OpaqueFrontendPlugin.isType);
const plugins = features
.filter(OpaqueFrontendPlugin.isType)
.map(normalizePlugin);
const modules = features.filter(isInternalFrontendModule);
const filterForbidden = (
@@ -57,7 +67,7 @@ export function resolveAppNodeSpecs(options: {
if (forbidden.has(extension.id)) {
collector.report({
code: 'EXTENSION_IGNORED',
message: `It is forbidden to override the '${extension.id}' extension, attempted by the '${extension.plugin.id}' plugin`,
message: `It is forbidden to override the '${extension.id}' extension, attempted by the '${extension.plugin.pluginId}' plugin`,
context: {
plugin: extension.plugin,
extensionId: extension.id,
@@ -80,7 +90,7 @@ export function resolveAppNodeSpecs(options: {
toInternalFrontendModule(mod)
.extensions.flatMap(extension => {
// Modules for plugins that are not installed are ignored
const plugin = plugins.find(p => p.id === mod.pluginId);
const plugin = plugins.find(p => p.pluginId === mod.pluginId);
if (!plugin) {
return [];
}
@@ -91,7 +101,7 @@ export function resolveAppNodeSpecs(options: {
);
const appPlugin =
plugins.find(plugin => plugin.id === 'app') ??
plugins.find(plugin => plugin.pluginId === 'app') ??
createFrontendPlugin({
pluginId: 'app',
});
@@ -159,7 +169,7 @@ export function resolveAppNodeSpecs(options: {
if (seenExtensionIds.has(extension.id)) {
collector.report({
code: 'EXTENSION_IGNORED',
message: `The '${extension.id}' extension from the '${params.plugin.id}' plugin is a duplicate and will be ignored`,
message: `The '${extension.id}' extension from the '${params.plugin.pluginId}' plugin is a duplicate and will be ignored`,
context: {
plugin: params.plugin,
extensionId: extension.id,
@@ -77,7 +77,10 @@ export function createPluginInfoAttacher(
}),
});
const infoWithOverrides = applyInfoOverrides(plugin.id, resolvedInfo);
const infoWithOverrides = applyInfoOverrides(
plugin.pluginId,
resolvedInfo,
);
return normalizePluginInfo(infoWithOverrides);
}),
};
@@ -401,7 +401,7 @@ function createApiFactories(options: {
if (apiFactory) {
const apiRefId = apiFactory.api.id;
const ownerId = getApiOwnerId(apiRefId);
const pluginId = apiNode.spec.plugin.id ?? 'app';
const pluginId = apiNode.spec.plugin.pluginId ?? 'app';
const existingFactory = factoriesById.get(apiRefId);
// This allows modules to override factories provided by the plugin, but
@@ -35,13 +35,15 @@ function AppErrorItem(props: { error: AppError }): JSX.Element {
'extensionId' in context ? context.extensionId : node?.spec.id;
const routeId = 'routeId' in context ? context.routeId : undefined;
const plugin = 'plugin' in context ? context.plugin : node?.spec.plugin;
const pluginId = plugin?.id ?? 'N/A';
const pluginId = plugin?.pluginId ?? 'N/A';
const [info, setInfo] = useState<FrontendPluginInfo | undefined>(undefined);
useEffect(() => {
plugin?.info().then(setInfo, error => {
// eslint-disable-next-line no-console
console.error(`Failed to load info for plugin ${plugin.id}: ${error}`);
console.error(
`Failed to load info for plugin ${plugin.pluginId}: ${error}`,
);
});
}, [plugin]);
+2 -1
View File
@@ -1383,9 +1383,10 @@ export interface FrontendPlugin<
readonly $$type: '@backstage/FrontendPlugin';
// (undocumented)
readonly externalRoutes: TExternalRoutes;
// (undocumented)
// @deprecated
readonly id: string;
info(): Promise<FrontendPluginInfo>;
readonly pluginId: string;
// (undocumented)
readonly routes: TRoutes;
}
@@ -90,7 +90,7 @@ export function ExtensionBoundary(props: ExtensionBoundaryProps) {
);
const plugin = node.spec.plugin;
const pluginId = plugin.id ?? 'app';
const pluginId = plugin.pluginId ?? 'app';
const pluginWrapperApi = useOptionalPluginWrapperApi();
@@ -130,6 +130,15 @@ export interface FrontendPlugin<
},
> {
readonly $$type: '@backstage/FrontendPlugin';
/**
* The plugin ID.
*/
readonly pluginId: string;
/**
* Deprecated alias for `pluginId`.
*
* @deprecated Use `pluginId` instead.
*/
readonly id: string;
readonly routes: TRoutes;
readonly externalRoutes: TExternalRoutes;
@@ -231,6 +240,7 @@ export function createFrontendPlugin<
}
return OpaqueFrontendPlugin.createInstance('v1', {
pluginId,
id: pluginId,
routes: options.routes ?? ({} as TRoutes),
externalRoutes: options.externalRoutes ?? ({} as TExternalRoutes),