-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathserver.js
87 lines (78 loc) · 2.83 KB
/
server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
const encoder = new TextEncoder();
const trailer = '</body></html>';
export function injectRSCPayload(rscStream) {
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)
.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) {
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);
} catch (err) {
let base64 = JSON.stringify(btoa(String.fromCodePoint(...chunk)));
writeChunk(`Uint8Array.from(atob(${base64}), m => m.codePointAt(0))`, controller);
}
}
let remaining = decoder.decode();
if (remaining.length) {
writeChunk(JSON.stringify(remaining), controller);
}
}
function writeChunk(chunk, controller) {
controller.enqueue(encoder.encode(`<script>${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');
}