main
js 352 lines 9.46 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
13
14 let clientExports;
15 let turbopackMap;
16 let turbopackModules;
17 let turbopackModuleLoading;
18 let React;
19 let ReactDOMServer;
20 let ReactServer;
21 let ReactServerDOMServer;
22 let ReactServerDOMClient;
23 let Stream;
24 let use;
25 let serverAct;
26
27 const streamOptions = {
28 objectMode: true,
29 };
30
31 describe('ReactFlightTurbopackDOMNode', () => {
32 beforeEach(() => {
33 jest.resetModules();
34
35 patchSetImmediate();
36 serverAct = require('internal-test-utils').serverAct;
37
38 // Simulate the condition resolution
39 jest.mock('react', () => require('react/react.react-server'));
40 jest.mock('react-server-dom-turbopack/server', () =>
41 require('react-server-dom-turbopack/server.node'),
42 );
43 ReactServer = require('react');
44 ReactServerDOMServer = require('react-server-dom-turbopack/server');
45
46 const TurbopackMock = require('./utils/TurbopackMock');
47 clientExports = TurbopackMock.clientExports;
48 turbopackMap = TurbopackMock.turbopackMap;
49 turbopackModules = TurbopackMock.turbopackModules;
50 turbopackModuleLoading = TurbopackMock.moduleLoading;
51
52 jest.resetModules();
53 __unmockReact();
54 jest.unmock('react-server-dom-turbopack/server');
55 jest.mock('react-server-dom-turbopack/client', () =>
56 jest.requireActual('react-server-dom-turbopack/client.node'),
57 );
58
59 React = require('react');
60 ReactDOMServer = require('react-dom/server.node');
61 ReactServerDOMClient = require('react-server-dom-turbopack/client');
62 Stream = require('stream');
63 use = React.use;
64 });
65
66 function readResult(stream) {
67 return new Promise((resolve, reject) => {
68 let buffer = '';
69 const writable = new Stream.PassThrough();
70 writable.setEncoding('utf8');
71 writable.on('data', chunk => {
72 buffer += chunk;
73 });
74 writable.on('error', error => {
75 reject(error);
76 });
77 writable.on('end', () => {
78 resolve(buffer);
79 });
80 stream.pipe(writable);
81 });
82 }
83
84 function normalizeCodeLocInfo(str) {
85 return (
86 str &&
87 str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
88 return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
89 })
90 );
91 }
92
93 function createDelayedStream() {
94 let resolveDelayedStream;
95 const promise = new Promise(resolve => (resolveDelayedStream = resolve));
96 const delayedStream = new Stream.Transform({
97 ...streamOptions,
98 transform(chunk, encoding, callback) {
99 // Artificially delay pushing the chunk.
100 promise.then(() => {
101 this.push(chunk);
102 callback();
103 });
104 },
105 });
106 return {delayedStream, resolveDelayedStream};
107 }
108
109 it('should allow an alternative module mapping to be used for SSR', async () => {
110 function ClientComponent() {
111 return <span>Client Component</span>;
112 }
113 // The Client build may not have the same IDs as the Server bundles for the same
114 // component.
115 const ClientComponentOnTheClient = clientExports(
116 ClientComponent,
117 'path/to/chunk.js',
118 );
119 const ClientComponentOnTheServer = clientExports(ClientComponent);
120
121 // In the SSR bundle this module won't exist. We simulate this by deleting it.
122 const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
123 delete turbopackModules[clientId];
124
125 // Instead, we have to provide a translation from the client meta data to the SSR
126 // meta data.
127 const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
128 const translationMap = {
129 [clientId]: {
130 '*': ssrMetadata,
131 },
132 };
133
134 function App() {
135 return <ClientComponentOnTheClient />;
136 }
137
138 const stream = await serverAct(() =>
139 ReactServerDOMServer.renderToPipeableStream(<App />, turbopackMap),
140 );
141 const readable = new Stream.PassThrough();
142
143 stream.pipe(readable);
144
145 let response;
146 function ClientRoot() {
147 if (!response) {
148 response = ReactServerDOMClient.createFromNodeStream(readable, {
149 moduleMap: translationMap,
150 moduleLoading: turbopackModuleLoading,
151 });
152 }
153 return use(response);
154 }
155
156 const ssrStream = await serverAct(() =>
157 ReactDOMServer.renderToPipeableStream(<ClientRoot />),
158 );
159 const result = await readResult(ssrStream);
160 expect(result).toEqual(
161 '<script src="/prefix/path/to/chunk.js" async=""></script><span>Client Component</span>',
162 );
163 });
164
165 // @gate __DEV__
166 it('can transport debug info through a separate debug channel', async () => {
167 function Thrower() {
168 throw new Error('ssr-throw');
169 }
170
171 const ClientComponentOnTheClient = clientExports(
172 Thrower,
173 123,
174 'path/to/chunk.js',
175 );
176
177 const ClientComponentOnTheServer = clientExports(Thrower);
178
179 function App() {
180 return ReactServer.createElement(
181 ReactServer.Suspense,
182 null,
183 ReactServer.createElement(ClientComponentOnTheClient, null),
184 );
185 }
186
187 const debugReadable = new Stream.PassThrough(streamOptions);
188
189 const rscStream = await serverAct(() =>
190 ReactServerDOMServer.renderToPipeableStream(
191 ReactServer.createElement(App, null),
192 turbopackMap,
193 {
194 debugChannel: new Stream.Writable({
195 write(chunk, encoding, callback) {
196 debugReadable.write(chunk, encoding);
197 callback();
198 },
199 final() {
200 debugReadable.end();
201 },
202 }),
203 },
204 ),
205 );
206
207 // Create a delayed stream to simulate that the RSC stream might be
208 // transported slower than the debug channel, which must not lead to a
209 // `Connection closed` error in the Flight client.
210 const {delayedStream, resolveDelayedStream} = createDelayedStream();
211
212 rscStream.pipe(delayedStream);
213
214 function ClientRoot({response}) {
215 return use(response);
216 }
217
218 const serverConsumerManifest = {
219 moduleMap: {
220 [turbopackMap[ClientComponentOnTheClient.$$id].id]: {
221 '*': turbopackMap[ClientComponentOnTheServer.$$id],
222 },
223 },
224 moduleLoading: null,
225 };
226
227 const response = ReactServerDOMClient.createFromNodeStream(
228 delayedStream,
229 serverConsumerManifest,
230 {debugChannel: debugReadable},
231 );
232
233 setTimeout(resolveDelayedStream);
234
235 let ownerStack;
236
237 const ssrStream = await serverAct(() =>
238 ReactDOMServer.renderToPipeableStream(
239 <ClientRoot response={response} />,
240 {
241 onError(err, errorInfo) {
242 ownerStack = React.captureOwnerStack
243 ? React.captureOwnerStack()
244 : null;
245 },
246 },
247 ),
248 );
249
250 const result = await readResult(ssrStream);
251
252 expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
253
254 expect(result).toContain(
255 'Switched to client rendering because the server rendering errored:\n\nssr-throw',
256 );
257 });
258
259 // @gate __DEV__
260 it('can transport debug info through a slow debug channel', async () => {
261 function Thrower() {
262 throw new Error('ssr-throw');
263 }
264
265 const ClientComponentOnTheClient = clientExports(
266 Thrower,
267 123,
268 'path/to/chunk.js',
269 );
270
271 const ClientComponentOnTheServer = clientExports(Thrower);
272
273 function App() {
274 return ReactServer.createElement(
275 ReactServer.Suspense,
276 null,
277 ReactServer.createElement(ClientComponentOnTheClient, null),
278 );
279 }
280
281 // Create a delayed stream to simulate that the debug stream might be
282 // transported slower than the RSC stream, which must not lead to missing
283 // debug info.
284 const {delayedStream, resolveDelayedStream} = createDelayedStream();
285
286 const rscStream = await serverAct(() =>
287 ReactServerDOMServer.renderToPipeableStream(
288 ReactServer.createElement(App, null),
289 turbopackMap,
290 {
291 debugChannel: new Stream.Writable({
292 write(chunk, encoding, callback) {
293 delayedStream.write(chunk, encoding);
294 callback();
295 },
296 final() {
297 delayedStream.end();
298 },
299 }),
300 },
301 ),
302 );
303
304 const readable = new Stream.PassThrough(streamOptions);
305
306 rscStream.pipe(readable);
307
308 function ClientRoot({response}) {
309 return use(response);
310 }
311
312 const serverConsumerManifest = {
313 moduleMap: {
314 [turbopackMap[ClientComponentOnTheClient.$$id].id]: {
315 '*': turbopackMap[ClientComponentOnTheServer.$$id],
316 },
317 },
318 moduleLoading: null,
319 };
320
321 const response = ReactServerDOMClient.createFromNodeStream(
322 readable,
323 serverConsumerManifest,
324 {debugChannel: delayedStream},
325 );
326
327 setTimeout(resolveDelayedStream);
328
329 let ownerStack;
330
331 const ssrStream = await serverAct(() =>
332 ReactDOMServer.renderToPipeableStream(
333 <ClientRoot response={response} />,
334 {
335 onError(err, errorInfo) {
336 ownerStack = React.captureOwnerStack
337 ? React.captureOwnerStack()
338 : null;
339 },
340 },
341 ),
342 );
343
344 const result = await readResult(ssrStream);
345
346 expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
347
348 expect(result).toContain(
349 'Switched to client rendering because the server rendering errored:\n\nssr-throw',
350 );
351 });
352 });