diff --git a/.changeset/thick-squids-drive.md b/.changeset/thick-squids-drive.md
new file mode 100644
index 0000000000..0e798063c0
--- /dev/null
+++ b/.changeset/thick-squids-drive.md
@@ -0,0 +1,34 @@
+---
+'@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({
+ // 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();
+
+ yield coreExentsionData.reactElement(
+
+ {originalOutput.get(coreExentsionData.reactElement)}
+
+ );
+ }
+});
+
+```
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/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 ab6a6c19a5..f9e39e98de 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 { createExtension } from './createExtension';
import { createExtensionDataRef } from './createExtensionDataRef';
import { createExtensionInput } from './createExtensionInput';
@@ -304,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;
@@ -320,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}',
);
@@ -561,4 +558,179 @@ describe('createExtension', () => {
}),
).toMatchObject({ version: 'v2' });
});
+
+ describe('overrides', () => {
+ it('should allow overriding of config and merging', () => {
+ 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);
+ });
+
+ 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 override1 = 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 override2 = testExtension.override({
+ output: [numberDataRef],
+ factory(_, { inputs }) {
+ return [stringDataRef(inputs.test.get(stringDataRef))];
+ },
+ });
+
+ unused(override1, override2);
+
+ 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);
+
+ // @ts-expect-error - fails because original factory does not return number
+ const number: boolean = response.get(numberDataRef);
+
+ return [stringDataRef(`foo-${foo}-override`)];
+ },
+ });
+
+ 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 tester = createExtensionTester(overridden);
+
+ expect(tester.data(numberDataRef)).toBe(43);
+ });
+
+ 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().default('hello'),
+ },
+ },
+ factory(originalFactory, { config }) {
+ const response = originalFactory();
+
+ const foo: string = response.get(stringDataRef);
+
+ return [stringDataRef(`foo-${foo}-override-${config.bar}`)];
+ },
+ });
+
+ 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');
+ });
+ });
});
diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts
index 2a29fe1ab4..600b96ebe0 100644
--- a/packages/frontend-plugin-api/src/wiring/createExtension.ts
+++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts
@@ -17,6 +17,7 @@
import { AppNode } from '../apis';
import { PortableSchema, createSchemaFromZod } from '../schema';
import { Expand } from '../types';
+import { createDataContainer } from './createExtensionBlueprint';
import {
AnyExtensionDataRef,
ExtensionDataRef,
@@ -127,7 +128,6 @@ export interface LegacyCreateExtensionOptions<
TInputs extends AnyExtensionInputMap,
TConfig,
TConfigInput,
- TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
> {
kind?: string;
namespace?: string;
@@ -136,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>;
}
@@ -192,8 +181,6 @@ export type CreateExtensionOptions<
{ optional: boolean; singleton: boolean }
>;
},
- TConfig,
- TConfigInput,
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
UFactoryOutput extends ExtensionDataValue,
> = {
@@ -204,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;
@@ -232,6 +222,70 @@ export interface ExtensionDefinition {
readonly attachTo: { id: string; input: string };
readonly disabled: boolean;
readonly configSchema?: PortableSchema;
+
+ 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
+ >;
}
/** @internal */
@@ -301,34 +355,26 @@ 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;
- }>
- >)
+ {
+ [key in keyof TConfigSchema]: z.infer>;
+ },
+ z.input<
+ z.ZodObject<{
+ [key in keyof TConfigSchema]: ReturnType;
+ }>
+ >,
+ UOutput,
+ TInputs
>;
/**
* @public
@@ -339,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 {
@@ -379,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 &
@@ -408,21 +429,29 @@ export function createExtension<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType;
}>
- >)
+ >),
+ UOutput,
+ TInputs
> {
- const newConfigSchema = options.config?.schema;
- if (newConfigSchema && options.configSchema) {
+ 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',
@@ -450,6 +479,141 @@ export function createExtension<
parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`);
return `ExtensionDefinition{${parts.join(',')}}`;
},
+ override: <
+ TExtensionConfigSchema extends {
+ [key in string]: (zImpl: typeof z) => z.ZodType;
+ },
+ 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,
+ TConfigSchema,
+ UFactoryOutput
+ >;
+ const overrideNewConfigSchema = overrideOptions.config?.schema;
+
+ const schema = {
+ ...newOptions.config?.schema,
+ ...overrideNewConfigSchema,
+ } as TConfigSchema & TExtensionConfigSchema;
+
+ return createExtension({
+ 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 newOptions.factory({
+ node,
+ config,
+ inputs: inputs as unknown as Expand<
+ ResolvedExtensionInputs
+ >,
+ });
+ }
+ const parentResult = 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,
+ },
+ );
+
+ const deduplicatedResult = new Map();
+ for (const item of parentResult) {
+ deduplicatedResult.set(item.id, item);
+ }
+
+ return deduplicatedResult.values() as Iterable;
+ },
+ } as CreateExtensionOptions);
+ },
} as InternalExtensionDefinition<
TConfig &
(string extends keyof TConfigSchema
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
- >);
+ >
+ >;
}
}
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)