@samitouri / QOS-React / commits / e483df4658

[Flight ESM] Wire up Source Maps in the flight-esm fixture (#30758)

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

Sebastian Markbåge committed Aug 22, 2024 at 12:34 UTC e483df4658473ca9c917a42be4869d445be00807
4 files changed +147 -8
fixtures/flight-esm/server/global.js
+39
@@ -131,6 +131,45 @@ app.use(
131 express.static('node_modules/react-server-dom-esm/esm')
132 );
133
134 +if (process.env.NODE_ENV === 'development') {
135 + app.get('/source-maps', async function (req, res, next) {
136 + // Proxy the request to the regional server.
137 + const proxiedHeaders = {
138 + 'X-Forwarded-Host': req.hostname,
139 + 'X-Forwarded-For': req.ips,
140 + 'X-Forwarded-Port': 3000,
141 + 'X-Forwarded-Proto': req.protocol,
142 + };
143 +
144 + const promiseForData = request(
145 + {
146 + host: '127.0.0.1',
147 + port: 3001,
148 + method: req.method,
149 + path: req.originalUrl,
150 + headers: proxiedHeaders,
151 + },
152 + req
153 + );
154 +
155 + try {
156 + const rscResponse = await promiseForData;
157 + res.set('Content-type', 'application/json');
158 + rscResponse.on('data', data => {
159 + res.write(data);
160 + res.flush();
161 + });
162 + rscResponse.on('end', data => {
163 + res.end();
164 + });
165 + } catch (e) {
166 + console.error(`Failed to proxy request: ${e.stack}`);
167 + res.statusCode = 500;
168 + res.end();
169 + }
170 + });
171 +}
172 +
173 app.listen(3000, () => {
174 console.log('Global Fizz/Webpack Server listening on port 3000...');
175 });
fixtures/flight-esm/server/region.js
+84
@@ -17,6 +17,8 @@ const app = express();
17 const compress = require('compression');
18 const {Readable} = require('node:stream');
19
20 +const nodeModule = require('node:module');
21 +
22 app.use(compress());
23
24 // Application
@@ -116,6 +118,88 @@ app.get('/todos', function (req, res) {
118 ]);
119 });
120
121 +if (process.env.NODE_ENV === 'development') {
122 + const rootDir = path.resolve(__dirname, '../');
123 +
124 + app.get('/source-maps', async function (req, res, next) {
125 + try {
126 + res.set('Content-type', 'application/json');
127 + let requestedFilePath = req.query.name;
128 +
129 + let isCompiledOutput = false;
130 + if (requestedFilePath.startsWith('file://')) {
131 + // We assume that if it was prefixed with file:// it's referring to the compiled output
132 + // and if it's a direct file path we assume it's source mapped back to original format.
133 + isCompiledOutput = true;
134 + requestedFilePath = url.fileURLToPath(requestedFilePath);
135 + }
136 +
137 + const relativePath = path.relative(rootDir, requestedFilePath);
138 + if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
139 + // This is outside the root directory of the app. Forbid it to be served.
140 + res.status = 403;
141 + res.write('{}');
142 + res.end();
143 + return;
144 + }
145 +
146 + const sourceMap = nodeModule.findSourceMap(requestedFilePath);
147 + let map;
148 + if (requestedFilePath.startsWith('node:')) {
149 + // This is a node internal. We don't include any source code for this but we still
150 + // generate a source map for it so that we can add it to an ignoreList automatically.
151 + map = {
152 + version: 3,
153 + // We use the node:// protocol convention to teach Chrome DevTools that this is
154 + // on a different protocol and not part of the current page.
155 + sources: ['node:///' + requestedFilePath.slice(5)],
156 + sourcesContent: ['// Node Internals'],
157 + mappings: 'AAAA',
158 + ignoreList: [0],
159 + sourceRoot: '',
160 + };
161 + } else if (!sourceMap || !isCompiledOutput) {
162 + // If a file doesn't have a source map, such as this file, then we generate a blank
163 + // source map that just contains the original content and segments pointing to the
164 + // original lines. If a line number points to uncompiled output, like if source mapping
165 + // was already applied we also use this path.
166 + const sourceContent = await readFile(requestedFilePath, 'utf8');
167 + const lines = sourceContent.split('\n').length;
168 + // We ensure to absolute
169 + const sourceURL = url.pathToFileURL(requestedFilePath);
170 + map = {
171 + version: 3,
172 + sources: [sourceURL],
173 + sourcesContent: [sourceContent],
174 + // Note: This approach to mapping each line only lets you jump to each line
175 + // not jump to a column within a line. To do that, you need a proper source map
176 + // generated for each parsed segment or add a segment for each column.
177 + mappings: 'AAAA' + ';AACA'.repeat(lines - 1),
178 + sourceRoot: '',
179 + // Add any node_modules to the ignore list automatically.
180 + ignoreList: requestedFilePath.includes('node_modules')
181 + ? [0]
182 + : undefined,
183 + };
184 + } else {
185 + // We always set prepareStackTrace before reading the stack so that we get the stack
186 + // without source maps applied. Therefore we have to use the original source map.
187 + // If something read .stack before we did, we might observe the line/column after
188 + // source mapping back to the original file. We use the isCompiledOutput check above
189 + // in that case.
190 + map = sourceMap.payload;
191 + }
192 + res.write(JSON.stringify(map));
193 + res.end();
194 + } catch (x) {
195 + res.status = 500;
196 + res.write('{}');
197 + res.end();
198 + console.error(x);
199 + }
200 + });
201 +}
202 +
203 app.listen(3001, () => {
204 console.log('Regional Flight Server listening on port 3001...');
205 });
fixtures/flight-esm/src/index.js
+11
@@ -4,6 +4,15 @@ import ReactDOM from 'react-dom/client';
4 import {createFromFetch, encodeReply} from 'react-server-dom-esm/client';
5
6 const moduleBaseURL = '/src/';
7 +
8 +function findSourceMapURL(fileName) {
9 + return (
10 + document.location.origin +
11 + '/source-maps?name=' +
12 + encodeURIComponent(fileName)
13 + );
14 +}
15 +
16 let updateRoot;
17 async function callServer(id, args) {
18 const response = fetch('/', {
@@ -17,6 +26,7 @@ async function callServer(id, args) {
26 const {returnValue, root} = await createFromFetch(response, {
27 callServer,
28 moduleBaseURL,
29 + findSourceMapURL,
30 });
31 // Refresh the tree with the new RSC payload.
32 startTransition(() => {
@@ -34,6 +44,7 @@ let data = createFromFetch(
44 {
45 callServer,
46 moduleBaseURL,
47 + findSourceMapURL,
48 }
49 );
50
fixtures/flight/src/index.js
+13 -8
@@ -6,6 +6,14 @@ import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
6 // TODO: This should be a dependency of the App but we haven't implemented CSS in Node yet.
7 import './style.css';
8
9 +function findSourceMapURL(fileName) {
10 + return (
11 + document.location.origin +
12 + '/source-maps?name=' +
13 + encodeURIComponent(fileName)
14 + );
15 +}
16 +
17 let updateRoot;
18 async function callServer(id, args) {
19 const response = fetch('/', {
@@ -16,7 +24,10 @@ async function callServer(id, args) {
24 },
25 body: await encodeReply(args),
26 });
19 - const {returnValue, root} = await createFromFetch(response, {callServer});
27 + const {returnValue, root} = await createFromFetch(response, {
28 + callServer,
29 + findSourceMapURL,
30 + });
31 // Refresh the tree with the new RSC payload.
32 startTransition(() => {
33 updateRoot(root);
@@ -39,13 +50,7 @@ async function hydrateApp() {
50 }),
51 {
52 callServer,
42 - findSourceMapURL(fileName) {
43 - return (
44 - document.location.origin +
45 - '/source-maps?name=' +
46 - encodeURIComponent(fileName)
47 - );
48 - },
53 + findSourceMapURL,
54 }
55 );
56