Skip to content

Commit 8ac6070

Browse files
committed
Add entry points for "static" server rendering passes
This will be used to add optimizations for static server rendering.
1 parent f09c2bf commit 8ac6070

16 files changed

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

0 commit comments

Comments
 (0)