diff --git a/fixtures/flight-browser/index.html b/fixtures/flight-browser/index.html index 07489d30bea64..2ac19b77b3755 100644 --- a/fixtures/flight-browser/index.html +++ b/fixtures/flight-browser/index.html @@ -18,6 +18,7 @@

Flight Example

+ diff --git a/packages/react-dom/src/__tests__/ReactFlightDOM-test.js b/packages/react-dom/src/__tests__/ReactFlightDOM-test.js new file mode 100644 index 0000000000000..5fb8440cf8716 --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactFlightDOM-test.js @@ -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 {children}; + } + function HTML() { + return ( +
+ hello + world +
+ ); + } + + function App() { + let model = { + html: , + }; + return model; + } + + let {writable, readable} = getTestStream(); + ReactFlightDOMServer.pipeToNodeWritable(, writable); + let result = ReactFlightDOMClient.readFromReadableStream(readable); + await waitForSuspense(() => { + expect(result.model).toEqual({ + html: '
helloworld
', + }); + }); + }); +}); diff --git a/packages/react-dom/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-dom/src/__tests__/ReactFlightDOMBrowser-test.js index 39b0956553558..70387e585f999 100644 --- a/packages/react-dom/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactFlightDOMBrowser-test.js @@ -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 {children}; } @@ -48,14 +54,19 @@ describe('ReactFlightDOM', () => { ); } - let model = { - html: , - }; - let stream = ReactFlightDOMServer.renderToReadableStream(model); - jest.runAllTimers(); - let result = JSON.parse(await readResult(stream)); - expect(result).toEqual({ - html: '
helloworld
', + function App() { + let model = { + html: , + }; + return model; + } + + let stream = ReactFlightDOMServer.renderToReadableStream(); + let result = ReactFlightDOMClient.readFromReadableStream(stream); + await waitForSuspense(() => { + expect(result.model).toEqual({ + html: '
helloworld
', + }); }); }); }); diff --git a/packages/react-dom/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-dom/src/__tests__/ReactFlightDOMNode-test.js deleted file mode 100644 index 79494a49164eb..0000000000000 --- a/packages/react-dom/src/__tests__/ReactFlightDOMNode-test.js +++ /dev/null @@ -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 {children}; - } - function HTML() { - return ( -
- hello - world -
- ); - } - - let writable = getTestWritable(); - let model = { - html: , - }; - ReactFlightDOMServer.pipeToNodeWritable(model, writable); - jest.runAllTimers(); - let result = JSON.parse(writable.result); - expect(result).toEqual({ - html: '
helloworld
', - }); - }); -}); diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index d20d0119b87ca..ece3e63890582 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -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, +): 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); +} diff --git a/packages/react-dom/src/server/flight/ReactFlightDOMServerNode.js b/packages/react-dom/src/server/flight/ReactFlightDOMServerNode.js index 1ca6255204caa..82ece33fa52db 100644 --- a/packages/react-dom/src/server/flight/ReactFlightDOMServerNode.js +++ b/packages/react-dom/src/server/flight/ReactFlightDOMServerNode.js @@ -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 { diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index 70e54ef24cac1..e5d77117dec8e 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -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; -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): string { + throw new Error('The noop rendered do not support host components'); + }, }); function render(model: ReactModel): Destination { diff --git a/packages/react-server/flight.inline-typed.js b/packages/react-server/flight.inline-typed.js index 7f435c945c3c1..8afc4525e9559 100644 --- a/packages/react-server/flight.inline-typed.js +++ b/packages/react-server/flight.inline-typed.js @@ -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'; diff --git a/packages/react-server/flight.inline.dom-browser.js b/packages/react-server/flight.inline.dom-browser.js index 34b97ffff0533..2c140b0a7e498 100644 --- a/packages/react-server/flight.inline.dom-browser.js +++ b/packages/react-server/flight.inline.dom-browser.js @@ -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'; diff --git a/packages/react-server/flight.inline.dom.js b/packages/react-server/flight.inline.dom.js index 34b97ffff0533..2c140b0a7e498 100644 --- a/packages/react-server/flight.inline.dom.js +++ b/packages/react-server/flight.inline.dom.js @@ -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'; diff --git a/packages/react-server/flight.js b/packages/react-server/flight.js index b5f6cc0b9d263..26fd4bd3ed558 100644 --- a/packages/react-server/flight.js +++ b/packages/react-server/flight.js @@ -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; diff --git a/packages/react-server/src/ReactFlightStreamer.js b/packages/react-server/src/ReactFlightServer.js similarity index 73% rename from packages/react-server/src/ReactFlightStreamer.js rename to packages/react-server/src/ReactFlightServer.js index 5642510100ed3..34dd032dcff89 100644 --- a/packages/react-server/src/ReactFlightStreamer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -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) - .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 = (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.'); } diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js index f00ecbf2529aa..a864d5f6beb92 100644 --- a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js @@ -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; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index c48581a372de2..ee0cdd2cf25c0 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -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 *******/ diff --git a/scripts/rollup/modules.js b/scripts/rollup/modules.js index fe09b951bb2cd..968f98bc3f081 100644 --- a/scripts/rollup/modules.js +++ b/scripts/rollup/modules.js @@ -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',