diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 920fe1a759..467145acfc 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -35,31 +35,9 @@ import { createRoutableExtension, analyticsApiRef, } from '@backstage/core-plugin-api'; -import { generateBoundRoutes, AppManager } from './AppManager'; +import { AppManager } from './AppManager'; import { AppComponents, AppIcons } from './types'; -describe('generateBoundRoutes', () => { - it('runs happy path', () => { - const external = { myRoute: createExternalRouteRef({ id: '1' }) }; - const ref = createRouteRef({ id: 'ref-1' }); - const result = generateBoundRoutes(({ bind }) => { - bind(external, { myRoute: ref }); - }); - - expect(result.get(external.myRoute)).toBe(ref); - }); - - it('throws on unknown keys', () => { - const external = { myRoute: createExternalRouteRef({ id: '2' }) }; - const ref = createRouteRef({ id: 'ref-2' }); - expect(() => - generateBoundRoutes(({ bind }) => { - bind(external, { someOtherRoute: ref } as any); - }), - ).toThrow('Key someOtherRoute is not an existing external route'); - }); -}); - describe('Integration Test', () => { const noOpAnalyticsApi = createApiFactory( analyticsApiRef, diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 820b4f5c11..0262ece8f4 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -45,9 +45,6 @@ import { IdentityApi, identityApiRef, BackstagePlugin, - RouteRef, - SubRouteRef, - ExternalRouteRef, } from '@backstage/core-plugin-api'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { @@ -75,13 +72,13 @@ import { AppConfigLoader, AppContext, AppOptions, - AppRouteBinder, BackstageApp, SignInPageProps, } from './types'; import { AppThemeProvider } from './AppThemeProvider'; import { defaultConfigLoader } from './defaultConfigLoader'; import { ApiRegistry } from '../apis/system/ApiRegistry'; +import { resolveRouteBindings } from './resolveRouteBindings'; type CompatiblePlugin = | BackstagePlugin @@ -89,35 +86,6 @@ type CompatiblePlugin = output(): Array<{ type: 'feature-flag'; name: string }>; }); -export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) { - 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; -} - /** * Get the app base path from the configured app baseUrl. * @@ -196,7 +164,7 @@ export class AppManager implements BackstageApp { private readonly themes: AppTheme[]; private readonly configLoader?: AppConfigLoader; private readonly defaultApis: Iterable; - private readonly routeBindings: Map; + private readonly bindRoutes: AppOptions['bindRoutes']; private readonly appIdentityProxy = new AppIdentityProxy(); private readonly apiFactoryRegistry: ApiFactoryRegistry; @@ -209,7 +177,7 @@ export class AppManager implements BackstageApp { this.themes = options.themes as AppTheme[]; this.configLoader = options.configLoader ?? defaultConfigLoader; this.defaultApis = options.defaultApis ?? []; - this.routeBindings = generateBoundRoutes(options.bindRoutes); + this.bindRoutes = options.bindRoutes; this.apiFactoryRegistry = new ApiFactoryRegistry(); } @@ -237,37 +205,45 @@ export class AppManager implements BackstageApp { [], ); - const { routePaths, routeParents, routeObjects, featureFlags } = - useMemo(() => { - const result = traverseElementTree({ - root: children, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routePaths: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, - collectedPlugins: pluginCollector, - featureFlags: featureFlagCollector, - }, - }); + const { + routePaths, + routeParents, + routeObjects, + featureFlags, + routeBindings, + } = useMemo(() => { + const result = traverseElementTree({ + root: children, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routePaths: routePathCollector, + routeParents: routeParentCollector, + routeObjects: routeObjectCollector, + collectedPlugins: pluginCollector, + featureFlags: featureFlagCollector, + }, + }); - // TODO(Rugvip): Restructure the public API so that we can get an immediate view of - // the app, rather than having to wait for the provider to render. - // For now we need to push the additional plugins we find during - // collection and then make sure we initialize things afterwards. - result.collectedPlugins.forEach(plugin => this.plugins.add(plugin)); - this.verifyPlugins(this.plugins); + // TODO(Rugvip): Restructure the public API so that we can get an immediate view of + // the app, rather than having to wait for the provider to render. + // For now we need to push the additional plugins we find during + // collection and then make sure we initialize things afterwards. + result.collectedPlugins.forEach(plugin => this.plugins.add(plugin)); + this.verifyPlugins(this.plugins); - // Initialize APIs once all plugins are available - this.getApiHolder(); - return result; - }, [children]); + // Initialize APIs once all plugins are available + this.getApiHolder(); + return { + ...result, + routeBindings: resolveRouteBindings(this.bindRoutes), + }; + }, [children]); if (!routesHaveBeenValidated) { routesHaveBeenValidated = true; validateRouteParameters(routePaths, routeParents); validateRouteBindings( - this.routeBindings, + routeBindings, this.plugins as Iterable>, ); } @@ -331,7 +307,7 @@ export class AppManager implements BackstageApp { routePaths={routePaths} routeParents={routeParents} routeObjects={routeObjects} - routeBindings={this.routeBindings} + routeBindings={routeBindings} basePath={getBasePath(loadedConfig.api)} > {children} diff --git a/packages/core-app-api/src/app/resolveRouteBindings.test.ts b/packages/core-app-api/src/app/resolveRouteBindings.test.ts new file mode 100644 index 0000000000..5c9fde53b7 --- /dev/null +++ b/packages/core-app-api/src/app/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/core-plugin-api'; +import { resolveRouteBindings } from './resolveRouteBindings'; + +describe('resolveRouteBindings', () => { + it('runs happy path', () => { + const external = { myRoute: createExternalRouteRef({ id: '1' }) }; + const ref = createRouteRef({ id: 'ref-1' }); + const result = resolveRouteBindings(({ bind }) => { + bind(external, { myRoute: ref }); + }); + + expect(result.get(external.myRoute)).toBe(ref); + }); + + it('throws on unknown keys', () => { + const external = { myRoute: createExternalRouteRef({ id: '2' }) }; + const ref = createRouteRef({ id: 'ref-2' }); + expect(() => + resolveRouteBindings(({ bind }) => { + bind(external, { someOtherRoute: ref } as any); + }), + ).toThrow('Key someOtherRoute is not an existing external route'); + }); +}); diff --git a/packages/core-app-api/src/app/resolveRouteBindings.ts b/packages/core-app-api/src/app/resolveRouteBindings.ts new file mode 100644 index 0000000000..63119b204b --- /dev/null +++ b/packages/core-app-api/src/app/resolveRouteBindings.ts @@ -0,0 +1,51 @@ +/* + * 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/core-plugin-api'; +import { AppOptions, AppRouteBinder } from './types'; + +export function resolveRouteBindings(bindRoutes: AppOptions['bindRoutes']) { + 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; +}