[Fizz] Implement debugInfo (#30174)
Stacked on #30170. This lets us track Server Component parent stacks in Fizz which also lets us track the correct owner stack for lazy. In Fiber we're careful not to make any DEV only fibers but since the ReactFizzComponentStack data structures just exist for debug meta data anyway we can just expand on that.
Sebastian Markbåge committed
Jul 2, 2024 at 18:26 UTC
cfb8945f511add040e1d5427d9961337f98f7618
4 files changed
+194
-80
packages/react-html/src/__tests__/ReactHTMLServer-test.js
+10
-8
@@ -244,17 +244,19 @@ if (!__EXPERIMENTAL__) {
244
expect(caughtErrors.length).toBe(1);
245
expect(caughtErrors[0].error).toBe(thrownError);
246
expect(normalizeCodeLocInfo(caughtErrors[0].parentStack)).toBe(
247
- // TODO: Because Fizz doesn't yet implement debugInfo for parent stacks
248
- // it doesn't have the Server Components in the parent stacks.
249
- '\n in Lazy (at **)' +
250
- '\n in div (at **)' +
251
- '\n in div (at **)',
247
+ __DEV__
248
+ ? '\n in Baz (at **)' +
249
+ '\n in div (at **)' +
250
+ '\n in Bar (at **)' +
251
+ '\n in Foo (at **)' +
252
+ '\n in div (at **)'
253
+ : '\n in Lazy (at **)' +
254
+ '\n in div (at **)' +
255
+ '\n in div (at **)',
256
);
257
expect(normalizeCodeLocInfo(caughtErrors[0].ownerStack)).toBe(
258
__DEV__ && gate(flags => flags.enableOwnerStacks)
255
- ? // TODO: Because Fizz doesn't yet implement debugInfo for parent stacks
256
- // it doesn't have the Server Components in the parent stacks.
257
- '\n in Lazy (at **)'
259
+ ? '\n in Bar (at **)' + '\n in Foo (at **)'
260
: null,
261
);
262
});
packages/react-reconciler/src/ReactFiberComponentStack.js
+4
-4
@@ -185,11 +185,11 @@ export function getOwnerStackByFiberInDev(
185
// another code path anyway. I.e. this is likely NOT a V8 based browser.
186
// This will cause some of the stack to have different formatting.
187
// TODO: Normalize server component stacks to the client formatting.
188
- if (owner.stack !== '') {
189
- info += '\n' + owner.stack;
188
+ const ownerStack: string = owner.stack;
189
+ owner = owner.owner;
190
+ if (owner && ownerStack !== '') {
191
+ info += '\n' + ownerStack;
192
}
191
- const componentInfo: ReactComponentInfo = (owner: any);
192
- owner = componentInfo.owner;
193
} else {
194
break;
195
}
packages/react-server/src/ReactFizzComponentStack.js
+24
-5
@@ -41,10 +41,19 @@ type ClassComponentStackNode = {
41
owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only
42
stack?: null | string | Error, // DEV only
43
};
44
+type ServerComponentStackNode = {
45
+ // DEV only
46
+ tag: 3,
47
+ parent: null | ComponentStackNode,
48
+ type: string, // name + env
49
+ owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only
50
+ stack?: null | string | Error, // DEV only
51
+};
52
export type ComponentStackNode =
53
| BuiltInComponentStackNode
54
| FunctionComponentStackNode
47
- | ClassComponentStackNode;
55
+ | ClassComponentStackNode
56
+ | ServerComponentStackNode;
57
58
export function getStackByComponentStackNode(
59
componentStack: ComponentStackNode,
@@ -63,6 +72,11 @@ export function getStackByComponentStackNode(
72
case 2:
73
info += describeClassComponentFrame(node.type);
74
break;
75
+ case 3:
76
+ if (__DEV__) {
77
+ info += describeBuiltInComponentFrame(node.type);
78
+ break;
79
+ }
80
}
81
// $FlowFixMe[incompatible-type] we bail out when we get a null
82
node = node.parent;
@@ -110,6 +124,11 @@ export function getOwnerStackByComponentStackNodeInDev(
124
);
125
}
126
break;
127
+ case 3:
128
+ if (!componentStack.owner) {
129
+ info += describeBuiltInComponentFrame(componentStack.type);
130
+ }
131
+ break;
132
}
133
134
let owner: void | null | ComponentStackNode | ReactComponentInfo =
@@ -137,11 +156,11 @@ export function getOwnerStackByComponentStackNodeInDev(
156
}
157
} else if (typeof owner.stack === 'string') {
158
// Server Component
140
- if (owner.stack !== '') {
141
- info += '\n' + owner.stack;
159
+ const ownerStack: string = owner.stack;
160
+ owner = owner.owner;
161
+ if (owner && ownerStack !== '') {
162
+ info += '\n' + ownerStack;
163
}
143
- const componentInfo: ReactComponentInfo = (owner: any);
144
- owner = componentInfo.owner;
164
} else {
165
break;
166
}
packages/react-server/src/ReactFizzServer.js
+156
-63
@@ -21,6 +21,7 @@ import type {
21
Thenable,
22
ReactFormState,
23
ReactComponentInfo,
24
+ ReactDebugInfo,
25
} from 'shared/ReactTypes';
26
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
27
import type {
@@ -886,6 +887,41 @@ function createClassComponentStack(
887
type,
888
};
889
}
890
+function createServerComponentStack(
891
+ task: Task,
892
+ debugInfo: void | null | ReactDebugInfo,
893
+): null | ComponentStackNode {
894
+ // Build a Server Component parent stack from the debugInfo.
895
+ if (__DEV__) {
896
+ let node = task.componentStack;
897
+ if (debugInfo != null) {
898
+ const stack: ReactDebugInfo = debugInfo;
899
+ for (let i = 0; i < stack.length; i++) {
900
+ const componentInfo: ReactComponentInfo = (stack[i]: any);
901
+ if (typeof componentInfo.name !== 'string') {
902
+ continue;
903
+ }
904
+ let name = componentInfo.name;
905
+ const env = componentInfo.env;
906
+ if (env) {
907
+ name += ' (' + env + ')';
908
+ }
909
+ node = {
910
+ tag: 3,
911
+ parent: node,
912
+ type: name,
913
+ owner: componentInfo.owner,
914
+ stack: componentInfo.stack,
915
+ };
916
+ }
917
+ }
918
+ return node;
919
+ }
920
+ // eslint-disable-next-line react-internal/prod-error-codes
921
+ throw new Error(
922
+ 'createServerComponentStack should never be called in production. This is a bug in React.',
923
+ );
924
+}
925
926
function createComponentStackFromType(
927
task: Task,
@@ -1982,6 +2018,7 @@ function renderLazyComponent(
2018
stack: null | Error, // DEV only
2019
): void {
2020
const previousComponentStack = task.componentStack;
2021
+ // TODO: Do we really need this stack frame? We don't on the client.
2022
task.componentStack = createBuiltInComponentStack(task, 'Lazy', owner, stack);
2023
let Component;
2024
if (__DEV__) {
@@ -2533,72 +2570,90 @@ function renderNodeDestructive(
2570
const owner = __DEV__ ? element._owner : null;
2571
const stack = __DEV__ && enableOwnerStacks ? element._debugStack : null;
2572
2573
+ const previousComponentStack = task.componentStack;
2574
+ if (__DEV__) {
2575
+ task.componentStack = createServerComponentStack(
2576
+ task,
2577
+ element._debugInfo,
2578
+ );
2579
+ }
2580
+
2581
const name = getComponentNameFromType(type);
2582
const keyOrIndex =
2583
key == null ? (childIndex === -1 ? 0 : childIndex) : key;
2584
const keyPath = [task.keyPath, name, keyOrIndex];
2585
if (task.replay !== null) {
2541
- if (__DEV__ && enableOwnerStacks) {
2542
- const debugTask: null | ConsoleTask = element._debugTask;
2543
- if (debugTask) {
2544
- debugTask.run(
2545
- replayElement.bind(
2546
- null,
2547
- request,
2548
- task,
2549
- keyPath,
2550
- name,
2551
- keyOrIndex,
2552
- childIndex,
2553
- type,
2554
- props,
2555
- ref,
2556
- task.replay,
2557
- owner,
2558
- stack,
2559
- ),
2560
- );
2561
- return;
2562
- }
2586
+ const debugTask: null | ConsoleTask =
2587
+ __DEV__ && enableOwnerStacks ? element._debugTask : null;
2588
+ if (debugTask) {
2589
+ debugTask.run(
2590
+ replayElement.bind(
2591
+ null,
2592
+ request,
2593
+ task,
2594
+ keyPath,
2595
+ name,
2596
+ keyOrIndex,
2597
+ childIndex,
2598
+ type,
2599
+ props,
2600
+ ref,
2601
+ task.replay,
2602
+ owner,
2603
+ stack,
2604
+ ),
2605
+ );
2606
+ } else {
2607
+ replayElement(
2608
+ request,
2609
+ task,
2610
+ keyPath,
2611
+ name,
2612
+ keyOrIndex,
2613
+ childIndex,
2614
+ type,
2615
+ props,
2616
+ ref,
2617
+ task.replay,
2618
+ owner,
2619
+ stack,
2620
+ );
2621
}
2564
- replayElement(
2565
- request,
2566
- task,
2567
- keyPath,
2568
- name,
2569
- keyOrIndex,
2570
- childIndex,
2571
- type,
2572
- props,
2573
- ref,
2574
- task.replay,
2575
- owner,
2576
- stack,
2577
- );
2622
// No matches found for this node. We assume it's already emitted in the
2623
// prelude and skip it during the replay.
2624
} else {
2625
// We're doing a plain render.
2582
- if (__DEV__ && enableOwnerStacks) {
2583
- const debugTask: null | ConsoleTask = element._debugTask;
2584
- if (debugTask) {
2585
- debugTask.run(
2586
- renderElement.bind(
2587
- null,
2588
- request,
2589
- task,
2590
- keyPath,
2591
- type,
2592
- props,
2593
- ref,
2594
- owner,
2595
- stack,
2596
- ),
2597
- );
2598
- return;
2599
- }
2626
+ const debugTask: null | ConsoleTask =
2627
+ __DEV__ && enableOwnerStacks ? element._debugTask : null;
2628
+ if (debugTask) {
2629
+ debugTask.run(
2630
+ renderElement.bind(
2631
+ null,
2632
+ request,
2633
+ task,
2634
+ keyPath,
2635
+ type,
2636
+ props,
2637
+ ref,
2638
+ owner,
2639
+ stack,
2640
+ ),
2641
+ );
2642
+ } else {
2643
+ renderElement(
2644
+ request,
2645
+ task,
2646
+ keyPath,
2647
+ type,
2648
+ props,
2649
+ ref,
2650
+ owner,
2651
+ stack,
2652
+ );
2653
}
2601
- renderElement(request, task, keyPath, type, props, ref, owner, stack);
2654
+ }
2655
+ if (__DEV__) {
2656
+ task.componentStack = previousComponentStack;
2657
}
2658
return;
2659
}
@@ -2608,14 +2663,23 @@ function renderNodeDestructive(
2663
'Render them conditionally so that they only appear on the client render.',
2664
);
2665
case REACT_LAZY_TYPE: {
2611
- const previousComponentStack = task.componentStack;
2612
- task.componentStack = createBuiltInComponentStack(
2613
- task,
2614
- 'Lazy',
2615
- null,
2616
- null,
2617
- );
2666
const lazyNode: LazyComponentType<any, any> = (node: any);
2667
+ const previousComponentStack = task.componentStack;
2668
+ if (__DEV__) {
2669
+ task.componentStack = createServerComponentStack(
2670
+ task,
2671
+ lazyNode._debugInfo,
2672
+ );
2673
+ }
2674
+ if (!__DEV__ || task.componentStack === previousComponentStack) {
2675
+ // TODO: Do we really need this stack frame? We don't on the client.
2676
+ task.componentStack = createBuiltInComponentStack(
2677
+ task,
2678
+ 'Lazy',
2679
+ null,
2680
+ null,
2681
+ );
2682
+ }
2683
let resolvedNode;
2684
if (__DEV__) {
2685
resolvedNode = callLazyInitInDEV(lazyNode);
@@ -2746,12 +2810,23 @@ function renderNodeDestructive(
2810
// Clear any previous thenable state that was created by the unwrapping.
2811
task.thenableState = null;
2812
const thenable: Thenable<ReactNodeList> = (maybeUsable: any);
2749
- return renderNodeDestructive(
2813
+ const previousComponentStack = task.componentStack;
2814
+ if (__DEV__) {
2815
+ task.componentStack = createServerComponentStack(
2816
+ task,
2817
+ thenable._debugInfo,
2818
+ );
2819
+ }
2820
+ const result = renderNodeDestructive(
2821
request,
2822
task,
2823
unwrapThenable(thenable),
2824
childIndex,
2825
);
2826
+ if (__DEV__) {
2827
+ task.componentStack = previousComponentStack;
2828
+ }
2829
+ return result;
2830
}
2831
2832
if (maybeUsable.$$typeof === REACT_CONTEXT_TYPE) {
@@ -2982,6 +3057,15 @@ function renderChildrenArray(
3057
childIndex: number,
3058
): void {
3059
const prevKeyPath = task.keyPath;
3060
+ const previousComponentStack = task.componentStack;
3061
+ if (__DEV__) {
3062
+ // We read debugInfo from task.node instead of children because it might have been an
3063
+ // unwrapped iterable so we read from the original node.
3064
+ task.componentStack = createServerComponentStack(
3065
+ task,
3066
+ (task.node: any)._debugInfo,
3067
+ );
3068
+ }
3069
if (childIndex !== -1) {
3070
task.keyPath = [task.keyPath, 'Fragment', childIndex];
3071
if (task.replay !== null) {
@@ -2993,6 +3077,9 @@ function renderChildrenArray(
3077
childIndex,
3078
);
3079
task.keyPath = prevKeyPath;
3080
+ if (__DEV__) {
3081
+ task.componentStack = previousComponentStack;
3082
+ }
3083
return;
3084
}
3085
}
@@ -3023,6 +3110,9 @@ function renderChildrenArray(
3110
}
3111
task.treeContext = prevTreeContext;
3112
task.keyPath = prevKeyPath;
3113
+ if (__DEV__) {
3114
+ task.componentStack = previousComponentStack;
3115
+ }
3116
return;
3117
}
3118
}
@@ -3042,6 +3132,9 @@ function renderChildrenArray(
3132
// only need to reset it to the previous value at the very end.
3133
task.treeContext = prevTreeContext;
3134
task.keyPath = prevKeyPath;
3135
+ if (__DEV__) {
3136
+ task.componentStack = previousComponentStack;
3137
+ }
3138
}
3139
3140
function trackPostpone(