core-app-api: moved out and delay the route binding logic

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-01-19 10:00:08 +01:00
parent 3d87019269
commit 319ca2b20a
4 changed files with 131 additions and 83 deletions
@@ -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,
+36 -60
View File
@@ -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<any, any>
@@ -89,35 +86,6 @@ type CompatiblePlugin =
output(): Array<{ type: 'feature-flag'; name: string }>;
});
export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) {
const result = new Map<ExternalRouteRef, RouteRef | SubRouteRef>();
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<AnyApiFactory>;
private readonly routeBindings: Map<ExternalRouteRef, RouteRef | SubRouteRef>;
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<BackstagePlugin<any, any>>,
);
}
@@ -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}
@@ -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');
});
});
@@ -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<ExternalRouteRef, RouteRef | SubRouteRef>();
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;
}