@@ -13,6 +13,7 @@ import type {
13
13
FulfilledThenable ,
14
14
RejectedThenable ,
15
15
ReactCustomFormAction ,
16
+ ReactCallSite ,
16
17
} from 'shared/ReactTypes' ;
17
18
import type { LazyComponent } from 'react/src/ReactLazy' ;
18
19
import type { TemporaryReferenceSet } from './ReactFlightTemporaryReferences' ;
@@ -1023,7 +1024,99 @@ function isSignatureEqual(
1023
1024
}
1024
1025
}
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 (
1027
1120
proxy : any ,
1028
1121
reference : { id : ServerReferenceId , bound : null | Thenable < Array < any >> } ,
1029
1122
encodeFormAction: void | EncodeFormActionCallback,
@@ -1098,16 +1191,163 @@ function bind(this: Function): Function {
1098
1191
return newFn ;
1099
1192
}
1100
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
+
1101
1313
export function createServerReference < A : Iterable < any > , T > (
1102
1314
id : ServerReferenceId ,
1103
1315
callServer : CallServerCallback ,
1104
1316
encodeFormAction ?: EncodeFormActionCallback ,
1317
+ findSourceMapURL ?: FindSourceMapURLCallback , // DEV-only
1318
+ functionName ?: string ,
1105
1319
) : ( ...A ) => Promise < T > {
1106
- const proxy = function ( ) : Promise < T > {
1320
+ let action = function ( ) : Promise < T > {
1107
1321
// $FlowFixMe[method-unbinding]
1108
1322
const args = Array . prototype . slice . call ( arguments ) ;
1109
1323
return callServer ( id , args ) ;
1110
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;
1113
1353
}
0 commit comments