backend-*-api: refactor for more explicit internal feature types

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-08-05 12:16:19 +02:00
parent dc0d284419
commit d4d048b3ff
5 changed files with 96 additions and 47 deletions
@@ -26,7 +26,13 @@ import {
import { ServiceOrExtensionPoint } from './types';
// Direct internal import to avoid duplication
// eslint-disable-next-line @backstage/no-forbidden-package-imports
import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types';
import type {
InternalBackendFeature,
InternalBackendFeatureLoader,
InternalBackendRegistrations,
} from '@backstage/backend-plugin-api/src/wiring/types';
// eslint-disable-next-line @backstage/no-forbidden-package-imports
import type { InternalServiceFactory } from '@backstage/backend-plugin-api/src/services/system/types';
import { ForwardedError, ConflictError } from '@backstage/errors';
import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha';
import { DependencyGraph } from '../lib/DependencyGraph';
@@ -44,7 +50,7 @@ export interface BackendRegisterInit {
export class BackendInitializer {
#startPromise?: Promise<void>;
#features = new Array<InternalBackendFeature>();
#registrations = new Array<InternalBackendRegistrations>();
#extensionPoints = new Map<string, { impl: unknown; pluginId: string }>();
#serviceRegistry: ServiceRegistry;
#registeredFeatures = new Array<Promise<BackendFeature>>();
@@ -101,21 +107,15 @@ export class BackendInitializer {
}
#addFeature(feature: BackendFeature) {
if (feature.$$type !== '@backstage/BackendFeature') {
throw new Error(
`Failed to add feature, invalid type '${feature.$$type}'`,
);
}
if (isServiceFactory(feature)) {
this.#serviceRegistry.add(feature);
} else if (isInternalBackendFeature(feature)) {
} else if (isBackendRegistrations(feature)) {
if (feature.version !== 'v1') {
throw new Error(
`Failed to add feature, invalid version '${feature.version}'`,
);
}
this.#features.push(feature);
this.#registrations.push(feature);
} else {
throw new Error(
`Failed to add feature, invalid feature ${JSON.stringify(feature)}`,
@@ -176,8 +176,8 @@ export class BackendInitializer {
const pluginInits = new Map<string, BackendRegisterInit>();
const moduleInits = new Map<string, Map<string, BackendRegisterInit>>();
// Enumerate all features
for (const feature of this.#features) {
// Enumerate all registrations
for (const feature of this.#registrations) {
for (const r of feature.getRegistrations()) {
const provides = new Set<ExtensionPoint<unknown>>();
@@ -205,7 +205,7 @@ export class BackendInitializer {
consumes: new Set(Object.values(r.init.deps)),
init: r.init,
});
} else {
} else if (r.type === 'module') {
let modules = moduleInits.get(r.pluginId);
if (!modules) {
modules = new Map();
@@ -221,6 +221,8 @@ export class BackendInitializer {
consumes: new Set(Object.values(r.init.deps)),
init: r.init,
});
} else {
throw new Error(`Invalid registration type '${(r as any).type}'`);
}
}
}
@@ -385,14 +387,45 @@ export class BackendInitializer {
}
}
function isServiceFactory(feature: BackendFeature): feature is ServiceFactory {
return !!(feature as ServiceFactory).service;
function toInternalBackendFeature(
feature: BackendFeature,
): InternalBackendFeature {
if (feature.$$type !== '@backstage/BackendFeature') {
throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`);
}
const internal = feature as InternalBackendFeature;
if (internal.version !== 'v1') {
throw new Error(
`Invalid BackendFeature, bad version '${internal.version}'`,
);
}
return internal;
}
function isInternalBackendFeature(
function isServiceFactory(
feature: BackendFeature,
): feature is InternalBackendFeature {
return (
typeof (feature as InternalBackendFeature).getRegistrations === 'function'
);
): feature is InternalServiceFactory {
const internal = toInternalBackendFeature(feature);
if (internal.featureType === 'service') {
return true;
}
// Backwards compatibility for v1 registrations that use duck typing
if ('service' in internal) {
return true;
}
return false;
}
function isBackendRegistrations(
feature: BackendFeature,
): feature is InternalBackendRegistrations {
const internal = toInternalBackendFeature(feature);
if (internal.featureType === 'registrations') {
return true;
}
// Backwards compatibility for v1 registrations that use duck typing
if ('getRegistrations' in internal) {
return true;
}
return false;
}
@@ -15,7 +15,7 @@
*/
import { createBackendModule } from './createBackendModule';
import { InternalBackendFeature } from './types';
import { InternalBackendRegistrations } from './types';
describe('createBackendModule', () => {
it('should create a BackendModule', () => {
@@ -28,13 +28,13 @@ describe('createBackendModule', () => {
});
// legacy form
const legacy = result() as unknown as InternalBackendFeature;
const legacy = result() as unknown as InternalBackendRegistrations;
expect(legacy.$$type).toEqual('@backstage/BackendFeature');
expect(legacy.version).toEqual('v1');
expect(legacy.getRegistrations).toEqual(expect.any(Function));
// new form
const module = result as unknown as InternalBackendFeature;
const module = result as unknown as InternalBackendRegistrations;
expect(module.$$type).toEqual('@backstage/BackendFeature');
expect(module.version).toEqual('v1');
expect(module.getRegistrations).toEqual(expect.any(Function));
@@ -15,7 +15,7 @@
*/
import { createBackendPlugin } from './createBackendPlugin';
import { InternalBackendFeature } from './types';
import { InternalBackendRegistrations } from './types';
describe('createBackendPlugin', () => {
it('should create a BackendPlugin', () => {
@@ -27,13 +27,13 @@ describe('createBackendPlugin', () => {
});
// legacy form
const legacy = result() as unknown as InternalBackendFeature;
const legacy = result() as unknown as InternalBackendRegistrations;
expect(legacy.$$type).toEqual('@backstage/BackendFeature');
expect(legacy.version).toEqual('v1');
expect(legacy.getRegistrations).toEqual(expect.any(Function));
// new form
const plugin = result as unknown as InternalBackendFeature;
const plugin = result as unknown as InternalBackendRegistrations;
expect(plugin.$$type).toEqual('@backstage/BackendFeature');
expect(plugin.version).toEqual('v1');
expect(plugin.getRegistrations).toEqual(expect.any(Function));
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ServiceRef } from '../services/system/types';
import { InternalServiceFactory, ServiceRef } from '../services/system/types';
import { BackendFeature } from '../types';
/**
@@ -73,7 +73,7 @@ export interface BackendModuleRegistrationPoints {
}
/** @internal */
export interface InternalBackendFeature extends BackendFeature {
export interface InternalBackendRegistrations extends BackendFeature {
version: 'v1';
featureType: 'registrations';
getRegistrations(): Array<
@@ -114,3 +114,9 @@ export interface InternalBackendFeatureLoader extends BackendFeature {
deps: Record<string, ServiceRef<unknown>>;
loader(deps: Record<string, unknown>): Promise<BackendFeature[]>;
}
/** @internal */
export type InternalBackendFeature =
| InternalBackendRegistrations
| InternalBackendFeatureLoader
| InternalServiceFactory;
@@ -36,7 +36,10 @@ import { ConfigReader } from '@backstage/config';
import express from 'express';
// Direct internal import to avoid duplication
// eslint-disable-next-line @backstage/no-forbidden-package-imports
import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types';
import {
InternalBackendFeature,
InternalBackendRegistrations,
} from '@backstage/backend-plugin-api/src/wiring/types';
import { createHealthRouter } from '@backstage/backend-defaults/rootHttpRouter';
/** @public */
@@ -94,7 +97,7 @@ function createPluginsForOrphanModules(features: Array<BackendFeature>) {
const modulePluginIds = new Set<string>();
for (const feature of features) {
if (isInternalBackendFeature(feature)) {
if (isInternalBackendRegistrations(feature)) {
const registrations = feature.getRegistrations();
for (const registration of registrations) {
if (registration.type === 'plugin') {
@@ -137,18 +140,7 @@ function createExtensionPointTestModules(
}
const registrations = features.flatMap(feature => {
if (feature.$$type !== '@backstage/BackendFeature') {
throw new Error(
`Failed to add feature, invalid type '${feature.$$type}'`,
);
}
if (isInternalBackendFeature(feature)) {
if (feature.version !== 'v1') {
throw new Error(
`Failed to add feature, invalid version '${feature.version}'`,
);
}
if (isInternalBackendRegistrations(feature)) {
return feature.getRegistrations();
}
return [];
@@ -361,10 +353,28 @@ function registerTestHooks() {
registerTestHooks();
function isInternalBackendFeature(
function toInternalBackendFeature(
feature: BackendFeature,
): feature is InternalBackendFeature {
return (
typeof (feature as InternalBackendFeature).getRegistrations === 'function'
);
): InternalBackendFeature {
if (feature.$$type !== '@backstage/BackendFeature') {
throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`);
}
const internal = feature as InternalBackendFeature;
if (internal.version !== 'v1') {
throw new Error(
`Invalid BackendFeature, bad version '${internal.version}'`,
);
}
return internal;
}
function isInternalBackendRegistrations(
feature: BackendFeature,
): feature is InternalBackendRegistrations {
const internal = toInternalBackendFeature(feature);
if (internal.featureType === 'registrations') {
return true;
}
// Backwards compatibility for v1 registrations that use duck typing
return 'getRegistrations' in internal;
}