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:
@@ -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`.
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user