frontend-app-api: make app initialization async

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-09-22 12:23:51 +02:00
parent 8498c27ee3
commit 9d03dfe5e3
4 changed files with 70 additions and 45 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': minor
---
The `createApp` config option has been replaced by a new `configLoader` option. There is now also a `pluginLoader` option that can be used to dynamically load plugins into the app.
+2 -1
View File
@@ -12,7 +12,8 @@ import { JSX as JSX_2 } from 'react';
// @public (undocumented)
export function createApp(options: {
plugins: BackstagePlugin[];
config?: ConfigApi;
configLoader?: () => Promise<ConfigApi>;
pluginLoader?: (ctx: { config: ConfigApi }) => Promise<BackstagePlugin[]>;
}): {
createRoot(): JSX_2.Element;
};
@@ -247,50 +247,67 @@ export function createInstances(options: {
/** @public */
export function createApp(options: {
plugins: BackstagePlugin[];
config?: ConfigApi;
configLoader?: () => Promise<ConfigApi>;
pluginLoader?: (ctx: { config: ConfigApi }) => Promise<BackstagePlugin[]>;
}): {
createRoot(): JSX.Element;
} {
const discoveredPlugins = getAvailablePlugins();
const allPlugins = Array.from(
new Set([...discoveredPlugins, ...options.plugins]),
);
const appConfig =
options?.config ??
ConfigReader.fromConfigs(overrideBaseUrlConfigs(defaultConfigLoaderSync()));
async function appLoader() {
const config =
(await options?.configLoader?.()) ??
ConfigReader.fromConfigs(
overrideBaseUrlConfigs(defaultConfigLoaderSync()),
);
const { rootInstances } = createInstances({
plugins: allPlugins,
config: appConfig,
});
const discoveredPlugins = getAvailablePlugins();
const loadedPlugins = (await options.pluginLoader?.({ config })) ?? [];
const allPlugins = Array.from(
new Set([...discoveredPlugins, ...options.plugins, ...loadedPlugins]),
);
const routePaths = extractRouteInfoFromInstanceTree(rootInstances);
const { rootInstances } = createInstances({
plugins: allPlugins,
config,
});
const coreInstance = rootInstances.find(({ id }) => id === 'core');
if (!coreInstance) {
throw Error('Unable to find core extension instance');
const routePaths = extractRouteInfoFromInstanceTree(rootInstances);
const coreInstance = rootInstances.find(({ id }) => id === 'core');
if (!coreInstance) {
throw Error('Unable to find core extension instance');
}
const apiHolder = createApiHolder(coreInstance, config);
const appContext = createLegacyAppContext(allPlugins);
const rootElements = rootInstances
.map(e => e.getData(coreExtensionData.reactElement))
.filter((x): x is JSX.Element => !!x);
const App = () => (
<ApiProvider apis={apiHolder}>
<AppContextProvider appContext={appContext}>
<AppThemeProvider>
<RoutingProvider routePaths={routePaths}>
{/* TODO: set base path using the logic from AppRouter */}
<BrowserRouter>{rootElements}</BrowserRouter>
</RoutingProvider>
</AppThemeProvider>
</AppContextProvider>
</ApiProvider>
);
return { default: App };
}
const apiHolder = createApiHolder(coreInstance, appConfig);
const appContext = createLegacyAppContext(allPlugins);
return {
createRoot() {
const rootElements = rootInstances
.map(e => e.getData(coreExtensionData.reactElement))
.filter((x): x is JSX.Element => !!x);
const LazyApp = React.lazy(appLoader);
return (
<ApiProvider apis={apiHolder}>
<AppContextProvider appContext={appContext}>
<AppThemeProvider>
<RoutingProvider routePaths={routePaths}>
{/* TODO: set base path using the logic from AppRouter */}
<BrowserRouter>{rootElements}</BrowserRouter>
</RoutingProvider>
</AppThemeProvider>
</AppContextProvider>
</ApiProvider>
<React.Suspense fallback="Loading...">
<LazyApp />
</React.Suspense>
);
},
};
@@ -16,14 +16,14 @@
import React from 'react';
import { createApp } from '@backstage/frontend-app-api';
import { render, screen } from '@testing-library/react';
import { screen } from '@testing-library/react';
import { createSchemaFromZod } from '../schema/createSchemaFromZod';
import { createPlugin, BackstagePlugin } from './createPlugin';
import { JsonObject } from '@backstage/types';
import { createExtension } from './createExtension';
import { createExtensionDataRef } from './createExtensionDataRef';
import { coreExtensionData } from './coreExtensionData';
import { MockConfigApi } from '@backstage/test-utils';
import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
import { createExtensionInput } from './createExtensionInput';
const nameExtensionDataRef = createExtensionDataRef<string>('name');
@@ -112,7 +112,7 @@ function createTestAppRoot({
}) {
return createApp({
plugins: plugins,
config: new MockConfigApi(config),
configLoader: async () => new MockConfigApi(config),
}).createRoot();
}
@@ -123,24 +123,26 @@ describe('createPlugin', () => {
expect(plugin).toBeDefined();
});
it('should create a plugin with extension instances', () => {
it('should create a plugin with extension instances', async () => {
const plugin = createPlugin({
id: 'empty',
extensions: [TechRadarPage, CatalogPage, outputExtension],
});
expect(plugin).toBeDefined();
render(
await renderWithEffects(
createTestAppRoot({
plugins: [plugin],
config: { app: { extensions: [{ 'core.layout': false }] } },
}),
);
expect(screen.getByText('Names: TechRadar, Catalog')).toBeInTheDocument();
await expect(
screen.findByText('Names: TechRadar, Catalog'),
).resolves.toBeInTheDocument();
});
it('should create a plugin with nested extension instances', () => {
it('should create a plugin with nested extension instances', async () => {
const plugin = createPlugin({
id: 'empty',
extensions: [
@@ -153,7 +155,7 @@ describe('createPlugin', () => {
});
expect(plugin).toBeDefined();
render(
await renderWithEffects(
createTestAppRoot({
plugins: [plugin],
config: {
@@ -171,10 +173,10 @@ describe('createPlugin', () => {
}),
);
expect(
screen.getByText(
await expect(
screen.findByText(
'Names: TechRadar, CatalogRenamed, TechDocs-TechDocsAddon',
),
).toBeInTheDocument();
).resolves.toBeInTheDocument();
});
});