@@ -81,8 +81,6 @@ export function describeNativeComponentFrame(
81
81
}
82
82
}
83
83
84
- let control ;
85
-
86
84
const previousPrepareStackTrace = Error . prepareStackTrace ;
87
85
// $FlowFixMe[incompatible-type] It does accept undefined.
88
86
Error . prepareStackTrace = undefined ;
@@ -98,64 +96,140 @@ export function describeNativeComponentFrame(
98
96
currentDispatcherRef . current = null ;
99
97
disableLogs ( ) ;
100
98
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
+ }
123
166
}
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 ] ;
130
171
}
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 ;
139
172
}
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 ) {
145
200
// This extracts the first frame from the sample that isn't also in the control.
146
201
// 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
+ }
159
233
}
160
234
for ( ; s >= 1 && c >= 0 ; s -- , c -- ) {
161
235
// Next we find the first one that isn't the same which should be the
@@ -174,7 +248,15 @@ export function describeNativeComponentFrame(
174
248
// The next one that isn't the same should be our match though.
175
249
if ( c < 0 || sampleLines [ s ] !== controlLines [ c ] ) {
176
250
// 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
+
178
260
if ( __DEV__ ) {
179
261
if ( typeof fn === 'function' ) {
180
262
componentFrameCache . set ( fn , frame ) ;
0 commit comments