core-compat-api: add initial compatWrapper

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-11-27 16:12:25 +01:00
parent 422521a645
commit 822644285a
10 changed files with 296 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-compat-api': patch
---
Added `compatWrapper`, which can be used to wrap any React element to provide bi-directional interoperability between the `@backstage/core-*-api` and `@backstage/frontend-*-api` APIs.
+4
View File
@@ -13,6 +13,7 @@ import { ExtensionOverrides } from '@backstage/frontend-plugin-api';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { ExternalRouteRef as ExternalRouteRef_2 } from '@backstage/frontend-plugin-api';
import { default as React_2 } from 'react';
import { ReactNode } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api';
import { SubRouteRef } from '@backstage/core-plugin-api';
@@ -28,6 +29,9 @@ export function collectLegacyRoutes(
flatRoutesElement: JSX.Element,
): BackstagePlugin[];
// @public
export function compatWrapper(element: ReactNode): React_2.JSX.Element;
// @public (undocumented)
export function convertLegacyApp(
rootElement: React_2.JSX.Element,
+4 -1
View File
@@ -24,10 +24,12 @@
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/frontend-test-utils": "workspace:^",
"@backstage/plugin-puppetdb": "workspace:^",
"@backstage/plugin-stackstorm": "workspace:^",
"@oriflame/backstage-plugin-score-card": "^0.7.0",
"@testing-library/jest-dom": "^6.0.0"
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0"
},
"files": [
"dist"
@@ -40,6 +42,7 @@
"@backstage/core-app-api": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/version-bridge": "workspace:^",
"@types/react": "^16.13.1 || ^17.0.0"
}
}
@@ -0,0 +1,129 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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, { useMemo } from 'react';
import { ReactNode } from 'react';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppContextProvider } from '../../../core-app-api/src/app/AppContext';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import {
components as defaultComponents,
icons as defaultIcons,
} from '../../../app-defaults/src/defaults';
import {
BackstagePlugin as NewBackstagePlugin,
appTreeApiRef,
useApi,
} from '@backstage/frontend-plugin-api';
import {
AppComponents,
IconComponent,
BackstagePlugin as LegacyBackstagePlugin,
} from '@backstage/core-plugin-api';
import { getOrCreateGlobalSingleton } from '@backstage/version-bridge';
// Make sure that we only convert each new plugin instance to its legacy equivalent once
const legacyPluginStore = getOrCreateGlobalSingleton(
'legacy-plugin-compatibility-store',
() => new WeakMap<NewBackstagePlugin, LegacyBackstagePlugin>(),
);
function toLegacyPlugin(plugin: NewBackstagePlugin): LegacyBackstagePlugin {
let legacy = legacyPluginStore.get(plugin);
if (legacy) {
return legacy;
}
const errorMsg = 'Not implemented in legacy plugin compatibility layer';
const notImplemented = () => {
throw new Error(errorMsg);
};
legacy = {
getId(): string {
return plugin.id;
},
get routes() {
return {};
},
get externalRoutes() {
return {};
},
getApis: notImplemented,
getFeatureFlags: notImplemented,
provide: notImplemented,
};
legacyPluginStore.set(plugin, legacy);
return legacy;
}
// Recreates the old AppContext APIs using the various new APIs that replaced it
function LegacyAppContextProvider(props: { children: ReactNode }) {
const appTreeApi = useApi(appTreeApiRef);
const appContext = useMemo(() => {
const { tree } = appTreeApi.getTree();
let gatheredPlugins: LegacyBackstagePlugin[] | undefined = undefined;
return {
getPlugins(): LegacyBackstagePlugin[] {
if (gatheredPlugins) {
return gatheredPlugins;
}
const pluginSet = new Set<LegacyBackstagePlugin>();
for (const node of tree.nodes.values()) {
const plugin = node.spec.source;
if (plugin) {
pluginSet.add(toLegacyPlugin(plugin));
}
}
gatheredPlugins = Array.from(pluginSet);
return gatheredPlugins;
},
// TODO: Grab these from new API once it exists
getSystemIcon(key: string): IconComponent | undefined {
return key in defaultIcons
? defaultIcons[key as keyof typeof defaultIcons]
: undefined;
},
// TODO: Grab these from new API once it exists
getSystemIcons(): Record<string, IconComponent> {
return defaultIcons;
},
// TODO: Grab these from new API once it exists
getComponents(): AppComponents {
return defaultComponents;
},
};
}, [appTreeApi]);
return (
<AppContextProvider appContext={appContext}>
{props.children}
</AppContextProvider>
);
}
export function BackwardsCompatProvider(props: { children: ReactNode }) {
return <LegacyAppContextProvider>{props.children}</LegacyAppContextProvider>;
}
@@ -0,0 +1,23 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { ReactNode } from 'react';
export function ForwardsCompatProvider(props: { children: ReactNode }) {
// TODO(Rugvip): Implement
return <>{props.children}</>;
}
@@ -0,0 +1,67 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 {
coreExtensionData,
createExtension,
} from '@backstage/frontend-plugin-api';
import { createExtensionTester } from '@backstage/frontend-test-utils';
import { screen } from '@testing-library/react';
import { compatWrapper } from './compatWrapper';
import { useApp } from '@backstage/core-plugin-api';
describe('BackwardsCompatProvider', () => {
it('should convert the app context', () => {
// TODO(Rugvip): Replace with the new renderInTestApp once it's available, and have some plugins
createExtensionTester(
createExtension({
attachTo: { id: 'ignored', input: 'ignored' },
output: {
element: coreExtensionData.reactElement,
},
factory() {
function Component() {
const app = useApp();
return (
<div data-testid="ctx">
plugins:
{app
.getPlugins()
.map(p => p.getId())
.join(', ')}
{'\n'}
components: {Object.keys(app.getComponents()).join(', ')}
{'\n'}
icons: {Object.keys(app.getSystemIcons()).join(', ')}
</div>
);
}
return {
element: compatWrapper(<Component />),
};
},
}),
).render();
expect(screen.getByTestId('ctx').textContent).toMatchInlineSnapshot(`
"plugins:
components: Progress, Router, NotFoundErrorPage, BootErrorPage, ErrorBoundaryFallback
icons: brokenImage, catalog, scaffolder, techdocs, search, chat, dashboard, docs, email, github, group, help, kind:api, kind:component, kind:domain, kind:group, kind:location, kind:system, kind:user, kind:resource, kind:template, user, warning"
`);
});
});
@@ -0,0 +1,42 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { useVersionedContext } from '@backstage/version-bridge';
import { ReactNode } from 'react';
import { BackwardsCompatProvider } from './BackwardsCompatProvider';
import { ForwardsCompatProvider } from './ForwardsCompatProvider';
function BidirectionalCompatProvider(props: { children: ReactNode }) {
const isInNewApp = !useVersionedContext<{ 1: unknown }>('app-context');
if (isInNewApp) {
return <BackwardsCompatProvider {...props} />;
}
return <ForwardsCompatProvider {...props} />;
}
/**
* Wraps a React element in a bidirectional compatibility provider, allow APIs
* from `@backstage/core-plugin-api` to be used in an app from `@backstage/frontend-app-api`,
* and APIs from `@backstage/frontend-plugin-api` to be used in an app from `@backstage/core-app-api`.
*
* @public
*/
export function compatWrapper(element: ReactNode) {
return <BidirectionalCompatProvider>{element}</BidirectionalCompatProvider>;
}
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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.
*/
export { compatWrapper } from './compatWrapper';
+2
View File
@@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './compatWrapper';
export { collectLegacyRoutes } from './collectLegacyRoutes';
export { collectLegacyComponents } from './collectLegacyComponents';
export { convertLegacyApp } from './convertLegacyApp';
+3
View File
@@ -3851,10 +3851,13 @@ __metadata:
"@backstage/core-app-api": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/frontend-test-utils": "workspace:^"
"@backstage/plugin-puppetdb": "workspace:^"
"@backstage/plugin-stackstorm": "workspace:^"
"@backstage/version-bridge": "workspace:^"
"@oriflame/backstage-plugin-score-card": ^0.7.0
"@testing-library/jest-dom": ^6.0.0
"@testing-library/react": ^14.0.0
"@types/react": ^16.13.1 || ^17.0.0
peerDependencies:
react: ^16.13.1 || ^17.0.0 || ^18.0.0