frontend-plugin-api: make all route refs optional at all times

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-08-06 10:44:30 +02:00
parent e46805cdd0
commit 72754db000
27 changed files with 172 additions and 318 deletions
@@ -60,7 +60,7 @@ export interface RouteResolutionApi {
anyRouteRef:
| RouteRef<TParams>
| SubRouteRef<TParams>
| ExternalRouteRef<TParams, any>,
| ExternalRouteRef<TParams>,
options?: RouteResolutionApiResolveOptions,
): RouteFunc<TParams> | undefined;
}
@@ -26,7 +26,6 @@ describe('ExternalRouteRef', () => {
const routeRef: ExternalRouteRef<undefined> = createExternalRouteRef();
const internal = toInternalExternalRouteRef(routeRef);
expect(internal.getParams()).toEqual([]);
expect(internal.optional).toBe(false);
expect(String(internal)).toMatch(
/^ExternalRouteRef\{created at '.*ExternalRouteRef\.test\.ts.*'\}$/,
@@ -35,16 +34,6 @@ describe('ExternalRouteRef', () => {
expect(String(internal)).toBe('ExternalRouteRef{some-id}');
});
it('should be created as optional', () => {
const routeRef: ExternalRouteRef<undefined, true> = createExternalRouteRef({
params: [],
optional: true,
});
const internal = toInternalExternalRouteRef(routeRef);
expect(internal.getParams()).toEqual([]);
expect(internal.optional).toEqual(true);
});
it('should be created with params', () => {
const routeRef: ExternalRouteRef<{
x: string;
@@ -52,63 +41,39 @@ describe('ExternalRouteRef', () => {
}> = createExternalRouteRef({ params: ['x', 'y'] });
const internal = toInternalExternalRouteRef(routeRef);
expect(internal.getParams()).toEqual(['x', 'y']);
expect(internal.optional).toEqual(false);
});
it('should be created as optional with params', () => {
const routeRef: ExternalRouteRef<{
x: string;
y: string;
}> = createExternalRouteRef({ params: ['x', 'y'], optional: true });
const internal = toInternalExternalRouteRef(routeRef);
expect(internal.getParams()).toEqual(['x', 'y']);
expect(internal.optional).toEqual(true);
});
it('should properly infer and validate parameter types and assignments', () => {
function checkRouteRef<
T extends AnyRouteRefParams,
TOptional extends boolean,
TCheck extends TOptional,
>(
_ref: ExternalRouteRef<T, TOptional>,
function checkRouteRef<T extends AnyRouteRefParams>(
_ref: ExternalRouteRef<T>,
_params: T extends undefined ? undefined : T,
_optional: TCheck,
) {}
const _1 = createExternalRouteRef({ params: ['notX'] });
checkRouteRef(_1, { notX: '' }, false);
const _1 = createExternalRouteRef();
checkRouteRef(_1, undefined);
// @ts-expect-error
checkRouteRef(_1, { x: '' }, false);
checkRouteRef(_1, { x: '' });
const _2 = createExternalRouteRef({ params: ['x'], optional: true });
checkRouteRef(_2, { x: '' }, true);
const _2 = createExternalRouteRef({ params: ['x'] });
checkRouteRef(_2, { x: '' });
// @ts-expect-error
checkRouteRef(_2, undefined, false);
checkRouteRef(_2, { notX: '' });
// @ts-expect-error
checkRouteRef(_2, undefined);
const _3 = createExternalRouteRef({ params: ['x', 'y'] });
checkRouteRef(_3, { x: '', y: '' }, false);
checkRouteRef(_3, { x: '', y: '' });
// @ts-expect-error
checkRouteRef(_3, { x: '' }, false);
checkRouteRef(_3, { x: '' });
// @ts-expect-error
checkRouteRef(_3, { x: '', y: '', z: '' }, false);
checkRouteRef(_3, { x: '', y: '', z: '' });
const _4 = createExternalRouteRef({ params: [] });
checkRouteRef(_4, undefined, false);
checkRouteRef(_4, undefined);
// @ts-expect-error
checkRouteRef<any>(_4, { x: '' });
const _5 = createExternalRouteRef();
checkRouteRef(_5, undefined, false);
// @ts-expect-error
checkRouteRef<any>(_5, { x: '' });
const _6 = createExternalRouteRef({ optional: true });
checkRouteRef(_6, undefined, true);
// @ts-expect-error
checkRouteRef(_6, undefined, false);
checkRouteRef(_4, { x: '' });
// To avoid complains about missing expectations and unused vars
expect([_1, _2, _3, _4, _5, _6].join('')).toEqual(expect.any(String));
expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String));
});
});
@@ -29,18 +29,15 @@ import { AnyRouteRefParams } from './types';
*/
export interface ExternalRouteRef<
TParams extends AnyRouteRefParams = AnyRouteRefParams,
TOptional extends boolean = boolean,
> {
readonly $$type: '@backstage/ExternalRouteRef';
readonly T: TParams;
readonly optional: TOptional;
}
/** @internal */
export interface InternalExternalRouteRef<
TParams extends AnyRouteRefParams = AnyRouteRefParams,
TOptional extends boolean = boolean,
> extends ExternalRouteRef<TParams, TOptional> {
> extends ExternalRouteRef<TParams> {
readonly version: 'v1';
getParams(): string[];
getDescription(): string;
@@ -52,11 +49,8 @@ export interface InternalExternalRouteRef<
/** @internal */
export function toInternalExternalRouteRef<
TParams extends AnyRouteRefParams = AnyRouteRefParams,
TOptional extends boolean = boolean,
>(
resource: ExternalRouteRef<TParams, TOptional>,
): InternalExternalRouteRef<TParams, TOptional> {
const r = resource as InternalExternalRouteRef<TParams, TOptional>;
>(resource: ExternalRouteRef<TParams>): InternalExternalRouteRef<TParams> {
const r = resource as InternalExternalRouteRef<TParams>;
if (r.$$type !== '@backstage/ExternalRouteRef') {
throw new Error(`Invalid ExternalRouteRef, bad type '${r.$$type}'`);
}
@@ -79,7 +73,6 @@ class ExternalRouteRefImpl
readonly $$type = '@backstage/ExternalRouteRef' as any;
constructor(
readonly optional: boolean,
readonly params: string[] = [],
readonly defaultTarget: string | undefined,
creationSite: string,
@@ -104,7 +97,6 @@ class ExternalRouteRefImpl
*/
export function createExternalRouteRef<
TParams extends { [param in TParamKeys]: string } | undefined = undefined,
TOptional extends boolean = false,
TParamKeys extends string = string,
>(options?: {
/**
@@ -114,14 +106,6 @@ export function createExternalRouteRef<
? (keyof TParams)[]
: TParamKeys[];
/**
* Whether or not this route is optional, defaults to false.
*
* Optional external routes are not required to be bound in the app, and
* if they aren't, `useExternalRouteRef` will return `undefined`.
*/
optional?: TOptional;
/**
* The route (typically in another plugin) that this should map to by default.
*
@@ -134,13 +118,11 @@ export function createExternalRouteRef<
? undefined
: string extends TParamKeys
? TParams
: { [param in TParamKeys]: string },
TOptional
: { [param in TParamKeys]: string }
> {
return new ExternalRouteRefImpl(
Boolean(options?.optional),
options?.params as string[] | undefined,
options?.defaultTarget,
describeParentCallSite(),
) as ExternalRouteRef<any, any>;
);
}
@@ -45,7 +45,7 @@ describe('v1 consumer', () => {
});
const routeFunc = renderedHook.result.current;
expect(routeFunc()).toBe('/hello');
expect(routeFunc?.()).toBe('/hello');
expect(resolve).toHaveBeenCalledWith(
routeRef,
expect.objectContaining({
@@ -54,6 +54,23 @@ describe('v1 consumer', () => {
);
});
it('should ignore missing routes', () => {
const routeRef = createRouteRef();
const renderedHook = renderHook(() => useRouteRef(routeRef), {
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
<TestApiProvider
apis={[[routeResolutionApiRef, { resolve: () => undefined }]]}
>
<MemoryRouter initialEntries={['/my-page']} children={children} />
</TestApiProvider>
),
});
const routeFunc = renderedHook.result.current;
expect(routeFunc).toBeUndefined();
});
it('re-resolves the routeFunc when the search parameters change', () => {
const resolve = jest.fn(() => () => '/hello');
@@ -30,47 +30,14 @@ import { RouteFunc, routeResolutionApiRef, useApi } from '../apis';
* See {@link https://backstage.io/docs/plugins/composability#routing-system}
*
* @param routeRef - The ref to route that should be converted to URL.
* @returns A function that will in turn return the concrete URL of the `routeRef`.
* @public
*/
export function useRouteRef<
TOptional extends boolean,
TParams extends AnyRouteRefParams,
>(
routeRef: ExternalRouteRef<TParams, TOptional>,
): TOptional extends true ? RouteFunc<TParams> | undefined : RouteFunc<TParams>;
/**
* React hook for constructing URLs to routes.
*
* @remarks
*
* See {@link https://backstage.io/docs/plugins/composability#routing-system}
*
* @param routeRef - The ref to route that should be converted to URL.
* @returns A function that will in turn return the concrete URL of the `routeRef`.
* @public
*/
export function useRouteRef<TParams extends AnyRouteRefParams>(
routeRef: RouteRef<TParams> | SubRouteRef<TParams>,
): RouteFunc<TParams>;
/**
* React hook for constructing URLs to routes.
*
* @remarks
*
* See {@link https://backstage.io/docs/plugins/composability#routing-system}
*
* @param routeRef - The ref to route that should be converted to URL.
* @returns A function that will in turn return the concrete URL of the `routeRef`.
* @returns A function that will in turn return the concrete URL of the `routeRef`, or `undefined` if the route is not available.
* @public
*/
export function useRouteRef<TParams extends AnyRouteRefParams>(
routeRef:
| RouteRef<TParams>
| SubRouteRef<TParams>
| ExternalRouteRef<TParams, any>,
| ExternalRouteRef<TParams>,
): RouteFunc<TParams> | undefined {
const { pathname } = useLocation();
const routeResolutionApi = useApi(routeResolutionApiRef);
@@ -80,10 +47,5 @@ export function useRouteRef<TParams extends AnyRouteRefParams>(
[routeResolutionApi, routeRef, pathname],
);
const isOptional = 'optional' in routeRef && routeRef.optional;
if (!routeFunc && !isOptional) {
throw new Error(`No path for ${routeRef}`);
}
return routeFunc;
}