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