diff --git a/.changeset/rich-socks-move.md b/.changeset/rich-socks-move.md new file mode 100644 index 0000000000..5aeb3121f1 --- /dev/null +++ b/.changeset/rich-socks-move.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-api': patch +'@backstage/core': patch +--- + +Internal refactor of how component data is access to avoid polluting components and make it possible to bridge across versions. diff --git a/packages/core-api/src/extensions/componentData.test.tsx b/packages/core-api/src/extensions/componentData.test.tsx index 6c4abda40f..cb0918b290 100644 --- a/packages/core-api/src/extensions/componentData.test.tsx +++ b/packages/core-api/src/extensions/componentData.test.tsx @@ -59,4 +59,63 @@ describe('elementData', () => { '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__' + ].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__'].store.set( + Component, + { + map: new Map([['my-data', data]]), + }, + ); + + const element = ; + expect(getComponentData(element, 'my-data')).toBe(data); + }); + }); }); diff --git a/packages/core-api/src/extensions/componentData.tsx b/packages/core-api/src/extensions/componentData.tsx index 1f88356a48..3d2558276d 100644 --- a/packages/core-api/src/extensions/componentData.tsx +++ b/packages/core-api/src/extensions/componentData.tsx @@ -15,21 +15,40 @@ */ import { ComponentType, ReactNode } from 'react'; +import { globalObject } from '../lib/globalObject'; +// TODO(Rugvip): Access via symbol is deprecated, remove once on 0.3.x const DATA_KEY = Symbol('backstage-component-data'); -type DataContainer = { - map: Map; -}; - type ComponentWithData

= ComponentType

& { [DATA_KEY]?: DataContainer; }; -type ReactNodeWithData = ReactNode & { - type?: { [DATA_KEY]?: DataContainer }; +type DataContainer = { + map: Map; }; +type MaybeComponentNode = ReactNode & { + type?: ComponentType & { [DATA_KEY]?: DataContainer }; +}; + +const GLOBAL_KEY = '__@backstage/component-data-store__'; + +// The store is bridged across versions using the global object +function getStore() { + let storeObj = globalObject[GLOBAL_KEY] as + | { store: WeakMap, DataContainer> } + | undefined; + if (!storeObj) { + const store = new WeakMap, DataContainer>(); + storeObj = { store }; + globalObject[GLOBAL_KEY] = storeObj; + } + return storeObj.store; +} + +const store = getStore(); + export function attachComponentData

( component: ComponentType

, type: string, @@ -37,9 +56,11 @@ export function attachComponentData

( ) { const dataComponent = component as ComponentWithData

; - let container = dataComponent[DATA_KEY]; + let container = store.get(component) || dataComponent[DATA_KEY]; if (!container) { - container = dataComponent[DATA_KEY] = { map: new Map() }; + container = { map: new Map() }; + store.set(component, container); + dataComponent[DATA_KEY] = container; } if (container.map.has(type)) { @@ -60,7 +81,12 @@ export function getComponentData( return undefined; } - const container = (node as ReactNodeWithData).type?.[DATA_KEY]; + const component = (node as MaybeComponentNode).type; + if (!component) { + return undefined; + } + + const container = store.get(component) || component[DATA_KEY]; if (!container) { return undefined; } diff --git a/packages/core-api/src/lib/globalObject.ts b/packages/core-api/src/lib/globalObject.ts new file mode 100644 index 0000000000..85f16212fe --- /dev/null +++ b/packages/core-api/src/lib/globalObject.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2021 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. + */ + +// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 +function getGlobal() { + if (typeof window !== 'undefined' && window.Math === Math) { + return window; + } + if (typeof self !== 'undefined' && self.Math === Math) { + return self; + } + // eslint-disable-next-line no-new-func + return Function('return this')(); +} + +export const globalObject = getGlobal();