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:
Patrik Oldsberg
2023-09-06 16:01:41 +02:00
parent 4d5eeec52d
commit 3b30b179cb
5 changed files with 72 additions and 41 deletions
+5
View File
@@ -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;
}
+6 -1
View File
@@ -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',
);
});
});