@tanstack/router-core
Version:
Modern and scalable routing for React applications
1 lines • 15 kB
Source Map (JSON)
{"version":3,"file":"ssr-client.cjs","names":[],"sources":["../../../src/ssr/ssr-client.ts"],"sourcesContent":["import { invariant } from '../invariant'\nimport { isNotFound } from '../not-found'\nimport { createControlledPromise } from '../utils'\nimport { hydrateSsrMatchId } from './ssr-match-id'\nimport type { GLOBAL_SEROVAL, GLOBAL_TSR } from './constants'\nimport type { DehydratedMatch, TsrSsrGlobal } from './types'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { AnyRouter } from '../router'\nimport type { RouteContextOptions } from '../route'\nimport type { AnySerializationAdapter } from './serializer/transformer'\n\ndeclare global {\n interface Window {\n [GLOBAL_TSR]?: TsrSsrGlobal\n [GLOBAL_SEROVAL]?: any\n }\n}\n\nfunction hydrateMatch(\n match: AnyRouteMatch,\n deyhydratedMatch: DehydratedMatch,\n): void {\n match.id = deyhydratedMatch.i\n match.__beforeLoadContext = deyhydratedMatch.b\n match.loaderData = deyhydratedMatch.l\n match.status = deyhydratedMatch.s\n match.ssr = deyhydratedMatch.ssr\n match.updatedAt = deyhydratedMatch.u\n match.error = deyhydratedMatch.e\n // Only hydrate global-not-found when a defined value is present in the\n // dehydrated payload. If omitted, preserve the value computed from the\n // current client location (important for SPA fallback HTML served at unknown\n // URLs, where dehydrated matches may come from `/` but client matching marks\n // root as globalNotFound).\n if (deyhydratedMatch.g !== undefined) {\n match.globalNotFound = deyhydratedMatch.g\n }\n}\n\nexport async function hydrate(router: AnyRouter): Promise<any> {\n if (!window.$_TSR) {\n if (process.env.NODE_ENV !== 'production') {\n throw new Error(\n 'Invariant failed: Expected to find bootstrap data on window.$_TSR, but we did not. Please file an issue!',\n )\n }\n\n invariant()\n }\n\n const serializationAdapters = router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n\n if (serializationAdapters?.length) {\n const fromSerializableMap = new Map()\n serializationAdapters.forEach((adapter) => {\n fromSerializableMap.set(adapter.key, adapter.fromSerializable)\n })\n window.$_TSR.t = fromSerializableMap\n window.$_TSR.buffer.forEach((script) => script())\n }\n window.$_TSR.initialized = true\n\n if (!window.$_TSR.router) {\n if (process.env.NODE_ENV !== 'production') {\n throw new Error(\n 'Invariant failed: Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',\n )\n }\n\n invariant()\n }\n\n const dehydratedRouter = window.$_TSR.router\n dehydratedRouter.matches.forEach((dehydratedMatch) => {\n dehydratedMatch.i = hydrateSsrMatchId(dehydratedMatch.i)\n })\n if (dehydratedRouter.lastMatchId) {\n dehydratedRouter.lastMatchId = hydrateSsrMatchId(\n dehydratedRouter.lastMatchId,\n )\n }\n const { manifest, dehydratedData, lastMatchId } = dehydratedRouter\n\n router.ssr = {\n manifest,\n }\n const meta = document.querySelector('meta[property=\"csp-nonce\"]') as\n | HTMLMetaElement\n | undefined\n const nonce = meta?.content\n router.options.ssr = {\n nonce,\n }\n\n // Allow the user to handle custom hydration data before matching routes.\n // This lets hydration install router config that affects matching, e.g. rewrites.\n await router.options.hydrate?.(dehydratedData)\n\n // Hydrate the router state\n const matches = router.matchRoutes(router.stores.location.get())\n\n // kick off loading the route chunks\n const routeChunkPromise = Promise.all(\n matches.map((match) =>\n router.loadRouteChunk(router.looseRoutesById[match.routeId]!),\n ),\n )\n\n function setMatchForcePending(match: AnyRouteMatch) {\n // usually the minPendingPromise is created in the Match component if a pending match is rendered\n // however, this might be too late if the match synchronously resolves\n const route = router.looseRoutesById[match.routeId]!\n const pendingMinMs =\n route.options.pendingMinMs ?? router.options.defaultPendingMinMs\n if (pendingMinMs) {\n const minPendingPromise = createControlledPromise<void>()\n match._nonReactive.minPendingPromise = minPendingPromise\n match._forcePending = true\n\n setTimeout(() => {\n minPendingPromise.resolve()\n // We've handled the minPendingPromise, so we can delete it\n router.updateMatch(match.id, (prev) => {\n prev._nonReactive.minPendingPromise = undefined\n return {\n ...prev,\n _forcePending: undefined,\n }\n })\n }, pendingMinMs)\n }\n }\n\n function setRouteSsr(match: AnyRouteMatch) {\n const route = router.looseRoutesById[match.routeId]\n if (route) {\n route.options.ssr = match.ssr\n }\n }\n // Right after hydration and before the first render, we need to rehydrate each match\n // First step is to reyhdrate loaderData and __beforeLoadContext\n let firstNonSsrMatchIndex: number | undefined = undefined\n matches.forEach((match) => {\n const dehydratedMatch = dehydratedRouter.matches.find(\n (d) => d.i === match.id,\n )\n if (!dehydratedMatch) {\n match._nonReactive.dehydrated = false\n match.ssr = false\n setRouteSsr(match)\n return\n }\n\n hydrateMatch(match, dehydratedMatch)\n setRouteSsr(match)\n\n match._nonReactive.dehydrated = match.ssr !== false\n\n if (match.ssr === 'data-only' || match.ssr === false) {\n if (firstNonSsrMatchIndex === undefined) {\n firstNonSsrMatchIndex = match.index\n setMatchForcePending(match)\n }\n }\n })\n\n router.stores.setMatches(matches)\n\n // now that all necessary data is hydrated:\n // 1) fully reconstruct the route context\n // 2) execute `head()` and `scripts()` for each match\n const activeMatches = router.stores.matches.get()\n const location = router.stores.location.get()\n await Promise.all(\n activeMatches.map(async (match) => {\n try {\n const route = router.looseRoutesById[match.routeId]!\n\n const parentMatch = activeMatches[match.index - 1]\n const parentContext = parentMatch?.context ?? router.options.context\n\n // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed\n // so run it again and merge route context\n if (route.options.context) {\n const contextFnContext: RouteContextOptions<any, any, any, any, any> =\n {\n deps: match.loaderDeps,\n params: match.params,\n context: parentContext ?? {},\n location,\n navigate: (opts: any) =>\n router.navigate({\n ...opts,\n _fromLocation: location,\n }),\n buildLocation: router.buildLocation,\n cause: match.cause,\n abortController: match.abortController,\n preload: false,\n matches,\n routeId: route.id,\n }\n match.__routeContext =\n route.options.context(contextFnContext) ?? undefined\n }\n\n match.context = {\n ...parentContext,\n ...match.__routeContext,\n ...match.__beforeLoadContext,\n }\n\n const assetContext = {\n ssr: router.options.ssr,\n matches: activeMatches,\n match,\n params: match.params,\n loaderData: match.loaderData,\n }\n const headFnContent = await route.options.head?.(assetContext)\n\n const scripts = await route.options.scripts?.(assetContext)\n\n match.meta = headFnContent?.meta\n match.links = headFnContent?.links\n match.headScripts = headFnContent?.scripts\n match.styles = headFnContent?.styles\n match.scripts = scripts\n } catch (err) {\n if (isNotFound(err)) {\n match.error = { isNotFound: true }\n console.error(\n `NotFound error during hydration for routeId: ${match.routeId}`,\n err,\n )\n } else {\n match.error = err as any\n console.error(\n `Error during hydration for route ${match.routeId}:`,\n err,\n )\n throw err\n }\n }\n }),\n )\n\n const isSpaMode = matches[matches.length - 1]!.id !== lastMatchId\n const hasSsrFalseMatches = matches.some((m) => m.ssr === false)\n // all matches have data from the server and we are not in SPA mode so we don't need to kick of router.load()\n if (!hasSsrFalseMatches && !isSpaMode) {\n matches.forEach((match) => {\n // remove the dehydrated flag since we won't run router.load() which would remove it\n match._nonReactive.dehydrated = undefined\n })\n // Mark the current location as resolved so that later load cycles\n // (e.g. preloads, invalidations) don't mistakenly detect a href change\n // (resolvedLocation defaults to undefined and router.load() is skipped\n // in the normal SSR hydration path).\n router.stores.resolvedLocation.set(router.stores.location.get())\n return routeChunkPromise\n }\n\n // schedule router.load() to run after the next tick so we can store the promise in the match before loading starts\n const loadPromise = Promise.resolve()\n .then(() => router.load())\n .catch((err) => {\n console.error('Error during router hydration:', err)\n })\n\n // in SPA mode we need to keep the first match below the root route pending until router.load() is finished\n // this will prevent that other pending components are rendered but hydration is not blocked\n if (isSpaMode) {\n const match = matches[1]\n if (!match) {\n if (process.env.NODE_ENV !== 'production') {\n throw new Error(\n 'Invariant failed: Expected to find a match below the root match in SPA mode.',\n )\n }\n\n invariant()\n }\n setMatchForcePending(match)\n\n match._displayPending = true\n match._nonReactive.displayPendingPromise = loadPromise\n\n loadPromise.then(() => {\n router.batch(() => {\n // ensure router is not in status 'pending' anymore\n // this usually happens in Transitioner but if loading synchronously resolves,\n // Transitioner won't be rendered while loading so it cannot track the change from loading:true to loading:false\n if (router.stores.status.get() === 'pending') {\n router.stores.status.set('idle')\n router.stores.resolvedLocation.set(router.stores.location.get())\n }\n // hide the pending component once the load is finished\n router.updateMatch(match.id, (prev) => ({\n ...prev,\n _displayPending: undefined,\n displayPendingPromise: undefined,\n }))\n })\n })\n }\n return routeChunkPromise\n}\n"],"mappings":"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"}