Skip to content

Commit

Permalink
Add entry points for "static" server rendering passes (#24752)
Browse files Browse the repository at this point in the history
This will be used to add optimizations for static server rendering.
  • Loading branch information
sebmarkbage authored Jun 19, 2022
1 parent f796fa1 commit 0f216ae
Show file tree
Hide file tree
Showing 16 changed files with 1,379 additions and 1 deletion.
7 changes: 7 additions & 0 deletions packages/react-dom/npm/static.browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-dom-static.browser.production.min.js');
} else {
module.exports = require('./cjs/react-dom-static.browser.development.js');
}
3 changes: 3 additions & 0 deletions packages/react-dom/npm/static.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('./static.node');
7 changes: 7 additions & 0 deletions packages/react-dom/npm/static.node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-dom-static.node.production.min.js');
} else {
module.exports = require('./cjs/react-dom-static.node.development.js');
}
14 changes: 13 additions & 1 deletion packages/react-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"server.js",
"server.browser.js",
"server.node.js",
"static.js",
"static.browser.js",
"static.node.js",
"test-utils.js",
"unstable_testing.js",
"cjs/",
Expand All @@ -48,14 +51,23 @@
},
"./server.browser": "./server.browser.js",
"./server.node": "./server.node.js",
"./static": {
"deno": "./static.browser.js",
"worker": "./static.browser.js",
"browser": "./static.browser.js",
"default": "./static.node.js"
},
"./static.browser": "./static.browser.js",
"./static.node": "./static.node.js",
"./profiling": "./profiling.js",
"./test-utils": "./test-utils.js",
"./unstable_testing": "./unstable_testing.js",
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"browser": {
"./server.js": "./server.browser.js"
"./server.js": "./server.browser.js",
"./static.js": "./static.browser.js"
},
"browserify": {
"transform": [
Expand Down
237 changes: 237 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/

'use strict';

let JSDOM;
let Stream;
let React;
let ReactDOMClient;
let ReactDOMFizzStatic;
let Suspense;
let textCache;
let document;
let writable;
let container;
let buffer = '';
let hasErrored = false;
let fatalError = undefined;

describe('ReactDOMFizzStatic', () => {
beforeEach(() => {
jest.resetModules();
JSDOM = require('jsdom').JSDOM;
React = require('react');
ReactDOMClient = require('react-dom/client');
if (__EXPERIMENTAL__) {
ReactDOMFizzStatic = require('react-dom/static');
}
Stream = require('stream');
Suspense = React.Suspense;

textCache = new Map();

// Test Environment
const jsdom = new JSDOM(
'<!DOCTYPE html><html><head></head><body><div id="container">',
{
runScripts: 'dangerously',
},
);
document = jsdom.window.document;
container = document.getElementById('container');

buffer = '';
hasErrored = false;

writable = new Stream.PassThrough();
writable.setEncoding('utf8');
writable.on('data', chunk => {
buffer += chunk;
});
writable.on('error', error => {
hasErrored = true;
fatalError = error;
});
});

async function act(callback) {
await callback();
// Await one turn around the event loop.
// This assumes that we'll flush everything we have so far.
await new Promise(resolve => {
setImmediate(resolve);
});
if (hasErrored) {
throw fatalError;
}
// JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
// We also want to execute any scripts that are embedded.
// We assume that we have now received a proper fragment of HTML.
const bufferedContent = buffer;
buffer = '';
const fakeBody = document.createElement('body');
fakeBody.innerHTML = bufferedContent;
while (fakeBody.firstChild) {
const node = fakeBody.firstChild;
if (node.nodeName === 'SCRIPT') {
const script = document.createElement('script');
script.textContent = node.textContent;
fakeBody.removeChild(node);
container.appendChild(script);
} else {
container.appendChild(node);
}
}
}

function getVisibleChildren(element) {
const children = [];
let node = element.firstChild;
while (node) {
if (node.nodeType === 1) {
if (
node.tagName !== 'SCRIPT' &&
node.tagName !== 'TEMPLATE' &&
node.tagName !== 'template' &&
!node.hasAttribute('hidden') &&
!node.hasAttribute('aria-hidden')
) {
const props = {};
const attributes = node.attributes;
for (let i = 0; i < attributes.length; i++) {
if (
attributes[i].name === 'id' &&
attributes[i].value.includes(':')
) {
// We assume this is a React added ID that's a non-visual implementation detail.
continue;
}
props[attributes[i].name] = attributes[i].value;
}
props.children = getVisibleChildren(node);
children.push(React.createElement(node.tagName.toLowerCase(), props));
}
} else if (node.nodeType === 3) {
children.push(node.data);
}
node = node.nextSibling;
}
return children.length === 0
? undefined
: children.length === 1
? children[0]
: children;
}

function resolveText(text) {
const record = textCache.get(text);
if (record === undefined) {
const newRecord = {
status: 'resolved',
value: text,
};
textCache.set(text, newRecord);
} else if (record.status === 'pending') {
const thenable = record.value;
record.status = 'resolved';
record.value = text;
thenable.pings.forEach(t => t());
}
}

/*
function rejectText(text, error) {
const record = textCache.get(text);
if (record === undefined) {
const newRecord = {
status: 'rejected',
value: error,
};
textCache.set(text, newRecord);
} else if (record.status === 'pending') {
const thenable = record.value;
record.status = 'rejected';
record.value = error;
thenable.pings.forEach(t => t());
}
}
*/

function readText(text) {
const record = textCache.get(text);
if (record !== undefined) {
switch (record.status) {
case 'pending':
throw record.value;
case 'rejected':
throw record.value;
case 'resolved':
return record.value;
}
} else {
const thenable = {
pings: [],
then(resolve) {
if (newRecord.status === 'pending') {
thenable.pings.push(resolve);
} else {
Promise.resolve().then(() => resolve(newRecord.value));
}
},
};

const newRecord = {
status: 'pending',
value: thenable,
};
textCache.set(text, newRecord);

throw thenable;
}
}

function Text({text}) {
return text;
}

function AsyncText({text}) {
return readText(text);
}

// @gate experimental
it('should render a fully static document, send it and then hydrate it', async () => {
function App() {
return (
<div>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Hello" />
</Suspense>
</div>
);
}

const promise = ReactDOMFizzStatic.prerenderToNodeStreams(<App />);

resolveText('Hello');

const result = await promise;

await act(async () => {
result.prelude.pipe(writable);
});
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);

await act(async () => {
ReactDOMClient.hydrateRoot(container, <App />);
});

expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
});
});
Loading

0 comments on commit 0f216ae

Please sign in to comment.