Skip to content

Commit da6c23a

Browse files
authored
[Flight] Fallback to importing the whole module instead of encoding every name (#26624)
We currently don't just "require" a module by its module id/path. We encode the pair of module id/path AND its export name. That's because with module splitting, a single original module can end up in two or more separate modules by name. Therefore the manifest files need to encode how to require the whole module as well as how to require each export name. In practice, we don't currently use this because we end up forcing Webpack to deopt and keep it together as a single module, and we don't even have the code in the Webpack plugin to write separate values for each export name. The problem is with CJS we don't statically know what all the export names will be. Since these cases will never be module split, we don't really need to know. This changes the Flight requires to first look for the specific name we're loading and then if that name doesn't exist in the manifest we fallback to looking for the `"*"` name containing the entire module and look for the name in there at runtime. We could probably optimize this a bit if we assume that CJS modules on the server never get built with a name. That way we don't have to do the failed lookup. Additionally, since we've recently merged filepath + name into a single string instead of two values, we now have to split those back out by parsing the string. This is especially unfortunate for server references since those should really not reveal what they are but be a hash or something. The solution might just be to split them back out into two separate fields again. cc @shuding
1 parent 2bfe4b2 commit da6c23a

File tree

7 files changed

+263
-52
lines changed

7 files changed

+263
-52
lines changed

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,29 @@ export function resolveClientReference<T>(
3939
bundlerConfig: SSRManifest,
4040
metadata: ClientReferenceMetadata,
4141
): ClientReference<T> {
42-
const resolvedModuleData = bundlerConfig[metadata.id][metadata.name];
43-
return resolvedModuleData;
42+
const moduleExports = bundlerConfig[metadata.id];
43+
let resolvedModuleData = moduleExports[metadata.name];
44+
let name;
45+
if (resolvedModuleData) {
46+
// The potentially aliased name.
47+
name = resolvedModuleData.name;
48+
} else {
49+
// If we don't have this specific name, we might have the full module.
50+
resolvedModuleData = moduleExports['*'];
51+
if (!resolvedModuleData) {
52+
throw new Error(
53+
'Could not find the module "' +
54+
metadata.id +
55+
'" in the React SSR Manifest. ' +
56+
'This is probably a bug in the React Server Components bundler.',
57+
);
58+
}
59+
name = metadata.name;
60+
}
61+
return {
62+
specifier: resolvedModuleData.specifier,
63+
name: name,
64+
};
4465
}
4566

4667
export function resolveServerReference<T>(

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

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,31 @@ export function resolveClientReference<T>(
4040
metadata: ClientReferenceMetadata,
4141
): ClientReference<T> {
4242
if (bundlerConfig) {
43-
const resolvedModuleData = bundlerConfig[metadata.id][metadata.name];
44-
if (metadata.async) {
45-
return {
46-
id: resolvedModuleData.id,
47-
chunks: resolvedModuleData.chunks,
48-
name: resolvedModuleData.name,
49-
async: true,
50-
};
43+
const moduleExports = bundlerConfig[metadata.id];
44+
let resolvedModuleData = moduleExports[metadata.name];
45+
let name;
46+
if (resolvedModuleData) {
47+
// The potentially aliased name.
48+
name = resolvedModuleData.name;
5149
} else {
52-
return resolvedModuleData;
50+
// If we don't have this specific name, we might have the full module.
51+
resolvedModuleData = moduleExports['*'];
52+
if (!resolvedModuleData) {
53+
throw new Error(
54+
'Could not find the module "' +
55+
metadata.id +
56+
'" in the React SSR Manifest. ' +
57+
'This is probably a bug in the React Server Components bundler.',
58+
);
59+
}
60+
name = metadata.name;
5361
}
62+
return {
63+
id: resolvedModuleData.id,
64+
chunks: resolvedModuleData.chunks,
65+
name: name,
66+
async: !!metadata.async,
67+
};
5468
}
5569
return metadata;
5670
}
@@ -59,8 +73,37 @@ export function resolveServerReference<T>(
5973
bundlerConfig: ServerManifest,
6074
id: ServerReferenceId,
6175
): ClientReference<T> {
62-
// This needs to return async: true if it's an async module.
63-
return bundlerConfig[id];
76+
let name = '';
77+
let resolvedModuleData = bundlerConfig[id];
78+
if (resolvedModuleData) {
79+
// The potentially aliased name.
80+
name = resolvedModuleData.name;
81+
} else {
82+
// We didn't find this specific export name but we might have the * export
83+
// which contains this name as well.
84+
// TODO: It's unfortunate that we now have to parse this string. We should
85+
// probably go back to encoding path and name separately on the client reference.
86+
const idx = id.lastIndexOf('#');
87+
if (idx !== -1) {
88+
name = id.substr(idx + 1);
89+
resolvedModuleData = bundlerConfig[id.substr(0, idx)];
90+
}
91+
if (!resolvedModuleData) {
92+
throw new Error(
93+
'Could not find the module "' +
94+
id +
95+
'" in the React Server Manifest. ' +
96+
'This is probably a bug in the React Server Components bundler.',
97+
);
98+
}
99+
}
100+
// 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+
};
64107
}
65108

66109
// The chunk cache contains all the chunks we've preloaded so far.

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

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,37 @@ export function resolveClientReferenceMetadata<T>(
5858
config: ClientManifest,
5959
clientReference: ClientReference<T>,
6060
): ClientReferenceMetadata {
61-
const resolvedModuleData = config[clientReference.$$id];
62-
if (clientReference.$$async) {
63-
return {
64-
id: resolvedModuleData.id,
65-
chunks: resolvedModuleData.chunks,
66-
name: resolvedModuleData.name,
67-
async: true,
68-
};
61+
const modulePath = clientReference.$$id;
62+
let name = '';
63+
let resolvedModuleData = config[modulePath];
64+
if (resolvedModuleData) {
65+
// The potentially aliased name.
66+
name = resolvedModuleData.name;
6967
} else {
70-
return resolvedModuleData;
68+
// We didn't find this specific export name but we might have the * export
69+
// which contains this name as well.
70+
// TODO: It's unfortunate that we now have to parse this string. We should
71+
// probably go back to encoding path and name separately on the client reference.
72+
const idx = modulePath.lastIndexOf('#');
73+
if (idx !== -1) {
74+
name = modulePath.substr(idx + 1);
75+
resolvedModuleData = config[modulePath.substr(0, idx)];
76+
}
77+
if (!resolvedModuleData) {
78+
throw new Error(
79+
'Could not find the module "' +
80+
modulePath +
81+
'" in the React Client Manifest. ' +
82+
'This is probably a bug in the React Server Components bundler.',
83+
);
84+
}
7185
}
86+
return {
87+
id: resolvedModuleData.id,
88+
chunks: resolvedModuleData.chunks,
89+
name: name,
90+
async: !!clientReference.$$async,
91+
};
7292
}
7393

7494
export function getServerReferenceId<T>(

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,6 @@ export default class ReactFlightWebpackPlugin {
247247
return;
248248
}
249249

250-
const moduleProvidedExports = compilation.moduleGraph
251-
.getExportsInfo(module)
252-
.getProvidedExports();
253-
254250
const href = pathToFileURL(module.resource).href;
255251

256252
if (href !== undefined) {
@@ -267,6 +263,13 @@ export default class ReactFlightWebpackPlugin {
267263
specifier: href,
268264
name: '*',
269265
};
266+
267+
// TODO: If this module ends up split into multiple modules, then
268+
// we should encode each the chunks needed for the specific export.
269+
// When the module isn't split, it doesn't matter and we can just
270+
// encode the id of the whole module. This code doesn't currently
271+
// deal with module splitting so is likely broken from ESM anyway.
272+
/*
270273
clientManifest[href + '#'] = {
271274
id,
272275
chunks: chunkIds,
@@ -277,6 +280,10 @@ export default class ReactFlightWebpackPlugin {
277280
name: '',
278281
};
279282
283+
const moduleProvidedExports = compilation.moduleGraph
284+
.getExportsInfo(module)
285+
.getProvidedExports();
286+
280287
if (Array.isArray(moduleProvidedExports)) {
281288
moduleProvidedExports.forEach(function (name) {
282289
clientManifest[href + '#' + name] = {
@@ -290,6 +297,7 @@ export default class ReactFlightWebpackPlugin {
290297
};
291298
});
292299
}
300+
*/
293301

294302
ssrManifest[id] = ssrExports;
295303
}

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,45 @@ describe('ReactFlightDOM', () => {
334334
expect(container.innerHTML).toBe('<p>Hello World</p>');
335335
});
336336

337+
// @gate enableUseHook
338+
it('should be able to render a module split named component export', async () => {
339+
const Module = {
340+
// This gets split into a separate module from the original one.
341+
split: function ({greeting}) {
342+
return greeting + ' World';
343+
},
344+
};
345+
346+
function Print({response}) {
347+
return <p>{use(response)}</p>;
348+
}
349+
350+
function App({response}) {
351+
return (
352+
<Suspense fallback={<h1>Loading...</h1>}>
353+
<Print response={response} />
354+
</Suspense>
355+
);
356+
}
357+
358+
const {split: Component} = clientExports(Module);
359+
360+
const {writable, readable} = getTestStream();
361+
const {pipe} = ReactServerDOMServer.renderToPipeableStream(
362+
<Component greeting={'Hello'} />,
363+
webpackMap,
364+
);
365+
pipe(writable);
366+
const response = ReactServerDOMClient.createFromReadableStream(readable);
367+
368+
const container = document.createElement('div');
369+
const root = ReactDOMClient.createRoot(container);
370+
await act(() => {
371+
root.render(<App response={response} />);
372+
});
373+
expect(container.innerHTML).toBe('<p>Hello World</p>');
374+
});
375+
337376
// @gate enableUseHook
338377
it('should unwrap async module references', async () => {
339378
const AsyncModule = Promise.resolve(function AsyncModule({text}) {

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,35 @@ describe('ReactFlightDOMBrowser', () => {
7575
}
7676

7777
function requireServerRef(ref) {
78-
const metaData = webpackServerMap[ref];
79-
const mod = __webpack_require__(metaData.id);
80-
if (metaData.name === '*') {
78+
let name = '';
79+
let resolvedModuleData = webpackServerMap[ref];
80+
if (resolvedModuleData) {
81+
// The potentially aliased name.
82+
name = resolvedModuleData.name;
83+
} else {
84+
// We didn't find this specific export name but we might have the * export
85+
// which contains this name as well.
86+
// TODO: It's unfortunate that we now have to parse this string. We should
87+
// probably go back to encoding path and name separately on the client reference.
88+
const idx = ref.lastIndexOf('#');
89+
if (idx !== -1) {
90+
name = ref.substr(idx + 1);
91+
resolvedModuleData = webpackServerMap[ref.substr(0, idx)];
92+
}
93+
if (!resolvedModuleData) {
94+
throw new Error(
95+
'Could not find the module "' +
96+
ref +
97+
'" in the React Client Manifest. ' +
98+
'This is probably a bug in the React Server Components bundler.',
99+
);
100+
}
101+
}
102+
const mod = __webpack_require__(resolvedModuleData.id);
103+
if (name === '*') {
81104
return mod;
82105
}
83-
return mod[metaData.name];
106+
return mod[name];
84107
}
85108

86109
async function callServer(actionId, body) {
@@ -824,6 +847,52 @@ describe('ReactFlightDOMBrowser', () => {
824847
expect(result).toBe('Hello HI');
825848
});
826849

850+
it('can call a module split server function', async () => {
851+
let actionProxy;
852+
853+
function Client({action}) {
854+
actionProxy = action;
855+
return 'Click Me';
856+
}
857+
858+
function greet(text) {
859+
return 'Hello ' + text;
860+
}
861+
862+
const ServerModule = serverExports({
863+
// This gets split into another module
864+
split: greet,
865+
});
866+
const ClientRef = clientExports(Client);
867+
868+
const stream = ReactServerDOMServer.renderToReadableStream(
869+
<ClientRef action={ServerModule.split} />,
870+
webpackMap,
871+
);
872+
873+
const response = ReactServerDOMClient.createFromReadableStream(stream, {
874+
async callServer(ref, args) {
875+
const body = await ReactServerDOMClient.encodeReply(args);
876+
return callServer(ref, body);
877+
},
878+
});
879+
880+
function App() {
881+
return use(response);
882+
}
883+
884+
const container = document.createElement('div');
885+
const root = ReactDOMClient.createRoot(container);
886+
await act(() => {
887+
root.render(<App />);
888+
});
889+
expect(container.innerHTML).toBe('Click Me');
890+
expect(typeof actionProxy).toBe('function');
891+
892+
const result = await actionProxy('Split');
893+
expect(result).toBe('Hello Split');
894+
});
895+
827896
it('can bind arguments to a server reference', async () => {
828897
let actionProxy;
829898

0 commit comments

Comments
 (0)