UNPKG

@redwoodjs/sdk

Version:

A full-stack webapp toolkit designed for TypeScript, Vite, and React Server Components

80 lines (79 loc) 3.49 kB
// port(justinvdm, 10 Mar 2025): This is a modified version of https://github.com/devongovett/rsc-html-stream/blob/main/server.js // Modification: We needed to add a nonce attribute to the script tag for CSP const encoder = new TextEncoder(); const trailer = "</body></html>"; export function injectRSCPayload(rscStream, { nonce }) { let decoder = new TextDecoder(); let resolveFlightDataPromise; let flightDataPromise = new Promise((resolve) => (resolveFlightDataPromise = resolve)); let startedRSC = false; // Buffer all HTML chunks enqueued during the current tick of the event loop (roughly) // and write them to the output stream all at once. This ensures that we don't generate // invalid HTML by injecting RSC in between two partial chunks of HTML. let buffered = []; let timeout = null; function flushBufferedChunks(controller) { for (let chunk of buffered) { let buf = decoder.decode(chunk); if (buf.endsWith(trailer)) { buf = buf.slice(0, -trailer.length); } controller.enqueue(encoder.encode(buf)); } buffered.length = 0; timeout = null; } return new TransformStream({ transform(chunk, controller) { buffered.push(chunk); if (timeout) { return; } timeout = setTimeout(async () => { flushBufferedChunks(controller); if (!startedRSC) { startedRSC = true; writeRSCStream(rscStream, controller, { nonce }) .catch((err) => controller.error(err)) .then(resolveFlightDataPromise); } }, 0); }, async flush(controller) { await flightDataPromise; if (timeout) { clearTimeout(timeout); flushBufferedChunks(controller); } controller.enqueue(encoder.encode("</body></html>")); }, }); } async function writeRSCStream(rscStream, controller, { nonce }) { let decoder = new TextDecoder("utf-8", { fatal: true }); for await (let chunk of rscStream) { // Try decoding the chunk to send as a string. // If that fails (e.g. binary data that is invalid unicode), write as base64. try { writeChunk(JSON.stringify(decoder.decode(chunk, { stream: true })), controller, { nonce }); } catch (err) { let base64 = JSON.stringify(btoa(String.fromCodePoint(...chunk))); writeChunk(`Uint8Array.from(atob(${base64}), m => m.codePointAt(0))`, controller, { nonce }); } } let remaining = decoder.decode(); if (remaining.length) { writeChunk(JSON.stringify(remaining), controller, { nonce }); } } function writeChunk(chunk, controller, { nonce }) { controller.enqueue(encoder.encode(`<script nonce="${nonce}">${escapeScript(`(self.__FLIGHT_DATA||=[]).push(${chunk})`)}</script>`)); } // Escape closing script tags and HTML comments in JS content. // https://www.w3.org/TR/html52/semantics-scripting.html#restrictions-for-contents-of-script-elements // Avoid replacing </script with <\/script as it would break the following valid JS: 0</script/ (i.e. regexp literal). // Instead, escape the s character. function escapeScript(script) { return script.replace(/<!--/g, "<\\!--").replace(/<\/(script)/gi, "</\\$1"); }