main
js 316 lines 11.6 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 // This is a DevTools fork of ReactComponentStackFrame.
11 // This fork enables DevTools to use the same "native" component stack format,
12 // while still maintaining support for multiple renderer versions
13 // (which use different values for ReactTypeOfWork).
14
15 import type {CurrentDispatcherRef} from '../types';
16
17 // The shared console patching code is DEV-only.
18 // We can't use it since DevTools only ships production builds.
19 import {disableLogs, reenableLogs} from './DevToolsConsolePatching';
20
21 let prefix;
22 export function describeBuiltInComponentFrame(name: string): string {
23 if (prefix === undefined) {
24 // Extract the VM specific prefix used by each line.
25 try {
26 throw Error();
27 } catch (x) {
28 const match = x.stack.trim().match(/\n( *(at )?)/);
29 prefix = (match && match[1]) || '';
30 }
31 }
32 let suffix = '';
33 if (__IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__) {
34 suffix = ' (<anonymous>)';
35 } else if (__IS_FIREFOX__) {
36 suffix = '@unknown:0:0';
37 }
38 // We use the prefix to ensure our stacks line up with native stack frames.
39 // We use a suffix to ensure it gets parsed natively.
40 return '\n' + prefix + name + suffix;
41 }
42
43 export function describeDebugInfoFrame(name: string, env: ?string): string {
44 return describeBuiltInComponentFrame(name + (env ? ' [' + env + ']' : ''));
45 }
46
47 let reentry = false;
48 let componentFrameCache;
49 if (__DEV__) {
50 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
51 componentFrameCache = new PossiblyWeakMap<$FlowFixMe, string>();
52 }
53
54 export function describeNativeComponentFrame(
55 fn: Function,
56 construct: boolean,
57 currentDispatcherRef: CurrentDispatcherRef,
58 ): string {
59 // If something asked for a stack inside a fake render, it should get ignored.
60 if (!fn || reentry) {
61 return '';
62 }
63
64 if (__DEV__) {
65 const frame = componentFrameCache.get(fn);
66 if (frame !== undefined) {
67 return frame;
68 }
69 }
70
71 const previousPrepareStackTrace = Error.prepareStackTrace;
72 // $FlowFixMe[incompatible-type] It does accept undefined.
73 Error.prepareStackTrace = undefined;
74
75 reentry = true;
76
77 // Override the dispatcher so effects scheduled by this shallow render are thrown away.
78 //
79 // Note that unlike the code this was forked from (in ReactComponentStackFrame)
80 // DevTools should override the dispatcher even when DevTools is compiled in production mode,
81 // because the app itself may be in development mode and log errors/warnings.
82 const previousDispatcher = currentDispatcherRef.H;
83 currentDispatcherRef.H = null;
84 disableLogs();
85 try {
86 // NOTE: keep in sync with the implementation in ReactComponentStackFrame
87
88 /**
89 * Finding a common stack frame between sample and control errors can be
90 * tricky given the different types and levels of stack trace truncation from
91 * different JS VMs. So instead we'll attempt to control what that common
92 * frame should be through this object method:
93 * Having both the sample and control errors be in the function under the
94 * `DescribeNativeComponentFrameRoot` property, + setting the `name` and
95 * `displayName` properties of the function ensures that a stack
96 * frame exists that has the method name `DescribeNativeComponentFrameRoot` in
97 * it for both control and sample stacks.
98 */
99 const RunInRootFrame = {
100 DetermineComponentFrameRoot(): [?string, ?string] {
101 let control;
102 try {
103 // This should throw.
104 if (construct) {
105 // Something should be setting the props in the constructor.
106 const Fake = function () {
107 throw Error();
108 };
109 // $FlowFixMe[prop-missing]
110 Object.defineProperty(Fake.prototype, 'props', {
111 set: function () {
112 // We use a throwing setter instead of frozen or non-writable props
113 // because that won't throw in a non-strict mode function.
114 throw Error();
115 },
116 });
117 if (typeof Reflect === 'object' && Reflect.construct) {
118 // We construct a different control for this case to include any extra
119 // frames added by the construct call.
120 try {
121 Reflect.construct(Fake, []);
122 } catch (x) {
123 control = x;
124 }
125 Reflect.construct(fn, [], Fake);
126 } else {
127 try {
128 Fake.call();
129 } catch (x) {
130 control = x;
131 }
132
133 let prototypeModified = false;
134 let prevProps;
135 try {
136 prevProps = Object.getOwnPropertyDescriptor(
137 fn.prototype,
138 'props',
139 );
140 Object.defineProperty(fn.prototype, 'props', {
141 configurable: true,
142 set() {
143 throw Error();
144 },
145 });
146 prototypeModified = true;
147
148 // eslint-disable-next-line no-new
149 new fn();
150 } finally {
151 if (prototypeModified) {
152 if (prevProps !== undefined) {
153 Object.defineProperty(fn.prototype, 'props', prevProps);
154 } else {
155 delete fn.prototype.props;
156 }
157 }
158 }
159 }
160 } else {
161 try {
162 throw Error();
163 } catch (x) {
164 control = x;
165 }
166 // TODO(luna): This will currently only throw if the function component
167 // tries to access React/ReactDOM/props. We should probably make this throw
168 // in simple components too
169 const maybePromise = fn();
170
171 // If the function component returns a promise, it's likely an async
172 // component, which we don't yet support. Attach a noop catch handler to
173 // silence the error.
174 // TODO: Implement component stacks for async client components?
175 if (maybePromise && typeof maybePromise.catch === 'function') {
176 maybePromise.catch(() => {});
177 }
178 }
179 } catch (sample) {
180 // This is inlined manually because closure doesn't do it for us.
181 if (sample && control && typeof sample.stack === 'string') {
182 return [sample.stack, control.stack];
183 }
184 }
185 return [null, null];
186 },
187 };
188 // $FlowFixMe[prop-missing]
189 RunInRootFrame.DetermineComponentFrameRoot.displayName =
190 'DetermineComponentFrameRoot';
191 const namePropDescriptor = Object.getOwnPropertyDescriptor(
192 RunInRootFrame.DetermineComponentFrameRoot,
193 'name',
194 );
195 // Before ES6, the `name` property was not configurable.
196 if (namePropDescriptor && namePropDescriptor.configurable) {
197 // V8 utilizes a function's `name` property when generating a stack trace.
198 Object.defineProperty(
199 RunInRootFrame.DetermineComponentFrameRoot,
200 // Configurable properties can be updated even if its writable descriptor
201 // is set to `false`.
202 // $FlowFixMe[cannot-write]
203 'name',
204 {value: 'DetermineComponentFrameRoot'},
205 );
206 }
207
208 const [sampleStack, controlStack] =
209 RunInRootFrame.DetermineComponentFrameRoot();
210 if (sampleStack && controlStack) {
211 // This extracts the first frame from the sample that isn't also in the control.
212 // Skipping one frame that we assume is the frame that calls the two.
213 const sampleLines = sampleStack.split('\n');
214 const controlLines = controlStack.split('\n');
215 let s = 0;
216 let c = 0;
217 while (
218 s < sampleLines.length &&
219 !sampleLines[s].includes('DetermineComponentFrameRoot')
220 ) {
221 s++;
222 }
223 while (
224 c < controlLines.length &&
225 !controlLines[c].includes('DetermineComponentFrameRoot')
226 ) {
227 c++;
228 }
229 // We couldn't find our intentionally injected common root frame, attempt
230 // to find another common root frame by search from the bottom of the
231 // control stack...
232 if (s === sampleLines.length || c === controlLines.length) {
233 s = sampleLines.length - 1;
234 c = controlLines.length - 1;
235 while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {
236 // We expect at least one stack frame to be shared.
237 // Typically this will be the root most one. However, stack frames may be
238 // cut off due to maximum stack limits. In this case, one maybe cut off
239 // earlier than the other. We assume that the sample is longer or the same
240 // and there for cut off earlier. So we should find the root most frame in
241 // the sample somewhere in the control.
242 c--;
243 }
244 }
245 for (; s >= 1 && c >= 0; s--, c--) {
246 // Next we find the first one that isn't the same which should be the
247 // frame that called our sample function and the control.
248 if (sampleLines[s] !== controlLines[c]) {
249 // In V8, the first line is describing the message but other VMs don't.
250 // If we're about to return the first line, and the control is also on the same
251 // line, that's a pretty good indicator that our sample threw at same line as
252 // the control. I.e. before we entered the sample frame. So we ignore this result.
253 // This can happen if you passed a class to function component, or non-function.
254 if (s !== 1 || c !== 1) {
255 do {
256 s--;
257 c--;
258 // We may still have similar intermediate frames from the construct call.
259 // The next one that isn't the same should be our match though.
260 if (c < 0 || sampleLines[s] !== controlLines[c]) {
261 // V8 adds a "new" prefix for native classes. Let's remove it to make it prettier.
262 let frame = '\n' + sampleLines[s].replace(' at new ', ' at ');
263
264 // If our component frame is labeled "<anonymous>"
265 // but we have a user-provided "displayName"
266 // splice it in to make the stack more readable.
267 if (fn.displayName && frame.includes('<anonymous>')) {
268 frame = frame.replace('<anonymous>', fn.displayName);
269 }
270
271 if (__DEV__) {
272 if (typeof fn === 'function') {
273 componentFrameCache.set(fn, frame);
274 }
275 }
276 // Return the line we found.
277 return frame;
278 }
279 } while (s >= 1 && c >= 0);
280 }
281 break;
282 }
283 }
284 }
285 } finally {
286 reentry = false;
287
288 Error.prepareStackTrace = previousPrepareStackTrace;
289
290 currentDispatcherRef.H = previousDispatcher;
291 reenableLogs();
292 }
293 // Fallback to just using the name if we couldn't make it throw.
294 const name = fn ? fn.displayName || fn.name : '';
295 const syntheticFrame = name ? describeBuiltInComponentFrame(name) : '';
296 if (__DEV__) {
297 if (typeof fn === 'function') {
298 componentFrameCache.set(fn, syntheticFrame);
299 }
300 }
301 return syntheticFrame;
302 }
303
304 export function describeClassComponentFrame(
305 ctor: Function,
306 currentDispatcherRef: CurrentDispatcherRef,
307 ): string {
308 return describeNativeComponentFrame(ctor, true, currentDispatcherRef);
309 }
310
311 export function describeFunctionComponentFrame(
312 fn: Function,
313 currentDispatcherRef: CurrentDispatcherRef,
314 ): string {
315 return describeNativeComponentFrame(fn, false, currentDispatcherRef);
316 }