From eb15b229a484e34c44a71cfb113f54e1b958c282 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 19 Jun 2026 19:07:03 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(core):=20upgrade=20React=20Router=20v5?= =?UTF-8?q?=20=E2=86=92=20v8=20(and=20history=20v5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POC migrating Docusaurus from react-router v5 to react-router v8, and from the bundled history v4 to the standalone history package v5 (latest versions). The bulk of the work is the v5 → v6 jump (react-router-config removed, controlled `` removed, `NavLink` `isActive`/`activeClassName` removed, no more exposed `history` object / `useHistory`). v6 → v7 → v8 are mostly package consolidation (`react-router-dom` merged into `react-router`, then dropped) plus ESM-only / Node 22+ / React 19+ requirements. Key changes: - Reimplement `react-router-config`'s `renderRoutes` as a small ``-like component (string-based, recursive, first-match-wins route config), and keep a thin v5-compatible `matchPath`/`matchRoutes` built on top of React Router's own `matchPath` (so we don't hand-roll path matching). React Router v8 is ESM-only, so the CommonJS broken-links checker loads `matchPath` via a dynamic `import('react-router')`. - Drive react-router with a `history`-package instance through the low-level `` (replaces `BrowserRouter`/`HashRouter`/ `StaticRouter` and the removed `unstable_HistoryRouter`); `@docusaurus/router` `useHistory()` returns it so history blocking/listening/querystring helpers keep working. - Replace the controlled `` trick used by `PendingNavigation` with a location-override React context. - Reimplement Docusaurus ``/NavLink active behavior (`isActive`/`activeClassName`/`activeStyle`) on top of react-router's `` + `useLocation`/`matchPath`. - `Redirect` → `Navigate` compatibility shim; adapt `historyUtils` to history v5's new `block()` transition API. Not done (POC scope): full type-safety polish. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../package.json | 7 +- .../src/index.d.ts | 69 ++++++- .../package.json | 1 - .../src/plugin-content-docs.d.ts | 20 +- .../docusaurus-theme-classic/package.json | 2 +- packages/docusaurus-theme-common/package.json | 3 +- .../src/utils/historyUtils.ts | 29 ++- .../src/utils/routesUtils.ts | 8 +- packages/docusaurus-types/package.json | 2 +- packages/docusaurus/package.json | 8 +- .../src/client/PendingNavigation.tsx | 11 +- .../docusaurus/src/client/clientEntry.tsx | 26 +-- packages/docusaurus/src/client/docusaurus.ts | 2 +- .../docusaurus/src/client/exports/Link.tsx | 46 ++++- .../src/client/exports/renderRoutes.ts | 8 - .../src/client/exports/renderRoutes.tsx | 75 +++++++ .../docusaurus/src/client/exports/router.ts | 8 - .../docusaurus/src/client/exports/router.tsx | 131 ++++++++++++ packages/docusaurus/src/client/matchRoutes.ts | 126 +++++++++++ .../src/client/normalizeLocation.ts | 2 +- packages/docusaurus/src/client/preload.ts | 2 +- .../docusaurus/src/client/serverEntry.tsx | 13 +- .../src/server/__tests__/brokenLinks.test.ts | 39 ++-- packages/docusaurus/src/server/brokenLinks.ts | 35 +++- pnpm-lock.yaml | 195 ++++-------------- pnpm-workspace.yaml | 3 + 26 files changed, 605 insertions(+), 266 deletions(-) delete mode 100644 packages/docusaurus/src/client/exports/renderRoutes.ts create mode 100644 packages/docusaurus/src/client/exports/renderRoutes.tsx delete mode 100644 packages/docusaurus/src/client/exports/router.ts create mode 100644 packages/docusaurus/src/client/exports/router.tsx create mode 100644 packages/docusaurus/src/client/matchRoutes.ts diff --git a/packages/docusaurus-module-type-aliases/package.json b/packages/docusaurus-module-type-aliases/package.json index 0949776dde5b..d5c96ae48a55 100644 --- a/packages/docusaurus-module-type-aliases/package.json +++ b/packages/docusaurus-module-type-aliases/package.json @@ -13,12 +13,11 @@ }, "dependencies": { "@docusaurus/types": "3.10.1", - "@types/history": "^4.7.11", "@types/react": "19.2.14", - "@types/react-router-config": "*", - "@types/react-router-dom": "*", + "history": "^5.3.0", "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", + "react-router": "^8.0.1" }, "peerDependencies": { "react": "*", diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index 4ab1e21a54e4..b331c3244f77 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -41,12 +41,17 @@ declare module '@generated/registry' { } declare module '@generated/routes' { - import type {RouteConfig as RRRouteConfig} from 'react-router-config'; import type Loadable from 'react-loadable'; - type RouteConfig = RRRouteConfig & { + // At runtime the route `component` is a react-loadable component (with a + // `.preload()` method), not the string module path used in the route config. + type RouteConfig = { path: string; component: ReturnType; + exact?: boolean; + strict?: boolean; + routes?: RouteConfig[]; + [attribute: string]: unknown; }; const routes: RouteConfig[]; export default routes; @@ -169,9 +174,17 @@ declare module '@docusaurus/Head' { declare module '@docusaurus/Link' { import type {CSSProperties, ComponentProps, ReactNode} from 'react'; - import type {NavLinkProps as RRNavLinkProps} from 'react-router-dom'; - - type NavLinkProps = Partial; + import type {Location} from 'history'; + + // React Router v6+ removed NavLink's `isActive`/`activeClassName`/`exact`/ + // `strict` props; Docusaurus reimplements them, so we declare them ourselves. + type NavLinkProps = { + readonly exact?: boolean; + readonly strict?: boolean; + readonly activeClassName?: string; + readonly activeStyle?: CSSProperties; + readonly isActive?: (match: unknown, location: Location) => boolean; + }; export type Props = NavLinkProps & ComponentProps<'a'> & { readonly className?: string; @@ -260,7 +273,34 @@ declare module '@docusaurus/Translate' { } declare module '@docusaurus/router' { - export {useHistory, useLocation, Redirect, matchPath} from 'react-router-dom'; + import type {ComponentType, ReactNode} from 'react'; + import type {History, Location, To} from 'history'; + + export function useHistory(): History; + export function useLocation(): Location; + export function matchPath( + pathname: string, + options?: + | string + | { + path?: string | string[]; + exact?: boolean; + strict?: boolean; + sensitive?: boolean; + }, + ): { + path: string | undefined; + url: string; + isExact: boolean; + params: {[paramName: string]: string | undefined}; + } | null; + + export type RedirectProps = { + readonly to: To; + readonly push?: boolean; + readonly children?: ReactNode; + }; + export const Redirect: ComponentType; } declare module '@docusaurus/useIsomorphicLayoutEffect' { @@ -351,9 +391,22 @@ declare module '@docusaurus/Noop' { } declare module '@docusaurus/renderRoutes' { - import {renderRoutes} from 'react-router-config'; + import type {ReactElement, ReactNode} from 'react'; + + type RouteConfig = { + path?: string; + component?: unknown; + exact?: boolean; + strict?: boolean; + render?: (props: {[key: string]: unknown}) => ReactNode; + routes?: RouteConfig[]; + [attribute: string]: unknown; + }; - export default renderRoutes; + export default function renderRoutes( + routes: RouteConfig[], + extraProps?: {[propName: string]: unknown}, + ): ReactElement; } declare module '@docusaurus/useGlobalData' { diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json index 45f84b567cbc..7e3e19b08dc6 100644 --- a/packages/docusaurus-plugin-content-docs/package.json +++ b/packages/docusaurus-plugin-content-docs/package.json @@ -44,7 +44,6 @@ "@docusaurus/utils": "3.10.1", "@docusaurus/utils-common": "3.10.1", "@docusaurus/utils-validation": "3.10.1", - "@types/react-router-config": "^5.0.7", "combine-promises": "^1.1.0", "fs-extra": "^11.2.0", "js-yaml": "^4.1.0", 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 f89b3f63169e..d83a18ed18db 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 @@ -635,10 +635,11 @@ 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 '@docusaurus/types'; - export interface Props extends Required {} + export interface Props { + readonly route: RouteConfig; + } export default function DocsRoot(props: Props): ReactNode; } @@ -646,10 +647,10 @@ declare module '@theme/DocsRoot' { declare module '@theme/DocVersionRoot' { import type {ReactNode} from 'react'; import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs'; - import type {RouteConfigComponentProps} from 'react-router-config'; - import type {Required} from 'utility-types'; + import type {RouteConfig} from '@docusaurus/types'; - export interface Props extends Required { + export interface Props { + readonly route: RouteConfig; readonly version: PropVersionMetadata; } @@ -658,10 +659,11 @@ declare module '@theme/DocVersionRoot' { 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 '@docusaurus/types'; - export interface Props extends Required {} + export interface Props { + readonly route: RouteConfig; + } export default function DocRoot(props: Props): ReactNode; } diff --git a/packages/docusaurus-theme-classic/package.json b/packages/docusaurus-theme-classic/package.json index b55b06573f33..099b4a8808dd 100644 --- a/packages/docusaurus-theme-classic/package.json +++ b/packages/docusaurus-theme-classic/package.json @@ -42,7 +42,7 @@ "postcss": "^8.5.12", "prism-react-renderer": "^2.4.1", "prismjs": "^1.29.0", - "react-router-dom": "^5.3.4", + "react-router": "^8.0.1", "rtlcss": "^4.1.0", "tslib": "^2.6.0", "utility-types": "^3.10.0" diff --git a/packages/docusaurus-theme-common/package.json b/packages/docusaurus-theme-common/package.json index cde98294dc37..eabce4586d1b 100644 --- a/packages/docusaurus-theme-common/package.json +++ b/packages/docusaurus-theme-common/package.json @@ -34,10 +34,9 @@ "@docusaurus/module-type-aliases": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-common": "3.10.1", - "@types/history": "^4.7.11", "@types/react": "19.2.14", - "@types/react-router-config": "*", "clsx": "^2.0.0", + "history": "^5.3.0", "parse-numeric-range": "^1.3.0", "prism-react-renderer": "^2.4.1", "tslib": "^2.6.0", diff --git a/packages/docusaurus-theme-common/src/utils/historyUtils.ts b/packages/docusaurus-theme-common/src/utils/historyUtils.ts index 45f9cc22dd01..1324127f8263 100644 --- a/packages/docusaurus-theme-common/src/utils/historyUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/historyUtils.ts @@ -21,11 +21,30 @@ type HistoryBlockHandler = (location: Location, action: Action) => void | false; function useHistoryActionHandler(handler: HistoryBlockHandler): void { const history = useHistory(); const stableHandler = useEvent(handler); - useEffect( + useEffect(() => { + // history v5 changed the block() API: the blocker now receives a + // "transition" object and the navigation stays blocked until `retry()` is + // called. We adapt it to the previous `(location, action) => false` API + // (returning `false` cancels the navigation), re-registering the blocker + // after each allowed navigation. // See https://github.com/remix-run/history/blob/main/docs/blocking-transitions.md - () => history.block((location, action) => stableHandler(location, action)), - [history, stableHandler], - ); + let unblock: () => void = () => {}; + const block = () => { + unblock = history.block((transition) => { + const result = stableHandler(transition.location, transition.action); + if (result === false) { + // Cancel the navigation: keep blocking, don't retry. + return; + } + // Allow the navigation, then re-arm the blocker for next time. + unblock(); + transition.retry(); + block(); + }); + }; + block(); + return () => unblock(); + }, [history, stableHandler]); } /** @@ -51,7 +70,7 @@ export function useHistoryPopHandler(handler: HistoryBlockHandler): void { * @param selector */ export function useHistorySelector( - selector: (history: History) => Value, + selector: (history: History) => Value, ): Value { const history = useHistory(); return useSyncExternalStore( diff --git a/packages/docusaurus-theme-common/src/utils/routesUtils.ts b/packages/docusaurus-theme-common/src/utils/routesUtils.ts index b662ab4bf23d..107f950e14e5 100644 --- a/packages/docusaurus-theme-common/src/utils/routesUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/routesUtils.ts @@ -8,7 +8,7 @@ import {useMemo} from 'react'; import generatedRoutes from '@generated/routes'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import type {RouteConfig} from 'react-router-config'; +import type {RouteConfig} from '@docusaurus/types'; /** * Compare the 2 paths, case insensitive and ignoring trailing slash @@ -69,7 +69,11 @@ export function findHomePageRoute({ export function useHomePageRoute(): RouteConfig | undefined { const {baseUrl} = useDocusaurusContext().siteConfig; return useMemo( - () => findHomePageRoute({routes: generatedRoutes, baseUrl}), + () => + findHomePageRoute({ + routes: generatedRoutes as unknown as RouteConfig[], + baseUrl, + }), [baseUrl], ); } diff --git a/packages/docusaurus-types/package.json b/packages/docusaurus-types/package.json index cbb4a252ae99..408b6ee152fb 100644 --- a/packages/docusaurus-types/package.json +++ b/packages/docusaurus-types/package.json @@ -14,10 +14,10 @@ "license": "MIT", "dependencies": { "@mdx-js/mdx": "^3.1.1", - "@types/history": "^4.7.11", "@types/mdast": "^4.0.2", "@types/react": "19.2.14", "commander": "^5.1.0", + "history": "^5.3.0", "joi": "^18.1.2", "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "utility-types": "^3.10.0", diff --git a/packages/docusaurus/package.json b/packages/docusaurus/package.json index 13ff3da23ad0..779c348d6480 100644 --- a/packages/docusaurus/package.json +++ b/packages/docusaurus/package.json @@ -53,6 +53,7 @@ "eval": "^0.1.8", "execa": "^5.1.1", "fs-extra": "^11.2.0", + "history": "^5.3.0", "html-tags": "^3.3.1", "html-webpack-plugin": "^5.6.7", "leven": "^3.1.0", @@ -63,9 +64,7 @@ "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", "react-loadable-ssr-addon-v5-slorber": "^1.0.3", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", + "react-router": "^8.0.1", "semver": "^7.7.4", "serve-handler": "^6.1.7", "tinypool": "^2.1.0", @@ -81,10 +80,7 @@ "@docusaurus/types": "3.10.1", "@total-typescript/shoehorn": "^0.1.2", "@types/escape-html": "^1.0.4", - "@types/history": "^4.7.11", "@types/react-dom": "^19.2.3", - "@types/react-router-dom": "^5.3.3", - "@types/react-router-config": "^5.0.7", "@types/serve-handler": "^6.1.4", "@types/update-notifier": "^6.0.4", "@types/webpack-bundle-analyzer": "^4.7.0", diff --git a/packages/docusaurus/src/client/PendingNavigation.tsx b/packages/docusaurus/src/client/PendingNavigation.tsx index 80855c16fed4..714e66680f4a 100644 --- a/packages/docusaurus/src/client/PendingNavigation.tsx +++ b/packages/docusaurus/src/client/PendingNavigation.tsx @@ -6,11 +6,11 @@ */ import React, {type ReactNode} from 'react'; -import {Route} from 'react-router-dom'; import ClientLifecyclesDispatcher, { dispatchLifecycleAction, } from './ClientLifecyclesDispatcher'; import ExecutionEnvironment from './exports/ExecutionEnvironment'; +import {LocationOverrideProvider} from './exports/router'; import preload from './preload'; import type {Location} from 'history'; @@ -82,13 +82,16 @@ 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. + // Use a location override context to trick all descendants into rendering + // the old location until the next route has loaded (React Router v6+ removed + // the controlled `` API used previously). return ( - children} /> + + {children} + ); } diff --git a/packages/docusaurus/src/client/clientEntry.tsx b/packages/docusaurus/src/client/clientEntry.tsx index db521bb39f50..fbad4ab34af1 100644 --- a/packages/docusaurus/src/client/clientEntry.tsx +++ b/packages/docusaurus/src/client/clientEntry.tsx @@ -5,24 +5,17 @@ * LICENSE file in the root directory of this source tree. */ -import React, {startTransition, type ReactNode} from 'react'; +import React, {startTransition} from 'react'; import ReactDOM, {type ErrorInfo} from 'react-dom/client'; import {HelmetProvider} from 'react-helmet-async'; -import {BrowserRouter, HashRouter} from 'react-router-dom'; +import {createBrowserHistory, createHashHistory} from 'history'; import siteConfig from '@generated/docusaurus.config'; import ExecutionEnvironment from './exports/ExecutionEnvironment'; +import {DocusaurusRouter} from './exports/router'; import App from './App'; import preload from './preload'; import docusaurus from './docusaurus'; -function Router({children}: {children: ReactNode}): ReactNode { - return siteConfig.future.experimental_router === 'hash' ? ( - {children} - ) : ( - {children} - ); -} - const hydrate = Boolean(process.env.HYDRATE_CLIENT_ENTRY); // Client-side render (e.g: running in browser) to become single-page @@ -31,11 +24,20 @@ if (ExecutionEnvironment.canUseDOM) { window.docusaurus = docusaurus; const container = document.getElementById('__docusaurus')!; + // React Router v6+ no longer exposes a mutable history object, so Docusaurus + // creates its own (from the `history` package) and drives React Router with it + // through . This powers useHistory() (blocking, listening, + // querystring updates). + const history = + siteConfig.future.experimental_router === 'hash' + ? createHashHistory() + : createBrowserHistory(); + const app = ( - + - + ); diff --git a/packages/docusaurus/src/client/docusaurus.ts b/packages/docusaurus/src/client/docusaurus.ts index 24dff371728b..10768f458697 100644 --- a/packages/docusaurus/src/client/docusaurus.ts +++ b/packages/docusaurus/src/client/docusaurus.ts @@ -5,9 +5,9 @@ * 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 {matchRoutes} from './matchRoutes'; import prefetchHelper from './prefetch'; import preloadHelper from './preload'; import flat from './flat'; diff --git a/packages/docusaurus/src/client/exports/Link.tsx b/packages/docusaurus/src/client/exports/Link.tsx index 1fab775967ee..228d340e9371 100644 --- a/packages/docusaurus/src/client/exports/Link.tsx +++ b/packages/docusaurus/src/client/exports/Link.tsx @@ -9,16 +9,16 @@ import React, { useEffect, useImperativeHandle, useRef, - type ComponentType, type ReactNode, } from 'react'; -import {NavLink, Link as RRLink} from 'react-router-dom'; +import {Link as RRLink} from 'react-router'; import {applyTrailingSlash} from '@docusaurus/utils-common'; import useDocusaurusContext from './useDocusaurusContext'; import isInternalUrl from './isInternalUrl'; import ExecutionEnvironment from './ExecutionEnvironment'; import useBrokenLinks from './useBrokenLinks'; import {useBaseUrlUtils} from './useBaseUrl'; +import {useLocation, matchPath} from './router'; import type {Props} from '@docusaurus/Link'; // TODO all this wouldn't be necessary if we used ReactRouter basename feature @@ -33,7 +33,10 @@ function Link({ to, href, activeClassName, + activeStyle, isActive, + exact, + strict, 'data-noBrokenLinkCheck': noBrokenLinkCheck, autoAddBaseUrl = true, ...props @@ -93,7 +96,24 @@ function Link({ } const preloaded = useRef(false); - const LinkComponent = (isNavLink ? NavLink : RRLink) as ComponentType; + + // React Router v6+ removed NavLink's `isActive`/`activeClassName` props, so we + // compute the active state ourselves to preserve Docusaurus' NavLink behavior + // (including the custom `isActive(match, location)` signature used by navbar + // items for `activeBasePath`/`activeBaseRegex` etc.). + const location = useLocation(); + const navLinkActive = + isNavLink && + (() => { + const match = targetLink + ? matchPath(location.pathname, { + path: targetLink, + exact: exact ?? false, + strict: strict ?? false, + }) + : null; + return isActive ? Boolean(isActive(match, location)) : Boolean(match); + })(); const IOSupported = ExecutionEnvironment.canUseIntersectionObserver; @@ -190,15 +210,23 @@ function Link({ {...testOnlyProps} /> ) : ( - ); 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/renderRoutes.tsx b/packages/docusaurus/src/client/exports/renderRoutes.tsx new file mode 100644 index 000000000000..ceca30204c54 --- /dev/null +++ b/packages/docusaurus/src/client/exports/renderRoutes.tsx @@ -0,0 +1,75 @@ +/** + * 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 React, { + type ComponentType, + type ReactElement, + type ReactNode, +} from 'react'; +import {matchPath, type Match} from '../matchRoutes'; +import {useLocation} from './router'; + +// React Router v6+ removed `react-router-config` (and its `renderRoutes`). +// Docusaurus relies on a string-based, recursive route config where each route +// component renders its own subroutes via `renderRoutes(route.routes)`. We +// reimplement the v5 `` semantics (first-match-wins against the full, +// absolute pathname) on top of our dependency-free `matchPath`. + +type RouteToRender = { + path?: string; + exact?: boolean; + strict?: boolean; + // The route config types `component` as a string (a module path), but at + // runtime it is a react-loadable component. We keep it `unknown` here so that + // both the config-time and runtime route shapes can be passed in. + component?: unknown; + render?: (props: {[key: string]: unknown}) => ReactNode; + routes?: RouteToRender[]; + [attribute: string]: unknown; +}; + +function RenderRoutes({ + routes, + extraProps, +}: { + routes: RouteToRender[]; + extraProps: {[propName: string]: unknown}; +}): ReactNode { + const location = useLocation(); + + for (const route of routes) { + const match: Match | null = route.path + ? matchPath(location.pathname, route) + : {path: undefined, url: location.pathname, isExact: true, params: {}}; + + if (match) { + const routeProps = {...extraProps, location, match, route}; + if (route.render) { + return route.render(routeProps); + } + const Component = route.component as + | ComponentType<{[key: string]: unknown}> + | undefined; + return Component ? : null; + } + } + + return null; +} + +/** + * Renders the first route (in order) whose `path` matches the current location, + * passing it the `route` config so it can render its own subroutes. Mirrors the + * `react-router-config` `renderRoutes` signature so existing callers (and + * swizzled theme components) keep working. + */ +export default function renderRoutes( + routes: RouteToRender[], + extraProps: {[propName: string]: unknown} = {}, +): ReactElement { + return ; +} diff --git a/packages/docusaurus/src/client/exports/router.ts b/packages/docusaurus/src/client/exports/router.ts deleted file mode 100644 index 453530b353dd..000000000000 --- a/packages/docusaurus/src/client/exports/router.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 {useHistory, useLocation, Redirect, matchPath} from 'react-router-dom'; diff --git a/packages/docusaurus/src/client/exports/router.tsx b/packages/docusaurus/src/client/exports/router.tsx new file mode 100644 index 000000000000..d9f1276bee29 --- /dev/null +++ b/packages/docusaurus/src/client/exports/router.tsx @@ -0,0 +1,131 @@ +/** + * 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 React, { + createContext, + useContext, + useState, + type ComponentProps, + type ReactNode, +} from 'react'; +import { + Router, + Navigate, + useLocation as useReactRouterLocation, +} from 'react-router'; +import {matchPath} from '../matchRoutes'; +import ExecutionEnvironment from './ExecutionEnvironment'; +import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'; +import type {History, Location, To} from 'history'; + +// Re-export the v5-compatible matchPath: React Router v6+ changed the matchPath +// signature/return shape, so we keep our own to preserve the public API. +export {matchPath}; +export type {History, Location}; + +/** + * Holds the `history` (from the `history` package) instance that drives React + * Router. React Router v6+ no longer exposes a `useHistory()` hook nor a + * mutable history object, so Docusaurus creates its own and shares it through + * context. + * This is what powers `useHistory()`, used for navigation blocking, listening, + * and querystring updates. + */ +const HistoryContext = createContext(null); + +/** + * Lets `` override the location seen by route descendants + * while a navigation is pending (it keeps rendering the previous location until + * the next route's code has been preloaded). This replaces React Router v5's + * controlled `` trick, which no longer exists in v6+. + */ +const LocationOverrideContext = createContext(null); + +export function useHistory(): History { + const history = useContext(HistoryContext); + if (!history) { + throw new Error( + 'Docusaurus useHistory() was called outside of the Docusaurus router.', + ); + } + return history; +} + +export function useLocation(): Location { + const overriddenLocation = useContext(LocationOverrideContext); + const location = useReactRouterLocation(); + return (overriddenLocation ?? location) as Location; +} + +export type RedirectProps = { + readonly to: To; + /** When `true`, pushes a new entry instead of replacing the current one. */ + readonly push?: boolean; + readonly children?: ReactNode; +}; + +/** + * Backward-compatible `` (removed in React Router v6+), built on + * top of ``. + */ +export function Redirect({to, push = false}: RedirectProps): ReactNode { + return ; +} + +/** + * Provides the location override to route descendants. Used by + * `` to keep showing the current route until the next route + * has loaded. + */ +export function LocationOverrideProvider({ + location, + children, +}: { + location: Location; + children: ReactNode; +}): ReactNode { + return ( + + {children} + + ); +} + +/** + * The Docusaurus router. It drives React Router with a `history` instance + * (browser/hash on the client, memory on the server) using the low-level + * `` component, mirroring what React Router's own `BrowserRouter` and + * (removed) `unstable_HistoryRouter` do internally. This is what allows + * `useHistory()` to keep returning a real, listenable/blockable history object. + */ +export function DocusaurusRouter({ + history, + children, +}: { + history: History; + children: ReactNode; +}): ReactNode { + const [state, setState] = useState({ + action: history.action, + location: history.location, + }); + useIsomorphicLayoutEffect(() => history.listen(setState), [history]); + + return ( + + ['navigator'] + } + static={!ExecutionEnvironment.canUseDOM}> + {children} + + + ); +} diff --git a/packages/docusaurus/src/client/matchRoutes.ts b/packages/docusaurus/src/client/matchRoutes.ts new file mode 100644 index 000000000000..94d1d94ab503 --- /dev/null +++ b/packages/docusaurus/src/client/matchRoutes.ts @@ -0,0 +1,126 @@ +/** + * 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 {matchPath as reactRouterMatchPath} from 'react-router'; + +// React Router v6+ removed `react-router-config` (`renderRoutes`/`matchRoutes`) +// and changed `matchPath`'s signature/return shape. Docusaurus' route config is +// string-based, recursive (`routes`), and order-based (first-match-wins), so we +// keep two thin helpers on top of React Router's own `matchPath`: +// - a v5-compatible `matchPath(pathname, {path, exact, strict})` (also the +// public `@docusaurus/router` API), and +// - a `matchRoutes` that walks the recursive route config. + +export type Match = { + /** The route path pattern that was matched. */ + path: string | undefined; + /** The matched portion of the URL. */ + url: string; + /** Whether the match consumed the whole pathname. */ + isExact: boolean; + /** The values of the dynamic params declared in the path pattern. */ + params: {[paramName: string]: string | undefined}; +}; + +export type MatchPathOptions = { + path?: string | string[]; + exact?: boolean; + // Trailing-slash sensitivity. React Router v6+ `matchPath` has no `strict` + // option and tolerates trailing slashes; kept for API compatibility. + strict?: boolean; + sensitive?: boolean; +}; + +/** + * React Router v5-compatible `matchPath`, implemented on top of React Router's + * own `matchPath`. Kept for the public `@docusaurus/router` API and internal + * usage: `matchPath(pathname, {path, exact, strict, sensitive})`. + */ +export function matchPath( + pathname: string, + options: MatchPathOptions | string = {}, +): Match | null { + const { + path, + exact = false, + strict = false, + sensitive = false, + } = typeof options === 'string' ? {path: options} : options; + + const patterns = ([] as (string | undefined)[]).concat(path); + for (const pattern of patterns) { + // A route without a path always matches (it inherits the parent match). + if (pattern === undefined || pattern === null) { + return {path: pattern, url: pathname, isExact: true, params: {}}; + } + const match = reactRouterMatchPath( + {path: pattern, end: exact, caseSensitive: sensitive}, + pathname, + ); + // React Router's matchPath is tolerant of trailing slashes and has no + // `strict` option, so we re-enforce trailing-slash sensitivity ourselves: + // a strict pattern ending with "/" only matches a pathname that has it. + if (match && strict && pattern.endsWith('/') && pattern !== '/') { + const actual = sensitive ? pathname : pathname.toLowerCase(); + const expected = sensitive ? pattern : pattern.toLowerCase(); + if (!actual.startsWith(expected)) { + continue; + } + } + if (match) { + return { + path: pattern, + url: match.pathname, + isExact: match.pathname === pathname, + params: match.params, + }; + } + } + return null; +} + +// Minimal structural route type, kept generic so callers preserve their richer +// route type (e.g. the runtime `component` with its `.preload()` method). +type RouteLike = { + path?: string; + exact?: boolean; + strict?: boolean; + routes?: unknown; +}; + +export type MatchedRoute = {route: Route; match: Match}; + +/** + * Walks the recursive route config in order (first-match-wins, like the v5 + * `` / removed `react-router-config`), recursing into the `routes` of + * the first matching route. Returns the matched branch. + */ +export function matchRoutes( + routes: Route[], + pathname: string, + branch: Array> = [], +): Array> { + routes.some((route) => { + let match: Match | null; + if (route.path) { + match = matchPath(pathname, route); + } else if (branch.length) { + match = branch[branch.length - 1]!.match; + } else { + match = {path: '/', url: '/', params: {}, isExact: pathname === '/'}; + } + + if (match) { + branch.push({route, match}); + if (Array.isArray(route.routes)) { + matchRoutes(route.routes as Route[], pathname, branch); + } + } + return Boolean(match); + }); + return branch; +} diff --git a/packages/docusaurus/src/client/normalizeLocation.ts b/packages/docusaurus/src/client/normalizeLocation.ts index 74192a451c08..d2974475b658 100644 --- a/packages/docusaurus/src/client/normalizeLocation.ts +++ b/packages/docusaurus/src/client/normalizeLocation.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import {matchRoutes} from 'react-router-config'; import routes from '@generated/routes'; +import {matchRoutes} from './matchRoutes'; import type {Location} from 'history'; // Memoize previously normalized pathnames. diff --git a/packages/docusaurus/src/client/preload.ts b/packages/docusaurus/src/client/preload.ts index 08f7c81cad9f..c53a11a590c2 100644 --- a/packages/docusaurus/src/client/preload.ts +++ b/packages/docusaurus/src/client/preload.ts @@ -6,7 +6,7 @@ */ import routes from '@generated/routes'; -import {matchRoutes} from 'react-router-config'; +import {matchRoutes} from './matchRoutes'; /** * Helper function to make sure all async components for that particular route diff --git a/packages/docusaurus/src/client/serverEntry.tsx b/packages/docusaurus/src/client/serverEntry.tsx index 1ca10a1d3826..a3963663d386 100644 --- a/packages/docusaurus/src/client/serverEntry.tsx +++ b/packages/docusaurus/src/client/serverEntry.tsx @@ -6,11 +6,12 @@ */ import React from 'react'; -import {StaticRouter} from 'react-router-dom'; +import {createMemoryHistory} from 'history'; import {HelmetProvider, type FilledContext} from 'react-helmet-async'; import Loadable from 'react-loadable'; import {renderToHtml} from './renderToHtml'; import preload from './preload'; +import {DocusaurusRouter} from './exports/router'; import App from './App'; import { createStatefulBrokenLinks, @@ -23,19 +24,23 @@ const render: AppRenderer['render'] = async ({pathname}) => { await preload(pathname); const modules = new Set(); - const routerContext = {}; const helmetContext = {}; const statefulBrokenLinks = createStatefulBrokenLinks(); + // We use a memory history seeded with the rendered pathname. Driving React + // Router with a real history object (instead of ) keeps + // useHistory() working uniformly on both server and client. + const history = createMemoryHistory({initialEntries: [pathname]}); + const app = ( // @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 e35d81d111cd..96cf9135157a 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -6,18 +6,17 @@ */ 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, - matchRoutes: vi.fn(actual.matchRoutes), + matchPath: vi.fn(actual.matchPath), }; }); @@ -875,8 +874,8 @@ describe('handleBrokenLinks', () => { `); }); - it('is performant and minimize calls to matchRoutes', async () => { - const matchRoutesMock = vi.mocked(reactRouterConfig.matchRoutes); + it('is performant and minimizes route matching work', async () => { + const matchPathMock = vi.mocked(reactRouter.matchPath); vi.clearAllMocks(); const scale = 100; @@ -925,15 +924,25 @@ describe('handleBrokenLinks', () => { }); // console.timeEnd('testBrokenLinks'); - // Idiomatic code calling matchRoutes multiple times is not performant - // We try to minimize the calls to this expensive function - // Otherwise large sites will have super long execution times + // Matching every link against every route is not performant. + // We minimize the expensive matching work, otherwise large sites would have + // super long execution times. // See https://github.com/facebook/docusaurus/issues/9754 // See https://x.com/sebastienlorber/status/1749392773415858587 - // We expect no more matchRoutes calls than number of dynamic route links - expect(matchRoutesMock).toHaveBeenCalledTimes(scale * 2); - // We expect matchRoutes to be called with a reduced number of routes expect(routes).toHaveLength(scale * 3); - expect(matchRoutesMock.mock.calls[0]![0]).toHaveLength(scale * 2); + // Only dynamic/non-exact pathnames need matching (exact ones are + // pre-validated), and thanks to caching each is matched exactly once - so + // matchPath sees no more than `scale * 2` distinct pathnames. + const matchedPathnames = new Set( + matchPathMock.mock.calls.map((call) => call[1]), + ); + expect(matchedPathnames.size).toBe(scale * 2); + // The pre-validated exact routes are excluded from matching entirely. + const matchedAgainstExactRoute = matchPathMock.mock.calls.some((call) => { + const pattern = call[0]; + const path = typeof pattern === 'string' ? pattern : pattern.path; + return /^\/page\d+$/.test(path ?? ''); + }); + expect(matchedAgainstExactRoute).toBe(false); }); }); diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 3ee1971f8128..dfbd3e44761f 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -7,22 +7,18 @@ import _ from 'lodash'; import logger from '@docusaurus/logger'; -import {matchRoutes as reactRouterMatchRoutes} from 'react-router-config'; +import {addTrailingSlash, removeTrailingSlash} from '@docusaurus/utils-common'; import { parseURLPath, serializeURLPath, flattenRoutes, type URLPath, } from '@docusaurus/utils'; -import {addTrailingSlash, 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); -} +// react-router is ESM-only and this module runs as CommonJS, so matchPath is +// loaded via a dynamic import (see handleBrokenLinks) and threaded down. +type MatchPath = typeof import('react-router').matchPath; type BrokenLink = { link: string; @@ -54,9 +50,11 @@ type BrokenLinksHelper = { function createBrokenLinksHelper({ collectedLinks, routes, + matchPath, }: { collectedLinks: CollectedLinksNormalized; routes: RouteConfig[]; + matchPath: MatchPath; }): BrokenLinksHelper { const validPathnames = new Set(collectedLinks.keys()); @@ -89,7 +87,19 @@ function createBrokenLinksHelper({ })(); function isPathnameMatchingAnyRoute(pathname: string): boolean { - if (matchRoutes(remainingRoutes, pathname).length > 0) { + const matchesRoute = (route: RouteConfig): boolean => { + if (!matchPath({path: route.path, end: !!route.exact}, pathname)) { + return false; + } + // React Router's matchPath ignores trailing slashes and has no `strict` + // option, so we honor strict routes here: a strict pattern ending with + // "/" only matches a pathname that includes that trailing slash. + if (route.strict && route.path.endsWith('/') && route.path !== '/') { + return pathname.startsWith(route.path); + } + return true; + }; + if (remainingRoutes.some(matchesRoute)) { // IMPORTANT: this is an optimization // See https://github.com/facebook/docusaurus/issues/9754 // Large Docusaurus sites have many routes! @@ -201,15 +211,18 @@ function filterIntermediateRoutes(routesInput: RouteConfig[]): RouteConfig[] { function getBrokenLinks({ collectedLinks, routes, + matchPath, }: { collectedLinks: CollectedLinksNormalized; routes: RouteConfig[]; + matchPath: MatchPath; }): BrokenLinksMap { const filteredRoutes = filterIntermediateRoutes(routes); const helper = createBrokenLinksHelper({ collectedLinks, routes: filteredRoutes, + matchPath, }); const result: BrokenLinksMap = {}; @@ -413,9 +426,13 @@ export async function handleBrokenLinks({ if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') { return; } + // react-router is ESM-only, so we load matchPath via a dynamic import here + // (this module runs as CommonJS, which can import ESM dynamically). + const {matchPath} = await import('react-router'); const brokenLinks = getBrokenLinks({ routes, collectedLinks: normalizeCollectedLinks(collectedLinks), + matchPath, }); reportBrokenLinks({brokenLinks, onBrokenLinks, onBrokenAnchors}); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73e67963d488..2cf2407bdf47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -394,6 +394,9 @@ importers: fs-extra: specifier: ^11.2.0 version: 11.3.5 + history: + specifier: ^5.3.0 + version: 5.3.0 html-tags: specifier: ^3.3.1 version: 3.3.1 @@ -431,14 +434,8 @@ importers: specifier: ^1.0.3 version: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.6))(webpack@5.107.2(@swc/core@1.15.40)(postcss@8.5.15)) react-router: - specifier: ^5.3.4 - version: 5.3.4(react@19.2.6) - react-router-config: - specifier: ^5.1.1 - version: 5.1.1(react-router@5.3.4(react@19.2.6))(react@19.2.6) - react-router-dom: - specifier: ^5.3.4 - version: 5.3.4(react@19.2.6) + specifier: ^8.0.1 + version: 8.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) semver: specifier: ^7.7.4 version: 7.8.1 @@ -479,18 +476,9 @@ importers: '@types/escape-html': specifier: ^1.0.4 version: 1.0.4 - '@types/history': - specifier: ^4.7.11 - version: 4.7.11 '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) - '@types/react-router-config': - specifier: ^5.0.7 - version: 5.0.11 - '@types/react-router-dom': - specifier: ^5.3.3 - version: 5.3.3 '@types/serve-handler': specifier: ^6.1.4 version: 6.1.4 @@ -840,18 +828,12 @@ importers: '@docusaurus/types': specifier: 3.10.1 version: link:../docusaurus-types - '@types/history': - specifier: ^4.7.11 - version: 4.7.11 '@types/react': specifier: 19.2.14 version: 19.2.14 - '@types/react-router-config': - specifier: '*' - version: 5.0.11 - '@types/react-router-dom': - specifier: '*' - version: 5.3.3 + history: + specifier: ^5.3.0 + version: 5.3.0 react: specifier: '*' version: 19.2.6 @@ -864,6 +846,9 @@ importers: react-loadable: specifier: npm:@docusaurus/react-loadable@6.0.0 version: '@docusaurus/react-loadable@6.0.0(react@19.2.6)' + react-router: + specifier: ^8.0.1 + version: 8.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) packages/docusaurus-plugin-client-redirects: dependencies: @@ -1010,9 +995,6 @@ importers: '@docusaurus/utils-validation': specifier: 3.10.1 version: link:../docusaurus-utils-validation - '@types/react-router-config': - specifier: ^5.0.7 - version: 5.0.11 combine-promises: specifier: ^1.1.0 version: 1.2.0 @@ -1591,9 +1573,9 @@ importers: react-dom: specifier: ^19.2.5 version: 19.2.6(react@19.2.6) - react-router-dom: - specifier: ^5.3.4 - version: 5.3.4(react@19.2.6) + react-router: + specifier: ^8.0.1 + version: 8.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) rtlcss: specifier: ^4.1.0 version: 4.3.0 @@ -1631,18 +1613,15 @@ importers: '@docusaurus/utils-common': specifier: 3.10.1 version: link:../docusaurus-utils-common - '@types/history': - specifier: ^4.7.11 - version: 4.7.11 '@types/react': specifier: 19.2.14 version: 19.2.14 - '@types/react-router-config': - specifier: '*' - version: 5.0.11 clsx: specifier: ^2.0.0 version: 2.1.1 + history: + specifier: ^5.3.0 + version: 5.3.0 parse-numeric-range: specifier: ^1.3.0 version: 1.3.0 @@ -1869,9 +1848,6 @@ importers: '@mdx-js/mdx': specifier: ^3.1.1 version: 3.1.1 - '@types/history': - specifier: ^4.7.11 - version: 4.7.11 '@types/mdast': specifier: ^4.0.2 version: 4.0.4 @@ -1881,6 +1857,9 @@ importers: commander: specifier: ^5.1.0 version: 5.1.0 + history: + specifier: ^5.3.0 + version: 5.3.0 joi: specifier: ^18.1.2 version: 18.2.1 @@ -5857,9 +5836,6 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/history@4.7.11': - resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} - '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} @@ -5955,15 +5931,6 @@ packages: peerDependencies: '@types/react': ^19.2.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.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} @@ -7143,6 +7110,9 @@ packages: resolution: {integrity: sha512-JtXpxqDqJ8P0UwEHwhxLzCIXQy97vlYBZR222Sbzb1q1Erex9ASrztJ29SyhWFQjod1AeFBaPzEEC8YvtZMIYg==} engines: {node: '>=6'} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} @@ -8694,11 +8664,8 @@ 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==} + history@5.3.0: + resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==} hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} @@ -9237,9 +9204,6 @@ packages: resolution: {integrity: sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==} engines: {node: '>=12'} - isarray@0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -10644,9 +10608,6 @@ packages: path-to-regexp@0.1.13: resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} - 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==} @@ -11376,21 +11337,15 @@ packages: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.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==} + react-router@8.0.1: + resolution: {integrity: sha512-5EL/fANovVUhRK50NLS8RYfX0BxrimoKsHWUPPy8v5UEl8i6vzF7e4POo3u+AhPItDwccUAJjMfIOmydxBJmQw==} + engines: {node: '>=22.22.0'} peerDependencies: - react: '>=15' - - react-router@5.3.4: - resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} - peerDependencies: - react: '>=15' + react: '>=19.2.7' + react-dom: '>=19.2.7' + peerDependenciesMeta: + react-dom: + optional: true react@16.14.0: resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==} @@ -12413,12 +12368,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@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -12830,9 +12779,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'} @@ -17321,8 +17267,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/history@4.7.11': {} - '@types/html-minifier-terser@6.1.0': {} '@types/html-minifier-terser@7.0.2': {} @@ -17404,23 +17348,6 @@ snapshots: dependencies: '@types/react': 19.2.14 - '@types/react-router-config@5.0.11': - dependencies: - '@types/history': 4.7.11 - '@types/react': 19.2.14 - '@types/react-router': 5.1.20 - - '@types/react-router-dom@5.3.3': - dependencies: - '@types/history': 4.7.11 - '@types/react': 19.2.14 - '@types/react-router': 5.1.20 - - '@types/react-router@5.1.20': - dependencies: - '@types/history': 4.7.11 - '@types/react': 19.2.14 - '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -18773,6 +18700,8 @@ snapshots: lodash.clonedeep: 4.5.0 yargs-parser: 20.2.9 + cookie-es@3.1.1: {} + cookie-signature@1.0.7: {} cookie@0.7.2: {} @@ -20657,18 +20586,9 @@ snapshots: highlight.js@10.7.3: {} - history@4.10.1: + history@5.3.0: 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: {} @@ -21175,8 +21095,6 @@ snapshots: is-yarn-global@0.4.1: {} - isarray@0.0.1: {} - isarray@1.0.0: {} isarray@2.0.5: {} @@ -23257,10 +23175,6 @@ snapshots: path-to-regexp@0.1.13: {} - path-to-regexp@1.9.0: - dependencies: - isarray: 0.0.1 - path-to-regexp@3.3.0: {} path-type@3.0.0: @@ -23998,35 +23912,12 @@ snapshots: react-refresh@0.18.0: {} - react-router-config@5.1.1(react-router@5.3.4(react@19.2.6))(react@19.2.6): - dependencies: - '@babel/runtime': 7.29.7 - react: 19.2.6 - react-router: 5.3.4(react@19.2.6) - - react-router-dom@5.3.4(react@19.2.6): - dependencies: - '@babel/runtime': 7.29.7 - history: 4.10.1 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 19.2.6 - react-router: 5.3.4(react@19.2.6) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - - react-router@5.3.4(react@19.2.6): + react-router@8.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6): 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 + cookie-es: 3.1.1 react: 19.2.6 - react-is: 16.13.1 - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 + optionalDependencies: + react-dom: 19.2.6(react@19.2.6) react@16.14.0: dependencies: @@ -25360,10 +25251,6 @@ snapshots: thunky@1.1.0: {} - tiny-invariant@1.3.3: {} - - tiny-warning@1.0.3: {} - tinybench@2.9.0: {} tinycolor2@1.6.0: {} @@ -25762,8 +25649,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 acbd9a90c99b..8fa592fce132 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -28,6 +28,9 @@ minimumReleaseAgeStrict: true minimumReleaseAgeIgnoreMissingTime: false minimumReleaseAgeExclude: - tar@7.5.16 + # react-router v8 was released only 1-2 days ago (POC upgrade) + - react-router@8.0.1 + - react-router@8.0.0 trustPolicy: no-downgrade trustPolicyIgnoreAfter: 525600 # ignore packages published 1+ year ago From 26c51a7ef0dc4358f2a175a797366c923e17a6aa Mon Sep 17 00:00:00 2001 From: slorber <749374+slorber@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:21:13 +0000 Subject: [PATCH 2/2] refactor: apply lint autofix --- project-words.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/project-words.txt b/project-words.txt index c1fc134eee5e..467cba3345ee 100644 --- a/project-words.txt +++ b/project-words.txt @@ -19,6 +19,7 @@ Autolinks Bartosz beforeinstallprompt Bhatt +blockable blockquotes Bokmål bunfig