Skip to content

Commit

Permalink
Merge pull request backstage#4638 from backstage/rugvip/corefactor
Browse files Browse the repository at this point in the history
core-api: switch component data to use a global store
  • Loading branch information
Rugvip authored Feb 23, 2021
2 parents c4cb181 + 2a271d8 commit 51e7fef
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .changeset/rich-socks-move.md
Original file line number Diff line number Diff line change
@@ -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.
59 changes: 59 additions & 0 deletions packages/core-api/src/extensions/componentData.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <Component />;
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 = <Component />;
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 = <Component />;
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 = <Component />;
expect(getComponentData(element, 'my-data')).toBe(data);
});
});
});
42 changes: 34 additions & 8 deletions packages/core-api/src/extensions/componentData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,39 @@
*/

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 ComponentWithData<P> = ComponentType<P> & {
[DATA_KEY]?: DataContainer;
};

type DataContainer = {
map: Map<string, unknown>;
};

type ComponentWithData<P> = ComponentType<P> & {
[DATA_KEY]?: DataContainer;
type MaybeComponentNode = ReactNode & {
type?: ComponentType<any> & { [DATA_KEY]?: DataContainer };
};

type ReactNodeWithData = ReactNode & {
type?: { [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<ComponentType<any>, DataContainer> }
| undefined;
if (!storeObj) {
const store = new WeakMap<ComponentType<any>, DataContainer>();
storeObj = { store };
globalObject[GLOBAL_KEY] = storeObj;
}
return storeObj.store;
}

const store = getStore();

export function attachComponentData<P>(
component: ComponentType<P>,
Expand All @@ -37,9 +56,11 @@ export function attachComponentData<P>(
) {
const dataComponent = component as ComponentWithData<P>;

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)) {
Expand All @@ -60,7 +81,12 @@ export function getComponentData<T>(
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;
}
Expand Down
29 changes: 29 additions & 0 deletions packages/core-api/src/lib/globalObject.ts
Original file line number Diff line number Diff line change
@@ -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();

0 comments on commit 51e7fef

Please sign in to comment.