frontend-plugin-api: refactor RouteRef to modern structure
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user