@samitouri / QOS-React-2 / commits / 60144a04da

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.

Sebastian Markbåge committed Feb 21, 2023 at 13:18 UTC 60144a04da7970e30266f591dbcd67afe1097e41
28 files changed +657 -124
fixtures/flight/loader/index.js
+21 -4
@@ -23,8 +23,17 @@ async function babelLoad(url, context, defaultLoad) {
23 const result = await defaultLoad(url, context, defaultLoad);
24 if (result.format === 'module') {
25 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'};
37 }
38 return defaultLoad(url, context, defaultLoad);
39 }
@@ -39,8 +48,16 @@ async function babelTransformSource(source, context, defaultTransformSource) {
48 const {format} = context;
49 if (format === 'module') {
50 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};
61 }
62 return defaultTransformSource(source, context, defaultTransformSource);
63 }
fixtures/flight/server/handler.js
+3 -1
@@ -1,11 +1,13 @@
1 'use strict';
2
3 -const {renderToPipeableStream} = require('react-server-dom-webpack/server');
3 const {readFile} = require('fs').promises;
4 const {resolve} = require('path');
5 const React = require('react');
6
7 module.exports = async function (req, res) {
8 + const {renderToPipeableStream} = await import(
9 + 'react-server-dom-webpack/server'
10 + );
11 switch (req.method) {
12 case 'POST': {
13 const serverReference = JSON.parse(req.get('rsc-action'));
packages/react-client/src/ReactFlightClientHostConfigNode.js new
+34
@@ -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
+1 -1
@@ -7,6 +7,6 @@
7 * @flow
8 */
9
10 -export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
10 +export * from 'react-client/src/ReactFlightClientHostConfigNode';
11 export * from 'react-client/src/ReactFlightClientHostConfigStream';
12 export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node.js
+2 -2
@@ -7,6 +7,6 @@
7 * @flow
8 */
9
10 -export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
10 +export * from 'react-client/src/ReactFlightClientHostConfigNode';
11 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
+1 -1
@@ -7,4 +7,4 @@
7 * @flow
8 */
9
10 -export * from './src/ReactFlightDOMClient';
10 +export * from './src/ReactFlightDOMClientBrowser';
packages/react-server-dom-webpack/client.edge.js
+1 -1
@@ -7,4 +7,4 @@
7 * @flow
8 */
9
10 -export * from './src/ReactFlightDOMClient';
10 +export * from './src/ReactFlightDOMClientEdge';
packages/react-server-dom-webpack/client.node.js
+1 -1
@@ -7,4 +7,4 @@
7 * @flow
8 */
9
10 -export * from './src/ReactFlightDOMClient';
10 +export * from './src/ReactFlightDOMClientNode';
packages/react-server-dom-webpack/client.node.unbundled.js
+1 -1
@@ -7,4 +7,4 @@
7 * @flow
8 */
9
10 -export * from './src/ReactFlightDOMClient';
10 +export * from './src/ReactFlightDOMClientNode';
packages/react-server-dom-webpack/esm/react-server-dom-webpack-node-loader.production.min.js renamed
packages/react-server-dom-webpack/package.json
+1 -1
@@ -64,7 +64,7 @@
64 "./server.edge": "./server.edge.js",
65 "./server.node": "./server.node.js",
66 "./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",
68 "./node-register": "./node-register.js",
69 "./src/*": "./src/*",
70 "./package.json": "./package.json"
packages/react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig.js new
+98
@@ -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/ReactFlightDOMClientBrowser.js renamed
+10 -15
@@ -11,8 +11,6 @@ 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 -
14 import {
15 createResponse,
16 getRoot,
@@ -28,10 +26,16 @@ type CallServerCallback = <A, T>(
26 ) => Promise<T>;
27
28 export type Options = {
31 - moduleMap?: BundlerConfig,
29 callServer?: CallServerCallback,
30 };
31
32 +function createResponseFromOptions(options: void | Options) {
33 + return createResponse(
34 + null,
35 + options && options.callServer ? options.callServer : undefined,
36 + );
37 +}
38 +
39 function startReadingFromStream(
40 response: FlightResponse,
41 stream: ReadableStream,
@@ -63,10 +67,7 @@ function createFromReadableStream<T>(
67 stream: ReadableStream,
68 options?: Options,
69 ): 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);
71 startReadingFromStream(response, stream);
72 return getRoot(response);
73 }
@@ -75,10 +76,7 @@ function createFromFetch<T>(
76 promiseForResponse: Promise<Response>,
77 options?: Options,
78 ): 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);
80 promiseForResponse.then(
81 function (r) {
82 startReadingFromStream(response, (r.body: any));
@@ -94,10 +92,7 @@ function createFromXHR<T>(
92 request: XMLHttpRequest,
93 options?: Options,
94 ): 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);
96 let processedLength = 0;
97 function progress(e: ProgressEvent): void {
98 const chunk = request.responseText;
packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js new
+95
@@ -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};
packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js new
+54
@@ -0,0 +1,54 @@
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} from 'react-client/src/ReactFlightClientStream';
13 +
14 +import type {BundlerConfig} from 'react-client/src/ReactFlightClientHostConfig';
15 +
16 +import type {Readable} from 'stream';
17 +
18 +import {
19 + createResponse,
20 + getRoot,
21 + reportGlobalError,
22 + processBinaryChunk,
23 + close,
24 +} from 'react-client/src/ReactFlightClientStream';
25 +import {processStringChunk} from '../../react-client/src/ReactFlightClientStream';
26 +
27 +function noServerCall() {
28 + throw new Error(
29 + 'Server Functions cannot be called during initial render. ' +
30 + 'This would create a fetch waterfall. Try to use a Server Component ' +
31 + 'to pass data to Client Components instead.',
32 + );
33 +}
34 +
35 +function createFromNodeStream<T>(
36 + stream: Readable,
37 + moduleMap: $NonMaybeType<BundlerConfig>,
38 +): Thenable<T> {
39 + const response: Response = createResponse(moduleMap, noServerCall);
40 + stream.on('data', chunk => {
41 + if (typeof chunk === 'string') {
42 + processStringChunk(response, chunk, 0);
43 + } else {
44 + processBinaryChunk(response, chunk);
45 + }
46 + });
47 + stream.on('error', error => {
48 + reportGlobalError(response, error);
49 + });
50 + stream.on('end', () => close(response));
51 + return getRoot(response);
52 +}
53 +
54 +export {createFromNodeStream};
packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js
+39 -13
@@ -55,7 +55,8 @@ type Options = {
55 isServer: boolean,
56 clientReferences?: ClientReferencePath | $ReadOnlyArray<ClientReferencePath>,
57 chunkName?: string,
58 - manifestFilename?: string,
58 + clientManifestFilename?: string,
59 + ssrManifestFilename?: string,
60 };
61
62 const PLUGIN_NAME = 'React Server Plugin';
@@ -63,7 +64,8 @@ const PLUGIN_NAME = 'React Server Plugin';
64 export default class ReactFlightWebpackPlugin {
65 clientReferences: $ReadOnlyArray<ClientReferencePath>;
66 chunkName: string;
66 - manifestFilename: string;
67 + clientManifestFilename: string;
68 + ssrManifestFilename: string;
69
70 constructor(options: Options) {
71 if (!options || typeof options.isServer !== 'boolean') {
@@ -99,8 +101,10 @@ export default class ReactFlightWebpackPlugin {
101 } else {
102 this.chunkName = 'client[index]';
103 }
102 - this.manifestFilename =
103 - options.manifestFilename || 'react-client-manifest.json';
104 + this.clientManifestFilename =
105 + options.clientManifestFilename || 'react-client-manifest.json';
106 + this.ssrManifestFilename =
107 + options.ssrManifestFilename || 'react-ssr-manifest.json';
108 }
109
110 apply(compiler: any) {
@@ -209,15 +213,20 @@ export default class ReactFlightWebpackPlugin {
213 if (clientFileNameFound === false) {
214 compilation.warnings.push(
215 new WebpackError(
212 - `Client runtime at ${clientImportName} was not found. React Server Components module map file ${_this.manifestFilename} was not created.`,
216 + `Client runtime at ${clientImportName} was not found. React Server Components module map file ${_this.clientManifestFilename} was not created.`,
217 ),
218 );
219 return;
220 }
221
218 - const json: {
222 + const clientManifest: {
223 [string]: {
220 - [string]: {chunks: $FlowFixMe, id: $FlowFixMe, name: string},
224 + [string]: {chunks: $FlowFixMe, id: string, name: string},
225 + },
226 + } = {};
227 + const ssrManifest: {
228 + [string]: {
229 + [string]: {specifier: string, name: string},
230 },
231 } = {};
232 compilation.chunkGroups.forEach(function (chunkGroup) {
@@ -239,9 +248,16 @@ export default class ReactFlightWebpackPlugin {
248 .getExportsInfo(module)
249 .getProvidedExports();
250
242 - const moduleExports: {
251 + const clientExports: {
252 [string]: {chunks: $FlowFixMe, id: $FlowFixMe, name: string},
253 } = {};
254 +
255 + const ssrExports: {
256 + [string]: {specifier: string, name: string},
257 + } = {};
258 +
259 + ssrManifest[id] = ssrExports;
260 +
261 ['', '*']
262 .concat(
263 Array.isArray(moduleProvidedExports)
@@ -249,16 +265,21 @@ export default class ReactFlightWebpackPlugin {
265 : [],
266 )
267 .forEach(function (name) {
252 - moduleExports[name] = {
268 + clientExports[name] = {
269 id,
270 chunks: chunkIds,
271 name: name,
272 };
273 + ssrExports[name] = {
274 + specifier: module.resource,
275 + name: name,
276 + };
277 });
278 const href = pathToFileURL(module.resource).href;
279
280 if (href !== undefined) {
261 - json[href] = moduleExports;
281 + clientManifest[href] = clientExports;
282 + ssrManifest[href] = ssrExports;
283 }
284 }
285
@@ -280,10 +301,15 @@ export default class ReactFlightWebpackPlugin {
301 });
302 });
303
283 - const output = JSON.stringify(json, null, 2);
304 + const clientOutput = JSON.stringify(clientManifest, null, 2);
305 + compilation.emitAsset(
306 + _this.clientManifestFilename,
307 + new sources.RawSource(clientOutput, false),
308 + );
309 + const ssrOutput = JSON.stringify(ssrManifest, null, 2);
310 compilation.emitAsset(
285 - _this.manifestFilename,
286 - new sources.RawSource(output, false),
311 + _this.ssrManifestFilename,
312 + new sources.RawSource(ssrOutput, false),
313 );
314 },
315 );
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
-62
@@ -18,12 +18,10 @@ global.TextDecoder = require('util').TextDecoder;
18 let clientExports;
19 let serverExports;
20 let webpackMap;
21 -let webpackModules;
21 let webpackServerMap;
22 let act;
23 let React;
24 let ReactDOMClient;
26 -let ReactDOMServer;
25 let ReactServerDOMWriter;
26 let ReactServerDOMReader;
27 let Suspense;
@@ -37,29 +35,15 @@ describe('ReactFlightDOMBrowser', () => {
35 clientExports = WebpackMock.clientExports;
36 serverExports = WebpackMock.serverExports;
37 webpackMap = WebpackMock.webpackMap;
40 - webpackModules = WebpackMock.webpackModules;
38 webpackServerMap = WebpackMock.webpackServerMap;
39 React = require('react');
40 ReactDOMClient = require('react-dom/client');
44 - ReactDOMServer = require('react-dom/server.browser');
41 ReactServerDOMWriter = require('react-server-dom-webpack/server.browser');
42 ReactServerDOMReader = require('react-server-dom-webpack/client');
43 Suspense = React.Suspense;
44 use = React.use;
45 });
46
51 - async function readResult(stream) {
52 - const reader = stream.getReader();
53 - let result = '';
54 - while (true) {
55 - const {done, value} = await reader.read();
56 - if (done) {
57 - return result;
58 - }
59 - result += Buffer.from(value).toString('utf8');
60 - }
61 - }
62 -
47 function makeDelayedText(Model) {
48 let error, _resolve, _reject;
49 let promise = new Promise((resolve, reject) => {
@@ -466,52 +450,6 @@ describe('ReactFlightDOMBrowser', () => {
450 expect(isDone).toBeTruthy();
451 });
452
469 - // @gate enableUseHook
470 - it('should allow an alternative module mapping to be used for SSR', async () => {
471 - function ClientComponent() {
472 - return <span>Client Component</span>;
473 - }
474 - // The Client build may not have the same IDs as the Server bundles for the same
475 - // component.
476 - const ClientComponentOnTheClient = clientExports(ClientComponent);
477 - const ClientComponentOnTheServer = clientExports(ClientComponent);
478 -
479 - // In the SSR bundle this module won't exist. We simulate this by deleting it.
480 - const clientId = webpackMap[ClientComponentOnTheClient.filepath]['*'].id;
481 - delete webpackModules[clientId];
482 -
483 - // Instead, we have to provide a translation from the client meta data to the SSR
484 - // meta data.
485 - const ssrMetadata = webpackMap[ClientComponentOnTheServer.filepath]['*'];
486 - const translationMap = {
487 - [clientId]: {
488 - '*': ssrMetadata,
489 - },
490 - };
491 -
492 - function App() {
493 - return <ClientComponentOnTheClient />;
494 - }
495 -
496 - const stream = ReactServerDOMWriter.renderToReadableStream(
497 - <App />,
498 - webpackMap,
499 - );
500 - const response = ReactServerDOMReader.createFromReadableStream(stream, {
501 - moduleMap: translationMap,
502 - });
503 -
504 - function ClientRoot() {
505 - return use(response);
506 - }
507 -
508 - const ssrStream = await ReactDOMServer.renderToReadableStream(
509 - <ClientRoot />,
510 - );
511 - const result = await readResult(ssrStream);
512 - expect(result).toEqual('<span>Client Component</span>');
513 - });
514 -
453 // @gate enableUseHook
454 it('should be able to complete after aborting and throw the reason client-side', async () => {
455 const reportedErrors = [];
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js new
+98
@@ -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 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +// Polyfills for test environment
13 +global.ReadableStream =
14 + require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15 +global.TextEncoder = require('util').TextEncoder;
16 +global.TextDecoder = require('util').TextDecoder;
17 +
18 +let clientExports;
19 +let webpackMap;
20 +let webpackModules;
21 +let React;
22 +let ReactDOMServer;
23 +let ReactServerDOMWriter;
24 +let ReactServerDOMReader;
25 +let use;
26 +
27 +describe('ReactFlightDOMEdge', () => {
28 + beforeEach(() => {
29 + jest.resetModules();
30 + const WebpackMock = require('./utils/WebpackMock');
31 + clientExports = WebpackMock.clientExports;
32 + webpackMap = WebpackMock.webpackMap;
33 + webpackModules = WebpackMock.webpackModules;
34 + React = require('react');
35 + ReactDOMServer = require('react-dom/server.edge');
36 + ReactServerDOMWriter = require('react-server-dom-webpack/server.edge');
37 + ReactServerDOMReader = require('react-server-dom-webpack/client.edge');
38 + use = React.use;
39 + });
40 +
41 + async function readResult(stream) {
42 + const reader = stream.getReader();
43 + let result = '';
44 + while (true) {
45 + const {done, value} = await reader.read();
46 + if (done) {
47 + return result;
48 + }
49 + result += Buffer.from(value).toString('utf8');
50 + }
51 + }
52 +
53 + // @gate enableUseHook
54 + it('should allow an alternative module mapping to be used for SSR', async () => {
55 + function ClientComponent() {
56 + return <span>Client Component</span>;
57 + }
58 + // The Client build may not have the same IDs as the Server bundles for the same
59 + // component.
60 + const ClientComponentOnTheClient = clientExports(ClientComponent);
61 + const ClientComponentOnTheServer = clientExports(ClientComponent);
62 +
63 + // In the SSR bundle this module won't exist. We simulate this by deleting it.
64 + const clientId = webpackMap[ClientComponentOnTheClient.filepath]['*'].id;
65 + delete webpackModules[clientId];
66 +
67 + // Instead, we have to provide a translation from the client meta data to the SSR
68 + // meta data.
69 + const ssrMetadata = webpackMap[ClientComponentOnTheServer.filepath]['*'];
70 + const translationMap = {
71 + [clientId]: {
72 + '*': ssrMetadata,
73 + },
74 + };
75 +
76 + function App() {
77 + return <ClientComponentOnTheClient />;
78 + }
79 +
80 + const stream = ReactServerDOMWriter.renderToReadableStream(
81 + <App />,
82 + webpackMap,
83 + );
84 + const response = ReactServerDOMReader.createFromReadableStream(stream, {
85 + moduleMap: translationMap,
86 + });
87 +
88 + function ClientRoot() {
89 + return use(response);
90 + }
91 +
92 + const ssrStream = await ReactDOMServer.renderToReadableStream(
93 + <ClientRoot />,
94 + );
95 + const result = await readResult(ssrStream);
96 + expect(result).toEqual('<span>Client Component</span>');
97 + });
98 +});
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js new
+108
@@ -0,0 +1,108 @@
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 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +// Don't wait before processing work on the server.
13 +// TODO: we can replace this with FlightServer.act().
14 +global.setImmediate = cb => cb();
15 +
16 +let clientExports;
17 +let webpackMap;
18 +let webpackModules;
19 +let React;
20 +let ReactDOMServer;
21 +let ReactServerDOMWriter;
22 +let ReactServerDOMReader;
23 +let Stream;
24 +let use;
25 +
26 +describe('ReactFlightDOMNode', () => {
27 + beforeEach(() => {
28 + jest.resetModules();
29 + const WebpackMock = require('./utils/WebpackMock');
30 + clientExports = WebpackMock.clientExports;
31 + webpackMap = WebpackMock.webpackMap;
32 + webpackModules = WebpackMock.webpackModules;
33 + React = require('react');
34 + ReactDOMServer = require('react-dom/server.node');
35 + ReactServerDOMWriter = require('react-server-dom-webpack/server.node');
36 + ReactServerDOMReader = require('react-server-dom-webpack/client.node');
37 + Stream = require('stream');
38 + use = React.use;
39 + });
40 +
41 + function readResult(stream) {
42 + return new Promise((resolve, reject) => {
43 + let buffer = '';
44 + const writable = new Stream.PassThrough();
45 + writable.setEncoding('utf8');
46 + writable.on('data', chunk => {
47 + buffer += chunk;
48 + });
49 + writable.on('error', error => {
50 + reject(error);
51 + });
52 + writable.on('end', () => {
53 + resolve(buffer);
54 + });
55 + stream.pipe(writable);
56 + });
57 + }
58 +
59 + // @gate enableUseHook
60 + it('should allow an alternative module mapping to be used for SSR', async () => {
61 + function ClientComponent() {
62 + return <span>Client Component</span>;
63 + }
64 + // The Client build may not have the same IDs as the Server bundles for the same
65 + // component.
66 + const ClientComponentOnTheClient = clientExports(ClientComponent);
67 + const ClientComponentOnTheServer = clientExports(ClientComponent);
68 +
69 + // In the SSR bundle this module won't exist. We simulate this by deleting it.
70 + const clientId = webpackMap[ClientComponentOnTheClient.filepath]['*'].id;
71 + delete webpackModules[clientId];
72 +
73 + // Instead, we have to provide a translation from the client meta data to the SSR
74 + // meta data.
75 + const ssrMetadata = webpackMap[ClientComponentOnTheServer.filepath]['*'];
76 + const translationMap = {
77 + [clientId]: {
78 + '*': ssrMetadata,
79 + },
80 + };
81 +
82 + function App() {
83 + return <ClientComponentOnTheClient />;
84 + }
85 +
86 + const stream = ReactServerDOMWriter.renderToPipeableStream(
87 + <App />,
88 + webpackMap,
89 + );
90 + const readable = new Stream.PassThrough();
91 + const response = ReactServerDOMReader.createFromNodeStream(
92 + readable,
93 + translationMap,
94 + );
95 +
96 + stream.pipe(readable);
97 +
98 + function ClientRoot() {
99 + return use(response);
100 + }
101 +
102 + const ssrStream = await ReactDOMServer.renderToPipeableStream(
103 + <ClientRoot />,
104 + );
105 + const result = await readResult(ssrStream);
106 + expect(result).toEqual('<span>Client Component</span>');
107 + });
108 +});
scripts/flow/environment.js
+17
@@ -141,6 +141,23 @@ declare module 'util' {
141 declare function deprecate(f: Function, string: string): Function;
142 declare function promisify(f: Function): Function;
143 declare function callbackify(f: Function): Function;
144 + declare class TextDecoder {
145 + constructor(
146 + encoding?: string,
147 + options?: {
148 + fatal?: boolean,
149 + ignoreBOM?: boolean,
150 + ...
151 + },
152 + ): void;
153 + decode(
154 + input?: ArrayBuffer | DataView | $TypedArray,
155 + options?: {stream?: boolean, ...},
156 + ): string;
157 + encoding: string;
158 + fatal: boolean;
159 + ignoreBOM: boolean;
160 + }
161 declare class TextEncoder {
162 constructor(encoding?: string): TextEncoder;
163 encode(buffer: string): Uint8Array;
scripts/rollup/build.js
+18 -7
@@ -19,6 +19,7 @@ const Sync = require('./sync');
19 const sizes = require('./plugins/sizes-plugin');
20 const useForks = require('./plugins/use-forks-plugin');
21 const stripUnusedImports = require('./plugins/strip-unused-imports');
22 +const dynamicImports = require('./plugins/dynamic-imports');
23 const Packaging = require('./packaging');
24 const {asyncRimRaf} = require('./utils');
25 const codeFrame = require('@babel/code-frame');
@@ -45,7 +46,8 @@ process.on('unhandledRejection', err => {
46
47 const {
48 NODE_ES2015,
48 - NODE_ESM,
49 + ESM_DEV,
50 + ESM_PROD,
51 UMD_DEV,
52 UMD_PROD,
53 UMD_PROFILING,
@@ -216,7 +218,8 @@ function getFormat(bundleType) {
218 case RN_FB_PROD:
219 case RN_FB_PROFILING:
220 return `cjs`;
219 - case NODE_ESM:
221 + case ESM_DEV:
222 + case ESM_PROD:
223 return `es`;
224 case BROWSER_SCRIPT:
225 return `iife`;
@@ -226,8 +229,8 @@ function getFormat(bundleType) {
229 function isProductionBundleType(bundleType) {
230 switch (bundleType) {
231 case NODE_ES2015:
229 - case NODE_ESM:
232 return true;
233 + case ESM_DEV:
234 case UMD_DEV:
235 case NODE_DEV:
236 case BUN_DEV:
@@ -235,6 +238,7 @@ function isProductionBundleType(bundleType) {
238 case RN_OSS_DEV:
239 case RN_FB_DEV:
240 return false;
241 + case ESM_PROD:
242 case UMD_PROD:
243 case NODE_PROD:
244 case BUN_PROD:
@@ -256,7 +260,6 @@ function isProductionBundleType(bundleType) {
260 function isProfilingBundleType(bundleType) {
261 switch (bundleType) {
262 case NODE_ES2015:
259 - case NODE_ESM:
263 case FB_WWW_DEV:
264 case FB_WWW_PROD:
265 case NODE_DEV:
@@ -267,6 +270,8 @@ function isProfilingBundleType(bundleType) {
270 case RN_FB_PROD:
271 case RN_OSS_DEV:
272 case RN_OSS_PROD:
273 + case ESM_DEV:
274 + case ESM_PROD:
275 case UMD_DEV:
276 case UMD_PROD:
277 case BROWSER_SCRIPT:
@@ -328,6 +333,8 @@ function getPlugins(
333 bundleType === RN_FB_PROFILING;
334 const shouldStayReadable = isFBWWWBundle || isRNBundle || forcePrettyOutput;
335 return [
336 + // Keep dynamic imports as externals
337 + dynamicImports(),
338 {
339 name: 'rollup-plugin-flow-remove-types',
340 transform(code) {
@@ -385,7 +392,7 @@ function getPlugins(
392 // Apply dead code elimination and/or minification.
393 // closure doesn't yet support leaving ESM imports intact
394 isProduction &&
388 - bundleType !== NODE_ESM &&
395 + bundleType !== ESM_PROD &&
396 closure({
397 compilation_level: 'SIMPLE',
398 language_in: 'ECMASCRIPT_2020',
@@ -396,7 +403,9 @@ function getPlugins(
403 ? 'ECMASCRIPT5'
404 : 'ECMASCRIPT5_STRICT',
405 emit_use_strict:
399 - bundleType !== BROWSER_SCRIPT && bundleType !== NODE_ESM,
406 + bundleType !== BROWSER_SCRIPT &&
407 + bundleType !== ESM_PROD &&
408 + bundleType !== ESM_DEV,
409 env: 'CUSTOM',
410 warning_level: 'QUIET',
411 apply_input_source_maps: false,
@@ -404,6 +413,7 @@ function getPlugins(
413 process_common_js_modules: false,
414 rewrite_polyfills: false,
415 inject_libraries: false,
416 + allow_dynamic_import: true,
417
418 // Don't let it create global variables in the browser.
419 // https://github.com/facebook/react/issues/10909
@@ -740,7 +750,8 @@ async function buildEverything() {
750 for (const bundle of Bundles.bundles) {
751 bundles.push(
752 [bundle, NODE_ES2015],
743 - [bundle, NODE_ESM],
753 + [bundle, ESM_DEV],
754 + [bundle, ESM_PROD],
755 [bundle, UMD_DEV],
756 [bundle, UMD_PROD],
757 [bundle, UMD_PROFILING],
scripts/rollup/bundles.js
+11 -7
@@ -9,7 +9,8 @@ const __EXPERIMENTAL__ =
9
10 const bundleTypes = {
11 NODE_ES2015: 'NODE_ES2015',
12 - NODE_ESM: 'NODE_ESM',
12 + ESM_DEV: 'ESM_DEV',
13 + ESM_PROD: 'ESM_PROD',
14 UMD_DEV: 'UMD_DEV',
15 UMD_PROD: 'UMD_PROD',
16 UMD_PROFILING: 'UMD_PROFILING',
@@ -32,7 +33,8 @@ const bundleTypes = {
33
34 const {
35 NODE_ES2015,
35 - NODE_ESM,
36 + ESM_DEV,
37 + ESM_PROD,
38 UMD_DEV,
39 UMD_PROD,
40 UMD_PROFILING,
@@ -393,7 +395,7 @@ const bundles = [
395 global: 'ReactServerDOMClient',
396 minifyWithProdErrorCodes: false,
397 wrapWithModuleBoundaries: false,
396 - externals: ['react'],
398 + externals: ['react', 'util'],
399 },
400 {
401 bundleTypes: [NODE_DEV, NODE_PROD],
@@ -402,7 +404,7 @@ const bundles = [
404 global: 'ReactServerDOMClient',
405 minifyWithProdErrorCodes: false,
406 wrapWithModuleBoundaries: false,
405 - externals: ['react'],
407 + externals: ['react', 'util'],
408 },
409 {
410 bundleTypes: [NODE_DEV, NODE_PROD],
@@ -427,7 +429,7 @@ const bundles = [
429
430 /******* React Server DOM Webpack Node.js Loader *******/
431 {
430 - bundleTypes: [NODE_ESM],
432 + bundleTypes: [ESM_PROD],
433 moduleType: RENDERER_UTILS,
434 entry: 'react-server-dom-webpack/node-loader',
435 global: 'ReactServerWebpackNodeLoader',
@@ -1025,12 +1027,14 @@ function getFilename(bundle, bundleType) {
1027 switch (bundleType) {
1028 case NODE_ES2015:
1029 return `${name}.js`;
1028 - case NODE_ESM:
1029 - return `${name}.js`;
1030 case BUN_DEV:
1031 return `${name}.development.js`;
1032 case BUN_PROD:
1033 return `${name}.production.min.js`;
1034 + case ESM_DEV:
1035 + return `${name}.development.js`;
1036 + case ESM_PROD:
1037 + return `${name}.production.min.js`;
1038 case UMD_DEV:
1039 return `${name}.development.js`;
1040 case UMD_PROD:
scripts/rollup/packaging.js
+4 -2
@@ -17,7 +17,8 @@ const {
17
18 const {
19 NODE_ES2015,
20 - NODE_ESM,
20 + ESM_DEV,
21 + ESM_PROD,
22 UMD_DEV,
23 UMD_PROD,
24 UMD_PROFILING,
@@ -49,7 +50,8 @@ function getBundleOutputPath(bundle, bundleType, filename, packageName) {
50 switch (bundleType) {
51 case NODE_ES2015:
52 return `build/node_modules/${packageName}/cjs/${filename}`;
52 - case NODE_ESM:
53 + case ESM_DEV:
54 + case ESM_PROD:
55 return `build/node_modules/${packageName}/esm/${filename}`;
56 case BUN_DEV:
57 case BUN_PROD:
scripts/rollup/plugins/dynamic-imports.js new
+19
@@ -0,0 +1,19 @@
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 +'use strict';
8 +
9 +module.exports = function dynamicImports() {
10 + return {
11 + name: 'scripts/rollup/plugins/dynamic-imports',
12 + renderDynamicImport({targetModuleId}) {
13 + if (targetModuleId === null) {
14 + return {left: 'import(', right: ')'};
15 + }
16 + return null;
17 + },
18 + };
19 +};
scripts/rollup/validate/eslintrc.cjs.js
+1 -1
@@ -53,7 +53,7 @@ module.exports = {
53 IS_REACT_ACT_ENVIRONMENT: 'readonly',
54 },
55 parserOptions: {
56 - ecmaVersion: 5,
56 + ecmaVersion: 2020,
57 sourceType: 'script',
58 },
59 rules: {
scripts/rollup/validate/eslintrc.esm.js
+1 -1
@@ -52,7 +52,7 @@ module.exports = {
52 IS_REACT_ACT_ENVIRONMENT: 'readonly',
53 },
54 parserOptions: {
55 - ecmaVersion: 2017,
55 + ecmaVersion: 2020,
56 sourceType: 'module',
57 },
58 rules: {
scripts/rollup/wrappers.js
+16 -3
@@ -6,7 +6,8 @@ const {bundleTypes, moduleTypes} = require('./bundles');
6
7 const {
8 NODE_ES2015,
9 - NODE_ESM,
9 + ESM_DEV,
10 + ESM_PROD,
11 UMD_DEV,
12 UMD_PROD,
13 UMD_PROFILING,
@@ -66,8 +67,20 @@ ${license}
67 ${source}`;
68 },
69
69 - /***************** NODE_ESM *****************/
70 - [NODE_ESM](source, globalName, filename, moduleType) {
70 + /***************** ESM_DEV *****************/
71 + [ESM_DEV](source, globalName, filename, moduleType) {
72 + return `/**
73 +* @license React
74 + * ${filename}
75 + *
76 +${license}
77 + */
78 +
79 +${source}`;
80 + },
81 +
82 + /***************** ESM_PROD *****************/
83 + [ESM_PROD](source, globalName, filename, moduleType) {
84 return `/**
85 * @license React
86 * ${filename}
scripts/shared/inlinedHostConfigs.js
+2
@@ -83,6 +83,7 @@ module.exports = [
83 'react-server-dom-webpack/client',
84 'react-server-dom-webpack/client.browser',
85 'react-server-dom-webpack/server.browser',
86 + 'react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js', // react-server-dom-webpack/client.browser
87 'react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js', // react-server-dom-webpack/server.browser
88 'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations.
89 'react-devtools',
@@ -114,6 +115,7 @@ module.exports = [
115 'react-server-dom-webpack',
116 'react-server-dom-webpack/client.edge',
117 'react-server-dom-webpack/server.edge',
118 + 'react-server-dom-webpack/src/ReactFlightDOMClientEdge.js', // react-server-dom-webpack/client.edge
119 'react-server-dom-webpack/src/ReactFlightDOMServerEdge.js', // react-server-dom-webpack/server.edge
120 'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations.
121 'react-devtools',