[Flight] Cleanup turbopack tests (#27552)
Renames turbopack flight test files. removes tests which are primarily testing internal implemenation behavior of Flight Client itself
Josh Story committed
Oct 20, 2023 at 23:21 UTC
90172d12e8f06936ade78ed916397c16a2bec4f8
10 files changed
+388
-2266
packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js
deleted
-1572
@@ -1,1572 +0,0 @@
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
-// Polyfills for test environment
13
-global.ReadableStream =
14
- require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15
-global.TextEncoder = require('util').TextEncoder;
16
-global.TextDecoder = require('util').TextDecoder;
17
-
18
-// Don't wait before processing work on the server.
19
-// TODO: we can replace this with FlightServer.act().
20
-global.setImmediate = cb => cb();
21
-
22
-let act;
23
-let use;
24
-let clientExports;
25
-let clientModuleError;
26
-let turbopackMap;
27
-let Stream;
28
-let FlightReact;
29
-let React;
30
-let FlightReactDOM;
31
-let ReactDOMClient;
32
-let ReactServerDOMServer;
33
-let ReactServerDOMClient;
34
-let ReactDOMFizzServer;
35
-let Suspense;
36
-let ErrorBoundary;
37
-let JSDOM;
38
-
39
-describe('ReactFlightDOM', () => {
40
- beforeEach(() => {
41
- // For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server
42
- // This can be thought of as essentially being the React Server Components scope with react-server
43
- // condition
44
- jest.resetModules();
45
-
46
- JSDOM = require('jsdom').JSDOM;
47
-
48
- // Simulate the condition resolution
49
- jest.mock('react-server-dom-turbopack/server', () =>
50
- require('react-server-dom-turbopack/server.node.unbundled'),
51
- );
52
- jest.mock('react', () => require('react/react.shared-subset'));
53
-
54
- const TurbopackMock = require('./utils/TurbopackMock');
55
- clientExports = TurbopackMock.clientExports;
56
- clientModuleError = TurbopackMock.clientModuleError;
57
- turbopackMap = TurbopackMock.turbopackMap;
58
-
59
- ReactServerDOMServer = require('react-server-dom-turbopack/server');
60
- FlightReact = require('react');
61
- FlightReactDOM = require('react-dom');
62
-
63
- // This reset is to load modules for the SSR/Browser scope.
64
- jest.resetModules();
65
- __unmockReact();
66
- act = require('internal-test-utils').act;
67
- Stream = require('stream');
68
- React = require('react');
69
- use = React.use;
70
- Suspense = React.Suspense;
71
- ReactDOMClient = require('react-dom/client');
72
- ReactDOMFizzServer = require('react-dom/server.node');
73
- ReactServerDOMClient = require('react-server-dom-turbopack/client');
74
-
75
- ErrorBoundary = class extends React.Component {
76
- state = {hasError: false, error: null};
77
- static getDerivedStateFromError(error) {
78
- return {
79
- hasError: true,
80
- error,
81
- };
82
- }
83
- render() {
84
- if (this.state.hasError) {
85
- return this.props.fallback(this.state.error);
86
- }
87
- return this.props.children;
88
- }
89
- };
90
- });
91
-
92
- function getTestStream() {
93
- const writable = new Stream.PassThrough();
94
- const readable = new ReadableStream({
95
- start(controller) {
96
- writable.on('data', chunk => {
97
- controller.enqueue(chunk);
98
- });
99
- writable.on('end', () => {
100
- controller.close();
101
- });
102
- },
103
- });
104
- return {
105
- readable,
106
- writable,
107
- };
108
- }
109
-
110
- const theInfinitePromise = new Promise(() => {});
111
- function InfiniteSuspend() {
112
- throw theInfinitePromise;
113
- }
114
-
115
- function getMeaningfulChildren(element) {
116
- const children = [];
117
- let node = element.firstChild;
118
- while (node) {
119
- if (node.nodeType === 1) {
120
- if (
121
- // some tags are ambiguous and might be hidden because they look like non-meaningful children
122
- // so we have a global override where if this data attribute is included we also include the node
123
- node.hasAttribute('data-meaningful') ||
124
- (node.tagName === 'SCRIPT' &&
125
- node.hasAttribute('src') &&
126
- node.hasAttribute('async')) ||
127
- (node.tagName !== 'SCRIPT' &&
128
- node.tagName !== 'TEMPLATE' &&
129
- node.tagName !== 'template' &&
130
- !node.hasAttribute('hidden') &&
131
- !node.hasAttribute('aria-hidden'))
132
- ) {
133
- const props = {};
134
- const attributes = node.attributes;
135
- for (let i = 0; i < attributes.length; i++) {
136
- if (
137
- attributes[i].name === 'id' &&
138
- attributes[i].value.includes(':')
139
- ) {
140
- // We assume this is a React added ID that's a non-visual implementation detail.
141
- continue;
142
- }
143
- props[attributes[i].name] = attributes[i].value;
144
- }
145
- props.children = getMeaningfulChildren(node);
146
- children.push(React.createElement(node.tagName.toLowerCase(), props));
147
- }
148
- } else if (node.nodeType === 3) {
149
- children.push(node.data);
150
- }
151
- node = node.nextSibling;
152
- }
153
- return children.length === 0
154
- ? undefined
155
- : children.length === 1
156
- ? children[0]
157
- : children;
158
- }
159
-
160
- it('should resolve HTML using Node streams', async () => {
161
- function Text({children}) {
162
- return <span>{children}</span>;
163
- }
164
- function HTML() {
165
- return (
166
- <div>
167
- <Text>hello</Text>
168
- <Text>world</Text>
169
- </div>
170
- );
171
- }
172
-
173
- function App() {
174
- const model = {
175
- html: <HTML />,
176
- };
177
- return model;
178
- }
179
-
180
- const {writable, readable} = getTestStream();
181
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
182
- <App />,
183
- turbopackMap,
184
- );
185
- pipe(writable);
186
- const response = ReactServerDOMClient.createFromReadableStream(readable);
187
- const model = await response;
188
- expect(model).toEqual({
189
- html: (
190
- <div>
191
- <span>hello</span>
192
- <span>world</span>
193
- </div>
194
- ),
195
- });
196
- });
197
-
198
- it('should resolve the root', async () => {
199
- // Model
200
- function Text({children}) {
201
- return <span>{children}</span>;
202
- }
203
- function HTML() {
204
- return (
205
- <div>
206
- <Text>hello</Text>
207
- <Text>world</Text>
208
- </div>
209
- );
210
- }
211
- function RootModel() {
212
- return {
213
- html: <HTML />,
214
- };
215
- }
216
-
217
- // View
218
- function Message({response}) {
219
- return <section>{use(response).html}</section>;
220
- }
221
- function App({response}) {
222
- return (
223
- <Suspense fallback={<h1>Loading...</h1>}>
224
- <Message response={response} />
225
- </Suspense>
226
- );
227
- }
228
-
229
- const {writable, readable} = getTestStream();
230
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
231
- <RootModel />,
232
- turbopackMap,
233
- );
234
- pipe(writable);
235
- const response = ReactServerDOMClient.createFromReadableStream(readable);
236
-
237
- const container = document.createElement('div');
238
- const root = ReactDOMClient.createRoot(container);
239
- await act(() => {
240
- root.render(<App response={response} />);
241
- });
242
- expect(container.innerHTML).toBe(
243
- '<section><div><span>hello</span><span>world</span></div></section>',
244
- );
245
- });
246
-
247
- it('should not get confused by $', async () => {
248
- // Model
249
- function RootModel() {
250
- return {text: '$1'};
251
- }
252
-
253
- // View
254
- function Message({response}) {
255
- return <p>{use(response).text}</p>;
256
- }
257
- function App({response}) {
258
- return (
259
- <Suspense fallback={<h1>Loading...</h1>}>
260
- <Message response={response} />
261
- </Suspense>
262
- );
263
- }
264
-
265
- const {writable, readable} = getTestStream();
266
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
267
- <RootModel />,
268
- turbopackMap,
269
- );
270
- pipe(writable);
271
- const response = ReactServerDOMClient.createFromReadableStream(readable);
272
-
273
- const container = document.createElement('div');
274
- const root = ReactDOMClient.createRoot(container);
275
- await act(() => {
276
- root.render(<App response={response} />);
277
- });
278
- expect(container.innerHTML).toBe('<p>$1</p>');
279
- });
280
-
281
- it('should not get confused by @', async () => {
282
- // Model
283
- function RootModel() {
284
- return {text: '@div'};
285
- }
286
-
287
- // View
288
- function Message({response}) {
289
- return <p>{use(response).text}</p>;
290
- }
291
- function App({response}) {
292
- return (
293
- <Suspense fallback={<h1>Loading...</h1>}>
294
- <Message response={response} />
295
- </Suspense>
296
- );
297
- }
298
-
299
- const {writable, readable} = getTestStream();
300
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
301
- <RootModel />,
302
- turbopackMap,
303
- );
304
- pipe(writable);
305
- const response = ReactServerDOMClient.createFromReadableStream(readable);
306
-
307
- const container = document.createElement('div');
308
- const root = ReactDOMClient.createRoot(container);
309
- await act(() => {
310
- root.render(<App response={response} />);
311
- });
312
- expect(container.innerHTML).toBe('<p>@div</p>');
313
- });
314
-
315
- it('should be able to esm compat test module references', async () => {
316
- const ESMCompatModule = {
317
- __esModule: true,
318
- default: function ({greeting}) {
319
- return greeting + ' World';
320
- },
321
- hi: 'Hello',
322
- };
323
-
324
- function Print({response}) {
325
- return <p>{use(response)}</p>;
326
- }
327
-
328
- function App({response}) {
329
- return (
330
- <Suspense fallback={<h1>Loading...</h1>}>
331
- <Print response={response} />
332
- </Suspense>
333
- );
334
- }
335
-
336
- function interopWebpack(obj) {
337
- // Basically what Webpack's ESM interop feature testing does.
338
- if (typeof obj === 'object' && obj.__esModule) {
339
- return obj;
340
- }
341
- return Object.assign({default: obj}, obj);
342
- }
343
-
344
- const {default: Component, hi} = interopWebpack(
345
- clientExports(ESMCompatModule),
346
- );
347
-
348
- const {writable, readable} = getTestStream();
349
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
350
- <Component greeting={hi} />,
351
- turbopackMap,
352
- );
353
- pipe(writable);
354
- const response = ReactServerDOMClient.createFromReadableStream(readable);
355
-
356
- const container = document.createElement('div');
357
- const root = ReactDOMClient.createRoot(container);
358
- await act(() => {
359
- root.render(<App response={response} />);
360
- });
361
- expect(container.innerHTML).toBe('<p>Hello World</p>');
362
- });
363
-
364
- it('should be able to render a named component export', async () => {
365
- const Module = {
366
- Component: function ({greeting}) {
367
- return greeting + ' World';
368
- },
369
- };
370
-
371
- function Print({response}) {
372
- return <p>{use(response)}</p>;
373
- }
374
-
375
- function App({response}) {
376
- return (
377
- <Suspense fallback={<h1>Loading...</h1>}>
378
- <Print response={response} />
379
- </Suspense>
380
- );
381
- }
382
-
383
- const {Component} = clientExports(Module);
384
-
385
- const {writable, readable} = getTestStream();
386
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
387
- <Component greeting={'Hello'} />,
388
- turbopackMap,
389
- );
390
- pipe(writable);
391
- const response = ReactServerDOMClient.createFromReadableStream(readable);
392
-
393
- const container = document.createElement('div');
394
- const root = ReactDOMClient.createRoot(container);
395
- await act(() => {
396
- root.render(<App response={response} />);
397
- });
398
- expect(container.innerHTML).toBe('<p>Hello World</p>');
399
- });
400
-
401
- it('should be able to render a module split named component export', async () => {
402
- const Module = {
403
- // This gets split into a separate module from the original one.
404
- split: function ({greeting}) {
405
- return greeting + ' World';
406
- },
407
- };
408
-
409
- function Print({response}) {
410
- return <p>{use(response)}</p>;
411
- }
412
-
413
- function App({response}) {
414
- return (
415
- <Suspense fallback={<h1>Loading...</h1>}>
416
- <Print response={response} />
417
- </Suspense>
418
- );
419
- }
420
-
421
- const {split: Component} = clientExports(Module);
422
-
423
- const {writable, readable} = getTestStream();
424
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
425
- <Component greeting={'Hello'} />,
426
- turbopackMap,
427
- );
428
- pipe(writable);
429
- const response = ReactServerDOMClient.createFromReadableStream(readable);
430
-
431
- const container = document.createElement('div');
432
- const root = ReactDOMClient.createRoot(container);
433
- await act(() => {
434
- root.render(<App response={response} />);
435
- });
436
- expect(container.innerHTML).toBe('<p>Hello World</p>');
437
- });
438
-
439
- it('should unwrap async module references', async () => {
440
- const AsyncModule = Promise.resolve(function AsyncModule({text}) {
441
- return 'Async: ' + text;
442
- });
443
-
444
- const AsyncModule2 = Promise.resolve({
445
- exportName: 'Module',
446
- });
447
-
448
- function Print({response}) {
449
- return <p>{use(response)}</p>;
450
- }
451
-
452
- function App({response}) {
453
- return (
454
- <Suspense fallback={<h1>Loading...</h1>}>
455
- <Print response={response} />
456
- </Suspense>
457
- );
458
- }
459
-
460
- const AsyncModuleRef = await clientExports(AsyncModule);
461
- const AsyncModuleRef2 = await clientExports(AsyncModule2);
462
-
463
- const {writable, readable} = getTestStream();
464
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
465
- <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
466
- turbopackMap,
467
- );
468
- pipe(writable);
469
- const response = ReactServerDOMClient.createFromReadableStream(readable);
470
-
471
- const container = document.createElement('div');
472
- const root = ReactDOMClient.createRoot(container);
473
- await act(() => {
474
- root.render(<App response={response} />);
475
- });
476
- expect(container.innerHTML).toBe('<p>Async: Module</p>');
477
- });
478
-
479
- it('should unwrap async module references using use', async () => {
480
- const AsyncModule = Promise.resolve('Async Text');
481
-
482
- function Print({response}) {
483
- return use(response);
484
- }
485
-
486
- function App({response}) {
487
- return (
488
- <Suspense fallback={<h1>Loading...</h1>}>
489
- <Print response={response} />
490
- </Suspense>
491
- );
492
- }
493
-
494
- const AsyncModuleRef = clientExports(AsyncModule);
495
-
496
- function ServerComponent() {
497
- const text = FlightReact.use(AsyncModuleRef);
498
- return <p>{text}</p>;
499
- }
500
-
501
- const {writable, readable} = getTestStream();
502
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
503
- <ServerComponent />,
504
- turbopackMap,
505
- );
506
- pipe(writable);
507
- const response = ReactServerDOMClient.createFromReadableStream(readable);
508
-
509
- const container = document.createElement('div');
510
- const root = ReactDOMClient.createRoot(container);
511
- await act(() => {
512
- root.render(<App response={response} />);
513
- });
514
- expect(container.innerHTML).toBe('<p>Async Text</p>');
515
- });
516
-
517
- it('should be able to import a name called "then"', async () => {
518
- const thenExports = {
519
- then: function then() {
520
- return 'and then';
521
- },
522
- };
523
-
524
- function Print({response}) {
525
- return <p>{use(response)}</p>;
526
- }
527
-
528
- function App({response}) {
529
- return (
530
- <Suspense fallback={<h1>Loading...</h1>}>
531
- <Print response={response} />
532
- </Suspense>
533
- );
534
- }
535
-
536
- const ThenRef = clientExports(thenExports).then;
537
-
538
- const {writable, readable} = getTestStream();
539
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
540
- <ThenRef />,
541
- turbopackMap,
542
- );
543
- pipe(writable);
544
- const response = ReactServerDOMClient.createFromReadableStream(readable);
545
-
546
- const container = document.createElement('div');
547
- const root = ReactDOMClient.createRoot(container);
548
- await act(() => {
549
- root.render(<App response={response} />);
550
- });
551
- expect(container.innerHTML).toBe('<p>and then</p>');
552
- });
553
-
554
- it('throws when accessing a member below the client exports', () => {
555
- const ClientModule = clientExports({
556
- Component: {deep: 'thing'},
557
- });
558
- function dotting() {
559
- return ClientModule.Component.deep;
560
- }
561
- expect(dotting).toThrowError(
562
- 'Cannot access Component.deep on the server. ' +
563
- 'You cannot dot into a client module from a server component. ' +
564
- 'You can only pass the imported name through.',
565
- );
566
- });
567
-
568
- it('does not throw when React inspects any deep props', () => {
569
- const ClientModule = clientExports({
570
- Component: function () {},
571
- });
572
- <ClientModule.Component key="this adds instrumentation" />;
573
- });
574
-
575
- it('throws when accessing a Context.Provider below the client exports', () => {
576
- const Context = React.createContext();
577
- const ClientModule = clientExports({
578
- Context,
579
- });
580
- function dotting() {
581
- return ClientModule.Context.Provider;
582
- }
583
- expect(dotting).toThrowError(
584
- `Cannot render a Client Context Provider on the Server. ` +
585
- `Instead, you can export a Client Component wrapper ` +
586
- `that itself renders a Client Context Provider.`,
587
- );
588
- });
589
-
590
- it('should progressively reveal server components', async () => {
591
- let reportedErrors = [];
592
-
593
- // Client Components
594
-
595
- function MyErrorBoundary({children}) {
596
- return (
597
- <ErrorBoundary
598
- fallback={e => (
599
- <p>
600
- {__DEV__ ? e.message + ' + ' : null}
601
- {e.digest}
602
- </p>
603
- )}>
604
- {children}
605
- </ErrorBoundary>
606
- );
607
- }
608
-
609
- // Model
610
- function Text({children}) {
611
- return children;
612
- }
613
-
614
- function makeDelayedText() {
615
- let _resolve, _reject;
616
- let promise = new Promise((resolve, reject) => {
617
- _resolve = () => {
618
- promise = null;
619
- resolve();
620
- };
621
- _reject = e => {
622
- promise = null;
623
- reject(e);
624
- };
625
- });
626
- async function DelayedText({children}) {
627
- await promise;
628
- return <Text>{children}</Text>;
629
- }
630
- return [DelayedText, _resolve, _reject];
631
- }
632
-
633
- const [Friends, resolveFriends] = makeDelayedText();
634
- const [Name, resolveName] = makeDelayedText();
635
- const [Posts, resolvePosts] = makeDelayedText();
636
- const [Photos, resolvePhotos] = makeDelayedText();
637
- const [Games, , rejectGames] = makeDelayedText();
638
-
639
- // View
640
- function ProfileDetails({avatar}) {
641
- return (
642
- <div>
643
- <Name>:name:</Name>
644
- {avatar}
645
- </div>
646
- );
647
- }
648
- function ProfileSidebar({friends}) {
649
- return (
650
- <div>
651
- <Photos>:photos:</Photos>
652
- {friends}
653
- </div>
654
- );
655
- }
656
- function ProfilePosts({posts}) {
657
- return <div>{posts}</div>;
658
- }
659
- function ProfileGames({games}) {
660
- return <div>{games}</div>;
661
- }
662
-
663
- const MyErrorBoundaryClient = clientExports(MyErrorBoundary);
664
-
665
- function ProfileContent() {
666
- return (
667
- <>
668
- <ProfileDetails avatar={<Text>:avatar:</Text>} />
669
- <Suspense fallback={<p>(loading sidebar)</p>}>
670
- <ProfileSidebar friends={<Friends>:friends:</Friends>} />
671
- </Suspense>
672
- <Suspense fallback={<p>(loading posts)</p>}>
673
- <ProfilePosts posts={<Posts>:posts:</Posts>} />
674
- </Suspense>
675
- <MyErrorBoundaryClient>
676
- <Suspense fallback={<p>(loading games)</p>}>
677
- <ProfileGames games={<Games>:games:</Games>} />
678
- </Suspense>
679
- </MyErrorBoundaryClient>
680
- </>
681
- );
682
- }
683
-
684
- const model = {
685
- rootContent: <ProfileContent />,
686
- };
687
-
688
- function ProfilePage({response}) {
689
- return use(response).rootContent;
690
- }
691
-
692
- const {writable, readable} = getTestStream();
693
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
694
- model,
695
- turbopackMap,
696
- {
697
- onError(x) {
698
- reportedErrors.push(x);
699
- return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
700
- },
701
- },
702
- );
703
- pipe(writable);
704
- const response = ReactServerDOMClient.createFromReadableStream(readable);
705
-
706
- const container = document.createElement('div');
707
- const root = ReactDOMClient.createRoot(container);
708
- await act(() => {
709
- root.render(
710
- <Suspense fallback={<p>(loading)</p>}>
711
- <ProfilePage response={response} />
712
- </Suspense>,
713
- );
714
- });
715
- expect(container.innerHTML).toBe('<p>(loading)</p>');
716
-
717
- // This isn't enough to show anything.
718
- await act(() => {
719
- resolveFriends();
720
- });
721
- expect(container.innerHTML).toBe('<p>(loading)</p>');
722
-
723
- // We can now show the details. Sidebar and posts are still loading.
724
- await act(() => {
725
- resolveName();
726
- });
727
- // Advance time enough to trigger a nested fallback.
728
- await act(() => {
729
- jest.advanceTimersByTime(500);
730
- });
731
- expect(container.innerHTML).toBe(
732
- '<div>:name::avatar:</div>' +
733
- '<p>(loading sidebar)</p>' +
734
- '<p>(loading posts)</p>' +
735
- '<p>(loading games)</p>',
736
- );
737
-
738
- expect(reportedErrors).toEqual([]);
739
-
740
- const theError = new Error('Game over');
741
- // Let's *fail* loading games.
742
- await act(async () => {
743
- await rejectGames(theError);
744
- await 'the inner async function';
745
- });
746
- const expectedGamesValue = __DEV__
747
- ? '<p>Game over + a dev digest</p>'
748
- : '<p>digest("Game over")</p>';
749
- expect(container.innerHTML).toBe(
750
- '<div>:name::avatar:</div>' +
751
- '<p>(loading sidebar)</p>' +
752
- '<p>(loading posts)</p>' +
753
- expectedGamesValue,
754
- );
755
-
756
- expect(reportedErrors).toEqual([theError]);
757
- reportedErrors = [];
758
-
759
- // We can now show the sidebar.
760
- await act(async () => {
761
- await resolvePhotos();
762
- await 'the inner async function';
763
- });
764
- expect(container.innerHTML).toBe(
765
- '<div>:name::avatar:</div>' +
766
- '<div>:photos::friends:</div>' +
767
- '<p>(loading posts)</p>' +
768
- expectedGamesValue,
769
- );
770
-
771
- // Show everything.
772
- await act(async () => {
773
- await resolvePosts();
774
- await 'the inner async function';
775
- });
776
- expect(container.innerHTML).toBe(
777
- '<div>:name::avatar:</div>' +
778
- '<div>:photos::friends:</div>' +
779
- '<div>:posts:</div>' +
780
- expectedGamesValue,
781
- );
782
-
783
- expect(reportedErrors).toEqual([]);
784
- });
785
-
786
- it('should preserve state of client components on refetch', async () => {
787
- // Client
788
-
789
- function Page({response}) {
790
- return use(response);
791
- }
792
-
793
- function Input() {
794
- return <input />;
795
- }
796
-
797
- const InputClient = clientExports(Input);
798
-
799
- // Server
800
-
801
- function App({color}) {
802
- // Verify both DOM and Client children.
803
- return (
804
- <div style={{color}}>
805
- <input />
806
- <InputClient />
807
- </div>
808
- );
809
- }
810
-
811
- const container = document.createElement('div');
812
- const root = ReactDOMClient.createRoot(container);
813
-
814
- const stream1 = getTestStream();
815
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
816
- <App color="red" />,
817
- turbopackMap,
818
- );
819
- pipe(stream1.writable);
820
- const response1 = ReactServerDOMClient.createFromReadableStream(
821
- stream1.readable,
822
- );
823
- await act(() => {
824
- root.render(
825
- <Suspense fallback={<p>(loading)</p>}>
826
- <Page response={response1} />
827
- </Suspense>,
828
- );
829
- });
830
- expect(container.children.length).toBe(1);
831
- expect(container.children[0].tagName).toBe('DIV');
832
- expect(container.children[0].style.color).toBe('red');
833
-
834
- // Change the DOM state for both inputs.
835
- const inputA = container.children[0].children[0];
836
- expect(inputA.tagName).toBe('INPUT');
837
- inputA.value = 'hello';
838
- const inputB = container.children[0].children[1];
839
- expect(inputB.tagName).toBe('INPUT');
840
- inputB.value = 'goodbye';
841
-
842
- const stream2 = getTestStream();
843
- const {pipe: pipe2} = ReactServerDOMServer.renderToPipeableStream(
844
- <App color="blue" />,
845
- turbopackMap,
846
- );
847
- pipe2(stream2.writable);
848
- const response2 = ReactServerDOMClient.createFromReadableStream(
849
- stream2.readable,
850
- );
851
- await act(() => {
852
- root.render(
853
- <Suspense fallback={<p>(loading)</p>}>
854
- <Page response={response2} />
855
- </Suspense>,
856
- );
857
- });
858
- expect(container.children.length).toBe(1);
859
- expect(container.children[0].tagName).toBe('DIV');
860
- expect(container.children[0].style.color).toBe('blue');
861
-
862
- // Verify we didn't destroy the DOM for either input.
863
- expect(inputA === container.children[0].children[0]).toBe(true);
864
- expect(inputA.tagName).toBe('INPUT');
865
- expect(inputA.value).toBe('hello');
866
- expect(inputB === container.children[0].children[1]).toBe(true);
867
- expect(inputB.tagName).toBe('INPUT');
868
- expect(inputB.value).toBe('goodbye');
869
- });
870
-
871
- it('should be able to complete after aborting and throw the reason client-side', async () => {
872
- const reportedErrors = [];
873
-
874
- const {writable, readable} = getTestStream();
875
- const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
876
- <div>
877
- <InfiniteSuspend />
878
- </div>,
879
- turbopackMap,
880
- {
881
- onError(x) {
882
- reportedErrors.push(x);
883
- const message = typeof x === 'string' ? x : x.message;
884
- return __DEV__ ? 'a dev digest' : `digest("${message}")`;
885
- },
886
- },
887
- );
888
- pipe(writable);
889
- const response = ReactServerDOMClient.createFromReadableStream(readable);
890
-
891
- const container = document.createElement('div');
892
- const root = ReactDOMClient.createRoot(container);
893
-
894
- function App({res}) {
895
- return use(res);
896
- }
897
-
898
- await act(() => {
899
- root.render(
900
- <ErrorBoundary
901
- fallback={e => (
902
- <p>
903
- {__DEV__ ? e.message + ' + ' : null}
904
- {e.digest}
905
- </p>
906
- )}>
907
- <Suspense fallback={<p>(loading)</p>}>
908
- <App res={response} />
909
- </Suspense>
910
- </ErrorBoundary>,
911
- );
912
- });
913
- expect(container.innerHTML).toBe('<p>(loading)</p>');
914
-
915
- await act(() => {
916
- abort('for reasons');
917
- });
918
- if (__DEV__) {
919
- expect(container.innerHTML).toBe(
920
- '<p>Error: for reasons + a dev digest</p>',
921
- );
922
- } else {
923
- expect(container.innerHTML).toBe('<p>digest("for reasons")</p>');
924
- }
925
-
926
- expect(reportedErrors).toEqual(['for reasons']);
927
- });
928
-
929
- it('should be able to recover from a direct reference erroring client-side', async () => {
930
- const reportedErrors = [];
931
-
932
- const ClientComponent = clientExports(function ({prop}) {
933
- return 'This should never render';
934
- });
935
-
936
- const ClientReference = clientModuleError(new Error('module init error'));
937
-
938
- const {writable, readable} = getTestStream();
939
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
940
- <div>
941
- <ClientComponent prop={ClientReference} />
942
- </div>,
943
- turbopackMap,
944
- {
945
- onError(x) {
946
- reportedErrors.push(x);
947
- },
948
- },
949
- );
950
- pipe(writable);
951
- const response = ReactServerDOMClient.createFromReadableStream(readable);
952
-
953
- const container = document.createElement('div');
954
- const root = ReactDOMClient.createRoot(container);
955
-
956
- function App({res}) {
957
- return use(res);
958
- }
959
-
960
- await act(() => {
961
- root.render(
962
- <ErrorBoundary fallback={e => <p>{e.message}</p>}>
963
- <Suspense fallback={<p>(loading)</p>}>
964
- <App res={response} />
965
- </Suspense>
966
- </ErrorBoundary>,
967
- );
968
- });
969
- expect(container.innerHTML).toBe('<p>module init error</p>');
970
-
971
- expect(reportedErrors).toEqual([]);
972
- });
973
-
974
- it('should be able to recover from a direct reference erroring client-side async', async () => {
975
- const reportedErrors = [];
976
-
977
- const ClientComponent = clientExports(function ({prop}) {
978
- return 'This should never render';
979
- });
980
-
981
- let rejectPromise;
982
- const ClientReference = await clientExports(
983
- new Promise((resolve, reject) => {
984
- rejectPromise = reject;
985
- }),
986
- );
987
-
988
- const {writable, readable} = getTestStream();
989
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
990
- <div>
991
- <ClientComponent prop={ClientReference} />
992
- </div>,
993
- turbopackMap,
994
- {
995
- onError(x) {
996
- reportedErrors.push(x);
997
- },
998
- },
999
- );
1000
- pipe(writable);
1001
- const response = ReactServerDOMClient.createFromReadableStream(readable);
1002
-
1003
- const container = document.createElement('div');
1004
- const root = ReactDOMClient.createRoot(container);
1005
-
1006
- function App({res}) {
1007
- return use(res);
1008
- }
1009
-
1010
- await act(() => {
1011
- root.render(
1012
- <ErrorBoundary fallback={e => <p>{e.message}</p>}>
1013
- <Suspense fallback={<p>(loading)</p>}>
1014
- <App res={response} />
1015
- </Suspense>
1016
- </ErrorBoundary>,
1017
- );
1018
- });
1019
-
1020
- expect(container.innerHTML).toBe('<p>(loading)</p>');
1021
-
1022
- await act(() => {
1023
- rejectPromise(new Error('async module init error'));
1024
- });
1025
-
1026
- expect(container.innerHTML).toBe('<p>async module init error</p>');
1027
-
1028
- expect(reportedErrors).toEqual([]);
1029
- });
1030
-
1031
- it('should be able to recover from a direct reference erroring server-side', async () => {
1032
- const reportedErrors = [];
1033
-
1034
- const ClientComponent = clientExports(function ({prop}) {
1035
- return 'This should never render';
1036
- });
1037
-
1038
- // We simulate a bug in the Webpack bundler which causes an error on the server.
1039
- for (const id in turbopackMap) {
1040
- Object.defineProperty(turbopackMap, id, {
1041
- get: () => {
1042
- throw new Error('bug in the bundler');
1043
- },
1044
- });
1045
- }
1046
-
1047
- const {writable, readable} = getTestStream();
1048
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1049
- <div>
1050
- <ClientComponent />
1051
- </div>,
1052
- turbopackMap,
1053
- {
1054
- onError(x) {
1055
- reportedErrors.push(x.message);
1056
- return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
1057
- },
1058
- },
1059
- );
1060
- pipe(writable);
1061
-
1062
- const response = ReactServerDOMClient.createFromReadableStream(readable);
1063
-
1064
- const container = document.createElement('div');
1065
- const root = ReactDOMClient.createRoot(container);
1066
-
1067
- function App({res}) {
1068
- return use(res);
1069
- }
1070
-
1071
- await act(() => {
1072
- root.render(
1073
- <ErrorBoundary
1074
- fallback={e => (
1075
- <p>
1076
- {__DEV__ ? e.message + ' + ' : null}
1077
- {e.digest}
1078
- </p>
1079
- )}>
1080
- <Suspense fallback={<p>(loading)</p>}>
1081
- <App res={response} />
1082
- </Suspense>
1083
- </ErrorBoundary>,
1084
- );
1085
- });
1086
- if (__DEV__) {
1087
- expect(container.innerHTML).toBe(
1088
- '<p>bug in the bundler + a dev digest</p>',
1089
- );
1090
- } else {
1091
- expect(container.innerHTML).toBe('<p>digest("bug in the bundler")</p>');
1092
- }
1093
-
1094
- expect(reportedErrors).toEqual(['bug in the bundler']);
1095
- });
1096
-
1097
- it('should pass a Promise through props and be able use() it on the client', async () => {
1098
- async function getData() {
1099
- return 'async hello';
1100
- }
1101
-
1102
- function Component({data}) {
1103
- const text = use(data);
1104
- return <p>{text}</p>;
1105
- }
1106
-
1107
- const ClientComponent = clientExports(Component);
1108
-
1109
- function ServerComponent() {
1110
- const data = getData(); // no await here
1111
- return <ClientComponent data={data} />;
1112
- }
1113
-
1114
- function Print({response}) {
1115
- return use(response);
1116
- }
1117
-
1118
- function App({response}) {
1119
- return (
1120
- <Suspense fallback={<h1>Loading...</h1>}>
1121
- <Print response={response} />
1122
- </Suspense>
1123
- );
1124
- }
1125
-
1126
- const {writable, readable} = getTestStream();
1127
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1128
- <ServerComponent />,
1129
- turbopackMap,
1130
- );
1131
- pipe(writable);
1132
- const response = ReactServerDOMClient.createFromReadableStream(readable);
1133
-
1134
- const container = document.createElement('div');
1135
- const root = ReactDOMClient.createRoot(container);
1136
- await act(() => {
1137
- root.render(<App response={response} />);
1138
- });
1139
- expect(container.innerHTML).toBe('<p>async hello</p>');
1140
- });
1141
-
1142
- it('should throw on the client if a passed promise eventually rejects', async () => {
1143
- const reportedErrors = [];
1144
- const theError = new Error('Server throw');
1145
-
1146
- async function getData() {
1147
- throw theError;
1148
- }
1149
-
1150
- function Component({data}) {
1151
- const text = use(data);
1152
- return <p>{text}</p>;
1153
- }
1154
-
1155
- const ClientComponent = clientExports(Component);
1156
-
1157
- function ServerComponent() {
1158
- const data = getData(); // no await here
1159
- return <ClientComponent data={data} />;
1160
- }
1161
-
1162
- function Await({response}) {
1163
- return use(response);
1164
- }
1165
-
1166
- function App({response}) {
1167
- return (
1168
- <Suspense fallback={<h1>Loading...</h1>}>
1169
- <ErrorBoundary
1170
- fallback={e => (
1171
- <p>
1172
- {__DEV__ ? e.message + ' + ' : null}
1173
- {e.digest}
1174
- </p>
1175
- )}>
1176
- <Await response={response} />
1177
- </ErrorBoundary>
1178
- </Suspense>
1179
- );
1180
- }
1181
-
1182
- const {writable, readable} = getTestStream();
1183
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1184
- <ServerComponent />,
1185
- turbopackMap,
1186
- {
1187
- onError(x) {
1188
- reportedErrors.push(x);
1189
- return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
1190
- },
1191
- },
1192
- );
1193
- pipe(writable);
1194
- const response = ReactServerDOMClient.createFromReadableStream(readable);
1195
-
1196
- const container = document.createElement('div');
1197
- const root = ReactDOMClient.createRoot(container);
1198
- await act(() => {
1199
- root.render(<App response={response} />);
1200
- });
1201
- expect(container.innerHTML).toBe(
1202
- __DEV__
1203
- ? '<p>Server throw + a dev digest</p>'
1204
- : '<p>digest("Server throw")</p>',
1205
- );
1206
- expect(reportedErrors).toEqual([theError]);
1207
- });
1208
-
1209
- it('should support float methods when rendering in Fiber', async () => {
1210
- function Component() {
1211
- return <p>hello world</p>;
1212
- }
1213
-
1214
- const ClientComponent = clientExports(Component);
1215
-
1216
- async function ServerComponent() {
1217
- FlightReactDOM.prefetchDNS('d before');
1218
- FlightReactDOM.preconnect('c before');
1219
- FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
1220
- FlightReactDOM.preload('l before', {as: 'style'});
1221
- FlightReactDOM.preloadModule('lm before');
1222
- FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
1223
- FlightReactDOM.preinit('i before', {as: 'script'});
1224
- FlightReactDOM.preinitModule('m before');
1225
- FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
1226
- await 1;
1227
- FlightReactDOM.prefetchDNS('d after');
1228
- FlightReactDOM.preconnect('c after');
1229
- FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
1230
- FlightReactDOM.preload('l after', {as: 'style'});
1231
- FlightReactDOM.preloadModule('lm after');
1232
- FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
1233
- FlightReactDOM.preinit('i after', {as: 'script'});
1234
- FlightReactDOM.preinitModule('m after');
1235
- FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
1236
- return <ClientComponent />;
1237
- }
1238
-
1239
- const {writable, readable} = getTestStream();
1240
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1241
- <ServerComponent />,
1242
- turbopackMap,
1243
- );
1244
- pipe(writable);
1245
-
1246
- let response = null;
1247
- function getResponse() {
1248
- if (response === null) {
1249
- response = ReactServerDOMClient.createFromReadableStream(readable);
1250
- }
1251
- return response;
1252
- }
1253
-
1254
- function App() {
1255
- return getResponse();
1256
- }
1257
-
1258
- // We pause to allow the float call after the await point to process before the
1259
- // HostDispatcher gets set for Fiber by createRoot. This is only needed in testing
1260
- // because the module graphs are not different and the HostDispatcher is shared.
1261
- // In a real environment the Fiber and Flight code would each have their own independent
1262
- // dispatcher.
1263
- // @TODO consider what happens when Server-Components-On-The-Client exist. we probably
1264
- // want to use the Fiber HostDispatcher there too since it is more about the host than the runtime
1265
- // but we need to make sure that actually makes sense
1266
- await 1;
1267
-
1268
- const container = document.createElement('div');
1269
- const root = ReactDOMClient.createRoot(container);
1270
- await act(() => {
1271
- root.render(<App />);
1272
- });
1273
-
1274
- expect(getMeaningfulChildren(document)).toEqual(
1275
- <html>
1276
- <head>
1277
- <link rel="dns-prefetch" href="d before" />
1278
- <link rel="preconnect" href="c before" />
1279
- <link rel="preconnect" href="c2 before" crossorigin="" />
1280
- <link rel="preload" as="style" href="l before" />
1281
- <link rel="modulepreload" href="lm before" />
1282
- <link rel="modulepreload" href="lm2 before" crossorigin="" />
1283
- <script async="" src="i before" />
1284
- <script type="module" async="" src="m before" />
1285
- <script type="module" async="" src="m2 before" crossorigin="" />
1286
- <link rel="dns-prefetch" href="d after" />
1287
- <link rel="preconnect" href="c after" />
1288
- <link rel="preconnect" href="c2 after" crossorigin="" />
1289
- <link rel="preload" as="style" href="l after" />
1290
- <link rel="modulepreload" href="lm after" />
1291
- <link rel="modulepreload" href="lm2 after" crossorigin="" />
1292
- <script async="" src="i after" />
1293
- <script type="module" async="" src="m after" />
1294
- <script type="module" async="" src="m2 after" crossorigin="" />
1295
- </head>
1296
- <body />
1297
- </html>,
1298
- );
1299
- expect(getMeaningfulChildren(container)).toEqual(<p>hello world</p>);
1300
- });
1301
-
1302
- it('should support float methods when rendering in Fizz', async () => {
1303
- function Component() {
1304
- return <p>hello world</p>;
1305
- }
1306
-
1307
- const ClientComponent = clientExports(Component);
1308
-
1309
- async function ServerComponent() {
1310
- FlightReactDOM.prefetchDNS('d before');
1311
- FlightReactDOM.preconnect('c before');
1312
- FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
1313
- FlightReactDOM.preload('l before', {as: 'style'});
1314
- FlightReactDOM.preloadModule('lm before');
1315
- FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
1316
- FlightReactDOM.preinit('i before', {as: 'script'});
1317
- FlightReactDOM.preinitModule('m before');
1318
- FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
1319
- await 1;
1320
- FlightReactDOM.prefetchDNS('d after');
1321
- FlightReactDOM.preconnect('c after');
1322
- FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
1323
- FlightReactDOM.preload('l after', {as: 'style'});
1324
- FlightReactDOM.preloadModule('lm after');
1325
- FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
1326
- FlightReactDOM.preinit('i after', {as: 'script'});
1327
- FlightReactDOM.preinitModule('m after');
1328
- FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
1329
- return <ClientComponent />;
1330
- }
1331
-
1332
- const {writable: flightWritable, readable: flightReadable} =
1333
- getTestStream();
1334
- const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
1335
-
1336
- // In a real environment you would want to call the render during the Fizz render.
1337
- // The reason we cannot do this in our test is because we don't actually have two separate
1338
- // module graphs and we are contriving the sequencing to work in a way where
1339
- // the right HostDispatcher is in scope during the Flight Server Float calls and the
1340
- // Flight Client hint dispatches
1341
- const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1342
- <ServerComponent />,
1343
- turbopackMap,
1344
- );
1345
- pipe(flightWritable);
1346
-
1347
- let response = null;
1348
- function getResponse() {
1349
- if (response === null) {
1350
- response =
1351
- ReactServerDOMClient.createFromReadableStream(flightReadable);
1352
- }
1353
- return response;
1354
- }
1355
-
1356
- function App() {
1357
- return (
1358
- <html>
1359
- <body>{getResponse()}</body>
1360
- </html>
1361
- );
1362
- }
1363
-
1364
- await act(async () => {
1365
- ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
1366
- });
1367
-
1368
- const decoder = new TextDecoder();
1369
- const reader = fizzReadable.getReader();
1370
- let content = '';
1371
- while (true) {
1372
- const {done, value} = await reader.read();
1373
- if (done) {
1374
- content += decoder.decode();
1375
- break;
1376
- }
1377
- content += decoder.decode(value, {stream: true});
1378
- }
1379
-
1380
- const doc = new JSDOM(content).window.document;
1381
- expect(getMeaningfulChildren(doc)).toEqual(
1382
- <html>
1383
- <head>
1384
- <link rel="dns-prefetch" href="d before" />
1385
- <link rel="preconnect" href="c before" />
1386
- <link rel="preconnect" href="c2 before" crossorigin="" />
1387
- <link rel="dns-prefetch" href="d after" />
1388
- <link rel="preconnect" href="c after" />
1389
- <link rel="preconnect" href="c2 after" crossorigin="" />
1390
- <script async="" src="i before" />
1391
- <script type="module" async="" src="m before" />
1392
- <script type="module" async="" src="m2 before" crossorigin="" />
1393
- <script async="" src="i after" />
1394
- <script type="module" async="" src="m after" />
1395
- <script type="module" async="" src="m2 after" crossorigin="" />
1396
- <link rel="preload" as="style" href="l before" />
1397
- <link rel="modulepreload" href="lm before" />
1398
- <link rel="modulepreload" href="lm2 before" crossorigin="" />
1399
- <link rel="preload" as="style" href="l after" />
1400
- <link rel="modulepreload" href="lm after" />
1401
- <link rel="modulepreload" href="lm2 after" crossorigin="" />
1402
- </head>
1403
- <body>
1404
- <p>hello world</p>
1405
- </body>
1406
- </html>,
1407
- );
1408
- });
1409
-
1410
- it('supports Float hints from concurrent Flight -> Fizz renders', async () => {
1411
- function Component() {
1412
- return <p>hello world</p>;
1413
- }
1414
-
1415
- const ClientComponent = clientExports(Component);
1416
-
1417
- async function ServerComponent1() {
1418
- FlightReactDOM.preload('before1', {as: 'style'});
1419
- await 1;
1420
- FlightReactDOM.preload('after1', {as: 'style'});
1421
- return <ClientComponent />;
1422
- }
1423
-
1424
- async function ServerComponent2() {
1425
- FlightReactDOM.preload('before2', {as: 'style'});
1426
- await 1;
1427
- FlightReactDOM.preload('after2', {as: 'style'});
1428
- return <ClientComponent />;
1429
- }
1430
-
1431
- const {writable: flightWritable1, readable: flightReadable1} =
1432
- getTestStream();
1433
- const {writable: flightWritable2, readable: flightReadable2} =
1434
- getTestStream();
1435
-
1436
- ReactServerDOMServer.renderToPipeableStream(
1437
- <ServerComponent1 />,
1438
- turbopackMap,
1439
- ).pipe(flightWritable1);
1440
-
1441
- ReactServerDOMServer.renderToPipeableStream(
1442
- <ServerComponent2 />,
1443
- turbopackMap,
1444
- ).pipe(flightWritable2);
1445
-
1446
- const responses = new Map();
1447
- function getResponse(stream) {
1448
- let response = responses.get(stream);
1449
- if (!response) {
1450
- response = ReactServerDOMClient.createFromReadableStream(stream);
1451
- responses.set(stream, response);
1452
- }
1453
- return response;
1454
- }
1455
-
1456
- function App({stream}) {
1457
- return (
1458
- <html>
1459
- <body>{getResponse(stream)}</body>
1460
- </html>
1461
- );
1462
- }
1463
-
1464
- // pausing to let Flight runtime tick. This is a test only artifact of the fact that
1465
- // we aren't operating separate module graphs for flight and fiber. In a real app
1466
- // each would have their own dispatcher and there would be no cross dispatching.
1467
- await 1;
1468
-
1469
- const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream();
1470
- const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream();
1471
- await act(async () => {
1472
- ReactDOMFizzServer.renderToPipeableStream(
1473
- <App stream={flightReadable1} />,
1474
- ).pipe(fizzWritable1);
1475
- ReactDOMFizzServer.renderToPipeableStream(
1476
- <App stream={flightReadable2} />,
1477
- ).pipe(fizzWritable2);
1478
- });
1479
-
1480
- async function read(stream) {
1481
- const decoder = new TextDecoder();
1482
- const reader = stream.getReader();
1483
- let buffer = '';
1484
- while (true) {
1485
- const {done, value} = await reader.read();
1486
- if (done) {
1487
- buffer += decoder.decode();
1488
- break;
1489
- }
1490
- buffer += decoder.decode(value, {stream: true});
1491
- }
1492
- return buffer;
1493
- }
1494
-
1495
- const [content1, content2] = await Promise.all([
1496
- read(fizzReadable1),
1497
- read(fizzReadable2),
1498
- ]);
1499
-
1500
- expect(content1).toEqual(
1501
- '<!DOCTYPE html><html><head><link rel="preload" href="before1" as="style"/>' +
1502
- '<link rel="preload" href="after1" as="style"/></head><body><p>hello world</p></body></html>',
1503
- );
1504
- expect(content2).toEqual(
1505
- '<!DOCTYPE html><html><head><link rel="preload" href="before2" as="style"/>' +
1506
- '<link rel="preload" href="after2" as="style"/></head><body><p>hello world</p></body></html>',
1507
- );
1508
- });
1509
-
1510
- it('supports deduping hints by Float key', async () => {
1511
- function Component() {
1512
- return <p>hello world</p>;
1513
- }
1514
-
1515
- const ClientComponent = clientExports(Component);
1516
-
1517
- async function ServerComponent() {
1518
- FlightReactDOM.prefetchDNS('dns');
1519
- FlightReactDOM.preconnect('preconnect');
1520
- FlightReactDOM.preload('load', {as: 'style'});
1521
- FlightReactDOM.preinit('init', {as: 'script'});
1522
- // again but vary preconnect to demonstrate crossOrigin participates in the key
1523
- FlightReactDOM.prefetchDNS('dns');
1524
- FlightReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'});
1525
- FlightReactDOM.preload('load', {as: 'style'});
1526
- FlightReactDOM.preinit('init', {as: 'script'});
1527
- await 1;
1528
- // after an async point
1529
- FlightReactDOM.prefetchDNS('dns');
1530
- FlightReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'});
1531
- FlightReactDOM.preload('load', {as: 'style'});
1532
- FlightReactDOM.preinit('init', {as: 'script'});
1533
- return <ClientComponent />;
1534
- }
1535
-
1536
- const {writable, readable} = getTestStream();
1537
-
1538
- ReactServerDOMServer.renderToPipeableStream(
1539
- <ServerComponent />,
1540
- turbopackMap,
1541
- ).pipe(writable);
1542
-
1543
- const hintRows = [];
1544
- async function collectHints(stream) {
1545
- const decoder = new TextDecoder();
1546
- const reader = stream.getReader();
1547
- let buffer = '';
1548
- while (true) {
1549
- const {done, value} = await reader.read();
1550
- if (done) {
1551
- buffer += decoder.decode();
1552
- if (buffer.includes(':H')) {
1553
- hintRows.push(buffer);
1554
- }
1555
- break;
1556
- }
1557
- buffer += decoder.decode(value, {stream: true});
1558
- let line;
1559
- while ((line = buffer.indexOf('\n')) > -1) {
1560
- const row = buffer.slice(0, line);
1561
- buffer = buffer.slice(line + 1);
1562
- if (row.includes(':H')) {
1563
- hintRows.push(row);
1564
- }
1565
- }
1566
- }
1567
- }
1568
-
1569
- await collectHints(readable);
1570
- expect(hintRows.length).toEqual(6);
1571
- });
1572
-});
packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMNode-test.js
deleted
-256
@@ -1,256 +0,0 @@
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
-// Don't wait before processing work on the server.
13
-// TODO: we can replace this with FlightServer.act().
14
-global.setImmediate = cb => cb();
15
-
16
-let clientExports;
17
-let turbopackMap;
18
-let turbopackModules;
19
-let turbopackModuleLoading;
20
-let React;
21
-let ReactDOMServer;
22
-let ReactServerDOMServer;
23
-let ReactServerDOMClient;
24
-let Stream;
25
-let use;
26
-
27
-describe('ReactFlightDOMNode', () => {
28
- beforeEach(() => {
29
- jest.resetModules();
30
-
31
- // Simulate the condition resolution
32
- jest.mock('react', () => require('react/react.shared-subset'));
33
- jest.mock('react-server-dom-turbopack/server', () =>
34
- require('react-server-dom-turbopack/server.node'),
35
- );
36
- ReactServerDOMServer = require('react-server-dom-turbopack/server');
37
-
38
- const TurbopackMock = require('./utils/TurbopackMock');
39
- clientExports = TurbopackMock.clientExports;
40
- turbopackMap = TurbopackMock.turbopackMap;
41
- turbopackModules = TurbopackMock.turbopackModules;
42
- turbopackModuleLoading = TurbopackMock.moduleLoading;
43
-
44
- jest.resetModules();
45
- __unmockReact();
46
- jest.unmock('react-server-dom-turbopack/server');
47
- jest.mock('react-server-dom-turbopack/client', () =>
48
- require('react-server-dom-turbopack/client.node'),
49
- );
50
-
51
- React = require('react');
52
- ReactDOMServer = require('react-dom/server.node');
53
- ReactServerDOMClient = require('react-server-dom-turbopack/client');
54
- Stream = require('stream');
55
- use = React.use;
56
- });
57
-
58
- function readResult(stream) {
59
- return new Promise((resolve, reject) => {
60
- let buffer = '';
61
- const writable = new Stream.PassThrough();
62
- writable.setEncoding('utf8');
63
- writable.on('data', chunk => {
64
- buffer += chunk;
65
- });
66
- writable.on('error', error => {
67
- reject(error);
68
- });
69
- writable.on('end', () => {
70
- resolve(buffer);
71
- });
72
- stream.pipe(writable);
73
- });
74
- }
75
-
76
- it('should allow an alternative module mapping to be used for SSR', async () => {
77
- function ClientComponent() {
78
- return <span>Client Component</span>;
79
- }
80
- // The Client build may not have the same IDs as the Server bundles for the same
81
- // component.
82
- const ClientComponentOnTheClient = clientExports(
83
- ClientComponent,
84
- 'path/to/chunk.js',
85
- );
86
- const ClientComponentOnTheServer = clientExports(ClientComponent);
87
-
88
- // In the SSR bundle this module won't exist. We simulate this by deleting it.
89
- const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
90
- delete turbopackModules[clientId];
91
-
92
- // Instead, we have to provide a translation from the client meta data to the SSR
93
- // meta data.
94
- const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
95
- const translationMap = {
96
- [clientId]: {
97
- '*': ssrMetadata,
98
- },
99
- };
100
-
101
- function App() {
102
- return <ClientComponentOnTheClient />;
103
- }
104
-
105
- const stream = ReactServerDOMServer.renderToPipeableStream(
106
- <App />,
107
- turbopackMap,
108
- );
109
- const readable = new Stream.PassThrough();
110
-
111
- stream.pipe(readable);
112
-
113
- let response;
114
- function ClientRoot() {
115
- if (!response) {
116
- response = ReactServerDOMClient.createFromNodeStream(readable, {
117
- moduleMap: translationMap,
118
- moduleLoading: turbopackModuleLoading,
119
- });
120
- }
121
- return use(response);
122
- }
123
-
124
- const ssrStream = await ReactDOMServer.renderToPipeableStream(
125
- <ClientRoot />,
126
- );
127
- const result = await readResult(ssrStream);
128
- expect(result).toEqual(
129
- '<script src="/prefix/path/to/chunk.js" async=""></script><span>Client Component</span>',
130
- );
131
- });
132
-
133
- it('should encode long string in a compact format', async () => {
134
- const testString = '"\n\t'.repeat(500) + '🙃';
135
-
136
- const stream = ReactServerDOMServer.renderToPipeableStream({
137
- text: testString,
138
- });
139
-
140
- const readable = new Stream.PassThrough();
141
-
142
- const stringResult = readResult(readable);
143
- const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, {
144
- moduleMap: turbopackMap,
145
- moduleLoading: turbopackModuleLoading,
146
- });
147
-
148
- stream.pipe(readable);
149
-
150
- const serializedContent = await stringResult;
151
- // The content should be compact an unescaped
152
- expect(serializedContent.length).toBeLessThan(2000);
153
- expect(serializedContent).not.toContain('\\n');
154
- expect(serializedContent).not.toContain('\\t');
155
- expect(serializedContent).not.toContain('\\"');
156
- expect(serializedContent).toContain('\t');
157
-
158
- const result = await parsedResult;
159
- // Should still match the result when parsed
160
- expect(result.text).toBe(testString);
161
- });
162
-
163
- // @gate enableBinaryFlight
164
- it('should be able to serialize any kind of typed array', async () => {
165
- const buffer = new Uint8Array([
166
- 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
167
- ]).buffer;
168
- const buffers = [
169
- buffer,
170
- new Int8Array(buffer, 1),
171
- new Uint8Array(buffer, 2),
172
- new Uint8ClampedArray(buffer, 2),
173
- new Int16Array(buffer, 2),
174
- new Uint16Array(buffer, 2),
175
- new Int32Array(buffer, 4),
176
- new Uint32Array(buffer, 4),
177
- new Float32Array(buffer, 4),
178
- new Float64Array(buffer, 0),
179
- new BigInt64Array(buffer, 0),
180
- new BigUint64Array(buffer, 0),
181
- new DataView(buffer, 3),
182
- ];
183
- const stream = ReactServerDOMServer.renderToPipeableStream(buffers);
184
- const readable = new Stream.PassThrough();
185
- const promise = ReactServerDOMClient.createFromNodeStream(readable, {
186
- moduleMap: turbopackMap,
187
- moduleLoading: turbopackModuleLoading,
188
- });
189
- stream.pipe(readable);
190
- const result = await promise;
191
- expect(result).toEqual(buffers);
192
- });
193
-
194
- it('should allow accept a nonce option for Flight preinitialized scripts', async () => {
195
- function ClientComponent() {
196
- return <span>Client Component</span>;
197
- }
198
- // The Client build may not have the same IDs as the Server bundles for the same
199
- // component.
200
- const ClientComponentOnTheClient = clientExports(
201
- ClientComponent,
202
- 'path/to/chunk.js',
203
- );
204
- const ClientComponentOnTheServer = clientExports(ClientComponent);
205
-
206
- // In the SSR bundle this module won't exist. We simulate this by deleting it.
207
- const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
208
- delete turbopackModules[clientId];
209
-
210
- // Instead, we have to provide a translation from the client meta data to the SSR
211
- // meta data.
212
- const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
213
- const translationMap = {
214
- [clientId]: {
215
- '*': ssrMetadata,
216
- },
217
- };
218
- const ssrManifest = {
219
- moduleMap: translationMap,
220
- moduleLoading: turbopackModuleLoading,
221
- };
222
-
223
- function App() {
224
- return <ClientComponentOnTheClient />;
225
- }
226
-
227
- const stream = ReactServerDOMServer.renderToPipeableStream(
228
- <App />,
229
- turbopackMap,
230
- );
231
- const readable = new Stream.PassThrough();
232
- let response;
233
-
234
- stream.pipe(readable);
235
-
236
- function ClientRoot() {
237
- if (response) return use(response);
238
- response = ReactServerDOMClient.createFromNodeStream(
239
- readable,
240
- ssrManifest,
241
- {
242
- nonce: 'r4nd0m',
243
- },
244
- );
245
- return use(response);
246
- }
247
-
248
- const ssrStream = await ReactDOMServer.renderToPipeableStream(
249
- <ClientRoot />,
250
- );
251
- const result = await readResult(ssrStream);
252
- expect(result).toEqual(
253
- '<script src="/prefix/path/to/chunk.js" async="" nonce="r4nd0m"></script><span>Client Component</span>',
254
- );
255
- });
256
-});
packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js
deleted
-234
@@ -1,234 +0,0 @@
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
-// Polyfills for test environment
13
-global.ReadableStream =
14
- require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15
-global.TextEncoder = require('util').TextEncoder;
16
-global.TextDecoder = require('util').TextDecoder;
17
-
18
-// let serverExports;
19
-let turbopackServerMap;
20
-let ReactServerDOMServer;
21
-let ReactServerDOMClient;
22
-
23
-describe('ReactFlightDOMReply', () => {
24
- beforeEach(() => {
25
- jest.resetModules();
26
- // Simulate the condition resolution
27
- jest.mock('react', () => require('react/react.shared-subset'));
28
- jest.mock('react-server-dom-turbopack/server', () =>
29
- require('react-server-dom-turbopack/server.browser'),
30
- );
31
- const TurbopackMock = require('./utils/TurbopackMock');
32
- // serverExports = TurbopackMock.serverExports;
33
- turbopackServerMap = TurbopackMock.turbopackServerMap;
34
- ReactServerDOMServer = require('react-server-dom-turbopack/server.browser');
35
- jest.resetModules();
36
- ReactServerDOMClient = require('react-server-dom-turbopack/client');
37
- });
38
-
39
- // This method should exist on File but is not implemented in JSDOM
40
- async function arrayBuffer(file) {
41
- return new Promise((resolve, reject) => {
42
- const reader = new FileReader();
43
- reader.onload = function () {
44
- return resolve(reader.result);
45
- };
46
- reader.onerror = function () {
47
- return reject(reader.error);
48
- };
49
- reader.readAsArrayBuffer(file);
50
- });
51
- }
52
-
53
- it('can pass undefined as a reply', async () => {
54
- const body = await ReactServerDOMClient.encodeReply(undefined);
55
- const missing = await ReactServerDOMServer.decodeReply(
56
- body,
57
- turbopackServerMap,
58
- );
59
- expect(missing).toBe(undefined);
60
-
61
- const body2 = await ReactServerDOMClient.encodeReply({
62
- array: [undefined, null, undefined],
63
- prop: undefined,
64
- });
65
- const object = await ReactServerDOMServer.decodeReply(
66
- body2,
67
- turbopackServerMap,
68
- );
69
- expect(object.array.length).toBe(3);
70
- expect(object.array[0]).toBe(undefined);
71
- expect(object.array[1]).toBe(null);
72
- expect(object.array[3]).toBe(undefined);
73
- expect(object.prop).toBe(undefined);
74
- // These should really be true but our deserialization doesn't currently deal with it.
75
- expect('3' in object.array).toBe(false);
76
- expect('prop' in object).toBe(false);
77
- });
78
-
79
- it('can pass an iterable as a reply', async () => {
80
- const body = await ReactServerDOMClient.encodeReply({
81
- [Symbol.iterator]: function* () {
82
- yield 'A';
83
- yield 'B';
84
- yield 'C';
85
- },
86
- });
87
- const iterable = await ReactServerDOMServer.decodeReply(
88
- body,
89
- turbopackServerMap,
90
- );
91
- const items = [];
92
- // eslint-disable-next-line no-for-of-loops/no-for-of-loops
93
- for (const item of iterable) {
94
- items.push(item);
95
- }
96
- expect(items).toEqual(['A', 'B', 'C']);
97
- });
98
-
99
- it('can pass weird numbers as a reply', async () => {
100
- const nums = [0, -0, Infinity, -Infinity, NaN];
101
- const body = await ReactServerDOMClient.encodeReply(nums);
102
- const nums2 = await ReactServerDOMServer.decodeReply(
103
- body,
104
- turbopackServerMap,
105
- );
106
-
107
- expect(nums).toEqual(nums2);
108
- expect(nums.every((n, i) => Object.is(n, nums2[i]))).toBe(true);
109
- });
110
-
111
- it('can pass a BigInt as a reply', async () => {
112
- const body = await ReactServerDOMClient.encodeReply(90071992547409910000n);
113
- const n = await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
114
-
115
- expect(n).toEqual(90071992547409910000n);
116
- });
117
-
118
- it('can pass FormData as a reply', async () => {
119
- const formData = new FormData();
120
- formData.set('hello', 'world');
121
- formData.append('list', '1');
122
- formData.append('list', '2');
123
- formData.append('list', '3');
124
- const typedArray = new Uint8Array([0, 1, 2, 3]);
125
- const blob = new Blob([typedArray]);
126
- formData.append('blob', blob, 'filename.blob');
127
-
128
- const body = await ReactServerDOMClient.encodeReply(formData);
129
- const formData2 = await ReactServerDOMServer.decodeReply(
130
- body,
131
- turbopackServerMap,
132
- );
133
-
134
- expect(formData2).not.toBe(formData);
135
- expect(Array.from(formData2).length).toBe(5);
136
- expect(formData2.get('hello')).toBe('world');
137
- expect(formData2.getAll('list')).toEqual(['1', '2', '3']);
138
- const blob2 = formData.get('blob');
139
- expect(blob2.size).toBe(4);
140
- expect(blob2.name).toBe('filename.blob');
141
- expect(blob2.type).toBe('');
142
- const typedArray2 = new Uint8Array(await arrayBuffer(blob2));
143
- expect(typedArray2).toEqual(typedArray);
144
- });
145
-
146
- it('can pass multiple Files in FormData', async () => {
147
- const typedArrayA = new Uint8Array([0, 1, 2, 3]);
148
- const typedArrayB = new Uint8Array([4, 5]);
149
- const blobA = new Blob([typedArrayA]);
150
- const blobB = new Blob([typedArrayB]);
151
- const formData = new FormData();
152
- formData.append('filelist', 'string');
153
- formData.append('filelist', blobA);
154
- formData.append('filelist', blobB);
155
-
156
- const body = await ReactServerDOMClient.encodeReply(formData);
157
- const formData2 = await ReactServerDOMServer.decodeReply(
158
- body,
159
- turbopackServerMap,
160
- );
161
-
162
- const filelist2 = formData2.getAll('filelist');
163
- expect(filelist2.length).toBe(3);
164
- expect(filelist2[0]).toBe('string');
165
- const blobA2 = filelist2[1];
166
- expect(blobA2.size).toBe(4);
167
- expect(blobA2.name).toBe('blob');
168
- expect(blobA2.type).toBe('');
169
- const typedArrayA2 = new Uint8Array(await arrayBuffer(blobA2));
170
- expect(typedArrayA2).toEqual(typedArrayA);
171
- const blobB2 = filelist2[2];
172
- expect(blobB2.size).toBe(2);
173
- expect(blobB2.name).toBe('blob');
174
- expect(blobB2.type).toBe('');
175
- const typedArrayB2 = new Uint8Array(await arrayBuffer(blobB2));
176
- expect(typedArrayB2).toEqual(typedArrayB);
177
- });
178
-
179
- it('can pass two independent FormData with same keys', async () => {
180
- const formDataA = new FormData();
181
- formDataA.set('greeting', 'hello');
182
- const formDataB = new FormData();
183
- formDataB.set('greeting', 'hi');
184
-
185
- const body = await ReactServerDOMClient.encodeReply({
186
- a: formDataA,
187
- b: formDataB,
188
- });
189
- const {a: formDataA2, b: formDataB2} =
190
- await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
191
-
192
- expect(Array.from(formDataA2).length).toBe(1);
193
- expect(Array.from(formDataB2).length).toBe(1);
194
- expect(formDataA2.get('greeting')).toBe('hello');
195
- expect(formDataB2.get('greeting')).toBe('hi');
196
- });
197
-
198
- it('can pass a Date as a reply', async () => {
199
- const d = new Date(1234567890123);
200
- const body = await ReactServerDOMClient.encodeReply(d);
201
- const d2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
202
-
203
- expect(d).toEqual(d2);
204
- expect(d % 1000).toEqual(123); // double-check the milliseconds made it through
205
- });
206
-
207
- it('can pass a Map as a reply', async () => {
208
- const objKey = {obj: 'key'};
209
- const m = new Map([
210
- ['hi', {greet: 'world'}],
211
- [objKey, 123],
212
- ]);
213
- const body = await ReactServerDOMClient.encodeReply(m);
214
- const m2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
215
-
216
- expect(m2 instanceof Map).toBe(true);
217
- expect(m2.size).toBe(2);
218
- expect(m2.get('hi').greet).toBe('world');
219
- expect(m2).toEqual(m);
220
- });
221
-
222
- it('can pass a Set as a reply', async () => {
223
- const objKey = {obj: 'key'};
224
- const s = new Set(['hi', objKey]);
225
-
226
- const body = await ReactServerDOMClient.encodeReply(s);
227
- const s2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
228
-
229
- expect(s2 instanceof Set).toBe(true);
230
- expect(s2.size).toBe(2);
231
- expect(s2.has('hi')).toBe(true);
232
- expect(s2).toEqual(s);
233
- });
234
-});
packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js
new
+208
@@ -0,0 +1,208 @@
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
+// Polyfills for test environment
13
+global.ReadableStream =
14
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15
+global.TextEncoder = require('util').TextEncoder;
16
+global.TextDecoder = require('util').TextDecoder;
17
+
18
+// Don't wait before processing work on the server.
19
+// TODO: we can replace this with FlightServer.act().
20
+global.setImmediate = cb => cb();
21
+
22
+let act;
23
+let use;
24
+let clientExports;
25
+let turbopackMap;
26
+let Stream;
27
+let React;
28
+let ReactDOMClient;
29
+let ReactServerDOMServer;
30
+let ReactServerDOMClient;
31
+let Suspense;
32
+
33
+describe('ReactFlightDOM', () => {
34
+ beforeEach(() => {
35
+ // For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server
36
+ // This can be thought of as essentially being the React Server Components scope with react-server
37
+ // condition
38
+ jest.resetModules();
39
+
40
+ // Simulate the condition resolution
41
+ jest.mock('react-server-dom-turbopack/server', () =>
42
+ require('react-server-dom-turbopack/server.node.unbundled'),
43
+ );
44
+ jest.mock('react', () => require('react/react.shared-subset'));
45
+
46
+ const TurbopackMock = require('./utils/TurbopackMock');
47
+ clientExports = TurbopackMock.clientExports;
48
+ turbopackMap = TurbopackMock.turbopackMap;
49
+
50
+ ReactServerDOMServer = require('react-server-dom-turbopack/server');
51
+
52
+ // This reset is to load modules for the SSR/Browser scope.
53
+ jest.resetModules();
54
+ __unmockReact();
55
+ act = require('internal-test-utils').act;
56
+ Stream = require('stream');
57
+ React = require('react');
58
+ use = React.use;
59
+ Suspense = React.Suspense;
60
+ ReactDOMClient = require('react-dom/client');
61
+ ReactServerDOMClient = require('react-server-dom-turbopack/client');
62
+ });
63
+
64
+ function getTestStream() {
65
+ const writable = new Stream.PassThrough();
66
+ const readable = new ReadableStream({
67
+ start(controller) {
68
+ writable.on('data', chunk => {
69
+ controller.enqueue(chunk);
70
+ });
71
+ writable.on('end', () => {
72
+ controller.close();
73
+ });
74
+ },
75
+ });
76
+ return {
77
+ readable,
78
+ writable,
79
+ };
80
+ }
81
+
82
+ it('should resolve HTML using Node streams', async () => {
83
+ function Text({children}) {
84
+ return <span>{children}</span>;
85
+ }
86
+ function HTML() {
87
+ return (
88
+ <div>
89
+ <Text>hello</Text>
90
+ <Text>world</Text>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ function App() {
96
+ const model = {
97
+ html: <HTML />,
98
+ };
99
+ return model;
100
+ }
101
+
102
+ const {writable, readable} = getTestStream();
103
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
104
+ <App />,
105
+ turbopackMap,
106
+ );
107
+ pipe(writable);
108
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
109
+ const model = await response;
110
+ expect(model).toEqual({
111
+ html: (
112
+ <div>
113
+ <span>hello</span>
114
+ <span>world</span>
115
+ </div>
116
+ ),
117
+ });
118
+ });
119
+
120
+ it('should resolve the root', async () => {
121
+ // Model
122
+ function Text({children}) {
123
+ return <span>{children}</span>;
124
+ }
125
+ function HTML() {
126
+ return (
127
+ <div>
128
+ <Text>hello</Text>
129
+ <Text>world</Text>
130
+ </div>
131
+ );
132
+ }
133
+ function RootModel() {
134
+ return {
135
+ html: <HTML />,
136
+ };
137
+ }
138
+
139
+ // View
140
+ function Message({response}) {
141
+ return <section>{use(response).html}</section>;
142
+ }
143
+ function App({response}) {
144
+ return (
145
+ <Suspense fallback={<h1>Loading...</h1>}>
146
+ <Message response={response} />
147
+ </Suspense>
148
+ );
149
+ }
150
+
151
+ const {writable, readable} = getTestStream();
152
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
153
+ <RootModel />,
154
+ turbopackMap,
155
+ );
156
+ pipe(writable);
157
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
158
+
159
+ const container = document.createElement('div');
160
+ const root = ReactDOMClient.createRoot(container);
161
+ await act(() => {
162
+ root.render(<App response={response} />);
163
+ });
164
+ expect(container.innerHTML).toBe(
165
+ '<section><div><span>hello</span><span>world</span></div></section>',
166
+ );
167
+ });
168
+
169
+ it('should unwrap async module references', async () => {
170
+ const AsyncModule = Promise.resolve(function AsyncModule({text}) {
171
+ return 'Async: ' + text;
172
+ });
173
+
174
+ const AsyncModule2 = Promise.resolve({
175
+ exportName: 'Module',
176
+ });
177
+
178
+ function Print({response}) {
179
+ return <p>{use(response)}</p>;
180
+ }
181
+
182
+ function App({response}) {
183
+ return (
184
+ <Suspense fallback={<h1>Loading...</h1>}>
185
+ <Print response={response} />
186
+ </Suspense>
187
+ );
188
+ }
189
+
190
+ const AsyncModuleRef = await clientExports(AsyncModule);
191
+ const AsyncModuleRef2 = await clientExports(AsyncModule2);
192
+
193
+ const {writable, readable} = getTestStream();
194
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
195
+ <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
196
+ turbopackMap,
197
+ );
198
+ pipe(writable);
199
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
200
+
201
+ const container = document.createElement('div');
202
+ const root = ReactDOMClient.createRoot(container);
203
+ await act(() => {
204
+ root.render(<App response={response} />);
205
+ });
206
+ expect(container.innerHTML).toBe('<p>Async: Module</p>');
207
+ });
208
+});
packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js
renamed
packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js
renamed
-95
@@ -54,37 +54,6 @@ describe('ReactFlightDOMEdge', () => {
54
use = React.use;
55
});
56
57
- function passThrough(stream) {
58
- // Simulate more realistic network by splitting up and rejoining some chunks.
59
- // This lets us test that we don't accidentally rely on particular bounds of the chunks.
60
- return new ReadableStream({
61
- async start(controller) {
62
- const reader = stream.getReader();
63
- let prevChunk = new Uint8Array(0);
64
- function push() {
65
- reader.read().then(({done, value}) => {
66
- if (done) {
67
- controller.enqueue(prevChunk);
68
- controller.close();
69
- return;
70
- }
71
- const chunk = new Uint8Array(prevChunk.length + value.length);
72
- chunk.set(prevChunk, 0);
73
- chunk.set(value, prevChunk.length);
74
- if (chunk.length > 50) {
75
- controller.enqueue(chunk.subarray(0, chunk.length - 50));
76
- prevChunk = chunk.subarray(chunk.length - 50);
77
- } else {
78
- prevChunk = chunk;
79
- }
80
- push();
81
- });
82
- }
83
- push();
84
- },
85
- });
86
- }
87
-
57
async function readResult(stream) {
58
const reader = stream.getReader();
59
let result = '';
@@ -144,68 +113,4 @@ describe('ReactFlightDOMEdge', () => {
113
const result = await readResult(ssrStream);
114
expect(result).toEqual('<span>Client Component</span>');
115
});
147
-
148
- it('should encode long string in a compact format', async () => {
149
- const testString = '"\n\t'.repeat(500) + '🙃';
150
- const testString2 = 'hello'.repeat(400);
151
-
152
- const stream = ReactServerDOMServer.renderToReadableStream({
153
- text: testString,
154
- text2: testString2,
155
- });
156
- const [stream1, stream2] = passThrough(stream).tee();
157
-
158
- const serializedContent = await readResult(stream1);
159
- // The content should be compact an unescaped
160
- expect(serializedContent.length).toBeLessThan(4000);
161
- expect(serializedContent).not.toContain('\\n');
162
- expect(serializedContent).not.toContain('\\t');
163
- expect(serializedContent).not.toContain('\\"');
164
- expect(serializedContent).toContain('\t');
165
-
166
- const result = await ReactServerDOMClient.createFromReadableStream(
167
- stream2,
168
- {
169
- ssrManifest: {
170
- moduleMap: null,
171
- moduleLoading: null,
172
- },
173
- },
174
- );
175
- // Should still match the result when parsed
176
- expect(result.text).toBe(testString);
177
- expect(result.text2).toBe(testString2);
178
- });
179
-
180
- // @gate enableBinaryFlight
181
- it('should be able to serialize any kind of typed array', async () => {
182
- const buffer = new Uint8Array([
183
- 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
184
- ]).buffer;
185
- const buffers = [
186
- buffer,
187
- new Int8Array(buffer, 1),
188
- new Uint8Array(buffer, 2),
189
- new Uint8ClampedArray(buffer, 2),
190
- new Int16Array(buffer, 2),
191
- new Uint16Array(buffer, 2),
192
- new Int32Array(buffer, 4),
193
- new Uint32Array(buffer, 4),
194
- new Float32Array(buffer, 4),
195
- new Float64Array(buffer, 0),
196
- new BigInt64Array(buffer, 0),
197
- new BigUint64Array(buffer, 0),
198
- new DataView(buffer, 3),
199
- ];
200
- const stream = passThrough(
201
- ReactServerDOMServer.renderToReadableStream(buffers),
202
- );
203
- const result = await ReactServerDOMClient.createFromReadableStream(stream, {
204
- ssrManifest: {
205
- moduleMap: null,
206
- moduleLoading: null,
207
- },
208
- });
209
- expect(result).toEqual(buffers);
210
- });
116
});
packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMForm-test.js
renamed
-109
@@ -145,113 +145,4 @@ describe('ReactFlightDOMForm', () => {
145
expect(result).toBe('hello');
146
expect(foo).toBe('bar');
147
});
148
-
149
- // @gate enableFormActions
150
- it('can submit an imported server action without hydrating it', async () => {
151
- let foo = null;
152
-
153
- const ServerModule = serverExports(function action(formData) {
154
- foo = formData.get('foo');
155
- return 'hi';
156
- });
157
- const serverAction = ReactServerDOMClient.createServerReference(
158
- ServerModule.$$id,
159
- );
160
- function App() {
161
- return (
162
- <form action={serverAction}>
163
- <input type="text" name="foo" defaultValue="bar" />
164
- </form>
165
- );
166
- }
167
-
168
- const ssrStream = await ReactDOMServer.renderToReadableStream(<App />);
169
- await readIntoContainer(ssrStream);
170
-
171
- const form = container.firstChild;
172
-
173
- expect(foo).toBe(null);
174
-
175
- const result = await submit(form);
176
-
177
- expect(result).toBe('hi');
178
-
179
- expect(foo).toBe('bar');
180
- });
181
-
182
- // @gate enableFormActions
183
- it('can submit a complex closure server action without hydrating it', async () => {
184
- let foo = null;
185
-
186
- const serverAction = serverExports(function action(bound, formData) {
187
- foo = formData.get('foo') + bound.complex;
188
- return 'hello';
189
- });
190
- function App() {
191
- return (
192
- <form action={serverAction.bind(null, {complex: 'object'})}>
193
- <input type="text" name="foo" defaultValue="bar" />
194
- </form>
195
- );
196
- }
197
- const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
198
- const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
199
- ssrManifest: {
200
- moduleMap: null,
201
- moduleLoading: null,
202
- },
203
- });
204
- const ssrStream = await ReactDOMServer.renderToReadableStream(response);
205
- await readIntoContainer(ssrStream);
206
-
207
- const form = container.firstChild;
208
-
209
- expect(foo).toBe(null);
210
-
211
- const result = await submit(form);
212
-
213
- expect(result).toBe('hello');
214
- expect(foo).toBe('barobject');
215
- });
216
-
217
- // @gate enableFormActions
218
- it('can submit a multiple complex closure server action without hydrating it', async () => {
219
- let foo = null;
220
-
221
- const serverAction = serverExports(function action(bound, formData) {
222
- foo = formData.get('foo') + bound.complex;
223
- return 'hello' + bound.complex;
224
- });
225
- function App() {
226
- return (
227
- <form action={serverAction.bind(null, {complex: 'a'})}>
228
- <input type="text" name="foo" defaultValue="bar" />
229
- <button formAction={serverAction.bind(null, {complex: 'b'})} />
230
- <button formAction={serverAction.bind(null, {complex: 'c'})} />
231
- <input
232
- type="submit"
233
- formAction={serverAction.bind(null, {complex: 'd'})}
234
- />
235
- </form>
236
- );
237
- }
238
- const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
239
- const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
240
- ssrManifest: {
241
- moduleMap: null,
242
- moduleLoading: null,
243
- },
244
- });
245
- const ssrStream = await ReactDOMServer.renderToReadableStream(response);
246
- await readIntoContainer(ssrStream);
247
-
248
- const form = container.firstChild;
249
-
250
- expect(foo).toBe(null);
251
-
252
- const result = await submit(form.getElementsByTagName('button')[1]);
253
-
254
- expect(result).toBe('helloc');
255
- expect(foo).toBe('barc');
256
- });
148
});
packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMNode-test.js
new
+132
@@ -0,0 +1,132 @@
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
+// Don't wait before processing work on the server.
13
+// TODO: we can replace this with FlightServer.act().
14
+global.setImmediate = cb => cb();
15
+
16
+let clientExports;
17
+let turbopackMap;
18
+let turbopackModules;
19
+let turbopackModuleLoading;
20
+let React;
21
+let ReactDOMServer;
22
+let ReactServerDOMServer;
23
+let ReactServerDOMClient;
24
+let Stream;
25
+let use;
26
+
27
+describe('ReactFlightDOMNode', () => {
28
+ beforeEach(() => {
29
+ jest.resetModules();
30
+
31
+ // Simulate the condition resolution
32
+ jest.mock('react', () => require('react/react.shared-subset'));
33
+ jest.mock('react-server-dom-turbopack/server', () =>
34
+ require('react-server-dom-turbopack/server.node'),
35
+ );
36
+ ReactServerDOMServer = require('react-server-dom-turbopack/server');
37
+
38
+ const TurbopackMock = require('./utils/TurbopackMock');
39
+ clientExports = TurbopackMock.clientExports;
40
+ turbopackMap = TurbopackMock.turbopackMap;
41
+ turbopackModules = TurbopackMock.turbopackModules;
42
+ turbopackModuleLoading = TurbopackMock.moduleLoading;
43
+
44
+ jest.resetModules();
45
+ __unmockReact();
46
+ jest.unmock('react-server-dom-turbopack/server');
47
+ jest.mock('react-server-dom-turbopack/client', () =>
48
+ require('react-server-dom-turbopack/client.node'),
49
+ );
50
+
51
+ React = require('react');
52
+ ReactDOMServer = require('react-dom/server.node');
53
+ ReactServerDOMClient = require('react-server-dom-turbopack/client');
54
+ Stream = require('stream');
55
+ use = React.use;
56
+ });
57
+
58
+ function readResult(stream) {
59
+ return new Promise((resolve, reject) => {
60
+ let buffer = '';
61
+ const writable = new Stream.PassThrough();
62
+ writable.setEncoding('utf8');
63
+ writable.on('data', chunk => {
64
+ buffer += chunk;
65
+ });
66
+ writable.on('error', error => {
67
+ reject(error);
68
+ });
69
+ writable.on('end', () => {
70
+ resolve(buffer);
71
+ });
72
+ stream.pipe(writable);
73
+ });
74
+ }
75
+
76
+ it('should allow an alternative module mapping to be used for SSR', async () => {
77
+ function ClientComponent() {
78
+ return <span>Client Component</span>;
79
+ }
80
+ // The Client build may not have the same IDs as the Server bundles for the same
81
+ // component.
82
+ const ClientComponentOnTheClient = clientExports(
83
+ ClientComponent,
84
+ 'path/to/chunk.js',
85
+ );
86
+ const ClientComponentOnTheServer = clientExports(ClientComponent);
87
+
88
+ // In the SSR bundle this module won't exist. We simulate this by deleting it.
89
+ const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
90
+ delete turbopackModules[clientId];
91
+
92
+ // Instead, we have to provide a translation from the client meta data to the SSR
93
+ // meta data.
94
+ const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
95
+ const translationMap = {
96
+ [clientId]: {
97
+ '*': ssrMetadata,
98
+ },
99
+ };
100
+
101
+ function App() {
102
+ return <ClientComponentOnTheClient />;
103
+ }
104
+
105
+ const stream = ReactServerDOMServer.renderToPipeableStream(
106
+ <App />,
107
+ turbopackMap,
108
+ );
109
+ const readable = new Stream.PassThrough();
110
+
111
+ stream.pipe(readable);
112
+
113
+ let response;
114
+ function ClientRoot() {
115
+ if (!response) {
116
+ response = ReactServerDOMClient.createFromNodeStream(readable, {
117
+ moduleMap: translationMap,
118
+ moduleLoading: turbopackModuleLoading,
119
+ });
120
+ }
121
+ return use(response);
122
+ }
123
+
124
+ const ssrStream = await ReactDOMServer.renderToPipeableStream(
125
+ <ClientRoot />,
126
+ );
127
+ const result = await readResult(ssrStream);
128
+ expect(result).toEqual(
129
+ '<script src="/prefix/path/to/chunk.js" async=""></script><span>Client Component</span>',
130
+ );
131
+ });
132
+});
packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js
new
+48
@@ -0,0 +1,48 @@
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
+// Polyfills for test environment
13
+global.ReadableStream =
14
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15
+global.TextEncoder = require('util').TextEncoder;
16
+global.TextDecoder = require('util').TextDecoder;
17
+
18
+// let serverExports;
19
+let turbopackServerMap;
20
+let ReactServerDOMServer;
21
+let ReactServerDOMClient;
22
+
23
+describe('ReactFlightDOMReply', () => {
24
+ beforeEach(() => {
25
+ jest.resetModules();
26
+ // Simulate the condition resolution
27
+ jest.mock('react', () => require('react/react.shared-subset'));
28
+ jest.mock('react-server-dom-turbopack/server', () =>
29
+ require('react-server-dom-turbopack/server.browser'),
30
+ );
31
+ const TurbopackMock = require('./utils/TurbopackMock');
32
+ // serverExports = TurbopackMock.serverExports;
33
+ turbopackServerMap = TurbopackMock.turbopackServerMap;
34
+ ReactServerDOMServer = require('react-server-dom-turbopack/server.browser');
35
+ jest.resetModules();
36
+ ReactServerDOMClient = require('react-server-dom-turbopack/client');
37
+ });
38
+
39
+ it('can encode a reply', async () => {
40
+ const body = await ReactServerDOMClient.encodeReply({some: 'object'});
41
+ const decoded = await ReactServerDOMServer.decodeReply(
42
+ body,
43
+ turbopackServerMap,
44
+ );
45
+
46
+ expect(decoded).toEqual({some: 'object'});
47
+ });
48
+});