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'.
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..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,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 { useVersionedContext } from '@backstage/version-bridge';
describe('ApiProvider', () => {
type Api = () => string;
@@ -116,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/,
@@ -131,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/,
@@ -185,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/apis/system/ApiProvider.tsx b/packages/core-app-api/src/apis/system/ApiProvider.tsx
index ce6c388087..aa57fa9310 100644
--- a/packages/core-app-api/src/apis/system/ApiProvider.tsx
+++ b/packages/core-app-api/src/apis/system/ApiProvider.tsx
@@ -14,30 +14,21 @@
* 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 { getOrCreateGlobalSingleton } from '../../lib/globalObject';
import {
- VersionedValue,
createVersionedValueMap,
-} from '../../lib/versionedValues';
+ createVersionedContext,
+} from '@backstage/version-bridge';
type ApiProviderProps = {
apis: ApiHolder;
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.test.tsx b/packages/core-app-api/src/app/AppContext.test.tsx
index 6ce164b9f0..a1131969f2 100644
--- a/packages/core-app-api/src/app/AppContext.test.tsx
+++ b/packages/core-app-api/src/app/AppContext.test.tsx
@@ -14,21 +14,16 @@
* limitations under the License.
*/
-import React, { useContext, Context } from 'react';
+import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
-import { VersionedValue } from '../lib/versionedValues';
-import { getGlobalSingleton } from '../lib/globalObject';
+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/app/AppContext.tsx b/packages/core-app-api/src/app/AppContext.tsx
index c583478ec2..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,
-} from '../lib/versionedValues';
-import { getOrCreateGlobalSingleton } from '../lib/globalObject';
+ 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/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/routing/RoutingProvider.test.tsx b/packages/core-app-api/src/routing/RoutingProvider.test.tsx
index 47f9c87c96..397217e34a 100644
--- a/packages/core-app-api/src/routing/RoutingProvider.test.tsx
+++ b/packages/core-app-api/src/routing/RoutingProvider.test.tsx
@@ -14,17 +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 } from '../lib/versionedValues';
-import { getGlobalSingleton } from '../lib/globalObject';
+import { useVersionedContext } from '@backstage/version-bridge';
import {
childDiscoverer,
routeElementDiscoverer,
@@ -331,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/core-app-api/src/routing/RoutingProvider.tsx b/packages/core-app-api/src/routing/RoutingProvider.tsx
index 2192f5df26..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 { getOrCreateGlobalSingleton } from '../lib/globalObject';
import {
createVersionedValueMap,
- VersionedValue,
-} from '../lib/versionedValues';
+ 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;
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,
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..ef598a4c0e 100644
--- a/packages/core-plugin-api/src/apis/system/useApi.tsx
+++ b/packages/core-plugin-api/src/apis/system/useApi.tsx
@@ -16,10 +16,13 @@
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');
+ 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.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..05d2e1c5c0 100644
--- a/packages/core-plugin-api/src/app/useApp.tsx
+++ b/packages/core-plugin-api/src/app/useApp.tsx
@@ -14,12 +14,16 @@
* 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 => {
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/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/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..ea05cc14fd 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,
@@ -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/.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..eca13071ed
--- /dev/null
+++ b/packages/version-bridge/api-report.md
@@ -0,0 +1,50 @@
+## 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
+import { Context } from 'react';
+
+// @public
+export function createVersionedContext<
+ Versions extends {
+ [version in number]: unknown;
+ },
+>(key: string): Context | undefined>;
+
+// @public
+export function createVersionedContextForTesting(key: string): {
+ set(versions: { [x: number]: unknown }): void;
+ reset(): void;
+};
+
+// @public
+export function createVersionedValueMap<
+ Versions extends {
+ [version: number]: unknown;
+ },
+>(versions: Versions): VersionedValue;
+
+// @public
+export function getOrCreateGlobalSingleton(id: string, supplier: () => T): T;
+
+// @public
+export function useVersionedContext<
+ Versions extends {
+ [version in number]: unknown;
+ },
+>(key: string): VersionedValue | undefined;
+
+// @public
+export type VersionedValue<
+ Versions extends {
+ [version: number]: unknown;
+ },
+> = {
+ 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..3adbac681d
--- /dev/null
+++ b/packages/version-bridge/package.json
@@ -0,0 +1,44 @@
+{
+ "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": {
+ "@types/react": "*",
+ "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/VersionedContext.test.tsx b/packages/version-bridge/src/lib/VersionedContext.test.tsx
new file mode 100644
index 0000000000..659fd9b5b9
--- /dev/null
+++ b/packages/version-bridge/src/lib/VersionedContext.test.tsx
@@ -0,0 +1,94 @@
+/*
+ * 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).toBeUndefined();
+ context.set({ 1: '3v1' });
+ expect(rendered.result.current).toBeUndefined();
+ // 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).toBeUndefined();
+
+ 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/VersionedContext.ts b/packages/version-bridge/src/lib/VersionedContext.ts
new file mode 100644
index 0000000000..ea0c40bbc4
--- /dev/null
+++ b/packages/version-bridge/src/lib/VersionedContext.ts
@@ -0,0 +1,106 @@
+/*
+ * 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 { 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.
+ * @public
+ * @example
+ *
+ * ```ts
+ * const MyContext = createVersionedContext<{ 1: string }>('my-context');
+ *
+ * const MyContextProvider = ({children}) => (
+ *
+ * {children}
+ *
+ * )
+ * ```
+ */
+export function createVersionedContext<
+ Versions extends { [version in number]: unknown },
+>(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.
+ * @public
+ * @example
+ *
+ * ```ts
+ * const versionedHolder = useVersionedContext<{ 1: string }>('my-context');
+ *
+ * if (!versionedHolder) {
+ * throw new Error('My context is not available!')
+ * }
+ *
+ * const myValue = versionedHolder.atVersion(1);
+ *
+ * // ...
+ * ```
+ */
+export function useVersionedContext<
+ Versions extends { [version in number]: unknown },
+>(key: string): VersionedValue | undefined {
+ return useContext(createVersionedContext(key));
+}
+
+/**
+ * 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.
+ * @public
+ * @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 }) {
+ (globalThis as any)[`__@backstage/${key}__`] = createContext(
+ createVersionedValueMap(versions),
+ );
+ },
+ reset() {
+ delete (globalThis as any)[`__@backstage/${key}__`];
+ },
+ };
+}
diff --git a/packages/core-app-api/src/lib/versionedValues.test.ts b/packages/version-bridge/src/lib/VersionedValue.test.ts
similarity index 95%
rename from packages/core-app-api/src/lib/versionedValues.test.ts
rename to packages/version-bridge/src/lib/VersionedValue.test.ts
index 19f9c8f349..2b6637a96f 100644
--- a/packages/core-app-api/src/lib/versionedValues.test.ts
+++ b/packages/version-bridge/src/lib/VersionedValue.test.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { createVersionedValueMap } from './versionedValues';
+import { createVersionedValueMap } from './VersionedValue';
describe('createVersionedValueMap', () => {
it('should be empty', () => {
diff --git a/packages/core-app-api/src/lib/versionedValues.ts b/packages/version-bridge/src/lib/VersionedValue.ts
similarity index 93%
rename from packages/core-app-api/src/lib/versionedValues.ts
rename to packages/version-bridge/src/lib/VersionedValue.ts
index 3b3064fc7e..b7c2153e38 100644
--- a/packages/core-app-api/src/lib/versionedValues.ts
+++ b/packages/version-bridge/src/lib/VersionedValue.ts
@@ -18,8 +18,10 @@
* 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 = {
+export type VersionedValue = {
atVersion(
version: Version,
): Versions[Version] | undefined;
@@ -27,9 +29,11 @@ export type VersionedValue = {
/**
* Creates a container for a map of versioned values that implements VersionedValue.
+ *
+ * @public
*/
export function createVersionedValueMap<
- Versions extends { [version: number]: any },
+ Versions extends { [version: number]: unknown },
>(versions: Versions): VersionedValue {
Object.freeze(versions);
return {
diff --git a/packages/core-plugin-api/src/lib/globalObject.test.ts b/packages/version-bridge/src/lib/globalObject.test.ts
similarity index 65%
rename from packages/core-plugin-api/src/lib/globalObject.test.ts
rename to packages/version-bridge/src/lib/globalObject.test.ts
index 13634f1f08..9c12ae1dfa 100644
--- a/packages/core-plugin-api/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/core-plugin-api/src/lib/globalObject.ts b/packages/version-bridge/src/lib/globalObject.ts
similarity index 70%
rename from packages/core-plugin-api/src/lib/globalObject.ts
rename to packages/version-bridge/src/lib/globalObject.ts
index 7a400148fb..b29e81e4a0 100644
--- a/packages/core-plugin-api/src/lib/globalObject.ts
+++ b/packages/version-bridge/src/lib/globalObject.ts
@@ -30,31 +30,10 @@ 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.
+ *
+ * @public
*/
export function getOrCreateGlobalSingleton(
id: string,
diff --git a/packages/version-bridge/src/lib/index.ts b/packages/version-bridge/src/lib/index.ts
new file mode 100644
index 0000000000..191abe7444
--- /dev/null
+++ b/packages/version-bridge/src/lib/index.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { getOrCreateGlobalSingleton } from './globalObject';
+export {
+ createVersionedContextForTesting,
+ useVersionedContext,
+ createVersionedContext,
+} from './VersionedContext';
+export { createVersionedValueMap } from './VersionedValue';
+export type { VersionedValue } from './VersionedValue';
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';