From d4400f5e276bb1c0bbc1bb5828c95d1ed91fbb54 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Oct 2023 16:26:38 +0200 Subject: [PATCH] frontend-app-api: lift over resolveRouteBindings and RoutingProvider Signed-off-by: Patrik Oldsberg --- .../src/routing/RoutingProvider.tsx | 66 +++++++++++ .../frontend-app-api/src/routing/index.ts | 17 +++ .../src/routing/resolveRouteBindings.test.ts | 43 ++++++++ .../src/routing/resolveRouteBindings.ts | 104 ++++++++++++++++++ .../frontend-app-api/src/wiring/createApp.tsx | 64 +---------- 5 files changed, 235 insertions(+), 59 deletions(-) create mode 100644 packages/frontend-app-api/src/routing/RoutingProvider.tsx create mode 100644 packages/frontend-app-api/src/routing/index.ts create mode 100644 packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts create mode 100644 packages/frontend-app-api/src/routing/resolveRouteBindings.ts diff --git a/packages/frontend-app-api/src/routing/RoutingProvider.tsx b/packages/frontend-app-api/src/routing/RoutingProvider.tsx new file mode 100644 index 0000000000..965faa3655 --- /dev/null +++ b/packages/frontend-app-api/src/routing/RoutingProvider.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactNode } from 'react'; +import { + ExternalRouteRef, + RouteRef, + SubRouteRef, +} from '@backstage/frontend-plugin-api'; +import { + createVersionedValueMap, + createVersionedContext, +} from '@backstage/version-bridge'; +import { RouteResolver } from './RouteResolver'; +import { BackstageRouteObject } from './types'; + +const RoutingContext = createVersionedContext<{ 1: RouteResolver }>( + 'routing-context', +); + +type ProviderProps = { + routePaths: Map; + routeParents: Map; + routeObjects: BackstageRouteObject[]; + routeBindings: Map; + basePath?: string; + children: ReactNode; +}; + +// TODO(Rugvip): Migrate to a routing API instead +export const RoutingProvider = ({ + routePaths, + routeParents, + routeObjects, + routeBindings, + basePath = '', + children, +}: ProviderProps) => { + const resolver = new RouteResolver( + routePaths, + routeParents, + routeObjects, + routeBindings, + basePath, + ); + + const versionedValue = createVersionedValueMap({ 1: resolver }); + return ( + + {children} + + ); +}; diff --git a/packages/frontend-app-api/src/routing/index.ts b/packages/frontend-app-api/src/routing/index.ts new file mode 100644 index 0000000000..8c26a732d3 --- /dev/null +++ b/packages/frontend-app-api/src/routing/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { type AppRouteBinder } from './resolveRouteBindings'; diff --git a/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts b/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts new file mode 100644 index 0000000000..9ea288c196 --- /dev/null +++ b/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createExternalRouteRef, + createRouteRef, +} from '@backstage/frontend-plugin-api'; +import { resolveRouteBindings } from './resolveRouteBindings'; + +describe('resolveRouteBindings', () => { + it('runs happy path', () => { + const external = { myRoute: createExternalRouteRef() }; + const ref = createRouteRef(); + const result = resolveRouteBindings(({ bind }) => { + bind(external, { myRoute: ref }); + }); + + expect(result.get(external.myRoute)).toBe(ref); + }); + + it('throws on unknown keys', () => { + const external = { myRoute: createExternalRouteRef() }; + const ref = createRouteRef(); + expect(() => + resolveRouteBindings(({ bind }) => { + bind(external, { someOtherRoute: ref } as any); + }), + ).toThrow('Key someOtherRoute is not an existing external route'); + }); +}); diff --git a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts new file mode 100644 index 0000000000..968da005ec --- /dev/null +++ b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts @@ -0,0 +1,104 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + RouteRef, + SubRouteRef, + ExternalRouteRef, +} from '@backstage/frontend-plugin-api'; + +/** + * Extracts a union of the keys in a map whose value extends the given type + * + * @ignore + */ +type KeysWithType = { + [key in keyof Obj]: Obj[key] extends Type ? key : never; +}[keyof Obj]; + +/** + * Takes a map Map required values and makes all keys matching Keys optional + * + * @ignore + */ +type PartialKeys< + Map extends { [name in string]: any }, + Keys extends keyof Map, +> = Partial> & Required>; + +/** + * Creates a map of target routes with matching parameters based on a map of external routes. + * + * @ignore + */ +type TargetRouteMap< + ExternalRoutes extends { [name: string]: ExternalRouteRef }, +> = { + [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef< + infer Params, + any + > + ? RouteRef | SubRouteRef + : never; +}; + +/** + * A function that can bind from external routes of a given plugin, to concrete + * routes of other plugins. See {@link createApp}. + * + * @public + */ +export type AppRouteBinder = < + TExternalRoutes extends { [name: string]: ExternalRouteRef }, +>( + externalRoutes: TExternalRoutes, + targetRoutes: PartialKeys< + TargetRouteMap, + KeysWithType> + >, +) => void; + +/** @internal */ +export function resolveRouteBindings( + bindRoutes?: (context: { bind: AppRouteBinder }) => void, +): Map { + const result = new Map(); + + if (bindRoutes) { + const bind: AppRouteBinder = ( + externalRoutes, + targetRoutes: { [name: string]: RouteRef | SubRouteRef }, + ) => { + for (const [key, value] of Object.entries(targetRoutes)) { + const externalRoute = externalRoutes[key]; + if (!externalRoute) { + throw new Error(`Key ${key} is not an existing external route`); + } + if (!value && !externalRoute.optional) { + throw new Error( + `External route ${key} is required but was undefined`, + ); + } + if (value) { + result.set(externalRoute, value); + } + } + }; + bindRoutes({ bind }); + } + + return result; +} diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index beb29eacff..4842383096 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -21,9 +21,7 @@ import { coreExtensionData, ExtensionDataRef, ExtensionOverrides, - ExternalRouteRef, RouteRef, - SubRouteRef, useRouteRef, } from '@backstage/frontend-plugin-api'; import { Core } from '../extensions/Core'; @@ -76,10 +74,6 @@ import { defaultConfigLoaderSync } from '../../../core-app-api/src/app/defaultCo // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { overrideBaseUrlConfigs } from '../../../core-app-api/src/app/overrideBaseUrlConfigs'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { RoutingProvider } from '../../../core-app-api/src/routing/RoutingProvider'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { resolveRouteBindings } from '../../../core-app-api/src/app/resolveRouteBindings'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppLanguageSelector } from '../../../core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi'; @@ -98,57 +92,9 @@ import { appLanguageApiRef, translationApiRef, } from '@backstage/core-plugin-api/alpha'; - -/** - * Extracts a union of the keys in a map whose value extends the given type - * - * @ignore - */ -type KeysWithType = { - [key in keyof Obj]: Obj[key] extends Type ? key : never; -}[keyof Obj]; - -/** - * Takes a map Map required values and makes all keys matching Keys optional - * - * @ignore - */ -type PartialKeys< - Map extends { [name in string]: any }, - Keys extends keyof Map, -> = Partial> & Required>; - -/** - * Creates a map of target routes with matching parameters based on a map of external routes. - * - * @ignore - */ -type TargetRouteMap< - ExternalRoutes extends { [name: string]: ExternalRouteRef }, -> = { - [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef< - infer Params, - any - > - ? RouteRef | SubRouteRef - : never; -}; - -/** - * A function that can bind from external routes of a given plugin, to concrete - * routes of other plugins. See {@link createApp}. - * - * @public - */ -export type AppRouteBinder = < - TExternalRoutes extends { [name: string]: ExternalRouteRef }, ->( - externalRoutes: TExternalRoutes, - targetRoutes: PartialKeys< - TargetRouteMap, - KeysWithType> - >, -) => void; +import { AppRouteBinder } from '../routing'; +import { RoutingProvider } from '../routing/RoutingProvider'; +import { resolveRouteBindings } from '../routing/resolveRouteBindings'; /** @public */ export interface ExtensionTreeNode { @@ -365,8 +311,8 @@ export function createApp(options: { {/* TODO: set base path using the logic from AppRouter */}