[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