Skip to content

Commit f4148b2

Browse files
authored
[Flight] Move around the Server side a bit (#17251)
* Rename ReactFlightStreamer -> ReactFlightServer * Unify Browser/Node stream tests into one file and use the client reader * Defer to the actual ReactDOM for HTML rendering for now This will need to use a variant of Fizz to do inline SSR in Flight. However, I don't want to build the whole impl right now but also don't want to exclude the use case yet. So I outsource it to the existing renderer. Ofc, this doesn't work with Suspense atm.
1 parent fadc971 commit f4148b2

15 files changed

+154
-114
lines changed

fixtures/flight-browser/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ <h1>Flight Example</h1>
1818
</div>
1919
<script src="../../build/dist/react.development.js"></script>
2020
<script src="../../build/dist/react-dom.development.js"></script>
21+
<script src="../../build/dist/react-dom-server.browser.development.js"></script>
2122
<script src="../../build/dist/react-dom-unstable-flight-server.browser.development.js"></script>
2223
<script src="../../build/dist/react-dom-unstable-flight-client.development.js"></script>
2324
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
* @jest-environment node
9+
*/
10+
11+
'use strict';
12+
13+
// Polyfills for test environment
14+
global.ReadableStream = require('@mattiasbuelens/web-streams-polyfill/ponyfill/es6').ReadableStream;
15+
global.TextDecoder = require('util').TextDecoder;
16+
17+
let Stream;
18+
let React;
19+
let ReactFlightDOMServer;
20+
let ReactFlightDOMClient;
21+
22+
describe('ReactFlightDOM', () => {
23+
beforeEach(() => {
24+
jest.resetModules();
25+
Stream = require('stream');
26+
React = require('react');
27+
ReactFlightDOMServer = require('react-dom/unstable-flight-server');
28+
ReactFlightDOMClient = require('react-dom/unstable-flight-client');
29+
});
30+
31+
function getTestStream() {
32+
let writable = new Stream.PassThrough();
33+
let readable = new ReadableStream({
34+
start(controller) {
35+
writable.on('data', chunk => {
36+
controller.enqueue(chunk);
37+
});
38+
writable.on('end', () => {
39+
controller.close();
40+
});
41+
},
42+
});
43+
return {
44+
writable,
45+
readable,
46+
};
47+
}
48+
49+
async function waitForSuspense(fn) {
50+
while (true) {
51+
try {
52+
return fn();
53+
} catch (promise) {
54+
if (typeof promise.then === 'function') {
55+
await promise;
56+
} else {
57+
throw promise;
58+
}
59+
}
60+
}
61+
}
62+
63+
it('should resolve HTML using Node streams', async () => {
64+
function Text({children}) {
65+
return <span>{children}</span>;
66+
}
67+
function HTML() {
68+
return (
69+
<div>
70+
<Text>hello</Text>
71+
<Text>world</Text>
72+
</div>
73+
);
74+
}
75+
76+
function App() {
77+
let model = {
78+
html: <HTML />,
79+
};
80+
return model;
81+
}
82+
83+
let {writable, readable} = getTestStream();
84+
ReactFlightDOMServer.pipeToNodeWritable(<App />, writable);
85+
let result = ReactFlightDOMClient.readFromReadableStream(readable);
86+
await waitForSuspense(() => {
87+
expect(result.model).toEqual({
88+
html: '<div><span>hello</span><span>world</span></div>',
89+
});
90+
});
91+
});
92+
});

packages/react-dom/src/__tests__/ReactFlightDOMBrowser-test.js

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,43 @@
55
* LICENSE file in the root directory of this source tree.
66
*
77
* @emails react-core
8+
* @jest-environment node
89
*/
910

1011
'use strict';
1112

1213
// Polyfills for test environment
1314
global.ReadableStream = require('@mattiasbuelens/web-streams-polyfill/ponyfill/es6').ReadableStream;
1415
global.TextEncoder = require('util').TextEncoder;
16+
global.TextDecoder = require('util').TextDecoder;
1517

1618
let React;
1719
let ReactFlightDOMServer;
20+
let ReactFlightDOMClient;
1821

19-
describe('ReactFlightDOM', () => {
22+
describe('ReactFlightDOMBrowser', () => {
2023
beforeEach(() => {
2124
jest.resetModules();
2225
React = require('react');
2326
ReactFlightDOMServer = require('react-dom/unstable-flight-server.browser');
27+
ReactFlightDOMClient = require('react-dom/unstable-flight-client');
2428
});
2529

26-
async function readResult(stream) {
27-
let reader = stream.getReader();
28-
let result = '';
30+
async function waitForSuspense(fn) {
2931
while (true) {
30-
let {done, value} = await reader.read();
31-
if (done) {
32-
return result;
32+
try {
33+
return fn();
34+
} catch (promise) {
35+
if (typeof promise.then === 'function') {
36+
await promise;
37+
} else {
38+
throw promise;
39+
}
3340
}
34-
result += Buffer.from(value).toString('utf8');
3541
}
3642
}
3743

38-
it('should resolve HTML', async () => {
44+
it('should resolve HTML using W3C streams', async () => {
3945
function Text({children}) {
4046
return <span>{children}</span>;
4147
}
@@ -48,14 +54,19 @@ describe('ReactFlightDOM', () => {
4854
);
4955
}
5056

51-
let model = {
52-
html: <HTML />,
53-
};
54-
let stream = ReactFlightDOMServer.renderToReadableStream(model);
55-
jest.runAllTimers();
56-
let result = JSON.parse(await readResult(stream));
57-
expect(result).toEqual({
58-
html: '<div><span>hello</span><span>world</span></div>',
57+
function App() {
58+
let model = {
59+
html: <HTML />,
60+
};
61+
return model;
62+
}
63+
64+
let stream = ReactFlightDOMServer.renderToReadableStream(<App />);
65+
let result = ReactFlightDOMClient.readFromReadableStream(stream);
66+
await waitForSuspense(() => {
67+
expect(result.model).toEqual({
68+
html: '<div><span>hello</span><span>world</span></div>',
69+
});
5970
});
6071
});
6172
});

packages/react-dom/src/__tests__/ReactFlightDOMNode-test.js

Lines changed: 0 additions & 57 deletions
This file was deleted.

packages/react-dom/src/server/ReactDOMServerFormatConfig.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import {convertStringToBuffer} from 'react-server/src/ReactServerHostConfig';
1111

12+
import ReactDOMServer from 'react-dom/server';
13+
1214
export function formatChunkAsString(type: string, props: Object): string {
1315
let str = '<' + type + '>';
1416
if (typeof props.children === 'string') {
@@ -21,3 +23,13 @@ export function formatChunkAsString(type: string, props: Object): string {
2123
export function formatChunk(type: string, props: Object): Uint8Array {
2224
return convertStringToBuffer(formatChunkAsString(type, props));
2325
}
26+
27+
export function renderHostChildrenToString(
28+
children: React$Element<any>,
29+
): string {
30+
// TODO: This file is used to actually implement a server renderer
31+
// so we can't actually reference the renderer here. Instead, we
32+
// should replace this method with a reference to Fizz which
33+
// then uses this file to implement the server renderer.
34+
return ReactDOMServer.renderToStaticMarkup(children);
35+
}

packages/react-dom/src/server/flight/ReactFlightDOMServerNode.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @flow
88
*/
99

10-
import type {ReactModel} from 'react-server/src/ReactFlightStreamer';
10+
import type {ReactModel} from 'react-server/flight.inline-typed';
1111
import type {Writable} from 'stream';
1212

1313
import {

packages/react-noop-renderer/src/ReactNoopFlightServer.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616

1717
import type {ReactModel} from 'react-server/flight.inline-typed';
1818

19-
import ReactFlightStreamer from 'react-server/flight';
19+
import ReactFlightServer from 'react-server/flight';
2020

2121
type Destination = Array<string>;
2222

23-
const ReactNoopFlightServer = ReactFlightStreamer({
23+
const ReactNoopFlightServer = ReactFlightServer({
2424
scheduleWork(callback: () => void) {
2525
callback();
2626
},
@@ -40,6 +40,9 @@ const ReactNoopFlightServer = ReactFlightStreamer({
4040
formatChunk(type: string, props: Object): Uint8Array {
4141
return Buffer.from(JSON.stringify({type, props}), 'utf8');
4242
},
43+
renderHostChildrenToString(children: React$Element<any>): string {
44+
throw new Error('The noop rendered do not support host components');
45+
},
4346
});
4447

4548
function render(model: ReactModel): Destination {

packages/react-server/flight.inline-typed.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@
2121
// renderers have different host config types. So we check them one by one.
2222
// We run Flow on all renderers on CI.
2323

24-
export * from './src/ReactFlightStreamer';
24+
export * from './src/ReactFlightServer';

packages/react-server/flight.inline.dom-browser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
// This file intentionally does *not* have the Flow annotation.
99
// Don't add it. See `./inline-typed.js` for an explanation.
1010

11-
export * from './src/ReactFlightStreamer';
11+
export * from './src/ReactFlightServer';

packages/react-server/flight.inline.dom.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
// This file intentionally does *not* have the Flow annotation.
99
// Don't add it. See `./inline-typed.js` for an explanation.
1010

11-
export * from './src/ReactFlightStreamer';
11+
export * from './src/ReactFlightServer';

packages/react-server/flight.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919

2020
'use strict';
2121

22-
const ReactFlightStreamer = require('./src/ReactFlightStreamer');
22+
const ReactFlightServer = require('./src/ReactFlightServer');
2323

2424
// TODO: decide on the top-level export form.
2525
// This is hacky but makes it work with both Rollup and Jest.
26-
module.exports = ReactFlightStreamer.default || ReactFlightStreamer;
26+
module.exports = ReactFlightServer.default || ReactFlightServer;

packages/react-server/src/ReactFlightStreamer.js renamed to packages/react-server/src/ReactFlightServer.js

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
close,
1919
convertStringToBuffer,
2020
} from './ReactServerHostConfig';
21-
import {formatChunkAsString} from './ReactServerFormatConfig';
21+
import {renderHostChildrenToString} from './ReactServerFormatConfig';
2222
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
2323

2424
export type ReactModel =
@@ -56,32 +56,6 @@ export function createRequest(
5656
return {destination, model, completedChunks: [], flowing: false};
5757
}
5858

59-
function resolveChildToHostFormat(child: ReactJSONValue): string {
60-
if (typeof child === 'string') {
61-
return child;
62-
} else if (typeof child === 'number') {
63-
return '' + child;
64-
} else if (typeof child === 'boolean' || child === null) {
65-
// Booleans are like null when they're React children.
66-
return '';
67-
} else if (Array.isArray(child)) {
68-
return (child: Array<ReactModel>)
69-
.map(c => resolveChildToHostFormat(resolveModelToJSON('', c)))
70-
.join('');
71-
} else {
72-
throw new Error('Object models are not valid as children of host nodes.');
73-
}
74-
}
75-
76-
function resolveElementToHostFormat(type: string, props: Object): string {
77-
let child = resolveModelToJSON('', props.children);
78-
let childString = resolveChildToHostFormat(child);
79-
return formatChunkAsString(
80-
type,
81-
Object.assign({}, props, {children: childString}),
82-
);
83-
}
84-
8559
function resolveModelToJSON(key: string, value: ReactModel): ReactJSONValue {
8660
while (value && value.$$typeof === REACT_ELEMENT_TYPE) {
8761
let element: React$Element<any> = (value: any);
@@ -93,7 +67,7 @@ function resolveModelToJSON(key: string, value: ReactModel): ReactJSONValue {
9367
continue;
9468
} else if (typeof type === 'string') {
9569
// This is a host element. E.g. HTML.
96-
return resolveElementToHostFormat(type, props);
70+
return renderHostChildrenToString(element);
9771
} else {
9872
throw new Error('Unsupported type.');
9973
}

packages/react-server/src/forks/ReactServerFormatConfig.custom.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,5 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef
2828

2929
export const formatChunkAsString = $$$hostConfig.formatChunkAsString;
3030
export const formatChunk = $$$hostConfig.formatChunk;
31+
export const renderHostChildrenToString =
32+
$$$hostConfig.renderHostChildrenToString;

0 commit comments

Comments
 (0)