Skip to content

Commit 2e4ef53

Browse files
huozhijankaifer
authored andcommitted
Update app dir react for client reference updates (vercel#45490)
x-ref: facebook/react#26059 x-ref: facebook/react#26083 x-ref: facebook/react#26093 x-ref: facebook/react#26083 Closes NEXT-445 * Remove extra `await` * Check if a component result is client reference, then we access for other exports
1 parent 1058d42 commit 2e4ef53

31 files changed

+4330
-3920
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,11 @@
189189
"random-seed": "0.3.0",
190190
"react": "18.2.0",
191191
"react-17": "npm:[email protected]",
192-
"react-builtin": "npm:[email protected]3ba7add60-20221201",
192+
"react-builtin": "npm:[email protected]4bf2113a1-20230206",
193193
"react-dom": "18.2.0",
194194
"react-dom-17": "npm:[email protected]",
195-
"react-dom-builtin": "npm:[email protected]3ba7add60-20221201",
196-
"react-server-dom-webpack": "18.3.0-next-3ba7add60-20221201",
195+
"react-dom-builtin": "npm:[email protected]4bf2113a1-20230206",
196+
"react-server-dom-webpack": "18.3.0-next-4bf2113a1-20230206",
197197
"react-ssr-prepass": "1.0.8",
198198
"react-virtualized": "9.22.3",
199199
"relay-compiler": "13.0.2",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function isClientReference(reference: any): boolean {
2+
return reference?.$$typeof === Symbol.for('react.client.reference')
3+
}

packages/next/src/build/utils.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
overrideBuiltInReactPackages,
5454
} from './webpack/require-hook'
5555
import { AssetBinding } from './webpack/loaders/get-module-build-info'
56+
import { isClientReference } from './is-client-reference'
5657

