frontend-app-api: make app initialization async
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user