Skip to content

Commit 4735f9c

Browse files
committed
Add Node Web Streams bundle for SSR
1 parent 3705486 commit 4735f9c

13 files changed

+557
-12
lines changed

packages/react-dom/npm/server.node.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
'use strict';
22

3-
var l, s;
3+
var l, s, w;
44
if (process.env.NODE_ENV === 'production') {
55
l = require('./cjs/react-dom-server-legacy.node.production.js');
66
s = require('./cjs/react-dom-server.node.production.js');
7+
w = require('./cjs/react-dom-server.node-webstreams.production.js');
78
} else {
89
l = require('./cjs/react-dom-server-legacy.node.development.js');
910
s = require('./cjs/react-dom-server.node.development.js');
11+
w = require('./cjs/react-dom-server.node-webstreams.development.js');
1012
}
1113

1214
exports.version = l.version;
@@ -16,3 +18,7 @@ exports.renderToPipeableStream = s.renderToPipeableStream;
1618
if (s.resumeToPipeableStream) {
1719
exports.resumeToPipeableStream = s.resumeToPipeableStream;
1820
}
21+
exports.renderToReadableStream = w.renderToReadableStream;
22+
if (w.resume) {
23+
exports.resume = w.resume;
24+
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
'use strict';
22

3-
var s;
3+
var s, w;
44
if (process.env.NODE_ENV === 'production') {
55
s = require('./cjs/react-dom-server.node.production.js');
6+
w = require('./cjs/react-dom-server.node-webstreams.production.js');
67
} else {
78
s = require('./cjs/react-dom-server.node.development.js');
9+
w = require('./cjs/react-dom-server.node-webstreams.development.js');
810
}
911

1012
exports.version = s.version;
1113
exports.prerenderToNodeStream = s.prerenderToNodeStream;
1214
exports.resumeAndPrerenderToNodeStream = s.resumeAndPrerenderToNodeStream;
15+
exports.prerender = w.prerender;
16+
exports.resumeAndPrerender = w.resumeAndPrerender;

packages/react-dom/server.node.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,17 @@ export function resumeToPipeableStream() {
3737
arguments,
3838
);
3939
}
40+
41+
export function renderToReadableStream() {
42+
return require('./src/server/react-dom-server.node-webstreams').renderToReadableStream.apply(
43+
this,
44+
arguments,
45+
);
46+
}
47+
48+
export function resume() {
49+
return require('./src/server/react-dom-server.node-webstreams').resume.apply(
50+
this,
51+
arguments,
52+
);
53+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 node
9+
*/
10+
11+
'use strict';
12+
13+
let React;
14+
let ReactDOMFizzServer;
15+
16+
describe('ReactDOMFizzServerNodeWebStreams', () => {
17+
beforeEach(() => {
18+
jest.resetModules();
19+
jest.useRealTimers();
20+
React = require('react');
21+
ReactDOMFizzServer = require('react-dom/server.node');
22+
});
23+
24+
async function readResult(stream) {
25+
const reader = stream.getReader();
26+
let result = '';
27+
while (true) {
28+
const {done, value} = await reader.read();
29+
if (done) {
30+
return result;
31+
}
32+
result += Buffer.from(value).toString('utf8');
33+
}
34+
}
35+
36+
it('should call renderToPipeableStream', async () => {
37+
const stream = await ReactDOMFizzServer.renderToReadableStream(
38+
<div>hello world</div>,
39+
);
40+
const result = await readResult(stream);
41+
expect(result).toMatchInlineSnapshot(`"<div>hello world</div>"`);
42+
});
43+
});
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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 node
9+
*/
10+
11+
'use strict';
12+
13+
import {
14+
getVisibleChildren,
15+
insertNodesAndExecuteScripts,
16+
} from '../test-utils/FizzTestUtils';
17+
18+
let JSDOM;
19+
let React;
20+
let ReactDOMFizzServer;
21+
let ReactDOMFizzStatic;
22+
let Suspense;
23+
let container;
24+
let serverAct;
25+
26+
describe('ReactDOMFizzStaticNodeWebStreams', () => {
27+
beforeEach(() => {
28+
jest.resetModules();
29+
serverAct = require('internal-test-utils').serverAct;
30+
31+
JSDOM = require('jsdom').JSDOM;
32+
33+
React = require('react');
34+
ReactDOMFizzServer = require('react-dom/server.node');
35+
ReactDOMFizzStatic = require('react-dom/static.node');
36+
Suspense = React.Suspense;
37+
38+
const jsdom = new JSDOM(
39+
// The Fizz runtime assumes requestAnimationFrame exists so we need to polyfill it.
40+
'<script>window.requestAnimationFrame = setTimeout;</script>',
41+
{
42+
runScripts: 'dangerously',
43+
},
44+
);
45+
global.window = jsdom.window;
46+
global.document = jsdom.window.document;
47+
container = document.createElement('div');
48+
document.body.appendChild(container);
49+
});
50+
51+
afterEach(() => {
52+
document.body.removeChild(container);
53+
});
54+
55+
async function readContent(stream) {
56+
const reader = stream.getReader();
57+
let content = '';
58+
while (true) {
59+
const {done, value} = await reader.read();
60+
if (done) {
61+
return content;
62+
}
63+
content += Buffer.from(value).toString('utf8');
64+
}
65+
}
66+
67+
async function readIntoContainer(stream) {
68+
const reader = stream.getReader();
69+
let result = '';
70+
while (true) {
71+
const {done, value} = await reader.read();
72+
if (done) {
73+
break;
74+
}
75+
result += Buffer.from(value).toString('utf8');
76+
}
77+
const temp = document.createElement('div');
78+
temp.innerHTML = result;
79+
await insertNodesAndExecuteScripts(temp, container, null);
80+
jest.runAllTimers();
81+
}
82+
83+
it('should call prerender', async () => {
84+
const result = await serverAct(() =>
85+
ReactDOMFizzStatic.prerender(<div>hello world</div>),
86+
);
87+
const prelude = await readContent(result.prelude);
88+
expect(prelude).toMatchInlineSnapshot(`"<div>hello world</div>"`);
89+
});
90+
91+
// @gate enableHalt
92+
it('can resume render of a prerender', async () => {
93+
const errors = [];
94+
95+
let resolveA;
96+
const promiseA = new Promise(r => (resolveA = r));
97+
let resolveB;
98+
const promiseB = new Promise(r => (resolveB = r));
99+
100+
async function ComponentA() {
101+
await promiseA;
102+
return (
103+
<Suspense fallback="Loading B">
104+
<ComponentB />
105+
</Suspense>
106+
);
107+
}
108+
109+
async function ComponentB() {
110+
await promiseB;
111+
return 'Hello';
112+
}
113+
114+
function App() {
115+
return (
116+
<div>
117+
<Suspense fallback="Loading A">
118+
<ComponentA />
119+
</Suspense>
120+
</div>
121+
);
122+
}
123+
124+
const controller = new AbortController();
125+
let pendingResult;
126+
await serverAct(async () => {
127+
pendingResult = ReactDOMFizzStatic.prerender(<App />, {
128+
signal: controller.signal,
129+
onError(x) {
130+
errors.push(x.message);
131+
},
132+
});
133+
});
134+
135+
controller.abort();
136+
const prerendered = await pendingResult;
137+
const postponedState = JSON.stringify(prerendered.postponed);
138+
139+
await readIntoContainer(prerendered.prelude);
140+
expect(getVisibleChildren(container)).toEqual(<div>Loading A</div>);
141+
142+
await resolveA();
143+
144+
expect(prerendered.postponed).not.toBe(null);
145+
146+
const controller2 = new AbortController();
147+
await serverAct(async () => {
148+
pendingResult = ReactDOMFizzStatic.resumeAndPrerender(
149+
<App />,
150+
JSON.parse(postponedState),
151+
{
152+
signal: controller2.signal,
153+
onError(x) {
154+
errors.push(x.message);
155+
},
156+
},
157+
);
158+
});
159+
160+
controller2.abort();
161+
162+
const prerendered2 = await pendingResult;
163+
const postponedState2 = JSON.stringify(prerendered2.postponed);
164+
165+
await readIntoContainer(prerendered2.prelude);
166+
expect(getVisibleChildren(container)).toEqual(<div>Loading B</div>);
167+
168+
await resolveB();
169+
170+
const dynamic = await serverAct(() =>
171+
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState2)),
172+
);
173+
174+
await readIntoContainer(dynamic);
175+
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
176+
});
177+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
* @flow
8+
*/
9+
10+
export * from './ReactDOMFizzServerEdge.js';
11+
export {prerender, resumeAndPrerender} from './ReactDOMFizzStaticEdge.js';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
* @flow
8+
*/
9+
10+
export {renderToReadableStream, version} from './ReactDOMFizzServerEdge.js';
11+
export {prerender} from './ReactDOMFizzStaticEdge.js';

packages/react-dom/static.node.js

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,51 @@
33
*
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
6-
*
7-
* @flow
86
*/
97

10-
export {
11-
prerenderToNodeStream,
12-
resumeAndPrerenderToNodeStream,
13-
version,
14-
} from './src/server/react-dom-server.node';
8+
// This file is only used for tests.
9+
// It lazily loads the implementation so that we get the correct set of host configs.
10+
11+
import ReactVersion from 'shared/ReactVersion';
12+
export {ReactVersion as version};
13+
14+
export function renderToString() {
15+
return require('./src/server/ReactDOMLegacyServerNode').renderToString.apply(
16+
this,
17+
arguments,
18+
);
19+
}
20+
export function renderToStaticMarkup() {
21+
return require('./src/server/ReactDOMLegacyServerNode').renderToStaticMarkup.apply(
22+
this,
23+
arguments,
24+
);
25+
}
26+
27+
export function prerenderToNodeStream() {
28+
return require('./src/server/react-dom-server.node').prerenderToNodeStream.apply(
29+
this,
30+
arguments,
31+
);
32+
}
33+
34+
export function resumeAndPrerenderToNodeStream() {
35+
return require('./src/server/react-dom-server.node').resumeAndPrerenderToNodeStream.apply(
36+
this,
37+
arguments,
38+
);
39+
}
40+
41+
export function prerender() {
42+
return require('./src/server/react-dom-server.node-webstreams').prerender.apply(
43+
this,
44+
arguments,
45+
);
46+
}
47+
48+
export function resumeAndPrerender() {
49+
return require('./src/server/react-dom-server.node-webstreams').resumeAndPrerender.apply(
50+
this,
51+
arguments,
52+
);
53+
}

0 commit comments

Comments
 (0)