From 9b737e5f2ee7974d5ca8ebf80bb9fd41f2bb2e60 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 17 Oct 2022 13:27:17 +0200 Subject: [PATCH] core-app-api: use new basename router prop Signed-off-by: Patrik Oldsberg --- .changeset/brave-eels-allow.md | 5 + .../src/app/AppManager.compat.test.tsx | 107 ++++++++++++++++ .../src/app/AppManager.stable.test.tsx | 118 ++++++++++++++++++ packages/core-app-api/src/app/AppManager.tsx | 51 ++++++-- packages/core-app-api/src/app/types.ts | 2 +- 5 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 .changeset/brave-eels-allow.md create mode 100644 packages/core-app-api/src/app/AppManager.compat.test.tsx create mode 100644 packages/core-app-api/src/app/AppManager.stable.test.tsx diff --git a/.changeset/brave-eels-allow.md b/.changeset/brave-eels-allow.md new file mode 100644 index 0000000000..8fbe7ba8d2 --- /dev/null +++ b/.changeset/brave-eels-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +Updated the React Router wiring to make use of the new `basename` property of the router components in React Router v6 stable. To implement this, a new optional `basename` property has been added to the `Router` app component, which can be forwarded to the concrete router implementation in order to support this new behavior. This is done by default in any app that does not have a `Router` component override. diff --git a/packages/core-app-api/src/app/AppManager.compat.test.tsx b/packages/core-app-api/src/app/AppManager.compat.test.tsx new file mode 100644 index 0000000000..bdebd79dc4 --- /dev/null +++ b/packages/core-app-api/src/app/AppManager.compat.test.tsx @@ -0,0 +1,107 @@ +/* + * 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 tlr, { render } from '@testing-library/react'; +import React from 'react'; + +describe.each(['beta', 'stable'])('react-router %s', rrVersion => { + beforeAll(() => { + jest.doMock('react', () => React); + // This has some side effects, so need this to be stable to avoid re-require + jest.doMock('@testing-library/react', () => tlr); + jest.doMock('react-router', () => + rrVersion === 'beta' + ? jest.requireActual('react-router-beta') + : jest.requireActual('react-router-stable'), + ); + jest.doMock('react-router-dom', () => + rrVersion === 'beta' + ? jest.requireActual('react-router-dom-beta') + : jest.requireActual('react-router-dom-stable'), + ); + }); + + afterAll(() => { + jest.resetModules(); + }); + + function requireDeps() { + return { + ...(require('./AppManager') as typeof import('./AppManager')), + ...(require('../routing') as typeof import('../routing')), + ...(require('react-router-dom') as typeof import('react-router-dom')), + ...(require('@backstage/test-utils') as typeof import('@backstage/test-utils')), + }; + } + + describe('AppManager', () => { + it('supports base path', async () => { + const { AppManager, MemoryRouter, Navigate, Route, FlatRoutes } = + requireDeps(); + const app = new AppManager({ + apis: [], + defaultApis: [], + themes: [ + { + id: 'light', + title: 'Light Theme', + variant: 'light', + Provider: ({ children }) => <>{children}, + }, + ], + icons: {} as any, + plugins: [], + components: { + NotFoundErrorPage: () => null, + BootErrorPage: () => null, + Progress: () => null, + Router: ({ children, basename }) => ( + + ), + ErrorBoundaryFallback: () => null, + ThemeProvider: ({ children }) => <>{children}, + }, + configLoader: async () => [ + { + context: 'test', + data: { app: { baseUrl: 'http://localhost/foo' } }, + }, + ], + bindRoutes: () => {}, + }); + + const AppProvider = app.getProvider(); + const AppRouter = app.getRouter(); + + const rendered = render( + + + + } /> + bar} /> + + + , + ); + + await expect(rendered.findByText('bar')).resolves.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/core-app-api/src/app/AppManager.stable.test.tsx b/packages/core-app-api/src/app/AppManager.stable.test.tsx new file mode 100644 index 0000000000..a9948a6070 --- /dev/null +++ b/packages/core-app-api/src/app/AppManager.stable.test.tsx @@ -0,0 +1,118 @@ +/* + * 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 { render } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter, Navigate, Route } from 'react-router-dom'; +import { FlatRoutes } from '../routing'; +import { AppManager } from './AppManager'; +import { AppOptions } from './types'; + +jest.mock('react-router', () => jest.requireActual('react-router-stable')); +jest.mock('react-router-dom', () => + jest.requireActual('react-router-dom-stable'), +); + +const mockAppOptions: AppOptions = { + apis: [], + defaultApis: [], + themes: [ + { + id: 'light', + title: 'Light Theme', + variant: 'light', + Provider: ({ children }) => <>{children}, + }, + ], + icons: {} as any, + plugins: [], + components: { + NotFoundErrorPage: () => null, + BootErrorPage: () => null, + Progress: () => null, + Router: props => , + ErrorBoundaryFallback: () => null, + ThemeProvider: ({ children }) => <>{children}, + }, + configLoader: async () => [], + bindRoutes: () => {}, +}; + +describe('AppManager', () => { + it('supports base path', async () => { + const app = new AppManager({ + ...mockAppOptions, + components: { + ...mockAppOptions.components, + Router: props => , + }, + configLoader: async () => [ + { + context: 'test', + data: { app: { baseUrl: 'http://localhost/foo' } }, + }, + ], + }); + + const AppProvider = app.getProvider(); + const AppRouter = app.getRouter(); + + const rendered = render( + + + + } /> + bar} /> + + + , + ); + + await expect(rendered.findByText('bar')).resolves.toBeInTheDocument(); + }); + + it('supports base path with absolute navigation', async () => { + const app = new AppManager({ + ...mockAppOptions, + components: { + ...mockAppOptions.components, + Router: props => , + }, + configLoader: async () => [ + { + context: 'test', + data: { app: { baseUrl: 'http://localhost/foo' } }, + }, + ], + }); + + const AppProvider = app.getProvider(); + const AppRouter = app.getRouter(); + + const rendered = render( + + + + } /> + bar} /> + + + , + ); + + await expect(rendered.findByText('bar')).resolves.toBeInTheDocument(); + }); +}); diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 89402476e3..0ca09c0a0c 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -99,6 +99,21 @@ const InternalAppContext = createContext<{ * The returned path does not have a trailing slash. */ function getBasePath(configApi: Config) { + if (!isReactRouterBeta()) { + // When using rr v6 stable the base path is handled through the + // basename prop on the router component instead. + return ''; + } + + return readBasePath(configApi); +} + +/** + * Read the configured base path. + * + * The returned path does not have a trailing slash. + */ +function readBasePath(configApi: ConfigApi) { let { pathname } = new URL( configApi.getOptionalString('app.baseUrl') ?? '/', 'http://dummy.dev', // baseUrl can be specified as just a path @@ -361,7 +376,7 @@ export class AppManager implements BackstageApp { const AppRouter = ({ children }: PropsWithChildren<{}>) => { const configApi = useApi(configApiRef); - const basePath = getBasePath(configApi); + const basePath = readBasePath(configApi); const mountPath = `${basePath}/*`; const { routeObjects } = useContext(InternalAppContext); @@ -390,23 +405,43 @@ export class AppManager implements BackstageApp { { signOutTargetUrl: basePath || '/' }, ); + if (isReactRouterBeta()) { + return ( + + + + {children}} /> + + + ); + } + + return ( + + + {children} + + ); + } + + if (isReactRouterBeta()) { return ( - - {children}} /> - + + + {children}} /> + + ); } return ( - + - - {children}} /> - + <>{children} ); diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 39bf9c0554..70022534a1 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -69,7 +69,7 @@ export type AppComponents = { NotFoundErrorPage: ComponentType<{}>; BootErrorPage: ComponentType; Progress: ComponentType<{}>; - Router: ComponentType<{}>; + Router: ComponentType<{ basename?: string }>; ErrorBoundaryFallback: ComponentType; ThemeProvider?: ComponentType<{}>;