From cd9f3a526e34320fb1cd5166bb23ba8f41a10a0f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 26 Jul 2024 13:56:14 +0200 Subject: [PATCH] frontend-plugin-api: update createExtensionInput to accept an array of refs too + tests Signed-off-by: Patrik Oldsberg --- .../src/wiring/createExtensionInput.test.ts | 181 ++++++++++++++++++ .../src/wiring/createExtensionInput.ts | 60 +++++- 2 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts new file mode 100644 index 0000000000..0025789c73 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts @@ -0,0 +1,181 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createExtensionDataRef } from './createExtensionDataRef'; +import { ExtensionInput, createExtensionInput } from './createExtensionInput'; + +const stringDataRef = createExtensionDataRef().with({ id: 'str' }); +const numberDataRef = createExtensionDataRef().with({ id: 'num' }); + +function unused(..._any: any[]) {} + +describe('createExtensionInput', () => { + it('should create a regular input', () => { + const input = createExtensionInput([stringDataRef, numberDataRef]); + expect(input).toEqual({ + $$type: '@backstage/ExtensionInput', + extensionData: [stringDataRef, numberDataRef], + config: { singleton: false, optional: false }, + }); + + const x1: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + never, + { singleton: false; optional: false } + > = input; + // @ts-expect-error + const x2: ExtensionInput< + typeof stringDataRef, + never, + { singleton: false; optional: false } + > = input; + // @ts-expect-error + const x3: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + never, + { singleton: true; optional: false } + > = input; + // @ts-expect-error + const x4: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + never, + { singleton: false; optional: true } + > = input; + + unused(x1, x2, x3, x4); + }); + + it('should create a singleton input', () => { + const input = createExtensionInput([stringDataRef, numberDataRef], { + singleton: true, + }); + expect(input).toEqual({ + $$type: '@backstage/ExtensionInput', + extensionData: [stringDataRef, numberDataRef], + config: { singleton: true, optional: false }, + }); + + const x1: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + never, + { singleton: true; optional: false } + > = input; + // @ts-expect-error + const x2: ExtensionInput< + typeof stringDataRef, + never, + { singleton: true; optional: false } + > = input; + // @ts-expect-error + const x3: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + never, + { singleton: false; optional: false } + > = input; + // @ts-expect-error + const x4: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + never, + { singleton: false; optional: true } + > = input; + + unused(x1, x2, x3, x4); + }); + + it('should create an optional singleton input', () => { + const input = createExtensionInput([stringDataRef, numberDataRef], { + singleton: true, + optional: true, + }); + expect(input).toEqual({ + $$type: '@backstage/ExtensionInput', + extensionData: [stringDataRef, numberDataRef], + config: { singleton: true, optional: true }, + }); + + const x1: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + never, + { singleton: true; optional: true } + > = input; + // @ts-expect-error + const x2: ExtensionInput< + typeof stringDataRef, + never, + { singleton: true; optional: true } + > = input; + // @ts-expect-error + const x3: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + never, + { singleton: false; optional: false } + > = input; + // @ts-expect-error + const x4: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + never, + { singleton: false; optional: true } + > = input; + + unused(x1, x2, x3, x4); + }); + + it('should not allow duplicate data refs', () => { + expect(() => + createExtensionInput([stringDataRef, stringDataRef], { singleton: true }), + ).toThrow("ExtensionInput may not have duplicate data refs: 'str'"); + }); + + describe('old api', () => { + it('should create a regular input', () => { + const input = createExtensionInput({ + str: stringDataRef, + num: numberDataRef, + }); + expect(input).toEqual({ + $$type: '@backstage/ExtensionInput', + extensionData: { str: stringDataRef, num: numberDataRef }, + config: { singleton: false, optional: false }, + }); + + const x1: ExtensionInput< + never, + { str: typeof stringDataRef; num: typeof numberDataRef }, + { singleton: false; optional: false } + > = input; + // @ts-expect-error + const x2: ExtensionInput< + never, + { str: typeof numberDataRef; num: typeof stringDataRef }, // switched + { singleton: false; optional: false } + > = input; + // @ts-expect-error + const x3: ExtensionInput< + never, + { str: typeof stringDataRef; num: typeof numberDataRef }, + { singleton: true; optional: false } + > = input; + // @ts-expect-error + const x4: ExtensionInput< + never, + { str: typeof stringDataRef; num: typeof numberDataRef }, + { singleton: false; optional: true } + > = input; + + unused(x1, x2, x3, x4); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts b/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts index 1fd8fea972..3c09fcdaeb 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts @@ -15,31 +15,85 @@ */ import { AnyExtensionDataMap } from './createExtension'; +import { ExtensionDataRef } from './createExtensionDataRef'; /** @public */ export interface ExtensionInput< - TExtensionData extends AnyExtensionDataMap, + TExtensionData extends ExtensionDataRef, + TExtensionDataMap extends AnyExtensionDataMap, TConfig extends { singleton: boolean; optional: boolean }, > { $$type: '@backstage/ExtensionInput'; - extensionData: TExtensionData; + extensionData: TExtensionData | TExtensionDataMap; config: TConfig; } +/** + * @public + * @deprecated Use the following form instead: `createExtensionInput([dataRef1, dataRef2])` + */ +export function createExtensionInput< + TExtensionDataMap extends AnyExtensionDataMap, + TConfig extends { singleton?: boolean; optional?: boolean }, +>( + extensionData: TExtensionDataMap, + config?: TConfig, +): ExtensionInput< + never, + TExtensionDataMap, + { + singleton: TConfig['singleton'] extends true ? true : false; + optional: TConfig['optional'] extends true ? true : false; + } +>; /** @public */ export function createExtensionInput< - TExtensionData extends AnyExtensionDataMap, + UExtensionData extends ExtensionDataRef, + TConfig extends { singleton?: boolean; optional?: boolean }, +>( + extensionData: Array, + config?: TConfig, +): ExtensionInput< + UExtensionData, + never, + { + singleton: TConfig['singleton'] extends true ? true : false; + optional: TConfig['optional'] extends true ? true : false; + } +>; +export function createExtensionInput< + TExtensionData extends ExtensionDataRef, + TExtensionDataMap extends AnyExtensionDataMap, TConfig extends { singleton?: boolean; optional?: boolean }, >( extensionData: TExtensionData, config?: TConfig, ): ExtensionInput< TExtensionData, + TExtensionDataMap, { singleton: TConfig['singleton'] extends true ? true : false; optional: TConfig['optional'] extends true ? true : false; } > { + if (Array.isArray(extensionData)) { + const seen = new Set(); + const duplicates = []; + for (const dataRef of extensionData) { + if (seen.has(dataRef.id)) { + duplicates.push(dataRef.id); + } else { + seen.add(dataRef.id); + } + } + if (duplicates.length > 0) { + throw new Error( + `ExtensionInput may not have duplicate data refs: '${duplicates.join( + "', '", + )}'`, + ); + } + } return { $$type: '@backstage/ExtensionInput', extensionData,