| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 |
|
| 11 |
|
| 12 | const encoder = new TextEncoder();
|
| 13 | const trailer = "</body></html>";
|
| 14 | function injectRSCPayload(rscStream, { nonce } = {}) {
|
| 15 | let decoder = new TextDecoder();
|
| 16 | let resolveFlightDataPromise;
|
| 17 | let flightDataPromise = new Promise((resolve) => resolveFlightDataPromise = resolve);
|
| 18 | let startedRSC = false;
|
| 19 | let cancelled = false;
|
| 20 | let rscReader = null;
|
| 21 | let buffered = [];
|
| 22 | let timeout = null;
|
| 23 | function flushBufferedChunks(controller) {
|
| 24 | for (let chunk of buffered) {
|
| 25 | let buf = decoder.decode(chunk, { stream: true });
|
| 26 | if (buf.endsWith(trailer)) buf = buf.slice(0, -14);
|
| 27 | controller.enqueue(encoder.encode(buf));
|
| 28 | }
|
| 29 | buffered.length = 0;
|
| 30 | timeout = null;
|
| 31 | }
|
| 32 | return new TransformStream({
|
| 33 | transform(chunk, controller) {
|
| 34 | buffered.push(chunk);
|
| 35 | if (timeout) return;
|
| 36 | timeout = setTimeout(async () => {
|
| 37 | if (cancelled) return;
|
| 38 | flushBufferedChunks(controller);
|
| 39 | if (!startedRSC) {
|
| 40 | startedRSC = true;
|
| 41 | rscReader = rscStream.getReader();
|
| 42 | writeRSCStream(rscReader, controller, () => cancelled, nonce).catch((err) => controller.error(err)).then(resolveFlightDataPromise);
|
| 43 | }
|
| 44 | }, 0);
|
| 45 | },
|
| 46 | async flush(controller) {
|
| 47 | await flightDataPromise;
|
| 48 | if (timeout) {
|
| 49 | clearTimeout(timeout);
|
| 50 | flushBufferedChunks(controller);
|
| 51 | }
|
| 52 | controller.enqueue(encoder.encode("</body></html>"));
|
| 53 | },
|
| 54 | async cancel(reason) {
|
| 55 | cancelled = true;
|
| 56 | if (timeout) {
|
| 57 | clearTimeout(timeout);
|
| 58 | timeout = null;
|
| 59 | }
|
| 60 | buffered.length = 0;
|
| 61 | if (rscReader) await rscReader.cancel(reason).catch(() => {});
|
| 62 | else await rscStream.cancel(reason).catch(() => {});
|
| 63 | resolveFlightDataPromise();
|
| 64 | }
|
| 65 | });
|
| 66 | }
|
| 67 | async function writeRSCStream(reader, controller, isCancelled, nonce) {
|
| 68 | let decoder = new TextDecoder("utf-8", { fatal: true });
|
| 69 | try {
|
| 70 | let read;
|
| 71 | while ((read = await reader.read()) && !read.done) {
|
| 72 | if (isCancelled()) return;
|
| 73 | const chunk = read.value;
|
| 74 | try {
|
| 75 | writeChunk(JSON.stringify(decoder.decode(chunk, { stream: true })), controller, nonce);
|
| 76 | } catch (e) {
|
| 77 | writeChunk(`Uint8Array.from(atob(${JSON.stringify(btoa(String.fromCodePoint(...chunk)))}), m => m.codePointAt(0))`, controller, nonce);
|
| 78 | }
|
| 79 | }
|
| 80 | } finally {
|
| 81 | reader.releaseLock();
|
| 82 | }
|
| 83 | let remaining = decoder.decode();
|
| 84 | if (remaining.length && !isCancelled()) writeChunk(JSON.stringify(remaining), controller, nonce);
|
| 85 | }
|
| 86 | function writeChunk(chunk, controller, nonce) {
|
| 87 | let nonceAttr = nonce == null ? "" : ` nonce="${escapeAttribute(nonce)}"`;
|
| 88 | controller.enqueue(encoder.encode(`<script${nonceAttr}>${escapeScript(`(self.__FLIGHT_DATA||=[]).push(${chunk})`)}<\/script>`));
|
| 89 | }
|
| 90 | function escapeAttribute(value) {
|
| 91 | return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
| 92 | }
|
| 93 | function escapeScript(script) {
|
| 94 | return script.replace(/<!--/g, "<\\!--").replace(/<\/(script)/gi, "</\\$1");
|
| 95 | }
|
| 96 |
|
| 97 | export { injectRSCPayload };
|