backend-plugin-api: fix type inference of interface options

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-09-26 18:49:05 +02:00
parent 69af72171d
commit 28377dc89f
6 changed files with 222 additions and 24 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-plugin-api': patch
---
Allow interfaces to be used for inferred option types.
+7 -18
View File
@@ -73,11 +73,7 @@ export const configServiceRef: ServiceRef<Config, 'root'>;
// @public (undocumented)
export function createBackendModule<
TOptions extends
| {
[name: string]: unknown;
}
| undefined = undefined,
TOptions extends object | undefined = undefined,
>(
config: BackendModuleConfig<TOptions>,
): undefined extends TOptions
@@ -86,14 +82,11 @@ export function createBackendModule<
// @public (undocumented)
export function createBackendPlugin<
TOptions extends
| {
[name: string]: unknown;
}
| undefined = undefined,
>(
config: BackendPluginConfig<TOptions>,
): undefined extends TOptions
TOptions extends object | undefined = undefined,
>(config: {
id: string;
register(reg: BackendRegistrationPoints, options: TOptions): void;
}): undefined extends TOptions
? (options?: TOptions) => BackendFeature
: (options: TOptions) => BackendFeature;
@@ -110,11 +103,7 @@ export function createServiceFactory<
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
TOpts extends
| {
[name in string]: unknown;
}
| undefined = undefined,
TOpts extends object | undefined = undefined,
>(config: {
service: ServiceRef<TService, TScope>;
deps: TDeps;
@@ -58,6 +58,8 @@ describe('createServiceFactory', () => {
metaFactory({});
metaFactory({ x: 1 });
// @ts-expect-error
metaFactory({ x: 1, y: 2 });
// @ts-expect-error
metaFactory(null);
metaFactory(undefined);
metaFactory();
@@ -80,10 +82,145 @@ describe('createServiceFactory', () => {
metaFactory({});
metaFactory({ x: 1 });
// @ts-expect-error
metaFactory({ x: 1, y: 2 });
// @ts-expect-error
metaFactory(null);
// @ts-expect-error
metaFactory(undefined);
// @ts-expect-error
metaFactory();
});
it('should create a meta factory with optional options as interface', () => {
interface TestOptions {
x: number;
}
const ref = createServiceRef<string>({ id: 'x' });
const metaFactory = createServiceFactory({
service: ref,
deps: {},
async factory(_deps, _opts?: TestOptions) {
return async () => 'x';
},
});
expect(metaFactory).toEqual(expect.any(Function));
// @ts-expect-error
metaFactory('string');
// @ts-expect-error
metaFactory({});
metaFactory({ x: 1 });
// @ts-expect-error
metaFactory({ x: 1, y: 2 });
// @ts-expect-error
metaFactory(null);
metaFactory(undefined);
metaFactory();
});
it('should create a meta factory with required options as interface', () => {
interface TestOptions {
x: number;
}
const ref = createServiceRef<string>({ id: 'x' });
const metaFactory = createServiceFactory({
service: ref,
deps: {},
async factory(_deps, _opts: TestOptions) {
return async () => 'x';
},
});
expect(metaFactory).toEqual(expect.any(Function));
// @ts-expect-error
metaFactory('string');
// @ts-expect-error
metaFactory({});
metaFactory({ x: 1 });
// @ts-expect-error
metaFactory({ x: 1, y: 2 });
// @ts-expect-error
metaFactory(null);
// @ts-expect-error
metaFactory(undefined);
// @ts-expect-error
metaFactory();
});
it('should only allow objects as options', () => {
const ref = createServiceRef<string>({ id: 'x' });
const metaFactory = createServiceFactory({
service: ref,
deps: {},
// @ts-expect-error
async factory(_deps, _opts: string) {
return async () => 'x';
},
});
expect(metaFactory).toEqual(expect.any(Function));
createServiceFactory({
service: ref,
deps: {},
// @ts-expect-error
async factory(_deps, _opts: number) {
return async () => 'x';
},
});
createServiceFactory({
service: ref,
deps: {},
// @ts-expect-error
async factory(_deps, _opts: symbol) {
return async () => 'x';
},
});
createServiceFactory({
service: ref,
deps: {},
// @ts-expect-error
async factory(_deps, _opts: bigint) {
return async () => 'x';
},
});
createServiceFactory({
service: ref,
deps: {},
// @ts-expect-error
async factory(_deps, _opts: 'string') {
return async () => 'x';
},
});
createServiceFactory({
service: ref,
deps: {},
// @ts-expect-error
async factory(_deps, _opts: Array) {
return async () => 'x';
},
});
createServiceFactory({
service: ref,
deps: {},
// @ts-expect-error
async factory(_deps, _opts: Map) {
return async () => 'x';
},
});
createServiceFactory({
service: ref,
deps: {},
// @ts-expect-error
async factory(_deps, _opts: Set) {
return async () => 'x';
},
});
createServiceFactory({
service: ref,
deps: {},
// @ts-expect-error
async factory(_deps, _opts: null) {
return async () => 'x';
},
});
});
});
@@ -133,7 +133,7 @@ export function createServiceFactory<
TScope extends 'root' | 'plugin',
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
TOpts extends { [name in string]: unknown } | undefined = undefined,
TOpts extends object | undefined = undefined,
>(config: {
service: ServiceRef<TService, TScope>;
deps: TDeps;
@@ -68,6 +68,38 @@ describe('createBackendPlugin', () => {
// @ts-expect-error
expect(plugin({})).toBeDefined();
});
it('should create a BackendPlugin with options as interface', () => {
interface TestOptions {
a: string;
}
const plugin = createBackendPlugin({
id: 'x',
register(_reg, _options: TestOptions) {},
});
expect(plugin).toBeDefined();
expect(plugin({ a: 'a' })).toBeDefined();
expect(plugin({ a: 'a' }).id).toBe('x');
// @ts-expect-error
expect(plugin()).toBeDefined();
// @ts-expect-error
expect(plugin({ b: 'b' })).toBeDefined();
});
it('should create plugins with optional options as interface', () => {
interface TestOptions {
a: string;
}
const plugin = createBackendPlugin({
id: 'x',
register(_reg, _options?: TestOptions) {},
});
expect(plugin).toBeDefined();
expect(plugin({ a: 'a' })).toBeDefined();
expect(plugin()).toBeDefined();
// @ts-expect-error
expect(plugin({ b: 'b' })).toBeDefined();
});
});
describe('createBackendModule', () => {
@@ -111,4 +143,38 @@ describe('createBackendModule', () => {
// @ts-expect-error
expect(mod({})).toBeDefined();
});
it('should create a BackendModule as interface', () => {
interface TestOptions {
a: string;
}
const mod = createBackendModule({
pluginId: 'x',
moduleId: 'y',
register(_reg, _options: TestOptions) {},
});
expect(mod).toBeDefined();
expect(mod({ a: 'a' })).toBeDefined();
expect(mod({ a: 'a' }).id).toBe('x.y');
// @ts-expect-error
expect(mod()).toBeDefined();
// @ts-expect-error
expect(mod({ b: 'b' })).toBeDefined();
});
it('should create modules with optional options as interface', () => {
interface TestOptions {
a: string;
}
const mod = createBackendModule({
pluginId: 'x',
moduleId: 'y',
register(_reg, _options?: TestOptions) {},
});
expect(mod).toBeDefined();
expect(mod({ a: 'a' })).toBeDefined();
expect(mod()).toBeDefined();
// @ts-expect-error
expect(mod({ b: 'b' })).toBeDefined();
});
});
@@ -44,10 +44,11 @@ export interface BackendPluginConfig<TOptions> {
/** @public */
export function createBackendPlugin<
TOptions extends { [name: string]: unknown } | undefined = undefined,
>(
config: BackendPluginConfig<TOptions>,
): undefined extends TOptions
TOptions extends object | undefined = undefined,
>(config: {
id: string;
register(reg: BackendRegistrationPoints, options: TOptions): void;
}): undefined extends TOptions
? (options?: TOptions) => BackendFeature
: (options: TOptions) => BackendFeature {
return (options?: TOptions) => ({
@@ -70,7 +71,7 @@ export interface BackendModuleConfig<TOptions> {
/** @public */
export function createBackendModule<
TOptions extends { [name: string]: unknown } | undefined = undefined,
TOptions extends object | undefined = undefined,
>(
config: BackendModuleConfig<TOptions>,
): undefined extends TOptions