Skip to content

Commit 60144a0

Browse files
authored
Split out Edge and Node implementations of the Flight Client (#26187)
This splits out the Edge and Node implementations of Flight Client into their own implementations. The Node implementation now takes a Node Stream as input. I removed the bundler config from the Browser variant because you're never supposed to use that in the browser since it's only for SSR. Similarly, it's required on the server. This also enables generating a SSR manifest from the Webpack plugin. This is necessary for SSR so that you can reverse look up what a client module is called on the server. I also removed the option to pass a callServer from the server. We might want to add it back in the future but basically, we don't recommend calling Server Functions from render for initial render because if that happened client-side it would be a client-side waterfall. If it's never called in initial render, then it also shouldn't ever happen during SSR. This might be considered too restrictive. ~This also compiles the unbundled packages as ESM. This isn't strictly necessary because we only need access to dynamic import to load the modules but we don't have any other build options that leave `import(...)` intact, and seems appropriate that this would also be an ESM module.~ Went with `import(...)` in CJS instead.
1 parent 70b0bbd commit 60144a0

28 files changed

+657
-124
lines changed

fixtures/flight/loader/index.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,17 @@ async function babelLoad(url, context, defaultLoad) {
2323
const result = await defaultLoad(url, context, defaultLoad);
2424
if (result.format === 'module') {
2525
const opt = Object.assign({filename: url}, babelOptions);
26-
const {code} = await babel.transformAsync(result.source, opt);
27-
return {source: code, format: 'module'};
26+
const newResult = await babel.transformAsync(result.source, opt);
27+
if (!newResult) {
28+
if (typeof result.source === 'string') {
29+
return result;
30+
}
31+
return {
32+
source: Buffer.from(result.source).toString('utf8'),
33+
format: 'module',
34+
};
35+
}
36+
return {source: newResult.code, format: 'module'};
2837
}
2938
return defaultLoad(url, context, defaultLoad);
3039
}
@@ -39,8 +48,16 @@ async function babelTransformSource(source, context, defaultTransformSource) {
3948
const {format} = context;
4049
if (format === 'module') {
4150
const opt = Object.assign({filename: context.url}, babelOptions);
42-
const {code} = await babel.transformAsync(source, opt);
43-
return {source: code};
51+
const newResult = await babel.transformAsync(source, opt);
52+
if (!newResult) {
53+
if (typeof source === 'string') {
54+
return {source};
55+
}
56+
return {
57+
source: Buffer.from(source).toString('utf8'),
58+
};
59+
}
60+
return {source: newResult.code};
4461
}
4562
return defaultTransformSource(source, context, defaultTransformSource);
4663
}

fixtures/flight/server/handler.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
'use strict';
22

3-
const {renderToPipeableStream} = require('react-server-dom-webpack/server');
43
const {readFile} = require('fs').promises;
54
const {resolve} = require('path');
65
const React = require('react');
76

87
module.exports = async function (req, res) {
8+
const {renderToPipeableStream} = await import(
9+
'react-server-dom-webpack/server'
10+
);
911
switch (req.method) {
1012
case 'POST': {
1113
const serverReference = JSON.parse(req.get('rsc-action'));
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and 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+
import {TextDecoder} from 'util';
11+
12+
export type StringDecoder = TextDecoder;
13+
14+
export const supportsBinaryStreams = true;
15+
16+
export function createStringDecoder(): StringDecoder {
17+
return new TextDecoder();
18+
}
19+
20+
const decoderOptions = {stream: true};
21+
22+
export function readPartialStringChunk(
23+
decoder: StringDecoder,
24+
buffer: Uint8Array,
25+
): string {
26+
return decoder.decode(buffer, decoderOptions);
27+
}
28+
29+
export function readFinalStringChunk(
30+
decoder: StringDecoder,
31+
buffer: Uint8Array,
32+
): string {
33+
return decoder.decode(buffer);
34+
}

packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node-webpack.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
* @flow
88
*/
99

10-
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
10+
export * from 'react-client/src/ReactFlightClientHostConfigNode';
1111
export * from 'react-client/src/ReactFlightClientHostConfigStream';
1212
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';

packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
* @flow
88
*/
99

10-
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
10+
export * from 'react-client/src/ReactFlightClientHostConfigNode';
1111
export * from 'react-client/src/ReactFlightClientHostConfigStream';
12-
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
12+
export * from 'react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig';

packages/react-server-dom-webpack/client.browser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
* @flow
88
*/
99

10-
export * from './src/ReactFlightDOMClient';
10+
export * from './src/ReactFlightDOMClientBrowser';

packages/react-server-dom-webpack/client.edge.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
* @flow
88
*/
99

10-
export * from './src/ReactFlightDOMClient';
10+
export * from './src/ReactFlightDOMClientEdge';

packages/react-server-dom-webpack/client.node.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
* @flow
88
*/
99

10-
export * from './src/ReactFlightDOMClient';
10+
export * from './src/ReactFlightDOMClientNode';

packages/react-server-dom-webpack/client.node.unbundled.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
* @flow
88
*/
99

10-
export * from './src/ReactFlightDOMClient';
10+
export * from './src/ReactFlightDOMClientNode';

packages/react-server-dom-webpack/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"./server.edge": "./server.edge.js",
6565
"./server.node": "./server.node.js",
6666
"./server.node.unbundled": "./server.node.unbundled.js",
67-
"./node-loader": "./esm/react-server-dom-webpack-node-loader.js",
67+
"./node-loader": "./esm/react-server-dom-webpack-node-loader.production.min.js",
6868
"./node-register": "./node-register.js",
6969
"./src/*": "./src/*",
7070
"./package.json": "./package.json"
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and 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+
import type {
11+
Thenable,
12+
FulfilledThenable,
13+
RejectedThenable,
14+
} from 'shared/ReactTypes';
15+
16+
export type WebpackSSRMap = {
17+
[clientId: string]: {
18+
[clientExportName: string]: ClientReference<any>,
19+
},
20+
};
21+
22+
export type BundlerConfig = WebpackSSRMap;
23+
24+
export opaque type ClientReferenceMetadata = {
25+
id: string,
26+
chunks: Array<string>,
27+
name: string,
28+
async: boolean,
29+
};
30+
31+
// eslint-disable-next-line no-unused-vars
32+
export opaque type ClientReference<T> = {
33+
specifier: string,
34+
name: string,
35+
};
36+
37+
export function resolveClientReference<T>(
38+
bundlerConfig: BundlerConfig,
39+
metadata: ClientReferenceMetadata,
40+
): ClientReference<T> {
41+
const resolvedModuleData = bundlerConfig[metadata.id][metadata.name];
42+
return resolvedModuleData;
43+
}
44+
45+
const asyncModuleCache: Map<string, Thenable<any>> = new Map();
46+
47+
export function preloadModule<T>(
48+
metadata: ClientReference<T>,
49+
): null | Thenable<any> {
50+
const existingPromise = asyncModuleCache.get(metadata.specifier);
51+
if (existingPromise) {
52+
if (existingPromise.status === 'fulfilled') {
53+
return null;
54+
}
55+
return existingPromise;
56+
} else {
57+
// $FlowFixMe[unsupported-syntax]
58+
const modulePromise: Thenable<T> = import(metadata.specifier);
59+
modulePromise.then(
60+
value => {
61+
const fulfilledThenable: FulfilledThenable<mixed> =
62+
(modulePromise: any);
63+
fulfilledThenable.status = 'fulfilled';
64+
fulfilledThenable.value = value;
65+
},
66+
reason => {
67+
const rejectedThenable: RejectedThenable<mixed> = (modulePromise: any);
68+
rejectedThenable.status = 'rejected';
69+
rejectedThenable.reason = reason;
70+
},
71+
);
72+
asyncModuleCache.set(metadata.specifier, modulePromise);
73+
return modulePromise;
74+
}
75+
}
76+
77+
export function requireModule<T>(metadata: ClientReference<T>): T {
78+
let moduleExports;
79+
// We assume that preloadModule has been called before, which
80+
// should have added something to the module cache.
81+
const promise: any = asyncModuleCache.get(metadata.specifier);
82+
if (promise.status === 'fulfilled') {
83+
moduleExports = promise.value;
84+
} else {
85+
throw promise.reason;
86+
}
87+
if (metadata.name === '*') {
88+
// This is a placeholder value that represents that the caller imported this
89+
// as a CommonJS module as is.
90+
return moduleExports;
91+
}
92+
if (metadata.name === '') {
93+
// This is a placeholder value that represents that the caller accessed the
94+
// default property of this if it was an ESM interop module.
95+
return moduleExports.default;
96+
}
97+
return moduleExports[metadata.name];
98+
}

packages/react-server-dom-webpack/src/ReactFlightDOMClient.js renamed to packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import type {Thenable} from 'shared/ReactTypes.js';
1111

1212
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream';
1313

14-
import type {BundlerConfig} from './ReactFlightClientWebpackBundlerConfig';
15-
1614
import {
1715
createResponse,
1816
getRoot,
@@ -28,10 +26,16 @@ type CallServerCallback = <A, T>(
2826
) => Promise<T>;
2927

3028
export type Options = {
31-
moduleMap?: BundlerConfig,
3229
callServer?: CallServerCallback,
3330
};
3431

32+
function createResponseFromOptions(options: void | Options) {
33+
return createResponse(
34+
null,
35+
options && options.callServer ? options.callServer : undefined,
36+
);
37+
}
38+
3539
function startReadingFromStream(
3640
response: FlightResponse,
3741
stream: ReadableStream,
@@ -63,10 +67,7 @@ function createFromReadableStream<T>(
6367
stream: ReadableStream,
6468
options?: Options,
6569
): Thenable<T> {
66-
const response: FlightResponse = createResponse(
67-
options && options.moduleMap ? options.moduleMap : null,
68-
options && options.callServer ? options.callServer : undefined,
69-
);
70+
const response: FlightResponse = createResponseFromOptions(options);
7071
startReadingFromStream(response, stream);
7172
return getRoot(response);
7273
}
@@ -75,10 +76,7 @@ function createFromFetch<T>(
7576
promiseForResponse: Promise<Response>,
7677
options?: Options,
7778
): Thenable<T> {
78-
const response: FlightResponse = createResponse(
79-
options && options.moduleMap ? options.moduleMap : null,
80-
options && options.callServer ? options.callServer : undefined,
81-
);
79+
const response: FlightResponse = createResponseFromOptions(options);
8280
promiseForResponse.then(
8381
function (r) {
8482
startReadingFromStream(response, (r.body: any));
@@ -94,10 +92,7 @@ function createFromXHR<T>(
9492
request: XMLHttpRequest,
9593
options?: Options,
9694
): Thenable<T> {
97-
const response: FlightResponse = createResponse(
98-
options && options.moduleMap ? options.moduleMap : null,
99-
options && options.callServer ? options.callServer : undefined,
100-
);
95+
const response: FlightResponse = createResponseFromOptions(options);
10196
let processedLength = 0;
10297
function progress(e: ProgressEvent): void {
10398
const chunk = request.responseText;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and 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+
import type {Thenable} from 'shared/ReactTypes.js';
11+
12+
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream';
13+
14+
import type {BundlerConfig} from './ReactFlightClientWebpackBundlerConfig';
15+
16+
import {
17+
createResponse,
18+
getRoot,
19+
reportGlobalError,
20+
processBinaryChunk,
21+
close,
22+
} from 'react-client/src/ReactFlightClientStream';
23+
24+
function noServerCall() {
25+
throw new Error(
26+
'Server Functions cannot be called during initial render. ' +
27+
'This would create a fetch waterfall. Try to use a Server Component ' +
28+
'to pass data to Client Components instead.',
29+
);
30+
}
31+
32+
export type Options = {
33+
moduleMap?: BundlerConfig,
34+
};
35+
36+
function createResponseFromOptions(options: void | Options) {
37+
return createResponse(
38+
options && options.moduleMap ? options.moduleMap : null,
39+
noServerCall,
40+
);
41+
}
42+
43+
function startReadingFromStream(
44+
response: FlightResponse,
45+
stream: ReadableStream,
46+
): void {
47+
const reader = stream.getReader();
48+
function progress({
49+
done,
50+
value,
51+
}: {
52+
done: boolean,
53+
value: ?any,
54+
...
55+
}): void | Promise<void> {
56+
if (done) {
57+
close(response);
58+
return;
59+
}
60+
const buffer: Uint8Array = (value: any);
61+
processBinaryChunk(response, buffer);
62+
return reader.read().then(progress).catch(error);
63+
}
64+
function error(e: any) {
65+
reportGlobalError(response, e);
66+
}
67+
reader.read().then(progress).catch(error);
68+
}
69+
70+
function createFromReadableStream<T>(
71+
stream: ReadableStream,
72+
options?: Options,
73+
): Thenable<T> {
74+
const response: FlightResponse = createResponseFromOptions(options);
75+
startReadingFromStream(response, stream);
76+
return getRoot(response);
77+
}
78+
79+
function createFromFetch<T>(
80+
promiseForResponse: Promise<Response>,
81+
options?: Options,
82+
): Thenable<T> {
83+
const response: FlightResponse = createResponseFromOptions(options);
84+
promiseForResponse.then(
85+
function (r) {
86+
startReadingFromStream(response, (r.body: any));
87+
},
88+
function (e) {
89+
reportGlobalError(response, e);
90+
},
91+
);
92+
return getRoot(response);
93+
}
94+
95+
export {createFromFetch, createFromReadableStream};

0 commit comments

Comments
 (0)