@samitouri / QOS-React-2 / commits / ce09ace9a2

Improve Error Messages when Access Client References (#26059)

This renames Module References to Client References, since they are in the server->client direction. I also changed the Proxies exposed from the `node-register` loader to provide better error messages. Ideally, some of this should be replicated in the ESM loader too but neither are the source of truth. We'll replicate this in the static form in the Next.js loaders. cc @huozhi @shuding - All references are now functions so that when you call them on the server, we can yield a better error message. - References that are themselves already referring to an export name are now proxies that error when you dot into them. - `use(...)` can now be used on a client reference to unwrap it server side and then pass a reference to the awaited value.

Sebastian Markbåge committed Jan 27, 2023 at 20:08 UTC ce09ace9a23b61dfba53f5a83d52b377bc642bae
23 files changed +415 -159
packages/react-client/src/ReactFlightClient.js
+6 -6
@@ -11,7 +11,7 @@ import type {Thenable} from 'shared/ReactTypes';
11 import type {LazyComponent} from 'react/src/ReactLazy';
12
13 import type {
14 - ModuleReference,
14 + ClientReference,
15 ModuleMetaData,
16 UninitializedModel,
17 Response,
@@ -19,7 +19,7 @@ import type {
19 } from './ReactFlightClientHostConfig';
20
21 import {
22 - resolveModuleReference,
22 + resolveClientReference,
23 preloadModule,
24 requireModule,
25 parseModel,
@@ -67,7 +67,7 @@ type ResolvedModelChunk<T> = {
67 };
68 type ResolvedModuleChunk<T> = {
69 status: 'resolved_module',
70 - value: ModuleReference<T>,
70 + value: ClientReference<T>,
71 reason: null,
72 _response: Response,
73 then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
@@ -262,7 +262,7 @@ function createResolvedModelChunk<T>(
262
263 function createResolvedModuleChunk<T>(
264 response: Response,
265 - value: ModuleReference<T>,
265 + value: ClientReference<T>,
266 ): ResolvedModuleChunk<T> {
267 // $FlowFixMe Flow doesn't support functions as constructors
268 return new Chunk(RESOLVED_MODULE, value, null, response);
@@ -293,7 +293,7 @@ function resolveModelChunk<T>(
293
294 function resolveModuleChunk<T>(
295 chunk: SomeChunk<T>,
296 - value: ModuleReference<T>,
296 + value: ClientReference<T>,
297 ): void {
298 if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
299 // We already resolved. We didn't expect to see this.
@@ -589,7 +589,7 @@ export function resolveModule(
589 const chunks = response._chunks;
590 const chunk = chunks.get(id);
591 const moduleMetaData: ModuleMetaData = parseModel(response, model);
592 - const moduleReference = resolveModuleReference(
592 + const moduleReference = resolveClientReference(
593 response._bundlerConfig,
594 moduleMetaData,
595 );
packages/react-client/src/__tests__/ReactFlight-test.js
+37 -18
@@ -91,11 +91,16 @@ describe('ReactFlight', () => {
91 };
92 });
93
94 - function moduleReference(value) {
95 - return {
96 - $$typeof: Symbol.for('react.module.reference'),
97 - value: value,
98 - };
94 + function clientReference(value) {
95 + return Object.defineProperties(
96 + function() {
97 + throw new Error('Cannot call a client function from the server.');
98 + },
99 + {
100 + $$typeof: {value: Symbol.for('react.client.reference')},
101 + value: {value: value},
102 + },
103 + );
104 }
105
106 it('can render a Server Component', async () => {
@@ -136,7 +141,7 @@ describe('ReactFlight', () => {
141 </span>
142 );
143 }
139 - const User = moduleReference(UserClient);
144 + const User = clientReference(UserClient);
145
146 function Greeting({firstName, lastName}) {
147 return <User greeting="Hello" name={firstName + ' ' + lastName} />;
@@ -327,7 +332,7 @@ describe('ReactFlight', () => {
332 return <div>I am client</div>;
333 }
334
330 - const ClientComponentReference = moduleReference(ClientComponent);
335 + const ClientComponentReference = clientReference(ClientComponent);
336
337 let load = null;
338 const loadClientComponentReference = () => {
@@ -369,7 +374,7 @@ describe('ReactFlight', () => {
374 function ClientImpl({children}) {
375 return children;
376 }
372 - const Client = moduleReference(ClientImpl);
377 + const Client = clientReference(ClientImpl);
378
379 function EventHandlerProp() {
380 return (
@@ -488,7 +493,7 @@ describe('ReactFlight', () => {
493 );
494 }
495
491 - const ClientComponentReference = moduleReference(ClientComponent);
496 + const ClientComponentReference = clientReference(ClientComponent);
497
498 function Server() {
499 return (
@@ -576,7 +581,7 @@ describe('ReactFlight', () => {
581 function ClientImpl({value}) {
582 return <div>{value}</div>;
583 }
579 - const Client = moduleReference(ClientImpl);
584 + const Client = clientReference(ClientImpl);
585 expect(() => {
586 const transport = ReactNoopFlightServer.render(
587 <Client value={new Date()} />,
@@ -593,7 +598,7 @@ describe('ReactFlight', () => {
598 function ClientImpl({children}) {
599 return <div>{children}</div>;
600 }
596 - const Client = moduleReference(ClientImpl);
601 + const Client = clientReference(ClientImpl);
602 expect(() => {
603 const transport = ReactNoopFlightServer.render(
604 <Client>Current date: {new Date()}</Client>,
@@ -612,7 +617,7 @@ describe('ReactFlight', () => {
617 function ClientImpl({value}) {
618 return <div>{value}</div>;
619 }
615 - const Client = moduleReference(ClientImpl);
620 + const Client = clientReference(ClientImpl);
621 expect(() => {
622 const transport = ReactNoopFlightServer.render(<Client value={Math} />);
623 ReactNoopFlightClient.read(transport);
@@ -629,7 +634,7 @@ describe('ReactFlight', () => {
634 function ClientImpl({value}) {
635 return <div>{value}</div>;
636 }
632 - const Client = moduleReference(ClientImpl);
637 + const Client = clientReference(ClientImpl);
638 expect(() => {
639 const transport = ReactNoopFlightServer.render(
640 <Client value={{[Symbol.iterator]: {}}} />,
@@ -646,7 +651,7 @@ describe('ReactFlight', () => {
651 function ClientImpl({value}) {
652 return <div>{value}</div>;
653 }
649 - const Client = moduleReference(ClientImpl);
654 + const Client = clientReference(ClientImpl);
655 expect(() => {
656 const transport = ReactNoopFlightServer.render(
657 <Client value={{hello: Math, title: <h1>hi</h1>}} />,
@@ -665,7 +670,7 @@ describe('ReactFlight', () => {
670 function ClientImpl({value}) {
671 return <div>{value}</div>;
672 }
668 - const Client = moduleReference(ClientImpl);
673 + const Client = clientReference(ClientImpl);
674 expect(() => {
675 const transport = ReactNoopFlightServer.render(
676 <Client
@@ -702,6 +707,20 @@ describe('ReactFlight', () => {
707 );
708 });
709
710 + it('should warn in DEV if a a client reference is passed to useContext()', () => {
711 + const Context = React.createContext();
712 + const ClientContext = clientReference(Context);
713 + function ServerComponent() {
714 + return React.useContext(ClientContext);
715 + }
716 + expect(() => {
717 + const transport = ReactNoopFlightServer.render(<ServerComponent />);
718 + ReactNoopFlightClient.read(transport);
719 + }).toErrorDev('Cannot read a Client Context from a Server Component.', {
720 + withoutStack: true,
721 + });
722 + });
723 +
724 describe('Hooks', () => {
725 function DivWithId({children}) {
726 const id = React.useId();
@@ -776,7 +795,7 @@ describe('ReactFlight', () => {
795 );
796 }
797
779 - const ClientDoublerModuleRef = moduleReference(ClientDoubler);
798 + const ClientDoublerModuleRef = clientReference(ClientDoubler);
799
800 const transport = ReactNoopFlightServer.render(<App />);
801 expect(Scheduler).toHaveYielded([]);
@@ -1000,7 +1019,7 @@ describe('ReactFlight', () => {
1019 return <span>{context}</span>;
1020 }
1021
1003 - const Bar = moduleReference(ClientBar);
1022 + const Bar = clientReference(ClientBar);
1023
1024 function Foo() {
1025 return (
@@ -1077,7 +1096,7 @@ describe('ReactFlight', () => {
1096 return <div>{value}</div>;
1097 }
1098
1080 - const Baz = moduleReference(ClientBaz);
1099 + const Baz = clientReference(ClientBaz);
1100
1101 function Bar() {
1102 return (
packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js
+2 -2
@@ -28,8 +28,8 @@ declare var $$$hostConfig: any;
28 export type Response = any;
29 export opaque type BundlerConfig = mixed;
30 export opaque type ModuleMetaData = mixed;
31 -export opaque type ModuleReference<T> = mixed; // eslint-disable-line no-unused-vars
32 -export const resolveModuleReference = $$$hostConfig.resolveModuleReference;
31 +export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
32 +export const resolveClientReference = $$$hostConfig.resolveClientReference;
33 export const preloadModule = $$$hostConfig.preloadModule;
34 export const requireModule = $$$hostConfig.requireModule;
35
packages/react-noop-renderer/src/ReactNoopFlightClient.js
+1 -1
@@ -22,7 +22,7 @@ type Source = Array<string>;
22
23 const {createResponse, processStringChunk, getRoot, close} = ReactFlightClient({
24 supportsBinaryStreams: false,
25 - resolveModuleReference(bundlerConfig: null, idx: string) {
25 + resolveClientReference(bundlerConfig: null, idx: string) {
26 return idx;
27 },
28 preloadModule(idx: string) {},
packages/react-noop-renderer/src/ReactNoopFlightServer.js
+3 -3
@@ -48,10 +48,10 @@ const ReactNoopFlightServer = ReactFlightServer({
48 clonePrecomputedChunk(chunk: string): string {
49 return chunk;
50 },
51 - isModuleReference(reference: Object): boolean {
52 - return reference.$$typeof === Symbol.for('react.module.reference');
51 + isClientReference(reference: Object): boolean {
52 + return reference.$$typeof === Symbol.for('react.client.reference');
53 },
54 - getModuleKey(reference: Object): Object {
54 + getClientReferenceKey(reference: Object): Object {
55 return reference;
56 },
57 resolveModuleMetaData(
packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js
+5 -5
@@ -13,7 +13,7 @@ import type {JSResourceReference} from 'JSResourceReference';
13
14 import type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration';
15
16 -export type ModuleReference<T> = JSResourceReference<T>;
16 +export type ClientReference<T> = JSResourceReference<T>;
17
18 import {
19 parseModelString,
@@ -25,7 +25,7 @@ export {
25 requireModule,
26 } from 'ReactFlightDOMRelayClientIntegration';
27
28 -import {resolveModuleReference as resolveModuleReferenceImpl} from 'ReactFlightDOMRelayClientIntegration';
28 +import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightDOMRelayClientIntegration';
29
30 import isArray from 'shared/isArray';
31
@@ -37,11 +37,11 @@ export type UninitializedModel = JSONValue;
37
38 export type Response = ResponseBase;
39
40 -export function resolveModuleReference<T>(
40 +export function resolveClientReference<T>(
41 bundlerConfig: BundlerConfig,
42 moduleData: ModuleMetaData,
43 -): ModuleReference<T> {
44 - return resolveModuleReferenceImpl(moduleData);
43 +): ClientReference<T> {
44 + return resolveClientReferenceImpl(moduleData);
45 }
46
47 // $FlowFixMe[missing-local-annot]
packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js
+7 -5
@@ -17,7 +17,7 @@ import JSResourceReferenceImpl from 'JSResourceReferenceImpl';
17 import hasOwnProperty from 'shared/hasOwnProperty';
18 import isArray from 'shared/isArray';
19
20 -export type ModuleReference<T> = JSResourceReference<T>;
20 +export type ClientReference<T> = JSResourceReference<T>;
21
22 import type {
23 Destination,
@@ -39,13 +39,15 @@ export type {
39 ModuleMetaData,
40 } from 'ReactFlightDOMRelayServerIntegration';
41
42 -export function isModuleReference(reference: Object): boolean {
42 +export function isClientReference(reference: Object): boolean {
43 return reference instanceof JSResourceReferenceImpl;
44 }
45
46 -export type ModuleKey = ModuleReference<any>;
46 +export type ClientReferenceKey = ClientReference<any>;
47
48 -export function getModuleKey(reference: ModuleReference<any>): ModuleKey {
48 +export function getClientReferenceKey(
49 + reference: ClientReference<any>,
50 +): ClientReferenceKey {
51 // We use the reference object itself as the key because we assume the
52 // object will be cached by the bundler runtime.
53 return reference;
@@ -53,7 +55,7 @@ export function getModuleKey(reference: ModuleReference<any>): ModuleKey {
55
56 export function resolveModuleMetaData<T>(
57 config: BundlerConfig,
56 - resource: ModuleReference<T>,
58 + resource: ClientReference<T>,
59 ): ModuleMetaData {
60 return resolveModuleMetaDataImpl(config, resource);
61 }
packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js
+1 -1
@@ -10,7 +10,7 @@
10 import JSResourceReferenceImpl from 'JSResourceReferenceImpl';
11
12 const ReactFlightDOMRelayClientIntegration = {
13 - resolveModuleReference(moduleData) {
13 + resolveClientReference(moduleData) {
14 return new JSResourceReferenceImpl(moduleData);
15 },
16 preloadModule(moduleReference) {},
packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js
+5 -5
@@ -29,12 +29,12 @@ export opaque type ModuleMetaData = {
29 };
30
31 // eslint-disable-next-line no-unused-vars
32 -export opaque type ModuleReference<T> = ModuleMetaData;
32 +export opaque type ClientReference<T> = ModuleMetaData;
33
34 -export function resolveModuleReference<T>(
34 +export function resolveClientReference<T>(
35 bundlerConfig: BundlerConfig,
36 moduleData: ModuleMetaData,
37 -): ModuleReference<T> {
37 +): ClientReference<T> {
38 if (bundlerConfig) {
39 const resolvedModuleData = bundlerConfig[moduleData.id][moduleData.name];
40 if (moduleData.async) {
@@ -64,7 +64,7 @@ function ignoreReject() {
64 // Start preloading the modules since we might need them soon.
65 // This function doesn't suspend.
66 export function preloadModule<T>(
67 - moduleData: ModuleReference<T>,
67 + moduleData: ClientReference<T>,
68 ): null | Thenable<any> {
69 const chunks = moduleData.chunks;
70 const promises = [];
@@ -117,7 +117,7 @@ export function preloadModule<T>(
117
118 // Actually require the module or suspend if it's not yet ready.
119 // Increase priority if necessary.
120 -export function requireModule<T>(moduleData: ModuleReference<T>): T {
120 +export function requireModule<T>(moduleData: ClientReference<T>): T {
121 let moduleExports;
122 if (moduleData.async) {
123 // We assume that preloadModule has been called before, which
packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js
+11 -9
@@ -16,7 +16,7 @@ type WebpackMap = {
16 export type BundlerConfig = WebpackMap;
17
18 // eslint-disable-next-line no-unused-vars
19 -export type ModuleReference<T> = {
19 +export type ClientReference<T> = {
20 $$typeof: symbol,
21 filepath: string,
22 name: string,
@@ -30,11 +30,13 @@ export type ModuleMetaData = {
30 async: boolean,
31 };
32
33 -export type ModuleKey = string;
33 +export type ClientReferenceKey = string;
34
35 -const MODULE_TAG = Symbol.for('react.module.reference');
35 +const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
36
37 -export function getModuleKey(reference: ModuleReference<any>): ModuleKey {
37 +export function getClientReferenceKey(
38 + reference: ClientReference<any>,
39 +): ClientReferenceKey {
40 return (
41 reference.filepath +
42 '#' +
@@ -43,17 +45,17 @@ export function getModuleKey(reference: ModuleReference<any>): ModuleKey {
45 );
46 }
47
46 -export function isModuleReference(reference: Object): boolean {
47 - return reference.$$typeof === MODULE_TAG;
48 +export function isClientReference(reference: Object): boolean {
49 + return reference.$$typeof === CLIENT_REFERENCE_TAG;
50 }
51
52 export function resolveModuleMetaData<T>(
53 config: BundlerConfig,
52 - moduleReference: ModuleReference<T>,
54 + clientReference: ClientReference<T>,
55 ): ModuleMetaData {
56 const resolvedModuleData =
55 - config[moduleReference.filepath][moduleReference.name];
56 - if (moduleReference.async) {
57 + config[clientReference.filepath][clientReference.name];
58 + if (clientReference.async) {
59 return {
60 id: resolvedModuleData.id,
61 chunks: resolvedModuleData.chunks,
packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js
+26 -6
@@ -246,19 +246,39 @@ export async function transformSource(
246 );
247
248 let newSrc =
249 - "const MODULE_REFERENCE = Symbol.for('react.module.reference');\n";
249 + "const CLIENT_REFERENCE = Symbol.for('react.client.reference');\n";
250 for (let i = 0; i < names.length; i++) {
251 const name = names[i];
252 if (name === 'default') {
253 newSrc += 'export default ';
254 + newSrc += 'Object.defineProperties(function() {';
255 + newSrc +=
256 + 'throw new Error(' +
257 + JSON.stringify(
258 + `Attempted to call the default export of ${context.url} from the server` +
259 + `but it's on the client. It's not possible to invoke a client function from ` +
260 + `the server, it can only be rendered as a Component or passed to props of a` +
261 + `Client Component.`,
262 + ) +
263 + ');';
264 } else {
265 newSrc += 'export const ' + name + ' = ';
266 + newSrc += 'export default ';
267 + newSrc += 'Object.defineProperties(function() {';
268 + newSrc +=
269 + 'throw new Error(' +
270 + JSON.stringify(
271 + `Attempted to call ${name}() from the server but ${name} is on the client. ` +
272 + `It's not possible to invoke a client function from the server, it can ` +
273 + `only be rendered as a Component or passed to props of a Client Component.`,
274 + ) +
275 + ');';
276 }
257 - newSrc += '{ $$typeof: MODULE_REFERENCE, filepath: ';
258 - newSrc += JSON.stringify(context.url);
259 - newSrc += ', name: ';
260 - newSrc += JSON.stringify(name);
261 - newSrc += '};\n';
277 + newSrc += '},{';
278 + newSrc += 'name: { value: ' + JSON.stringify(name) + '},';
279 + newSrc += '$$typeof: {value: CLIENT_REFERENCE},';
280 + newSrc += 'filepath: {value: ' + JSON.stringify(context.url) + '}';
281 + newSrc += '});\n';
282 }
283
284 return {source: newSrc};
packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js
+180 -47
@@ -12,15 +12,69 @@ const url = require('url');
12 const Module = require('module');
13
14 module.exports = function register() {
15 - const MODULE_REFERENCE = Symbol.for('react.module.reference');
15 + const CLIENT_REFERENCE = Symbol.for('react.client.reference');
16 const PROMISE_PROTOTYPE = Promise.prototype;
17
18 + const deepProxyHandlers = {
19 + get: function(target: Function, name: string, receiver: Proxy<Function>) {
20 + switch (name) {
21 + // These names are read by the Flight runtime if you end up using the exports object.
22 + case '$$typeof':
23 + // These names are a little too common. We should probably have a way to
24 + // have the Flight runtime extract the inner target instead.
25 + return target.$$typeof;
26 + case 'filepath':
27 + return target.filepath;
28 + case 'name':
29 + return target.name;
30 + case 'async':
31 + return target.async;
32 + // We need to special case this because createElement reads it if we pass this
33 + // reference.
34 + case 'defaultProps':
35 + return undefined;
36 + case 'getDefaultProps':
37 + return undefined;
38 + // Avoid this attempting to be serialized.
39 + case 'toJSON':
40 + return undefined;
41 + case Symbol.toPrimitive:
42 + // $FlowFixMe[prop-missing]
43 + return Object.prototype[Symbol.toPrimitive];
44 + case 'Provider':
45 + throw new Error(
46 + `Cannot render a Client Context Provider on the Server. ` +
47 + `Instead, you can export a Client Component wrapper ` +
48 + `that itself renders a Client Context Provider.`,
49 + );
50 + }
51 + let expression;
52 + switch (target.name) {
53 + case '':
54 + // eslint-disable-next-line react-internal/safe-string-coercion
55 + expression = String(name);
56 + break;
57 + case '*':
58 + // eslint-disable-next-line react-internal/safe-string-coercion
59 + expression = String(name);
60 + break;
61 + default:
62 + // eslint-disable-next-line react-internal/safe-string-coercion
63 + expression = String(target.name) + '.' + String(name);
64 + }
65 + throw new Error(
66 + `Cannot access ${expression} on the server. ` +
67 + 'You cannot dot into a client module from a server component. ' +
68 + 'You can only pass the imported name through.',
69 + );
70 + },
71 + set: function() {
72 + throw new Error('Cannot assign to a client module from a server module.');
73 + },
74 + };
75 +
76 const proxyHandlers = {
19 - get: function(
20 - target: {[string]: $FlowFixMe},
21 - name: string,
22 - receiver: Proxy<{[string]: $FlowFixMe}>,
23 - ) {
77 + get: function(target: Function, name: string, receiver: Proxy<Function>) {
78 switch (name) {
79 // These names are read by the Flight runtime if you end up using the exports object.
80 case '$$typeof':
@@ -37,57 +91,125 @@ module.exports = function register() {
91 // reference.
92 case 'defaultProps':
93 return undefined;
94 + case 'getDefaultProps':
95 + return undefined;
96 + // Avoid this attempting to be serialized.
97 + case 'toJSON':
98 + return undefined;
99 + case Symbol.toPrimitive:
100 + // $FlowFixMe[prop-missing]
101 + return Object.prototype[Symbol.toPrimitive];
102 case '__esModule':
103 // Something is conditionally checking which export to use. We'll pretend to be
104 // an ESM compat module but then we'll check again on the client.
43 - target.default = {
44 - $$typeof: MODULE_REFERENCE,
45 - filepath: target.filepath,
46 - // This a placeholder value that tells the client to conditionally use the
47 - // whole object or just the default export.
48 - name: '',
49 - async: target.async,
50 - };
105 + const moduleId = target.filepath;
106 + target.default = Object.defineProperties(
107 + (function() {
108 + throw new Error(
109 + `Attempted to call the default export of ${moduleId} from the server` +
110 + `but it's on the client. It's not possible to invoke a client function from ` +
111 + `the server, it can only be rendered as a Component or passed to props of a` +
112 + `Client Component.`,
113 + );
114 + }: any),
115 + {
116 + // This a placeholder value that tells the client to conditionally use the
117 + // whole object or just the default export.
118 + name: {value: ''},
119 + $$typeof: {value: CLIENT_REFERENCE},
120 + filepath: {value: target.filepath},
121 + async: {value: target.async},
122 + },
123 + );
124 return true;
125 case 'then':
126 + if (target.then) {
127 + // Use a cached value
128 + return target.then;
129 + }
130 if (!target.async) {
131 // If this module is expected to return a Promise (such as an AsyncModule) then
132 // we should resolve that with a client reference that unwraps the Promise on
133 // the client.
134 +
135 + const innerModuleId = target.filepath;
136 + const clientReference: Function = Object.defineProperties(
137 + (function() {
138 + throw new Error(
139 + `Attempted to call the module exports of ${innerModuleId} from the server` +
140 + `but it's on the client. It's not possible to invoke a client function from ` +
141 + `the server, it can only be rendered as a Component or passed to props of a` +
142 + `Client Component.`,
143 + );
144 + }: any),
145 + {
146 + // Represents the whole object instead of a particular import.
147 + name: {value: '*'},
148 + $$typeof: {value: CLIENT_REFERENCE},
149 + filepath: {value: target.filepath},
150 + async: {value: true},
151 + },
152 + );
153 + const proxy = new Proxy(clientReference, proxyHandlers);
154 +
155 + // Treat this as a resolved Promise for React's use()
156 + target.status = 'fulfilled';
157 + target.value = proxy;
158 +
159 // $FlowFixMe[missing-local-annot]
58 - const then = function then(resolve, reject: any) {
59 - const moduleReference: {[string]: any, ...} = {
60 - $$typeof: MODULE_REFERENCE,
61 - filepath: target.filepath,
62 - name: '*', // Represents the whole object instead of a particular import.
63 - async: true,
64 - };
65 - return Promise.resolve(
66 - // $FlowFixMe[incompatible-call] found when upgrading Flow
67 - resolve(new Proxy(moduleReference, proxyHandlers)),
68 - );
69 - };
70 - // If this is not used as a Promise but is treated as a reference to a `.then`
71 - // export then we should treat it as a reference to that name.
72 - then.$$typeof = MODULE_REFERENCE;
73 - then.filepath = target.filepath;
74 - // then.name is conveniently already "then" which is the export name we need.
75 - // This will break if it's minified though.
160 + const then = (target.then = Object.defineProperties(
161 + (function then(resolve, reject: any) {
162 + // Expose to React.
163 + return Promise.resolve(
164 + // $FlowFixMe[incompatible-call] found when upgrading Flow
165 + resolve(proxy),
166 + );
167 + }: any),
168 + // If this is not used as a Promise but is treated as a reference to a `.then`
169 + // export then we should treat it as a reference to that name.
170 + {
171 + name: {value: 'then'},
172 + $$typeof: {value: CLIENT_REFERENCE},
173 + filepath: {value: target.filepath},
174 + async: {value: false},
175 + },
176 + ));
177 return then;
178 + } else {
179 + // Since typeof .then === 'function' is a feature test we'd continue recursing
180 + // indefinitely if we return a function. Instead, we return an object reference
181 + // if we check further.
182 + return undefined;
183 }
184 }
185 let cachedReference = target[name];
186 if (!cachedReference) {
81 - cachedReference = target[name] = {
82 - $$typeof: MODULE_REFERENCE,
83 - filepath: target.filepath,
84 - name: name,
85 - async: target.async,
86 - };
187 + const reference = Object.defineProperties(
188 + (function() {
189 + throw new Error(
190 + // eslint-disable-next-line react-internal/safe-string-coercion
191 + `Attempted to call ${String(name)}() from the server but ${String(
192 + name,
193 + )} is on the client. ` +
194 + `It's not possible to invoke a client function from the server, it can ` +
195 + `only be rendered as a Component or passed to props of a Client Component.`,
196 + );
197 + }: any),
198 + {
199 + name: {value: name},
200 + $$typeof: {value: CLIENT_REFERENCE},
201 + filepath: {value: target.filepath},
202 + async: {value: target.async},
203 + },
204 + );
205 + cachedReference = target[name] = new Proxy(
206 + reference,
207 + deepProxyHandlers,
208 + );
209 }
210 return cachedReference;
211 },
90 - getPrototypeOf(target: {[string]: $FlowFixMe}) {
212 + getPrototypeOf(target: Function): Object {
213 // Pretend to be a Promise in case anyone asks.
214 return PROMISE_PROTOTYPE;
215 },
@@ -98,15 +220,26 @@ module.exports = function register() {
220
221 // $FlowFixMe[prop-missing] found when upgrading Flow
222 Module._extensions['.client.js'] = function(module, path) {
101 - const moduleId = url.pathToFileURL(path).href;
102 - const moduleReference: {[string]: any, ...} = {
103 - $$typeof: MODULE_REFERENCE,
104 - filepath: moduleId,
105 - name: '*', // Represents the whole object instead of a particular import.
106 - async: false,
107 - };
223 + const moduleId: string = (url.pathToFileURL(path).href: any);
224 + const clientReference: Function = Object.defineProperties(
225 + (function() {
226 + throw new Error(
227 + `Attempted to call the module exports of ${moduleId} from the server` +
228 + `but it's on the client. It's not possible to invoke a client function from ` +
229 + `the server, it can only be rendered as a Component or passed to props of a` +
230 + `Client Component.`,
231 + );
232 + }: any),
233 + {
234 + // Represents the whole object instead of a particular import.
235 + name: {value: '*'},
236 + $$typeof: {value: CLIENT_REFERENCE},
237 + filepath: {value: moduleId},
238 + async: {value: false},
239 + },
240 + );
241 // $FlowFixMe[incompatible-call] found when upgrading Flow
109 - module.exports = new Proxy(moduleReference, proxyHandlers);
242 + module.exports = new Proxy(clientReference, proxyHandlers);
243 };
244
245 // $FlowFixMe[prop-missing] found when upgrading Flow
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+68
@@ -286,6 +286,45 @@ describe('ReactFlightDOM', () => {
286 expect(container.innerHTML).toBe('<p>Async: Module</p>');
287 });
288
289 + // @gate enableUseHook
290 + it('should unwrap async module references using use', async () => {
291 + const AsyncModule = Promise.resolve('Async Text');
292 +
293 + function Print({response}) {
294 + return use(response);
295 + }
296 +
297 + function App({response}) {
298 + return (
299 + <Suspense fallback={<h1>Loading...</h1>}>
300 + <Print response={response} />
301 + </Suspense>
302 + );
303 + }
304 +
305 + const AsyncModuleRef = clientExports(AsyncModule);
306 +
307 + function ServerComponent() {
308 + const text = use(AsyncModuleRef);
309 + return <p>{text}</p>;
310 + }
311 +
312 + const {writable, readable} = getTestStream();
313 + const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
314 + <ServerComponent />,
315 + webpackMap,
316 + );
317 + pipe(writable);
318 + const response = ReactServerDOMReader.createFromReadableStream(readable);
319 +
320 + const container = document.createElement('div');
321 + const root = ReactDOMClient.createRoot(container);
322 + await act(async () => {
323 + root.render(<App response={response} />);
324 + });
325 + expect(container.innerHTML).toBe('<p>Async Text</p>');
326 + });
327 +
328 // @gate enableUseHook
329 it('should be able to import a name called "then"', async () => {
330 const thenExports = {
@@ -324,6 +363,35 @@ describe('ReactFlightDOM', () => {
363 expect(container.innerHTML).toBe('<p>and then</p>');
364 });
365
366 + it('throws when accessing a member below the client exports', () => {
367 + const ClientModule = clientExports({
368 + Component: {deep: 'thing'},
369 + });
370 + function dotting() {
371 + return ClientModule.Component.deep;
372 + }
373 + expect(dotting).toThrowError(
374 + 'Cannot access Component.deep on the server. ' +
375 + 'You cannot dot into a client module from a server component. ' +
376 + 'You can only pass the imported name through.',
377 + );
378 + });
379 +
380 + it('throws when accessing a Context.Provider below the client exports', () => {
381 + const Context = React.createContext();
382 + const ClientModule = clientExports({
383 + Context,
384 + });
385 + function dotting() {
386 + return ClientModule.Context.Provider;
387 + }
388 + expect(dotting).toThrowError(
389 + `Cannot render a Client Context Provider on the Server. ` +
390 + `Instead, you can export a Client Component wrapper ` +
391 + `that itself renders a Client Context Provider.`,
392 + );
393 + });
394 +
395 // @gate enableUseHook
396 it('should progressively reveal server components', async () => {
397 let reportedErrors = [];
packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js
+8 -12
@@ -81,12 +81,10 @@ exports.clientExports = function clientExports(moduleExports) {
81 moduleExports.then(
82 asyncModuleExports => {
83 for (const name in asyncModuleExports) {
84 - webpackMap[path] = {
85 - [name]: {
86 - id: idx,
87 - chunks: [],
88 - name: name,
89 - },
84 + webpackMap[path][name] = {
85 + id: idx,
86 + chunks: [],
87 + name: name,
88 };
89 }
90 },
@@ -94,12 +92,10 @@ exports.clientExports = function clientExports(moduleExports) {
92 );
93 }
94 for (const name in moduleExports) {
97 - webpackMap[path] = {
98 - [name]: {
99 - id: idx,
100 - chunks: [],
101 - name: name,
102 - },
95 + webpackMap[path][name] = {
96 + id: idx,
97 + chunks: [],
98 + name: name,
99 };
100 }
101 const mod = {exports: {}};
packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js
+5 -5
@@ -13,7 +13,7 @@ import type {JSResourceReference} from 'JSResourceReference';
13
14 import type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration';
15
16 -export type ModuleReference<T> = JSResourceReference<T>;
16 +export type ClientReference<T> = JSResourceReference<T>;
17
18 import {
19 parseModelString,
@@ -25,7 +25,7 @@ export {
25 requireModule,
26 } from 'ReactFlightNativeRelayClientIntegration';
27
28 -import {resolveModuleReference as resolveModuleReferenceImpl} from 'ReactFlightNativeRelayClientIntegration';
28 +import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightNativeRelayClientIntegration';
29
30 import isArray from 'shared/isArray';
31
@@ -37,11 +37,11 @@ export type UninitializedModel = JSONValue;
37
38 export type Response = ResponseBase;
39
40 -export function resolveModuleReference<T>(
40 +export function resolveClientReference<T>(
41 bundlerConfig: BundlerConfig,
42 moduleData: ModuleMetaData,
43 -): ModuleReference<T> {
44 - return resolveModuleReferenceImpl(moduleData);
43 +): ClientReference<T> {
44 + return resolveClientReferenceImpl(moduleData);
45 }
46
47 // $FlowFixMe[missing-local-annot]
packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js
+7 -5
@@ -14,7 +14,7 @@ import isArray from 'shared/isArray';
14 import type {JSResourceReference} from 'JSResourceReference';
15 import JSResourceReferenceImpl from 'JSResourceReferenceImpl';
16
17 -export type ModuleReference<T> = JSResourceReference<T>;
17 +export type ClientReference<T> = JSResourceReference<T>;
18
19 import type {
20 Destination,
@@ -36,13 +36,15 @@ export type {
36 ModuleMetaData,
37 } from 'ReactFlightNativeRelayServerIntegration';
38
39 -export function isModuleReference(reference: Object): boolean {
39 +export function isClientReference(reference: Object): boolean {
40 return reference instanceof JSResourceReferenceImpl;
41 }
42
43 -export type ModuleKey = ModuleReference<any>;
43 +export type ClientReferenceKey = ClientReference<any>;
44
45 -export function getModuleKey(reference: ModuleReference<any>): ModuleKey {
45 +export function getClientReferenceKey(
46 + reference: ClientReference<any>,
47 +): ClientReferenceKey {
48 // We use the reference object itself as the key because we assume the
49 // object will be cached by the bundler runtime.
50 return reference;
@@ -50,7 +52,7 @@ export function getModuleKey(reference: ModuleReference<any>): ModuleKey {
52
53 export function resolveModuleMetaData<T>(
54 config: BundlerConfig,
53 - resource: ModuleReference<T>,
55 + resource: ClientReference<T>,
56 ): ModuleMetaData {
57 return resolveModuleMetaDataImpl(config, resource);
58 }
packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js
+1 -1
@@ -10,7 +10,7 @@
10 import JSResourceReferenceImpl from 'JSResourceReferenceImpl';
11
12 const ReactFlightNativeRelayClientIntegration = {
13 - resolveModuleReference(moduleData) {
13 + resolveClientReference(moduleData) {
14 return new JSResourceReferenceImpl(moduleData);
15 },
16 preloadModule(moduleReference) {},
packages/react-server/src/ReactFlightHooks.js
+18 -4
@@ -18,6 +18,7 @@ import {
18 import {readContext as readContextImpl} from './ReactFlightNewContext';
19 import {enableUseHook} from 'shared/ReactFeatureFlags';
20 import {createThenableState, trackUsedThenable} from './ReactFlightThenable';
21 +import {isClientReference} from './ReactFlightServerConfig';
22
23 let currentRequest = null;
24 let thenableIndexCounter = 0;
@@ -47,9 +48,13 @@ export function getThenableStateAfterSuspending(): null | ThenableState {
48 function readContext<T>(context: ReactServerContext<T>): T {
49 if (__DEV__) {
50 if (context.$$typeof !== REACT_SERVER_CONTEXT_TYPE) {
50 - console.error(
51 - 'Only createServerContext is supported in Server Components.',
52 - );
51 + if (isClientReference(context)) {
52 + console.error('Cannot read a Client Context from a Server Component.');
53 + } else {
54 + console.error(
55 + 'Only createServerContext is supported in Server Components.',
56 + );
57 + }
58 }
59 if (currentRequest === null) {
60 console.error(
@@ -118,7 +123,10 @@ function useId(): string {
123 }
124
125 function use<T>(usable: Usable<T>): T {
121 - if (usable !== null && typeof usable === 'object') {
126 + if (
127 + (usable !== null && typeof usable === 'object') ||
128 + typeof usable === 'function'
129 + ) {
130 // $FlowFixMe[method-unbinding]
131 if (typeof usable.then === 'function') {
132 // This is a thenable.
@@ -138,6 +146,12 @@ function use<T>(usable: Usable<T>): T {
146 }
147 }
148
149 + if (__DEV__) {
150 + if (isClientReference(usable)) {
151 + console.error('Cannot use() an already resolved Client Reference.');
152 + }
153 + }
154 +
155 // eslint-disable-next-line react-internal/safe-string-coercion
156 throw new Error('An unsupported type was passed to use(): ' + String(usable));
157 }
packages/react-server/src/ReactFlightServer.js
+14 -14
@@ -12,8 +12,8 @@ import type {
12 Chunk,
13 BundlerConfig,
14 ModuleMetaData,
15 - ModuleReference,
16 - ModuleKey,
15 + ClientReference,
16 + ClientReferenceKey,
17 } from './ReactFlightServerConfig';
18 import type {ContextSnapshot} from './ReactFlightNewContext';
19 import type {ThenableState} from './ReactFlightThenable';
@@ -44,8 +44,8 @@ import {
44 processErrorChunkDev,
45 processReferenceChunk,
46 resolveModuleMetaData,
47 - getModuleKey,
48 - isModuleReference,
47 + getClientReferenceKey,
48 + isClientReference,
49 supportsRequestStorage,
50 requestStorage,
51 } from './ReactFlightServerConfig';
@@ -135,7 +135,7 @@ export type Request = {
135 completedJSONChunks: Array<Chunk>,
136 completedErrorChunks: Array<Chunk>,
137 writtenSymbols: Map<symbol, number>,
138 - writtenModules: Map<ModuleKey, number>,
138 + writtenModules: Map<ClientReferenceKey, number>,
139 writtenProviders: Map<string, number>,
140 identifierPrefix: string,
141 identifierCount: number,
@@ -293,7 +293,7 @@ function attemptResolveElement(
293 }
294 }
295 if (typeof type === 'function') {
296 - if (isModuleReference(type)) {
296 + if (isClientReference(type)) {
297 // This is a reference to a Client Component.
298 return [REACT_ELEMENT_TYPE, type, key, props];
299 }
@@ -323,7 +323,7 @@ function attemptResolveElement(
323 // Any built-in works as long as its props are serializable.
324 return [REACT_ELEMENT_TYPE, type, key, props];
325 } else if (type != null && typeof type === 'object') {
326 - if (isModuleReference(type)) {
326 + if (isClientReference(type)) {
327 // This is a reference to a Client Component.
328 return [REACT_ELEMENT_TYPE, type, key, props];
329 }
@@ -420,13 +420,13 @@ function serializeByRefID(id: number): string {
420 return '@' + id.toString(16);
421 }
422
423 -function serializeModuleReference(
423 +function serializeClientReference(
424 request: Request,
425 parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray<ReactModel>,
426 key: string,
427 - moduleReference: ModuleReference<any>,
427 + moduleReference: ClientReference<any>,
428 ): string {
429 - const moduleKey: ModuleKey = getModuleKey(moduleReference);
429 + const moduleKey: ClientReferenceKey = getClientReferenceKey(moduleReference);
430 const writtenModules = request.writtenModules;
431 const existingId = writtenModules.get(moduleKey);
432 if (existingId !== undefined) {
@@ -891,8 +891,8 @@ export function resolveModelToJSON(
891 }
892
893 if (typeof value === 'object') {
894 - if (isModuleReference(value)) {
895 - return serializeModuleReference(request, parent, key, (value: any));
894 + if (isClientReference(value)) {
895 + return serializeClientReference(request, parent, key, (value: any));
896 } else if ((value: any).$$typeof === REACT_PROVIDER_TYPE) {
897 const providerKey = ((value: any): ReactProviderType<any>)._context
898 ._globalName;
@@ -961,8 +961,8 @@ export function resolveModelToJSON(
961 }
962
963 if (typeof value === 'function') {
964 - if (isModuleReference(value)) {
965 - return serializeModuleReference(request, parent, key, (value: any));
964 + if (isClientReference(value)) {
965 + return serializeClientReference(request, parent, key, (value: any));
966 }
967 if (/^on[A-Z]/.test(key)) {
968 throw new Error(
packages/react-server/src/ReactFlightServerBundlerConfigCustom.js
+4 -4
@@ -10,9 +10,9 @@
10 declare var $$$hostConfig: any;
11
12 export opaque type BundlerConfig = mixed;
13 -export opaque type ModuleReference<T> = mixed; // eslint-disable-line no-unused-vars
13 +export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
14 export opaque type ModuleMetaData: any = mixed;
15 -export opaque type ModuleKey: any = mixed;
16 -export const isModuleReference = $$$hostConfig.isModuleReference;
17 -export const getModuleKey = $$$hostConfig.getModuleKey;
15 +export opaque type ClientReferenceKey: any = mixed;
16 +export const isClientReference = $$$hostConfig.isClientReference;
17 +export const getClientReferenceKey = $$$hostConfig.getClientReferenceKey;
18 export const resolveModuleMetaData = $$$hostConfig.resolveModuleMetaData;
packages/shared/isValidElementType.js
+2 -2
@@ -33,7 +33,7 @@ import {
33 enableLegacyHidden,
34 } from './ReactFeatureFlags';
35
36 -const REACT_MODULE_REFERENCE: symbol = Symbol.for('react.module.reference');
36 +const REACT_CLIENT_REFERENCE: symbol = Symbol.for('react.client.reference');
37
38 export default function isValidElementType(type: mixed): boolean {
39 if (typeof type === 'string' || typeof type === 'function') {
@@ -68,7 +68,7 @@ export default function isValidElementType(type: mixed): boolean {
68 // types supported by any Flight configuration anywhere since
69 // we don't know which Flight build this will end up being used
70 // with.
71 - type.$$typeof === REACT_MODULE_REFERENCE ||
71 + type.$$typeof === REACT_CLIENT_REFERENCE ||
72 type.getModuleId !== undefined
73 ) {
74 return true;
scripts/flow/react-relay-hooks.js
+2 -2
@@ -57,7 +57,7 @@ declare module 'ReactFlightDOMRelayClientIntegration' {
57 import type {JSResourceReference} from 'JSResourceReference';
58
59 declare export opaque type ModuleMetaData;
60 - declare export function resolveModuleReference<T>(
60 + declare export function resolveClientReference<T>(
61 moduleData: ModuleMetaData,
62 ): JSResourceReference<T>;
63 declare export function preloadModule<T>(
@@ -90,7 +90,7 @@ declare module 'ReactFlightNativeRelayClientIntegration' {
90 import type {JSResourceReference} from 'JSResourceReference';
91
92 declare export opaque type ModuleMetaData;
93 - declare export function resolveModuleReference<T>(
93 + declare export function resolveClientReference<T>(
94 moduleData: ModuleMetaData,
95 ): JSResourceReference<T>;
96 declare export function preloadModule<T>(
scripts/jest/setupHostConfigs.js
+2 -2
@@ -82,8 +82,8 @@ jest.mock('react-server/flight', () => {
82 jest.mock(shimServerStreamConfigPath, () => config);
83 jest.mock(shimServerFormatConfigPath, () => config);
84 jest.mock('react-server/src/ReactFlightServerBundlerConfigCustom', () => ({
85 - isModuleReference: config.isModuleReference,
86 - getModuleKey: config.getModuleKey,
85 + isClientReference: config.isClientReference,
86 + getClientReferenceKey: config.getClientReferenceKey,
87 resolveModuleMetaData: config.resolveModuleMetaData,
88 }));
89 jest.mock(shimFlightServerConfigPath, () =>