[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.
Sebastian Markbåge committed
Nov 1, 2019 at 17:39 UTC
f4148b2561d468c292f184fb35b186efbc5dd758
15 files changed
+154
-114
fixtures/flight-browser/index.html
+1
@@ -18,6 +18,7 @@
18
</div>
19
<script src="../../build/dist/react.development.js"></script>
20
<script src="../../build/dist/react-dom.development.js"></script>
21
+ <script src="../../build/dist/react-dom-server.browser.development.js"></script>
22
<script src="../../build/dist/react-dom-unstable-flight-server.browser.development.js"></script>
23
<script src="../../build/dist/react-dom-unstable-flight-client.development.js"></script>
24
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
packages/react-dom/src/__tests__/ReactFlightDOM-test.js
new
+92
@@ -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
+28
-17
@@ -5,6 +5,7 @@
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,30 +13,35 @@
13
// Polyfills for test environment
14
global.ReadableStream = require('@mattiasbuelens/web-streams-polyfill/ponyfill/es6').ReadableStream;
15
global.TextEncoder = require('util').TextEncoder;
16
+global.TextDecoder = require('util').TextDecoder;
17
18
let React;
19
let ReactFlightDOMServer;
20
+let ReactFlightDOMClient;
21
19
-describe('ReactFlightDOM', () => {
22
+describe('ReactFlightDOMBrowser', () => {
23
beforeEach(() => {
24
jest.resetModules();
25
React = require('react');
26
ReactFlightDOMServer = require('react-dom/unstable-flight-server.browser');
27
+ ReactFlightDOMClient = require('react-dom/unstable-flight-client');
28
});
29
26
- async function readResult(stream) {
27
- let reader = stream.getReader();
28
- let result = '';
30
+ async function waitForSuspense(fn) {
31
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
+ }
40
}
34
- result += Buffer.from(value).toString('utf8');
41
}
42
}
43
38
- it('should resolve HTML', async () => {
44
+ it('should resolve HTML using W3C streams', async () => {
45
function Text({children}) {
46
return <span>{children}</span>;
47
}
@@ -48,14 +54,19 @@ describe('ReactFlightDOM', () => {
54
);
55
}
56
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
+ });
70
});
71
});
72
});
packages/react-dom/src/__tests__/ReactFlightDOMNode-test.js
deleted
-57
@@ -1,57 +0,0 @@
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
-let Stream;
14
-let React;
15
-let ReactFlightDOMServer;
16
-
17
-describe('ReactFlightDOM', () => {
18
- beforeEach(() => {
19
- jest.resetModules();
20
- React = require('react');
21
- ReactFlightDOMServer = require('react-dom/unstable-flight-server');
22
- Stream = require('stream');
23
- });
24
-
25
- function getTestWritable() {
26
- let writable = new Stream.PassThrough();
27
- writable.setEncoding('utf8');
28
- writable.result = '';
29
- writable.on('data', chunk => (writable.result += chunk));
30
- return writable;
31
- }
32
-
33
- it('should resolve HTML', () => {
34
- function Text({children}) {
35
- return <span>{children}</span>;
36
- }
37
- function HTML() {
38
- return (
39
- <div>
40
- <Text>hello</Text>
41
- <Text>world</Text>
42
- </div>
43
- );
44
- }
45
-
46
- let writable = getTestWritable();
47
- let model = {
48
- html: <HTML />,
49
- };
50
- ReactFlightDOMServer.pipeToNodeWritable(model, writable);
51
- jest.runAllTimers();
52
- let result = JSON.parse(writable.result);
53
- expect(result).toEqual({
54
- html: '<div><span>hello</span><span>world</span></div>',
55
- });
56
- });
57
-});
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+12
@@ -9,6 +9,8 @@
9
10
import {convertStringToBuffer} from 'react-server/src/ReactServerHostConfig';
11
12
+import ReactDOMServer from 'react-dom/server';
13
+
14
export function formatChunkAsString(type: string, props: Object): string {
15
let str = '<' + type + '>';
16
if (typeof props.children === 'string') {
@@ -21,3 +23,13 @@ export function formatChunkAsString(type: string, props: Object): string {
23
export function formatChunk(type: string, props: Object): Uint8Array {
24
return convertStringToBuffer(formatChunkAsString(type, props));
25
}
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
+1
-1
@@ -7,7 +7,7 @@
7
* @flow
8
*/
9
10
-import type {ReactModel} from 'react-server/src/ReactFlightStreamer';
10
+import type {ReactModel} from 'react-server/flight.inline-typed';
11
import type {Writable} from 'stream';
12
13
import {
packages/react-noop-renderer/src/ReactNoopFlightServer.js
+5
-2
@@ -16,11 +16,11 @@
16
17
import type {ReactModel} from 'react-server/flight.inline-typed';
18
19
-import ReactFlightStreamer from 'react-server/flight';
19
+import ReactFlightServer from 'react-server/flight';
20
21
type Destination = Array<string>;
22
23
-const ReactNoopFlightServer = ReactFlightStreamer({
23
+const ReactNoopFlightServer = ReactFlightServer({
24
scheduleWork(callback: () => void) {
25
callback();
26
},
@@ -40,6 +40,9 @@ const ReactNoopFlightServer = ReactFlightStreamer({
40
formatChunk(type: string, props: Object): Uint8Array {
41
return Buffer.from(JSON.stringify({type, props}), 'utf8');
42
},
43
+ renderHostChildrenToString(children: React$Element<any>): string {
44
+ throw new Error('The noop rendered do not support host components');
45
+ },
46
});
47
48
function render(model: ReactModel): Destination {
packages/react-server/flight.inline-typed.js
+1
-1
@@ -21,4 +21,4 @@
21
// renderers have different host config types. So we check them one by one.
22
// We run Flow on all renderers on CI.
23
24
-export * from './src/ReactFlightStreamer';
24
+export * from './src/ReactFlightServer';
packages/react-server/flight.inline.dom-browser.js
+1
-1
@@ -8,4 +8,4 @@
8
// This file intentionally does *not* have the Flow annotation.
9
// Don't add it. See `./inline-typed.js` for an explanation.
10
11
-export * from './src/ReactFlightStreamer';
11
+export * from './src/ReactFlightServer';
packages/react-server/flight.inline.dom.js
+1
-1
@@ -8,4 +8,4 @@
8
// This file intentionally does *not* have the Flow annotation.
9
// Don't add it. See `./inline-typed.js` for an explanation.
10
11
-export * from './src/ReactFlightStreamer';
11
+export * from './src/ReactFlightServer';
packages/react-server/flight.js
+2
-2
@@ -19,8 +19,8 @@
19
20
'use strict';
21
22
-const ReactFlightStreamer = require('./src/ReactFlightStreamer');
22
+const ReactFlightServer = require('./src/ReactFlightServer');
23
24
// TODO: decide on the top-level export form.
25
// 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/ReactFlightServer.js
renamed
+2
-28
@@ -18,7 +18,7 @@ import {
18
close,
19
convertStringToBuffer,
20
} from './ReactServerHostConfig';
21
-import {formatChunkAsString} from './ReactServerFormatConfig';
21
+import {renderHostChildrenToString} from './ReactServerFormatConfig';
22
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
23
24
export type ReactModel =
@@ -56,32 +56,6 @@ export function createRequest(
56
return {destination, model, completedChunks: [], flowing: false};
57
}
58
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
-
59
function resolveModelToJSON(key: string, value: ReactModel): ReactJSONValue {
60
while (value && value.$$typeof === REACT_ELEMENT_TYPE) {
61
let element: React$Element<any> = (value: any);
@@ -93,7 +67,7 @@ function resolveModelToJSON(key: string, value: ReactModel): ReactJSONValue {
67
continue;
68
} else if (typeof type === 'string') {
69
// This is a host element. E.g. HTML.
96
- return resolveElementToHostFormat(type, props);
70
+ return renderHostChildrenToString(element);
71
} else {
72
throw new Error('Unsupported type.');
73
}
packages/react-server/src/forks/ReactServerFormatConfig.custom.js
+2
@@ -28,3 +28,5 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef
28
29
export const formatChunkAsString = $$$hostConfig.formatChunkAsString;
30
export const formatChunk = $$$hostConfig.formatChunk;
31
+export const renderHostChildrenToString =
32
+ $$$hostConfig.renderHostChildrenToString;
scripts/rollup/bundles.js
+4
-4
@@ -160,14 +160,14 @@ const bundles = [
160
moduleType: RENDERER,
161
entry: 'react-dom/unstable-fizz.browser',
162
global: 'ReactDOMFizzServer',
163
- externals: ['react'],
163
+ externals: ['react', 'react-dom/server'],
164
},
165
{
166
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
167
moduleType: RENDERER,
168
entry: 'react-dom/unstable-fizz.node',
169
global: 'ReactDOMFizzServer',
170
- externals: ['react'],
170
+ externals: ['react', 'react-dom/server'],
171
},
172
173
/******* React DOM Flight Server *******/
@@ -176,14 +176,14 @@ const bundles = [
176
moduleType: RENDERER,
177
entry: 'react-dom/unstable-flight-server.browser',
178
global: 'ReactFlightDOMServer',
179
- externals: ['react'],
179
+ externals: ['react', 'react-dom/server'],
180
},
181
{
182
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
183
moduleType: RENDERER,
184
entry: 'react-dom/unstable-flight-server.node',
185
global: 'ReactFlightDOMServer',
186
- externals: ['react'],
186
+ externals: ['react', 'react-dom/server'],
187
},
188
189
/******* React DOM Flight Client *******/
scripts/rollup/modules.js
+2
@@ -17,12 +17,14 @@ const importSideEffects = Object.freeze({
17
'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
18
scheduler: HAS_NO_SIDE_EFFECTS_ON_IMPORT,
19
'scheduler/tracing': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
20
+ 'react-dom/server': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
21
});
22
23
// Bundles exporting globals that other modules rely on.
24
const knownGlobals = Object.freeze({
25
react: 'React',
26
'react-dom': 'ReactDOM',
27
+ 'react-dom/server': 'ReactDOMServer',
28
'react-interactions/events/keyboard': 'ReactEventsKeyboard',
29
'react-interactions/events/tap': 'ReactEventsTap',
30
scheduler: 'Scheduler',