Merge pull request #25870 from backstage/blam/nfs/overrides

NFS: Support Extension Overriding
This commit is contained in:
Patrik Oldsberg
2024-08-08 16:19:07 +02:00
committed by GitHub
24 changed files with 626 additions and 189 deletions
+34
View File
@@ -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(
<Wrapping>
{originalOutput.get(coreExentsionData.reactElement)}
</Wrapping>
);
}
});
```
@@ -68,7 +68,7 @@ function makeNode<TConfig, TConfigInput>(
function makeInstanceWithId<TConfig, TConfigInput>(
extension: Extension<TConfig, TConfigInput>,
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),
@@ -48,6 +48,7 @@ function makeExtDef(
name,
attachTo: { id: attachId, input: 'default' },
disabled: status === 'disabled',
override: () => ({} as ExtensionDefinition<unknown>),
} as ExtensionDefinition<unknown>;
}
+108 -72
View File
@@ -396,7 +396,7 @@ export function createApiExtension<
configSchema?: PortableSchema<TConfig>;
inputs?: TInputs;
},
): ExtensionDefinition<TConfig & {}, TConfig & {}>;
): ExtensionDefinition<TConfig, TConfig, never, never>;
// @public (undocumented)
export namespace createApiExtension {
@@ -492,7 +492,7 @@ export function createComponentExtension<
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
}) => ComponentType<TProps>;
};
}): ExtensionDefinition<TConfig & {}, TConfig & {}>;
}): ExtensionDefinition<TConfig, TConfig, never, never>;
// @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<ReturnType<TConfigSchema[key]>>;
}),
TConfigInput &
(string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>)
{
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
},
z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>,
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<ReturnType<TConfigSchema[key]>>;
}),
TConfigInput &
(string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>)
>;
): ExtensionDefinition<TConfig, TConfigInput, never, never>;
// @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<UOutput>;
configSchema?: PortableSchema<TConfig, TConfigInput>;
config?: {
schema: TConfigSchema;
};
factory(context: {
node: AppNode;
config: TConfig &
(string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<TConfigSchema[key]>
>;
});
config: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
}): Iterable<UFactoryOutput>;
} & VerifyExtensionFactoryOutput<UOutput, UFactoryOutput>;
@@ -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<unknown, unknown, never, never>;
// @public (undocumented)
export namespace createNavLogoExtension {
@@ -1005,7 +972,7 @@ export function createSubRouteRef<
// @public (undocumented)
export function createThemeExtension(
theme: AppTheme,
): ExtensionDefinition<{}, {}>;
): ExtensionDefinition<unknown, unknown, never, never>;
// @public (undocumented)
export namespace createThemeExtension {
@@ -1021,7 +988,7 @@ export namespace createThemeExtension {
export function createTranslationExtension(options: {
name?: string;
resource: TranslationResource | TranslationMessages;
}): ExtensionDefinition<{}, {}>;
}): ExtensionDefinition<unknown, unknown, never, never>;
// @public (undocumented)
export namespace createTranslationExtension {
@@ -1259,7 +1226,20 @@ export type ExtensionDataValues<TExtensionData extends AnyExtensionDataMap> = {
};
// @public (undocumented)
export interface ExtensionDefinition<TConfig, TConfigInput = TConfig> {
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<TConfig, TConfigInput = TConfig> {
readonly name?: string;
// (undocumented)
readonly namespace?: string;
// (undocumented)
override<
TExtensionConfigSchema extends {
[key in string]: (zImpl: typeof z) => z.ZodType;
},
UFactoryOutput extends ExtensionDataValue<any, any>,
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<UNewOutput>;
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<ResolvedExtensionInputs<TInputs>>;
}) => ExtensionDataContainer<UOutput>,
context: {
node: AppNode;
config: TConfig & {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs & TExtraInputs>>;
},
): Iterable<UFactoryOutput>;
} & VerifyExtensionFactoryOutput<
AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput,
UFactoryOutput
>,
): ExtensionDefinition<
{
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
>;
} & 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<TConfig, TConfigInput>;
// (undocumented)
disabled?: boolean;
// (undocumented)
factory(context: {
node: AppNode;
config: TConfig &
(string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<TConfigSchema[key]>
>;
});
config: TConfig;
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
}): Expand<ExtensionDataValues<TOutput>>;
// (undocumented)
@@ -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),
});
});
});
@@ -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' } })
@@ -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),
});
@@ -41,6 +41,7 @@ describe('createNavLogoExtension', () => {
logos: expect.anything(),
},
factory: expect.any(Function),
override: expect.any(Function),
toString: expect.any(Function),
});
});
@@ -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),
});
});
@@ -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),
});
@@ -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,
@@ -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');
});
});
});
@@ -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<TConfig, TConfigInput>;
config?: {
schema: TConfigSchema;
};
factory(context: {
node: AppNode;
config: TConfig &
(string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<TConfigSchema[key]>
>;
});
config: TConfig;
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
}): Expand<ExtensionDataValues<TOutput>>;
}
@@ -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<any, any>,
> = {
@@ -204,27 +191,30 @@ export type CreateExtensionOptions<
disabled?: boolean;
inputs?: TInputs;
output: Array<UOutput>;
/** @deprecated - use `config.schema` instead */
configSchema?: PortableSchema<TConfig, TConfigInput>;
config?: {
schema: TConfigSchema;
};
factory(context: {
node: AppNode;
config: TConfig &
(string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<TConfigSchema[key]>
>;
});
config: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
}): Iterable<UFactoryOutput>;
} & VerifyExtensionFactoryOutput<UOutput, UFactoryOutput>;
/** @public */
export interface ExtensionDefinition<TConfig, TConfigInput = TConfig> {
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<TConfig, TConfigInput = TConfig> {
readonly attachTo: { id: string; input: string };
readonly disabled: boolean;
readonly configSchema?: PortableSchema<TConfig, TConfigInput>;
override<
TExtensionConfigSchema extends {
[key in string]: (zImpl: typeof z) => z.ZodType;
},
UFactoryOutput extends ExtensionDataValue<any, any>,
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<UNewOutput>;
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<ResolvedExtensionInputs<TInputs>>;
}) => ExtensionDataContainer<UOutput>,
context: {
node: AppNode;
config: TConfig & {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs & TExtraInputs>>;
},
): Iterable<UFactoryOutput>;
} & VerifyExtensionFactoryOutput<
AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput,
UFactoryOutput
>,
): ExtensionDefinition<
{
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
>;
} & 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<any, any>,
>(
options: CreateExtensionOptions<
UOutput,
TInputs,
TConfig,
TConfigInput,
TConfigSchema,
UFactoryOutput
>,
): ExtensionDefinition<
TConfig &
(string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
}),
TConfigInput &
(string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>)
{
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
},
z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>,
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<ReturnType<TConfigSchema[key]>>;
}),
TConfigInput &
(string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>)
>;
): ExtensionDefinition<TConfig, TConfigInput, never, never>;
export function createExtension<
UOutput extends AnyExtensionDataRef,
TInputs extends {
@@ -379,20 +408,12 @@ export function createExtension<
UFactoryOutput extends ExtensionDataValue<any, any>,
>(
options:
| CreateExtensionOptions<
UOutput,
TInputs,
TConfig,
TConfigInput,
TConfigSchema,
UFactoryOutput
>
| CreateExtensionOptions<UOutput, TInputs, TConfigSchema, UFactoryOutput>
| LegacyCreateExtensionOptions<
AnyExtensionDataMap,
TLegacyInputs,
TConfig,
TConfigInput,
TConfigSchema
TConfigInput
>,
): ExtensionDefinition<
TConfig &
@@ -408,21 +429,29 @@ export function createExtension<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>)
>),
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<any, any> | 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<any, any>,
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<UNewOutput>;
config?: {
schema: TExtensionConfigSchema;
};
factory(
originalFactory: (context?: {
config?: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<TConfigSchema[key]>
>;
};
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
}) => ExtensionDataContainer<UOutput>,
context: {
node: AppNode;
config: {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
>;
} & {
[key in keyof TConfigSchema]: z.infer<
ReturnType<TConfigSchema[key]>
>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs & TExtraInputs>>;
},
): Iterable<UOverrideFactoryOutput>;
}): ExtensionDefinition<
{
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
>;
} & {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
},
z.input<
z.ZodObject<
{
[key in keyof TExtensionConfigSchema]: ReturnType<
TExtensionConfigSchema[key]
>;
} & {
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}
>
>,
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<TInputs>
>,
});
}
const parentResult = overrideOptions.factory(
(innerContext?: {
config?: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<TConfigSchema[key]>
>;
};
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
}): ExtensionDataContainer<UOutput> => {
return createDataContainer<UOutput>(
newOptions.factory({
node,
config: innerContext?.config ?? config,
inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden
}) as Iterable<any>,
);
},
{
node,
config,
inputs,
},
);
const deduplicatedResult = new Map<string, UOverrideFactoryOutput>();
for (const item of parentResult) {
deduplicatedResult.set(item.id, item);
}
return deduplicatedResult.values() as Iterable<UOverrideFactoryOutput>;
},
} as CreateExtensionOptions<AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, TInputs & TExtraInputs, TConfigSchema & TExtensionConfigSchema, UOverrideFactoryOutput>);
},
} as InternalExtensionDefinition<
TConfig &
(string extends keyof TConfigSchema
@@ -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",
}
@@ -171,7 +171,7 @@ export interface ExtensionBlueprint<
}
/** @internal */
function createDataContainer<UData extends AnyExtensionDataRef>(
export function createDataContainer<UData extends AnyExtensionDataRef>(
values: Iterable<
UData extends ExtensionDataRef<infer IData, infer IId>
? ExtensionDataValue<IData, IId>
@@ -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<UNewOutput>;
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<UOutput, TInputs & TExtraInputs, TConfigSchema & TExtensionConfigSchema, UFactoryOutput>) as ExtensionDefinition<
{
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
@@ -383,10 +378,8 @@ class ExtensionBlueprintImpl<
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}
>
>,
TConfigSchema,
UFactoryOutput
>);
>
>;
}
}
@@ -23,6 +23,7 @@ describe('resolveExtensionDefinition', () => {
version: 'v2',
attachTo: { id: '', input: '' },
disabled: false,
override: () => ({} as ExtensionDefinition<unknown>),
};
it.each([
@@ -63,6 +64,7 @@ describe('old resolveExtensionDefinition', () => {
version: 'v1',
attachTo: { id: '', input: '' },
disabled: false,
override: () => ({} as ExtensionDefinition<unknown>),
};
it.each([
@@ -100,7 +100,13 @@ export function resolveExtensionDefinition<TConfig, TConfigInput>(
context?: { namespace?: string },
): Extension<TConfig, TConfigInput> {
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 =
@@ -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<any, any>
>,
).filter(val => val.id !== coreExtensionData.routePath.id);
+4 -2
View File
@@ -115,7 +115,7 @@ export function createEntityCardExtension<
config: TConfig;
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
}) => Promise<JSX.Element>;
}): ExtensionDefinition<TConfig & {}, TConfig & {}>;
}): ExtensionDefinition<TConfig, TConfig, never, never>;
// @alpha (undocumented)
export function createEntityContentExtension<
@@ -148,7 +148,9 @@ export function createEntityContentExtension<
filter?: string | undefined;
title?: string | undefined;
path?: string | undefined;
}
},
never,
never
>;
// @alpha
+1 -1
View File
@@ -108,7 +108,7 @@ export function createCatalogFilterExtension<
inputs?: TInputs;
configSchema?: PortableSchema<TConfig>;
loader: (options: { config: TConfig }) => Promise<JSX.Element>;
}): ExtensionDefinition<TConfig & {}, TConfig & {}>;
}): ExtensionDefinition<TConfig, TConfig, never, never>;
// @alpha (undocumented)
const _default: BackstagePlugin<
+1 -1
View File
@@ -25,7 +25,7 @@ export function createSearchResultListItemExtension<
},
>(
options: SearchResultItemExtensionOptions<TConfig>,
): ExtensionDefinition<TConfig & {}, TConfig & {}>;
): ExtensionDefinition<TConfig, TConfig, never, never>;
// @alpha (undocumented)
export namespace createSearchResultListItemExtension {
+8 -3
View File
@@ -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)
+3 -1
View File
@@ -37,7 +37,9 @@ export const techDocsSearchResultListItemExtension: ExtensionDefinition<
asListItem: boolean;
asLink: boolean;
title?: string | undefined;
}
},
never,
never
>;
// (No @packageDocumentation comment for this package)
+3 -1
View File
@@ -24,7 +24,9 @@ export const settingsNavItem: ExtensionDefinition<
},
{
title?: string | undefined;
}
},
never,
never
>;
// @alpha (undocumented)