Merge pull request #7143 from backstage/rugvip/bridge
Extract version bridging logic from core into new version-bridge package
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
'@backstage/core-plugin-api': patch
|
||||
---
|
||||
|
||||
Switch to using utilities from `@backstage/version-bridge'.
|
||||
@@ -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": "*",
|
||||
|
||||
@@ -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(<MyHookConsumer />);
|
||||
}).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 <MyHookConsumer> component/,
|
||||
@@ -131,11 +130,11 @@ describe('ApiProvider', () => {
|
||||
withLogCollector(['error'], () => {
|
||||
expect(() => {
|
||||
render(<MyHocConsumer />);
|
||||
}).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 <withApis\(Component\)> component/,
|
||||
@@ -185,13 +184,10 @@ describe('ApiProvider', () => {
|
||||
});
|
||||
|
||||
describe('v1 consumer', () => {
|
||||
const ApiContext =
|
||||
getGlobalSingleton<Context<VersionedValue<{ 1: ApiHolder }>>>(
|
||||
'api-context',
|
||||
);
|
||||
|
||||
function useMockApiV1<T>(apiRef: ApiRef<T>): 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');
|
||||
}
|
||||
|
||||
@@ -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<ApiContextType>(undefined),
|
||||
);
|
||||
const ApiContext = createVersionedContext<{ 1: ApiHolder }>('api-context');
|
||||
|
||||
export const ApiProvider = ({
|
||||
apis,
|
||||
|
||||
@@ -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<Context<VersionedValue<{ 1: AppContextV1 }>>>(
|
||||
'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');
|
||||
}
|
||||
|
||||
@@ -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<AppContextType | undefined>(undefined),
|
||||
);
|
||||
const AppContext = createVersionedContext<{ 1: AppContextV1 }>('app-context');
|
||||
|
||||
type Props = {
|
||||
appContext: AppContextV1;
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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<T>(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<T>(
|
||||
id: string,
|
||||
supplier: () => T,
|
||||
): T {
|
||||
const key = makeKey(id);
|
||||
|
||||
let value = globalObject[key];
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
value = supplier();
|
||||
globalObject[key] = value;
|
||||
return value;
|
||||
}
|
||||
@@ -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<Context<VersionedValue<{ 1: RouteResolver }>>>(
|
||||
'routing-context',
|
||||
);
|
||||
|
||||
function useMockRouteRefV1(
|
||||
routeRef: AnyRouteRef,
|
||||
location: string,
|
||||
): RouteFunc<any> | undefined {
|
||||
const resolver = useContext(RoutingContext)?.atVersion(1);
|
||||
const resolver = useVersionedContext<{
|
||||
1: RouteResolver;
|
||||
}>('routing-context')?.atVersion(1);
|
||||
if (!resolver) {
|
||||
throw new Error('no impl');
|
||||
}
|
||||
|
||||
@@ -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<RoutingContextType>(undefined),
|
||||
);
|
||||
const RoutingContext =
|
||||
createVersionedContext<{ 1: RouteResolver }>('routing-context');
|
||||
|
||||
type ProviderProps = {
|
||||
routePaths: Map<RouteRef, string>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ComponentType, ReactNode } from 'react';
|
||||
import { getOrCreateGlobalSingleton } from '../lib/globalObject';
|
||||
import { getOrCreateGlobalSingleton } from '@backstage/version-bridge';
|
||||
|
||||
type DataContainer = {
|
||||
map: Map<string, unknown>;
|
||||
|
||||
@@ -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<Versions extends { [version: number]: any }> = {
|
||||
atVersion<Version extends keyof Versions>(
|
||||
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<Versions> {
|
||||
Object.freeze(versions);
|
||||
return {
|
||||
atVersion(version) {
|
||||
return versions[version];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useVersionedContext<
|
||||
Versions extends { [version in number]: any },
|
||||
>(key: string): VersionedValue<Versions> {
|
||||
const versionedValue = useContext(
|
||||
getGlobalSingleton<Context<VersionedValue<Versions>>>(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}__`];
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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<Params extends AnyParams> = keyof Params extends never
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<Params extends AnyParams>(
|
||||
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),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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<VersionedValue<Versions> | 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<Versions>;
|
||||
|
||||
// @public
|
||||
export function getOrCreateGlobalSingleton<T>(id: string, supplier: () => T): T;
|
||||
|
||||
// @public
|
||||
export function useVersionedContext<
|
||||
Versions extends {
|
||||
[version in number]: unknown;
|
||||
},
|
||||
>(key: string): VersionedValue<Versions> | undefined;
|
||||
|
||||
// @public
|
||||
export type VersionedValue<
|
||||
Versions extends {
|
||||
[version: number]: unknown;
|
||||
},
|
||||
> = {
|
||||
atVersion<Version extends keyof Versions>(
|
||||
version: Version,
|
||||
): Versions[Version] | undefined;
|
||||
};
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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<ContextType>('test-context-1');
|
||||
|
||||
const rendered = renderHook(() => useContext(Context), {
|
||||
wrapper: ({ children }) => (
|
||||
<Context.Provider
|
||||
value={createVersionedValueMap({ 1: '1v1', 2: '1v2' })}
|
||||
>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
),
|
||||
});
|
||||
|
||||
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<ContextType>('test-context-2');
|
||||
|
||||
const rendered = renderHook(() => useVersionedContext('test-context-2'), {
|
||||
wrapper: ({ children }) => (
|
||||
<Context.Provider
|
||||
value={createVersionedValueMap({ 1: '2v1', 2: '2v2' })}
|
||||
>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
),
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -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}) => (
|
||||
* <MyContext.Provider value={createVersionedValueMap({ 1: 'value-for-version-1' })}>
|
||||
* {children}
|
||||
* <MyContext.Provider>
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function createVersionedContext<
|
||||
Versions extends { [version in number]: unknown },
|
||||
>(key: string): Context<VersionedValue<Versions> | undefined> {
|
||||
return getOrCreateGlobalSingleton(key, () =>
|
||||
createContext<VersionedValue<Versions> | 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<Versions> | undefined {
|
||||
return useContext(createVersionedContext<Versions>(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}__`];
|
||||
},
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createVersionedValueMap } from './versionedValues';
|
||||
import { createVersionedValueMap } from './VersionedValue';
|
||||
|
||||
describe('createVersionedValueMap', () => {
|
||||
it('should be empty', () => {
|
||||
+6
-2
@@ -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<Versions extends { [version: number]: any }> = {
|
||||
export type VersionedValue<Versions extends { [version: number]: unknown }> = {
|
||||
atVersion<Version extends keyof Versions>(
|
||||
version: Version,
|
||||
): Versions[Version] | undefined;
|
||||
@@ -27,9 +29,11 @@ export type VersionedValue<Versions extends { [version: number]: any }> = {
|
||||
|
||||
/**
|
||||
* 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<Versions> {
|
||||
Object.freeze(versions);
|
||||
return {
|
||||
+1
-24
@@ -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__'];
|
||||
+2
-23
@@ -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<T>(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<T>(
|
||||
id: string,
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user