diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx index af8ae89b04..baa3cf27d2 100644 --- a/packages/react-router/lib/dom-export/hydrated-router.tsx +++ b/packages/react-router/lib/dom-export/hydrated-router.tsx @@ -24,6 +24,10 @@ import { matchRoutes, } from "react-router"; import { RouterProvider } from "./dom-router-provider"; +import { + defaultLoadRouteModule, + LoadRouteModuleFunction, +} from "../dom/ssr/routeModules"; type SSRInfo = { context: NonNullable<(typeof window)["__reactRouterContext"]>; @@ -60,7 +64,11 @@ function initSsrInfo(): void { } } -function createHydratedRouter(): DataRouter { +function createHydratedRouter({ + loadRouteModule = defaultLoadRouteModule, +}: { + loadRouteModule?: LoadRouteModuleFunction; +} = {}): DataRouter { initSsrInfo(); if (!ssrInfo) { @@ -102,7 +110,8 @@ function createHydratedRouter(): DataRouter { ssrInfo.manifest.routes, ssrInfo.routeModules, ssrInfo.context.state, - ssrInfo.context.isSpaMode + ssrInfo.context.isSpaMode, + loadRouteModule ); let hydrationData: HydrationState | undefined = undefined; @@ -177,7 +186,8 @@ function createHydratedRouter(): DataRouter { ssrInfo.manifest, ssrInfo.routeModules, ssrInfo.context.isSpaMode, - ssrInfo.context.basename + ssrInfo.context.basename, + loadRouteModule ), }); ssrInfo.router = router; @@ -201,9 +211,13 @@ function createHydratedRouter(): DataRouter { /** * @category Router Components */ -export function HydratedRouter() { +export function HydratedRouter({ + loadRouteModule = defaultLoadRouteModule, +}: { + loadRouteModule?: LoadRouteModuleFunction; +}) { if (!router) { - router = createHydratedRouter(); + router = createHydratedRouter({ loadRouteModule }); } // Critical CSS can become stale after code changes, e.g. styles might be @@ -247,7 +261,8 @@ export function HydratedRouter() { router, ssrInfo.manifest, ssrInfo.routeModules, - ssrInfo.context.isSpaMode + ssrInfo.context.isSpaMode, + loadRouteModule ); // We need to include a wrapper RemixErrorBoundary here in case the root error @@ -265,6 +280,7 @@ export function HydratedRouter() { future: ssrInfo.context.future, criticalCss, isSpaMode: ssrInfo.context.isSpaMode, + loadRouteModule, }} > diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index eb292bd7e0..861d18974c 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -288,7 +288,7 @@ export function PrefetchPageLinks({ } function useKeyedPrefetchLinks(matches: AgnosticDataRouteMatch[]) { - let { manifest, routeModules } = useFrameworkContext(); + let { manifest, routeModules, loadRouteModule } = useFrameworkContext(); let [keyedPrefetchLinks, setKeyedPrefetchLinks] = React.useState< KeyedHtmlLinkDescriptor[] @@ -297,13 +297,16 @@ function useKeyedPrefetchLinks(matches: AgnosticDataRouteMatch[]) { React.useEffect(() => { let interrupted: boolean = false; - void getKeyedPrefetchLinks(matches, manifest, routeModules).then( - (links) => { + void getKeyedPrefetchLinks( + matches, + manifest, + routeModules, + loadRouteModule + ).then((links) => { if (!interrupted) { setKeyedPrefetchLinks(links); } - } - ); + }); return () => { interrupted = true; diff --git a/packages/react-router/lib/dom/ssr/entry.ts b/packages/react-router/lib/dom/ssr/entry.ts index 18094245de..e9c0b2f429 100644 --- a/packages/react-router/lib/dom/ssr/entry.ts +++ b/packages/react-router/lib/dom/ssr/entry.ts @@ -1,7 +1,7 @@ import type { StaticHandlerContext } from "../../router/router"; import type { EntryRoute } from "./routes"; -import type { RouteModules } from "./routeModules"; +import type { LoadRouteModuleFunction, RouteModules } from "./routeModules"; import type { RouteManifest } from "../../router/utils"; type SerializedError = { @@ -17,6 +17,7 @@ export interface FrameworkContextObject { serverHandoffString?: string; future: FutureConfig; isSpaMode: boolean; + loadRouteModule: LoadRouteModuleFunction; serializeError?(error: Error): SerializedError; renderMeta?: { didRenderScripts?: boolean; diff --git a/packages/react-router/lib/dom/ssr/fog-of-war.ts b/packages/react-router/lib/dom/ssr/fog-of-war.ts index a9bfa8de6d..12a9349997 100644 --- a/packages/react-router/lib/dom/ssr/fog-of-war.ts +++ b/packages/react-router/lib/dom/ssr/fog-of-war.ts @@ -4,7 +4,7 @@ import type { Router as DataRouter } from "../../router/router"; import type { RouteManifest } from "../../router/utils"; import { matchRoutes } from "../../router/utils"; import type { AssetsManifest } from "./entry"; -import type { RouteModules } from "./routeModules"; +import type { LoadRouteModuleFunction, RouteModules } from "./routeModules"; import type { EntryRoute } from "./routes"; import { createClientRoutes } from "./routes"; @@ -71,7 +71,8 @@ export function getPatchRoutesOnNavigationFunction( manifest: AssetsManifest, routeModules: RouteModules, isSpaMode: boolean, - basename: string | undefined + basename: string | undefined, + loadRouteModule: LoadRouteModuleFunction ): PatchRoutesOnNavigationFunction | undefined { if (!isFogOfWarEnabled(isSpaMode)) { return undefined; @@ -86,6 +87,7 @@ export function getPatchRoutesOnNavigationFunction( manifest, routeModules, isSpaMode, + loadRouteModule, basename, patch ); @@ -96,7 +98,8 @@ export function useFogOFWarDiscovery( router: DataRouter, manifest: AssetsManifest, routeModules: RouteModules, - isSpaMode: boolean + isSpaMode: boolean, + loadRouteModule: LoadRouteModuleFunction ) { React.useEffect(() => { // Don't prefetch if not enabled or if the user has `saveData` enabled @@ -142,6 +145,7 @@ export function useFogOFWarDiscovery( manifest, routeModules, isSpaMode, + loadRouteModule, router.basename, router.patchRoutes ); @@ -204,6 +208,7 @@ export async function fetchAndApplyManifestPatches( manifest: AssetsManifest, routeModules: RouteModules, isSpaMode: boolean, + loadRouteModule: LoadRouteModuleFunction, basename: string | undefined, patchRoutes: DataRouter["patchRoutes"] ): Promise { @@ -257,7 +262,14 @@ export async function fetchAndApplyManifestPatches( parentIds.forEach((parentId) => patchRoutes( parentId || null, - createClientRoutes(patches, routeModules, null, isSpaMode, parentId) + createClientRoutes( + patches, + routeModules, + null, + isSpaMode, + loadRouteModule, + parentId + ) ) ); } diff --git a/packages/react-router/lib/dom/ssr/links.ts b/packages/react-router/lib/dom/ssr/links.ts index 860aaa8f89..9be82e63c4 100644 --- a/packages/react-router/lib/dom/ssr/links.ts +++ b/packages/react-router/lib/dom/ssr/links.ts @@ -5,7 +5,7 @@ import type { AgnosticDataRouteMatch } from "../../router/utils"; import type { AssetsManifest } from "./entry"; import type { RouteModules, RouteModule } from "./routeModules"; import type { EntryRoute } from "./routes"; -import { loadRouteModule } from "./routeModules"; +import type { LoadRouteModuleFunction } from "./routeModules"; import type { HtmlLinkDescriptor, LinkDescriptor, @@ -136,7 +136,8 @@ export type KeyedHtmlLinkDescriptor = { key: string; link: HtmlLinkDescriptor }; export async function getKeyedPrefetchLinks( matches: AgnosticDataRouteMatch[], manifest: AssetsManifest, - routeModules: RouteModules + routeModules: RouteModules, + loadRouteModule: LoadRouteModuleFunction ): Promise { let links = await Promise.all( matches.map(async (match) => { diff --git a/packages/react-router/lib/dom/ssr/routeModules.ts b/packages/react-router/lib/dom/ssr/routeModules.ts index fa70b57e86..8e16a93baf 100644 --- a/packages/react-router/lib/dom/ssr/routeModules.ts +++ b/packages/react-router/lib/dom/ssr/routeModules.ts @@ -230,10 +230,15 @@ export type RouteComponent = ComponentType<{}>; */ export type RouteHandle = unknown; -export async function loadRouteModule( +export type LoadRouteModuleFunction = ( route: EntryRoute, routeModulesCache: RouteModules -): Promise { +) => Promise; + +export const defaultLoadRouteModule: LoadRouteModuleFunction = async ( + route, + routeModulesCache +) => { if (route.id in routeModulesCache) { return routeModulesCache[route.id] as RouteModule; } @@ -281,4 +286,4 @@ export async function loadRouteModule( // check out of this hook cause the DJs never gonna re[s]olve this }); } -} +}; diff --git a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx index d9c826f333..1a85a4d248 100644 --- a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx +++ b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx @@ -10,7 +10,13 @@ import type { IndexRouteObject, NonIndexRouteObject, } from "../../context"; -import type { LinksFunction, MetaFunction, RouteModules } from "./routeModules"; +import { + defaultLoadRouteModule, + type LinksFunction, + type LoadRouteModuleFunction, + type MetaFunction, + type RouteModules, +} from "./routeModules"; import type { InitialEntry } from "../../router/history"; import type { HydrationState } from "../../router/router"; import { convertRoutesToDataRoutes } from "../../router/utils"; @@ -84,6 +90,11 @@ export interface RoutesTestStubProps { * Future flags mimicking the settings in react-router.config.ts */ future?: Partial; + + /** + * LoadRouteModuleFunction to use in the test + */ + loadRouteModule?: LoadRouteModuleFunction; } /** @@ -98,6 +109,7 @@ export function createRoutesStub( initialIndex, hydrationData, future, + loadRouteModule = defaultLoadRouteModule, }: RoutesTestStubProps) { let routerRef = React.useRef>(); let remixContextRef = React.useRef(); @@ -113,6 +125,7 @@ export function createRoutesStub( }, routeModules: {}, isSpaMode: false, + loadRouteModule, }; // Update the routes to include context in the loader/action and populate diff --git a/packages/react-router/lib/dom/ssr/routes.tsx b/packages/react-router/lib/dom/ssr/routes.tsx index 375e4da6e3..cc57a95f5b 100644 --- a/packages/react-router/lib/dom/ssr/routes.tsx +++ b/packages/react-router/lib/dom/ssr/routes.tsx @@ -10,7 +10,7 @@ import type { } from "../../router/utils"; import { ErrorResponseImpl } from "../../router/utils"; import type { RouteModule, RouteModules } from "./routeModules"; -import { loadRouteModule } from "./routeModules"; +import type { LoadRouteModuleFunction } from "./routeModules"; import type { FutureConfig } from "./entry"; import { prefetchStyleLinks } from "./links"; import { RemixRootDefaultErrorBoundary } from "./errorBoundaries"; @@ -171,13 +171,15 @@ export function createClientRoutesWithHMRRevalidationOptOut( routeModulesCache: RouteModules, initialState: HydrationState, future: FutureConfig, - isSpaMode: boolean + isSpaMode: boolean, + loadRouteModule: LoadRouteModuleFunction ) { return createClientRoutes( manifest, routeModulesCache, initialState, isSpaMode, + loadRouteModule, "", groupRoutesByParentId(manifest), needsRevalidation @@ -226,6 +228,7 @@ export function createClientRoutes( routeModulesCache: RouteModules, initialState: HydrationState | null, isSpaMode: boolean, + loadRouteModule: LoadRouteModuleFunction, parentId: string = "", routesByParentId: Record< string, @@ -418,7 +421,8 @@ export function createClientRoutes( dataRoute.lazy = async () => { let mod = await loadRouteModuleWithBlockingLinks( route, - routeModulesCache + routeModulesCache, + loadRouteModule ); let lazyRoute: Partial = { ...mod }; @@ -475,6 +479,7 @@ export function createClientRoutes( routeModulesCache, initialState, isSpaMode, + loadRouteModule, route.id, routesByParentId, needsRevalidation @@ -531,7 +536,8 @@ function wrapShouldRevalidateForHdr( async function loadRouteModuleWithBlockingLinks( route: EntryRoute, - routeModules: RouteModules + routeModules: RouteModules, + loadRouteModule: LoadRouteModuleFunction ) { let routeModule = await loadRouteModule(route, routeModules); await prefetchStyleLinks(route, routeModule); diff --git a/packages/react-router/lib/dom/ssr/server.tsx b/packages/react-router/lib/dom/ssr/server.tsx index 92d8597b45..4fb43b9d42 100644 --- a/packages/react-router/lib/dom/ssr/server.tsx +++ b/packages/react-router/lib/dom/ssr/server.tsx @@ -7,6 +7,7 @@ import type { EntryContext } from "./entry"; import { RemixErrorBoundary } from "./errorBoundaries"; import { createServerRoutes, shouldHydrateRouteLoader } from "./routes"; import { StreamTransfer } from "./single-fetch"; +import { defaultLoadRouteModule } from "./routeModules"; export interface ServerRouterProps { context: EntryContext; @@ -78,6 +79,7 @@ export function ServerRouter({ isSpaMode: context.isSpaMode, serializeError: context.serializeError, renderMeta: context.renderMeta, + loadRouteModule: defaultLoadRouteModule, }} > diff --git a/packages/react-router/lib/server-runtime/server.ts b/packages/react-router/lib/server-runtime/server.ts index ef1f9291af..df45cf1dc7 100644 --- a/packages/react-router/lib/server-runtime/server.ts +++ b/packages/react-router/lib/server-runtime/server.ts @@ -31,6 +31,10 @@ import { import { getDocumentHeaders } from "./headers"; import invariant from "./invariant"; import type { EntryRoute } from "../dom/ssr/routes"; +import { + defaultLoadRouteModule, + type LoadRouteModuleFunction, +} from "../dom/ssr/routeModules"; export type RequestHandler = ( request: Request, @@ -392,6 +396,7 @@ async function handleDocumentRequest( future: build.future, isSpaMode: build.isSpaMode, serializeError: (err) => serializeError(err, serverMode), + loadRouteModule: defaultLoadRouteModule, }; let handleDocumentRequestFunction = build.entry.module.default;