Repository: facebook/react
Pull request: [Flight] Run recreated Errors within a fake native stack

Stacked on #29740.

Before:

<img width="719" alt="Screenshot 2024-06-02 at 11 51 20 AM" src="https://github.com/facebook/react/assets/63648/8f79fa82-2474-4583-894e-a2329e9a6304">

After (updated with my patches to Chrome):

<img width="813" alt="Screenshot 2024-06-06 at 5 16 20 PM" src="https://github.com/facebook/react/assets/63648/bcc4f52f-e0ac-4708-ac2b-9629acdff705">

Sources panel after:

<img width="1188" alt="Screenshot 2024-06-06 at 5 14 21 PM" src="https://github.com/facebook/react/assets/63648/2c673fac-d32d-42e4-8fac-bb63704e4b7f">

The fake eval file is now under "React" and the real file is now under `file://`

Changed files:
filename: fixtures/flight/config/webpack.config.js
patch: @@ -7,6 +7,7 @@ const ReactFlightWebpackPlugin = require('react-server-dom-webpack/plugin');
 const fs = require('fs');
 const {createHash} = require('crypto');
 const path = require('path');
+const {pathToFileURL} = require('url');
 const webpack = require('webpack');
 const resolve = require('resolve');
 const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
@@ -235,7 +236,7 @@ module.exports = function (webpackEnv) {
               .relative(paths.appSrc, info.absoluteResourcePath)
               .replace(/\\/g, '/')
         : isEnvDevelopment &&
-          (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
+          (info => pathToFileURL(path.resolve(info.absoluteResourcePath))),
     },
     cache: {
       type: 'filesystem',

filename: fixtures/flight/server/region.js
patch: @@ -3,6 +3,7 @@
 // This is a server to host data-local resources like databases and RSC
 
 const path = require('path');
+const url = require('url');
 
 const register = require('react-server-dom-webpack/node-register');
 register();
@@ -192,7 +193,7 @@ if (process.env.NODE_ENV === 'development') {
         // We assume that if it was prefixed with file:// it's referring to the compiled output
         // and if it's a direct file path we assume it's source mapped back to original format.
         isCompiledOutput = true;
-        requestedFilePath = requestedFilePath.slice(7);
+        requestedFilePath = url.fileURLToPath(requestedFilePath);
       }
 
       const relativePath = path.relative(rootDir, requestedFilePath);
@@ -206,24 +207,41 @@ if (process.env.NODE_ENV === 'development') {
 
       const sourceMap = nodeModule.findSourceMap(requestedFilePath);
       let map;
-      // There are two ways to return a source map depending on what we observe in error.stack.
-      // A real app will have a similar choice to make for which strategy to pick.
-      if (!sourceMap || !isCompiledOutput) {
+      if (requestedFilePath.startsWith('node:')) {
+        // This is a node internal. We don't include any source code for this but we still
+        // generate a source map for it so that we can add it to an ignoreList automatically.
+        map = {
+          version: 3,
+          // We use the node:// protocol convention to teach Chrome DevTools that this is
+          // on a different protocol and not part of the current page.
+          sources: ['node:///' + requestedFilePath.slice(5)],
+          sourcesContent: ['// Node Internals'],
+          mappings: 'AAAA',
+          ignoreList: [0],
+          sourceRoot: '',
+        };
+      } else if (!sourceMap || !isCompiledOutput) {
         // If a file doesn't have a source map, such as this file, then we generate a blank
         // source map that just contains the original content and segments pointing to the
-        // original lines.
-        // Similarly
+        // original lines. If a line number points to uncompiled output, like if source mapping
+        // was already applied we also use this path.
         const sourceContent = await readFile(requestedFilePath, 'utf8');
         const lines = sourceContent.split('\n').length;
+        // We ensure to absolute
+        const sourceURL = url.pathToFileURL(requestedFilePath);
         map = {
           version: 3,
-          sources: [requestedFilePath],
+          sources: [sourceURL],
           sourcesContent: [sourceContent],
           // Note: This approach to mapping each line only lets you jump to each line
           // not jump to a column within a line. To do that, you need a proper source map
           // generated for each parsed segment or add a segment for each column.
           mappings: 'AAAA' + ';AACA'.repeat(lines - 1),
           sourceRoot: '',
+          // Add any node_modules to the ignore list automatically.
+          ignoreList: requestedFilePath.includes('node_modules')
+            ? [0]
+            : undefined,
         };
       } else {
         // We always set prepareStackTrace before reading the stack so that we get the stack

filename: fixtures/flight/src/index.js
patch: @@ -40,7 +40,11 @@ async function hydrateApp() {
     {
       callServer,
       findSourceMapURL(fileName) {
-        return '/source-maps?name=' + encodeURIComponent(fileName);
+        return (
+          document.location.origin +
+          '/source-maps?name=' +
+          encodeURIComponent(fileName)
+        );
       },
     }
   );

filename: packages/react-client/src/ReactFlightClient.js
patch: @@ -1586,12 +1586,36 @@ function resolveErrorDev(
       'resolveErrorDev should never be called in production mode. Use resolveErrorProd instead. This is a bug in React.',
     );
   }
-  // eslint-disable-next-line react-internal/prod-error-codes
-  const error = new Error(
-    message ||
-      'An error occurred in the Server Components render but no message was provided',
-  );
-  error.stack = stack;
+
+  let error;
+  if (!enableOwnerStacks) {
+    // Executing Error within a native stack isn't really limited to owner stacks
+    // but we gate it behind the same flag for now while iterating.
+    // eslint-disable-next-line react-internal/prod-error-codes
+    error = Error(
+      message ||
+        'An error occurred in the Server Components render but no message was provided',
+    );
+    error.stack = stack;
+  } else {
+    const callStack = buildFakeCallStack(
+      response,
+      stack,
+      // $FlowFixMe[incompatible-use]
+      Error.bind(
+        null,
+        message ||
+          'An error occurred in the Server Components render but no message was provided',
+      ),
+    );
+    const rootTask = response._debugRootTask;
+    if (rootTask != null) {
+      error = rootTask.run(callStack);
+    } else {
+      error = callStack();
+    }
+  }
+
   (error: any).digest = digest;
   const errorWithDigest: ErrorWithDigest = (error: any);
   const chunks = response._chunks;
@@ -1677,6 +1701,7 @@ const fakeFunctionCache: Map<string, FakeFunction<any>> = __DEV__
   ? new Map()
   : (null: any);
 
+let fakeFunctionIdx = 0;
 function createFakeFunction<T>(
   name: string,
   filename: string,
@@ -1695,20 +1720,36 @@ function createFakeFunction<T>(
   // point to the original source.
   let code;
   if (line <= 1) {
-    code = '_=>' + ' '.repeat(col < 4 ? 0 : col - 4) + '_()\n' + comment + '\n';
+    code = '_=>' + ' '.repeat(col < 4 ? 0 : col - 4) + '_()\n' + comment;
   } else {
     code =
       comment +
       '\n'.repeat(line - 2) +
       '_=>\n' +
       ' '.repeat(col < 1 ? 0 : col - 1) +
-      '_()\n';
+      '_()';
+  }
+
+  if (filename.startsWith('/')) {
+    // If the filename starts with `/` we assume that it is a file system file
+    // rather than relative to the current host. Since on the server fully qualified
+    // stack traces use the file path.
+    // TODO: What does this look like on Windows?
+    filename = 'file://' + filename;
   }
 
   if (sourceMap) {
-    code += '//# sourceMappingURL=' + sourceMap;
+    // We use the prefix rsc://React/ to separate these from other files listed in
+    // the Chrome DevTools. We need a "host name" and not just a protocol because
+    // otherwise the group name becomes the root folder. Ideally we don't want to
+    // show these at all but there's two reasons to assign a fake URL.
+    // 1) A printed stack trace string needs a unique URL to be able to source map it.
+    // 2) If source maps are disabled or fails, you should at least be able to tell
+    //    which file it was.
+    code += '\n//# sourceURL=rsc://React/' + filename + '?' + fakeFunctionIdx++;
+    code += '\n//# sourceMappingURL=' + sourceMap;
   } else if (filename) {
-    code += '//# sourceURL=' + filename;
+    code += '\n//# sourceURL=' + filename;
   }
 
   let fn: FakeFunction<T>;