diff --git a/.changeset/tall-beds-bake.md b/.changeset/tall-beds-bake.md
new file mode 100644
index 0000000000..2a0c9f7023
--- /dev/null
+++ b/.changeset/tall-beds-bake.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-app-api': patch
+---
+
+Removed deprecated internal functions.
diff --git a/.changeset/wicked-files-brush.md b/.changeset/wicked-files-brush.md
new file mode 100644
index 0000000000..c4391ba4dd
--- /dev/null
+++ b/.changeset/wicked-files-brush.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-plugin-api': patch
+---
+
+Migrated component data attachment method to have better compatibility with component proxies such as `react-hot-loader`.
diff --git a/packages/core-app-api/src/extensions/componentData.test.tsx b/packages/core-app-api/src/extensions/componentData.test.tsx
deleted file mode 100644
index 808ab08cf1..0000000000
--- a/packages/core-app-api/src/extensions/componentData.test.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright 2020 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 { attachComponentData, getComponentData } from './componentData';
-
-describe('elementData', () => {
- it('should attach a single piece of data', () => {
- const data = { foo: 'bar' };
- const Component = () => null;
- attachComponentData(Component, 'my-data', data);
-
- const element = ;
- expect(getComponentData(element, 'my-data')).toBe(data);
- });
-
- it('should attach several distinct pieces of data', () => {
- const data1 = { foo: 'bar' };
- const data2 = { test: 'value' };
- const Component = () => null;
- attachComponentData(Component, 'my-data', data1);
- attachComponentData(Component, 'second', data2);
-
- const element = ;
- expect(getComponentData(element, 'my-data')).toBe(data1);
- expect(getComponentData(element, 'second')).toBe(data2);
- });
-
- it('returns undefined for missing data', () => {
- const data = { foo: 'bar' };
- const Component1 = () => null;
- const Component2 = () => null;
- attachComponentData(Component2, 'my-data', data);
-
- const element1 = ;
- const element2 = ;
- expect(getComponentData(element1, 'missing')).toBeUndefined();
- expect(getComponentData(element2, 'missing')).toBeUndefined();
- });
-
- it('should throw when attempting to overwrite data', () => {
- const data = { foo: 'bar' };
- const MyComponent = () => null;
- attachComponentData(MyComponent, 'my-data', data);
- expect(() => attachComponentData(MyComponent, 'my-data', data)).toThrow(
- 'Attempted to attach duplicate data "my-data" to component "MyComponent"',
- );
- });
-
- describe('works across versions', () => {
- function getDataSymbol() {
- const Component = () => null;
- attachComponentData(Component, 'my-data', {});
- const [symbol] = Object.getOwnPropertySymbols(Component);
- return symbol;
- }
-
- it('should should be able to get data from older versions', () => {
- const symbol = getDataSymbol();
-
- const data = { foo: 'bar' };
- const Component = () => null;
- attachComponentData(Component, 'my-data', data);
-
- const element = ;
- expect((element as any).type[symbol].map.get('my-data')).toBe(data);
- });
-
- it('should should be able to attach data for older versions', () => {
- const symbol = getDataSymbol();
-
- const data = { foo: 'bar' };
- const Component = () => null;
- (Component as any)[symbol] = {
- map: new Map([['my-data', data]]),
- };
-
- const element = ;
- expect(getComponentData(element, 'my-data')).toBe(data);
- });
-
- it('should be able to get data from newer versions', () => {
- const data = { foo: 'bar' };
- const Component = () => null;
- attachComponentData(Component, 'my-data', data);
-
- const element = ;
- const container = (global as any)[
- '__@backstage/component-data-store__'
- ].get(element.type);
- expect(container.map.get('my-data')).toBe(data);
- });
-
- it('should should be able to attach data for newer versions', () => {
- const data = { foo: 'bar' };
- const Component = () => null;
- (global as any)['__@backstage/component-data-store__'].set(Component, {
- map: new Map([['my-data', data]]),
- });
-
- const element = ;
- expect(getComponentData(element, 'my-data')).toBe(data);
- });
- });
-});
diff --git a/packages/core-app-api/src/extensions/componentData.tsx b/packages/core-app-api/src/extensions/componentData.tsx
deleted file mode 100644
index d4975d9eef..0000000000
--- a/packages/core-app-api/src/extensions/componentData.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright 2020 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 { ComponentType, ReactNode } from 'react';
-import { getOrCreateGlobalSingleton } from '../lib/globalObject';
-
-// TODO(Rugvip): Access via symbol is deprecated, remove once on 0.3.x
-const DATA_KEY = Symbol('backstage-component-data');
-
-type ComponentWithData
= ComponentType
& {
- [DATA_KEY]?: DataContainer;
-};
-
-type DataContainer = {
- map: Map;
-};
-
-type MaybeComponentNode = ReactNode & {
- type?: ComponentType & { [DATA_KEY]?: DataContainer };
-};
-
-// The store is bridged across versions using the global object
-const store = getOrCreateGlobalSingleton(
- 'component-data-store',
- () => new WeakMap, DataContainer>(),
-);
-
-export function attachComponentData
;
-
- let container = store.get(component) || dataComponent[DATA_KEY];
- if (!container) {
- container = { map: new Map() };
- store.set(component, container);
- dataComponent[DATA_KEY] = container;
- }
-
- if (container.map.has(type)) {
- const name = component.displayName || component.name;
- throw new Error(
- `Attempted to attach duplicate data "${type}" to component "${name}"`,
- );
- }
-
- container.map.set(type, data);
-}
-
-export function getComponentData(
- node: ReactNode,
- type: string,
-): T | undefined {
- if (!node) {
- return undefined;
- }
-
- const component = (node as MaybeComponentNode).type;
- if (!component) {
- return undefined;
- }
-
- const container = store.get(component) || component[DATA_KEY];
- if (!container) {
- return undefined;
- }
-
- return container.map.get(type) as T | undefined;
-}
diff --git a/packages/core-app-api/src/extensions/extensions.tsx b/packages/core-app-api/src/extensions/extensions.tsx
deleted file mode 100644
index 199430c619..0000000000
--- a/packages/core-app-api/src/extensions/extensions.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2020 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 { Extension, RouteRef } from '@backstage/core-plugin-api';
-
-type ComponentLoader =
- | {
- lazy: () => Promise;
- }
- | {
- sync: T;
- };
-
-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: {
- component: () => Promise;
- mountPoint: RouteRef;
-}): Extension {
- throw new Error(ERROR_MESSAGE);
-}
-
-/** @deprecated Import from @backstage/core-plugin-api instead */
-export function createComponentExtension<
- T extends (props: any) => JSX.Element | null,
->(_options: { component: ComponentLoader }): Extension {
- throw new Error(ERROR_MESSAGE);
-}
-
-/** @deprecated Import from @backstage/core-plugin-api instead */
-export function createReactExtension<
- T extends (props: any) => JSX.Element | null,
->(_options: {
- component: ComponentLoader;
- data?: Record;
-}): Extension {
- throw new Error(ERROR_MESSAGE);
-}
diff --git a/packages/core-app-api/src/extensions/index.ts b/packages/core-app-api/src/extensions/index.ts
deleted file mode 100644
index 914d76aebf..0000000000
--- a/packages/core-app-api/src/extensions/index.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright 2020 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 { attachComponentData, getComponentData } from './componentData';
-export {
- createReactExtension,
- createRoutableExtension,
- createComponentExtension,
-} from './extensions';
diff --git a/packages/core-app-api/src/plugins/collectors.ts b/packages/core-app-api/src/plugins/collectors.ts
index 11e5cd51b3..0f8b65ad62 100644
--- a/packages/core-app-api/src/plugins/collectors.ts
+++ b/packages/core-app-api/src/plugins/collectors.ts
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-import { BackstagePlugin } from '@backstage/core-plugin-api';
-import { getComponentData } from '../extensions';
+import { BackstagePlugin, getComponentData } from '@backstage/core-plugin-api';
import { createCollector } from '../extensions/traversal';
export const pluginCollector = createCollector(
diff --git a/packages/core-app-api/src/routing/collectors.test.tsx b/packages/core-app-api/src/routing/collectors.test.tsx
index d37b5e3016..36d99d2938 100644
--- a/packages/core-app-api/src/routing/collectors.test.tsx
+++ b/packages/core-app-api/src/routing/collectors.test.tsx
@@ -31,8 +31,8 @@ import {
createRouteRef,
createPlugin,
RouteRef,
+ attachComponentData,
} from '@backstage/core-plugin-api';
-import { attachComponentData } from '../extensions';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => (
diff --git a/packages/core-app-api/src/routing/collectors.tsx b/packages/core-app-api/src/routing/collectors.tsx
index fcbe3ca923..0ba83e90f8 100644
--- a/packages/core-app-api/src/routing/collectors.tsx
+++ b/packages/core-app-api/src/routing/collectors.tsx
@@ -15,9 +15,8 @@
*/
import { isValidElement, ReactElement, ReactNode } from 'react';
-import { RouteRef } from '@backstage/core-plugin-api';
+import { RouteRef, getComponentData } from '@backstage/core-plugin-api';
import { BackstageRouteObject } from './types';
-import { getComponentData } from '../extensions';
import { createCollector } from '../extensions/traversal';
import { FeatureFlagged, FeatureFlaggedProps } from './FeatureFlagged';
diff --git a/packages/core-components/src/components/Button/Button.stories.tsx b/packages/core-components/src/components/Button/Button.stories.tsx
index 28deb1ad33..3451a77765 100644
--- a/packages/core-components/src/components/Button/Button.stories.tsx
+++ b/packages/core-components/src/components/Button/Button.stories.tsx
@@ -15,7 +15,7 @@
*/
import React, { ComponentType } from 'react';
import { Button } from './Button';
-import { MemoryRouter, useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
import { createRouteRef, useRouteRef } from '@backstage/core-plugin-api';
import {
Divider,
@@ -26,11 +26,7 @@ import {
Typography,
Button as MaterialButton,
} from '@material-ui/core';
-// We don't want to export RoutingProvider from core-app-api, but it's way easier to
-// use here. This hack only works in storybook stories.
-// TODO: Export a nicer to user routing provider, perhaps from test-utils
-// eslint-disable-next-line monorepo/no-internal-import
-import { RoutingProvider } from '@backstage/core-app-api/src/routing/RoutingProvider';
+import { wrapInTestApp } from '@backstage/test-utils';
const routeRef = createRouteRef({
id: 'storybook.test-route',
@@ -45,36 +41,29 @@ export default {
title: 'Inputs/Button',
component: Button,
decorators: [
- (Story: ComponentType<{}>) => (
- <>
-
- A collection of buttons that should be used in the Backstage
- interface. These leverage the properties inherited from{' '}
-
- Material-UI Button
-
- , but include an opinionated set that align to the Backstage design.
-
+ (Story: ComponentType<{}>) =>
+ wrapInTestApp(
+ <>
+
+ A collection of buttons that should be used in the Backstage
+ interface. These leverage the properties inherited from{' '}
+
+ Material-UI Button
+
+ , but include an opinionated set that align to the Backstage design.
+
-
+
-
-
+
-
-
-
-
+
-
-
- >
- ),
+
+
+ >,
+ { mountedRoutes: { '/hello': routeRef } },
+ ),
],
};
diff --git a/packages/core-components/src/components/Link/Link.stories.tsx b/packages/core-components/src/components/Link/Link.stories.tsx
index 516a5adcff..d9944c1c51 100644
--- a/packages/core-components/src/components/Link/Link.stories.tsx
+++ b/packages/core-components/src/components/Link/Link.stories.tsx
@@ -15,17 +15,9 @@
*/
import React, { ComponentType } from 'react';
import { Link } from './Link';
-import {
- MemoryRouter,
- Route,
- useLocation,
- NavLink as RouterNavLink,
-} from 'react-router-dom';
+import { Route, useLocation, NavLink as RouterNavLink } from 'react-router-dom';
import { createRouteRef, useRouteRef } from '@backstage/core-plugin-api';
-// We don't want to export RoutingProvider from core-app-api, but it's way easier to
-// use here. This hack only works in storybook stories.
-// eslint-disable-next-line monorepo/no-internal-import
-import { RoutingProvider } from '@backstage/core-app-api/src/routing/RoutingProvider';
+import { wrapInTestApp } from '@backstage/test-utils';
const routeRef = createRouteRef({
id: 'storybook.test-route',
@@ -40,23 +32,16 @@ export default {
title: 'Navigation/Link',
component: Link,
decorators: [
- (Story: ComponentType<{}>) => (
-
-
+ (Story: ComponentType<{}>) =>
+ wrapInTestApp(
+
-
-
-
-
+
-
-
- ),
+
+
,
+ { mountedRoutes: { '/hello': routeRef } },
+ ),
],
};
diff --git a/packages/core-plugin-api/src/extensions/componentData.test.tsx b/packages/core-plugin-api/src/extensions/componentData.test.tsx
index 49a1d59cc4..6f2694a512 100644
--- a/packages/core-plugin-api/src/extensions/componentData.test.tsx
+++ b/packages/core-plugin-api/src/extensions/componentData.test.tsx
@@ -17,7 +17,7 @@
import React from 'react';
import { attachComponentData, getComponentData } from './componentData';
-describe('elementData', () => {
+describe('componentData', () => {
it('should attach a single piece of data', () => {
const data = { foo: 'bar' };
const Component = () => null;
@@ -59,4 +59,51 @@ describe('elementData', () => {
'Attempted to attach duplicate data "my-data" to component "MyComponent"',
);
});
+
+ describe('works across versions', () => {
+ it('should should be able to get data from newer versions', () => {
+ const data = { foo: 'bar' };
+ const Component = () => null;
+ attachComponentData(Component, 'my-data', data);
+
+ const element = ;
+ expect((element as any).type.__backstage_data.map.get('my-data')).toBe(
+ data,
+ );
+ });
+
+ it('should should be able to attach data for newer versions', () => {
+ const data = { foo: 'bar' };
+ const Component = () => null;
+ (Component as any).__backstage_data = {
+ map: new Map([['my-data', data]]),
+ };
+
+ const element = ;
+ expect(getComponentData(element, 'my-data')).toBe(data);
+ });
+
+ it('should be able to get data from older versions', () => {
+ const data = { foo: 'bar' };
+ const Component = () => null;
+ attachComponentData(Component, 'my-data', data);
+
+ const element = ;
+ const container = (global as any)[
+ '__@backstage/component-data-store__'
+ ].get(element.type);
+ expect(container.map.get('my-data')).toBe(data);
+ });
+
+ it('should should be able to attach data for older versions', () => {
+ const data = { foo: 'bar' };
+ const Component = () => null;
+ (global as any)['__@backstage/component-data-store__'].set(Component, {
+ map: new Map([['my-data', data]]),
+ });
+
+ const element = ;
+ expect(getComponentData(element, 'my-data')).toBe(data);
+ });
+ });
});
diff --git a/packages/core-plugin-api/src/extensions/componentData.tsx b/packages/core-plugin-api/src/extensions/componentData.tsx
index 85527950ab..d28d17d8ca 100644
--- a/packages/core-plugin-api/src/extensions/componentData.tsx
+++ b/packages/core-plugin-api/src/extensions/componentData.tsx
@@ -21,24 +21,42 @@ type DataContainer = {
map: Map;
};
-type MaybeComponentNode = ReactNode & {
- type?: ComponentType;
-};
-
-// The store is bridged across versions using the global object
+// This method of storing the component data was deprecated in September 2021, it
+// will be removed in the future for the reasons described below.
const globalStore = getOrCreateGlobalSingleton(
'component-data-store',
() => new WeakMap, DataContainer>(),
);
+// This key is used to attach component data to the component type (function or class)
+// itself. This method is used because it has better compatibility component wrappers
+// like react-hot-loader, as opposed to the WeakMap method or using a symbol.
+const componentDataKey = '__backstage_data';
+
+type ComponentWithData = ComponentType & {
+ [componentDataKey]?: DataContainer;
+};
+
+type MaybeComponentNode = ReactNode & {
+ type?: ComponentWithData;
+};
+
export function attachComponentData