main
js 329 lines 9.23 KB
Raw
1 'use strict';
2
3 const {PassThrough, Transform} = require('stream');
4
5 // ---------------------------------------------------------------------------
6 // Fizz (Node) — renders App directly via Node streams.
7 // Returns a Node Readable stream of HTML.
8 // ---------------------------------------------------------------------------
9
10 function renderFizzNode(AppComponent, itemCount) {
11 const React = require('react');
12 const {renderToPipeableStream} = require('react-dom/server');
13
14 const output = new PassThrough();
15 const {pipe} = renderToPipeableStream(
16 React.createElement(AppComponent, {itemCount}),
17 {
18 onShellReady() {
19 pipe(output);
20 },
21 onError(e) {
22 console.error('Fizz Node error:', e);
23 output.destroy(e);
24 },
25 }
26 );
27 return output;
28 }
29
30 // ---------------------------------------------------------------------------
31 // Fizz (Edge) — renders App directly via web streams.
32 // Returns a promise that resolves to a web ReadableStream of HTML.
33 // ---------------------------------------------------------------------------
34
35 function renderFizzEdge(AppComponent, itemCount) {
36 const React = require('react');
37 const {renderToReadableStream} = require('react-dom/server');
38
39 return renderToReadableStream(React.createElement(AppComponent, {itemCount}));
40 }
41
42 // ---------------------------------------------------------------------------
43 // Flight + Fizz (Node) — RSC render → tee → Fizz + script injection.
44 // HTML chunks are buffered within a tick to avoid injecting scripts mid-tag.
45 // Returns a Node Readable stream of HTML with injected Flight scripts.
46 // ---------------------------------------------------------------------------
47
48 function renderFlightFizzNode(
49 renderRSCNode,
50 AppComponent,
51 itemCount,
52 clientManifest,
53 ssrManifest,
54 opts
55 ) {
56 const inject = !opts || opts.inject !== false;
57 const React = require('react');
58 const {renderToPipeableStream} = require('react-dom/server');
59 const {createFromNodeStream} = require('react-server-dom-webpack/client');
60
61 const {pipe: rscPipe} = renderRSCNode(
62 clientManifest,
63 AppComponent,
64 itemCount
65 );
66
67 let flightStream;
68 let flightScripts = '';
69 if (inject) {
70 // Tee the Flight stream into SSR + script injection
71 const trunk = new PassThrough();
72 const forSsr = new PassThrough();
73 const forInline = new PassThrough();
74 trunk.pipe(forSsr);
75 trunk.pipe(forInline);
76
77 forInline.on('data', function (chunk) {
78 flightScripts +=
79 '<script>(self.__FLIGHT_DATA||=[]).push(' +
80 JSON.stringify(chunk.toString()) +
81 ')</script>';
82 });
83
84 rscPipe(trunk);
85 flightStream = forSsr;
86 } else {
87 flightStream = new PassThrough();
88 rscPipe(flightStream);
89 }
90
91 let cachedResult;
92 function Root() {
93 if (!cachedResult) {
94 cachedResult = createFromNodeStream(flightStream, ssrManifest);
95 }
96 return React.use(cachedResult);
97 }
98
99 const output = new PassThrough();
100
101 const {pipe} = renderToPipeableStream(React.createElement(Root), {
102 onShellReady() {
103 if (inject) {
104 // Buffer HTML chunks within a tick to avoid injecting scripts mid-tag.
105 const trailer = '</body></html>';
106 let buffered = [];
107 let timeout = null;
108 const injector = new Transform({
109 transform(chunk, _encoding, cb) {
110 buffered.push(chunk);
111 if (!timeout) {
112 timeout = setTimeout(() => {
113 for (const buf of buffered) {
114 let str = buf.toString();
115 if (str.endsWith(trailer)) {
116 str = str.slice(0, -trailer.length);
117 }
118 this.push(str);
119 }
120 buffered.length = 0;
121 timeout = null;
122 if (flightScripts) {
123 this.push(flightScripts);
124 flightScripts = '';
125 }
126 }, 0);
127 }
128 cb();
129 },
130 flush(cb) {
131 if (timeout) {
132 clearTimeout(timeout);
133 for (const buf of buffered) {
134 let str = buf.toString();
135 if (str.endsWith(trailer)) {
136 str = str.slice(0, -trailer.length);
137 }
138 this.push(str);
139 }
140 buffered.length = 0;
141 }
142 if (flightScripts) {
143 this.push(flightScripts);
144 flightScripts = '';
145 }
146 this.push(trailer);
147 cb();
148 },
149 });
150 pipe(injector);
151 injector.pipe(output);
152 } else {
153 pipe(output);
154 }
155 },
156 onError(e) {
157 console.error('Flight+Fizz Node error:', e);
158 output.destroy(e);
159 },
160 });
161
162 return output;
163 }
164
165 // ---------------------------------------------------------------------------
166 // Flight + Fizz (Edge) — RSC render → tee → Fizz + script injection via web
167 // streams. HTML chunks are buffered within a tick to avoid injecting scripts
168 // mid-tag. The </body></html> trailer is stripped, Flight scripts injected,
169 // and the trailer re-added at flush.
170 // Returns a promise that resolves to a web ReadableStream.
171 // ---------------------------------------------------------------------------
172
173 function renderFlightFizzEdge(
174 renderRSCEdge,
175 AppComponent,
176 itemCount,
177 clientManifest,
178 ssrManifest,
179 opts
180 ) {
181 const inject = !opts || opts.inject !== false;
182 const React = require('react');
183 const {renderToReadableStream} = require('react-dom/server');
184 const {
185 createFromReadableStream,
186 } = require('react-server-dom-webpack/client.edge');
187
188 const webStream = renderRSCEdge(clientManifest, AppComponent, itemCount);
189
190 let forSsr;
191 let injector;
192
193 if (inject) {
194 const htmlTrailer = '</body></html>';
195 const enc = new TextEncoder();
196
197 let forInline;
198 [forSsr, forInline] = webStream.tee();
199
200 let resolveInline;
201 const inlinePromise = new Promise(function (r) {
202 resolveInline = r;
203 });
204 const htmlDecoder = new TextDecoder();
205 let buffered = [];
206 let timeout = null;
207
208 function flushBuffered(controller) {
209 for (const chunk of buffered) {
210 let buf = htmlDecoder.decode(chunk, {stream: true});
211 if (buf.endsWith(htmlTrailer)) {
212 buf = buf.slice(0, -htmlTrailer.length);
213 }
214 controller.enqueue(enc.encode(buf));
215 }
216 const remaining = htmlDecoder.decode();
217 if (remaining.length) {
218 let buf = remaining;
219 if (buf.endsWith(htmlTrailer)) {
220 buf = buf.slice(0, -htmlTrailer.length);
221 }
222 controller.enqueue(enc.encode(buf));
223 }
224 buffered.length = 0;
225 timeout = null;
226 }
227
228 function writeFlightChunk(data, controller) {
229 controller.enqueue(
230 enc.encode(
231 '<script>(self.__FLIGHT_DATA||=[]).push(' +
232 JSON.stringify(data) +
233 ')</script>'
234 )
235 );
236 }
237
238 injector = new TransformStream({
239 start(controller) {
240 (async function () {
241 const reader = forInline.getReader();
242 const decoder = new TextDecoder('utf-8', {fatal: true});
243 for (;;) {
244 const {done, value} = await reader.read();
245 if (done) break;
246 writeFlightChunk(decoder.decode(value, {stream: true}), controller);
247 }
248 const remaining = decoder.decode();
249 if (remaining.length) {
250 writeFlightChunk(remaining, controller);
251 }
252 resolveInline();
253 })();
254 },
255 transform(chunk, controller) {
256 buffered.push(chunk);
257 if (!timeout) {
258 timeout = setTimeout(function () {
259 flushBuffered(controller);
260 }, 0);
261 }
262 },
263 async flush(controller) {
264 await inlinePromise;
265 if (timeout) {
266 clearTimeout(timeout);
267 flushBuffered(controller);
268 }
269 controller.enqueue(enc.encode(htmlTrailer));
270 },
271 });
272 } else {
273 forSsr = webStream;
274 }
275
276 const cachedResult = createFromReadableStream(forSsr, {
277 serverConsumerManifest: ssrManifest,
278 });
279 function Root() {
280 return React.use(cachedResult);
281 }
282
283 return renderToReadableStream(React.createElement(Root)).then(
284 function (htmlStream) {
285 return injector ? htmlStream.pipeThrough(injector) : htmlStream;
286 }
287 );
288 }
289
290 // ---------------------------------------------------------------------------
291 // Utilities: collect streams into strings.
292 // ---------------------------------------------------------------------------
293
294 function nodeStreamToString(nodeStream) {
295 return new Promise(function (resolve, reject) {
296 const chunks = [];
297 nodeStream.on('data', function (chunk) {
298 chunks.push(chunk);
299 });
300 nodeStream.on('end', function () {
301 resolve(Buffer.concat(chunks).toString('utf-8'));
302 });
303 nodeStream.on('error', reject);
304 });
305 }
306
307 function webStreamToString(webStream) {
308 const reader = webStream.getReader();
309 const chunks = [];
310 function read() {
311 return reader.read().then(function ({done, value}) {
312 if (done) {
313 return Buffer.concat(chunks).toString('utf-8');
314 }
315 chunks.push(Buffer.from(value));
316 return read();
317 });
318 }
319 return read();
320 }
321
322 module.exports = {
323 renderFizzNode,
324 renderFizzEdge,
325 renderFlightFizzNode,
326 renderFlightFizzEdge,
327 nodeStreamToString,
328 webStreamToString,
329 };