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:
Patrik Oldsberg
2021-06-08 18:40:14 +02:00
parent ae0e8ceecf
commit da8cba44f4
5 changed files with 73 additions and 197 deletions
+5
View File
@@ -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.
+5
View File
@@ -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>;