From ee560a45f53bada4ceea427952bc6948e0a3c3be Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 16:43:46 +0200 Subject: [PATCH 01/16] 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'; From e65b6622e6ae9bde37e2c2e3071169c0b4b65b79 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 16:52:59 +0200 Subject: [PATCH 02/16] version-bridge: restructure module layout Signed-off-by: Patrik Oldsberg --- ...versionedValues.ts => VersionedContext.ts} | 26 +----------- .../version-bridge/src/lib/VersionedValue.ts | 40 +++++++++++++++++++ packages/version-bridge/src/lib/index.ts | 13 +++++- 3 files changed, 52 insertions(+), 27 deletions(-) rename packages/version-bridge/src/lib/{versionedValues.ts => VersionedContext.ts} (65%) create mode 100644 packages/version-bridge/src/lib/VersionedValue.ts diff --git a/packages/version-bridge/src/lib/versionedValues.ts b/packages/version-bridge/src/lib/VersionedContext.ts similarity index 65% rename from packages/version-bridge/src/lib/versionedValues.ts rename to packages/version-bridge/src/lib/VersionedContext.ts index 7686842ecd..310e091d0b 100644 --- a/packages/version-bridge/src/lib/versionedValues.ts +++ b/packages/version-bridge/src/lib/VersionedContext.ts @@ -16,31 +16,7 @@ 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]; - }, - }; -} +import { createVersionedValueMap, VersionedValue } from './VersionedValue'; export function useVersionedContext< Versions extends { [version in number]: any }, diff --git a/packages/version-bridge/src/lib/VersionedValue.ts b/packages/version-bridge/src/lib/VersionedValue.ts new file mode 100644 index 0000000000..3b3064fc7e --- /dev/null +++ b/packages/version-bridge/src/lib/VersionedValue.ts @@ -0,0 +1,40 @@ +/* + * 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. + */ + +/** + * 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]; + }, + }; +} diff --git a/packages/version-bridge/src/lib/index.ts b/packages/version-bridge/src/lib/index.ts index 2c4c4cbe2a..8951694428 100644 --- a/packages/version-bridge/src/lib/index.ts +++ b/packages/version-bridge/src/lib/index.ts @@ -14,5 +14,14 @@ * limitations under the License. */ -export * from './globalObject'; -export * from './versionedValues'; +export { + getGlobalSingleton, + getOrCreateGlobalSingleton, + setGlobalSingleton, +} from './globalObject'; +export { + createVersionedContextForTesting, + useVersionedContext, +} from './versionedValues'; +export { createVersionedValueMap } from './VersionedValue'; +export type { VersionedValue } from './VersionedValue'; From 9dacb366a115a83a2dda851d61e3a34d0da47244 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 16:53:30 +0200 Subject: [PATCH 03/16] version-bridge: remove setGlobalSingleton Signed-off-by: Patrik Oldsberg --- packages/version-bridge/src/lib/VersionedContext.ts | 6 ++++-- packages/version-bridge/src/lib/globalObject.ts | 11 ----------- packages/version-bridge/src/lib/index.ts | 8 ++------ 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/packages/version-bridge/src/lib/VersionedContext.ts b/packages/version-bridge/src/lib/VersionedContext.ts index 310e091d0b..3cce599d11 100644 --- a/packages/version-bridge/src/lib/VersionedContext.ts +++ b/packages/version-bridge/src/lib/VersionedContext.ts @@ -15,7 +15,7 @@ */ import { createContext, useContext, Context } from 'react'; -import { getGlobalSingleton, setGlobalSingleton } from './globalObject'; +import { getGlobalSingleton } from './globalObject'; import { createVersionedValueMap, VersionedValue } from './VersionedValue'; export function useVersionedContext< @@ -33,7 +33,9 @@ export function useVersionedContext< export function createVersionedContextForTesting(key: string) { return { set(versions: { [version in number]: unknown }) { - setGlobalSingleton(key, createContext(createVersionedValueMap(versions))); + (globalThis as any)[`__@backstage/${key}__`] = createContext( + createVersionedValueMap(versions), + ); }, reset() { delete (globalThis as any)[`__@backstage/${key}__`]; diff --git a/packages/version-bridge/src/lib/globalObject.ts b/packages/version-bridge/src/lib/globalObject.ts index 7a400148fb..280885f768 100644 --- a/packages/version-bridge/src/lib/globalObject.ts +++ b/packages/version-bridge/src/lib/globalObject.ts @@ -30,17 +30,6 @@ 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. */ diff --git a/packages/version-bridge/src/lib/index.ts b/packages/version-bridge/src/lib/index.ts index 8951694428..08a9261ee9 100644 --- a/packages/version-bridge/src/lib/index.ts +++ b/packages/version-bridge/src/lib/index.ts @@ -14,14 +14,10 @@ * limitations under the License. */ -export { - getGlobalSingleton, - getOrCreateGlobalSingleton, - setGlobalSingleton, -} from './globalObject'; +export { getGlobalSingleton, getOrCreateGlobalSingleton } from './globalObject'; export { createVersionedContextForTesting, useVersionedContext, -} from './versionedValues'; +} from './VersionedContext'; export { createVersionedValueMap } from './VersionedValue'; export type { VersionedValue } from './VersionedValue'; From c52410ab4200dbad1b24e9cde2dce717810d04d0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 17:00:17 +0200 Subject: [PATCH 04/16] version-bridge: document versioned context helpers Signed-off-by: Patrik Oldsberg --- .../src/lib/VersionedContext.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/version-bridge/src/lib/VersionedContext.ts b/packages/version-bridge/src/lib/VersionedContext.ts index 3cce599d11..62aed0d9ed 100644 --- a/packages/version-bridge/src/lib/VersionedContext.ts +++ b/packages/version-bridge/src/lib/VersionedContext.ts @@ -18,6 +18,20 @@ import { createContext, useContext, Context } from 'react'; import { getGlobalSingleton } from './globalObject'; import { createVersionedValueMap, VersionedValue } from './VersionedValue'; +/** + * A hook that simplifies the consumption of a versioned contexts that's + * stored inside a global singleton. + * + * @example + * + * ```ts + * const versionedHolder = useVersionedContext<{ 1: string }>('my-context'); + * + * const myValue = versionedHolder.atVersion(1); + * + * // ... + * ```` + */ export function useVersionedContext< Versions extends { [version in number]: any }, >(key: string): VersionedValue { @@ -30,6 +44,26 @@ export function useVersionedContext< return versionedValue; } +/** + * Creates a helper for writing tests towards multiple different + * combinations of versions provided from a context. + * + * @example + * + * ```ts + * const context = createVersionedContextForTesting('my-context'); + * + * afterEach(() => { + * context.reset(); + * }); + * + * it('should work when provided with version 1', () => { + * context.set({1: 'value-for-version-1'}) + * + * // ... + * }) + * ``` + */ export function createVersionedContextForTesting(key: string) { return { set(versions: { [version in number]: unknown }) { From 041cf56036086ebf7593b7e950063968015eeb2e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 17:09:09 +0200 Subject: [PATCH 05/16] version-bridge: added createVersionedContext Signed-off-by: Patrik Oldsberg --- .../src/lib/VersionedContext.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/version-bridge/src/lib/VersionedContext.ts b/packages/version-bridge/src/lib/VersionedContext.ts index 62aed0d9ed..bd80365c2f 100644 --- a/packages/version-bridge/src/lib/VersionedContext.ts +++ b/packages/version-bridge/src/lib/VersionedContext.ts @@ -15,13 +15,39 @@ */ import { createContext, useContext, Context } from 'react'; -import { getGlobalSingleton } from './globalObject'; +import { getGlobalSingleton, getOrCreateGlobalSingleton } from './globalObject'; import { createVersionedValueMap, VersionedValue } from './VersionedValue'; +/** + * Get the existing or create a new versioned React context that's + * stored inside a global singleton. + * + * @param key - A key that uniquely identifies the context. + * @example + * + * ```ts + * const MyContext = createVersionedContext<{ 1: string }>('my-context'); + * + * const MyContextProvider = ({children}) => ( + * + * {children} + * + * ) + * ``` + */ +export function createVersionedContext< + Versions extends { [version in number]: any }, +>(key: string): Context | undefined> { + return getOrCreateGlobalSingleton(key, () => + createContext | undefined>(undefined), + ); +} + /** * A hook that simplifies the consumption of a versioned contexts that's * stored inside a global singleton. * + * @param key - A key that uniquely identifies the context. * @example * * ```ts @@ -48,6 +74,7 @@ export function useVersionedContext< * Creates a helper for writing tests towards multiple different * combinations of versions provided from a context. * + * @param key - A key that uniquely identifies the context. * @example * * ```ts From ddedf7aab5142a652116f4db82d6e2e828c9dab9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 17:44:06 +0200 Subject: [PATCH 06/16] version-bridge: added tests Signed-off-by: Patrik Oldsberg --- .../src/lib/VersionedContext.test.tsx | 100 ++++++++++++++++++ .../src/lib/VersionedValue.test.ts | 40 +++++++ 2 files changed, 140 insertions(+) create mode 100644 packages/version-bridge/src/lib/VersionedContext.test.tsx create mode 100644 packages/version-bridge/src/lib/VersionedValue.test.ts diff --git a/packages/version-bridge/src/lib/VersionedContext.test.tsx b/packages/version-bridge/src/lib/VersionedContext.test.tsx new file mode 100644 index 0000000000..bbd924780d --- /dev/null +++ b/packages/version-bridge/src/lib/VersionedContext.test.tsx @@ -0,0 +1,100 @@ +/* + * 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 React, { useContext } from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { + createVersionedContext, + createVersionedContextForTesting, + useVersionedContext, +} from './VersionedContext'; +import { createVersionedValueMap } from './VersionedValue'; + +type ContextType = { 1: string; 2: string }; + +describe('VersionedContext', () => { + it('should provide a versioned value', () => { + const Context = createVersionedContext('test-context-1'); + + const rendered = renderHook(() => useContext(Context), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(rendered.result.current?.atVersion(1)).toBe('1v1'); + expect(rendered.result.current?.atVersion(2)).toBe('1v2'); + }); + + it('should provide a versioned value to hook', () => { + const Context = createVersionedContext('test-context-2'); + + const rendered = renderHook(() => useVersionedContext('test-context-2'), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(rendered.result.current?.atVersion(1)).toBe('2v1'); + expect(rendered.result.current?.atVersion(2)).toBe('2v2'); + }); + + it('should be provide a test utility', () => { + const context = createVersionedContextForTesting('test-context-3'); + + const rendered = renderHook(() => useVersionedContext('test-context-3')); + + expect(() => rendered.result.current).toThrow( + 'Global test-context-3 is not set', + ); + context.set({ 1: '3v1' }); + expect(() => rendered.result.current).toThrow( + 'Global test-context-3 is not set', + ); + // should need a rerender before update + rendered.rerender(); + + expect(rendered.result.current?.atVersion(1)).toBe('3v1'); + expect(rendered.result.current?.atVersion(2)).toBeUndefined(); + + context.set({ 2: '3v2' }); + rendered.rerender(); + + expect(rendered.result.current?.atVersion(1)).toBeUndefined(); + expect(rendered.result.current?.atVersion(2)).toBe('3v2'); + + context.reset(); + rendered.rerender(); + expect(() => rendered.result.current).toThrow( + 'Global test-context-3 is not set', + ); + + context.set({ 1: '3v1', 2: '3v2' }); + + rendered.rerender(); + + expect(rendered.result.current?.atVersion(1)).toBe('3v1'); + expect(rendered.result.current?.atVersion(2)).toBe('3v2'); + }); +}); diff --git a/packages/version-bridge/src/lib/VersionedValue.test.ts b/packages/version-bridge/src/lib/VersionedValue.test.ts new file mode 100644 index 0000000000..2b6637a96f --- /dev/null +++ b/packages/version-bridge/src/lib/VersionedValue.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { createVersionedValueMap } from './VersionedValue'; + +describe('createVersionedValueMap', () => { + it('should be empty', () => { + const map = createVersionedValueMap({}); + + // @ts-expect-error + expect(map.atVersion(1 as any)).toBe(undefined); + }); + + it('should access values by version', () => { + const map = createVersionedValueMap({ 1: 'v1', 2: 'v2' }); + + expect(map.atVersion(1)).toBe('v1'); + expect(map.atVersion(2)).toBe('v2'); + + // @ts-expect-error + expect(map.atVersion(0)).toBe(undefined); + // @ts-expect-error + expect(map.atVersion(NaN)).toBe(undefined); + // @ts-expect-error + expect(map.atVersion(Infinity)).toBe(undefined); + }); +}); From d6eb28c91a71f420f37f67915f4e30bfe41e54bc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 17:51:29 +0200 Subject: [PATCH 07/16] version-bridge: updated API report + fixups Signed-off-by: Patrik Oldsberg --- packages/version-bridge/api-report.md | 19 +------------------ .../src/lib/VersionedContext.ts | 5 ++++- .../version-bridge/src/lib/VersionedValue.ts | 4 ++++ .../version-bridge/src/lib/globalObject.ts | 4 ++++ 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/version-bridge/api-report.md b/packages/version-bridge/api-report.md index 21b6cd508b..94ed54ddaa 100644 --- a/packages/version-bridge/api-report.md +++ b/packages/version-bridge/api-report.md @@ -3,16 +3,12 @@ > 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) +// @public 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 { @@ -20,32 +16,19 @@ export function createVersionedValueMap< }, >(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 { diff --git a/packages/version-bridge/src/lib/VersionedContext.ts b/packages/version-bridge/src/lib/VersionedContext.ts index bd80365c2f..4a08739c16 100644 --- a/packages/version-bridge/src/lib/VersionedContext.ts +++ b/packages/version-bridge/src/lib/VersionedContext.ts @@ -23,6 +23,7 @@ import { createVersionedValueMap, VersionedValue } from './VersionedValue'; * stored inside a global singleton. * * @param key - A key that uniquely identifies the context. + * @public * @example * * ```ts @@ -48,6 +49,7 @@ export function createVersionedContext< * stored inside a global singleton. * * @param key - A key that uniquely identifies the context. + * @public * @example * * ```ts @@ -56,7 +58,7 @@ export function createVersionedContext< * const myValue = versionedHolder.atVersion(1); * * // ... - * ```` + * ``` */ export function useVersionedContext< Versions extends { [version in number]: any }, @@ -75,6 +77,7 @@ export function useVersionedContext< * combinations of versions provided from a context. * * @param key - A key that uniquely identifies the context. + * @public * @example * * ```ts diff --git a/packages/version-bridge/src/lib/VersionedValue.ts b/packages/version-bridge/src/lib/VersionedValue.ts index 3b3064fc7e..28b4de5903 100644 --- a/packages/version-bridge/src/lib/VersionedValue.ts +++ b/packages/version-bridge/src/lib/VersionedValue.ts @@ -18,6 +18,8 @@ * 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. + * + * @public */ export type VersionedValue = { atVersion( @@ -27,6 +29,8 @@ export type VersionedValue = { /** * Creates a container for a map of versioned values that implements VersionedValue. + * + * @public */ export function createVersionedValueMap< Versions extends { [version: number]: any }, diff --git a/packages/version-bridge/src/lib/globalObject.ts b/packages/version-bridge/src/lib/globalObject.ts index 280885f768..765bc62822 100644 --- a/packages/version-bridge/src/lib/globalObject.ts +++ b/packages/version-bridge/src/lib/globalObject.ts @@ -32,6 +32,8 @@ const makeKey = (id: string) => `__@backstage/${id}__`; /** * Used to access a global singleton value, failing if it is not already set. + * + * @public */ export function getGlobalSingleton(id: string): T { const key = makeKey(id); @@ -44,6 +46,8 @@ export function getGlobalSingleton(id: string): T { /** * Serializes access to a global singleton value, with the first caller creating the value. + * + * @public */ export function getOrCreateGlobalSingleton( id: string, From 8b0e02e0bb6efb9da7eb306a96b7cad49a92c687 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 17:51:43 +0200 Subject: [PATCH 08/16] core-plugin-api: switch to using version-bridge Signed-off-by: Patrik Oldsberg --- packages/core-plugin-api/package.json | 1 + .../src/apis/system/useApi.test.tsx | 2 +- .../src/apis/system/useApi.tsx | 2 +- .../core-plugin-api/src/app/useApp.test.tsx | 2 +- packages/core-plugin-api/src/app/useApp.tsx | 2 +- .../src/extensions/componentData.tsx | 2 +- .../src/lib/globalObject.test.ts | 67 ----------------- .../core-plugin-api/src/lib/globalObject.ts | 73 ------------------- .../src/lib/versionedValues.ts | 66 ----------------- packages/core-plugin-api/src/routing/types.ts | 2 +- .../src/routing/useRouteRef.test.tsx | 2 +- .../src/routing/useRouteRef.tsx | 2 +- 12 files changed, 9 insertions(+), 214 deletions(-) delete mode 100644 packages/core-plugin-api/src/lib/globalObject.test.ts delete mode 100644 packages/core-plugin-api/src/lib/globalObject.ts delete mode 100644 packages/core-plugin-api/src/lib/versionedValues.ts diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 414ca2afe1..d9380a3245 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -31,6 +31,7 @@ "dependencies": { "@backstage/config": "^0.1.9", "@backstage/theme": "^0.2.9", + "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", "@types/react": "*", "history": "^5.0.0", diff --git a/packages/core-plugin-api/src/apis/system/useApi.test.tsx b/packages/core-plugin-api/src/apis/system/useApi.test.tsx index b23e44b815..6f473d8f59 100644 --- a/packages/core-plugin-api/src/apis/system/useApi.test.tsx +++ b/packages/core-plugin-api/src/apis/system/useApi.test.tsx @@ -15,7 +15,7 @@ */ import { renderHook } from '@testing-library/react-hooks'; -import { createVersionedContextForTesting } from '../../lib/versionedValues'; +import { createVersionedContextForTesting } from '@backstage/version-bridge'; import { createApiRef } from './ApiRef'; import { useApi } from './useApi'; diff --git a/packages/core-plugin-api/src/apis/system/useApi.tsx b/packages/core-plugin-api/src/apis/system/useApi.tsx index eae6b98654..10fe1df4a7 100644 --- a/packages/core-plugin-api/src/apis/system/useApi.tsx +++ b/packages/core-plugin-api/src/apis/system/useApi.tsx @@ -16,7 +16,7 @@ import React, { PropsWithChildren } from 'react'; import { ApiRef, ApiHolder, TypesToApiRefs } from './types'; -import { useVersionedContext } from '../../lib/versionedValues'; +import { useVersionedContext } from '@backstage/version-bridge'; export function useApiHolder(): ApiHolder { const versionedHolder = useVersionedContext<{ 1: ApiHolder }>('api-context'); diff --git a/packages/core-plugin-api/src/app/useApp.test.tsx b/packages/core-plugin-api/src/app/useApp.test.tsx index 12b315ae87..f2e330e847 100644 --- a/packages/core-plugin-api/src/app/useApp.test.tsx +++ b/packages/core-plugin-api/src/app/useApp.test.tsx @@ -15,7 +15,7 @@ */ import { renderHook } from '@testing-library/react-hooks'; -import { createVersionedContextForTesting } from '../lib/versionedValues'; +import { createVersionedContextForTesting } from '@backstage/version-bridge'; import { useApp } from './useApp'; describe('v1 consumer', () => { diff --git a/packages/core-plugin-api/src/app/useApp.tsx b/packages/core-plugin-api/src/app/useApp.tsx index aa323db4e2..d4615eb532 100644 --- a/packages/core-plugin-api/src/app/useApp.tsx +++ b/packages/core-plugin-api/src/app/useApp.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useVersionedContext } from '../lib/versionedValues'; +import { useVersionedContext } from '@backstage/version-bridge'; import { AppContext as AppContextV1 } from './types'; export const useApp = (): AppContextV1 => { diff --git a/packages/core-plugin-api/src/extensions/componentData.tsx b/packages/core-plugin-api/src/extensions/componentData.tsx index d28d17d8ca..97a0b74e5b 100644 --- a/packages/core-plugin-api/src/extensions/componentData.tsx +++ b/packages/core-plugin-api/src/extensions/componentData.tsx @@ -15,7 +15,7 @@ */ import { ComponentType, ReactNode } from 'react'; -import { getOrCreateGlobalSingleton } from '../lib/globalObject'; +import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; type DataContainer = { map: Map; diff --git a/packages/core-plugin-api/src/lib/globalObject.test.ts b/packages/core-plugin-api/src/lib/globalObject.test.ts deleted file mode 100644 index 13634f1f08..0000000000 --- a/packages/core-plugin-api/src/lib/globalObject.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/core-plugin-api/src/lib/globalObject.ts b/packages/core-plugin-api/src/lib/globalObject.ts deleted file mode 100644 index 7a400148fb..0000000000 --- a/packages/core-plugin-api/src/lib/globalObject.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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/core-plugin-api/src/lib/versionedValues.ts b/packages/core-plugin-api/src/lib/versionedValues.ts deleted file mode 100644 index 7686842ecd..0000000000 --- a/packages/core-plugin-api/src/lib/versionedValues.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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/core-plugin-api/src/routing/types.ts b/packages/core-plugin-api/src/routing/types.ts index 5afcc72265..86d469c2cc 100644 --- a/packages/core-plugin-api/src/routing/types.ts +++ b/packages/core-plugin-api/src/routing/types.ts @@ -15,7 +15,7 @@ */ import { OldIconComponent } from '../icons/types'; -import { getOrCreateGlobalSingleton } from '../lib/globalObject'; +import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; export type AnyParams = { [param in string]: string } | undefined; export type ParamKeys = keyof Params extends never diff --git a/packages/core-plugin-api/src/routing/useRouteRef.test.tsx b/packages/core-plugin-api/src/routing/useRouteRef.test.tsx index ace2d1a58a..62f6d6da42 100644 --- a/packages/core-plugin-api/src/routing/useRouteRef.test.tsx +++ b/packages/core-plugin-api/src/routing/useRouteRef.test.tsx @@ -17,7 +17,7 @@ import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { createVersionedContextForTesting } from '../lib/versionedValues'; +import { createVersionedContextForTesting } from '@backstage/version-bridge'; import { useRouteRef } from './useRouteRef'; import { createRouteRef } from './RouteRef'; diff --git a/packages/core-plugin-api/src/routing/useRouteRef.tsx b/packages/core-plugin-api/src/routing/useRouteRef.tsx index ed261489a4..56c5baff8e 100644 --- a/packages/core-plugin-api/src/routing/useRouteRef.tsx +++ b/packages/core-plugin-api/src/routing/useRouteRef.tsx @@ -16,7 +16,7 @@ import { useMemo } from 'react'; import { matchRoutes, useLocation } from 'react-router-dom'; -import { useVersionedContext } from '../lib/versionedValues'; +import { useVersionedContext } from '@backstage/version-bridge'; import { AnyParams, ExternalRouteRef, From 73a752ff4f1767fc00f4119c749475dd7219130f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 17:53:13 +0200 Subject: [PATCH 09/16] core-app-api: switch to using version-bridge Signed-off-by: Patrik Oldsberg --- packages/core-app-api/package.json | 1 + .../src/apis/system/ApiProvider.test.tsx | 3 +- .../src/apis/system/ApiProvider.tsx | 4 +- .../core-app-api/src/app/AppContext.test.tsx | 3 +- packages/core-app-api/src/app/AppContext.tsx | 4 +- .../core-app-api/src/lib/globalObject.test.ts | 90 ------------------- packages/core-app-api/src/lib/globalObject.ts | 73 --------------- .../src/lib/versionedValues.test.ts | 40 --------- .../core-app-api/src/lib/versionedValues.ts | 40 --------- .../src/routing/RoutingProvider.test.tsx | 3 +- .../src/routing/RoutingProvider.tsx | 6 +- packages/core-app-api/src/routing/types.ts | 2 +- 12 files changed, 12 insertions(+), 257 deletions(-) delete mode 100644 packages/core-app-api/src/lib/globalObject.test.ts delete mode 100644 packages/core-app-api/src/lib/globalObject.ts delete mode 100644 packages/core-app-api/src/lib/versionedValues.test.ts delete mode 100644 packages/core-app-api/src/lib/versionedValues.ts diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 5223d2b27e..790b1e29fb 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -33,6 +33,7 @@ "@backstage/config": "^0.1.9", "@backstage/core-plugin-api": "^0.1.7", "@backstage/theme": "^0.2.10", + "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@types/react": "*", diff --git a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx index b94aea6b67..c80b0c117f 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx @@ -26,8 +26,7 @@ import { ApiProvider } from './ApiProvider'; import { ApiRegistry } from './ApiRegistry'; import { render } from '@testing-library/react'; import { withLogCollector } from '@backstage/test-utils-core'; -import { getGlobalSingleton } from '../../lib/globalObject'; -import { VersionedValue } from '../../lib/versionedValues'; +import { getGlobalSingleton, VersionedValue } from '@backstage/version-bridge'; describe('ApiProvider', () => { type Api = () => string; diff --git a/packages/core-app-api/src/apis/system/ApiProvider.tsx b/packages/core-app-api/src/apis/system/ApiProvider.tsx index ce6c388087..bffb9986c7 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.tsx @@ -23,11 +23,11 @@ import React, { import PropTypes from 'prop-types'; import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiAggregator } from './ApiAggregator'; -import { getOrCreateGlobalSingleton } from '../../lib/globalObject'; import { VersionedValue, createVersionedValueMap, -} from '../../lib/versionedValues'; + getOrCreateGlobalSingleton, +} from '@backstage/version-bridge'; type ApiProviderProps = { apis: ApiHolder; diff --git a/packages/core-app-api/src/app/AppContext.test.tsx b/packages/core-app-api/src/app/AppContext.test.tsx index 6ce164b9f0..3397eb4620 100644 --- a/packages/core-app-api/src/app/AppContext.test.tsx +++ b/packages/core-app-api/src/app/AppContext.test.tsx @@ -16,8 +16,7 @@ import React, { useContext, Context } from 'react'; import { renderHook } from '@testing-library/react-hooks'; -import { VersionedValue } from '../lib/versionedValues'; -import { getGlobalSingleton } from '../lib/globalObject'; +import { getGlobalSingleton, VersionedValue } from '@backstage/version-bridge'; import { AppContext as AppContextV1 } from './types'; import { AppContextProvider } from './AppContext'; diff --git a/packages/core-app-api/src/app/AppContext.tsx b/packages/core-app-api/src/app/AppContext.tsx index c583478ec2..640d814da0 100644 --- a/packages/core-app-api/src/app/AppContext.tsx +++ b/packages/core-app-api/src/app/AppContext.tsx @@ -18,8 +18,8 @@ import React, { createContext, PropsWithChildren } from 'react'; import { VersionedValue, createVersionedValueMap, -} from '../lib/versionedValues'; -import { getOrCreateGlobalSingleton } from '../lib/globalObject'; + getOrCreateGlobalSingleton, +} from '@backstage/version-bridge'; import { AppContext as AppContextV1 } from './types'; type AppContextType = VersionedValue<{ 1: AppContextV1 }> | undefined; diff --git a/packages/core-app-api/src/lib/globalObject.test.ts b/packages/core-app-api/src/lib/globalObject.test.ts deleted file mode 100644 index a658b253a6..0000000000 --- a/packages/core-app-api/src/lib/globalObject.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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, - setGlobalSingleton, -} 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); - }); -}); - -describe('setGlobalSingleton', () => { - beforeEach(() => { - delete anyGlobal['__@backstage/my-thing__']; - }); - - it('should set a global value', () => { - setGlobalSingleton('my-thing', 'global value'); - - expect(anyGlobal['__@backstage/my-thing__']).toBe('global value'); - }); - - it('should throw if global value is set', () => { - anyGlobal['__@backstage/my-thing__'] = 'already defined'; - expect(() => setGlobalSingleton('my-thing', () => 'global value')).toThrow( - 'Global my-thing is already se', - ); - }); -}); diff --git a/packages/core-app-api/src/lib/globalObject.ts b/packages/core-app-api/src/lib/globalObject.ts deleted file mode 100644 index ad70a61110..0000000000 --- a/packages/core-app-api/src/lib/globalObject.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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`); // TODO some sort of special build err - } - 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`); // TODO some sort of special build err - } - - 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/core-app-api/src/lib/versionedValues.test.ts b/packages/core-app-api/src/lib/versionedValues.test.ts deleted file mode 100644 index 19f9c8f349..0000000000 --- a/packages/core-app-api/src/lib/versionedValues.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { createVersionedValueMap } from './versionedValues'; - -describe('createVersionedValueMap', () => { - it('should be empty', () => { - const map = createVersionedValueMap({}); - - // @ts-expect-error - expect(map.atVersion(1 as any)).toBe(undefined); - }); - - it('should access values by version', () => { - const map = createVersionedValueMap({ 1: 'v1', 2: 'v2' }); - - expect(map.atVersion(1)).toBe('v1'); - expect(map.atVersion(2)).toBe('v2'); - - // @ts-expect-error - expect(map.atVersion(0)).toBe(undefined); - // @ts-expect-error - expect(map.atVersion(NaN)).toBe(undefined); - // @ts-expect-error - expect(map.atVersion(Infinity)).toBe(undefined); - }); -}); diff --git a/packages/core-app-api/src/lib/versionedValues.ts b/packages/core-app-api/src/lib/versionedValues.ts deleted file mode 100644 index 3b3064fc7e..0000000000 --- a/packages/core-app-api/src/lib/versionedValues.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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. - */ - -/** - * 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]; - }, - }; -} diff --git a/packages/core-app-api/src/routing/RoutingProvider.test.tsx b/packages/core-app-api/src/routing/RoutingProvider.test.tsx index 47f9c87c96..bf6d06aaf7 100644 --- a/packages/core-app-api/src/routing/RoutingProvider.test.tsx +++ b/packages/core-app-api/src/routing/RoutingProvider.test.tsx @@ -23,8 +23,7 @@ import React, { import { MemoryRouter, Routes } from 'react-router-dom'; import { render } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; -import { VersionedValue } from '../lib/versionedValues'; -import { getGlobalSingleton } from '../lib/globalObject'; +import { VersionedValue, getGlobalSingleton } from '@backstage/version-bridge'; import { childDiscoverer, routeElementDiscoverer, diff --git a/packages/core-app-api/src/routing/RoutingProvider.tsx b/packages/core-app-api/src/routing/RoutingProvider.tsx index 2192f5df26..b2aaf3df3b 100644 --- a/packages/core-app-api/src/routing/RoutingProvider.tsx +++ b/packages/core-app-api/src/routing/RoutingProvider.tsx @@ -20,11 +20,11 @@ import { RouteRef, SubRouteRef, } from '@backstage/core-plugin-api'; -import { getOrCreateGlobalSingleton } from '../lib/globalObject'; import { - createVersionedValueMap, VersionedValue, -} from '../lib/versionedValues'; + createVersionedValueMap, + getOrCreateGlobalSingleton, +} from '@backstage/version-bridge'; import { RouteResolver } from './RouteResolver'; import { BackstageRouteObject } from './types'; diff --git a/packages/core-app-api/src/routing/types.ts b/packages/core-app-api/src/routing/types.ts index a12e914aba..12bf0d1a0f 100644 --- a/packages/core-app-api/src/routing/types.ts +++ b/packages/core-app-api/src/routing/types.ts @@ -19,7 +19,7 @@ import { SubRouteRef, ExternalRouteRef, } from '@backstage/core-plugin-api'; -import { getOrCreateGlobalSingleton } from '../lib/globalObject'; +import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; type RouteRefType = Exclude< keyof RouteRef, From ac95cf2e03925443419f00bf213fe594376c4b6a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 17:56:34 +0200 Subject: [PATCH 10/16] version-bridge: export createVersionedContext Signed-off-by: Patrik Oldsberg --- packages/version-bridge/api-report.md | 9 +++++++++ packages/version-bridge/src/lib/index.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/packages/version-bridge/api-report.md b/packages/version-bridge/api-report.md index 94ed54ddaa..9f653a11d9 100644 --- a/packages/version-bridge/api-report.md +++ b/packages/version-bridge/api-report.md @@ -3,6 +3,15 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { Context } from 'react'; + +// @public +export function createVersionedContext< + Versions extends { + [version in number]: any; + }, +>(key: string): Context | undefined>; + // @public export function createVersionedContextForTesting(key: string): { set(versions: { [x: number]: unknown }): void; diff --git a/packages/version-bridge/src/lib/index.ts b/packages/version-bridge/src/lib/index.ts index 08a9261ee9..7e3e1cc0ae 100644 --- a/packages/version-bridge/src/lib/index.ts +++ b/packages/version-bridge/src/lib/index.ts @@ -18,6 +18,7 @@ export { getGlobalSingleton, getOrCreateGlobalSingleton } from './globalObject'; export { createVersionedContextForTesting, useVersionedContext, + createVersionedContext, } from './VersionedContext'; export { createVersionedValueMap } from './VersionedValue'; export type { VersionedValue } from './VersionedValue'; From d1a598d6b56009d6cb0016ab71a2f99b426db417 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 17:56:55 +0200 Subject: [PATCH 11/16] core-app-api: use createVersionedContext Signed-off-by: Patrik Oldsberg --- .../core-app-api/src/apis/system/ApiProvider.tsx | 15 +++------------ packages/core-app-api/src/app/AppContext.tsx | 10 +++------- .../core-app-api/src/routing/RoutingProvider.tsx | 11 ++++------- 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/packages/core-app-api/src/apis/system/ApiProvider.tsx b/packages/core-app-api/src/apis/system/ApiProvider.tsx index bffb9986c7..aa57fa9310 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.tsx @@ -14,19 +14,13 @@ * limitations under the License. */ -import React, { - createContext, - useContext, - ReactNode, - PropsWithChildren, -} from 'react'; +import React, { useContext, ReactNode, PropsWithChildren } from 'react'; import PropTypes from 'prop-types'; import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiAggregator } from './ApiAggregator'; import { - VersionedValue, createVersionedValueMap, - getOrCreateGlobalSingleton, + createVersionedContext, } from '@backstage/version-bridge'; type ApiProviderProps = { @@ -34,10 +28,7 @@ type ApiProviderProps = { children: ReactNode; }; -type ApiContextType = VersionedValue<{ 1: ApiHolder }> | undefined; -const ApiContext = getOrCreateGlobalSingleton('api-context', () => - createContext(undefined), -); +const ApiContext = createVersionedContext<{ 1: ApiHolder }>('api-context'); export const ApiProvider = ({ apis, diff --git a/packages/core-app-api/src/app/AppContext.tsx b/packages/core-app-api/src/app/AppContext.tsx index 640d814da0..f39d9a095e 100644 --- a/packages/core-app-api/src/app/AppContext.tsx +++ b/packages/core-app-api/src/app/AppContext.tsx @@ -14,18 +14,14 @@ * limitations under the License. */ -import React, { createContext, PropsWithChildren } from 'react'; +import React, { PropsWithChildren } from 'react'; import { - VersionedValue, createVersionedValueMap, - getOrCreateGlobalSingleton, + createVersionedContext, } from '@backstage/version-bridge'; import { AppContext as AppContextV1 } from './types'; -type AppContextType = VersionedValue<{ 1: AppContextV1 }> | undefined; -const AppContext = getOrCreateGlobalSingleton('app-context', () => - createContext(undefined), -); +const AppContext = createVersionedContext<{ 1: AppContextV1 }>('app-context'); type Props = { appContext: AppContextV1; diff --git a/packages/core-app-api/src/routing/RoutingProvider.tsx b/packages/core-app-api/src/routing/RoutingProvider.tsx index b2aaf3df3b..2ab1686368 100644 --- a/packages/core-app-api/src/routing/RoutingProvider.tsx +++ b/packages/core-app-api/src/routing/RoutingProvider.tsx @@ -14,24 +14,21 @@ * limitations under the License. */ -import React, { createContext, ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { ExternalRouteRef, RouteRef, SubRouteRef, } from '@backstage/core-plugin-api'; import { - VersionedValue, createVersionedValueMap, - getOrCreateGlobalSingleton, + createVersionedContext, } from '@backstage/version-bridge'; import { RouteResolver } from './RouteResolver'; import { BackstageRouteObject } from './types'; -type RoutingContextType = VersionedValue<{ 1: RouteResolver }> | undefined; -const RoutingContext = getOrCreateGlobalSingleton('routing-context', () => - createContext(undefined), -); +const RoutingContext = + createVersionedContext<{ 1: RouteResolver }>('routing-context'); type ProviderProps = { routePaths: Map; From cc3b48335c3e2ddbe2a6112e1a0de0fa47e408d6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 18:56:15 +0200 Subject: [PATCH 12/16] version-bridge: add missing @types/react dependency Signed-off-by: Patrik Oldsberg --- packages/version-bridge/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index 738ec570eb..3adbac681d 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -29,6 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@types/react": "*", "react": "^16.12.0" }, "devDependencies": { From 671015f1323a8d7ad47814cb9d69993a38dd6366 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Sep 2021 18:56:58 +0200 Subject: [PATCH 13/16] changesets: add changeset for using version-bridge in core Signed-off-by: Patrik Oldsberg --- .changeset/silly-trainers-clean.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/silly-trainers-clean.md diff --git a/.changeset/silly-trainers-clean.md b/.changeset/silly-trainers-clean.md new file mode 100644 index 0000000000..b4690a0e8f --- /dev/null +++ b/.changeset/silly-trainers-clean.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-app-api': patch +'@backstage/core-plugin-api': patch +--- + +Switch to using utilities from `@backstage/version-bridge'. From 921569b5004d5642a7d9f3f94337256eba181f01 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 11 Sep 2021 12:48:28 +0200 Subject: [PATCH 14/16] version-bridge: leave it to the caller of useVersionedContext to check availability Signed-off-by: Patrik Oldsberg --- .../src/apis/system/ApiProvider.test.tsx | 8 ++++---- .../core-plugin-api/src/apis/system/useApi.tsx | 3 +++ packages/core-plugin-api/src/app/useApp.tsx | 4 ++++ .../core-plugin-api/src/routing/useRouteRef.tsx | 4 ++++ packages/version-bridge/api-report.md | 2 +- .../src/lib/VersionedContext.test.tsx | 12 +++--------- .../version-bridge/src/lib/VersionedContext.ts | 16 +++++++--------- 7 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx index c80b0c117f..f3a0a67263 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx @@ -115,11 +115,11 @@ describe('ApiProvider', () => { withLogCollector(['error'], () => { expect(() => { render(); - }).toThrow(/^No provider available for api-context context/); + }).toThrow(/^API context is not available/); }).error, ).toEqual([ expect.stringMatching( - /^Error: Uncaught \[Error: No provider available for api-context context/, + /^Error: Uncaught \[Error: API context is not available/, ), expect.stringMatching( /^The above error occurred in the component/, @@ -130,11 +130,11 @@ describe('ApiProvider', () => { withLogCollector(['error'], () => { expect(() => { render(); - }).toThrow(/^No provider available for api-context context/); + }).toThrow(/^API context is not available/); }).error, ).toEqual([ expect.stringMatching( - /^Error: Uncaught \[Error: No provider available for api-context context/, + /^Error: Uncaught \[Error: API context is not available/, ), expect.stringMatching( /^The above error occurred in the component/, diff --git a/packages/core-plugin-api/src/apis/system/useApi.tsx b/packages/core-plugin-api/src/apis/system/useApi.tsx index 10fe1df4a7..ef598a4c0e 100644 --- a/packages/core-plugin-api/src/apis/system/useApi.tsx +++ b/packages/core-plugin-api/src/apis/system/useApi.tsx @@ -20,6 +20,9 @@ import { useVersionedContext } from '@backstage/version-bridge'; export function useApiHolder(): ApiHolder { const versionedHolder = useVersionedContext<{ 1: ApiHolder }>('api-context'); + if (!versionedHolder) { + throw new Error('API context is not available'); + } const apiHolder = versionedHolder.atVersion(1); if (!apiHolder) { diff --git a/packages/core-plugin-api/src/app/useApp.tsx b/packages/core-plugin-api/src/app/useApp.tsx index d4615eb532..05d2e1c5c0 100644 --- a/packages/core-plugin-api/src/app/useApp.tsx +++ b/packages/core-plugin-api/src/app/useApp.tsx @@ -20,6 +20,10 @@ import { AppContext as AppContextV1 } from './types'; export const useApp = (): AppContextV1 => { const versionedContext = useVersionedContext<{ 1: AppContextV1 }>('app-context'); + if (!versionedContext) { + throw new Error('App context is not available'); + } + const appContext = versionedContext.atVersion(1); if (!appContext) { throw new Error('AppContext v1 not available'); diff --git a/packages/core-plugin-api/src/routing/useRouteRef.tsx b/packages/core-plugin-api/src/routing/useRouteRef.tsx index 56c5baff8e..ea05cc14fd 100644 --- a/packages/core-plugin-api/src/routing/useRouteRef.tsx +++ b/packages/core-plugin-api/src/routing/useRouteRef.tsx @@ -50,6 +50,10 @@ export function useRouteRef( const sourceLocation = useLocation(); const versionedContext = useVersionedContext<{ 1: RouteResolver }>('routing-context'); + if (!versionedContext) { + throw new Error('Routing context is not available'); + } + const resolver = versionedContext.atVersion(1); const routeFunc = useMemo( () => resolver && resolver.resolve(routeRef, sourceLocation), diff --git a/packages/version-bridge/api-report.md b/packages/version-bridge/api-report.md index 9f653a11d9..367c2da102 100644 --- a/packages/version-bridge/api-report.md +++ b/packages/version-bridge/api-report.md @@ -36,7 +36,7 @@ export function useVersionedContext< Versions extends { [version in number]: any; }, ->(key: string): VersionedValue; +>(key: string): VersionedValue | undefined; // @public export type VersionedValue< diff --git a/packages/version-bridge/src/lib/VersionedContext.test.tsx b/packages/version-bridge/src/lib/VersionedContext.test.tsx index bbd924780d..659fd9b5b9 100644 --- a/packages/version-bridge/src/lib/VersionedContext.test.tsx +++ b/packages/version-bridge/src/lib/VersionedContext.test.tsx @@ -65,13 +65,9 @@ describe('VersionedContext', () => { const rendered = renderHook(() => useVersionedContext('test-context-3')); - expect(() => rendered.result.current).toThrow( - 'Global test-context-3 is not set', - ); + expect(rendered.result.current).toBeUndefined(); context.set({ 1: '3v1' }); - expect(() => rendered.result.current).toThrow( - 'Global test-context-3 is not set', - ); + expect(rendered.result.current).toBeUndefined(); // should need a rerender before update rendered.rerender(); @@ -86,9 +82,7 @@ describe('VersionedContext', () => { context.reset(); rendered.rerender(); - expect(() => rendered.result.current).toThrow( - 'Global test-context-3 is not set', - ); + expect(rendered.result.current).toBeUndefined(); context.set({ 1: '3v1', 2: '3v2' }); diff --git a/packages/version-bridge/src/lib/VersionedContext.ts b/packages/version-bridge/src/lib/VersionedContext.ts index 4a08739c16..ecf7ab46e6 100644 --- a/packages/version-bridge/src/lib/VersionedContext.ts +++ b/packages/version-bridge/src/lib/VersionedContext.ts @@ -15,7 +15,7 @@ */ import { createContext, useContext, Context } from 'react'; -import { getGlobalSingleton, getOrCreateGlobalSingleton } from './globalObject'; +import { getOrCreateGlobalSingleton } from './globalObject'; import { createVersionedValueMap, VersionedValue } from './VersionedValue'; /** @@ -55,6 +55,10 @@ export function createVersionedContext< * ```ts * const versionedHolder = useVersionedContext<{ 1: string }>('my-context'); * + * if (!versionedHolder) { + * throw new Error('My context is not available!') + * } + * * const myValue = versionedHolder.atVersion(1); * * // ... @@ -62,14 +66,8 @@ export function createVersionedContext< */ 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; +>(key: string): VersionedValue | undefined { + return useContext(createVersionedContext(key)); } /** From 00fee0b11da431c9dfa46d954ed910715ff082fa Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 11 Sep 2021 13:03:05 +0200 Subject: [PATCH 15/16] version-bridge: remove getGlobalSingleton Signed-off-by: Patrik Oldsberg --- .../src/apis/system/ApiProvider.test.tsx | 13 ++++------ .../core-app-api/src/app/AppContext.test.tsx | 12 +++------ .../src/routing/RoutingProvider.test.tsx | 18 ++++--------- packages/version-bridge/api-report.md | 3 --- .../src/lib/globalObject.test.ts | 25 +------------------ .../version-bridge/src/lib/globalObject.ts | 14 ----------- packages/version-bridge/src/lib/index.ts | 2 +- 7 files changed, 16 insertions(+), 71 deletions(-) diff --git a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx index f3a0a67263..d1793f890e 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { Context, useContext } from 'react'; +import React from 'react'; import { useApi, createApiRef, @@ -26,7 +26,7 @@ import { ApiProvider } from './ApiProvider'; import { ApiRegistry } from './ApiRegistry'; import { render } from '@testing-library/react'; import { withLogCollector } from '@backstage/test-utils-core'; -import { getGlobalSingleton, VersionedValue } from '@backstage/version-bridge'; +import { useVersionedContext } from '@backstage/version-bridge'; describe('ApiProvider', () => { type Api = () => string; @@ -184,13 +184,10 @@ describe('ApiProvider', () => { }); describe('v1 consumer', () => { - const ApiContext = - getGlobalSingleton>>( - 'api-context', - ); - function useMockApiV1(apiRef: ApiRef): T { - const impl = useContext(ApiContext)?.atVersion(1)?.get(apiRef); + const impl = useVersionedContext<{ 1: ApiHolder }>('api-context') + ?.atVersion(1) + ?.get(apiRef); if (!impl) { throw new Error('no impl'); } diff --git a/packages/core-app-api/src/app/AppContext.test.tsx b/packages/core-app-api/src/app/AppContext.test.tsx index 3397eb4620..a1131969f2 100644 --- a/packages/core-app-api/src/app/AppContext.test.tsx +++ b/packages/core-app-api/src/app/AppContext.test.tsx @@ -14,20 +14,16 @@ * limitations under the License. */ -import React, { useContext, Context } from 'react'; +import React from 'react'; import { renderHook } from '@testing-library/react-hooks'; -import { getGlobalSingleton, VersionedValue } from '@backstage/version-bridge'; +import { useVersionedContext } from '@backstage/version-bridge'; import { AppContext as AppContextV1 } from './types'; import { AppContextProvider } from './AppContext'; describe('v1 consumer', () => { - const AppContext = - getGlobalSingleton>>( - 'app-context', - ); - function useMockAppV1(): AppContextV1 { - const impl = useContext(AppContext)?.atVersion(1); + const impl = + useVersionedContext<{ 1: AppContextV1 }>('app-context')?.atVersion(1); if (!impl) { throw new Error('no impl'); } diff --git a/packages/core-app-api/src/routing/RoutingProvider.test.tsx b/packages/core-app-api/src/routing/RoutingProvider.test.tsx index bf6d06aaf7..397217e34a 100644 --- a/packages/core-app-api/src/routing/RoutingProvider.test.tsx +++ b/packages/core-app-api/src/routing/RoutingProvider.test.tsx @@ -14,16 +14,11 @@ * limitations under the License. */ -import React, { - PropsWithChildren, - ReactElement, - useContext, - Context, -} from 'react'; +import React, { PropsWithChildren, ReactElement } from 'react'; import { MemoryRouter, Routes } from 'react-router-dom'; import { render } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; -import { VersionedValue, getGlobalSingleton } from '@backstage/version-bridge'; +import { useVersionedContext } from '@backstage/version-bridge'; import { childDiscoverer, routeElementDiscoverer, @@ -330,16 +325,13 @@ describe('discovery', () => { }); describe('v1 consumer', () => { - const RoutingContext = - getGlobalSingleton>>( - 'routing-context', - ); - function useMockRouteRefV1( routeRef: AnyRouteRef, location: string, ): RouteFunc | undefined { - const resolver = useContext(RoutingContext)?.atVersion(1); + const resolver = useVersionedContext<{ + 1: RouteResolver; + }>('routing-context')?.atVersion(1); if (!resolver) { throw new Error('no impl'); } diff --git a/packages/version-bridge/api-report.md b/packages/version-bridge/api-report.md index 367c2da102..1d14412f10 100644 --- a/packages/version-bridge/api-report.md +++ b/packages/version-bridge/api-report.md @@ -25,9 +25,6 @@ export function createVersionedValueMap< }, >(versions: Versions): VersionedValue; -// @public -export function getGlobalSingleton(id: string): T; - // @public export function getOrCreateGlobalSingleton(id: string, supplier: () => T): T; diff --git a/packages/version-bridge/src/lib/globalObject.test.ts b/packages/version-bridge/src/lib/globalObject.test.ts index 13634f1f08..9c12ae1dfa 100644 --- a/packages/version-bridge/src/lib/globalObject.test.ts +++ b/packages/version-bridge/src/lib/globalObject.test.ts @@ -14,33 +14,10 @@ * limitations under the License. */ -import { getGlobalSingleton, getOrCreateGlobalSingleton } from './globalObject'; +import { 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__']; diff --git a/packages/version-bridge/src/lib/globalObject.ts b/packages/version-bridge/src/lib/globalObject.ts index 765bc62822..b29e81e4a0 100644 --- a/packages/version-bridge/src/lib/globalObject.ts +++ b/packages/version-bridge/src/lib/globalObject.ts @@ -30,20 +30,6 @@ const globalObject = getGlobalObject(); const makeKey = (id: string) => `__@backstage/${id}__`; -/** - * Used to access a global singleton value, failing if it is not already set. - * - * @public - */ -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. * diff --git a/packages/version-bridge/src/lib/index.ts b/packages/version-bridge/src/lib/index.ts index 7e3e1cc0ae..191abe7444 100644 --- a/packages/version-bridge/src/lib/index.ts +++ b/packages/version-bridge/src/lib/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { getGlobalSingleton, getOrCreateGlobalSingleton } from './globalObject'; +export { getOrCreateGlobalSingleton } from './globalObject'; export { createVersionedContextForTesting, useVersionedContext, From 10e3c97d9143641b57b37128a6d1b670bfe5cc1e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 11 Sep 2021 13:04:30 +0200 Subject: [PATCH 16/16] version-bridge: avoid use of any in versioned values Signed-off-by: Patrik Oldsberg --- packages/version-bridge/api-report.md | 8 ++++---- packages/version-bridge/src/lib/VersionedContext.ts | 4 ++-- packages/version-bridge/src/lib/VersionedValue.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/version-bridge/api-report.md b/packages/version-bridge/api-report.md index 1d14412f10..eca13071ed 100644 --- a/packages/version-bridge/api-report.md +++ b/packages/version-bridge/api-report.md @@ -8,7 +8,7 @@ import { Context } from 'react'; // @public export function createVersionedContext< Versions extends { - [version in number]: any; + [version in number]: unknown; }, >(key: string): Context | undefined>; @@ -21,7 +21,7 @@ export function createVersionedContextForTesting(key: string): { // @public export function createVersionedValueMap< Versions extends { - [version: number]: any; + [version: number]: unknown; }, >(versions: Versions): VersionedValue; @@ -31,14 +31,14 @@ export function getOrCreateGlobalSingleton(id: string, supplier: () => T): T; // @public export function useVersionedContext< Versions extends { - [version in number]: any; + [version in number]: unknown; }, >(key: string): VersionedValue | undefined; // @public export type VersionedValue< Versions extends { - [version: number]: any; + [version: number]: unknown; }, > = { atVersion( diff --git a/packages/version-bridge/src/lib/VersionedContext.ts b/packages/version-bridge/src/lib/VersionedContext.ts index ecf7ab46e6..ea0c40bbc4 100644 --- a/packages/version-bridge/src/lib/VersionedContext.ts +++ b/packages/version-bridge/src/lib/VersionedContext.ts @@ -37,7 +37,7 @@ import { createVersionedValueMap, VersionedValue } from './VersionedValue'; * ``` */ export function createVersionedContext< - Versions extends { [version in number]: any }, + Versions extends { [version in number]: unknown }, >(key: string): Context | undefined> { return getOrCreateGlobalSingleton(key, () => createContext | undefined>(undefined), @@ -65,7 +65,7 @@ export function createVersionedContext< * ``` */ export function useVersionedContext< - Versions extends { [version in number]: any }, + Versions extends { [version in number]: unknown }, >(key: string): VersionedValue | undefined { return useContext(createVersionedContext(key)); } diff --git a/packages/version-bridge/src/lib/VersionedValue.ts b/packages/version-bridge/src/lib/VersionedValue.ts index 28b4de5903..b7c2153e38 100644 --- a/packages/version-bridge/src/lib/VersionedValue.ts +++ b/packages/version-bridge/src/lib/VersionedValue.ts @@ -21,7 +21,7 @@ * * @public */ -export type VersionedValue = { +export type VersionedValue = { atVersion( version: Version, ): Versions[Version] | undefined; @@ -33,7 +33,7 @@ export type VersionedValue = { * @public */ export function createVersionedValueMap< - Versions extends { [version: number]: any }, + Versions extends { [version: number]: unknown }, >(versions: Versions): VersionedValue { Object.freeze(versions); return {