From 47bc4046c57c7bdd10e7f20c2463fd447f505750 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 28 Jul 2024 21:24:03 +0200 Subject: [PATCH] frontend-plugin-api: add initial support for v2 extensions for blueprints Signed-off-by: Patrik Oldsberg --- .changeset/dry-squids-tap.md | 8 +- packages/frontend-plugin-api/api-report.md | 143 +++++++++++------- .../src/extensions/IconBundleBlueprint.ts | 8 +- .../src/wiring/createExtension.ts | 38 +++-- .../wiring/createExtensionBlueprint.test.tsx | 54 ++----- .../src/wiring/createExtensionBlueprint.ts | 137 ++++++++++++----- .../src/wiring/createExtensionDataRef.ts | 6 + .../frontend-plugin-api/src/wiring/index.ts | 1 + 8 files changed, 235 insertions(+), 160 deletions(-) diff --git a/.changeset/dry-squids-tap.md b/.changeset/dry-squids-tap.md index 613ec4b7cb..0a95332eed 100644 --- a/.changeset/dry-squids-tap.md +++ b/.changeset/dry-squids-tap.md @@ -11,13 +11,9 @@ This allows the creation of extension instances with the following pattern: const EntityCardBlueprint = createExtensionBlueprint({ kind: 'entity-card', attachTo: { id: 'test', input: 'default' }, - output: { - element: coreExtensionData.reactElement, - }, + output: [coreExtensionData.reactElement], factory(params: { text: string }) { - return { - element:

{params.text}

, - }; + return [coreExtensionData.reactElement(

{params.text}

)]; }, }); diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 96c7675c0d..d37f0a3a56 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -593,24 +593,38 @@ export function createExtension< // @public export function createExtensionBlueprint< TParams, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + UExtraOutput extends AnyExtensionDataRef, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, - TDataRefs extends AnyExtensionDataMap = never, + UFactoryOutput extends ExtensionDataValue, + TDataRefs extends { + [name in string]: AnyExtensionDataRef; + } = never, >( options: CreateExtensionBlueprintOptions< TParams, + UOutput, TInputs, - TOutput, TConfigSchema, + UFactoryOutput, TDataRefs >, ): ExtensionBlueprint< TParams, + UOutput, TInputs, - TOutput, + UExtraOutput, string extends keyof TConfigSchema ? {} : { @@ -627,29 +641,38 @@ export function createExtensionBlueprint< >; // @public (undocumented) -export interface CreateExtensionBlueprintOptions< +export type CreateExtensionBlueprintOptions< TParams, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, - TDataRefs extends AnyExtensionDataMap, -> { - // (undocumented) + UFactoryOutput extends ExtensionDataValue, + TDataRefs extends { + [name in string]: AnyExtensionDataRef; + }, +> = { + kind: string; + namespace?: string; attachTo: { id: string; input: string; }; - // (undocumented) + disabled?: boolean; + inputs?: TInputs; + output: Array; config?: { schema: TConfigSchema; }; - // (undocumented) - dataRefs?: TDataRefs; - // (undocumented) - disabled?: boolean; - // (undocumented) factory( params: TParams, context: { @@ -659,16 +682,9 @@ export interface CreateExtensionBlueprintOptions< }; inputs: Expand>; }, - ): Expand>; - // (undocumented) - inputs?: TInputs; - // (undocumented) - kind: string; - // (undocumented) - namespace?: string; - // (undocumented) - output: TOutput; -} + ): Iterable; + dataRefs?: TDataRefs; +} & VerifyExtensionFactoryOutput; // @public @deprecated (undocumented) export function createExtensionDataRef( @@ -769,22 +785,7 @@ export type CreateExtensionOptions< }); inputs: Expand>; }): Iterable; -} & (( - UOutput extends any - ? UOutput['config']['optional'] extends true - ? never - : UOutput['id'] - : never -) extends infer IRequiredOutputIds - ? [IRequiredOutputIds] extends [UFactoryOutput['id']] - ? {} - : { - 'Error: The extension factory is missing the following outputs': Exclude< - IRequiredOutputIds, - UFactoryOutput['id'] - >; - } - : never); +} & VerifyExtensionFactoryOutput; // @public (undocumented) export function createExtensionOverrides( @@ -1081,15 +1082,26 @@ export interface Extension { // @public (undocumented) export interface ExtensionBlueprint< TParams, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + UExtraOutput extends AnyExtensionDataRef, TConfig extends { [key in string]: unknown; }, TConfigInput extends { [key in string]: unknown; }, - TDataRefs extends AnyExtensionDataMap, + TDataRefs extends { + [name in string]: AnyExtensionDataRef; + }, > { // (undocumented) dataRefs: TDataRefs; @@ -1097,6 +1109,7 @@ export interface ExtensionBlueprint< TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, + UFactoryOutput extends ExtensionDataValue, >( args: { namespace?: string; @@ -1107,7 +1120,7 @@ export interface ExtensionBlueprint< }; disabled?: boolean; inputs?: TInputs; - output?: TOutput; + output?: Array; config?: { schema: TExtensionConfigSchema & { [KName in keyof TConfig]?: `Error: Config key '${KName & @@ -1115,7 +1128,7 @@ export interface ExtensionBlueprint< }; }; } & ( - | { + | ({ factory( originalFactory: ( params: TParams, @@ -1123,7 +1136,7 @@ export interface ExtensionBlueprint< config?: TConfig; inputs?: Expand>; }, - ) => Expand>, + ) => Iterable>, context: { node: AppNode; config: TConfig & { @@ -1133,8 +1146,11 @@ export interface ExtensionBlueprint< }; inputs: Expand>; }, - ): Expand>; - } + ): Iterable; + } & VerifyExtensionFactoryOutput< + UOutput & UExtraOutput, + UFactoryOutput + >) | { params: TParams; } @@ -1196,6 +1212,12 @@ export type ExtensionDataRef< readonly config: TConfig; }; +// @public (undocumented) +export type ExtensionDataRefToValue = + TDataRef extends ExtensionDataRef + ? ExtensionDataValue + : never; + // @public (undocumented) export type ExtensionDataValue = { readonly $$type: '@backstage/ExtensionDataValue'; @@ -1323,16 +1345,23 @@ export const IconBundleBlueprint: ExtensionBlueprint< [x: string]: IconComponent; }; }, - AnyExtensionInputMap, + ConfigurableExtensionDataRef< + { + [x: string]: IconComponent; + }, + 'core.icons', + {} + >, { - icons: ConfigurableExtensionDataRef< + [x: string]: ExtensionInput< + AnyExtensionDataRef, { - [x: string]: IconComponent; - }, - 'core.icons', - {} + optional: boolean; + singleton: boolean; + } >; }, + AnyExtensionDataRef, { icons: string; test: string; diff --git a/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts b/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts index 438da09b4a..deeabdb44c 100644 --- a/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts +++ b/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts @@ -25,16 +25,16 @@ export const IconBundleBlueprint = createExtensionBlueprint({ kind: 'icon-bundle', namespace: 'app', attachTo: { id: 'app', input: 'icons' }, - output: { - icons: iconsDataRef, - }, + output: [iconsDataRef], config: { schema: { icons: z => z.string().default('blob'), test: z => z.string(), }, }, - factory: (params: { icons: { [key in string]: IconComponent } }) => params, + factory: (params: { icons: { [key in string]: IconComponent } }) => [ + iconsDataRef(params.icons), + ], dataRefs: { icons: iconsDataRef, }, diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 4034ef7238..4d31982ed3 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -145,6 +145,27 @@ export interface LegacyCreateExtensionOptions< }): Expand>; } +/** @ignore */ +export type VerifyExtensionFactoryOutput< + UDeclaredOutput extends AnyExtensionDataRef, + UFactoryOutput extends ExtensionDataValue, +> = ( + UDeclaredOutput extends any + ? UDeclaredOutput['config']['optional'] extends true + ? never + : UDeclaredOutput['id'] + : never +) extends infer IRequiredOutputIds + ? [IRequiredOutputIds] extends [UFactoryOutput['id']] + ? {} + : { + 'Error: The extension factory is missing the following outputs': Exclude< + IRequiredOutputIds, + UFactoryOutput['id'] + >; + } + : never; + /** @public */ export type CreateExtensionOptions< UOutput extends AnyExtensionDataRef, @@ -183,22 +204,7 @@ export type CreateExtensionOptions< }); inputs: Expand>; }): Iterable; -} & (( - UOutput extends any - ? UOutput['config']['optional'] extends true - ? never - : UOutput['id'] - : never -) extends infer IRequiredOutputIds - ? [IRequiredOutputIds] extends [UFactoryOutput['id']] - ? {} - : { - 'Error: The extension factory is missing the following outputs': Exclude< - IRequiredOutputIds, - UFactoryOutput['id'] - >; - } - : never); +} & VerifyExtensionFactoryOutput; /** @public */ export interface ExtensionDefinition { diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index e4cb9e26f1..8b364f33c1 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -27,13 +27,9 @@ describe('createExtensionBlueprint', () => { const TestExtensionBlueprint = createExtensionBlueprint({ kind: 'test-extension', attachTo: { id: 'test', input: 'default' }, - output: { - element: coreExtensionData.reactElement, - }, + output: [coreExtensionData.reactElement], factory(params: { text: string }) { - return { - element:

