frontend-plugin-api: return output from extension factories instead

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-10-27 13:51:02 +02:00
parent 6d041afa0c
commit 77f009b35d
28 changed files with 313 additions and 173 deletions
+2 -4
View File
@@ -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(', ')}`,
]),
});
};
},
});