Skip to content
Merged
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