diff --git a/.changeset/olive-singers-sparkle.md b/.changeset/olive-singers-sparkle.md new file mode 100644 index 0000000000..14e45867a0 --- /dev/null +++ b/.changeset/olive-singers-sparkle.md @@ -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. diff --git a/.changeset/pink-bags-tickle.md b/.changeset/pink-bags-tickle.md new file mode 100644 index 0000000000..9c600459e7 --- /dev/null +++ b/.changeset/pink-bags-tickle.md @@ -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`. diff --git a/packages/core-app-api/src/extensions/extensions.test.tsx b/packages/core-app-api/src/extensions/extensions.test.tsx deleted file mode 100644 index e8770dddbb..0000000000 --- a/packages/core-app-api/src/extensions/extensions.test.tsx +++ /dev/null @@ -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 = () =>
; - - const extension = createReactExtension({ - component: { - sync: Component, - }, - data: { - myData: { foo: 'bar' }, - }, - }); - - const ExtensionComponent = plugin.provide(extension); - const element = ; - - expect(getComponentData(element, 'core.plugin')).toBe(plugin); - expect(getComponentData(element, 'myData')).toEqual({ foo: 'bar' }); - }); - - it('should create react extensions of different types', () => { - const Component = () =>
; - 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 = ; - const element2 = ; - - expect(getComponentData(element1, 'core.plugin')).toBe(plugin); - expect(getComponentData(element2, 'core.plugin')).toBe(plugin); - expect(getComponentData(element2, 'core.mountPoint')).toBe(routeRef); - }); -}); diff --git a/packages/core-app-api/src/extensions/extensions.tsx b/packages/core-app-api/src/extensions/extensions.tsx index 8eab3b905f..a9121c4d15 100644 --- a/packages/core-app-api/src/extensions/extensions.tsx +++ b/packages/core-app-api/src/extensions/extensions.tsx @@ -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 = | { @@ -32,114 +24,31 @@ type ComponentLoader = 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; mountPoint: RouteRef; }): Extension { - 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 ; - }; - - 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 ; - }; - 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 }): Extension { - const { component } = options; - return createReactExtension({ component }); +>(_options: { component: ComponentLoader }): Extension { + 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; data?: Record; }): Extension { - 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) { - const Result: any = (props: any) => ( - - - - ); - - 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); } diff --git a/packages/core-plugin-api/src/extensions/extensions.tsx b/packages/core-plugin-api/src/extensions/extensions.tsx index e56b6901dd..d6597ba8a9 100644 --- a/packages/core-plugin-api/src/extensions/extensions.tsx +++ b/packages/core-plugin-api/src/extensions/extensions.tsx @@ -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 = 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; 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 ; - }) 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 ; + }; + + 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 ; + }; + 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 }): Extension { 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; data?: Record;