From b65bb44eeed939d8b0c1047433396a702cc1620a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 9 Jun 2021 22:57:57 +0200 Subject: [PATCH] core: further improve forwards compatibility with core-* through route ref re-use Signed-off-by: Patrik Oldsberg --- .../core-api/src/routing/ExternalRouteRef.ts | 49 +--------- packages/core-api/src/routing/RouteRef.ts | 66 +------------ packages/core-api/src/routing/SubRouteRef.ts | 96 +------------------ packages/core-api/src/routing/types.ts | 46 ++------- 4 files changed, 12 insertions(+), 245 deletions(-) diff --git a/packages/core-api/src/routing/ExternalRouteRef.ts b/packages/core-api/src/routing/ExternalRouteRef.ts index e6af9cf8a7..af61ad8dfc 100644 --- a/packages/core-api/src/routing/ExternalRouteRef.ts +++ b/packages/core-api/src/routing/ExternalRouteRef.ts @@ -20,56 +20,9 @@ import { ExternalRouteRef, routeRefType, AnyParams, - ParamKeys, - OptionalParams, } from './types'; -export class ExternalRouteRefImpl< - Params extends AnyParams, - Optional extends boolean -> implements ExternalRouteRef { - readonly [routeRefType] = 'external'; - - constructor( - private readonly id: string, - readonly params: ParamKeys, - 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, Optional> { - return new ExternalRouteRefImpl( - options.id, - (options.params ?? []) as ParamKeys>, - Boolean(options.optional) as Optional, - ); -} +export { createExternalRouteRef } from '@backstage/core-plugin-api'; export function isExternalRouteRef< Params extends AnyParams, diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index 4603b03a30..a6a8d60326 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -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?: ParamKeys; @@ -33,69 +34,6 @@ export type RouteRefConfig = { title: string; }; -export class RouteRefImpl - implements RouteRef { - readonly [routeRefType] = 'absolute'; - - constructor( - private readonly id: string, - readonly params: ParamKeys, - 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> { - 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>, - config, - ); -} - export function isRouteRef( routeRef: | RouteRef diff --git a/packages/core-api/src/routing/SubRouteRef.ts b/packages/core-api/src/routing/SubRouteRef.ts index 7ddfc89c80..61d3450e27 100644 --- a/packages/core-api/src/routing/SubRouteRef.ts +++ b/packages/core-api/src/routing/SubRouteRef.ts @@ -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 - implements SubRouteRef { - readonly [routeRefType] = 'sub'; - - constructor( - private readonly id: string, - readonly path: string, - readonly parent: RouteRef, - readonly params: ParamKeys, - ) {} - - 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 `:${infer Param}` ? Param : never; -type ParamNames = S extends `${infer Part}/${infer Rest}` - ? ParamPart | ParamNames - : ParamPart; -type PathParams = { [name in ParamNames]: 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>> - : never; - -export function createSubRouteRef< - Path extends string, - ParentParams extends AnyParams = never ->(config: { - id: string; - path: Path; - parent: RouteRef; -}): MakeSubRouteRef, ParentParams> { - const { id, path, parent } = config; - type Params = PathParams; - - // 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>, - ) as SubRouteRef>>; - - // 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( routeRef: diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index e88b2f748c..0e5a786770 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -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 = keyof Params extends never @@ -36,7 +41,7 @@ export type RouteFunc = ( ) => string; type RouteRefType = Exclude< - keyof NewRouteRef, + keyof RouteRef, 'params' | 'path' | 'title' | 'icon' >; export const routeRefType: RouteRefType = getOrCreateGlobalSingleton( @@ -44,41 +49,6 @@ export const routeRefType: RouteRefType = getOrCreateGlobalSingleton( () => Symbol('route-ref-type'), ); -export type RouteRef = { - readonly [routeRefType]: 'absolute'; - - params: ParamKeys; - - // 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 = { - readonly [routeRefType]: 'sub'; - - parent: RouteRef; - - path: string; - - params: ParamKeys; -}; - -export type ExternalRouteRef< - Params extends AnyParams = any, - Optional extends boolean = any -> = { - readonly [routeRefType]: 'external'; - - params: ParamKeys; - - optional?: Optional; -}; - export type AnyRouteRef = | RouteRef | SubRouteRef