@samitouri / QOS-React-2 / commits / cc1ec60d0d

[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>;