main
js 173 lines 5.33 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 } from 'shared/ReactTypes';
15
16 import type {ImportMetadata} from '../shared/ReactFlightImportMetadata';
17 import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig';
18
19 import {
20 ID,
21 CHUNKS,
22 NAME,
23 isAsyncImport,
24 } from '../shared/ReactFlightImportMetadata';
25 import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
26
27 import hasOwnProperty from 'shared/hasOwnProperty';
28
29 export type ServerConsumerModuleMap = {
30 [clientId: string]: {
31 [clientExportName: string]: ClientReference<any>,
32 },
33 };
34
35 export type ServerManifest = void;
36
37 export type ServerReferenceId = string;
38
39 export opaque type ClientReferenceMetadata = ImportMetadata;
40
41 // eslint-disable-next-line no-unused-vars
42 export opaque type ClientReference<T> = {
43 specifier: string,
44 name: string,
45 async?: boolean,
46 };
47
48 // The reason this function needs to defined here in this file instead of just
49 // being exported directly from the WebpackDestination... file is because the
50 // ClientReferenceMetadata is opaque and we can't unwrap it there.
51 // This should get inlined and we could also just implement an unwrapping function
52 // though that risks it getting used in places it shouldn't be. This is unfortunate
53 // but currently it seems to be the best option we have.
54 export function prepareDestinationForModule(
55 moduleLoading: ModuleLoading,
56 nonce: ?string,
57 metadata: ClientReferenceMetadata,
58 ) {
59 prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce);
60 }
61
62 export function resolveClientReference<T>(
63 bundlerConfig: ServerConsumerModuleMap,
64 metadata: ClientReferenceMetadata,
65 ): ClientReference<T> {
66 const moduleExports = bundlerConfig[metadata[ID]];
67 let resolvedModuleData = moduleExports && moduleExports[metadata[NAME]];
68 let name;
69 if (resolvedModuleData) {
70 // The potentially aliased name.
71 name = resolvedModuleData.name;
72 } else {
73 // If we don't have this specific name, we might have the full module.
74 resolvedModuleData = moduleExports && moduleExports['*'];
75 if (!resolvedModuleData) {
76 throw new Error(
77 'Could not find the module "' +
78 metadata[ID] +
79 '" in the React Server Consumer Manifest. ' +
80 'This is probably a bug in the React Server Components bundler.',
81 );
82 }
83 name = metadata[NAME];
84 }
85 return {
86 specifier: resolvedModuleData.specifier,
87 name: name,
88 async: isAsyncImport(metadata),
89 };
90 }
91
92 export function resolveServerReference<T>(
93 bundlerConfig: ServerManifest,
94 id: ServerReferenceId,
95 ): ClientReference<T> {
96 const idx = id.lastIndexOf('#');
97 const specifier = id.slice(0, idx);
98 const name = id.slice(idx + 1);
99 return {specifier, name};
100 }
101
102 const asyncModuleCache: Map<string, Thenable<any>> = new Map();
103
104 export function preloadModule<T>(
105 metadata: ClientReference<T>,
106 ): null | Thenable<any> {
107 const existingPromise = asyncModuleCache.get(metadata.specifier);
108 if (existingPromise) {
109 if (existingPromise.status === 'fulfilled') {
110 return null;
111 }
112 return existingPromise;
113 } else {
114 // $FlowFixMe[unsupported-syntax]
115 let modulePromise: Promise<T> = import(metadata.specifier);
116 if (metadata.async) {
117 // If the module is async, it must have been a CJS module.
118 // CJS modules are accessed through the default export in
119 // Node.js so we have to get the default export to get the
120 // full module exports.
121 modulePromise = modulePromise.then(function (value) {
122 return (value as any).default;
123 });
124 }
125 modulePromise.then(
126 value => {
127 const fulfilledThenable: FulfilledThenable<mixed> =
128 modulePromise as any;
129 fulfilledThenable.status = 'fulfilled';
130 fulfilledThenable.value = value;
131 },
132 reason => {
133 const rejectedThenable: RejectedThenable<mixed> = modulePromise as any;
134 rejectedThenable.status = 'rejected';
135 rejectedThenable.reason = reason;
136 },
137 );
138 asyncModuleCache.set(metadata.specifier, modulePromise);
139 return modulePromise;
140 }
141 }
142
143 export function requireModule<T>(metadata: ClientReference<T>): T {
144 let moduleExports;
145 // We assume that preloadModule has been called before, which
146 // should have added something to the module cache.
147 const promise: any = asyncModuleCache.get(metadata.specifier);
148 if (promise.status === 'fulfilled') {
149 moduleExports = promise.value;
150 } else {
151 throw promise.reason;
152 }
153 if (metadata.name === '*') {
154 // This is a placeholder value that represents that the caller imported this
155 // as a CommonJS module as is.
156 return moduleExports;
157 }
158 if (metadata.name === '') {
159 // This is a placeholder value that represents that the caller accessed the
160 // default property of this if it was an ESM interop module.
161 return moduleExports.default;
162 }
163 if (hasOwnProperty.call(moduleExports, metadata.name)) {
164 return moduleExports[metadata.name];
165 }
166 return undefined as any;
167 }
168
169 export function getModuleDebugInfo<T>(metadata: ClientReference<T>): null {
170 // We don't emit any debug info on the server since we assume the loading
171 // of the bundle is insignificant on the server.
172 return null;
173 }