From 720b6cda3db10ae16aab51a0659914735aaca5b1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Oct 2023 00:52:41 +0200 Subject: [PATCH] frontend-plugin-api: refactor ExternalRouteRef to modern structure Signed-off-by: Patrik Oldsberg --- .../src/routing/ExternalRouteRef.test.ts | 105 +++++++-------- .../src/routing/ExternalRouteRef.ts | 127 ++++++++++++------ .../frontend-plugin-api/src/routing/types.ts | 20 --- 3 files changed, 138 insertions(+), 114 deletions(-) diff --git a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts index f6e072f760..a38937775c 100644 --- a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts +++ b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts @@ -14,97 +14,94 @@ * limitations under the License. */ -import { AnyParams, ExternalRouteRef } from './types'; -import { createExternalRouteRef } from './ExternalRouteRef'; +import { + ExternalRouteRef, + createExternalRouteRef, + toInternalExternalRouteRef, +} from './ExternalRouteRef'; +import { AnyRouteParams } from './types'; describe('ExternalRouteRef', () => { it('should be created', () => { - const routeRef: ExternalRouteRef = createExternalRouteRef({ - id: 'my-route-ref', - }); - expect(routeRef.params).toEqual([]); - expect(routeRef.optional).toBe(false); - expect(String(routeRef)).toBe('routeRef{type=external,id=my-route-ref}'); + const routeRef: ExternalRouteRef = createExternalRouteRef(); + const internal = toInternalExternalRouteRef(routeRef); + expect(internal.getParams()).toEqual([]); + expect(internal.optional).toBe(false); + expect(String(internal)).toBe('routeRef{type=external,id=my-route-ref}'); }); it('should be created as optional', () => { - const routeRef: ExternalRouteRef<{ - x: string; - y: string; - }> = createExternalRouteRef({ - id: 'my-other-route-ref', + const routeRef: ExternalRouteRef = createExternalRouteRef({ params: [], optional: true, }); - expect(routeRef.params).toEqual([]); - expect(routeRef.optional).toEqual(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; y: string; - }> = createExternalRouteRef({ - id: 'my-other-route-ref', - params: ['x', 'y'], - }); - expect(routeRef.params).toEqual(['x', 'y']); - expect(routeRef.optional).toEqual(false); + }> = 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({ - id: 'my-other-route-ref', - params: ['x', 'y'], - optional: true, - }); - expect(routeRef.params).toEqual(['x', 'y']); - expect(routeRef.optional).toEqual(true); + }> = 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 validateType( - _ref: ExternalRouteRef, + function checkRouteRef< + T extends AnyRouteParams, + TOptional extends boolean, + TCheck extends TOptional, + >( + _ref: ExternalRouteRef, + _params: T extends undefined ? undefined : T, + _optional: TCheck, ) {} - const _1 = createExternalRouteRef({ id: '1', params: ['notX'] }); + const _1 = createExternalRouteRef({ params: ['notX'] }); + checkRouteRef(_1, { notX: '' }, false); // @ts-expect-error - validateType<{ x: string }, any>(_1); - validateType<{ notX: string }, any>(_1); + checkRouteRef(_1, { x: '' }, false); - const _2 = createExternalRouteRef({ - id: '2', - params: ['x'], - optional: true, - }); + const _2 = createExternalRouteRef({ params: ['x'], optional: true }); + checkRouteRef(_2, { x: '' }, true); // @ts-expect-error - validateType(_2); - validateType<{ x: string }, true>(_2); + checkRouteRef(_2, undefined, false); - const _3 = createExternalRouteRef({ id: '3', params: ['x', 'y'] }); + const _3 = createExternalRouteRef({ params: ['x', 'y'] }); + checkRouteRef(_3, { x: '', y: '' }, false); // @ts-expect-error - validateType<{ x: string }, any>(_3); - // extra z, we validate this at runtime instead - validateType<{ x: string; y: string; z: string }, any>(_3); - validateType<{ x: string; y: string }, false>(_3); + checkRouteRef(_3, { x: '' }, false); + // @ts-expect-error + checkRouteRef(_3, { x: '', y: '', z: '' }, false); - const _4 = createExternalRouteRef({ id: '4', params: [] }); + const _4 = createExternalRouteRef({ params: [] }); + checkRouteRef(_4, undefined, false); // @ts-expect-error - validateType<{ x: string }, any>(_4); - validateType(_4); + checkRouteRef(_4, { x: '' }); - const _5 = createExternalRouteRef({ id: '5' }); + const _5 = createExternalRouteRef(); + checkRouteRef(_5, undefined, false); // @ts-expect-error - validateType<{ x: string }, any>(_5); - validateType(_5); + checkRouteRef(_5, { x: '' }); - const _6 = createExternalRouteRef({ id: '6', optional: true }); + const _6 = createExternalRouteRef({ optional: true }); + checkRouteRef(_6, undefined, true); // @ts-expect-error - validateType(_6); - validateType(_6); + checkRouteRef(_6, undefined, false); // To avoid complains about missing expectations and unused vars expect([_1, _2, _3, _4, _5, _6].join('')).toEqual(expect.any(String)); diff --git a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts index 2ebb81dbfb..edc546ee23 100644 --- a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts +++ b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts @@ -14,34 +14,78 @@ * limitations under the License. */ -import { - ExternalRouteRef, - routeRefType, - AnyParams, - ParamKeys, - OptionalParams, -} from './types'; +import { AnyRouteParams } from './types'; /** - * @internal + * Route descriptor, to be later bound to a concrete route by the app. Used to implement cross-plugin route references. + * + * @remarks + * + * See {@link https://backstage.io/docs/plugins/composability#routing-system}. + * + * @public */ -export class ExternalRouteRefImpl< - Params extends AnyParams, - Optional extends boolean, -> implements ExternalRouteRef -{ - // The marker is used for type checking while the symbol is used at runtime. - declare $$routeRefType: 'external'; - readonly [routeRefType] = 'external'; +export interface ExternalRouteRef< + TParams extends AnyRouteParams = AnyRouteParams, + TOptional extends boolean = boolean, +> { + readonly $$type: '@backstage/ExternalRouteRef'; + readonly T: TParams; + readonly optional: TOptional; +} - constructor( - private readonly id: string, - readonly params: ParamKeys, - readonly optional: Optional, - ) {} +/** @internal */ +export interface InternalExternalRouteRef< + TParams extends AnyRouteParams = AnyRouteParams, + TOptional extends boolean = boolean, +> extends ExternalRouteRef { + readonly version: 'v1'; + getParams(): string[]; +} - toString() { - return `routeRef{type=external,id=${this.id}}`; +/** @internal */ +export function toInternalExternalRouteRef< + TParams extends AnyRouteParams = AnyRouteParams, + TOptional extends boolean = boolean, +>( + resource: ExternalRouteRef, +): InternalExternalRouteRef { + const r = resource as InternalExternalRouteRef; + if (r.$$type !== '@backstage/ExternalRouteRef') { + throw new Error(`Invalid ExternalRouteRef, bad type '${r.$$type}'`); + } + + return r; +} + +/** @internal */ +export function isExternalRouteRef(opaque: { + $$type: string; +}): opaque is ExternalRouteRef { + return opaque.$$type === '@backstage/ExternalRouteRef'; +} + +/** @internal */ +export class ExternalRouteRefImpl implements InternalExternalRouteRef { + readonly $$type = '@backstage/ExternalRouteRef'; + readonly version = 'v1'; + + #params: string[]; + + constructor(readonly optional: boolean, readonly params: string[] = []) { + this.#params = params; + } + + get T(): never { + throw new Error(`tried to read ExternalRouteRef.T of ${this}`); + } + + getParams(): string[] { + return this.#params; + } + + toString(): string { + return `ExternalRouteRef{}`; } } @@ -56,31 +100,34 @@ export class ExternalRouteRefImpl< * @public */ export function createExternalRouteRef< - Params extends { [param in ParamKey]: string }, - Optional extends boolean = false, - ParamKey extends string = never, ->(options: { - /** - * An identifier for this route, used to identify it in error messages - */ - id: string; - + TParams extends { [param in TParamKeys]: string } | undefined = undefined, + TOptional extends boolean = false, + TParamKeys extends string = string, +>(options?: { /** * The parameters that will be provided to the external route reference. */ - params?: ParamKey[]; + readonly params?: string extends TParamKeys + ? (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, `useRouteRef` will return `undefined`. + * if they aren't, `useExternalRouteRef` will return `undefined`. */ - optional?: Optional; -}): ExternalRouteRef, Optional> { + optional?: TOptional; +}): ExternalRouteRef< + keyof TParams extends never + ? undefined + : string extends TParamKeys + ? TParams + : { [param in TParamKeys]: string }, + TOptional +> { return new ExternalRouteRefImpl( - options.id, - (options.params ?? []) as ParamKeys>, - Boolean(options.optional) as Optional, - ); + Boolean(options?.optional), + options?.params as string[] | undefined, + ) as ExternalRouteRef; } diff --git a/packages/frontend-plugin-api/src/routing/types.ts b/packages/frontend-plugin-api/src/routing/types.ts index d5afc9213c..2eaa25ba94 100644 --- a/packages/frontend-plugin-api/src/routing/types.ts +++ b/packages/frontend-plugin-api/src/routing/types.ts @@ -70,26 +70,6 @@ export type OptionalParams = export type ParamKeys = keyof TParams extends never ? [] : (keyof TParams)[]; -/** - * Route descriptor, to be later bound to a concrete route by the app. Used to implement cross-plugin route references. - * - * @remarks - * - * See {@link https://backstage.io/docs/plugins/composability#routing-system}. - * - * @public - */ -export type ExternalRouteRef< - Params extends AnyParams = any, - Optional extends boolean = any, -> = { - $$routeRefType: 'external'; // See routeRefType above - - params: ParamKeys; - - optional?: Optional; -}; - /** * A duplicate of the react-router RouteObject, but with routeRef added * @internal