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