main
js 239 lines 7.21 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 {
11 Thenable,
12 FulfilledThenable,
13 RejectedThenable,
14 ReactDebugInfo,
15 ReactIOInfo,
16 ReactAsyncInfo,
17 } from 'shared/ReactTypes';
18
19 import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig';
20
21 import {canUseDOM} from 'shared/ExecutionEnvironment';
22
23 export type ServerConsumerModuleMap = null;
24
25 export type ServerManifest = null;
26
27 export type ServerReferenceId = string;
28
29 import {prepareDestinationForModuleImpl} from 'react-client/src/ReactFlightClientConfig';
30
31 export opaque type ClientReferenceMetadata = {
32 $$typeof: symbol,
33 $$id: string,
34 $$hblp: mixed,
35 };
36
37 // eslint-disable-next-line no-unused-vars
38 export opaque type ClientReference<T> = {
39 $$typeof: symbol,
40 $$id: string,
41 $$hblp: mixed,
42 };
43
44 export function prepareDestinationForModule(
45 moduleLoading: ModuleLoading,
46 nonce: ?string,
47 metadata: ClientReferenceMetadata,
48 ) {
49 prepareDestinationForModuleImpl(moduleLoading, metadata.$$hblp, nonce);
50 }
51
52 export function resolveClientReference<T>(
53 bundlerConfig: ServerConsumerModuleMap,
54 metadata: ClientReferenceMetadata,
55 ): ClientReference<T> {
56 return metadata;
57 }
58
59 // Called on the server when decoding a client's action reply (via decodeReply).
60 // Maps the $$id string sent by the client back to module metadata so the server
61 // can locate and execute the actual function. $$hblp is null because this runs
62 // on the server — there is no client module to download.
63 export function resolveServerReference<T>(
64 config: ServerManifest,
65 id: ServerReferenceId,
66 ): ClientReference<T> {
67 return {
68 $$typeof: Symbol.for('react.client.reference'),
69 $$id: id,
70 $$hblp: null,
71 } as any;
72 }
73
74 const asyncModuleCache: Map<string, Thenable<any>> = new Map();
75
76 export function preloadModule<T>(
77 metadata: ClientReference<T>,
78 ): null | Thenable<any> {
79 if (!canUseDOM) {
80 // Server environment: modules are synchronously available via require().
81 return null;
82 }
83
84 // $FlowFixMe[cannot-resolve-module] JSResource is a Meta-internal module
85 const jsr: any = require('JSResource')(metadata.$$id);
86
87 const previouslyLoadedModule = jsr.getModuleIfRequireable();
88 if (previouslyLoadedModule != null) {
89 return null;
90 }
91
92 if (metadata.$$hblp != null) {
93 // Register our updates with the bootloader.
94 window.Bootloader.handlePayload(metadata.$$hblp);
95 }
96
97 const modulePromise: Thenable<T> = jsr.load();
98 modulePromise.then(
99 value => {
100 const fulfilledThenable: FulfilledThenable<mixed> = modulePromise as any;
101 fulfilledThenable.status = 'fulfilled';
102 fulfilledThenable.value = value;
103 },
104 reason => {
105 const rejectedThenable: RejectedThenable<mixed> = modulePromise as any;
106 rejectedThenable.status = 'rejected';
107 rejectedThenable.reason = reason;
108 },
109 );
110 asyncModuleCache.set(metadata.$$id, modulePromise);
111 return modulePromise;
112 }
113
114 export function requireModule<T>(metadata: ClientReference<T>): T {
115 if (!canUseDOM) {
116 // When the Flight client runs on the server to consume a Flight stream,
117 // modules are resolved synchronously via require() with Haste module names.
118 const id = metadata.$$id;
119 const idx = id.lastIndexOf('#');
120 if (idx !== -1) {
121 const moduleName = id.slice(0, idx);
122 const exportName = id.slice(idx + 1);
123 // Use .call to prevent bundlers from statically resolving this require.
124 const mod = require.call(null, moduleName); // eslint-disable-line no-useless-call
125 if (exportName === '' || exportName === 'default') {
126 return mod.__esModule ? mod.default : mod;
127 }
128 return mod[exportName];
129 }
130 // Use .call to prevent bundlers from statically resolving this require.
131 return require.call(null, id); // eslint-disable-line no-useless-call
132 }
133
134 // $FlowFixMe[cannot-resolve-module] JSResource is a Meta-internal module
135 const jsr: any = require('JSResource')(metadata.$$id);
136
137 const moduleExports = jsr.getModuleIfRequireable();
138 if (moduleExports != null) {
139 return moduleExports;
140 }
141
142 // Fall back to the async cache if JSResource doesn't have it yet.
143 const promise: any = asyncModuleCache.get(metadata.$$id);
144 if (promise && promise.status === 'fulfilled') {
145 return promise.value;
146 } else {
147 throw promise.reason;
148 }
149 }
150
151 // We cache ReactIOInfo across requests so that inner refreshes can dedupe with outer.
152 const moduleIOInfoCache: Map<string, ReactIOInfo> = __DEV__
153 ? new Map()
154 : (null as any);
155
156 export function getModuleDebugInfo<T>(
157 metadata: ClientReference<T>,
158 ): null | ReactDebugInfo {
159 if (!__DEV__) {
160 return null;
161 }
162 const filename = metadata.$$id;
163 let ioInfo = moduleIOInfoCache.get(filename);
164 if (ioInfo === undefined) {
165 let href;
166 try {
167 // $FlowFixMe[incompatible-type]
168 href = new URL(filename, document.baseURI).href;
169 } catch (_) {
170 href = filename;
171 }
172 let start = -1;
173 let end = -1;
174 let byteSize = 0;
175 // $FlowFixMe[method-unbinding]
176 if (typeof performance.getEntriesByType === 'function') {
177 // We may be able to collect the start and end time of this resource from Performance Observer.
178 const resourceEntries = performance.getEntriesByType('resource');
179 for (let i = 0; i < resourceEntries.length; i++) {
180 const resourceEntry = resourceEntries[i];
181 if (resourceEntry.name === href) {
182 start = resourceEntry.startTime;
183 end = start + resourceEntry.duration;
184 // $FlowFixMe[prop-missing]
185 byteSize = (resourceEntry.transferSize as any) || 0;
186 }
187 }
188 }
189 const value = Promise.resolve(href);
190 // $FlowFixMe[prop-missing]
191 value.status = 'fulfilled';
192 // Is there some more useful representation for the chunk?
193 // $FlowFixMe[prop-missing]
194 value.value = href;
195 // Create a fake stack frame that points to the beginning of the chunk. This is
196 // probably not source mapped so will link to the compiled source rather than
197 // any individual file that goes into the chunks.
198 const fakeStack = new Error('react-stack-top-frame');
199 if (fakeStack.stack.startsWith('Error: react-stack-top-frame')) {
200 // Looks like V8
201 fakeStack.stack =
202 'Error: react-stack-top-frame\n' +
203 // Add two frames since we always trim one off the top.
204 ' at Client Component Bundle (' +
205 href +
206 ':1:1)\n' +
207 ' at Client Component Bundle (' +
208 href +
209 ':1:1)';
210 } else {
211 // Looks like Firefox or Safari.
212 // Add two frames since we always trim one off the top.
213 fakeStack.stack =
214 'Client Component Bundle@' +
215 href +
216 ':1:1\n' +
217 'Client Component Bundle@' +
218 href +
219 ':1:1';
220 }
221 ioInfo = {
222 name: 'script',
223 start: start,
224 end: end,
225 value: value,
226 debugStack: fakeStack,
227 } as ReactIOInfo;
228 if (byteSize > 0) {
229 // $FlowFixMe[cannot-write]
230 ioInfo.byteSize = byteSize;
231 }
232 moduleIOInfoCache.set(filename, ioInfo);
233 }
234 // We could dedupe the async info too but conceptually each request is its own await.
235 const asyncInfo: ReactAsyncInfo = {
236 awaited: ioInfo,
237 };
238 return [asyncInfo];
239 }