diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 70e9f5ddaab95..5293bc794af26 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -11,7 +11,7 @@ import type {Thenable} from 'shared/ReactTypes'; import type {LazyComponent} from 'react/src/ReactLazy'; import type { - ModuleReference, + ClientReference, ModuleMetaData, UninitializedModel, Response, @@ -19,7 +19,7 @@ import type { } from './ReactFlightClientHostConfig'; import { - resolveModuleReference, + resolveClientReference, preloadModule, requireModule, parseModel, @@ -67,7 +67,7 @@ type ResolvedModelChunk = { }; type ResolvedModuleChunk = { status: 'resolved_module', - value: ModuleReference, + value: ClientReference, reason: null, _response: Response, then(resolve: (T) => mixed, reject: (mixed) => mixed): void, @@ -262,7 +262,7 @@ function createResolvedModelChunk( function createResolvedModuleChunk( response: Response, - value: ModuleReference, + value: ClientReference, ): ResolvedModuleChunk { // $FlowFixMe Flow doesn't support functions as constructors return new Chunk(RESOLVED_MODULE, value, null, response); @@ -293,7 +293,7 @@ function resolveModelChunk( function resolveModuleChunk( chunk: SomeChunk, - value: ModuleReference, + value: ClientReference, ): void { if (chunk.status !== PENDING && chunk.status !== BLOCKED) { // We already resolved. We didn't expect to see this. @@ -589,7 +589,7 @@ export function resolveModule( const chunks = response._chunks; const chunk = chunks.get(id); const moduleMetaData: ModuleMetaData = parseModel(response, model); - const moduleReference = resolveModuleReference( + const moduleReference = resolveClientReference( response._bundlerConfig, moduleMetaData, ); diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 6760531f75839..416ad45bc671b 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -91,11 +91,16 @@ describe('ReactFlight', () => { }; }); - function moduleReference(value) { - return { - $$typeof: Symbol.for('react.module.reference'), - value: value, - }; + function clientReference(value) { + return Object.defineProperties( + function() { + throw new Error('Cannot call a client function from the server.'); + }, + { + $$typeof: {value: Symbol.for('react.client.reference')}, + value: {value: value}, + }, + ); } it('can render a Server Component', async () => { @@ -136,7 +141,7 @@ describe('ReactFlight', () => { ); } - const User = moduleReference(UserClient); + const User = clientReference(UserClient); function Greeting({firstName, lastName}) { return ; @@ -327,7 +332,7 @@ describe('ReactFlight', () => { return
I am client
; } - const ClientComponentReference = moduleReference(ClientComponent); + const ClientComponentReference = clientReference(ClientComponent); let load = null; const loadClientComponentReference = () => { @@ -369,7 +374,7 @@ describe('ReactFlight', () => { function ClientImpl({children}) { return children; } - const Client = moduleReference(ClientImpl); + const Client = clientReference(ClientImpl); function EventHandlerProp() { return ( @@ -488,7 +493,7 @@ describe('ReactFlight', () => { ); } - const ClientComponentReference = moduleReference(ClientComponent); + const ClientComponentReference = clientReference(ClientComponent); function Server() { return ( @@ -576,7 +581,7 @@ describe('ReactFlight', () => { function ClientImpl({value}) { return
{value}
; } - const Client = moduleReference(ClientImpl); + const Client = clientReference(ClientImpl); expect(() => { const transport = ReactNoopFlightServer.render( , @@ -593,7 +598,7 @@ describe('ReactFlight', () => { function ClientImpl({children}) { return
{children}
; } - const Client = moduleReference(ClientImpl); + const Client = clientReference(ClientImpl); expect(() => { const transport = ReactNoopFlightServer.render( Current date: {new Date()}, @@ -612,7 +617,7 @@ describe('ReactFlight', () => { function ClientImpl({value}) { return
{value}
; } - const Client = moduleReference(ClientImpl); + const Client = clientReference(ClientImpl); expect(() => { const transport = ReactNoopFlightServer.render(); ReactNoopFlightClient.read(transport); @@ -629,7 +634,7 @@ describe('ReactFlight', () => { function ClientImpl({value}) { return
{value}
; } - const Client = moduleReference(ClientImpl); + const Client = clientReference(ClientImpl); expect(() => { const transport = ReactNoopFlightServer.render( , @@ -646,7 +651,7 @@ describe('ReactFlight', () => { function ClientImpl({value}) { return
{value}
; } - const Client = moduleReference(ClientImpl); + const Client = clientReference(ClientImpl); expect(() => { const transport = ReactNoopFlightServer.render( hi}} />, @@ -665,7 +670,7 @@ describe('ReactFlight', () => { function ClientImpl({value}) { return
{value}
; } - const Client = moduleReference(ClientImpl); + const Client = clientReference(ClientImpl); expect(() => { const transport = ReactNoopFlightServer.render( { ); }); + it('should warn in DEV if a a client reference is passed to useContext()', () => { + const Context = React.createContext(); + const ClientContext = clientReference(Context); + function ServerComponent() { + return React.useContext(ClientContext); + } + expect(() => { + const transport = ReactNoopFlightServer.render(); + ReactNoopFlightClient.read(transport); + }).toErrorDev('Cannot read a Client Context from a Server Component.', { + withoutStack: true, + }); + }); + describe('Hooks', () => { function DivWithId({children}) { const id = React.useId(); @@ -776,7 +795,7 @@ describe('ReactFlight', () => { ); } - const ClientDoublerModuleRef = moduleReference(ClientDoubler); + const ClientDoublerModuleRef = clientReference(ClientDoubler); const transport = ReactNoopFlightServer.render(); expect(Scheduler).toHaveYielded([]); @@ -1000,7 +1019,7 @@ describe('ReactFlight', () => { return {context}; } - const Bar = moduleReference(ClientBar); + const Bar = clientReference(ClientBar); function Foo() { return ( @@ -1077,7 +1096,7 @@ describe('ReactFlight', () => { return
{value}
; } - const Baz = moduleReference(ClientBaz); + const Baz = clientReference(ClientBaz); function Bar() { return ( diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js index 76262bce219de..c34bb7e25bde5 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js @@ -28,8 +28,8 @@ declare var $$$hostConfig: any; export type Response = any; export opaque type BundlerConfig = mixed; export opaque type ModuleMetaData = mixed; -export opaque type ModuleReference = mixed; // eslint-disable-line no-unused-vars -export const resolveModuleReference = $$$hostConfig.resolveModuleReference; +export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars +export const resolveClientReference = $$$hostConfig.resolveClientReference; export const preloadModule = $$$hostConfig.preloadModule; export const requireModule = $$$hostConfig.requireModule; diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index f3517ae7d1f8d..5cb6e20e532f3 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -22,7 +22,7 @@ type Source = Array; const {createResponse, processStringChunk, getRoot, close} = ReactFlightClient({ supportsBinaryStreams: false, - resolveModuleReference(bundlerConfig: null, idx: string) { + resolveClientReference(bundlerConfig: null, idx: string) { return idx; }, preloadModule(idx: string) {}, diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index 586512bc963f4..d50025b87efb0 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -48,10 +48,10 @@ const ReactNoopFlightServer = ReactFlightServer({ clonePrecomputedChunk(chunk: string): string { return chunk; }, - isModuleReference(reference: Object): boolean { - return reference.$$typeof === Symbol.for('react.module.reference'); + isClientReference(reference: Object): boolean { + return reference.$$typeof === Symbol.for('react.client.reference'); }, - getModuleKey(reference: Object): Object { + getClientReferenceKey(reference: Object): Object { return reference; }, resolveModuleMetaData( diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js index a4dccf3513a35..a5f2b243f70cd 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js @@ -13,7 +13,7 @@ import type {JSResourceReference} from 'JSResourceReference'; import type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration'; -export type ModuleReference = JSResourceReference; +export type ClientReference = JSResourceReference; import { parseModelString, @@ -25,7 +25,7 @@ export { requireModule, } from 'ReactFlightDOMRelayClientIntegration'; -import {resolveModuleReference as resolveModuleReferenceImpl} from 'ReactFlightDOMRelayClientIntegration'; +import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightDOMRelayClientIntegration'; import isArray from 'shared/isArray'; @@ -37,11 +37,11 @@ export type UninitializedModel = JSONValue; export type Response = ResponseBase; -export function resolveModuleReference( +export function resolveClientReference( bundlerConfig: BundlerConfig, moduleData: ModuleMetaData, -): ModuleReference { - return resolveModuleReferenceImpl(moduleData); +): ClientReference { + return resolveClientReferenceImpl(moduleData); } // $FlowFixMe[missing-local-annot] diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js index 50d31ea1316ca..27fbaf25f64ad 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js @@ -17,7 +17,7 @@ import JSResourceReferenceImpl from 'JSResourceReferenceImpl'; import hasOwnProperty from 'shared/hasOwnProperty'; import isArray from 'shared/isArray'; -export type ModuleReference = JSResourceReference; +export type ClientReference = JSResourceReference; import type { Destination, @@ -39,13 +39,15 @@ export type { ModuleMetaData, } from 'ReactFlightDOMRelayServerIntegration'; -export function isModuleReference(reference: Object): boolean { +export function isClientReference(reference: Object): boolean { return reference instanceof JSResourceReferenceImpl; } -export type ModuleKey = ModuleReference; +export type ClientReferenceKey = ClientReference; -export function getModuleKey(reference: ModuleReference): ModuleKey { +export function getClientReferenceKey( + reference: ClientReference, +): ClientReferenceKey { // We use the reference object itself as the key because we assume the // object will be cached by the bundler runtime. return reference; @@ -53,7 +55,7 @@ export function getModuleKey(reference: ModuleReference): ModuleKey { export function resolveModuleMetaData( config: BundlerConfig, - resource: ModuleReference, + resource: ClientReference, ): ModuleMetaData { return resolveModuleMetaDataImpl(config, resource); } diff --git a/packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js b/packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js index ede723236670a..50cc6f38221b3 100644 --- a/packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js +++ b/packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js @@ -10,7 +10,7 @@ import JSResourceReferenceImpl from 'JSResourceReferenceImpl'; const ReactFlightDOMRelayClientIntegration = { - resolveModuleReference(moduleData) { + resolveClientReference(moduleData) { return new JSResourceReferenceImpl(moduleData); }, preloadModule(moduleReference) {}, diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js b/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js index abb472f744d21..f4307c8832377 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js @@ -29,12 +29,12 @@ export opaque type ModuleMetaData = { }; // eslint-disable-next-line no-unused-vars -export opaque type ModuleReference = ModuleMetaData; +export opaque type ClientReference = ModuleMetaData; -export function resolveModuleReference( +export function resolveClientReference( bundlerConfig: BundlerConfig, moduleData: ModuleMetaData, -): ModuleReference { +): ClientReference { if (bundlerConfig) { const resolvedModuleData = bundlerConfig[moduleData.id][moduleData.name]; if (moduleData.async) { @@ -64,7 +64,7 @@ function ignoreReject() { // Start preloading the modules since we might need them soon. // This function doesn't suspend. export function preloadModule( - moduleData: ModuleReference, + moduleData: ClientReference, ): null | Thenable { const chunks = moduleData.chunks; const promises = []; @@ -117,7 +117,7 @@ export function preloadModule( // Actually require the module or suspend if it's not yet ready. // Increase priority if necessary. -export function requireModule(moduleData: ModuleReference): T { +export function requireModule(moduleData: ClientReference): T { let moduleExports; if (moduleData.async) { // We assume that preloadModule has been called before, which diff --git a/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js b/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js index 8951a3cce88c3..c662d6d51f243 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js +++ b/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js @@ -16,7 +16,7 @@ type WebpackMap = { export type BundlerConfig = WebpackMap; // eslint-disable-next-line no-unused-vars -export type ModuleReference = { +export type ClientReference = { $$typeof: symbol, filepath: string, name: string, @@ -30,11 +30,13 @@ export type ModuleMetaData = { async: boolean, }; -export type ModuleKey = string; +export type ClientReferenceKey = string; -const MODULE_TAG = Symbol.for('react.module.reference'); +const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference'); -export function getModuleKey(reference: ModuleReference): ModuleKey { +export function getClientReferenceKey( + reference: ClientReference, +): ClientReferenceKey { return ( reference.filepath + '#' + @@ -43,17 +45,17 @@ export function getModuleKey(reference: ModuleReference): ModuleKey { ); } -export function isModuleReference(reference: Object): boolean { - return reference.$$typeof === MODULE_TAG; +export function isClientReference(reference: Object): boolean { + return reference.$$typeof === CLIENT_REFERENCE_TAG; } export function resolveModuleMetaData( config: BundlerConfig, - moduleReference: ModuleReference, + clientReference: ClientReference, ): ModuleMetaData { const resolvedModuleData = - config[moduleReference.filepath][moduleReference.name]; - if (moduleReference.async) { + config[clientReference.filepath][clientReference.name]; + if (clientReference.async) { return { id: resolvedModuleData.id, chunks: resolvedModuleData.chunks, diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js index 8f2f5539292aa..bdbcb73a2a5e3 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js @@ -246,19 +246,39 @@ export async function transformSource( ); let newSrc = - "const MODULE_REFERENCE = Symbol.for('react.module.reference');\n"; + "const CLIENT_REFERENCE = Symbol.for('react.client.reference');\n"; for (let i = 0; i < names.length; i++) { const name = names[i]; if (name === 'default') { newSrc += 'export default '; + newSrc += 'Object.defineProperties(function() {'; + newSrc += + 'throw new Error(' + + JSON.stringify( + `Attempted to call the default export of ${context.url} 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.`, + ) + + ');'; } else { newSrc += 'export const ' + name + ' = '; + newSrc += 'export default '; + newSrc += 'Object.defineProperties(function() {'; + newSrc += + 'throw new Error(' + + JSON.stringify( + `Attempted to call ${name}() from the server but ${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.`, + ) + + ');'; } - newSrc += '{ $$typeof: MODULE_REFERENCE, filepath: '; - newSrc += JSON.stringify(context.url); - newSrc += ', name: '; - newSrc += JSON.stringify(name); - newSrc += '};\n'; + newSrc += '},{'; + newSrc += 'name: { value: ' + JSON.stringify(name) + '},'; + newSrc += '$$typeof: {value: CLIENT_REFERENCE},'; + newSrc += 'filepath: {value: ' + JSON.stringify(context.url) + '}'; + newSrc += '});\n'; } return {source: newSrc}; diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js index 13fb79d78b8ca..c03b1687c73c5 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js @@ -12,15 +12,69 @@ const url = require('url'); const Module = require('module'); module.exports = function register() { - const MODULE_REFERENCE = Symbol.for('react.module.reference'); + const CLIENT_REFERENCE = Symbol.for('react.client.reference'); const PROMISE_PROTOTYPE = Promise.prototype; + const deepProxyHandlers = { + get: function(target: Function, name: string, receiver: Proxy) { + 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; + case 'getDefaultProps': + return undefined; + // Avoid this attempting to be serialized. + case 'toJSON': + return undefined; + case Symbol.toPrimitive: + // $FlowFixMe[prop-missing] + 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.`, + ); + } + let expression; + switch (target.name) { + case '': + // eslint-disable-next-line react-internal/safe-string-coercion + expression = String(name); + break; + case '*': + // eslint-disable-next-line react-internal/safe-string-coercion + expression = String(name); + break; + default: + // eslint-disable-next-line react-internal/safe-string-coercion + 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: {[string]: $FlowFixMe}, - name: string, - receiver: Proxy<{[string]: $FlowFixMe}>, - ) { + get: function(target: Function, name: string, receiver: Proxy) { switch (name) { // These names are read by the Flight runtime if you end up using the exports object. case '$$typeof': @@ -37,57 +91,125 @@ module.exports = function register() { // reference. case 'defaultProps': return undefined; + case 'getDefaultProps': + return undefined; + // Avoid this attempting to be serialized. + case 'toJSON': + return undefined; + case Symbol.toPrimitive: + // $FlowFixMe[prop-missing] + 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.`, + ); + }: any), + { + // 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 innerModuleId = target.filepath; + const clientReference: Function = Object.defineProperties( + (function() { + throw new Error( + `Attempted to call the module exports of ${innerModuleId} 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.`, + ); + }: any), + { + // Represents the whole object instead of a particular import. + name: {value: '*'}, + $$typeof: {value: CLIENT_REFERENCE}, + filepath: {value: target.filepath}, + 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; + // $FlowFixMe[missing-local-annot] - const then = function then(resolve, reject: any) { - const moduleReference: {[string]: any, ...} = { - $$typeof: MODULE_REFERENCE, - filepath: target.filepath, - name: '*', // Represents the whole object instead of a particular import. - async: true, - }; - return Promise.resolve( - // $FlowFixMe[incompatible-call] found when upgrading Flow - 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 then = (target.then = Object.defineProperties( + (function then(resolve, reject: any) { + // Expose to React. + return Promise.resolve( + // $FlowFixMe[incompatible-call] found when upgrading Flow + resolve(proxy), + ); + }: any), + // 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; } } 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( + // eslint-disable-next-line react-internal/safe-string-coercion + `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.`, + ); + }: any), + { + 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: {[string]: $FlowFixMe}) { + getPrototypeOf(target: Function): Object { // Pretend to be a Promise in case anyone asks. return PROMISE_PROTOTYPE; }, @@ -98,15 +220,26 @@ module.exports = function register() { // $FlowFixMe[prop-missing] found when upgrading Flow Module._extensions['.client.js'] = function(module, path) { - const moduleId = url.pathToFileURL(path).href; - const moduleReference: {[string]: any, ...} = { - $$typeof: MODULE_REFERENCE, - filepath: moduleId, - name: '*', // Represents the whole object instead of a particular import. - async: false, - }; + const moduleId: string = (url.pathToFileURL(path).href: any); + const clientReference: Function = Object.defineProperties( + (function() { + throw new Error( + `Attempted to call the module exports 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.`, + ); + }: any), + { + // Represents the whole object instead of a particular import. + name: {value: '*'}, + $$typeof: {value: CLIENT_REFERENCE}, + filepath: {value: moduleId}, + async: {value: false}, + }, + ); // $FlowFixMe[incompatible-call] found when upgrading Flow - module.exports = new Proxy(moduleReference, proxyHandlers); + module.exports = new Proxy(clientReference, proxyHandlers); }; // $FlowFixMe[prop-missing] found when upgrading Flow diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 524227cacd763..83d4b5ffbdba4 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -286,6 +286,45 @@ describe('ReactFlightDOM', () => { expect(container.innerHTML).toBe('

Async: Module

'); }); + // @gate enableUseHook + it('should unwrap async module references using use', async () => { + const AsyncModule = Promise.resolve('Async Text'); + + function Print({response}) { + return use(response); + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const AsyncModuleRef = clientExports(AsyncModule); + + function ServerComponent() { + const text = use(AsyncModuleRef); + return

{text}

; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMReader.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.innerHTML).toBe('

Async Text

'); + }); + // @gate enableUseHook it('should be able to import a name called "then"', async () => { const thenExports = { @@ -324,6 +363,35 @@ describe('ReactFlightDOM', () => { expect(container.innerHTML).toBe('

and then

'); }); + it('throws when accessing a member below the client exports', () => { + const ClientModule = clientExports({ + Component: {deep: 'thing'}, + }); + function dotting() { + return ClientModule.Component.deep; + } + expect(dotting).toThrowError( + 'Cannot access Component.deep on the server. ' + + 'You cannot dot into a client module from a server component. ' + + 'You can only pass the imported name through.', + ); + }); + + it('throws when accessing a Context.Provider below the client exports', () => { + const Context = React.createContext(); + const ClientModule = clientExports({ + Context, + }); + function dotting() { + return ClientModule.Context.Provider; + } + expect(dotting).toThrowError( + `Cannot render a Client Context Provider on the Server. ` + + `Instead, you can export a Client Component wrapper ` + + `that itself renders a Client Context Provider.`, + ); + }); + // @gate enableUseHook it('should progressively reveal server components', async () => { let reportedErrors = []; diff --git a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js index 82a65409e5074..5e33802448988 100644 --- a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js +++ b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js @@ -81,12 +81,10 @@ exports.clientExports = function clientExports(moduleExports) { moduleExports.then( asyncModuleExports => { for (const name in asyncModuleExports) { - webpackMap[path] = { - [name]: { - id: idx, - chunks: [], - name: name, - }, + webpackMap[path][name] = { + id: idx, + chunks: [], + name: name, }; } }, @@ -94,12 +92,10 @@ exports.clientExports = function clientExports(moduleExports) { ); } for (const name in moduleExports) { - webpackMap[path] = { - [name]: { - id: idx, - chunks: [], - name: name, - }, + webpackMap[path][name] = { + id: idx, + chunks: [], + name: name, }; } const mod = {exports: {}}; diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js index ae2e2c0afb028..981b351f18433 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js @@ -13,7 +13,7 @@ import type {JSResourceReference} from 'JSResourceReference'; import type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration'; -export type ModuleReference = JSResourceReference; +export type ClientReference = JSResourceReference; import { parseModelString, @@ -25,7 +25,7 @@ export { requireModule, } from 'ReactFlightNativeRelayClientIntegration'; -import {resolveModuleReference as resolveModuleReferenceImpl} from 'ReactFlightNativeRelayClientIntegration'; +import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightNativeRelayClientIntegration'; import isArray from 'shared/isArray'; @@ -37,11 +37,11 @@ export type UninitializedModel = JSONValue; export type Response = ResponseBase; -export function resolveModuleReference( +export function resolveClientReference( bundlerConfig: BundlerConfig, moduleData: ModuleMetaData, -): ModuleReference { - return resolveModuleReferenceImpl(moduleData); +): ClientReference { + return resolveClientReferenceImpl(moduleData); } // $FlowFixMe[missing-local-annot] diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js index 814773b3f128a..552b37913819b 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js @@ -14,7 +14,7 @@ import isArray from 'shared/isArray'; import type {JSResourceReference} from 'JSResourceReference'; import JSResourceReferenceImpl from 'JSResourceReferenceImpl'; -export type ModuleReference = JSResourceReference; +export type ClientReference = JSResourceReference; import type { Destination, @@ -36,13 +36,15 @@ export type { ModuleMetaData, } from 'ReactFlightNativeRelayServerIntegration'; -export function isModuleReference(reference: Object): boolean { +export function isClientReference(reference: Object): boolean { return reference instanceof JSResourceReferenceImpl; } -export type ModuleKey = ModuleReference; +export type ClientReferenceKey = ClientReference; -export function getModuleKey(reference: ModuleReference): ModuleKey { +export function getClientReferenceKey( + reference: ClientReference, +): ClientReferenceKey { // We use the reference object itself as the key because we assume the // object will be cached by the bundler runtime. return reference; @@ -50,7 +52,7 @@ export function getModuleKey(reference: ModuleReference): ModuleKey { export function resolveModuleMetaData( config: BundlerConfig, - resource: ModuleReference, + resource: ClientReference, ): ModuleMetaData { return resolveModuleMetaDataImpl(config, resource); } diff --git a/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js b/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js index 2d258288359ba..ec0f44c840b36 100644 --- a/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js +++ b/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js @@ -10,7 +10,7 @@ import JSResourceReferenceImpl from 'JSResourceReferenceImpl'; const ReactFlightNativeRelayClientIntegration = { - resolveModuleReference(moduleData) { + resolveClientReference(moduleData) { return new JSResourceReferenceImpl(moduleData); }, preloadModule(moduleReference) {}, diff --git a/packages/react-server/src/ReactFlightHooks.js b/packages/react-server/src/ReactFlightHooks.js index d1fb683260620..a6f13d6900fce 100644 --- a/packages/react-server/src/ReactFlightHooks.js +++ b/packages/react-server/src/ReactFlightHooks.js @@ -18,6 +18,7 @@ import { import {readContext as readContextImpl} from './ReactFlightNewContext'; import {enableUseHook} from 'shared/ReactFeatureFlags'; import {createThenableState, trackUsedThenable} from './ReactFlightThenable'; +import {isClientReference} from './ReactFlightServerConfig'; let currentRequest = null; let thenableIndexCounter = 0; @@ -47,9 +48,13 @@ export function getThenableStateAfterSuspending(): null | ThenableState { function readContext(context: ReactServerContext): T { if (__DEV__) { if (context.$$typeof !== REACT_SERVER_CONTEXT_TYPE) { - console.error( - 'Only createServerContext is supported in Server Components.', - ); + if (isClientReference(context)) { + console.error('Cannot read a Client Context from a Server Component.'); + } else { + console.error( + 'Only createServerContext is supported in Server Components.', + ); + } } if (currentRequest === null) { console.error( @@ -118,7 +123,10 @@ function useId(): string { } function use(usable: Usable): T { - if (usable !== null && typeof usable === 'object') { + if ( + (usable !== null && typeof usable === 'object') || + typeof usable === 'function' + ) { // $FlowFixMe[method-unbinding] if (typeof usable.then === 'function') { // This is a thenable. @@ -138,6 +146,12 @@ function use(usable: Usable): T { } } + if (__DEV__) { + if (isClientReference(usable)) { + console.error('Cannot use() an already resolved Client Reference.'); + } + } + // eslint-disable-next-line react-internal/safe-string-coercion throw new Error('An unsupported type was passed to use(): ' + String(usable)); } diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 548a7f5252d28..1d1b32f8ca49f 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -12,8 +12,8 @@ import type { Chunk, BundlerConfig, ModuleMetaData, - ModuleReference, - ModuleKey, + ClientReference, + ClientReferenceKey, } from './ReactFlightServerConfig'; import type {ContextSnapshot} from './ReactFlightNewContext'; import type {ThenableState} from './ReactFlightThenable'; @@ -44,8 +44,8 @@ import { processErrorChunkDev, processReferenceChunk, resolveModuleMetaData, - getModuleKey, - isModuleReference, + getClientReferenceKey, + isClientReference, supportsRequestStorage, requestStorage, } from './ReactFlightServerConfig'; @@ -135,7 +135,7 @@ export type Request = { completedJSONChunks: Array, completedErrorChunks: Array, writtenSymbols: Map, - writtenModules: Map, + writtenModules: Map, writtenProviders: Map, identifierPrefix: string, identifierCount: number, @@ -293,7 +293,7 @@ function attemptResolveElement( } } if (typeof type === 'function') { - if (isModuleReference(type)) { + if (isClientReference(type)) { // This is a reference to a Client Component. return [REACT_ELEMENT_TYPE, type, key, props]; } @@ -323,7 +323,7 @@ function attemptResolveElement( // Any built-in works as long as its props are serializable. return [REACT_ELEMENT_TYPE, type, key, props]; } else if (type != null && typeof type === 'object') { - if (isModuleReference(type)) { + if (isClientReference(type)) { // This is a reference to a Client Component. return [REACT_ELEMENT_TYPE, type, key, props]; } @@ -420,13 +420,13 @@ function serializeByRefID(id: number): string { return '@' + id.toString(16); } -function serializeModuleReference( +function serializeClientReference( request: Request, parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray, key: string, - moduleReference: ModuleReference, + moduleReference: ClientReference, ): string { - const moduleKey: ModuleKey = getModuleKey(moduleReference); + const moduleKey: ClientReferenceKey = getClientReferenceKey(moduleReference); const writtenModules = request.writtenModules; const existingId = writtenModules.get(moduleKey); if (existingId !== undefined) { @@ -891,8 +891,8 @@ export function resolveModelToJSON( } if (typeof value === 'object') { - if (isModuleReference(value)) { - return serializeModuleReference(request, parent, key, (value: any)); + if (isClientReference(value)) { + return serializeClientReference(request, parent, key, (value: any)); } else if ((value: any).$$typeof === REACT_PROVIDER_TYPE) { const providerKey = ((value: any): ReactProviderType)._context ._globalName; @@ -961,8 +961,8 @@ export function resolveModelToJSON( } if (typeof value === 'function') { - if (isModuleReference(value)) { - return serializeModuleReference(request, parent, key, (value: any)); + if (isClientReference(value)) { + return serializeClientReference(request, parent, key, (value: any)); } if (/^on[A-Z]/.test(key)) { throw new Error( diff --git a/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js b/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js index 7024968c16d9c..0d2f84dbecc27 100644 --- a/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js +++ b/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js @@ -10,9 +10,9 @@ declare var $$$hostConfig: any; export opaque type BundlerConfig = mixed; -export opaque type ModuleReference = mixed; // eslint-disable-line no-unused-vars +export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars export opaque type ModuleMetaData: any = mixed; -export opaque type ModuleKey: any = mixed; -export const isModuleReference = $$$hostConfig.isModuleReference; -export const getModuleKey = $$$hostConfig.getModuleKey; +export opaque type ClientReferenceKey: any = mixed; +export const isClientReference = $$$hostConfig.isClientReference; +export const getClientReferenceKey = $$$hostConfig.getClientReferenceKey; export const resolveModuleMetaData = $$$hostConfig.resolveModuleMetaData; diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js index a7715592602a6..5b8511e084dba 100644 --- a/packages/shared/isValidElementType.js +++ b/packages/shared/isValidElementType.js @@ -33,7 +33,7 @@ import { enableLegacyHidden, } from './ReactFeatureFlags'; -const REACT_MODULE_REFERENCE: symbol = Symbol.for('react.module.reference'); +const REACT_CLIENT_REFERENCE: symbol = Symbol.for('react.client.reference'); export default function isValidElementType(type: mixed): boolean { if (typeof type === 'string' || typeof type === 'function') { @@ -68,7 +68,7 @@ export default function isValidElementType(type: mixed): boolean { // types supported by any Flight configuration anywhere since // we don't know which Flight build this will end up being used // with. - type.$$typeof === REACT_MODULE_REFERENCE || + type.$$typeof === REACT_CLIENT_REFERENCE || type.getModuleId !== undefined ) { return true; diff --git a/scripts/flow/react-relay-hooks.js b/scripts/flow/react-relay-hooks.js index 36673f1e89026..4b52e1d857564 100644 --- a/scripts/flow/react-relay-hooks.js +++ b/scripts/flow/react-relay-hooks.js @@ -57,7 +57,7 @@ declare module 'ReactFlightDOMRelayClientIntegration' { import type {JSResourceReference} from 'JSResourceReference'; declare export opaque type ModuleMetaData; - declare export function resolveModuleReference( + declare export function resolveClientReference( moduleData: ModuleMetaData, ): JSResourceReference; declare export function preloadModule( @@ -90,7 +90,7 @@ declare module 'ReactFlightNativeRelayClientIntegration' { import type {JSResourceReference} from 'JSResourceReference'; declare export opaque type ModuleMetaData; - declare export function resolveModuleReference( + declare export function resolveClientReference( moduleData: ModuleMetaData, ): JSResourceReference; declare export function preloadModule( diff --git a/scripts/jest/setupHostConfigs.js b/scripts/jest/setupHostConfigs.js index cd763c57ff6f2..495b1126fc733 100644 --- a/scripts/jest/setupHostConfigs.js +++ b/scripts/jest/setupHostConfigs.js @@ -82,8 +82,8 @@ jest.mock('react-server/flight', () => { jest.mock(shimServerStreamConfigPath, () => config); jest.mock(shimServerFormatConfigPath, () => config); jest.mock('react-server/src/ReactFlightServerBundlerConfigCustom', () => ({ - isModuleReference: config.isModuleReference, - getModuleKey: config.getModuleKey, + isClientReference: config.isClientReference, + getClientReferenceKey: config.getClientReferenceKey, resolveModuleMetaData: config.resolveModuleMetaData, })); jest.mock(shimFlightServerConfigPath, () =>