-
Notifications
You must be signed in to change notification settings - Fork 48.6k
Add Experimental Flight Infrastructure #16398
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
95494d6
Add Flight Build and Unify HostFormat Config between Flight and Fizz
sebmarkbage 0966d18
Add basic resolution of models
sebmarkbage 5ae68d6
Add basic Flight fixture
sebmarkbage b785a7b
Rename to flight-server to distinguish from the client parts
sebmarkbage f53a31d
Add Flight Client package and entry point
sebmarkbage e5c85fd
Fix fixture
sebmarkbage File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
<!DOCTYPE html> | ||
<html style="width: 100%; height: 100%; overflow: hidden"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>Flight Example</title> | ||
</head> | ||
<body> | ||
<h1>Flight 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.development.js"></script> | ||
<script src="../../build/dist/react-dom-unstable-flight-client.development.js"></script> | ||
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script> | ||
<script type="text/babel"> | ||
function Text({children}) { | ||
return <span>{children}</span>; | ||
} | ||
function HTML() { | ||
return ( | ||
<div> | ||
<Text>hello</Text> | ||
<Text>world</Text> | ||
</div> | ||
); | ||
} | ||
|
||
let model = { | ||
title: 'Title', | ||
content: { | ||
__html: <HTML />, | ||
} | ||
}; | ||
|
||
let stream = ReactFlightDOMClient.renderToReadableStream(model); | ||
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 response = await fetch(url); | ||
let body = await response.body; | ||
|
||
let reader = body.getReader(); | ||
let charsReceived = 0; | ||
let decoder = new TextDecoder(); | ||
|
||
let json = ''; | ||
reader.read().then(function processChunk({ done, value }) { | ||
if (done) { | ||
renderResult(json); | ||
return; | ||
} | ||
json += decoder.decode(value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this needs a second argument json += decoder.decode(); to finish. https://encoding.spec.whatwg.org/#example-end-of-stream |
||
return reader.read().then(processChunk); | ||
}); | ||
} | ||
|
||
function Shell({ model }) { | ||
return <div> | ||
<h1>{model.title}</h1> | ||
<div dangerouslySetInnerHTML={model.content} /> | ||
</div>; | ||
} | ||
|
||
function renderResult(json) { | ||
let model = JSON.parse(json); | ||
let container = document.getElementById('container'); | ||
ReactDOM.render(<Shell model={model} />, container); | ||
} | ||
</script> | ||
</body> | ||
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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-flight-client.production.min.js'); | ||
} else { | ||
module.exports = require('./cjs/react-dom-unstable-flight-client.development.js'); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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-flight-server.browser.production.min.js'); | ||
} else { | ||
module.exports = require('./cjs/react-dom-unstable-flight-server.browser.development.js'); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
'use strict'; | ||
|
||
module.exports = require('./unstable-flight-server.node'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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-flight-server.node.production.min.js'); | ||
} else { | ||
module.exports = require('./cjs/react-dom-unstable-flight-server.node.development.js'); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 34 additions & 0 deletions
34
packages/react-dom/src/client/flight/ReactFlightDOMClient.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {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, | ||
}; |
50 changes: 50 additions & 0 deletions
50
packages/react-dom/src/client/flight/ReactFlightDOMHostConfig.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 += '</' + type + '>'; | ||
return str; | ||
} | ||
|
||
export function formatChunk(type: string, props: Object): Uint8Array { | ||
return convertStringToBuffer(formatChunkAsString(type, props)); | ||
} |
61 changes: 61 additions & 0 deletions
61
packages/react-dom/src/client/flight/__tests__/ReactFlightDOMBrowser-test.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <span>{children}</span>; | ||
} | ||
function HTML() { | ||
return ( | ||
<div> | ||
<Text>hello</Text> | ||
<Text>world</Text> | ||
</div> | ||
); | ||
} | ||
|
||
let model = { | ||
html: <HTML />, | ||
}; | ||
let stream = ReactFlightDOMServer.renderToReadableStream(model); | ||
jest.runAllTimers(); | ||
let result = JSON.parse(await readResult(stream)); | ||
expect(result).toEqual({ | ||
html: '<div><span>hello</span><span>world</span></div>', | ||
}); | ||
}); | ||
}); |
57 changes: 57 additions & 0 deletions
57
packages/react-dom/src/client/flight/__tests__/ReactFlightDOMNode-test.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <span>{children}</span>; | ||
} | ||
function HTML() { | ||
return ( | ||
<div> | ||
<Text>hello</Text> | ||
<Text>world</Text> | ||
</div> | ||
); | ||
} | ||
|
||
let writable = getTestWritable(); | ||
let model = { | ||
html: <HTML />, | ||
}; | ||
ReactFlightDOMServer.pipeToNodeWritable(model, writable); | ||
jest.runAllTimers(); | ||
let result = JSON.parse(writable.result); | ||
expect(result).toEqual({ | ||
html: '<div><span>hello</span><span>world</span></div>', | ||
}); | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just checking that I'm following what's happening here: "model" values can render components, which are (at least for now) effectively rendered on the server and sent down as an HTML string (this is the "E.g. a host component tree can be flattened into raw HTML." from your description, right?).
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea. The HTML component is actually just a Model function. Then that model happens to contains "host" components (leaves) which are flattened into HTML. This will likely need to be much more clever to protect against XSS attacks like #3473 and to allow targeting nested nodes with client components etc.
I could also do
return { __html: <div>...</div> }
inside the HTML component since it's just a Model function and can return arbitrary data structures.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nice thing about this pattern is that if you have a tree of stateless functional components, the same code can be one shot rendered like this, or client rendered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you clarify in your current mental model: Is a Model function actually a component (i.e. can I use hooks in it), or are you using
<HTML />
here for sugar?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A Model function can not use hooks. It's not a client-side component so it cannot have effects or state. (We could potentially let it have access to useContext though.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can it suspend? If not, how can it fetch any async data?
I know this is just a plumbing PR, but it would be super helpful if you could include a small end-to-end example 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That’s one possible solution but that’s really not decided. It could be async functions, it could be something else. :)