diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts index 24cf450fcd..b556662236 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts @@ -15,7 +15,11 @@ */ import React from 'react'; -import { RouteRef, createRouteRef } from '@backstage/core-plugin-api'; +import { + BackstagePlugin, + RouteRef, + createRouteRef, +} from '@backstage/core-plugin-api'; import { extractRouteInfoFromInstanceTree } from './extractRouteInfoFromInstanceTree'; import { coreExtensionData, @@ -23,7 +27,7 @@ import { createPageExtension, createPlugin, } from '@backstage/frontend-plugin-api'; -import { createInstances } from '../wiring/createApp'; +import { createInstances, toLegacyPlugin } from '../wiring/createApp'; import { MockConfigApi } from '@backstage/test-utils'; const ref1 = createRouteRef({ id: 'page1' }); @@ -39,31 +43,31 @@ function sortedEntries(map: Map): [RouteRef, T][] { ); } -// async function routeObj( -// path: string, -// refs: RouteRef[], -// children: any[] = [], -// type: 'mounted' | 'gathered' = 'mounted', -// backstagePlugin?: BackstagePlugin, -// ) { -// return { -// path: path, -// caseSensitive: false, -// element: type, -// routeRefs: new Set(refs), -// children: [ -// { -// path: '*', -// caseSensitive: false, -// element: 'match-all', -// routeRefs: new Set(), -// plugins: new Set(), -// }, -// ...children, -// ], -// plugins: backstagePlugin ? new Set([backstagePlugin]) : new Set(), -// }; -// } +function routeObj( + path: string, + refs: RouteRef[], + children: any[] = [], + type: 'mounted' | 'gathered' = 'mounted', + backstagePlugin?: BackstagePlugin, +) { + return { + path: path, + caseSensitive: false, + element: type, + routeRefs: new Set(refs), + children: [ + { + path: '*', + caseSensitive: false, + element: 'match-all', + routeRefs: new Set(), + plugins: new Set(), + }, + ...children, + ], + plugins: backstagePlugin ? new Set([backstagePlugin]) : new Set(), + }; +} const emptyLoader = () => Promise.resolve(React.createElement('div')); @@ -120,14 +124,13 @@ describe('discovery', () => { }), ]; + const plugin = createPlugin({ + id: 'test', + extensions, + }); const { rootInstances } = createInstances({ config: new MockConfigApi({}), - plugins: [ - createPlugin({ - id: 'test', - extensions, - }), - ], + plugins: [plugin], }); const info = extractRouteInfoFromInstanceTree(rootInstances); @@ -146,25 +149,26 @@ describe('discovery', () => { [ref4, undefined], [ref5, ref1], ]); - // expect(routing.objects).toEqual([ - // routeObj( - // 'foo', - // [ref1], - // [ - // routeObj( - // 'bar/:id', - // [ref2], - // [routeObj('baz', [ref3], undefined, undefined, plugin)], - // undefined, - // plugin, - // ), - // routeObj('blop', [ref5], undefined, undefined, plugin), - // ], - // undefined, - // plugin, - // ), - // routeObj('divsoup', [ref4], undefined, undefined, plugin), - // ]); + expect(info.routeObjects).toEqual([ + routeObj('nothing', []), + routeObj( + 'foo', + [ref1], + [ + routeObj( + 'bar/:id', + [ref2], + [routeObj('baz', [ref3], undefined, undefined, expect.any(Object))], + undefined, + expect.any(Object), + ), + routeObj('blop', [ref5], undefined, undefined, expect.any(Object)), + ], + undefined, + expect.any(Object), + ), + routeObj('divsoup', [ref4], undefined, undefined, expect.any(Object)), + ]); }); // it('should handle all react router Route patterns', () => { diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts index 290f5707e9..8d3947b970 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts @@ -19,6 +19,19 @@ import { coreExtensionData } from '@backstage/frontend-plugin-api'; import { ExtensionInstance } from '../wiring/createExtensionInstance'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { BackstageRouteObject } from '../../../core-app-api/src/routing/types'; +import { toLegacyPlugin } from '../wiring/createApp'; + +// We always add a child that matches all subroutes but without any route refs. This makes +// sure that we're always able to match each route no matter how deep the navigation goes. +// The route resolver then takes care of selecting the most specific match in order to find +// mount points that are as deep in the routing tree as possible. +export const MATCH_ALL_ROUTE: BackstageRouteObject = { + caseSensitive: false, + path: '*', + element: 'match-all', // These elements aren't used, so we add in a bit of debug information + routeRefs: new Set(), + plugins: new Set(), +}; // Joins a list of paths together, avoiding trailing and duplicate slashes export function joinPaths(...paths: string[]): string { @@ -36,10 +49,30 @@ export function extractRouteInfoFromInstanceTree(roots: ExtensionInstance[]): { } { const routePaths = new Map(); const routeParents = new Map(); + const routeObjects = new Array(); - function visit(current: ExtensionInstance, parent?: RouteRef) { - const routePath = current.getData(coreExtensionData.routePath) ?? ''; + function visit( + current: ExtensionInstance, + parentRef?: RouteRef, + parentObj?: BackstageRouteObject, + ) { + const routePath = current.getData(coreExtensionData.routePath) ?? ''; // TODO: need to gather routes instead const routeRef = current.getData(coreExtensionData.routeRef); + const parentChildren = parentObj?.children ?? routeObjects; + let currentObj = parentObj; + + if (routePath) { + currentObj = { + path: routePath, + element: 'mounted', + routeRefs: new Set(), + caseSensitive: false, + children: [MATCH_ALL_ROUTE], + plugins: new Set(), + }; + + parentChildren.push(currentObj); + } if (routeRef) { const routeRefId = (routeRef as any).id; // TODO: properly @@ -49,12 +82,16 @@ export function extractRouteInfoFromInstanceTree(roots: ExtensionInstance[]): { ); } routePaths.set(routeRef, routePath); - routeParents.set(routeRef, parent); + routeParents.set(routeRef, parentRef); + currentObj?.routeRefs.add(routeRef); + if (current.source) { + currentObj?.plugins.add(toLegacyPlugin(current.source)); + } } for (const children of current.attachments.values()) { for (const child of children) { - visit(child, routeRef ?? parent); + visit(child, routeRef ?? parentRef, currentObj); } } } @@ -63,5 +100,5 @@ export function extractRouteInfoFromInstanceTree(roots: ExtensionInstance[]): { visit(root); } - return { routePaths, routeParents, routeObjects: [] }; + return { routePaths, routeParents, routeObjects }; } diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 4afd1d1a2f..4f32bc2219 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -336,7 +336,7 @@ export function createApp(options: { }; } -function toLegacyPlugin(plugin: BackstagePlugin): LegacyBackstagePlugin { +export function toLegacyPlugin(plugin: BackstagePlugin): LegacyBackstagePlugin { const errorMsg = 'Not implemented in legacy plugin compatibility layer'; const notImplemented = () => { throw new Error(errorMsg); diff --git a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts index afa3cbfefc..18551eb8da 100644 --- a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts +++ b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts @@ -36,6 +36,8 @@ export interface ExtensionInstance { * Maps input names to the actual instances given to them. */ readonly attachments: Map; + + readonly source?: BackstagePlugin; } function resolveInputData( @@ -141,7 +143,7 @@ export function createExtensionInstance(options: { getData(ref: ExtensionDataRef): T | undefined { return extensionData.get(ref.id) as T | undefined; }, - + source, attachments, }; }