diff --git a/fixtures/flight-esm/server/global.js b/fixtures/flight-esm/server/global.js index 1088d42967a2f..c0b148fc063c4 100644 --- a/fixtures/flight-esm/server/global.js +++ b/fixtures/flight-esm/server/global.js @@ -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...'); }); diff --git a/fixtures/flight-esm/server/region.js b/fixtures/flight-esm/server/region.js index c7e8d9aad33cc..fe992b6daf538 100644 --- a/fixtures/flight-esm/server/region.js +++ b/fixtures/flight-esm/server/region.js @@ -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...'); }); diff --git a/fixtures/flight-esm/src/index.js b/fixtures/flight-esm/src/index.js index 30d060af6342c..6cef6c6c3c547 100644 --- a/fixtures/flight-esm/src/index.js +++ b/fixtures/flight-esm/src/index.js @@ -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, } ); diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js index b4b538a3a9992..755551047535b 100644 --- a/fixtures/flight/src/index.js +++ b/fixtures/flight/src/index.js @@ -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, } );