frontend-app-api: make it possible to bind routes through config
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
+7
@@ -24,6 +24,13 @@ export interface Config {
|
||||
packages?: 'all' | { include?: string[]; exclude?: string[] };
|
||||
};
|
||||
|
||||
routes?: {
|
||||
/**
|
||||
* @deepVisibility frontend
|
||||
*/
|
||||
bindings?: { [externalRouteRefId: string]: string };
|
||||
};
|
||||
|
||||
/**
|
||||
* @deepVisibility frontend
|
||||
*/
|
||||
|
||||
@@ -19,14 +19,21 @@ import {
|
||||
createRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { resolveRouteBindings } from './resolveRouteBindings';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
const emptyIds = { routes: new Map(), externalRoutes: new Map() };
|
||||
|
||||
describe('resolveRouteBindings', () => {
|
||||
it('runs happy path', () => {
|
||||
const external = { myRoute: createExternalRouteRef() };
|
||||
const ref = createRouteRef();
|
||||
const result = resolveRouteBindings(({ bind }) => {
|
||||
bind(external, { myRoute: ref });
|
||||
});
|
||||
const result = resolveRouteBindings(
|
||||
({ bind }) => {
|
||||
bind(external, { myRoute: ref });
|
||||
},
|
||||
new ConfigReader({}),
|
||||
emptyIds,
|
||||
);
|
||||
|
||||
expect(result.get(external.myRoute)).toBe(ref);
|
||||
});
|
||||
@@ -35,9 +42,79 @@ describe('resolveRouteBindings', () => {
|
||||
const external = { myRoute: createExternalRouteRef() };
|
||||
const ref = createRouteRef();
|
||||
expect(() =>
|
||||
resolveRouteBindings(({ bind }) => {
|
||||
bind(external, { someOtherRoute: ref } as any);
|
||||
}),
|
||||
resolveRouteBindings(
|
||||
({ bind }) => {
|
||||
bind(external, { someOtherRoute: ref } as any);
|
||||
},
|
||||
new ConfigReader({}),
|
||||
emptyIds,
|
||||
),
|
||||
).toThrow('Key someOtherRoute is not an existing external route');
|
||||
});
|
||||
|
||||
it('reads bindings from config', () => {
|
||||
const mySource = createExternalRouteRef();
|
||||
const myTarget = createRouteRef();
|
||||
const result = resolveRouteBindings(
|
||||
() => {},
|
||||
new ConfigReader({
|
||||
app: { routes: { bindings: { mySource: 'myTarget' } } },
|
||||
}),
|
||||
{
|
||||
routes: new Map([['myTarget', myTarget]]),
|
||||
externalRoutes: new Map([['mySource', mySource]]),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.get(mySource)).toBe(myTarget);
|
||||
});
|
||||
|
||||
it('throws on invalid config', () => {
|
||||
expect(() =>
|
||||
resolveRouteBindings(
|
||||
() => {},
|
||||
new ConfigReader({ app: { routes: { bindings: 'derp' } } }),
|
||||
emptyIds,
|
||||
),
|
||||
).toThrow(
|
||||
"Invalid type in config for key 'app.routes.bindings' in 'mock-config', got string, wanted object",
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolveRouteBindings(
|
||||
() => {},
|
||||
new ConfigReader({ app: { routes: { bindings: { mySource: true } } } }),
|
||||
emptyIds,
|
||||
),
|
||||
).toThrow(
|
||||
"Invalid config at app.routes.bindings['mySource'], value must be a non-empty string",
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolveRouteBindings(
|
||||
() => {},
|
||||
new ConfigReader({
|
||||
app: { routes: { bindings: { mySource: 'myTarget' } } },
|
||||
}),
|
||||
emptyIds,
|
||||
),
|
||||
).toThrow(
|
||||
"Invalid config at app.routes.bindings, 'mySource' is not a valid external route",
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolveRouteBindings(
|
||||
() => {},
|
||||
new ConfigReader({
|
||||
app: { routes: { bindings: { mySource: 'myTarget' } } },
|
||||
}),
|
||||
{
|
||||
...emptyIds,
|
||||
externalRoutes: new Map([['mySource', createExternalRouteRef()]]),
|
||||
},
|
||||
),
|
||||
).toThrow(
|
||||
"Invalid config at app.routes.bindings['mySource'], 'myTarget' is not a valid route",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { RouteRefsById } from './collectRouteIds';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
/**
|
||||
* Extracts a union of the keys in a map whose value extends the given type
|
||||
@@ -73,7 +75,9 @@ export type AppRouteBinder = <
|
||||
|
||||
/** @internal */
|
||||
export function resolveRouteBindings(
|
||||
bindRoutes?: (context: { bind: AppRouteBinder }) => void,
|
||||
bindRoutes: ((context: { bind: AppRouteBinder }) => void) | undefined,
|
||||
config: Config,
|
||||
routesById: RouteRefsById,
|
||||
): Map<ExternalRouteRef, RouteRef | SubRouteRef> {
|
||||
const result = new Map<ExternalRouteRef, RouteRef | SubRouteRef>();
|
||||
|
||||
@@ -100,5 +104,42 @@ export function resolveRouteBindings(
|
||||
bindRoutes({ bind });
|
||||
}
|
||||
|
||||
const bindingsConfig = config.getOptionalConfig('app.routes.bindings');
|
||||
if (!bindingsConfig) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const bindings = bindingsConfig.get();
|
||||
if (bindings === null || typeof bindings !== 'object') {
|
||||
throw new Error('Invalid config at app.routes.bindings, must be an object');
|
||||
}
|
||||
|
||||
for (const [externalRefId, targetRefId] of Object.entries(bindings)) {
|
||||
if (typeof targetRefId !== 'string' || targetRefId === '') {
|
||||
throw new Error(
|
||||
`Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string`,
|
||||
);
|
||||
}
|
||||
|
||||
const externalRef = routesById.externalRoutes.get(externalRefId);
|
||||
if (!externalRef) {
|
||||
throw new Error(
|
||||
`Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`,
|
||||
);
|
||||
}
|
||||
// Route bindings defined in config have lower priority than those defined in code
|
||||
if (result.has(externalRef)) {
|
||||
continue;
|
||||
}
|
||||
const targetRef = routesById.routes.get(targetRefId);
|
||||
if (!targetRef) {
|
||||
throw new Error(
|
||||
`Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`,
|
||||
);
|
||||
}
|
||||
|
||||
result.set(externalRef, targetRef);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ import {
|
||||
import { AppRouteBinder } from '../routing';
|
||||
import { RoutingProvider } from '../routing/RoutingProvider';
|
||||
import { resolveRouteBindings } from '../routing/resolveRouteBindings';
|
||||
import { collectRouteIds } from '../routing/collectRouteIds';
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionTreeNode {
|
||||
@@ -305,14 +306,19 @@ export function createApp(options: {
|
||||
),
|
||||
);
|
||||
|
||||
const routeIds = collectRouteIds(allFeatures);
|
||||
|
||||
const App = () => (
|
||||
<ApiProvider apis={createApiHolder(coreInstance, config)}>
|
||||
<AppContextProvider appContext={appContext}>
|
||||
<AppThemeProvider>
|
||||
<RoutingProvider
|
||||
// TODO(Rugvip): Move over routing app API to new system to avoid these casts
|
||||
{...extractRouteInfoFromInstanceTree(coreInstance)}
|
||||
routeBindings={resolveRouteBindings(options.bindRoutes)}
|
||||
routeBindings={resolveRouteBindings(
|
||||
options.bindRoutes,
|
||||
config,
|
||||
routeIds,
|
||||
)}
|
||||
>
|
||||
{/* TODO: set base path using the logic from AppRouter */}
|
||||
<BrowserRouter>
|
||||
|
||||
Reference in New Issue
Block a user