{params.text}

, - }; + return [coreExtensionData.reactElement(

{params.text}

)]; }, }); @@ -56,12 +52,10 @@ describe('createExtensionBlueprint', () => { kind: 'test-extension', name: 'my-extension', namespace: undefined, - output: { - element: coreExtensionData.reactElement, - }, + output: [coreExtensionData.reactElement], factory: expect.any(Function), toString: expect.any(Function), - version: 'v1', + version: 'v2', }); const { container } = createExtensionTester(extension).render(); @@ -72,13 +66,9 @@ describe('createExtensionBlueprint', () => { const TestExtensionBlueprint = createExtensionBlueprint({ kind: 'test-extension', attachTo: { id: 'test', input: 'default' }, - output: { - element: coreExtensionData.reactElement, - }, + output: [coreExtensionData.reactElement], factory(params: { text: string }) { - return { - element:

{params.text}

, - }; + return [coreExtensionData.reactElement(

{params.text}

)]; }, }); @@ -103,16 +93,12 @@ describe('createExtensionBlueprint', () => { const TestExtensionBlueprint = createExtensionBlueprint({ kind: 'test-extension', attachTo: { id: 'test', input: 'default' }, - output: { - element: coreExtensionData.reactElement, - }, + output: [coreExtensionData.reactElement], dataRefs: { data: dataRef, }, factory(params: { text: string }) { - return { - element:

{params.text}

, - }; + return [coreExtensionData.reactElement(

{params.text}

)]; }, }); @@ -125,9 +111,7 @@ describe('createExtensionBlueprint', () => { const TestExtensionBlueprint = createExtensionBlueprint({ kind: 'test-extension', attachTo: { id: 'test', input: 'default' }, - output: { - element: coreExtensionData.reactElement, - }, + output: [coreExtensionData.reactElement], config: { schema: { text: z => z.string(), @@ -142,9 +126,7 @@ describe('createExtensionBlueprint', () => { expect(config.text).toBe('Hello, world!'); - return { - element:

{config.text}

, - }; + return [coreExtensionData.reactElement(

{config.text}

)]; }, }); @@ -188,18 +170,14 @@ describe('createExtensionBlueprint', () => { const TestExtensionBlueprint = createExtensionBlueprint({ kind: 'test-extension', attachTo: { id: 'test', input: 'default' }, - output: { - element: coreExtensionData.reactElement, - }, + output: [coreExtensionData.reactElement], config: { schema: { text: z => z.string(), }, }, factory(params: { text: string }) { - return { - element:
{params.text}
, - }; + return [coreExtensionData.reactElement(
{params.text}
)]; }, }); @@ -224,16 +202,12 @@ describe('createExtensionBlueprint', () => { const TestExtensionBlueprint = createExtensionBlueprint({ kind: 'test-extension', attachTo: { id: 'test', input: 'default' }, - output: { - element: coreExtensionData.reactElement, - }, + output: [coreExtensionData.reactElement], factory(_, { config }) { // @ts-expect-error const b = config.something; - return { - element:
, - }; + return [coreExtensionData.reactElement(
)]; }, }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 7393ca233e..b579cc207a 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -17,31 +17,42 @@ import { AppNode } from '../apis'; import { Expand } from '../types'; import { - AnyExtensionDataMap, - AnyExtensionInputMap, - ExtensionDataValues, + CreateExtensionOptions, ExtensionDefinition, ResolvedExtensionInputs, + VerifyExtensionFactoryOutput, createExtension, } from './createExtension'; import { z } from 'zod'; +import { ExtensionInput } from './createExtensionInput'; +import { + AnyExtensionDataRef, + ExtensionDataRefToValue, + ExtensionDataValue, +} from './createExtensionDataRef'; /** * @public */ -export interface CreateExtensionBlueprintOptions< +export type CreateExtensionBlueprintOptions< TParams, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, - TDataRefs extends AnyExtensionDataMap, -> { + UFactoryOutput extends ExtensionDataValue, + TDataRefs extends { [name in string]: AnyExtensionDataRef }, +> = { kind: string; namespace?: string; attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; - output: TOutput; + output: Array; config?: { schema: TConfigSchema; }; @@ -54,21 +65,27 @@ export interface CreateExtensionBlueprintOptions< }; inputs: Expand>; }, - ): Expand>; + ): Iterable; dataRefs?: TDataRefs; -} +} & VerifyExtensionFactoryOutput; /** * @public */ export interface ExtensionBlueprint< TParams, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + UExtraOutput extends AnyExtensionDataRef, TConfig extends { [key in string]: unknown }, TConfigInput extends { [key in string]: unknown }, - TDataRefs extends AnyExtensionDataMap, + TDataRefs extends { [name in string]: AnyExtensionDataRef }, > { dataRefs: TDataRefs; @@ -82,6 +99,7 @@ export interface ExtensionBlueprint< TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, + UFactoryOutput extends ExtensionDataValue, >( args: { namespace?: string; @@ -89,7 +107,7 @@ export interface ExtensionBlueprint< attachTo?: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; - output?: TOutput; + output?: Array; config?: { schema: TExtensionConfigSchema & { [KName in keyof TConfig]?: `Error: Config key '${KName & @@ -97,7 +115,7 @@ export interface ExtensionBlueprint< }; }; } & ( - | { + | ({ factory( originalFactory: ( params: TParams, @@ -105,7 +123,7 @@ export interface ExtensionBlueprint< config?: TConfig; inputs?: Expand>; }, - ) => Expand>, + ) => Iterable>, context: { node: AppNode; config: TConfig & { @@ -115,8 +133,11 @@ export interface ExtensionBlueprint< }; inputs: Expand>; }, - ): Expand>; - } + ): Iterable; + } & VerifyExtensionFactoryOutput< + UOutput & UExtraOutput, + UFactoryOutput + >) | { params: TParams; } @@ -143,17 +164,24 @@ export interface ExtensionBlueprint< */ class ExtensionBlueprintImpl< TParams, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + UExtraOutput extends AnyExtensionDataRef, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, - TDataRefs extends AnyExtensionDataMap, + TDataRefs extends { [name in string]: AnyExtensionDataRef }, > { constructor( private readonly options: CreateExtensionBlueprintOptions< TParams, + UOutput, TInputs, - TOutput, TConfigSchema, + any, TDataRefs >, ) { @@ -166,13 +194,14 @@ class ExtensionBlueprintImpl< TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, + UFactoryOutput extends ExtensionDataValue, >(args: { namespace?: string; name?: string; attachTo?: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; - output?: TOutput; + output?: Array; params?: TParams; config?: { schema: TExtensionConfigSchema; @@ -188,7 +217,7 @@ class ExtensionBlueprintImpl< }; inputs?: Expand>; }, - ) => Expand>, + ) => Iterable>, context: { node: AppNode; config: { @@ -200,7 +229,7 @@ class ExtensionBlueprintImpl< }; inputs: Expand>; }, - ): Expand>; + ): Iterable; }): ExtensionDefinition< { [key in keyof TExtensionConfigSchema]: z.infer< @@ -232,7 +261,7 @@ class ExtensionBlueprintImpl< attachTo: args.attachTo ?? this.options.attachTo, disabled: args.disabled ?? this.options.disabled, inputs: args.inputs ?? this.options.inputs, - output: args.output ?? this.options.output, + output: [...(args.output ?? []), ...this.options.output], config: Object.keys(schema).length === 0 ? undefined : { schema }, factory: ({ node, config, inputs }) => { if (args.factory) { @@ -247,12 +276,13 @@ class ExtensionBlueprintImpl< }; inputs?: Expand>; }, - ) => - this.options.factory(innerParams, { + ): Iterable> => { + return this.options.factory(innerParams, { node, config: innerContext?.config ?? config, inputs: innerContext?.inputs ?? inputs, - }), + }); + }, { node, config, @@ -268,7 +298,30 @@ class ExtensionBlueprintImpl< } throw new Error('Either params or factory must be provided'); }, - }); + } as CreateExtensionOptions< + UOutput, + TInputs, + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject< + { + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + } & { + [key in keyof TConfigSchema]: ReturnType; + } + > + >, + TConfigSchema, + UFactoryOutput + >); } } @@ -280,22 +333,31 @@ class ExtensionBlueprintImpl< */ export function createExtensionBlueprint< TParams, - TInputs extends AnyExtensionInputMap, - TOutput extends AnyExtensionDataMap, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + UExtraOutput extends AnyExtensionDataRef, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, - TDataRefs extends AnyExtensionDataMap = never, + UFactoryOutput extends ExtensionDataValue, + TDataRefs extends { [name in string]: AnyExtensionDataRef } = never, >( options: CreateExtensionBlueprintOptions< TParams, + UOutput, TInputs, - TOutput, TConfigSchema, + UFactoryOutput, TDataRefs >, ): ExtensionBlueprint< TParams, + UOutput, TInputs, - TOutput, + UExtraOutput, string extends keyof TConfigSchema ? {} : { [key in keyof TConfigSchema]: z.infer> }, @@ -310,8 +372,9 @@ export function createExtensionBlueprint< > { return new ExtensionBlueprintImpl(options) as ExtensionBlueprint< TParams, + UOutput, TInputs, - TOutput, + UExtraOutput, string extends keyof TConfigSchema ? {} : { diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts index f4b348ae51..c77aa8c394 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts @@ -33,6 +33,12 @@ export type ExtensionDataRef< readonly config: TConfig; }; +/** @public */ +export type ExtensionDataRefToValue = + TDataRef extends ExtensionDataRef + ? ExtensionDataValue + : never; + /** @public */ export type AnyExtensionDataRef = ExtensionDataRef< unknown, diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index 4b99ba8ebc..a17dfedc16 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -36,6 +36,7 @@ export { createExtensionDataRef, type AnyExtensionDataRef, type ExtensionDataRef, + type ExtensionDataRefToValue, type ExtensionDataValue, type ConfigurableExtensionDataRef, } from './createExtensionDataRef';