@samitouri / QOS-React / commits / 701ac2e572

[Flight][Float] Preinitialize module imports during SSR (#27314)

Currently when we SSR a Flight response we do not emit any resources for module imports. This means that when the client hydrates it won't have already loaded the necessary scripts to satisfy the Imports defined in the Flight payload which will lead to a delay in hydration completing. This change updates `react-server-dom-webpack` and `react-server-dom-esm` to emit async script tags in the head when we encounter a modules in the flight response. To support this we need some additional server configuration. We need to know the path prefix for chunk loading and whether the chunks will load with CORS or not (and if so with what configuration).

Josh Story committed Sep 27, 2023 at 09:53 UTC 701ac2e57290f913502c6012c15e815f412f84b7
48 files changed +1021 -288
fixtures/flight-esm/.nvmrc new
+1
@@ -0,0 +1 @@
1 +v18
fixtures/flight-esm/server/global.js
+28 -10
@@ -10,6 +10,7 @@ const compress = require('compression');
10 const chalk = require('chalk');
11 const express = require('express');
12 const http = require('http');
13 +const React = require('react');
14
15 const {renderToPipeableStream} = require('react-dom/server');
16 const {createFromNodeStream} = require('react-server-dom-esm/client');
@@ -62,23 +63,39 @@ app.all('/', async function (req, res, next) {
63 if (req.accepts('text/html')) {
64 try {
65 const rscResponse = await promiseForData;
65 -
66 const moduleBaseURL = '/src';
67
68 // For HTML, we're a "client" emulator that runs the client code,
69 // so we start by consuming the RSC payload. This needs the local file path
70 // to load the source files from as well as the URL path for preloads.
71 - const root = await createFromNodeStream(
72 - rscResponse,
73 - moduleBasePath,
74 - moduleBaseURL
75 - );
71 +
72 + let root;
73 + let Root = () => {
74 + if (root) {
75 + return React.use(root);
76 + }
77 +
78 + return React.use(
79 + (root = createFromNodeStream(
80 + rscResponse,
81 + moduleBasePath,
82 + moduleBaseURL
83 + ))
84 + );
85 + };
86 // Render it into HTML by resolving the client components
87 res.set('Content-type', 'text/html');
78 - const {pipe} = renderToPipeableStream(root, {
79 - // TODO: bootstrapModules inserts a preload before the importmap which causes
80 - // the import map to be invalid. We need to fix that in Float somehow.
81 - // bootstrapModules: ['/src/index.js'],
88 + const {pipe} = renderToPipeableStream(React.createElement(Root), {
89 + importMap: {
90 + imports: {
91 + react: 'https://esm.sh/react@experimental?pin=v124&dev',
92 + 'react-dom': 'https://esm.sh/react-dom@experimental?pin=v124&dev',
93 + 'react-dom/': 'https://esm.sh/react-dom@experimental&pin=v124&dev/',
94 + 'react-server-dom-esm/client':
95 + '/node_modules/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js',
96 + },
97 + },
98 + bootstrapModules: ['/src/index.js'],
99 });
100 pipe(res);
101 } catch (e) {
@@ -89,6 +106,7 @@ app.all('/', async function (req, res, next) {
106 } else {
107 try {
108 const rscResponse = await promiseForData;
109 +
110 // For other request, we pass-through the RSC payload.
111 res.set('Content-type', 'text/x-component');
112 rscResponse.on('data', data => {
fixtures/flight-esm/src/App.js
+1 -19
@@ -9,16 +9,6 @@ import {getServerState} from './ServerState.js';
9
10 const h = React.createElement;
11
12 -const importMap = {
13 - imports: {
14 - react: 'https://esm.sh/react@experimental?pin=v124&dev',
15 - 'react-dom': 'https://esm.sh/react-dom@experimental?pin=v124&dev',
16 - 'react-dom/': 'https://esm.sh/react-dom@experimental&pin=v124&dev/',
17 - 'react-server-dom-esm/client':
18 - '/node_modules/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js',
19 - },
20 -};
21 -
12 export default async function App() {
13 const res = await fetch('http://localhost:3001/todos');
14 const todos = await res.json();
@@ -42,12 +32,6 @@ export default async function App() {
32 rel: 'stylesheet',
33 href: '/src/style.css',
34 precedence: 'default',
45 - }),
46 - h('script', {
47 - type: 'importmap',
48 - dangerouslySetInnerHTML: {
49 - __html: JSON.stringify(importMap),
50 - },
35 })
36 ),
37 h(
@@ -84,9 +68,7 @@ export default async function App() {
68 'Like'
69 )
70 )
87 - ),
88 - // TODO: Move this to bootstrapModules.
89 - h('script', {type: 'module', src: '/src/index.js'})
71 + )
72 )
73 );
74 }
fixtures/flight-esm/yarn.lock
+11 -11
@@ -540,17 +540,17 @@ raw-body@2.5.2:
540 unpipe "1.0.0"
541
542 react-dom@experimental:
543 - version "0.0.0-experimental-018c58c9c-20230601"
544 - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-experimental-018c58c9c-20230601.tgz#2cc0ac824b83bab2ac1c6187f241dbd5dcd5201b"
545 - integrity sha512-hwRsyoG1R3Tub0nUa72YvNcqPvU+pTcr9dadOnUCKKfSiYVbBCy7LxmkqLauCD8OjNJMlwtMgG4UAgtidclYGQ==
543 + version "0.0.0-experimental-b9be4537c-20230905"
544 + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-experimental-b9be4537c-20230905.tgz#b078d6d06041e0c98ce5a2f5e9ff26a2e308eb41"
545 + integrity sha512-veAFNVj81lUYhYlucYm3kbj2BhakG57XYkWC/QHVEZDk4Hm2qxM9RUk7gn8dWs9Eq7KR6Q+JWiSH3ZbObQTV9g==
546 dependencies:
547 loose-envify "^1.1.0"
548 - scheduler "0.0.0-experimental-018c58c9c-20230601"
548 + scheduler "0.0.0-experimental-b9be4537c-20230905"
549
550 react@experimental:
551 - version "0.0.0-experimental-018c58c9c-20230601"
552 - resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-experimental-018c58c9c-20230601.tgz#ab04d1243c8f83b0166ed342056fa6b38ab2cd23"
553 - integrity sha512-nSQIBsZ26Ii899pZ9cRt/6uQLbIUEAcDIivvAQyaHp4pWm289aB+7AK7VCWojAJIf4OStCuWs2berZsk4mzLVg==
551 + version "0.0.0-experimental-b9be4537c-20230905"
552 + resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-experimental-b9be4537c-20230905.tgz#3c2352b42b8024544a12dcd96f2700313cebcb6b"
553 + integrity sha512-QNeK74S7AU94j4vCxet2S76HqxpF6CJo1pG3XcgY2NravyXdWYszrRDNHrfu86gGNwAQvSU+YpStYn/i0b9tLA==
554 dependencies:
555 loose-envify "^1.1.0"
556
@@ -588,10 +588,10 @@ safe-buffer@5.1.2:
588 resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
589 integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
590
591 -scheduler@0.0.0-experimental-018c58c9c-20230601:
592 - version "0.0.0-experimental-018c58c9c-20230601"
593 - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-018c58c9c-20230601.tgz#4f083614f8e857bab63dd90b4b37b03783dafe6b"
594 - integrity sha512-otUM7AAAnCoJ5/0jTQwUQ7NhxjgcPEdrfzW7NfkpocrDoTUbql1kIGIhj9L9POMVFDI/wcZzRNK/oIEWsB4DPw==
591 +scheduler@0.0.0-experimental-b9be4537c-20230905:
592 + version "0.0.0-experimental-b9be4537c-20230905"
593 + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-b9be4537c-20230905.tgz#f0fe5a710ce15a9d637c28e9f019a4100e1f3f34"
594 + integrity sha512-V5P9LOS+c5CG7qaCJu+Qgcz9eh/dP4nBszj3w1MCgZnMtAna6+J8ZuuUnRDMeY86F8KH+cY8Q5beIvAL2noMzA==
595 dependencies:
596 loose-envify "^1.1.0"
597
fixtures/flight/.nvmrc new
+1
@@ -0,0 +1 @@
1 +v18
fixtures/flight/server/global.js
+34 -7
@@ -33,6 +33,7 @@ const compress = require('compression');
33 const chalk = require('chalk');
34 const express = require('express');
35 const http = require('http');
36 +const React = require('react');
37
38 const {renderToPipeableStream} = require('react-dom/server');
39 const {createFromNodeStream} = require('react-server-dom-webpack/client');
@@ -62,6 +63,11 @@ if (process.env.NODE_ENV === 'development') {
63 webpackMiddleware(compiler, {
64 publicPath: paths.publicUrlOrPath.slice(0, -1),
65 serverSideRender: true,
66 + headers: () => {
67 + return {
68 + 'Cache-Control': 'no-store, must-revalidate',
69 + };
70 + },
71 })
72 );
73 app.use(webpackHotMiddleware(compiler));
@@ -121,12 +127,13 @@ app.all('/', async function (req, res, next) {
127 buildPath = path.join(__dirname, '../build/');
128 }
129 // Read the module map from the virtual file system.
124 - const moduleMap = JSON.parse(
130 + const ssrManifest = JSON.parse(
131 await virtualFs.readFile(
132 path.join(buildPath, 'react-ssr-manifest.json'),
133 'utf8'
134 )
135 );
136 +
137 // Read the entrypoints containing the initial JS to bootstrap everything.
138 // For other pages, the chunks in the RSC payload are enough.
139 const mainJSChunks = JSON.parse(
@@ -138,15 +145,35 @@ app.all('/', async function (req, res, next) {
145 // For HTML, we're a "client" emulator that runs the client code,
146 // so we start by consuming the RSC payload. This needs a module
147 // map that reverse engineers the client-side path to the SSR path.
141 - const {root, formState} = await createFromNodeStream(
142 - rscResponse,
143 - moduleMap
144 - );
148 +
149 + // This is a bad hack to set the form state after SSR has started. It works
150 + // because we block the root component until we have the form state and
151 + // any form that reads it necessarily will come later. It also only works
152 + // because the formstate type is an object which may change in the future
153 + const lazyFormState = [];
154 +
155 + let cachedResult = null;
156 + async function getRootAndFormState() {
157 + const {root, formState} = await createFromNodeStream(
158 + rscResponse,
159 + ssrManifest
160 + );
161 + // We shouldn't be assuming formState is an object type but at the moment
162 + // we have no way of setting the form state from within the render
163 + Object.assign(lazyFormState, formState);
164 + return root;
165 + }
166 + let Root = () => {
167 + if (!cachedResult) {
168 + cachedResult = getRootAndFormState();
169 + }
170 + return React.use(cachedResult);
171 + };
172 // Render it into HTML by resolving the client components
173 res.set('Content-type', 'text/html');
147 - const {pipe} = renderToPipeableStream(root, {
174 + const {pipe} = renderToPipeableStream(React.createElement(Root), {
175 bootstrapScripts: mainJSChunks,
149 - experimental_formState: formState,
176 + experimental_formState: lazyFormState,
177 });
178 pipe(res);
179 } catch (e) {
packages/react-client/src/ReactFlightClient.js
+17 -3
@@ -13,8 +13,9 @@ import type {LazyComponent} from 'react/src/ReactLazy';
13 import type {
14 ClientReference,
15 ClientReferenceMetadata,
16 - SSRManifest,
16 + SSRModuleMap,
17 StringDecoder,
18 + ModuleLoading,
19 } from './ReactFlightClientConfig';
20
21 import type {
@@ -36,6 +37,7 @@ import {
37 readPartialStringChunk,
38 readFinalStringChunk,
39 createStringDecoder,
40 + prepareDestinationForModule,
41 } from './ReactFlightClientConfig';
42
43 import {registerServerReference} from './ReactFlightReplyClient';
@@ -178,8 +180,10 @@ Chunk.prototype.then = function <T>(
180 };
181
182 export type Response = {
181 - _bundlerConfig: SSRManifest,
183 + _bundlerConfig: SSRModuleMap,
184 + _moduleLoading: ModuleLoading,
185 _callServer: CallServerCallback,
186 + _nonce: ?string,
187 _chunks: Map<number, SomeChunk<any>>,
188 _fromJSON: (key: string, value: JSONValue) => any,
189 _stringDecoder: StringDecoder,
@@ -706,13 +710,17 @@ function missingCall() {
710 }
711
712 export function createResponse(
709 - bundlerConfig: SSRManifest,
713 + bundlerConfig: SSRModuleMap,
714 + moduleLoading: ModuleLoading,
715 callServer: void | CallServerCallback,
716 + nonce: void | string,
717 ): Response {
718 const chunks: Map<number, SomeChunk<any>> = new Map();
719 const response: Response = {
720 _bundlerConfig: bundlerConfig,
721 + _moduleLoading: moduleLoading,
722 _callServer: callServer !== undefined ? callServer : missingCall,
723 + _nonce: nonce,
724 _chunks: chunks,
725 _stringDecoder: createStringDecoder(),
726 _fromJSON: (null: any),
@@ -774,6 +782,12 @@ function resolveModule(
782 clientReferenceMetadata,
783 );
784
785 + prepareDestinationForModule(
786 + response._moduleLoading,
787 + response._nonce,
788 + clientReferenceMetadata,
789 + );
790 +
791 // TODO: Add an option to encode modules that are lazy loaded.
792 // For now we preload all modules as early as possible since it's likely
793 // that we'll need them.
packages/react-client/src/forks/ReactFlightClientConfig.custom.js
+4 -1
@@ -25,7 +25,8 @@
25
26 declare var $$$config: any;
27
28 -export opaque type SSRManifest = mixed;
28 +export opaque type ModuleLoading = mixed;
29 +export opaque type SSRModuleMap = mixed;
30 export opaque type ServerManifest = mixed;
31 export opaque type ServerReferenceId = string;
32 export opaque type ClientReferenceMetadata = mixed;
@@ -35,6 +36,8 @@ export const resolveServerReference = $$$config.resolveServerReference;
36 export const preloadModule = $$$config.preloadModule;
37 export const requireModule = $$$config.requireModule;
38 export const dispatchHint = $$$config.dispatchHint;
39 +export const prepareDestinationForModule =
40 + $$$config.prepareDestinationForModule;
41 export const usedWithSSR = true;
42
43 export opaque type Source = mixed;
packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js
+2 -1
@@ -8,6 +8,7 @@
8 */
9
10 export * from 'react-client/src/ReactFlightClientConfigBrowser';
11 -export * from 'react-server-dom-esm/src/ReactFlightClientConfigESMBundler';
11 +export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM';
12 +export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser';
13 export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
14 export const usedWithSSR = false;
packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js
+3 -1
@@ -8,6 +8,8 @@
8 */
9
10 export * from 'react-client/src/ReactFlightClientConfigBrowser';
11 -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
11 +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
12 +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser';
13 +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser';
14 export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
15 export const usedWithSSR = false;
packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js
+3 -1
@@ -11,7 +11,8 @@ export * from 'react-client/src/ReactFlightClientConfigBrowser';
11 export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
12
13 export type Response = any;
14 -export opaque type SSRManifest = mixed;
14 +export opaque type ModuleLoading = mixed;
15 +export opaque type SSRModuleMap = mixed;
16 export opaque type ServerManifest = mixed;
17 export opaque type ServerReferenceId = string;
18 export opaque type ClientReferenceMetadata = mixed;
@@ -20,4 +21,5 @@ export const resolveClientReference: any = null;
21 export const resolveServerReference: any = null;
22 export const preloadModule: any = null;
23 export const requireModule: any = null;
24 +export const prepareDestinationForModule: any = null;
25 export const usedWithSSR = true;
packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js
+3 -1
@@ -8,6 +8,8 @@
8 */
9
10 export * from 'react-client/src/ReactFlightClientConfigBrowser';
11 -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
11 +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
12 +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer';
13 +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';
14 export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
15 export const usedWithSSR = true;
packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js
+3 -1
@@ -11,7 +11,8 @@ export * from 'react-client/src/ReactFlightClientConfigBrowser';
11 export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
12
13 export type Response = any;
14 -export opaque type SSRManifest = mixed;
14 +export opaque type ModuleLoading = mixed;
15 +export opaque type SSRModuleMap = mixed;
16 export opaque type ServerManifest = mixed;
17 export opaque type ServerReferenceId = string;
18 export opaque type ClientReferenceMetadata = mixed;
@@ -20,4 +21,5 @@ export const resolveClientReference: any = null;
21 export const resolveServerReference: any = null;
22 export const preloadModule: any = null;
23 export const requireModule: any = null;
24 +export const prepareDestinationForModule: any = null;
25 export const usedWithSSR = true;
packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js
+3 -2
@@ -7,7 +7,8 @@
7 * @flow
8 */
9
10 -export * from 'react-client/src/ReactFlightClientConfigBrowser';
11 -export * from 'react-server-dom-esm/src/ReactFlightClientConfigESMBundler';
10 +export * from 'react-client/src/ReactFlightClientConfigNode';
11 +export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM';
12 +export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer';
13 export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
14 export const usedWithSSR = true;
packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js
+3 -1
@@ -8,6 +8,8 @@
8 */
9
10 export * from 'react-client/src/ReactFlightClientConfigNode';
11 -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
11 +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
12 +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer';
13 +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';
14 export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
15 export const usedWithSSR = true;
packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js
+2 -1
@@ -8,6 +8,7 @@
8 */
9
10 export * from 'react-client/src/ReactFlightClientConfigNode';
11 -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler';
11 +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode';
12 +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';
13 export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
14 export const usedWithSSR = true;
packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js
+30
@@ -15,6 +15,8 @@ import type {HintCode, HintModel} from '../server/ReactFlightServerConfigDOM';
15 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
16 const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
17
18 +import {getCrossOriginString} from './crossOriginStrings';
19 +
20 export function dispatchHint<Code: HintCode>(
21 code: Code,
22 model: HintModel<Code>,
@@ -110,3 +112,31 @@ export function dispatchHint<Code: HintCode>(
112 function refineModel<T>(code: T, model: HintModel<any>): HintModel<T> {
113 return model;
114 }
115 +
116 +export function preinitModuleForSSR(
117 + href: string,
118 + nonce: ?string,
119 + crossOrigin: ?string,
120 +) {
121 + const dispatcher = ReactDOMCurrentDispatcher.current;
122 + if (dispatcher) {
123 + dispatcher.preinitModuleScript(href, {
124 + crossOrigin: getCrossOriginString(crossOrigin),
125 + nonce,
126 + });
127 + }
128 +}
129 +
130 +export function preinitScriptForSSR(
131 + href: string,
132 + nonce: ?string,
133 + crossOrigin: ?string,
134 +) {
135 + const dispatcher = ReactDOMCurrentDispatcher.current;
136 + if (dispatcher) {
137 + dispatcher.preinitScript(href, {
138 + crossOrigin: getCrossOriginString(crossOrigin),
139 + nonce,
140 + });
141 + }
142 +}
packages/react-dom-bindings/src/shared/crossOriginStrings.js new
+30
@@ -0,0 +1,30 @@
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 +export opaque type CrossOriginString: string = string;
11 +
12 +export function getCrossOriginString(input: ?string): ?CrossOriginString {
13 + if (typeof input === 'string') {
14 + return input === 'use-credentials' ? input : '';
15 + }
16 + return undefined;
17 +}
18 +
19 +export function getCrossOriginStringAs(
20 + as: ?string,
21 + input: ?string,
22 +): ?CrossOriginString {
23 + if (as === 'font') {
24 + return '';
25 + }
26 + if (typeof input === 'string') {
27 + return input === 'use-credentials' ? input : '';
28 + }
29 + return undefined;
30 +}
packages/react-dom/src/shared/ReactDOMFloat.js
+29 -30
@@ -7,7 +7,6 @@
7 * @flow
8 */
9 import type {
10 - CrossOriginEnum,
10 PreconnectOptions,
11 PreloadOptions,
12 PreloadModuleOptions,
@@ -18,6 +17,11 @@ import type {
17 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
18 const Dispatcher = ReactDOMSharedInternals.Dispatcher;
19
20 +import {
21 + getCrossOriginString,
22 + getCrossOriginStringAs,
23 +} from 'react-dom-bindings/src/shared/crossOriginStrings';
24 +
25 export function prefetchDNS(href: string) {
26 if (__DEV__) {
27 if (typeof href !== 'string' || !href) {
@@ -74,7 +78,7 @@ export function preconnect(href: string, options?: ?PreconnectOptions) {
78 const dispatcher = Dispatcher.current;
79 if (dispatcher && typeof href === 'string') {
80 const crossOrigin = options
77 - ? getCrossOrigin('preconnect', options.crossOrigin)
81 + ? getCrossOriginString(options.crossOrigin)
82 : null;
83 dispatcher.preconnect(href, crossOrigin);
84 }
@@ -117,7 +121,7 @@ export function preload(href: string, options: PreloadOptions) {
121 typeof options.as === 'string'
122 ) {
123 const as = options.as;
120 - const crossOrigin = getCrossOrigin(as, options.crossOrigin);
124 + const crossOrigin = getCrossOriginStringAs(as, options.crossOrigin);
125 dispatcher.preload(href, as, {
126 crossOrigin,
127 integrity:
@@ -172,7 +176,10 @@ export function preloadModule(href: string, options?: ?PreloadModuleOptions) {
176 const dispatcher = Dispatcher.current;
177 if (dispatcher && typeof href === 'string') {
178 if (options) {
175 - const crossOrigin = getCrossOrigin(options.as, options.crossOrigin);
179 + const crossOrigin = getCrossOriginStringAs(
180 + options.as,
181 + options.crossOrigin,
182 + );
183 dispatcher.preloadModule(href, {
184 as:
185 typeof options.as === 'string' && options.as !== 'script'
@@ -218,7 +225,7 @@ export function preinit(href: string, options: PreinitOptions) {
225 typeof options.as === 'string'
226 ) {
227 const as = options.as;
221 - const crossOrigin = getCrossOrigin(as, options.crossOrigin);
228 + const crossOrigin = getCrossOriginStringAs(as, options.crossOrigin);
229 const integrity =
230 typeof options.integrity === 'string' ? options.integrity : undefined;
231 const fetchPriority =
@@ -296,21 +303,23 @@ export function preinitModule(href: string, options?: ?PreinitModuleOptions) {
303 }
304 const dispatcher = Dispatcher.current;
305 if (dispatcher && typeof href === 'string') {
299 - if (
300 - options == null ||
301 - (typeof options === 'object' &&
302 - (options.as == null || options.as === 'script'))
303 - ) {
304 - const crossOrigin = options
305 - ? getCrossOrigin(undefined, options.crossOrigin)
306 - : undefined;
307 - dispatcher.preinitModuleScript(href, {
308 - crossOrigin,
309 - integrity:
310 - options && typeof options.integrity === 'string'
311 - ? options.integrity
312 - : undefined,
313 - });
306 + if (typeof options === 'object' && options !== null) {
307 + if (options.as == null || options.as === 'script') {
308 + const crossOrigin = getCrossOriginStringAs(
309 + options.as,
310 + options.crossOrigin,
311 + );
312 + dispatcher.preinitModuleScript(href, {
313 + crossOrigin,
314 + integrity:
315 + typeof options.integrity === 'string'
316 + ? options.integrity
317 + : undefined,
318 + nonce: typeof options.nonce === 'string' ? options.nonce : undefined,
319 + });
320 + }
321 + } else if (options == null) {
322 + dispatcher.preinitModuleScript(href);
323 }
324 }
325 // We don't error because preinit needs to be resilient to being called in a variety of scopes
@@ -318,16 +327,6 @@ export function preinitModule(href: string, options?: ?PreinitModuleOptions) {
327 // so we favor silent bailout over warning or erroring.
328 }
329
321 -function getCrossOrigin(as: ?string, crossOrigin: ?string): ?CrossOriginEnum {
322 - return as === 'font'
323 - ? ''
324 - : typeof crossOrigin === 'string'
325 - ? crossOrigin === 'use-credentials'
326 - ? 'use-credentials'
327 - : ''
328 - : undefined;
329 -}
330 -
330 function getValueDescriptorExpectingObjectForWarning(thing: any): string {
331 return thing === null
332 ? '`null`'
packages/react-dom/src/shared/ReactDOMTypes.js
+5 -3
@@ -7,6 +7,8 @@
7 * @flow
8 */
9
10 +import type {CrossOriginString} from 'react-dom-bindings/src/shared/crossOriginStrings';
11 +
12 export type PrefetchDNSOptions = {};
13 export type PreconnectOptions = {crossOrigin?: string};
14 export type PreloadOptions = {
@@ -41,7 +43,7 @@ export type PreinitModuleOptions = {
43 nonce?: string,
44 };
45
44 -export type CrossOriginEnum = '' | 'use-credentials';
46 +export type CrossOriginEnum = '' | 'use-credentials' | CrossOriginString;
47 export type FetchPriorityEnum = 'high' | 'low' | 'auto';
48
49 export type PreloadImplOptions = {
@@ -61,12 +63,12 @@ export type PreloadModuleImplOptions = {
63 nonce?: ?string,
64 };
65 export type PreinitStyleOptions = {
64 - crossOrigin?: ?string,
66 + crossOrigin?: ?CrossOriginEnum,
67 integrity?: ?string,
68 fetchPriority?: ?FetchPriorityEnum,
69 };
70 export type PreinitScriptOptions = {
69 - crossOrigin?: ?string,
71 + crossOrigin?: ?CrossOriginEnum,
72 integrity?: ?string,
73 fetchPriority?: ?FetchPriorityEnum,
74 nonce?: ?string,
packages/react-noop-renderer/src/ReactNoopFlightClient.js
+1
@@ -35,6 +35,7 @@ const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
35 resolveClientReference(bundlerConfig: null, idx: string) {
36 return idx;
37 },
38 + prepareDestinationForModule(moduleLoading: null, metadata: string) {},
39 preloadModule(idx: string) {},
40 requireModule(idx: string) {
41 return readModule(idx);
packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js renamed
+19 -2
@@ -12,13 +12,16 @@ import type {
12 FulfilledThenable,
13 RejectedThenable,
14 } from 'shared/ReactTypes';
15 +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig';
16
16 -export type SSRManifest = string; // Module root path
17 +export type SSRModuleMap = string; // Module root path
18
19 export type ServerManifest = string; // Module root path
20
21 export type ServerReferenceId = string;
22
23 +import {prepareDestinationForModuleImpl} from 'react-client/src/ReactFlightClientConfig';
24 +
25 export opaque type ClientReferenceMetadata = [
26 string, // module path
27 string, // export name
@@ -30,8 +33,22 @@ export opaque type ClientReference<T> = {
33 name: string,
34 };
35
36 +// The reason this function needs to defined here in this file instead of just
37 +// being exported directly from the WebpackDestination... file is because the
38 +// ClientReferenceMetadata is opaque and we can't unwrap it there.
39 +// This should get inlined and we could also just implement an unwrapping function
40 +// though that risks it getting used in places it shouldn't be. This is unfortunate
41 +// but currently it seems to be the best option we have.
42 +export function prepareDestinationForModule(
43 + moduleLoading: ModuleLoading,
44 + nonce: ?string,
45 + metadata: ClientReferenceMetadata,
46 +) {
47 + prepareDestinationForModuleImpl(moduleLoading, metadata[0], nonce);
48 +}
49 +
50 export function resolveClientReference<T>(
34 - bundlerConfig: SSRManifest,
51 + bundlerConfig: SSRModuleMap,
52 metadata: ClientReferenceMetadata,
53 ): ClientReference<T> {
54 const baseURL = bundlerConfig;
packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser.js new
+18
@@ -0,0 +1,18 @@
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 +export type ModuleLoading = null;
11 +
12 +export function prepareDestinationForModuleImpl(
13 + moduleLoading: ModuleLoading,
14 + chunks: mixed,
15 + nonce: ?string,
16 +) {
17 + // In the browser we don't need to prepare our destination since the browser is the Destination
18 +}
packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.js new
+35
@@ -0,0 +1,35 @@
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 {preinitModuleForSSR} from 'react-client/src/ReactFlightClientConfig';
11 +
12 +export type ModuleLoading =
13 + | null
14 + | string
15 + | {
16 + prefix: string,
17 + crossOrigin?: string,
18 + };
19 +
20 +export function prepareDestinationForModuleImpl(
21 + moduleLoading: ModuleLoading,
22 + // Chunks are double-indexed [..., idx, filenamex, idy, filenamey, ...]
23 + mod: string,
24 + nonce: ?string,
25 +) {
26 + if (typeof moduleLoading === 'string') {
27 + preinitModuleForSSR(moduleLoading + mod, nonce, undefined);
28 + } else if (moduleLoading !== null) {
29 + preinitModuleForSSR(
30 + moduleLoading.prefix + mod,
31 + nonce,
32 + moduleLoading.crossOrigin,
33 + );
34 + }
35 +}
packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js
+2
@@ -36,7 +36,9 @@ export type Options = {
36 function createResponseFromOptions(options: void | Options) {
37 return createResponse(
38 options && options.moduleBaseURL ? options.moduleBaseURL : '',
39 + null,
40 options && options.callServer ? options.callServer : undefined,
41 + undefined, // nonce
42 );
43 }
44
packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js
+12 -2
@@ -38,12 +38,22 @@ export function createServerReference<A: Iterable<any>, T>(
38 return createServerReferenceImpl(id, noServerCall);
39 }
40
41 +export type Options = {
42 + nonce?: string,
43 +};
44 +
45 function createFromNodeStream<T>(
46 stream: Readable,
47 moduleRootPath: string,
44 - moduleBaseURL: string, // TODO: Used for preloading hints
48 + moduleBaseURL: string,
49 + options?: Options,
50 ): Thenable<T> {
46 - const response: Response = createResponse(moduleRootPath, noServerCall);
51 + const response: Response = createResponse(
52 + moduleRootPath,
53 + moduleBaseURL,
54 + noServerCall,
55 + options && typeof options.nonce === 'string' ? options.nonce : undefined,
56 + );
57 stream.on('data', chunk => {
58 processBinaryChunk(response, chunk);
59 });
packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js renamed
+33 -13
@@ -13,7 +13,18 @@ import type {
13 RejectedThenable,
14 } from 'shared/ReactTypes';
15
16 -export type SSRManifest = {
16 +import type {ImportMetadata} from './shared/ReactFlightImportMetadata';
17 +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig';
18 +
19 +import {
20 + ID,
21 + CHUNKS,
22 + NAME,
23 + isAsyncImport,
24 +} from './shared/ReactFlightImportMetadata';
25 +import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
26 +
27 +export type SSRModuleMap = {
28 [clientId: string]: {
29 [clientExportName: string]: ClientReference<any>,
30 },
@@ -23,12 +34,7 @@ export type ServerManifest = void;
34
35 export type ServerReferenceId = string;
36
26 -export opaque type ClientReferenceMetadata = {
27 - id: string,
28 - chunks: Array<string>,
29 - name: string,
30 - async?: boolean,
31 -};
37 +export opaque type ClientReferenceMetadata = ImportMetadata;
38
39 // eslint-disable-next-line no-unused-vars
40 export opaque type ClientReference<T> = {
@@ -37,12 +43,26 @@ export opaque type ClientReference<T> = {
43 async?: boolean,
44 };
45
46 +// The reason this function needs to defined here in this file instead of just
47 +// being exported directly from the WebpackDestination... file is because the
48 +// ClientReferenceMetadata is opaque and we can't unwrap it there.
49 +// This should get inlined and we could also just implement an unwrapping function
50 +// though that risks it getting used in places it shouldn't be. This is unfortunate
51 +// but currently it seems to be the best option we have.
52 +export function prepareDestinationForModule(
53 + moduleLoading: ModuleLoading,
54 + nonce: ?string,
55 + metadata: ClientReferenceMetadata,
56 +) {
57 + prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce);
58 +}
59 +
60 export function resolveClientReference<T>(
41 - bundlerConfig: SSRManifest,
61 + bundlerConfig: SSRModuleMap,
62 metadata: ClientReferenceMetadata,
63 ): ClientReference<T> {
44 - const moduleExports = bundlerConfig[metadata.id];
45 - let resolvedModuleData = moduleExports[metadata.name];
64 + const moduleExports = bundlerConfig[metadata[ID]];
65 + let resolvedModuleData = moduleExports[metadata[NAME]];
66 let name;
67 if (resolvedModuleData) {
68 // The potentially aliased name.
@@ -53,17 +73,17 @@ export function resolveClientReference<T>(
73 if (!resolvedModuleData) {
74 throw new Error(
75 'Could not find the module "' +
56 - metadata.id +
76 + metadata[ID] +
77 '" in the React SSR Manifest. ' +
78 'This is probably a bug in the React Server Components bundler.',
79 );
80 }
61 - name = metadata.name;
81 + name = metadata[NAME];
82 }
83 return {
84 specifier: resolvedModuleData.specifier,
85 name: name,
66 - async: metadata.async,
86 + async: isAsyncImport(metadata),
87 };
88 }
89
packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js renamed
+66 -38
@@ -13,35 +13,62 @@ import type {
13 RejectedThenable,
14 } from 'shared/ReactTypes';
15
16 -export type SSRManifest = null | {
16 +import type {
17 + ImportMetadata,
18 + ImportManifestEntry,
19 +} from './shared/ReactFlightImportMetadata';
20 +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig';
21 +
22 +import {
23 + ID,
24 + CHUNKS,
25 + NAME,
26 + isAsyncImport,
27 +} from './shared/ReactFlightImportMetadata';
28 +
29 +import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
30 +
31 +import {loadChunk} from 'react-client/src/ReactFlightClientConfig';
32 +
33 +export type SSRModuleMap = null | {
34 [clientId: string]: {
18 - [clientExportName: string]: ClientReferenceMetadata,
35 + [clientExportName: string]: ClientReferenceManifestEntry,
36 },
37 };
38
39 export type ServerManifest = {
23 - [id: string]: ClientReference<any>,
40 + [id: string]: ImportManifestEntry,
41 };
42
43 export type ServerReferenceId = string;
44
28 -export opaque type ClientReferenceMetadata = {
29 - id: string,
30 - chunks: Array<string>,
31 - name: string,
32 - async: boolean,
33 -};
45 +export opaque type ClientReferenceManifestEntry = ImportManifestEntry;
46 +export opaque type ClientReferenceMetadata = ImportMetadata;
47
48 // eslint-disable-next-line no-unused-vars
49 export opaque type ClientReference<T> = ClientReferenceMetadata;
50
51 +// The reason this function needs to defined here in this file instead of just
52 +// being exported directly from the WebpackDestination... file is because the
53 +// ClientReferenceMetadata is opaque and we can't unwrap it there.
54 +// This should get inlined and we could also just implement an unwrapping function
55 +// though that risks it getting used in places it shouldn't be. This is unfortunate
56 +// but currently it seems to be the best option we have.
57 +export function prepareDestinationForModule(
58 + moduleLoading: ModuleLoading,
59 + nonce: ?string,
60 + metadata: ClientReferenceMetadata,
61 +) {
62 + prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce);
63 +}
64 +
65 export function resolveClientReference<T>(
39 - bundlerConfig: SSRManifest,
66 + bundlerConfig: SSRModuleMap,
67 metadata: ClientReferenceMetadata,
68 ): ClientReference<T> {
69 if (bundlerConfig) {
43 - const moduleExports = bundlerConfig[metadata.id];
44 - let resolvedModuleData = moduleExports[metadata.name];
70 + const moduleExports = bundlerConfig[metadata[ID]];
71 + let resolvedModuleData = moduleExports[metadata[NAME]];
72 let name;
73 if (resolvedModuleData) {
74 // The potentially aliased name.
@@ -52,19 +79,23 @@ export function resolveClientReference<T>(
79 if (!resolvedModuleData) {
80 throw new Error(
81 'Could not find the module "' +
55 - metadata.id +
82 + metadata[ID] +
83 '" in the React SSR Manifest. ' +
84 'This is probably a bug in the React Server Components bundler.',
85 );
86 }
60 - name = metadata.name;
87 + name = metadata[NAME];
88 + }
89 + if (isAsyncImport(metadata)) {
90 + return [
91 + resolvedModuleData.id,
92 + resolvedModuleData.chunks,
93 + name,
94 + 1 /* async */,
95 + ];
96 + } else {
97 + return [resolvedModuleData.id, resolvedModuleData.chunks, name];
98 }
62 - return {
63 - id: resolvedModuleData.id,
64 - chunks: resolvedModuleData.chunks,
65 - name: name,
66 - async: !!metadata.async,
67 - };
99 }
100 return metadata;
101 }
@@ -98,12 +129,7 @@ export function resolveServerReference<T>(
129 }
130 }
131 // TODO: This needs to return async: true if it's an async module.
101 - return {
102 - id: resolvedModuleData.id,
103 - chunks: resolvedModuleData.chunks,
104 - name: name,
105 - async: false,
106 - };
132 + return [resolvedModuleData.id, resolvedModuleData.chunks, name];
133 }
134
135 // The chunk cache contains all the chunks we've preloaded so far.
@@ -147,13 +173,15 @@ function ignoreReject() {
173 export function preloadModule<T>(
174 metadata: ClientReference<T>,
175 ): null | Thenable<any> {
150 - const chunks = metadata.chunks;
176 + const chunks = metadata[CHUNKS];
177 const promises = [];
152 - for (let i = 0; i < chunks.length; i++) {
153 - const chunkId = chunks[i];
178 + let i = 0;
179 + while (i < chunks.length) {
180 + const chunkId = chunks[i++];
181 + const chunkFilename = chunks[i++];
182 const entry = chunkCache.get(chunkId);
183 if (entry === undefined) {
156 - const thenable = __webpack_chunk_load__(chunkId);
184 + const thenable = loadChunk(chunkId, chunkFilename);
185 promises.push(thenable);
186 // $FlowFixMe[method-unbinding]
187 const resolve = chunkCache.set.bind(chunkCache, chunkId, null);
@@ -163,12 +191,12 @@ export function preloadModule<T>(
191 promises.push(entry);
192 }
193 }
166 - if (metadata.async) {
194 + if (isAsyncImport(metadata)) {
195 if (promises.length === 0) {
168 - return requireAsyncModule(metadata.id);
196 + return requireAsyncModule(metadata[ID]);
197 } else {
198 return Promise.all(promises).then(() => {
171 - return requireAsyncModule(metadata.id);
199 + return requireAsyncModule(metadata[ID]);
200 });
201 }
202 } else if (promises.length > 0) {
@@ -181,8 +209,8 @@ export function preloadModule<T>(
209 // Actually require the module or suspend if it's not yet ready.
210 // Increase priority if necessary.
211 export function requireModule<T>(metadata: ClientReference<T>): T {
184 - let moduleExports = __webpack_require__(metadata.id);
185 - if (metadata.async) {
212 + let moduleExports = __webpack_require__(metadata[ID]);
213 + if (isAsyncImport(metadata)) {
214 if (typeof moduleExports.then !== 'function') {
215 // This wasn't a promise after all.
216 } else if (moduleExports.status === 'fulfilled') {
@@ -192,15 +220,15 @@ export function requireModule<T>(metadata: ClientReference<T>): T {
220 throw moduleExports.reason;
221 }
222 }
195 - if (metadata.name === '*') {
223 + if (metadata[NAME] === '*') {
224 // This is a placeholder value that represents that the caller imported this
225 // as a CommonJS module as is.
226 return moduleExports;
227 }
200 - if (metadata.name === '') {
228 + if (metadata[NAME] === '') {
229 // This is a placeholder value that represents that the caller accessed the
230 // default property of this if it was an ESM interop module.
231 return moduleExports.__esModule ? moduleExports.default : moduleExports;
232 }
205 - return moduleExports[metadata.name];
233 + return moduleExports[metadata[NAME]];
234 }
packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser.js new
+28
@@ -0,0 +1,28 @@
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 +const chunkMap: Map<string, string> = new Map();
11 +
12 +/**
13 + * We patch the chunk filename function in webpack to insert our own resolution
14 + * of chunks that come from Flight and may not be known to the webpack runtime
15 + */
16 +const webpackGetChunkFilename = __webpack_require__.u;
17 +__webpack_require__.u = function (chunkId: string) {
18 + const flightChunk = chunkMap.get(chunkId);
19 + if (flightChunk !== undefined) {
20 + return flightChunk;
21 + }
22 + return webpackGetChunkFilename(chunkId);
23 +};
24 +
25 +export function loadChunk(chunkId: string, filename: string): Promise<mixed> {
26 + chunkMap.set(chunkId, filename);
27 + return __webpack_chunk_load__(chunkId);
28 +}
packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js new
+12
@@ -0,0 +1,12 @@
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 +export function loadChunk(chunkId: string, filename: string): Promise<mixed> {
11 + return __webpack_chunk_load__(chunkId);
12 +}
packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser.js new
+18
@@ -0,0 +1,18 @@
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 +export type ModuleLoading = null;
11 +
12 +export function prepareDestinationWithChunks(
13 + moduleLoading: ModuleLoading,
14 + chunks: mixed,
15 + nonce: ?string,
16 +) {
17 + // In the browser we don't need to prepare our destination since the browser is the Destination
18 +}
packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js new
+32
@@ -0,0 +1,32 @@
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 {preinitScriptForSSR} from 'react-client/src/ReactFlightClientConfig';
11 +
12 +export type ModuleLoading = null | {
13 + prefix: string,
14 + crossOrigin?: 'use-credentials' | '',
15 +};
16 +
17 +export function prepareDestinationWithChunks(
18 + moduleLoading: ModuleLoading,
19 + // Chunks are double-indexed [..., idx, filenamex, idy, filenamey, ...]
20 + chunks: Array<string>,
21 + nonce: ?string,
22 +) {
23 + if (moduleLoading !== null) {
24 + for (let i = 1; i < chunks.length; i += 2) {
25 + preinitScriptForSSR(
26 + moduleLoading.prefix + chunks[i],
27 + nonce,
28 + moduleLoading.crossOrigin,
29 + );
30 + }
31 + }
32 +}
packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js
+2
@@ -34,8 +34,10 @@ export type Options = {
34
35 function createResponseFromOptions(options: void | Options) {
36 return createResponse(
37 + null,
38 null,
39 options && options.callServer ? options.callServer : undefined,
40 + undefined, // nonce
41 );
42 }
43
packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js
+17 -6
@@ -11,7 +11,15 @@ import type {Thenable} from 'shared/ReactTypes.js';
11
12 import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
13
14 -import type {SSRManifest} from './ReactFlightClientConfigWebpackBundler';
14 +import type {
15 + SSRModuleMap,
16 + ModuleLoading,
17 +} from 'react-client/src/ReactFlightClientConfig';
18 +
19 +type SSRManifest = {
20 + moduleMap: SSRModuleMap,
21 + moduleLoading: ModuleLoading,
22 +};
23
24 import {
25 createResponse,
@@ -39,13 +47,16 @@ export function createServerReference<A: Iterable<any>, T>(
47 }
48
49 export type Options = {
42 - moduleMap?: $NonMaybeType<SSRManifest>,
50 + ssrManifest: SSRManifest,
51 + nonce?: string,
52 };
53
45 -function createResponseFromOptions(options: void | Options) {
54 +function createResponseFromOptions(options: Options) {
55 return createResponse(
47 - options && options.moduleMap ? options.moduleMap : null,
56 + options.ssrManifest.moduleMap,
57 + options.ssrManifest.moduleLoading,
58 noServerCall,
59 + typeof options.nonce === 'string' ? options.nonce : undefined,
60 );
61 }
62
@@ -78,7 +89,7 @@ function startReadingFromStream(
89
90 function createFromReadableStream<T>(
91 stream: ReadableStream,
81 - options?: Options,
92 + options: Options,
93 ): Thenable<T> {
94 const response: FlightResponse = createResponseFromOptions(options);
95 startReadingFromStream(response, stream);
@@ -87,7 +98,7 @@ function createFromReadableStream<T>(
98
99 function createFromFetch<T>(
100 promiseForResponse: Promise<Response>,
90 - options?: Options,
101 + options: Options,
102 ): Thenable<T> {
103 const response: FlightResponse = createResponseFromOptions(options);
104 promiseForResponse.then(
packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js
+21 -3
@@ -11,7 +11,15 @@ import type {Thenable} from 'shared/ReactTypes.js';
11
12 import type {Response} from 'react-client/src/ReactFlightClient';
13
14 -import type {SSRManifest} from 'react-client/src/ReactFlightClientConfig';
14 +import type {
15 + SSRModuleMap,
16 + ModuleLoading,
17 +} from 'react-client/src/ReactFlightClientConfig';
18 +
19 +type SSRManifest = {
20 + moduleMap: SSRModuleMap,
21 + moduleLoading: ModuleLoading,
22 +};
23
24 import type {Readable} from 'stream';
25
@@ -40,11 +48,21 @@ export function createServerReference<A: Iterable<any>, T>(
48 return createServerReferenceImpl(id, noServerCall);
49 }
50
51 +export type Options = {
52 + nonce?: string,
53 +};
54 +
55 function createFromNodeStream<T>(
56 stream: Readable,
45 - moduleMap: $NonMaybeType<SSRManifest>,
57 + ssrManifest: SSRManifest,
58 + options?: Options,
59 ): Thenable<T> {
47 - const response: Response = createResponse(moduleMap, noServerCall);
60 + const response: Response = createResponse(
61 + ssrManifest.moduleMap,
62 + ssrManifest.moduleLoading,
63 + noServerCall,
64 + options && typeof options.nonce === 'string' ? options.nonce : undefined,
65 + );
66 stream.on('data', chunk => {
67 processBinaryChunk(response, chunk);
68 });
packages/react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler.js
+12 -13
@@ -8,6 +8,10 @@
8 */
9
10 import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
11 +import type {
12 + ImportMetadata,
13 + ImportManifestEntry,
14 +} from './shared/ReactFlightImportMetadata';
15
16 import type {
17 ClientReference,
@@ -17,17 +21,13 @@ import type {
21 export type {ClientReference, ServerReference};
22
23 export type ClientManifest = {
20 - [id: string]: ClientReferenceMetadata,
24 + [id: string]: ClientReferenceManifestEntry,
25 };
26
27 export type ServerReferenceId = string;
28
25 -export type ClientReferenceMetadata = {
26 - id: string,
27 - chunks: Array<string>,
28 - name: string,
29 - async: boolean,
30 -};
29 +export type ClientReferenceMetadata = ImportMetadata;
30 +export opaque type ClientReferenceManifestEntry = ImportManifestEntry;
31
32 export type ClientReferenceKey = string;
33
@@ -71,12 +71,11 @@ export function resolveClientReferenceMetadata<T>(
71 );
72 }
73 }
74 - return {
75 - id: resolvedModuleData.id,
76 - chunks: resolvedModuleData.chunks,
77 - name: name,
78 - async: !!clientReference.$$async,
79 - };
74 + if (clientReference.$$async === true) {
75 + return [resolvedModuleData.id, resolvedModuleData.chunks, name, 1];
76 + } else {
77 + return [resolvedModuleData.id, resolvedModuleData.chunks, name];
78 + }
79 }
80
81 export function getServerReferenceId<T>(
packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js
+56 -10
@@ -7,6 +7,8 @@
7 * @flow
8 */
9
10 +import type {ImportManifestEntry} from './shared/ReactFlightImportMetadata';
11 +
12 import {join} from 'path';
13 import {pathToFileURL} from 'url';
14 import asyncLib from 'neo-async';
@@ -221,21 +223,65 @@ export default class ReactFlightWebpackPlugin {
223 return;
224 }
225
226 + const configuredCrossOriginLoading =
227 + compilation.outputOptions.crossOriginLoading;
228 + const crossOriginMode =
229 + typeof configuredCrossOriginLoading === 'string'
230 + ? configuredCrossOriginLoading === 'use-credentials'
231 + ? configuredCrossOriginLoading
232 + : 'anonymous'
233 + : null;
234 +
235 const resolvedClientFiles = new Set(
236 (resolvedClientReferences || []).map(ref => ref.request),
237 );
238
239 const clientManifest: {
229 - [string]: {chunks: $FlowFixMe, id: string, name: string},
240 + [string]: ImportManifestEntry,
241 } = {};
231 - const ssrManifest: {
242 + type SSRModuleMap = {
243 [string]: {
244 [string]: {specifier: string, name: string},
245 },
235 - } = {};
246 + };
247 + const moduleMap: SSRModuleMap = {};
248 + const ssrBundleConfig: {
249 + moduleLoading: {
250 + prefix: string,
251 + crossOrigin: string | null,
252 + },
253 + moduleMap: SSRModuleMap,
254 + } = {
255 + moduleLoading: {
256 + prefix: compilation.outputOptions.publicPath || '',
257 + crossOrigin: crossOriginMode,
258 + },
259 + moduleMap,
260 + };
261 +
262 + // We figure out which files are always loaded by any initial chunk (entrypoint).
263 + // We use this to filter out chunks that Flight will never need to load
264 + const emptySet: Set<string> = new Set();
265 + const runtimeChunkFiles: Set<string> = emptySet;
266 + compilation.entrypoints.forEach(entrypoint => {
267 + const runtimeChunk = entrypoint.getRuntimeChunk();
268 + if (runtimeChunk) {
269 + runtimeChunk.files.forEach(runtimeFile => {
270 + runtimeChunkFiles.add(runtimeFile);
271 + });
272 + }
273 + });
274 +
275 compilation.chunkGroups.forEach(function (chunkGroup) {
237 - const chunkIds = chunkGroup.chunks.map(function (c) {
238 - return c.id;
276 + const chunks: Array<string> = [];
277 + chunkGroup.chunks.forEach(function (c) {
278 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
279 + for (const file of c.files) {
280 + if (!file.endsWith('.js')) return;
281 + if (file.endsWith('.hot-update.js')) return;
282 + chunks.push(c.id, file);
283 + break;
284 + }
285 });
286
287 // $FlowFixMe[missing-local-annot]
@@ -256,7 +302,7 @@ export default class ReactFlightWebpackPlugin {
302
303 clientManifest[href] = {
304 id,
259 - chunks: chunkIds,
305 + chunks,
306 name: '*',
307 };
308 ssrExports['*'] = {
@@ -272,7 +318,7 @@ export default class ReactFlightWebpackPlugin {
318 /*
319 clientManifest[href + '#'] = {
320 id,
275 - chunks: chunkIds,
321 + chunks,
322 name: '',
323 };
324 ssrExports[''] = {
@@ -288,7 +334,7 @@ export default class ReactFlightWebpackPlugin {
334 moduleProvidedExports.forEach(function (name) {
335 clientManifest[href + '#' + name] = {
336 id,
291 - chunks: chunkIds,
337 + chunks,
338 name: name,
339 };
340 ssrExports[name] = {
@@ -299,7 +345,7 @@ export default class ReactFlightWebpackPlugin {
345 }
346 */
347
302 - ssrManifest[id] = ssrExports;
348 + moduleMap[id] = ssrExports;
349 }
350 }
351
@@ -326,7 +372,7 @@ export default class ReactFlightWebpackPlugin {
372 _this.clientManifestFilename,
373 new sources.RawSource(clientOutput, false),
374 );
329 - const ssrOutput = JSON.stringify(ssrManifest, null, 2);
375 + const ssrOutput = JSON.stringify(ssrBundleConfig, null, 2);
376 compilation.emitAsset(
377 _this.ssrManifestFilename,
378 new sources.RawSource(ssrOutput, false),
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+70 -60
@@ -25,8 +25,9 @@ let clientExports;
25 let clientModuleError;
26 let webpackMap;
27 let Stream;
28 +let FlightReact;
29 let React;
29 -let ReactDOM;
30 +let FlightReactDOM;
31 let ReactDOMClient;
32 let ReactServerDOMServer;
33 let ReactServerDOMClient;
@@ -37,6 +38,9 @@ let JSDOM;
38
39 describe('ReactFlightDOM', () => {
40 beforeEach(() => {
41 + // For this first reset we are going to load the dom-node version of react-server-dom-webpack/server
42 + // This can be thought of as essentially being the React Server Components scope with react-server
43 + // condition
44 jest.resetModules();
45
46 JSDOM = require('jsdom').JSDOM;
@@ -45,23 +49,29 @@ describe('ReactFlightDOM', () => {
49 jest.mock('react-server-dom-webpack/server', () =>
50 require('react-server-dom-webpack/server.node.unbundled'),
51 );
52 + jest.mock('react', () => require('react/react.shared-subset'));
53
49 - ReactServerDOMClient = require('react-server-dom-webpack/client');
50 -
51 - act = require('internal-test-utils').act;
54 const WebpackMock = require('./utils/WebpackMock');
55 clientExports = WebpackMock.clientExports;
56 clientModuleError = WebpackMock.clientModuleError;
57 webpackMap = WebpackMock.webpackMap;
58
59 + ReactServerDOMServer = require('react-server-dom-webpack/server');
60 + FlightReact = require('react');
61 + FlightReactDOM = require('react-dom');
62 +
63 + // This reset is to load modules for the SSR/Browser scope.
64 + jest.unmock('react-server-dom-webpack/server');
65 + jest.unmock('react');
66 + jest.resetModules();
67 + act = require('internal-test-utils').act;
68 Stream = require('stream');
69 React = require('react');
59 - ReactDOM = require('react-dom');
60 - ReactDOMFizzServer = require('react-dom/server.node');
70 use = React.use;
71 Suspense = React.Suspense;
72 ReactDOMClient = require('react-dom/client');
64 - ReactServerDOMServer = require('react-server-dom-webpack/server.node.unbundled');
73 + ReactDOMFizzServer = require('react-dom/server.node');
74 + ReactServerDOMClient = require('react-server-dom-webpack/client');
75
76 ErrorBoundary = class extends React.Component {
77 state = {hasError: false, error: null};
@@ -485,7 +495,7 @@ describe('ReactFlightDOM', () => {
495 const AsyncModuleRef = clientExports(AsyncModule);
496
497 function ServerComponent() {
488 - const text = use(AsyncModuleRef);
498 + const text = FlightReact.use(AsyncModuleRef);
499 return <p>{text}</p>;
500 }
501
@@ -1205,25 +1215,25 @@ describe('ReactFlightDOM', () => {
1215 const ClientComponent = clientExports(Component);
1216
1217 async function ServerComponent() {
1208 - ReactDOM.prefetchDNS('d before');
1209 - ReactDOM.preconnect('c before');
1210 - ReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
1211 - ReactDOM.preload('l before', {as: 'style'});
1212 - ReactDOM.preloadModule('lm before');
1213 - ReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
1214 - ReactDOM.preinit('i before', {as: 'script'});
1215 - ReactDOM.preinitModule('m before');
1216 - ReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
1218 + FlightReactDOM.prefetchDNS('d before');
1219 + FlightReactDOM.preconnect('c before');
1220 + FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
1221 + FlightReactDOM.preload('l before', {as: 'style'});
1222 + FlightReactDOM.preloadModule('lm before');
1223 + FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
1224 + FlightReactDOM.preinit('i before', {as: 'script'});
1225 + FlightReactDOM.preinitModule('m before');
1226 + FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
1227 await 1;
1218 - ReactDOM.prefetchDNS('d after');
1219 - ReactDOM.preconnect('c after');
1220 - ReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
1221 - ReactDOM.preload('l after', {as: 'style'});
1222 - ReactDOM.preloadModule('lm after');
1223 - ReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
1224 - ReactDOM.preinit('i after', {as: 'script'});
1225 - ReactDOM.preinitModule('m after');
1226 - ReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
1228 + FlightReactDOM.prefetchDNS('d after');
1229 + FlightReactDOM.preconnect('c after');
1230 + FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
1231 + FlightReactDOM.preload('l after', {as: 'style'});
1232 + FlightReactDOM.preloadModule('lm after');
1233 + FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
1234 + FlightReactDOM.preinit('i after', {as: 'script'});
1235 + FlightReactDOM.preinitModule('m after');
1236 + FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
1237 return <ClientComponent />;
1238 }
1239
@@ -1298,25 +1308,25 @@ describe('ReactFlightDOM', () => {
1308 const ClientComponent = clientExports(Component);
1309
1310 async function ServerComponent() {
1301 - ReactDOM.prefetchDNS('d before');
1302 - ReactDOM.preconnect('c before');
1303 - ReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
1304 - ReactDOM.preload('l before', {as: 'style'});
1305 - ReactDOM.preloadModule('lm before');
1306 - ReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
1307 - ReactDOM.preinit('i before', {as: 'script'});
1308 - ReactDOM.preinitModule('m before');
1309 - ReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
1311 + FlightReactDOM.prefetchDNS('d before');
1312 + FlightReactDOM.preconnect('c before');
1313 + FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
1314 + FlightReactDOM.preload('l before', {as: 'style'});
1315 + FlightReactDOM.preloadModule('lm before');
1316 + FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
1317 + FlightReactDOM.preinit('i before', {as: 'script'});
1318 + FlightReactDOM.preinitModule('m before');
1319 + FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
1320 await 1;
1311 - ReactDOM.prefetchDNS('d after');
1312 - ReactDOM.preconnect('c after');
1313 - ReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
1314 - ReactDOM.preload('l after', {as: 'style'});
1315 - ReactDOM.preloadModule('lm after');
1316 - ReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
1317 - ReactDOM.preinit('i after', {as: 'script'});
1318 - ReactDOM.preinitModule('m after');
1319 - ReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
1321 + FlightReactDOM.prefetchDNS('d after');
1322 + FlightReactDOM.preconnect('c after');
1323 + FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
1324 + FlightReactDOM.preload('l after', {as: 'style'});
1325 + FlightReactDOM.preloadModule('lm after');
1326 + FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
1327 + FlightReactDOM.preinit('i after', {as: 'script'});
1328 + FlightReactDOM.preinitModule('m after');
1329 + FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
1330 return <ClientComponent />;
1331 }
1332
@@ -1406,16 +1416,16 @@ describe('ReactFlightDOM', () => {
1416 const ClientComponent = clientExports(Component);
1417
1418 async function ServerComponent1() {
1409 - ReactDOM.preload('before1', {as: 'style'});
1419 + FlightReactDOM.preload('before1', {as: 'style'});
1420 await 1;
1411 - ReactDOM.preload('after1', {as: 'style'});
1421 + FlightReactDOM.preload('after1', {as: 'style'});
1422 return <ClientComponent />;
1423 }
1424
1425 async function ServerComponent2() {
1416 - ReactDOM.preload('before2', {as: 'style'});
1426 + FlightReactDOM.preload('before2', {as: 'style'});
1427 await 1;
1418 - ReactDOM.preload('after2', {as: 'style'});
1428 + FlightReactDOM.preload('after2', {as: 'style'});
1429 return <ClientComponent />;
1430 }
1431
@@ -1506,21 +1516,21 @@ describe('ReactFlightDOM', () => {
1516 const ClientComponent = clientExports(Component);
1517
1518 async function ServerComponent() {
1509 - ReactDOM.prefetchDNS('dns');
1510 - ReactDOM.preconnect('preconnect');
1511 - ReactDOM.preload('load', {as: 'style'});
1512 - ReactDOM.preinit('init', {as: 'script'});
1519 + FlightReactDOM.prefetchDNS('dns');
1520 + FlightReactDOM.preconnect('preconnect');
1521 + FlightReactDOM.preload('load', {as: 'style'});
1522 + FlightReactDOM.preinit('init', {as: 'script'});
1523 // again but vary preconnect to demonstrate crossOrigin participates in the key
1514 - ReactDOM.prefetchDNS('dns');
1515 - ReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'});
1516 - ReactDOM.preload('load', {as: 'style'});
1517 - ReactDOM.preinit('init', {as: 'script'});
1524 + FlightReactDOM.prefetchDNS('dns');
1525 + FlightReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'});
1526 + FlightReactDOM.preload('load', {as: 'style'});
1527 + FlightReactDOM.preinit('init', {as: 'script'});
1528 await 1;
1529 // after an async point
1520 - ReactDOM.prefetchDNS('dns');
1521 - ReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'});
1522 - ReactDOM.preload('load', {as: 'style'});
1523 - ReactDOM.preinit('init', {as: 'script'});
1530 + FlightReactDOM.prefetchDNS('dns');
1531 + FlightReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'});
1532 + FlightReactDOM.preload('load', {as: 'style'});
1533 + FlightReactDOM.preinit('init', {as: 'script'});
1534 return <ClientComponent />;
1535 }
1536
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+32 -5
@@ -22,6 +22,7 @@ global.setTimeout = cb => cb();
22 let clientExports;
23 let webpackMap;
24 let webpackModules;
25 +let webpackModuleLoading;
26 let React;
27 let ReactDOMServer;
28 let ReactServerDOMServer;
@@ -33,18 +34,28 @@ describe('ReactFlightDOMEdge', () => {
34 jest.resetModules();
35
36 // Simulate the condition resolution
37 + jest.mock('react', () => require('react/react.shared-subset'));
38 jest.mock('react-server-dom-webpack/server', () =>
39 require('react-server-dom-webpack/server.edge'),
40 );
41 + ReactServerDOMServer = require('react-server-dom-webpack/server');
42
43 const WebpackMock = require('./utils/WebpackMock');
44 +
45 clientExports = WebpackMock.clientExports;
46 webpackMap = WebpackMock.webpackMap;
47 webpackModules = WebpackMock.webpackModules;
48 + webpackModuleLoading = WebpackMock.moduleLoading;
49 +
50 + jest.resetModules();
51 + jest.unmock('react');
52 + jest.unmock('react-server-dom-webpack/server');
53 + jest.mock('react-server-dom-webpack/client', () =>
54 + require('react-server-dom-webpack/client.edge'),
55 + );
56 React = require('react');
57 ReactDOMServer = require('react-dom/server.edge');
46 - ReactServerDOMServer = require('react-server-dom-webpack/server.edge');
47 - ReactServerDOMClient = require('react-server-dom-webpack/client.edge');
58 + ReactServerDOMClient = require('react-server-dom-webpack/client');
59 use = React.use;
60 });
61
@@ -122,7 +133,10 @@ describe('ReactFlightDOMEdge', () => {
133 webpackMap,
134 );
135 const response = ReactServerDOMClient.createFromReadableStream(stream, {
125 - moduleMap: translationMap,
136 + ssrManifest: {
137 + moduleMap: translationMap,
138 + moduleLoading: webpackModuleLoading,
139 + },
140 });
141
142 function ClientRoot() {
@@ -154,7 +168,15 @@ describe('ReactFlightDOMEdge', () => {
168 expect(serializedContent).not.toContain('\\"');
169 expect(serializedContent).toContain('\t');
170
157 - const result = await ReactServerDOMClient.createFromReadableStream(stream2);
171 + const result = await ReactServerDOMClient.createFromReadableStream(
172 + stream2,
173 + {
174 + ssrManifest: {
175 + moduleMap: null,
176 + moduleLoading: null,
177 + },
178 + },
179 + );
180 // Should still match the result when parsed
181 expect(result.text).toBe(testString);
182 expect(result.text2).toBe(testString2);
@@ -183,7 +205,12 @@ describe('ReactFlightDOMEdge', () => {
205 const stream = passThrough(
206 ReactServerDOMServer.renderToReadableStream(buffers),
207 );
186 - const result = await ReactServerDOMClient.createFromReadableStream(stream);
208 + const result = await ReactServerDOMClient.createFromReadableStream(stream, {
209 + ssrManifest: {
210 + moduleMap: null,
211 + moduleLoading: null,
212 + },
213 + });
214 expect(result).toEqual(buffers);
215 });
216 });
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js
+123 -23
@@ -140,7 +140,12 @@ describe('ReactFlightDOMForm', () => {
140 );
141 }
142 const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
143 - const response = ReactServerDOMClient.createFromReadableStream(rscStream);
143 + const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
144 + ssrManifest: {
145 + moduleMap: null,
146 + moduleLoading: null,
147 + },
148 + });
149 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
150 await readIntoContainer(ssrStream);
151
@@ -203,7 +208,12 @@ describe('ReactFlightDOMForm', () => {
208 );
209 }
210 const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
206 - const response = ReactServerDOMClient.createFromReadableStream(rscStream);
211 + const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
212 + ssrManifest: {
213 + moduleMap: null,
214 + moduleLoading: null,
215 + },
216 + });
217 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
218 await readIntoContainer(ssrStream);
219
@@ -239,7 +249,12 @@ describe('ReactFlightDOMForm', () => {
249 );
250 }
251 const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
242 - const response = ReactServerDOMClient.createFromReadableStream(rscStream);
252 + const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
253 + ssrManifest: {
254 + moduleMap: null,
255 + moduleLoading: null,
256 + },
257 + });
258 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
259 await readIntoContainer(ssrStream);
260
@@ -307,7 +322,12 @@ describe('ReactFlightDOMForm', () => {
322 <ClientRef action={serverAction} />,
323 webpackMap,
324 );
310 - const response = ReactServerDOMClient.createFromReadableStream(rscStream);
325 + const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
326 + ssrManifest: {
327 + moduleMap: null,
328 + moduleLoading: null,
329 + },
330 + });
331 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
332 await readIntoContainer(ssrStream);
333
@@ -349,7 +369,12 @@ describe('ReactFlightDOMForm', () => {
369 <ClientRef action={serverAction} />,
370 webpackMap,
371 );
352 - const response = ReactServerDOMClient.createFromReadableStream(rscStream);
372 + const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
373 + ssrManifest: {
374 + moduleMap: null,
375 + moduleLoading: null,
376 + },
377 + });
378 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
379 await readIntoContainer(ssrStream);
380
@@ -392,7 +417,12 @@ describe('ReactFlightDOMForm', () => {
417 <ClientRef action={serverAction} />,
418 webpackMap,
419 );
395 - const response = ReactServerDOMClient.createFromReadableStream(rscStream);
420 + const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
421 + ssrManifest: {
422 + moduleMap: null,
423 + moduleLoading: null,
424 + },
425 + });
426 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
427 await readIntoContainer(ssrStream);
428
@@ -410,8 +440,15 @@ describe('ReactFlightDOMForm', () => {
440 <ClientRef action={serverAction} />,
441 webpackMap,
442 );
413 - const postbackResponse =
414 - ReactServerDOMClient.createFromReadableStream(postbackRscStream);
443 + const postbackResponse = ReactServerDOMClient.createFromReadableStream(
444 + postbackRscStream,
445 + {
446 + ssrManifest: {
447 + moduleMap: null,
448 + moduleLoading: null,
449 + },
450 + },
451 + );
452 const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
453 postbackResponse,
454 {experimental_formState: formState},
@@ -471,7 +508,15 @@ describe('ReactFlightDOMForm', () => {
508 <ClientRef action={serverAction.bind(null, 1)} />,
509 webpackMap,
510 );
474 - const response = ReactServerDOMClient.createFromReadableStream(rscStream);
511 + const response = ReactServerDOMClient.createFromReadableStream(
512 + rscStream,
513 + {
514 + ssrManifest: {
515 + moduleMap: null,
516 + moduleLoading: null,
517 + },
518 + },
519 + );
520 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
521 await readIntoContainer(ssrStream);
522
@@ -493,8 +538,15 @@ describe('ReactFlightDOMForm', () => {
538 <ClientRef action={serverAction.bind(null, 5)} />,
539 webpackMap,
540 );
496 - const postbackResponse =
497 - ReactServerDOMClient.createFromReadableStream(postbackRscStream);
541 + const postbackResponse = ReactServerDOMClient.createFromReadableStream(
542 + postbackRscStream,
543 + {
544 + ssrManifest: {
545 + moduleMap: null,
546 + moduleLoading: null,
547 + },
548 + },
549 + );
550 const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
551 postbackResponse,
552 {experimental_formState: formState},
@@ -518,8 +570,15 @@ describe('ReactFlightDOMForm', () => {
570 <ClientRef action={serverAction.bind(null, 5)} />,
571 webpackMap,
572 );
521 - const postbackResponse2 =
522 - ReactServerDOMClient.createFromReadableStream(postbackRscStream2);
573 + const postbackResponse2 = ReactServerDOMClient.createFromReadableStream(
574 + postbackRscStream2,
575 + {
576 + ssrManifest: {
577 + moduleMap: null,
578 + moduleLoading: null,
579 + },
580 + },
581 + );
582 const postbackSsrStream2 = await ReactDOMServer.renderToReadableStream(
583 postbackResponse2,
584 {experimental_formState: formState2},
@@ -570,7 +629,12 @@ describe('ReactFlightDOMForm', () => {
629 <ClientRef action={increaseBy1} />,
630 webpackMap,
631 );
573 - const response = ReactServerDOMClient.createFromReadableStream(rscStream);
632 + const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
633 + ssrManifest: {
634 + moduleMap: null,
635 + moduleLoading: null,
636 + },
637 + });
638 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
639 await readIntoContainer(ssrStream);
640
@@ -590,8 +654,15 @@ describe('ReactFlightDOMForm', () => {
654 <ClientRef action={increaseBy5} />,
655 webpackMap,
656 );
593 - const postbackResponse =
594 - ReactServerDOMClient.createFromReadableStream(postbackRscStream);
657 + const postbackResponse = ReactServerDOMClient.createFromReadableStream(
658 + postbackRscStream,
659 + {
660 + ssrManifest: {
661 + moduleMap: null,
662 + moduleLoading: null,
663 + },
664 + },
665 + );
666 const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
667 postbackResponse,
668 {experimental_formState: formState},
@@ -633,7 +704,12 @@ describe('ReactFlightDOMForm', () => {
704 <Page1Ref action={serverAction} permalink="/permalink" />,
705 webpackMap,
706 );
636 - const response = ReactServerDOMClient.createFromReadableStream(rscStream);
707 + const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
708 + ssrManifest: {
709 + moduleMap: null,
710 + moduleLoading: null,
711 + },
712 + });
713 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
714 await readIntoContainer(ssrStream);
715
@@ -654,8 +730,15 @@ describe('ReactFlightDOMForm', () => {
730 <Page2Ref action={serverAction} permalink="/permalink" />,
731 webpackMap,
732 );
657 - const postbackResponse =
658 - ReactServerDOMClient.createFromReadableStream(postbackRscStream);
733 + const postbackResponse = ReactServerDOMClient.createFromReadableStream(
734 + postbackRscStream,
735 + {
736 + ssrManifest: {
737 + moduleMap: null,
738 + moduleLoading: null,
739 + },
740 + },
741 + );
742 const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
743 postbackResponse,
744 {experimental_formState: formState},
@@ -675,8 +758,15 @@ describe('ReactFlightDOMForm', () => {
758 <Page1Ref action={serverAction} permalink="/some-other-permalink" />,
759 webpackMap,
760 );
678 - const postbackResponse2 =
679 - ReactServerDOMClient.createFromReadableStream(postbackRscStream2);
761 + const postbackResponse2 = ReactServerDOMClient.createFromReadableStream(
762 + postbackRscStream2,
763 + {
764 + ssrManifest: {
765 + moduleMap: null,
766 + moduleLoading: null,
767 + },
768 + },
769 + );
770 const postbackSsrStream2 = await ReactDOMServer.renderToReadableStream(
771 postbackResponse2,
772 {experimental_formState: formState2},
@@ -713,7 +803,12 @@ describe('ReactFlightDOMForm', () => {
803 <ClientRef action={serverAction} />,
804 webpackMap,
805 );
716 - const response = ReactServerDOMClient.createFromReadableStream(rscStream);
806 + const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
807 + ssrManifest: {
808 + moduleMap: null,
809 + moduleLoading: null,
810 + },
811 + });
812 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
813 await readIntoContainer(ssrStream);
814
@@ -754,7 +849,12 @@ describe('ReactFlightDOMForm', () => {
849 <ClientRef action={serverAction} />,
850 webpackMap,
851 );
757 - const response = ReactServerDOMClient.createFromReadableStream(rscStream);
852 + const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
853 + ssrManifest: {
854 + moduleMap: null,
855 + moduleLoading: null,
856 + },
857 + });
858 const ssrStream = await ReactDOMServer.renderToReadableStream(response);
859 await readIntoContainer(ssrStream);
860
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+103 -10
@@ -16,6 +16,7 @@ global.setImmediate = cb => cb();
16 let clientExports;
17 let webpackMap;
18 let webpackModules;
19 +let webpackModuleLoading;
20 let React;
21 let ReactDOMServer;
22 let ReactServerDOMServer;
@@ -28,18 +29,28 @@ describe('ReactFlightDOMNode', () => {
29 jest.resetModules();
30
31 // Simulate the condition resolution
32 + jest.mock('react', () => require('react/react.shared-subset'));
33 jest.mock('react-server-dom-webpack/server', () =>
34 require('react-server-dom-webpack/server.node'),
35 );
36 + ReactServerDOMServer = require('react-server-dom-webpack/server');
37
38 const WebpackMock = require('./utils/WebpackMock');
39 clientExports = WebpackMock.clientExports;
40 webpackMap = WebpackMock.webpackMap;
41 webpackModules = WebpackMock.webpackModules;
42 + webpackModuleLoading = WebpackMock.moduleLoading;
43 +
44 + jest.resetModules();
45 + jest.unmock('react');
46 + jest.unmock('react-server-dom-webpack/server');
47 + jest.mock('react-server-dom-webpack/client', () =>
48 + require('react-server-dom-webpack/client.node'),
49 + );
50 +
51 React = require('react');
52 ReactDOMServer = require('react-dom/server.node');
41 - ReactServerDOMServer = require('react-server-dom-webpack/server.node');
42 - ReactServerDOMClient = require('react-server-dom-webpack/client.node');
53 + ReactServerDOMClient = require('react-server-dom-webpack/client');
54 Stream = require('stream');
55 use = React.use;
56 });
@@ -68,7 +79,11 @@ describe('ReactFlightDOMNode', () => {
79 }
80 // The Client build may not have the same IDs as the Server bundles for the same
81 // component.
71 - const ClientComponentOnTheClient = clientExports(ClientComponent);
82 + const ClientComponentOnTheClient = clientExports(
83 + ClientComponent,
84 + 123,
85 + 'path/to/chunk.js',
86 + );
87 const ClientComponentOnTheServer = clientExports(ClientComponent);
88
89 // In the SSR bundle this module won't exist. We simulate this by deleting it.
@@ -83,6 +98,10 @@ describe('ReactFlightDOMNode', () => {
98 '*': ssrMetadata,
99 },
100 };
101 + const ssrManifest = {
102 + moduleMap: translationMap,
103 + moduleLoading: webpackModuleLoading,
104 + };
105
106 function App() {
107 return <ClientComponentOnTheClient />;
@@ -93,14 +112,16 @@ describe('ReactFlightDOMNode', () => {
112 webpackMap,
113 );
114 const readable = new Stream.PassThrough();
96 - const response = ReactServerDOMClient.createFromNodeStream(
97 - readable,
98 - translationMap,
99 - );
115 + let response;
116
117 stream.pipe(readable);
118
119 function ClientRoot() {
120 + if (response) return use(response);
121 + response = ReactServerDOMClient.createFromNodeStream(
122 + readable,
123 + ssrManifest,
124 + );
125 return use(response);
126 }
127
@@ -108,7 +129,9 @@ describe('ReactFlightDOMNode', () => {
129 <ClientRoot />,
130 );
131 const result = await readResult(ssrStream);
111 - expect(result).toEqual('<span>Client Component</span>');
132 + expect(result).toEqual(
133 + '<script src="/path/to/chunk.js" async=""></script><span>Client Component</span>',
134 + );
135 });
136
137 it('should encode long string in a compact format', async () => {
@@ -121,7 +144,10 @@ describe('ReactFlightDOMNode', () => {
144 const readable = new Stream.PassThrough();
145
146 const stringResult = readResult(readable);
124 - const parsedResult = ReactServerDOMClient.createFromNodeStream(readable);
147 + const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, {
148 + moduleMap: {},
149 + moduleLoading: webpackModuleLoading,
150 + });
151
152 stream.pipe(readable);
153
@@ -160,9 +186,76 @@ describe('ReactFlightDOMNode', () => {
186 ];
187 const stream = ReactServerDOMServer.renderToPipeableStream(buffers);
188 const readable = new Stream.PassThrough();
163 - const promise = ReactServerDOMClient.createFromNodeStream(readable);
189 + const promise = ReactServerDOMClient.createFromNodeStream(readable, {
190 + moduleMap: {},
191 + moduleLoading: webpackModuleLoading,
192 + });
193 stream.pipe(readable);
194 const result = await promise;
195 expect(result).toEqual(buffers);
196 });
197 +
198 + it('should allow accept a nonce option for Flight preinitialized scripts', async () => {
199 + function ClientComponent() {
200 + return <span>Client Component</span>;
201 + }
202 + // The Client build may not have the same IDs as the Server bundles for the same
203 + // component.
204 + const ClientComponentOnTheClient = clientExports(
205 + ClientComponent,
206 + 123,
207 + 'path/to/chunk.js',
208 + );
209 + const ClientComponentOnTheServer = clientExports(ClientComponent);
210 +
211 + // In the SSR bundle this module won't exist. We simulate this by deleting it.
212 + const clientId = webpackMap[ClientComponentOnTheClient.$$id].id;
213 + delete webpackModules[clientId];
214 +
215 + // Instead, we have to provide a translation from the client meta data to the SSR
216 + // meta data.
217 + const ssrMetadata = webpackMap[ClientComponentOnTheServer.$$id];
218 + const translationMap = {
219 + [clientId]: {
220 + '*': ssrMetadata,
221 + },
222 + };
223 + const ssrManifest = {
224 + moduleMap: translationMap,
225 + moduleLoading: webpackModuleLoading,
226 + };
227 +
228 + function App() {
229 + return <ClientComponentOnTheClient />;
230 + }
231 +
232 + const stream = ReactServerDOMServer.renderToPipeableStream(
233 + <App />,
234 + webpackMap,
235 + );
236 + const readable = new Stream.PassThrough();
237 + let response;
238 +
239 + stream.pipe(readable);
240 +
241 + function ClientRoot() {
242 + if (response) return use(response);
243 + response = ReactServerDOMClient.createFromNodeStream(
244 + readable,
245 + ssrManifest,
246 + {
247 + nonce: 'r4nd0m',
248 + },
249 + );
250 + return use(response);
251 + }
252 +
253 + const ssrStream = await ReactDOMServer.renderToPipeableStream(
254 + <ClientRoot />,
255 + );
256 + const result = await readResult(ssrStream);
257 + expect(result).toEqual(
258 + '<script src="/path/to/chunk.js" async="" nonce="r4nd0m"></script><span>Client Component</span>',
259 + );
260 + });
261 });
packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js
+16 -5
@@ -42,6 +42,9 @@ Module.prototype._compile = previousCompile;
42 exports.webpackMap = webpackClientMap;
43 exports.webpackModules = webpackClientModules;
44 exports.webpackServerMap = webpackServerMap;
45 +exports.moduleLoading = {
46 + prefix: '/',
47 +};
48
49 exports.clientModuleError = function clientModuleError(moduleError) {
50 const idx = '' + webpackModuleIdx++;
@@ -57,20 +60,28 @@ exports.clientModuleError = function clientModuleError(moduleError) {
60 return mod.exports;
61 };
62
60 -exports.clientExports = function clientExports(moduleExports) {
63 +exports.clientExports = function clientExports(
64 + moduleExports,
65 + chunkId,
66 + chunkFilename,
67 +) {
68 + const chunks = [];
69 + if (chunkId) {
70 + chunks.push(chunkId, chunkFilename);
71 + }
72 const idx = '' + webpackModuleIdx++;
73 webpackClientModules[idx] = moduleExports;
74 const path = url.pathToFileURL(idx).href;
75 webpackClientMap[path] = {
76 id: idx,
66 - chunks: [],
77 + chunks,
78 name: '*',
79 };
80 // We only add this if this test is testing ESM compat.
81 if ('__esModule' in moduleExports) {
82 webpackClientMap[path + '#'] = {
83 id: idx,
73 - chunks: [],
84 + chunks,
85 name: '',
86 };
87 }
@@ -80,7 +91,7 @@ exports.clientExports = function clientExports(moduleExports) {
91 for (const name in asyncModuleExports) {
92 webpackClientMap[path + '#' + name] = {
93 id: idx,
83 - chunks: [],
94 + chunks,
95 name: name,
96 };
97 }
@@ -96,7 +107,7 @@ exports.clientExports = function clientExports(moduleExports) {
107 };
108 webpackClientMap[path + '#split'] = {
109 id: splitIdx,
99 - chunks: [],
110 + chunks,
111 name: 's',
112 };
113 }
packages/react-server-dom-webpack/src/shared/ReactFlightImportMetadata.js new
+43
@@ -0,0 +1,43 @@
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 +export type ImportManifestEntry = {
11 + id: string,
12 + // chunks is a double indexed array of chunkId / chunkFilename pairs
13 + chunks: Array<string>,
14 + name: string,
15 +};
16 +
17 +// This is the parsed shape of the wire format which is why it is
18 +// condensed to only the essentialy information
19 +export type ImportMetadata =
20 + | [
21 + /* id */ string,
22 + /* chunks id/filename pairs, double indexed */ Array<string>,
23 + /* name */ string,
24 + /* async */ 1,
25 + ]
26 + | [
27 + /* id */ string,
28 + /* chunks id/filename pairs, double indexed */ Array<string>,
29 + /* name */ string,
30 + ];
31 +
32 +export const ID = 0;
33 +export const CHUNKS = 1;
34 +export const NAME = 2;
35 +// export const ASYNC = 3;
36 +
37 +// This logic is correct because currently only include the 4th tuple member
38 +// when the module is async. If that changes we will need to actually assert
39 +// the value is true. We don't index into the 4th slot because flow does not
40 +// like the potential out of bounds access
41 +export function isAsyncImport(metadata: ImportMetadata): boolean {
42 + return metadata.length === 4;
43 +}
packages/react/src/__tests__/ReactFetch-test.js
+9 -1
@@ -46,9 +46,17 @@ describe('ReactFetch', () => {
46 if (gate(flags => !flags.www)) {
47 jest.mock('react', () => require('react/react.shared-subset'));
48 }
49 + jest.mock('react-server-dom-webpack/server', () =>
50 + require('react-server-dom-webpack/server.browser'),
51 + );
52 + require('react-server-dom-webpack/src/__tests__/utils/WebpackMock');
53
54 React = require('react');
51 - ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
55 + ReactServerDOMServer = require('react-server-dom-webpack/server');
56 +
57 + jest.resetModules();
58 + jest.unmock('react');
59 + jest.unmock('react-server-dom-webpack/server');
60 ReactServerDOMClient = require('react-server-dom-webpack/client');
61 use = React.use;
62 cache = React.cache;
packages/react/src/__tests__/ReactFetchEdge-test.js
+9 -1
@@ -51,9 +51,17 @@ describe('ReactFetch', () => {
51 if (gate(flags => !flags.www)) {
52 jest.mock('react', () => require('react/react.shared-subset'));
53 }
54 + jest.mock('react-server-dom-webpack/server', () =>
55 + require('react-server-dom-webpack/server.edge'),
56 + );
57 + require('react-server-dom-webpack/src/__tests__/utils/WebpackMock');
58
59 React = require('react');
56 - ReactServerDOMServer = require('react-server-dom-webpack/server.edge');
60 + ReactServerDOMServer = require('react-server-dom-webpack/server');
61 +
62 + jest.resetModules();
63 + jest.unmock('react');
64 + jest.unmock('react-server-dom-webpack/server');
65 ReactServerDOMClient = require('react-server-dom-webpack/client');
66 use = React.use;
67 });
scripts/flow/environment.js
+3 -1
@@ -74,7 +74,9 @@ declare module 'EventListener' {
74 }
75
76 declare function __webpack_chunk_load__(id: string): Promise<mixed>;
77 -declare function __webpack_require__(id: string): any;
77 +declare var __webpack_require__: ((id: string) => any) & {
78 + u: string => string,
79 +};
80
81 declare module 'fs/promises' {
82 declare var access: (path: string, mode?: number) => Promise<void>;
scripts/jest/setupHostConfigs.js
+3
@@ -125,6 +125,9 @@ function mockAllConfigs(rendererInfo) {
125 fs.statSync(nodePath.join(process.cwd(), 'packages', candidate));
126 return jest.requireActual(candidate);
127 } catch (error) {
128 + if (error.code !== 'ENOENT') {
129 + throw error;
130 + }
131 // try without a part
132 }
133 parts.pop();
scripts/shared/inlinedHostConfigs.js
+13 -2
@@ -37,6 +37,8 @@ module.exports = [
37 'react-server-dom-webpack/server',
38 'react-server-dom-webpack/server.node.unbundled',
39 'react-server-dom-webpack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node
40 + 'react-server-dom-webpack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node
41 + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js',
42 'react-devtools',
43 'react-devtools-core',
44 'react-devtools-shell',
@@ -90,6 +92,8 @@ module.exports = [
92 'react-server-dom-webpack/server.browser',
93 'react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js', // react-server-dom-webpack/client.browser
94 'react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js', // react-server-dom-webpack/server.browser
95 + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js',
96 + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser.js',
97 'react-devtools',
98 'react-devtools-core',
99 'react-devtools-shell',
@@ -112,6 +116,7 @@ module.exports = [
116 'react-server-dom-esm',
117 'react-server-dom-esm/client',
118 'react-server-dom-esm/client.browser',
119 + 'react-server-dom-esm/src/ReactFlightDOMClientBrowser.js', // react-server-dom-esm/client.browser
120 'react-devtools',
121 'react-devtools-core',
122 'react-devtools-shell',
@@ -146,6 +151,8 @@ module.exports = [
151 'react-server-dom-webpack/server.edge',
152 'react-server-dom-webpack/src/ReactFlightDOMClientEdge.js', // react-server-dom-webpack/client.edge
153 'react-server-dom-webpack/src/ReactFlightDOMServerEdge.js', // react-server-dom-webpack/server.edge
154 + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js',
155 + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js',
156 'react-devtools',
157 'react-devtools-core',
158 'react-devtools-shell',
@@ -178,6 +185,9 @@ module.exports = [
185 'react-server-dom-webpack/server',
186 'react-server-dom-webpack/server.node',
187 'react-server-dom-webpack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node
188 + 'react-server-dom-webpack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node
189 + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js',
190 + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js',
191 'react-server-dom-webpack/node-register',
192 'react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js',
193 'react-devtools',
@@ -212,7 +222,8 @@ module.exports = [
222 'react-server-dom-esm/client.node',
223 'react-server-dom-esm/server',
224 'react-server-dom-esm/server.node',
215 - 'react-server-dom-esm/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node
225 + 'react-server-dom-esm/src/ReactFlightDOMServerNode.js', // react-server-dom-esm/server.node
226 + 'react-server-dom-esm/src/ReactFlightDOMClientNode.js', // react-server-dom-esm/client.node
227 'react-devtools',
228 'react-devtools-core',
229 'react-devtools-shell',
@@ -234,7 +245,7 @@ module.exports = [
245 'react-dom/src/ReactDOMSharedSubset.js',
246 'react-dom-bindings',
247 'react-server-dom-webpack',
237 - 'react-dom/src/server/ReactDOMLegacyServerImpl.js', // not an entrypoint, but only usable in *Brower and *Node files
248 + 'react-dom/src/server/ReactDOMLegacyServerImpl.js', // not an entrypoint, but only usable in *Browser and *Node files
249 'react-dom/src/server/ReactDOMLegacyServerBrowser.js', // react-dom/server.browser
250 'react-dom/src/server/ReactDOMLegacyServerNode.js', // react-dom/server.node
251 'react-dom/src/server/ReactDOMLegacyServerNode.classic.fb.js',