make the loader into a proper interface

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2023-12-14 15:59:25 +01:00
parent 8f5b7a0a62
commit 51754bca19
3 changed files with 59 additions and 32 deletions
+6 -3
View File
@@ -24,9 +24,12 @@ export function createApp(options?: {
};
// @public
export type CreateAppFeatureLoader = (options: {
config: ConfigApi;
}) => Promise<FrontendFeature[]>;
export interface CreateAppFeatureLoader {
getLoaderName(): string;
load(options: { config: ConfigApi }): Promise<{
features: FrontendFeature[];
}>;
}
// @public
export type CreateAppRouteBinder = <
@@ -25,7 +25,7 @@ import {
createThemeExtension,
} from '@backstage/frontend-plugin-api';
import { screen, waitFor } from '@testing-library/react';
import { createApp } from './createApp';
import { CreateAppFeatureLoader, createApp } from './createApp';
import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
import React from 'react';
import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api';
@@ -100,23 +100,32 @@ describe('createApp', () => {
});
it('should support feature loaders', async () => {
const loader: CreateAppFeatureLoader = {
getLoaderName() {
return 'test-loader';
},
async load({ config }) {
return {
features: [
createPlugin({
id: 'test',
extensions: [
createPageExtension({
defaultPath: '/',
loader: async () => <div>{config.getString('key')}</div>,
}),
],
}),
],
};
},
};
const app = createApp({
configLoader: async () => ({
config: new MockConfigApi({ key: 'config-value' }),
}),
features: [
async ({ config }) => [
createPlugin({
id: 'test',
extensions: [
createPageExtension({
defaultPath: '/',
loader: async () => <div>{config.getString('key')}</div>,
}),
],
}),
],
],
features: [loader],
});
await renderWithEffects(app.createRoot());
@@ -127,21 +136,26 @@ describe('createApp', () => {
});
it('should propagate errors thrown by feature loaders', async () => {
const loader: CreateAppFeatureLoader = {
getLoaderName() {
return 'test-loader';
},
async load() {
throw new TypeError('boom');
},
};
const app = createApp({
configLoader: async () => ({
config: new MockConfigApi({}),
}),
features: [
async () => {
throw new TypeError('boom');
},
],
features: [loader],
});
await expect(
renderWithEffects(app.createRoot()),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to read frontend features from loader, TypeError: boom"`,
`"Failed to read frontend features from loader 'test-loader', TypeError: boom"`,
);
});
@@ -244,9 +244,19 @@ function deduplicateFeatures(
*
* @public
*/
export type CreateAppFeatureLoader = (options: {
config: ConfigApi;
}) => Promise<FrontendFeature[]>;
export interface CreateAppFeatureLoader {
/**
* Returns name of this loader. suitable for showing to users.
*/
getLoaderName(): string;
/**
* Loads a number of features dynamically.
*/
load(options: { config: ConfigApi }): Promise<{
features: FrontendFeature[];
}>;
}
/** @public */
export function createApp(options?: {
@@ -266,20 +276,20 @@ export function createApp(options?: {
const discoveredFeatures = getAvailableFeatures(config);
const providedFeatures: FrontendFeature[] = [];
for (const feature of options?.features ?? []) {
if (typeof feature === 'function') {
for (const entry of options?.features ?? []) {
if ('load' in entry) {
try {
const loadedFeatures = await feature({ config });
providedFeatures.push(...loadedFeatures);
const result = await entry.load({ config });
providedFeatures.push(...result.features);
} catch (e) {
throw new Error(
`Failed to read frontend features from loader, ${stringifyError(
`Failed to read frontend features from loader '${entry.getLoaderName()}', ${stringifyError(
e,
)}`,
);
}
} else {
providedFeatures.push(feature);
providedFeatures.push(entry);
}
}