@tanstack/vue-router
Version:
Modern and scalable routing for Vue applications
158 lines • 5.67 kB
JSX
import { ReadableStream as NodeReadableStream } from 'node:stream/web';
import * as Vue from 'vue';
import { pipeToWebWritable, renderToString } from 'vue/server-renderer';
import { isbot } from 'isbot';
import { createSsrStreamResponse, transformReadableStreamWithRouter, } from '@tanstack/router-core/ssr/server';
function prependDoctype(readable) {
const encoder = new TextEncoder();
let sentDoctype = false;
let reader;
const releaseReader = () => {
try {
reader?.releaseLock();
}
catch {
// ignore
}
reader = undefined;
};
return new NodeReadableStream({
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 {
// ignore
}
releaseReader();
},
});
}
export const 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 {
// already released / errored
}
};
const abortVuePipe = (reason) => {
if (writerDone)
return;
writerDone = true;
void 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);
},
});
// `pipeToWebWritable` returns void (see @vue/server-renderer). Pass a
// proxy writable so request aborts can abort the real TransformStream
// writer even while Vue holds a lock on the proxy writable.
try {
pipeToWebWritable(app, {}, vueWritable);
}
catch (err) {
console.error('Error in Vue pipeToWebWritable:', err);
// Setup failed before any pipe was wired; abort writable so the
// readable side errors instead of hanging until the lifetime timeout.
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 doctypedStream = prependDoctype(readable);
const responseStream = transformReadableStreamWithRouter(router, doctypedStream, { onAbort: abortVuePipe });
return createSsrStreamResponse(router, new Response(responseStream, {
status: router.stores.statusCode.get(),
headers: responseHeaders,
}));
};
//# sourceMappingURL=renderRouterToStream.jsx.map