@samitouri / QOS-React-2 / commits / f4148b2561

[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',