UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

130 lines (114 loc) 3.7 kB
import { createServerHistory } from '@tanstack/history' import { _getRenderedMatches } from '../load-client' import { waitForReason as waitForRequest } from '../await-signal' import { mergeHeaders } from './headers' import { attachRouterServerSsrUtils, getNormalizedURL, getOrigin, } from './ssr-server' import { bindSsrResponseToRequest, disposeSsrResponse, isSsrResponse, } from './handlerCallback' import type { HandlerCallback } from './handlerCallback' import type { AnyHeaders } from './headers' import type { AnyRouter } from '../router' import type { ServerManifest } from '../manifest' export type RequestHandler<TRouter extends AnyRouter> = ( cb: HandlerCallback<TRouter>, ) => Promise<Response> export { waitForRequest } function createLateResponseDisposer(signal: AbortSignal) { return (result: unknown) => { if (result instanceof Response || isSsrResponse(result)) { disposeSsrResponse(result, signal.reason) } } } export function createRequestHandler<TRouter extends AnyRouter>({ createRouter, request, getRouterManifest, }: { createRouter: () => TRouter request: Request getRouterManifest?: () => ServerManifest | Promise<ServerManifest> }): RequestHandler<TRouter> { return async (cb) => { const signal = request.signal signal.throwIfAborted() const manifest = getRouterManifest ? await waitForRequest(getRouterManifest(), signal) : undefined signal.throwIfAborted() const router = createRouter() let responseOwnsCleanup = false try { attachRouterServerSsrUtils({ router, manifest, }) // normalizing and sanitizing the pathname here for server, so we always deal with the same format during SSR. const { url } = getNormalizedURL(request.url, 'http://localhost') const origin = getOrigin(request) const href = url.href.replace(url.origin, '') // Create a history for the router const history = createServerHistory(href) // Update the router with the history and context router.update({ history, origin: router.options.origin ?? origin, }) await router.load({ _signal: signal, }) signal.throwIfAborted() const result = router._serverResult if (result?.type === 'redirect') { return result.redirect } await router.serverSsr?.dehydrate({ signal }) signal.throwIfAborted() const responseHeaders = getRequestHeaders({ router, }) signal.throwIfAborted() const disposeLate = createLateResponseDisposer(signal) const response = await waitForRequest( cb({ request, router, responseHeaders, }), signal, disposeLate, disposeLate, ) const ssrResponse = bindSsrResponseToRequest(router, response, signal) signal.throwIfAborted() responseOwnsCleanup = ssrResponse.serverSsrCleanup === 'stream' return ssrResponse.response } finally { if (!responseOwnsCleanup) { // Clean up router SSR state if the callback won't handle it // (e.g., if an error occurred before the callback was invoked). // Transformed streaming response bodies clean up when consumed/cancelled. router.serverSsr?.cleanup() } } } } function getRequestHeaders(opts: { router: AnyRouter }): Headers { const matchHeaders: Array<AnyHeaders> = [] for (const match of _getRenderedMatches(opts.router.stores.matches.get())) { matchHeaders.push(match.headers) } return mergeHeaders( { 'Content-Type': 'text/html; charset=UTF-8', }, ...matchHeaders, ) }