mirror of
https://github.com/facebook/react.git
synced 2026-02-22 03:42:05 +00:00
[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:
committed by
GitHub
parent
142b2a8230
commit
cc1ec60d0d
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
61
packages/react-client/src/ReactFlightClient.js
vendored
61
packages/react-client/src/ReactFlightClient.js
vendored
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user