Skip to content

Commit

Permalink
[Fizz] New Server Rendering Infra (facebook#14144)
Browse files Browse the repository at this point in the history
* [Fizz] Add Flow/Jest/Rollup build infra

Add a new package for react-stream which allows for custom server renderer
outputs. I picked the name because it's a reasonable name but also
because the npm name is currently owned by a friend of the project.

The react-dom build has its own inlined server renderer under the
name `react-dom/fizz`.

There is also a noop renderer to be used for testing. At some point
we might add a public one to test-renderer but for now I don't want to have
to think about public API design for the tests.

* Add FormatConfig too

We need to separate the format (DOM, React Native, etc) from the host
running the server (Node, Browser, etc).

* Basic wiring between Node, Noop and DOM configs

The Node DOM API is pipeToNodeStream which accepts a writable stream.

* Merge host and format config in dynamic react-stream entry point

Simpler API this way but also avoids having to fork the wrapper config.

Fixes noop builds.

* Add setImmediate/Buffer globals to lint config

Used by the server renderer

* Properly include fizz.node.js

Also use forwarding to it from fizz.js in builds so that tests covers
this.

* Make react-stream private since we're not ready to publish

or even name it yet

* Rename Renderer -> Streamer

* Prefix react-dom/fizz with react-dom/unstable-fizz

* Add Fizz Browser host config

This lets Fizz render to WHATWG streams. E.g. for rendering in a
Service Worker.

I added react-dom/unstable-fizz.browser as the entry point for this.

Since we now have two configurations of DOM. I had to add another
inlinedHostConfigs configuration called `dom-browser`. The reconciler
treats this configuration the same as `dom`. For stream it checks
against the ReactFizzHostConfigBrowser instead of the Node one.

* Add Fizz Browser Fixture

This is for testing server rendering - on the client.

* Lower version number to detach it from react-reconciler version
  • Loading branch information
sebmarkbage authored and jetoneza committed Jan 23, 2019
1 parent e25e27e commit 732a839
Show file tree
Hide file tree
Showing 48 changed files with 1,214 additions and 87 deletions.
40 changes: 40 additions & 0 deletions fixtures/fizz-ssr-browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html style="width: 100%; height: 100%; overflow: hidden">
<head>
<meta charset="utf-8">
<title>Fizz Example</title>
</head>
<body>
<h1>Fizz Example</h1>
<div id="container">
<p>
To install React, follow the instructions on
<a href="https://github.com/facebook/react/">GitHub</a>.
</p>
<p>
If you can see this, React is <strong>not</strong> working right.
If you checked out the source from GitHub make sure to run <code>npm run build</code>.
</p>
</div>
<script src="../../build/dist/react.development.js"></script>
<script src="../../build/dist/react-dom-unstable-fizz.browser.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
<script type="text/babel">
let stream = ReactDOMFizzServer.renderToReadableStream(<body>Success</body>);
let response = new Response(stream, {
headers: {'Content-Type': 'text/html'},
});
display(response);

async function display(responseToDisplay) {
let blob = await responseToDisplay.blob();
let url = URL.createObjectURL(blob);
let iframe = document.createElement('iframe');
iframe.src = url;
let container = document.getElementById('container');
container.innerHTML = '';
container.appendChild(iframe);
}
</script>
</body>
</html>
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
"targz": "^1.0.1",
"through2": "^2.0.0",
"tmp": "~0.0.28",
"typescript": "~1.8.10"
"typescript": "~1.8.10",
"@mattiasbuelens/web-streams-polyfill": "0.1.0"
},
"devEngines": {
"node": "8.x || 9.x || 10.x"
Expand Down
7 changes: 7 additions & 0 deletions packages/react-dom/npm/unstable-fizz.browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-dom-unstable-fizz.browser.production.min.js');
} else {
module.exports = require('./cjs/react-dom-unstable-fizz.browser.development.js');
}
3 changes: 3 additions & 0 deletions packages/react-dom/npm/unstable-fizz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('./unstable-fizz.node');
7 changes: 7 additions & 0 deletions packages/react-dom/npm/unstable-fizz.node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-dom-unstable-fizz.node.production.min.js');
} else {
module.exports = require('./cjs/react-dom-unstable-fizz.node.development.js');
}
6 changes: 5 additions & 1 deletion packages/react-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@
"server.node.js",
"test-utils.js",
"unstable-fire.js",
"unstable-fizz.js",
"unstable-fizz.browser.js",
"unstable-fizz.node.js",
"unstable-native-dependencies.js",
"cjs/",
"umd/"
],
"browser": {
"./server.js": "./server.browser.js"
"./server.js": "./server.browser.js",
"./unstable-fizz.js": "./unstable-fizz.browser.js"
},
"browserify": {
"transform": [
Expand Down
45 changes: 45 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* 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 ReactDOMFizzServer;

describe('ReactDOMFizzServer', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOMFizzServer = require('react-dom/unstable-fizz.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 call renderToReadableStream', async () => {
let stream = ReactDOMFizzServer.renderToReadableStream(
<div>hello world</div>,
);
let result = await readResult(stream);
expect(result).toBe('<div>hello world</div>');
});
});
39 changes: 39 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* 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 ReactDOMFizzServer;

describe('ReactDOMFizzServer', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOMFizzServer = require('react-dom/unstable-fizz');
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 call pipeToNodeWritable', () => {
let writable = getTestWritable();
ReactDOMFizzServer.pipeToNodeWritable(<div>hello world</div>, writable);
jest.runAllTimers();
expect(writable.result).toBe('<div>hello world</div>');
});
});
34 changes: 34 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
@@ -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 {ReactNodeList} from 'shared/ReactTypes';

