[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.
This commit is contained in:
parent
fadc97167f
commit
f4148b2561
|
@ -18,6 +18,7 @@
|
|||
</div>
|
||||
<script src="../../build/dist/react.development.js"></script>
|
||||
<script src="../../build/dist/react-dom.development.js"></script>
|
||||
<script src="../../build/dist/react-dom-server.browser.development.js"></script>
|
||||
<script src="../../build/dist/react-dom-unstable-flight-server.browser.development.js"></script>
|
||||
<script src="../../build/dist/react-dom-unstable-flight-client.development.js"></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream = require('@mattiasbuelens/web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.TextDecoder = require('util').TextDecoder;
|
||||
|
||||
let Stream;
|
||||
let React;
|
||||
let ReactFlightDOMServer;
|
||||
let ReactFlightDOMClient;
|
||||
|
||||
describe('ReactFlightDOM', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
Stream = require('stream');
|
||||
React = require('react');
|
||||
ReactFlightDOMServer = require('react-dom/unstable-flight-server');
|
||||
ReactFlightDOMClient = require('react-dom/unstable-flight-client');
|
||||
});
|
||||
|
||||
function getTestStream() {
|
||||
let writable = new Stream.PassThrough();
|
||||
let readable = new ReadableStream({
|
||||
start(controller) {
|
||||
writable.on('data', chunk => {
|
||||
controller.enqueue(chunk);
|
||||
});
|
||||
writable.on('end', () => {
|
||||
controller.close();
|
||||
});
|
||||
},
|
||||
});
|
||||
return {
|
||||
writable,
|
||||
readable,
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForSuspense(fn) {
|
||||
while (true) {
|
||||
try {
|
||||
return fn();
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
await promise;
|
||||
} else {
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('should resolve HTML using Node streams', async () => {
|
||||
function Text({children}) {
|
||||
return <span>{children}</span>;
|
||||
}
|
||||
function HTML() {
|
||||
return (
|
||||
<div>
|
||||
<Text>hello</Text>
|
||||
<Text>world</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
let model = {
|
||||
html: <HTML />,
|
||||
};
|
||||
return model;
|
||||
}
|
||||
|
||||
let {writable, readable} = getTestStream();
|
||||
ReactFlightDOMServer.pipeToNodeWritable(<App />, writable);
|
||||
let result = ReactFlightDOMClient.readFromReadableStream(readable);
|
||||
await waitForSuspense(() => {
|
||||
expect(result.model).toEqual({
|
||||
html: '<div><span>hello</span><span>world</span></div>',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,6 +5,7 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
@ -12,30 +13,35 @@
|
|||
// Polyfills for test environment
|
||||
global.ReadableStream = require('@mattiasbuelens/web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
global.TextDecoder = require('util').TextDecoder;
|
||||
|
||||
let React;
|
||||
let ReactFlightDOMServer;
|
||||
let ReactFlightDOMClient;
|
||||
|
||||
describe('ReactFlightDOM', () => {
|
||||
describe('ReactFlightDOMBrowser', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactFlightDOMServer = require('react-dom/unstable-flight-server.browser');
|
||||
ReactFlightDOMClient = require('react-dom/unstable-flight-client');
|
||||
});
|
||||
|
||||
async function readResult(stream) {
|
||||
let reader = stream.getReader();
|
||||
let result = '';
|
||||
async function waitForSuspense(fn) {
|
||||
while (true) {
|
||||
let {done, value} = await reader.read();
|
||||
if (done) {
|
||||
return result;
|
||||
try {
|
||||
return fn();
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
await promise;
|
||||
} else {
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
result += Buffer.from(value).toString('utf8');
|
||||
}
|
||||
}
|
||||
|
||||
it('should resolve HTML', async () => {
|
||||
it('should resolve HTML using W3C streams', async () => {
|
||||
function Text({children}) {
|
||||
return <span>{children}</span>;
|
||||
}
|
||||
|
@ -48,14 +54,19 @@ describe('ReactFlightDOM', () => {
|
|||
);
|
||||
}
|
||||
|
||||
let model = {
|
||||
html: <HTML />,
|
||||
};
|
||||
let stream = ReactFlightDOMServer.renderToReadableStream(model);
|
||||
jest.runAllTimers();
|
||||
let result = JSON.parse(await readResult(stream));
|
||||
expect(result).toEqual({
|
||||
html: '<div><span>hello</span><span>world</span></div>',
|
||||
function App() {
|
||||
let model = {
|
||||
html: <HTML />,
|
||||
};
|
||||
return model;
|
||||
}
|
||||
|
||||
let stream = ReactFlightDOMServer.renderToReadableStream(<App />);
|
||||
let result = ReactFlightDOMClient.readFromReadableStream(stream);
|
||||
await waitForSuspense(() => {
|
||||
expect(result.model).toEqual({
|
||||
html: '<div><span>hello</span><span>world</span></div>',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let Stream;
|
||||
let React;
|
||||
let ReactFlightDOMServer;
|
||||
|
||||
describe('ReactFlightDOM', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactFlightDOMServer = require('react-dom/unstable-flight-server');
|
||||
Stream = require('stream');
|
||||
});
|
||||
|
||||
function getTestWritable() {
|
||||
let writable = new Stream.PassThrough();
|
||||
writable.setEncoding('utf8');
|
||||
writable.result = '';
|
||||
writable.on('data', chunk => (writable.result += chunk));
|
||||
return writable;
|
||||
}
|
||||
|
||||
it('should resolve HTML', () => {
|
||||
function Text({children}) {
|
||||
return <span>{children}</span>;
|
||||
}
|
||||
function HTML() {
|
||||
return (
|
||||
<div>
|
||||
<Text>hello</Text>
|
||||
<Text>world</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let writable = getTestWritable();
|
||||
let model = {
|
||||
html: <HTML />,
|
||||
};
|
||||
ReactFlightDOMServer.pipeToNodeWritable(model, writable);
|
||||
jest.runAllTimers();
|
||||
let result = JSON.parse(writable.result);
|
||||
expect(result).toEqual({
|
||||
html: '<div><span>hello</span><span>world</span></div>',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,6 +9,8 @@
|
|||
|
||||
import {convertStringToBuffer} from 'react-server/src/ReactServerHostConfig';
|
||||
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
|
||||
export function formatChunkAsString(type: string, props: Object): string {
|
||||
let str = '<' + type + '>';
|
||||
if (typeof props.children === 'string') {
|
||||
|
@ -21,3 +23,13 @@ export function formatChunkAsString(type: string, props: Object): string {
|
|||
export function formatChunk(type: string, props: Object): Uint8Array {
|
||||
return convertStringToBuffer(formatChunkAsString(type, props));
|
||||
}
|
||||
|
||||
export function renderHostChildrenToString(
|
||||
children: React$Element<any>,
|
||||
): string {
|
||||
// TODO: This file is used to actually implement a server renderer
|
||||
// so we can't actually reference the renderer here. Instead, we
|
||||
// should replace this method with a reference to Fizz which
|
||||
// then uses this file to implement the server renderer.
|
||||
return ReactDOMServer.renderToStaticMarkup(children);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactModel} from 'react-server/src/ReactFlightStreamer';
|
||||
import type {ReactModel} from 'react-server/flight.inline-typed';
|
||||
import type {Writable} from 'stream';
|
||||
|
||||
import {
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
|
||||
import type {ReactModel} from 'react-server/flight.inline-typed';
|
||||
|
||||
import ReactFlightStreamer from 'react-server/flight';
|
||||
import ReactFlightServer from 'react-server/flight';
|
||||
|
||||
type Destination = Array<string>;
|
||||
|
||||
const ReactNoopFlightServer = ReactFlightStreamer({
|
||||
const ReactNoopFlightServer = ReactFlightServer({
|
||||
scheduleWork(callback: () => void) {
|
||||
callback();
|
||||
},
|
||||
|
@ -40,6 +40,9 @@ const ReactNoopFlightServer = ReactFlightStreamer({
|
|||
formatChunk(type: string, props: Object): Uint8Array {
|
||||
return Buffer.from(JSON.stringify({type, props}), 'utf8');
|
||||
},
|
||||
renderHostChildrenToString(children: React$Element<any>): string {
|
||||
throw new Error('The noop rendered do not support host components');
|
||||
},
|
||||
});
|
||||
|
||||
function render(model: ReactModel): Destination {
|
||||
|
|
|
@ -21,4 +21,4 @@
|
|||
// renderers have different host config types. So we check them one by one.
|
||||
// We run Flow on all renderers on CI.
|
||||
|
||||
export * from './src/ReactFlightStreamer';
|
||||
export * from './src/ReactFlightServer';
|
||||
|
|
|
@ -8,4 +8,4 @@
|
|||
// This file intentionally does *not* have the Flow annotation.
|
||||
// Don't add it. See `./inline-typed.js` for an explanation.
|
||||
|
||||
export * from './src/ReactFlightStreamer';
|
||||
export * from './src/ReactFlightServer';
|
||||
|
|
|
@ -8,4 +8,4 @@
|
|||
// This file intentionally does *not* have the Flow annotation.
|
||||
// Don't add it. See `./inline-typed.js` for an explanation.
|
||||
|
||||
export * from './src/ReactFlightStreamer';
|
||||
export * from './src/ReactFlightServer';
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const ReactFlightStreamer = require('./src/ReactFlightStreamer');
|
||||
const ReactFlightServer = require('./src/ReactFlightServer');
|
||||
|
||||
// TODO: decide on the top-level export form.
|
||||
// This is hacky but makes it work with both Rollup and Jest.
|
||||
module.exports = ReactFlightStreamer.default || ReactFlightStreamer;
|
||||
module.exports = ReactFlightServer.default || ReactFlightServer;
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
close,
|
||||
convertStringToBuffer,
|
||||
} from './ReactServerHostConfig';
|
||||
import {formatChunkAsString} from './ReactServerFormatConfig';
|
||||
import {renderHostChildrenToString} from './ReactServerFormatConfig';
|
||||
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
|
||||
|
||||
export type ReactModel =
|
||||
|
@ -56,32 +56,6 @@ export function createRequest(
|
|||
return {destination, model, completedChunks: [], flowing: false};
|
||||
}
|
||||
|
||||
function resolveChildToHostFormat(child: ReactJSONValue): string {
|
||||
if (typeof child === 'string') {
|
||||
return child;
|
||||
} else if (typeof child === 'number') {
|
||||
return '' + child;
|
||||
} else if (typeof child === 'boolean' || child === null) {
|
||||
// Booleans are like null when they're React children.
|
||||
return '';
|
||||
} else if (Array.isArray(child)) {
|
||||
return (child: Array<ReactModel>)
|
||||
.map(c => resolveChildToHostFormat(resolveModelToJSON('', c)))
|
||||
.join('');
|
||||
} else {
|
||||
throw new Error('Object models are not valid as children of host nodes.');
|
||||
}
|
||||
}
|
||||
|
||||
function resolveElementToHostFormat(type: string, props: Object): string {
|
||||
let child = resolveModelToJSON('', props.children);
|
||||
let childString = resolveChildToHostFormat(child);
|
||||
return formatChunkAsString(
|
||||
type,
|
||||
Object.assign({}, props, {children: childString}),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveModelToJSON(key: string, value: ReactModel): ReactJSONValue {
|
||||
while (value && value.$$typeof === REACT_ELEMENT_TYPE) {
|
||||
let element: React$Element<any> = (value: any);
|
||||
|
@ -93,7 +67,7 @@ function resolveModelToJSON(key: string, value: ReactModel): ReactJSONValue {
|
|||
continue;
|
||||
} else if (typeof type === 'string') {
|
||||
// This is a host element. E.g. HTML.
|
||||
return resolveElementToHostFormat(type, props);
|
||||
return renderHostChildrenToString(element);
|
||||
} else {
|
||||
throw new Error('Unsupported type.');
|
||||
}
|
|
@ -28,3 +28,5 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef
|
|||
|
||||
export const formatChunkAsString = $$$hostConfig.formatChunkAsString;
|
||||
export const formatChunk = $$$hostConfig.formatChunk;
|
||||
export const renderHostChildrenToString =
|
||||
$$$hostConfig.renderHostChildrenToString;
|
||||
|
|
|
@ -160,14 +160,14 @@ const bundles = [
|
|||
moduleType: RENDERER,
|
||||
entry: 'react-dom/unstable-fizz.browser',
|
||||
global: 'ReactDOMFizzServer',
|
||||
externals: ['react'],
|
||||
externals: ['react', 'react-dom/server'],
|
||||
},
|
||||
{
|
||||
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
|
||||
moduleType: RENDERER,
|
||||
entry: 'react-dom/unstable-fizz.node',
|
||||
global: 'ReactDOMFizzServer',
|
||||
externals: ['react'],
|
||||
externals: ['react', 'react-dom/server'],
|
||||
},
|
||||
|
||||
/******* React DOM Flight Server *******/
|
||||
|
@ -176,14 +176,14 @@ const bundles = [
|
|||
moduleType: RENDERER,
|
||||
entry: 'react-dom/unstable-flight-server.browser',
|
||||
global: 'ReactFlightDOMServer',
|
||||
externals: ['react'],
|
||||
externals: ['react', 'react-dom/server'],
|
||||
},
|
||||
{
|
||||
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
|
||||
moduleType: RENDERER,
|
||||
entry: 'react-dom/unstable-flight-server.node',
|
||||
global: 'ReactFlightDOMServer',
|
||||
externals: ['react'],
|
||||
externals: ['react', 'react-dom/server'],
|
||||
},
|
||||
|
||||
/******* React DOM Flight Client *******/
|
||||
|
|
|
@ -17,12 +17,14 @@ const importSideEffects = Object.freeze({
|
|||
'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
|
||||
scheduler: HAS_NO_SIDE_EFFECTS_ON_IMPORT,
|
||||
'scheduler/tracing': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
|
||||
'react-dom/server': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
|
||||
});
|
||||
|
||||
// Bundles exporting globals that other modules rely on.
|
||||
const knownGlobals = Object.freeze({
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
'react-dom/server': 'ReactDOMServer',
|
||||
'react-interactions/events/keyboard': 'ReactEventsKeyboard',
|
||||
'react-interactions/events/tap': 'ReactEventsTap',
|
||||
scheduler: 'Scheduler',
|
||||
|
|
Loading…
Reference in New Issue