From 8a2a8acad21b1c3c6de492f54f86ca7db9285b64 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 30 Oct 2026 12:03:43 +0100 Subject: [PATCH 1/2] break(core): upgrade to React Router v8 (AI-assisted) Co-Authored-By: Claude Opus 5.5 --- knip.config.ts | 4 - .../package.json | 6 +- .../src/index.d.ts | 44 ++-- .../package.json | 4 +- .../src/client/__tests__/docsUtils.test.tsx | 2 +- .../src/client/docsClientUtils.ts | 20 +- .../src/client/docsUtils.tsx | 7 +- .../src/plugin-content-docs.d.ts | 21 +- .../src/theme/DebugLayout/index.tsx | 4 +- .../src/theme/DebugLayout/styles.module.css | 4 + .../docusaurus-theme-classic/package.json | 3 +- .../src/theme/DocVersionRoot/index.tsx | 12 +- .../src/theme/DocsRoot/index.tsx | 12 +- .../src/theme/Tabs/__tests__/index.test.tsx | 2 +- packages/docusaurus-theme-common/package.json | 5 +- .../src/contexts/navbarMobileSidebar.tsx | 41 +-- .../src/utils/__tests__/routesUtils.test.ts | 23 +- .../__tests__/useAlternatePageUtils.test.tsx | 2 +- .../utils/__tests__/useLocalPathname.test.tsx | 2 +- .../src/utils/historyUtils.ts | 98 ++------ .../src/utils/routesUtils.ts | 3 +- .../src/utils/skipToContentUtils.tsx | 12 +- .../src/utils/tabsUtils.tsx | 14 +- .../src/utils/useLocationChange.ts | 3 +- .../src/theme/SearchBar/index.tsx | 6 +- packages/docusaurus-types/package.json | 2 +- .../docusaurus-types/src/clientModule.d.ts | 2 +- packages/docusaurus-types/src/routing.d.ts | 8 +- .../src/__tests__/routeUtils.test.ts | 30 +++ packages/docusaurus-utils-common/src/index.ts | 1 + .../docusaurus-utils-common/src/routeUtils.ts | 25 ++ packages/docusaurus/package.json | 5 - packages/docusaurus/src/client/App.tsx | 10 +- .../src/client/ClientLifecyclesDispatcher.tsx | 2 +- .../src/client/PendingNavigation.tsx | 13 +- .../__tests__/normalizeLocation.test.ts | 7 +- .../src/client/__tests__/routeObjects.test.ts | 57 +++++ .../docusaurus/src/client/clientEntry.tsx | 7 +- packages/docusaurus/src/client/docusaurus.ts | 9 +- .../docusaurus/src/client/exports/Link.tsx | 160 +++++++----- .../client/exports/__tests__/Link.test.tsx | 4 +- .../src/client/exports/renderRoutes.ts | 8 - .../docusaurus/src/client/exports/router.ts | 8 +- .../src/client/normalizeLocation.ts | 10 +- packages/docusaurus/src/client/preload.ts | 11 +- .../docusaurus/src/client/routeObjects.tsx | 28 +++ .../docusaurus/src/client/serverEntry.tsx | 5 +- .../src/server/__tests__/brokenLinks.test.ts | 90 +------ packages/docusaurus/src/server/brokenLinks.ts | 31 ++- .../src/server/codegen/codegenRoutes.ts | 2 +- .../__tests__/__snapshots__/base.test.ts.snap | 1 - .../__snapshots__/index.test.ts.snap | 1 - pnpm-lock.yaml | 236 ++++-------------- pnpm-workspace.yaml | 7 +- .../_pages tests/history-tests.tsx | 32 --- website/_dogfooding/_pages tests/index.mdx | 1 - website/_dogfooding/clientModuleExample.ts | 2 +- website/docs/advanced/routing.mdx | 2 +- .../api/plugin-methods/lifecycle-apis.mdx | 8 +- website/docs/docusaurus-core.mdx | 10 +- website/package.json | 1 - website/src/components/APITable/index.tsx | 11 +- 62 files changed, 515 insertions(+), 686 deletions(-) create mode 100644 packages/docusaurus-utils-common/src/__tests__/routeUtils.test.ts create mode 100644 packages/docusaurus-utils-common/src/routeUtils.ts create mode 100644 packages/docusaurus/src/client/__tests__/routeObjects.test.ts delete mode 100644 packages/docusaurus/src/client/exports/renderRoutes.ts create mode 100644 packages/docusaurus/src/client/routeObjects.tsx delete mode 100644 website/_dogfooding/_pages tests/history-tests.tsx diff --git a/knip.config.ts b/knip.config.ts index 9a6e3bdeca0c..7d2ab126406f 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -139,10 +139,6 @@ const config: KnipConfig = ({ // Knip doesn't detect imports in "declare module" blocks ignoreDependencies: ['utility-types'], }, - 'packages/docusaurus-plugin-content-docs': { - // Knip doesn't detect imports in "declare module" blocks - ignoreDependencies: ['@types/react-router-config'], - }, 'packages/docusaurus-plugin-debug': themePackageConfig(), // Client modules are resolved through getClientModules() 'packages/docusaurus-plugin-google-gtag': { diff --git a/packages/docusaurus-module-type-aliases/package.json b/packages/docusaurus-module-type-aliases/package.json index 8a690c0661ef..2a968436ecc8 100644 --- a/packages/docusaurus-module-type-aliases/package.json +++ b/packages/docusaurus-module-type-aliases/package.json @@ -13,13 +13,11 @@ }, "dependencies": { "@docusaurus/types": "workspace:*", - "@types/history": "catalog:", "@types/react": "19.3.0", "@types/react-dom": "catalog:", - "@types/react-router-config": "catalog:", - "@types/react-router-dom": "catalog:", "react-helmet-async": "catalog:", - "react-loadable": "catalog:" + "react-loadable": "catalog:", + "react-router": "catalog:" }, "peerDependencies": { "react": "catalog:", diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index ff62e02e2832..59589c62a268 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -41,12 +41,19 @@ declare module '@generated/registry' { } declare module '@generated/routes' { - import type {RouteConfig as RRRouteConfig} from 'react-router-config'; - import type Loadable from 'react-loadable'; + import type {ComponentType} from 'react'; + import type {LoadableComponent} from 'react-loadable'; - type RouteConfig = RRRouteConfig & { + /** + * The client-side route config, passed to the route component as a `route` + * prop. + */ + export type RouteConfig = { path: string; - component: ReturnType; + component: ComponentType<{route: RouteConfig}> & LoadableComponent; + exact?: boolean; + routes?: RouteConfig[]; + [attributeName: string]: unknown; }; const routes: RouteConfig[]; export default routes; @@ -169,14 +176,22 @@ declare module '@docusaurus/Head' { declare module '@docusaurus/Link' { import type {CSSProperties, ComponentProps, ReactNode} from 'react'; - import type {NavLinkProps as RRNavLinkProps} from 'react-router-dom'; + import type {LinkProps, Location, PathMatch} from 'react-router'; - type NavLinkProps = Partial; - export type Props = NavLinkProps & + export type Props = Partial & ComponentProps<'a'> & { readonly className?: string; readonly style?: CSSProperties; readonly isNavLink?: boolean; + /** NavLink only: active only when the location matches exactly. */ + readonly exact?: boolean; + /** NavLink only: class name added when the link is active. */ + readonly activeClassName?: string; + /** NavLink only: overrides the default active state computation. */ + readonly isActive?: ( + match: PathMatch | null, + location: Location, + ) => boolean; readonly to?: string; readonly href?: string; readonly autoAddBaseUrl?: boolean; @@ -260,7 +275,14 @@ declare module '@docusaurus/Translate' { } declare module '@docusaurus/router' { - export {useHistory, useLocation, Redirect, matchPath} from 'react-router-dom'; + export { + useLocation, + useNavigate, + Navigate, + Outlet, + matchPath, + type Location, + } from 'react-router'; } // TODO Docusaurus v5: remove this deprecated hook @@ -355,12 +377,6 @@ declare module '@docusaurus/Noop' { export default function (): null; } -declare module '@docusaurus/renderRoutes' { - import {renderRoutes} from 'react-router-config'; - - export default renderRoutes; -} - declare module '@docusaurus/useGlobalData' { import type {GlobalData, UseDataOptions} from '@docusaurus/types'; diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json index e5e3a78413dc..57171224835b 100644 --- a/packages/docusaurus-plugin-content-docs/package.json +++ b/packages/docusaurus-plugin-content-docs/package.json @@ -46,7 +46,6 @@ "@docusaurus/utils": "workspace:*", "@docusaurus/utils-common": "workspace:*", "@docusaurus/utils-validation": "workspace:*", - "@types/react-router-config": "catalog:", "combine-promises": "catalog:", "js-yaml": "catalog:", "lodash": "catalog:", @@ -58,10 +57,9 @@ "devDependencies": { "@total-typescript/shoehorn": "catalog:dev", "@types/picomatch": "catalog:dev", - "@types/react-router-dom": "catalog:", "commander": "catalog:", "picomatch": "catalog:dev", - "react-router-dom": "catalog:" + "react-router": "catalog:" }, "peerDependencies": { "react": "catalog:", diff --git a/packages/docusaurus-plugin-content-docs/src/client/__tests__/docsUtils.test.tsx b/packages/docusaurus-plugin-content-docs/src/client/__tests__/docsUtils.test.tsx index 62d3b8678a24..495414d894b9 100644 --- a/packages/docusaurus-plugin-content-docs/src/client/__tests__/docsUtils.test.tsx +++ b/packages/docusaurus-plugin-content-docs/src/client/__tests__/docsUtils.test.tsx @@ -7,7 +7,7 @@ // @vitest-environment jsdom import {describe, expect, it} from 'vitest'; import {renderHook} from '@testing-library/react'; -import {StaticRouter} from 'react-router-dom'; +import {StaticRouter} from 'react-router'; import {Context} from '@docusaurus/core/src/client/docusaurusContext'; import {RouteContextProvider} from '@docusaurus/core/src/client/routeContext'; import { diff --git a/packages/docusaurus-plugin-content-docs/src/client/docsClientUtils.ts b/packages/docusaurus-plugin-content-docs/src/client/docsClientUtils.ts index 29bb9c0e0622..73e0ca5f9faa 100644 --- a/packages/docusaurus-plugin-content-docs/src/client/docsClientUtils.ts +++ b/packages/docusaurus-plugin-content-docs/src/client/docsClientUtils.ts @@ -32,11 +32,7 @@ export function getActivePlugin( .sort((a, b) => b[1].path.localeCompare(a[1].path)) .find( ([, pluginData]) => - !!matchPath(pathname, { - path: pluginData.path, - exact: false, - strict: false, - }), + !!matchPath({path: pluginData.path, end: false}, pathname), ); const activePlugin: ActivePlugin | undefined = activeEntry @@ -80,12 +76,7 @@ export function getActiveVersion( }); return sortedVersions.find( - (version) => - !!matchPath(pathname, { - path: version.path, - exact: false, - strict: false, - }), + (version) => !!matchPath({path: version.path, end: false}, pathname), ); } @@ -95,12 +86,7 @@ export function getActiveDocContext( ): ActiveDocContext { const activeVersion = getActiveVersion(data, pathname); const activeDoc = activeVersion?.docs.find( - (doc) => - !!matchPath(pathname, { - path: doc.path, - exact: true, - strict: false, - }), + (doc) => !!matchPath({path: doc.path, end: true}, pathname), ); function getAlternateVersionDocs( diff --git a/packages/docusaurus-plugin-content-docs/src/client/docsUtils.tsx b/packages/docusaurus-plugin-content-docs/src/client/docsUtils.tsx index 474a58895ade..cf665001257d 100644 --- a/packages/docusaurus-plugin-content-docs/src/client/docsUtils.tsx +++ b/packages/docusaurus-plugin-content-docs/src/client/docsUtils.tsx @@ -6,8 +6,7 @@ */ import {type ReactNode, useMemo} from 'react'; -import {matchPath, useLocation} from '@docusaurus/router'; -import renderRoutes from '@docusaurus/renderRoutes'; +import {matchPath, useLocation, Outlet} from '@docusaurus/router'; import { useActivePlugin, useActiveDocContext, @@ -403,7 +402,7 @@ export function useDocRootMetadata({route}: DocRootProps): null | { const versionMetadata = useDocsVersion(); const docRoutes = route.routes!; const currentDocRoute = docRoutes.find((docRoute) => - matchPath(location.pathname, docRoute), + matchPath(docRoute.path, location.pathname), ); if (!currentDocRoute) { return null; @@ -416,7 +415,7 @@ export function useDocRootMetadata({route}: DocRootProps): null | { ? versionMetadata.docsSidebars[sidebarName] : undefined; - const docElement = renderRoutes(docRoutes); + const docElement = ; return { docElement, diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index 766a7469ab4a..bac3d278f1c3 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -634,30 +634,33 @@ declare module '@theme/DocBreadcrumbs' { declare module '@theme/DocsRoot' { import type {ReactNode} from 'react'; - import type {RouteConfigComponentProps} from 'react-router-config'; - import type {Required} from 'utility-types'; + import type {RouteConfig} from '@generated/routes'; - export interface Props extends Required {} + export interface Props { + readonly route: RouteConfig; + } export default function DocsRoot(props: Props): ReactNode; } declare module '@theme/DocVersionRoot' { import type {ReactNode} from 'react'; - import type {RouteConfigComponentProps} from 'react-router-config'; - import type {Required} from 'utility-types'; + import type {RouteConfig} from '@generated/routes'; - export interface Props extends Required {} + export interface Props { + readonly route: RouteConfig; + } export default function DocVersionRoot(props: Props): ReactNode; } declare module '@theme/DocRoot' { import type {ReactNode} from 'react'; - import type {RouteConfigComponentProps} from 'react-router-config'; - import type {Required} from 'utility-types'; + import type {RouteConfig} from '@generated/routes'; - export interface Props extends Required {} + export interface Props { + readonly route: RouteConfig; + } export default function DocRoot(props: Props): ReactNode; } diff --git a/packages/docusaurus-plugin-debug/src/theme/DebugLayout/index.tsx b/packages/docusaurus-plugin-debug/src/theme/DebugLayout/index.tsx index e1057c7b9b56..55258aa41f35 100644 --- a/packages/docusaurus-plugin-debug/src/theme/DebugLayout/index.tsx +++ b/packages/docusaurus-plugin-debug/src/theme/DebugLayout/index.tsx @@ -17,9 +17,7 @@ function DebugNavLink({to, children}: {to: string; children: ReactNode}) { isNavLink to={to} exact - activeStyle={{ - backgroundColor: '#363739', - }}> + activeClassName={styles.navlinkActive}> {children} ); diff --git a/packages/docusaurus-plugin-debug/src/theme/DebugLayout/styles.module.css b/packages/docusaurus-plugin-debug/src/theme/DebugLayout/styles.module.css index 6a64a24df87e..00787e98f771 100644 --- a/packages/docusaurus-plugin-debug/src/theme/DebugLayout/styles.module.css +++ b/packages/docusaurus-plugin-debug/src/theme/DebugLayout/styles.module.css @@ -39,6 +39,10 @@ padding: 6px; } +.navlinkActive { + background-color: #363739; +} + .navlink:hover { text-decoration: none; background-color: #292a2b; diff --git a/packages/docusaurus-theme-classic/package.json b/packages/docusaurus-theme-classic/package.json index aeb0ed945a9a..d10018b77980 100644 --- a/packages/docusaurus-theme-classic/package.json +++ b/packages/docusaurus-theme-classic/package.json @@ -48,9 +48,8 @@ "devDependencies": { "@types/nprogress": "catalog:dev", "@types/prismjs": "catalog:dev", - "@types/react-router-dom": "catalog:", "@types/rtlcss": "catalog:dev", - "react-router-dom": "catalog:" + "react-router": "catalog:" }, "peerDependencies": { "react": "catalog:", diff --git a/packages/docusaurus-theme-classic/src/theme/DocVersionRoot/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocVersionRoot/index.tsx index 433e51b0694a..843090c52c76 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocVersionRoot/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocVersionRoot/index.tsx @@ -11,11 +11,9 @@ import { getDocsVersionSearchTag, useDocsVersion, } from '@docusaurus/plugin-content-docs/client'; -import renderRoutes from '@docusaurus/renderRoutes'; +import {Outlet} from '@docusaurus/router'; import SearchMetadata from '@theme/SearchMetadata'; -import type {Props} from '@theme/DocVersionRoot'; - function DocVersionRootMetadata(): ReactNode { const version = useDocsVersion(); return ( @@ -31,19 +29,19 @@ function DocVersionRootMetadata(): ReactNode { ); } -function DocVersionRootContent(props: Props): ReactNode { +function DocVersionRootContent(): ReactNode { const version = useDocsVersion(); return ( - {renderRoutes(props.route.routes!)} + ); } -export default function DocVersionRoot(props: Props): ReactNode { +export default function DocVersionRoot(): ReactNode { return ( <> - + ); } diff --git a/packages/docusaurus-theme-classic/src/theme/DocsRoot/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocsRoot/index.tsx index 3ffd06dce53e..1d3221a08fb4 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocsRoot/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocsRoot/index.tsx @@ -6,11 +6,13 @@ */ import type {ReactNode} from 'react'; -import renderRoutes from '@docusaurus/renderRoutes'; +import {Outlet} from '@docusaurus/router'; import Layout from '@theme/Layout'; -import type {Props} from '@theme/DocsRoot'; - -export default function DocsRoot(props: Props): ReactNode { - return {renderRoutes(props.route.routes!)}; +export default function DocsRoot(): ReactNode { + return ( + + + + ); } diff --git a/packages/docusaurus-theme-classic/src/theme/Tabs/__tests__/index.test.tsx b/packages/docusaurus-theme-classic/src/theme/Tabs/__tests__/index.test.tsx index c0926162f669..6c3bada34c76 100644 --- a/packages/docusaurus-theme-classic/src/theme/Tabs/__tests__/index.test.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Tabs/__tests__/index.test.tsx @@ -10,7 +10,7 @@ import type {PropsWithChildren, ReactNode} from 'react'; import {render} from '@testing-library/react'; import '@testing-library/jest-dom/vitest'; import {ScrollControllerProvider} from '@docusaurus/theme-common/internal'; -import {StaticRouter} from 'react-router-dom'; +import {StaticRouter} from 'react-router'; import Tabs from '../index'; import TabItem from '../../TabItem'; diff --git a/packages/docusaurus-theme-common/package.json b/packages/docusaurus-theme-common/package.json index 272d294a99d7..c68d34f09ff6 100644 --- a/packages/docusaurus-theme-common/package.json +++ b/packages/docusaurus-theme-common/package.json @@ -34,9 +34,7 @@ "@docusaurus/module-type-aliases": "workspace:*", "@docusaurus/utils": "workspace:*", "@docusaurus/utils-common": "workspace:*", - "@types/history": "catalog:", "@types/react": "19.3.0", - "@types/react-router-config": "catalog:", "clsx": "catalog:", "parse-numeric-range": "catalog:", "prism-react-renderer": "catalog:", @@ -47,9 +45,8 @@ "@docusaurus/core": "workspace:*", "@docusaurus/types": "workspace:*", "@total-typescript/shoehorn": "catalog:dev", - "@types/react-router-dom": "catalog:", "lodash": "catalog:", - "react-router-dom": "catalog:" + "react-router": "catalog:" }, "peerDependencies": { "react": "catalog:", diff --git a/packages/docusaurus-theme-common/src/contexts/navbarMobileSidebar.tsx b/packages/docusaurus-theme-common/src/contexts/navbarMobileSidebar.tsx index cbd9e73403ae..e6897ba8d620 100644 --- a/packages/docusaurus-theme-common/src/contexts/navbarMobileSidebar.tsx +++ b/packages/docusaurus-theme-common/src/contexts/navbarMobileSidebar.tsx @@ -14,7 +14,7 @@ import React, { } from 'react'; import {useNavbarSecondaryMenuContent} from './navbarSecondaryMenu/content'; import {useWindowSize} from '../hooks/useWindowSize'; -import {useHistoryPopHandler} from '../utils/historyUtils'; +import {useLocationChange} from '../utils/useLocationChange'; import {useThemeConfig} from '../utils/useThemeConfig'; import {ReactContextError} from '../utils/reactUtils'; @@ -63,51 +63,24 @@ function useContextValue(): ContextValue { } }, [windowSize]); + // Close mobile sidebar on navigation, including the Android back button + useLocationChange(() => { + setShown(false); + }); + return useMemo( () => ({disabled, shouldRender, toggle, shown}), [disabled, shouldRender, toggle, shown], ); } -// A component hook wrapper enables conditional rendering -// See reason here: https://github.com/facebook/docusaurus/issues/10988 -function OnHistoryPop({ - handler, -}: { - handler: Parameters[0]; -}) { - useHistoryPopHandler(handler); - return null; -} - export function NavbarMobileSidebarProvider({ children, }: { children: ReactNode; }): ReactNode { const value = useContextValue(); - return ( - <> - { - // Close mobile sidebar on navigation pop - // Most likely firing when using the Android back button (but not only) - // Important: we can only have a single history blocker at a time - // That's why this needs to be rendered conditionally - // See bug report https://github.com/facebook/docusaurus/issues/10988 - value.shown && ( - { - value.toggle(); - // Prevent pop navigation; seems desirable enough - // See https://github.com/facebook/docusaurus/pull/5462#issuecomment-911699846 - return false; - }} - /> - ) - } - {children} - - ); + return {children}; } export function useNavbarMobileSidebar(): ContextValue { diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/routesUtils.test.ts b/packages/docusaurus-theme-common/src/utils/__tests__/routesUtils.test.ts index 07d9ed9b9b58..bfa2bc52122e 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/routesUtils.test.ts +++ b/packages/docusaurus-theme-common/src/utils/__tests__/routesUtils.test.ts @@ -6,8 +6,9 @@ */ import {describe, expect, it} from 'vitest'; +import {fromPartial} from '@total-typescript/shoehorn'; import {findHomePageRoute, isSamePath} from '../routesUtils'; -import type {RouteConfig} from 'react-router-config'; +import type {RouteConfig} from '@generated/routes'; describe('isSamePath', () => { it('returns true for compared path without trailing slash', () => { @@ -42,10 +43,10 @@ describe('isSamePath', () => { }); describe('findHomePageRoute', () => { - const homePage: RouteConfig = { + const homePage = fromPartial({ path: '/', exact: true, - }; + }); it('returns undefined for no routes', () => { expect(findHomePageRoute({baseUrl: '/', routes: []})).toBeUndefined(); @@ -55,7 +56,7 @@ describe('findHomePageRoute', () => { expect( findHomePageRoute({ baseUrl: '/', - routes: [ + routes: fromPartial([ {path: '/a', exact: true}, {path: '/b', exact: false}, {path: '/c', exact: undefined}, @@ -68,7 +69,7 @@ describe('findHomePageRoute', () => { {path: '/d/3', exact: undefined}, ], }, - ], + ]), }), ).toBeUndefined(); }); @@ -77,14 +78,14 @@ describe('findHomePageRoute', () => { expect( findHomePageRoute({ baseUrl: '/', - routes: [ + routes: fromPartial([ {path: '/a', exact: true}, {path: '/b', exact: false}, {path: '/c', exact: undefined}, {...homePage, exact: false}, homePage, {...homePage, exact: undefined}, - ], + ]), }), ).toEqual(homePage); }); @@ -93,7 +94,7 @@ describe('findHomePageRoute', () => { expect( findHomePageRoute({ baseUrl: '/', - routes: [ + routes: fromPartial([ {path: '/a', exact: true}, { path: '/', @@ -108,7 +109,7 @@ describe('findHomePageRoute', () => { ], }, {path: '/d', exact: true}, - ], + ]), }), ).toEqual(homePage); }); @@ -119,7 +120,7 @@ describe('findHomePageRoute', () => { expect( findHomePageRoute({ baseUrl, - routes: [ + routes: fromPartial([ {path: `${baseUrl}a`, exact: true}, { path: baseUrl, @@ -134,7 +135,7 @@ describe('findHomePageRoute', () => { ], }, {path: `${baseUrl}d`, exact: true}, - ], + ]), }), ).toEqual(baseUrlHomePage); }); diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/useAlternatePageUtils.test.tsx b/packages/docusaurus-theme-common/src/utils/__tests__/useAlternatePageUtils.test.tsx index 79e73d6400f7..3cf9e0df6d18 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/useAlternatePageUtils.test.tsx +++ b/packages/docusaurus-theme-common/src/utils/__tests__/useAlternatePageUtils.test.tsx @@ -7,7 +7,7 @@ // @vitest-environment jsdom import {describe, expect, it} from 'vitest'; import {renderHook} from '@testing-library/react'; -import {StaticRouter} from 'react-router-dom'; +import {StaticRouter} from 'react-router'; import {Context} from '@docusaurus/core/src/client/docusaurusContext'; import {fromPartial} from '@total-typescript/shoehorn'; import {useAlternatePageUtils} from '../useAlternatePageUtils'; diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/useLocalPathname.test.tsx b/packages/docusaurus-theme-common/src/utils/__tests__/useLocalPathname.test.tsx index a74f9783db60..4b7c15ad8bab 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/useLocalPathname.test.tsx +++ b/packages/docusaurus-theme-common/src/utils/__tests__/useLocalPathname.test.tsx @@ -7,7 +7,7 @@ // @vitest-environment jsdom import {describe, expect, it} from 'vitest'; import {renderHook} from '@testing-library/react'; -import {StaticRouter} from 'react-router-dom'; +import {StaticRouter} from 'react-router'; import {Context} from '@docusaurus/core/src/client/docusaurusContext'; import {useLocalPathname} from '../useLocalPathname'; import type {DocusaurusContext} from '@docusaurus/types'; diff --git a/packages/docusaurus-theme-common/src/utils/historyUtils.ts b/packages/docusaurus-theme-common/src/utils/historyUtils.ts index 45f9cc22dd01..f59312234154 100644 --- a/packages/docusaurus-theme-common/src/utils/historyUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/historyUtils.ts @@ -5,71 +5,31 @@ * LICENSE file in the root directory of this source tree. */ -import {useCallback, useEffect, useMemo, useSyncExternalStore} from 'react'; -import {useHistory} from '@docusaurus/router'; -import {useEvent} from './reactUtils'; - -import type {History, Location, Action} from 'history'; - -type HistoryBlockHandler = (location: Location, action: Action) => void | false; - -/** - * Permits to register a handler that will be called on history actions (pop, - * push, replace). If the handler returns `false`, the navigation transition - * will be blocked/cancelled. - */ -function useHistoryActionHandler(handler: HistoryBlockHandler): void { - const history = useHistory(); - const stableHandler = useEvent(handler); - useEffect( - // See https://github.com/remix-run/history/blob/main/docs/blocking-transitions.md - () => history.block((location, action) => stableHandler(location, action)), - [history, stableHandler], - ); -} +import {useCallback, useMemo} from 'react'; +import {useLocation, useNavigate, type Location} from '@docusaurus/router'; +import useIsBrowser from '@docusaurus/useIsBrowser'; /** - * Permits to register a handler that will be called on history pop navigation - * (backward/forward). If the handler returns `false`, the backward/forward - * transition will be blocked. Unfortunately there's no good way to detect the - * "direction" (backward/forward) of the POP event. - */ -export function useHistoryPopHandler(handler: HistoryBlockHandler): void { - useHistoryActionHandler((location, action) => { - if (action === 'POP') { - // Maybe block navigation if handler returns false - return handler(location, action); - } - // Don't block other navigation actions - return undefined; - }); -} - -/** - * Permits to efficiently subscribe to a slice of the history - * See https://thisweekinreact.com/articles/useSyncExternalStore-the-underrated-react-api + * Permits to subscribe to a slice of the history location * @param selector */ export function useHistorySelector( - selector: (history: History) => Value, + selector: (history: {location: Location}) => Value, ): Value { - const history = useHistory(); - return useSyncExternalStore( - history.listen, - () => selector(history), - () => - selector({ - ...history, - location: { - ...history.location, + const location = useLocation(); + const isBrowser = useIsBrowser(); + return selector({ + location: isBrowser + ? location + : { + ...location, // On the server/hydration, these attributes should always be empty // Forcing empty state makes this hook safe from hydration errors search: '', hash: '', state: undefined, }, - }), - ); + }); } /** @@ -88,21 +48,19 @@ export function useQueryStringValue(key: string | null): string | null { function useQueryStringUpdater( key: string, ): (newValue: string | null, options?: {push: boolean}) => void { - const history = useHistory(); + const {search} = useLocation(); + const navigate = useNavigate(); return useCallback( (newValue, options) => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(search); if (newValue) { searchParams.set(key, newValue); } else { searchParams.delete(key); } - const updateHistory = options?.push ? history.push : history.replace; - updateHistory({ - search: searchParams.toString(), - }); + navigate({search: searchParams.toString()}, {replace: !options?.push}); }, - [key, history], + [key, search, navigate], ); } @@ -132,22 +90,20 @@ type ListUpdateFunction = ( ) => void; function useQueryStringListUpdater(key: string): ListUpdateFunction { - const history = useHistory(); + const {search} = useLocation(); + const navigate = useNavigate(); const setValues: ListUpdateFunction = useCallback( (update, options) => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(search); const newValues = Array.isArray(update) ? update : update(searchParams.getAll(key)); searchParams.delete(key); newValues.forEach((v) => searchParams.append(key, v)); - const updateHistory = options?.push ? history.push : history.replace; - updateHistory({ - search: searchParams.toString(), - }); + navigate({search: searchParams.toString()}, {replace: !options?.push}); }, - [history, key], + [key, search, navigate], ); return setValues; } @@ -161,12 +117,10 @@ export function useQueryStringList( } export function useClearQueryString(): () => void { - const history = useHistory(); + const navigate = useNavigate(); return useCallback(() => { - history.replace({ - search: undefined, - }); - }, [history]); + navigate({search: ''}, {replace: true}); + }, [navigate]); } export function mergeSearchParams( diff --git a/packages/docusaurus-theme-common/src/utils/routesUtils.ts b/packages/docusaurus-theme-common/src/utils/routesUtils.ts index b662ab4bf23d..4cb6a3737380 100644 --- a/packages/docusaurus-theme-common/src/utils/routesUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/routesUtils.ts @@ -6,9 +6,8 @@ */ import {useMemo} from 'react'; -import generatedRoutes from '@generated/routes'; +import generatedRoutes, {type RouteConfig} from '@generated/routes'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import type {RouteConfig} from 'react-router-config'; /** * Compare the 2 paths, case insensitive and ignoring trailing slash diff --git a/packages/docusaurus-theme-common/src/utils/skipToContentUtils.tsx b/packages/docusaurus-theme-common/src/utils/skipToContentUtils.tsx index f7e5a02ede73..983cc93cefdf 100644 --- a/packages/docusaurus-theme-common/src/utils/skipToContentUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/skipToContentUtils.tsx @@ -7,7 +7,6 @@ import type React from 'react'; import {useCallback, useRef, type ComponentProps, type ReactNode} from 'react'; -import {useHistory} from '@docusaurus/router'; import {translate} from '@docusaurus/Translate'; import {useLocationChange} from './useLocationChange'; @@ -56,7 +55,6 @@ function useSkipToContent(): { onClick: (e: React.MouseEvent) => void; } { const containerRef = useRef(null); - const {action} = useHistory(); const onClick = useCallback((e: React.MouseEvent) => { e.preventDefault(); @@ -66,10 +64,14 @@ function useSkipToContent(): { } }, []); - // "Reset" focus when navigating. + // "Reset" focus when navigating to another page. // See https://github.com/facebook/docusaurus/pull/8204#issuecomment-1276547558 - useLocationChange(({location}) => { - if (containerRef.current && !location.hash && action === 'PUSH') { + useLocationChange(({location, previousLocation}) => { + if ( + containerRef.current && + !location.hash && + location.pathname !== previousLocation?.pathname + ) { programmaticFocus(containerRef.current); } }); diff --git a/packages/docusaurus-theme-common/src/utils/tabsUtils.tsx b/packages/docusaurus-theme-common/src/utils/tabsUtils.tsx index 3f3a8dffe13a..02e10b782e2c 100644 --- a/packages/docusaurus-theme-common/src/utils/tabsUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/tabsUtils.tsx @@ -15,7 +15,7 @@ import React, { type ReactNode, type ReactElement, } from 'react'; -import {useHistory} from '@docusaurus/router'; +import {useLocation, useNavigate} from '@docusaurus/router'; import {useQueryStringValue} from '@docusaurus/theme-common/internal'; import {duplicates, useStorageSlot} from '../index'; @@ -187,7 +187,8 @@ function useTabQueryString({ queryString = false, groupId, }: Pick) { - const history = useHistory(); + const location = useLocation(); + const navigate = useNavigate(); const key = getQueryStringKey({queryString, groupId}); const value = useQueryStringValue(key); @@ -196,11 +197,14 @@ function useTabQueryString({ if (!key) { return; // no-op } - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(location.search); searchParams.set(key, newValue); - history.replace({...history.location, search: searchParams.toString()}); + navigate( + {search: searchParams.toString(), hash: location.hash}, + {replace: true, state: location.state}, + ); }, - [key, history], + [key, location, navigate], ); return [value, setValue] as const; diff --git a/packages/docusaurus-theme-common/src/utils/useLocationChange.ts b/packages/docusaurus-theme-common/src/utils/useLocationChange.ts index 56708058cf28..cc54641fe1ed 100644 --- a/packages/docusaurus-theme-common/src/utils/useLocationChange.ts +++ b/packages/docusaurus-theme-common/src/utils/useLocationChange.ts @@ -6,9 +6,8 @@ */ import {useEffect} from 'react'; -import {useLocation} from '@docusaurus/router'; +import {useLocation, type Location} from '@docusaurus/router'; import {useEvent, usePrevious} from './reactUtils'; -import type {Location} from 'history'; /** * Fires an effect when the location changes (which includes hash, query, etc.). diff --git a/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx b/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx index 6b46c11930ad..99aad3826b84 100644 --- a/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx +++ b/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx @@ -11,7 +11,7 @@ import {DocSearchButton} from '@docsearch/react/button'; import {useDocSearchKeyboardEvents} from '@docsearch/react/useDocSearchKeyboardEvents'; import Head from '@docusaurus/Head'; import Link from '@docusaurus/Link'; -import {useHistory} from '@docusaurus/router'; +import {useNavigate} from '@docusaurus/router'; import {isRegexpStringMatch} from '@docusaurus/theme-common'; import { useAlgoliaContextualFacetFilters, @@ -77,7 +77,7 @@ function importDocSearchModalIfNeeded() { function useNavigator({ externalUrlRegex, }: Pick) { - const history = useHistory(); + const navigate = useNavigate(); const [navigator] = useState(() => { return { navigate(params) { @@ -86,7 +86,7 @@ function useNavigator({ if (isRegexpStringMatch(externalUrlRegex, params.itemUrl)) { window.location.href = params.itemUrl; } else { - history.push(params.itemUrl); + navigate(params.itemUrl); } }, }; diff --git a/packages/docusaurus-types/package.json b/packages/docusaurus-types/package.json index 99ede8e37bca..d7362809f544 100644 --- a/packages/docusaurus-types/package.json +++ b/packages/docusaurus-types/package.json @@ -14,12 +14,12 @@ "license": "MIT", "dependencies": { "@mdx-js/mdx": "catalog:", - "@types/history": "catalog:", "@types/mdast": "catalog:", "@types/react": "19.3.0", "commander": "catalog:", "joi": "catalog:", "mdast-util-directive": "catalog:", + "react-router": "catalog:", "utility-types": "catalog:", "webpack": "catalog:", "webpack-merge": "catalog:" diff --git a/packages/docusaurus-types/src/clientModule.d.ts b/packages/docusaurus-types/src/clientModule.d.ts index 07c0ddf358e2..3c6442f6defb 100644 --- a/packages/docusaurus-types/src/clientModule.d.ts +++ b/packages/docusaurus-types/src/clientModule.d.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import type {Location} from 'history'; +import type {Location} from 'react-router'; export type ClientModule = { onRouteDidUpdate?: (args: { diff --git a/packages/docusaurus-types/src/routing.d.ts b/packages/docusaurus-types/src/routing.d.ts index d5c89fc9aa71..d860bfb8304f 100644 --- a/packages/docusaurus-types/src/routing.d.ts +++ b/packages/docusaurus-types/src/routing.d.ts @@ -100,14 +100,10 @@ export type RouteConfig = { */ routes?: RouteConfig[]; /** - * React router config option: `exact` routes would not match subroutes. + * `exact` routes only match their own path, other routes also match their + * subpaths. */ exact?: boolean; - /** - * React router config option: `strict` routes are sensitive to the presence - * of a trailing slash. - */ - strict?: boolean; /** * Used to sort routes. * Higher-priority routes will be matched first. diff --git a/packages/docusaurus-utils-common/src/__tests__/routeUtils.test.ts b/packages/docusaurus-utils-common/src/__tests__/routeUtils.test.ts new file mode 100644 index 000000000000..dd964eea70f2 --- /dev/null +++ b/packages/docusaurus-utils-common/src/__tests__/routeUtils.test.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {describe, expect, it} from 'vitest'; +import {getLeafRouteRouterPath} from '../routeUtils'; + +describe('getLeafRouteRouterPath', () => { + it('keeps the path of exact routes', () => { + expect(getLeafRouteRouterPath({path: '/', exact: true})).toBe('/'); + expect(getLeafRouteRouterPath({path: '/a/b', exact: true})).toBe('/a/b'); + expect(getLeafRouteRouterPath({path: '/a/b/', exact: true})).toBe('/a/b/'); + }); + + it('adds a splat to the path of non-exact routes', () => { + expect(getLeafRouteRouterPath({path: '/'})).toBe('/*'); + expect(getLeafRouteRouterPath({path: '/a/b'})).toBe('/a/b/*'); + expect(getLeafRouteRouterPath({path: '/a/b/', exact: false})).toBe( + '/a/b/*', + ); + }); + + it('keeps the path of splat routes', () => { + expect(getLeafRouteRouterPath({path: '*'})).toBe('*'); + expect(getLeafRouteRouterPath({path: '/a/*'})).toBe('/a/*'); + }); +}); diff --git a/packages/docusaurus-utils-common/src/index.ts b/packages/docusaurus-utils-common/src/index.ts index 0c4aae428147..a3fe592d15a6 100644 --- a/packages/docusaurus-utils-common/src/index.ts +++ b/packages/docusaurus-utils-common/src/index.ts @@ -18,3 +18,4 @@ export { } from './applyTrailingSlash'; export {addPrefix, removeSuffix, addSuffix, removePrefix} from './stringUtils'; export {getErrorCausalChain} from './errorUtils'; +export {getLeafRouteRouterPath} from './routeUtils'; diff --git a/packages/docusaurus-utils-common/src/routeUtils.ts b/packages/docusaurus-utils-common/src/routeUtils.ts new file mode 100644 index 000000000000..91e1b0685fb9 --- /dev/null +++ b/packages/docusaurus-utils-common/src/routeUtils.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {removeTrailingSlash} from './applyTrailingSlash'; + +/** + * Docusaurus leaf routes also match their subpaths unless they are `exact`, + * React Router routes need a `/*` splat path suffix for that. + */ +export function getLeafRouteRouterPath({ + path, + exact, +}: { + path: string; + exact?: boolean; +}): string { + if (exact || path.endsWith('*')) { + return path; + } + return `${removeTrailingSlash(path)}/*`; +} diff --git a/packages/docusaurus/package.json b/packages/docusaurus/package.json index 16cd79799fc7..475ba7cd9054 100644 --- a/packages/docusaurus/package.json +++ b/packages/docusaurus/package.json @@ -62,8 +62,6 @@ "react-loadable": "catalog:", "react-loadable-ssr-addon-v5-slorber": "catalog:", "react-router": "catalog:", - "react-router-config": "catalog:", - "react-router-dom": "catalog:", "semver": "catalog:", "serve-handler": "catalog:", "tinypool": "catalog:", @@ -78,10 +76,7 @@ "@docusaurus/types": "workspace:*", "@total-typescript/shoehorn": "catalog:dev", "@types/escape-html": "catalog:dev", - "@types/history": "catalog:", "@types/react-dom": "catalog:", - "@types/react-router-config": "catalog:", - "@types/react-router-dom": "catalog:", "@types/serve-handler": "catalog:dev", "@types/update-notifier": "catalog:dev", "@types/webpack-env": "catalog:dev", diff --git a/packages/docusaurus/src/client/App.tsx b/packages/docusaurus/src/client/App.tsx index f9748a86f9cd..b6edecfb979b 100644 --- a/packages/docusaurus/src/client/App.tsx +++ b/packages/docusaurus/src/client/App.tsx @@ -8,13 +8,13 @@ import type {ReactNode} from 'react'; import '@generated/client-modules'; -import routes from '@generated/routes'; +import {useRoutes, type Location} from 'react-router'; import {useLocation} from '@docusaurus/router'; -import renderRoutes from '@docusaurus/renderRoutes'; import Root from '@theme/Root'; import ThemeProvider from '@theme/ThemeProvider'; import SiteMetadata from '@theme/SiteMetadata'; import normalizeLocation from './normalizeLocation'; +import {routeObjects} from './routeObjects'; import {DocusaurusContextProvider} from './docusaurusContext'; import PendingNavigation from './PendingNavigation'; import BaseUrlIssueBanner from './BaseUrlIssueBanner'; @@ -25,14 +25,16 @@ import SiteMetadataDefaults from './SiteMetadataDefaults'; import ErrorBoundary from '@docusaurus/ErrorBoundary'; import HasHydratedDataAttribute from './hasHydratedDataAttribute'; -const routesElement = renderRoutes(routes); +function AppRoutes({location}: {location: Location}): ReactNode { + return useRoutes(routeObjects, location); +} function AppNavigation() { const location = useLocation(); const normalizedLocation = normalizeLocation(location); return ( - {routesElement} + {(pendingLocation) => } ); } diff --git a/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx b/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx index 8eb017a0d14a..bebdcd4ef3dd 100644 --- a/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx +++ b/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx @@ -8,7 +8,7 @@ import {useLayoutEffect, type ReactElement, type ReactNode} from 'react'; import clientModules from '@generated/client-modules'; import type {ClientModule} from '@docusaurus/types'; -import type {Location} from 'history'; +import type {Location} from 'react-router'; export function dispatchLifecycleAction( lifecycleAction: K, diff --git a/packages/docusaurus/src/client/PendingNavigation.tsx b/packages/docusaurus/src/client/PendingNavigation.tsx index 80855c16fed4..879142c45402 100644 --- a/packages/docusaurus/src/client/PendingNavigation.tsx +++ b/packages/docusaurus/src/client/PendingNavigation.tsx @@ -5,18 +5,17 @@ * LICENSE file in the root directory of this source tree. */ -import React, {type ReactNode} from 'react'; -import {Route} from 'react-router-dom'; +import React, {type ReactElement, type ReactNode} from 'react'; import ClientLifecyclesDispatcher, { dispatchLifecycleAction, } from './ClientLifecyclesDispatcher'; import ExecutionEnvironment from './exports/ExecutionEnvironment'; import preload from './preload'; -import type {Location} from 'history'; +import type {Location} from 'react-router'; type Props = { readonly location: Location; - readonly children: ReactNode; + readonly children: (location: Location) => ReactElement; }; type State = { nextRouteHasLoaded: boolean; @@ -82,13 +81,13 @@ class PendingNavigation extends React.Component { override render(): ReactNode { const {children, location} = this.props; - // Use a controlled to trick all descendants into rendering the old - // location. + // The children must render routes for this location, so that all + // descendants keep rendering the old location while the next one loads. return ( - children} /> + {children(location)} ); } diff --git a/packages/docusaurus/src/client/__tests__/normalizeLocation.test.ts b/packages/docusaurus/src/client/__tests__/normalizeLocation.test.ts index 001cc22b1f28..b6450000f908 100644 --- a/packages/docusaurus/src/client/__tests__/normalizeLocation.test.ts +++ b/packages/docusaurus/src/client/__tests__/normalizeLocation.test.ts @@ -6,9 +6,10 @@ */ import {describe, expect, it, vi} from 'vitest'; +import {matchRoutes, type Location} from 'react-router'; import normalizeLocation from '../normalizeLocation'; -import type {Location} from 'history'; +vi.mock('react-router', {spy: true}); vi.mock('@generated/routes', () => import('./__mocks__/@generated/routes')); describe('normalizeLocation', () => { @@ -74,7 +75,7 @@ describe('normalizeLocation', () => { }); it('leaves pathnames untouched', () => { - const replaceMock = vi.spyOn(String.prototype, 'replace'); + vi.mocked(matchRoutes).mockClear(); expect( normalizeLocation({ @@ -100,7 +101,7 @@ describe('normalizeLocation', () => { search: '', hash: '#features', }); - expect(replaceMock).toHaveBeenCalledTimes(1); + expect(matchRoutes).toHaveBeenCalledTimes(1); expect( normalizeLocation({ diff --git a/packages/docusaurus/src/client/__tests__/routeObjects.test.ts b/packages/docusaurus/src/client/__tests__/routeObjects.test.ts new file mode 100644 index 000000000000..7e4a8827e73b --- /dev/null +++ b/packages/docusaurus/src/client/__tests__/routeObjects.test.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {describe, expect, it, vi} from 'vitest'; +import {matchRouteConfigs} from '../routeObjects'; + +vi.mock('@generated/routes', () => ({ + default: [ + { + path: '/docs/', + component: '', + routes: [ + { + path: '/docs', + component: '', + routes: [ + {path: '/docs', exact: true, component: ''}, + {path: '/docs/intro', exact: true, component: ''}, + ], + }, + ], + }, + {path: '/app', component: ''}, + {path: '*', component: ''}, + ], +})); + +function matchPaths(pathname: string) { + return matchRouteConfigs(pathname).map((route) => route.path); +} + +describe('matchRouteConfigs', () => { + it('matches nested routes', () => { + expect(matchPaths('/docs')).toEqual(['/docs/', '/docs', '/docs']); + expect(matchPaths('/docs/')).toEqual(['/docs/', '/docs', '/docs']); + expect(matchPaths('/docs/intro')).toEqual([ + '/docs/', + '/docs', + '/docs/intro', + ]); + }); + + it('matches subpaths of non-exact routes', () => { + expect(matchPaths('/app')).toEqual(['/app']); + expect(matchPaths('/app/sub/path')).toEqual(['/app']); + }); + + it('matches the catch-all route', () => { + expect(matchPaths('/unknown')).toEqual(['*']); + expect(matchPaths('/docs/unknown')).toEqual(['*']); + expect(matchPaths('/docs/intro/unknown')).toEqual(['*']); + }); +}); diff --git a/packages/docusaurus/src/client/clientEntry.tsx b/packages/docusaurus/src/client/clientEntry.tsx index cce23f98a6e6..68014214d13a 100644 --- a/packages/docusaurus/src/client/clientEntry.tsx +++ b/packages/docusaurus/src/client/clientEntry.tsx @@ -8,7 +8,7 @@ import {startTransition, type ReactNode} from 'react'; import ReactDOM, {type ErrorInfo} from 'react-dom/client'; import {HelmetProvider} from 'react-helmet-async'; -import {BrowserRouter, HashRouter} from 'react-router-dom'; +import {BrowserRouter, HashRouter} from 'react-router'; import siteConfig from '@generated/docusaurus.config'; import ExecutionEnvironment from './exports/ExecutionEnvironment'; import App from './App'; @@ -16,10 +16,11 @@ import preload from './preload'; import docusaurus from './docusaurus'; function Router({children}: {children: ReactNode}): ReactNode { + // No transitions: PendingNavigation runs side effects on location updates return siteConfig.future.experimental_router === 'hash' ? ( - {children} + {children} ) : ( - {children} + {children} ); } diff --git a/packages/docusaurus/src/client/docusaurus.ts b/packages/docusaurus/src/client/docusaurus.ts index 24dff371728b..13dc7238c23c 100644 --- a/packages/docusaurus/src/client/docusaurus.ts +++ b/packages/docusaurus/src/client/docusaurus.ts @@ -5,9 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import {matchRoutes} from 'react-router-config'; import routesChunkNames from '@generated/routesChunkNames'; -import routes from '@generated/routes'; +import {matchRouteConfigs} from './routeObjects'; import prefetchHelper from './prefetch'; import preloadHelper from './preload'; import flat from './flat'; @@ -55,10 +54,8 @@ const prefetch: Docusaurus['prefetch'] = ( fetched.add(routePath); // Find all webpack chunk names needed. - const matches = matchRoutes(routes, routePath); - - const chunkNamesNeeded = matches.flatMap((match) => - getChunkNamesToLoad(match.route.path), + const chunkNamesNeeded = matchRouteConfigs(routePath).flatMap((route) => + getChunkNamesToLoad(route.path), ); // Prefetch all webpack chunk assets file needed. diff --git a/packages/docusaurus/src/client/exports/Link.tsx b/packages/docusaurus/src/client/exports/Link.tsx index e53f3b186da0..0472823779d3 100644 --- a/packages/docusaurus/src/client/exports/Link.tsx +++ b/packages/docusaurus/src/client/exports/Link.tsx @@ -5,15 +5,14 @@ * LICENSE file in the root directory of this source tree. */ +import {useEffect, useImperativeHandle, useRef, type ReactNode} from 'react'; import { - useCallback, - useEffect, - useImperativeHandle, - useRef, - type ComponentType, - type ReactNode, -} from 'react'; -import {NavLink, Link as RRLink} from 'react-router-dom'; + Link as RRLink, + matchPath, + parsePath, + useLocation, + type Location, +} from 'react-router'; import {applyTrailingSlash} from '@docusaurus/utils-common'; import useDocusaurusContext from './useDocusaurusContext'; import isInternalUrl from './isInternalUrl'; @@ -29,16 +28,53 @@ import type {Props} from '@docusaurus/Link'; // like "introduction" to "/baseUrl/introduction" => bad behavior to fix const shouldAddBaseUrlAutomatically = (to: string) => to.startsWith('/'); +// React Router resolves relative links against the route hierarchy, but we +// want to resolve them like the browser does for +function resolveRelativeLink(link: string, location: Location): string { + if (link.startsWith('/')) { + return link; + } + const url = new URL(link, `https://example.com${location.pathname}`); + return `${url.pathname}${url.search}${url.hash}`; +} + +// React Router's NavLink doesn't support a custom isActive function anymore +function getNavLinkProps({ + to, + location, + exact, + isActive, + className, + activeClassName = 'active', +}: Pick & { + to: string; + location: Location; +}) { + const match = matchPath( + {path: parsePath(to).pathname!, end: exact}, + location.pathname, + ); + if (isActive ? !isActive(match, location) : !match) { + return {className}; + } + return { + className: className ? `${className} ${activeClassName}` : activeClassName, + 'aria-current': 'page' as const, + }; +} + function Link({ isNavLink, to, href, activeClassName, isActive, + exact, 'data-noBrokenLinkCheck': noBrokenLinkCheck, autoAddBaseUrl = true, ...props }: Props): ReactNode { + const location = useLocation(); const {siteConfig} = useDocusaurusContext(); const {trailingSlash, baseUrl} = siteConfig; const router = siteConfig.future.experimental_router; @@ -94,46 +130,9 @@ function Link({ } const preloaded = useRef(false); - const LinkComponent = (isNavLink ? NavLink : RRLink) as ComponentType; const IOSupported = ExecutionEnvironment.canUseIntersectionObserver; - const ioRef = useRef(undefined); - - const handleRef = useCallback( - (el: HTMLAnchorElement | null) => { - innerRef.current = el; - ioRef.current?.disconnect(); - ioRef.current = undefined; - - if (IOSupported && el && isInternal) { - // If IO supported and element reference found, set up Observer. - const observer = new window.IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (el === entry.target) { - // If element is in viewport, stop observing and run callback. - // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API - if (entry.isIntersecting || entry.intersectionRatio > 0) { - observer.unobserve(el); - observer.disconnect(); - if (ioRef.current === observer) { - ioRef.current = undefined; - } - if (targetLink != null) { - window.docusaurus.prefetch(targetLink); - } - } - } - }); - }); - // Add element to the observer. - ioRef.current = observer; - observer.observe(el); - } - }, - [IOSupported, isInternal, targetLink], - ); - const onInteractionEnter = () => { if (!preloaded.current && targetLink != null) { window.docusaurus.preload(targetLink); @@ -168,6 +167,30 @@ function Link({ // See also https://github.com/facebook/docusaurus/pull/10311 (isAnchorLink && router !== 'hash'); + // Not a callback ref: React Router's Link calls it again on every render + useEffect(() => { + const el = innerRef.current; + if (!IOSupported || !el || !isInternal || isRegularHtmlLink) { + return undefined; + } + const observer = new window.IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (el === entry.target) { + // If element is in viewport, stop observing and run callback. + // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API + if (entry.isIntersecting || entry.intersectionRatio > 0) { + observer.disconnect(); + if (targetLink != null) { + window.docusaurus.prefetch(targetLink); + } + } + } + }); + }); + observer.observe(el); + return () => observer.disconnect(); + }, [IOSupported, isInternal, isRegularHtmlLink, targetLink]); + if (!noBrokenLinkCheck && (isAnchorLink || !isRegularHtmlLink)) { brokenLinks.collectLink(targetLink!); } @@ -182,26 +205,39 @@ function Link({ ? {'data-test-link-type': isRegularHtmlLink ? 'regular' : 'react-router'} : {}; - return isRegularHtmlLink ? ( - // eslint-disable-next-line jsx-a11y/anchor-has-content, @docusaurus/no-html-links - - ) : ( - + ); + } + + const routerLink = resolveRelativeLink(targetLink!, location); + return ( + ); diff --git a/packages/docusaurus/src/client/exports/__tests__/Link.test.tsx b/packages/docusaurus/src/client/exports/__tests__/Link.test.tsx index c746991ef84f..62c5619a56e9 100644 --- a/packages/docusaurus/src/client/exports/__tests__/Link.test.tsx +++ b/packages/docusaurus/src/client/exports/__tests__/Link.test.tsx @@ -11,7 +11,7 @@ import type {ReactNode} from 'react'; import {render as renderRTL} from '@testing-library/react'; import '@testing-library/jest-dom/vitest'; import {fromPartial} from '@total-typescript/shoehorn'; -import {MemoryRouter, StaticRouter} from 'react-router-dom'; +import {MemoryRouter, StaticRouter} from 'react-router'; import Link from '../Link'; import ExecutionEnvironment from '../ExecutionEnvironment'; import {Context} from '../../docusaurusContext'; @@ -61,7 +61,7 @@ function createLinkRenderer(defaultRendererOptions: Partial = {}) { }; const docusaurusContext = createDocusaurusContext(options); return renderRTL( - + {linkJsx} , ); diff --git a/packages/docusaurus/src/client/exports/renderRoutes.ts b/packages/docusaurus/src/client/exports/renderRoutes.ts deleted file mode 100644 index 144ae6efd204..000000000000 --- a/packages/docusaurus/src/client/exports/renderRoutes.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -export {renderRoutes as default} from 'react-router-config'; diff --git a/packages/docusaurus/src/client/exports/router.ts b/packages/docusaurus/src/client/exports/router.ts index 453530b353dd..f9c5c9fa8b46 100644 --- a/packages/docusaurus/src/client/exports/router.ts +++ b/packages/docusaurus/src/client/exports/router.ts @@ -5,4 +5,10 @@ * LICENSE file in the root directory of this source tree. */ -export {useHistory, useLocation, Redirect, matchPath} from 'react-router-dom'; +export { + useLocation, + useNavigate, + Navigate, + Outlet, + matchPath, +} from 'react-router'; diff --git a/packages/docusaurus/src/client/normalizeLocation.ts b/packages/docusaurus/src/client/normalizeLocation.ts index 74192a451c08..1b2bf81b421a 100644 --- a/packages/docusaurus/src/client/normalizeLocation.ts +++ b/packages/docusaurus/src/client/normalizeLocation.ts @@ -5,9 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import {matchRoutes} from 'react-router-config'; -import routes from '@generated/routes'; -import type {Location} from 'history'; +import {matchRouteConfigs} from './routeObjects'; +import type {Location} from 'react-router'; // Memoize previously normalized pathnames. const pathnames = new Map(); @@ -22,8 +21,9 @@ export default function normalizeLocation(location: T): T { // If the location was registered with an `.html` extension, we don't strip it // away, or it will render to a 404 page. - const matchedRoutes = matchRoutes(routes, location.pathname); - if (matchedRoutes.some(({route}) => route.exact === true)) { + if ( + matchRouteConfigs(location.pathname).some((route) => route.exact === true) + ) { pathnames.set(location.pathname, location.pathname); return location; } diff --git a/packages/docusaurus/src/client/preload.ts b/packages/docusaurus/src/client/preload.ts index 08f7c81cad9f..cf39646c8566 100644 --- a/packages/docusaurus/src/client/preload.ts +++ b/packages/docusaurus/src/client/preload.ts @@ -5,8 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import routes from '@generated/routes'; -import {matchRoutes} from 'react-router-config'; +import {matchRouteConfigs} from './routeObjects'; /** * Helper function to make sure all async components for that particular route @@ -17,9 +16,9 @@ import {matchRoutes} from 'react-router-config'; * @returns Promise object represents whether pathname has been preloaded */ export default function preload(pathname: string): Promise { - const matches = Array.from(new Set([pathname, decodeURI(pathname)])).flatMap( - (p) => matchRoutes(routes, p), - ); + const matchedRoutes = Array.from( + new Set([pathname, decodeURI(pathname)]), + ).flatMap((p) => matchRouteConfigs(p)); - return Promise.all(matches.map((match) => match.route.component.preload?.())); + return Promise.all(matchedRoutes.map((route) => route.component.preload?.())); } diff --git a/packages/docusaurus/src/client/routeObjects.tsx b/packages/docusaurus/src/client/routeObjects.tsx new file mode 100644 index 000000000000..29abc7f1813a --- /dev/null +++ b/packages/docusaurus/src/client/routeObjects.tsx @@ -0,0 +1,28 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {matchRoutes, type RouteObject} from 'react-router'; +import {getLeafRouteRouterPath} from '@docusaurus/utils-common'; +import routes, {type RouteConfig} from '@generated/routes'; + +function toRouteObject(route: RouteConfig): RouteObject { + return { + // Parent routes are pathless layout routes: React Router rejects nested + // absolute paths that repeat the parent path, like the docs routes do + path: route.routes ? undefined : getLeafRouteRouterPath(route), + element: , + children: route.routes?.map(toRouteObject), + handle: route, + }; +} + +export const routeObjects: RouteObject[] = routes.map(toRouteObject); + +export function matchRouteConfigs(pathname: string): RouteConfig[] { + const matches = matchRoutes(routeObjects, pathname) ?? []; + return matches.map((match) => match.route.handle as RouteConfig); +} diff --git a/packages/docusaurus/src/client/serverEntry.tsx b/packages/docusaurus/src/client/serverEntry.tsx index d0c1198071a7..38d8864d9ff0 100644 --- a/packages/docusaurus/src/client/serverEntry.tsx +++ b/packages/docusaurus/src/client/serverEntry.tsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {StaticRouter} from 'react-router-dom'; +import {StaticRouter} from 'react-router'; import {HelmetProvider, type FilledContext} from 'react-helmet-async'; import Loadable from 'react-loadable'; import {renderToHtml} from './renderToHtml'; @@ -22,7 +22,6 @@ const render: AppRenderer['render'] = async ({pathname}) => { await preload(pathname); const modules = new Set(); - const routerContext = {}; const helmetContext = {}; const statefulBrokenLinks = createStatefulBrokenLinks(); @@ -30,7 +29,7 @@ const render: AppRenderer['render'] = async ({pathname}) => { // @ts-expect-error: we are migrating away from react-loadable anyways modules.add(moduleName)}> - + diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 5abe3f16299d..a924a0712092 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -6,14 +6,13 @@ */ import {describe, expect, it, vi} from 'vitest'; -import * as reactRouterConfig from 'react-router-config'; +import * as reactRouter from 'react-router'; import {handleBrokenLinks} from '../brokenLinks'; import type {RouteConfig} from '@docusaurus/types'; -vi.mock('react-router-config', async () => { - const actual = await vi.importActual( - 'react-router-config', - ); +vi.mock('react-router', async () => { + const actual = + await vi.importActual('react-router'); return { ...actual, @@ -28,7 +27,6 @@ type SimpleRoute = { path: string; routes?: SimpleRoute[]; exact?: boolean; - strict?: boolean; }; // Conveniently apply defaults to function under test @@ -89,9 +87,9 @@ describe('handleBrokenLinks', () => { }); }); - it('accepts valid non-strict link', async () => { + it('accepts valid link with or without trailing slash', async () => { await testBrokenLinks({ - routes: [{path: '/page1', strict: false}, {path: '/page2/'}], + routes: [{path: '/page1'}, {path: '/page2/'}], collectedLinks: { '/page1': { links: ['/page1', '/page1/', '/page2', '/page2/'], @@ -159,9 +157,9 @@ describe('handleBrokenLinks', () => { }); }); - it('accepts valid non-strict link with anchor', async () => { + it('accepts valid link with or without trailing slash with anchor', async () => { await testBrokenLinks({ - routes: [{path: '/page1', strict: false}, {path: '/page2/'}], + routes: [{path: '/page1'}, {path: '/page2/'}], collectedLinks: { '/page1': { links: [ @@ -445,76 +443,6 @@ describe('handleBrokenLinks', () => { `); }); - it('rejects broken link due to strict matching', async () => { - await expect(() => - testBrokenLinks({ - routes: [ - {path: '/page1', strict: true}, - {path: '/page2/', strict: true}, - ], - - collectedLinks: { - '/page1': { - links: ['/page1', '/page1/', '/page2', '/page2/'], - anchors: [], - }, - '/page2/': { - links: ['/page1', '/page1/', '/page2', '/page2/'], - anchors: [], - }, - }, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot(` - [Error: Docusaurus found broken links! - - Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist. - Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass. - - Exhaustive list of all broken links found: - - Broken link on source page path = /page1: - -> linking to /page2 - - Broken link on source page path = /page2/: - -> linking to /page2 - ] - `); - }); - - it('rejects broken link due to strict exact matching', async () => { - await expect(() => - testBrokenLinks({ - routes: [ - {path: '/page1', exact: true, strict: true}, - {path: '/page2/', exact: true, strict: true}, - ], - - collectedLinks: { - '/page1': { - links: ['/page1', '/page1/', '/page2', '/page2/'], - anchors: [], - }, - '/page2/': { - links: ['/page1', '/page1/', '/page2', '/page2/'], - anchors: [], - }, - }, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot(` - [Error: Docusaurus found broken links! - - Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist. - Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass. - - Exhaustive list of all broken links found: - - Broken link on source page path = /page1: - -> linking to /page1/ - -> linking to /page2 - - Broken link on source page path = /page2/: - -> linking to /page1/ - -> linking to /page2 - ] - `); - }); - it('rejects broken link with anchor', async () => { await expect(() => testBrokenLinks({ @@ -941,7 +869,7 @@ describe('handleBrokenLinks', () => { }); it('is performant and minimize calls to matchRoutes', async () => { - const matchRoutesMock = vi.mocked(reactRouterConfig.matchRoutes); + const matchRoutesMock = vi.mocked(reactRouter.matchRoutes); const scale = 100; diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 3cf0d8322c95..cf1d638b6226 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -7,23 +7,20 @@ import _ from 'lodash'; import logger from '@docusaurus/logger'; -import {matchRoutes as reactRouterMatchRoutes} from 'react-router-config'; +import {matchRoutes} from 'react-router'; import { parseURLPath, serializeURLPath, flattenRoutes, type URLPath, } from '@docusaurus/utils'; -import {addTrailingSlash, removeTrailingSlash} from '@docusaurus/utils-common'; +import { + addTrailingSlash, + getLeafRouteRouterPath, + removeTrailingSlash, +} from '@docusaurus/utils-common'; import type {RouteConfig, ReportingSeverity} from '@docusaurus/types'; -function matchRoutes(routeConfig: RouteConfig[], pathname: string) { - // @ts-expect-error: React router types RouteConfig with an actual React - // component, but we load route components with string paths. - // We don't actually access component here, so it's fine. - return reactRouterMatchRoutes(routeConfig, pathname); -} - type BrokenLink = { link: string; resolvedLink: string; @@ -87,25 +84,27 @@ function createBrokenLinksHelper({ if (disableOptimization) { return routes; } - // We must consider the "exact" and "strict" match attribute + // We must consider the "exact" match attribute // We can only infer pre-validated pathnames from a route from exact routes const [validPathnameRoutes, otherRoutes] = _.partition( routes, (route) => route.exact && validPathnames.has(route.path), ); - // If a route is non-strict (non-sensitive to trailing slashes) + // Routes are not sensitive to trailing slashes // We must pre-validate all possible paths validPathnameRoutes.forEach((validPathnameRoute) => { - if (!validPathnameRoute.strict) { - validPathnames.add(addTrailingSlash(validPathnameRoute.path)); - validPathnames.add(removeTrailingSlash(validPathnameRoute.path)); - } + validPathnames.add(addTrailingSlash(validPathnameRoute.path)); + validPathnames.add(removeTrailingSlash(validPathnameRoute.path)); }); return otherRoutes; })(); + const remainingRouterRoutes = remainingRoutes.map((route) => ({ + path: getLeafRouteRouterPath(route), + })); + function isPathnameMatchingAnyRoute(pathname: string): boolean { - if (matchRoutes(remainingRoutes, pathname).length > 0) { + if (matchRoutes(remainingRouterRoutes, pathname)) { // IMPORTANT: this is an optimization // See https://github.com/facebook/docusaurus/issues/9754 // Large Docusaurus sites have many routes! diff --git a/packages/docusaurus/src/server/codegen/codegenRoutes.ts b/packages/docusaurus/src/server/codegen/codegenRoutes.ts index 9f6a9dc1c0d2..e41cdd7ee72f 100644 --- a/packages/docusaurus/src/server/codegen/codegenRoutes.ts +++ b/packages/docusaurus/src/server/codegen/codegenRoutes.ts @@ -83,7 +83,7 @@ export function genChunkName( /** * Takes a piece of route config, and serializes it into raw JS code. The shape - * is the same as react-router's `RouteConfig`. Formatting is similar to + * is the `RouteConfig` of `@generated/routes`. Formatting is similar to * `JSON.stringify` but without all the quotes. */ function serializeRouteConfig({ diff --git a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap index 997ed1b3f24f..fdc86b5ca29e 100644 --- a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap +++ b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap @@ -13,7 +13,6 @@ exports[`base webpack config > creates webpack aliases 1`] = ` "@docusaurus/Translate": "../../../../client/exports/Translate.tsx", "@docusaurus/constants": "../../../../client/exports/constants.ts", "@docusaurus/isInternalUrl": "../../../../client/exports/isInternalUrl.ts", - "@docusaurus/renderRoutes": "../../../../client/exports/renderRoutes.ts", "@docusaurus/router": "../../../../client/exports/router.ts", "@docusaurus/useBaseUrl": "../../../../client/exports/useBaseUrl.ts", "@docusaurus/useBrokenLinks": "../../../../client/exports/useBrokenLinks.ts", diff --git a/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap index 50f0f435fe47..5d063514417a 100644 --- a/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap @@ -13,7 +13,6 @@ exports[`getDocusaurusAliases > returns appropriate webpack aliases 1`] = ` "@docusaurus/Translate": "/packages/docusaurus/src/client/exports/Translate.tsx", "@docusaurus/constants": "/packages/docusaurus/src/client/exports/constants.ts", "@docusaurus/isInternalUrl": "/packages/docusaurus/src/client/exports/isInternalUrl.ts", - "@docusaurus/renderRoutes": "/packages/docusaurus/src/client/exports/renderRoutes.ts", "@docusaurus/router": "/packages/docusaurus/src/client/exports/router.ts", "@docusaurus/useBaseUrl": "/packages/docusaurus/src/client/exports/useBaseUrl.ts", "@docusaurus/useBrokenLinks": "/packages/docusaurus/src/client/exports/useBrokenLinks.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74267d512015..3aab3a119a05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,9 +185,6 @@ catalogs: '@swc/html': specifier: ^1.16.2 version: 1.16.2 - '@types/history': - specifier: ^4.7.11 - version: 4.7.11 '@types/mdast': specifier: ^4.0.4 version: 4.0.4 @@ -197,12 +194,6 @@ catalogs: '@types/react-dom': specifier: ^19.3.0 version: 19.3.0 - '@types/react-router-config': - specifier: ^5.0.11 - version: 5.0.11 - '@types/react-router-dom': - specifier: ^5.3.3 - version: 5.3.3 '@typescript-eslint/types': specifier: ^8.71.0 version: 8.71.1 @@ -411,14 +402,8 @@ catalogs: specifier: ^5.4.9 version: 5.4.9 react-router: - specifier: ^5.3.4 - version: 5.3.4 - react-router-config: - specifier: ^5.1.1 - version: 5.1.1 - react-router-dom: - specifier: ^5.3.4 - version: 5.3.4 + specifier: ^8.5.0 + version: 8.5.0 recma-mdx-displayname: specifier: ^0.4.1 version: 0.4.1 @@ -1190,13 +1175,7 @@ importers: version: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.3.0))(webpack@5.111.0(@swc/core@1.16.2)(@swc/html@1.16.2)(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.28)(sharp@0.35.5(@types/node@26.6.1))(svgo@4.1.0)(uglify-js@3.19.3)) react-router: specifier: 'catalog:' - version: 5.3.4(react@19.3.0) - react-router-config: - specifier: 'catalog:' - version: 5.1.1(react-router@5.3.4(react@19.3.0))(react@19.3.0) - react-router-dom: - specifier: 'catalog:' - version: 5.3.4(react@19.3.0) + version: 8.5.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) semver: specifier: 'catalog:' version: 7.8.5 @@ -1234,18 +1213,9 @@ importers: '@types/escape-html': specifier: catalog:dev version: 1.0.4 - '@types/history': - specifier: 'catalog:' - version: 4.7.11 '@types/react-dom': specifier: 'catalog:' version: 19.3.0(@types/react@19.3.0) - '@types/react-router-config': - specifier: 'catalog:' - version: 5.0.11 - '@types/react-router-dom': - specifier: 'catalog:' - version: 5.3.3 '@types/serve-handler': specifier: catalog:dev version: 6.1.4 @@ -1556,21 +1526,12 @@ importers: '@docusaurus/types': specifier: workspace:* version: link:../docusaurus-types - '@types/history': - specifier: 'catalog:' - version: 4.7.11 '@types/react': specifier: 19.3.0 version: 19.3.0 '@types/react-dom': specifier: 'catalog:' version: 19.3.0(@types/react@19.3.0) - '@types/react-router-config': - specifier: 'catalog:' - version: 5.0.11 - '@types/react-router-dom': - specifier: 'catalog:' - version: 5.3.3 react: specifier: 'catalog:' version: 19.3.0 @@ -1583,6 +1544,9 @@ importers: react-loadable: specifier: 'catalog:' version: '@docusaurus/react-loadable@6.0.0(react@19.3.0)' + react-router: + specifier: 'catalog:' + version: 8.5.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) packages/docusaurus-plugin-client-redirects: dependencies: @@ -1753,9 +1717,6 @@ importers: '@docusaurus/utils-validation': specifier: workspace:* version: link:../docusaurus-utils-validation - '@types/react-router-config': - specifier: 'catalog:' - version: 5.0.11 combine-promises: specifier: 'catalog:' version: 1.2.0 @@ -1790,18 +1751,15 @@ importers: '@types/picomatch': specifier: catalog:dev version: 2.3.4 - '@types/react-router-dom': - specifier: 'catalog:' - version: 5.3.3 commander: specifier: 'catalog:' version: 15.0.0 picomatch: specifier: catalog:dev version: 2.3.2 - react-router-dom: + react-router: specifier: 'catalog:' - version: 5.3.4(react@19.3.0) + version: 8.5.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) packages/docusaurus-plugin-content-pages: dependencies: @@ -2373,15 +2331,12 @@ importers: '@types/prismjs': specifier: catalog:dev version: 1.26.6 - '@types/react-router-dom': - specifier: 'catalog:' - version: 5.3.3 '@types/rtlcss': specifier: catalog:dev version: 3.5.4 - react-router-dom: + react-router: specifier: 'catalog:' - version: 5.3.4(react@19.3.0) + version: 8.5.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) packages/docusaurus-theme-common: dependencies: @@ -2397,15 +2352,9 @@ importers: '@docusaurus/utils-common': specifier: workspace:* version: link:../docusaurus-utils-common - '@types/history': - specifier: 'catalog:' - version: 4.7.11 '@types/react': specifier: 19.3.0 version: 19.3.0 - '@types/react-router-config': - specifier: 'catalog:' - version: 5.0.11 clsx: specifier: 'catalog:' version: 2.1.1 @@ -2437,15 +2386,12 @@ importers: '@total-typescript/shoehorn': specifier: catalog:dev version: 0.1.2 - '@types/react-router-dom': - specifier: 'catalog:' - version: 5.3.3 lodash: specifier: 'catalog:' version: 4.18.1 - react-router-dom: + react-router: specifier: 'catalog:' - version: 5.3.4(react@19.3.0) + version: 8.5.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) packages/docusaurus-theme-live-codeblock: dependencies: @@ -2623,9 +2569,6 @@ importers: '@mdx-js/mdx': specifier: 'catalog:' version: 3.1.1(supports-color@10.2.2) - '@types/history': - specifier: 'catalog:' - version: 4.7.11 '@types/mdast': specifier: 'catalog:' version: 4.0.4 @@ -2647,6 +2590,9 @@ importers: react-dom: specifier: 'catalog:' version: 19.3.0(react@19.3.0) + react-router: + specifier: 'catalog:' + version: 8.5.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) utility-types: specifier: 'catalog:' version: 3.11.0 @@ -2918,9 +2864,6 @@ importers: '@types/color': specifier: catalog:dev version: 3.0.7 - '@types/history': - specifier: 'catalog:' - version: 4.7.11 '@types/node': specifier: catalog:dev version: 26.6.1 @@ -5840,6 +5783,9 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@remix-run/route-pattern@0.22.1': + resolution: {integrity: sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==} + '@rolldown/binding-android-arm-eabi@1.2.7': resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6916,9 +6862,6 @@ packages: '@types/hast@3.0.5': resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} - '@types/history@4.7.11': - resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} - '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -6978,15 +6921,6 @@ packages: peerDependencies: '@types/react': ^19.3.0 - '@types/react-router-config@5.0.11': - resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} - - '@types/react-router-dom@5.3.3': - resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} - - '@types/react-router@5.1.20': - resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - '@types/react@19.3.0': resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} @@ -8153,6 +8087,9 @@ packages: resolution: {integrity: sha512-JtXpxqDqJ8P0UwEHwhxLzCIXQy97vlYBZR222Sbzb1q1Erex9ASrztJ29SyhWFQjod1AeFBaPzEEC8YvtZMIYg==} engines: {node: '>=6'} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -9543,12 +9480,6 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - history@4.10.1: - resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} - - hoist-non-react-statics@3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} @@ -10022,9 +9953,6 @@ packages: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} - isarray@0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -11338,9 +11266,6 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} - path-to-regexp@1.9.0: - resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} - path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} @@ -11989,21 +11914,21 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-router-config@5.1.1: - resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==} - peerDependencies: - react: '>=15' - react-router: '>=5' - - react-router-dom@5.3.4: - resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} - peerDependencies: - react: '>=15' - - react-router@5.3.4: - resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} + react-router@8.5.0: + resolution: {integrity: sha512-9nEd/deifcTJIIcUaNIyQGkyXD0D6QVB9XD2O5GgHpP3xu3U5RXm87YQ8++Wn8JQWgMBM/5+PjWPM/7n7O7WHA==} + engines: {node: '>=22.22.0'} peerDependencies: - react: '>=15' + '@types/react': '>=19.2.0' + '@types/react-dom': '>=19.2.0' + react: '>=19.2.7' + react-dom: '>=19.2.7' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + react-dom: + optional: true react@16.14.0: resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==} @@ -12875,12 +12800,6 @@ packages: thunky@1.1.0: resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - - tiny-warning@1.0.3: - resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - tinybench@6.1.4: resolution: {integrity: sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==} engines: {node: '>=20.0.0'} @@ -13228,9 +13147,6 @@ packages: resolution: {integrity: sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==} engines: {node: ^18.17.0 || >=20.5.0} - value-equal@1.0.1: - resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -16962,6 +16878,8 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@remix-run/route-pattern@0.22.1': {} + '@rolldown/binding-android-arm-eabi@1.2.7': optional: true @@ -17965,8 +17883,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/history@4.7.11': {} - '@types/http-errors@2.0.5': {} '@types/json-schema@7.0.15': {} @@ -18019,23 +17935,6 @@ snapshots: dependencies: '@types/react': 19.3.0 - '@types/react-router-config@5.0.11': - dependencies: - '@types/history': 4.7.11 - '@types/react': 19.3.0 - '@types/react-router': 5.1.20 - - '@types/react-router-dom@5.3.3': - dependencies: - '@types/history': 4.7.11 - '@types/react': 19.3.0 - '@types/react-router': 5.1.20 - - '@types/react-router@5.1.20': - dependencies: - '@types/history': 4.7.11 - '@types/react': 19.3.0 - '@types/react@19.3.0': dependencies: csstype: 3.2.3 @@ -19311,6 +19210,8 @@ snapshots: lodash.clonedeep: 4.5.0 yargs-parser: 20.2.9 + cookie-es@3.1.1: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -21132,19 +21033,6 @@ snapshots: highlight.js@10.7.3: {} - history@4.10.1: - dependencies: - '@babel/runtime': 7.29.7 - loose-envify: 1.4.0 - resolve-pathname: 3.0.0 - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - value-equal: 1.0.1 - - hoist-non-react-statics@3.3.2: - dependencies: - react-is: 16.13.1 - hookified@1.15.1: {} hookified@2.2.0: {} @@ -21592,8 +21480,6 @@ snapshots: dependencies: is-inside-container: 1.0.0 - isarray@0.0.1: {} - isarray@2.0.5: {} isexe@2.0.0: {} @@ -23594,10 +23480,6 @@ snapshots: lru-cache: 11.5.2 minipass: 7.1.3 - path-to-regexp@1.9.0: - dependencies: - isarray: 0.0.1 - path-to-regexp@3.3.0: {} path-to-regexp@8.4.2: {} @@ -24282,35 +24164,15 @@ snapshots: react: 19.3.0 react-dom: 19.3.0(react@19.3.0) - react-router-config@5.1.1(react-router@5.3.4(react@19.3.0))(react@19.3.0): - dependencies: - '@babel/runtime': 7.29.7 - react: 19.3.0 - react-router: 5.3.4(react@19.3.0) - - react-router-dom@5.3.4(react@19.3.0): - dependencies: - '@babel/runtime': 7.29.7 - history: 4.10.1 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 19.3.0 - react-router: 5.3.4(react@19.3.0) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - - react-router@5.3.4(react@19.3.0): + react-router@8.5.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - '@babel/runtime': 7.29.7 - history: 4.10.1 - hoist-non-react-statics: 3.3.2 - loose-envify: 1.4.0 - path-to-regexp: 1.9.0 - prop-types: 15.8.1 + '@remix-run/route-pattern': 0.22.1 + cookie-es: 3.1.1 react: 19.3.0 - react-is: 16.13.1 - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + react-dom: 19.3.0(react@19.3.0) react@16.14.0: dependencies: @@ -25468,10 +25330,6 @@ snapshots: thunky@1.1.0: {} - tiny-invariant@1.3.3: {} - - tiny-warning@1.0.3: {} - tinybench@6.1.4: {} tinycolor2@1.6.0: @@ -25836,8 +25694,6 @@ snapshots: validate-npm-package-name@6.0.2: {} - value-equal@1.0.1: {} - vary@1.1.2: {} vfile-location@5.0.3: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a8b57c511c7a..f06450af4c2b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -47,12 +47,9 @@ catalog: '@svgr/webpack': ^8.1.0 '@swc/core': ^1.16.2 '@swc/html': ^1.16.2 - '@types/history': ^4.7.11 '@types/mdast': ^4.0.4 '@types/react': ^19.3.0 '@types/react-dom': ^19.3.0 - '@types/react-router-config': ^5.0.11 - '@types/react-router-dom': ^5.3.3 '@typescript-eslint/types': ^8.71.0 '@typescript-eslint/utils': ^8.71.0 '@vercel/analytics': ^2.0.1 @@ -122,9 +119,7 @@ catalog: react-loadable: npm:@docusaurus/react-loadable@6.0.0 react-loadable-ssr-addon-v5-slorber: ^1.0.3 react-medium-image-zoom: ^5.4.9 - react-router: ^5.3.4 - react-router-config: ^5.1.1 - react-router-dom: ^5.3.4 + react-router: ^8.5.0 recma-mdx-displayname: ^0.4.1 rehype-katex: ^7.0.1 rehype-raw: ^7.0.0 diff --git a/website/_dogfooding/_pages tests/history-tests.tsx b/website/_dogfooding/_pages tests/history-tests.tsx deleted file mode 100644 index 7cb8ebfd7248..000000000000 --- a/website/_dogfooding/_pages tests/history-tests.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -import {useEffect, type ReactNode} from 'react'; -import {useHistory} from '@docusaurus/router'; -import Layout from '@theme/Layout'; -import Heading from '@theme/Heading'; - -// Test for https://github.com/facebook/docusaurus/issues/10988 -function BlockNavigation() { - const history = useHistory(); - useEffect(() => { - return history.block(() => { - alert('navigation blocked successfully'); - return false; - }); - }, [history]); - return false; -} - -export default function HistoryTestsPage(): ReactNode { - return ( - - History tests -

This page should block navigation

- -
- ); -} diff --git a/website/_dogfooding/_pages tests/index.mdx b/website/_dogfooding/_pages tests/index.mdx index 82d51d6847c5..8043e1aba278 100644 --- a/website/_dogfooding/_pages tests/index.mdx +++ b/website/_dogfooding/_pages tests/index.mdx @@ -39,7 +39,6 @@ import Readme from "../README.mdx" - [Head metadata tests](/tests/pages/head-metadata) - [Unlisted page](/tests/pages/my-custom/unlisted/slug) - [Analytics](/tests/pages/analytics) -- [History tests](/tests/pages/history-tests) - [Embeds](/tests/pages/embeds) - [Style Isolation tests](/tests/pages/style-isolation) - [IdealImage tests](/tests/pages/ideal-image) diff --git a/website/_dogfooding/clientModuleExample.ts b/website/_dogfooding/clientModuleExample.ts index e7b673e74e94..94d065601aad 100644 --- a/website/_dogfooding/clientModuleExample.ts +++ b/website/_dogfooding/clientModuleExample.ts @@ -6,7 +6,7 @@ */ import siteConfig from '@generated/docusaurus.config'; -import type {Location} from 'history'; +import type {Location} from '@docusaurus/router'; function logPage( event: string, diff --git a/website/docs/advanced/routing.mdx b/website/docs/advanced/routing.mdx index ed29569dd6eb..026e249518f0 100644 --- a/website/docs/advanced/routing.mdx +++ b/website/docs/advanced/routing.mdx @@ -226,7 +226,7 @@ The `addRoute` lifecycle action is used to generate routes. It registers a piece All routes are aggregated in `.docusaurus/routes.js`, which you can view with the debug plugin's [routes panel](/__docusaurus/debug/routes). -On the client side, we offer `@docusaurus/router` to access the page's route. `@docusaurus/router` is a re-export of the [`react-router-dom`](https://www.npmjs.com/package/react-router-dom/v/5.3.0) package. For example, you can use `useLocation` to get the current page's [location](https://developer.mozilla.org/en-US/docs/Web/API/Location), and `useHistory` to access the [history object](https://developer.mozilla.org/en-US/docs/Web/API/History). (They are not the same as the browser API, although similar in functionality. Refer to the React Router documentation for specific APIs.) +On the client side, we offer `@docusaurus/router` to access the page's route. `@docusaurus/router` re-exports a subset of the [React Router](https://reactrouter.com/start/declarative/routing) APIs: `useLocation`, `useNavigate`, `Navigate`, `Outlet`, and `matchPath`. For example, you can use `useLocation` to get the current page's [location](https://developer.mozilla.org/en-US/docs/Web/API/Location), and [`useNavigate`](https://reactrouter.com/api/hooks/useNavigate) to navigate to another page. (They are not the same as the browser API, although similar in functionality. Refer to the React Router documentation for specific APIs.) This API is **SSR safe**, as opposed to the browser-only `window.location`. diff --git a/website/docs/api/plugin-methods/lifecycle-apis.mdx b/website/docs/api/plugin-methods/lifecycle-apis.mdx index 04ad27711342..439662d97662 100644 --- a/website/docs/api/plugin-methods/lifecycle-apis.mdx +++ b/website/docs/api/plugin-methods/lifecycle-apis.mdx @@ -68,14 +68,10 @@ export type RouteConfig = { */ routes?: RouteConfig[]; /** - * React router config option: `exact` routes would not match subroutes. + * `exact` routes only match their own path, other routes also match their + * subpaths. */ exact?: boolean; - /** - * React router config option: `strict` routes are sensitive to the presence - * of a trailing slash. - */ - strict?: boolean; /** * Used to sort routes. * Higher-priority routes will be matched first. diff --git a/website/docs/docusaurus-core.mdx b/website/docs/docusaurus-core.mdx index f5f15b989e89..85481b61daf4 100644 --- a/website/docs/docusaurus-core.mdx +++ b/website/docs/docusaurus-core.mdx @@ -157,26 +157,26 @@ Prefer this component to vanilla `
` tags because Docusaurus does a lot of opt ::: -### `` {/* #redirect */} +### `` {/* #navigate */} -Rendering a `` will navigate to a new location. The new location will override the current location in the history stack like server-side redirects (HTTP 3xx) do. You can refer to [React Router's Redirect documentation](https://reacttraining.com/react-router/web/api/Redirect) for more info on available props. +Rendering a `` will navigate to a new location. With the `replace` prop, the new location will override the current location in the history stack like server-side redirects (HTTP 3xx) do. You can refer to [React Router's Navigate documentation](https://reactrouter.com/api/components/Navigate) for more info on available props. Example usage: ```jsx import React from 'react'; // highlight-next-line -import {Redirect} from '@docusaurus/router'; +import {Navigate} from '@docusaurus/router'; const Home = () => { // highlight-next-line - return ; + return ; }; ``` :::note -`@docusaurus/router` implements [React Router](https://reacttraining.com/react-router/web/guides/quick-start) and supports its features. +`@docusaurus/router` re-exports a subset of [React Router](https://reactrouter.com/start/declarative/routing) APIs. ::: diff --git a/website/package.json b/website/package.json index ef4503b8fe70..33bd1cea9bc1 100644 --- a/website/package.json +++ b/website/package.json @@ -85,7 +85,6 @@ "@docusaurus/tsconfig": "workspace:*", "@mdx-js/react": "catalog:", "@types/color": "catalog:dev", - "@types/history": "catalog:", "@types/node": "catalog:dev", "@types/prismjs": "catalog:dev", "copy-text-to-clipboard": "catalog:", diff --git a/website/src/components/APITable/index.tsx b/website/src/components/APITable/index.tsx index 3c780740eeb3..e0bee52e4870 100644 --- a/website/src/components/APITable/index.tsx +++ b/website/src/components/APITable/index.tsx @@ -14,7 +14,7 @@ import React, { useEffect, } from 'react'; import useBrokenLinks from '@docusaurus/useBrokenLinks'; -import {useHistory} from '@docusaurus/router'; +import {useLocation, useNavigate} from '@docusaurus/router'; import styles from './styles.module.css'; interface Props { @@ -54,13 +54,14 @@ function APITableRow({ const entryName = getRowName(children); const id = name ? `${name}-${entryName}` : entryName; const anchor = `#${id}`; - const history = useHistory(); + const location = useLocation(); + const navigate = useNavigate(); useBrokenLinks().collectAnchor(id); return ( { const isTDClick = (e.target as HTMLElement).tagName.toUpperCase() === 'TD'; @@ -68,12 +69,12 @@ function APITableRow({ const shouldNavigate = isTDClick && !hasSelectedText; if (shouldNavigate) { - history.push(anchor); + navigate(anchor); } }} onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter') { - history.push(anchor); + navigate(anchor); } }}> {children.props.children} From a67205a9dcd414f3908f8d006466274e3805d55d Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 30 Oct 2026 12:05:32 +0100 Subject: [PATCH 2/2] test(core): cover parent routes sharing the same path (AI-assisted) Co-Authored-By: Claude Opus 5.5 --- .../src/client/__tests__/routeObjects.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/docusaurus/src/client/__tests__/routeObjects.test.ts b/packages/docusaurus/src/client/__tests__/routeObjects.test.ts index 7e4a8827e73b..39cdeaa590c4 100644 --- a/packages/docusaurus/src/client/__tests__/routeObjects.test.ts +++ b/packages/docusaurus/src/client/__tests__/routeObjects.test.ts @@ -25,6 +25,16 @@ vi.mock('@generated/routes', () => ({ ], }, {path: '/app', component: ''}, + { + path: '/', + component: '', + routes: [{path: '/plugin1-doc', exact: true, component: ''}], + }, + { + path: '/', + component: '', + routes: [{path: '/plugin2-doc', exact: true, component: ''}], + }, {path: '*', component: ''}, ], })); @@ -44,6 +54,11 @@ describe('matchRouteConfigs', () => { ]); }); + it('matches nested routes of parent routes sharing the same path', () => { + expect(matchPaths('/plugin1-doc')).toEqual(['/', '/plugin1-doc']); + expect(matchPaths('/plugin2-doc')).toEqual(['/', '/plugin2-doc']); + }); + it('matches subpaths of non-exact routes', () => { expect(matchPaths('/app')).toEqual(['/app']); expect(matchPaths('/app/sub/path')).toEqual(['/app']); @@ -53,5 +68,6 @@ describe('matchRouteConfigs', () => { expect(matchPaths('/unknown')).toEqual(['*']); expect(matchPaths('/docs/unknown')).toEqual(['*']); expect(matchPaths('/docs/intro/unknown')).toEqual(['*']); + expect(matchPaths('/plugin1-doc/unknown')).toEqual(['*']); }); });