Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fizz] Track postponed holes in the prerender pass #27317

Merged
merged 10 commits into from
Aug 31, 2023
4 changes: 2 additions & 2 deletions packages/react-dom/npm/server.node.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ exports.renderToStaticMarkup = l.renderToStaticMarkup;
exports.renderToNodeStream = l.renderToNodeStream;
exports.renderToStaticNodeStream = l.renderToStaticNodeStream;
exports.renderToPipeableStream = s.renderToPipeableStream;
if (s.resume) {
exports.resume = s.resume;
if (s.resumeToPipeableStream) {
exports.resumeToPipeableStream = s.resumeToPipeableStream;
}
4 changes: 2 additions & 2 deletions packages/react-dom/server.node.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export function renderToPipeableStream() {
);
}

export function resume() {
return require('./src/server/react-dom-server.node').resume.apply(
export function resumeToPipeableStream() {
return require('./src/server/react-dom-server.node').resumeToPipeableStream.apply(
this,
arguments,
);
Expand Down
101 changes: 61 additions & 40 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
mergeOptions,
stripExternalRuntimeInNodes,
withLoadingReadyState,
getVisibleChildren,
} from '../test-utils/FizzTestUtils';

let JSDOM;
Expand All @@ -23,6 +24,7 @@ let React;
let ReactDOM;
let ReactDOMClient;
let ReactDOMFizzServer;
let ReactDOMFizzStatic;
let Suspense;
let SuspenseList;
let useSyncExternalStore;
Expand Down Expand Up @@ -77,6 +79,9 @@ describe('ReactDOMFizzServer', () => {
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
ReactDOMFizzServer = require('react-dom/server');
if (__EXPERIMENTAL__) {
ReactDOMFizzStatic = require('react-dom/static');
}
Stream = require('stream');
Suspense = React.Suspense;
use = React.use;
Expand Down Expand Up @@ -289,46 +294,6 @@ describe('ReactDOMFizzServer', () => {
}, document);
}

function getVisibleChildren(element) {
const children = [];
let node = element.firstChild;
while (node) {
if (node.nodeType === 1) {
if (
node.tagName !== 'SCRIPT' &&
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) {
Expand Down Expand Up @@ -6227,4 +6192,60 @@ describe('ReactDOMFizzServer', () => {
);
},
);

// @gate enablePostpone
it('supports postponing in prerender and resuming later', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}

function App() {
return (
<div>
<Suspense fallback="Loading...">
<Postpone />
</Suspense>
</div>
);
}

const prerendered = await ReactDOMFizzStatic.prerenderToNodeStream(<App />);
expect(prerendered.postponed).not.toBe(null);

prerendering = false;

const resumed = ReactDOMFizzServer.resumeToPipeableStream(
<App />,
prerendered.postponed,
);

// Create a separate stream so it doesn't close the writable. I.e. simple concat.
const preludeWritable = new Stream.PassThrough();
preludeWritable.setEncoding('utf8');
preludeWritable.on('data', chunk => {
writable.write(chunk);
});

await act(() => {
prerendered.prelude.pipe(preludeWritable);
});

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

const b = new Stream.PassThrough();
b.setEncoding('utf8');
b.on('data', chunk => {
writable.write(chunk);
});

await act(() => {
resumed.pipe(writable);
});
Comment on lines +6239 to +6247
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you mean to pipe to b or is the passthrough not necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intentionally do this so that it still closes the writable at the end. The purpose of b is to just exclude the close().


// TODO: expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
});
});
137 changes: 137 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,37 @@

'use strict';

import {
getVisibleChildren,
insertNodesAndExecuteScripts,
} from '../test-utils/FizzTestUtils';

// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;

let React;
let ReactDOMFizzServer;
let ReactDOMFizzStatic;
let Suspense;
let container;

describe('ReactDOMFizzStaticBrowser', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOMFizzServer = require('react-dom/server.browser');
if (__EXPERIMENTAL__) {
ReactDOMFizzStatic = require('react-dom/static.browser');
}
Suspense = React.Suspense;
container = document.createElement('div');
document.body.appendChild(container);
});

afterEach(() => {
document.body.removeChild(container);
});

const theError = new Error('This is an error');
Expand All @@ -37,6 +51,36 @@ describe('ReactDOMFizzStaticBrowser', () => {
throw theInfinitePromise;
}

function concat(streamA, streamB) {
const readerA = streamA.getReader();
const readerB = streamB.getReader();
return new ReadableStream({
start(controller) {
function readA() {
readerA.read().then(({done, value}) => {
if (done) {
readB();
return;
}
controller.enqueue(value);
readA();
});
}
function readB() {
readerB.read().then(({done, value}) => {
if (done) {
controller.close();
return;
}
controller.enqueue(value);
readB();
});
}
readA();
},
});
}

async function readContent(stream) {
const reader = stream.getReader();
let content = '';
Expand All @@ -49,6 +93,21 @@ describe('ReactDOMFizzStaticBrowser', () => {
}
}

async function readIntoContainer(stream) {
const reader = stream.getReader();
let result = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
result += Buffer.from(value).toString('utf8');
}
const temp = document.createElement('div');
temp.innerHTML = result;
insertNodesAndExecuteScripts(temp, container, null);
}

// @gate experimental
it('should call prerender', async () => {
const result = await ReactDOMFizzStatic.prerender(<div>hello world</div>);
Expand Down Expand Up @@ -394,4 +453,82 @@ describe('ReactDOMFizzStaticBrowser', () => {

expect(errors).toEqual(['uh oh', 'uh oh']);
});

// @gate enablePostpone
it('supports postponing in prerender and resuming later', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}

function App() {
return (
<div>
<Suspense fallback="Loading...">
<Postpone />
</Suspense>
</div>
);
}

const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);

prerendering = false;

const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
);

await readIntoContainer(prerendered.prelude);

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

await readIntoContainer(resumed);

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

// @gate enablePostpone
it('only emits end tags once when resuming', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}

function App() {
return (
<html>
<body>
<Suspense fallback="Loading...">
<Postpone />
</Suspense>
</body>
</html>
);
}

const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);

prerendering = false;

const content = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
);

const html = await readContent(concat(prerendered.prelude, content));
const htmlEndTags = /<\/html\s*>/gi;
const bodyEndTags = /<\/body\s*>/gi;
expect(Array.from(html.matchAll(htmlEndTags)).length).toBe(1);
expect(Array.from(html.matchAll(bodyEndTags)).length).toBe(1);
});
});
6 changes: 3 additions & 3 deletions packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import ReactVersion from 'shared/ReactVersion';

import {
createRequest,
startWork,
startRender,
startFlowing,
abort,
} from 'react-server/src/ReactFizzServer';
Expand Down Expand Up @@ -129,7 +129,7 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
startWork(request);
startRender(request);
});
}

Expand Down Expand Up @@ -200,7 +200,7 @@ function resume(
signal.addEventListener('abort', listener);
}
}
startWork(request);
startRender(request);
});
}

Expand Down
4 changes: 2 additions & 2 deletions packages/react-dom/src/server/ReactDOMFizzServerBun.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import ReactVersion from 'shared/ReactVersion';

import {
createRequest,
startWork,
startRender,
startFlowing,
abort,
} from 'react-server/src/ReactFizzServer';
Expand Down Expand Up @@ -121,7 +121,7 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
startWork(request);
startRender(request);
});
}

Expand Down
Loading