@tanstack/react-router
Version:
Modern and scalable routing for React applications
1 lines • 9.72 kB
Source Map (JSON)
{"version":3,"file":"renderRouterToStream.cjs","names":[],"sources":["../../../src/ssr/renderRouterToStream.tsx"],"sourcesContent":["import { PassThrough } from 'node:stream'\nimport ReactDOMServer from 'react-dom/server'\nimport { isbot } from 'isbot'\nimport {\n createSsrStreamResponse,\n transformPipeableStreamWithRouter,\n transformReadableStreamWithRouter,\n} from '@tanstack/router-core/ssr/server'\nimport type { AnyRouter } from '@tanstack/router-core'\nimport type { ReadableStream } from 'node:stream/web'\nimport type { ReactNode } from 'react'\n\nconst noop = () => {}\n\n// Bot responses wait for `allReady` so crawlers receive complete HTML.\n// If the request disconnects during that wait, React may not settle quickly;\n// unblock the wait so the response pipeline can abort and clean up.\nasync function waitForReadyOrAbort(\n ready: Promise<unknown>,\n signal: AbortSignal,\n) {\n let cleanup = noop\n try {\n await Promise.race([\n ready,\n new Promise<void>((resolve) => {\n const onAbort = () => resolve()\n cleanup = () => signal.removeEventListener('abort', onAbort)\n signal.addEventListener('abort', onAbort, { once: true })\n if (signal.aborted) resolve()\n }),\n ])\n } finally {\n cleanup()\n }\n}\n\n// A client disconnecting mid-stream is normal operation, not a render\n// failure; don't let React's onError log it as one.\nconst isAbortError = (request: Request, error: unknown) =>\n (request.signal.aborted && error === request.signal.reason) ||\n (error instanceof Error && error.name === 'AbortError')\n\nexport const renderRouterToStream = async ({\n request,\n router,\n responseHeaders,\n children,\n}: {\n request: Request\n router: AnyRouter\n responseHeaders: Headers\n children: ReactNode\n}) => {\n if (typeof ReactDOMServer.renderToReadableStream === 'function') {\n const stream = await ReactDOMServer.renderToReadableStream(children, {\n signal: request.signal,\n nonce: router.options.ssr?.nonce,\n progressiveChunkSize: Number.POSITIVE_INFINITY,\n onError: (error, info) => {\n if (!isAbortError(request, error)) {\n console.error('Error in renderToReadableStream:', error, info)\n }\n },\n })\n\n if (isbot(request.headers.get('User-Agent'))) {\n await waitForReadyOrAbort(stream.allReady, request.signal)\n }\n\n const responseStream = transformReadableStreamWithRouter(\n router,\n stream as unknown as ReadableStream,\n { onAbort: () => stream.cancel().catch(() => {}) },\n )\n return createSsrStreamResponse(\n router,\n new Response(responseStream as any, {\n status: router.stores.statusCode.get(),\n headers: responseHeaders,\n }),\n )\n }\n\n if (typeof ReactDOMServer.renderToPipeableStream === 'function') {\n const reactAppPassthrough = new PassThrough()\n\n let pipeable:\n | ReturnType<typeof ReactDOMServer.renderToPipeableStream>\n | undefined\n let responseAttached = false\n let aborted = false\n let endedBeforeAttach = false\n let pendingAbortReason: unknown\n const toError = (reason: unknown) =>\n reason instanceof Error\n ? reason\n : new Error(String(reason ?? 'SSR aborted'))\n const destroyError = (reason: unknown) =>\n reason === undefined ? undefined : toError(reason)\n const pendingDestroyError = () =>\n pendingAbortReason === undefined\n ? toError(pendingAbortReason)\n : destroyError(pendingAbortReason)\n const finishPassThrough = (\n reason: unknown,\n opts?: { defaultError?: boolean },\n ) => {\n if (reactAppPassthrough.destroyed) return\n if (responseAttached) {\n reactAppPassthrough.destroy(\n opts?.defaultError ? toError(reason) : destroyError(reason),\n )\n } else {\n endedBeforeAttach = true\n // onError can fire synchronously before React returns the pipeable\n // handle and before Readable.toWeb() is attached. Defer touching the\n // PassThrough until after the router transform can observe the error.\n }\n }\n const abortPipeable = (\n reason?: unknown,\n opts?: { defaultError?: boolean },\n ) => {\n if (aborted) return\n aborted = true\n pendingAbortReason = reason\n const err = toError(reason)\n try {\n pipeable?.abort(err)\n } catch {\n // ignore — React may throw if already aborted/finished\n }\n finishPassThrough(reason, opts)\n }\n\n // Register before attaching the router transform; the transform may\n // synchronously cleanup/error, and cleanup must still remove this listener.\n if (request.signal.aborted) {\n abortPipeable(request.signal.reason)\n } else {\n const onRequestAbort = () => abortPipeable(request.signal.reason)\n request.signal.addEventListener('abort', onRequestAbort, { once: true })\n router.serverSsr?.onCleanup(() => {\n request.signal.removeEventListener('abort', onRequestAbort)\n })\n }\n\n try {\n pipeable = ReactDOMServer.renderToPipeableStream(children, {\n nonce: router.options.ssr?.nonce,\n progressiveChunkSize: Number.POSITIVE_INFINITY,\n ...(isbot(request.headers.get('User-Agent'))\n ? {\n onAllReady() {\n pipeable!.pipe(reactAppPassthrough)\n },\n }\n : {\n onShellReady() {\n pipeable!.pipe(reactAppPassthrough)\n },\n }),\n onError: (error, info) => {\n if (!isAbortError(request, error)) {\n console.error('Error in renderToPipeableStream:', error, info)\n }\n abortPipeable(error, { defaultError: true })\n },\n })\n } catch (e) {\n console.error('Error in renderToPipeableStream:', e)\n router.serverSsr?.cleanup()\n throw e\n }\n\n const responseStream = transformPipeableStreamWithRouter(\n router,\n reactAppPassthrough,\n { onAbort: abortPipeable },\n )\n responseAttached = true\n\n if (endedBeforeAttach) {\n reactAppPassthrough.destroy(pendingDestroyError())\n }\n\n // React's onError may have fired synchronously inside\n // renderToPipeableStream before `pipeable` was assigned. If so,\n // abortPipeable ran without a pipeable handle; re-apply the abort now.\n if (aborted && pipeable) {\n try {\n pipeable.abort(toError(pendingAbortReason))\n } catch {\n // ignore — React may throw if already aborted/finished\n }\n }\n\n return createSsrStreamResponse(\n router,\n new Response(responseStream as any, {\n status: router.stores.statusCode.get(),\n headers: responseHeaders,\n }),\n )\n }\n\n throw new Error(\n 'No renderToReadableStream or renderToPipeableStream found in react-dom/server. Ensure you are using a version of react-dom that supports streaming.',\n )\n}\n"],"mappings":"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"}