diff --git a/.changeset/nasty-rockets-bathe.md b/.changeset/nasty-rockets-bathe.md
new file mode 100644
index 0000000000..1320b000b7
--- /dev/null
+++ b/.changeset/nasty-rockets-bathe.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-compat-api': minor
+---
+
+Discover plugins and routes recursively beneath the root routes in `collectLecacyRoutes`
diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json
index f713b49cf6..dd602f5380 100644
--- a/packages/core-compat-api/package.json
+++ b/packages/core-compat-api/package.json
@@ -22,9 +22,21 @@
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
+ "dependencies": {
+ "@backstage/core-app-api": "workspace:^",
+ "@backstage/core-plugin-api": "workspace:^",
+ "@backstage/frontend-plugin-api": "workspace:^",
+ "@backstage/version-bridge": "workspace:^",
+ "@types/react": "^16.13.1 || ^17.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.13.1 || ^17.0.0 || ^18.0.0",
+ "react-router-dom": "6.0.0-beta.0 || ^6.3.0"
+ },
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/frontend-test-utils": "workspace:^",
+ "@backstage/plugin-catalog": "workspace:^",
"@backstage/plugin-puppetdb": "workspace:^",
"@backstage/plugin-stackstorm": "workspace:^",
"@oriflame/backstage-plugin-score-card": "^0.7.0",
@@ -33,16 +45,5 @@
},
"files": [
"dist"
- ],
- "peerDependencies": {
- "react": "^16.13.1 || ^17.0.0 || ^18.0.0",
- "react-router-dom": "6.0.0-beta.0 || ^6.3.0"
- },
- "dependencies": {
- "@backstage/core-app-api": "workspace:^",
- "@backstage/core-plugin-api": "workspace:^",
- "@backstage/frontend-plugin-api": "workspace:^",
- "@backstage/version-bridge": "workspace:^",
- "@types/react": "^16.13.1 || ^17.0.0"
- }
+ ]
}
diff --git a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx
index 3dd9f330cc..862ef68bb5 100644
--- a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx
+++ b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx
@@ -15,11 +15,19 @@
*/
import { FlatRoutes } from '@backstage/core-app-api';
+import {
+ CatalogEntityPage,
+ CatalogIndexPage,
+ EntityAboutCard,
+ EntityLayout,
+ EntitySwitch,
+ isKind,
+} from '@backstage/plugin-catalog';
import { PuppetDbPage } from '@backstage/plugin-puppetdb';
import { StackstormPage } from '@backstage/plugin-stackstorm';
import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card';
-import React from 'react';
-import { Route } from 'react-router-dom';
+import React, { Fragment } from 'react';
+import { Route, Routes } from 'react-router-dom';
import { collectLegacyRoutes } from './collectLegacyRoutes';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
@@ -89,7 +97,7 @@ describe('collectLegacyRoutes', () => {
defaultConfig: { path: 'puppetdb' },
},
{
- id: 'page:puppetDb/2',
+ id: 'page:puppetDb/1',
attachTo: { id: 'core/routes', input: 'routes' },
disabled: false,
defaultConfig: { path: 'puppetdb' },
@@ -103,4 +111,134 @@ describe('collectLegacyRoutes', () => {
},
]);
});
+
+ it('supports recursion into children, including passing through fragments', () => {
+ const collected = collectLegacyRoutes(
+
+ } />
+ }
+ >
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ expect(
+ collected.map(p => ({
+ id: p.id,
+ extensions: toInternalBackstagePlugin(p).extensions.map(e => ({
+ id: e.id,
+ attachTo: e.attachTo,
+ disabled: e.disabled,
+ defaultConfig: e.configSchema?.parse({}),
+ })),
+ })),
+ ).toEqual([
+ {
+ id: 'catalog',
+ extensions: [
+ {
+ id: 'page:catalog',
+ attachTo: { id: 'core/routes', input: 'routes' },
+ disabled: false,
+ defaultConfig: { path: 'catalog' },
+ },
+ {
+ id: 'page:catalog/1',
+ attachTo: { id: 'core/routes', input: 'routes' },
+ defaultConfig: { path: 'catalog/:namespace/:kind/:name' },
+ disabled: false,
+ },
+ {
+ id: 'routing-shim:catalog/2',
+ attachTo: {
+ id: 'page:catalog/1',
+ input: 'childRoutingShims',
+ },
+ defaultConfig: undefined,
+ disabled: false,
+ },
+ {
+ id: 'routing-shim:catalog/3',
+ attachTo: {
+ id: 'routing-shim:catalog/2',
+ input: 'childRoutingShims',
+ },
+ defaultConfig: undefined,
+ disabled: false,
+ },
+ {
+ id: 'routing-shim:catalog/4',
+ attachTo: {
+ id: 'routing-shim:catalog/3',
+ input: 'childRoutingShims',
+ },
+ defaultConfig: undefined,
+ disabled: false,
+ },
+ {
+ id: 'api:plugin.catalog.service',
+ attachTo: {
+ id: 'core',
+ input: 'apis',
+ },
+ defaultConfig: undefined,
+ disabled: false,
+ },
+ {
+ id: 'api:catalog-react.starred-entities',
+ attachTo: {
+ id: 'core',
+ input: 'apis',
+ },
+ defaultConfig: undefined,
+ disabled: false,
+ },
+ {
+ id: 'api:plugin.catalog.entity-presentation',
+ attachTo: {
+ id: 'core',
+ input: 'apis',
+ },
+ defaultConfig: undefined,
+ disabled: false,
+ },
+ ],
+ },
+ {
+ id: 'score-card',
+ extensions: [
+ {
+ id: 'api:plugin.scoringdata.service',
+ attachTo: { id: 'core', input: 'apis' },
+ disabled: false,
+ },
+ ],
+ },
+ ]);
+ });
});
diff --git a/packages/core-compat-api/src/collectLegacyRoutes.tsx b/packages/core-compat-api/src/collectLegacyRoutes.tsx
index 92bced29a2..729ead2786 100644
--- a/packages/core-compat-api/src/collectLegacyRoutes.tsx
+++ b/packages/core-compat-api/src/collectLegacyRoutes.tsx
@@ -14,20 +14,24 @@
* limitations under the License.
*/
-import React, { ReactNode } from 'react';
-import {
- createApiExtension,
- createPageExtension,
- createPlugin,
- BackstagePlugin,
- ExtensionDefinition,
-} from '@backstage/frontend-plugin-api';
-import { Route, Routes } from 'react-router-dom';
import {
+ AnyRouteRefParams,
BackstagePlugin as LegacyBackstagePlugin,
RouteRef,
getComponentData,
} from '@backstage/core-plugin-api';
+import {
+ BackstagePlugin,
+ ExtensionDefinition,
+ coreExtensionData,
+ createApiExtension,
+ createExtension,
+ createExtensionInput,
+ createPageExtension,
+ createPlugin,
+} from '@backstage/frontend-plugin-api';
+import React, { Children, ReactNode, isValidElement } from 'react';
+import { Route, Routes } from 'react-router-dom';
import { convertLegacyRouteRef } from './convertLegacyRouteRef';
/*
@@ -58,58 +62,150 @@ Existing tasks:
*/
+// Creates a shim extension whose purpose is to build up the tree (anchored at
+// the root page) of paths/routeRefs so that the app can bind them properly.
+function makeRoutingShimExtension(options: {
+ name: string;
+ parentExtensionId: string;
+ routePath?: string;
+ routeRef?: RouteRef;
+}) {
+ const { name, parentExtensionId, routePath, routeRef } = options;
+ return createExtension({
+ kind: 'routing-shim',
+ name,
+ attachTo: { id: parentExtensionId, input: 'childRoutingShims' },
+ inputs: {
+ childRoutingShims: createExtensionInput({
+ routePath: coreExtensionData.routePath.optional(),
+ routeRef: coreExtensionData.routeRef.optional(),
+ }),
+ },
+ output: {
+ routePath: coreExtensionData.routePath.optional(),
+ routeRef: coreExtensionData.routeRef.optional(),
+ },
+ factory: () => ({
+ routePath,
+ routeRef: routeRef ? convertLegacyRouteRef(routeRef) : undefined,
+ }),
+ });
+}
+
+function visitRouteChildren(options: {
+ children: ReactNode;
+ parentExtensionId: string;
+ context: {
+ pluginId: string;
+ extensions: ExtensionDefinition[];
+ getUniqueName: () => string;
+ discoverPlugin: (plugin: LegacyBackstagePlugin) => void;
+ };
+}): void {
+ const { children, parentExtensionId, context } = options;
+ const { pluginId, extensions, getUniqueName, discoverPlugin } = context;
+
+ Children.forEach(children, node => {
+ if (!isValidElement(node)) {
+ return;
+ }
+
+ const plugin = getComponentData(node, 'core.plugin');
+ const routeRef = getComponentData>(
+ node,
+ 'core.mountPoint',
+ );
+ const routePath: string | undefined = node.props?.path;
+
+ if (plugin) {
+ // We just mark the plugin as discovered, but don't change the context
+ discoverPlugin(plugin);
+ }
+
+ let nextParentExtensionId = parentExtensionId;
+ if (routeRef || routePath) {
+ const nextParentExtensionName = getUniqueName();
+ nextParentExtensionId = `routing-shim:${pluginId}/${nextParentExtensionName}`;
+ extensions.push(
+ makeRoutingShimExtension({
+ name: nextParentExtensionName,
+ parentExtensionId,
+ routePath,
+ routeRef,
+ }),
+ );
+ }
+
+ visitRouteChildren({
+ children: node.props.children,
+ parentExtensionId: nextParentExtensionId,
+ context,
+ });
+ });
+}
+
/** @public */
export function collectLegacyRoutes(
flatRoutesElement: JSX.Element,
): BackstagePlugin[] {
- const createdPluginIds = new Map<
+ const pluginExtensions = new Map<
LegacyBackstagePlugin,
ExtensionDefinition[]
>();
+ const getUniqueName = (() => {
+ let currentIndex = 1;
+ return () => String(currentIndex++);
+ })();
+
+ const getPluginExtensions = (plugin: LegacyBackstagePlugin) => {
+ let extensions = pluginExtensions.get(plugin);
+ if (!extensions) {
+ extensions = [];
+ pluginExtensions.set(plugin, extensions);
+ }
+ return extensions;
+ };
+
React.Children.forEach(
flatRoutesElement.props.children,
(route: ReactNode) => {
- if (!React.isValidElement(route)) {
- return;
- }
-
// TODO(freben): Handle feature flag and permissions framework wrapper elements
- if (route.type !== Route) {
+ if (!React.isValidElement(route) || route.type !== Route) {
return;
}
const routeElement = route.props.element;
-
- // TODO: to support deeper extension component, e.g. hidden within , use https://github.com/backstage/backstage/blob/518a34646b79ec2028cc0ed6bc67d4366c51c4d6/packages/core-app-api/src/routing/collectors.tsx#L69
+ const path: string | undefined = route.props.path;
const plugin = getComponentData(
routeElement,
'core.plugin',
);
- if (!plugin) {
- return;
- }
-
const routeRef = getComponentData(
routeElement,
'core.mountPoint',
);
+ if (!plugin || !path) {
+ return;
+ }
- const detectedExtensions =
- createdPluginIds.get(plugin) ??
- new Array>();
- createdPluginIds.set(plugin, detectedExtensions);
+ const extensions = getPluginExtensions(plugin);
+ const pageExtensionName = extensions.length ? getUniqueName() : undefined;
+ const pageExtensionId = `page:${plugin.getId()}${
+ pageExtensionName ? `/${pageExtensionName}` : pageExtensionName
+ }`;
- const path: string = route.props.path;
-
- detectedExtensions.push(
+ extensions.push(
createPageExtension({
- name: detectedExtensions.length
- ? String(detectedExtensions.length + 1)
- : undefined,
+ name: pageExtensionName,
defaultPath: path[0] === '/' ? path.slice(1) : path,
routeRef: routeRef ? convertLegacyRouteRef(routeRef) : undefined,
-
+ inputs: {
+ childRoutingShims: createExtensionInput({
+ routePath: coreExtensionData.routePath.optional(),
+ routeRef: coreExtensionData.routeRef.optional(),
+ }),
+ },
loader: async () =>
route.props.children ? (
@@ -122,10 +218,21 @@ export function collectLegacyRoutes(
),
}),
);
+
+ visitRouteChildren({
+ children: route.props.children,
+ parentExtensionId: pageExtensionId,
+ context: {
+ pluginId: plugin.getId(),
+ extensions,
+ getUniqueName,
+ discoverPlugin: getPluginExtensions,
+ },
+ });
},
);
- return Array.from(createdPluginIds).map(([plugin, extensions]) =>
+ return Array.from(pluginExtensions).map(([plugin, extensions]) =>
createPlugin({
id: plugin.getId(),
extensions: [
diff --git a/packages/core-compat-api/src/convertLegacyApp.test.tsx b/packages/core-compat-api/src/convertLegacyApp.test.tsx
index 4a5be57bc8..9fae0128b0 100644
--- a/packages/core-compat-api/src/convertLegacyApp.test.tsx
+++ b/packages/core-compat-api/src/convertLegacyApp.test.tsx
@@ -97,7 +97,7 @@ describe('convertLegacyApp', () => {
defaultConfig: { path: 'puppetdb' },
},
{
- id: 'page:puppetDb/2',
+ id: 'page:puppetDb/1',
attachTo: { id: 'core/routes', input: 'routes' },
disabled: false,
defaultConfig: { path: 'puppetdb' },
diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.ts b/packages/core-compat-api/src/convertLegacyRouteRef.ts
index 2495f3719c..c5ee603722 100644
--- a/packages/core-compat-api/src/convertLegacyRouteRef.ts
+++ b/packages/core-compat-api/src/convertLegacyRouteRef.ts
@@ -91,6 +91,7 @@ export function convertLegacyRouteRef(
if (type === 'absolute') {
const legacyRef = ref as LegacyRouteRef;
+ const legacyRefStr = String(legacyRef);
const newRef = toInternalRouteRef(
createRouteRef<{ [key in string]: string }>({
params: legacyRef.params as string[],
@@ -104,18 +105,19 @@ export function convertLegacyRouteRef(
return newRef.getParams();
},
getDescription() {
- return newRef.getDescription();
+ return legacyRefStr;
},
setId(id: string) {
newRef.setId(id);
},
toString() {
- return newRef.toString();
+ return legacyRefStr;
},
});
}
if (type === 'sub') {
const legacyRef = ref as LegacySubRouteRef;
+ const legacyRefStr = String(legacyRef);
const newRef = toInternalSubRouteRef(
createSubRouteRef({
path: legacyRef.path,
@@ -133,15 +135,16 @@ export function convertLegacyRouteRef(
return newRef.getParent();
},
getDescription() {
- return newRef.getDescription();
+ return legacyRefStr;
},
toString() {
- return newRef.toString();
+ return legacyRefStr;
},
});
}
if (type === 'external') {
const legacyRef = ref as LegacyExternalRouteRef;
+ const legacyRefStr = String(legacyRef);
const newRef = toInternalExternalRouteRef(
createExternalRouteRef<{ [key in string]: string }>({
params: legacyRef.params as string[],
@@ -157,13 +160,13 @@ export function convertLegacyRouteRef(
return newRef.getParams();
},
getDescription() {
- return newRef.getDescription();
+ return legacyRefStr;
},
setId(id: string) {
newRef.setId(id);
},
toString() {
- return newRef.toString();
+ return legacyRefStr;
},
});
}
diff --git a/yarn.lock b/yarn.lock
index f38145a560..1f22be1c74 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3856,6 +3856,7 @@ __metadata:
"@backstage/core-plugin-api": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/frontend-test-utils": "workspace:^"
+ "@backstage/plugin-catalog": "workspace:^"
"@backstage/plugin-puppetdb": "workspace:^"
"@backstage/plugin-stackstorm": "workspace:^"
"@backstage/version-bridge": "workspace:^"