[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,