From 66523e8d01fbb57e35bcfd164c20b11ba7fcae26 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 30 Jul 2024 15:47:45 +0200 Subject: [PATCH 01/15] chore: wip Signed-off-by: blam --- packages/app-next/src/App.tsx | 48 ++++++- .../src/extensions/createPageExtension.tsx | 131 ++++++++++++++++++ plugins/home/src/alpha.tsx | 58 +++++++- 3 files changed, 223 insertions(+), 14 deletions(-) diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index b57c680474..25d1aacf8f 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -20,6 +20,7 @@ import { pagesPlugin } from './examples/pagesPlugin'; import notFoundErrorPage from './examples/notFoundErrorPageExtension'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; import homePlugin, { + extensions, titleExtensionDataRef, } from '@backstage/plugin-home/alpha'; @@ -74,13 +75,46 @@ TODO: /* app.tsx */ -const homePageExtension = createExtension({ - name: 'myhomepage', - attachTo: { id: 'page:home', input: 'props' }, - output: { - children: coreExtensionData.reactElement, - title: titleExtensionDataRef, - }, +// const homePageExtension = createExtension({ +// name: 'myhomepage', +// attachTo: { id: 'page:home', input: 'props' }, +// output: { +// children: coreExtensionData.reactElement, +// title: titleExtensionDataRef, +// }, +// factory() { +// return { children: homePage, title: 'just a title' }; +// }, +// }); + +// const homePage = createPageExtension({ +// defaultPath: '/home', +// routeRef: rootRouteRef, +// inputs: { +// props: createExtensionInput( +// { +// children: coreExtensionData.reactElement.optional(), +// title: titleExtensionDataRef.optional(), +// }, + +// { +// singleton: true, +// optional: true, +// }, +// ), +// }, +// loader: ({ inputs }) => +// import('./components/').then(m => +// compatWrapper( +// , +// ), +// ), +// }); + +const homePageExtension = extensions.homePage.override({ factory() { return { children: homePage, title: 'just a title' }; }, diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index cd434a3ffc..b10901f1bd 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -22,6 +22,9 @@ import { createExtension, ResolvedExtensionInputs, AnyExtensionInputMap, + createExtensionBlueprint, + ExtensionInput, + AnyExtensionDataRef, } from '../wiring'; import { RouteRef } from '../routing'; import { Expand } from '../types'; @@ -95,3 +98,131 @@ export function createPageExtension< }, }); } + +export function createNewPageExtension< + TConfig extends { path: string }, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, +>( + options: ( + | { + defaultPath: string; + } + | { + configSchema: PortableSchema; + } + ) & { + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; + }, +): ExtensionDefinition { + const configSchema = + 'configSchema' in options + ? options.configSchema + : (createSchemaFromZod(z => + z.object({ path: z.string().default(options.defaultPath) }), + ) as PortableSchema); + + return createExtension({ + kind: 'page', + namespace: options.namespace, + name: options.name, + attachTo: options.attachTo ?? { id: 'app/routes', input: 'routes' }, + configSchema, + inputs: options.inputs, + disabled: options.disabled, + output: [ + coreExtensionData.routePath, + coreExtensionData.reactElement, + coreExtensionData.routeRef.optional(), + ], + factory({ config, inputs, node }) { + const ExtensionComponent = lazy(() => + options + .loader({ config, inputs }) + .then(element => ({ default: () => element })), + ); + + const outputs = [ + coreExtensionData.routePath(config.path), + coreExtensionData.reactElement( + + + , + ), + ]; + + if (options.routeRef) { + return [...outputs, coreExtensionData.routeRef(options.routeRef)]; + } + + return outputs; + }, + }); +} +/** + * A blueprint for creating extensions for routable React page components. + * @public + */ +export const PageExtensionBlueprint = createExtensionBlueprint({ + kind: 'page', + attachTo: { id: 'app/routes', input: 'routes' }, + output: [ + coreExtensionData.routePath, + coreExtensionData.reactElement, + coreExtensionData.routeRef.optional(), + ], + config: { + schema: { + path: z => z.string().optional(), + }, + }, + factory( + { + defaultPath, + loader, + routeRef, + }: { + defaultPath?: string; + // TODO(blam) This type is impossible to type properly here as we don't have access + // to the input type generic. Maybe not a deal breaker though. + // It means we have to override the factory function in the `.make` method instead. + loader: (opts: unknown) => Promise; + routeRef?: RouteRef; + }, + { config, inputs, node }, + ) { + const ExtensionComponent = lazy(() => + loader({ config, inputs }).then(element => ({ default: () => element })), + ); + + // TODO(blam): this is a little awkward for optional returns. I wonder if we should be using generators or yield instead + // for a better API here. + const outputs = [ + coreExtensionData.routePath(config.path ?? defaultPath!), + coreExtensionData.reactElement( + + + , + ), + ]; + + if (routeRef) { + return [...outputs, coreExtensionData.routeRef(routeRef)]; + } + + return outputs; + }, +}); diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index b13c2a2e0e..109eea333e 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -25,6 +25,10 @@ import { createRouteRef, } from '@backstage/frontend-plugin-api'; import { compatWrapper } from '@backstage/core-compat-api'; +import { + PageExtensionBlueprint, + createNewPageExtension, +} from '@backstage/frontend-plugin-api/src/extensions/createPageExtension'; const rootRouteRef = createRouteRef(); @@ -35,15 +39,15 @@ export const titleExtensionDataRef = createExtensionDataRef().with({ id: 'title', }); -const homePage = createPageExtension({ +const homePage = createNewPageExtension({ defaultPath: '/home', routeRef: rootRouteRef, inputs: { props: createExtensionInput( - { - children: coreExtensionData.reactElement.optional(), - title: titleExtensionDataRef.optional(), - }, + [ + coreExtensionData.reactElement.optional(), + titleExtensionDataRef.optional(), + ], { singleton: true, @@ -55,13 +59,53 @@ const homePage = createPageExtension({ import('./components/').then(m => compatWrapper( , ), ), }); +homePage.override({ + factory() {}, +}); + +// const homePage2 = PageExtensionBlueprint.make({ +// inputs: { +// props: createExtensionInput( +// [ +// coreExtensionData.reactElement.optional(), +// titleExtensionDataRef.optional(), +// ], +// { +// singleton: true, +// optional: true, +// }, +// ), +// }, +// factory(origFactory, { config, inputs, node }) { +// return origFactory({ +// defaultPath: '/home', +// routeRef: rootRouteRef, +// loader: () => +// import('./components/').then(m => +// compatWrapper( +// , +// ), +// }, +// }); + +/** + * @alpha + * This will not be the way to export extensions eventually, + * will be something like `homePlugin.extensions.homePage` or `homePlugin.extensions.get('page')` + * Just haven't worked out a nice way to fix the types yet + */ +export const extensions = { homePage }; + /** * @alpha */ From f30705fc23121030aa875f11b2d0bf12c04cf795 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 31 Jul 2024 14:00:06 +0200 Subject: [PATCH 02/15] chore: some more exploration Signed-off-by: blam --- packages/app-next/src/App.tsx | 4 +- .../src/extensions/createPageExtension.tsx | 2 +- .../src/wiring/createExtension.ts | 94 ++++++++++++++++++- plugins/home/src/alpha.tsx | 4 - 4 files changed, 95 insertions(+), 9 deletions(-) diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 25d1aacf8f..a33b2de8dc 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -115,8 +115,8 @@ TODO: // }); const homePageExtension = extensions.homePage.override({ - factory() { - return { children: homePage, title: 'just a title' }; + factory(origi) { + return origi(); }, }); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index b10901f1bd..efe6ab54d9 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -127,7 +127,7 @@ export function createNewPageExtension< inputs: Expand>; }) => Promise; }, -): ExtensionDefinition { +) { const configSchema = 'configSchema' in options ? options.configSchema diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 2a29fe1ab4..6684323b51 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -20,6 +20,7 @@ import { Expand } from '../types'; import { AnyExtensionDataRef, ExtensionDataRef, + ExtensionDataRefToValue, ExtensionDataValue, } from './createExtensionDataRef'; import { ExtensionInput, LegacyExtensionInput } from './createExtensionInput'; @@ -234,6 +235,79 @@ export interface ExtensionDefinition { readonly configSchema?: PortableSchema; } +export type OverrideExtensionOptions< + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TConfig, + TConfigInput, + TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, + UFactoryOutput extends ExtensionDataValue, +> = { + kind?: string; + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + output: Array; + /** @deprecated - use `config.schema` instead */ + configSchema?: PortableSchema; + config?: { + schema: TConfigSchema; + }; + factory?( + originalFactory: (context?: { + config?: { + [key in keyof TConfigSchema]: z.infer>; + }; + inputs?: Expand>; + }) => Iterable>, + context: { + node: AppNode; + config: TConfig & + (string extends keyof TConfigSchema + ? {} + : { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }); + inputs: Expand>; + }, + ): Iterable; +} & VerifyExtensionFactoryOutput; + +/** @public */ +export type OverridableExtension< + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TConfig, + TConfigInput, + TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, + UFactoryOutput extends ExtensionDataValue, +> = { + override( + options: OverrideExtensionOptions< + UOutput, + TInputs, + TConfig, + TConfigInput, + TConfigSchema, + UFactoryOutput + >, + ): ExtensionDefinition; +}; + /** @internal */ export type InternalExtensionDefinition = ExtensionDefinition & @@ -329,7 +403,15 @@ export function createExtension< [key in keyof TConfigSchema]: ReturnType; }> >) ->; +> & + OverridableExtension< + UOutput, + TInputs, + TConfig, + TConfigInput, + TConfigSchema, + UFactoryOutput + >; /** * @public * @deprecated - use the array format of `output` instead, see TODO-doc-link @@ -409,7 +491,15 @@ export function createExtension< [key in keyof TConfigSchema]: ReturnType; }> >) -> { +> & + OverridableExtension< + UOutput, + TInputs, + TConfig, + TConfigInput, + TConfigSchema, + UFactoryOutput + > { const newConfigSchema = options.config?.schema; if (newConfigSchema && options.configSchema) { throw new Error(`Cannot provide both configSchema and config.schema`); diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index 109eea333e..31851726e5 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -66,10 +66,6 @@ const homePage = createNewPageExtension({ ), }); -homePage.override({ - factory() {}, -}); - // const homePage2 = PageExtensionBlueprint.make({ // inputs: { // props: createExtensionInput( From 665448b21fe97ed448894ddd226b850ad95b6647 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 31 Jul 2024 15:01:22 +0200 Subject: [PATCH 03/15] chore: overriding of config seems to work in typescript Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 31 +++++++++++++++++++ .../src/wiring/createExtension.ts | 26 +++++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index ab6a6c19a5..8da3504434 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -561,4 +561,35 @@ describe('createExtension', () => { }), ).toMatchObject({ version: 'v2' }); }); + + describe('overrides', () => { + it('should allow overriding of the default factory', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + config: { + schema: { + foo: z => z.string().optional(), + }, + }, + factory() { + return [stringDataRef('default')]; + }, + }); + + testExtension.override({ + config: { + schema: { + bar: z => z.string().optional(), + }, + }, + factory(_, { config }) { + return [stringDataRef(config.foo ?? config.bar ?? 'default')]; + }, + }); + + expect(true).toBe(true); + }); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 6684323b51..f1db16b4d2 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -247,6 +247,10 @@ export type OverrideExtensionOptions< TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, + TConfigSchemaOverrides extends { + [key: string]: (zImpl: typeof z) => z.ZodType; + }, + UOutputOverrides extends AnyExtensionDataRef = UOutput, > = { kind?: string; namespace?: string; @@ -254,11 +258,11 @@ export type OverrideExtensionOptions< attachTo?: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; - output: Array; + output?: Array; /** @deprecated - use `config.schema` instead */ configSchema?: PortableSchema; config?: { - schema: TConfigSchema; + schema: TConfigSchemaOverrides; }; factory?( originalFactory: (context?: { @@ -273,6 +277,10 @@ export type OverrideExtensionOptions< (string extends keyof TConfigSchema ? {} : { + [key in keyof TConfigSchemaOverrides]: z.infer< + ReturnType + >; + } & { [key in keyof TConfigSchema]: z.infer< ReturnType >; @@ -280,7 +288,7 @@ export type OverrideExtensionOptions< inputs: Expand>; }, ): Iterable; -} & VerifyExtensionFactoryOutput; +} & VerifyExtensionFactoryOutput; /** @public */ export type OverridableExtension< @@ -296,14 +304,19 @@ export type OverridableExtension< TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, > = { - override( + override< + TConfigSchemaOverrides extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + >( options: OverrideExtensionOptions< UOutput, TInputs, TConfig, TConfigInput, TConfigSchema, - UFactoryOutput + UFactoryOutput, + TConfigSchemaOverrides >, ): ExtensionDefinition; }; @@ -540,6 +553,9 @@ export function createExtension< parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`); return `ExtensionDefinition{${parts.join(',')}}`; }, + override() { + // todo(blam): implement. + }, } as InternalExtensionDefinition< TConfig & (string extends keyof TConfigSchema From 40ffc63b7016cdc35d3a2c9d3ecdebf75b152916 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 31 Jul 2024 15:30:33 +0200 Subject: [PATCH 04/15] chore: added some more overrides part Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 52 ++++++++++++++++++- .../src/wiring/createExtension.ts | 23 +++++--- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 8da3504434..7f27df4947 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { coreExtensionData } from './coreExtensionData'; import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; @@ -563,7 +564,7 @@ describe('createExtension', () => { }); describe('overrides', () => { - it('should allow overriding of the default factory', () => { + it('should allow overriding of config and merging', () => { const testExtension = createExtension({ namespace: 'test', attachTo: { id: 'root', input: 'blob' }, @@ -591,5 +592,54 @@ describe('createExtension', () => { expect(true).toBe(true); }); + + it('should allow overriding of outputs', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + inputs: { + test: createExtensionInput([stringDataRef], { singleton: true }), + }, + config: { + schema: { + foo: z => z.string().optional(), + }, + }, + factory({ inputs }) { + return [stringDataRef(inputs.test.get(stringDataRef))]; + }, + }); + + const override = testExtension.override({ + output: [numberDataRef], + factory(_, { inputs }) { + return [ + numberDataRef(inputs.test.get(stringDataRef).length), + stringDataRef('default'), + ]; + }, + }); + + // @ts-expect-error - should fail as the string output from previous is not provided + const override2 = testExtension.override({ + output: [numberDataRef], + factory(_, { inputs }) { + return [numberDataRef(inputs.test.get(stringDataRef).length)]; + }, + }); + + // @ts-expect-error - this should fail because string output should be merged? + const override3 = testExtension.override({ + output: [numberDataRef], + factory(_, { inputs }) { + return [stringDataRef(inputs.test.get(stringDataRef))]; + }, + }); + + unused(override, override2, override3); + + expect(true).toBe(true); + }); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index f1db16b4d2..3ec674f75f 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -246,11 +246,12 @@ export type OverrideExtensionOptions< TConfig, TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, - UFactoryOutput extends ExtensionDataValue, + UOriginalFactoryOutput extends ExtensionDataValue, TConfigSchemaOverrides extends { [key: string]: (zImpl: typeof z) => z.ZodType; }, - UOutputOverrides extends AnyExtensionDataRef = UOutput, + UOutputOverrides extends AnyExtensionDataRef, + UFactoryOverrideOutput extends ExtensionDataValue, > = { kind?: string; namespace?: string; @@ -262,7 +263,10 @@ export type OverrideExtensionOptions< /** @deprecated - use `config.schema` instead */ configSchema?: PortableSchema; config?: { - schema: TConfigSchemaOverrides; + schema: TConfigSchemaOverrides & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; }; factory?( originalFactory: (context?: { @@ -270,7 +274,7 @@ export type OverrideExtensionOptions< [key in keyof TConfigSchema]: z.infer>; }; inputs?: Expand>; - }) => Iterable>, + }) => Iterable, context: { node: AppNode; config: TConfig & @@ -287,8 +291,9 @@ export type OverrideExtensionOptions< }); inputs: Expand>; }, - ): Iterable; -} & VerifyExtensionFactoryOutput; + ): Iterable; + // todo(blam): need to verify that the outputs are meged properly. +} & VerifyExtensionFactoryOutput; /** @public */ export type OverridableExtension< @@ -308,6 +313,8 @@ export type OverridableExtension< TConfigSchemaOverrides extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, + UOutputOverrides extends AnyExtensionDataRef, + UFactoryOverrideOutput extends ExtensionDataValue, >( options: OverrideExtensionOptions< UOutput, @@ -316,7 +323,9 @@ export type OverridableExtension< TConfigInput, TConfigSchema, UFactoryOutput, - TConfigSchemaOverrides + TConfigSchemaOverrides, + UOutputOverrides, + UFactoryOverrideOutput >, ): ExtensionDefinition; }; From 33786b586e08ef27cea7beb4a1c6062bacc4fb89 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 31 Jul 2024 15:33:44 +0200 Subject: [PATCH 05/15] chore: reset some things Signed-off-by: blam --- packages/app-next/src/App.tsx | 52 +++---------- .../src/extensions/createPageExtension.tsx | 78 +------------------ plugins/home/src/alpha.tsx | 54 ++----------- 3 files changed, 18 insertions(+), 166 deletions(-) diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index a33b2de8dc..b57c680474 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -20,7 +20,6 @@ import { pagesPlugin } from './examples/pagesPlugin'; import notFoundErrorPage from './examples/notFoundErrorPageExtension'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; import homePlugin, { - extensions, titleExtensionDataRef, } from '@backstage/plugin-home/alpha'; @@ -75,48 +74,15 @@ TODO: /* app.tsx */ -// const homePageExtension = createExtension({ -// name: 'myhomepage', -// attachTo: { id: 'page:home', input: 'props' }, -// output: { -// children: coreExtensionData.reactElement, -// title: titleExtensionDataRef, -// }, -// factory() { -// return { children: homePage, title: 'just a title' }; -// }, -// }); - -// const homePage = createPageExtension({ -// defaultPath: '/home', -// routeRef: rootRouteRef, -// inputs: { -// props: createExtensionInput( -// { -// children: coreExtensionData.reactElement.optional(), -// title: titleExtensionDataRef.optional(), -// }, - -// { -// singleton: true, -// optional: true, -// }, -// ), -// }, -// loader: ({ inputs }) => -// import('./components/').then(m => -// compatWrapper( -// , -// ), -// ), -// }); - -const homePageExtension = extensions.homePage.override({ - factory(origi) { - return origi(); +const homePageExtension = createExtension({ + name: 'myhomepage', + attachTo: { id: 'page:home', input: 'props' }, + output: { + children: coreExtensionData.reactElement, + title: titleExtensionDataRef, + }, + factory() { + return { children: homePage, title: 'just a title' }; }, }); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index efe6ab54d9..27b6b53b63 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -23,8 +23,6 @@ import { ResolvedExtensionInputs, AnyExtensionInputMap, createExtensionBlueprint, - ExtensionInput, - AnyExtensionDataRef, } from '../wiring'; import { RouteRef } from '../routing'; import { Expand } from '../types'; @@ -99,79 +97,6 @@ export function createPageExtension< }); } -export function createNewPageExtension< - TConfig extends { path: string }, - TInputs extends { - [inputName in string]: ExtensionInput< - AnyExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, ->( - options: ( - | { - defaultPath: string; - } - | { - configSchema: PortableSchema; - } - ) & { - namespace?: string; - name?: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - routeRef?: RouteRef; - loader: (options: { - config: TConfig; - inputs: Expand>; - }) => Promise; - }, -) { - const configSchema = - 'configSchema' in options - ? options.configSchema - : (createSchemaFromZod(z => - z.object({ path: z.string().default(options.defaultPath) }), - ) as PortableSchema); - - return createExtension({ - kind: 'page', - namespace: options.namespace, - name: options.name, - attachTo: options.attachTo ?? { id: 'app/routes', input: 'routes' }, - configSchema, - inputs: options.inputs, - disabled: options.disabled, - output: [ - coreExtensionData.routePath, - coreExtensionData.reactElement, - coreExtensionData.routeRef.optional(), - ], - factory({ config, inputs, node }) { - const ExtensionComponent = lazy(() => - options - .loader({ config, inputs }) - .then(element => ({ default: () => element })), - ); - - const outputs = [ - coreExtensionData.routePath(config.path), - coreExtensionData.reactElement( - - - , - ), - ]; - - if (options.routeRef) { - return [...outputs, coreExtensionData.routeRef(options.routeRef)]; - } - - return outputs; - }, - }); -} /** * A blueprint for creating extensions for routable React page components. * @public @@ -208,7 +133,8 @@ export const PageExtensionBlueprint = createExtensionBlueprint({ loader({ config, inputs }).then(element => ({ default: () => element })), ); - // TODO(blam): this is a little awkward for optional returns. I wonder if we should be using generators or yield instead + // TODO(blam): this is a little awkward for optional returns. + // I wonder if we should be using generators or yield instead // for a better API here. const outputs = [ coreExtensionData.routePath(config.path ?? defaultPath!), diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index 31851726e5..b13c2a2e0e 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -25,10 +25,6 @@ import { createRouteRef, } from '@backstage/frontend-plugin-api'; import { compatWrapper } from '@backstage/core-compat-api'; -import { - PageExtensionBlueprint, - createNewPageExtension, -} from '@backstage/frontend-plugin-api/src/extensions/createPageExtension'; const rootRouteRef = createRouteRef(); @@ -39,15 +35,15 @@ export const titleExtensionDataRef = createExtensionDataRef().with({ id: 'title', }); -const homePage = createNewPageExtension({ +const homePage = createPageExtension({ defaultPath: '/home', routeRef: rootRouteRef, inputs: { props: createExtensionInput( - [ - coreExtensionData.reactElement.optional(), - titleExtensionDataRef.optional(), - ], + { + children: coreExtensionData.reactElement.optional(), + title: titleExtensionDataRef.optional(), + }, { singleton: true, @@ -59,49 +55,13 @@ const homePage = createNewPageExtension({ import('./components/').then(m => compatWrapper( , ), ), }); -// const homePage2 = PageExtensionBlueprint.make({ -// inputs: { -// props: createExtensionInput( -// [ -// coreExtensionData.reactElement.optional(), -// titleExtensionDataRef.optional(), -// ], -// { -// singleton: true, -// optional: true, -// }, -// ), -// }, -// factory(origFactory, { config, inputs, node }) { -// return origFactory({ -// defaultPath: '/home', -// routeRef: rootRouteRef, -// loader: () => -// import('./components/').then(m => -// compatWrapper( -// , -// ), -// }, -// }); - -/** - * @alpha - * This will not be the way to export extensions eventually, - * will be something like `homePlugin.extensions.homePage` or `homePlugin.extensions.get('page')` - * Just haven't worked out a nice way to fix the types yet - */ -export const extensions = { homePage }; - /** * @alpha */ From 3ac12d718954a906c1d07f66d7d894cd3978059c Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 31 Jul 2024 17:02:53 +0200 Subject: [PATCH 06/15] chore: some more tests and comments Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 30 ++++++++++++++++++- .../src/wiring/createExtension.ts | 9 ++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 7f27df4947..e38a24cebe 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -599,7 +599,9 @@ describe('createExtension', () => { attachTo: { id: 'root', input: 'blob' }, output: [stringDataRef], inputs: { - test: createExtensionInput([stringDataRef], { singleton: true }), + test: createExtensionInput([stringDataRef], { + singleton: true, + }), }, config: { schema: { @@ -641,5 +643,31 @@ describe('createExtension', () => { expect(true).toBe(true); }); + + it('should allow overriding the factory function and calling the original factory', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + config: { + schema: { + foo: z => z.string().optional(), + }, + }, + factory() { + return [stringDataRef('default')]; + }, + }); + + testExtension.override({ + factory(originalFactory) { + const response = originalFactory(); + + const foo: string = response.get(stringDataRef); + + return [stringDataRef(`foo-${foo}-override`)]; + }, + }); + }); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 3ec674f75f..678ce4e042 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -274,6 +274,8 @@ export type OverrideExtensionOptions< [key in keyof TConfigSchema]: z.infer>; }; inputs?: Expand>; + // todo(blam): Think this is better as a DataContainer instead + // should probably update that everywhere. }) => Iterable, context: { node: AppNode; @@ -292,8 +294,11 @@ export type OverrideExtensionOptions< inputs: Expand>; }, ): Iterable; - // todo(blam): need to verify that the outputs are meged properly. -} & VerifyExtensionFactoryOutput; + // todo(blam): need to verify that the outputs are merged and verified properly. +} & VerifyExtensionFactoryOutput< + UOutput & UFactoryOverrideOutput, + UFactoryOverrideOutput +>; /** @public */ export type OverridableExtension< From 45eb5b437a690b6a0d33fea3174d53ed02f88380 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 09:41:40 +0200 Subject: [PATCH 07/15] chore: got the data container working nicely Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 3 ++ .../src/wiring/createExtension.ts | 28 +++---------------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index e38a24cebe..c688f92004 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -665,6 +665,9 @@ describe('createExtension', () => { const foo: string = response.get(stringDataRef); + // @ts-expect-error - fails because original factory does not return number + const number: boolean = response.get(numberDataRef); + return [stringDataRef(`foo-${foo}-override`)]; }, }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 678ce4e042..87a4342df7 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -20,7 +20,6 @@ import { Expand } from '../types'; import { AnyExtensionDataRef, ExtensionDataRef, - ExtensionDataRefToValue, ExtensionDataValue, } from './createExtensionDataRef'; import { ExtensionInput, LegacyExtensionInput } from './createExtensionInput'; @@ -246,7 +245,6 @@ export type OverrideExtensionOptions< TConfig, TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, - UOriginalFactoryOutput extends ExtensionDataValue, TConfigSchemaOverrides extends { [key: string]: (zImpl: typeof z) => z.ZodType; }, @@ -274,9 +272,7 @@ export type OverrideExtensionOptions< [key in keyof TConfigSchema]: z.infer>; }; inputs?: Expand>; - // todo(blam): Think this is better as a DataContainer instead - // should probably update that everywhere. - }) => Iterable, + }) => ExtensionDataContainer, context: { node: AppNode; config: TConfig & @@ -296,7 +292,7 @@ export type OverrideExtensionOptions< ): Iterable; // todo(blam): need to verify that the outputs are merged and verified properly. } & VerifyExtensionFactoryOutput< - UOutput & UFactoryOverrideOutput, + UOutput & UOutputOverrides, UFactoryOverrideOutput >; @@ -312,7 +308,6 @@ export type OverridableExtension< TConfig, TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, - UFactoryOutput extends ExtensionDataValue, > = { override< TConfigSchemaOverrides extends { @@ -327,7 +322,6 @@ export type OverridableExtension< TConfig, TConfigInput, TConfigSchema, - UFactoryOutput, TConfigSchemaOverrides, UOutputOverrides, UFactoryOverrideOutput @@ -431,14 +425,7 @@ export function createExtension< }> >) > & - OverridableExtension< - UOutput, - TInputs, - TConfig, - TConfigInput, - TConfigSchema, - UFactoryOutput - >; + OverridableExtension; /** * @public * @deprecated - use the array format of `output` instead, see TODO-doc-link @@ -519,14 +506,7 @@ export function createExtension< }> >) > & - OverridableExtension< - UOutput, - TInputs, - TConfig, - TConfigInput, - TConfigSchema, - UFactoryOutput - > { + OverridableExtension { const newConfigSchema = options.config?.schema; if (newConfigSchema && options.configSchema) { throw new Error(`Cannot provide both configSchema and config.schema`); From 8d631773468d3d852335d918d7f43599be1d2a56 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 11:07:38 +0200 Subject: [PATCH 08/15] chore: added some tests Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 37 ++++++++ .../src/wiring/createExtension.ts | 86 ++++++++++++++++++- .../wiring/createExtensionOverrides.test.ts | 3 + 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index c688f92004..432fb7f89e 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { createExtensionTester } from '@backstage/frontend-test-utils'; import { coreExtensionData } from './coreExtensionData'; import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; @@ -671,6 +672,42 @@ describe('createExtension', () => { return [stringDataRef(`foo-${foo}-override`)]; }, }); + + expect(true).toBe(true); + }); + + it('should work functionally with overrides', () => { + const testExtension = createExtension({ + kind: 'thing', + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + config: { + schema: { + foo: z => z.string().default('boom'), + }, + }, + factory({ config }) { + return [stringDataRef(config.foo)]; + }, + }); + + const overriden = testExtension.override({ + config: { + schema: { + bar: z => z.string().optional(), + }, + }, + factory(originalFactory, { config }) { + const response = originalFactory(); + + const foo: string = response.get(stringDataRef); + + return [stringDataRef(`foo-${foo}-override-${config.bar}`)]; + }, + }); + + const tester = createExtensionTester(overriden).render(); }); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 87a4342df7..200d3d1133 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { over } from 'lodash'; import { AppNode } from '../apis'; import { PortableSchema, createSchemaFromZod } from '../schema'; import { Expand } from '../types'; @@ -547,8 +548,89 @@ export function createExtension< parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`); return `ExtensionDefinition{${parts.join(',')}}`; }, - override() { - // todo(blam): implement. + override< + TConfigSchemaOverrides extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UOutputOverrides extends AnyExtensionDataRef, + UFactoryOverrideOutput extends ExtensionDataValue, + >( + overrideOptions: OverrideExtensionOptions< + UOutput, + TInputs, + TConfig, + TConfigInput, + TConfigSchema, + TConfigSchemaOverrides, + UOutputOverrides, + UFactoryOverrideOutput + >, + ) { + const overrideNewConfigSchema = overrideOptions.config?.schema; + if (overrideNewConfigSchema && overrideOptions.configSchema) { + throw new Error(`Cannot provide both configSchema and config.schema`); + } + + const mergedConfigSchema = { + ...options.config?.schema, + ...overrideOptions.config?.schema, + }; + + const overrideConfigSchema = mergedConfigSchema + ? createSchemaFromZod(innerZ => + innerZ.object( + Object.fromEntries( + Object.entries(mergedConfigSchema).map(([k, v]) => [ + k, + v(innerZ), + ]), + ), + ), + ) + : overrideOptions.configSchema; + + const buildDataContainer = (outputs): ExtensionDataContainer => { + const dataMap = new Map(); + if (Symbol.iterator in outputs) { + for (const output of outputs) { + dataMap.set(output.id, output.value); + } + } + + return { + get(ref) { + return dataMap.get(ref.id); + }, + } as ExtensionDataContainer; + }; + + return createExtension({ + attachTo: options.attachTo, + output: overrideOptions.output ?? options.output, + configSchema: overrideConfigSchema, + factory: ({ node, config, inputs }) => { + if (overrideOptions.factory) { + return overrideOptions.factory( + innerCtx => { + const originalFactoryResponse = options.factory({ + node, + config: innerCtx?.config ?? config, + inputs: innerCtx?.inputs ?? inputs, + }); + + return buildDataContainer(originalFactoryResponse); + }, + { node, config, inputs }, + ); + } + return options.factory(originalFactory, context); + }, + disabled: overrideOptions.disabled ?? options.disabled, + inputs: overrideOptions.inputs ?? options.inputs, + kind: overrideOptions.kind ?? options.kind, + name: overrideOptions.name ?? options.name, + namespace: overrideOptions.namespace ?? options.namespace, + }); }, } as InternalExtensionDefinition< TConfig & diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts index aa00247222..555cd82d4c 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts @@ -75,6 +75,7 @@ describe('createExtensionOverrides', () => { "id": "a", "inputs": {}, "output": {}, + "override": [Function], "toString": [Function], "version": "v1", }, @@ -90,6 +91,7 @@ describe('createExtensionOverrides', () => { "id": "b", "inputs": {}, "output": {}, + "override": [Function], "toString": [Function], "version": "v1", }, @@ -105,6 +107,7 @@ describe('createExtensionOverrides', () => { "id": "k:c/n", "inputs": {}, "output": {}, + "override": [Function], "toString": [Function], "version": "v1", }, From 4f446c44f627f2081bd373727a8defef8f2113fa Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 12:06:16 +0200 Subject: [PATCH 09/15] chore: test with the extension tester Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 432fb7f89e..6aa172cc5a 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -695,7 +695,7 @@ describe('createExtension', () => { const overriden = testExtension.override({ config: { schema: { - bar: z => z.string().optional(), + bar: z => z.string().default('hello'), }, }, factory(originalFactory, { config }) { @@ -707,7 +707,15 @@ describe('createExtension', () => { }, }); - const tester = createExtensionTester(overriden).render(); + expect(createExtensionTester(overriden).data(stringDataRef)).toBe( + 'foo-boom-override-hello', + ); + + expect( + createExtensionTester(overriden, { + config: { foo: 'hello', bar: 'world' }, + }).data(stringDataRef), + ).toBe('foo-hello-override-world'); }); }); }); From 4a6d9dc85e2042f854d7999e1a08e98ddc43ad53 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 13:46:57 +0200 Subject: [PATCH 10/15] chore: reset page extension Signed-off-by: blam --- .../src/extensions/createPageExtension.tsx | 57 ------------------- 1 file changed, 57 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index 27b6b53b63..cd434a3ffc 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -22,7 +22,6 @@ import { createExtension, ResolvedExtensionInputs, AnyExtensionInputMap, - createExtensionBlueprint, } from '../wiring'; import { RouteRef } from '../routing'; import { Expand } from '../types'; @@ -96,59 +95,3 @@ export function createPageExtension< }, }); } - -/** - * A blueprint for creating extensions for routable React page components. - * @public - */ -export const PageExtensionBlueprint = createExtensionBlueprint({ - kind: 'page', - attachTo: { id: 'app/routes', input: 'routes' }, - output: [ - coreExtensionData.routePath, - coreExtensionData.reactElement, - coreExtensionData.routeRef.optional(), - ], - config: { - schema: { - path: z => z.string().optional(), - }, - }, - factory( - { - defaultPath, - loader, - routeRef, - }: { - defaultPath?: string; - // TODO(blam) This type is impossible to type properly here as we don't have access - // to the input type generic. Maybe not a deal breaker though. - // It means we have to override the factory function in the `.make` method instead. - loader: (opts: unknown) => Promise; - routeRef?: RouteRef; - }, - { config, inputs, node }, - ) { - const ExtensionComponent = lazy(() => - loader({ config, inputs }).then(element => ({ default: () => element })), - ); - - // TODO(blam): this is a little awkward for optional returns. - // I wonder if we should be using generators or yield instead - // for a better API here. - const outputs = [ - coreExtensionData.routePath(config.path ?? defaultPath!), - coreExtensionData.reactElement( - - - , - ), - ]; - - if (routeRef) { - return [...outputs, coreExtensionData.routeRef(routeRef)]; - } - - return outputs; - }, -}); From 9fdbbd0889ec6af08e21b4dc7092cedbe7fac3bf Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 17:11:24 +0200 Subject: [PATCH 11/15] chore: more work towards supporting extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Signed-off-by: blam --- .../src/extensions/createApiExtension.test.ts | 2 + .../createAppRootElementExtension.test.tsx | 2 + .../createAppRootWrapperExtension.test.tsx | 2 + .../createNavLogoExtension.test.tsx | 1 + .../extensions/createPageExtension.test.tsx | 3 + .../extensions/createRouterExtension.test.tsx | 2 + .../createTranslationExtension.test.ts | 3 + .../src/wiring/createExtension.test.ts | 28 +- .../src/wiring/createExtension.ts | 467 +++++++++--------- .../wiring/createExtensionBlueprint.test.tsx | 3 + .../src/wiring/createExtensionBlueprint.ts | 17 +- 11 files changed, 250 insertions(+), 280 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts index c4d0239094..8f8a0cd9ac 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts @@ -48,6 +48,7 @@ describe('createApiExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); }); @@ -85,6 +86,7 @@ describe('createApiExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx index c8bf6ee355..6e80f6fe13 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx @@ -41,6 +41,7 @@ describe('createAppRootElementExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); createExtensionTester(extension).render(); @@ -88,6 +89,7 @@ describe('createAppRootElementExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); createExtensionTester(extension, { config: { name: 'Robin' } }) diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx index d21cfa7583..993c4fdee3 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx @@ -42,6 +42,7 @@ describe('createAppRootWrapperExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); createExtensionTester( @@ -94,6 +95,7 @@ describe('createAppRootWrapperExtension', () => { component: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx index 8c54743f1d..17187a339d 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx @@ -41,6 +41,7 @@ describe('createNavLogoExtension', () => { logos: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx index 374400fce8..e5dd4027c0 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -56,6 +56,7 @@ describe('createPageExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); expect( @@ -78,6 +79,7 @@ describe('createPageExtension', () => { kind: 'page', attachTo: { id: 'other', input: 'place' }, configSchema: expect.anything(), + override: expect.any(Function), disabled: true, inputs: { first: createExtensionInput({ @@ -115,6 +117,7 @@ describe('createPageExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx index 7109e1a8aa..2c16076fcf 100644 --- a/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx @@ -50,6 +50,7 @@ describe('createRouterExtension', () => { component: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); @@ -119,6 +120,7 @@ describe('createRouterExtension', () => { component: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts index 975901b8e5..019c565cbd 100644 --- a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts @@ -46,6 +46,7 @@ describe('createTranslationExtension', () => { namespace: 'test', attachTo: { id: 'app', input: 'translations' }, disabled: false, + override: expect.any(Function), inputs: {}, output: { resource: createTranslationExtension.translationDataRef, @@ -84,6 +85,7 @@ describe('createTranslationExtension', () => { namespace: 'test', attachTo: { id: 'app', input: 'translations' }, disabled: false, + override: expect.any(Function), inputs: {}, output: { resource: createTranslationExtension.translationDataRef, @@ -120,6 +122,7 @@ describe('createTranslationExtension', () => { version: 'v1', kind: 'translation', namespace: 'test', + override: expect.any(Function), name: 'sv', attachTo: { id: 'app', input: 'translations' }, disabled: false, diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 6aa172cc5a..30375049bf 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -15,7 +15,6 @@ */ import { createExtensionTester } from '@backstage/frontend-test-utils'; -import { coreExtensionData } from './coreExtensionData'; import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; @@ -306,9 +305,7 @@ describe('createExtension', () => { baz: z => z.string().optional(), }, }, - output: { - foo: stringDataRef, - }, + output: [stringDataRef], factory({ config }) { const a1: string = config.foo; const a2: string = config.bar; @@ -322,12 +319,10 @@ describe('createExtension', () => { const c3: number = config.baz; unused(a1, a2, a3, c1, c2, c3); - return { - foo: 'bar', - }; + return [stringDataRef('bar')]; }, }); - expect(extension).toMatchObject({ version: 'v1', namespace: 'test' }); + expect(extension).toMatchObject({ version: 'v2', namespace: 'test' }); expect(String(extension)).toBe( 'ExtensionDefinition{namespace=test,attachTo=root@default}', ); @@ -614,18 +609,7 @@ describe('createExtension', () => { }, }); - const override = testExtension.override({ - output: [numberDataRef], - factory(_, { inputs }) { - return [ - numberDataRef(inputs.test.get(stringDataRef).length), - stringDataRef('default'), - ]; - }, - }); - - // @ts-expect-error - should fail as the string output from previous is not provided - const override2 = testExtension.override({ + const override1 = testExtension.override({ output: [numberDataRef], factory(_, { inputs }) { return [numberDataRef(inputs.test.get(stringDataRef).length)]; @@ -633,14 +617,14 @@ describe('createExtension', () => { }); // @ts-expect-error - this should fail because string output should be merged? - const override3 = testExtension.override({ + const override2 = testExtension.override({ output: [numberDataRef], factory(_, { inputs }) { return [stringDataRef(inputs.test.get(stringDataRef))]; }, }); - unused(override, override2, override3); + unused(override1, override2); expect(true).toBe(true); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 200d3d1133..6e75fc76e5 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { over } from 'lodash'; import { AppNode } from '../apis'; import { PortableSchema, createSchemaFromZod } from '../schema'; import { Expand } from '../types'; +import { createDataContainer } from './createExtensionBlueprint'; import { AnyExtensionDataRef, ExtensionDataRef, @@ -128,7 +128,6 @@ export interface LegacyCreateExtensionOptions< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, > { kind?: string; namespace?: string; @@ -137,21 +136,10 @@ export interface LegacyCreateExtensionOptions< disabled?: boolean; inputs?: TInputs; output: TOutput; - /** @deprecated - use `config.schema` instead */ configSchema?: PortableSchema; - config?: { - schema: TConfigSchema; - }; factory(context: { node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); + config: TConfig; inputs: Expand>; }): Expand>; } @@ -193,8 +181,6 @@ export type CreateExtensionOptions< { optional: boolean; singleton: boolean } >; }, - TConfig, - TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, > = { @@ -205,27 +191,30 @@ export type CreateExtensionOptions< disabled?: boolean; inputs?: TInputs; output: Array; - /** @deprecated - use `config.schema` instead */ - configSchema?: PortableSchema; config?: { schema: TConfigSchema; }; factory(context: { node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); + config: { + [key in keyof TConfigSchema]: z.infer>; + }; inputs: Expand>; }): Iterable; } & VerifyExtensionFactoryOutput; /** @public */ -export interface ExtensionDefinition { +export interface ExtensionDefinition< + TConfig, + TConfigInput = TConfig, + UOutput extends AnyExtensionDataRef = AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + } = {}, +> { $$type: '@backstage/ExtensionDefinition'; readonly kind?: string; readonly namespace?: string; @@ -233,102 +222,71 @@ export interface ExtensionDefinition { readonly attachTo: { id: string; input: string }; readonly disabled: boolean; readonly configSchema?: PortableSchema; -} -export type OverrideExtensionOptions< - UOutput extends AnyExtensionDataRef, - TInputs extends { - [inputName in string]: ExtensionInput< - AnyExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, - TConfig, - TConfigInput, - TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, - TConfigSchemaOverrides extends { - [key: string]: (zImpl: typeof z) => z.ZodType; - }, - UOutputOverrides extends AnyExtensionDataRef, - UFactoryOverrideOutput extends ExtensionDataValue, -> = { - kind?: string; - namespace?: string; - name?: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - output?: Array; - /** @deprecated - use `config.schema` instead */ - configSchema?: PortableSchema; - config?: { - schema: TConfigSchemaOverrides & { - [KName in keyof TConfig]?: `Error: Config key '${KName & - string}' is already defined in parent schema`; - }; - }; - factory?( - originalFactory: (context?: { - config?: { - [key in keyof TConfigSchema]: z.infer>; - }; - inputs?: Expand>; - }) => ExtensionDataContainer, - context: { - node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchemaOverrides]: z.infer< - ReturnType - >; - } & { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); - inputs: Expand>; - }, - ): Iterable; - // todo(blam): need to verify that the outputs are merged and verified properly. -} & VerifyExtensionFactoryOutput< - UOutput & UOutputOverrides, - UFactoryOverrideOutput ->; - -/** @public */ -export type OverridableExtension< - UOutput extends AnyExtensionDataRef, - TInputs extends { - [inputName in string]: ExtensionInput< - AnyExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, - TConfig, - TConfigInput, - TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, -> = { override< - TConfigSchemaOverrides extends { + TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, - UOutputOverrides extends AnyExtensionDataRef, - UFactoryOverrideOutput extends ExtensionDataValue, + UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, >( - options: OverrideExtensionOptions< - UOutput, - TInputs, - TConfig, - TConfigInput, - TConfigSchema, - TConfigSchemaOverrides, - UOutputOverrides, - UFactoryOverrideOutput + args: { + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; + }; + factory( + originalFactory: (context?: { + config?: TConfig; + inputs?: Expand>; + }) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + } & VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput >, - ): ExtensionDefinition; -}; + ): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & TConfig, + z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + > & + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs + >; +} /** @internal */ export type InternalExtensionDefinition = @@ -397,36 +355,27 @@ export function createExtension< { optional: boolean; singleton: boolean } >; }, - TConfig, - TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, >( options: CreateExtensionOptions< UOutput, TInputs, - TConfig, - TConfigInput, TConfigSchema, UFactoryOutput >, ): ExtensionDefinition< - TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }), - TConfigInput & - (string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >) -> & - OverridableExtension; + { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >, + UOutput, + TInputs +>; /** * @public * @deprecated - use the array format of `output` instead, see TODO-doc-link @@ -436,31 +385,14 @@ export function createExtension< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, >( options: LegacyCreateExtensionOptions< TOutput, TInputs, TConfig, - TConfigInput, - TConfigSchema + TConfigInput >, -): ExtensionDefinition< - TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }), - TConfigInput & - (string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >) ->; +): ExtensionDefinition; export function createExtension< UOutput extends AnyExtensionDataRef, TInputs extends { @@ -476,20 +408,12 @@ export function createExtension< UFactoryOutput extends ExtensionDataValue, >( options: - | CreateExtensionOptions< - UOutput, - TInputs, - TConfig, - TConfigInput, - TConfigSchema, - UFactoryOutput - > + | CreateExtensionOptions | LegacyCreateExtensionOptions< AnyExtensionDataMap, TLegacyInputs, TConfig, - TConfigInput, - TConfigSchema + TConfigInput >, ): ExtensionDefinition< TConfig & @@ -505,22 +429,29 @@ export function createExtension< z.ZodObject<{ [key in keyof TConfigSchema]: ReturnType; }> - >) -> & - OverridableExtension { - const newConfigSchema = options.config?.schema; - if (newConfigSchema && options.configSchema) { + >), + UOutput, + TInputs +> { + if ('configSchema' in options && 'config' in options) { throw new Error(`Cannot provide both configSchema and config.schema`); } - const configSchema = newConfigSchema - ? createSchemaFromZod(innerZ => + let configSchema: PortableSchema | undefined; + if ('configSchema' in options) { + configSchema = options.configSchema; + } + if ('config' in options) { + const newConfigSchema = options.config?.schema; + configSchema = + newConfigSchema && + createSchemaFromZod(innerZ => innerZ.object( Object.fromEntries( Object.entries(newConfigSchema).map(([k, v]) => [k, v(innerZ)]), ), ), - ) - : options.configSchema; + ); + } return { $$type: '@backstage/ExtensionDefinition', @@ -548,89 +479,133 @@ export function createExtension< parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`); return `ExtensionDefinition{${parts.join(',')}}`; }, - override< - TConfigSchemaOverrides extends { + override: < + TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, - UOutputOverrides extends AnyExtensionDataRef, - UFactoryOverrideOutput extends ExtensionDataValue, - >( - overrideOptions: OverrideExtensionOptions< + UOverrideFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + >(overrideOptions: { + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs; + output?: Array; + config?: { + schema: TExtensionConfigSchema; + }; + factory( + originalFactory: (context?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; + }) => ExtensionDataContainer, + context: { + node: AppNode; + config: { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + }): ExtensionDefinition< + { + [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; + } + > + >, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs + > => { + if (!Array.isArray(options.output)) { + throw new Error( + 'Cannot override an extension that is not declared using the new format with outputs as an array', + ); + } + const newOptions = options as CreateExtensionOptions< UOutput, TInputs, - TConfig, - TConfigInput, TConfigSchema, - TConfigSchemaOverrides, - UOutputOverrides, - UFactoryOverrideOutput - >, - ) { + UFactoryOutput + >; const overrideNewConfigSchema = overrideOptions.config?.schema; - if (overrideNewConfigSchema && overrideOptions.configSchema) { - throw new Error(`Cannot provide both configSchema and config.schema`); - } - const mergedConfigSchema = { - ...options.config?.schema, - ...overrideOptions.config?.schema, - }; - - const overrideConfigSchema = mergedConfigSchema - ? createSchemaFromZod(innerZ => - innerZ.object( - Object.fromEntries( - Object.entries(mergedConfigSchema).map(([k, v]) => [ - k, - v(innerZ), - ]), - ), - ), - ) - : overrideOptions.configSchema; - - const buildDataContainer = (outputs): ExtensionDataContainer => { - const dataMap = new Map(); - if (Symbol.iterator in outputs) { - for (const output of outputs) { - dataMap.set(output.id, output.value); - } - } - - return { - get(ref) { - return dataMap.get(ref.id); - }, - } as ExtensionDataContainer; - }; + const schema = { + ...newOptions.config?.schema, + ...overrideNewConfigSchema, + } as TConfigSchema & TExtensionConfigSchema; return createExtension({ - attachTo: options.attachTo, - output: overrideOptions.output ?? options.output, - configSchema: overrideConfigSchema, + kind: newOptions.kind, + namespace: newOptions.namespace, + name: newOptions.name, + attachTo: overrideOptions.attachTo ?? newOptions.attachTo, + disabled: overrideOptions.disabled ?? newOptions.disabled, + inputs: { ...overrideOptions.inputs, ...newOptions.inputs }, + output: overrideOptions.output ?? newOptions.output, + config: Object.keys(schema).length === 0 ? undefined : { schema }, factory: ({ node, config, inputs }) => { - if (overrideOptions.factory) { - return overrideOptions.factory( - innerCtx => { - const originalFactoryResponse = options.factory({ - node, - config: innerCtx?.config ?? config, - inputs: innerCtx?.inputs ?? inputs, - }); - - return buildDataContainer(originalFactoryResponse); - }, - { node, config, inputs }, - ); + if (!overrideOptions.factory) { + return newOptions.factory({ + node, + config, + inputs: inputs as unknown as Expand< + ResolvedExtensionInputs + >, + }); } - return options.factory(originalFactory, context); + return overrideOptions.factory( + (innerContext?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; + }): ExtensionDataContainer => { + return createDataContainer( + newOptions.factory({ + node, + config: innerContext?.config ?? config, + inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden + }) as Iterable, + ); + }, + { + node, + config, + inputs, + }, + ); }, - disabled: overrideOptions.disabled ?? options.disabled, - inputs: overrideOptions.inputs ?? options.inputs, - kind: overrideOptions.kind ?? options.kind, - name: overrideOptions.name ?? options.name, - namespace: overrideOptions.namespace ?? options.namespace, - }); + } as CreateExtensionOptions); }, } as InternalExtensionDefinition< TConfig & diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 66c3939a96..924eba2cba 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -69,6 +69,7 @@ describe('createExtensionBlueprint', () => { output: [coreExtensionData.reactElement], factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), version: 'v2', }); @@ -108,6 +109,7 @@ describe('createExtensionBlueprint', () => { output: [coreExtensionData.reactElement], factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), version: 'v2', }); @@ -389,6 +391,7 @@ describe('createExtensionBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 88b2523111..a8bf42c7f8 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -171,7 +171,7 @@ export interface ExtensionBlueprint< } /** @internal */ -function createDataContainer( +export function createDataContainer( values: Iterable< UData extends ExtensionDataRef ? ExtensionDataValue @@ -241,10 +241,7 @@ class ExtensionBlueprintImpl< name?: string; attachTo?: { id: string; input: string }; disabled?: boolean; - inputs?: TExtraInputs & { - [KName in keyof TInputs]?: `Error: Input '${KName & - string}' is already defined in parent definition`; - }; + inputs?: TExtraInputs; output?: Array; params?: TParams; config?: { @@ -363,9 +360,7 @@ class ExtensionBlueprintImpl< } throw new Error('Either params or factory must be provided'); }, - } as CreateExtensionOptions< - UOutput, - TInputs & TExtraInputs, + } as CreateExtensionOptions) as ExtensionDefinition< { [key in keyof TExtensionConfigSchema]: z.infer< ReturnType @@ -383,10 +378,8 @@ class ExtensionBlueprintImpl< [key in keyof TConfigSchema]: ReturnType; } > - >, - TConfigSchema, - UFactoryOutput - >); + > + >; } } From feb5fc73c386997e014714c22f6ceacbaa3d2b01 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 17:30:47 +0200 Subject: [PATCH 12/15] chore: fix tests and update api-reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Signed-off-by: blam --- .../src/tree/instantiateAppNodeTree.test.ts | 14 +- .../src/tree/resolveAppNodeSpecs.test.ts | 1 + packages/frontend-plugin-api/api-report.md | 180 +++++++++++------- .../wiring/createExtensionOverrides.test.ts | 3 - .../wiring/resolveExtensionDefinition.test.ts | 2 + .../src/wiring/resolveExtensionDefinition.ts | 8 +- .../src/app/createExtensionTester.tsx | 2 +- plugins/catalog-react/api-report-alpha.md | 6 +- plugins/catalog/api-report-alpha.md | 2 +- plugins/search-react/api-report-alpha.md | 2 +- plugins/search/api-report-alpha.md | 11 +- plugins/techdocs/api-report-alpha.md | 4 +- plugins/user-settings/api-report-alpha.md | 4 +- 13 files changed, 146 insertions(+), 93 deletions(-) diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index de8889daf0..aded5d7826 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -68,7 +68,7 @@ function makeNode( function makeInstanceWithId( extension: Extension, - config?: TConfig, + config?: TConfigInput, ): AppNode { const node = makeNode(extension, { config }); return { @@ -640,12 +640,12 @@ describe('instantiateAppNodeTree', () => { name: 'test', attachTo: { id: 'ignored', input: 'ignored' }, output: [testDataRef, otherDataRef.optional()], - configSchema: createSchemaFromZod(z => - z.object({ - output: z.string().default('test'), - other: z.number().optional(), - }), - ), + config: { + schema: { + output: z => z.string().default('test'), + other: z => z.number().optional(), + }, + }, factory({ config }) { return [ testDataRef(config.output), diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts index 8af2aef070..65f3f77640 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts @@ -48,6 +48,7 @@ function makeExtDef( name, attachTo: { id: attachId, input: 'default' }, disabled: status === 'disabled', + override: () => ({} as ExtensionDefinition), } as ExtensionDefinition; } diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 4dc9c99c78..325dbd193e 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -396,7 +396,7 @@ export function createApiExtension< configSchema?: PortableSchema; inputs?: TInputs; }, -): ExtensionDefinition; +): ExtensionDefinition; // @public (undocumented) export namespace createApiExtension { @@ -492,7 +492,7 @@ export function createComponentExtension< inputs: Expand>; }) => ComponentType; }; -}): ExtensionDefinition; +}): ExtensionDefinition; // @public (undocumented) export namespace createComponentExtension { @@ -524,8 +524,6 @@ export function createExtension< } >; }, - TConfig, - TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType; }, @@ -534,26 +532,20 @@ export function createExtension< options: CreateExtensionOptions< UOutput, TInputs, - TConfig, - TConfigInput, TConfigSchema, UFactoryOutput >, ): ExtensionDefinition< - TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }), - TConfigInput & - (string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >) + { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >, + UOutput, + TInputs >; // @public @deprecated (undocumented) @@ -562,33 +554,14 @@ export function createExtension< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { - [key: string]: (zImpl: typeof z) => z.ZodType; - }, >( options: LegacyCreateExtensionOptions< TOutput, TInputs, TConfig, - TConfigInput, - TConfigSchema + TConfigInput >, -): ExtensionDefinition< - TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }), - TConfigInput & - (string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >) ->; +): ExtensionDefinition; // @public export function createExtensionBlueprint< @@ -751,8 +724,6 @@ export type CreateExtensionOptions< } >; }, - TConfig, - TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType; }, @@ -768,20 +739,14 @@ export type CreateExtensionOptions< disabled?: boolean; inputs?: TInputs; output: Array; - configSchema?: PortableSchema; config?: { schema: TConfigSchema; }; factory(context: { node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); + config: { + [key in keyof TConfigSchema]: z.infer>; + }; inputs: Expand>; }): Iterable; } & VerifyExtensionFactoryOutput; @@ -827,7 +792,9 @@ export function createNavItemExtension(options: { }, { title?: string | undefined; - } + }, + never, + never >; // @public (undocumented) @@ -850,7 +817,7 @@ export function createNavLogoExtension(options: { namespace?: string; logoIcon: JSX.Element; logoFull: JSX.Element; -}): ExtensionDefinition<{}, {}>; +}): ExtensionDefinition; // @public (undocumented) export namespace createNavLogoExtension { @@ -1005,7 +972,7 @@ export function createSubRouteRef< // @public (undocumented) export function createThemeExtension( theme: AppTheme, -): ExtensionDefinition<{}, {}>; +): ExtensionDefinition; // @public (undocumented) export namespace createThemeExtension { @@ -1021,7 +988,7 @@ export namespace createThemeExtension { export function createTranslationExtension(options: { name?: string; resource: TranslationResource | TranslationMessages; -}): ExtensionDefinition<{}, {}>; +}): ExtensionDefinition; // @public (undocumented) export namespace createTranslationExtension { @@ -1259,7 +1226,20 @@ export type ExtensionDataValues = { }; // @public (undocumented) -export interface ExtensionDefinition { +export interface ExtensionDefinition< + TConfig, + TConfigInput = TConfig, + UOutput extends AnyExtensionDataRef = AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + } = {}, +> { // (undocumented) $$type: '@backstage/ExtensionDefinition'; // (undocumented) @@ -1277,6 +1257,76 @@ export interface ExtensionDefinition { readonly name?: string; // (undocumented) readonly namespace?: string; + // (undocumented) + override< + TExtensionConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + >( + args: { + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; + }; + factory( + originalFactory: (context?: { + config?: TConfig; + inputs?: Expand>; + }) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + } & VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >, + ): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & TConfig, + z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + > & + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs + >; } // @public (undocumented) @@ -1418,9 +1468,6 @@ export interface LegacyCreateExtensionOptions< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { - [key: string]: (zImpl: typeof z) => z.ZodType; - }, > { // (undocumented) attachTo: { @@ -1428,24 +1475,13 @@ export interface LegacyCreateExtensionOptions< input: string; }; // (undocumented) - config?: { - schema: TConfigSchema; - }; - // @deprecated (undocumented) configSchema?: PortableSchema; // (undocumented) disabled?: boolean; // (undocumented) factory(context: { node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); + config: TConfig; inputs: Expand>; }): Expand>; // (undocumented) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts index 555cd82d4c..aa00247222 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts @@ -75,7 +75,6 @@ describe('createExtensionOverrides', () => { "id": "a", "inputs": {}, "output": {}, - "override": [Function], "toString": [Function], "version": "v1", }, @@ -91,7 +90,6 @@ describe('createExtensionOverrides', () => { "id": "b", "inputs": {}, "output": {}, - "override": [Function], "toString": [Function], "version": "v1", }, @@ -107,7 +105,6 @@ describe('createExtensionOverrides', () => { "id": "k:c/n", "inputs": {}, "output": {}, - "override": [Function], "toString": [Function], "version": "v1", }, diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts index e1bfcbfc11..74d081e95d 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts @@ -23,6 +23,7 @@ describe('resolveExtensionDefinition', () => { version: 'v2', attachTo: { id: '', input: '' }, disabled: false, + override: () => ({} as ExtensionDefinition), }; it.each([ @@ -63,6 +64,7 @@ describe('old resolveExtensionDefinition', () => { version: 'v1', attachTo: { id: '', input: '' }, disabled: false, + override: () => ({} as ExtensionDefinition), }; it.each([ diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts index 18b6f0a653..0c8a7959f7 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts @@ -100,7 +100,13 @@ export function resolveExtensionDefinition( context?: { namespace?: string }, ): Extension { const internalDefinition = toInternalExtensionDefinition(definition); - const { name, kind, namespace: _, ...rest } = internalDefinition; + const { + name, + kind, + namespace: _skip1, + override: _skip2, + ...rest + } = internalDefinition; const namespace = internalDefinition.namespace ?? context?.namespace; const namePart = diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index bbe52c3fbc..ce7da3701d 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -169,7 +169,7 @@ export class ExtensionTester { : [...internal.output, coreExtensionData.routePath], factory: params => { const parentOutput = Array.from( - internal.factory(params) as Iterable< + internal.factory(params as any) as Iterable< ExtensionDataValue >, ).filter(val => val.id !== coreExtensionData.routePath.id); diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md index 206c0160e2..3ff0788b04 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -115,7 +115,7 @@ export function createEntityCardExtension< config: TConfig; inputs: Expand>; }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition; // @alpha (undocumented) export function createEntityContentExtension< @@ -148,7 +148,9 @@ export function createEntityContentExtension< filter?: string | undefined; title?: string | undefined; path?: string | undefined; - } + }, + never, + never >; // @alpha diff --git a/plugins/catalog/api-report-alpha.md b/plugins/catalog/api-report-alpha.md index 336bf4bc2b..726ee80573 100644 --- a/plugins/catalog/api-report-alpha.md +++ b/plugins/catalog/api-report-alpha.md @@ -108,7 +108,7 @@ export function createCatalogFilterExtension< inputs?: TInputs; configSchema?: PortableSchema; loader: (options: { config: TConfig }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition; // @alpha (undocumented) const _default: BackstagePlugin< diff --git a/plugins/search-react/api-report-alpha.md b/plugins/search-react/api-report-alpha.md index c6e011fe41..56c0b6eab9 100644 --- a/plugins/search-react/api-report-alpha.md +++ b/plugins/search-react/api-report-alpha.md @@ -25,7 +25,7 @@ export function createSearchResultListItemExtension< }, >( options: SearchResultItemExtensionOptions, -): ExtensionDefinition; +): ExtensionDefinition; // @alpha (undocumented) export namespace createSearchResultListItemExtension { diff --git a/plugins/search/api-report-alpha.md b/plugins/search/api-report-alpha.md index e9b86f1f87..bb670522e4 100644 --- a/plugins/search/api-report-alpha.md +++ b/plugins/search/api-report-alpha.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { RouteRef } from '@backstage/frontend-plugin-api'; @@ -17,7 +18,7 @@ const _default: BackstagePlugin< export default _default; // @alpha (undocumented) -export const searchApi: ExtensionDefinition<{}, {}>; +export const searchApi: ExtensionDefinition<{}, {}, never, never>; // @alpha (undocumented) export const searchNavItem: ExtensionDefinition< @@ -26,7 +27,9 @@ export const searchNavItem: ExtensionDefinition< }, { title?: string | undefined; - } + }, + never, + never >; // @alpha (undocumented) @@ -38,7 +41,9 @@ export const searchPage: ExtensionDefinition< { path: string; noTrack: boolean; - } + }, + AnyExtensionDataRef, + {} >; // (No @packageDocumentation comment for this package) diff --git a/plugins/techdocs/api-report-alpha.md b/plugins/techdocs/api-report-alpha.md index 04a0c5c512..e3c48ba214 100644 --- a/plugins/techdocs/api-report-alpha.md +++ b/plugins/techdocs/api-report-alpha.md @@ -37,7 +37,9 @@ export const techDocsSearchResultListItemExtension: ExtensionDefinition< asListItem: boolean; asLink: boolean; title?: string | undefined; - } + }, + never, + never >; // (No @packageDocumentation comment for this package) diff --git a/plugins/user-settings/api-report-alpha.md b/plugins/user-settings/api-report-alpha.md index fdb2c5f6bf..d0fad90e08 100644 --- a/plugins/user-settings/api-report-alpha.md +++ b/plugins/user-settings/api-report-alpha.md @@ -24,7 +24,9 @@ export const settingsNavItem: ExtensionDefinition< }, { title?: string | undefined; - } + }, + never, + never >; // @alpha (undocumented) From 467ec43f55ac9f4795bc95ba34dee1c2748b613d Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Aug 2024 12:13:05 +0200 Subject: [PATCH 13/15] chore: wip Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 30375049bf..547ec2a160 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -660,6 +660,57 @@ describe('createExtension', () => { expect(true).toBe(true); }); + it('should allow overriding the returned values from the parent factory', () => { + const testExtension = createExtension({ + kind: 'thing', + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + config: { + schema: { + foo: z => z.string().default('boom'), + }, + }, + factory({ config }) { + return [stringDataRef(config.foo), numberDataRef(42)]; + }, + }); + + const overridden = testExtension.override({ + output: [numberDataRef, stringDataRef], + *factory(originalFactory) { + const output = originalFactory(); + yield* output; + + yield numberDataRef(output.get(numberDataRef) + 1); + }, + }); + + const overridden = testExtension.override({ + output: [numberDataRef, stringDataRef], + *factory(originalFactory) { + const output = originalFactory(); + yield* output.omit(numberDataRef); + + yield numberDataRef(output.get(numberDataRef) + 1); + }, + }); + + const overridden = testExtension.override({ + output: [numberDataRef, stringDataRef], + *factory(originalFactory) { + const output = originalFactory(); + yield* output; + + yield numberDataRef(output.get(numberDataRef) + 1); + }, + }); + + const tester = createExtensionTester(overridden); + + expect(tester.data(numberDataRef)).toBe(43); + }); + it('should work functionally with overrides', () => { const testExtension = createExtension({ kind: 'thing', From 2d215993f14e9018a9fd031ed69c662d9b74783e Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Aug 2024 13:22:59 +0200 Subject: [PATCH 14/15] chore: added changeset Signed-off-by: blam --- .changeset/thick-squids-drive.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .changeset/thick-squids-drive.md diff --git a/.changeset/thick-squids-drive.md b/.changeset/thick-squids-drive.md new file mode 100644 index 0000000000..07654da27d --- /dev/null +++ b/.changeset/thick-squids-drive.md @@ -0,0 +1,31 @@ +--- +'@backstage/frontend-plugin-api': patch +'@backstage/frontend-test-utils': patch +--- + +Added support for being able to override extension definitions. + +```tsx +const TestCard = EntityCardBlueprint.make({ + ... +}); + +TestCard.override({ + attachTo: { id: 'something-else', input: 'overriden' }, + config: { + schema: { + newConfig: z => z.string().optional(), + } + }, + *factory(originalFactory, { inputs, config }){ + const originalOutput = originalFactory(); + + yield coreExentsionData.reactElement( + + {originalOutput.get(coreExentsionData.reactElement)} + + ); + } +}); + +``` From 8fb60cb2f133a36684dd13028b02de54cb623a49 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Aug 2024 13:41:00 +0200 Subject: [PATCH 15/15] chore: some more work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Signed-off-by: blam --- .changeset/thick-squids-drive.md | 5 ++++- .../src/wiring/createExtension.test.ts | 20 ------------------- .../src/wiring/createExtension.ts | 9 ++++++++- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/.changeset/thick-squids-drive.md b/.changeset/thick-squids-drive.md index 07654da27d..0e798063c0 100644 --- a/.changeset/thick-squids-drive.md +++ b/.changeset/thick-squids-drive.md @@ -11,12 +11,15 @@ const TestCard = EntityCardBlueprint.make({ }); TestCard.override({ - attachTo: { id: 'something-else', input: 'overriden' }, + // override attachment points + attachTo: { id: 'something-else', input: 'overridden' }, + // extend the config schema config: { schema: { newConfig: z => z.string().optional(), } }, + // override factory *factory(originalFactory, { inputs, config }){ const originalOutput = originalFactory(); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 547ec2a160..f9e39e98de 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -686,26 +686,6 @@ describe('createExtension', () => { }, }); - const overridden = testExtension.override({ - output: [numberDataRef, stringDataRef], - *factory(originalFactory) { - const output = originalFactory(); - yield* output.omit(numberDataRef); - - yield numberDataRef(output.get(numberDataRef) + 1); - }, - }); - - const overridden = testExtension.override({ - output: [numberDataRef, stringDataRef], - *factory(originalFactory) { - const output = originalFactory(); - yield* output; - - yield numberDataRef(output.get(numberDataRef) + 1); - }, - }); - const tester = createExtensionTester(overridden); expect(tester.data(numberDataRef)).toBe(43); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 6e75fc76e5..600b96ebe0 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -581,7 +581,7 @@ export function createExtension< >, }); } - return overrideOptions.factory( + const parentResult = overrideOptions.factory( (innerContext?: { config?: { [key in keyof TConfigSchema]: z.infer< @@ -604,6 +604,13 @@ export function createExtension< inputs, }, ); + + const deduplicatedResult = new Map(); + for (const item of parentResult) { + deduplicatedResult.set(item.id, item); + } + + return deduplicatedResult.values() as Iterable; }, } as CreateExtensionOptions); },