backend-plugin-api: make BackendFeature opaque

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-02-07 13:11:24 +01:00
parent 9a3063377a
commit 610d65e143
10 changed files with 250 additions and 109 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
@@ -19,6 +19,9 @@ import {
BackendPluginRegistrationPoints,
BackendFeature,
ExtensionPoint,
InternalBackendFeature,
InternalBackendModuleRegistration,
InternalBackendPluginRegistration,
} from './types';
/**
@@ -86,11 +89,59 @@ export function createBackendPlugin<TOptions extends [options?: object] = []>(
config: BackendPluginConfig | ((...params: TOptions) => BackendPluginConfig),
): (...params: TOptions) => BackendFeature {
const configCallback = typeof config === 'function' ? config : () => config;
return (...options: TOptions) => {
return (...options: TOptions): InternalBackendFeature => {
const c = configCallback(...options);
let registrations: InternalBackendPluginRegistration[];
return {
...c,
id: c.pluginId,
$$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;
},
};
};
}
@@ -127,11 +178,49 @@ export function createBackendModule<TOptions extends [options?: object] = []>(
config: BackendModuleConfig | ((...params: TOptions) => BackendModuleConfig),
): (...params: TOptions) => BackendFeature {
const configCallback = typeof config === 'function' ? config : () => config;
return (...options: TOptions) => {
return (...options: TOptions): InternalBackendFeature => {
const c = configCallback(...options);
let registrations: InternalBackendModuleRegistration[];
return {
id: `${c.pluginId}.${c.moduleId}`,
register: c.register,
$$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;
},
};
};
}
@@ -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>;
};
}