UNPKG

@tanstack/vue-router

Version:

Modern and scalable routing for Vue applications

127 lines (126 loc) 4.02 kB
import * as Vue from "vue"; import { createSsrStreamResponse, transformReadableStreamWithRouter } from "@tanstack/router-core/ssr/server"; import { pipeToWebWritable, renderToString } from "vue/server-renderer"; import { ReadableStream } from "node:stream/web"; import { isbot } from "isbot"; //#region src/ssr/renderRouterToStream.tsx function prependDoctype(readable) { const encoder = new TextEncoder(); let sentDoctype = false; let reader; const releaseReader = () => { try { reader?.releaseLock(); } catch {} reader = void 0; }; return new ReadableStream({ start() { reader = readable.getReader(); }, async pull(controller) { if (!sentDoctype) { sentDoctype = true; controller.enqueue(encoder.encode("<!DOCTYPE html>")); return; } try { const { done, value } = await reader.read(); if (done) { controller.close(); releaseReader(); return; } controller.enqueue(value); } catch (err) { controller.error(err); releaseReader(); } }, async cancel(reason) { try { await reader?.cancel(reason); } catch {} releaseReader(); } }); } var renderRouterToStream = async ({ request, router, responseHeaders, App }) => { const app = Vue.createSSRApp(App, { router }); if (isbot(request.headers.get("User-Agent"))) try { let cleanupAbortListener; const abortPromise = new Promise((_, reject) => { if (request.signal.aborted) { reject(request.signal.reason); return; } const onRequestAbort = () => reject(request.signal.reason); request.signal.addEventListener("abort", onRequestAbort, { once: true }); cleanupAbortListener = () => { request.signal.removeEventListener("abort", onRequestAbort); }; }); let fullHtml = await Promise.race([renderToString(app), abortPromise]).finally(() => cleanupAbortListener?.()); router.serverSsr.setRenderFinished(); const injectedHtml = router.serverSsr.takeBufferedHtml(); if (injectedHtml) fullHtml = fullHtml.replace(`</body>`, () => `${injectedHtml}</body>`); const htmlOpenIndex = fullHtml.indexOf("<html"); const htmlCloseIndex = fullHtml.indexOf("</html>"); if (htmlOpenIndex !== -1 && htmlCloseIndex !== -1) fullHtml = fullHtml.slice(htmlOpenIndex, htmlCloseIndex + 7); else if (htmlOpenIndex !== -1) fullHtml = fullHtml.slice(htmlOpenIndex); return new Response(`<!DOCTYPE html>${fullHtml}`, { status: router.stores.statusCode.get(), headers: responseHeaders }); } finally { router.serverSsr?.cleanup(); } const { writable, readable } = new TransformStream(); const innerWriter = writable.getWriter(); let writerDone = false; const releaseWriter = () => { try { innerWriter.releaseLock(); } catch {} }; const abortVuePipe = (reason) => { if (writerDone) return; writerDone = true; innerWriter.abort(reason).catch(() => {}).finally(releaseWriter); }; const vueWritable = new WritableStream({ write(chunk) { return innerWriter.write(chunk); }, close() { writerDone = true; return innerWriter.close().finally(releaseWriter); }, abort(reason) { writerDone = true; return innerWriter.abort(reason).finally(releaseWriter); } }); try { pipeToWebWritable(app, {}, vueWritable); } catch (err) { console.error("Error in Vue pipeToWebWritable:", err); abortVuePipe(err); } if (request.signal.aborted) abortVuePipe(request.signal.reason); else { const onRequestAbort = () => abortVuePipe(request.signal.reason); request.signal.addEventListener("abort", onRequestAbort, { once: true }); router.serverSsr?.onCleanup(() => { request.signal.removeEventListener("abort", onRequestAbort); }); } const responseStream = transformReadableStreamWithRouter(router, prependDoctype(readable), { onAbort: abortVuePipe }); return createSsrStreamResponse(router, new Response(responseStream, { status: router.stores.statusCode.get(), headers: responseHeaders })); }; //#endregion export { renderRouterToStream }; //# sourceMappingURL=renderRouterToStream.js.map