import { isPlainObject } from '@tanstack/router-core' import invariant from 'tiny-invariant' import { startSerializer } from './serializer' import type { AnyRouter, ControllablePromise, MakeRouteMatch, } from '@tanstack/react-router' import type { DeferredPromiseState, Manifest } from '@tanstack/router-core' declare global { interface Window { __TSR_SSR__?: StartSsrGlobal } } export interface StartSsrGlobal { matches: Array streamedValues: Record< string, { value: any parsed: any } > cleanScripts: () => void dehydrated?: any initMatch: (match: SsrMatch) => void resolvePromise: (opts: { matchId: string id: number promiseState: DeferredPromiseState }) => void injectChunk: (opts: { matchId: string; id: number; chunk: string }) => void closeStream: (opts: { matchId: string; id: number }) => void } export interface SsrMatch { id: string __beforeLoadContext: string loaderData?: string error?: string extracted?: Array updatedAt: MakeRouteMatch['updatedAt'] status: MakeRouteMatch['status'] } export type ClientExtractedEntry = | ClientExtractedStream | ClientExtractedPromise export interface ClientExtractedPromise extends ClientExtractedBaseEntry { type: 'promise' value?: ControllablePromise } export interface ClientExtractedStream extends ClientExtractedBaseEntry { type: 'stream' value?: ReadableStream & { controller?: ReadableStreamDefaultController } } export interface ClientExtractedBaseEntry { type: string path: Array } export interface ResolvePromiseState { matchId: string id: number promiseState: DeferredPromiseState } export interface DehydratedRouter { manifest: Manifest | undefined dehydratedData: any } export function hydrate(router: AnyRouter) { invariant( window.__TSR_SSR__?.dehydrated, 'Expected to find a dehydrated data on window.__TSR_SSR__.dehydrated... but we did not. Please file an issue!', ) const { manifest, dehydratedData } = startSerializer.parse( window.__TSR_SSR__.dehydrated, ) as DehydratedRouter router.ssr = { manifest, serializer: startSerializer, } router.clientSsr = { getStreamedValue: (key: string): T | undefined => { if (router.isServer) { return undefined } const streamedValue = window.__TSR_SSR__?.streamedValues[key] if (!streamedValue) { return } if (!streamedValue.parsed) { streamedValue.parsed = router.ssr!.serializer.parse(streamedValue.value) } return streamedValue.parsed }, } // Hydrate the router state const matches = router.matchRoutes(router.state.location) // kick off loading the route chunks const routeChunkPromise = Promise.all( matches.map((match) => { const route = router.looseRoutesById[match.routeId]! return router.loadRouteChunk(route) }), ) matches.forEach((match) => { const route = router.looseRoutesById[match.routeId]! // Right after hydration and before the first render, we need to rehydrate each match // This includes rehydrating the loaderData and also using the beforeLoadContext // to reconstruct any context that was serialized on the server const dehydratedMatch = window.__TSR_SSR__!.matches.find( (d) => d.id === match.id, ) if (dehydratedMatch) { Object.assign(match, dehydratedMatch) const parentMatch = matches[match.index - 1] const parentContext = parentMatch?.context ?? router.options.context ?? {} // Handle beforeLoadContext if (dehydratedMatch.__beforeLoadContext) { match.__beforeLoadContext = router.ssr!.serializer.parse( dehydratedMatch.__beforeLoadContext, ) as any match.context = { ...parentContext, ...match.__routeContext, ...match.__beforeLoadContext, } } // Handle loaderData if (dehydratedMatch.loaderData) { match.loaderData = router.ssr!.serializer.parse( dehydratedMatch.loaderData, ) } // Handle error if (dehydratedMatch.error) { match.error = router.ssr!.serializer.parse(dehydratedMatch.error) } // Handle extracted ;(match as unknown as SsrMatch).extracted?.forEach((ex) => { deepMutableSetByPath(match, ['loaderData', ...ex.path], ex.value) }) } else { Object.assign(match, { status: 'success', updatedAt: Date.now(), }) } const assetContext = { matches: router.state.matches, match, params: match.params, loaderData: match.loaderData, } const headFnContent = route.options.head?.(assetContext) const scripts = route.options.scripts?.(assetContext) match.meta = headFnContent?.meta match.links = headFnContent?.links match.headScripts = headFnContent?.scripts match.scripts = scripts return match }) router.__store.setState((s) => { return { ...s, matches, } }) // Allow the user to handle custom hydration data router.options.hydrate?.(dehydratedData) return routeChunkPromise } function deepMutableSetByPath(obj: T, path: Array, value: any) { // mutable set by path retaining array and object references if (path.length === 1) { ;(obj as any)[path[0]!] = value } const [key, ...rest] = path if (Array.isArray(obj)) { deepMutableSetByPath(obj[Number(key)], rest, value) } else if (isPlainObject(obj)) { deepMutableSetByPath((obj as any)[key!], rest, value) } }