From ee560a45f53bada4ceea427952bc6948e0a3c3be Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 16:43:46 +0200 Subject: [PATCH] packages: add initial version of version-bridge package Signed-off-by: Patrik Oldsberg --- packages/version-bridge/.eslintrc.js | 3 + packages/version-bridge/README.md | 10 +++ packages/version-bridge/api-report.md | 61 ++++++++++++++++ packages/version-bridge/package.json | 43 +++++++++++ packages/version-bridge/src/index.ts | 17 +++++ .../src/lib/globalObject.test.ts | 67 +++++++++++++++++ .../version-bridge/src/lib/globalObject.ts | 73 +++++++++++++++++++ packages/version-bridge/src/lib/index.ts | 18 +++++ .../version-bridge/src/lib/versionedValues.ts | 66 +++++++++++++++++ packages/version-bridge/src/setupTests.ts | 17 +++++ 10 files changed, 375 insertions(+) create mode 100644 packages/version-bridge/.eslintrc.js create mode 100644 packages/version-bridge/README.md create mode 100644 packages/version-bridge/api-report.md create mode 100644 packages/version-bridge/package.json create mode 100644 packages/version-bridge/src/index.ts create mode 100644 packages/version-bridge/src/lib/globalObject.test.ts create mode 100644 packages/version-bridge/src/lib/globalObject.ts create mode 100644 packages/version-bridge/src/lib/index.ts create mode 100644 packages/version-bridge/src/lib/versionedValues.ts create mode 100644 packages/version-bridge/src/setupTests.ts diff --git a/packages/version-bridge/.eslintrc.js b/packages/version-bridge/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/packages/version-bridge/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/packages/version-bridge/README.md b/packages/version-bridge/README.md new file mode 100644 index 0000000000..e687b8c5a3 --- /dev/null +++ b/packages/version-bridge/README.md @@ -0,0 +1,10 @@ +# @backstage/version-bridge + +This package provides utilities to help enable support for multiple concurrent package versions within an app. + +It's currently only intended for use internally within @backstage packages. + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/version-bridge/api-report.md b/packages/version-bridge/api-report.md new file mode 100644 index 0000000000..21b6cd508b --- /dev/null +++ b/packages/version-bridge/api-report.md @@ -0,0 +1,61 @@ +## API Report File for "@backstage/version-bridge" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// Warning: (ae-missing-release-tag) "createVersionedContextForTesting" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function createVersionedContextForTesting(key: string): { + set(versions: { [x: number]: unknown }): void; + reset(): void; +}; + +// Warning: (ae-missing-release-tag) "createVersionedValueMap" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function createVersionedValueMap< + Versions extends { + [version: number]: any; + }, +>(versions: Versions): VersionedValue; + +// Warning: (ae-missing-release-tag) "getGlobalSingleton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function getGlobalSingleton(id: string): T; + +// Warning: (ae-missing-release-tag) "getOrCreateGlobalSingleton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function getOrCreateGlobalSingleton(id: string, supplier: () => T): T; + +// Warning: (ae-missing-release-tag) "setGlobalSingleton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function setGlobalSingleton(id: string, value: unknown): void; + +// Warning: (ae-missing-release-tag) "useVersionedContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function useVersionedContext< + Versions extends { + [version in number]: any; + }, +>(key: string): VersionedValue; + +// Warning: (ae-missing-release-tag) "VersionedValue" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type VersionedValue< + Versions extends { + [version: number]: any; + }, +> = { + atVersion( + version: Version, + ): Versions[Version] | undefined; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json new file mode 100644 index 0000000000..738ec570eb --- /dev/null +++ b/packages/version-bridge/package.json @@ -0,0 +1,43 @@ +{ + "name": "@backstage/version-bridge", + "description": "Utilities used by @backstage packages to support multiple concurrent versions", + "version": "0.1.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/version-bridge" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "build": "backstage-cli build --outputs types,esm", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "react": "^16.12.0" + }, + "devDependencies": { + "@backstage/cli": "^0.7.11", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/react-hooks": "^3.4.2" + }, + "files": [ + "dist" + ] +} diff --git a/packages/version-bridge/src/index.ts b/packages/version-bridge/src/index.ts new file mode 100644 index 0000000000..3e6b2a00f4 --- /dev/null +++ b/packages/version-bridge/src/index.ts @@ -0,0 +1,17 @@ +/* + * 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 * from './lib'; diff --git a/packages/version-bridge/src/lib/globalObject.test.ts b/packages/version-bridge/src/lib/globalObject.test.ts new file mode 100644 index 0000000000..13634f1f08 --- /dev/null +++ b/packages/version-bridge/src/lib/globalObject.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2021 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 { getGlobalSingleton, getOrCreateGlobalSingleton } from './globalObject'; + +const anyGlobal = global as any; + +describe('getGlobalSingleton', () => { + beforeEach(() => { + delete anyGlobal['__@backstage/my-thing__']; + }); + + it('should return an existing value', () => { + const myThing = {}; + const myOtherThing = {}; + + anyGlobal['__@backstage/my-thing__'] = myThing; + expect(getGlobalSingleton('my-thing')).toBe(myThing); + expect(getGlobalSingleton('my-thing')).toBe(myThing); + anyGlobal['__@backstage/my-thing__'] = myOtherThing; + expect(getGlobalSingleton('my-thing')).toBe(myOtherThing); + }); + + it('should throw if the value is not set', () => { + expect(() => getGlobalSingleton('my-thing')).toThrow( + 'Global my-thing is not set', + ); + }); +}); + +describe('getOrCreateGlobalSingleton', () => { + beforeEach(() => { + delete anyGlobal['__@backstage/my-thing__']; + }); + + it('should return an existing value', () => { + const myThing = {}; + anyGlobal['__@backstage/my-thing__'] = myThing; + + expect(getOrCreateGlobalSingleton('my-thing', () => ({}))).toBe(myThing); + expect(getOrCreateGlobalSingleton('my-thing', () => ({}))).toBe(myThing); + }); + + it('should should create a new value', () => { + const myNewThing = {}; + + expect(anyGlobal['__@backstage/my-thing__']).toBe(undefined); + expect(getOrCreateGlobalSingleton('my-thing', () => myNewThing)).toBe( + myNewThing, + ); + expect(anyGlobal['__@backstage/my-thing__']).toBe(myNewThing); + expect(getOrCreateGlobalSingleton('my-thing', () => ({}))).toBe(myNewThing); + }); +}); diff --git a/packages/version-bridge/src/lib/globalObject.ts b/packages/version-bridge/src/lib/globalObject.ts new file mode 100644 index 0000000000..7a400148fb --- /dev/null +++ b/packages/version-bridge/src/lib/globalObject.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2021 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. + */ + +// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 +function getGlobalObject() { + 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')(); +} + +const globalObject = getGlobalObject(); + +const makeKey = (id: string) => `__@backstage/${id}__`; + +/** + * Used to provide a global singleton value, failing if it is already set. + */ +export function setGlobalSingleton(id: string, value: unknown): void { + const key = makeKey(id); + if (key in globalObject) { + throw new Error(`Global ${id} is already set`); + } + globalObject[key] = value; +} + +/** + * Used to access a global singleton value, failing if it is not already set. + */ +export function getGlobalSingleton(id: string): T { + const key = makeKey(id); + if (!(key in globalObject)) { + throw new Error(`Global ${id} is not set`); + } + + return globalObject[key]; +} + +/** + * Serializes access to a global singleton value, with the first caller creating the value. + */ +export function getOrCreateGlobalSingleton( + id: string, + supplier: () => T, +): T { + const key = makeKey(id); + + let value = globalObject[key]; + if (value) { + return value; + } + + value = supplier(); + globalObject[key] = value; + return value; +} diff --git a/packages/version-bridge/src/lib/index.ts b/packages/version-bridge/src/lib/index.ts new file mode 100644 index 0000000000..2c4c4cbe2a --- /dev/null +++ b/packages/version-bridge/src/lib/index.ts @@ -0,0 +1,18 @@ +/* + * 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 * from './globalObject'; +export * from './versionedValues'; diff --git a/packages/version-bridge/src/lib/versionedValues.ts b/packages/version-bridge/src/lib/versionedValues.ts new file mode 100644 index 0000000000..7686842ecd --- /dev/null +++ b/packages/version-bridge/src/lib/versionedValues.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2021 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 { createContext, useContext, Context } from 'react'; +import { getGlobalSingleton, setGlobalSingleton } from './globalObject'; + +/** + * The versioned value interface is a container for a set of values that + * can be looked up by version. It is intended to be used as a container + * for values that can be versioned independently of package versions. + */ +export type VersionedValue = { + atVersion( + version: Version, + ): Versions[Version] | undefined; +}; + +/** + * Creates a container for a map of versioned values that implements VersionedValue. + */ +export function createVersionedValueMap< + Versions extends { [version: number]: any }, +>(versions: Versions): VersionedValue { + Object.freeze(versions); + return { + atVersion(version) { + return versions[version]; + }, + }; +} + +export function useVersionedContext< + Versions extends { [version in number]: any }, +>(key: string): VersionedValue { + const versionedValue = useContext( + getGlobalSingleton>>(key), + ); + if (!versionedValue) { + throw new Error(`No provider available for ${key} context`); + } + return versionedValue; +} + +export function createVersionedContextForTesting(key: string) { + return { + set(versions: { [version in number]: unknown }) { + setGlobalSingleton(key, createContext(createVersionedValueMap(versions))); + }, + reset() { + delete (globalThis as any)[`__@backstage/${key}__`]; + }, + }; +} diff --git a/packages/version-bridge/src/setupTests.ts b/packages/version-bridge/src/setupTests.ts new file mode 100644 index 0000000000..963c0f188b --- /dev/null +++ b/packages/version-bridge/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom';