core-plugin-api: apply extension fixes and deprecate extensions in core-app-api
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-plugin-api': patch
|
||||
---
|
||||
|
||||
Apply fixes to the extension creation API that were mistakenly applied to `@backstage/core-app-api` instead.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Deprecate and disable the extension creation methods, which were added to this package by mistake and should only exist within `@backstage/core-plugin-api`.
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { createPlugin, createRouteRef } from '@backstage/core-plugin-api';
|
||||
import { getComponentData } from './componentData';
|
||||
import {
|
||||
createComponentExtension,
|
||||
createReactExtension,
|
||||
createRoutableExtension,
|
||||
} from './extensions';
|
||||
|
||||
const plugin = createPlugin({
|
||||
id: 'my-plugin',
|
||||
});
|
||||
|
||||
describe('extensions', () => {
|
||||
it('should create a react extension with component data', () => {
|
||||
const Component = () => <div />;
|
||||
|
||||
const extension = createReactExtension({
|
||||
component: {
|
||||
sync: Component,
|
||||
},
|
||||
data: {
|
||||
myData: { foo: 'bar' },
|
||||
},
|
||||
});
|
||||
|
||||
const ExtensionComponent = plugin.provide(extension);
|
||||
const element = <ExtensionComponent />;
|
||||
|
||||
expect(getComponentData(element, 'core.plugin')).toBe(plugin);
|
||||
expect(getComponentData(element, 'myData')).toEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('should create react extensions of different types', () => {
|
||||
const Component = () => <div />;
|
||||
const routeRef = createRouteRef({ id: 'foo' });
|
||||
|
||||
const extension1 = createComponentExtension({
|
||||
component: {
|
||||
sync: Component,
|
||||
},
|
||||
});
|
||||
|
||||
const extension2 = createRoutableExtension({
|
||||
component: () => Promise.resolve(Component),
|
||||
mountPoint: routeRef,
|
||||
});
|
||||
|
||||
const ExtensionComponent1 = plugin.provide(extension1);
|
||||
const ExtensionComponent2 = plugin.provide(extension2);
|
||||
|
||||
const element1 = <ExtensionComponent1 />;
|
||||
const element2 = <ExtensionComponent2 />;
|
||||
|
||||
expect(getComponentData(element1, 'core.plugin')).toBe(plugin);
|
||||
expect(getComponentData(element2, 'core.plugin')).toBe(plugin);
|
||||
expect(getComponentData(element2, 'core.mountPoint')).toBe(routeRef);
|
||||
});
|
||||
});
|
||||
@@ -14,15 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { attachComponentData } from './componentData';
|
||||
import {
|
||||
Extension,
|
||||
BackstagePlugin,
|
||||
RouteRef,
|
||||
useRouteRef,
|
||||
useApp,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { Extension, RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
type ComponentLoader<T> =
|
||||
| {
|
||||
@@ -32,114 +24,31 @@ type ComponentLoader<T> =
|
||||
sync: T;
|
||||
};
|
||||
|
||||
// We do not use ComponentType as the return type, since it doesn't let us convey the children prop.
|
||||
// ComponentType inserts children as an optional prop whether the inner component accepts it or not,
|
||||
// making it impossible to make the usage of children type safe.
|
||||
const ERROR_MESSAGE = 'Import this from @backstage/core-plugin-api';
|
||||
|
||||
/** @deprecated Import from @backstage/core-plugin-api instead */
|
||||
export function createRoutableExtension<
|
||||
T extends (props: any) => JSX.Element | null
|
||||
>(options: {
|
||||
>(_options: {
|
||||
component: () => Promise<T>;
|
||||
mountPoint: RouteRef;
|
||||
}): Extension<T> {
|
||||
const { component, mountPoint } = options;
|
||||
return createReactExtension({
|
||||
component: {
|
||||
lazy: () =>
|
||||
component().then(
|
||||
InnerComponent => {
|
||||
const RoutableExtensionWrapper: any = (props: any) => {
|
||||
// Validate that the routing is wired up correctly in the App.tsx
|
||||
try {
|
||||
useRouteRef(mountPoint);
|
||||
} catch (error) {
|
||||
if (error?.message.startsWith('No path for ')) {
|
||||
throw new Error(
|
||||
`Routable extension component with mount point ${mountPoint} was not discovered in the app element tree. ` +
|
||||
'Routable extension components may not be rendered by other components and must be ' +
|
||||
'directly available as an element within the App provider component.',
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return <InnerComponent {...props} />;
|
||||
};
|
||||
|
||||
const componentName =
|
||||
(InnerComponent as { displayName?: string }).displayName ||
|
||||
InnerComponent.name ||
|
||||
'LazyComponent';
|
||||
|
||||
RoutableExtensionWrapper.displayName = `RoutableExtension(${componentName})`;
|
||||
|
||||
return RoutableExtensionWrapper as T;
|
||||
},
|
||||
error => {
|
||||
const RoutableExtensionWrapper: any = (_: any) => {
|
||||
const app = useApp();
|
||||
const { BootErrorPage } = app.getComponents();
|
||||
|
||||
return <BootErrorPage step="load-chunk" error={error} />;
|
||||
};
|
||||
return RoutableExtensionWrapper;
|
||||
},
|
||||
),
|
||||
},
|
||||
data: {
|
||||
'core.mountPoint': mountPoint,
|
||||
},
|
||||
});
|
||||
throw new Error(ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
// We do not use ComponentType as the return type, since it doesn't let us convey the children prop.
|
||||
// ComponentType inserts children as an optional prop whether the inner component accepts it or not,
|
||||
// making it impossible to make the usage of children type safe.
|
||||
/** @deprecated Import from @backstage/core-plugin-api instead */
|
||||
export function createComponentExtension<
|
||||
T extends (props: any) => JSX.Element | null
|
||||
>(options: { component: ComponentLoader<T> }): Extension<T> {
|
||||
const { component } = options;
|
||||
return createReactExtension({ component });
|
||||
>(_options: { component: ComponentLoader<T> }): Extension<T> {
|
||||
throw new Error(ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
// We do not use ComponentType as the return type, since it doesn't let us convey the children prop.
|
||||
// ComponentType inserts children as an optional prop whether the inner component accepts it or not,
|
||||
// making it impossible to make the usage of children type safe.
|
||||
/** @deprecated Import from @backstage/core-plugin-api instead */
|
||||
export function createReactExtension<
|
||||
T extends (props: any) => JSX.Element | null
|
||||
>(options: {
|
||||
>(_options: {
|
||||
component: ComponentLoader<T>;
|
||||
data?: Record<string, unknown>;
|
||||
}): Extension<T> {
|
||||
const { data = {} } = options;
|
||||
|
||||
let Component: T;
|
||||
if ('lazy' in options.component) {
|
||||
const lazyLoader = options.component.lazy;
|
||||
Component = (lazy(() =>
|
||||
lazyLoader().then(component => ({ default: component })),
|
||||
) as unknown) as T;
|
||||
} else {
|
||||
Component = options.component.sync;
|
||||
}
|
||||
const componentName =
|
||||
(Component as { displayName?: string }).displayName ||
|
||||
Component.name ||
|
||||
'Component';
|
||||
|
||||
return {
|
||||
expose(plugin: BackstagePlugin<any, any>) {
|
||||
const Result: any = (props: any) => (
|
||||
<Suspense fallback="...">
|
||||
<Component {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
attachComponentData(Result, 'core.plugin', plugin);
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
attachComponentData(Result, key, value);
|
||||
}
|
||||
|
||||
Result.displayName = `Extension(${componentName})`;
|
||||
return Result;
|
||||
},
|
||||
};
|
||||
throw new Error(ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { useApp } from '../app';
|
||||
import { RouteRef, useRouteRef } from '../routing';
|
||||
import { attachComponentData } from './componentData';
|
||||
import { Extension, BackstagePlugin } from '../plugin/types';
|
||||
@@ -27,8 +28,11 @@ type ComponentLoader<T> =
|
||||
sync: T;
|
||||
};
|
||||
|
||||
// We do not use ComponentType as the return type, since it doesn't let us convey the children prop.
|
||||
// ComponentType inserts children as an optional prop whether the inner component accepts it or not,
|
||||
// making it impossible to make the usage of children type safe.
|
||||
export function createRoutableExtension<
|
||||
T extends (props: any) => JSX.Element
|
||||
T extends (props: any) => JSX.Element | null
|
||||
>(options: {
|
||||
component: () => Promise<T>;
|
||||
mountPoint: RouteRef;
|
||||
@@ -37,22 +41,44 @@ export function createRoutableExtension<
|
||||
return createReactExtension({
|
||||
component: {
|
||||
lazy: () =>
|
||||
component().then(InnerComponent => {
|
||||
const RoutableExtensionWrapper = ((props: any) => {
|
||||
// Validate that the routing is wired up correctly in the App.tsx
|
||||
try {
|
||||
useRouteRef(mountPoint);
|
||||
} catch {
|
||||
throw new Error(
|
||||
'Routable extension component was not discovered in the app element tree. ' +
|
||||
'Routable extension components may not be rendered by other components and must be ' +
|
||||
'directly available as an element within the App provider component.',
|
||||
);
|
||||
}
|
||||
return <InnerComponent {...props} />;
|
||||
}) as T;
|
||||
return RoutableExtensionWrapper;
|
||||
}),
|
||||
component().then(
|
||||
InnerComponent => {
|
||||
const RoutableExtensionWrapper: any = (props: any) => {
|
||||
// Validate that the routing is wired up correctly in the App.tsx
|
||||
try {
|
||||
useRouteRef(mountPoint);
|
||||
} catch (error) {
|
||||
if (error?.message.startsWith('No path for ')) {
|
||||
throw new Error(
|
||||
`Routable extension component with mount point ${mountPoint} was not discovered in the app element tree. ` +
|
||||
'Routable extension components may not be rendered by other components and must be ' +
|
||||
'directly available as an element within the App provider component.',
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return <InnerComponent {...props} />;
|
||||
};
|
||||
|
||||
const componentName =
|
||||
(InnerComponent as { displayName?: string }).displayName ||
|
||||
InnerComponent.name ||
|
||||
'LazyComponent';
|
||||
|
||||
RoutableExtensionWrapper.displayName = `RoutableExtension(${componentName})`;
|
||||
|
||||
return RoutableExtensionWrapper as T;
|
||||
},
|
||||
error => {
|
||||
const RoutableExtensionWrapper: any = (_: any) => {
|
||||
const app = useApp();
|
||||
const { BootErrorPage } = app.getComponents();
|
||||
|
||||
return <BootErrorPage step="load-chunk" error={error} />;
|
||||
};
|
||||
return RoutableExtensionWrapper;
|
||||
},
|
||||
),
|
||||
},
|
||||
data: {
|
||||
'core.mountPoint': mountPoint,
|
||||
@@ -60,15 +86,21 @@ export function createRoutableExtension<
|
||||
});
|
||||
}
|
||||
|
||||
// We do not use ComponentType as the return type, since it doesn't let us convey the children prop.
|
||||
// ComponentType inserts children as an optional prop whether the inner component accepts it or not,
|
||||
// making it impossible to make the usage of children type safe.
|
||||
export function createComponentExtension<
|
||||
T extends (props: any) => JSX.Element
|
||||
T extends (props: any) => JSX.Element | null
|
||||
>(options: { component: ComponentLoader<T> }): Extension<T> {
|
||||
const { component } = options;
|
||||
return createReactExtension({ component });
|
||||
}
|
||||
|
||||
// We do not use ComponentType as the return type, since it doesn't let us convey the children prop.
|
||||
// ComponentType inserts children as an optional prop whether the inner component accepts it or not,
|
||||
// making it impossible to make the usage of children type safe.
|
||||
export function createReactExtension<
|
||||
T extends (props: any) => JSX.Element
|
||||
T extends (props: any) => JSX.Element | null
|
||||
>(options: {
|
||||
component: ComponentLoader<T>;
|
||||
data?: Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user