@@ -13,6 +13,7 @@ import type {
1313 FulfilledThenable ,
1414 RejectedThenable ,
1515 ReactCustomFormAction ,
16+ ReactCallSite ,
1617} from 'shared/ReactTypes' ;
1718import type { LazyComponent } from 'react/src/ReactLazy' ;
1819import type { TemporaryReferenceSet } from './ReactFlightTemporaryReferences' ;
@@ -1023,7 +1024,99 @@ function isSignatureEqual(
10231024 }
10241025}
10251026
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 (
10271120 proxy : any ,
10281121 reference : { id : ServerReferenceId , bound : null | Thenable < Array < any >> } ,
10291122 encodeFormAction: void | EncodeFormActionCallback,
@@ -1098,16 +1191,169 @@ function bind(this: Function): Function {
10981191 return newFn ;
10991192}
11001193
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+
1278+ if ( stack . startsWith ( 'Error: react-stack-top-frame\n' ) ) {
1279+ // V8's default formatting prefixes with the error message which we
1280+ // don't want/need.
1281+ stack = stack . slice ( 29 ) ;
1282+ }
1283+ const endOfFirst = stack . indexOf ( '\n' ) ;
1284+ let secondFrame ;
1285+ if ( endOfFirst !== - 1 ) {
1286+ // Skip the first frame.
1287+ const endOfSecond = stack . indexOf ( '\n' , endOfFirst + 1 ) ;
1288+ if ( endOfSecond === - 1 ) {
1289+ secondFrame = stack . slice ( endOfFirst + 1 ) ;
1290+ } else {
1291+ secondFrame = stack . slice ( endOfFirst + 1 , endOfSecond ) ;
1292+ }
1293+ } else {
1294+ secondFrame = stack ;
1295+ }
1296+
1297+ let parsed = v8FrameRegExp . exec ( secondFrame ) ;
1298+ if ( ! parsed ) {
1299+ parsed = jscSpiderMonkeyFrameRegExp . exec ( secondFrame ) ;
1300+ if ( ! parsed ) {
1301+ return null ;
1302+ }
1303+ }
1304+
1305+ let name = parsed [ 1 ] || '' ;
1306+ if ( name === '<anonymous>' ) {
1307+ name = '' ;
1308+ }
1309+ let filename = parsed [ 2 ] || parsed [ 5 ] || '' ;
1310+ if ( filename === '<anonymous>' ) {
1311+ filename = '' ;
1312+ }
1313+ const line = + ( parsed [ 3 ] || parsed [ 6 ] ) ;
1314+ const col = + ( parsed [ 4 ] || parsed [ 7 ] ) ;
1315+
1316+ return [ name , filename , line , col ] ;
1317+ }
1318+
11011319export function createServerReference < A : Iterable < any > , T > (
11021320 id : ServerReferenceId ,
11031321 callServer : CallServerCallback ,
11041322 encodeFormAction ?: EncodeFormActionCallback ,
1323+ findSourceMapURL ?: FindSourceMapURLCallback , // DEV-only
1324+ functionName ?: string ,
11051325) : ( ...A ) => Promise < T > {
1106- const proxy = function ( ) : Promise < T > {
1326+ let action = function ( ) : Promise < T > {
11071327 // $FlowFixMe[method-unbinding]
11081328 const args = Array . prototype . slice . call ( arguments ) ;
11091329 return callServer ( id , args ) ;
11101330 } ;
1111- registerServerReference(proxy, { id , bound : null } , encodeFormAction);
1112- return proxy;
1331+ if (__DEV__) {
1332+ // Let's see if we can find a source map for the file which contained the
1333+ // server action. We extract it from the runtime so that it's resilient to
1334+ // multiple passes of compilation as long as we can find the final source map.
1335+ const location = parseStackLocation ( new Error ( 'react-stack-top-frame' ) ) ;
1336+ if ( location !== null ) {
1337+ const [ , filename , line , col ] = location ;
1338+ // While the environment that the Server Reference points to can be
1339+ // in any environment, what matters here is where the compiled source
1340+ // is from and that's in the currently executing environment. We hard
1341+ // code that as the value "Client" in case the findSourceMapURL helper
1342+ // needs it.
1343+ const env = 'Client' ;
1344+ const sourceMap =
1345+ findSourceMapURL == null ? null : findSourceMapURL ( filename , env ) ;
1346+ action = createFakeServerFunction (
1347+ functionName || '' ,
1348+ filename ,
1349+ sourceMap ,
1350+ line ,
1351+ col ,
1352+ env ,
1353+ action ,
1354+ ) ;
1355+ }
1356+ }
1357+ registerServerReference ( action , { id , bound : null } , encodeFormAction);
1358+ return action;
11131359}
0 commit comments