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-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

( component: ComponentType

, type: string, data: unknown, ) { - let container = globalStore.get(component); + const dataComponent = component as ComponentWithData; + + let container = dataComponent[componentDataKey] ?? globalStore.get(component); if (!container) { container = { map: new Map() }; + Object.defineProperty(dataComponent, componentDataKey, { + enumerable: false, + configurable: true, + writable: false, + value: container, + }); globalStore.set(component, container); } @@ -65,7 +83,7 @@ export function getComponentData( return undefined; } - const container = globalStore.get(component); + const container = component[componentDataKey] ?? globalStore.get(component); if (!container) { return undefined; }