@tanstack/router-core
Version:
Modern and scalable routing for React applications
130 lines (114 loc) • 3.7 kB
text/typescript
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,
)
}