Skip to content

Commit a6011c2

Browse files
committed
ESM implementation of module preinitialization
1 parent 2096cbe commit a6011c2

19 files changed

+141
-59
lines changed

fixtures/flight-esm/.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v18

fixtures/flight-esm/server/global.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const compress = require('compression');
1010
const chalk = require('chalk');
1111
const express = require('express');
1212
const http = require('http');
13+
const React = require('react');
1314

1415
const {renderToPipeableStream} = require('react-dom/server');
1516
const {createFromNodeStream} = require('react-server-dom-esm/client');
@@ -62,23 +63,39 @@ app.all('/', async function (req, res, next) {
6263
if (req.accepts('text/html')) {
6364
try {
6465
const rscResponse = await promiseForData;
65-
6666
const moduleBaseURL = '/src';
6767

6868
// For HTML, we're a "client" emulator that runs the client code,
6969
// so we start by consuming the RSC payload. This needs the local file path
7070
// 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+
};
7686
// Render it into HTML by resolving the client components
7787
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'],
8299
});
83100
pipe(res);
84101
} catch (e) {
@@ -89,6 +106,7 @@ app.all('/', async function (req, res, next) {
89106
} else {
90107
try {
91108
const rscResponse = await promiseForData;
109+
92110
// For other request, we pass-through the RSC payload.
93111
res.set('Content-type', 'text/x-component');
94112
rscResponse.on('data', data => {

fixtures/flight-esm/server/region.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ async function renderApp(res, returnValue) {
3636
// For client-invoked server actions we refresh the tree and return a return value.
3737
const payload = returnValue ? {returnValue, root} : root;
3838
const {pipe} = renderToPipeableStream(payload, moduleBasePath);
39+
await new Promise(res => {
40+
setTimeout(res, 1000);
41+
});
3942
pipe(res);
4043
}
4144

fixtures/flight-esm/src/App.js

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,6 @@ import {getServerState} from './ServerState.js';
99

1010
const h = React.createElement;
1111

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-
2212
export default async function App() {
2313
const res = await fetch('http://localhost:3001/todos');
2414
const todos = await res.json();
@@ -42,12 +32,6 @@ export default async function App() {
4232
rel: 'stylesheet',
4333
href: '/src/style.css',
4434
precedence: 'default',
45-
}),
46-
h('script', {
47-
type: 'importmap',
48-
dangerouslySetInnerHTML: {
49-
__html: JSON.stringify(importMap),
50-
},
5135
})
5236
),
5337
h(
@@ -84,9 +68,7 @@ export default async function App() {
8468
'Like'
8569
)
8670
)
87-
),
88-
// TODO: Move this to bootstrapModules.
89-
h('script', {type: 'module', src: '/src/index.js'})
71+
)
9072
)
9173
);
9274
}

