From f4e974d26e273c4eb58c0ffa9b479435bf598ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 29 Oct 2019 14:45:47 -0700 Subject: [PATCH] Add Experimental Flight Infrastructure (#16398) * Add Flight Build and Unify HostFormat Config between Flight and Fizz * Add basic resolution of models * Add basic Flight fixture Demonstrates the streaming protocol. * Rename to flight-server to distinguish from the client parts * Add Flight Client package and entry point * Fix fixture --- fixtures/flight-browser/index.html | 84 ++++++++++ .../react-dom/npm/unstable-flight-client.js | 7 + .../npm/unstable-flight-server.browser.js | 7 + .../react-dom/npm/unstable-flight-server.js | 3 + .../npm/unstable-flight-server.node.js | 7 + packages/react-dom/package.json | 6 +- .../src/client/flight/ReactFlightDOMClient.js | 34 +++++ .../client/flight/ReactFlightDOMHostConfig.js | 50 ++++++ .../__tests__/ReactFlightDOMBrowser-test.js | 61 ++++++++ .../__tests__/ReactFlightDOMNode-test.js | 57 +++++++ .../src/server/ReactDOMFizzServerBrowser.js | 2 +- .../src/server/ReactDOMFizzServerNode.js | 2 +- ...onfig.js => ReactDOMServerFormatConfig.js} | 10 +- .../flight/ReactFlightDOMServerBrowser.js | 34 +++++ .../server/flight/ReactFlightDOMServerNode.js | 31 ++++ .../__tests__/ReactFlightDOMBrowser-test.js | 61 ++++++++ .../__tests__/ReactFlightDOMNode-test.js | 57 +++++++ packages/react-dom/unstable-flight-client.js | 16 ++ .../unstable-flight-server.browser.js | 17 +++ packages/react-dom/unstable-flight-server.js | 12 ++ .../react-dom/unstable-flight-server.node.js | 16 ++ packages/react-flight/README.md | 7 + packages/react-flight/index.js | 26 ++++ packages/react-flight/inline-typed.js | 24 +++ packages/react-flight/inline.dom-browser.js | 11 ++ packages/react-flight/inline.dom.js | 11 ++ packages/react-flight/npm/index.js | 7 + packages/react-flight/package.json | 40 +++++ .../react-flight/src/ReactFlightClient.js | 144 ++++++++++++++++++ .../src/ReactFlightClientHostConfig.js} | 0 .../src/__tests__/ReactFlightClient-test.js | 38 +++++ .../ReactFlightClientHostConfig.custom.js | 37 +++++ ...ReactFlightClientHostConfig.dom-browser.js | 10 ++ .../forks/ReactFlightClientHostConfig.dom.js | 10 ++ packages/react-noop-renderer/flight-client.js | 16 ++ packages/react-noop-renderer/flight-server.js | 16 ++ .../react-noop-renderer/npm/flight-client.js | 7 + .../react-noop-renderer/npm/flight-server.js | 7 + packages/react-noop-renderer/package.json | 7 +- .../src/ReactNoopFlightClient.js | 54 +++++++ .../src/ReactNoopFlightServer.js | 54 +++++++ .../src/ReactNoopServer.js | 5 +- packages/react-server/README.md | 7 + packages/react-server/flight.inline-typed.js | 24 +++ .../react-server/flight.inline.dom-browser.js | 11 ++ packages/react-server/flight.inline.dom.js | 11 ++ packages/react-server/flight.js | 26 ++++ .../{react-stream => react-server}/index.js | 6 +- .../inline-typed.js | 2 +- .../inline.dom-browser.js | 0 .../inline.dom.js | 0 packages/react-server/npm/flight.js | 7 + packages/react-server/npm/index.js | 7 + .../package.json | 5 +- .../src/ReactFizzStreamer.js | 6 +- .../react-server/src/ReactFlightStreamer.js | 144 ++++++++++++++++++ .../src/ReactServerFormatConfig.js} | 0 .../react-server/src/ReactServerHostConfig.js | 22 +++ .../src/ReactServerHostConfigBrowser.js} | 0 .../src/ReactServerHostConfigNode.js} | 0 .../src/__tests__/ReactFlightServer-test.js | 38 +++++ .../src/__tests__/ReactServer-test.js | 0 .../forks/ReactServerFormatConfig.custom.js} | 7 +- .../ReactServerFormatConfig.dom-browser.js} | 2 +- .../src/forks/ReactServerFormatConfig.dom.js} | 2 +- .../forks/ReactServerHostConfig.custom.js} | 6 +- .../ReactServerHostConfig.dom-browser.js} | 2 +- .../src/forks/ReactServerHostConfig.dom.js} | 2 +- packages/react-stream/README.md | 25 --- packages/react-stream/npm/index.js | 7 - scripts/flow/createFlowConfigs.js | 15 +- scripts/jest/setupHostConfigs.js | 109 +++++++++++-- scripts/rollup/bundles.js | 74 ++++++++- scripts/rollup/forks.js | 50 ++++-- scripts/shared/inlinedHostConfigs.js | 31 ++-- 75 files changed, 1651 insertions(+), 102 deletions(-) create mode 100644 fixtures/flight-browser/index.html create mode 100644 packages/react-dom/npm/unstable-flight-client.js create mode 100644 packages/react-dom/npm/unstable-flight-server.browser.js create mode 100644 packages/react-dom/npm/unstable-flight-server.js create mode 100644 packages/react-dom/npm/unstable-flight-server.node.js create mode 100644 packages/react-dom/src/client/flight/ReactFlightDOMClient.js create mode 100644 packages/react-dom/src/client/flight/ReactFlightDOMHostConfig.js create mode 100644 packages/react-dom/src/client/flight/__tests__/ReactFlightDOMBrowser-test.js create mode 100644 packages/react-dom/src/client/flight/__tests__/ReactFlightDOMNode-test.js rename packages/react-dom/src/server/{ReactDOMFizzServerFormatConfig.js => ReactDOMServerFormatConfig.js} (63%) create mode 100644 packages/react-dom/src/server/flight/ReactFlightDOMServerBrowser.js create mode 100644 packages/react-dom/src/server/flight/ReactFlightDOMServerNode.js create mode 100644 packages/react-dom/src/server/flight/__tests__/ReactFlightDOMBrowser-test.js create mode 100644 packages/react-dom/src/server/flight/__tests__/ReactFlightDOMNode-test.js create mode 100644 packages/react-dom/unstable-flight-client.js create mode 100644 packages/react-dom/unstable-flight-server.browser.js create mode 100644 packages/react-dom/unstable-flight-server.js create mode 100644 packages/react-dom/unstable-flight-server.node.js create mode 100644 packages/react-flight/README.md create mode 100644 packages/react-flight/index.js create mode 100644 packages/react-flight/inline-typed.js create mode 100644 packages/react-flight/inline.dom-browser.js create mode 100644 packages/react-flight/inline.dom.js create mode 100644 packages/react-flight/npm/index.js create mode 100644 packages/react-flight/package.json create mode 100644 packages/react-flight/src/ReactFlightClient.js rename packages/{react-stream/src/ReactFizzFormatConfig.js => react-flight/src/ReactFlightClientHostConfig.js} (100%) create mode 100644 packages/react-flight/src/__tests__/ReactFlightClient-test.js create mode 100644 packages/react-flight/src/forks/ReactFlightClientHostConfig.custom.js create mode 100644 packages/react-flight/src/forks/ReactFlightClientHostConfig.dom-browser.js create mode 100644 packages/react-flight/src/forks/ReactFlightClientHostConfig.dom.js create mode 100644 packages/react-noop-renderer/flight-client.js create mode 100644 packages/react-noop-renderer/flight-server.js create mode 100644 packages/react-noop-renderer/npm/flight-client.js create mode 100644 packages/react-noop-renderer/npm/flight-server.js create mode 100644 packages/react-noop-renderer/src/ReactNoopFlightClient.js create mode 100644 packages/react-noop-renderer/src/ReactNoopFlightServer.js create mode 100644 packages/react-server/README.md create mode 100644 packages/react-server/flight.inline-typed.js create mode 100644 packages/react-server/flight.inline.dom-browser.js create mode 100644 packages/react-server/flight.inline.dom.js create mode 100644 packages/react-server/flight.js rename packages/{react-stream => react-server}/index.js (83%) rename packages/{react-stream => react-server}/inline-typed.js (93%) rename packages/{react-stream => react-server}/inline.dom-browser.js (100%) rename packages/{react-stream => react-server}/inline.dom.js (100%) create mode 100644 packages/react-server/npm/flight.js create mode 100644 packages/react-server/npm/index.js rename packages/{react-stream => react-server}/package.json (89%) rename packages/{react-stream => react-server}/src/ReactFizzStreamer.js (92%) create mode 100644 packages/react-server/src/ReactFlightStreamer.js rename packages/{react-stream/src/ReactFizzHostConfig.js => react-server/src/ReactServerFormatConfig.js} (100%) create mode 100644 packages/react-server/src/ReactServerHostConfig.js rename packages/{react-stream/src/ReactFizzHostConfigBrowser.js => react-server/src/ReactServerHostConfigBrowser.js} (100%) rename packages/{react-stream/src/ReactFizzHostConfigNode.js => react-server/src/ReactServerHostConfigNode.js} (100%) create mode 100644 packages/react-server/src/__tests__/ReactFlightServer-test.js rename packages/{react-stream => react-server}/src/__tests__/ReactServer-test.js (100%) rename packages/{react-stream/src/forks/ReactFizzFormatConfig.custom.js => react-server/src/forks/ReactServerFormatConfig.custom.js} (73%) rename packages/{react-stream/src/forks/ReactFizzFormatConfig.dom-browser.js => react-server/src/forks/ReactServerFormatConfig.dom-browser.js} (74%) rename packages/{react-stream/src/forks/ReactFizzFormatConfig.dom.js => react-server/src/forks/ReactServerFormatConfig.dom.js} (74%) rename packages/{react-stream/src/forks/ReactFizzHostConfig.custom.js => react-server/src/forks/ReactServerHostConfig.custom.js} (84%) rename packages/{react-stream/src/forks/ReactFizzHostConfig.dom-browser.js => react-server/src/forks/ReactServerHostConfig.dom-browser.js} (80%) rename packages/{react-stream/src/forks/ReactFizzHostConfig.dom.js => react-server/src/forks/ReactServerHostConfig.dom.js} (81%) delete mode 100644 packages/react-stream/README.md delete mode 100644 packages/react-stream/npm/index.js diff --git a/fixtures/flight-browser/index.html b/fixtures/flight-browser/index.html new file mode 100644 index 0000000000000..5f522a9b3a1a0 --- /dev/null +++ b/fixtures/flight-browser/index.html @@ -0,0 +1,84 @@ + + + + + Flight Example + + +

Flight Example

+
+

+ To install React, follow the instructions on + GitHub. +

+

+ If you can see this, React is not working right. + If you checked out the source from GitHub make sure to run npm run build. +

+
+ + + + + + + diff --git a/packages/react-dom/npm/unstable-flight-client.js b/packages/react-dom/npm/unstable-flight-client.js new file mode 100644 index 0000000000000..9116c361ebf74 --- /dev/null +++ b/packages/react-dom/npm/unstable-flight-client.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-dom-unstable-flight-client.production.min.js'); +} else { + module.exports = require('./cjs/react-dom-unstable-flight-client.development.js'); +} diff --git a/packages/react-dom/npm/unstable-flight-server.browser.js b/packages/react-dom/npm/unstable-flight-server.browser.js new file mode 100644 index 0000000000000..8a6b19b7ab1cf --- /dev/null +++ b/packages/react-dom/npm/unstable-flight-server.browser.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-dom-unstable-flight-server.browser.production.min.js'); +} else { + module.exports = require('./cjs/react-dom-unstable-flight-server.browser.development.js'); +} diff --git a/packages/react-dom/npm/unstable-flight-server.js b/packages/react-dom/npm/unstable-flight-server.js new file mode 100644 index 0000000000000..4b75a8eef3b45 --- /dev/null +++ b/packages/react-dom/npm/unstable-flight-server.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./unstable-flight-server.node'); diff --git a/packages/react-dom/npm/unstable-flight-server.node.js b/packages/react-dom/npm/unstable-flight-server.node.js new file mode 100644 index 0000000000000..30b967fd425da --- /dev/null +++ b/packages/react-dom/npm/unstable-flight-server.node.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-dom-unstable-flight-server.node.production.min.js'); +} else { + module.exports = require('./cjs/react-dom-unstable-flight-server.node.development.js'); +} diff --git a/packages/react-dom/package.json b/packages/react-dom/package.json index e05cac2ff5103..b413c885be8d0 100644 --- a/packages/react-dom/package.json +++ b/packages/react-dom/package.json @@ -39,13 +39,17 @@ "unstable-fizz.js", "unstable-fizz.browser.js", "unstable-fizz.node.js", + "unstable-flight-server.js", + "unstable-flight-server.browser.js", + "unstable-flight-server.node.js", "unstable-native-dependencies.js", "cjs/", "umd/" ], "browser": { "./server.js": "./server.browser.js", - "./unstable-fizz.js": "./unstable-fizz.browser.js" + "./unstable-fizz.js": "./unstable-fizz.browser.js", + "./unstable-flight-server.js": "./unstable-flight-server.browser.js" }, "browserify": { "transform": [ diff --git a/packages/react-dom/src/client/flight/ReactFlightDOMClient.js b/packages/react-dom/src/client/flight/ReactFlightDOMClient.js new file mode 100644 index 0000000000000..18bc4753a5294 --- /dev/null +++ b/packages/react-dom/src/client/flight/ReactFlightDOMClient.js @@ -0,0 +1,34 @@ +/** + * 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. + * + * @flow + */ + +import type {ReactModel} from 'react-flight/src/ReactFlightClient'; + +import { + createRequest, + startWork, + startFlowing, +} from 'react-flight/inline.dom-browser'; + +function renderToReadableStream(model: ReactModel): ReadableStream { + let request; + return new ReadableStream({ + start(controller) { + request = createRequest(model, controller); + startWork(request); + }, + pull(controller) { + startFlowing(request, controller.desiredSize); + }, + cancel(reason) {}, + }); +} + +export default { + renderToReadableStream, +}; diff --git a/packages/react-dom/src/client/flight/ReactFlightDOMHostConfig.js b/packages/react-dom/src/client/flight/ReactFlightDOMHostConfig.js new file mode 100644 index 0000000000000..9dde4993ad6ad --- /dev/null +++ b/packages/react-dom/src/client/flight/ReactFlightDOMHostConfig.js @@ -0,0 +1,50 @@ +/** + * 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. + * + * @flow + */ + +export type Destination = ReadableStreamController; + +export function scheduleWork(callback: () => void) { + callback(); +} + +export function flushBuffered(destination: Destination) { + // WHATWG Streams do not yet have a way to flush the underlying + // transform streams. https://github.com/whatwg/streams/issues/960 +} + +export function beginWriting(destination: Destination) {} + +export function writeChunk(destination: Destination, buffer: Uint8Array) { + destination.enqueue(buffer); +} + +export function completeWriting(destination: Destination) {} + +export function close(destination: Destination) { + destination.close(); +} + +const textEncoder = new TextEncoder(); + +export function convertStringToBuffer(content: string): Uint8Array { + return textEncoder.encode(content); +} + +export function formatChunkAsString(type: string, props: Object): string { + let str = '<' + type + '>'; + if (typeof props.children === 'string') { + str += props.children; + } + str += ''; + return str; +} + +export function formatChunk(type: string, props: Object): Uint8Array { + return convertStringToBuffer(formatChunkAsString(type, props)); +} diff --git a/packages/react-dom/src/client/flight/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-dom/src/client/flight/__tests__/ReactFlightDOMBrowser-test.js new file mode 100644 index 0000000000000..39b0956553558 --- /dev/null +++ b/packages/react-dom/src/client/flight/__tests__/ReactFlightDOMBrowser-test.js @@ -0,0 +1,61 @@ +/** + * 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 + */ + +'use strict'; + +// Polyfills for test environment +global.ReadableStream = require('@mattiasbuelens/web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; + +let React; +let ReactFlightDOMServer; + +describe('ReactFlightDOM', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactFlightDOMServer = require('react-dom/unstable-flight-server.browser'); + }); + + async function readResult(stream) { + let reader = stream.getReader(); + let result = ''; + while (true) { + let {done, value} = await reader.read(); + if (done) { + return result; + } + result += Buffer.from(value).toString('utf8'); + } + } + + it('should resolve HTML', async () => { + function Text({children}) { + return {children}; + } + function HTML() { + return ( +
+ hello + world +
+ ); + } + + let model = { + html: , + }; + let stream = ReactFlightDOMServer.renderToReadableStream(model); + jest.runAllTimers(); + let result = JSON.parse(await readResult(stream)); + expect(result).toEqual({ + html: '
helloworld
', + }); + }); +}); diff --git a/packages/react-dom/src/client/flight/__tests__/ReactFlightDOMNode-test.js b/packages/react-dom/src/client/flight/__tests__/ReactFlightDOMNode-test.js new file mode 100644 index 0000000000000..79494a49164eb --- /dev/null +++ b/packages/react-dom/src/client/flight/__tests__/ReactFlightDOMNode-test.js @@ -0,0 +1,57 @@ +/** + * 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/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index 17da2d7e48e87..649bf3bfad46f 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -13,7 +13,7 @@ import { createRequest, startWork, startFlowing, -} from 'react-stream/inline.dom-browser'; +} from 'react-server/inline.dom-browser'; function renderToReadableStream(children: ReactNodeList): ReadableStream { let request; diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index 72b1bcb74272c..0a6c4ba65c588 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -10,7 +10,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {Writable} from 'stream'; -import {createRequest, startWork, startFlowing} from 'react-stream/inline.dom'; +import {createRequest, startWork, startFlowing} from 'react-server/inline.dom'; function createDrainHandler(destination, request) { return () => startFlowing(request, 0); diff --git a/packages/react-dom/src/server/ReactDOMFizzServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js similarity index 63% rename from packages/react-dom/src/server/ReactDOMFizzServerFormatConfig.js rename to packages/react-dom/src/server/ReactDOMServerFormatConfig.js index a9c6dc79df830..d20d0119b87ca 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -7,13 +7,17 @@ * @flow */ -import {convertStringToBuffer} from 'react-stream/src/ReactFizzHostConfig'; +import {convertStringToBuffer} from 'react-server/src/ReactServerHostConfig'; -export function formatChunk(type: string, props: Object): Uint8Array { +export function formatChunkAsString(type: string, props: Object): string { let str = '<' + type + '>'; if (typeof props.children === 'string') { str += props.children; } str += ''; - return convertStringToBuffer(str); + return str; +} + +export function formatChunk(type: string, props: Object): Uint8Array { + return convertStringToBuffer(formatChunkAsString(type, props)); } diff --git a/packages/react-dom/src/server/flight/ReactFlightDOMServerBrowser.js b/packages/react-dom/src/server/flight/ReactFlightDOMServerBrowser.js new file mode 100644 index 0000000000000..39777cdba4050 --- /dev/null +++ b/packages/react-dom/src/server/flight/ReactFlightDOMServerBrowser.js @@ -0,0 +1,34 @@ +/** + * 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. + * + * @flow + */ + +import type {ReactModel} from 'react-server/flight.inline-typed'; + +import { + createRequest, + startWork, + startFlowing, +} from 'react-server/flight.inline.dom-browser'; + +function renderToReadableStream(model: ReactModel): ReadableStream { + let request; + return new ReadableStream({ + start(controller) { + request = createRequest(model, controller); + startWork(request); + }, + pull(controller) { + startFlowing(request, controller.desiredSize); + }, + cancel(reason) {}, + }); +} + +export default { + renderToReadableStream, +}; diff --git a/packages/react-dom/src/server/flight/ReactFlightDOMServerNode.js b/packages/react-dom/src/server/flight/ReactFlightDOMServerNode.js new file mode 100644 index 0000000000000..1ca6255204caa --- /dev/null +++ b/packages/react-dom/src/server/flight/ReactFlightDOMServerNode.js @@ -0,0 +1,31 @@ +/** + * 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. + * + * @flow + */ + +import type {ReactModel} from 'react-server/src/ReactFlightStreamer'; +import type {Writable} from 'stream'; + +import { + createRequest, + startWork, + startFlowing, +} from 'react-server/flight.inline.dom'; + +function createDrainHandler(destination, request) { + return () => startFlowing(request, 0); +} + +function pipeToNodeWritable(model: ReactModel, destination: Writable): void { + let request = createRequest(model, destination); + destination.on('drain', createDrainHandler(destination, request)); + startWork(request); +} + +export default { + pipeToNodeWritable, +}; diff --git a/packages/react-dom/src/server/flight/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-dom/src/server/flight/__tests__/ReactFlightDOMBrowser-test.js new file mode 100644 index 0000000000000..39b0956553558 --- /dev/null +++ b/packages/react-dom/src/server/flight/__tests__/ReactFlightDOMBrowser-test.js @@ -0,0 +1,61 @@ +/** + * 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 + */ + +'use strict'; + +// Polyfills for test environment +global.ReadableStream = require('@mattiasbuelens/web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; + +let React; +let ReactFlightDOMServer; + +describe('ReactFlightDOM', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactFlightDOMServer = require('react-dom/unstable-flight-server.browser'); + }); + + async function readResult(stream) { + let reader = stream.getReader(); + let result = ''; + while (true) { + let {done, value} = await reader.read(); + if (done) { + return result; + } + result += Buffer.from(value).toString('utf8'); + } + } + + it('should resolve HTML', async () => { + function Text({children}) { + return {children}; + } + function HTML() { + return ( +
+ hello + world +
+ ); + } + + let model = { + html: , + }; + let stream = ReactFlightDOMServer.renderToReadableStream(model); + jest.runAllTimers(); + let result = JSON.parse(await readResult(stream)); + expect(result).toEqual({ + html: '
helloworld
', + }); + }); +}); diff --git a/packages/react-dom/src/server/flight/__tests__/ReactFlightDOMNode-test.js b/packages/react-dom/src/server/flight/__tests__/ReactFlightDOMNode-test.js new file mode 100644 index 0000000000000..79494a49164eb --- /dev/null +++ b/packages/react-dom/src/server/flight/__tests__/ReactFlightDOMNode-test.js @@ -0,0 +1,57 @@ +/** + * 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/unstable-flight-client.js b/packages/react-dom/unstable-flight-client.js new file mode 100644 index 0000000000000..7aeff31bb74dc --- /dev/null +++ b/packages/react-dom/unstable-flight-client.js @@ -0,0 +1,16 @@ +/** + * 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. + * + * @flow + */ + +'use strict'; + +const ReactFlightDOMClient = require('./src/client/flight/ReactFlightDOMClient'); + +// TODO: decide on the top-level export form. +// This is hacky but makes it work with both Rollup and Jest +module.exports = ReactFlightDOMClient.default || ReactFlightDOMClient; diff --git a/packages/react-dom/unstable-flight-server.browser.js b/packages/react-dom/unstable-flight-server.browser.js new file mode 100644 index 0000000000000..794459f5dc5a8 --- /dev/null +++ b/packages/react-dom/unstable-flight-server.browser.js @@ -0,0 +1,17 @@ +/** + * 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. + * + * @flow + */ + +'use strict'; + +const ReactFlightDOMServerBrowser = require('./src/server/flight/ReactFlightDOMServerBrowser'); + +// TODO: decide on the top-level export form. +// This is hacky but makes it work with both Rollup and Jest +module.exports = + ReactFlightDOMServerBrowser.default || ReactFlightDOMServerBrowser; diff --git a/packages/react-dom/unstable-flight-server.js b/packages/react-dom/unstable-flight-server.js new file mode 100644 index 0000000000000..e4f1a3606c96c --- /dev/null +++ b/packages/react-dom/unstable-flight-server.js @@ -0,0 +1,12 @@ +/** + * 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. + * + * @flow + */ + +'use strict'; + +module.exports = require('./unstable-flight-server.node'); diff --git a/packages/react-dom/unstable-flight-server.node.js b/packages/react-dom/unstable-flight-server.node.js new file mode 100644 index 0000000000000..9466778779659 --- /dev/null +++ b/packages/react-dom/unstable-flight-server.node.js @@ -0,0 +1,16 @@ +/** + * 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. + * + * @flow + */ + +'use strict'; + +const ReactFlightDOMServerNode = require('./src/server/flight/ReactFlightDOMServerNode'); + +// TODO: decide on the top-level export form. +// This is hacky but makes it work with both Rollup and Jest +module.exports = ReactFlightDOMServerNode.default || ReactFlightDOMServerNode; diff --git a/packages/react-flight/README.md b/packages/react-flight/README.md new file mode 100644 index 0000000000000..3f4c4b5a81860 --- /dev/null +++ b/packages/react-flight/README.md @@ -0,0 +1,7 @@ +# react-flight + +This is an experimental package for consuming custom React streaming models. + +**Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.** + +**Use it at your own risk.** diff --git a/packages/react-flight/index.js b/packages/react-flight/index.js new file mode 100644 index 0000000000000..7abd9455bd0aa --- /dev/null +++ b/packages/react-flight/index.js @@ -0,0 +1,26 @@ +/** + * 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. + */ + +// This entry point is intentionally not typed. It exists only for third-party +// renderers. The renderers we ship (such as React DOM) instead import a named +// "inline" entry point (for example, `react-server/inline.dom`). It uses +// the same code, but the Flow configuration redirects the host config to its +// real implementation so we can check it against exact intended host types. +// +// Only one renderer (the one you passed to `yarn flow `) is fully +// type-checked at any given time. The Flow config maps the +// `react-server/inline.` import (which is *not* Flow typed) to +// `react-server/inline-typed` (which *is*) for the current renderer. +// On CI, we run Flow checks for each renderer separately. + +'use strict'; + +const ReactFlightClient = require('./src/ReactFlightClient'); + +// TODO: decide on the top-level export form. +// This is hacky but makes it work with both Rollup and Jest. +module.exports = ReactFlightClient.default || ReactFlightClient; diff --git a/packages/react-flight/inline-typed.js b/packages/react-flight/inline-typed.js new file mode 100644 index 0000000000000..70a74cb38eb4f --- /dev/null +++ b/packages/react-flight/inline-typed.js @@ -0,0 +1,24 @@ +/** + * 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. + * + * @flow + */ + +// This file must have the Flow annotation. +// +// This is the Flow-typed entry point for the renderer. It should not be +// imported directly in code. Instead, our Flow configuration uses this entry +// point for the currently checked renderer (the one you passed to `yarn flow`). +// +// For example, if you run `yarn flow dom`, `react-server/inline.dom` points +// to this module (and thus will be considered Flow-typed). But other renderers +// (e.g. `react-test-renderer`) will see stream as untyped during the check. +// +// We can't make all entry points typed at the same time because different +// renderers have different host config types. So we check them one by one. +// We run Flow on all renderers on CI. + +export * from './src/ReactFlightClient'; diff --git a/packages/react-flight/inline.dom-browser.js b/packages/react-flight/inline.dom-browser.js new file mode 100644 index 0000000000000..c42c00289e3a2 --- /dev/null +++ b/packages/react-flight/inline.dom-browser.js @@ -0,0 +1,11 @@ +/** + * 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. + */ + +// This file intentionally does *not* have the Flow annotation. +// Don't add it. See `./inline-typed.js` for an explanation. + +export * from './src/ReactFlightClient'; diff --git a/packages/react-flight/inline.dom.js b/packages/react-flight/inline.dom.js new file mode 100644 index 0000000000000..c42c00289e3a2 --- /dev/null +++ b/packages/react-flight/inline.dom.js @@ -0,0 +1,11 @@ +/** + * 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. + */ + +// This file intentionally does *not* have the Flow annotation. +// Don't add it. See `./inline-typed.js` for an explanation. + +export * from './src/ReactFlightClient'; diff --git a/packages/react-flight/npm/index.js b/packages/react-flight/npm/index.js new file mode 100644 index 0000000000000..1c9cdd060304a --- /dev/null +++ b/packages/react-flight/npm/index.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-flight.production.min.js'); +} else { + module.exports = require('./cjs/react-flight.development.js'); +} diff --git a/packages/react-flight/package.json b/packages/react-flight/package.json new file mode 100644 index 0000000000000..e6eabd3c4b02e --- /dev/null +++ b/packages/react-flight/package.json @@ -0,0 +1,40 @@ +{ + "name": "react-flight", + "description": "React package for consuming streaming models.", + "version": "0.1.0", + "private": true, + "keywords": [ + "react" + ], + "homepage": "https://reactjs.org/", + "bugs": "https://github.com/facebook/react/issues", + "license": "MIT", + "files": [ + "LICENSE", + "README.md", + "index.js", + "cjs/" + ], + "main": "index.js", + "repository": { + "type" : "git", + "url" : "https://github.com/facebook/react.git", + "directory": "packages/react-flight" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^16.0.0" + }, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "browserify": { + "transform": [ + "loose-envify" + ] + } +} diff --git a/packages/react-flight/src/ReactFlightClient.js b/packages/react-flight/src/ReactFlightClient.js new file mode 100644 index 0000000000000..bc0c47a3adf4e --- /dev/null +++ b/packages/react-flight/src/ReactFlightClient.js @@ -0,0 +1,144 @@ +/** + * 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. + * + * @flow + */ + +import type {Destination} from './ReactFlightClientHostConfig'; + +import { + scheduleWork, + beginWriting, + writeChunk, + completeWriting, + flushBuffered, + close, + convertStringToBuffer, + formatChunkAsString, +} from './ReactFlightClientHostConfig'; +import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; + +export type ReactModel = + | React$Element + | string + | boolean + | number + | null + | Iterable + | ReactModelObject; + +type ReactJSONValue = + | string + | boolean + | number + | null + | Array + | ReactModelObject; + +type ReactModelObject = { + +[key: string]: ReactModel, +}; + +type OpaqueRequest = { + destination: Destination, + model: ReactModel, + completedChunks: Array, + flowing: boolean, +}; + +export function createRequest( + model: ReactModel, + destination: Destination, +): OpaqueRequest { + 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); + let type = element.type; + let props = element.props; + if (typeof type === 'function') { + // This is a nested view model. + value = type(props); + continue; + } else if (typeof type === 'string') { + // This is a host element. E.g. HTML. + return resolveElementToHostFormat(type, props); + } else { + throw new Error('Unsupported type.'); + } + } + return value; +} + +function performWork(request: OpaqueRequest): void { + let rootModel = request.model; + request.model = null; + let json = JSON.stringify(rootModel, resolveModelToJSON); + request.completedChunks.push(convertStringToBuffer(json)); + if (request.flowing) { + flushCompletedChunks(request); + } + + flushBuffered(request.destination); +} + +function flushCompletedChunks(request: OpaqueRequest) { + let destination = request.destination; + let chunks = request.completedChunks; + request.completedChunks = []; + + beginWriting(destination); + try { + for (let i = 0; i < chunks.length; i++) { + let chunk = chunks[i]; + writeChunk(destination, chunk); + } + } finally { + completeWriting(destination); + } + close(destination); +} + +export function startWork(request: OpaqueRequest): void { + request.flowing = true; + scheduleWork(() => performWork(request)); +} + +export function startFlowing( + request: OpaqueRequest, + desiredBytes: number, +): void { + request.flowing = false; + flushCompletedChunks(request); +} diff --git a/packages/react-stream/src/ReactFizzFormatConfig.js b/packages/react-flight/src/ReactFlightClientHostConfig.js similarity index 100% rename from packages/react-stream/src/ReactFizzFormatConfig.js rename to packages/react-flight/src/ReactFlightClientHostConfig.js diff --git a/packages/react-flight/src/__tests__/ReactFlightClient-test.js b/packages/react-flight/src/__tests__/ReactFlightClient-test.js new file mode 100644 index 0000000000000..a020dc8b97d94 --- /dev/null +++ b/packages/react-flight/src/__tests__/ReactFlightClient-test.js @@ -0,0 +1,38 @@ +/** + * 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 React; +let ReactNoopFlightClient; + +describe('ReactFlightClient', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactNoopFlightClient = require('react-noop-renderer/flight-client'); + }); + + it('can resolve a model', () => { + function Bar({text}) { + return text.toUpperCase(); + } + function Foo() { + return { + bar: [, ], + }; + } + let result = ReactNoopFlightClient.render({ + foo: , + }); + expect(result).toEqual([{foo: {bar: ['A', 'B']}}]); + }); +}); diff --git a/packages/react-flight/src/forks/ReactFlightClientHostConfig.custom.js b/packages/react-flight/src/forks/ReactFlightClientHostConfig.custom.js new file mode 100644 index 0000000000000..1e0fbd5738e74 --- /dev/null +++ b/packages/react-flight/src/forks/ReactFlightClientHostConfig.custom.js @@ -0,0 +1,37 @@ +/** + * 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. + * + * @flow + */ + +// This is a host config that's used for the `react-server` package on npm. +// It is only used by third-party renderers. +// +// Its API lets you pass the host config as an argument. +// However, inside the `react-server` we treat host config as a module. +// This file is a shim between two worlds. +// +// It works because the `react-server` bundle is wrapped in something like: +// +// module.exports = function ($$$config) { +// /* renderer code */ +// } +// +// So `$$$config` looks like a global variable, but it's +// really an argument to a top-level wrapping function. + +declare var $$$hostConfig: any; +export opaque type Destination = mixed; // eslint-disable-line no-undef + +export const formatChunkAsString = $$$hostConfig.formatChunkAsString; +export const formatChunk = $$$hostConfig.formatChunk; +export const scheduleWork = $$$hostConfig.scheduleWork; +export const beginWriting = $$$hostConfig.beginWriting; +export const writeChunk = $$$hostConfig.writeChunk; +export const completeWriting = $$$hostConfig.completeWriting; +export const flushBuffered = $$$hostConfig.flushBuffered; +export const close = $$$hostConfig.close; +export const convertStringToBuffer = $$$hostConfig.convertStringToBuffer; diff --git a/packages/react-flight/src/forks/ReactFlightClientHostConfig.dom-browser.js b/packages/react-flight/src/forks/ReactFlightClientHostConfig.dom-browser.js new file mode 100644 index 0000000000000..e768f225d6b0f --- /dev/null +++ b/packages/react-flight/src/forks/ReactFlightClientHostConfig.dom-browser.js @@ -0,0 +1,10 @@ +/** + * 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. + * + * @flow + */ + +export * from 'react-dom/src/client/flight/ReactFlightDOMHostConfig'; diff --git a/packages/react-flight/src/forks/ReactFlightClientHostConfig.dom.js b/packages/react-flight/src/forks/ReactFlightClientHostConfig.dom.js new file mode 100644 index 0000000000000..e768f225d6b0f --- /dev/null +++ b/packages/react-flight/src/forks/ReactFlightClientHostConfig.dom.js @@ -0,0 +1,10 @@ +/** + * 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. + * + * @flow + */ + +export * from 'react-dom/src/client/flight/ReactFlightDOMHostConfig'; diff --git a/packages/react-noop-renderer/flight-client.js b/packages/react-noop-renderer/flight-client.js new file mode 100644 index 0000000000000..dd5e879335d83 --- /dev/null +++ b/packages/react-noop-renderer/flight-client.js @@ -0,0 +1,16 @@ +/** + * 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. + * + * @flow + */ + +'use strict'; + +const ReactNoopFlightClient = require('./src/ReactNoopFlightClient'); + +// TODO: decide on the top-level export form. +// This is hacky but makes it work with both Rollup and Jest. +module.exports = ReactNoopFlightClient.default || ReactNoopFlightClient; diff --git a/packages/react-noop-renderer/flight-server.js b/packages/react-noop-renderer/flight-server.js new file mode 100644 index 0000000000000..2ba00ffe78285 --- /dev/null +++ b/packages/react-noop-renderer/flight-server.js @@ -0,0 +1,16 @@ +/** + * 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. + * + * @flow + */ + +'use strict'; + +const ReactNoopFlightServer = require('./src/ReactNoopFlightServer'); + +// TODO: decide on the top-level export form. +// This is hacky but makes it work with both Rollup and Jest. +module.exports = ReactNoopFlightServer.default || ReactNoopFlightServer; diff --git a/packages/react-noop-renderer/npm/flight-client.js b/packages/react-noop-renderer/npm/flight-client.js new file mode 100644 index 0000000000000..d557e5036036a --- /dev/null +++ b/packages/react-noop-renderer/npm/flight-client.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-noop-renderer-flight-client.production.min.js'); +} else { + module.exports = require('./cjs/react-noop-renderer-flight-client.development.js'); +} diff --git a/packages/react-noop-renderer/npm/flight-server.js b/packages/react-noop-renderer/npm/flight-server.js new file mode 100644 index 0000000000000..de627385e808f --- /dev/null +++ b/packages/react-noop-renderer/npm/flight-server.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-noop-renderer-flight-server.production.min.js'); +} else { + module.exports = require('./cjs/react-noop-renderer-flight-server.development.js'); +} diff --git a/packages/react-noop-renderer/package.json b/packages/react-noop-renderer/package.json index 33823aaa4232c..0f95472ae41e5 100644 --- a/packages/react-noop-renderer/package.json +++ b/packages/react-noop-renderer/package.json @@ -2,7 +2,7 @@ "name": "react-noop-renderer", "version": "16.0.0", "private": true, - "description": "React package for testing the Fiber reconciler.", + "description": "React package for testing the Fiber, Fizz and Flight reconcilers.", "main": "index.js", "repository": { "type" : "git", @@ -15,7 +15,8 @@ "prop-types": "^15.6.2", "regenerator-runtime": "^0.11.0", "react-reconciler": "*", - "react-stream": "*" + "react-flight": "*", + "react-server": "*" }, "peerDependencies": { "react": "^16.0.0" @@ -27,6 +28,8 @@ "index.js", "persistent.js", "server.js", + "flight-client.js", + "flight-server.js", "cjs/" ] } diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js new file mode 100644 index 0000000000000..44b41cd7f788c --- /dev/null +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -0,0 +1,54 @@ +/** + * 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. + * + * @flow + */ + +/** + * This is a renderer of React that doesn't have a render target output. + * It is useful to demonstrate the internals of the reconciler in isolation + * and for testing semantics of reconciliation separate from the host + * environment. + */ + +import type {ReactModel} from 'react-flight/inline-typed'; + +import ReactFlightClient from 'react-flight'; + +type Destination = Array; + +const ReactNoopFlightClient = ReactFlightClient({ + scheduleWork(callback: () => void) { + callback(); + }, + beginWriting(destination: Destination): void {}, + writeChunk(destination: Destination, buffer: Uint8Array): void { + destination.push(JSON.parse(Buffer.from((buffer: any)).toString('utf8'))); + }, + completeWriting(destination: Destination): void {}, + close(destination: Destination): void {}, + flushBuffered(destination: Destination): void {}, + convertStringToBuffer(content: string): Uint8Array { + return Buffer.from(content, 'utf8'); + }, + formatChunkAsString(type: string, props: Object): string { + return JSON.stringify({type, props}); + }, + formatChunk(type: string, props: Object): Uint8Array { + return Buffer.from(JSON.stringify({type, props}), 'utf8'); + }, +}); + +function render(model: ReactModel): Destination { + let destination: Destination = []; + let request = ReactNoopFlightClient.createRequest(model, destination); + ReactNoopFlightClient.startWork(request); + return destination; +} + +export default { + render, +}; diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js new file mode 100644 index 0000000000000..569c45e62c154 --- /dev/null +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -0,0 +1,54 @@ +/** + * 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. + * + * @flow + */ + +/** + * This is a renderer of React that doesn't have a render target output. + * It is useful to demonstrate the internals of the reconciler in isolation + * and for testing semantics of reconciliation separate from the host + * environment. + */ + +import type {ReactModel} from 'react-server/flight.inline-typed'; + +import ReactFlightStreamer from 'react-server/flight'; + +type Destination = Array; + +const ReactNoopFlightServer = ReactFlightStreamer({ + scheduleWork(callback: () => void) { + callback(); + }, + beginWriting(destination: Destination): void {}, + writeChunk(destination: Destination, buffer: Uint8Array): void { + destination.push(JSON.parse(Buffer.from((buffer: any)).toString('utf8'))); + }, + completeWriting(destination: Destination): void {}, + close(destination: Destination): void {}, + flushBuffered(destination: Destination): void {}, + convertStringToBuffer(content: string): Uint8Array { + return Buffer.from(content, 'utf8'); + }, + formatChunkAsString(type: string, props: Object): string { + return JSON.stringify({type, props}); + }, + formatChunk(type: string, props: Object): Uint8Array { + return Buffer.from(JSON.stringify({type, props}), 'utf8'); + }, +}); + +function render(model: ReactModel): Destination { + let destination: Destination = []; + let request = ReactNoopFlightServer.createRequest(model, destination); + ReactNoopFlightServer.startWork(request); + return destination; +} + +export default { + render, +}; diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js index 2077687b9f9b1..0fe23437ce8de 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -14,7 +14,7 @@ * environment. */ -import ReactFizzStreamer from 'react-stream'; +import ReactFizzStreamer from 'react-server'; type Destination = Array; @@ -32,6 +32,9 @@ const ReactNoopServer = ReactFizzStreamer({ convertStringToBuffer(content: string): Uint8Array { return Buffer.from(content, 'utf8'); }, + formatChunkAsString(type: string, props: Object): string { + return JSON.stringify({type, props}); + }, formatChunk(type: string, props: Object): Uint8Array { return Buffer.from(JSON.stringify({type, props}), 'utf8'); }, diff --git a/packages/react-server/README.md b/packages/react-server/README.md new file mode 100644 index 0000000000000..2e5808da7581f --- /dev/null +++ b/packages/react-server/README.md @@ -0,0 +1,7 @@ +# react-server + +This is an experimental package for creating custom React streaming server renderers. + +**Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.** + +**Use it at your own risk.** diff --git a/packages/react-server/flight.inline-typed.js b/packages/react-server/flight.inline-typed.js new file mode 100644 index 0000000000000..7f435c945c3c1 --- /dev/null +++ b/packages/react-server/flight.inline-typed.js @@ -0,0 +1,24 @@ +/** + * 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. + * + * @flow + */ + +// This file must have the Flow annotation. +// +// This is the Flow-typed entry point for the renderer. It should not be +// imported directly in code. Instead, our Flow configuration uses this entry +// point for the currently checked renderer (the one you passed to `yarn flow`). +// +// For example, if you run `yarn flow dom`, `react-server/flight.inline.dom` points +// to this module (and thus will be considered Flow-typed). But other renderers +// (e.g. `react-test-renderer`) will see stream as untyped during the check. +// +// We can't make all entry points typed at the same time because different +// 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'; diff --git a/packages/react-server/flight.inline.dom-browser.js b/packages/react-server/flight.inline.dom-browser.js new file mode 100644 index 0000000000000..34b97ffff0533 --- /dev/null +++ b/packages/react-server/flight.inline.dom-browser.js @@ -0,0 +1,11 @@ +/** + * 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. + */ + +// This file intentionally does *not* have the Flow annotation. +// Don't add it. See `./inline-typed.js` for an explanation. + +export * from './src/ReactFlightStreamer'; diff --git a/packages/react-server/flight.inline.dom.js b/packages/react-server/flight.inline.dom.js new file mode 100644 index 0000000000000..34b97ffff0533 --- /dev/null +++ b/packages/react-server/flight.inline.dom.js @@ -0,0 +1,11 @@ +/** + * 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. + */ + +// This file intentionally does *not* have the Flow annotation. +// Don't add it. See `./inline-typed.js` for an explanation. + +export * from './src/ReactFlightStreamer'; diff --git a/packages/react-server/flight.js b/packages/react-server/flight.js new file mode 100644 index 0000000000000..b5f6cc0b9d263 --- /dev/null +++ b/packages/react-server/flight.js @@ -0,0 +1,26 @@ +/** + * 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. + */ + +// This entry point is intentionally not typed. It exists only for third-party +// renderers. The renderers we ship (such as React DOM) instead import a named +// "inline" entry point (for example, `react-server/flight.inline.dom`). It uses +// the same code, but the Flow configuration redirects the host config to its +// real implementation so we can check it against exact intended host types. +// +// Only one renderer (the one you passed to `yarn flow `) is fully +// type-checked at any given time. The Flow config maps the +// `react-server/flight.inline.` import (which is *not* Flow typed) to +// `react-server/flight.inline-typed` (which *is*) for the current renderer. +// On CI, we run Flow checks for each renderer separately. + +'use strict'; + +const ReactFlightStreamer = require('./src/ReactFlightStreamer'); + +// 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; diff --git a/packages/react-stream/index.js b/packages/react-server/index.js similarity index 83% rename from packages/react-stream/index.js rename to packages/react-server/index.js index 66445f67bf3eb..21657bf4e366a 100644 --- a/packages/react-stream/index.js +++ b/packages/react-server/index.js @@ -7,14 +7,14 @@ // This entry point is intentionally not typed. It exists only for third-party // renderers. The renderers we ship (such as React DOM) instead import a named -// "inline" entry point (for example, `react-stream/inline.dom`). It uses +// "inline" entry point (for example, `react-server/inline.dom`). It uses // the same code, but the Flow configuration redirects the host config to its // real implementation so we can check it against exact intended host types. // // Only one renderer (the one you passed to `yarn flow `) is fully // type-checked at any given time. The Flow config maps the -// `react-stream/inline.` import (which is *not* Flow typed) to -// `react-stream/inline-typed` (which *is*) for the current renderer. +// `react-server/inline.` import (which is *not* Flow typed) to +// `react-server/inline-typed` (which *is*) for the current renderer. // On CI, we run Flow checks for each renderer separately. 'use strict'; diff --git a/packages/react-stream/inline-typed.js b/packages/react-server/inline-typed.js similarity index 93% rename from packages/react-stream/inline-typed.js rename to packages/react-server/inline-typed.js index 32358e21e457b..27f5e86c80780 100644 --- a/packages/react-stream/inline-typed.js +++ b/packages/react-server/inline-typed.js @@ -13,7 +13,7 @@ // imported directly in code. Instead, our Flow configuration uses this entry // point for the currently checked renderer (the one you passed to `yarn flow`). // -// For example, if you run `yarn flow dom`, `react-stream/inline.dom` points +// For example, if you run `yarn flow dom`, `react-server/inline.dom` points // to this module (and thus will be considered Flow-typed). But other renderers // (e.g. `react-test-renderer`) will see stream as untyped during the check. // diff --git a/packages/react-stream/inline.dom-browser.js b/packages/react-server/inline.dom-browser.js similarity index 100% rename from packages/react-stream/inline.dom-browser.js rename to packages/react-server/inline.dom-browser.js diff --git a/packages/react-stream/inline.dom.js b/packages/react-server/inline.dom.js similarity index 100% rename from packages/react-stream/inline.dom.js rename to packages/react-server/inline.dom.js diff --git a/packages/react-server/npm/flight.js b/packages/react-server/npm/flight.js new file mode 100644 index 0000000000000..7e1d00ca14f13 --- /dev/null +++ b/packages/react-server/npm/flight.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-flight.production.min.js'); +} else { + module.exports = require('./cjs/react-server-flight.development.js'); +} diff --git a/packages/react-server/npm/index.js b/packages/react-server/npm/index.js new file mode 100644 index 0000000000000..e9f3d0324e3b9 --- /dev/null +++ b/packages/react-server/npm/index.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server.production.min.js'); +} else { + module.exports = require('./cjs/react-server.development.js'); +} diff --git a/packages/react-stream/package.json b/packages/react-server/package.json similarity index 89% rename from packages/react-stream/package.json rename to packages/react-server/package.json index f5fb76f3aecbc..77ff922fcd536 100644 --- a/packages/react-stream/package.json +++ b/packages/react-server/package.json @@ -1,5 +1,5 @@ { - "name": "react-stream", + "name": "react-server", "description": "React package for creating custom streaming server renderers.", "version": "0.1.0", "private": true, @@ -13,13 +13,14 @@ "LICENSE", "README.md", "index.js", + "flight.js", "cjs/" ], "main": "index.js", "repository": { "type" : "git", "url" : "https://github.com/facebook/react.git", - "directory": "packages/react-stream" + "directory": "packages/react-server" }, "engines": { "node": ">=0.10.0" diff --git a/packages/react-stream/src/ReactFizzStreamer.js b/packages/react-server/src/ReactFizzStreamer.js similarity index 92% rename from packages/react-stream/src/ReactFizzStreamer.js rename to packages/react-server/src/ReactFizzStreamer.js index 80239287c2160..d3ac8aaf3b452 100644 --- a/packages/react-stream/src/ReactFizzStreamer.js +++ b/packages/react-server/src/ReactFizzStreamer.js @@ -7,7 +7,7 @@ * @flow */ -import type {Destination} from './ReactFizzHostConfig'; +import type {Destination} from './ReactServerHostConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; import { @@ -17,8 +17,8 @@ import { completeWriting, flushBuffered, close, -} from './ReactFizzHostConfig'; -import {formatChunk} from './ReactFizzFormatConfig'; +} from './ReactServerHostConfig'; +import {formatChunk} from './ReactServerFormatConfig'; import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; type OpaqueRequest = { diff --git a/packages/react-server/src/ReactFlightStreamer.js b/packages/react-server/src/ReactFlightStreamer.js new file mode 100644 index 0000000000000..5642510100ed3 --- /dev/null +++ b/packages/react-server/src/ReactFlightStreamer.js @@ -0,0 +1,144 @@ +/** + * 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. + * + * @flow + */ + +import type {Destination} from './ReactServerHostConfig'; + +import { + scheduleWork, + beginWriting, + writeChunk, + completeWriting, + flushBuffered, + close, + convertStringToBuffer, +} from './ReactServerHostConfig'; +import {formatChunkAsString} from './ReactServerFormatConfig'; +import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; + +export type ReactModel = + | React$Element + | string + | boolean + | number + | null + | Iterable + | ReactModelObject; + +type ReactJSONValue = + | string + | boolean + | number + | null + | Array + | ReactModelObject; + +type ReactModelObject = { + +[key: string]: ReactModel, +}; + +type OpaqueRequest = { + destination: Destination, + model: ReactModel, + completedChunks: Array, + flowing: boolean, +}; + +export function createRequest( + model: ReactModel, + destination: Destination, +): OpaqueRequest { + 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); + let type = element.type; + let props = element.props; + if (typeof type === 'function') { + // This is a nested view model. + value = type(props); + continue; + } else if (typeof type === 'string') { + // This is a host element. E.g. HTML. + return resolveElementToHostFormat(type, props); + } else { + throw new Error('Unsupported type.'); + } + } + return value; +} + +function performWork(request: OpaqueRequest): void { + let rootModel = request.model; + request.model = null; + let json = JSON.stringify(rootModel, resolveModelToJSON); + request.completedChunks.push(convertStringToBuffer(json)); + if (request.flowing) { + flushCompletedChunks(request); + } + + flushBuffered(request.destination); +} + +function flushCompletedChunks(request: OpaqueRequest) { + let destination = request.destination; + let chunks = request.completedChunks; + request.completedChunks = []; + + beginWriting(destination); + try { + for (let i = 0; i < chunks.length; i++) { + let chunk = chunks[i]; + writeChunk(destination, chunk); + } + } finally { + completeWriting(destination); + } + close(destination); +} + +export function startWork(request: OpaqueRequest): void { + request.flowing = true; + scheduleWork(() => performWork(request)); +} + +export function startFlowing( + request: OpaqueRequest, + desiredBytes: number, +): void { + request.flowing = false; + flushCompletedChunks(request); +} diff --git a/packages/react-stream/src/ReactFizzHostConfig.js b/packages/react-server/src/ReactServerFormatConfig.js similarity index 100% rename from packages/react-stream/src/ReactFizzHostConfig.js rename to packages/react-server/src/ReactServerFormatConfig.js diff --git a/packages/react-server/src/ReactServerHostConfig.js b/packages/react-server/src/ReactServerHostConfig.js new file mode 100644 index 0000000000000..6b4d9f24fcdc7 --- /dev/null +++ b/packages/react-server/src/ReactServerHostConfig.js @@ -0,0 +1,22 @@ +/** + * 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. + * + * @flow + */ + +/* eslint-disable react-internal/warning-and-invariant-args */ + +import invariant from 'shared/invariant'; + +// We expect that our Rollup, Jest, and Flow configurations +// always shim this module with the corresponding host config +// (either provided by a renderer, or a generic shim for npm). +// +// We should never resolve to this file, but it exists to make +// sure that if we *do* accidentally break the configuration, +// the failure isn't silent. + +invariant(false, 'This module must be shimmed by a specific renderer.'); diff --git a/packages/react-stream/src/ReactFizzHostConfigBrowser.js b/packages/react-server/src/ReactServerHostConfigBrowser.js similarity index 100% rename from packages/react-stream/src/ReactFizzHostConfigBrowser.js rename to packages/react-server/src/ReactServerHostConfigBrowser.js diff --git a/packages/react-stream/src/ReactFizzHostConfigNode.js b/packages/react-server/src/ReactServerHostConfigNode.js similarity index 100% rename from packages/react-stream/src/ReactFizzHostConfigNode.js rename to packages/react-server/src/ReactServerHostConfigNode.js diff --git a/packages/react-server/src/__tests__/ReactFlightServer-test.js b/packages/react-server/src/__tests__/ReactFlightServer-test.js new file mode 100644 index 0000000000000..afa8fdb4bc7e8 --- /dev/null +++ b/packages/react-server/src/__tests__/ReactFlightServer-test.js @@ -0,0 +1,38 @@ +/** + * 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 React; +let ReactNoopFlight; + +describe('ReactFlightServer', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactNoopFlight = require('react-noop-renderer/flight-server'); + }); + + it('can resolve a model', () => { + function Bar({text}) { + return text.toUpperCase(); + } + function Foo() { + return { + bar: [, ], + }; + } + let result = ReactNoopFlight.render({ + foo: , + }); + expect(result).toEqual([{foo: {bar: ['A', 'B']}}]); + }); +}); diff --git a/packages/react-stream/src/__tests__/ReactServer-test.js b/packages/react-server/src/__tests__/ReactServer-test.js similarity index 100% rename from packages/react-stream/src/__tests__/ReactServer-test.js rename to packages/react-server/src/__tests__/ReactServer-test.js diff --git a/packages/react-stream/src/forks/ReactFizzFormatConfig.custom.js b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js similarity index 73% rename from packages/react-stream/src/forks/ReactFizzFormatConfig.custom.js rename to packages/react-server/src/forks/ReactServerFormatConfig.custom.js index 9c4e181652265..f00ecbf2529aa 100644 --- a/packages/react-stream/src/forks/ReactFizzFormatConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js @@ -7,14 +7,14 @@ * @flow */ -// This is a host config that's used for the `react-stream` package on npm. +// This is a host config that's used for the `react-server` package on npm. // It is only used by third-party renderers. // // Its API lets you pass the host config as an argument. -// However, inside the `react-stream` we treat host config as a module. +// However, inside the `react-server` we treat host config as a module. // This file is a shim between two worlds. // -// It works because the `react-stream` bundle is wrapped in something like: +// It works because the `react-server` bundle is wrapped in something like: // // module.exports = function ($$$config) { // /* renderer code */ @@ -26,4 +26,5 @@ declare var $$$hostConfig: any; export opaque type Destination = mixed; // eslint-disable-line no-undef +export const formatChunkAsString = $$$hostConfig.formatChunkAsString; export const formatChunk = $$$hostConfig.formatChunk; diff --git a/packages/react-stream/src/forks/ReactFizzFormatConfig.dom-browser.js b/packages/react-server/src/forks/ReactServerFormatConfig.dom-browser.js similarity index 74% rename from packages/react-stream/src/forks/ReactFizzFormatConfig.dom-browser.js rename to packages/react-server/src/forks/ReactServerFormatConfig.dom-browser.js index e3b8782adee7c..c6e482efeb60c 100644 --- a/packages/react-stream/src/forks/ReactFizzFormatConfig.dom-browser.js +++ b/packages/react-server/src/forks/ReactServerFormatConfig.dom-browser.js @@ -7,4 +7,4 @@ * @flow */ -export * from 'react-dom/src/server/ReactDOMFizzServerFormatConfig'; +export * from 'react-dom/src/server/ReactDOMServerFormatConfig'; diff --git a/packages/react-stream/src/forks/ReactFizzFormatConfig.dom.js b/packages/react-server/src/forks/ReactServerFormatConfig.dom.js similarity index 74% rename from packages/react-stream/src/forks/ReactFizzFormatConfig.dom.js rename to packages/react-server/src/forks/ReactServerFormatConfig.dom.js index e3b8782adee7c..c6e482efeb60c 100644 --- a/packages/react-stream/src/forks/ReactFizzFormatConfig.dom.js +++ b/packages/react-server/src/forks/ReactServerFormatConfig.dom.js @@ -7,4 +7,4 @@ * @flow */ -export * from 'react-dom/src/server/ReactDOMFizzServerFormatConfig'; +export * from 'react-dom/src/server/ReactDOMServerFormatConfig'; diff --git a/packages/react-stream/src/forks/ReactFizzHostConfig.custom.js b/packages/react-server/src/forks/ReactServerHostConfig.custom.js similarity index 84% rename from packages/react-stream/src/forks/ReactFizzHostConfig.custom.js rename to packages/react-server/src/forks/ReactServerHostConfig.custom.js index 71d876a10e5a1..c3dc6d83d64b0 100644 --- a/packages/react-stream/src/forks/ReactFizzHostConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerHostConfig.custom.js @@ -7,14 +7,14 @@ * @flow */ -// This is a host config that's used for the `react-stream` package on npm. +// This is a host config that's used for the `react-server` package on npm. // It is only used by third-party renderers. // // Its API lets you pass the host config as an argument. -// However, inside the `react-stream` we treat host config as a module. +// However, inside the `react-server` we treat host config as a module. // This file is a shim between two worlds. // -// It works because the `react-stream` bundle is wrapped in something like: +// It works because the `react-server` bundle is wrapped in something like: // // module.exports = function ($$$config) { // /* renderer code */ diff --git a/packages/react-stream/src/forks/ReactFizzHostConfig.dom-browser.js b/packages/react-server/src/forks/ReactServerHostConfig.dom-browser.js similarity index 80% rename from packages/react-stream/src/forks/ReactFizzHostConfig.dom-browser.js rename to packages/react-server/src/forks/ReactServerHostConfig.dom-browser.js index ff949e1c746a1..2937762e0c840 100644 --- a/packages/react-stream/src/forks/ReactFizzHostConfig.dom-browser.js +++ b/packages/react-server/src/forks/ReactServerHostConfig.dom-browser.js @@ -7,4 +7,4 @@ * @flow */ -export * from '../ReactFizzHostConfigBrowser'; +export * from '../ReactServerHostConfigBrowser'; diff --git a/packages/react-stream/src/forks/ReactFizzHostConfig.dom.js b/packages/react-server/src/forks/ReactServerHostConfig.dom.js similarity index 81% rename from packages/react-stream/src/forks/ReactFizzHostConfig.dom.js rename to packages/react-server/src/forks/ReactServerHostConfig.dom.js index 88d44bc2e4519..6f560aa1b4962 100644 --- a/packages/react-stream/src/forks/ReactFizzHostConfig.dom.js +++ b/packages/react-server/src/forks/ReactServerHostConfig.dom.js @@ -7,4 +7,4 @@ * @flow */ -export * from '../ReactFizzHostConfigNode'; +export * from '../ReactServerHostConfigNode'; diff --git a/packages/react-stream/README.md b/packages/react-stream/README.md deleted file mode 100644 index 087175bb7dc31..0000000000000 --- a/packages/react-stream/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# react-stream - -This is an experimental package for creating custom React streaming server renderers. - -**Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.** - -**Use it at your own risk.** - -## API - -```js -var Renderer = require('react-stream'); - -var HostConfig = { - // You'll need to implement some methods here. - // See below for more information and examples. -}; - -var MyRenderer = Renderer(HostConfig); - -var RendererPublicAPI = { -}; - -module.exports = RendererPublicAPI; -``` diff --git a/packages/react-stream/npm/index.js b/packages/react-stream/npm/index.js deleted file mode 100644 index fb7a252fd61a1..0000000000000 --- a/packages/react-stream/npm/index.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/react-stream.production.min.js'); -} else { - module.exports = require('./cjs/react-stream.development.js'); -} diff --git a/scripts/flow/createFlowConfigs.js b/scripts/flow/createFlowConfigs.js index 2fab449a04e98..a0a5e7a323039 100644 --- a/scripts/flow/createFlowConfigs.js +++ b/scripts/flow/createFlowConfigs.js @@ -18,20 +18,23 @@ const configTemplate = fs .readFileSync(__dirname + '/config/flowconfig') .toString(); -function writeConfig(renderer, isFizzSupported) { +function writeConfig(renderer, isServerSupported) { const folder = __dirname + '/' + renderer; mkdirp.sync(folder); - const fizzRenderer = isFizzSupported ? renderer : 'custom'; + const serverRenderer = isServerSupported ? renderer : 'custom'; const config = configTemplate .replace( '%REACT_RENDERER_FLOW_OPTIONS%', ` module.name_mapper='react-reconciler/inline.${renderer}$$' -> 'react-reconciler/inline-typed' module.name_mapper='ReactFiberHostConfig$$' -> 'forks/ReactFiberHostConfig.${renderer}' -module.name_mapper='react-stream/inline.${renderer}$$' -> 'react-stream/inline-typed' -module.name_mapper='ReactFizzHostConfig$$' -> 'forks/ReactFizzHostConfig.${fizzRenderer}' -module.name_mapper='ReactFizzFormatConfig$$' -> 'forks/ReactFizzFormatConfig.${fizzRenderer}' +module.name_mapper='react-server/inline.${renderer}$$' -> 'react-server/inline-typed' +module.name_mapper='react-server/flight.inline.${renderer}$$' -> 'react-server/flight.inline-typed' +module.name_mapper='ReactServerHostConfig$$' -> 'forks/ReactServerHostConfig.${serverRenderer}' +module.name_mapper='ReactServerFormatConfig$$' -> 'forks/ReactServerFormatConfig.${serverRenderer}' +module.name_mapper='react-flight/inline.${renderer}$$' -> 'react-flight/inline-typed' +module.name_mapper='ReactFlightClientHostConfig$$' -> 'forks/ReactFlightClientHostConfig.${serverRenderer}' `.trim(), ) .replace( @@ -78,6 +81,6 @@ ${disclaimer} // so that we can run those checks in parallel if we want. inlinedHostConfigs.forEach(rendererInfo => { if (rendererInfo.isFlowTyped) { - writeConfig(rendererInfo.shortName, rendererInfo.isFizzSupported); + writeConfig(rendererInfo.shortName, rendererInfo.isServerSupported); } }); diff --git a/scripts/jest/setupHostConfigs.js b/scripts/jest/setupHostConfigs.js index b4036f52c9676..e547ac2bf1607 100644 --- a/scripts/jest/setupHostConfigs.js +++ b/scripts/jest/setupHostConfigs.js @@ -17,13 +17,28 @@ jest.mock('react-reconciler/persistent', () => { return require.requireActual('react-reconciler/persistent'); }; }); -const shimFizzHostConfigPath = 'react-stream/src/ReactFizzHostConfig'; -const shimFizzFormatConfigPath = 'react-stream/src/ReactFizzFormatConfig'; -jest.mock('react-stream', () => { +const shimServerHostConfigPath = 'react-server/src/ReactServerHostConfig'; +const shimServerFormatConfigPath = 'react-server/src/ReactServerFormatConfig'; +jest.mock('react-server', () => { return config => { - jest.mock(shimFizzHostConfigPath, () => config); - jest.mock(shimFizzFormatConfigPath, () => config); - return require.requireActual('react-stream'); + jest.mock(shimServerHostConfigPath, () => config); + jest.mock(shimServerFormatConfigPath, () => config); + return require.requireActual('react-server'); + }; +}); +jest.mock('react-server/flight', () => { + return config => { + jest.mock(shimServerHostConfigPath, () => config); + jest.mock(shimServerFormatConfigPath, () => config); + return require.requireActual('react-server/flight'); + }; +}); +const shimFlightClientHostConfigPath = + 'react-flight/src/ReactFlightClientHostConfig'; +jest.mock('react-flight', () => { + return config => { + jest.mock(shimFlightClientHostConfigPath, () => config); + return require.requireActual('react-flight'); }; }); @@ -65,29 +80,97 @@ inlinedHostConfigs.forEach(rendererInfo => { return renderer; }); - if (rendererInfo.isFizzSupported) { - jest.mock(`react-stream/inline.${rendererInfo.shortName}`, () => { + if (rendererInfo.isServerSupported) { + jest.mock(`react-server/inline.${rendererInfo.shortName}`, () => { + let hasImportedShimmedConfig = false; + + // We want the renderer to pick up the host config for this renderer. + jest.mock(shimServerHostConfigPath, () => { + hasImportedShimmedConfig = true; + return require.requireActual( + `react-server/src/forks/ReactServerHostConfig.${ + rendererInfo.shortName + }.js` + ); + }); + jest.mock(shimServerFormatConfigPath, () => { + hasImportedShimmedConfig = true; + return require.requireActual( + `react-server/src/forks/ReactServerFormatConfig.${ + rendererInfo.shortName + }.js` + ); + }); + + const renderer = require.requireActual('react-server'); + // If the shimmed config factory function above has not run, + // it means this test file loads more than one renderer + // but doesn't reset modules between them. This won't work. + if (!hasImportedShimmedConfig) { + throw new Error( + `Could not import the "${rendererInfo.shortName}" renderer ` + + `in this suite because another renderer has already been ` + + `loaded earlier. Call jest.resetModules() before importing any ` + + `of the following entry points:\n\n` + + rendererInfo.entryPoints.map(entry => ` * ${entry}`) + ); + } + + return renderer; + }); + + jest.mock(`react-server/flight.inline.${rendererInfo.shortName}`, () => { let hasImportedShimmedConfig = false; // We want the renderer to pick up the host config for this renderer. - jest.mock(shimFizzHostConfigPath, () => { + jest.mock(shimServerHostConfigPath, () => { + hasImportedShimmedConfig = true; + return require.requireActual( + `react-server/src/forks/ReactServerHostConfig.${ + rendererInfo.shortName + }.js` + ); + }); + jest.mock(shimServerFormatConfigPath, () => { hasImportedShimmedConfig = true; return require.requireActual( - `react-stream/src/forks/ReactFizzHostConfig.${ + `react-server/src/forks/ReactServerFormatConfig.${ rendererInfo.shortName }.js` ); }); - jest.mock(shimFizzFormatConfigPath, () => { + + const renderer = require.requireActual('react-server/flight'); + // If the shimmed config factory function above has not run, + // it means this test file loads more than one renderer + // but doesn't reset modules between them. This won't work. + if (!hasImportedShimmedConfig) { + throw new Error( + `Could not import the "${rendererInfo.shortName}" renderer ` + + `in this suite because another renderer has already been ` + + `loaded earlier. Call jest.resetModules() before importing any ` + + `of the following entry points:\n\n` + + rendererInfo.entryPoints.map(entry => ` * ${entry}`) + ); + } + + return renderer; + }); + + jest.mock(`react-flight/inline.${rendererInfo.shortName}`, () => { + let hasImportedShimmedConfig = false; + + // We want the renderer to pick up the host config for this renderer. + jest.mock(shimFlightClientHostConfigPath, () => { hasImportedShimmedConfig = true; return require.requireActual( - `react-stream/src/forks/ReactFizzFormatConfig.${ + `react-flight/src/forks/ReactFlightClientHostConfig.${ rendererInfo.shortName }.js` ); }); - const renderer = require.requireActual('react-stream'); + const renderer = require.requireActual('react-flight'); // If the shimmed config factory function above has not run, // it means this test file loads more than one renderer // but doesn't reset modules between them. This won't work. diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index b43c9864d034e..c48581a372de2 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -170,6 +170,38 @@ const bundles = [ externals: ['react'], }, + /******* React DOM Flight Server *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD], + moduleType: RENDERER, + entry: 'react-dom/unstable-flight-server.browser', + global: 'ReactFlightDOMServer', + externals: ['react'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD], + moduleType: RENDERER, + entry: 'react-dom/unstable-flight-server.node', + global: 'ReactFlightDOMServer', + externals: ['react'], + }, + + /******* React DOM Flight Client *******/ + { + bundleTypes: [ + NODE_DEV, + NODE_PROD, + UMD_DEV, + UMD_PROD, + FB_WWW_DEV, + FB_WWW_PROD, + ], + moduleType: RENDERER, + entry: 'react-dom/unstable-flight-client', + global: 'ReactFlightDOMClient', + externals: ['react'], + }, + /******* React ART *******/ { bundleTypes: [ @@ -308,6 +340,24 @@ const bundles = [ externals: ['react', 'scheduler', 'expect'], }, + /******* React Noop Flight Server (used for tests) *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-noop-renderer/flight-server', + global: 'ReactNoopFlightServer', + externals: ['react', 'scheduler', 'expect'], + }, + + /******* React Noop Flight Client (used for tests) *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-noop-renderer/flight-client', + global: 'ReactNoopFlightClient', + externals: ['react', 'scheduler', 'expect'], + }, + /******* React Reconciler *******/ { bundleTypes: [NODE_DEV, NODE_PROD], @@ -326,12 +376,30 @@ const bundles = [ externals: ['react'], }, - /******* React Stream *******/ + /******* React Server *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RECONCILER, + entry: 'react-server', + global: 'ReactServer', + externals: ['react'], + }, + + /******* React Flight Server *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RECONCILER, + entry: 'react-server/flight', + global: 'ReactFlightServer', + externals: ['react'], + }, + + /******* React Flight Client *******/ { bundleTypes: [NODE_DEV, NODE_PROD], moduleType: RECONCILER, - entry: 'react-stream', - global: 'ReactStream', + entry: 'react-flight', + global: 'ReactFlightClient', externals: ['react'], }, diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index 1f6edfbeb8dc1..8c94593ec838d 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -307,13 +307,13 @@ const forks = Object.freeze({ ); }, - 'react-stream/src/ReactFizzHostConfig': ( + 'react-server/src/ReactServerHostConfig': ( bundleType, entry, dependencies, moduleType ) => { - if (dependencies.indexOf('react-stream') !== -1) { + if (dependencies.indexOf('react-server') !== -1) { return null; } if (moduleType !== RENDERER && moduleType !== RECONCILER) { @@ -322,28 +322,28 @@ const forks = Object.freeze({ // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (let rendererInfo of inlinedHostConfigs) { if (rendererInfo.entryPoints.indexOf(entry) !== -1) { - if (!rendererInfo.isFizzSupported) { + if (!rendererInfo.isServerSupported) { return null; } - return `react-stream/src/forks/ReactFizzHostConfig.${ + return `react-server/src/forks/ReactServerHostConfig.${ rendererInfo.shortName }.js`; } } throw new Error( - 'Expected ReactFizzHostConfig to always be replaced with a shim, but ' + + 'Expected ReactServerHostConfig to always be replaced with a shim, but ' + `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + 'Did you mean to add it there to associate it with a specific renderer?' ); }, - 'react-stream/src/ReactFizzFormatConfig': ( + 'react-server/src/ReactServerFormatConfig': ( bundleType, entry, dependencies, moduleType ) => { - if (dependencies.indexOf('react-stream') !== -1) { + if (dependencies.indexOf('react-server') !== -1) { return null; } if (moduleType !== RENDERER && moduleType !== RECONCILER) { @@ -352,16 +352,46 @@ const forks = Object.freeze({ // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (let rendererInfo of inlinedHostConfigs) { if (rendererInfo.entryPoints.indexOf(entry) !== -1) { - if (!rendererInfo.isFizzSupported) { + if (!rendererInfo.isServerSupported) { return null; } - return `react-stream/src/forks/ReactFizzFormatConfig.${ + return `react-server/src/forks/ReactServerFormatConfig.${ rendererInfo.shortName }.js`; } } throw new Error( - 'Expected ReactFizzFormatConfig to always be replaced with a shim, but ' + + 'Expected ReactServerFormatConfig to always be replaced with a shim, but ' + + `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + + 'Did you mean to add it there to associate it with a specific renderer?' + ); + }, + + 'react-flight/src/ReactFlightClientHostConfig': ( + bundleType, + entry, + dependencies, + moduleType + ) => { + if (dependencies.indexOf('react-flight') !== -1) { + return null; + } + if (moduleType !== RENDERER && moduleType !== RECONCILER) { + return null; + } + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (let rendererInfo of inlinedHostConfigs) { + if (rendererInfo.entryPoints.indexOf(entry) !== -1) { + if (!rendererInfo.isServerSupported) { + return null; + } + return `react-flight/src/forks/ReactFlightClientHostConfig.${ + rendererInfo.shortName + }.js`; + } + } + throw new Error( + 'Expected ReactFlightClientHostConfig to always be replaced with a shim, but ' + `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + 'Did you mean to add it there to associate it with a specific renderer?' ); diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 1f66699387638..1832b732eefc3 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -9,48 +9,59 @@ module.exports = [ { shortName: 'dom', - entryPoints: ['react-dom', 'react-dom/unstable-fizz.node'], + entryPoints: [ + 'react-dom', + 'react-dom/unstable-fizz.node', + 'react-dom/unstable-flight-server.node', + 'react-dom/unstable-flight-client', + ], isFlowTyped: true, - isFizzSupported: true, + isServerSupported: true, }, { shortName: 'dom-browser', - entryPoints: ['react-dom/unstable-fizz.browser'], + entryPoints: [ + 'react-dom/unstable-fizz.browser', + 'react-dom/unstable-flight-server.browser', + 'react-dom/unstable-flight-client', + ], isFlowTyped: true, - isFizzSupported: true, + isServerSupported: true, }, { shortName: 'art', entryPoints: ['react-art'], isFlowTyped: false, // TODO: type it. - isFizzSupported: false, + isServerSupported: false, }, { shortName: 'native', entryPoints: ['react-native-renderer'], isFlowTyped: true, - isFizzSupported: false, + isServerSupported: false, }, { shortName: 'fabric', entryPoints: ['react-native-renderer/fabric'], isFlowTyped: true, - isFizzSupported: false, + isServerSupported: false, }, { shortName: 'test', entryPoints: ['react-test-renderer'], isFlowTyped: true, - isFizzSupported: false, + isServerSupported: false, }, { shortName: 'custom', entryPoints: [ 'react-reconciler', 'react-reconciler/persistent', - 'react-stream', + 'react-flight', + 'react-server', + 'react-server/flight', ], isFlowTyped: true, - isFizzSupported: true, + isServerSupported: true, }, ];