Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions packages/react-devtools-shared/src/__tests__/utils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
formatWithStyles,
gt,
gte,
parseSourceFromComponentStack,
} from 'react-devtools-shared/src/backend/utils';
import {extractLocationFromComponentStack} from 'react-devtools-shared/src/backend/utils/parseStackTrace';
import {
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
REACT_STRICT_MODE_TYPE as StrictMode,
Expand Down Expand Up @@ -306,20 +306,20 @@ describe('utils', () => {
});
});

describe('parseSourceFromComponentStack', () => {
describe('extractLocationFromComponentStack', () => {
it('should return null if passed empty string', () => {
expect(parseSourceFromComponentStack('')).toEqual(null);
expect(extractLocationFromComponentStack('')).toEqual(null);
});

it('should construct the source from the first frame if available', () => {
expect(
parseSourceFromComponentStack(
extractLocationFromComponentStack(
'at l (https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js:1:10389)\n' +
'at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)\n' +
'at r (https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:498)\n',
),
).toEqual([
'',
'l',
'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js',
1,
10389,
Expand All @@ -328,7 +328,7 @@ describe('utils', () => {

it('should construct the source from highest available frame', () => {
expect(
parseSourceFromComponentStack(
extractLocationFromComponentStack(
' at Q\n' +
' at a\n' +
' at m (https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236)\n' +
Expand All @@ -342,7 +342,7 @@ describe('utils', () => {
' at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)',
),
).toEqual([
'',
'm',
'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
5,
9236,
Expand All @@ -351,7 +351,7 @@ describe('utils', () => {

it('should construct the source from frame, which has only url specified', () => {
expect(
parseSourceFromComponentStack(
extractLocationFromComponentStack(
' at Q\n' +
' at a\n' +
' at https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236\n',
Expand All @@ -366,13 +366,13 @@ describe('utils', () => {

it('should parse sourceURL correctly if it includes parentheses', () => {
expect(
parseSourceFromComponentStack(
extractLocationFromComponentStack(
'at HotReload (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:307:11)\n' +
' at Router (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js:181:11)\n' +
' at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:114:9)',
),
).toEqual([
'',
'HotReload',
'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js',
307,
11,
Expand All @@ -381,13 +381,13 @@ describe('utils', () => {

it('should support Firefox stack', () => {
expect(
parseSourceFromComponentStack(
extractLocationFromComponentStack(
'tt@https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js:1:165558\n' +
'f@https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8535\n' +
'r@https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:513',
),
).toEqual([
'',
'tt',
'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js',
1,
165558,
Expand Down
12 changes: 7 additions & 5 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@ import {
formatDurationToMicrosecondsGranularity,
gt,
gte,
parseSourceFromComponentStack,
parseSourceFromOwnerStack,
serializeToString,
} from 'react-devtools-shared/src/backend/utils';
import {
extractLocationFromComponentStack,
extractLocationFromOwnerStack,
} from 'react-devtools-shared/src/backend/utils/parseStackTrace';
import {
cleanForBridge,
copyWithDelete,
Expand Down Expand Up @@ -6340,7 +6342,7 @@ export function attach(
if (stackFrame === null) {
return null;
}
const source = parseSourceFromComponentStack(stackFrame);
const source = extractLocationFromComponentStack(stackFrame);
fiberInstance.source = source;
return source;
}
Expand Down Expand Up @@ -6369,15 +6371,15 @@ export function attach(
// any intermediate utility functions. This won't point to the top of the component function
// but it's at least somewhere within it.
if (isError(unresolvedSource)) {
return (instance.source = parseSourceFromOwnerStack(
return (instance.source = extractLocationFromOwnerStack(
(unresolvedSource: any),
));
}
if (typeof unresolvedSource === 'string') {
const idx = unresolvedSource.lastIndexOf('\n');
const lastLine =
idx === -1 ? unresolvedSource : unresolvedSource.slice(idx + 1);
return (instance.source = parseSourceFromComponentStack(lastLine));
return (instance.source = extractLocationFromComponentStack(lastLine));
}

// $FlowFixMe: refined.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@ export function formatOwnerStack(error: Error): string {
const prevPrepareStackTrace = Error.prepareStackTrace;
// $FlowFixMe[incompatible-type] It does accept undefined.
Error.prepareStackTrace = undefined;
const stack = error.stack;
let stack = error.stack;
Error.prepareStackTrace = prevPrepareStackTrace;
return formatOwnerStackString(stack);
}

export function formatOwnerStackString(stack: string): string {
if (stack.startsWith('Error: react-stack-top-frame\n')) {
// V8's default formatting prefixes with the error message which we
// don't want/need.
Expand Down
183 changes: 0 additions & 183 deletions packages/react-devtools-shared/src/backend/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,11 @@ import {compareVersions} from 'compare-versions';
import {dehydrate} from 'react-devtools-shared/src/hydration';
import isArray from 'shared/isArray';

import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {DehydratedData} from 'react-devtools-shared/src/frontend/types';

export {default as formatWithStyles} from './formatWithStyles';
export {default as formatConsoleArguments} from './formatConsoleArguments';

import {formatOwnerStackString} from '../shared/DevToolsOwnerStack';

// TODO: update this to the first React version that has a corresponding DevTools backend
const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9';
export function hasAssignedBackend(version?: string): boolean {
Expand Down Expand Up @@ -258,186 +255,6 @@ export const isReactNativeEnvironment = (): boolean => {
return window.document == null;
};

function extractLocation(url: string): null | {
functionName?: string,
sourceURL: string,
line?: string,
column?: string,
} {
if (url.indexOf(':') === -1) {
return null;
}

// remove any parentheses from start and end
const withoutParentheses = url.replace(/^\(+/, '').replace(/\)+$/, '');
const locationParts = /(at )?(.+?)(?::(\d+))?(?::(\d+))?$/.exec(
withoutParentheses,
);

if (locationParts == null) {
return null;
}

const functionName = ''; // TODO: Parse this in the regexp.
const [, , sourceURL, line, column] = locationParts;
return {functionName, sourceURL, line, column};
}

const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
function parseSourceFromChromeStack(
stack: string,
): ReactFunctionLocation | null {
const frames = stack.split('\n');
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const frame of frames) {
const sanitizedFrame = frame.trim();

const locationInParenthesesMatch = sanitizedFrame.match(/ (\(.+\)$)/);
const possibleLocation = locationInParenthesesMatch
? locationInParenthesesMatch[1]
: sanitizedFrame;

const location = extractLocation(possibleLocation);
// Continue the search until at least sourceURL is found
if (location == null) {
continue;
}

const {functionName, sourceURL, line = '1', column = '1'} = location;

return [
functionName || '',
sourceURL,
parseInt(line, 10),
parseInt(column, 10),
];
}

return null;
}

function parseSourceFromFirefoxStack(
stack: string,
): ReactFunctionLocation | null {
const frames = stack.split('\n');
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const frame of frames) {
const sanitizedFrame = frame.trim();
const frameWithoutFunctionName = sanitizedFrame.replace(
/((.*".+"[^@]*)?[^@]*)(?:@)/,
'',
);

const location = extractLocation(frameWithoutFunctionName);
// Continue the search until at least sourceURL is found
if (location == null) {
continue;
}

const {functionName, sourceURL, line = '1', column = '1'} = location;

return [
functionName || '',
sourceURL,
parseInt(line, 10),
parseInt(column, 10),
];
}

return null;
}

export function parseSourceFromComponentStack(
componentStack: string,
): ReactFunctionLocation | null {
if (componentStack.match(CHROME_STACK_REGEXP)) {
return parseSourceFromChromeStack(componentStack);
}

return parseSourceFromFirefoxStack(componentStack);
}

let collectedLocation: ReactFunctionLocation | null = null;

function collectStackTrace(
error: Error,
structuredStackTrace: CallSite[],
): string {
let result: null | ReactFunctionLocation = null;
// Collect structured stack traces from the callsites.
// We mirror how V8 serializes stack frames and how we later parse them.
for (let i = 0; i < structuredStackTrace.length; i++) {
const callSite = structuredStackTrace[i];
const name = callSite.getFunctionName();
if (
name != null &&
(name.includes('react_stack_bottom_frame') ||
name.includes('react-stack-bottom-frame'))
) {
// We pick the last frame that matches before the bottom frame since
// that will be immediately inside the component as opposed to some helper.
// If we don't find a bottom frame then we bail to string parsing.
collectedLocation = result;
// Skip everything after the bottom frame since it'll be internals.
break;
} else {
const sourceURL = callSite.getScriptNameOrSourceURL();
const line =
// $FlowFixMe[prop-missing]
typeof callSite.getEnclosingLineNumber === 'function'
? (callSite: any).getEnclosingLineNumber()
: callSite.getLineNumber();
const col =
// $FlowFixMe[prop-missing]
typeof callSite.getEnclosingColumnNumber === 'function'
? (callSite: any).getEnclosingColumnNumber()
: callSite.getColumnNumber();
if (!sourceURL || !line || !col) {
// Skip eval etc. without source url. They don't have location.
continue;
}
result = [name, sourceURL, line, col];
}
}
// At the same time we generate a string stack trace just in case someone
// else reads it.
const name = error.name || 'Error';
const message = error.message || '';
let stack = name + ': ' + message;
for (let i = 0; i < structuredStackTrace.length; i++) {
stack += '\n at ' + structuredStackTrace[i].toString();
}
return stack;
}

export function parseSourceFromOwnerStack(
error: Error,
): ReactFunctionLocation | null {
// First attempt to collected the structured data using prepareStackTrace.
collectedLocation = null;
const previousPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = collectStackTrace;
let stack;
try {
stack = error.stack;
} catch (e) {
// $FlowFixMe[incompatible-type] It does accept undefined.
Error.prepareStackTrace = undefined;
stack = error.stack;
} finally {
Error.prepareStackTrace = previousPrepare;
}
if (collectedLocation !== null) {
return collectedLocation;
}
if (stack == null) {
return null;
}
// Fallback to parsing the string form.
const componentStack = formatOwnerStackString(stack);
return parseSourceFromComponentStack(componentStack);
}

// 0.123456789 => 0.123
// Expects high-resolution timestamp in milliseconds, like from performance.now()
// Mainly used for optimizing the size of serialized profiling payload
Expand Down
Loading
Loading