From 5d52e20a44f5b9d79e2af9fccd5dd6a34f948222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 7 Dec 2020 15:59:37 +0100 Subject: [PATCH] core-api: run bindRoutes in the app impl and inject provider --- packages/core-api/src/app/App.test.ts | 40 ++++++ packages/core-api/src/app/App.tsx | 115 +++++++++++++----- packages/core-api/src/app/types.ts | 2 +- packages/core/src/api-wrappers/createApp.tsx | 1 + .../test-utils/src/testUtils/appWrappers.tsx | 1 + 5 files changed, 130 insertions(+), 29 deletions(-) create mode 100644 packages/core-api/src/app/App.test.ts diff --git a/packages/core-api/src/app/App.test.ts b/packages/core-api/src/app/App.test.ts new file mode 100644 index 0000000000..a25256491b --- /dev/null +++ b/packages/core-api/src/app/App.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 '../routing/RouteRef'; +import { generateBoundRoutes } from './App'; + +describe('generateBoundRoutes', () => { + it('runs happy path', () => { + const external = { myRoute: createExternalRouteRef() }; + const ref = createRouteRef({ path: '', title: '' }); + const result = generateBoundRoutes(({ bind }) => { + bind(external, { myRoute: ref }); + }); + + expect(result.get(external.myRoute)).toBe(ref); + }); + + it('throws on unknown keys', () => { + const external = { myRoute: createExternalRouteRef() }; + const ref = createRouteRef({ path: '', title: '' }); + expect(() => + generateBoundRoutes(({ bind }) => { + bind(external, { someOtherRoute: ref } as any); + }), + ).toThrow('Key someOtherRoute is not an existing external route'); + }); +}); diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index b42278e4a6..b7265fb5ac 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -15,46 +15,82 @@ */ import React, { ComponentType, + PropsWithChildren, + ReactElement, useMemo, useState, - ReactElement, - PropsWithChildren, } from 'react'; -import { Route, Routes, Navigate } from 'react-router-dom'; -import { AppContextProvider } from './AppContext'; -import { - BackstageApp, - AppComponents, - AppConfigLoader, - SignInResult, - SignInPageProps, -} from './types'; -import { BackstagePlugin } from '../plugin'; -import { - featureFlagsApiRef, - AppThemeApi, - ConfigApi, - identityApiRef, -} from '../apis/definitions'; -import { AppThemeProvider } from './AppThemeProvider'; - -import { IconComponent, SystemIcons, SystemIconKey } from '../icons'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { useAsync } from 'react-use'; import { + AnyApiFactory, + ApiHolder, ApiProvider, ApiRegistry, AppTheme, - AppThemeSelector, appThemeApiRef, + AppThemeSelector, configApiRef, ConfigReader, - useApi, - AnyApiFactory, - ApiHolder, LocalStorageFeatureFlags, + useApi, } from '../apis'; -import { useAsync } from 'react-use'; +import { + AppThemeApi, + ConfigApi, + featureFlagsApiRef, + identityApiRef, +} from '../apis/definitions'; +import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; +import { + childDiscoverer, + routeElementDiscoverer, + traverseElementTree, +} from '../extensions/traversal'; +import { IconComponent, SystemIconKey, SystemIcons } from '../icons'; +import { BackstagePlugin } from '../plugin'; +import { RouteRef } from '../routing'; +import { + routeObjectCollector, + routeParentCollector, + routePathCollector, +} from '../routing/collectors'; +import { RoutingProvider } from '../routing/hooks'; +import { ExternalRouteRef } from '../routing/RouteRef'; +import { AppContextProvider } from './AppContext'; import { AppIdentity } from './AppIdentity'; -import { ApiResolver, ApiFactoryRegistry } from '../apis/system'; +import { AppThemeProvider } from './AppThemeProvider'; +import { + AppComponents, + AppConfigLoader, + AppOptions, + BackstageApp, + BindRouteFunc, + SignInPageProps, + SignInResult, +} from './types'; + +export function generateBoundRoutes( + bindRoutes: AppOptions['bindRoutes'], +): Map { + const result = new Map(); + + if (bindRoutes) { + const bind: BindRouteFunc = (externalRoutes, targetRoutes) => { + 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`); + } + + result.set(externalRoute, value); + } + }; + bindRoutes({ bind }); + } + + return result; +} type FullAppOptions = { apis: Iterable; @@ -64,6 +100,7 @@ type FullAppOptions = { themes: AppTheme[]; configLoader?: AppConfigLoader; defaultApis: Iterable; + bindRoutes?: AppOptions['bindRoutes']; }; function useConfigLoader( @@ -112,6 +149,7 @@ export class PrivateAppImpl implements BackstageApp { private readonly themes: AppTheme[]; private readonly configLoader?: AppConfigLoader; private readonly defaultApis: Iterable; + private readonly bindRoutes: AppOptions['bindRoutes']; private readonly identityApi = new AppIdentity(); @@ -123,6 +161,7 @@ export class PrivateAppImpl implements BackstageApp { this.themes = options.themes; this.configLoader = options.configLoader; this.defaultApis = options.defaultApis; + this.bindRoutes = options.bindRoutes; } getPlugins(): BackstagePlugin[] { @@ -202,6 +241,17 @@ export class PrivateAppImpl implements BackstageApp { [], ); + // Probably want to memo this? + const { routePaths, routeParents, routeObjects } = traverseElementTree({ + root: children, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routePaths: routePathCollector, + routeParents: routeParentCollector, + routeObjects: routeObjectCollector, + }, + }); + const loadedConfig = useConfigLoader( this.configLoader, this.components, @@ -218,7 +268,16 @@ export class PrivateAppImpl implements BackstageApp { return ( - {children} + + + {children} + + ); diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index d121a2e2a0..3eeac72a30 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -79,7 +79,7 @@ export type AppComponents = { */ export type AppConfigLoader = () => Promise; -type BindRouteFunc = ( +export type BindRouteFunc = ( externalRoutes: T, targetRoutes: { [key in keyof T]: RouteRef }, ) => void; diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index c99ba0ed03..9a8f7a36df 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -142,6 +142,7 @@ export function createApp(options?: AppOptions) { themes, configLoader, defaultApis, + bindRoutes: options?.bindRoutes, }); app.verify(); diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 22bc8ceee4..1f6c7622b8 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -80,6 +80,7 @@ export function wrapInTestApp( }, ], defaultApis: mockApis, + bindRoutes: () => {}, }); let Wrapper: ComponentType;