Skip to content

Commit 0f216ae

Browse files
authored
Add entry points for "static" server rendering passes (#24752)
This will be used to add optimizations for static server rendering.
1 parent f796fa1 commit 0f216ae

16 files changed

+1379
-1
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
if (process.env.NODE_ENV === 'production') {
4+
module.exports = require('./cjs/react-dom-static.browser.production.min.js');
5+
} else {
6+
module.exports = require('./cjs/react-dom-static.browser.development.js');
7+
}

packages/react-dom/npm/static.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use strict';
2+
3+
module.exports = require('./static.node');

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
if (process.env.NODE_ENV === 'production') {
4+
module.exports = require('./cjs/react-dom-static.node.production.min.js');
5+
} else {
6+
module.exports = require('./cjs/react-dom-static.node.development.js');
7+
}

packages/react-dom/package.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
"server.js",
3333
"server.browser.js",
3434
"server.node.js",
35+
"static.js",
36+
"static.browser.js",
37+
"static.node.js",
3538
"test-utils.js",
3639
"unstable_testing.js",
3740
"cjs/",
@@ -48,14 +51,23 @@
4851
},
4952
"./server.browser": "./server.browser.js",
5053
"./server.node": "./server.node.js",
54+
"./static": {
55+
"deno": "./static.browser.js",
56+
"worker": "./static.browser.js",
57+
"browser": "./static.browser.js",
58+
"default": "./static.node.js"
59+
},
60+
"./static.browser": "./static.browser.js",
61+
"./static.node": "./static.node.js",
5162
"./profiling": "./profiling.js",
5263
"./test-utils": "./test-utils.js",
5364
"./unstable_testing": "./unstable_testing.js",
5465
"./src/*": "./src/*",
5566
"./package.json": "./package.json"
5667
},
5768
"browser": {
58-
"./server.js": "./server.browser.js"
69+
"./server.js": "./server.browser.js",
70+
"./static.js": "./static.browser.js"
5971
},
6072
"browserify": {
6173
"transform": [
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its 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+
let JSDOM;
13+
let Stream;
14+
let React;
15+
let ReactDOMClient;
16+
let ReactDOMFizzStatic;
17+
let Suspense;
18+
let textCache;
19+
let document;
20+
let writable;
21+
let container;
22+
let buffer = '';
23+
let hasErrored = false;
24+
let fatalError = undefined;
25+
26+
describe('ReactDOMFizzStatic', () => {
27+
beforeEach(() => {
28+
jest.resetModules();
29+
JSDOM = require('jsdom').JSDOM;
30+
React = require('react');
31+
ReactDOMClient = require('react-dom/client');
32+
if (__EXPERIMENTAL__) {
33+
ReactDOMFizzStatic = require('react-dom/static');
34+
}
35+
Stream = require('stream');
36+
Suspense = React.Suspense;
37+
38+
textCache = new Map();
39+
40+
// Test Environment
41+
const jsdom = new JSDOM(
42+
'<!DOCTYPE html><html><head></head><body><div id="container">',
43+
{
44+
runScripts: 'dangerously',
45+
},
46+
);
47+
document = jsdom.window.document;
48+
container = document.getElementById('container');
49+
50+
buffer = '';
51+
hasErrored = false;
52+
53+
writable = new Stream.PassThrough();
54+
writable.setEncoding('utf8');
55+
writable.on('data', chunk => {
56+
buffer += chunk;
57+
});
58+
writable.on('error', error => {
59+
hasErrored = true;
60+
fatalError = error;
61+
});
62+
});
63+
64+
async function act(callback) {
65+
await callback();
66+
// Await one turn around the event loop.
67+
// This assumes that we'll flush everything we have so far.
68+
await new Promise(resolve => {
69+
setImmediate(resolve);
70+
});
71+
if (hasErrored) {
72+
throw fatalError;
73+
}
74+
// JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
75+
// We also want to execute any scripts that are embedded.
76+
// We assume that we have now received a proper fragment of HTML.
77+
const bufferedContent = buffer;
78+
buffer = '';
79+
const fakeBody = document.createElement('body');
80+
fakeBody.innerHTML = bufferedContent;
81+
while (fakeBody.firstChild) {
82+
const node = fakeBody.firstChild;
83+
if (node.nodeName === 'SCRIPT') {
84+
const script = document.createElement('script');
85+
script.textContent = node.textContent;
86+
fakeBody.removeChild(node);
87+
container.appendChild(script);
88+
} else {
89+
container.appendChild(node);
90+
}
91+
}
92+
}
93+
94+
function getVisibleChildren(element) {
95+
const children = [];
96+
let node = element.firstChild;
97+
while (node) {
98+
if (node.nodeType === 1) {
99+
if (
100+
node.tagName !== 'SCRIPT' &&
101+
node.tagName !== 'TEMPLATE' &&
102+
node.tagName !== 'template' &&
103+
!node.hasAttribute('hidden') &&
104+
!node.hasAttribute('aria-hidden')
105+
) {
106+
const props = {};
107+
const attributes = node.attributes;
108+
for (let i = 0; i < attributes.length; i++) {
109+
if (
110+
attributes[i].name === 'id' &&
111+
attributes[i].value.includes(':')
112+
) {
113+
// We assume this is a React added ID that's a non-visual implementation detail.
114+
continue;
115+
}
116+
props[attributes[i].name] = attributes[i].value;
117+
}
118+
props.children = getVisibleChildren(node);
119+
children.push(React.createElement(node.tagName.toLowerCase(), props));
120+
}
121+
} else if (node.nodeType === 3) {
122+
children.push(node.data);
123+
}
124+
node = node.nextSibling;
125+
}
126+
return children.length === 0
127+
? undefined
128+
: children.length === 1
129+
? children[0]
130+
: children;
131+
}
132+
133+
function resolveText(text) {
134+
const record = textCache.get(text);
135+
if (record === undefined) {
136+
const newRecord = {
137+
status: 'resolved',
138+
value: text,
139+
};
140+
textCache.set(text, newRecord);
141+
} else if (record.status === 'pending') {
142+
const thenable = record.value;
143+
record.status = 'resolved';
144+
record.value = text;
145+
thenable.pings.forEach(t => t());
146+
}
147+
}
148+
149+
/*
150+
function rejectText(text, error) {
151+
const record = textCache.get(text);
152+
if (record === undefined) {
153+
const newRecord = {
154+
status: 'rejected',
155+
value: error,
156+
};
157+
textCache.set(text, newRecord);
158+
} else if (record.status === 'pending') {
159+
const thenable = record.value;
160+
record.status = 'rejected';
161+
record.value = error;
162+
thenable.pings.forEach(t => t());
163+
}
164+
}
165+
*/
166+
167+
function readText(text) {
168+
const record = textCache.get(text);
169+
if (record !== undefined) {
170+
switch (record.status) {
171+
case 'pending':
172+
throw record.value;
173+
case 'rejected':
174+
throw record.value;
175+
case 'resolved':
176+
return record.value;
177+
}
178+
} else {
179+
const thenable = {
180+
pings: [],
181+
then(resolve) {
182+
if (newRecord.status === 'pending') {
183+
thenable.pings.push(resolve);
184+
} else {
185+
Promise.resolve().then(() => resolve(newRecord.value));
186+
}
187+
},
188+
};
189+
190+
const newRecord = {
191+
status: 'pending',
192+
value: thenable,
193+
};
194+
textCache.set(text, newRecord);
195+
196+
throw thenable;
197+
}
198+
}
199+
200+
function Text({text}) {
201+
return text;
202+
}
203+
204+
function AsyncText({text}) {
205+
return readText(text);
206+
}
207+
208+
// @gate experimental
209+
it('should render a fully static document, send it and then hydrate it', async () => {
210+
function App() {
211+
return (
212+
<div>
213+
<Suspense fallback={<Text text="Loading..." />}>
214+
<AsyncText text="Hello" />
215+
</Suspense>
216+
</div>
217+
);
218+
}
219+
220+
const promise = ReactDOMFizzStatic.prerenderToNodeStreams(<App />);
221+
222+
resolveText('Hello');
223+
224+
const result = await promise;
225+
226+
await act(async () => {
227+
result.prelude.pipe(writable);
228+
});
229+
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
230+
231+
await act(async () => {
232+
ReactDOMClient.hydrateRoot(container, <App />);
233+
});
234+
235+
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
236+
});
237+
});

0 commit comments

Comments
 (0)