Skip to content

Commit

Permalink
Update app dir react for client reference updates (#45490)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
huozhi committed Feb 7, 2023
1 parent 25dfdbb commit c6fe3f3
Show file tree
Hide file tree
Showing 31 changed files with 4,330 additions and 3,920 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,11 @@
"random-seed": "0.3.0",
"react": "18.2.0",
"react-17": "npm:react@17.0.2",
"react-builtin": "npm:react@18.3.0-next-3ba7add60-20221201",
"react-builtin": "npm:react@18.3.0-next-4bf2113a1-20230206",
"react-dom": "18.2.0",
"react-dom-17": "npm:react-dom@17.0.2",
"react-dom-builtin": "npm:react-dom@18.3.0-next-3ba7add60-20221201",
"react-server-dom-webpack": "18.3.0-next-3ba7add60-20221201",
"react-dom-builtin": "npm:react-dom@18.3.0-next-4bf2113a1-20230206",
"react-server-dom-webpack": "18.3.0-next-4bf2113a1-20230206",
"react-ssr-prepass": "1.0.8",
"react-virtualized": "9.22.3",
"relay-compiler": "13.0.2",
Expand Down
3 changes: 3 additions & 0 deletions packages/next/src/build/is-client-reference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isClientReference(reference: any): boolean {
return reference?.$$typeof === Symbol.for('react.client.reference')
}
17 changes: 14 additions & 3 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
overrideBuiltInReactPackages,
} from './webpack/require-hook'
import { AssetBinding } from './webpack/loaders/get-module-build-info'
import { isClientReference } from './is-client-reference'

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

const isClientComponent = isClientReference(mod)

const result = {
isLayout,
segmentPath: `/${parentSegments.join('/')}${
segment[0] && parentSegments.length > 0 ? '/' : ''
}${segment[0]}`,
config,
getStaticPaths: mod?.getStaticPaths,
generateStaticParams: mod?.generateStaticParams,
getStaticPaths: isClientComponent ? undefined : mod?.getStaticPaths,
generateStaticParams: isClientComponent
? undefined
: mod?.generateStaticParams,
}

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

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

