-
Notifications
You must be signed in to change notification settings - Fork 27.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use
createClientModuleProxy
from Flight Server (#54232)
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
Showing
4 changed files
with
21 additions
and
215 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
199 changes: 4 additions & 195 deletions
199
packages/next/src/build/webpack/loaders/next-flight-loader/module-proxy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters