@samitouri / QOS-React / commits / c0b76a6831

[Flight] Allow parens in filenames when parsing stackframes (#30396)

Sebastian Silbermann committed Jul 24, 2024 at 20:02 UTC c0b76a68318687bf5260d623c7b70a8156be21d9
2 files changed +152 -1
packages/react-client/src/__tests__/ReactFlight-test.js
+151
@@ -10,6 +10,8 @@
10
11 'use strict';
12
13 +const path = require('path');
14 +
15 if (typeof Blob === 'undefined') {
16 global.Blob = require('buffer').Blob;
17 }
@@ -41,6 +43,26 @@ function formatV8Stack(stack) {
43 return v8StyleStack;
44 }
45
46 +const repoRoot = path.resolve(__dirname, '../../../../');
47 +function normalizeReactCodeLocInfo(str) {
48 + const repoRootForRegexp = repoRoot.replace(/\//g, '\\/');
49 + const repoFileLocMatch = new RegExp(`${repoRootForRegexp}.+?:\\d+:\\d+`, 'g');
50 + return str && str.replace(repoFileLocMatch, '**');
51 +}
52 +
53 +// If we just use the original Error prototype, Jest will only display the error message if assertions fail.
54 +// But we usually want to also assert on our expando properties or even the stack.
55 +// By hiding the fact from Jest that this is an error, it will show all enumerable properties on mismatch.
56 +
57 +function getErrorForJestMatcher(error) {
58 + return {
59 + ...error,
60 + // non-enumerable properties that are still relevant for testing
61 + message: error.message,
62 + stack: normalizeReactCodeLocInfo(error.stack),
63 + };
64 +}
65 +
66 function normalizeComponentInfo(debugInfo) {
67 if (Array.isArray(debugInfo.stack)) {
68 const {debugTask, debugStack, ...copy} = debugInfo;
@@ -1177,6 +1199,135 @@ describe('ReactFlight', () => {
1199 });
1200 });
1201
1202 + it('should handle exotic stack frames', async () => {
1203 + function ServerComponent() {
1204 + const error = new Error('This is an error');
1205 + const originalStackLines = error.stack.split('\n');
1206 + // Fake a stack
1207 + error.stack = [
1208 + originalStackLines[0],
1209 + // original
1210 + // ' at ServerComponentError (file://~/react/packages/react-client/src/__tests__/ReactFlight-test.js:1166:19)',
1211 + // nested eval (https://github.com/ChromeDevTools/devtools-frontend/blob/831be28facb4e85de5ee8c1acc4d98dfeda7a73b/test/unittests/front_end/panels/console/ErrorStackParser_test.ts#L198)
1212 + ' at eval (eval at testFunction (inspected-page.html:29:11), <anonymous>:1:10)',
1213 + // parens may be added by Webpack when bundle layers are used. They're also valid in directory names.
1214 + ' at ServerComponentError (file://~/(some)(really)(exotic-directory)/ReactFlight-test.js:1166:19)',
1215 + // anon function (https://github.com/ChromeDevTools/devtools-frontend/blob/831be28facb4e85de5ee8c1acc4d98dfeda7a73b/test/unittests/front_end/panels/console/ErrorStackParser_test.ts#L115C9-L115C35)
1216 + ' at file:///testing.js:42:3',
1217 + // async anon function (https://github.com/ChromeDevTools/devtools-frontend/blob/831be28facb4e85de5ee8c1acc4d98dfeda7a73b/test/unittests/front_end/panels/console/ErrorStackParser_test.ts#L130C9-L130C41)
1218 + ' at async file:///testing.js:42:3',
1219 + ...originalStackLines.slice(2),
1220 + ].join('\n');
1221 + throw error;
1222 + }
1223 +
1224 + const findSourceMapURL = jest.fn(() => null);
1225 + const errors = [];
1226 + class MyErrorBoundary extends React.Component {
1227 + state = {error: null};
1228 + static getDerivedStateFromError(error) {
1229 + return {error};
1230 + }
1231 + componentDidCatch(error, componentInfo) {
1232 + errors.push(error);
1233 + }
1234 + render() {
1235 + if (this.state.error) {
1236 + return null;
1237 + }
1238 + return this.props.children;
1239 + }
1240 + }
1241 + const ClientErrorBoundary = clientReference(MyErrorBoundary);
1242 +
1243 + function App() {
1244 + return (
1245 + <ClientErrorBoundary>
1246 + <ServerComponent />
1247 + </ClientErrorBoundary>
1248 + );
1249 + }
1250 +
1251 + const transport = ReactNoopFlightServer.render(<App />, {
1252 + onError(x) {
1253 + if (__DEV__) {
1254 + return 'a dev digest';
1255 + }
1256 + if (x instanceof Error) {
1257 + return `digest("${x.message}")`;
1258 + } else if (Array.isArray(x)) {
1259 + return `digest([])`;
1260 + } else if (typeof x === 'object' && x !== null) {
1261 + return `digest({})`;
1262 + }
1263 + return `digest(${String(x)})`;
1264 + },
1265 + });
1266 +
1267 + await act(() => {
1268 + startTransition(() => {
1269 + ReactNoop.render(
1270 + ReactNoopFlightClient.read(transport, {findSourceMapURL}),
1271 + );
1272 + });
1273 + });
1274 +
1275 + if (__DEV__) {
1276 + expect({
1277 + errors: errors.map(getErrorForJestMatcher),
1278 + findSourceMapURLCalls: findSourceMapURL.mock.calls,
1279 + }).toEqual({
1280 + errors: [
1281 + {
1282 + message: 'This is an error',
1283 + stack: gate(flags => flags.enableOwnerStacks)
1284 + ? expect.stringContaining(
1285 + 'Error: This is an error\n' +
1286 + ' at eval (eval at testFunction (eval at createFakeFunction (**), <anonymous>:1:35)\n' +
1287 + ' at ServerComponentError (file://~/(some)(really)(exotic-directory)/ReactFlight-test.js:1166:19)\n' +
1288 + ' at (anonymous) (file:///testing.js:42:3)\n' +
1289 + ' at (anonymous) (file:///testing.js:42:3)\n',
1290 + )
1291 + : expect.stringContaining(
1292 + 'Error: This is an error\n' +
1293 + ' at eval (eval at testFunction (inspected-page.html:29:11), <anonymous>:1:10)\n' +
1294 + ' at ServerComponentError (file://~/(some)(really)(exotic-directory)/ReactFlight-test.js:1166:19)\n' +
1295 + ' at file:///testing.js:42:3\n' +
1296 + ' at file:///testing.js:42:3',
1297 + ),
1298 + digest: 'a dev digest',
1299 + environmentName: 'Server',
1300 + },
1301 + ],
1302 + findSourceMapURLCalls: gate(flags => flags.enableOwnerStacks)
1303 + ? [
1304 + [__filename],
1305 + [__filename],
1306 + // TODO: What should we request here? The outer (<anonymous>) or the inner (inspected-page.html)?
1307 + ['inspected-page.html:29:11), <anonymous>'],
1308 + ['file://~/(some)(really)(exotic-directory)/ReactFlight-test.js'],
1309 + ['file:///testing.js'],
1310 + [__filename],
1311 + ]
1312 + : [],
1313 + });
1314 + } else {
1315 + expect(errors.map(getErrorForJestMatcher)).toEqual([
1316 + {
1317 + message:
1318 + 'An error occurred in the Server Components render. The specific message is omitted in production' +
1319 + ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
1320 + ' may provide additional details about the nature of the error.',
1321 + stack:
1322 + 'Error: An error occurred in the Server Components render. The specific message is omitted in production' +
1323 + ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
1324 + ' may provide additional details about the nature of the error.',
1325 + digest: 'digest("This is an error")',
1326 + },
1327 + ]);
1328 + }
1329 + });
1330 +
1331 it('should include server components in warning stacks', async () => {
1332 function Component() {
1333 // Trigger key warning
packages/react-server/src/ReactFlightStackConfigV8.js
+1 -1
@@ -44,7 +44,7 @@ function getStack(error: Error): string {
44 // at filename:0:0
45 // at async filename:0:0
46 const frameRegExp =
47 - /^ {3} at (?:(.+) \(([^\)]+):(\d+):(\d+)\)|(?:async )?([^\)]+):(\d+):(\d+))$/;
47 + /^ {3} at (?:(.+) \((.+):(\d+):(\d+)\)|(?:async )?(.+):(\d+):(\d+))$/;
48
49 export function parseStackTrace(
50 error: Error,