@@ -81,8 +81,6 @@ export function describeNativeComponentFrame(
8181 }
8282 }
8383
84- let control ;
85-
8684 const previousPrepareStackTrace = Error . prepareStackTrace ;
8785 // $FlowFixMe[incompatible-type] It does accept undefined.
8886 Error . prepareStackTrace = undefined ;
@@ -98,64 +96,140 @@ export function describeNativeComponentFrame(
9896 currentDispatcherRef . current = null ;
9997 disableLogs ( ) ;
10098
101- try {
102- // This should throw.
103- if ( construct ) {
104- // Something should be setting the props in the constructor.
105- const Fake = function ( ) {
106- throw Error ( ) ;
107- } ;
108- // $FlowFixMe[prop-missing]
109- Object . defineProperty ( Fake . prototype , 'props' , {
110- set : function ( ) {
111- // We use a throwing setter instead of frozen or non-writable props
112- // because that won't throw in a non-strict mode function.
113- throw Error ( ) ;
114- } ,
115- } ) ;
116- if ( typeof Reflect === 'object' && Reflect . construct ) {
117- // We construct a different control for this case to include any extra
118- // frames added by the construct call.
119- try {
120- Reflect . construct ( Fake , [ ] ) ;
121- } catch ( x ) {
122- control = x ;
99+ // NOTE: keep in sync with the implementation in ReactComponentStackFrame
100+
101+ /**
102+ * Finding a common stack frame between sample and control errors can be
103+ * tricky given the different types and levels of stack trace truncation from
104+ * different JS VMs. So instead we'll attempt to control what that common
105+ * frame should be through this object method:
106+ * Having both the sample and control errors be in the function under the
107+ * `DescribeNativeComponentFrameRoot` property, + setting the `name` and
108+ * `displayName` properties of the function ensures that a stack
109+ * frame exists that has the method name `DescribeNativeComponentFrameRoot` in
110+ * it for both control and sample stacks.
111+ */
112+ const RunInRootFrame = {
113+ DetermineComponentFrameRoot ( ) : [ ?string , ?string ] {
114+ let control ;
115+ try {
116+ // This should throw.
117+ if ( construct ) {
118+ // Something should be setting the props in the constructor.
119+ const Fake = function ( ) {
120+ throw Error ( ) ;
121+ } ;
122+ // $FlowFixMe[prop-missing]
123+ Object . defineProperty ( Fake . prototype , 'props' , {
124+ set : function ( ) {
125+ // We use a throwing setter instead of frozen or non-writable props
126+ // because that won't throw in a non-strict mode function.
127+ throw Error ( ) ;
128+ } ,
129+ } ) ;
130+ if ( typeof Reflect === 'object' && Reflect . construct ) {
131+ // We construct a different control for this case to include any extra
132+ // frames added by the construct call.
133+ try {
134+ Reflect . construct ( Fake , [ ] ) ;
135+ } catch ( x ) {
136+ control = x ;
137+ }
138+ Reflect . construct ( fn , [ ] , Fake ) ;
139+ } else {
140+ try {
141+ Fake . call ( ) ;
142+ } catch ( x ) {
143+ control = x ;
144+ }
145+ // $FlowFixMe[prop-missing] found when upgrading Flow
146+ fn . call ( Fake . prototype ) ;
147+ }
148+ } else {
149+ try {
150+ throw Error ( ) ;
151+ } catch ( x ) {
152+ control = x ;
153+ }
154+ // TODO(luna): This will currently only throw if the function component
155+ // tries to access React/ReactDOM/props. We should probably make this throw
156+ // in simple components too
157+ const maybePromise = fn ( ) ;
158+
159+ // If the function component returns a promise, it's likely an async
160+ // component, which we don't yet support. Attach a noop catch handler to
161+ // silence the error.
162+ // TODO: Implement component stacks for async client components?
163+ if ( maybePromise && typeof maybePromise . catch === 'function' ) {
164+ maybePromise . catch ( ( ) => { } ) ;
165+ }
123166 }
124- Reflect . construct ( fn , [ ] , Fake ) ;
125- } else {
126- try {
127- Fake . call ( ) ;
128- } catch ( x ) {
129- control = x ;
167+ } catch ( sample ) {
168+ // This is inlined manually because closure doesn't do it for us.
169+ if ( sample && control && typeof sample . stack === 'string' ) {
170+ return [ sample . stack , control . stack ] ;
130171 }
131- // $FlowFixMe[prop-missing] found when upgrading Flow
132- fn . call ( Fake . prototype ) ;
133- }
134- } else {
135- try {
136- throw Error ( ) ;
137- } catch ( x ) {
138- control = x ;
139172 }
140- fn ( ) ;
141- }
142- } catch ( sample ) {
143- // This is inlined manually because closure doesn't do it for us.
144- if ( sample && control && typeof sample . stack === 'string' ) {
173+ return [ null , null ] ;
174+ } ,
175+ } ;
176+ // $FlowFixMe[prop-missing]
177+ RunInRootFrame . DetermineComponentFrameRoot . displayName =
178+ 'DetermineComponentFrameRoot' ;
179+ const namePropDescriptor = Object . getOwnPropertyDescriptor (
180+ RunInRootFrame . DetermineComponentFrameRoot ,
181+ 'name' ,
182+ ) ;
183+ // Before ES6, the `name` property was not configurable.
184+ if ( namePropDescriptor && namePropDescriptor . configurable ) {
185+ // V8 utilizes a function's `name` property when generating a stack trace.
186+ Object . defineProperty (
187+ RunInRootFrame . DetermineComponentFrameRoot ,
188+ // Configurable properties can be updated even if its writable descriptor
189+ // is set to `false`.
190+ // $FlowFixMe[cannot-write]
191+ 'name' ,
192+ { value : 'DetermineComponentFrameRoot' } ,
193+ ) ;
194+ }
195+
196+ try {
197+ const [ sampleStack , controlStack ] =
198+ RunInRootFrame . DetermineComponentFrameRoot ( ) ;
199+ if ( sampleStack && controlStack ) {
145200 // This extracts the first frame from the sample that isn't also in the control.
146201 // Skipping one frame that we assume is the frame that calls the two.
147- const sampleLines = sample . stack . split ( '\n' ) ;
148- const controlLines = control . stack . split ( '\n' ) ;
149- let s = sampleLines . length - 1 ;
150- let c = controlLines . length - 1 ;
151- while ( s >= 1 && c >= 0 && sampleLines [ s ] !== controlLines [ c ] ) {
152- // We expect at least one stack frame to be shared.
153- // Typically this will be the root most one. However, stack frames may be
154- // cut off due to maximum stack limits. In this case, one maybe cut off
155- // earlier than the other. We assume that the sample is longer or the same
156- // and there for cut off earlier. So we should find the root most frame in
157- // the sample somewhere in the control.
158- c -- ;
202+ const sampleLines = sampleStack . split ( '\n' ) ;
203+ const controlLines = controlStack . split ( '\n' ) ;
204+ let s = 0 ;
205+ let c = 0 ;
206+ while (
207+ s < sampleLines . length &&
208+ ! sampleLines [ s ] . includes ( 'DetermineComponentFrameRoot' )
209+ ) {
210+ s ++ ;
211+ }
212+ while (
213+ c < controlLines . length &&
214+ ! controlLines [ c ] . includes ( 'DetermineComponentFrameRoot' )
215+ ) {
216+ c ++ ;
217+ }
218+ // We couldn't find our intentionally injected common root frame, attempt
219+ // to find another common root frame by search from the bottom of the
220+ // control stack...
221+ if ( s === sampleLines . length || c === controlLines . length ) {
222+ s = sampleLines . length - 1 ;
223+ c = controlLines . length - 1 ;
224+ while ( s >= 1 && c >= 0 && sampleLines [ s ] !== controlLines [ c ] ) {
225+ // We expect at least one stack frame to be shared.
226+ // Typically this will be the root most one. However, stack frames may be
227+ // cut off due to maximum stack limits. In this case, one maybe cut off
228+ // earlier than the other. We assume that the sample is longer or the same
229+ // and there for cut off earlier. So we should find the root most frame in
230+ // the sample somewhere in the control.
231+ c -- ;
232+ }
159233 }
160234 for ( ; s >= 1 && c >= 0 ; s -- , c -- ) {
161235 // Next we find the first one that isn't the same which should be the
@@ -174,7 +248,15 @@ export function describeNativeComponentFrame(
174248 // The next one that isn't the same should be our match though.
175249 if ( c < 0 || sampleLines [ s ] !== controlLines [ c ] ) {
176250 // V8 adds a "new" prefix for native classes. Let's remove it to make it prettier.
177- const frame = '\n' + sampleLines [ s ] . replace ( ' at new ' , ' at ' ) ;
251+ let frame = '\n' + sampleLines [ s ] . replace ( ' at new ' , ' at ' ) ;
252+
253+ // If our component frame is labeled "<anonymous>"
254+ // but we have a user-provided "displayName"
255+ // splice it in to make the stack more readable.
256+ if ( fn . displayName && frame . includes ( '<anonymous>' ) ) {
257+ frame = frame . replace ( '<anonymous>' , fn . displayName ) ;
258+ }
259+
178260 if ( __DEV__ ) {
179261 if ( typeof fn === 'function' ) {
180262 componentFrameCache . set ( fn , frame ) ;
0 commit comments