frontend-plugin-api: refactor RouteRef to modern structure

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-10-11 20:25:00 +02:00
parent d41c97e5db
commit 18882a9bbe
3 changed files with 107 additions and 98 deletions
@@ -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<undefined> = createRouteRef({
id: 'my-route-ref',
});
expect(routeRef.params).toEqual([]);
expect(String(routeRef)).toBe('routeRef{type=absolute,id=my-route-ref}');
const routeRef: RouteRef<undefined> = 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<T extends AnyParams>(_ref: RouteRef<T>) {}
function checkRouteRef<T extends AnyRouteParams>(
_ref: RouteRef<T>,
_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<undefined>(_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<undefined>(_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<undefined>(_3);
checkRouteRef(_3, { x: '' });
const _4 = createRouteRef({ id: '4' });
const _4 = createRouteRef();
checkRouteRef(_4, undefined);
// @ts-expect-error
validateType<{ x: string }>(_4);
validateType<undefined>(_4);
checkRouteRef(_4, { x: '' });
// To avoid complains about missing expectations and unused vars
expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String));
@@ -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<Params extends AnyParams>
implements RouteRef<Params>
{
// The marker is used for type checking while the symbol is used at runtime.
declare $$routeRefType: 'absolute';
readonly [routeRefType] = 'absolute';
export interface RouteRef<TParams extends AnyRouteParams = AnyRouteParams> {
readonly $$type: '@backstage/RouteRef';
readonly T: TParams;
}
constructor(
private readonly id: string,
readonly params: ParamKeys<Params>,
) {}
/** @internal */
export interface InternalRouteRef<
TParams extends AnyRouteParams = AnyRouteParams,
> extends RouteRef<TParams> {
readonly version: 'v1';
getParams(): string[];
}
get title() {
return this.id;
/** @internal */
export function toInternalRouteRef<
TParams extends AnyRouteParams = AnyRouteParams,
>(resource: RouteRef<TParams>): InternalRouteRef<TParams> {
const r = resource as InternalRouteRef<TParams>;
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<Params extends AnyParams>
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<OptionalParams<Params>> {
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<OptionalParams<Params>>,
);
config?.params as string[] | undefined,
) as RouteRef<any>;
}
@@ -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<Params extends AnyParams> = keyof Params extends never
? []
: (keyof Params)[];
/**
* Optional route params.
*
* @public
*/
export type OptionalParams<Params extends { [param in string]: string }> =
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<any>(
);
/**
* Absolute route reference.
* Optional route params.
*
* @remarks
*
* See {@link https://backstage.io/docs/plugins/composability#routing-system}.
*
* @public
* @internal
*/
export type RouteRef<Params extends AnyParams = any> = {
$$routeRefType: 'absolute'; // See routeRefType above
export type OptionalParams<Params extends { [param in string]: string }> =
Params[keyof Params] extends never ? undefined : Params;
params: ParamKeys<Params>;
};
/**
* Type describing the key type of a route parameter mapping.
*
* @ignore
*/
export type ParamKeys<TParams extends AnyRouteParams> =
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<any>
| SubRouteRef<any>
| ExternalRouteRef<any, any>;
/**
* A duplicate of the react-router RouteObject, but with routeRef added
* @internal