13
ReactDebugInfo,
14
ReactAsyncInfo,
15
ReactIOInfo,
16
+ ReactStackTrace,
17
+ ReactCallSite,
18
} from 'shared/ReactTypes';
19
20
+import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
21
+
22
import {
23
ComponentFilterDisplayName,
24
ComponentFilterElementType,
5191
return null;
5192
}
5193
5194
+ function inspectHooks(fiber: Fiber): HooksTree {
5195
+ const originalConsoleMethods: {[string]: $FlowFixMe} = {};
5196
+
5197
+ // Temporarily disable all console logging before re-running the hook.
5198
+ for (const method in console) {
5199
+ try {
5200
+ // $FlowFixMe[invalid-computed-prop]
5201
+ originalConsoleMethods[method] = console[method];
5202
+ // $FlowFixMe[prop-missing]
5203
+ console[method] = () => {};
5204
+ } catch (error) {}
5205
+ }
5206
+
5207
+ try {
5208
+ return inspectHooksOfFiber(fiber, getDispatcherRef(renderer));
5209
+ } finally {
5210
+ // Restore original console functionality.
5211
+ for (const method in originalConsoleMethods) {
5212
+ try {
5213
+ // $FlowFixMe[prop-missing]
5214
+ console[method] = originalConsoleMethods[method];
5215
+ } catch (error) {}
5216
+ }
5217
+ }
5218
+ }
5219
+
5220
function getSuspendedByOfSuspenseNode(
5221
suspenseNode: SuspenseNode,
5222
): Array<SerializedAsyncInfo> {
5226
if (!suspenseNode.hasUniqueSuspenders) {
5227
return result;
5228
}
5229
+ // Cache the inspection of Hooks in case we need it for multiple entries.
5230
+ // We don't need a full map here since it's likely that every ioInfo that's unique
5231
+ // to a specific instance will have those appear in order of when that instance was discovered.
5232
+ let hooksCacheKey: null | DevToolsInstance = null;
5233
+ let hooksCache: null | HooksTree = null;
5234
suspenseNode.suspendedBy.forEach((set, ioInfo) => {
5235
let parentNode = suspenseNode.parent;
5236
while (parentNode !== null) {
5252
ioInfo,
5253
);
5254
if (asyncInfo !== null) {
5220
- const index = result.length;
5221
- result.push(serializeAsyncInfo(asyncInfo, index, firstInstance));
5255
+ let hooks: null | HooksTree = null;
5256
+ if (asyncInfo.stack == null && asyncInfo.owner == null) {
5257
+ if (hooksCacheKey === firstInstance) {
5258
+ hooks = hooksCache;
5259
+ } else if (firstInstance.kind !== VIRTUAL_INSTANCE) {
5260
+ const fiber = firstInstance.data;
5261
+ if (
5262
+ fiber.dependencies &&
5263
+ fiber.dependencies._debugThenableState
5264
+ ) {
5265
+ // This entry had no stack nor owner but this Fiber used Hooks so we might
5266
+ // be able to get the stack from the Hook.
5267
+ hooksCacheKey = firstInstance;
5268
+ hooksCache = hooks = inspectHooks(fiber);
5269
+ }
5270
+ }
5271
+ }
5272
+ result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
5273
}
5274
}
5275
});
5276
return result;
5277
}
5278
5279
+ function getAwaitStackFromHooks(
5280
+ hooks: HooksTree,
5281
+ asyncInfo: ReactAsyncInfo,
5282
+ ): null | ReactStackTrace {
5283
+ // TODO: We search through the hooks tree generated by inspectHooksOfFiber so that we can
5284
+ // use the information already extracted but ideally this search would be faster since we
5285
+ // could know which index to extract from the debug state.
5286
+ for (let i = 0; i < hooks.length; i++) {
5287
+ const node = hooks[i];
5288
+ const debugInfo = node.debugInfo;
5289
+ if (debugInfo != null && debugInfo.indexOf(asyncInfo) !== -1) {
5290
+ // Found a matching Hook. We'll now use its source location to construct a stack.
5291
+ const source = node.hookSource;
5292
+ if (
5293
+ source != null &&
5294
+ source.functionName !== null &&
5295
+ source.fileName !== null &&
5296
+ source.lineNumber !== null &&
5297
+ source.columnNumber !== null
5298
+ ) {
5299
+ // Unfortunately this is in a slightly different format. TODO: Unify HookNode with ReactCallSite.
5300
+ const callSite: ReactCallSite = [
5301
+ source.functionName,
5302
+ source.fileName,
5303
+ source.lineNumber,
5304
+ source.columnNumber,
5305
+ 0,
5306
+ 0,
5307
+ false,
5308
+ ];
5309
+ // As we return we'll add any custom hooks parent stacks to the array.
5310
+ return [callSite];
5311
+ } else {
5312
+ return [];
5313
+ }
5314
+ }
5315
+ // Otherwise, search the sub hooks of any custom hook.
5316
+ const matchedStack = getAwaitStackFromHooks(node.subHooks, asyncInfo);
5317
+ if (matchedStack !== null) {
5318
+ // Append this custom hook to the stack trace since it must have been called inside of it.
5319
+ const source = node.hookSource;
5320
+ if (
5321
+ source != null &&
5322
+ source.functionName !== null &&
5323
+ source.fileName !== null &&
5324
+ source.lineNumber !== null &&
5325
+ source.columnNumber !== null
5326
+ ) {
5327
+ // Unfortunately this is in a slightly different format. TODO: Unify HookNode with ReactCallSite.
5328
+ const callSite: ReactCallSite = [
5329
+ source.functionName,
5330
+ source.fileName,
5331
+ source.lineNumber,
5332
+ source.columnNumber,
5333
+ 0,
5334
+ 0,
5335
+ false,
5336
+ ];
5337
+ matchedStack.push(callSite);
5338
+ }
5339
+ return matchedStack;
5340
+ }
5341
+ }
5342
+ return null;
5343
+ }
5344
+
5345
function serializeAsyncInfo(
5346
asyncInfo: ReactAsyncInfo,
5230
- index: number,
5347
parentInstance: DevToolsInstance,
5348
+ hooks: null | HooksTree,
5349
): SerializedAsyncInfo {
5350
const ioInfo = asyncInfo.awaited;
5351
const ioOwnerInstance = findNearestOwnerInstance(
5385
// If we awaited in the child position of a component, then the best stack would be the
5386
// return callsite but we don't have that available so instead we skip. The callsite of
5387
// the JSX would be misleading in this case. The same thing happens with throw-a-Promise.
5388
+ if (hooks !== null) {
5389
+ // If this component used Hooks we might be able to instead infer the stack from the
5390
+ // use() callsite if this async info came from a hook. Let's search the tree to find it.
5391
+ awaitStack = getAwaitStackFromHooks(hooks, asyncInfo);
5392
+ }
5393
break;
5394
default:
5395
// If we awaited by passing a Promise to a built-in element, then the JSX callsite is a
5660
const owners: null | Array<SerializedElement> =
5661
getOwnersListFromInstance(fiberInstance);
5662
5541
- let hooks = null;
5663
+ let hooks: null | HooksTree = null;
5664
if (usesHooks) {
5543
- const originalConsoleMethods: {[string]: $FlowFixMe} = {};
5544
-
5545
- // Temporarily disable all console logging before re-running the hook.
5546
- for (const method in console) {
5547
- try {
5548
- // $FlowFixMe[invalid-computed-prop]
5549
- originalConsoleMethods[method] = console[method];
5550
- // $FlowFixMe[prop-missing]
5551
- console[method] = () => {};
5552
- } catch (error) {}
5553
- }
5554
-
5555
- try {
5556
- hooks = inspectHooksOfFiber(fiber, getDispatcherRef(renderer));
5557
- } finally {
5558
- // Restore original console functionality.
5559
- for (const method in originalConsoleMethods) {
5560
- try {
5561
- // $FlowFixMe[prop-missing]
5562
- console[method] = originalConsoleMethods[method];
5563
- } catch (error) {}
5564
- }
5565
- }
5665
+ hooks = inspectHooks(fiber);
5666
}
5667
5668
let rootType = null;
5741
// TODO: Prepend other suspense sources like css, images and use().
5742
fiberInstance.suspendedBy === null
5743
? []
5644
- : fiberInstance.suspendedBy.map((info, index) =>
5645
- serializeAsyncInfo(info, index, fiberInstance),
5744
+ : fiberInstance.suspendedBy.map(info =>
5745
+ serializeAsyncInfo(info, fiberInstance, hooks),
5746
);
5747
return {
5748
id: fiberInstance.id,
5913
suspendedBy:
5914
suspendedBy === null
5915
? []
5816
- : suspendedBy.map((info, index) =>
5817
- serializeAsyncInfo(info, index, virtualInstance),
5916
+ : suspendedBy.map(info =>
5917
+ serializeAsyncInfo(info, virtualInstance, null),
5918
),
5919
5920
// List of owners