main
js 279 lines 9.05 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 WebpackDestination... 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 // Note that resolvedModuleData.async may be set if this is an Async Module.
96 // For Client References we don't actually care because what matters is whether
97 // the consumer expects an unwrapped async module or just a raw Promise so it
98 // has to already know which one it wants.
99 // We could error if this is an Async Import but it's not an Async Module.
100 // However, we also support plain CJS exporting a top level Promise which is not
101 // an Async Module according to the bundle graph but is effectively the same.
102 if (isAsyncImport(metadata)) {
103 return [
104 resolvedModuleData.id,
105 resolvedModuleData.chunks,
106 name,
107 1 /* async */,
108 ];
109 } else {
110 return [resolvedModuleData.id, resolvedModuleData.chunks, name];
111 }
112 }
113 return metadata;
114 }
115
116 export function resolveServerReference<T>(
117 bundlerConfig: ServerManifest,
118 id: ServerReferenceId,
119 ): ClientReference<T> {
120 let name = '';
121 let resolvedModuleData = bundlerConfig[id];
122 if (resolvedModuleData) {
123 // The potentially aliased name.
124 name = resolvedModuleData.name;
125 } else {
126 // We didn't find this specific export name but we might have the * export
127 // which contains this name as well.
128 // TODO: It's unfortunate that we now have to parse this string. We should
129 // probably go back to encoding path and name separately on the client reference.
130 const idx = id.lastIndexOf('#');
131 if (idx !== -1) {
132 name = id.slice(idx + 1);
133 resolvedModuleData = bundlerConfig[id.slice(0, idx)];
134 }
135 if (!resolvedModuleData) {
136 throw new Error(
137 'Could not find the module "' +
138 id +
139 '" in the React Server Manifest. ' +
140 'This is probably a bug in the React Server Components bundler.',
141 );
142 }
143 }
144 if (resolvedModuleData.async) {
145 // If the module is marked as async in a Client Reference, we don't actually care.
146 // What matters is whether the consumer wants to unwrap it or not.
147 // For Server References, it is different because the consumer is completely internal
148 // to the bundler. So instead of passing it to each reference we can mark it in the
149 // manifest.
150 return [
151 resolvedModuleData.id,
152 resolvedModuleData.chunks,
153 name,
154 1 /* async */,
155 ];
156 }
157 return [resolvedModuleData.id, resolvedModuleData.chunks, name];
158 }
159
160 // The chunk cache contains all the chunks we've preloaded so far.
161 // If they're still pending they're a thenable. This map also exists
162 // in Webpack but unfortunately it's not exposed so we have to
163 // replicate it in user space. null means that it has already loaded.
164 const chunkCache: Map<string, null | Promise<any>> = new Map();
165
166 function requireAsyncModule(id: string): null | Thenable<any> {
167 // We've already loaded all the chunks. We can require the module.
168 const promise = __webpack_require__(id);
169 if (typeof promise.then !== 'function') {
170 // This wasn't a promise after all.
171 return null;
172 } else if (promise.status === 'fulfilled') {
173 // This module was already resolved earlier.
174 return null;
175 } else {
176 // Instrument the Promise to stash the result.
177 promise.then(
178 value => {
179 const fulfilledThenable: FulfilledThenable<mixed> = promise as any;
180 fulfilledThenable.status = 'fulfilled';
181 fulfilledThenable.value = value;
182 },
183 reason => {
184 const rejectedThenable: RejectedThenable<mixed> = promise as any;
185 rejectedThenable.status = 'rejected';
186 rejectedThenable.reason = reason;
187 },
188 );
189 return promise;
190 }
191 }
192
193 function ignoreReject() {
194 // We rely on rejected promises to be handled by another listener.
195 }
196 // Start preloading the modules since we might need them soon.
197 // This function doesn't suspend.
198 export function preloadModule<T>(
199 metadata: ClientReference<T>,
200 ): null | Thenable<any> {
201 const chunks = metadata[CHUNKS];
202 const promises = [];
203 let i = 0;
204 while (i < chunks.length) {
205 const chunkId = chunks[i++];
206 const chunkFilename = chunks[i++];
207 const entry = chunkCache.get(chunkId);
208 if (entry === undefined) {
209 const thenable = loadChunk(chunkId, chunkFilename);
210 promises.push(thenable);
211 // $FlowFixMe[method-unbinding]
212 const resolve = chunkCache.set.bind(chunkCache, chunkId, null);
213 thenable.then(resolve, ignoreReject);
214 chunkCache.set(chunkId, thenable);
215 } else if (entry !== null) {
216 promises.push(entry);
217 }
218 }
219 if (isAsyncImport(metadata)) {
220 if (promises.length === 0) {
221 return requireAsyncModule(metadata[ID]);
222 } else {
223 return Promise.all(promises).then(() => {
224 return requireAsyncModule(metadata[ID]);
225 });
226 }
227 } else if (promises.length > 0) {
228 return Promise.all(promises);
229 } else {
230 return null;
231 }
232 }
233
234 // Actually require the module or suspend if it's not yet ready.
235 // Increase priority if necessary.
236 export function requireModule<T>(metadata: ClientReference<T>): T {
237 let moduleExports = __webpack_require__(metadata[ID]);
238 if (isAsyncImport(metadata)) {
239 if (typeof moduleExports.then !== 'function') {
240 // This wasn't a promise after all.
241 } else if (moduleExports.status === 'fulfilled') {
242 // This Promise should've been instrumented by preloadModule.
243 moduleExports = moduleExports.value;
244 } else {
245 throw moduleExports.reason;
246 }
247 }
248 if (metadata[NAME] === '*') {
249 // This is a placeholder value that represents that the caller imported this
250 // as a CommonJS module as is.
251 return moduleExports;
252 }
253 if (metadata[NAME] === '') {
254 // This is a placeholder value that represents that the caller accessed the
255 // default property of this if it was an ESM interop module.
256 return moduleExports.__esModule ? moduleExports.default : moduleExports;
257 }
258 if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
259 return moduleExports[metadata[NAME]];
260 }
261 return undefined as any;
262 }
263
264 export function getModuleDebugInfo<T>(
265 metadata: ClientReference<T>,
266 ): null | ReactDebugInfo {
267 if (!__DEV__) {
268 return null;
269 }
270 const chunks = metadata[CHUNKS];
271 const debugInfo: ReactDebugInfo = [];
272 let i = 0;
273 while (i < chunks.length) {
274 const chunkId = chunks[i++];
275 const chunkFilename = chunks[i++];
276 addChunkDebugInfo(debugInfo, chunkId, chunkFilename);
277 }
278 return debugInfo;
279 }