frontend-plugin-api: return output from extension factories instead
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -170,10 +170,9 @@ export interface CreateExtensionOptions<
|
||||
// (undocumented)
|
||||
factory(options: {
|
||||
source?: BackstagePlugin;
|
||||
bind(values: Expand<ExtensionDataValues<TOutput>>): void;
|
||||
config: TConfig;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}): void;
|
||||
}): Expand<ExtensionDataValues<TOutput>>;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
// (undocumented)
|
||||
@@ -313,13 +312,12 @@ export interface Extension<TConfig> {
|
||||
// (undocumented)
|
||||
factory(options: {
|
||||
source?: BackstagePlugin;
|
||||
bind(values: ExtensionInputValues<any>): void;
|
||||
config: TConfig;
|
||||
inputs: Record<
|
||||
string,
|
||||
undefined | Record<string, unknown> | Array<Record<string, unknown>>
|
||||
>;
|
||||
}): void;
|
||||
}): ExtensionDataValues<any>;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
// (undocumented)
|
||||
|
||||
@@ -66,8 +66,8 @@ const wrapInBoundaryExtension = (element: JSX.Element) => {
|
||||
path: coreExtensionData.routePath,
|
||||
routeRef: coreExtensionData.routeRef.optional(),
|
||||
},
|
||||
factory({ bind, source }) {
|
||||
bind({
|
||||
factory({ source }) {
|
||||
return {
|
||||
routeRef,
|
||||
path: '/',
|
||||
element: (
|
||||
@@ -75,7 +75,7 @@ const wrapInBoundaryExtension = (element: JSX.Element) => {
|
||||
{element}
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -58,12 +58,11 @@ export function createApiExtension<
|
||||
output: {
|
||||
api: coreExtensionData.apiFactory,
|
||||
},
|
||||
factory({ bind, config, inputs }) {
|
||||
factory({ config, inputs }) {
|
||||
if (typeof factory === 'function') {
|
||||
bind({ api: factory({ config, inputs }) });
|
||||
} else {
|
||||
bind({ api: factory });
|
||||
return { api: factory({ config, inputs }) };
|
||||
}
|
||||
return { api: factory };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,14 +41,12 @@ export function createNavItemExtension(options: {
|
||||
output: {
|
||||
navTarget: coreExtensionData.navTarget,
|
||||
},
|
||||
factory: ({ bind, config }) => {
|
||||
bind({
|
||||
navTarget: {
|
||||
title: config.title,
|
||||
icon,
|
||||
routeRef,
|
||||
},
|
||||
});
|
||||
},
|
||||
factory: ({ config }) => ({
|
||||
navTarget: {
|
||||
title: config.title,
|
||||
icon,
|
||||
routeRef,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useAnalytics } from '@backstage/core-plugin-api';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { PortableSchema } from '../schema';
|
||||
import {
|
||||
ExtensionInputValues,
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
createPlugin,
|
||||
@@ -127,16 +126,14 @@ describe('createPageExtension', () => {
|
||||
loader: async () => <div>Component</div>,
|
||||
});
|
||||
|
||||
extension.factory({
|
||||
bind: (values: ExtensionInputValues<any>) =>
|
||||
renderWithEffects(
|
||||
wrapInTestApp(values.element as unknown as JSX.Element),
|
||||
),
|
||||
const output = extension.factory({
|
||||
source: createPlugin({ id: 'plugin ' }),
|
||||
config: { path: '/' },
|
||||
inputs: {},
|
||||
});
|
||||
|
||||
renderWithEffects(wrapInTestApp(output.element as unknown as JSX.Element));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(captureEvent).toHaveBeenCalledWith(
|
||||
'_ROUTABLE-EXTENSION-RENDERED',
|
||||
|
||||
@@ -75,14 +75,14 @@ export function createPageExtension<
|
||||
path: coreExtensionData.routePath,
|
||||
routeRef: coreExtensionData.routeRef.optional(),
|
||||
},
|
||||
factory({ bind, config, inputs, source }) {
|
||||
factory({ config, inputs, source }) {
|
||||
const ExtensionComponent = lazy(() =>
|
||||
options
|
||||
.loader({ config, inputs })
|
||||
.then(element => ({ default: () => element })),
|
||||
);
|
||||
|
||||
bind({
|
||||
return {
|
||||
path: config.path,
|
||||
routeRef: options.routeRef,
|
||||
element: (
|
||||
@@ -90,7 +90,7 @@ export function createPageExtension<
|
||||
<ExtensionComponent />
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ export function createThemeExtension(theme: AppTheme) {
|
||||
output: {
|
||||
theme: coreExtensionData.theme,
|
||||
},
|
||||
factory({ bind }) {
|
||||
bind({ theme });
|
||||
},
|
||||
factory: () => ({ theme }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,71 +24,211 @@ function unused(..._any: any[]) {}
|
||||
|
||||
describe('createExtension', () => {
|
||||
it('should create an extension with a simple output', () => {
|
||||
const extension = createExtension({
|
||||
const baseConfig = {
|
||||
id: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: {
|
||||
foo: stringData,
|
||||
},
|
||||
factory({ bind }) {
|
||||
bind({
|
||||
};
|
||||
const extension = createExtension({
|
||||
...baseConfig,
|
||||
factory() {
|
||||
return {
|
||||
foo: 'bar',
|
||||
});
|
||||
bind({
|
||||
// @ts-expect-error
|
||||
foo: 3,
|
||||
});
|
||||
bind({
|
||||
// @ts-expect-error
|
||||
bar: 'bar',
|
||||
});
|
||||
// @ts-expect-error
|
||||
bind({});
|
||||
// @ts-expect-error
|
||||
bind();
|
||||
// @ts-expect-error
|
||||
bind('bar');
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(extension.id).toBe('test');
|
||||
|
||||
// When declared as an error function without a block the TypeScript errors
|
||||
// are a more specific and will point at the property that is problematic.
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () => ({
|
||||
// @ts-expect-error
|
||||
foo: 3,
|
||||
}),
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () =>
|
||||
// @ts-expect-error
|
||||
({
|
||||
bar: 'bar',
|
||||
}),
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () =>
|
||||
// @ts-expect-error
|
||||
({}),
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () =>
|
||||
// @ts-expect-error
|
||||
undefined,
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () =>
|
||||
// @ts-expect-error
|
||||
'bar',
|
||||
});
|
||||
|
||||
// When declared as a function with a block the TypeScript error will instead
|
||||
// be tied to the factory function declaration itself, but the error messages
|
||||
// is still helpful and points to part of the return type that is problematic.
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory() {
|
||||
return {
|
||||
foo: 3,
|
||||
};
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory() {
|
||||
return {
|
||||
bar: 'bar',
|
||||
};
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory() {
|
||||
return 'bar';
|
||||
},
|
||||
});
|
||||
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory: () => {
|
||||
return {
|
||||
foo: 3,
|
||||
};
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory: () => {
|
||||
return {
|
||||
bar: 'bar',
|
||||
};
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory: () => {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory: () => {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory: () => {
|
||||
return 'bar';
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create an extension with a some optional output', () => {
|
||||
const extension = createExtension({
|
||||
const baseConfig = {
|
||||
id: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: {
|
||||
foo: stringData,
|
||||
bar: stringData.optional(),
|
||||
},
|
||||
factory({ bind }) {
|
||||
bind({
|
||||
foo: 'bar',
|
||||
});
|
||||
bind({
|
||||
foo: 'bar',
|
||||
bar: 'baz',
|
||||
});
|
||||
bind({
|
||||
// @ts-expect-error
|
||||
foo: 3,
|
||||
});
|
||||
bind({
|
||||
foo: 'bar',
|
||||
// @ts-expect-error
|
||||
bar: 3,
|
||||
});
|
||||
// @ts-expect-error
|
||||
bind({ bar: 'bar' });
|
||||
// @ts-expect-error
|
||||
bind({});
|
||||
// @ts-expect-error
|
||||
bind();
|
||||
// @ts-expect-error
|
||||
bind('bar');
|
||||
},
|
||||
};
|
||||
const extension = createExtension({
|
||||
...baseConfig,
|
||||
factory: () => ({
|
||||
foo: 'bar',
|
||||
}),
|
||||
});
|
||||
expect(extension.id).toBe('test');
|
||||
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () => ({
|
||||
foo: 'bar',
|
||||
bar: 'baz',
|
||||
}),
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () => ({
|
||||
// @ts-expect-error
|
||||
foo: 3,
|
||||
}),
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () => ({
|
||||
foo: 'bar',
|
||||
// @ts-expect-error
|
||||
bar: 3,
|
||||
}),
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () =>
|
||||
// @ts-expect-error
|
||||
({ bar: 'bar' }),
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () =>
|
||||
// @ts-expect-error
|
||||
({}),
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () =>
|
||||
// @ts-expect-error
|
||||
undefined,
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory: () => {},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () =>
|
||||
// @ts-expect-error
|
||||
'bar',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create an extension with input', () => {
|
||||
@@ -110,7 +250,7 @@ describe('createExtension', () => {
|
||||
output: {
|
||||
foo: stringData,
|
||||
},
|
||||
factory({ bind, inputs }) {
|
||||
factory({ inputs }) {
|
||||
const a1: string = inputs.mixed?.[0].required;
|
||||
// @ts-expect-error
|
||||
const a2: number = inputs.mixed?.[0].required;
|
||||
@@ -141,9 +281,9 @@ describe('createExtension', () => {
|
||||
const d4: number | undefined = inputs.onlyOptional?.[0].optional;
|
||||
unused(d1, d2, d3, d4);
|
||||
|
||||
bind({
|
||||
return {
|
||||
foo: 'bar',
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(extension.id).toBe('test');
|
||||
|
||||
@@ -81,10 +81,9 @@ export interface CreateExtensionOptions<
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
factory(options: {
|
||||
source?: BackstagePlugin;
|
||||
bind(values: Expand<ExtensionDataValues<TOutput>>): void;
|
||||
config: TConfig;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}): void;
|
||||
}): Expand<ExtensionDataValues<TOutput>>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@@ -98,13 +97,12 @@ export interface Extension<TConfig> {
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
factory(options: {
|
||||
source?: BackstagePlugin;
|
||||
bind(values: ExtensionInputValues<any>): void;
|
||||
config: TConfig;
|
||||
inputs: Record<
|
||||
string,
|
||||
undefined | Record<string, unknown> | Array<Record<string, unknown>>
|
||||
>;
|
||||
}): void;
|
||||
}): ExtensionDataValues<any>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@@ -120,12 +118,11 @@ export function createExtension<
|
||||
disabled: options.disabled ?? false,
|
||||
$$type: '@backstage/Extension',
|
||||
inputs: options.inputs ?? {},
|
||||
factory({ bind, config, inputs }) {
|
||||
factory({ inputs, ...rest }) {
|
||||
// TODO: Simplify this, but TS wouldn't infer the input type for some reason
|
||||
return options.factory({
|
||||
bind,
|
||||
config,
|
||||
inputs: inputs as Expand<ExtensionInputValues<TInputs>>,
|
||||
...rest,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('createExtensionOverrides', () => {
|
||||
id: 'a',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
output: {},
|
||||
factory() {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
@@ -72,7 +72,7 @@ describe('createExtensionOverrides', () => {
|
||||
id: 'a',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
output: {},
|
||||
factory() {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -34,8 +34,8 @@ const TechRadarPage = createExtension({
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
factory({ bind }) {
|
||||
bind({ name: 'TechRadar' });
|
||||
factory() {
|
||||
return { name: 'TechRadar' };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -48,8 +48,8 @@ const CatalogPage = createExtension({
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({ name: z.string().default('Catalog') }),
|
||||
),
|
||||
factory({ bind, config }) {
|
||||
bind({ name: config.name });
|
||||
factory({ config }) {
|
||||
return { name: config.name };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -62,8 +62,8 @@ const TechDocsAddon = createExtension({
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({ name: z.string().default('TechDocsAddon') }),
|
||||
),
|
||||
factory({ bind, config }) {
|
||||
bind({ name: config.name });
|
||||
factory({ config }) {
|
||||
return { name: config.name };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,8 +78,8 @@ const TechDocsPage = createExtension({
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
factory({ bind, inputs }) {
|
||||
bind({ name: `TechDocs-${inputs.addons.map(n => n.name).join('-')}` });
|
||||
factory({ inputs }) {
|
||||
return { name: `TechDocs-${inputs.addons.map(n => n.name).join('-')}` };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -94,12 +94,12 @@ const outputExtension = createExtension({
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
factory({ bind, inputs }) {
|
||||
bind({
|
||||
factory({ inputs }) {
|
||||
return {
|
||||
element: React.createElement('span', {}, [
|
||||
`Names: ${inputs.names.map(n => n.name).join(', ')}`,
|
||||
]),
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user