main
js 362 lines 11.3 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
11
12 export type ServerReference<T: Function> = T & {
13 $$typeof: symbol,
14 $$id: string,
15 $$bound: null | Array<ReactClientValue>,
16 $$location?: Error,
17 };
18
19 // eslint-disable-next-line no-unused-vars
20 export type ClientReference<T> = {
21 $$typeof: symbol,
22 $$id: string,
23 $$async: boolean,
24 };
25
26 const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
27 const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference');
28
29 export function isClientReference(reference: Object): boolean {
30 return reference.$$typeof === CLIENT_REFERENCE_TAG;
31 }
32
33 export function isServerReference(reference: Object): boolean {
34 return reference.$$typeof === SERVER_REFERENCE_TAG;
35 }
36
37 export function registerClientReference<T>(
38 proxyImplementation: any,
39 id: string,
40 exportName: string,
41 ): ClientReference<T> {
42 return registerClientReferenceImpl(
43 proxyImplementation,
44 id + '#' + exportName,
45 false,
46 );
47 }
48
49 function registerClientReferenceImpl<T>(
50 proxyImplementation: any,
51 id: string,
52 async: boolean,
53 ): ClientReference<T> {
54 return Object.defineProperties(proxyImplementation, {
55 $$typeof: {value: CLIENT_REFERENCE_TAG},
56 $$id: {value: id},
57 $$async: {value: async},
58 });
59 }
60
61 // $FlowFixMe[method-unbinding]
62 const FunctionBind = Function.prototype.bind;
63 // $FlowFixMe[method-unbinding]
64 const ArraySlice = Array.prototype.slice;
65 function bind(this: ServerReference<any>): any {
66 // $FlowFixMe[incompatible-type]
67 const newFn = FunctionBind.apply(this, arguments);
68 if (this.$$typeof === SERVER_REFERENCE_TAG) {
69 if (__DEV__) {
70 const thisBind = arguments[0];
71 if (thisBind != null) {
72 console.error(
73 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().',
74 );
75 }
76 }
77 const args = ArraySlice.call(arguments, 1);
78 const $$typeof = {value: SERVER_REFERENCE_TAG};
79 const $$id = {value: this.$$id};
80 const $$bound = {value: this.$$bound ? this.$$bound.concat(args) : args};
81 return Object.defineProperties(
82 newFn as any,
83 (__DEV__
84 ? {
85 $$typeof,
86 $$id,
87 $$bound,
88 $$location: {
89 value: this.$$location,
90 configurable: true,
91 },
92 bind: {value: bind, configurable: true},
93 }
94 : {
95 $$typeof,
96 $$id,
97 $$bound,
98 bind: {value: bind, configurable: true},
99 }) as PropertyDescriptorMap,
100 );
101 }
102 return newFn;
103 }
104
105 const serverReferenceToString = {
106 value: () => 'function () { [omitted code] }',
107 configurable: true,
108 writable: true,
109 };
110
111 export function registerServerReference<T: Function>(
112 reference: T,
113 id: string,
114 exportName: null | string,
115 ): ServerReference<T> {
116 const $$typeof = {value: SERVER_REFERENCE_TAG};
117 const $$id = {
118 value: exportName === null ? id : id + '#' + exportName,
119 configurable: true,
120 };
121 const $$bound = {value: null, configurable: true};
122 return Object.defineProperties(
123 reference as any,
124 __DEV__
125 ? ({
126 $$typeof,
127 $$id,
128 $$bound,
129 $$location: {
130 value: Error('react-stack-top-frame'),
131 configurable: true,
132 },
133 bind: {value: bind, configurable: true},
134 toString: serverReferenceToString,
135 } as PropertyDescriptorMap)
136 : ({
137 $$typeof,
138 $$id,
139 $$bound,
140 bind: {value: bind, configurable: true},
141 toString: serverReferenceToString,
142 } as PropertyDescriptorMap),
143 );
144 }
145
146 const PROMISE_PROTOTYPE = Promise.prototype;
147
148 const deepProxyHandlers: Proxy$traps<mixed> = {
149 get: function (
150 target: Function,
151 name: string | symbol,
152 receiver: Proxy<Function>,
153 ) {
154 switch (name) {
155 // These names are read by the Flight runtime if you end up using the exports object.
156 case '$$typeof':
157 // These names are a little too common. We should probably have a way to
158 // have the Flight runtime extract the inner target instead.
159 return target.$$typeof;
160 case '$$id':
161 return target.$$id;
162 case '$$async':
163 return target.$$async;
164 case 'name':
165 return target.name;
166 case 'displayName':
167 return undefined;
168 // We need to special case this because createElement reads it if we pass this
169 // reference.
170 case 'defaultProps':
171 return undefined;
172 // React looks for debugInfo on thenables.
173 case '_debugInfo':
174 return undefined;
175 // Avoid this attempting to be serialized.
176 case 'toJSON':
177 return undefined;
178 case Symbol.toPrimitive:
179 // $FlowFixMe[prop-missing]
180 return Object.prototype[Symbol.toPrimitive];
181 case Symbol.toStringTag:
182 // $FlowFixMe[prop-missing]
183 return Object.prototype[Symbol.toStringTag];
184 case 'Provider':
185 // Context.Provider === Context in React, so return the same reference.
186 // This allows server components to render <ClientContext.Provider>
187 // which will be serialized and executed on the client.
188 return receiver;
189 case 'then':
190 throw new Error(
191 `Cannot await or return from a thenable. ` +
192 `You cannot await a client module from a server component.`,
193 );
194 }
195 // eslint-disable-next-line react-internal/safe-string-coercion
196 const expression = String(target.name) + '.' + String(name);
197 throw new Error(
198 `Cannot access ${expression} on the server. ` +
199 'You cannot dot into a client module from a server component. ' +
200 'You can only pass the imported name through.',
201 );
202 },
203 set: function () {
204 throw new Error('Cannot assign to a client module from a server module.');
205 },
206 };
207
208 function getReference(target: Function, name: string | symbol): $FlowFixMe {
209 switch (name) {
210 // These names are read by the Flight runtime if you end up using the exports object.
211 case '$$typeof':
212 return target.$$typeof;
213 case '$$id':
214 return target.$$id;
215 case '$$async':
216 return target.$$async;
217 case 'name':
218 return target.name;
219 // We need to special case this because createElement reads it if we pass this
220 // reference.
221 case 'defaultProps':
222 return undefined;
223 // React looks for debugInfo on thenables.
224 case '_debugInfo':
225 return undefined;
226 // Avoid this attempting to be serialized.
227 case 'toJSON':
228 return undefined;
229 case Symbol.toPrimitive:
230 // $FlowFixMe[prop-missing]
231 return Object.prototype[Symbol.toPrimitive];
232 case Symbol.toStringTag:
233 // $FlowFixMe[prop-missing]
234 return Object.prototype[Symbol.toStringTag];
235 case '__esModule':
236 // Something is conditionally checking which export to use. We'll pretend to be
237 // an ESM compat module but then we'll check again on the client.
238 const moduleId = target.$$id;
239 target.default = registerClientReferenceImpl(
240 function () {
241 throw new Error(
242 `Attempted to call the default export of ${moduleId} from the server ` +
243 `but it's on the client. It's not possible to invoke a client function from ` +
244 `the server, it can only be rendered as a Component or passed to props of a ` +
245 `Client Component.`,
246 );
247 } as any,
248 target.$$id + '#',
249 target.$$async,
250 );
251 return true;
252 case 'then':
253 if (target.then) {
254 // Use a cached value
255 return target.then;
256 }
257 if (!target.$$async) {
258 // If this module is expected to return a Promise (such as an AsyncModule) then
259 // we should resolve that with a client reference that unwraps the Promise on
260 // the client.
261
262 const clientReference: ClientReference<any> =
263 registerClientReferenceImpl({} as any, target.$$id, true);
264 // $FlowFixMe[incompatible-variance]
265 // $FlowFixMe[incompatible-type]
266 const proxy = new Proxy(clientReference, proxyHandlers);
267
268 // Treat this as a resolved Promise for React's use()
269 target.status = 'fulfilled';
270 target.value = proxy;
271
272 const then = (target.then = registerClientReferenceImpl(
273 function then(resolve, reject: any) {
274 // Expose to React.
275 return Promise.resolve(resolve(proxy));
276 } as any,
277 // If this is not used as a Promise but is treated as a reference to a `.then`
278 // export then we should treat it as a reference to that name.
279 target.$$id + '#then',
280 false,
281 ));
282 return then;
283 } else {
284 // Since typeof .then === 'function' is a feature test we'd continue recursing
285 // indefinitely if we return a function. Instead, we return an object reference
286 // if we check further.
287 return undefined;
288 }
289 }
290 if (typeof name === 'symbol') {
291 throw new Error(
292 'Cannot read Symbol exports. Only named exports are supported on a client module ' +
293 'imported on the server.',
294 );
295 }
296 let cachedReference = target[name];
297 if (!cachedReference) {
298 const reference: ClientReference<any> = registerClientReferenceImpl(
299 function () {
300 throw new Error(
301 // eslint-disable-next-line react-internal/safe-string-coercion
302 `Attempted to call ${String(name)}() from the server but ${String(name)} is on the client. ` +
303 `It's not possible to invoke a client function from the server, it can ` +
304 `only be rendered as a Component or passed to props of a Client Component.`,
305 );
306 } as any,
307 target.$$id + '#' + name,
308 target.$$async,
309 );
310 Object.defineProperty(reference as any, 'name', {value: name});
311 cachedReference = target[name] = new Proxy(reference, deepProxyHandlers);
312 }
313 return cachedReference;
314 }
315
316 const proxyHandlers = {
317 get: function (
318 target: Function,
319 name: string | symbol,
320 receiver: Proxy<Function>,
321 ): $FlowFixMe {
322 return getReference(target, name);
323 },
324 getOwnPropertyDescriptor: function (
325 target: Function,
326 name: string | symbol,
327 ): $FlowFixMe {
328 let descriptor = Object.getOwnPropertyDescriptor(target, name);
329 if (!descriptor) {
330 descriptor = {
331 value: getReference(target, name),
332 writable: false,
333 configurable: false,
334 enumerable: false,
335 };
336 Object.defineProperty(target, name, descriptor);
337 }
338 return descriptor;
339 },
340 getPrototypeOf(target: Function): Object {
341 // Pretend to be a Promise in case anyone asks.
342 return PROMISE_PROTOTYPE;
343 },
344 set: function (): empty {
345 throw new Error('Cannot assign to a client module from a server module.');
346 },
347 };
348
349 export function createClientModuleProxy<T>(
350 moduleId: string,
351 ): ClientReference<T> {
352 const clientReference: ClientReference<T> = registerClientReferenceImpl(
353 {} as any,
354 // Represents the whole Module object instead of a particular import.
355 moduleId,
356 false,
357 );
358 // $FlowFixMe[incompatible-variance]
359 // $FlowFixMe[incompatible-type]
360 // $FlowFixMe[incompatible-exact]
361 return new Proxy(clientReference, proxyHandlers);
362 }