@samitouri / QOS-React / commits / 6ebfd5b082

[Flight] Source Map Server Actions to their Server Location (#30741)

This uses a similar technique to what we use to generate fake stack frames for server components. This generates an eval:ed wrapper function around the Server Reference proxy we create on the client. This wrapper function gets the original `name` of the action on the server and I also add a source map if `findSourceMapURL` is defined that points back to the source of the server function. For `"use server"` on the server, there's no new API. It just uses the callsite of `registerServerReference()` on the Server. We can infer the function name from the actual function on the server and we already have the `findSourceMapURL` on the client receiving it. For `"use server"` imported from the client, there's two new options added to `createServerReference()` (in addition to the optional [`encodeFormAction`](#27563)). These are only used in DEV mode. The [`findSourceMapURL`](#29708) option is the same one added in #29708. We need to pass this these references aren't created in the context of any specific request but globally. The other weird thing about this case is that this is actually a case where the compiled environment is the client so any source maps are the same as for the client layer, so the environment name here is just `"Client"`. ```diff createServerReference( id: string, callServer: CallServerCallback, encodeFormAction?: EncodeFormActionCallback, + findSourceMapURL?: FindSourceMapURLCallback, // DEV-only + functionName?: string, // DEV-only ) ``` The key is that we use the location of the `registerServerReference()`/`createServerReference()` call as the location of the function. A compiler can either emit those at the same locations as the original functions or use source maps to have those segments refer to the original location of the function (or in the case of a re-export the original location of the re-export is also a fine approximate). The compiled output must call these directly without a wrapper function because the wrapper adds a stack frame. I decided against complicated and fragile dev-only options to skip n number of frames that would just end up in prod code. The implementation just skips one frame - our own. Otherwise it'll just point all source mapping to the wrapper. We don't have a `"use server"` imported from the client implementation in the reference implementation/fixture so it's a bit tricky to test that. In the case of CJS on the server, we just use a runtime instead of compiler so it's tricky to source map those appropriately. We can implement it for ESM on the server which is the main thing we're testing in the fixture. It's easier in a real implementation where all the compilation is just one pass. It's a little tricky since we have to parse and append to other source maps but I'd like to do that as a follow up. Or maybe that's just an exercise for the reader. You can right click an action and click "Go to Definition". <img width="1323" alt="Screenshot 2024-08-17 at 6 04 27 PM" src="https://github.com/user-attachments/assets/94d379b3-8871-4671-a20d-cbf9cfbc2c6e"> For now they simply don't point to the right place but you can still jump to the right file in the fixture: <img width="1512" alt="Screenshot 2024-08-17 at 5 58 40 PM" src="https://github.com/user-attachments/assets/1ea5d665-e25a-44ca-9515-481dd3c5c2fe"> In Firefox/Safari given that the location doesn't exist in the source map yet, the browser refuses to open the file. Where as Chrome does nearest (last) line.

Sebastian Markbåge committed Aug 18, 2024 at 12:31 UTC 6ebfd5b0829c3e7a977ef4d9a0bd96436c681251
12 files changed +418 -57
packages/react-client/src/ReactFlightClient.js
+15 -24
@@ -13,6 +13,7 @@ import type {
13 ReactComponentInfo,
14 ReactAsyncInfo,
15 ReactStackTrace,
16 + ReactCallSite,
17 } from 'shared/ReactTypes';
18 import type {LazyComponent} from 'react/src/ReactLazy';
19
@@ -59,7 +60,7 @@ import {
60 bindToConsole,
61 } from './ReactFlightClientConfig';
62
62 -import {registerServerReference} from './ReactFlightReplyClient';
63 +import {createBoundServerReference} from './ReactFlightReplyClient';
64
65 import {readTemporaryReference} from './ReactFlightTemporaryReferences';
66
@@ -1001,30 +1002,20 @@ function waitForReference<T>(
1002
1003 function createServerReferenceProxy<A: Iterable<any>, T>(
1004 response: Response,
1004 - metaData: {id: any, bound: null | Thenable<Array<any>>},
1005 + metaData: {
1006 + id: any,
1007 + bound: null | Thenable<Array<any>>,
1008 + name?: string, // DEV-only
1009 + env?: string, // DEV-only
1010 + location?: ReactCallSite, // DEV-only
1011 + },
1012 ): (...A) => Promise<T> {
1006 - const callServer = response._callServer;
1007 - const proxy = function (): Promise<T> {
1008 - // $FlowFixMe[method-unbinding]
1009 - const args = Array.prototype.slice.call(arguments);
1010 - const p = metaData.bound;
1011 - if (!p) {
1012 - return callServer(metaData.id, args);
1013 - }
1014 - if (p.status === INITIALIZED) {
1015 - const bound = p.value;
1016 - return callServer(metaData.id, bound.concat(args));
1017 - }
1018 - // Since this is a fake Promise whose .then doesn't chain, we have to wrap it.
1019 - // TODO: Remove the wrapper once that's fixed.
1020 - return ((Promise.resolve(p): any): Promise<Array<any>>).then(
1021 - function (bound) {
1022 - return callServer(metaData.id, bound.concat(args));
1023 - },
1024 - );
1025 - };
1026 - registerServerReference(proxy, metaData, response._encodeFormAction);
1027 - return proxy;
1013 + return createBoundServerReference(
1014 + metaData,
1015 + response._callServer,
1016 + response._encodeFormAction,
1017 + __DEV__ ? response._debugFindSourceMapURL : undefined,
1018 + );
1019 }
1020
1021 function getOutlinedModel<T>(
packages/react-client/src/ReactFlightReplyClient.js
+244 -4
@@ -13,6 +13,7 @@ import type {
13 FulfilledThenable,
14 RejectedThenable,
15 ReactCustomFormAction,
16 + ReactCallSite,
17 } from 'shared/ReactTypes';
18 import type {LazyComponent} from 'react/src/ReactLazy';
19 import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
@@ -1023,7 +1024,99 @@ function isSignatureEqual(
1024 }
1025 }
1026
1026 -export function registerServerReference(
1027 +let fakeServerFunctionIdx = 0;
1028 +
1029 +function createFakeServerFunction<A: Iterable<any>, T>(
1030 + name: string,
1031 + filename: string,
1032 + sourceMap: null | string,
1033 + line: number,
1034 + col: number,
1035 + environmentName: string,
1036 + innerFunction: (...A) => Promise<T>,
1037 +): (...A) => Promise<T> {
1038 + // This creates a fake copy of a Server Module. It represents the Server Action on the server.
1039 + // We use an eval so we can source map it to the original location.
1040 +
1041 + const comment =
1042 + '/* This module is a proxy to a Server Action. Turn on Source Maps to see the server source. */';
1043 +
1044 + if (!name) {
1045 + // An eval:ed function with no name gets the name "eval". We give it something more descriptive.
1046 + name = '<anonymous>';
1047 + }
1048 + const encodedName = JSON.stringify(name);
1049 + // We generate code where both the beginning of the function and its parenthesis is at the line
1050 + // and column of the server executed code. We use a method form since that lets us name it
1051 + // anything we want and because the beginning of the function and its parenthesis is the same
1052 + // column. Because Chrome inspects the location of the parenthesis and Firefox inspects the
1053 + // location of the beginning of the function. By not using a function expression we avoid the
1054 + // ambiguity.
1055 + let code;
1056 + if (line <= 1) {
1057 + const minSize = encodedName.length + 7;
1058 + code =
1059 + 's=>({' +
1060 + encodedName +
1061 + ' '.repeat(col < minSize ? 0 : col - minSize) +
1062 + ':' +
1063 + '(...args) => s(...args)' +
1064 + '})\n' +
1065 + comment;
1066 + } else {
1067 + code =
1068 + comment +
1069 + '\n'.repeat(line - 2) +
1070 + 'server=>({' +
1071 + encodedName +
1072 + ':\n' +
1073 + ' '.repeat(col < 1 ? 0 : col - 1) +
1074 + // The function body can get printed so we make it look nice.
1075 + // This "calls the server with the arguments".
1076 + '(...args) => server(...args)' +
1077 + '})';
1078 + }
1079 +
1080 + if (filename.startsWith('/')) {
1081 + // If the filename starts with `/` we assume that it is a file system file
1082 + // rather than relative to the current host. Since on the server fully qualified
1083 + // stack traces use the file path.
1084 + // TODO: What does this look like on Windows?
1085 + filename = 'file://' + filename;
1086 + }
1087 +
1088 + if (sourceMap) {
1089 + // We use the prefix rsc://React/ to separate these from other files listed in
1090 + // the Chrome DevTools. We need a "host name" and not just a protocol because
1091 + // otherwise the group name becomes the root folder. Ideally we don't want to
1092 + // show these at all but there's two reasons to assign a fake URL.
1093 + // 1) A printed stack trace string needs a unique URL to be able to source map it.
1094 + // 2) If source maps are disabled or fails, you should at least be able to tell
1095 + // which file it was.
1096 + code +=
1097 + '\n//# sourceURL=rsc://React/' +
1098 + encodeURIComponent(environmentName) +
1099 + '/' +
1100 + filename +
1101 + '?s' + // We add an extra s here to distinguish from the fake stack frames
1102 + fakeServerFunctionIdx++;
1103 + code += '\n//# sourceMappingURL=' + sourceMap;
1104 + } else if (filename) {
1105 + code += '\n//# sourceURL=' + filename;
1106 + }
1107 +
1108 + try {
1109 + // Eval a factory and then call it to create a closure over the inner function.
1110 + // eslint-disable-next-line no-eval
1111 + return (0, eval)(code)(innerFunction)[name];
1112 + } catch (x) {
1113 + // If eval fails, such as if in an environment that doesn't support it,
1114 + // we fallback to just returning the inner function.
1115 + return innerFunction;
1116 + }
1117 +}
1118 +
1119 +function registerServerReference(
1120 proxy: any,
1121 reference: {id: ServerReferenceId, bound: null | Thenable<Array<any>>},
1122 encodeFormAction: void | EncodeFormActionCallback,
@@ -1098,16 +1191,163 @@ function bind(this: Function): Function {
1191 return newFn;
1192 }
1193
1194 +export type FindSourceMapURLCallback = (
1195 + fileName: string,
1196 + environmentName: string,
1197 +) => null | string;
1198 +
1199 +export function createBoundServerReference<A: Iterable<any>, T>(
1200 + metaData: {
1201 + id: ServerReferenceId,
1202 + bound: null | Thenable<Array<any>>,
1203 + name?: string, // DEV-only
1204 + env?: string, // DEV-only
1205 + location?: ReactCallSite, // DEV-only
1206 + },
1207 + callServer: CallServerCallback,
1208 + encodeFormAction?: EncodeFormActionCallback,
1209 + findSourceMapURL?: FindSourceMapURLCallback, // DEV-only
1210 +): (...A) => Promise<T> {
1211 + const id = metaData.id;
1212 + const bound = metaData.bound;
1213 + let action = function (): Promise<T> {
1214 + // $FlowFixMe[method-unbinding]
1215 + const args = Array.prototype.slice.call(arguments);
1216 + const p = bound;
1217 + if (!p) {
1218 + return callServer(id, args);
1219 + }
1220 + if (p.status === 'fulfilled') {
1221 + const boundArgs = p.value;
1222 + return callServer(id, boundArgs.concat(args));
1223 + }
1224 + // Since this is a fake Promise whose .then doesn't chain, we have to wrap it.
1225 + // TODO: Remove the wrapper once that's fixed.
1226 + return ((Promise.resolve(p): any): Promise<Array<any>>).then(
1227 + function (boundArgs) {
1228 + return callServer(id, boundArgs.concat(args));
1229 + },
1230 + );
1231 + };
1232 + if (__DEV__) {
1233 + const location = metaData.location;
1234 + if (location) {
1235 + const functionName = metaData.name || '';
1236 + const [, filename, line, col] = location;
1237 + const env = metaData.env || 'Server';
1238 + const sourceMap =
1239 + findSourceMapURL == null ? null : findSourceMapURL(filename, env);
1240 + action = createFakeServerFunction(
1241 + functionName,
1242 + filename,
1243 + sourceMap,
1244 + line,
1245 + col,
1246 + env,
1247 + action,
1248 + );
1249 + }
1250 + }
1251 + registerServerReference(action, {id, bound}, encodeFormAction);
1252 + return action;
1253 +}
1254 +
1255 +// This matches either of these V8 formats.
1256 +// at name (filename:0:0)
1257 +// at filename:0:0
1258 +// at async filename:0:0
1259 +const v8FrameRegExp =
1260 + /^ {3} at (?:(.+) \((.+):(\d+):(\d+)\)|(?:async )?(.+):(\d+):(\d+))$/;
1261 +// This matches either of these JSC/SpiderMonkey formats.
1262 +// name@filename:0:0
1263 +// filename:0:0
1264 +const jscSpiderMonkeyFrameRegExp = /(?:(.*)@)?(.*):(\d+):(\d+)/;
1265 +
1266 +function parseStackLocation(error: Error): null | ReactCallSite {
1267 + // This parsing is special in that we know that the calling function will always
1268 + // be a module that initializes the server action. We also need this part to work
1269 + // cross-browser so not worth a Config. It's DEV only so not super code size
1270 + // sensitive but also a non-essential feature.
1271 + let stack = error.stack;
1272 + if (stack.startsWith('Error: react-stack-top-frame\n')) {
1273 + // V8's default formatting prefixes with the error message which we
1274 + // don't want/need.
1275 + stack = stack.slice(29);
1276 + }
1277 + const endOfFirst = stack.indexOf('\n');
1278 + let secondFrame;
1279 + if (endOfFirst !== -1) {
1280 + // Skip the first frame.
1281 + const endOfSecond = stack.indexOf('\n', endOfFirst + 1);
1282 + if (endOfSecond === -1) {
1283 + secondFrame = stack.slice(endOfFirst + 1);
1284 + } else {
1285 + secondFrame = stack.slice(endOfFirst + 1, endOfSecond);
1286 + }
1287 + } else {
1288 + secondFrame = stack;
1289 + }
1290 +
1291 + let parsed = v8FrameRegExp.exec(secondFrame);
1292 + if (!parsed) {
1293 + parsed = jscSpiderMonkeyFrameRegExp.exec(secondFrame);
1294 + if (!parsed) {
1295 + return null;
1296 + }
1297 + }
1298 +
1299 + let name = parsed[1] || '';
1300 + if (name === '<anonymous>') {
1301 + name = '';
1302 + }
1303 + let filename = parsed[2] || parsed[5] || '';
1304 + if (filename === '<anonymous>') {
1305 + filename = '';
1306 + }
1307 + const line = +(parsed[3] || parsed[6]);
1308 + const col = +(parsed[4] || parsed[7]);
1309 +
1310 + return [name, filename, line, col];
1311 +}
1312 +
1313 export function createServerReference<A: Iterable<any>, T>(
1314 id: ServerReferenceId,
1315 callServer: CallServerCallback,
1316 encodeFormAction?: EncodeFormActionCallback,
1317 + findSourceMapURL?: FindSourceMapURLCallback, // DEV-only
1318 + functionName?: string,
1319 ): (...A) => Promise<T> {
1106 - const proxy = function (): Promise<T> {
1320 + let action = function (): Promise<T> {
1321 // $FlowFixMe[method-unbinding]
1322 const args = Array.prototype.slice.call(arguments);
1323 return callServer(id, args);
1324 };
1111 - registerServerReference(proxy, {id, bound: null}, encodeFormAction);
1112 - return proxy;
1325 + if (__DEV__) {
1326 + // Let's see if we can find a source map for the file which contained the
1327 + // server action. We extract it from the runtime so that it's resilient to
1328 + // multiple passes of compilation as long as we can find the final source map.
1329 + const location = parseStackLocation(new Error('react-stack-top-frame'));
1330 + if (location !== null) {
1331 + const [, filename, line, col] = location;
1332 + // While the environment that the Server Reference points to can be
1333 + // in any environment, what matters here is where the compiled source
1334 + // is from and that's in the currently executing environment. We hard
1335 + // code that as the value "Client" in case the findSourceMapURL helper
1336 + // needs it.
1337 + const env = 'Client';
1338 + const sourceMap =
1339 + findSourceMapURL == null ? null : findSourceMapURL(filename, env);
1340 + action = createFakeServerFunction(
1341 + functionName || '',
1342 + filename,
1343 + sourceMap,
1344 + line,
1345 + col,
1346 + env,
1347 + action,
1348 + );
1349 + }
1350 + }
1351 + registerServerReference(action, {id, bound: null}, encodeFormAction);
1352 + return action;
1353 }
packages/react-server-dom-esm/src/ReactFlightESMReferences.js
+27 -6
@@ -13,6 +13,7 @@ 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
@@ -68,10 +69,30 @@ export function registerServerReference<T: Function>(
69 id: string,
70 exportName: string,
71 ): ServerReference<T> {
71 - return Object.defineProperties((reference: any), {
72 - $$typeof: {value: SERVER_REFERENCE_TAG},
73 - $$id: {value: id + '#' + exportName, configurable: true},
74 - $$bound: {value: null, configurable: true},
75 - bind: {value: bind, configurable: true},
76 - });
72 + const $$typeof = {value: SERVER_REFERENCE_TAG};
73 + const $$id = {
74 + value: id + '#' + exportName,
75 + configurable: true,
76 + };
77 + const $$bound = {value: null, configurable: true};
78 + return Object.defineProperties(
79 + (reference: any),
80 + __DEV__
81 + ? {
82 + $$typeof,
83 + $$id,
84 + $$bound,
85 + $$location: {
86 + value: Error('react-stack-top-frame'),
87 + configurable: true,
88 + },
89 + bind: {value: bind, configurable: true},
90 + }
91 + : {
92 + $$typeof,
93 + $$id,
94 + $$bound,
95 + bind: {value: bind, configurable: true},
96 + },
97 + );
98 }
packages/react-server-dom-esm/src/server/ReactFlightServerConfigESMBundler.js
+7
@@ -70,3 +70,10 @@ export function getServerReferenceBoundArguments<T>(
70 ): null | Array<ReactClientValue> {
71 return serverReference.$$bound;
72 }
73 +
74 +export function getServerReferenceLocation<T>(
75 + config: ClientManifest,
76 + serverReference: ServerReference<T>,
77 +): void | Error {
78 + return serverReference.$$location;
79 +}
packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js
+27 -9
@@ -13,6 +13,7 @@ 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
@@ -81,15 +82,32 @@ export function registerServerReference<T: Function>(
82 id: string,
83 exportName: null | string,
84 ): ServerReference<T> {
84 - return Object.defineProperties((reference: any), {
85 - $$typeof: {value: SERVER_REFERENCE_TAG},
86 - $$id: {
87 - value: exportName === null ? id : id + '#' + exportName,
88 - configurable: true,
89 - },
90 - $$bound: {value: null, configurable: true},
91 - bind: {value: bind, configurable: true},
92 - });
85 + const $$typeof = {value: SERVER_REFERENCE_TAG};
86 + const $$id = {
87 + value: exportName === null ? id : id + '#' + exportName,
88 + configurable: true,
89 + };
90 + const $$bound = {value: null, configurable: true};
91 + return Object.defineProperties(
92 + (reference: any),
93 + __DEV__
94 + ? {
95 + $$typeof,
96 + $$id,
97 + $$bound,
98 + $$location: {
99 + value: Error('react-stack-top-frame'),
100 + configurable: true,
101 + },
102 + bind: {value: bind, configurable: true},
103 + }
104 + : {
105 + $$typeof,
106 + $$id,
107 + $$bound,
108 + bind: {value: bind, configurable: true},
109 + },
110 + );
111 }
112
113 const PROMISE_PROTOTYPE = Promise.prototype;
packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js
+7
@@ -91,3 +91,10 @@ export function getServerReferenceBoundArguments<T>(
91 ): null | Array<ReactClientValue> {
92 return serverReference.$$bound;
93 }
94 +
95 +export function getServerReferenceLocation<T>(
96 + config: ClientManifest,
97 + serverReference: ServerReference<T>,
98 +): void | Error {
99 + return serverReference.$$location;
100 +}
packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js
+27 -9
@@ -13,6 +13,7 @@ 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
@@ -89,15 +90,32 @@ export function registerServerReference<T: Function>(
90 id: string,
91 exportName: null | string,
92 ): ServerReference<T> {
92 - return Object.defineProperties((reference: any), {
93 - $$typeof: {value: SERVER_REFERENCE_TAG},
94 - $$id: {
95 - value: exportName === null ? id : id + '#' + exportName,
96 - configurable: true,
97 - },
98 - $$bound: {value: null, configurable: true},
99 - bind: {value: bind, configurable: true},
100 - });
93 + const $$typeof = {value: SERVER_REFERENCE_TAG};
94 + const $$id = {
95 + value: exportName === null ? id : id + '#' + exportName,
96 + configurable: true,
97 + };
98 + const $$bound = {value: null, configurable: true};
99 + return Object.defineProperties(
100 + (reference: any),
101 + __DEV__
102 + ? {
103 + $$typeof,
104 + $$id,
105 + $$bound,
106 + $$location: {
107 + value: Error('react-stack-top-frame'),
108 + configurable: true,
109 + },
110 + bind: {value: bind, configurable: true},
111 + }
112 + : {
113 + $$typeof,
114 + $$id,
115 + $$bound,
116 + bind: {value: bind, configurable: true},
117 + },
118 + );
119 }
120
121 const PROMISE_PROTOTYPE = Promise.prototype;
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+12
@@ -1393,9 +1393,21 @@ describe('ReactFlightDOMBrowser', () => {
1393 const body = await ReactServerDOMClient.encodeReply(args);
1394 return callServer(ref, body);
1395 },
1396 + undefined,
1397 + undefined,
1398 + 'upper',
1399 ),
1400 };
1401
1402 + expect(ServerModuleBImportedOnClient.upper.name).toBe(
1403 + __DEV__ ? 'upper' : 'action',
1404 + );
1405 + if (__DEV__) {
1406 + expect(ServerModuleBImportedOnClient.upper.toString()).toBe(
1407 + '(...args) => server(...args)',
1408 + );
1409 + }
1410 +
1411 function Client({action}) {
1412 // Client side pass a Server Reference into an action.
1413 actionProxy = text => action(ServerModuleBImportedOnClient.upper, text);
packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js
+7
@@ -91,3 +91,10 @@ export function getServerReferenceBoundArguments<T>(
91 ): null | Array<ReactClientValue> {
92 return serverReference.$$bound;
93 }
94 +
95 +export function getServerReferenceLocation<T>(
96 + config: ClientManifest,
97 + serverReference: ServerReference<T>,
98 +): void | Error {
99 + return serverReference.$$location;
100 +}
packages/react-server/src/ReactFlightServer.js
+37 -5
@@ -64,6 +64,7 @@ import type {
64 ReactComponentInfo,
65 ReactAsyncInfo,
66 ReactStackTrace,
67 + ReactCallSite,
68 } from 'shared/ReactTypes';
69 import type {ReactElement} from 'shared/ReactElementType';
70 import type {LazyComponent} from 'react/src/ReactLazy';
@@ -72,6 +73,7 @@ import {
73 resolveClientReferenceMetadata,
74 getServerReferenceId,
75 getServerReferenceBoundArguments,
76 + getServerReferenceLocation,
77 getClientReferenceKey,
78 isClientReference,
79 isServerReference,
@@ -1955,17 +1957,47 @@ function serializeServerReference(
1957 return serializeServerReferenceID(existingId);
1958 }
1959
1958 - const bound: null | Array<any> = getServerReferenceBoundArguments(
1960 + const boundArgs: null | Array<any> = getServerReferenceBoundArguments(
1961 request.bundlerConfig,
1962 serverReference,
1963 );
1964 + const bound = boundArgs === null ? null : Promise.resolve(boundArgs);
1965 + const id = getServerReferenceId(request.bundlerConfig, serverReference);
1966 +
1967 + let location: null | ReactCallSite = null;
1968 + if (__DEV__) {
1969 + const error = getServerReferenceLocation(
1970 + request.bundlerConfig,
1971 + serverReference,
1972 + );
1973 + if (error) {
1974 + const frames = parseStackTrace(error, 1);
1975 + if (frames.length > 0) {
1976 + location = frames[0];
1977 + }
1978 + }
1979 + }
1980 +
1981 const serverReferenceMetadata: {
1982 id: ServerReferenceId,
1983 bound: null | Promise<Array<any>>,
1965 - } = {
1966 - id: getServerReferenceId(request.bundlerConfig, serverReference),
1967 - bound: bound ? Promise.resolve(bound) : null,
1968 - };
1984 + name?: string, // DEV-only
1985 + env?: string, // DEV-only
1986 + location?: ReactCallSite, // DEV-only
1987 + } =
1988 + __DEV__ && location !== null
1989 + ? {
1990 + id,
1991 + bound,
1992 + name:
1993 + typeof serverReference === 'function' ? serverReference.name : '',
1994 + env: (0, request.environmentName)(),
1995 + location,
1996 + }
1997 + : {
1998 + id,
1999 + bound,
2000 + };
2001 const metadataId = outlineModel(request, serverReferenceMetadata);
2002 writtenServerReferences.set(serverReference, metadataId);
2003 return serializeServerReferenceID(metadataId);
packages/react-server/src/ReactFlightServerConfigBundlerCustom.js
+1
@@ -23,3 +23,4 @@ export const resolveClientReferenceMetadata =
23 export const getServerReferenceId = $$$config.getServerReferenceId;
24 export const getServerReferenceBoundArguments =
25 $$$config.getServerReferenceBoundArguments;
26 +export const getServerReferenceLocation = $$$config.getServerReferenceLocation;
packages/react-server/src/forks/ReactFlightServerConfig.markup.js
+7
@@ -90,3 +90,10 @@ export function getServerReferenceBoundArguments<T>(
90 'Use a fixed URL for any forms instead.',
91 );
92 }
93 +
94 +export function getServerReferenceLocation<T>(
95 + config: ClientManifest,
96 + serverReference: ServerReference<T>,
97 +): void {
98 + return undefined;
99 +}