[Flight] Run recreated Errors within a fake native stack (#29717)
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://`
Sebastian Markbåge committed
Jun 7, 2024 at 11:54 UTC
cc1ec60d0de3be60948fc152b2377a42504f551a
4 files changed
+83
-19
fixtures/flight/config/webpack.config.js
+2
-1
@@ -7,6 +7,7 @@ const ReactFlightWebpackPlugin = require('react-server-dom-webpack/plugin');
7
const fs = require('fs');
8
const {createHash} = require('crypto');
9
const path = require('path');
10
+const {pathToFileURL} = require('url');
11
const webpack = require('webpack');
12
const resolve = require('resolve');
13
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
@@ -235,7 +236,7 @@ module.exports = function (webpackEnv) {
236
.relative(paths.appSrc, info.absoluteResourcePath)
237
.replace(/\\/g, '/')
238
: isEnvDevelopment &&
238
- (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
239
+ (info => pathToFileURL(path.resolve(info.absoluteResourcePath))),
240
},
241
cache: {
242
type: 'filesystem',
fixtures/flight/server/region.js
+25
-7
@@ -3,6 +3,7 @@
3
// This is a server to host data-local resources like databases and RSC
4
5
const path = require('path');
6
+const url = require('url');
7
8
const register = require('react-server-dom-webpack/node-register');
9
register();
@@ -192,7 +193,7 @@ if (process.env.NODE_ENV === 'development') {
193
// We assume that if it was prefixed with file:// it's referring to the compiled output
194
// and if it's a direct file path we assume it's source mapped back to original format.
195
isCompiledOutput = true;
195
- requestedFilePath = requestedFilePath.slice(7);
196
+ requestedFilePath = url.fileURLToPath(requestedFilePath);
197
}
198
199
const relativePath = path.relative(rootDir, requestedFilePath);
@@ -206,24 +207,41 @@ if (process.env.NODE_ENV === 'development') {
207
208
const sourceMap = nodeModule.findSourceMap(requestedFilePath);
209
let map;
209
- // There are two ways to return a source map depending on what we observe in error.stack.
210
- // A real app will have a similar choice to make for which strategy to pick.
211
- if (!sourceMap || !isCompiledOutput) {
210
+ if (requestedFilePath.startsWith('node:')) {
211
+ // This is a node internal. We don't include any source code for this but we still
212
+ // generate a source map for it so that we can add it to an ignoreList automatically.
213
+ map = {
214
+ version: 3,
215
+ // We use the node:// protocol convention to teach Chrome DevTools that this is
216
+ // on a different protocol and not part of the current page.
217
+ sources: ['node:///' + requestedFilePath.slice(5)],
218
+ sourcesContent: ['// Node Internals'],
219
+ mappings: 'AAAA',
220
+ ignoreList: [0],
221
+ sourceRoot: '',
222
+ };
223
+ } else if (!sourceMap || !isCompiledOutput) {
224
// If a file doesn't have a source map, such as this file, then we generate a blank
225
// source map that just contains the original content and segments pointing to the
214
- // original lines.
215
- // Similarly
226
+ // original lines. If a line number points to uncompiled output, like if source mapping
227
+ // was already applied we also use this path.
228
const sourceContent = await readFile(requestedFilePath, 'utf8');
229
const lines = sourceContent.split('\n').length;
230
+ // We ensure to absolute
231
+ const sourceURL = url.pathToFileURL(requestedFilePath);
232
map = {
233
version: 3,
220
- sources: [requestedFilePath],
234
+ sources: [sourceURL],
235
sourcesContent: [sourceContent],
236
// Note: This approach to mapping each line only lets you jump to each line
237
// not jump to a column within a line. To do that, you need a proper source map
238
// generated for each parsed segment or add a segment for each column.
239
mappings: 'AAAA' + ';AACA'.repeat(lines - 1),
240
sourceRoot: '',
241
+ // Add any node_modules to the ignore list automatically.
242
+ ignoreList: requestedFilePath.includes('node_modules')
243
+ ? [0]
244
+ : undefined,
245
};
246
} else {
247
// We always set prepareStackTrace before reading the stack so that we get the stack
fixtures/flight/src/index.js
+5
-1
@@ -40,7 +40,11 @@ async function hydrateApp() {
40
{
41
callServer,
42
findSourceMapURL(fileName) {
43
- return '/source-maps?name=' + encodeURIComponent(fileName);
43
+ return (
44
+ document.location.origin +
45
+ '/source-maps?name=' +
46
+ encodeURIComponent(fileName)
47
+ );
48
},
49
}
50
);
packages/react-client/src/ReactFlightClient.js
+51
-10
@@ -1586,12 +1586,36 @@ function resolveErrorDev(
1586
'resolveErrorDev should never be called in production mode. Use resolveErrorProd instead. This is a bug in React.',
1587
);
1588
}
1589
- // eslint-disable-next-line react-internal/prod-error-codes
1590
- const error = new Error(
1591
- message ||
1592
- 'An error occurred in the Server Components render but no message was provided',
1593
- );
1594
- error.stack = stack;
1589
+
1590
+ let error;
1591
+ if (!enableOwnerStacks) {
1592
+ // Executing Error within a native stack isn't really limited to owner stacks
1593
+ // but we gate it behind the same flag for now while iterating.
1594
+ // eslint-disable-next-line react-internal/prod-error-codes
1595
+ error = Error(
1596
+ message ||
1597
+ 'An error occurred in the Server Components render but no message was provided',
1598
+ );
1599
+ error.stack = stack;
1600
+ } else {
1601
+ const callStack = buildFakeCallStack(
1602
+ response,
1603
+ stack,
1604
+ // $FlowFixMe[incompatible-use]
1605
+ Error.bind(
1606
+ null,
1607
+ message ||
1608
+ 'An error occurred in the Server Components render but no message was provided',
1609
+ ),
1610
+ );
1611
+ const rootTask = response._debugRootTask;
1612
+ if (rootTask != null) {
1613
+ error = rootTask.run(callStack);
1614
+ } else {
1615
+ error = callStack();
1616
+ }
1617
+ }
1618
+
1619
(error: any).digest = digest;
1620
const errorWithDigest: ErrorWithDigest = (error: any);
1621
const chunks = response._chunks;
@@ -1677,6 +1701,7 @@ const fakeFunctionCache: Map<string, FakeFunction<any>> = __DEV__
1701
? new Map()
1702
: (null: any);
1703
1704
+let fakeFunctionIdx = 0;
1705
function createFakeFunction<T>(
1706
name: string,
1707
filename: string,
@@ -1695,20 +1720,36 @@ function createFakeFunction<T>(
1720
// point to the original source.
1721
let code;
1722
if (line <= 1) {
1698
- code = '_=>' + ' '.repeat(col < 4 ? 0 : col - 4) + '_()\n' + comment + '\n';
1723
+ code = '_=>' + ' '.repeat(col < 4 ? 0 : col - 4) + '_()\n' + comment;
1724
} else {
1725
code =
1726
comment +
1727
'\n'.repeat(line - 2) +
1728
'_=>\n' +
1729
' '.repeat(col < 1 ? 0 : col - 1) +
1705
- '_()\n';
1730
+ '_()';
1731
+ }
1732
+
1733
+ if (filename.startsWith('/')) {
1734
+ // If the filename starts with `/` we assume that it is a file system file
1735
+ // rather than relative to the current host. Since on the server fully qualified
1736
+ // stack traces use the file path.
1737
+ // TODO: What does this look like on Windows?
1738
+ filename = 'file://' + filename;
1739
}
1740
1741
if (sourceMap) {
1709
- code += '//# sourceMappingURL=' + sourceMap;
1742
+ // We use the prefix rsc://React/ to separate these from other files listed in
1743
+ // the Chrome DevTools. We need a "host name" and not just a protocol because
1744
+ // otherwise the group name becomes the root folder. Ideally we don't want to
1745
+ // show these at all but there's two reasons to assign a fake URL.
1746
+ // 1) A printed stack trace string needs a unique URL to be able to source map it.
1747
+ // 2) If source maps are disabled or fails, you should at least be able to tell
1748
+ // which file it was.
1749
+ code += '\n//# sourceURL=rsc://React/' + filename + '?' + fakeFunctionIdx++;
1750
+ code += '\n//# sourceMappingURL=' + sourceMap;
1751
} else if (filename) {
1711
- code += '//# sourceURL=' + filename;
1752
+ code += '\n//# sourceURL=' + filename;
1753
}
1754
1755
let fn: FakeFunction<T>;