Skip to content

Commit

Permalink
Use createClientModuleProxy from Flight Server (#54232)
Browse files Browse the repository at this point in the history
This PR removes our client module reference proxy implementation to directly use the one from the upstream Flight server, as it's added here: facebook/react#27033.

Also updated the server reference creation code a bit to use `defineProperties` - we can't switch to the upstream `registerServerReference` API yet as our Server Actions compiler needs to change a bit to adapt that API since we might have existing `bound` and/or `originalAction` provided.
  • Loading branch information
shuding authored Aug 18, 2023
1 parent b25407e commit b906fdf
Show file tree
Hide file tree
Showing 4 changed files with 21 additions and 215 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference')

export function createActionProxy(
id: string,
bound: null | any[],
Expand Down Expand Up @@ -30,8 +32,18 @@ export function createActionProxy(
return newAction
}

action.$$typeof = Symbol.for('react.server.reference')
action.$$id = id
action.$$bound = bound
action.bind = bindImpl.bind(action)
Object.defineProperties(action, {
$$typeof: {
value: SERVER_REFERENCE_TAG,
},
$$id: {
value: id,
},
$$bound: {
value: bound,
},
bind: {
value: bindImpl,
},
})
}
Original file line number Diff line number Diff line change
@@ -1,196 +1,5 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable import/no-extraneous-dependencies */
import { createClientModuleProxy } from 'react-server-dom-webpack/server.edge'

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

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

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':
// 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 '$$id':
return target.$$id
case '$$async':
return target.$$async
case 'name':
return target.name
case 'displayName':
return undefined
// 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 '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
}
const 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':
return target.$$typeof
case '$$id':
return target.$$id
case '$$async':
return target.$$async
case 'name':
return target.name
// 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.
const moduleId = target.$$id
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.`
)
},
{
$$typeof: { value: CLIENT_REFERENCE },
// This a placeholder value that tells the client to conditionally use the
// whole object or just the default export.
$$id: { value: target.$$id + '#' },
$$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 clientReference = Object.defineProperties(
{},
{
$$typeof: { value: CLIENT_REFERENCE },
$$id: { value: target.$$id },
$$async: { value: true },
}
)
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.
{
$$typeof: { value: CLIENT_REFERENCE },
$$id: { value: target.$$id },
$$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
}
default:
break
}
let cachedReference = target[name]
if (!cachedReference) {
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.`
)
},
{
$$typeof: { value: CLIENT_REFERENCE },
$$id: { value: target.$$id + '#' + name },
$$async: { value: target.$$async },
}
)
cachedReference = target[name] = new Proxy(reference, deepProxyHandlers)
}
return cachedReference
},
getPrototypeOf(_target: any): object {
// Pretend to be a Promise in case anyone asks.
return PROMISE_PROTOTYPE
},
set: function () {
throw new Error('Cannot assign to a client module from a server module.')
},
}

export function createProxy(moduleId: string) {
const clientReference = Object.defineProperties(
{},
{
$$typeof: { value: CLIENT_REFERENCE },
// Represents the whole Module object instead of a particular import.
$$id: { value: moduleId },
$$async: { value: false },
}
)
return new Proxy(clientReference, proxyHandlers)
}
// Re-assign to make it typed.
export const createProxy: (moduleId: string) => any = createClientModuleProxy
16 changes: 0 additions & 16 deletions packages/next/src/lib/client-reference.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,3 @@
/**
* filepath export module key
* "file" '*' "file"
* "file" '' "file#"
* "file" '<named>' "file#<named>"
*
* @param filepath file path to the module
* @param exports '' | '*' | '<named>'
*/
export function getClientReferenceModuleKey(
filepath: string,
exportName: string
): string {
return exportName === '*' ? filepath : filepath + '#' + exportName
}

export function isClientReference(reference: any): boolean {
return reference?.$$typeof === Symbol.for('react.client.reference')
}
1 change: 1 addition & 0 deletions packages/next/types/misc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ declare module 'next/dist/compiled/react-dom/server.edge'
declare module 'next/dist/compiled/react-dom/server.browser'
declare module 'next/dist/compiled/browserslist'
declare module 'react-server-dom-webpack/client'
declare module 'react-server-dom-webpack/server.edge'
declare module 'react-dom/server.browser'
declare module 'react-dom/server.edge'

Expand Down

0 comments on commit b906fdf

Please sign in to comment.