Skip to content

Commit b4f74ee

Browse files
authored
Handle async module for client components (#39953)
### Problem esm modules imports from client components will be compiled to `m = import('module-name')` when webpack bundles them for server components flight rendering. In this case, they will all become async modules since dyanmic imports will return a promise which react flight cannot handle it then results into module resolving error on server flight rendering. ### Solution * React flight renderer supports handling async modules in facebook/react#25138 * On next.js side leverage the module proxy change for each client reference, to make sure it always resolve the correct client module The idea is wrapping each module with a module proxy, and if the module is async and accessed as thenable, it will return a new module reference with `async` label to tell react to handle it as async modules: exported client reference `*` --> not async module (non thenable) --> original module reference `''` exported client reference `*` --> it's async module (thenable) --> wrapped module reference `'*'` with `async` label ### Note Since we need to check if user having incorrect gSSP/gSP specifying in layout client componet, so we still need to parse it and assign those info to the proxy (Does client module containing `ssr`, `ssg` exports). Otherwise the proxy will return the cached module reference
1 parent 1b61d1f commit b4f74ee

File tree

19 files changed

+12350
-4676
lines changed

19 files changed

+12350
-4676
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@
177177
"react-17": "npm:react@17.0.2",
178178
"react-dom": "18.2.0",
179179
"react-dom-17": "npm:react-dom@17.0.2",
180-
"react-dom-exp": "npm:react-dom@0.0.0-experimental-6ef466c68-20220816",
181-
"react-exp": "npm:react@0.0.0-experimental-6ef466c68-20220816",
180+
"react-dom-exp": "npm:react-dom@0.0.0-experimental-0de3ddf56-20220825",
181+
"react-exp": "npm:react@0.0.0-experimental-0de3ddf56-20220825",
182182
"react-ssr-prepass": "1.0.8",
183183
"react-virtualized": "9.22.3",
184184
"relay-compiler": "13.0.2",

packages/next/build/analysis/get-page-static-info.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export interface PageStaticInfo {
2828
* - Modules with `export function getStaticProps | getServerSideProps`
2929
* - Modules with `export { getStaticProps | getServerSideProps } <from ...>`
3030
*/
31-
function checkExports(swcAST: any) {
31+
export function checkExports(swcAST: any): { ssr: boolean; ssg: boolean } {
3232
if (Array.isArray(swcAST?.body)) {
3333
try {
3434
for (const node of swcAST.body) {

packages/next/build/webpack/loaders/next-flight-client-loader.ts

Lines changed: 0 additions & 177 deletions
This file was deleted.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its 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+
import { checkExports } from '../../../analysis/get-page-static-info'
9+
import { parse } from '../../../swc'
10+
11+
export default async function transformSource(
12+
this: any,
13+
source: string
14+
): Promise<string> {
15+
const { resourcePath } = this
16+
17+
const transformedSource = source
18+
if (typeof transformedSource !== 'string') {
19+
throw new Error('Expected source to have been transformed to a string.')
20+
}
21+
22+
const swcAST = await parse(transformedSource, {
23+
filename: resourcePath,
24+
isModule: 'unknown',
25+
})
26+
const { ssg, ssr } = checkExports(swcAST)
27+
28+
const output = `
29+
const { createProxy } = require("next/dist/build/webpack/loaders/next-flight-client-loader/module-proxy")\n
30+
module.exports = createProxy(${JSON.stringify(
31+
resourcePath
32+
)}, { ssr: ${ssr}, ssg: ${ssg} })
33+
`
34+
return output
35+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its 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+
// Modified from https://github.com/facebook/react/blob/main/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js
9+
10+
const MODULE_REFERENCE = Symbol.for('react.module.reference')
11+
const PROMISE_PROTOTYPE = Promise.prototype
12+
13+
const proxyHandlers: ProxyHandler<object> = {
14+
get: function (target: any, name: string, _receiver: any) {
15+
switch (name) {
16+
// These names are read by the Flight runtime if you end up using the exports object.
17+
case '$$typeof':
18+
// These names are a little too common. We should probably have a way to
19+
// have the Flight runtime extract the inner target instead.
20+
return target.$$typeof
21+
case 'filepath':
22+
return target.filepath
23+
case 'name':
24+
return target.name
25+
case 'async':
26+
return target.async
27+
// We need to special case this because createElement reads it if we pass this
28+
// reference.
29+
case 'defaultProps':
30+
return undefined
31+
case '__esModule':
32+
// Something is conditionally checking which export to use. We'll pretend to be
33+
// an ESM compat module but then we'll check again on the client.
34+
target.default = {
35+
$$typeof: MODULE_REFERENCE,
36+
filepath: target.filepath,
37+
// This a placeholder value that tells the client to conditionally use the
38+
// whole object or just the default export.
39+
name: '',
40+
async: target.async,
41+
42+
ssr: target.ssr,
43+
ssg: target.ssg,
44+
}
45+
return true
46+
case 'then':
47+
if (!target.async) {
48+
// If this module is expected to return a Promise (such as an AsyncModule) then
49+
// we should resolve that with a client reference that unwraps the Promise on
50+
// the client.
51+
const then = function then(
52+
resolve: (res: any) => void,
53+
_reject: (err: any) => void
54+
) {
55+
const moduleReference: Record<string, any> = {
56+
$$typeof: MODULE_REFERENCE,
57+
filepath: target.filepath,
58+
name: '*', // Represents the whole object instead of a particular import.
59+
async: true,
60+
61+
ssr: target.ssr,
62+
ssg: target.ssg,
63+
}
64+
return Promise.resolve(
65+
resolve(new Proxy(moduleReference, proxyHandlers))
66+
)
67+
}
68+
// If this is not used as a Promise but is treated as a reference to a `.then`
69+
// export then we should treat it as a reference to that name.
70+
then.$$typeof = MODULE_REFERENCE
71+
then.filepath = target.filepath
72+
// then.name is conveniently already "then" which is the export name we need.
73+
// This will break if it's minified though.
74+
return then
75+
}
76+
break
77+
78+
case 'ssg':
79+
return target.ssg
80+
case 'ssr':
81+
return target.ssr
82+
default:
83+
break
84+
}
85+
let cachedReference = target[name]
86+
if (!cachedReference) {
87+
cachedReference = target[name] = {
88+
$$typeof: MODULE_REFERENCE,
89+
filepath: target.filepath,
90+
name: name,
91+
async: target.async,
92+
}
93+
}
94+
return cachedReference
95+
},
96+
getPrototypeOf(_target: object) {
97+
// Pretend to be a Promise in case anyone asks.
98+
return PROMISE_PROTOTYPE
99+
},
100+
set: function () {
101+
throw new Error('Cannot assign to a client module from a server module.')
102+
},
103+
}
104+
105+
export function createProxy(
106+
moduleId: string,
107+
{ ssr, ssg }: { ssr: boolean; ssg: boolean }
108+
) {
109+
const moduleReference = {
110+
$$typeof: MODULE_REFERENCE,
111+
filepath: moduleId,
112+
name: '*', // Represents the whole object instead of a particular import.
113+
async: false,
114+
115+
ssr,
116+
ssg,
117+
}
118+
return new Proxy(moduleReference, proxyHandlers)
119+
}

0 commit comments

Comments
 (0)