core-app-api: add support for serving the app on a base path

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-08-07 14:07:41 +02:00
parent 9f8f8dd6b4
commit 3626576230
6 changed files with 68 additions and 20 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-app-api': patch
---
Add support for serving the app with a base path other than `/`, which is enabled by including the path in `app.baseUrl`.
+18 -10
View File
@@ -107,6 +107,20 @@ export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) {
return result;
}
/**
* Get the app base path from the configured app baseUrl.
*
* The returned path does not have a trailing slash.
*/
function getBasePath(configApi: Config) {
let { pathname } = new URL(
configApi.getOptionalString('app.baseUrl') ?? '/',
'http://dummy.dev', // baseUrl can be specified as just a path
);
pathname = pathname.replace(/\/*$/, '');
return pathname;
}
type FullAppOptions = {
apis: Iterable<AnyApiFactory>;
icons: NonNullable<AppOptions['icons']>;
@@ -302,6 +316,7 @@ export class PrivateAppImpl implements BackstageApp {
routeParents={routeParents}
routeObjects={routeObjects}
routeBindings={generateBoundRoutes(this.bindRoutes)}
basePath={getBasePath(loadedConfig.api)}
>
{children}
</RoutingProvider>
@@ -339,14 +354,7 @@ export class PrivateAppImpl implements BackstageApp {
const AppRouter = ({ children }: PropsWithChildren<{}>) => {
const configApi = useApi(configApiRef);
let { pathname } = new URL(
configApi.getOptionalString('app.baseUrl') ?? '/',
'http://dummy.dev', // baseUrl can be specified as just a path
);
if (pathname.endsWith('/')) {
pathname = pathname.replace(/\/$/, '');
}
const mountPath = `${getBasePath(configApi)}/*`;
// If the app hasn't configured a sign-in page, we just continue as guest.
if (!SignInPageComponent) {
@@ -361,7 +369,7 @@ export class PrivateAppImpl implements BackstageApp {
return (
<RouterComponent>
<Routes>
<Route path={`${pathname}/*`} element={<>{children}</>} />
<Route path={mountPath} element={<>{children}</>} />
</Routes>
</RouterComponent>
);
@@ -371,7 +379,7 @@ export class PrivateAppImpl implements BackstageApp {
<RouterComponent>
<SignInPageWrapper component={SignInPageComponent}>
<Routes>
<Route path={`${pathname}/*`} element={<>{children}</>} />
<Route path={mountPath} element={<>{children}</>} />
</Routes>
</SignInPageWrapper>
</RouterComponent>
@@ -65,7 +65,7 @@ const externalRef4 = createExternalRouteRef({
describe('RouteResolver', () => {
it('should not resolve anything with an empty resolver', () => {
const r = new RouteResolver(new Map(), new Map(), [], new Map());
const r = new RouteResolver(new Map(), new Map(), [], new Map(), '');
expect(r.resolve(ref1, '/')?.()).toBe(undefined);
expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe(undefined);
@@ -85,6 +85,7 @@ describe('RouteResolver', () => {
new Map(),
[{ routeRefs: new Set([ref1]), path: '/my-route', ...rest }],
new Map(),
'',
);
expect(r.resolve(ref1, '/')?.()).toBe('/my-route');
@@ -99,6 +100,29 @@ describe('RouteResolver', () => {
expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined);
});
it('should resolve an absolute route and an app base path', () => {
const r = new RouteResolver(
new Map([[ref1, '/my-route']]),
new Map(),
[{ routeRefs: new Set([ref1]), path: '/my-route', ...rest }],
new Map(),
'/base',
);
expect(r.resolve(ref1, '/')?.()).toBe('/base/my-route');
expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe(undefined);
expect(r.resolve(subRef1, '/')?.()).toBe('/base/my-route/foo');
expect(r.resolve(subRef2, '/')?.({ a: '2a' })).toBe(
'/base/my-route/foo/2a',
);
expect(r.resolve(subRef3, '/')?.({ x: '3x' })).toBe(undefined);
expect(r.resolve(subRef4, '/')?.({ x: '4x', a: '4a' })).toBe(undefined);
expect(r.resolve(externalRef1, '/')?.()).toBe(undefined);
expect(r.resolve(externalRef2, '/')?.()).toBe(undefined);
expect(r.resolve(externalRef3, '/')?.({ x: '5x' })).toBe(undefined);
expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined);
});
it('should resolve an absolute route with a param and with a parent', () => {
const r = new RouteResolver(
new Map<RouteRef, string>([
@@ -122,6 +146,7 @@ describe('RouteResolver', () => {
[externalRef3, ref2],
[externalRef4, subRef3],
]),
'',
);
expect(r.resolve(ref1, '/')?.()).toBe('/my-route');
@@ -179,6 +204,7 @@ describe('RouteResolver', () => {
},
],
new Map<ExternalRouteRef, RouteRef | SubRouteRef>(),
'',
);
expect(r.resolve(ref2, '/')?.({ x: 'x' })).toBe('/root/x');
@@ -232,6 +258,7 @@ describe('RouteResolver', () => {
[externalRef3, ref2],
[externalRef4, subRef3],
]),
'',
);
const l = '/my-grandparent/my-y/my-parent/my-x';
@@ -187,6 +187,7 @@ export class RouteResolver {
ExternalRouteRef,
RouteRef | SubRouteRef
>,
private readonly appBasePath: string, // base path without a trailing slash
) {}
resolve<Params extends AnyParams>(
@@ -209,13 +210,15 @@ export class RouteResolver {
// Next we figure out the base path, which is the combination of the common parent path
// between our current location and our target location, as well as the additional path
// that is the difference between the parent path and the base of our target location.
const basePath = resolveBasePath(
targetRef,
sourceLocation,
this.routePaths,
this.routeParents,
this.routeObjects,
);
const basePath =
this.appBasePath +
resolveBasePath(
targetRef,
sourceLocation,
this.routePaths,
this.routeParents,
this.routeObjects,
);
const routeFunc: RouteFunc<Params> = (...[params]) => {
return basePath + generatePath(targetPath, params);
@@ -152,6 +152,7 @@ function withRoutingProvider(
routeParents={routeParents}
routeObjects={routeObjects}
routeBindings={new Map(routeBindings)}
basePath=""
>
{root}
</RoutingProvider>
@@ -367,6 +368,7 @@ describe('v1 consumer', () => {
routeParents={new Map()}
routeObjects={[]}
routeBindings={new Map()}
basePath="/base"
children={children}
/>
),
@@ -375,8 +377,8 @@ describe('v1 consumer', () => {
expect(renderedHook.result.current).toBe(undefined);
renderedHook.rerender({ routeRef: routeRef2 });
expect(renderedHook.result.current?.()).toBe('/foo');
expect(renderedHook.result.current?.()).toBe('/base/foo');
renderedHook.rerender({ routeRef: routeRef3 });
expect(renderedHook.result.current?.({ x: 'my-x' })).toBe('/bar/my-x');
expect(renderedHook.result.current?.({ x: 'my-x' })).toBe('/base/bar/my-x');
});
});
@@ -38,6 +38,7 @@ type ProviderProps = {
routeParents: Map<RouteRef, RouteRef | undefined>;
routeObjects: BackstageRouteObject[];
routeBindings: Map<ExternalRouteRef, RouteRef | SubRouteRef>;
basePath?: string;
children: ReactNode;
};
@@ -46,6 +47,7 @@ export const RoutingProvider = ({
routeParents,
routeObjects,
routeBindings,
basePath = '',
children,
}: ProviderProps) => {
const resolver = new RouteResolver(
@@ -53,6 +55,7 @@ export const RoutingProvider = ({
routeParents,
routeObjects,
routeBindings,
basePath,
);
const versionedValue = createVersionedValueMap({ 1: resolver });