5758
loadRequireHook()
5859
if (process.env.NEXT_PREBUNDLED_REACT) {
@@ -1100,14 +1101,18 @@ export const collectGenerateParams = async (
11001101
: segment[2]?.page?.[0]?.())
11011102
const config = collectAppConfig(mod)
11021103

1104+
const isClientComponent = isClientReference(mod)
1105+
11031106
const result = {
11041107
isLayout,
11051108
segmentPath: `/${parentSegments.join('/')}${
11061109
segment[0] && parentSegments.length > 0 ? '/' : ''
11071110
}${segment[0]}`,
11081111
config,
1109-
getStaticPaths: mod?.getStaticPaths,
1110-
generateStaticParams: mod?.generateStaticParams,
1112+
getStaticPaths: isClientComponent ? undefined : mod?.getStaticPaths,
1113+
generateStaticParams: isClientComponent
1114+
? undefined
1115+
: mod?.generateStaticParams,
11111116
}
11121117

11131118
if (segment[0]) {
@@ -1269,6 +1274,7 @@ export async function isPageStatic({
12691274
let encodedPrerenderRoutes: Array<string> | undefined
12701275
let prerenderFallback: boolean | 'blocking' | undefined
12711276
let appConfig: AppConfig = {}
1277+
let isClientComponent: boolean = false
12721278

12731279
if (isEdgeRuntime(pageRuntime)) {
12741280
const runtime = await getRuntimeContext({
@@ -1288,6 +1294,7 @@ export async function isPageStatic({
12881294
const mod =
12891295
runtime.context._ENTRIES[`middleware_${edgeInfo.name}`].ComponentMod
12901296

1297+
isClientComponent = isClientReference(mod)
12911298
componentsResult = {
12921299
Component: mod.default,
12931300
ComponentMod: mod,
@@ -1313,6 +1320,7 @@ export async function isPageStatic({
13131320
| undefined
13141321

13151322
if (pageType === 'app') {
1323+
isClientComponent = isClientReference(componentsResult.ComponentMod)
13161324
const tree = componentsResult.ComponentMod.tree
13171325
const generateParams = await collectGenerateParams(tree)
13181326

@@ -1458,7 +1466,10 @@ export async function isPageStatic({
14581466
}
14591467

14601468
const isNextImageImported = (globalThis as any).__NEXT_IMAGE_IMPORTED
1461-
const config: PageConfig = componentsResult.pageConfig
1469+
const config: PageConfig = isClientComponent
1470+
? {}
1471+
: componentsResult.pageConfig
1472+
14621473
return {
14631474
isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps,
14641475
isHybridAmp: config.amp === 'hybrid',

packages/next/src/build/webpack/loaders/next-flight-loader/module-proxy.ts

Lines changed: 155 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77

88
// Modified from https://github.com/facebook/react/blob/main/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js
99

10-
const MODULE_REFERENCE = Symbol.for('react.module.reference')
10+
const CLIENT_REFERENCE = Symbol.for('react.client.reference')
1111
const PROMISE_PROTOTYPE = Promise.prototype
1212

13-
const proxyHandlers: ProxyHandler<object> = {
14-
get: function (target: any, name: string, _receiver: any) {
13+
const deepProxyHandlers = {
14+
get: function (target: any, name: string, _receiver: ProxyHandler<any>) {
1515
switch (name) {
1616
// These names are read by the Flight runtime if you end up using the exports object.
1717
case '$$typeof':
@@ -28,61 +28,167 @@ const proxyHandlers: ProxyHandler<object> = {
2828
// reference.
2929
case 'defaultProps':
3030
return undefined
31+
// Avoid this attempting to be serialized.
32+
case 'toJSON':
33+
return undefined
34+
case Symbol.toPrimitive.toString():
35+
// @ts-ignore
36+
return Object.prototype[Symbol.toPrimitive]
37+
case 'Provider':
38+
throw new Error(
39+
`Cannot render a Client Context Provider on the Server. ` +
40+
`Instead, you can export a Client Component wrapper ` +
41+
`that itself renders a Client Context Provider.`
42+
)
43+
default:
44+
break
45+
}
46+
let expression
47+
switch (target.name) {
48+
case '':
49+
expression = String(name)
50+
break
51+
case '*':
52+
expression = String(name)
53+
break
54+
default:
55+
expression = String(target.name) + '.' + String(name)
56+
}
57+
throw new Error(
58+
`Cannot access ${expression} on the server. ` +
59+
'You cannot dot into a client module from a server component. ' +
60+
'You can only pass the imported name through.'
61+
)
62+
},
63+
set: function () {
64+
throw new Error('Cannot assign to a client module from a server module.')
65+
},
66+
}
67+
68+
const proxyHandlers = {
69+
get: function (target: any, name: string, _receiver: ProxyHandler<any>) {
70+
switch (name) {
71+
// These names are read by the Flight runtime if you end up using the exports object.
72+
case '$$typeof':
73+
// These names are a little too common. We should probably have a way to
74+
// have the Flight runtime extract the inner target instead.
75+
return target.$$typeof
76+
case 'filepath':
77+
return target.filepath
78+
case 'name':
79+
return target.name
80+
case 'async':
81+
return target.async
82+
// We need to special case this because createElement reads it if we pass this
83+
// reference.
84+
case 'defaultProps':
85+
return undefined
86+
// Avoid this attempting to be serialized.
87+
case 'toJSON':
88+
return undefined
89+
case Symbol.toPrimitive.toString():
90+
// @ts-ignore
91+
return Object.prototype[Symbol.toPrimitive]
3192
case '__esModule':
3293
// Something is conditionally checking which export to use. We'll pretend to be
3394
// 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-
}
95+
const moduleId = target.filepath
96+
target.default = Object.defineProperties(
97+
function () {
98+
throw new Error(
99+
`Attempted to call the default export of ${moduleId} from the server ` +
100+
`but it's on the client. It's not possible to invoke a client function from ` +
101+
`the server, it can only be rendered as a Component or passed to props of a ` +
102+
`Client Component.`
103+
)
104+
},
105+
{
106+
// This a placeholder value that tells the client to conditionally use the
107+
// whole object or just the default export.
108+
name: { value: '' },
109+
$$typeof: { value: CLIENT_REFERENCE },
110+
filepath: { value: target.filepath },
111+
async: { value: target.async },
112+
}
113+
)
42114
return true
43115
case 'then':
116+
if (target.then) {
117+
// Use a cached value
118+
return target.then
119+
}
44120
if (!target.async) {
45121
// If this module is expected to return a Promise (such as an AsyncModule) then
46122
// we should resolve that with a client reference that unwraps the Promise on
47123
// the client.
48-
const then = function then(
49-
resolve: (res: any) => void,
50-
_reject: (err: any) => void
51-
) {
52-
const moduleReference: Record<string, any> = {
53-
$$typeof: MODULE_REFERENCE,
54-
filepath: target.filepath,
55-
name: '*', // Represents the whole object instead of a particular import.
56-
async: true,
124+
125+
const clientReference = Object.defineProperties(
126+
{},
127+
{
128+
// Represents the whole Module object instead of a particular import.
129+
name: { value: '*' },
130+
$$typeof: { value: CLIENT_REFERENCE },
131+
filepath: { value: target.filepath },
132+
async: { value: true },
57133
}
58-
return Promise.resolve(
59-
resolve(new Proxy(moduleReference, proxyHandlers))
60-
)
61-
}
62-
// If this is not used as a Promise but is treated as a reference to a `.then`
63-
// export then we should treat it as a reference to that name.
64-
then.$$typeof = MODULE_REFERENCE
65-
then.filepath = target.filepath
66-
// then.name is conveniently already "then" which is the export name we need.
67-
// This will break if it's minified though.
134+
)
135+
const proxy = new Proxy(clientReference, proxyHandlers)
136+
137+
// Treat this as a resolved Promise for React's use()
138+
target.status = 'fulfilled'
139+
target.value = proxy
140+
141+
const then = (target.then = Object.defineProperties(
142+
function then(resolve: any, _reject: any) {
143+
// Expose to React.
144+
return Promise.resolve(
145+
// $FlowFixMe[incompatible-call] found when upgrading Flow
146+
resolve(proxy)
147+
)
148+
},
149+
// If this is not used as a Promise but is treated as a reference to a `.then`
150+
// export then we should treat it as a reference to that name.
151+
{
152+
name: { value: 'then' },
153+
$$typeof: { value: CLIENT_REFERENCE },
154+
filepath: { value: target.filepath },
155+
async: { value: false },
156+
}
157+
))
68158
return then
159+
} else {
160+
// Since typeof .then === 'function' is a feature test we'd continue recursing
161+
// indefinitely if we return a function. Instead, we return an object reference
162+
// if we check further.
163+
return undefined
69164
}
70-
break
71165
default:
72166
break
73167
}
74168
let cachedReference = target[name]
75169
if (!cachedReference) {
76-
cachedReference = target[name] = {
77-
$$typeof: MODULE_REFERENCE,
78-
filepath: target.filepath,
79-
name: name,
80-
async: target.async,
81-
}
170+
const reference = Object.defineProperties(
171+
function () {
172+
throw new Error(
173+
`Attempted to call ${String(name)}() from the server but ${String(
174+
name
175+
)} is on the client. ` +
176+
`It's not possible to invoke a client function from the server, it can ` +
177+
`only be rendered as a Component or passed to props of a Client Component.`
178+
)
179+
},
180+
{
181+
name: { value: name },
182+
$$typeof: { value: CLIENT_REFERENCE },
183+
filepath: { value: target.filepath },
184+
async: { value: target.async },
185+
}
186+
)
187+
cachedReference = target[name] = new Proxy(reference, deepProxyHandlers)
82188
}
83189
return cachedReference
84190
},
85-
getPrototypeOf(_target: object) {
191+
getPrototypeOf(_target: any): object {
86192
// Pretend to be a Promise in case anyone asks.
87193
return PROMISE_PROTOTYPE
88194
},
@@ -92,11 +198,15 @@ const proxyHandlers: ProxyHandler<object> = {
92198
}
93199

94200
export function createProxy(moduleId: string) {
95-
const moduleReference = {
96-
$$typeof: MODULE_REFERENCE,
97-
filepath: moduleId,
98-
name: '*', // Represents the whole object instead of a particular import.
99-
async: false,
100-
}
101-
return new Proxy(moduleReference, proxyHandlers)
201+
const clientReference = Object.defineProperties(
202+
{},
203+
{
204+
// Represents the whole object instead of a particular import.
205+
name: { value: '*' },
206+
$$typeof: { value: CLIENT_REFERENCE },
207+
filepath: { value: moduleId },
208+
async: { value: false },
209+
}
210+
)
211+
return new Proxy(clientReference, proxyHandlers)
102212
}

0 commit comments

Comments
 (0)