fixtures/flight/server/global.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,17 @@ app.all('/', async function (req, res, next) {
147147
let root;
148148
let Root = () => {
149149
if (root) {
150-
return root;
150+
return React.use(root);
151151
}
152-
root = createFromNodeStream(rscResponse, ssrBundleConfig.ssrManifest, {
153-
moduleLoading: ssrBundleConfig.moduleLoading,
154-
});
155-
return root;
152+
return React.use(
153+
(root = createFromNodeStream(
154+
rscResponse,
155+
ssrBundleConfig.ssrManifest,
156+
{
157+
moduleLoading: ssrBundleConfig.moduleLoading,
158+
}
159+
))
160+
);
156161
};
157162
// Render it into HTML by resolving the client components
158163
res.set('Content-type', 'text/html');

fixtures/flight/server/region.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ async function renderApp(res, returnValue) {
9595
// For client-invoked server actions we refresh the tree and return a return value.
9696
const payload = returnValue ? {returnValue, root} : root;
9797
const {pipe} = renderToPipeableStream(payload, moduleMap);
98+
await new Promise(res => {
99+
setTimeout(res, 1000);
100+
});
98101
pipe(res);
99102
}
100103

packages/react-client/src/forks/ReactFlightClientConfig.custom.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
declare var $$$config: any;
2727

28+
export opaque type ModuleLoading = mixed;
2829
export opaque type SSRManifest = mixed;
2930
export opaque type ServerManifest = mixed;
3031
export opaque type ServerReferenceId = string;

packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
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';
1213
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1314
export const usedWithSSR = false;

packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js

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

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';
1213
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1314
export const usedWithSSR = true;

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5328,6 +5328,7 @@ function preinit(href: string, options: PreinitOptions): void {
53285328
}
53295329
return;
53305330
}
5331+
case 'module':
53315332
case 'script': {
53325333
const src = href;
53335334
const key = getResourceKey(as, src);

packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,18 @@ export function dispatchHint(code: string, model: HintModel): void {
6464
}
6565
}
6666

67-
export function preinitModulesForSSR(href: string, crossOrigin: ?string) {
67+
export function preinitModuleForSSR(href: string, crossOrigin: ?string) {
68+
const dispatcher = ReactDOMCurrentDispatcher.current;
69+
if (dispatcher) {
70+
if (typeof crossOrigin === 'string') {
71+
dispatcher.preinitModule(href, {crossOrigin});
72+
} else {
73+
dispatcher.preinitModule(href);
74+
}
75+
}
76+
}
77+
78+
export function preinitScriptForSSR(href: string, crossOrigin: ?string) {
6879
const dispatcher = ReactDOMCurrentDispatcher.current;
6980
if (dispatcher) {
7081
if (typeof crossOrigin === 'string') {

packages/react-server-dom-esm/src/ReactFlightClientConfigESMBundler.js renamed to packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ import type {
1212
FulfilledThenable,
1313
RejectedThenable,
1414
} from 'shared/ReactTypes';
15+
import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig';
1516

1617
export type SSRManifest = string; // Module root path
1718

1819
export type ServerManifest = string; // Module root path
1920

2021
export type ServerReferenceId = string;
2122

23+
import {prepareDestinationForModuleImpl} from 'react-client/src/ReactFlightClientConfig';
24+
2225
export opaque type ClientReferenceMetadata = [
2326
string, // module path
2427
string, // export name
@@ -30,6 +33,19 @@ export opaque type ClientReference<T> = {
3033
name: string,
3134
};
3235

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+
metadata: ClientReferenceMetadata,
45+
) {
46+
prepareDestinationForModuleImpl(moduleLoading, metadata[0]);
47+
}
48+
3349
export function resolveClientReference<T>(
3450
bundlerConfig: SSRManifest,
3551
metadata: ClientReferenceMetadata,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
) {
16+
// In the browser we don't need to prepare our destination since the browser is the Destination
17+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
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+
) {
25+
if (typeof moduleLoading === 'string') {
26+
preinitModuleForSSR(moduleLoading + mod, undefined);
27+
} else if (moduleLoading !== null) {
28+
preinitModuleForSSR(moduleLoading.prefix + mod, moduleLoading.crossOrigin);
29+
}
30+
}

packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export type Options = {
3636
function createResponseFromOptions(options: void | Options) {
3737
return createResponse(
3838
options && options.moduleBaseURL ? options.moduleBaseURL : '',
39+
null,
3940
options && options.callServer ? options.callServer : undefined,
4041
);
4142
}

packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,13 @@ export function createServerReference<A: Iterable<any>, T>(
4141
function createFromNodeStream<T>(
4242
stream: Readable,
4343
moduleRootPath: string,
44-
moduleBaseURL: string, // TODO: Used for preloading hints
44+
moduleBaseURL: string,
4545
): Thenable<T> {
46-
const response: Response = createResponse(moduleRootPath, noServerCall);
46+
const response: Response = createResponse(
47+
moduleRootPath,
48+
moduleBaseURL,
49+
noServerCall,
50+
);
4751
stream.on('data', chunk => {
4852
processBinaryChunk(response, chunk);
4953
});

packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js

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

10-
import {preinitModulesForSSR} from 'react-client/src/ReactFlightClientConfig';
10+
import {preinitScriptForSSR} from 'react-client/src/ReactFlightClientConfig';
1111

1212
export type ModuleLoading = null | {
1313
prefix: string,
@@ -21,7 +21,7 @@ export function prepareDestinationWithChunks(
2121
) {
2222
if (moduleLoading !== null) {
2323
for (let i = 1; i < chunks.length; i += 2) {
24-
preinitModulesForSSR(
24+
preinitScriptForSSR(
2525
moduleLoading.prefix + chunks[i],
2626
moduleLoading.crossOrigin,
2727
);

packages/shared/ReactVersion.js

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1 @@
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-
8-
// TODO: this is special because it gets imported during build.
9-
//
10-
// TODO: 18.0.0 has not been released to NPM;
11-
// It exists as a placeholder so that DevTools can support work tag changes between releases.
12-
// When we next publish a release, update the matching TODO in backend/renderer.js
13-
// TODO: This module is used both by the release scripts and to expose a version
14-
// at runtime. We should instead inject the version number as part of the build
15-
// process, and use the ReactVersions.js module as the single source of truth.
16-
export default '18.2.0';
1+
export default '18.3.0-PLACEHOLDER';

scripts/shared/inlinedHostConfigs.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ module.exports = [
112112
'react-server-dom-esm',
113113
'react-server-dom-esm/client',
114114
'react-server-dom-esm/client.browser',
115+
'react-server-dom-esm/src/ReactFlightDOMClientBrowser.js', // react-server-dom-esm/client.browser
115116
'react-devtools',
116117
'react-devtools-core',
117118
'react-devtools-shell',
@@ -214,7 +215,8 @@ module.exports = [
214215
'react-server-dom-esm/client.node',
215216
'react-server-dom-esm/server',
216217
'react-server-dom-esm/server.node',
217-
'react-server-dom-esm/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node
218+
'react-server-dom-esm/src/ReactFlightDOMServerNode.js', // react-server-dom-esm/server.node
219+
'react-server-dom-esm/src/ReactFlightDOMClientNode.js', // react-server-dom-esm/client.node
218220
'react-devtools',
219221
'react-devtools-core',
220222
'react-devtools-shell',

0 commit comments

Comments
 (0)