core: further improve forwards compatibility with core-* through route ref re-use

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-06-09 22:57:57 +02:00
parent d8c8f17be8
commit b65bb44eee
4 changed files with 12 additions and 245 deletions
@@ -20,56 +20,9 @@ import {
ExternalRouteRef,
routeRefType,
AnyParams,
ParamKeys,
OptionalParams,
} from './types';
export class ExternalRouteRefImpl<
Params extends AnyParams,
Optional extends boolean
> implements ExternalRouteRef<Params, Optional> {
readonly [routeRefType] = 'external';
constructor(
private readonly id: string,
readonly params: ParamKeys<Params>,
readonly optional: Optional,
) {}
toString() {
return `routeRef{type=external,id=${this.id}}`;
}
}
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;
/**
* The parameters that will be provided to the external route reference.
*/
params?: ParamKey[];
/**
* 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`.
*/
optional?: Optional;
}): ExternalRouteRef<OptionalParams<Params>, Optional> {
return new ExternalRouteRefImpl(
options.id,
(options.params ?? []) as ParamKeys<OptionalParams<Params>>,
Boolean(options.optional) as Optional,
);
}
export { createExternalRouteRef } from '@backstage/core-plugin-api';
export function isExternalRouteRef<
Params extends AnyParams,
+2 -64
View File
@@ -21,10 +21,11 @@ import {
routeRefType,
AnyParams,
ParamKeys,
OptionalParams,
} from './types';
import { IconComponent } from '../icons/types';
export { createRouteRef } from '@backstage/core-plugin-api';
// TODO(Rugvip): Remove this in the next breaking release, it's exported but unused
export type RouteRefConfig<Params extends AnyParams> = {
params?: ParamKeys<Params>;
@@ -33,69 +34,6 @@ export type RouteRefConfig<Params extends AnyParams> = {
title: string;
};
export class RouteRefImpl<Params extends AnyParams>
implements RouteRef<Params> {
readonly [routeRefType] = 'absolute';
constructor(
private readonly id: string,
readonly params: ParamKeys<Params>,
private readonly config: {
path?: string;
icon?: IconComponent;
title?: string;
},
) {}
get icon() {
return this.config.icon;
}
// TODO(Rugvip): Remove this, routes are looked up via the registry instead
get path() {
return this.config.path ?? '';
}
get title() {
return this.config.title ?? this.id;
}
toString() {
return `routeRef{type=absolute,id=${this.id}}`;
}
}
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;
/** A list of parameter names that the path that this route ref is bound to must contain */
params?: ParamKey[];
/** @deprecated Route refs no longer decide their own path */
path?: string;
/** @deprecated Route refs no longer decide their own icon */
icon?: IconComponent;
/** @deprecated Route refs no longer decide their own title */
title?: string;
}): RouteRef<OptionalParams<Params>> {
const id = config.id || config.title;
if (!id) {
throw new Error('RouteRef must be provided a non-empty id');
}
return new RouteRefImpl(
id,
(config.params ?? []) as ParamKeys<OptionalParams<Params>>,
config,
);
}
export function isRouteRef<Params extends AnyParams>(
routeRef:
| RouteRef<Params>
+1 -95
View File
@@ -17,106 +17,12 @@
import {
AnyParams,
ExternalRouteRef,
OptionalParams,
ParamKeys,
RouteRef,
routeRefType,
SubRouteRef,
} from './types';
// Should match the pattern in react-router
const PARAM_PATTERN = /^\w+$/;
export class SubRouteRefImpl<Params extends AnyParams>
implements SubRouteRef<Params> {
readonly [routeRefType] = 'sub';
constructor(
private readonly id: string,
readonly path: string,
readonly parent: RouteRef,
readonly params: ParamKeys<Params>,
) {}
toString() {
return `routeRef{type=sub,id=${this.id}}`;
}
}
// These utility types help us infer a Param object type from a string path
// For example, `/foo/:bar/:baz` inferred to `{ bar: string, baz: string }`
type ParamPart<S extends string> = S extends `:${infer Param}` ? Param : never;
type ParamNames<S extends string> = S extends `${infer Part}/${infer Rest}`
? ParamPart<Part> | ParamNames<Rest>
: ParamPart<S>;
type PathParams<S extends string> = { [name in ParamNames<S>]: string };
/**
* Merges a param object type with with an optional params type into a params object
*/
type MergeParams<
P1 extends { [param in string]: string },
P2 extends AnyParams
> = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2);
/**
* Creates a SubRouteRef type given the desired parameters and parent route parameters.
* The parameters types are merged together while ensuring that there is no overlap between the two.
*/
type MakeSubRouteRef<
Params extends { [param in string]: string },
ParentParams extends AnyParams
> = keyof Params & keyof ParentParams extends never
? SubRouteRef<OptionalParams<MergeParams<Params, ParentParams>>>
: never;
export function createSubRouteRef<
Path extends string,
ParentParams extends AnyParams = never
>(config: {
id: string;
path: Path;
parent: RouteRef<ParentParams>;
}): MakeSubRouteRef<PathParams<Path>, ParentParams> {
const { id, path, parent } = config;
type Params = PathParams<Path>;
// Collect runtime parameters from the path, e.g. ['bar', 'baz'] from '/foo/:bar/:baz'
const pathParams = path
.split('/')
.filter(p => p.startsWith(':'))
.map(p => p.substring(1));
const params = [...parent.params, ...pathParams];
if (parent.params.some(p => pathParams.includes(p as string))) {
throw new Error(
'SubRouteRef may not have params that overlap with its parent',
);
}
if (!path.startsWith('/')) {
throw new Error(`SubRouteRef path must start with '/', got '${path}'`);
}
if (path.endsWith('/')) {
throw new Error(`SubRouteRef path must not end with '/', got '${path}'`);
}
for (const param of pathParams) {
if (!PARAM_PATTERN.test(param)) {
throw new Error(`SubRouteRef path has invalid param, got '${param}'`);
}
}
// We ensure that the type of the return type is sane here
const subRouteRef = new SubRouteRefImpl(
id,
path,
parent,
params as ParamKeys<MergeParams<Params, ParentParams>>,
) as SubRouteRef<OptionalParams<MergeParams<Params, ParentParams>>>;
// But skip type checking of the return value itself, because the conditional
// type checking of the parent parameter overlap is tricky to express.
return subRouteRef as any;
}
export { createSubRouteRef } from '@backstage/core-plugin-api';
export function isSubRouteRef<Params extends AnyParams>(
routeRef:
+8 -38
View File
@@ -14,9 +14,14 @@
* limitations under the License.
*/
import { IconComponent } from '../icons/types';
import {
RouteRef,
SubRouteRef,
ExternalRouteRef,
} from '@backstage/core-plugin-api';
import { getOrCreateGlobalSingleton } from '../lib/globalObject';
import { RouteRef as NewRouteRef } from '@backstage/core-plugin-api';
export type { RouteRef, SubRouteRef, ExternalRouteRef };
export type AnyParams = { [param in string]: string } | undefined;
export type ParamKeys<Params extends AnyParams> = keyof Params extends never
@@ -36,7 +41,7 @@ export type RouteFunc<Params extends AnyParams> = (
) => string;
type RouteRefType = Exclude<
keyof NewRouteRef,
keyof RouteRef,
'params' | 'path' | 'title' | 'icon'
>;
export const routeRefType: RouteRefType = getOrCreateGlobalSingleton<any>(
@@ -44,41 +49,6 @@ export const routeRefType: RouteRefType = getOrCreateGlobalSingleton<any>(
() => Symbol('route-ref-type'),
);
export type RouteRef<Params extends AnyParams = any> = {
readonly [routeRefType]: 'absolute';
params: ParamKeys<Params>;
// TODO(Rugvip): Remove all of these once plugins don't rely on the path
/** @deprecated paths are no longer accessed directly from RouteRefs, use useRouteRef instead */
path: string;
/** @deprecated icons are no longer accessed via RouteRefs */
icon?: IconComponent;
/** @deprecated titles are no longer accessed via RouteRefs */
title?: string;
};
export type SubRouteRef<Params extends AnyParams = any> = {
readonly [routeRefType]: 'sub';
parent: RouteRef;
path: string;
params: ParamKeys<Params>;
};
export type ExternalRouteRef<
Params extends AnyParams = any,
Optional extends boolean = any
> = {
readonly [routeRefType]: 'external';
params: ParamKeys<Params>;
optional?: Optional;
};
export type AnyRouteRef =
| RouteRef<any>
| SubRouteRef<any>