mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
refactor[devtools]: lazily define source for fiber based on component stacks (#28351)
`_debugSource` was removed in https://github.com/facebook/react/pull/28265. This PR migrates DevTools to define `source` for Fiber based on component stacks. This will be done lazily for inspected elements, once user clicks on the element in the tree. `DevToolsComponentStackFrame.js` was just copy-pasted from the implementation in `ReactComponentStackFrame`. Symbolication part is done in https://github.com/facebook/react/pull/28471 and stacked on this commit.
This commit is contained in:
+139
-57
@@ -81,8 +81,6 @@ export function describeNativeComponentFrame(
|
||||
}
|
||||
}
|
||||
|
||||
let control;
|
||||
|
||||
const previousPrepareStackTrace = Error.prepareStackTrace;
|
||||
// $FlowFixMe[incompatible-type] It does accept undefined.
|
||||
Error.prepareStackTrace = undefined;
|
||||
@@ -98,64 +96,140 @@ export function describeNativeComponentFrame(
|
||||
currentDispatcherRef.current = null;
|
||||
disableLogs();
|
||||
|
||||
try {
|
||||
// This should throw.
|
||||
if (construct) {
|
||||
// Something should be setting the props in the constructor.
|
||||
const Fake = function () {
|
||||
throw Error();
|
||||
};
|
||||
// $FlowFixMe[prop-missing]
|
||||
Object.defineProperty(Fake.prototype, 'props', {
|
||||
set: function () {
|
||||
// We use a throwing setter instead of frozen or non-writable props
|
||||
// because that won't throw in a non-strict mode function.
|
||||
throw Error();
|
||||
},
|
||||
});
|
||||
if (typeof Reflect === 'object' && Reflect.construct) {
|
||||
// We construct a different control for this case to include any extra
|
||||
// frames added by the construct call.
|
||||
try {
|
||||
Reflect.construct(Fake, []);
|
||||
} catch (x) {
|
||||
control = x;
|
||||
}
|
||||
Reflect.construct(fn, [], Fake);
|
||||
} else {
|
||||
try {
|
||||
Fake.call();
|
||||
} catch (x) {
|
||||
control = x;
|
||||
}
|
||||
// $FlowFixMe[prop-missing] found when upgrading Flow
|
||||
fn.call(Fake.prototype);
|
||||
}
|
||||
} else {
|
||||
// NOTE: keep in sync with the implementation in ReactComponentStackFrame
|
||||
|
||||
/**
|
||||
* Finding a common stack frame between sample and control errors can be
|
||||
* tricky given the different types and levels of stack trace truncation from
|
||||
* different JS VMs. So instead we'll attempt to control what that common
|
||||
* frame should be through this object method:
|
||||
* Having both the sample and control errors be in the function under the
|
||||
* `DescribeNativeComponentFrameRoot` property, + setting the `name` and
|
||||
* `displayName` properties of the function ensures that a stack
|
||||
* frame exists that has the method name `DescribeNativeComponentFrameRoot` in
|
||||
* it for both control and sample stacks.
|
||||
*/
|
||||
const RunInRootFrame = {
|
||||
DetermineComponentFrameRoot(): [?string, ?string] {
|
||||
let control;
|
||||
try {
|
||||
throw Error();
|
||||
} catch (x) {
|
||||
control = x;
|
||||
// This should throw.
|
||||
if (construct) {
|
||||
// Something should be setting the props in the constructor.
|
||||
const Fake = function () {
|
||||
throw Error();
|
||||
};
|
||||
// $FlowFixMe[prop-missing]
|
||||
Object.defineProperty(Fake.prototype, 'props', {
|
||||
set: function () {
|
||||
// We use a throwing setter instead of frozen or non-writable props
|
||||
// because that won't throw in a non-strict mode function.
|
||||
throw Error();
|
||||
},
|
||||
});
|
||||
if (typeof Reflect === 'object' && Reflect.construct) {
|
||||
// We construct a different control for this case to include any extra
|
||||
// frames added by the construct call.
|
||||
try {
|
||||
Reflect.construct(Fake, []);
|
||||
} catch (x) {
|
||||
control = x;
|
||||
}
|
||||
Reflect.construct(fn, [], Fake);
|
||||
} else {
|
||||
try {
|
||||
Fake.call();
|
||||
} catch (x) {
|
||||
control = x;
|
||||
}
|
||||
// $FlowFixMe[prop-missing] found when upgrading Flow
|
||||
fn.call(Fake.prototype);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
throw Error();
|
||||
} catch (x) {
|
||||
control = x;
|
||||
}
|
||||
// TODO(luna): This will currently only throw if the function component
|
||||
// tries to access React/ReactDOM/props. We should probably make this throw
|
||||
// in simple components too
|
||||
const maybePromise = fn();
|
||||
|
||||
// If the function component returns a promise, it's likely an async
|
||||
// component, which we don't yet support. Attach a noop catch handler to
|
||||
// silence the error.
|
||||
// TODO: Implement component stacks for async client components?
|
||||
if (maybePromise && typeof maybePromise.catch === 'function') {
|
||||
maybePromise.catch(() => {});
|
||||
}
|
||||
}
|
||||
} catch (sample) {
|
||||
// This is inlined manually because closure doesn't do it for us.
|
||||
if (sample && control && typeof sample.stack === 'string') {
|
||||
return [sample.stack, control.stack];
|
||||
}
|
||||
}
|
||||
fn();
|
||||
}
|
||||
} catch (sample) {
|
||||
// This is inlined manually because closure doesn't do it for us.
|
||||
if (sample && control && typeof sample.stack === 'string') {
|
||||
return [null, null];
|
||||
},
|
||||
};
|
||||
// $FlowFixMe[prop-missing]
|
||||
RunInRootFrame.DetermineComponentFrameRoot.displayName =
|
||||
'DetermineComponentFrameRoot';
|
||||
const namePropDescriptor = Object.getOwnPropertyDescriptor(
|
||||
RunInRootFrame.DetermineComponentFrameRoot,
|
||||
'name',
|
||||
);
|
||||
// Before ES6, the `name` property was not configurable.
|
||||
if (namePropDescriptor && namePropDescriptor.configurable) {
|
||||
// V8 utilizes a function's `name` property when generating a stack trace.
|
||||
Object.defineProperty(
|
||||
RunInRootFrame.DetermineComponentFrameRoot,
|
||||
// Configurable properties can be updated even if its writable descriptor
|
||||
// is set to `false`.
|
||||
// $FlowFixMe[cannot-write]
|
||||
'name',
|
||||
{value: 'DetermineComponentFrameRoot'},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const [sampleStack, controlStack] =
|
||||
RunInRootFrame.DetermineComponentFrameRoot();
|
||||
if (sampleStack && controlStack) {
|
||||
// This extracts the first frame from the sample that isn't also in the control.
|
||||
// Skipping one frame that we assume is the frame that calls the two.
|
||||
const sampleLines = sample.stack.split('\n');
|
||||
const controlLines = control.stack.split('\n');
|
||||
let s = sampleLines.length - 1;
|
||||
let c = controlLines.length - 1;
|
||||
while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {
|
||||
// We expect at least one stack frame to be shared.
|
||||
// Typically this will be the root most one. However, stack frames may be
|
||||
// cut off due to maximum stack limits. In this case, one maybe cut off
|
||||
// earlier than the other. We assume that the sample is longer or the same
|
||||
// and there for cut off earlier. So we should find the root most frame in
|
||||
// the sample somewhere in the control.
|
||||
c--;
|
||||
const sampleLines = sampleStack.split('\n');
|
||||
const controlLines = controlStack.split('\n');
|
||||
let s = 0;
|
||||
let c = 0;
|
||||
while (
|
||||
s < sampleLines.length &&
|
||||
!sampleLines[s].includes('DetermineComponentFrameRoot')
|
||||
) {
|
||||
s++;
|
||||
}
|
||||
while (
|
||||
c < controlLines.length &&
|
||||
!controlLines[c].includes('DetermineComponentFrameRoot')
|
||||
) {
|
||||
c++;
|
||||
}
|
||||
// We couldn't find our intentionally injected common root frame, attempt
|
||||
// to find another common root frame by search from the bottom of the
|
||||
// control stack...
|
||||
if (s === sampleLines.length || c === controlLines.length) {
|
||||
s = sampleLines.length - 1;
|
||||
c = controlLines.length - 1;
|
||||
while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {
|
||||
// We expect at least one stack frame to be shared.
|
||||
// Typically this will be the root most one. However, stack frames may be
|
||||
// cut off due to maximum stack limits. In this case, one maybe cut off
|
||||
// earlier than the other. We assume that the sample is longer or the same
|
||||
// and there for cut off earlier. So we should find the root most frame in
|
||||
// the sample somewhere in the control.
|
||||
c--;
|
||||
}
|
||||
}
|
||||
for (; s >= 1 && c >= 0; s--, c--) {
|
||||
// Next we find the first one that isn't the same which should be the
|
||||
@@ -174,7 +248,15 @@ export function describeNativeComponentFrame(
|
||||
// The next one that isn't the same should be our match though.
|
||||
if (c < 0 || sampleLines[s] !== controlLines[c]) {
|
||||
// V8 adds a "new" prefix for native classes. Let's remove it to make it prettier.
|
||||
const frame = '\n' + sampleLines[s].replace(' at new ', ' at ');
|
||||
let frame = '\n' + sampleLines[s].replace(' at new ', ' at ');
|
||||
|
||||
// If our component frame is labeled "<anonymous>"
|
||||
// but we have a user-provided "displayName"
|
||||
// splice it in to make the stack more readable.
|
||||
if (fn.displayName && frame.includes('<anonymous>')) {
|
||||
frame = frame.replace('<anonymous>', fn.displayName);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
if (typeof fn === 'function') {
|
||||
componentFrameCache.set(fn, frame);
|
||||
|
||||
Reference in New Issue
Block a user