Merge pull request #16228 from backstage/rugvip/opaque
backend-plugin-api: switch BackendFeature to be an opaque type
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user