diff --git a/packages/app-api/src/apis/system/ApiAggregator.test.ts b/packages/app-api/src/apis/system/ApiAggregator.test.ts index 2d14087fa2..a72d6250c0 100644 --- a/packages/app-api/src/apis/system/ApiAggregator.test.ts +++ b/packages/app-api/src/apis/system/ApiAggregator.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ +import { createApiRef } from '@backstage/plugin-api'; import { ApiAggregator } from './ApiAggregator'; -import { createApiRef } from './ApiRef'; import { ApiRegistry } from './ApiRegistry'; describe('ApiAggregator', () => { - const apiARef = createApiRef({ id: 'a', description: '' }); - const apiBRef = createApiRef({ id: 'b', description: '' }); + const apiARef = createApiRef({ id: 'a' }); + const apiBRef = createApiRef({ id: 'b' }); it('should forward implementations', () => { const agg = new ApiAggregator( diff --git a/packages/app-api/src/apis/system/ApiAggregator.ts b/packages/app-api/src/apis/system/ApiAggregator.ts index 1587a1d10b..8224afd8cf 100644 --- a/packages/app-api/src/apis/system/ApiAggregator.ts +++ b/packages/app-api/src/apis/system/ApiAggregator.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ApiRef, ApiHolder } from './types'; +import { ApiRef, ApiHolder } from '@backstage/plugin-api'; /** * An ApiHolder that queries multiple other holders from for diff --git a/packages/app-api/src/apis/system/ApiFactoryRegistry.test.ts b/packages/app-api/src/apis/system/ApiFactoryRegistry.test.ts index a51ed03fea..7baa0f5f99 100644 --- a/packages/app-api/src/apis/system/ApiFactoryRegistry.test.ts +++ b/packages/app-api/src/apis/system/ApiFactoryRegistry.test.ts @@ -14,15 +14,15 @@ * limitations under the License. */ +import { createApiRef } from '@backstage/plugin-api'; import { ApiFactoryRegistry } from './ApiFactoryRegistry'; -import { createApiRef } from './ApiRef'; -const aRef = createApiRef({ id: 'a', description: '' }); +const aRef = createApiRef({ id: 'a' }); const aFactory1 = { api: aRef, deps: {}, factory: () => 1 }; const aFactory2 = { api: aRef, deps: {}, factory: () => 2 }; -const bRef = createApiRef({ id: 'b', description: '' }); +const bRef = createApiRef({ id: 'b' }); const bFactory = { api: bRef, deps: {}, factory: () => 'x' }; -const cRef = createApiRef({ id: 'c', description: '' }); +const cRef = createApiRef({ id: 'c' }); const cFactory = { api: cRef, deps: {}, factory: () => 'y' }; describe('ApiFactoryRegistry', () => { @@ -69,8 +69,8 @@ describe('ApiFactoryRegistry', () => { }); it('should identify ApiRefs by id but still return the correct factory ref when listing all apis', () => { - const ref1 = createApiRef({ id: 'a', description: 'ref1' }); - const ref2 = createApiRef({ id: 'a', description: 'ref2' }); + const ref1 = createApiRef({ id: 'a' }); + const ref2 = createApiRef({ id: 'a' }); const factory1 = { api: ref1, deps: {}, factory: () => 3 }; const factory2 = { api: ref2, deps: {}, factory: () => 3 }; diff --git a/packages/app-api/src/apis/system/ApiFactoryRegistry.ts b/packages/app-api/src/apis/system/ApiFactoryRegistry.ts index c5a76ee1d4..eb09e18a57 100644 --- a/packages/app-api/src/apis/system/ApiFactoryRegistry.ts +++ b/packages/app-api/src/apis/system/ApiFactoryRegistry.ts @@ -14,13 +14,13 @@ * limitations under the License. */ +import { ApiFactoryHolder } from './types'; import { ApiRef, - ApiFactoryHolder, ApiFactory, AnyApiRef, AnyApiFactory, -} from './types'; +} from '@backstage/plugin-api'; type ApiFactoryScope = | 'default' // Default factories registered by core and plugins diff --git a/packages/app-api/src/apis/system/ApiProvider.test.tsx b/packages/app-api/src/apis/system/ApiProvider.test.tsx index 4c0b3f5683..2cef83ee53 100644 --- a/packages/app-api/src/apis/system/ApiProvider.test.tsx +++ b/packages/app-api/src/apis/system/ApiProvider.test.tsx @@ -15,18 +15,23 @@ */ import React, { Context, useContext } from 'react'; -import { ApiProvider, useApi, withApis } from './ApiProvider'; -import { createApiRef } from './ApiRef'; +import { + useApi, + createApiRef, + withApis, + ApiHolder, + ApiRef, +} from '@backstage/plugin-api'; +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 { ApiHolder, ApiRef } from './types'; import { VersionedValue } from '../../lib/versionedValues'; describe('ApiProvider', () => { type Api = () => string; - const apiRef = createApiRef({ id: 'x', description: '' }); + const apiRef = createApiRef({ id: 'x' }); const registry = ApiRegistry.from([[apiRef, () => 'hello']]); const MyHookConsumer = () => { @@ -55,8 +60,8 @@ describe('ApiProvider', () => { }); it('should provide nested access to apis', () => { - const aRef = createApiRef({ id: 'a', description: '' }); - const bRef = createApiRef({ id: 'b', description: '' }); + const aRef = createApiRef({ id: 'a' }); + const bRef = createApiRef({ id: 'b' }); const MyComponent = () => { const a = useApi(aRef); @@ -85,7 +90,7 @@ describe('ApiProvider', () => { it('should ignore deps in prototype', () => { // 100% coverage + happy typescript = hasOwnProperty + this atrocity - const xRef = createApiRef({ id: 'x', description: '' }); + const xRef = createApiRef({ id: 'x' }); const proto = { x: xRef }; const props = { getMessage: { enumerable: true, value: apiRef } }; @@ -193,7 +198,7 @@ describe('v1 consumer', () => { } type Api = () => string; - const apiRef = createApiRef({ id: 'x', description: '' }); + const apiRef = createApiRef({ id: 'x' }); const registry = ApiRegistry.from([[apiRef, () => 'hello']]); const MyHookConsumerV1 = () => { diff --git a/packages/app-api/src/apis/system/ApiProvider.tsx b/packages/app-api/src/apis/system/ApiProvider.tsx index a99b849104..37e512f20d 100644 --- a/packages/app-api/src/apis/system/ApiProvider.tsx +++ b/packages/app-api/src/apis/system/ApiProvider.tsx @@ -19,26 +19,16 @@ import React, { useContext, ReactNode, PropsWithChildren, - Context, } from 'react'; import PropTypes from 'prop-types'; -import { ApiRef, ApiHolder, TypesToApiRefs } from './types'; +import { ApiHolder } from '@backstage/plugin-api'; import { ApiAggregator } from './ApiAggregator'; -import { - getGlobalSingleton, - getOrCreateGlobalSingleton, -} from '../../lib/globalObject'; +import { getOrCreateGlobalSingleton } from '../../lib/globalObject'; import { VersionedValue, createVersionedValueMap, } from '../../lib/versionedValues'; -const missingHolderMessage = - 'No ApiProvider available in react context. ' + - 'A common cause of this error is that multiple versions of @backstage/core-api are installed. ' + - `You can check if that is the case using 'yarn backstage-cli versions:check', and can in many cases ` + - `fix the issue either with the --fix flag or using 'yarn backstage-cli versions:bump'`; - type ApiProviderProps = { apis: ApiHolder; children: ReactNode; @@ -68,62 +58,3 @@ ApiProvider.propTypes = { apis: PropTypes.shape({ get: PropTypes.func.isRequired }).isRequired, children: PropTypes.node, }; - -export function useApiHolder(): ApiHolder { - const versionedHolder = useContext( - getGlobalSingleton>('api-context'), - ); - - if (!versionedHolder) { - throw new Error(missingHolderMessage); - } - - const apiHolder = versionedHolder.atVersion(1); - if (!apiHolder) { - throw new Error('ApiContext v1 not available'); - } - - return apiHolder; -} - -export function useApi(apiRef: ApiRef): T { - const apiHolder = useApiHolder(); - - const api = apiHolder.get(apiRef); - if (!api) { - throw new Error(`No implementation available for ${apiRef}`); - } - return api; -} - -export function withApis(apis: TypesToApiRefs) { - return function withApisWrapper

( - WrappedComponent: React.ComponentType

, - ) { - const Hoc = (props: PropsWithChildren>) => { - const apiHolder = useApiHolder(); - - const impls = {} as T; - - for (const key in apis) { - if (apis.hasOwnProperty(key)) { - const ref = apis[key]; - - const api = apiHolder.get(ref); - if (!api) { - throw new Error(`No implementation available for ${ref}`); - } - impls[key] = api; - } - } - - return ; - }; - const displayName = - WrappedComponent.displayName || WrappedComponent.name || 'Component'; - - Hoc.displayName = `withApis(${displayName})`; - - return Hoc; - }; -} diff --git a/packages/app-api/src/apis/system/ApiRef.test.ts b/packages/app-api/src/apis/system/ApiRef.test.ts deleted file mode 100644 index b9ea1470cd..0000000000 --- a/packages/app-api/src/apis/system/ApiRef.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createApiRef } from './ApiRef'; - -describe('ApiRef', () => { - it('should be created', () => { - const ref = createApiRef({ id: 'abc', description: '123' }); - expect(ref.id).toBe('abc'); - expect(ref.description).toBe('123'); - expect(String(ref)).toBe('apiRef{abc}'); - expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}'); - }); - - it('should reject invalid ids', () => { - for (const id of ['a', 'abc', 'ab-c', 'a.b.c', 'a-b.c', 'abc.a-b-c.abc3']) { - expect(createApiRef({ id, description: '123' }).id).toBe(id); - } - - for (const id of [ - '123', - 'ab-3', - 'ab_c', - '.', - '2ac', - 'ab.3a', - '.abc', - 'abc.', - 'ab..s', - '', - '_', - ]) { - expect(() => createApiRef({ id, description: '123' }).id).toThrow( - `API id must only contain period separated lowercase alphanum tokens with dashes, got '${id}'`, - ); - } - }); -}); diff --git a/packages/app-api/src/apis/system/ApiRef.ts b/packages/app-api/src/apis/system/ApiRef.ts deleted file mode 100644 index e61036c61c..0000000000 --- a/packages/app-api/src/apis/system/ApiRef.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { ApiRef } from './types'; - -export type ApiRefConfig = { - id: string; - description?: string; -}; - -class ApiRefImpl implements ApiRef { - constructor(private readonly config: ApiRefConfig) { - const valid = config.id - .split('.') - .flatMap(part => part.split('-')) - .every(part => part.match(/^[a-z][a-z0-9]*$/)); - if (!valid) { - throw new Error( - `API id must only contain period separated lowercase alphanum tokens with dashes, got '${config.id}'`, - ); - } - } - - get id(): string { - return this.config.id; - } - - get description() { - return this.config.description; - } - - // Utility for getting type of an api, using `typeof apiRef.T` - get T(): T { - throw new Error(`tried to read ApiRef.T of ${this}`); - } - - toString() { - return `apiRef{${this.config.id}}`; - } -} - -export function createApiRef(config: ApiRefConfig): ApiRef { - return new ApiRefImpl(config); -} diff --git a/packages/app-api/src/apis/system/ApiRegistry.test.ts b/packages/app-api/src/apis/system/ApiRegistry.test.ts index 93dd6dc085..89dd62ca07 100644 --- a/packages/app-api/src/apis/system/ApiRegistry.test.ts +++ b/packages/app-api/src/apis/system/ApiRegistry.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ +import { createApiRef } from '@backstage/plugin-api'; import { ApiRegistry } from './ApiRegistry'; -import { createApiRef } from './ApiRef'; describe('ApiRegistry', () => { - const x1Ref = createApiRef({ id: 'x1', description: '' }); - const x1DuplicateRef = createApiRef({ id: 'x1', description: '' }); - const x2Ref = createApiRef({ id: 'x2', description: '' }); + const x1Ref = createApiRef({ id: 'x1' }); + const x1DuplicateRef = createApiRef({ id: 'x1' }); + const x2Ref = createApiRef({ id: 'x2' }); it('should be created', () => { const registry = ApiRegistry.from([]); diff --git a/packages/app-api/src/apis/system/ApiRegistry.ts b/packages/app-api/src/apis/system/ApiRegistry.ts index 01101b2b62..ff3f3bb8be 100644 --- a/packages/app-api/src/apis/system/ApiRegistry.ts +++ b/packages/app-api/src/apis/system/ApiRegistry.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ApiRef, ApiHolder } from './types'; +import { ApiRef, ApiHolder } from '@backstage/plugin-api'; type ApiImpl = readonly [ApiRef, T]; diff --git a/packages/app-api/src/apis/system/ApiResolver.test.ts b/packages/app-api/src/apis/system/ApiResolver.test.ts index 7a46d2db3b..940ffb8b60 100644 --- a/packages/app-api/src/apis/system/ApiResolver.test.ts +++ b/packages/app-api/src/apis/system/ApiResolver.test.ts @@ -14,19 +14,16 @@ * limitations under the License. */ +import { createApiRef } from '@backstage/plugin-api'; import { ApiResolver } from './ApiResolver'; -import { createApiRef } from './ApiRef'; import { ApiFactoryRegistry } from './ApiFactoryRegistry'; -const aRef = createApiRef({ id: 'a', description: '' }); -const otherARef = createApiRef({ id: 'a', description: 'other' }); -const bRef = createApiRef({ id: 'b', description: '' }); -const otherBRef = createApiRef({ id: 'b', description: 'other' }); -const cRef = createApiRef<{ x: string }>({ id: 'c', description: '' }); -const otherCRef = createApiRef<{ x: string }>({ - id: 'c', - description: 'other', -}); +const aRef = createApiRef({ id: 'a' }); +const otherARef = createApiRef({ id: 'a' }); +const bRef = createApiRef({ id: 'b' }); +const otherBRef = createApiRef({ id: 'b' }); +const cRef = createApiRef<{ x: string }>({ id: 'c' }); +const otherCRef = createApiRef<{ x: string }>({ id: 'c' }); function createRegistry() { const registry = new ApiFactoryRegistry(); diff --git a/packages/app-api/src/apis/system/ApiResolver.ts b/packages/app-api/src/apis/system/ApiResolver.ts index 9738e09622..379d7356d1 100644 --- a/packages/app-api/src/apis/system/ApiResolver.ts +++ b/packages/app-api/src/apis/system/ApiResolver.ts @@ -17,10 +17,10 @@ import { ApiRef, ApiHolder, - ApiFactoryHolder, AnyApiRef, TypesToApiRefs, -} from './types'; +} from '@backstage/plugin-api'; +import { ApiFactoryHolder } from './types'; export class ApiResolver implements ApiHolder { /** diff --git a/packages/app-api/src/apis/system/helpers.ts b/packages/app-api/src/apis/system/helpers.ts deleted file mode 100644 index cabff73060..0000000000 --- a/packages/app-api/src/apis/system/helpers.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiRef, ApiFactory, TypesToApiRefs } from './types'; - -/** - * Used to infer types for a standalone ApiFactory that isn't immediately passed - * to another function. - * This function doesn't actually do anything, it's only used to infer types. - */ -export function createApiFactory< - Api, - Impl extends Api, - Deps extends { [name in string]: unknown } ->(factory: ApiFactory): ApiFactory; -export function createApiFactory( - api: ApiRef, - instance: Impl, -): ApiFactory; -export function createApiFactory< - Api, - Impl extends Api, - Deps extends { [name in string]: unknown } ->( - factory: ApiFactory | ApiRef, - instance?: Impl, -): ApiFactory { - if ('id' in factory) { - return { - api: factory, - deps: {} as TypesToApiRefs, - factory: () => instance!, - }; - } - return factory; -} diff --git a/packages/app-api/src/apis/system/index.ts b/packages/app-api/src/apis/system/index.ts index 10b2e0f084..dd7c081f62 100644 --- a/packages/app-api/src/apis/system/index.ts +++ b/packages/app-api/src/apis/system/index.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -export { ApiProvider, useApi, useApiHolder } from './ApiProvider'; +export { ApiProvider } from './ApiProvider'; export { ApiRegistry } from './ApiRegistry'; export { ApiResolver } from './ApiResolver'; export { ApiFactoryRegistry } from './ApiFactoryRegistry'; -export { createApiRef } from './ApiRef'; export * from './types'; -export * from './helpers'; diff --git a/packages/app-api/src/apis/system/types.ts b/packages/app-api/src/apis/system/types.ts index a4bc95a3c3..b20c19c113 100644 --- a/packages/app-api/src/apis/system/types.ts +++ b/packages/app-api/src/apis/system/types.ts @@ -14,41 +14,7 @@ * limitations under the License. */ -export type ApiRef = { - id: string; - description?: string; - T: T; -}; - -export type AnyApiRef = ApiRef; - -export type ApiRefType = T extends ApiRef ? U : never; - -export type TypesToApiRefs = { [key in keyof T]: ApiRef }; - -export type ApiRefsToTypes }> = { - [key in keyof T]: ApiRefType; -}; - -export type ApiHolder = { - get(api: ApiRef): T | undefined; -}; - -export type ApiFactory< - Api, - Impl extends Api, - Deps extends { [name in string]: unknown } -> = { - api: ApiRef; - deps: TypesToApiRefs; - factory(deps: Deps): Impl; -}; - -export type AnyApiFactory = ApiFactory< - unknown, - unknown, - { [key in string]: unknown } ->; +import { ApiFactory, ApiRef } from '@backstage/plugin-api'; export type ApiFactoryHolder = { get( diff --git a/packages/app-api/src/app/App.test.tsx b/packages/app-api/src/app/App.test.tsx index 47caae930a..22f2dedc11 100644 --- a/packages/app-api/src/app/App.test.tsx +++ b/packages/app-api/src/app/App.test.tsx @@ -21,13 +21,13 @@ import React, { PropsWithChildren } from 'react'; import { BrowserRouter, Routes } from 'react-router-dom'; import { createRoutableExtension } from '../extensions'; import { defaultAppIcons } from './icons'; -import { createPlugin } from '../plugin'; -import { useRouteRef } from '../routing/hooks'; import { + createPlugin, + useRouteRef, createExternalRouteRef, createRouteRef, createSubRouteRef, -} from '../routing'; +} from '@backstage/plugin-api'; import { generateBoundRoutes, PrivateAppImpl } from './App'; describe('generateBoundRoutes', () => { diff --git a/packages/app-api/src/app/App.tsx b/packages/app-api/src/app/App.tsx index 7aaac06623..bd6a8d6e31 100644 --- a/packages/app-api/src/app/App.tsx +++ b/packages/app-api/src/app/App.tsx @@ -21,19 +21,19 @@ import React, { useMemo, useState, } from 'react'; -import { Navigate, Route, Routes } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom'; import { useAsync } from 'react-use'; import { - AnyApiFactory, - ApiHolder, ApiProvider, ApiRegistry, AppThemeSelector, ConfigReader, LocalStorageFeatureFlags, - useApi, } from '../apis'; import { + useApi, + AnyApiFactory, + ApiHolder, IconComponent, AppTheme, appThemeApiRef, @@ -42,6 +42,9 @@ import { ConfigApi, featureFlagsApiRef, identityApiRef, + BackstagePlugin, + RouteRef, + ExternalRouteRef, } from '@backstage/plugin-api'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { @@ -49,15 +52,12 @@ import { routeElementDiscoverer, traverseElementTree, } from '../extensions/traversal'; -import { BackstagePlugin } from '../plugin'; -import { AnyRoutes } from '../plugin/types'; -import { RouteRef, ExternalRouteRef } from '../routing'; import { routeObjectCollector, routeParentCollector, routePathCollector, } from '../routing/collectors'; -import { RoutingProvider } from '../routing/hooks'; +import { RoutingProvider } from '../routing/RoutingProvider'; import { validateRoutes } from '../routing/validation'; import { AppContextProvider } from './AppContext'; import { AppIdentity } from './AppIdentity'; @@ -79,7 +79,10 @@ export function generateBoundRoutes( const result = new Map(); if (bindRoutes) { - const bind: AppRouteBinder = (externalRoutes, targetRoutes: AnyRoutes) => { + const bind: AppRouteBinder = ( + externalRoutes, + targetRoutes: { [name: string]: RouteRef }, + ) => { for (const [key, value] of Object.entries(targetRoutes)) { const externalRoute = externalRoutes[key]; if (!externalRoute) { @@ -150,12 +153,6 @@ function useConfigLoader( class AppContextImpl implements AppContext { constructor(private readonly app: PrivateAppImpl) {} - getPlugins(): BackstagePlugin[] { - // eslint-disable-next-line no-console - console.warn('appContext.getPlugins() is deprecated and will be removed'); - return this.app.getPlugins(); - } - getSystemIcon(key: string): IconComponent | undefined { return this.app.getSystemIcon(key); } @@ -163,24 +160,6 @@ class AppContextImpl implements AppContext { getComponents(): AppComponents { return this.app.getComponents(); } - - getProvider(): React.ComponentType<{}> { - // eslint-disable-next-line no-console - console.warn('appContext.getProvider() is deprecated and will be removed'); - return this.app.getProvider(); - } - - getRouter(): React.ComponentType<{}> { - // eslint-disable-next-line no-console - console.warn('appContext.getRouter() is deprecated and will be removed'); - return this.app.getRouter(); - } - - getRoutes(): JSX.Element[] { - // eslint-disable-next-line no-console - console.warn('appContext.getRoutes() is deprecated and will be removed'); - return this.app.getRoutes(); - } } export class PrivateAppImpl implements BackstageApp { @@ -221,59 +200,6 @@ export class PrivateAppImpl implements BackstageApp { return this.components; } - getRoutes(): JSX.Element[] { - const routes = new Array(); - - const { NotFoundErrorPage } = this.components; - - for (const plugin of this.plugins.values()) { - for (const output of plugin.output()) { - switch (output.type) { - case 'legacy-route': { - const { path, component: Component } = output; - routes.push( - } />, - ); - break; - } - case 'route': { - const { target, component: Component } = output; - routes.push( - } - />, - ); - break; - } - case 'legacy-redirect-route': { - const { path, target } = output; - routes.push(); - break; - } - case 'redirect-route': { - const { from, to } = output; - routes.push(); - break; - } - default: - break; - } - } - } - - routes.push( - } - />, - ); - - return routes; - } - getProvider(): ComponentType<{}> { const appContext = new AppContextImpl(this); const apiHolder = this.getApiHolder(); diff --git a/packages/app-api/src/app/AppContext.test.tsx b/packages/app-api/src/app/AppContext.test.tsx index 55d98c18c0..4299e46265 100644 --- a/packages/app-api/src/app/AppContext.test.tsx +++ b/packages/app-api/src/app/AppContext.test.tsx @@ -38,10 +38,6 @@ describe('v1 consumer', () => { const mockContext: AppContextV1 = { getComponents: jest.fn(), getSystemIcon: jest.fn(), - getPlugins: jest.fn(), - getProvider: jest.fn(), - getRouter: jest.fn(), - getRoutes: jest.fn(), }; const renderedHook = renderHook(() => useMockAppV1(), { @@ -59,21 +55,5 @@ describe('v1 consumer', () => { result.getSystemIcon('icon'); expect(mockContext.getSystemIcon).toHaveBeenCalledTimes(1); expect(mockContext.getSystemIcon).toHaveBeenCalledWith('icon'); - - expect(mockContext.getPlugins).toHaveBeenCalledTimes(0); - result.getPlugins(); - expect(mockContext.getPlugins).toHaveBeenCalledTimes(1); - - expect(mockContext.getProvider).toHaveBeenCalledTimes(0); - result.getProvider(); - expect(mockContext.getProvider).toHaveBeenCalledTimes(1); - - expect(mockContext.getRouter).toHaveBeenCalledTimes(0); - result.getRouter(); - expect(mockContext.getRouter).toHaveBeenCalledTimes(1); - - expect(mockContext.getRoutes).toHaveBeenCalledTimes(0); - result.getRoutes(); - expect(mockContext.getRoutes).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/app-api/src/app/AppContext.tsx b/packages/app-api/src/app/AppContext.tsx index cfe439fca5..9cdd4cdc13 100644 --- a/packages/app-api/src/app/AppContext.tsx +++ b/packages/app-api/src/app/AppContext.tsx @@ -14,20 +14,12 @@ * limitations under the License. */ -import React, { - createContext, - PropsWithChildren, - useContext, - Context, -} from 'react'; +import React, { createContext, PropsWithChildren } from 'react'; import { VersionedValue, createVersionedValueMap, } from '../lib/versionedValues'; -import { - getGlobalSingleton, - getOrCreateGlobalSingleton, -} from '../lib/globalObject'; +import { getOrCreateGlobalSingleton } from '../lib/globalObject'; import { AppContext as AppContextV1 } from './types'; type AppContextType = VersionedValue<{ 1: AppContextV1 }> | undefined; @@ -47,17 +39,3 @@ export const AppContextProvider = ({ return ; }; - -export const useApp = (): AppContextV1 => { - const versionedContext = useContext( - getGlobalSingleton>('app-context'), - ); - if (!versionedContext) { - throw new Error('No app context available'); - } - const appContext = versionedContext.atVersion(1); - if (!appContext) { - throw new Error('AppContext v1 not available'); - } - return appContext; -}; diff --git a/packages/app-api/src/app/defaultApis.ts b/packages/app-api/src/app/defaultApis.ts index 40c2c68ca0..cd373543cf 100644 --- a/packages/app-api/src/app/defaultApis.ts +++ b/packages/app-api/src/app/defaultApis.ts @@ -27,13 +27,13 @@ import { MicrosoftAuth, OAuthRequestManager, WebStorage, - createApiFactory, UrlPatternDiscovery, SamlAuth, OneLoginAuth, } from '../apis'; import { + createApiFactory, alertApiRef, errorApiRef, discoveryApiRef, diff --git a/packages/app-api/src/app/index.ts b/packages/app-api/src/app/index.ts index 17610ea3ee..a7cdf22a43 100644 --- a/packages/app-api/src/app/index.ts +++ b/packages/app-api/src/app/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { useApp } from './AppContext'; +export { createApp } from './createApp'; export * from './types'; diff --git a/packages/app-api/src/app/types.ts b/packages/app-api/src/app/types.ts index c309eb8f8a..089a682246 100644 --- a/packages/app-api/src/app/types.ts +++ b/packages/app-api/src/app/types.ts @@ -15,12 +15,17 @@ */ import { ComponentType } from 'react'; -import { AnyExternalRoutes, BackstagePlugin } from '../plugin/types'; -import { ExternalRouteRef, RouteRef } from '../routing'; -import { AnyApiFactory } from '../apis'; -import { AppTheme, ProfileInfo, IconComponent } from '@backstage/plugin-api'; +import { + AnyApiFactory, + AppTheme, + ProfileInfo, + IconComponent, + BackstagePlugin, + RouteRef, + SubRouteRef, + ExternalRouteRef, +} from '@backstage/plugin-api'; import { AppConfig } from '@backstage/config'; -import { SubRouteRef } from '../routing/types'; import { AppIcons } from './icons'; export type BootErrorPageProps = { @@ -98,7 +103,9 @@ type PartialKeys< /** * Creates a map of target routes with matching parameters based on a map of external routes. */ -type TargetRouteMap = { +type TargetRouteMap< + ExternalRoutes extends { [name: string]: ExternalRouteRef } +> = { [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef< infer Params, any @@ -107,7 +114,9 @@ type TargetRouteMap = { : never; }; -export type AppRouteBinder = ( +export type AppRouteBinder = < + ExternalRoutes extends { [name: string]: ExternalRouteRef } +>( externalRoutes: ExternalRoutes, targetRoutes: PartialKeys< TargetRouteMap, @@ -215,21 +224,9 @@ export type BackstageApp = { * and any other components that should only be available while signed in. */ getRouter(): ComponentType<{}>; - - /** - * Routes component that contains all routes for plugin pages in the app. - * - * @deprecated Registering routes in plugins is deprecated and this method will be removed. - */ - getRoutes(): JSX.Element[]; }; export type AppContext = { - /** - * @deprecated Will be removed - */ - getPlugins(): BackstagePlugin[]; - /** * Get a common or custom icon for this app. */ @@ -239,19 +236,4 @@ export type AppContext = { * Get the components registered for various purposes in the app. */ getComponents(): AppComponents; - - /** - * @deprecated Will be removed - */ - getProvider(): ComponentType<{}>; - - /** - * @deprecated Will be removed - */ - getRouter(): ComponentType<{}>; - - /** - * @deprecated Will be removed - */ - getRoutes(): JSX.Element[]; }; diff --git a/packages/app-api/src/extensions/extensions.test.tsx b/packages/app-api/src/extensions/extensions.test.tsx index 26755b3bcf..7bb74cc854 100644 --- a/packages/app-api/src/extensions/extensions.test.tsx +++ b/packages/app-api/src/extensions/extensions.test.tsx @@ -15,8 +15,7 @@ */ import React from 'react'; -import { createPlugin } from '../plugin'; -import { createRouteRef } from '../routing'; +import { createPlugin, createRouteRef } from '@backstage/plugin-api'; import { getComponentData } from './componentData'; import { createComponentExtension, @@ -50,7 +49,7 @@ describe('extensions', () => { it('should create react extensions of different types', () => { const Component = () =>

; - const routeRef = createRouteRef({ path: '/foo', title: 'Foo' }); + const routeRef = createRouteRef({ id: 'foo' }); const extension1 = createComponentExtension({ component: { diff --git a/packages/app-api/src/extensions/extensions.tsx b/packages/app-api/src/extensions/extensions.tsx index e56b6901dd..637ef59ff9 100644 --- a/packages/app-api/src/extensions/extensions.tsx +++ b/packages/app-api/src/extensions/extensions.tsx @@ -15,9 +15,13 @@ */ import React, { lazy, Suspense } from 'react'; -import { RouteRef, useRouteRef } from '../routing'; import { attachComponentData } from './componentData'; -import { Extension, BackstagePlugin } from '../plugin/types'; +import { + Extension, + BackstagePlugin, + RouteRef, + useRouteRef, +} from '@backstage/plugin-api'; type ComponentLoader = | { diff --git a/packages/app-api/src/index.test.ts b/packages/app-api/src/index.test.ts index 0ba9ad13f0..bec176fda8 100644 --- a/packages/app-api/src/index.test.ts +++ b/packages/app-api/src/index.test.ts @@ -17,7 +17,7 @@ import * as index from '.'; describe('index', () => { - it('exports the plugin api', () => { + it('exports the app api', () => { expect(index).toEqual({ // Public API createApp: expect.any(Function), diff --git a/packages/app-api/src/index.ts b/packages/app-api/src/index.ts index 144e125204..23e31f6a2c 100644 --- a/packages/app-api/src/index.ts +++ b/packages/app-api/src/index.ts @@ -16,7 +16,5 @@ export * from './apis'; export * from './app'; -export * from './extensions'; -export * from './plugin'; export * from './routing'; export * from './types'; diff --git a/packages/app-api/src/plugin/Plugin.tsx b/packages/app-api/src/plugin/Plugin.tsx deleted file mode 100644 index cc168707be..0000000000 --- a/packages/app-api/src/plugin/Plugin.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - PluginConfig, - PluginOutput, - BackstagePlugin, - Extension, - AnyRoutes, - AnyExternalRoutes, -} from './types'; -import { AnyApiFactory } from '../apis'; - -export class PluginImpl< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes -> implements BackstagePlugin { - private storedOutput?: PluginOutput[]; - - constructor(private readonly config: PluginConfig) {} - - getId(): string { - return this.config.id; - } - - getApis(): Iterable { - return this.config.apis ?? []; - } - - get routes(): Routes { - return this.config.routes ?? ({} as Routes); - } - - get externalRoutes(): ExternalRoutes { - return this.config.externalRoutes ?? ({} as ExternalRoutes); - } - - output(): PluginOutput[] { - if (this.storedOutput) { - return this.storedOutput; - } - if (!this.config.register) { - return []; - } - - const outputs = new Array(); - - this.config.register({ - router: { - addRoute(target, component, options) { - outputs.push({ - type: 'route', - target, - component, - options, - }); - }, - }, - featureFlags: { - register(name) { - outputs.push({ type: 'feature-flag', name }); - }, - }, - }); - - this.storedOutput = outputs; - return this.storedOutput; - } - - provide(extension: Extension): T { - return extension.expose(this); - } - - toString() { - return `plugin{${this.config.id}}`; - } -} - -export function createPlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {} ->( - config: PluginConfig, -): BackstagePlugin { - return new PluginImpl(config); -} diff --git a/packages/app-api/src/plugin/collectors.test.tsx b/packages/app-api/src/plugin/collectors.test.tsx deleted file mode 100644 index 5baf2539ab..0000000000 --- a/packages/app-api/src/plugin/collectors.test.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { PropsWithChildren } from 'react'; -import { createRouteRef } from '../routing'; -import { createPlugin } from './Plugin'; -import { - createRoutableExtension, - createComponentExtension, -} from '../extensions'; -import { MemoryRouter, Routes, Route } from 'react-router-dom'; -import { - traverseElementTree, - childDiscoverer, - routeElementDiscoverer, -} from '../extensions/traversal'; -import { pluginCollector } from './collectors'; - -const mockConfig = () => ({ path: '/foo', title: 'Foo' }); -const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( - <>{children} -); - -const pluginA = createPlugin({ id: 'my-plugin-a' }); -const pluginB = createPlugin({ id: 'my-plugin-b' }); -const pluginC = createPlugin({ id: 'my-plugin-c' }); - -const ref1 = createRouteRef(mockConfig()); -const ref2 = createRouteRef(mockConfig()); - -const Extension1 = pluginA.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockComponent), - mountPoint: ref1, - }), -); -const Extension2 = pluginB.provide( - createRoutableExtension({ - component: () => Promise.resolve(MockComponent), - mountPoint: ref2, - }), -); -const Extension3 = pluginA.provide( - createComponentExtension({ component: { sync: MockComponent } }), -); -const Extension4 = pluginB.provide( - createComponentExtension({ component: { sync: MockComponent } }), -); -const Extension5 = pluginC.provide( - createComponentExtension({ component: { sync: MockComponent } }), -); - -describe('collection', () => { - it('should collect the plugins', () => { - const root = ( - - - -
- -
-
- {[]} - Some text here shouldn't be a problem -
- {null} -
- -
- - {false} - {true} - {0} -
- -
- } /> -
- - - ); - - const { plugins } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - plugins: pluginCollector, - }, - }); - - expect(plugins).toEqual(new Set([pluginA, pluginB, pluginC])); - }); -}); diff --git a/packages/app-api/src/plugin/collectors.ts b/packages/app-api/src/plugin/collectors.ts deleted file mode 100644 index b04c7b41c4..0000000000 --- a/packages/app-api/src/plugin/collectors.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { BackstagePlugin } from './types'; -import { getComponentData } from '../extensions'; -import { createCollector } from '../extensions/traversal'; - -export const pluginCollector = createCollector( - () => new Set>(), - (acc, node) => { - const plugin = getComponentData>( - node, - 'core.plugin', - ); - if (plugin) { - acc.add(plugin); - } - }, -); diff --git a/packages/app-api/src/plugin/index.ts b/packages/app-api/src/plugin/index.ts deleted file mode 100644 index bbeeca4824..0000000000 --- a/packages/app-api/src/plugin/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { createPlugin } from './Plugin'; -export type { - BackstagePlugin, - Extension, - FeatureFlagOutput, - FeatureFlagsHooks, - LegacyRedirectRouteOutput, - LegacyRouteOutput, - PluginConfig, - PluginHooks, - PluginOutput, - RedirectRouteOutput, - RouteOptions, - RouteOutput, - RoutePath, - RouterHooks, -} from './types'; diff --git a/packages/app-api/src/plugin/types.ts b/packages/app-api/src/plugin/types.ts deleted file mode 100644 index c3b64d8cb6..0000000000 --- a/packages/app-api/src/plugin/types.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ComponentType } from 'react'; -import { RouteRef, ExternalRouteRef } from '../routing'; -import { AnyApiFactory } from '../apis/system'; - -export type RouteOptions = { - // Whether the route path must match exactly, defaults to true. - exact?: boolean; -}; - -export type RoutePath = string; - -// Replace with using RouteRefs -export type LegacyRouteOutput = { - type: 'legacy-route'; - path: RoutePath; - component: ComponentType<{}>; - options?: RouteOptions; -}; - -export type RouteOutput = { - type: 'route'; - target: RouteRef; - component: ComponentType<{}>; - options?: RouteOptions; -}; - -export type RedirectRouteOutput = { - type: 'redirect-route'; - from: RouteRef; - to: RouteRef; - options?: RouteOptions; -}; - -export type LegacyRedirectRouteOutput = { - type: 'legacy-redirect-route'; - path: RoutePath; - target: RoutePath; - options?: RouteOptions; -}; - -export type FeatureFlagOutput = { - type: 'feature-flag'; - name: string; -}; - -export type PluginOutput = - | LegacyRouteOutput - | RouteOutput - | LegacyRedirectRouteOutput - | RedirectRouteOutput - | FeatureFlagOutput; - -export type Extension = { - expose(plugin: BackstagePlugin): T; -}; - -export type AnyRoutes = { [name: string]: RouteRef }; - -export type AnyExternalRoutes = { [name: string]: ExternalRouteRef }; - -export type BackstagePlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {} -> = { - getId(): string; - output(): PluginOutput[]; - getApis(): Iterable; - provide(extension: Extension): T; - routes: Routes; - externalRoutes: ExternalRoutes; -}; - -export type PluginConfig< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes -> = { - id: string; - apis?: Iterable; - register?(hooks: PluginHooks): void; - routes?: Routes; - externalRoutes?: ExternalRoutes; -}; - -export type PluginHooks = { - /** - * @deprecated All router hooks have been deprecated - */ - router: RouterHooks; - featureFlags: FeatureFlagsHooks; -}; - -export type RouterHooks = { - /** - * @deprecated Use a routable extension instead, see https://backstage.io/docs/plugins/composability#porting-existing-plugins - */ - addRoute( - target: RouteRef, - Component: ComponentType, - options?: RouteOptions, - ): void; -}; - -export type FeatureFlagsHooks = { - register(name: string): void; -}; diff --git a/packages/app-api/src/routing/ExternalRouteRef.test.ts b/packages/app-api/src/routing/ExternalRouteRef.test.ts deleted file mode 100644 index 785ad2732f..0000000000 --- a/packages/app-api/src/routing/ExternalRouteRef.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AnyParams, ExternalRouteRef } from './types'; -import { createExternalRouteRef, isExternalRouteRef } from './ExternalRouteRef'; -import { isSubRouteRef } from './SubRouteRef'; -import { isRouteRef } from './RouteRef'; - -describe('ExternalRouteRef', () => { - it('should be created', () => { - const routeRef: ExternalRouteRef = createExternalRouteRef({ - id: 'my-route-ref', - }); - expect(routeRef.params).toEqual([]); - expect(routeRef.optional).toBe(false); - expect(String(routeRef)).toBe('routeRef{type=external,id=my-route-ref}'); - expect(isRouteRef(routeRef)).toBe(false); - expect(isSubRouteRef(routeRef)).toBe(false); - expect(isExternalRouteRef(routeRef)).toBe(true); - - expect(isRouteRef({} as ExternalRouteRef)).toBe(false); - }); - - it('should be created as optional', () => { - const routeRef: ExternalRouteRef<{ - x: string; - y: string; - }> = createExternalRouteRef({ - id: 'my-other-route-ref', - params: [], - optional: true, - }); - expect(routeRef.params).toEqual([]); - expect(routeRef.optional).toEqual(true); - }); - - it('should be created with params', () => { - const routeRef: ExternalRouteRef<{ - x: string; - y: string; - }> = createExternalRouteRef({ - id: 'my-other-route-ref', - params: ['x', 'y'], - }); - expect(routeRef.params).toEqual(['x', 'y']); - expect(routeRef.optional).toEqual(false); - }); - - it('should be created as optional with params', () => { - const routeRef: ExternalRouteRef<{ - x: string; - y: string; - }> = createExternalRouteRef({ - id: 'my-other-route-ref', - params: ['x', 'y'], - optional: true, - }); - expect(routeRef.params).toEqual(['x', 'y']); - expect(routeRef.optional).toEqual(true); - }); - - it('should properly infer and validate parameter types and assignments', () => { - function validateType( - _ref: ExternalRouteRef, - ) {} - - const _1 = createExternalRouteRef({ id: '1', params: ['notX'] }); - // @ts-expect-error - validateType<{ x: string }, any>(_1); - validateType<{ notX: string }, any>(_1); - - const _2 = createExternalRouteRef({ - id: '2', - params: ['x'], - optional: true, - }); - // @ts-expect-error - validateType(_2); - validateType<{ x: string }, true>(_2); - - const _3 = createExternalRouteRef({ id: '3', params: ['x', 'y'] }); - // @ts-expect-error - validateType<{ x: string }, any>(_3); - // TODO(Rugvip): Ideally this would fail as well, but settle for validating it at runtime instead - validateType<{ x: string; y: string; z: string }, any>(_3); - validateType<{ x: string; y: string }, false>(_3); - - const _4 = createExternalRouteRef({ id: '4', params: [] }); - // @ts-expect-error - validateType<{ x: string }, any>(_4); - validateType(_4); - - const _5 = createExternalRouteRef({ id: '5' }); - // @ts-expect-error - validateType<{ x: string }, any>(_5); - validateType(_5); - - const _6 = createExternalRouteRef({ id: '6', optional: true }); - // @ts-expect-error - validateType(_6); - validateType(_6); - - // To avoid complains about missing expectations and unused vars - expect([_1, _2, _3, _4, _5, _6].join('')).toEqual(expect.any(String)); - }); -}); diff --git a/packages/app-api/src/routing/ExternalRouteRef.ts b/packages/app-api/src/routing/ExternalRouteRef.ts deleted file mode 100644 index e6af9cf8a7..0000000000 --- a/packages/app-api/src/routing/ExternalRouteRef.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - RouteRef, - SubRouteRef, - ExternalRouteRef, - routeRefType, - AnyParams, - ParamKeys, - OptionalParams, -} from './types'; - -export class ExternalRouteRefImpl< - Params extends AnyParams, - Optional extends boolean -> implements ExternalRouteRef { - readonly [routeRefType] = 'external'; - - constructor( - private readonly id: string, - readonly params: ParamKeys, - readonly optional: Optional, - ) {} - - toString() { - return `routeRef{type=external,id=${this.id}}`; - } -} - -export function createExternalRouteRef< - Params extends { [param in ParamKey]: string }, - Optional extends boolean = false, - ParamKey extends string = never ->(options: { - /** - * An identifier for this route, used to identify it in error messages - */ - id: string; - - /** - * The parameters that will be provided to the external route reference. - */ - params?: ParamKey[]; - - /** - * Whether or not this route is optional, defaults to false. - * - * Optional external routes are not required to be bound in the app, and - * if they aren't, `useRouteRef` will return `undefined`. - */ - optional?: Optional; -}): ExternalRouteRef, Optional> { - return new ExternalRouteRefImpl( - options.id, - (options.params ?? []) as ParamKeys>, - Boolean(options.optional) as Optional, - ); -} - -export function isExternalRouteRef< - Params extends AnyParams, - Optional extends boolean ->( - routeRef: - | RouteRef - | SubRouteRef - | ExternalRouteRef, -): routeRef is ExternalRouteRef { - return routeRef[routeRefType] === 'external'; -} diff --git a/packages/app-api/src/routing/FlatRoutes.tsx b/packages/app-api/src/routing/FlatRoutes.tsx index 5f2cfc150d..bbfd0aef50 100644 --- a/packages/app-api/src/routing/FlatRoutes.tsx +++ b/packages/app-api/src/routing/FlatRoutes.tsx @@ -16,7 +16,7 @@ import React, { ReactNode, Children, isValidElement, Fragment } from 'react'; import { useRoutes } from 'react-router-dom'; -import { useApp } from '../app'; +import { useApp } from '@backstage/plugin-api'; type RouteObject = { path: string; diff --git a/packages/app-api/src/routing/RouteRef.test.ts b/packages/app-api/src/routing/RouteRef.test.ts deleted file mode 100644 index 279589a6c7..0000000000 --- a/packages/app-api/src/routing/RouteRef.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AnyParams, RouteRef } from './types'; -import { createRouteRef, isRouteRef } from './RouteRef'; -import { isSubRouteRef } from './SubRouteRef'; -import { isExternalRouteRef } from './ExternalRouteRef'; -import MyIcon from '@material-ui/icons/AcUnit'; - -describe('RouteRef', () => { - it('should be created', () => { - const routeRef: RouteRef = createRouteRef({ - id: 'my-route-ref', - }); - expect(routeRef.params).toEqual([]); - expect(String(routeRef)).toBe('routeRef{type=absolute,id=my-route-ref}'); - expect(isRouteRef(routeRef)).toBe(true); - expect(isSubRouteRef(routeRef)).toBe(false); - expect(isExternalRouteRef(routeRef)).toBe(false); - - expect(isRouteRef({} as RouteRef)).toBe(false); - }); - - it('should be created with params', () => { - const routeRef: RouteRef<{ - x: string; - y: string; - }> = createRouteRef({ - id: 'my-other-route-ref', - params: ['x', 'y'], - }); - expect(routeRef.params).toEqual(['x', 'y']); - }); - - it('should properly infer and validate parameter types and assignments', () => { - function validateType(_ref: RouteRef) {} - - const _1 = createRouteRef({ id: '1', params: ['x'] }); - // @ts-expect-error - validateType<{ y: string }>(_1); - // @ts-expect-error - validateType(_1); - validateType<{ x: string }>(_1); - - const _2 = createRouteRef({ id: '2', params: ['x', 'y'] }); - // @ts-expect-error - validateType<{ x: string }>(_2); - // @ts-expect-error - validateType(_2); - // @ts-expect-error - validateType<{ x: string; z: string }>(_2); - // TODO(Rugvip): Ideally this would fail as well, but settle for validating it at runtime instead - validateType<{ x: string; y: string; z: string }>(_2); - validateType<{ x: string; y: string }>(_2); - - const _3 = createRouteRef({ id: '3', params: [] }); - // @ts-expect-error - validateType<{ x: string }>(_3); - validateType(_3); - - const _4 = createRouteRef({ id: '4' }); - // @ts-expect-error - validateType<{ x: string }>(_4); - validateType(_4); - - // To avoid complains about missing expectations and unused vars - expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String)); - }); - - it('should support deprecated access', () => { - const routeRef = createRouteRef({ - title: 'My Ref', - path: '/my-path', - icon: MyIcon, - }); - expect(routeRef.title).toBe('My Ref'); - expect(routeRef.path).toBe('/my-path'); - expect(routeRef.icon).toBe(MyIcon); - expect(String(routeRef)).toBe('routeRef{type=absolute,id=My Ref}'); - }); -}); diff --git a/packages/app-api/src/routing/RouteRef.ts b/packages/app-api/src/routing/RouteRef.ts deleted file mode 100644 index 5d0dd9647f..0000000000 --- a/packages/app-api/src/routing/RouteRef.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - RouteRef, - SubRouteRef, - ExternalRouteRef, - routeRefType, - AnyParams, - ParamKeys, - OptionalParams, -} from './types'; -import { IconComponent } from '@backstage/plugin-api'; - -// TODO(Rugvip): Remove this in the next breaking release, it's exported but unused -export type RouteRefConfig = { - params?: ParamKeys; - path?: string; - icon?: IconComponent; - title: string; -}; - -export class RouteRefImpl - implements RouteRef { - readonly [routeRefType] = 'absolute'; - - constructor( - private readonly id: string, - readonly params: ParamKeys, - private readonly config: { - path?: string; - icon?: IconComponent; - title?: string; - }, - ) {} - - get icon() { - return this.config.icon; - } - - // TODO(Rugvip): Remove this, routes are looked up via the registry instead - get path() { - return this.config.path ?? ''; - } - - get title() { - return this.config.title ?? this.id; - } - - toString() { - return `routeRef{type=absolute,id=${this.id}}`; - } -} - -export function createRouteRef< - // Params is the type that we care about and the one to be embedded in the route ref. - // For example, given the params ['name', 'kind'], Params will be {name: string, kind: string} - Params extends { [param in ParamKey]: string }, - // ParamKey is here to make sure the Params type properly has its keys narrowed down - // to only the elements of params. Defaulting to never makes sure we end up with - // Param = {} if the params array is empty. - ParamKey extends string = never ->(config: { - /** The id of the route ref, used to identify it when printed */ - id?: string; - /** A list of parameter names that the path that this route ref is bound to must contain */ - params?: ParamKey[]; - /** @deprecated Route refs no longer decide their own path */ - path?: string; - /** @deprecated Route refs no longer decide their own icon */ - icon?: IconComponent; - /** @deprecated Route refs no longer decide their own title */ - title?: string; -}): RouteRef> { - const id = config.id || config.title; - if (!id) { - throw new Error('RouteRef must be provided a non-empty id'); - } - return new RouteRefImpl( - id, - (config.params ?? []) as ParamKeys>, - config, - ); -} - -export function isRouteRef( - routeRef: - | RouteRef - | SubRouteRef - | ExternalRouteRef, -): routeRef is RouteRef { - return routeRef[routeRefType] === 'absolute'; -} diff --git a/packages/app-api/src/routing/RouteResolver.test.ts b/packages/app-api/src/routing/RouteResolver.test.ts index e5394f6163..93daedac1e 100644 --- a/packages/app-api/src/routing/RouteResolver.test.ts +++ b/packages/app-api/src/routing/RouteResolver.test.ts @@ -14,11 +14,15 @@ * limitations under the License. */ -import { createRouteRef } from './RouteRef'; -import { createSubRouteRef } from './SubRouteRef'; -import { createExternalRouteRef } from './ExternalRouteRef'; +import { + createRouteRef, + createSubRouteRef, + createExternalRouteRef, + ExternalRouteRef, + RouteRef, + SubRouteRef, +} from '@backstage/plugin-api'; import { RouteResolver } from './RouteResolver'; -import { ExternalRouteRef, RouteRef, SubRouteRef } from './types'; const element = () => null; const rest = { element, caseSensitive: false }; diff --git a/packages/app-api/src/routing/RouteResolver.ts b/packages/app-api/src/routing/RouteResolver.ts index fb44b8c486..a5d266b2e4 100644 --- a/packages/app-api/src/routing/RouteResolver.ts +++ b/packages/app-api/src/routing/RouteResolver.ts @@ -18,16 +18,18 @@ import { generatePath, matchRoutes } from 'react-router-dom'; import { AnyRouteRef, BackstageRouteObject, + AnyParams, + RouteFunc, + routeRefType, +} from './types'; +import { RouteRef, ExternalRouteRef, - AnyParams, SubRouteRef, - routeRefType, - RouteFunc, -} from './types'; -import { isRouteRef } from './RouteRef'; -import { isSubRouteRef } from './SubRouteRef'; -import { isExternalRouteRef } from './ExternalRouteRef'; + isRouteRef, + isSubRouteRef, + isExternalRouteRef, +} from '@backstage/plugin-api'; // Joins a list of paths together, avoiding trailing and duplicate slashes function joinPaths(...paths: string[]): string { diff --git a/packages/app-api/src/routing/hooks.test.tsx b/packages/app-api/src/routing/RoutingProvider.test.tsx similarity index 93% rename from packages/app-api/src/routing/hooks.test.tsx rename to packages/app-api/src/routing/RoutingProvider.test.tsx index d16f9808ab..5a0d0026cf 100644 --- a/packages/app-api/src/routing/hooks.test.tsx +++ b/packages/app-api/src/routing/RoutingProvider.test.tsx @@ -31,38 +31,35 @@ import { routeElementDiscoverer, traverseElementTree, } from '../extensions/traversal'; -import { createPlugin } from '../plugin'; +import { + createPlugin, + useRouteRef, + createRouteRef, + createExternalRouteRef, + RouteRef, + ExternalRouteRef, +} from '@backstage/plugin-api'; +import { RoutingProvider } from './RoutingProvider'; import { routePathCollector, routeParentCollector, routeObjectCollector, } from './collectors'; import { validateRoutes } from './validation'; -import { useRouteRef, RoutingProvider } from './hooks'; -import { createRouteRef, RouteRefConfig } from './RouteRef'; import { RouteResolver } from './RouteResolver'; -import { createExternalRouteRef } from './ExternalRouteRef'; -import { AnyRouteRef, RouteFunc, RouteRef, ExternalRouteRef } from './types'; +import { AnyRouteRef, RouteFunc } from './types'; -const mockConfig = (extra?: Partial>) => ({ - path: '/unused', - title: 'Unused', - ...extra, -}); const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( <>{children} ); const plugin = createPlugin({ id: 'my-plugin' }); -const ref1 = createRouteRef(mockConfig({ path: '/wat1' })); -const ref2 = createRouteRef(mockConfig({ path: '/wat2' })); -const ref3 = createRouteRef(mockConfig({ path: '/wat3' })); -const ref4 = createRouteRef(mockConfig({ path: '/wat4' })); -const ref5 = createRouteRef({ - ...mockConfig({ path: '/wat5' }), - params: ['x'], -}); +const ref1 = createRouteRef({ id: 'ref1' }); +const ref2 = createRouteRef({ id: 'ref2' }); +const ref3 = createRouteRef({ id: 'ref3' }); +const ref4 = createRouteRef({ id: 'ref4' }); +const ref5 = createRouteRef({ id: 'ref5', params: ['x'] }); const eRefA = createExternalRouteRef({ id: '1' }); const eRefB = createExternalRouteRef({ id: '2' }); const eRefC = createExternalRouteRef({ id: '3', params: ['y'] }); diff --git a/packages/app-api/src/routing/hooks.tsx b/packages/app-api/src/routing/RoutingProvider.tsx similarity index 50% rename from packages/app-api/src/routing/hooks.tsx rename to packages/app-api/src/routing/RoutingProvider.tsx index 4c4a7a6a05..882bf91cb0 100644 --- a/packages/app-api/src/routing/hooks.tsx +++ b/packages/app-api/src/routing/RoutingProvider.tsx @@ -14,74 +14,21 @@ * limitations under the License. */ -import React, { - createContext, - ReactNode, - useContext, - useMemo, - Context, -} from 'react'; -import { useLocation } from 'react-router-dom'; +import React, { createContext, ReactNode } from 'react'; +import { ExternalRouteRef, RouteRef, SubRouteRef } from '@backstage/plugin-api'; +import { getOrCreateGlobalSingleton } from '../lib/globalObject'; import { - BackstageRouteObject, - RouteRef, - ExternalRouteRef, - AnyParams, - SubRouteRef, - RouteFunc, -} from './types'; -import { RouteResolver } from './RouteResolver'; -import { - VersionedValue, createVersionedValueMap, + VersionedValue, } from '../lib/versionedValues'; -import { - getGlobalSingleton, - getOrCreateGlobalSingleton, -} from '../lib/globalObject'; +import { RouteResolver } from './RouteResolver'; +import { BackstageRouteObject } from './types'; type RoutingContextType = VersionedValue<{ 1: RouteResolver }> | undefined; const RoutingContext = getOrCreateGlobalSingleton('routing-context', () => createContext(undefined), ); -export function useRouteRef( - routeRef: ExternalRouteRef, -): Optional extends true ? RouteFunc | undefined : RouteFunc; -export function useRouteRef( - routeRef: RouteRef | SubRouteRef, -): RouteFunc; -export function useRouteRef( - routeRef: - | RouteRef - | SubRouteRef - | ExternalRouteRef, -): RouteFunc | undefined { - const sourceLocation = useLocation(); - const versionedContext = useContext( - getGlobalSingleton>('routing-context'), - ); - const resolver = versionedContext?.atVersion(1); - const routeFunc = useMemo( - () => resolver && resolver.resolve(routeRef, sourceLocation), - [resolver, routeRef, sourceLocation], - ); - - if (!versionedContext) { - throw new Error('useRouteRef used outside of routing context'); - } - if (!resolver) { - throw new Error('RoutingContext v1 not available'); - } - - const isOptional = 'optional' in routeRef && routeRef.optional; - if (!routeFunc && !isOptional) { - throw new Error(`No path for ${routeRef}`); - } - - return routeFunc; -} - type ProviderProps = { routePaths: Map; routeParents: Map; diff --git a/packages/app-api/src/routing/SubRouteRef.test.ts b/packages/app-api/src/routing/SubRouteRef.test.ts deleted file mode 100644 index 1c1a3c1b21..0000000000 --- a/packages/app-api/src/routing/SubRouteRef.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AnyParams, SubRouteRef } from './types'; -import { createSubRouteRef, isSubRouteRef } from './SubRouteRef'; -import { createRouteRef, isRouteRef } from './RouteRef'; -import { isExternalRouteRef } from './ExternalRouteRef'; - -const parent = createRouteRef({ id: 'parent' }); -const parentX = createRouteRef({ id: 'parent-x', params: ['x'] }); - -describe('SubRouteRef', () => { - it('should be created', () => { - const routeRef: SubRouteRef = createSubRouteRef({ - parent, - id: 'my-route-ref', - path: '/foo', - }); - expect(routeRef.path).toBe('/foo'); - expect(routeRef.parent).toBe(parent); - expect(routeRef.params).toEqual([]); - expect(String(routeRef)).toBe('routeRef{type=sub,id=my-route-ref}'); - expect(isRouteRef(routeRef)).toBe(false); - expect(isSubRouteRef(routeRef)).toBe(true); - expect(isExternalRouteRef(routeRef)).toBe(false); - - expect(isRouteRef({} as SubRouteRef)).toBe(false); - }); - - it('should be created with params', () => { - const routeRef: SubRouteRef<{ bar: string }> = createSubRouteRef({ - parent, - id: 'my-other-route-ref', - path: '/foo/:bar', - }); - expect(routeRef.path).toBe('/foo/:bar'); - expect(routeRef.parent).toBe(parent); - expect(routeRef.params).toEqual(['bar']); - }); - - it('should be created with merged params', () => { - const routeRef: SubRouteRef<{ - x: string; - y: string; - z: string; - }> = createSubRouteRef({ - parent: parentX, - id: 'my-other-route-ref', - path: '/foo/:y/:z', - }); - expect(routeRef.path).toBe('/foo/:y/:z'); - expect(routeRef.parent).toBe(parentX); - expect(routeRef.params).toEqual(['x', 'y', 'z']); - }); - - it('should be created with params from parent', () => { - const routeRef: SubRouteRef<{ x: string }> = createSubRouteRef({ - parent: parentX, - id: 'my-other-route-ref', - path: '/foo/bar', - }); - expect(routeRef.path).toBe('/foo/bar'); - expect(routeRef.parent).toBe(parentX); - expect(routeRef.params).toEqual(['x']); - }); - - it.each([ - ['foo', "SubRouteRef path must start with '/', got 'foo'"], - [':foo', "SubRouteRef path must start with '/', got ':foo'"], - ['', "SubRouteRef path must start with '/', got ''"], - ['/', "SubRouteRef path must not end with '/', got '/'"], - ['/foo/', "SubRouteRef path must not end with '/', got '/foo/'"], - ['/foo/:x', 'SubRouteRef may not have params that overlap with its parent'], - ['/:/foo', "SubRouteRef path has invalid param, got ''"], - ['/:inva:lid/foo', "SubRouteRef path has invalid param, got 'inva:lid'"], - ['/:inva=lid/foo', "SubRouteRef path has invalid param, got 'inva=lid'"], - ])('should throw if path is invalid, %s', (path, message) => { - expect(() => - createSubRouteRef({ path, parent: parentX, id: path }), - ).toThrow(message); - }); - - it('should properly infer and parse path parameters', () => { - function validateType(_ref: SubRouteRef) {} - - const _1 = createSubRouteRef({ id: '1', parent, path: '/foo/bar' }); - // @ts-expect-error - validateType<{ x: string }>(_1); - validateType(_1); - - const _2 = createSubRouteRef({ id: '2', parent, path: '/foo/:x/:y' }); - // @ts-expect-error - validateType(_2); - // @ts-expect-error - validateType<{ x: string; z: string }>(_2); - // @ts-expect-error - validateType<{ y: string }>(_2); - // TODO(Rugvip): Ideally this would fail as well, but settle for validating it at runtime instead - validateType<{ x: string; y: string; z: string }>(_2); - validateType<{ x: string; y: string }>(_2); - - const _3 = createSubRouteRef({ id: '3', parent: parentX, path: '/foo' }); - // @ts-expect-error - validateType(_3); - // @ts-expect-error - validateType<{ y: string }>(_3); - validateType<{ x: string }>(_3); - - const _4 = createSubRouteRef({ id: '4', parent: parentX, path: '/foo/:y' }); - // @ts-expect-error - validateType(_4); - // @ts-expect-error - validateType<{ x: string; z: string }>(_4); - // @ts-expect-error - validateType<{ y: string }>(_4); - validateType<{ x: string; y: string }>(_4); - - // To avoid complains about missing expectations and unused vars - expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String)); - }); -}); diff --git a/packages/app-api/src/routing/SubRouteRef.ts b/packages/app-api/src/routing/SubRouteRef.ts deleted file mode 100644 index 7ddfc89c80..0000000000 --- a/packages/app-api/src/routing/SubRouteRef.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - AnyParams, - ExternalRouteRef, - OptionalParams, - ParamKeys, - RouteRef, - routeRefType, - SubRouteRef, -} from './types'; - -// Should match the pattern in react-router -const PARAM_PATTERN = /^\w+$/; - -export class SubRouteRefImpl - implements SubRouteRef { - readonly [routeRefType] = 'sub'; - - constructor( - private readonly id: string, - readonly path: string, - readonly parent: RouteRef, - readonly params: ParamKeys, - ) {} - - toString() { - return `routeRef{type=sub,id=${this.id}}`; - } -} - -// These utility types help us infer a Param object type from a string path -// For example, `/foo/:bar/:baz` inferred to `{ bar: string, baz: string }` -type ParamPart = S extends `:${infer Param}` ? Param : never; -type ParamNames = S extends `${infer Part}/${infer Rest}` - ? ParamPart | ParamNames - : ParamPart; -type PathParams = { [name in ParamNames]: string }; - -/** - * Merges a param object type with with an optional params type into a params object - */ -type MergeParams< - P1 extends { [param in string]: string }, - P2 extends AnyParams -> = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2); - -/** - * Creates a SubRouteRef type given the desired parameters and parent route parameters. - * The parameters types are merged together while ensuring that there is no overlap between the two. - */ -type MakeSubRouteRef< - Params extends { [param in string]: string }, - ParentParams extends AnyParams -> = keyof Params & keyof ParentParams extends never - ? SubRouteRef>> - : never; - -export function createSubRouteRef< - Path extends string, - ParentParams extends AnyParams = never ->(config: { - id: string; - path: Path; - parent: RouteRef; -}): MakeSubRouteRef, ParentParams> { - const { id, path, parent } = config; - type Params = PathParams; - - // Collect runtime parameters from the path, e.g. ['bar', 'baz'] from '/foo/:bar/:baz' - const pathParams = path - .split('/') - .filter(p => p.startsWith(':')) - .map(p => p.substring(1)); - const params = [...parent.params, ...pathParams]; - - if (parent.params.some(p => pathParams.includes(p as string))) { - throw new Error( - 'SubRouteRef may not have params that overlap with its parent', - ); - } - if (!path.startsWith('/')) { - throw new Error(`SubRouteRef path must start with '/', got '${path}'`); - } - if (path.endsWith('/')) { - throw new Error(`SubRouteRef path must not end with '/', got '${path}'`); - } - for (const param of pathParams) { - if (!PARAM_PATTERN.test(param)) { - throw new Error(`SubRouteRef path has invalid param, got '${param}'`); - } - } - - // We ensure that the type of the return type is sane here - const subRouteRef = new SubRouteRefImpl( - id, - path, - parent, - params as ParamKeys>, - ) as SubRouteRef>>; - - // But skip type checking of the return value itself, because the conditional - // type checking of the parent parameter overlap is tricky to express. - return subRouteRef as any; -} - -export function isSubRouteRef( - routeRef: - | RouteRef - | SubRouteRef - | ExternalRouteRef, -): routeRef is SubRouteRef { - return routeRef[routeRefType] === 'sub'; -} diff --git a/packages/app-api/src/routing/collectors.test.tsx b/packages/app-api/src/routing/collectors.test.tsx index 44d53856b4..041e109eda 100644 --- a/packages/app-api/src/routing/collectors.test.tsx +++ b/packages/app-api/src/routing/collectors.test.tsx @@ -26,11 +26,9 @@ import { childDiscoverer, routeElementDiscoverer, } from '../extensions/traversal'; -import { createRouteRef } from './RouteRef'; -import { createPlugin } from '../plugin'; +import { createRouteRef, createPlugin, RouteRef } from '@backstage/plugin-api'; import { attachComponentData, createRoutableExtension } from '../extensions'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; -import { RouteRef } from './types'; const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( <>{children} @@ -38,11 +36,11 @@ const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( const plugin = createPlugin({ id: 'my-plugin' }); -const ref1 = createRouteRef({ path: '/foo1', title: 'Foo' }); -const ref2 = createRouteRef({ path: '/foo2', title: 'Foo' }); -const ref3 = createRouteRef({ path: '/foo3', title: 'Foo' }); -const ref4 = createRouteRef({ path: '/foo4', title: 'Foo' }); -const ref5 = createRouteRef({ path: '/foo5', title: 'Foo' }); +const ref1 = createRouteRef({ id: 'ref1' }); +const ref2 = createRouteRef({ id: 'ref2' }); +const ref3 = createRouteRef({ id: 'ref3' }); +const ref4 = createRouteRef({ id: 'ref4' }); +const ref5 = createRouteRef({ id: 'ref5' }); const refOrder = [ref1, ref2, ref3, ref4, ref5]; const Extension1 = plugin.provide( diff --git a/packages/app-api/src/routing/collectors.tsx b/packages/app-api/src/routing/collectors.tsx index 37362cf8d4..d681ff2906 100644 --- a/packages/app-api/src/routing/collectors.tsx +++ b/packages/app-api/src/routing/collectors.tsx @@ -15,7 +15,8 @@ */ import { isValidElement, ReactElement, ReactNode } from 'react'; -import { BackstageRouteObject, RouteRef } from '../routing/types'; +import { RouteRef } from '@backstage/plugin-api'; +import { BackstageRouteObject } from '../routing/types'; import { getComponentData } from '../extensions'; import { createCollector } from '../extensions/traversal'; diff --git a/packages/app-api/src/routing/index.ts b/packages/app-api/src/routing/index.ts index c6134cfe4f..7982333f4b 100644 --- a/packages/app-api/src/routing/index.ts +++ b/packages/app-api/src/routing/index.ts @@ -14,16 +14,4 @@ * limitations under the License. */ -export type { - RouteRef, - AbsoluteRouteRef, - ConcreteRoute, - MutableRouteRef, - ExternalRouteRef, -} from './types'; export { FlatRoutes } from './FlatRoutes'; -export { createRouteRef } from './RouteRef'; -export { createSubRouteRef } from './SubRouteRef'; -export { createExternalRouteRef } from './ExternalRouteRef'; -export type { RouteRefConfig } from './RouteRef'; -export { useRouteRef } from './hooks'; diff --git a/packages/app-api/src/routing/types.ts b/packages/app-api/src/routing/types.ts index cf6f572061..d379f43324 100644 --- a/packages/app-api/src/routing/types.ts +++ b/packages/app-api/src/routing/types.ts @@ -14,16 +14,20 @@ * limitations under the License. */ -import { IconComponent } from '@backstage/plugin-api'; +import { RouteRef, SubRouteRef, ExternalRouteRef } from '@backstage/plugin-api'; import { getOrCreateGlobalSingleton } from '../lib/globalObject'; +export const routeRefType: unique symbol = getOrCreateGlobalSingleton( + 'route-ref-type', + () => Symbol('route-ref-type'), +); + export type AnyParams = { [param in string]: string } | undefined; -export type ParamKeys = keyof Params extends never - ? [] - : (keyof Params)[]; -export type OptionalParams< - Params extends { [param in string]: string } -> = Params[keyof Params] extends never ? undefined : Params; + +export type AnyRouteRef = + | RouteRef + | SubRouteRef + | ExternalRouteRef; // The extra TS magic here is to require a single params argument if the RouteRef // had at least one param defined, but require 0 arguments if there are no params defined. @@ -34,59 +38,6 @@ export type RouteFunc = ( ...[params]: Params extends undefined ? readonly [] : readonly [Params] ) => string; -export const routeRefType: unique symbol = getOrCreateGlobalSingleton( - 'route-ref-type', - () => Symbol('route-ref-type'), -); - -export type RouteRef = { - readonly [routeRefType]: 'absolute'; - - params: ParamKeys; - - // TODO(Rugvip): Remove all of these once plugins don't rely on the path - /** @deprecated paths are no longer accessed directly from RouteRefs, use useRouteRef instead */ - path: string; - /** @deprecated icons are no longer accessed via RouteRefs */ - icon?: IconComponent; - /** @deprecated titles are no longer accessed via RouteRefs */ - title?: string; -}; - -export type SubRouteRef = { - readonly [routeRefType]: 'sub'; - - parent: RouteRef; - - path: string; - - params: ParamKeys; -}; - -export type ExternalRouteRef< - Params extends AnyParams = any, - Optional extends boolean = any -> = { - readonly [routeRefType]: 'external'; - - params: ParamKeys; - - optional?: Optional; -}; - -export type AnyRouteRef = - | RouteRef - | SubRouteRef - | ExternalRouteRef; - -// TODO(Rugvip): None of these should be found in the wild anymore, remove in next minor release -/** @deprecated */ -export type ConcreteRoute = {}; -/** @deprecated */ -export type AbsoluteRouteRef = RouteRef<{}>; -/** @deprecated */ -export type MutableRouteRef = RouteRef<{}>; - // A duplicate of the react-router RouteObject, but with routeRef added export interface BackstageRouteObject { caseSensitive: boolean;