@samitouri / QOS-React / commits / a56309fb88

[Flight] Integrate Blocks into Flight (#18371)

* Resolve Server-side Blocks instead of Components React elements should no longer be used to extract arbitrary data but only for prerendering trees. Blocks are used to create asynchronous behavior. * Resolve Blocks in the Client * Tests * Bug fix relay JSON traversal It's supposed to pass the original object and not the new one. * Lint * Move Noop Module Test Helpers to top level entry points This module has shared state. It needs to be external from builds. This lets us test the built versions of the Noop renderer.

Sebastian Markbåge committed Mar 23, 2020 at 17:53 UTC a56309fb883fcb04d7c3d1e4bc40987175c473f0
22 files changed +530 -155
packages/react-client/src/ReactFlightClient.js
+114 -33
@@ -7,18 +7,25 @@
7 * @flow
8 */
9
10 -import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
10 +import type {BlockComponent, BlockRenderFunction} from 'react/src/ReactBlock';
11 +import type {LazyComponent} from 'react/src/ReactLazy';
12
12 -// import type {
13 -// ModuleReference,
14 -// ModuleMetaData,
15 -// } from './ReactFlightClientHostConfig';
13 +import type {
14 + ModuleReference,
15 + ModuleMetaData,
16 +} from './ReactFlightClientHostConfig';
17
17 -// import {
18 -// resolveModuleReference,
19 -// preloadModule,
20 -// requireModule,
21 -// } from './ReactFlightClientHostConfig';
18 +import {
19 + resolveModuleReference,
20 + preloadModule,
21 + requireModule,
22 +} from './ReactFlightClientHostConfig';
23 +
24 +import {
25 + REACT_LAZY_TYPE,
26 + REACT_BLOCK_TYPE,
27 + REACT_ELEMENT_TYPE,
28 +} from 'shared/ReactSymbols';
29
30 export type ReactModelRoot<T> = {|
31 model: T,
@@ -32,40 +39,43 @@ export type JSONValue =
39 | {[key: string]: JSONValue}
40 | Array<JSONValue>;
41
35 -const isArray = Array.isArray;
36 -
42 const PENDING = 0;
43 const RESOLVED = 1;
44 const ERRORED = 2;
45
46 +const CHUNK_TYPE = Symbol('flight.chunk');
47 +
48 type PendingChunk = {|
49 + $$typeof: Symbol,
50 status: 0,
51 value: Promise<void>,
52 resolve: () => void,
53 |};
46 -type ResolvedChunk = {|
54 +type ResolvedChunk<T> = {|
55 + $$typeof: Symbol,
56 status: 1,
48 - value: mixed,
57 + value: T,
58 resolve: null,
59 |};
60 type ErroredChunk = {|
61 + $$typeof: Symbol,
62 status: 2,
63 value: Error,
64 resolve: null,
65 |};
56 -type Chunk = PendingChunk | ResolvedChunk | ErroredChunk;
66 +type Chunk<T> = PendingChunk | ResolvedChunk<T> | ErroredChunk;
67
68 export type Response = {
69 partialRow: string,
70 modelRoot: ReactModelRoot<any>,
61 - chunks: Map<number, Chunk>,
71 + chunks: Map<number, Chunk<any>>,
72 };
73
74 export function createResponse(): Response {
75 let modelRoot: ReactModelRoot<any> = ({}: any);
66 - let rootChunk: Chunk = createPendingChunk();
76 + let rootChunk: Chunk<any> = createPendingChunk();
77 definePendingProperty(modelRoot, 'model', rootChunk);
68 - let chunks: Map<number, Chunk> = new Map();
78 + let chunks: Map<number, Chunk<any>> = new Map();
79 chunks.set(0, rootChunk);
80 let response = {
81 partialRow: '',
@@ -79,6 +89,7 @@ function createPendingChunk(): PendingChunk {
89 let resolve: () => void = (null: any);
90 let promise = new Promise(r => (resolve = r));
91 return {
92 + $$typeof: CHUNK_TYPE,
93 status: PENDING,
94 value: promise,
95 resolve: resolve,
@@ -87,13 +98,14 @@ function createPendingChunk(): PendingChunk {
98
99 function createErrorChunk(error: Error): ErroredChunk {
100 return {
101 + $$typeof: CHUNK_TYPE,
102 status: ERRORED,
103 value: error,
104 resolve: null,
105 };
106 }
107
96 -function triggerErrorOnChunk(chunk: Chunk, error: Error): void {
108 +function triggerErrorOnChunk<T>(chunk: Chunk<T>, error: Error): void {
109 if (chunk.status !== PENDING) {
110 // We already resolved. We didn't expect to see this.
111 return;
@@ -106,21 +118,22 @@ function triggerErrorOnChunk(chunk: Chunk, error: Error): void {
118 resolve();
119 }
120
109 -function createResolvedChunk(value: mixed): ResolvedChunk {
121 +function createResolvedChunk<T>(value: T): ResolvedChunk<T> {
122 return {
123 + $$typeof: CHUNK_TYPE,
124 status: RESOLVED,
125 value: value,
126 resolve: null,
127 };
128 }
129
117 -function resolveChunk(chunk: Chunk, value: mixed): void {
130 +function resolveChunk<T>(chunk: Chunk<T>, value: T): void {
131 if (chunk.status !== PENDING) {
132 // We already resolved. We didn't expect to see this.
133 return;
134 }
135 let resolve = chunk.resolve;
123 - let resolvedChunk: ResolvedChunk = (chunk: any);
136 + let resolvedChunk: ResolvedChunk<T> = (chunk: any);
137 resolvedChunk.status = RESOLVED;
138 resolvedChunk.value = value;
139 resolvedChunk.resolve = null;
@@ -138,10 +151,23 @@ export function reportGlobalError(response: Response, error: Error): void {
151 });
152 }
153
141 -function definePendingProperty(
154 +function readMaybeChunk<T>(maybeChunk: Chunk<T> | T): T {
155 + if ((maybeChunk: any).$$typeof !== CHUNK_TYPE) {
156 + // $FlowFixMe
157 + return maybeChunk;
158 + }
159 + let chunk: Chunk<T> = (maybeChunk: any);
160 + if (chunk.status === RESOLVED) {
161 + return chunk.value;
162 + } else {
163 + throw chunk.value;
164 + }
165 +}
166 +
167 +function definePendingProperty<T>(
168 object: Object,
169 key: string,
144 - chunk: Chunk,
170 + chunk: Chunk<T>,
171 ): void {
172 Object.defineProperty(object, key, {
173 configurable: false,
@@ -197,6 +223,55 @@ function createElement(type, key, props): React$Element<any> {
223 return element;
224 }
225
226 +type UninitializedBlockPayload<Data> = [
227 + mixed,
228 + ModuleMetaData | Chunk<ModuleMetaData>,
229 + Data | Chunk<Data>,
230 +];
231 +
232 +type Thenable<T> = {
233 + then(resolve: (T) => mixed, reject?: (mixed) => mixed): Thenable<any>,
234 +};
235 +
236 +function initializeBlock<Props, Data>(
237 + tuple: UninitializedBlockPayload<Data>,
238 +): BlockComponent<Props, Data> {
239 + // Require module first and then data. The ordering matters.
240 + let moduleMetaData: ModuleMetaData = readMaybeChunk(tuple[1]);
241 + let moduleReference: ModuleReference<
242 + BlockRenderFunction<Props, Data>,
243 + > = resolveModuleReference(moduleMetaData);
244 + // TODO: Do this earlier, as the chunk is resolved.
245 + preloadModule(moduleReference);
246 +
247 + let moduleExport = requireModule(moduleReference);
248 +
249 + // The ordering here is important because this call might suspend.
250 + // We don't want that to prevent the module graph for being initialized.
251 + let data: Data = readMaybeChunk(tuple[2]);
252 +
253 + return {
254 + $$typeof: REACT_BLOCK_TYPE,
255 + _status: -1,
256 + _data: data,
257 + _render: moduleExport,
258 + };
259 +}
260 +
261 +function createLazyBlock<Props, Data>(
262 + tuple: UninitializedBlockPayload<Data>,
263 +): LazyComponent<BlockComponent<Props, Data>, UninitializedBlockPayload<Data>> {
264 + let lazyType: LazyComponent<
265 + BlockComponent<Props, Data>,
266 + UninitializedBlockPayload<Data>,
267 + > = {
268 + $$typeof: REACT_LAZY_TYPE,
269 + _payload: tuple,
270 + _init: initializeBlock,
271 + };
272 + return lazyType;
273 +}
274 +
275 export function parseModelFromJSON(
276 response: Response,
277 targetObj: Object,
@@ -217,20 +292,26 @@ export function parseModelFromJSON(
292 if (!chunk) {
293 chunk = createPendingChunk();
294 chunks.set(id, chunk);
220 - } else if (chunk.status === RESOLVED) {
221 - return chunk.value;
295 }
223 - definePendingProperty(targetObj, key, chunk);
224 - return undefined;
296 + return chunk;
297 }
298 }
299 + if (value === '@') {
300 + return REACT_BLOCK_TYPE;
301 + }
302 }
228 - if (isArray(value)) {
303 + if (typeof value === 'object' && value !== null) {
304 let tuple: [mixed, mixed, mixed, mixed] = (value: any);
230 - if (tuple[0] === REACT_ELEMENT_TYPE) {
231 - // TODO: Consider having React just directly accept these arrays as elements.
232 - // Or even change the ReactElement type to be an array.
233 - return createElement(tuple[1], tuple[2], tuple[3]);
305 + switch (tuple[0]) {
306 + case REACT_ELEMENT_TYPE: {
307 + // TODO: Consider having React just directly accept these arrays as elements.
308 + // Or even change the ReactElement type to be an array.
309 + return createElement(tuple[1], tuple[2], tuple[3]);
310 + }
311 + case REACT_BLOCK_TYPE: {
312 + // TODO: Consider having React just directly accept these arrays as blocks.
313 + return createLazyBlock((tuple: any));
314 + }
315 }
316 }
317 return value;
packages/react-client/src/__tests__/ReactFlight-test.js
+61 -3
@@ -10,7 +10,11 @@
10
11 'use strict';
12
13 +const ReactFeatureFlags = require('shared/ReactFeatureFlags');
14 +
15 +let act;
16 let React;
17 +let ReactNoop;
18 let ReactNoopFlightServer;
19 let ReactNoopFlightClient;
20
@@ -19,17 +23,32 @@ describe('ReactFlight', () => {
23 jest.resetModules();
24
25 React = require('react');
26 + ReactNoop = require('react-noop-renderer');
27 ReactNoopFlightServer = require('react-noop-renderer/flight-server');
28 ReactNoopFlightClient = require('react-noop-renderer/flight-client');
29 + act = ReactNoop.act;
30 });
31
26 - it('can resolve a model', () => {
32 + function block(query, render) {
33 + return function(...args) {
34 + let curriedQuery = () => {
35 + return query(...args);
36 + };
37 + return [Symbol.for('react.server.block'), render, curriedQuery];
38 + };
39 + }
40 +
41 + it('can render a server component', () => {
42 function Bar({text}) {
43 return text.toUpperCase();
44 }
45 function Foo() {
46 return {
32 - bar: [<Bar text="a" />, <Bar text="b" />],
47 + bar: (
48 + <div>
49 + <Bar text="a" />, <Bar text="b" />
50 + </div>
51 + ),
52 };
53 }
54 let transport = ReactNoopFlightServer.render({
@@ -37,6 +56,45 @@ describe('ReactFlight', () => {
56 });
57 let root = ReactNoopFlightClient.read(transport);
58 let model = root.model;
40 - expect(model).toEqual({foo: {bar: ['A', 'B']}});
59 + expect(model).toEqual({
60 + foo: {
61 + bar: (
62 + <div>
63 + {'A'}
64 + {', '}
65 + {'B'}
66 + </div>
67 + ),
68 + },
69 + });
70 });
71 +
72 + if (ReactFeatureFlags.enableBlocksAPI) {
73 + it('can transfer a Block to the client and render there', () => {
74 + function Query(firstName, lastName) {
75 + return {name: firstName + ' ' + lastName};
76 + }
77 + function User(props, data) {
78 + return (
79 + <span>
80 + {props.greeting}, {data.name}
81 + </span>
82 + );
83 + }
84 + let loadUser = block(Query, User);
85 + let model = {
86 + User: loadUser('Seb', 'Smith'),
87 + };
88 +
89 + let transport = ReactNoopFlightServer.render(model);
90 + let root = ReactNoopFlightClient.read(transport);
91 +
92 + act(() => {
93 + let UserClient = root.model.User;
94 + ReactNoop.render(<UserClient greeting="Hello" />);
95 + });
96 +
97 + expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
98 + });
99 + }
100 });
packages/react-flight-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js
+17 -7
@@ -43,7 +43,7 @@ type JSONValue =
43 | number
44 | boolean
45 | null
46 - | {[key: string]: JSONValue}
46 + | {+[key: string]: JSONValue}
47 | Array<JSONValue>;
48
49 export type Chunk =
@@ -78,19 +78,29 @@ export function processErrorChunk(
78 };
79 }
80
81 -function convertModelToJSON(request: Request, model: ReactModel): JSONValue {
82 - let json = resolveModelToJSON(request, model);
81 +function convertModelToJSON(
82 + request: Request,
83 + parent: {+[key: string]: ReactModel} | $ReadOnlyArray<ReactModel>,
84 + key: string,
85 + model: ReactModel,
86 +): JSONValue {
87 + let json = resolveModelToJSON(request, parent, key, model);
88 if (typeof json === 'object' && json !== null) {
89 if (Array.isArray(json)) {
90 let jsonArray: Array<JSONValue> = [];
91 for (let i = 0; i < json.length; i++) {
87 - jsonArray[i] = convertModelToJSON(request, json[i]);
92 + jsonArray[i] = convertModelToJSON(request, json, '' + i, json[i]);
93 }
94 return jsonArray;
95 } else {
96 let jsonObj: {[key: string]: JSONValue} = {};
92 - for (let key in json) {
93 - jsonObj[key] = convertModelToJSON(request, json[key]);
97 + for (let nextKey in json) {
98 + jsonObj[nextKey] = convertModelToJSON(
99 + request,
100 + json,
101 + nextKey,
102 + json[nextKey],
103 + );
104 }
105 return jsonObj;
106 }
@@ -103,7 +113,7 @@ export function processModelChunk(
113 id: number,
114 model: ReactModel,
115 ): Chunk {
106 - let json = convertModelToJSON(request, model);
116 + let json = convertModelToJSON(request, {}, '', model);
117 return {
118 type: 'json',
119 id: id,
packages/react-flight-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js
+1 -7
@@ -7,19 +7,13 @@
7
8 'use strict';
9
10 -function getFakeModule() {
11 - return function FakeModule(props, data) {
12 - return data;
13 - };
14 -}
15 -
10 const ReactFlightDOMRelayClientIntegration = {
11 resolveModuleReference(moduleData) {
12 return moduleData;
13 },
14 preloadModule(moduleReference) {},
15 requireModule(moduleReference) {
22 - return getFakeModule();
16 + return moduleReference;
17 },
18 };
19
packages/react-flight-dom-relay/src/__mocks__/ReactFlightDOMRelayServerIntegration.js
+1 -1
@@ -23,7 +23,7 @@ const ReactFlightDOMRelayServerIntegration = {
23 });
24 },
25 close(destination) {},
26 - resolveModuleMetaDataImpl(resource) {
26 + resolveModuleMetaData(resource) {
27 return resource;
28 },
29 };
packages/react-flight-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js
+81 -18
@@ -7,7 +7,9 @@
7
8 'use strict';
9
10 +let act;
11 let React;
12 +let ReactDOM;
13 let ReactDOMFlightRelayServer;
14 let ReactDOMFlightRelayClient;
15
@@ -15,28 +17,14 @@ describe('ReactFlightDOMRelay', () => {
17 beforeEach(() => {
18 jest.resetModules();
19
20 + act = require('react-dom/test-utils').act;
21 React = require('react');
22 + ReactDOM = require('react-dom');
23 ReactDOMFlightRelayServer = require('react-flight-dom-relay/server');
24 ReactDOMFlightRelayClient = require('react-flight-dom-relay');
25 });
26
23 - it('can resolve a model', () => {
24 - function Bar({text}) {
25 - return text.toUpperCase();
26 - }
27 - function Foo() {
28 - return {
29 - bar: [<Bar text="a" />, <Bar text="b" />],
30 - };
31 - }
32 - let data = [];
33 - ReactDOMFlightRelayServer.render(
34 - {
35 - foo: <Foo />,
36 - },
37 - data,
38 - );
39 -
27 + function readThrough(data) {
28 let response = ReactDOMFlightRelayClient.createResponse();
29 for (let i = 0; i < data.length; i++) {
30 let chunk = data[i];
@@ -53,6 +41,81 @@ describe('ReactFlightDOMRelay', () => {
41 }
42 let model = ReactDOMFlightRelayClient.getModelRoot(response).model;
43 ReactDOMFlightRelayClient.close(response);
56 - expect(model).toEqual({foo: {bar: ['A', 'B']}});
44 + return model;
45 + }
46 +
47 + function block(query, render) {
48 + return function(...args) {
49 + let curriedQuery = () => {
50 + return query(...args);
51 + };
52 + return [Symbol.for('react.server.block'), render, curriedQuery];
53 + };
54 + }
55 +
56 + it('can render a server component', () => {
57 + function Bar({text}) {
58 + return text.toUpperCase();
59 + }
60 + function Foo() {
61 + return {
62 + bar: (
63 + <div>
64 + <Bar text="a" />, <Bar text="b" />
65 + </div>
66 + ),
67 + };
68 + }
69 + let transport = [];
70 + ReactDOMFlightRelayServer.render(
71 + {
72 + foo: <Foo />,
73 + },
74 + transport,
75 + );
76 +
77 + let model = readThrough(transport);
78 + expect(model).toEqual({
79 + foo: {
80 + bar: (
81 + <div>
82 + {'A'}
83 + {', '}
84 + {'B'}
85 + </div>
86 + ),
87 + },
88 + });
89 + });
90 +
91 + it.experimental('can transfer a Block to the client and render there', () => {
92 + function Query(firstName, lastName) {
93 + return {name: firstName + ' ' + lastName};
94 + }
95 + function User(props, data) {
96 + return (
97 + <span>
98 + {props.greeting}, {data.name}
99 + </span>
100 + );
101 + }
102 + let loadUser = block(Query, User);
103 + let model = {
104 + User: loadUser('Seb', 'Smith'),
105 + };
106 +
107 + let transport = [];
108 + ReactDOMFlightRelayServer.render(model, transport);
109 +
110 + let modelClient = readThrough(transport);
111 +
112 + let container = document.createElement('div');
113 + let root = ReactDOM.createRoot(container);
114 + act(() => {
115 + let UserClient = modelClient.User;
116 + root.render(<UserClient greeting="Hello" />);
117 + });
118 +
119 + expect(container.innerHTML).toEqual('<span>Hello, Seb Smith</span>');
120 });
121 });
packages/react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js
+4 -3
@@ -33,9 +33,8 @@ type Thenable = {
33 // replicate it in user space. null means that it has already loaded.
34 const chunkCache: Map<string, null | Thenable | Error> = new Map();
35
36 -// Returning null means that all dependencies are fulfilled and we
37 -// can synchronously require the module now. A thenable is returned
38 -// that when resolved, means we can try again.
36 +// Start preloading the modules since we might need them soon.
37 +// This function doesn't suspend.
38 export function preloadModule<T>(moduleData: ModuleReference<T>): void {
39 let chunks = moduleData.chunks;
40 for (let i = 0; i < chunks.length; i++) {
@@ -51,6 +50,8 @@ export function preloadModule<T>(moduleData: ModuleReference<T>): void {
50 }
51 }
52
53 +// Actually require the module or suspend if it's not yet ready.
54 +// Increase priority if necessary.
55 export function requireModule<T>(moduleData: ModuleReference<T>): T {
56 let chunks = moduleData.chunks;
57 for (let i = 0; i < chunks.length; i++) {
packages/react-flight-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+56 -15
@@ -17,6 +17,13 @@ global.TextDecoder = require('util').TextDecoder;
17 // TODO: we can replace this with FlightServer.act().
18 global.setImmediate = cb => cb();
19
20 +let webpackModuleIdx = 0;
21 +let webpackModules = {};
22 +let webpackMap = {};
23 +global.__webpack_require__ = function(id) {
24 + return webpackModules[id];
25 +};
26 +
27 let act;
28 let Stream;
29 let React;
@@ -27,6 +34,8 @@ let ReactFlightDOMClient;
34 describe('ReactFlightDOM', () => {
35 beforeEach(() => {
36 jest.resetModules();
37 + webpackModules = {};
38 + webpackMap = {};
39 act = require('react-dom/test-utils').act;
40 Stream = require('stream');
41 React = require('react');
@@ -53,6 +62,24 @@ describe('ReactFlightDOM', () => {
62 };
63 }
64
65 + function block(query, render) {
66 + let idx = webpackModuleIdx++;
67 + webpackModules[idx] = {
68 + d: render,
69 + };
70 + webpackMap['path/' + idx] = {
71 + id: '' + idx,
72 + chunks: [],
73 + name: 'd',
74 + };
75 + return function(...args) {
76 + let curriedQuery = () => {
77 + return query(...args);
78 + };
79 + return [Symbol.for('react.server.block'), 'path/' + idx, curriedQuery];
80 + };
81 + }
82 +
83 async function waitForSuspense(fn) {
84 while (true) {
85 try {
@@ -88,7 +115,7 @@ describe('ReactFlightDOM', () => {
115 }
116
117 let {writable, readable} = getTestStream();
91 - ReactFlightDOMServer.pipeToNodeWritable(<App />, writable);
118 + ReactFlightDOMServer.pipeToNodeWritable(<App />, writable, webpackMap);
119 let result = ReactFlightDOMClient.readFromReadableStream(readable);
120 await waitForSuspense(() => {
121 expect(result.model).toEqual({
@@ -136,7 +163,11 @@ describe('ReactFlightDOM', () => {
163 }
164
165 let {writable, readable} = getTestStream();
139 - ReactFlightDOMServer.pipeToNodeWritable(<RootModel />, writable);
166 + ReactFlightDOMServer.pipeToNodeWritable(
167 + <RootModel />,
168 + writable,
169 + webpackMap,
170 + );
171 let result = ReactFlightDOMClient.readFromReadableStream(readable);
172
173 let container = document.createElement('div');
@@ -170,7 +201,11 @@ describe('ReactFlightDOM', () => {
201 }
202
203 let {writable, readable} = getTestStream();
173 - ReactFlightDOMServer.pipeToNodeWritable(<RootModel />, writable);
204 + ReactFlightDOMServer.pipeToNodeWritable(
205 + <RootModel />,
206 + writable,
207 + webpackMap,
208 + );
209 let result = ReactFlightDOMClient.readFromReadableStream(readable);
210
211 let container = document.createElement('div');
@@ -202,7 +237,11 @@ describe('ReactFlightDOM', () => {
237 }
238
239 let {writable, readable} = getTestStream();
205 - ReactFlightDOMServer.pipeToNodeWritable(<RootModel />, writable);
240 + ReactFlightDOMServer.pipeToNodeWritable(
241 + <RootModel />,
242 + writable,
243 + webpackMap,
244 + );
245 let result = ReactFlightDOMClient.readFromReadableStream(readable);
246
247 let container = document.createElement('div');
@@ -213,7 +252,7 @@ describe('ReactFlightDOM', () => {
252 expect(container.innerHTML).toBe('<p>@div</p>');
253 });
254
216 - it.experimental('should progressively reveal chunks', async () => {
255 + it.experimental('should progressively reveal Blocks', async () => {
256 let {Suspense} = React;
257
258 class ErrorBoundary extends React.Component {
@@ -249,16 +288,20 @@ describe('ReactFlightDOM', () => {
288 reject(e);
289 };
290 });
252 - function DelayedText({children}) {
291 + function Query() {
292 if (promise) {
293 throw promise;
294 }
295 if (error) {
296 throw error;
297 }
298 + return 'data';
299 + }
300 + function DelayedText({children}, data) {
301 return <Text>{children}</Text>;
302 }
261 - return [DelayedText, _resolve, _reject];
303 + let _block = block(Query, DelayedText);
304 + return [_block(), _resolve, _reject];
305 }
306
307 const [FriendsModel, resolveFriendsModel] = makeDelayedText();
@@ -274,13 +317,11 @@ describe('ReactFlightDOM', () => {
317 games: <GamesModel>:games:</GamesModel>,
318 };
319 }
277 - function ProfileModel() {
278 - return {
279 - photos: <PhotosModel>:photos:</PhotosModel>,
280 - name: <NameModel>:name:</NameModel>,
281 - more: <ProfileMore />,
282 - };
283 - }
320 + let profileModel = {
321 + photos: <PhotosModel>:photos:</PhotosModel>,
322 + name: <NameModel>:name:</NameModel>,
323 + more: <ProfileMore />,
324 + };
325
326 // View
327 function ProfileDetails({result}) {
@@ -327,7 +368,7 @@ describe('ReactFlightDOM', () => {
368 }
369
370 let {writable, readable} = getTestStream();
330 - ReactFlightDOMServer.pipeToNodeWritable(<ProfileModel />, writable);
371 + ReactFlightDOMServer.pipeToNodeWritable(profileModel, writable, webpackMap);
372 let result = ReactFlightDOMClient.readFromReadableStream(readable);
373
374 let container = document.createElement('div');
packages/react-noop-renderer/flight-modules.js new
+23
@@ -0,0 +1,23 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +// This file is used as temporary storage for modules generated in Flight tests.
11 +let moduleIdx = 0;
12 +let modules: Map<string, Function> = new Map();
13 +
14 +// This simulates what the compiler will do when it replaces render functions with server blocks.
15 +export function saveModule(render: Function): string {
16 + let idx = '' + moduleIdx++;
17 + modules.set(idx, render);
18 + return idx;
19 +}
20 +
21 +export function readModule(idx: string): Function {
22 + return modules.get(idx);
23 +}
packages/react-noop-renderer/npm/flight-modules.js new
+16
@@ -0,0 +1,16 @@
1 +'use strict';
2 +
3 +// This file is used as temporary storage for modules generated in Flight tests.
4 +var moduleIdx = 0;
5 +var modules = new Map();
6 +
7 +// This simulates what the compiler will do when it replaces render functions with server blocks.
8 +exports.saveModule = function saveModule(render) {
9 + var idx = '' + moduleIdx++;
10 + modules.set(idx, render);
11 + return idx;
12 +};
13 +
14 +exports.readModule = function readModule(idx) {
15 + return modules.get(idx);
16 +};
packages/react-noop-renderer/package.json
+1
@@ -28,6 +28,7 @@
28 "persistent.js",
29 "server.js",
30 "flight-client.js",
31 + "flight-modules.js",
32 "flight-server.js",
33 "cjs/"
34 ]
packages/react-noop-renderer/src/ReactNoopFlightClient.js
+7 -7
@@ -16,6 +16,8 @@
16
17 import type {ReactModelRoot} from 'react-client/flight';
18
19 +import {readModule} from 'react-noop-renderer/flight-modules';
20 +
21 import ReactFlightClient from 'react-client/flight';
22
23 type Source = Array<string>;
@@ -27,14 +29,12 @@ const {
29 close,
30 } = ReactFlightClient({
31 supportsBinaryStreams: false,
30 - resolveModuleReference(name: string) {
31 - return name;
32 + resolveModuleReference(idx: string) {
33 + return idx;
34 },
33 - preloadModule(name: string) {},
34 - requireModule(name: string) {
35 - return function FakeModule() {
36 - return name;
37 - };
35 + preloadModule(idx: string) {},
36 + requireModule(idx: string) {
37 + return readModule(idx);
38 },
39 });
40
packages/react-noop-renderer/src/ReactNoopFlightServer.js
+4 -2
@@ -16,6 +16,8 @@
16
17 import type {ReactModel} from 'react-server/src/ReactFlightServer';
18
19 +import {saveModule} from 'react-noop-renderer/flight-modules';
20 +
21 import ReactFlightServer from 'react-server/flight';
22
23 type Destination = Array<string>;
@@ -40,8 +42,8 @@ const ReactNoopFlightServer = ReactFlightServer({
42 formatChunk(type: string, props: Object): Uint8Array {
43 return Buffer.from(JSON.stringify({type, props}), 'utf8');
44 },
43 - renderHostChildrenToString(children: React$Element<any>): string {
44 - throw new Error('The noop rendered do not support host components');
45 + resolveModuleMetaData(config: void, renderFn: Function) {
46 + return saveModule(renderFn);
47 },
48 });
49
packages/react-server/src/ReactFlightServer.js
+109 -50
@@ -11,8 +11,8 @@ import type {
11 Destination,
12 Chunk,
13 BundlerConfig,
14 - // ModuleReference,
15 - // ModuleMetaData,
14 + ModuleMetaData,
15 + ModuleReference,
16 } from './ReactFlightServerConfig';
17
18 import {
@@ -24,17 +24,25 @@ import {
24 close,
25 processModelChunk,
26 processErrorChunk,
27 - // resolveModuleMetaData,
27 + resolveModuleMetaData,
28 } from './ReactFlightServerConfig';
29
30 -import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
30 +import {
31 + REACT_BLOCK_TYPE,
32 + REACT_SERVER_BLOCK_TYPE,
33 + REACT_ELEMENT_TYPE,
34 + REACT_FRAGMENT_TYPE,
35 + REACT_LAZY_TYPE,
36 +} from 'shared/ReactSymbols';
37 +
38 +import invariant from 'shared/invariant';
39
40 type ReactJSONValue =
41 | string
42 | boolean
43 | number
44 | null
37 - | Array<ReactModel>
45 + | $ReadOnlyArray<ReactJSONValue>
46 | ReactModelObject;
47
48 export type ReactModel =
@@ -50,7 +58,7 @@ type ReactModelObject = {+[key: string]: ReactModel};
58
59 type Segment = {
60 id: number,
53 - model: ReactModel,
61 + query: () => ReactModel,
62 ping: () => void,
63 };
64
@@ -81,26 +89,31 @@ export function createRequest(
89 completedJSONChunks: [],
90 completedErrorChunks: [],
91 flowing: false,
84 - toJSON: (key: string, value: ReactModel) =>
85 - resolveModelToJSON(request, value),
92 + toJSON: function(key: string, value: ReactModel): ReactJSONValue {
93 + return resolveModelToJSON(request, this, key, value);
94 + },
95 };
96 request.pendingChunks++;
88 - let rootSegment = createSegment(request, model);
97 + let rootSegment = createSegment(request, () => model);
98 pingedSegments.push(rootSegment);
99 return request;
100 }
101
93 -function attemptResolveModelComponent(element: React$Element<any>): ReactModel {
102 +function attemptResolveElement(element: React$Element<any>): ReactModel {
103 let type = element.type;
104 let props = element.props;
105 if (typeof type === 'function') {
97 - // This is a nested view model.
106 + // This is a server-side component.
107 return type(props);
108 } else if (typeof type === 'string') {
109 // This is a host element. E.g. HTML.
110 return [REACT_ELEMENT_TYPE, type, element.key, element.props];
111 + } else if (type[0] === REACT_SERVER_BLOCK_TYPE) {
112 + return [REACT_ELEMENT_TYPE, type, element.key, element.props];
113 + } else if (type === REACT_FRAGMENT_TYPE) {
114 + return element.props.children;
115 } else {
103 - throw new Error('Unsupported type.');
116 + invariant(false, 'Unsupported type.');
117 }
118 }
119
@@ -112,11 +125,11 @@ function pingSegment(request: Request, segment: Segment): void {
125 }
126 }
127
115 -function createSegment(request: Request, model: ReactModel): Segment {
128 +function createSegment(request: Request, query: () => ReactModel): Segment {
129 let id = request.nextChunkId++;
130 let segment = {
131 id,
119 - model,
132 + query,
133 ping: () => pingSegment(request, segment),
134 };
135 return segment;
@@ -127,9 +140,9 @@ function serializeIDRef(id: number): string {
140 }
141
142 function escapeStringValue(value: string): string {
130 - if (value[0] === '$') {
131 - // We need to escape $ prefixed strings since we use that to encode
132 - // references to IDs and as a special symbol value.
143 + if (value[0] === '$' || value[0] === '@') {
144 + // We need to escape $ or @ prefixed strings since we use those to encode
145 + // references to IDs and as special symbol values.
146 return '$' + value;
147 } else {
148 return value;
@@ -138,39 +151,95 @@ function escapeStringValue(value: string): string {
151
152 export function resolveModelToJSON(
153 request: Request,
154 + parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray<ReactModel>,
155 + key: string,
156 value: ReactModel,
157 ): ReactJSONValue {
143 - if (typeof value === 'string') {
144 - return escapeStringValue(value);
158 + // Special Symbols
159 + switch (value) {
160 + case REACT_ELEMENT_TYPE:
161 + return '$';
162 + case REACT_SERVER_BLOCK_TYPE:
163 + return '@';
164 + case REACT_LAZY_TYPE:
165 + case REACT_BLOCK_TYPE:
166 + invariant(
167 + false,
168 + 'React Blocks (and Lazy Components) are expected to be replaced by a ' +
169 + 'compiler on the server. Try configuring your compiler set up and avoid ' +
170 + 'using React.lazy inside of Blocks.',
171 + );
172 }
173
147 - if (value === REACT_ELEMENT_TYPE) {
148 - return '$';
174 + if (parent[0] === REACT_SERVER_BLOCK_TYPE) {
175 + // We're currently encoding part of a Block. Look up which key.
176 + switch (key) {
177 + case '1': {
178 + // Module reference
179 + let moduleReference: ModuleReference = (value: any);
180 + try {
181 + let moduleMetaData: ModuleMetaData = resolveModuleMetaData(
182 + request.bundlerConfig,
183 + moduleReference,
184 + );
185 + return (moduleMetaData: ReactJSONValue);
186 + } catch (x) {
187 + request.pendingChunks++;
188 + let errorId = request.nextChunkId++;
189 + emitErrorChunk(request, errorId, x);
190 + return serializeIDRef(errorId);
191 + }
192 + }
193 + case '2': {
194 + // Query
195 + let query: () => ReactModel = (value: any);
196 + try {
197 + // Attempt to resolve the query.
198 + return query();
199 + } catch (x) {
200 + if (
201 + typeof x === 'object' &&
202 + x !== null &&
203 + typeof x.then === 'function'
204 + ) {
205 + // Something suspended, we'll need to create a new segment and resolve it later.
206 + request.pendingChunks++;
207 + let newSegment = createSegment(request, query);
208 + let ping = newSegment.ping;
209 + x.then(ping, ping);
210 + return serializeIDRef(newSegment.id);
211 + } else {
212 + // This query failed, encode the error as a separate row and reference that.
213 + request.pendingChunks++;
214 + let errorId = request.nextChunkId++;
215 + emitErrorChunk(request, errorId, x);
216 + return serializeIDRef(errorId);
217 + }
218 + }
219 + }
220 + default: {
221 + invariant(
222 + false,
223 + 'A server block should never encode any other slots. This is a bug in React.',
224 + );
225 + }
226 + }
227 + }
228 +
229 + if (typeof value === 'string') {
230 + return escapeStringValue(value);
231 }
232
233 + // Resolve server components.
234 while (
235 typeof value === 'object' &&
236 value !== null &&
237 value.$$typeof === REACT_ELEMENT_TYPE
238 ) {
239 + // TODO: Concatenate keys of parents onto children.
240 + // TODO: Allow elements to suspend independently and serialize as references to future elements.
241 let element: React$Element<any> = (value: any);
157 - try {
158 - value = attemptResolveModelComponent(element);
159 - } catch (x) {
160 - if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
161 - // Something suspended, we'll need to create a new segment and resolve it later.
162 - request.pendingChunks++;
163 - let newSegment = createSegment(request, element);
164 - let ping = newSegment.ping;
165 - x.then(ping, ping);
166 - return serializeIDRef(newSegment.id);
167 - } else {
168 - request.pendingChunks++;
169 - let errorId = request.nextChunkId++;
170 - emitErrorChunk(request, errorId, x);
171 - return serializeIDRef(errorId);
172 - }
173 - }
242 + value = attemptResolveElement(element);
243 }
244
245 return value;
@@ -198,19 +267,9 @@ function emitErrorChunk(request: Request, id: number, error: mixed): void {
267 }
268
269 function retrySegment(request: Request, segment: Segment): void {
201 - let value = segment.model;
270 + let query = segment.query;
271 try {
203 - while (
204 - typeof value === 'object' &&
205 - value !== null &&
206 - value.$$typeof === REACT_ELEMENT_TYPE
207 - ) {
208 - // If this is a nested model, there's no need to create another chunk,
209 - // we can reuse the existing one and try again.
210 - let element: React$Element<any> = (value: any);
211 - segment.model = element;
212 - value = attemptResolveModelComponent(element);
213 - }
272 + let value = query();
273 let processedChunk = processModelChunk(request, segment.id, value);
274 request.completedJSONChunks.push(processedChunk);
275 } catch (x) {
packages/react/src/ReactBlock.js
+1 -1
@@ -17,7 +17,7 @@ import {
17 } from 'shared/ReactSymbols';
18
19 type BlockQueryFunction<Args: Iterable<any>, Data> = (...args: Args) => Data;
20 -type BlockRenderFunction<Props, Data> = (
20 +export type BlockRenderFunction<Props, Data> = (
21 props: Props,
22 data: Data,
23 ) => React$Node;
packages/shared/ReactSymbols.js
+3
@@ -52,6 +52,9 @@ export const REACT_SUSPENSE_LIST_TYPE = hasSymbol
52 export const REACT_MEMO_TYPE = hasSymbol ? Symbol.for('react.memo') : 0xead3;
53 export const REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4;
54 export const REACT_BLOCK_TYPE = hasSymbol ? Symbol.for('react.block') : 0xead9;
55 +export const REACT_SERVER_BLOCK_TYPE = hasSymbol
56 + ? Symbol.for('react.server.block')
57 + : 0xeada;
58 export const REACT_FUNDAMENTAL_TYPE = hasSymbol
59 ? Symbol.for('react.fundamental')
60 : 0xead5;
packages/shared/isValidElementType.js
+3 -1
@@ -23,6 +23,7 @@ import {
23 REACT_RESPONDER_TYPE,
24 REACT_SCOPE_TYPE,
25 REACT_BLOCK_TYPE,
26 + REACT_SERVER_BLOCK_TYPE,
27 } from 'shared/ReactSymbols';
28
29 export default function isValidElementType(type: mixed) {
@@ -46,6 +47,7 @@ export default function isValidElementType(type: mixed) {
47 type.$$typeof === REACT_FUNDAMENTAL_TYPE ||
48 type.$$typeof === REACT_RESPONDER_TYPE ||
49 type.$$typeof === REACT_SCOPE_TYPE ||
49 - type.$$typeof === REACT_BLOCK_TYPE))
50 + type.$$typeof === REACT_BLOCK_TYPE ||
51 + type[(0: any)] === REACT_SERVER_BLOCK_TYPE))
52 );
53 }
scripts/error-codes/codes.json
+4 -1
@@ -348,5 +348,8 @@
348 "347": "Maps are not valid as a React child (found: %s). Consider converting children to an array of keyed ReactElements instead.",
349 "348": "ensureListeningTo(): received a container that was not an element node. This is likely a bug in React.",
350 "349": "Expected a work-in-progress root. This is a bug in React. Please file an issue.",
351 - "350": "Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue."
351 + "350": "Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.",
352 + "351": "Unsupported type.",
353 + "352": "React Blocks (and Lazy Components) are expected to be replaced by a compiler on the server. Try configuring your compiler set up and avoid using React.lazy inside of Blocks.",
354 + "353": "A server block should never encode any other slots. This is a bug in React."
355 }
scripts/flow/react-relay-hooks.js
+4 -4
@@ -9,11 +9,11 @@
9
10 type JSONValue =
11 | string
12 - | number
12 | boolean
13 + | number
14 | null
15 - | {[key: string]: JSONValue}
16 - | Array<JSONValue>;
15 + | {+[key: string]: JSONValue}
16 + | $ReadOnlyArray<JSONValue>;
17
18 declare module 'ReactFlightDOMRelayServerIntegration' {
19 declare export opaque type Destination;
@@ -31,7 +31,7 @@ declare module 'ReactFlightDOMRelayServerIntegration' {
31 declare export function close(destination: Destination): void;
32
33 declare export opaque type ModuleReference;
34 - declare export opaque type ModuleMetaData;
34 + declare export type ModuleMetaData = JSONValue;
35 declare export function resolveModuleMetaData(
36 resourceReference: ModuleReference,
37 ): ModuleMetaData;
scripts/rollup/bundles.js
+12 -2
@@ -405,7 +405,12 @@ const bundles = [
405 moduleType: RENDERER,
406 entry: 'react-noop-renderer/flight-server',
407 global: 'ReactNoopFlightServer',
408 - externals: ['react', 'scheduler', 'expect'],
408 + externals: [
409 + 'react',
410 + 'scheduler',
411 + 'expect',
412 + 'react-noop-renderer/flight-modules',
413 + ],
414 },
415
416 /******* React Noop Flight Client (used for tests) *******/
@@ -414,7 +419,12 @@ const bundles = [
419 moduleType: RENDERER,
420 entry: 'react-noop-renderer/flight-client',
421 global: 'ReactNoopFlightClient',
417 - externals: ['react', 'scheduler', 'expect'],
422 + externals: [
423 + 'react',
424 + 'scheduler',
425 + 'expect',
426 + 'react-noop-renderer/flight-modules',
427 + ],
428 },
429
430 /******* React Reconciler *******/
scripts/rollup/validate/eslintrc.cjs.js
+4
@@ -32,6 +32,10 @@ module.exports = {
32 // Flight
33 Uint8Array: true,
34 Promise: true,
35 +
36 + // Flight Webpack
37 + __webpack_chunk_load__: true,
38 + __webpack_require__: true,
39 },
40 parserOptions: {
41 ecmaVersion: 5,
scripts/rollup/validate/eslintrc.umd.js
+4
@@ -36,6 +36,10 @@ module.exports = {
36 // Flight
37 Uint8Array: true,
38 Promise: true,
39 +
40 + // Flight Webpack
41 + __webpack_chunk_load__: true,
42 + __webpack_require__: true,
43 },
44 parserOptions: {
45 ecmaVersion: 5,