From 18882a9bbe87591f1ff6b5e0254da4546beff0b7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 11 Oct 2023 20:25:00 +0200 Subject: [PATCH] frontend-plugin-api: refactor RouteRef to modern structure Signed-off-by: Patrik Oldsberg --- .../src/routing/RouteRef.test.ts | 54 ++++----- .../src/routing/RouteRef.ts | 103 ++++++++++++------ .../frontend-plugin-api/src/routing/types.ts | 48 ++------ 3 files changed, 107 insertions(+), 98 deletions(-) diff --git a/packages/frontend-plugin-api/src/routing/RouteRef.test.ts b/packages/frontend-plugin-api/src/routing/RouteRef.test.ts index 5314957a25..b8e86d1b99 100644 --- a/packages/frontend-plugin-api/src/routing/RouteRef.test.ts +++ b/packages/frontend-plugin-api/src/routing/RouteRef.test.ts @@ -14,16 +14,14 @@ * limitations under the License. */ -import { AnyParams, RouteRef } from './types'; -import { createRouteRef } from './RouteRef'; +import { AnyRouteParams } from './types'; +import { RouteRef, createRouteRef } from './RouteRef'; describe('RouteRef', () => { it('should be created', () => { - const routeRef: RouteRef = createRouteRef({ - id: 'my-route-ref', - }); - expect(routeRef.params).toEqual([]); - expect(String(routeRef)).toBe('routeRef{type=absolute,id=my-route-ref}'); + const routeRef: RouteRef = createRouteRef(); + expect(() => routeRef.T).toThrow(); + expect(String(routeRef)).toBe('RouteRef{}'); }); it('should be created with params', () => { @@ -31,42 +29,44 @@ describe('RouteRef', () => { x: string; y: string; }> = createRouteRef({ - id: 'my-other-route-ref', params: ['x', 'y'], }); - expect(routeRef.params).toEqual(['x', 'y']); + expect(() => routeRef.T).toThrow(); }); it('should properly infer and validate parameter types and assignments', () => { - function validateType(_ref: RouteRef) {} + function checkRouteRef( + _ref: RouteRef, + _params: T extends undefined ? undefined : T, + ) {} - const _1 = createRouteRef({ id: '1', params: ['x'] }); + const _1 = createRouteRef({ params: ['x'] }); + checkRouteRef(_1, { x: '' }); // @ts-expect-error - validateType<{ y: string }>(_1); + checkRouteRef(_1, { y: '' }); // @ts-expect-error - validateType(_1); - validateType<{ x: string }>(_1); + checkRouteRef(_1, undefined); - const _2 = createRouteRef({ id: '2', params: ['x', 'y'] }); + const _2 = createRouteRef({ params: ['x', 'y'] }); + checkRouteRef(_2, { x: '', y: '' }); // @ts-expect-error - validateType<{ x: string }>(_2); + checkRouteRef(_2, { x: '' }); // @ts-expect-error - validateType(_2); + checkRouteRef(_2, undefined); // @ts-expect-error - validateType<{ x: string; z: string }>(_2); - // extra z, we validate this at runtime instead - validateType<{ x: string; y: string; z: string }>(_2); - validateType<{ x: string; y: string }>(_2); + checkRouteRef(_2, { x: '', z: '' }); + // @ts-expect-error + checkRouteRef(_2, { x: '', y: '', z: '' }); - const _3 = createRouteRef({ id: '3', params: [] }); + const _3 = createRouteRef({ params: [] }); + checkRouteRef(_3, undefined); // @ts-expect-error - validateType<{ x: string }>(_3); - validateType(_3); + checkRouteRef(_3, { x: '' }); - const _4 = createRouteRef({ id: '4' }); + const _4 = createRouteRef(); + checkRouteRef(_4, undefined); // @ts-expect-error - validateType<{ x: string }>(_4); - validateType(_4); + checkRouteRef(_4, { x: '' }); // To avoid complains about missing expectations and unused vars expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String)); diff --git a/packages/frontend-plugin-api/src/routing/RouteRef.ts b/packages/frontend-plugin-api/src/routing/RouteRef.ts index 36ca3f073d..b19b77d298 100644 --- a/packages/frontend-plugin-api/src/routing/RouteRef.ts +++ b/packages/frontend-plugin-api/src/routing/RouteRef.ts @@ -14,35 +14,68 @@ * limitations under the License. */ -import { - RouteRef, - routeRefType, - AnyParams, - ParamKeys, - OptionalParams, -} from './types'; +import { AnyRouteParams } from './types'; /** - * @internal + * Absolute route reference. + * + * @remarks + * + * See {@link https://backstage.io/docs/plugins/composability#routing-system}. + * + * @public */ -export class RouteRefImpl - implements RouteRef -{ - // The marker is used for type checking while the symbol is used at runtime. - declare $$routeRefType: 'absolute'; - readonly [routeRefType] = 'absolute'; +export interface RouteRef { + readonly $$type: '@backstage/RouteRef'; + readonly T: TParams; +} - constructor( - private readonly id: string, - readonly params: ParamKeys, - ) {} +/** @internal */ +export interface InternalRouteRef< + TParams extends AnyRouteParams = AnyRouteParams, +> extends RouteRef { + readonly version: 'v1'; + getParams(): string[]; +} - get title() { - return this.id; +/** @internal */ +export function toInternalRouteRef< + TParams extends AnyRouteParams = AnyRouteParams, +>(resource: RouteRef): InternalRouteRef { + const r = resource as InternalRouteRef; + if (r.$$type !== '@backstage/RouteRef') { + throw new Error(`Invalid RouteRef, bad type '${r.$$type}'`); } - toString() { - return `routeRef{type=absolute,id=${this.id}}`; + return r; +} + +/** @internal */ +export function isRouteRef(opaque: { $$type: string }): opaque is RouteRef { + return opaque.$$type === '@backstage/RouteRef'; +} + +/** @internal */ +export class RouteRefImpl implements InternalRouteRef { + readonly $$type = '@backstage/RouteRef'; + readonly version = 'v1'; + + #params: string[]; + + constructor(readonly params: string[] = []) { + this.#params = params; + } + + get T(): never { + throw new Error(`tried to read RouteRef.T of ${this}`); + } + + getParams(): string[] { + return this.#params; + } + + toString(): string { + return `RouteRef{}`; } } @@ -55,19 +88,19 @@ export class RouteRefImpl export function createRouteRef< // Params is the type that we care about and the one to be embedded in the route ref. // For example, given the params ['name', 'kind'], Params will be {name: string, kind: string} - Params extends { [param in ParamKey]: string }, - // ParamKey is here to make sure the Params type properly has its keys narrowed down - // to only the elements of params. Defaulting to never makes sure we end up with - // Param = {} if the params array is empty. - ParamKey extends string = never, ->(config: { - /** The id of the route ref, used to identify it when printed */ - id: string; + TParams extends { [param in TParamKeys]: string } | undefined = undefined, + TParamKeys extends string = string, +>(config?: { /** A list of parameter names that the path that this route ref is bound to must contain */ - params?: ParamKey[]; -}): RouteRef> { + readonly params: string extends TParamKeys ? (keyof TParams)[] : TParamKeys[]; +}): RouteRef< + keyof TParams extends never + ? undefined + : string extends TParamKeys + ? TParams + : { [param in TParamKeys]: string } +> { return new RouteRefImpl( - config.id, - (config.params ?? []) as ParamKeys>, - ); + config?.params as string[] | undefined, + ) as RouteRef; } diff --git a/packages/frontend-plugin-api/src/routing/types.ts b/packages/frontend-plugin-api/src/routing/types.ts index 80653518bb..b9893c1fdf 100644 --- a/packages/frontend-plugin-api/src/routing/types.ts +++ b/packages/frontend-plugin-api/src/routing/types.ts @@ -21,24 +21,7 @@ import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; * * @public */ -export type AnyParams = { [param in string]: string } | undefined; - -/** - * Type describing the key type of a route parameter mapping. - * - * @public - */ -export type ParamKeys = keyof Params extends never - ? [] - : (keyof Params)[]; - -/** - * Optional route params. - * - * @public - */ -export type OptionalParams = - Params[keyof Params] extends never ? undefined : Params; +export type AnyRouteParams = { [param in string]: string } | undefined; /** * TS magic for handling route parameters. @@ -72,19 +55,20 @@ export const routeRefType: unique symbol = getOrCreateGlobalSingleton( ); /** - * Absolute route reference. + * Optional route params. * - * @remarks - * - * See {@link https://backstage.io/docs/plugins/composability#routing-system}. - * - * @public + * @internal */ -export type RouteRef = { - $$routeRefType: 'absolute'; // See routeRefType above +export type OptionalParams = + Params[keyof Params] extends never ? undefined : Params; - params: ParamKeys; -}; +/** + * Type describing the key type of a route parameter mapping. + * + * @ignore + */ +export type ParamKeys = + keyof TParams extends never ? [] : (keyof TParams)[]; /** * Descriptor of a route relative to an absolute {@link RouteRef}. @@ -125,14 +109,6 @@ export type ExternalRouteRef< optional?: Optional; }; -/** - * @internal - */ -export type AnyRouteRef = - | RouteRef - | SubRouteRef - | ExternalRouteRef; - /** * A duplicate of the react-router RouteObject, but with routeRef added * @internal