[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://`
This commit is contained in:
Sebastian Markbåge
2024-06-07 11:54:14 -04:00
committed by GitHub
parent 142b2a8230
commit cc1ec60d0d
4 changed files with 87 additions and 23 deletions

View File

@@ -7,6 +7,7 @@ const ReactFlightWebpackPlugin = require('react-server-dom-webpack/plugin');
const fs = require('fs');
const {createHash} = require('crypto');
const path = require('path');
const {pathToFileURL} = require('url');
const webpack = require('webpack');
const resolve = require('resolve');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
@@ -235,7 +236,7 @@ module.exports = function (webpackEnv) {
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
(info => pathToFileURL(path.resolve(info.absoluteResourcePath))),
},
cache: {
type: 'filesystem',

View File

@@ -3,6 +3,7 @@
// This is a server to host data-local resources like databases and RSC
const path = require('path');
const url = require('url');
const register = require('react-server-dom-webpack/node-register');
register();
@@ -192,7 +193,7 @@ if (process.env.NODE_ENV === 'development') {
// 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 = requestedFilePath.slice(7);
requestedFilePath = url.fileURLToPath(requestedFilePath);
}
const relativePath = path.relative(rootDir, requestedFilePath);
@@ -206,24 +207,41 @@ if (process.env.NODE_ENV === 'development') {
const sourceMap = nodeModule.findSourceMap(requestedFilePath);
let map;
// There are two ways to return a source map depending on what we observe in error.stack.
// A real app will have a similar choice to make for which strategy to pick.
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.
// Similarly
const sourceContent = await readFile(requestedFilePath, 'utf8');
const lines = sourceContent.split('\n').length;
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,
sources: [requestedFilePath],
// 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

View File

@@ -40,7 +40,11 @@ async function hydrateApp() {
{
callServer,
findSourceMapURL(fileName) {
return '/source-maps?name=' + encodeURIComponent(fileName);
return (
document.location.origin +
'/source-maps?name=' +
encodeURIComponent(fileName)
);
},
}
);

View File

@@ -1586,12 +1586,36 @@ function resolveErrorDev(
'resolveErrorDev should never be called in production mode. Use resolveErrorProd instead. This is a bug in React.',
);
}
// eslint-disable-next-line react-internal/prod-error-codes
const error = new Error(
message ||
'An error occurred in the Server Components render but no message was provided',
);
error.stack = stack;
let error;
if (!enableOwnerStacks) {
// Executing Error within a native stack isn't really limited to owner stacks
// but we gate it behind the same flag for now while iterating.
// eslint-disable-next-line react-internal/prod-error-codes
error = Error(
message ||
'An error occurred in the Server Components render but no message was provided',
);
error.stack = stack;
} else {
const callStack = buildFakeCallStack(
response,
stack,
// $FlowFixMe[incompatible-use]
Error.bind(
null,
message ||
'An error occurred in the Server Components render but no message was provided',
),
);
const rootTask = response._debugRootTask;
if (rootTask != null) {
error = rootTask.run(callStack);
} else {
error = callStack();
}
}
(error: any).digest = digest;
const errorWithDigest: ErrorWithDigest = (error: any);
const chunks = response._chunks;
@@ -1677,6 +1701,7 @@ const fakeFunctionCache: Map<string, FakeFunction<any>> = __DEV__
? new Map()
: (null: any);
let fakeFunctionIdx = 0;
function createFakeFunction<T>(
name: string,
filename: string,
@@ -1695,20 +1720,36 @@ function createFakeFunction<T>(
// point to the original source.
let code;
if (line <= 1) {
code = '_=>' + ' '.repeat(col < 4 ? 0 : col - 4) + '_()\n' + comment + '\n';
code = '_=>' + ' '.repeat(col < 4 ? 0 : col - 4) + '_()\n' + comment;
} else {
code =
comment +
'\n'.repeat(line - 2) +
'_=>\n' +
' '.repeat(col < 1 ? 0 : col - 1) +
'_()\n';
'_()';
}
if (filename.startsWith('/')) {
// If the filename starts with `/` we assume that it is a file system file
// rather than relative to the current host. Since on the server fully qualified
// stack traces use the file path.
// TODO: What does this look like on Windows?
filename = 'file://' + filename;
}
if (sourceMap) {
code += '//# sourceMappingURL=' + sourceMap;
// We use the prefix rsc://React/ to separate these from other files listed in
// the Chrome DevTools. We need a "host name" and not just a protocol because
// otherwise the group name becomes the root folder. Ideally we don't want to
// show these at all but there's two reasons to assign a fake URL.
// 1) A printed stack trace string needs a unique URL to be able to source map it.
// 2) If source maps are disabled or fails, you should at least be able to tell
// which file it was.
code += '\n//# sourceURL=rsc://React/' + filename + '?' + fakeFunctionIdx++;
code += '\n//# sourceMappingURL=' + sourceMap;
} else if (filename) {
code += '//# sourceURL=' + filename;
code += '\n//# sourceURL=' + filename;
}
let fn: FakeFunction<T>;