@tanstack/vue-router
Version:
Modern and scalable routing for Vue applications
127 lines (126 loc) • 4.02 kB
JavaScript
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