isClientComponent = isClientReference(mod)
componentsResult = {
Component: mod.default,
ComponentMod: mod,
Expand All @@ -1313,6 +1320,7 @@ export async function isPageStatic({
| undefined

if (pageType === 'app') {
isClientComponent = isClientReference(componentsResult.ComponentMod)
const tree = componentsResult.ComponentMod.tree
const generateParams = await collectGenerateParams(tree)

Expand Down Expand Up @@ -1458,7 +1466,10 @@ export async function isPageStatic({
}

const isNextImageImported = (globalThis as any).__NEXT_IMAGE_IMPORTED
const config: PageConfig = componentsResult.pageConfig
const config: PageConfig = isClientComponent
? {}
: componentsResult.pageConfig

return {
isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps,
isHybridAmp: config.amp === 'hybrid',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

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

const MODULE_REFERENCE = Symbol.for('react.module.reference')
const CLIENT_REFERENCE = Symbol.for('react.client.reference')
const PROMISE_PROTOTYPE = Promise.prototype

const proxyHandlers: ProxyHandler<object> = {
get: function (target: any, name: string, _receiver: any) {
const deepProxyHandlers = {
get: function (target: any, name: string, _receiver: ProxyHandler<any>) {
switch (name) {
// These names are read by the Flight runtime if you end up using the exports object.
case '$$typeof':
Expand All @@ -28,61 +28,167 @@ const proxyHandlers: ProxyHandler<object> = {
// reference.
case 'defaultProps':
return undefined
// Avoid this attempting to be serialized.
case 'toJSON':
return undefined
case Symbol.toPrimitive.toString():
// @ts-ignore
return Object.prototype[Symbol.toPrimitive]
case 'Provider':
throw new Error(
`Cannot render a Client Context Provider on the Server. ` +
`Instead, you can export a Client Component wrapper ` +
`that itself renders a Client Context Provider.`
)
default:
break
}
let expression
switch (target.name) {
case '':
expression = String(name)
break
case '*':
expression = String(name)
break
default:
expression = String(target.name) + '.' + String(name)
}
throw new Error(
`Cannot access ${expression} on the server. ` +
'You cannot dot into a client module from a server component. ' +
'You can only pass the imported name through.'
)
},
set: function () {
throw new Error('Cannot assign to a client module from a server module.')
},
}

const proxyHandlers = {
get: function (target: any, name: string, _receiver: ProxyHandler<any>) {
switch (name) {
// These names are read by the Flight runtime if you end up using the exports object.
case '$$typeof':
// These names are a little too common. We should probably have a way to
// have the Flight runtime extract the inner target instead.
return target.$$typeof
case 'filepath':
return target.filepath
case 'name':
return target.name
case 'async':
return target.async
// We need to special case this because createElement reads it if we pass this
// reference.
case 'defaultProps':
return undefined
// Avoid this attempting to be serialized.
case 'toJSON':
return undefined
case Symbol.toPrimitive.toString():
// @ts-ignore
return Object.prototype[Symbol.toPrimitive]
case '__esModule':
// Something is conditionally checking which export to use. We'll pretend to be
// an ESM compat module but then we'll check again on the client.
target.default = {
$$typeof: MODULE_REFERENCE,
filepath: target.filepath,
// This a placeholder value that tells the client to conditionally use the
// whole object or just the default export.
name: '',
async: target.async,
}
const moduleId = target.filepath
target.default = Object.defineProperties(
function () {
throw new Error(
`Attempted to call the default export of ${moduleId} from the server ` +
`but it's on the client. It's not possible to invoke a client function from ` +
`the server, it can only be rendered as a Component or passed to props of a ` +
`Client Component.`
)
},
{
// This a placeholder value that tells the client to conditionally use the
// whole object or just the default export.
name: { value: '' },
$$typeof: { value: CLIENT_REFERENCE },
filepath: { value: target.filepath },
async: { value: target.async },
}
)
return true
case 'then':
if (target.then) {
// Use a cached value
return target.then
}
if (!target.async) {
// If this module is expected to return a Promise (such as an AsyncModule) then
// we should resolve that with a client reference that unwraps the Promise on
// the client.
const then = function then(
resolve: (res: any) => void,
_reject: (err: any) => void
) {
const moduleReference: Record<string, any> = {
$$typeof: MODULE_REFERENCE,
filepath: target.filepath,
name: '*', // Represents the whole object instead of a particular import.
async: true,

const clientReference = Object.defineProperties(
{},
{
// Represents the whole Module object instead of a particular import.
name: { value: '*' },
$$typeof: { value: CLIENT_REFERENCE },
filepath: { value: target.filepath },
async: { value: true },
}
return Promise.resolve(
resolve(new Proxy(moduleReference, proxyHandlers))
)
}
// If this is not used as a Promise but is treated as a reference to a `.then`
// export then we should treat it as a reference to that name.
then.$$typeof = MODULE_REFERENCE
then.filepath = target.filepath
// then.name is conveniently already "then" which is the export name we need.
// This will break if it's minified though.
)
const proxy = new Proxy(clientReference, proxyHandlers)

// Treat this as a resolved Promise for React's use()
target.status = 'fulfilled'
target.value = proxy

const then = (target.then = Object.defineProperties(
function then(resolve: any, _reject: any) {
// Expose to React.
return Promise.resolve(
// $FlowFixMe[incompatible-call] found when upgrading Flow
resolve(proxy)
)
},
// If this is not used as a Promise but is treated as a reference to a `.then`
// export then we should treat it as a reference to that name.
{
name: { value: 'then' },
$$typeof: { value: CLIENT_REFERENCE },
filepath: { value: target.filepath },
async: { value: false },
}
))
return then
} else {
// Since typeof .then === 'function' is a feature test we'd continue recursing
// indefinitely if we return a function. Instead, we return an object reference
// if we check further.
return undefined
}
break
default:
break
}
let cachedReference = target[name]
if (!cachedReference) {
cachedReference = target[name] = {
$$typeof: MODULE_REFERENCE,
filepath: target.filepath,
name: name,
async: target.async,
}
const reference = Object.defineProperties(
function () {
throw new Error(
`Attempted to call ${String(name)}() from the server but ${String(
name
)} is on the client. ` +
`It's not possible to invoke a client function from the server, it can ` +
`only be rendered as a Component or passed to props of a Client Component.`
)
},
{
name: { value: name },
$$typeof: { value: CLIENT_REFERENCE },
filepath: { value: target.filepath },
async: { value: target.async },
}
)
cachedReference = target[name] = new Proxy(reference, deepProxyHandlers)
}
return cachedReference
},
getPrototypeOf(_target: object) {
getPrototypeOf(_target: any): object {
// Pretend to be a Promise in case anyone asks.
return PROMISE_PROTOTYPE
},
Expand All @@ -92,11 +198,15 @@ const proxyHandlers: ProxyHandler<object> = {
}

export function createProxy(moduleId: string) {
const moduleReference = {
$$typeof: MODULE_REFERENCE,
filepath: moduleId,
name: '*', // Represents the whole object instead of a particular import.
async: false,
}
return new Proxy(moduleReference, proxyHandlers)
const clientReference = Object.defineProperties(
{},
{
// Represents the whole object instead of a particular import.
name: { value: '*' },
$$typeof: { value: CLIENT_REFERENCE },
filepath: { value: moduleId },
async: { value: false },
}
)
return new Proxy(clientReference, proxyHandlers)
}
Loading

0 comments on commit c6fe3f3

Please sign in to comment.