main
js 271 lines 8.55 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 } from 'shared/ReactTypes';
16
17 import type {
18 ImportMetadata,
19 ImportManifestEntry,
20 } from '../shared/ReactFlightImportMetadata';
21 import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig';
22
23 import {
24 ID,
25 CHUNKS,
26 NAME,
27 isAsyncImport,
28 } from '../shared/ReactFlightImportMetadata';
29
30 import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
31
32 import {
33 loadChunk,
34 addChunkDebugInfo,
35 } from 'react-client/src/ReactFlightClientConfig';
36
37 import hasOwnProperty from 'shared/hasOwnProperty';
38
39 export type ServerConsumerModuleMap = null | {
40 [clientId: string]: {
41 [clientExportName: string]: ClientReferenceManifestEntry,
42 },
43 };
44
45 export type ServerManifest = {
46 [id: string]: ImportManifestEntry,
47 };
48
49 export type ServerReferenceId = string;
50
51 export opaque type ClientReferenceManifestEntry = ImportManifestEntry;
52 export opaque type ClientReferenceMetadata = ImportMetadata;
53
54 // eslint-disable-next-line no-unused-vars
55 export opaque type ClientReference<T> = ClientReferenceMetadata;
56
57 // The reason this function needs to defined here in this file instead of just
58 // being exported directly from the TurbopackDestination... file is because the
59 // ClientReferenceMetadata is opaque and we can't unwrap it there.
60 // This should get inlined and we could also just implement an unwrapping function
61 // though that risks it getting used in places it shouldn't be. This is unfortunate
62 // but currently it seems to be the best option we have.
63 export function prepareDestinationForModule(
64 moduleLoading: ModuleLoading,
65 nonce: ?string,
66 metadata: ClientReferenceMetadata,
67 ) {
68 prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce);
69 }
70
71 export function resolveClientReference<T>(
72 bundlerConfig: ServerConsumerModuleMap,
73 metadata: ClientReferenceMetadata,
74 ): ClientReference<T> {
75 if (bundlerConfig) {
76 const moduleExports = bundlerConfig[metadata[ID]];
77 let resolvedModuleData = moduleExports && moduleExports[metadata[NAME]];
78 let name;
79 if (resolvedModuleData) {
80 // The potentially aliased name.
81 name = resolvedModuleData.name;
82 } else {
83 // If we don't have this specific name, we might have the full module.
84 resolvedModuleData = moduleExports && moduleExports['*'];
85 if (!resolvedModuleData) {
86 throw new Error(
87 'Could not find the module "' +
88 metadata[ID] +
89 '" in the React Server Consumer Manifest. ' +
90 'This is probably a bug in the React Server Components bundler.',
91 );
92 }
93 name = metadata[NAME];
94 }
95 if (isAsyncImport(metadata)) {
96 return [
97 resolvedModuleData.id,
98 resolvedModuleData.chunks,
99 name,
100 1 /* async */,
101 ];
102 } else {
103 return [resolvedModuleData.id, resolvedModuleData.chunks, name];
104 }
105 }
106 return metadata;
107 }
108
109 export function resolveServerReference<T>(
110 bundlerConfig: ServerManifest,
111 id: ServerReferenceId,
112 ): ClientReference<T> {
113 let name = '';
114 let resolvedModuleData = bundlerConfig[id];
115 if (resolvedModuleData) {
116 // The potentially aliased name.
117 name = resolvedModuleData.name;
118 } else {
119 // We didn't find this specific export name but we might have the * export
120 // which contains this name as well.
121 // TODO: It's unfortunate that we now have to parse this string. We should
122 // probably go back to encoding path and name separately on the client reference.
123 const idx = id.lastIndexOf('#');
124 if (idx !== -1) {
125 name = id.slice(idx + 1);
126 resolvedModuleData = bundlerConfig[id.slice(0, idx)];
127 }
128 if (!resolvedModuleData) {
129 throw new Error(
130 'Could not find the module "' +
131 id +
132 '" in the React Server Manifest. ' +
133 'This is probably a bug in the React Server Components bundler.',
134 );
135 }
136 }
137 if (resolvedModuleData.async) {
138 // If the module is marked as async in a Client Reference, we don't actually care.
139 // What matters is whether the consumer wants to unwrap it or not.
140 // For Server References, it is different because the consumer is completely internal
141 // to the bundler. So instead of passing it to each reference we can mark it in the
142 // manifest.
143 return [
144 resolvedModuleData.id,
145 resolvedModuleData.chunks,
146 name,
147 1 /* async */,
148 ];
149 }
150 return [resolvedModuleData.id, resolvedModuleData.chunks, name];
151 }
152
153 function requireAsyncModule(id: string): null | Thenable<any> {
154 // We've already loaded all the chunks. We can require the module.
155 const promise = __turbopack_require__(id);
156 if (typeof promise.then !== 'function') {
157 // This wasn't a promise after all.
158 return null;
159 } else if (promise.status === 'fulfilled') {
160 // This module was already resolved earlier.
161 return null;
162 } else {
163 // Instrument the Promise to stash the result.
164 promise.then(
165 value => {
166 const fulfilledThenable: FulfilledThenable<mixed> = promise as any;
167 fulfilledThenable.status = 'fulfilled';
168 fulfilledThenable.value = value;
169 },
170 reason => {
171 const rejectedThenable: RejectedThenable<mixed> = promise as any;
172 rejectedThenable.status = 'rejected';
173 rejectedThenable.reason = reason;
174 },
175 );
176 return promise;
177 }
178 }
179
180 // Turbopack will return cached promises for the same chunk.
181 // We still want to keep track of which chunks we have already instrumented
182 // and which chunks have already been loaded until Turbopack returns instrumented
183 // thenables directly.
184 const instrumentedChunks: WeakSet<Thenable<any>> = new WeakSet();
185 const loadedChunks: WeakSet<Thenable<any>> = new WeakSet();
186
187 function ignoreReject() {
188 // We rely on rejected promises to be handled by another listener.
189 }
190 // Start preloading the modules since we might need them soon.
191 // This function doesn't suspend.
192 export function preloadModule<T>(
193 metadata: ClientReference<T>,
194 ): null | Thenable<any> {
195 const chunks = metadata[CHUNKS];
196 const promises: Promise<any>[] = [];
197 for (let i = 0; i < chunks.length; i++) {
198 const chunkFilename = chunks[i];
199 const thenable = loadChunk(chunkFilename);
200 if (!loadedChunks.has(thenable)) {
201 promises.push(thenable);
202 }
203
204 if (!instrumentedChunks.has(thenable)) {
205 // $FlowFixMe[method-unbinding]
206 const resolve = loadedChunks.add.bind(loadedChunks, thenable);
207 thenable.then(resolve, ignoreReject);
208 instrumentedChunks.add(thenable);
209 }
210 }
211 if (isAsyncImport(metadata)) {
212 if (promises.length === 0) {
213 return requireAsyncModule(metadata[ID]);
214 } else {
215 return Promise.all(promises).then(() => {
216 return requireAsyncModule(metadata[ID]);
217 });
218 }
219 } else if (promises.length > 0) {
220 return Promise.all(promises);
221 } else {
222 return null;
223 }
224 }
225
226 // Actually require the module or suspend if it's not yet ready.
227 // Increase priority if necessary.
228 export function requireModule<T>(metadata: ClientReference<T>): T {
229 let moduleExports = __turbopack_require__(metadata[ID]);
230 if (isAsyncImport(metadata)) {
231 if (typeof moduleExports.then !== 'function') {
232 // This wasn't a promise after all.
233 } else if (moduleExports.status === 'fulfilled') {
234 // This Promise should've been instrumented by preloadModule.
235 moduleExports = moduleExports.value;
236 } else {
237 throw moduleExports.reason;
238 }
239 }
240 if (metadata[NAME] === '*') {
241 // This is a placeholder value that represents that the caller imported this
242 // as a CommonJS module as is.
243 return moduleExports;
244 }
245 if (metadata[NAME] === '') {
246 // This is a placeholder value that represents that the caller accessed the
247 // default property of this if it was an ESM interop module.
248 return moduleExports.__esModule ? moduleExports.default : moduleExports;
249 }
250 if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
251 return moduleExports[metadata[NAME]];
252 }
253 return undefined as any;
254 }
255
256 export function getModuleDebugInfo<T>(
257 metadata: ClientReference<T>,
258 ): null | ReactDebugInfo {
259 if (!__DEV__) {
260 return null;
261 }
262 const chunks = metadata[CHUNKS];
263 const debugInfo: ReactDebugInfo = [];
264 let i = 0;
265 while (i < chunks.length) {
266 const chunk = chunks[i++];
267 // A merged chunk is `[mergedChunkFilename, ...]`; use its own filename.
268 addChunkDebugInfo(debugInfo, typeof chunk === 'string' ? chunk : chunk[0]);
269 }
270 return debugInfo;
271 }