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';
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,
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
}