import {
createRequest,
startWork,
startFlowing,
} from 'react-stream/inline.dom-browser';

function renderToReadableStream(children: ReactNodeList): ReadableStream {
let request;
return new ReadableStream({
start(controller) {
request = createRequest(children, controller);
startWork(request);
},
pull(controller) {
startFlowing(request, controller.desiredSize);
},
cancel(reason) {},
});
}

export default {
renderToReadableStream,
};
19 changes: 19 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerFormatConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* 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 {convertStringToBuffer} from 'react-stream/src/ReactFizzHostConfig';

export function formatChunk(type: string, props: Object): Uint8Array {
let str = '<' + type + '>';
if (typeof props.children === 'string') {
str += props.children;
}
str += '</' + type + '>';
return convertStringToBuffer(str);
}
30 changes: 30 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* 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 {ReactNodeList} from 'shared/ReactTypes';
import type {Writable} from 'stream';

import {createRequest, startWork, startFlowing} from 'react-stream/inline.dom';

function createDrainHandler(destination, request) {
return () => startFlowing(request, 0);
}

function pipeToNodeWritable(
children: ReactNodeList,
destination: Writable,
): void {
let request = createRequest(children, destination);
destination.on('drain', createDrainHandler(destination, request));
startWork(request);
}

export default {
pipeToNodeWritable,
};
16 changes: 16 additions & 0 deletions packages/react-dom/unstable-fizz.browser.js
Original file line number Diff line number Diff line change
@@ -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 ReactDOMFizzServerBrowser = require('./src/server/ReactDOMFizzServerBrowser');

// TODO: decide on the top-level export form.
// This is hacky but makes it work with both Rollup and Jest
module.exports = ReactDOMFizzServerBrowser.default || ReactDOMFizzServerBrowser;
12 changes: 12 additions & 0 deletions packages/react-dom/unstable-fizz.js
Original file line number Diff line number Diff line change
@@ -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-fizz.node');
16 changes: 16 additions & 0 deletions packages/react-dom/unstable-fizz.node.js
Original file line number Diff line number Diff line change
@@ -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 ReactDOMFizzServerNode = require('./src/server/ReactDOMFizzServerNode');

// TODO: decide on the top-level export form.
// This is hacky but makes it work with both Rollup and Jest
module.exports = ReactDOMFizzServerNode.default || ReactDOMFizzServerNode;
7 changes: 7 additions & 0 deletions packages/react-noop-renderer/npm/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-noop-renderer-server.production.min.js');
} else {
module.exports = require('./cjs/react-noop-renderer-server.development.js');
}
4 changes: 3 additions & 1 deletion packages/react-noop-renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"regenerator-runtime": "^0.11.0",
"react-reconciler": "*"
"react-reconciler": "*",
"react-stream": "*"
},
"peerDependencies": {
"react": "^16.0.0"
Expand All @@ -21,6 +22,7 @@
"build-info.json",
"index.js",
"persistent.js",
"server.js",
"cjs/"
]
}
16 changes: 16 additions & 0 deletions packages/react-noop-renderer/server.js
Original file line number Diff line number Diff line change
@@ -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 ReactNoopServer = require('./src/ReactNoopServer');

// TODO: decide on the top-level export form.
// This is hacky but makes it work with both Rollup and Jest.
module.exports = ReactNoopServer.default || ReactNoopServer;
Loading

0 comments on commit 732a839

Please sign in to comment.