Merge pull request #16228 from backstage/rugvip/opaque

backend-plugin-api: switch BackendFeature to be an opaque type
This commit is contained in:
Patrik Oldsberg
2023-02-07 14:21:06 +01:00
committed by GitHub
10 changed files with 258 additions and 132 deletions
@@ -19,6 +19,7 @@ import {
createBackendPlugin,
createExtensionPoint,
} from './factories';
import { InternalBackendFeature } from './types';
describe('createExtensionPoint', () => {
it('should create an ExtensionPoint', () => {
@@ -34,11 +35,30 @@ describe('createBackendPlugin', () => {
it('should create a BackendPlugin', () => {
const plugin = createBackendPlugin((_options: { a: string }) => ({
pluginId: 'x',
register() {},
register(r) {
r.registerInit({ deps: {}, async init() {} });
},
}));
expect(plugin).toBeDefined();
expect(plugin({ a: 'a' })).toBeDefined();
expect(plugin({ a: 'a' }).id).toBe('x');
expect(plugin({ a: 'a' })).toEqual({
$$type: '@backstage/BackendFeature',
version: 'v1',
getRegistrations: expect.any(Function),
});
expect(
(plugin({ a: 'a' }) as InternalBackendFeature).getRegistrations(),
).toEqual([
{
type: 'plugin',
pluginId: 'x',
extensionPoints: [],
init: {
deps: expect.any(Object),
func: expect.any(Function),
},
},
]);
// @ts-expect-error
expect(plugin()).toBeDefined();
// @ts-expect-error
@@ -79,7 +99,11 @@ describe('createBackendPlugin', () => {
}));
expect(plugin).toBeDefined();
expect(plugin({ a: 'a' })).toBeDefined();
expect(plugin({ a: 'a' }).id).toBe('x');
expect(plugin({ a: 'a' })).toEqual({
$$type: '@backstage/BackendFeature',
version: 'v1',
getRegistrations: expect.any(Function),
});
// @ts-expect-error
expect(plugin()).toBeDefined();
// @ts-expect-error
@@ -107,11 +131,30 @@ describe('createBackendModule', () => {
const mod = createBackendModule((_options: { a: string }) => ({
pluginId: 'x',
moduleId: 'y',
register() {},
register(r) {
r.registerInit({ deps: {}, async init() {} });
},
}));
expect(mod).toBeDefined();
expect(mod({ a: 'a' })).toBeDefined();
expect(mod({ a: 'a' }).id).toBe('x.y');
expect(mod({ a: 'a' })).toEqual({
$$type: '@backstage/BackendFeature',
version: 'v1',
getRegistrations: expect.any(Function),
});
expect(
(mod({ a: 'a' }) as InternalBackendFeature).getRegistrations(),
).toEqual([
{
type: 'module',
pluginId: 'x',
moduleId: 'y',
init: {
deps: expect.any(Object),
func: expect.any(Function),
},
},
]);
// @ts-expect-error
expect(mod()).toBeDefined();
// @ts-expect-error
@@ -155,7 +198,11 @@ describe('createBackendModule', () => {
}));
expect(mod).toBeDefined();
expect(mod({ a: 'a' })).toBeDefined();
expect(mod({ a: 'a' }).id).toBe('x.y');
expect(mod({ a: 'a' })).toEqual({
$$type: '@backstage/BackendFeature',
version: 'v1',
getRegistrations: expect.any(Function),
});
// @ts-expect-error
expect(mod()).toBeDefined();
// @ts-expect-error
@@ -17,9 +17,11 @@
import {
BackendModuleRegistrationPoints,
BackendPluginRegistrationPoints,
BackendRegistrationPoints,
BackendFeature,
ExtensionPoint,
InternalBackendFeature,
InternalBackendModuleRegistration,
InternalBackendPluginRegistration,
} from './types';
/**
@@ -86,19 +88,62 @@ export interface BackendPluginConfig {
export function createBackendPlugin<TOptions extends [options?: object] = []>(
config: BackendPluginConfig | ((...params: TOptions) => BackendPluginConfig),
): (...params: TOptions) => BackendFeature {
if (typeof config === 'function') {
return (...options: TOptions) => {
const c = config(...options);
return {
...c,
id: c.pluginId,
};
const configCallback = typeof config === 'function' ? config : () => config;
return (...options: TOptions): InternalBackendFeature => {
const c = configCallback(...options);
let registrations: InternalBackendPluginRegistration[];
return {
$$type: '@backstage/BackendFeature',
version: 'v1',
getRegistrations() {
if (registrations) {
return registrations;
}
const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] =
[];
let init: InternalBackendPluginRegistration['init'] | undefined =
undefined;
c.register({
registerExtensionPoint(ext, impl) {
if (init) {
throw new Error(
'registerExtensionPoint called after registerInit',
);
}
extensionPoints.push([ext, impl]);
},
registerInit(regInit) {
if (init) {
throw new Error('registerInit must only be called once');
}
init = {
deps: regInit.deps,
func: regInit.init,
};
},
});
if (!init) {
throw new Error(
`registerInit was not called by register in ${c.pluginId}`,
);
}
registrations = [
{
type: 'plugin',
pluginId: c.pluginId,
extensionPoints,
init,
},
];
return registrations;
},
};
}
return () => ({
...config,
id: config.pluginId,
});
};
}
/**
@@ -132,21 +177,50 @@ export interface BackendModuleConfig {
export function createBackendModule<TOptions extends [options?: object] = []>(
config: BackendModuleConfig | ((...params: TOptions) => BackendModuleConfig),
): (...params: TOptions) => BackendFeature {
if (typeof config === 'function') {
return (...options: TOptions) => {
const c = config(...options);
return {
id: `${c.pluginId}.${c.moduleId}`,
register: c.register,
};
const configCallback = typeof config === 'function' ? config : () => config;
return (...options: TOptions): InternalBackendFeature => {
const c = configCallback(...options);
let registrations: InternalBackendModuleRegistration[];
return {
$$type: '@backstage/BackendFeature',
version: 'v1',
getRegistrations() {
if (registrations) {
return registrations;
}
let init: InternalBackendModuleRegistration['init'] | undefined =
undefined;
c.register({
registerInit(regInit) {
if (init) {
throw new Error('registerInit must only be called once');
}
init = {
deps: regInit.deps,
func: regInit.init,
};
},
});
if (!init) {
throw new Error(
`registerInit was not called by register in ${c.moduleId} module for ${c.pluginId}`,
);
}
registrations = [
{
type: 'module',
pluginId: c.pluginId,
moduleId: c.moduleId,
init,
},
];
return registrations;
},
};
}
return () => ({
id: `${config.pluginId}.${config.moduleId}`,
register(register: BackendRegistrationPoints) {
return config.register({
registerInit: register.registerInit.bind(register),
});
},
});
};
}
@@ -32,7 +32,6 @@ export {
export type {
BackendModuleRegistrationPoints,
BackendPluginRegistrationPoints,
BackendRegistrationPoints,
BackendFeature,
ExtensionPoint,
} from './types';
+32 -23
View File
@@ -35,26 +35,6 @@ export type ExtensionPoint<T> = {
$$type: '@backstage/ExtensionPoint';
};
/**
* The callbacks passed to the `register` method of a backend feature; this is
* essentially a superset of {@link BackendPluginRegistrationPoints} and
* {@link BackendModuleRegistrationPoints}.
*
* @public
*/
export interface BackendRegistrationPoints {
registerExtensionPoint<TExtensionPoint>(
ref: ExtensionPoint<TExtensionPoint>,
impl: TExtensionPoint,
): void;
registerInit<Deps extends { [name in string]: unknown }>(options: {
deps: {
[name in keyof Deps]: ServiceRef<Deps[name]> | ExtensionPoint<Deps[name]>;
};
init(deps: Deps): Promise<void>;
}): void;
}
/**
* The callbacks passed to the `register` method of a backend plugin.
*
@@ -89,7 +69,36 @@ export interface BackendModuleRegistrationPoints {
/** @public */
export interface BackendFeature {
// TODO(Rugvip): Try to get rid of the ID at this level, allowing for a feature to register multiple features as a bundle
id: string;
register(reg: BackendRegistrationPoints): void;
// NOTE: This type is opaque in order to simplify future API evolution.
$$type: '@backstage/BackendFeature';
}
/** @internal */
export interface InternalBackendFeature extends BackendFeature {
version: 'v1';
getRegistrations(): Array<
InternalBackendPluginRegistration | InternalBackendModuleRegistration
>;
}
/** @internal */
export interface InternalBackendPluginRegistration {
pluginId: string;
type: 'plugin';
extensionPoints: Array<readonly [ExtensionPoint<unknown>, unknown]>;
init: {
deps: Record<string, ServiceRef<unknown>>;
func(deps: Record<string, unknown>): Promise<void>;
};
}
/** @internal */
export interface InternalBackendModuleRegistration {
pluginId: string;
moduleId: string;
type: 'module';
init: {
deps: Record<string, ServiceRef<unknown> | ExtensionPoint<unknown>>;
func(deps: Record<string, unknown>): Promise<void>;
};
}