backend-app-api: add support for adding module imports
Co-authored-by: Fredrik Adelöw <freben@gmail.com> Co-authored-by: Johan Haals <johan.haals@gmail.com> Co-authored-by: Camila Belo <camilaibs@gmail.com> Co-authored-by: Philipp Hugenroth <philipph@spotify.com> Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Add support for installing features as package imports, for example `backend.add(import('my-plugin'))`.
|
||||
@@ -46,6 +46,7 @@ export class BackendInitializer {
|
||||
#features = new Array<InternalBackendFeature>();
|
||||
#extensionPoints = new Map<string, { impl: unknown; pluginId: string }>();
|
||||
#serviceRegistry: ServiceRegistry;
|
||||
#registeredFeatures = new Array<Promise<BackendFeature>>();
|
||||
|
||||
constructor(defaultApiFactories: ServiceFactory[]) {
|
||||
this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]);
|
||||
@@ -90,11 +91,11 @@ export class BackendInitializer {
|
||||
return Object.fromEntries(result);
|
||||
}
|
||||
|
||||
add(feature: BackendFeature) {
|
||||
add(feature: Promise<BackendFeature>) {
|
||||
if (this.#startPromise) {
|
||||
throw new Error('feature can not be added after the backend has started');
|
||||
}
|
||||
this.#addFeature(feature);
|
||||
this.#registeredFeatures.push(feature);
|
||||
}
|
||||
|
||||
#addFeature(feature: BackendFeature) {
|
||||
@@ -150,6 +151,10 @@ export class BackendInitializer {
|
||||
async #doStart(): Promise<void> {
|
||||
this.#serviceRegistry.checkForCircularDeps();
|
||||
|
||||
for (const feature of this.#registeredFeatures) {
|
||||
this.#addFeature(await feature);
|
||||
}
|
||||
|
||||
const featureDiscovery = await this.#serviceRegistry.get(
|
||||
featureDiscoveryServiceRef,
|
||||
'root',
|
||||
|
||||
@@ -25,8 +25,17 @@ export class BackstageBackend implements Backend {
|
||||
this.#initializer = new BackendInitializer(defaultServiceFactories);
|
||||
}
|
||||
|
||||
add(feature: BackendFeature | (() => BackendFeature)): void {
|
||||
this.#initializer.add(typeof feature === 'function' ? feature() : feature);
|
||||
add(
|
||||
feature:
|
||||
| BackendFeature
|
||||
| (() => BackendFeature)
|
||||
| Promise<{ default: BackendFeature | (() => BackendFeature) }>,
|
||||
): void {
|
||||
if (isPromise(feature)) {
|
||||
this.#initializer.add(feature.then(f => unwrapFeature(f.default)));
|
||||
} else {
|
||||
this.#initializer.add(Promise.resolve(unwrapFeature(feature)));
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
@@ -37,3 +46,18 @@ export class BackstageBackend implements Backend {
|
||||
await this.#initializer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
function isPromise<T>(value: unknown | Promise<T>): value is Promise<T> {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'then' in value &&
|
||||
typeof value.then === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
function unwrapFeature(
|
||||
feature: BackendFeature | (() => BackendFeature),
|
||||
): BackendFeature {
|
||||
return typeof feature === 'function' ? feature() : feature;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,12 @@ import {
|
||||
* @public
|
||||
*/
|
||||
export interface Backend {
|
||||
add(feature: BackendFeature | (() => BackendFeature)): void;
|
||||
add(
|
||||
feature:
|
||||
| BackendFeature
|
||||
| (() => BackendFeature)
|
||||
| Promise<{ default: BackendFeature | (() => BackendFeature) }>,
|
||||
): void;
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -18,27 +18,19 @@ import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { createBackend } from './CreateBackend';
|
||||
|
||||
describe('createBackend', () => {
|
||||
it('should not throw when overriding a default service implementation', () => {
|
||||
it('should not throw when overriding a default service implementation', async () => {
|
||||
const backend = createBackend();
|
||||
|
||||
expect(() => {
|
||||
backend.add(
|
||||
createServiceFactory({
|
||||
service: coreServices.rootLifecycle,
|
||||
deps: {},
|
||||
factory: async () => ({
|
||||
addStartupHook: () => {},
|
||||
addShutdownHook: () => {},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}).not.toThrow();
|
||||
backend.add(mockServices.rootConfig.factory());
|
||||
|
||||
await expect(backend.start()).resolves.toBe(undefined);
|
||||
});
|
||||
|
||||
it('should throw on duplicate service implementations', () => {
|
||||
it('should throw on duplicate service implementations', async () => {
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(
|
||||
@@ -51,34 +43,34 @@ describe('createBackend', () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
backend.add(
|
||||
createServiceFactory({
|
||||
service: coreServices.rootLifecycle,
|
||||
deps: {},
|
||||
factory: async () => ({
|
||||
addStartupHook: () => {},
|
||||
addShutdownHook: () => {},
|
||||
}),
|
||||
backend.add(
|
||||
createServiceFactory({
|
||||
service: coreServices.rootLifecycle,
|
||||
deps: {},
|
||||
factory: async () => ({
|
||||
addStartupHook: () => {},
|
||||
addShutdownHook: () => {},
|
||||
}),
|
||||
);
|
||||
}).toThrow(
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(backend.start()).rejects.toThrow(
|
||||
'Duplicate service implementations provided for core.rootLifecycle',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when providing a plugin metadata service implementation', () => {
|
||||
it('should throw when providing a plugin metadata service implementation', async () => {
|
||||
const backend = createBackend();
|
||||
backend.add(
|
||||
createServiceFactory({
|
||||
service: coreServices.pluginMetadata,
|
||||
deps: {},
|
||||
factory: () => ({ getId: () => 'test' }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
backend.add(
|
||||
createServiceFactory({
|
||||
service: coreServices.pluginMetadata,
|
||||
deps: {},
|
||||
factory: async () => ({ getId: () => 'test' }),
|
||||
}),
|
||||
),
|
||||
).toThrow('The core.pluginMetadata service cannot be overridden');
|
||||
await expect(backend.start()).rejects.toThrow(
|
||||
'The core.pluginMetadata service cannot be overridden',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user