frontend-app-api: add support for collecting route objects

Co-authored-by: Camila Belo <camilaibs@gmail.com>
Co-authored-by: Philipp Hugenroth <philipph@spotify.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-09-29 16:53:47 +02:00
parent 3f2e598671
commit 6fd3af6912
4 changed files with 102 additions and 59 deletions
@@ -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<T>(map: Map<RouteRef, T>): [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', () => {
@@ -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<RouteRef, string>();
const routeParents = new Map<RouteRef, RouteRef | undefined>();
const routeObjects = new Array<BackstageRouteObject>();
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<RouteRef>(),
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 };
}
@@ -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);
@@ -36,6 +36,8 @@ export interface ExtensionInstance {
* Maps input names to the actual instances given to them.
*/
readonly attachments: Map<string, ExtensionInstance[]>;
readonly source?: BackstagePlugin;
}
function resolveInputData(
@@ -141,7 +143,7 @@ export function createExtensionInstance(options: {
getData<T>(ref: ExtensionDataRef<T>): T | undefined {
return extensionData.get(ref.id) as T | undefined;
},
source,
attachments,
};
}