Repository: facebook/react
Pull request: [Flight ESM] Wire up Source Maps in the flight-esm fixture

Same as #29708 but for the flight-esm fixture.


Changed files:
filename: fixtures/flight-esm/server/global.js
patch: @@ -131,6 +131,45 @@ app.use(
   express.static('node_modules/react-server-dom-esm/esm')
 );
 
+if (process.env.NODE_ENV === 'development') {
+  app.get('/source-maps', async function (req, res, next) {
+    // Proxy the request to the regional server.
+    const proxiedHeaders = {
+      'X-Forwarded-Host': req.hostname,
+      'X-Forwarded-For': req.ips,
+      'X-Forwarded-Port': 3000,
+      'X-Forwarded-Proto': req.protocol,
+    };
+
+    const promiseForData = request(
+      {
+        host: '127.0.0.1',
+        port: 3001,
+        method: req.method,
+        path: req.originalUrl,
+        headers: proxiedHeaders,
+      },
+      req
+    );
+
+    try {
+      const rscResponse = await promiseForData;
+      res.set('Content-type', 'application/json');
+      rscResponse.on('data', data => {
+        res.write(data);
+        res.flush();
+      });
+      rscResponse.on('end', data => {
+        res.end();
+      });
+    } catch (e) {
+      console.error(`Failed to proxy request: ${e.stack}`);
+      res.statusCode = 500;
+      res.end();
+    }
+  });
+}
+
 app.listen(3000, () => {
   console.log('Global Fizz/Webpack Server listening on port 3000...');
 });

filename: fixtures/flight-esm/server/region.js
patch: @@ -17,6 +17,8 @@ const app = express();
 const compress = require('compression');
 const {Readable} = require('node:stream');
 
+const nodeModule = require('node:module');
+
 app.use(compress());
 
 // Application
@@ -116,6 +118,88 @@ app.get('/todos', function (req, res) {
   ]);
 });
 
+if (process.env.NODE_ENV === 'development') {
+  const rootDir = path.resolve(__dirname, '../');
+
+  app.get('/source-maps', async function (req, res, next) {
+    try {
+      res.set('Content-type', 'application/json');
+      let requestedFilePath = req.query.name;
+
+      let isCompiledOutput = false;
+      if (requestedFilePath.startsWith('file://')) {
+        // 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 = url.fileURLToPath(requestedFilePath);
+      }
+
+      const relativePath = path.relative(rootDir, requestedFilePath);
+      if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
+        // This is outside the root directory of the app. Forbid it to be served.
+        res.status = 403;
+        res.write('{}');
+        res.end();
+        return;
+      }
+
+      const sourceMap = nodeModule.findSourceMap(requestedFilePath);
+      let map;
+      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. 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: [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
+        // without source maps applied. Therefore we have to use the original source map.
+        // If something read .stack before we did, we might observe the line/column after
+        // source mapping back to the original file. We use the isCompiledOutput check above
+        // in that case.
+        map = sourceMap.payload;
+      }
+      res.write(JSON.stringify(map));
+      res.end();
+    } catch (x) {
+      res.status = 500;
+      res.write('{}');
+      res.end();
+      console.error(x);
+    }
+  });
+}
+
 app.listen(3001, () => {
   console.log('Regional Flight Server listening on port 3001...');
 });

filename: fixtures/flight-esm/src/index.js
patch: @@ -4,6 +4,15 @@ import ReactDOM from 'react-dom/client';
 import {createFromFetch, encodeReply} from 'react-server-dom-esm/client';
 
 const moduleBaseURL = '/src/';
+
+function findSourceMapURL(fileName) {
+  return (
+    document.location.origin +
+    '/source-maps?name=' +
+    encodeURIComponent(fileName)
+  );
+}
+
 let updateRoot;
 async function callServer(id, args) {
   const response = fetch('/', {
@@ -17,6 +26,7 @@ async function callServer(id, args) {
   const {returnValue, root} = await createFromFetch(response, {
     callServer,
     moduleBaseURL,
+    findSourceMapURL,
   });
   // Refresh the tree with the new RSC payload.
   startTransition(() => {
@@ -34,6 +44,7 @@ let data = createFromFetch(
   {
     callServer,
     moduleBaseURL,
+    findSourceMapURL,
   }
 );
 

filename: fixtures/flight/src/index.js
patch: @@ -6,6 +6,14 @@ import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
 // TODO: This should be a dependency of the App but we haven't implemented CSS in Node yet.
 import './style.css';
 
+function findSourceMapURL(fileName) {
+  return (
+    document.location.origin +
+    '/source-maps?name=' +
+    encodeURIComponent(fileName)
+  );
+}
+
 let updateRoot;
 async function callServer(id, args) {
   const response = fetch('/', {
@@ -16,7 +24,10 @@ async function callServer(id, args) {
     },
     body: await encodeReply(args),
   });
-  const {returnValue, root} = await createFromFetch(response, {callServer});
+  const {returnValue, root} = await createFromFetch(response, {
+    callServer,
+    findSourceMapURL,
+  });
   // Refresh the tree with the new RSC payload.
   startTransition(() => {
     updateRoot(root);
@@ -39,13 +50,7 @@ async function hydrateApp() {
     }),
     {
       callServer,
-      findSourceMapURL(fileName) {
-        return (
-          document.location.origin +
-          '/source-maps?name=' +
-          encodeURIComponent(fileName)
-        );
-      },
+      findSourceMapURL,
     }
   );