diff --git a/packages/core-compat-api/src/collectEntityPageContents.test.tsx b/packages/core-compat-api/src/collectEntityPageContents.test.tsx
new file mode 100644
index 0000000000..53aa90f247
--- /dev/null
+++ b/packages/core-compat-api/src/collectEntityPageContents.test.tsx
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ExtensionAttachToSpec } from '@backstage/frontend-plugin-api';
+import { EntityLayout, EntitySwitch, isKind } from '@backstage/plugin-catalog';
+import React from 'react';
+import { collectEntityPageContents } from './collectEntityPageContents';
+import {
+ createComponentExtension,
+ createPlugin,
+} from '@backstage/core-plugin-api';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import {
+ resolveExtensionDefinition,
+ toInternalExtension,
+} from '../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
+
+const fooPlugin = createPlugin({
+ id: 'foo',
+});
+
+const FooContent = fooPlugin.provide(
+ createComponentExtension({
+ name: 'FooContent',
+ component: { sync: () =>
foo content
},
+ }),
+);
+const OtherFooContent = fooPlugin.provide(
+ createComponentExtension({
+ name: 'OtherFooContent',
+ component: { sync: () => other foo content
},
+ }),
+);
+
+const simpleTestContent = (
+
+
+ overview content
+
+
+
+
+
+ bar content
+
+
+);
+
+const otherTestContent = (
+
+
+ other overview content
+
+
+
+
+
+);
+
+function collect(element: React.JSX.Element) {
+ const result = new Array<{
+ id: string;
+ attachTo: ExtensionAttachToSpec;
+ }>();
+
+ collectEntityPageContents(element, {
+ discoverExtension(extension, plugin) {
+ const ext = toInternalExtension(
+ resolveExtensionDefinition(extension, {
+ namespace: plugin?.getId() ?? 'test',
+ }),
+ );
+ result.push({ id: ext.id, attachTo: ext.attachTo });
+ },
+ });
+ return result;
+}
+
+describe('collectEntityPageContents', () => {
+ it('should collect contents from a simple entity page', () => {
+ expect(collect(simpleTestContent)).toMatchInlineSnapshot(`
+ [
+ {
+ "attachTo": {
+ "id": "entity-content:catalog/overview",
+ "input": "cards",
+ },
+ "id": "entity-card:test/discovered-1",
+ },
+ {
+ "attachTo": {
+ "id": "page:catalog/entity",
+ "input": "contents",
+ },
+ "id": "entity-content:foo/discovered-1",
+ },
+ {
+ "attachTo": {
+ "id": "page:catalog/entity",
+ "input": "contents",
+ },
+ "id": "entity-content:test/discovered-2",
+ },
+ ]
+ `);
+ });
+
+ it('should collect contents from an entity page with an entity switch', () => {
+ expect(
+ collect(
+
+
+ {simpleTestContent}
+
+ {otherTestContent}
+ ,
+ ),
+ ).toMatchInlineSnapshot(`
+ [
+ {
+ "attachTo": {
+ "id": "entity-content:catalog/overview",
+ "input": "cards",
+ },
+ "id": "entity-card:test/discovered-1",
+ },
+ {
+ "attachTo": {
+ "id": "page:catalog/entity",
+ "input": "contents",
+ },
+ "id": "entity-content:foo/discovered-1",
+ },
+ {
+ "attachTo": {
+ "id": "page:catalog/entity",
+ "input": "contents",
+ },
+ "id": "entity-content:test/discovered-2",
+ },
+ {
+ "attachTo": {
+ "id": "entity-content:catalog/overview",
+ "input": "cards",
+ },
+ "id": "entity-card:test/discovered-2",
+ },
+ {
+ "attachTo": {
+ "id": "page:catalog/entity",
+ "input": "contents",
+ },
+ "id": "entity-content:foo/discovered-3",
+ },
+ ]
+ `);
+ });
+});
diff --git a/packages/core-compat-api/src/collectEntityPageContents.ts b/packages/core-compat-api/src/collectEntityPageContents.ts
index c2a3ba2145..7c26cd29ef 100644
--- a/packages/core-compat-api/src/collectEntityPageContents.ts
+++ b/packages/core-compat-api/src/collectEntityPageContents.ts
@@ -74,7 +74,7 @@ function invertFilter(ifFunc?: EntityFilter): EntityFilter {
export function collectEntityPageContents(
entityPageElement: React.JSX.Element,
context: {
- discoveryExtension(
+ discoverExtension(
extension: ExtensionDefinition,
plugin?: LegacyBackstagePlugin,
): void;
@@ -94,7 +94,7 @@ export function collectEntityPageContents(
const mergedIf = allFilters(parentFilter, pageNode.if);
if (pageNode.path === '/') {
- context.discoveryExtension(
+ context.discoverExtension(
EntityCardBlueprint.makeWithOverrides({
name: `discovered-${cardCounter++}`,
factory(originalFactory, { apis }) {
@@ -109,7 +109,7 @@ export function collectEntityPageContents(
} else {
const name = `discovered-${routeCounter++}`;
- context.discoveryExtension(
+ context.discoverExtension(
EntityContentBlueprint.makeWithOverrides({
name,
factory(originalFactory, { apis }) {
diff --git a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx
index ce0b055c00..2d64f88535 100644
--- a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx
+++ b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx
@@ -57,7 +57,7 @@ describe('collectLegacyRoutes', () => {
expect(
collected.map(p => ({
- id: p.id,
+ id: p.$$type === '@backstage/FrontendPlugin' ? p.id : p.pluginId,
extensions: OpaqueFrontendPlugin.toInternal(p).extensions.map(e => ({
id: e.id,
attachTo: e.attachTo,
@@ -177,7 +177,7 @@ describe('collectLegacyRoutes', () => {
expect(
collected.map(p => ({
- id: p.id,
+ id: p.$$type === '@backstage/FrontendPlugin' ? p.id : p.pluginId,
extensions: OpaqueFrontendPlugin.toInternal(p).extensions.map(e => ({
id: e.id,
attachTo: e.attachTo,
diff --git a/packages/core-compat-api/src/collectLegacyRoutes.tsx b/packages/core-compat-api/src/collectLegacyRoutes.tsx
index 3cf16b96c0..f6408b8b3f 100644
--- a/packages/core-compat-api/src/collectLegacyRoutes.tsx
+++ b/packages/core-compat-api/src/collectLegacyRoutes.tsx
@@ -270,7 +270,7 @@ export function collectLegacyRoutes(
if (entityPage) {
collectEntityPageContents(entityPage, {
- discoveryExtension(extension, plugin) {
+ discoverExtension(extension, plugin) {
if (!plugin || plugin.getId() === 'catalog') {
getPluginExtensions(orphanRoutesPlugin).push(extension);
} else {
diff --git a/packages/core-compat-api/src/convertLegacyApp.test.tsx b/packages/core-compat-api/src/convertLegacyApp.test.tsx
index 7153fcb4be..c532c6da1a 100644
--- a/packages/core-compat-api/src/convertLegacyApp.test.tsx
+++ b/packages/core-compat-api/src/convertLegacyApp.test.tsx
@@ -21,6 +21,16 @@ import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card';
import React, { ReactNode } from 'react';
import { Route } from 'react-router-dom';
import { convertLegacyApp } from './convertLegacyApp';
+import {
+ createApiFactory,
+ createComponentExtension,
+ createPlugin,
+} from '@backstage/core-plugin-api';
+import { EntityLayout, EntitySwitch, isKind } from '@backstage/plugin-catalog';
+import { renderInTestApp } from '@backstage/frontend-test-utils';
+import { default as catalogPlugin } from '@backstage/plugin-catalog/alpha';
+import { catalogApiRef } from '@backstage/plugin-catalog-react';
+import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
const Root = ({ children }: { children: ReactNode }) => <>{children}>;
@@ -159,4 +169,153 @@ describe('convertLegacyApp', () => {
}),
]);
});
+
+ it('should convert entity pages', async () => {
+ const fooPlugin = createPlugin({
+ id: 'foo',
+ });
+
+ const FooContent = fooPlugin.provide(
+ createComponentExtension({
+ name: 'FooContent',
+ component: { sync: () => foo content
},
+ }),
+ );
+ const OtherFooContent = fooPlugin.provide(
+ createComponentExtension({
+ name: 'OtherFooContent',
+ component: { sync: () => other foo content
},
+ }),
+ );
+
+ const simpleTestContent = (
+
+
+ overview content
+
+
+
+
+
+ bar content
+
+
+ );
+
+ const otherTestContent = (
+
+
+ other overview content
+
+
+
+
+
+ );
+
+ const entityPage = (
+
+
+ {simpleTestContent}
+
+ {otherTestContent}
+
+ );
+
+ const converted = convertLegacyApp(
+
+ test} />
+ ,
+ { entityPage },
+ );
+
+ const catalogOverride = catalogPlugin.withOverrides({
+ extensions: [
+ catalogPlugin.getExtension('api:catalog').override({
+ params: {
+ factory: createApiFactory(
+ catalogApiRef,
+ catalogApiMock({
+ entities: [
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'test',
+ metadata: {
+ name: 'x',
+ },
+ spec: {},
+ },
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'other',
+ metadata: {
+ name: 'x',
+ },
+ spec: {},
+ },
+ ],
+ }),
+ ),
+ },
+ }),
+ ],
+ });
+
+ // Overview
+ const renderOverviewTest = await renderInTestApp(, {
+ features: [catalogOverride, ...converted],
+ initialRouteEntries: ['/catalog/default/test/x'],
+ });
+ await expect(
+ renderOverviewTest.findByText('overview content'),
+ ).resolves.toBeInTheDocument();
+ renderOverviewTest.unmount();
+
+ const renderOverviewOther = await renderInTestApp(, {
+ features: [catalogOverride, ...converted],
+ initialRouteEntries: ['/catalog/default/other/x'],
+ });
+ await expect(
+ renderOverviewOther.findByText('other overview content'),
+ ).resolves.toBeInTheDocument();
+ renderOverviewOther.unmount();
+
+ // Foo tab
+ const renderFooTest = await renderInTestApp(, {
+ features: [catalogOverride, ...converted],
+ initialRouteEntries: ['/catalog/default/test/x/foo'],
+ });
+ await expect(
+ renderFooTest.findByText('foo content'),
+ ).resolves.toBeInTheDocument();
+ renderFooTest.unmount();
+
+ const renderFooOther = await renderInTestApp(, {
+ features: [catalogOverride, ...converted],
+ initialRouteEntries: ['/catalog/default/other/x/foo'],
+ });
+ await expect(
+ renderFooOther.findByText('other foo content'),
+ ).resolves.toBeInTheDocument();
+ renderFooOther.unmount();
+
+ // Bar tab
+ const renderBarTest = await renderInTestApp(, {
+ features: [catalogOverride, ...converted],
+ initialRouteEntries: ['/catalog/default/test/x/bar'],
+ });
+ await expect(
+ renderBarTest.findByText('bar content'),
+ ).resolves.toBeInTheDocument();
+ renderBarTest.unmount();
+
+ const renderBarOther = await renderInTestApp(, {
+ features: [catalogOverride, ...converted],
+ initialRouteEntries: ['/catalog/default/other/x/bar'],
+ });
+ await expect(
+ renderBarOther.findByText('other overview content'),
+ ).resolves.toBeInTheDocument(); // /bar does not exist, fall back to rendering overview
+ renderBarOther.unmount();
+ });
});
diff --git a/packages/core-compat-api/src/convertLegacyApp.ts b/packages/core-compat-api/src/convertLegacyApp.ts
index eb72cda204..dac9849aac 100644
--- a/packages/core-compat-api/src/convertLegacyApp.ts
+++ b/packages/core-compat-api/src/convertLegacyApp.ts
@@ -86,7 +86,7 @@ export function convertLegacyApp(
options: ConvertLegacyAppOptions = {},
): (FrontendPlugin | FrontendModule)[] {
if (getComponentData(rootElement, 'core.type') === 'FlatRoutes') {
- return collectLegacyRoutes(rootElement);
+ return collectLegacyRoutes(rootElement, options?.entityPage);
}
const appRouterEls = selectChildren(