From 67b6106dd407b4bf73aabf694fa3630ada0d6157 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 13 Sep 2023 19:15:34 +0200 Subject: [PATCH] frontend-plugin-api: introduce createExtensionInput + refactor Signed-off-by: Patrik Oldsberg --- packages/frontend-plugin-api/api-report.md | 167 ++++++++++-------- .../src/extensions/createApiExtension.ts | 8 +- .../extensions/createPageExtension.test.tsx | 14 +- .../src/extensions/createPageExtension.tsx | 8 +- .../src/wiring/createExtension.test.ts | 27 ++- .../src/wiring/createExtension.ts | 115 ++++++++---- .../src/wiring/createExtensionInput.ts | 55 ++++++ .../src/wiring/createPlugin.test.ts | 17 +- .../src/wiring/createPlugin.ts | 2 +- .../frontend-plugin-api/src/wiring/index.ts | 12 +- .../frontend-plugin-api/src/wiring/types.ts | 42 ----- 11 files changed, 263 insertions(+), 204 deletions(-) create mode 100644 packages/frontend-plugin-api/src/wiring/createExtensionInput.ts delete mode 100644 packages/frontend-plugin-api/src/wiring/types.ts diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index abba3e7f2f..c5d104fc55 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -22,6 +22,17 @@ export type AnyExtensionDataMap = { [name in string]: ExtensionDataRef; }; +// @public (undocumented) +export type AnyExtensionInputMap = { + [inputName in string]: ExtensionInput< + AnyExtensionDataMap, + { + optional: boolean; + singleton: boolean; + } + >; +}; + // @public (undocumented) export interface BackstagePlugin { // (undocumented) @@ -60,19 +71,14 @@ export const coreExtensionData: { // @public (undocumented) export function createApiExtension< TConfig extends {}, - TInputs extends Record< - string, - { - extensionData: AnyExtensionDataMap; - } - >, + TInputs extends AnyExtensionInputMap, >( options: ( | { api: AnyApiRef; factory: (options: { config: TConfig; - inputs: ExtensionDataInputValues; + inputs: Expand>; }) => AnyApiFactory; } | { @@ -87,12 +93,7 @@ export function createApiExtension< // @public (undocumented) export function createExtension< TOutput extends AnyExtensionDataMap, - TInputs extends Record< - string, - { - extensionData: AnyExtensionDataMap; - } - >, + TInputs extends AnyExtensionInputMap, TConfig = never, >( options: CreateExtensionOptions, @@ -103,15 +104,28 @@ export function createExtensionDataRef( id: string, ): ConfigurableExtensionDataRef; +// @public (undocumented) +export function createExtensionInput< + TExtensionData extends AnyExtensionDataMap, + TConfig extends { + singleton?: boolean; + optional?: boolean; + }, +>( + extensionData: TExtensionData, + config?: TConfig, +): ExtensionInput< + TExtensionData, + { + singleton: TConfig['singleton'] extends true ? true : false; + optional: TConfig['optional'] extends true ? true : false; + } +>; + // @public (undocumented) export interface CreateExtensionOptions< TOutput extends AnyExtensionDataMap, - TInputs extends Record< - string, - { - extensionData: AnyExtensionDataMap; - } - >, + TInputs extends AnyExtensionInputMap, TConfig, > { // (undocumented) @@ -123,9 +137,9 @@ export interface CreateExtensionOptions< // (undocumented) factory(options: { source?: BackstagePlugin; - bind: ExtensionDataBind; + bind(values: Expand>): void; config: TConfig; - inputs: ExtensionDataInputValues; + inputs: Expand>; }): void; // (undocumented) id: string; @@ -150,12 +164,7 @@ export function createPageExtension< TConfig extends { path: string; }, - TInputs extends Record< - string, - { - extensionData: AnyExtensionDataMap; - } - >, + TInputs extends AnyExtensionInputMap, >( options: ( | { @@ -172,7 +181,7 @@ export function createPageExtension< routeRef?: RouteRef; loader: (options: { config: TConfig; - inputs: ExtensionDataInputValues; + inputs: Expand>; }) => Promise; }, ): Extension; @@ -198,19 +207,17 @@ export interface Extension { // (undocumented) factory(options: { source?: BackstagePlugin; - bind: ExtensionDataBind; + bind(values: ExtensionInputValues): void; config: TConfig; - inputs: Record>>; + inputs: Record< + string, + undefined | Record | Array> + >; }): void; // (undocumented) id: string; // (undocumented) - inputs: Record< - string, - { - extensionData: AnyExtensionDataMap; - } - >; + inputs: AnyExtensionInputMap; // (undocumented) output: AnyExtensionDataMap; } @@ -228,48 +235,6 @@ export interface ExtensionBoundaryProps { source?: BackstagePlugin; } -// @public (undocumented) -export type ExtensionDataBind = ( - values: { - [DataName in keyof TMap as TMap[DataName]['config'] extends { - optional: true; - } - ? never - : DataName]: TMap[DataName]['T']; - } & { - [DataName in keyof TMap as TMap[DataName]['config'] extends { - optional: true; - } - ? DataName - : never]?: TMap[DataName]['T']; - }, -) => void; - -// @public (undocumented) -export type ExtensionDataInputValues< - TInputs extends { - [name in string]: { - extensionData: AnyExtensionDataMap; - }; - }, -> = { - [InputName in keyof TInputs]: Array< - { - [DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends { - optional: true; - } - ? never - : DataName]: TInputs[InputName]['extensionData'][DataName]['T']; - } & { - [DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends { - optional: true; - } - ? DataName - : never]?: TInputs[InputName]['extensionData'][DataName]['T']; - } - >; -}; - // @public (undocumented) export type ExtensionDataRef< TData, @@ -283,6 +248,52 @@ export type ExtensionDataRef< $$type: '@backstage/ExtensionDataRef'; }; +// @public +export type ExtensionDataValues = { + [DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends { + optional: true; + } + ? never + : DataName]: TExtensionData[DataName]['T']; +} & { + [DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends { + optional: true; + } + ? DataName + : never]?: TExtensionData[DataName]['T']; +}; + +// @public (undocumented) +export interface ExtensionInput< + TExtensionData extends AnyExtensionDataMap, + TConfig extends { + singleton: boolean; + optional: boolean; + }, +> { + // (undocumented) + $$type: '@backstage/ExtensionInput'; + // (undocumented) + config: TConfig; + // (undocumented) + extensionData: TExtensionData; +} + +// @public +export type ExtensionInputValues< + TInputs extends { + [name in string]: ExtensionInput; + }, +> = { + [InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton'] + ? Array>> + : false extends TInputs[InputName]['config']['optional'] + ? Expand> + : Expand< + ExtensionDataValues | undefined + >; +}; + // @public (undocumented) export type NavTarget = { title: string; diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts index f81f07ca72..a142a5af61 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts @@ -17,23 +17,23 @@ import { AnyApiFactory, AnyApiRef } from '@backstage/core-plugin-api'; import { PortableSchema } from '../schema'; import { - AnyExtensionDataMap, - ExtensionDataInputValues, + ExtensionInputValues, createExtension, coreExtensionData, } from '../wiring'; +import { AnyExtensionInputMap, Expand } from '../wiring/createExtension'; /** @public */ export function createApiExtension< TConfig extends {}, - TInputs extends Record, + TInputs extends AnyExtensionInputMap, >( options: ( | { api: AnyApiRef; factory: (options: { config: TConfig; - inputs: ExtensionDataInputValues; + inputs: Expand>; }) => AnyApiFactory; } | { diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx index 8bca1153bf..7de6ee3253 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { PortableSchema } from '../schema'; -import { coreExtensionData } from '../wiring'; +import { coreExtensionData, createExtensionInput } from '../wiring'; import { createPageExtension } from './createPageExtension'; describe('createPageExtension', () => { @@ -54,9 +54,9 @@ describe('createPageExtension', () => { disabled: true, configSchema, inputs: { - first: { - extensionData: { element: coreExtensionData.reactElement }, - }, + first: createExtensionInput({ + element: coreExtensionData.reactElement, + }), }, loader: async () =>
, }), @@ -67,9 +67,9 @@ describe('createPageExtension', () => { configSchema: expect.anything(), disabled: true, inputs: { - first: { - extensionData: { element: coreExtensionData.reactElement }, - }, + first: createExtensionInput({ + element: coreExtensionData.reactElement, + }), }, output: { element: expect.anything(), diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index 4c5ff2d8be..5460fb518d 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -19,12 +19,12 @@ import React from 'react'; import { ExtensionBoundary } from '../components'; import { createSchemaFromZod, PortableSchema } from '../schema'; import { - AnyExtensionDataMap, coreExtensionData, createExtension, Extension, - ExtensionDataInputValues, + ExtensionInputValues, } from '../wiring'; +import { AnyExtensionInputMap, Expand } from '../wiring/createExtension'; /** * Helper for creating extensions for a routable React page component. @@ -33,7 +33,7 @@ import { */ export function createPageExtension< TConfig extends { path: string }, - TInputs extends Record, + TInputs extends AnyExtensionInputMap, >( options: ( | { @@ -50,7 +50,7 @@ export function createPageExtension< routeRef?: RouteRef; loader: (options: { config: TConfig; - inputs: ExtensionDataInputValues; + inputs: Expand>; }) => Promise; }, ): Extension { diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index a992642d61..6f4a21399d 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -16,6 +16,7 @@ import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; +import { createExtensionInput } from './createExtensionInput'; const stringData = createExtensionDataRef('string'); @@ -95,22 +96,16 @@ describe('createExtension', () => { id: 'test', at: 'root', inputs: { - mixed: { - extensionData: { - required: stringData, - optional: stringData.optional(), - }, - }, - onlyRequired: { - extensionData: { - required: stringData, - }, - }, - onlyOptional: { - extensionData: { - optional: stringData.optional(), - }, - }, + mixed: createExtensionInput({ + required: stringData, + optional: stringData.optional(), + }), + onlyRequired: createExtensionInput({ + required: stringData, + }), + onlyOptional: createExtensionInput({ + optional: stringData.optional(), + }), }, output: { foo: stringData, diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 1a7e02804e..f9eb159000 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -15,51 +15,68 @@ */ import { PortableSchema } from '../schema'; +import { ExtensionDataRef } from './createExtensionDataRef'; +import { ExtensionInput } from './createExtensionInput'; import { BackstagePlugin } from './createPlugin'; -import { AnyExtensionDataMap, Extension } from './types'; /** @public */ -export type ExtensionDataInputValues< - TInputs extends { [name in string]: { extensionData: AnyExtensionDataMap } }, -> = { - [InputName in keyof TInputs]: Array< - { - [DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends { - optional: true; - } - ? never - : DataName]: TInputs[InputName]['extensionData'][DataName]['T']; - } & { - [DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends { - optional: true; - } - ? DataName - : never]?: TInputs[InputName]['extensionData'][DataName]['T']; - } - >; +export type AnyExtensionDataMap = { + [name in string]: ExtensionDataRef; }; /** @public */ -export type ExtensionDataBind = ( - values: { - [DataName in keyof TMap as TMap[DataName]['config'] extends { - optional: true; - } - ? never - : DataName]: TMap[DataName]['T']; - } & { - [DataName in keyof TMap as TMap[DataName]['config'] extends { - optional: true; - } - ? DataName - : never]?: TMap[DataName]['T']; - }, -) => void; +export type AnyExtensionInputMap = { + [inputName in string]: ExtensionInput< + AnyExtensionDataMap, + { optional: boolean; singleton: boolean } + >; +}; + +// TODO(Rugvip): This might be a quite useful utility type, maybe add to @backstage/types? +/** + * Utility type to expand type aliases into their equivalent type. + * @ignore + */ +export type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; + +/** + * Converts an extension data map into the matching concrete data values type. + * @public + */ +export type ExtensionDataValues = { + [DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends { + optional: true; + } + ? never + : DataName]: TExtensionData[DataName]['T']; +} & { + [DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends { + optional: true; + } + ? DataName + : never]?: TExtensionData[DataName]['T']; +}; + +/** + * Converts an extension input map into the matching concrete input values type. + * @public + */ +export type ExtensionInputValues< + TInputs extends { [name in string]: ExtensionInput }, +> = { + [InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton'] + ? Array>> + : false extends TInputs[InputName]['config']['optional'] + ? Expand> + : Expand< + ExtensionDataValues | undefined + >; +}; /** @public */ export interface CreateExtensionOptions< TOutput extends AnyExtensionDataMap, - TInputs extends Record, + TInputs extends AnyExtensionInputMap, TConfig, > { id: string; @@ -70,16 +87,36 @@ export interface CreateExtensionOptions< configSchema?: PortableSchema; factory(options: { source?: BackstagePlugin; - bind: ExtensionDataBind; + bind(values: Expand>): void; config: TConfig; - inputs: ExtensionDataInputValues; + inputs: Expand>; + }): void; +} + +/** @public */ +export interface Extension { + $$type: '@backstage/Extension'; + id: string; + at: string; + disabled: boolean; + inputs: AnyExtensionInputMap; + output: AnyExtensionDataMap; + configSchema?: PortableSchema; + factory(options: { + source?: BackstagePlugin; + bind(values: ExtensionInputValues): void; + config: TConfig; + inputs: Record< + string, + undefined | Record | Array> + >; }): void; } /** @public */ export function createExtension< TOutput extends AnyExtensionDataMap, - TInputs extends Record, + TInputs extends AnyExtensionInputMap, TConfig = never, >( options: CreateExtensionOptions, @@ -94,7 +131,7 @@ export function createExtension< return options.factory({ bind, config, - inputs: inputs as ExtensionDataInputValues, + inputs: inputs as Expand>, }); }, }; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts b/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts new file mode 100644 index 0000000000..1fd8fea972 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts @@ -0,0 +1,55 @@ +/* + * 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 { AnyExtensionDataMap } from './createExtension'; + +/** @public */ +export interface ExtensionInput< + TExtensionData extends AnyExtensionDataMap, + TConfig extends { singleton: boolean; optional: boolean }, +> { + $$type: '@backstage/ExtensionInput'; + extensionData: TExtensionData; + config: TConfig; +} + +/** @public */ +export function createExtensionInput< + TExtensionData extends AnyExtensionDataMap, + TConfig extends { singleton?: boolean; optional?: boolean }, +>( + extensionData: TExtensionData, + config?: TConfig, +): ExtensionInput< + TExtensionData, + { + singleton: TConfig['singleton'] extends true ? true : false; + optional: TConfig['optional'] extends true ? true : false; + } +> { + return { + $$type: '@backstage/ExtensionInput', + extensionData, + config: { + singleton: Boolean(config?.singleton) as TConfig['singleton'] extends true + ? true + : false, + optional: Boolean(config?.optional) as TConfig['optional'] extends true + ? true + : false, + }, + }; +} diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index 08d07f2226..1d45c221af 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -24,6 +24,7 @@ import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; import { coreExtensionData } from './coreExtensionData'; import { MockConfigApi } from '@backstage/test-utils'; +import { createExtensionInput } from './createExtensionInput'; const nameExtensionDataRef = createExtensionDataRef('name'); @@ -70,11 +71,9 @@ const TechDocsPage = createExtension({ id: 'plugin.techdocs.page', at: 'test.output/names', inputs: { - addons: { - extensionData: { - name: nameExtensionDataRef, - }, - }, + addons: createExtensionInput({ + name: nameExtensionDataRef, + }), }, output: { name: nameExtensionDataRef, @@ -88,11 +87,9 @@ const outputExtension = createExtension({ id: 'test.output', at: 'root', inputs: { - names: { - extensionData: { - name: nameExtensionDataRef, - }, - }, + names: createExtensionInput({ + name: nameExtensionDataRef, + }), }, output: { element: coreExtensionData.reactElement, diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts index bb372f63c1..bd8e9954ef 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Extension } from './types'; +import { Extension } from './createExtension'; /** @public */ export interface PluginOptions { diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index 4177ffbfcc..b7f0acefd9 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -17,10 +17,17 @@ export { coreExtensionData, type NavTarget } from './coreExtensionData'; export { createExtension, + type Extension, type CreateExtensionOptions, - type ExtensionDataBind, - type ExtensionDataInputValues, + type ExtensionDataValues, + type ExtensionInputValues, + type AnyExtensionInputMap, + type AnyExtensionDataMap, } from './createExtension'; +export { + createExtensionInput, + type ExtensionInput, +} from './createExtensionInput'; export { createExtensionDataRef, type ExtensionDataRef, @@ -31,4 +38,3 @@ export { type BackstagePlugin, type PluginOptions, } from './createPlugin'; -export type { AnyExtensionDataMap, Extension } from './types'; diff --git a/packages/frontend-plugin-api/src/wiring/types.ts b/packages/frontend-plugin-api/src/wiring/types.ts deleted file mode 100644 index 46b76e30f4..0000000000 --- a/packages/frontend-plugin-api/src/wiring/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { PortableSchema } from '../schema'; -import { ExtensionDataBind } from './createExtension'; -import { ExtensionDataRef } from './createExtensionDataRef'; -import { BackstagePlugin } from './createPlugin'; - -/** @public */ -export type AnyExtensionDataMap = { - [name in string]: ExtensionDataRef; -}; - -/** @public */ -export interface Extension { - $$type: '@backstage/Extension'; - id: string; - at: string; - disabled: boolean; - inputs: Record; - output: AnyExtensionDataMap; - configSchema?: PortableSchema; - factory(options: { - source?: BackstagePlugin; - bind: ExtensionDataBind; - config: TConfig; - inputs: Record>>; - }): void; -}