Skip to content

Commit 41b3e9a

Browse files
authored
[Fizz] Push a stalled use() to the ownerStack/debugTask (#35226)
1 parent 195fd22 commit 41b3e9a

4 files changed

Lines changed: 527 additions & 27 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 188 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,28 @@ describe('ReactFlightDOMNode', () => {
108108
);
109109
}
110110

111+
/**
112+
* Removes all stackframes not pointing into this file
113+
*/
114+
functionignoreListStack(str){
115+
if(!str){
116+
returnstr;
117+
}
118+
119+
letignoreListedStack='';
120+
constlines=str.split('\n');
121+
122+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
123+
for(constlineoflines){
124+
if(line.indexOf(__filename)===-1){
125+
}else{
126+
ignoreListedStack+='\n'+line.replace(__dirname,'.');
127+
}
128+
}
129+
130+
returnignoreListedStack;
131+
}
132+
111133
functionreadResult(stream){
112134
returnnewPromise((resolve,reject)=>{
113135
letbuffer='';
@@ -784,6 +806,165 @@ describe('ReactFlightDOMNode', () => {
784806
}
785807
});
786808

809+
// @gate enableHalt
810+
it('includes source locations in component and owner stacks for halted Client components',async()=>{
811+
functionSharedComponent({p1, p2, p3}){
812+
use(p1);
813+
use(p2);
814+
use(p3);
815+
return<div>Hello, Dave!</div>;
816+
}
817+
constClientComponentOnTheServer=clientExports(SharedComponent);
818+
constClientComponentOnTheClient=clientExports(
819+
SharedComponent,
820+
123,
821+
'path/to/chunk.js',
822+
);
823+
824+
letresolvePendingPromise;
825+
functionServerComponent(){
826+
constp1=Promise.resolve();
827+
constp2=newPromise(resolve=>{
828+
resolvePendingPromise=value=>{
829+
p2.status='fulfilled';
830+
p2.value=value;
831+
resolve(value);
832+
};
833+
});
834+
constp3=newPromise(()=>{});
835+
returnReactServer.createElement(ClientComponentOnTheClient,{
836+
p1: p1,
837+
p2: p2,
838+
p3: p3,
839+
});
840+
}
841+
842+
functionApp(){
843+
returnReactServer.createElement(
844+
'html',
845+
null,
846+
ReactServer.createElement(
847+
'body',
848+
null,
849+
ReactServer.createElement(
850+
ReactServer.Suspense,
851+
{fallback: 'Loading...'},
852+
ReactServer.createElement(ServerComponent,null),
853+
),
854+
),
855+
);
856+
}
857+
858+
consterrors=[];
859+
constrscStream=awaitserverAct(()=>
860+
ReactServerDOMServer.renderToPipeableStream(
861+
ReactServer.createElement(App,null),
862+
webpackMap,
863+
),
864+
);
865+
866+
constreadable=newStream.PassThrough(streamOptions);
867+
rscStream.pipe(readable);
868+
869+
functionClientRoot({response}){
870+
returnuse(response);
871+
}
872+
873+
constserverConsumerManifest={
874+
moduleMap: {
875+
[webpackMap[ClientComponentOnTheClient.$$id].id]: {
876+
'*': webpackMap[ClientComponentOnTheServer.$$id],
877+
},
878+
},
879+
moduleLoading: webpackModuleLoading,
880+
};
881+
882+
expect(errors).toEqual([]);
883+
884+
functionClientRoot({response}){
885+
returnuse(response);
886+
}
887+
888+
constresponse=ReactServerDOMClient.createFromNodeStream(
889+
readable,
890+
serverConsumerManifest,
891+
);
892+
893+
letcomponentStack;
894+
letownerStack;
895+
896+
constclientAbortController=newAbortController();
897+
898+
constfizzPrerenderStreamResult=ReactDOMFizzStatic.prerender(
899+
React.createElement(ClientRoot,{response}),
900+
{
901+
signal: clientAbortController.signal,
902+
onError(error,errorInfo){
903+
componentStack=errorInfo.componentStack;
904+
ownerStack=React.captureOwnerStack
905+
? React.captureOwnerStack()
906+
: null;
907+
},
908+
},
909+
);
910+
911+
resolvePendingPromise('custom-instrum-resolve');
912+
awaitserverAct(
913+
async()=>
914+
newPromise(resolve=>{
915+
setImmediate(()=>{
916+
clientAbortController.abort();
917+
resolve();
918+
});
919+
}),
920+
);
921+
922+
constfizzPrerenderStream=awaitfizzPrerenderStreamResult;
923+
constprerenderHTML=awaitreadWebResult(fizzPrerenderStream.prelude);
924+
925+
expect(prerenderHTML).toContain('Loading...');
926+
927+
if(__DEV__){
928+
expect(normalizeCodeLocInfo(componentStack)).toBe(
929+
'\n'+
930+
' in SharedComponent (at **)\n'+
931+
' in ServerComponent'+
932+
(gate(flags=>flags.enableAsyncDebugInfo) ? ' (at **)' : '')+
933+
'\n'+
934+
' in Suspense\n'+
935+
' in body\n'+
936+
' in html\n'+
937+
' in App (at **)\n'+
938+
' in ClientRoot (at **)',
939+
);
940+
}else{
941+
expect(normalizeCodeLocInfo(componentStack)).toBe(
942+
'\n'+
943+
' in SharedComponent (at **)\n'+
944+
' in Suspense\n'+
945+
' in body\n'+
946+
' in html\n'+
947+
' in ClientRoot (at **)',
948+
);
949+
}
950+
951+
if(__DEV__){
952+
expect(ignoreListStack(ownerStack)).toBe(
953+
// eslint-disable-next-line react-internal/safe-string-coercion
954+
''+
955+
// The concrete location may change as this test is updated.
956+
// Just make sure they still point at React.use(p2)
957+
(gate(flags=>flags.enableAsyncDebugInfo)
958+
? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)'
959+
: '')+
960+
'\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)'+
961+
'\n at App (file://./ReactFlightDOMNode-test.js:852:25)',
962+
);
963+
}else{
964+
expect(ownerStack).toBeNull();
965+
}
966+
});
967+
787968
// @gate enableHalt
788969
it('includes deeper location for aborted stacks',async()=>{
789970
asyncfunctiongetData(){
@@ -1364,12 +1545,12 @@ describe('ReactFlightDOMNode', () => {
13641545
'\n'+
13651546
' in Dynamic'+
13661547
(gate(flags=>flags.enableAsyncDebugInfo)
1367-
? ' (file://ReactFlightDOMNode-test.js:1238:27)\n'
1548+
? ' (file://ReactFlightDOMNode-test.js:1419:27)\n'
13681549
: '\n')+
13691550
' in body\n'+
13701551
' in html\n'+
1371-
' in App (file://ReactFlightDOMNode-test.js:1251:25)\n'+
1372-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1552+
' in App (file://ReactFlightDOMNode-test.js:1432:25)\n'+
1553+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13731554
);
13741555
}else{
13751556
expect(
@@ -1378,7 +1559,7 @@ describe('ReactFlightDOMNode', () => {
13781559
'\n'+
13791560
' in body\n'+
13801561
' in html\n'+
1381-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1562+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13821563
);
13831564
}
13841565

@@ -1388,16 +1569,16 @@ describe('ReactFlightDOMNode', () => {
13881569
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13891570
).toBe(
13901571
'\n'+
1391-
' in Dynamic (file://ReactFlightDOMNode-test.js:1238:27)\n'+
1392-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1572+
' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n'+
1573+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
13931574
);
13941575
}else{
13951576
expect(
13961577
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13971578
).toBe(
13981579
''+
13991580
'\n'+
1400-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1581+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
14011582
);
14021583
}
14031584
}else{

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,14 @@ import assign from 'shared/assign';
190190
importnoopfrom'shared/noop';
191191
importgetComponentNameFromTypefrom'shared/getComponentNameFromType';
192192
importisArrayfrom'shared/isArray';
193-
import{SuspenseException,getSuspendedThenable}from'./ReactFizzThenable';
193+
import{
194+
SuspenseException,
195+
getSuspendedThenable,
196+
ensureSuspendableThenableStateDEV,
197+
getSuspendedCallSiteStackDEV,
198+
getSuspendedCallSiteDebugTaskDEV,
199+
setCaptureSuspendedCallSiteDEV,
200+
}from'./ReactFizzThenable';
194201

195202
// Linked list representing the identity of a component given the component/tag name and key.
196203
// The name might be minified but we assume that it's going to be the same generated name. Typically
@@ -355,6 +362,7 @@ const OPEN = 11;
355362
constABORTING=12;
356363
constCLOSING=13;
357364
constCLOSED=14;
365+
constSTALLED_DEV=15;
358366

359367
exportopaquetypeRequest={
360368
destination: null|Destination,
@@ -363,7 +371,7 @@ export opaque type Request = {
363371
+renderState: RenderState,
364372
+rootFormatContext: FormatContext,
365373
+progressiveChunkSize: number,
366-
status: 10|11|12|13|14,
374+
status: 10|11|12|13|14|15,
367375
fatalError: mixed,
368376
nextSegmentId: number,
369377
allPendingTasks: number,// when it reaches zero, we can close the connection.
@@ -1023,6 +1031,89 @@ function pushHaltedAwaitOnComponentStack(
10231031
}
10241032
}
10251033

1034+
// performWork + retryTask without mutation
1035+
functionrerenderStalledTask(request: Request,task: Task): void{
1036+
constprevStatus=request.status;
1037+
request.status=STALLED_DEV;
1038+
1039+
constprevContext=getActiveContext();
1040+
constprevDispatcher=ReactSharedInternals.H;
1041+
ReactSharedInternals.H=HooksDispatcher;
1042+
constprevAsyncDispatcher=ReactSharedInternals.A;
1043+
ReactSharedInternals.A=DefaultAsyncDispatcher;
1044+
1045+
constprevRequest=currentRequest;
1046+
currentRequest=request;
1047+
1048+
constprevGetCurrentStackImpl=ReactSharedInternals.getCurrentStack;
1049+
ReactSharedInternals.getCurrentStack=getCurrentStackInDEV;
1050+
1051+
constprevResumableState=currentResumableState;
1052+
setCurrentResumableState(request.resumableState);
1053+
switchContext(task.context);
1054+
constprevTaskInDEV=currentTaskInDEV;
1055+
setCurrentTaskInDEV(task);
1056+
try{
1057+
retryNode(request,task);
1058+
}catch(x){
1059+
// Suspended again.
1060+
resetHooksState();
1061+
}finally{
1062+
setCurrentTaskInDEV(prevTaskInDEV);
1063+
setCurrentResumableState(prevResumableState);
1064+
1065+
ReactSharedInternals.H =prevDispatcher;
1066+
ReactSharedInternals.A=prevAsyncDispatcher;
1067+
1068+
ReactSharedInternals.getCurrentStack=prevGetCurrentStackImpl;
1069+
if(prevDispatcher===HooksDispatcher){
1070+
// This means that we were in a reentrant work loop. This could happen
1071+
// in a renderer that supports synchronous work like renderToString,
1072+
// when it's called from within another renderer.
1073+
// Normally we don't bother switching the contexts to their root/default
1074+
// values when leaving because we'll likely need the same or similar
1075+
// context again. However, when we're inside a synchronous loop like this
1076+
// we'll to restore the context to what it was before returning.
1077+
switchContext(prevContext);
1078+
}
1079+
currentRequest =prevRequest;
1080+
request.status=prevStatus;
1081+
}
1082+
}
1083+
1084+
functionpushSuspendedCallSiteOnComponentStack(
1085+
request: Request,
1086+
task: Task,
1087+
): void{
1088+
setCaptureSuspendedCallSiteDEV(true);
1089+
constrestoreThenableState=ensureSuspendableThenableStateDEV(
1090+
// refined at the callsite
1091+
((task.thenableState: any): ThenableState),
1092+
);
1093+
try{
1094+
rerenderStalledTask(request,task);
1095+
}finally{
1096+
restoreThenableState();
1097+
setCaptureSuspendedCallSiteDEV(false);
1098+
}
1099+
1100+
const suspendCallSiteStack =getSuspendedCallSiteStackDEV();
1101+
constsuspendCallSiteDebugTask=getSuspendedCallSiteDebugTaskDEV();
1102+
1103+
if(suspendCallSiteStack!==null){
1104+
const ownerStack =task.componentStack;
1105+
task.componentStack={
1106+
// The owner of the suspended call site would be the owner of this task.
1107+
// We need the task itself otherwise we'd miss a frame.
1108+
owner: ownerStack,
1109+
parent: suspendCallSiteStack.parent,
1110+
stack: suspendCallSiteStack.stack,
1111+
type: suspendCallSiteStack.type,
1112+
};
1113+
}
1114+
task.debugTask=suspendCallSiteDebugTask;
1115+
}
1116+
10261117
functionpushServerComponentStack(
10271118
task: Task,
10281119
debugInfo: void|null|ReactDebugInfo,
@@ -2723,7 +2814,12 @@ function renderLazyComponent(
27232814
constinit=lazyComponent._init;
27242815
Component=init(payload);
27252816
}
2726-
if(request.status===ABORTING){
2817+
if(
2818+
request.status===ABORTING&&
2819+
// We're going to discard this render anyway.
2820+
// We just need to reach the point where we suspended in dev.
2821+
(!__DEV__||request.status!==STALLED_DEV)
2822+
){
27272823
// eslint-disable-next-line no-throw-literal
27282824
thrownull;
27292825
}
@@ -4535,12 +4631,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
45354631
debugInfo=node._debugInfo;
45364632
}
45374633
pushHaltedAwaitOnComponentStack(task,debugInfo);
4538-
/*
45394634
if(task.thenableState!==null){
4540-
// TODO: If we were stalled inside use() of a Client Component then we should
4541-
// rerender to get the stack trace from the use() call.
4635+
pushSuspendedCallSiteOnComponentStack(request,task);
45424636
}
4543-
*/
45444637
}
45454638
}
45464639

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
[Fizz] Push a stalled use() to the ownerStack/debugTask (#35226) · react/react@41b3e9a · GitHub
Skip to content

Commit 41b3e9a

Browse files
authored
[Fizz] Push a stalled use() to the ownerStack/debugTask (#35226)
1 parent 195fd22 commit 41b3e9a

4 files changed

Lines changed: 527 additions & 27 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 188 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,28 @@ describe('ReactFlightDOMNode', () => {
108108
);
109109
}
110110

111+
/**
112+
* Removes all stackframes not pointing into this file
113+
*/
114+
functionignoreListStack(str){
115+
if(!str){
116+
returnstr;
117+
}
118+
119+
letignoreListedStack='';
120+
constlines=str.split('\n');
121+
122+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
123+
for(constlineoflines){
124+
if(line.indexOf(__filename)===-1){
125+
}else{
126+
ignoreListedStack+='\n'+line.replace(__dirname,'.');
127+
}
128+
}
129+
130+
returnignoreListedStack;
131+
}
132+
111133
functionreadResult(stream){
112134
returnnewPromise((resolve,reject)=>{
113135
letbuffer='';
@@ -784,6 +806,165 @@ describe('ReactFlightDOMNode', () => {
784806
}
785807
});
786808

809+
// @gate enableHalt
810+
it('includes source locations in component and owner stacks for halted Client components',async()=>{
811+
functionSharedComponent({p1, p2, p3}){
812+
use(p1);
813+
use(p2);
814+
use(p3);
815+
return<div>Hello, Dave!</div>;
816+
}
817+
constClientComponentOnTheServer=clientExports(SharedComponent);
818+
constClientComponentOnTheClient=clientExports(
819+
SharedComponent,
820+
123,
821+
'path/to/chunk.js',
822+
);
823+
824+
letresolvePendingPromise;
825+
functionServerComponent(){
826+
constp1=Promise.resolve();
827+
constp2=newPromise(resolve=>{
828+
resolvePendingPromise=value=>{
829+
p2.status='fulfilled';
830+
p2.value=value;
831+
resolve(value);
832+
};
833+
});
834+
constp3=newPromise(()=>{});
835+
returnReactServer.createElement(ClientComponentOnTheClient,{
836+
p1: p1,
837+
p2: p2,
838+
p3: p3,
839+
});
840+
}
841+
842+
functionApp(){
843+
returnReactServer.createElement(
844+
'html',
845+
null,
846+
ReactServer.createElement(
847+
'body',
848+
null,
849+
ReactServer.createElement(
850+
ReactServer.Suspense,
851+
{fallback: 'Loading...'},
852+
ReactServer.createElement(ServerComponent,null),
853+
),
854+
),
855+
);
856+
}
857+
858+
consterrors=[];
859+
constrscStream=awaitserverAct(()=>
860+
ReactServerDOMServer.renderToPipeableStream(
861+
ReactServer.createElement(App,null),
862+
webpackMap,
863+
),
864+
);
865+
866+
constreadable=newStream.PassThrough(streamOptions);
867+
rscStream.pipe(readable);
868+
869+
functionClientRoot({response}){
870+
returnuse(response);
871+
}
872+
873+
constserverConsumerManifest={
874+
moduleMap: {
875+
[webpackMap[ClientComponentOnTheClient.$$id].id]: {
876+
'*': webpackMap[ClientComponentOnTheServer.$$id],
877+
},
878+
},
879+
moduleLoading: webpackModuleLoading,
880+
};
881+
882+
expect(errors).toEqual([]);
883+
884+
functionClientRoot({response}){
885+
returnuse(response);
886+
}
887+
888+
constresponse=ReactServerDOMClient.createFromNodeStream(
889+
readable,
890+
serverConsumerManifest,
891+
);
892+
893+
letcomponentStack;
894+
letownerStack;
895+
896+
constclientAbortController=newAbortController();
897+
898+
constfizzPrerenderStreamResult=ReactDOMFizzStatic.prerender(
899+
React.createElement(ClientRoot,{response}),
900+
{
901+
signal: clientAbortController.signal,
902+
onError(error,errorInfo){
903+
componentStack=errorInfo.componentStack;
904+
ownerStack=React.captureOwnerStack
905+
? React.captureOwnerStack()
906+
: null;
907+
},
908+
},
909+
);
910+
911+
resolvePendingPromise('custom-instrum-resolve');
912+
awaitserverAct(
913+
async()=>
914+
newPromise(resolve=>{
915+
setImmediate(()=>{
916+
clientAbortController.abort();
917+
resolve();
918+
});
919+
}),
920+
);
921+
922+
constfizzPrerenderStream=awaitfizzPrerenderStreamResult;
923+
constprerenderHTML=awaitreadWebResult(fizzPrerenderStream.prelude);
924+
925+
expect(prerenderHTML).toContain('Loading...');
926+
927+
if(__DEV__){
928+
expect(normalizeCodeLocInfo(componentStack)).toBe(
929+
'\n'+
930+
' in SharedComponent (at **)\n'+
931+
' in ServerComponent'+
932+
(gate(flags=>flags.enableAsyncDebugInfo) ? ' (at **)' : '')+
933+
'\n'+
934+
' in Suspense\n'+
935+
' in body\n'+
936+
' in html\n'+
937+
' in App (at **)\n'+
938+
' in ClientRoot (at **)',
939+
);
940+
}else{
941+
expect(normalizeCodeLocInfo(componentStack)).toBe(
942+
'\n'+
943+
' in SharedComponent (at **)\n'+
944+
' in Suspense\n'+
945+
' in body\n'+
946+
' in html\n'+
947+
' in ClientRoot (at **)',
948+
);
949+
}
950+
951+
if(__DEV__){
952+
expect(ignoreListStack(ownerStack)).toBe(
953+
// eslint-disable-next-line react-internal/safe-string-coercion
954+
''+
955+
// The concrete location may change as this test is updated.
956+
// Just make sure they still point at React.use(p2)
957+
(gate(flags=>flags.enableAsyncDebugInfo)
958+
? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)'
959+
: '')+
960+
'\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)'+
961+
'\n at App (file://./ReactFlightDOMNode-test.js:852:25)',
962+
);
963+
}else{
964+
expect(ownerStack).toBeNull();
965+
}
966+
});
967+
787968
// @gate enableHalt
788969
it('includes deeper location for aborted stacks',async()=>{
789970
asyncfunctiongetData(){
@@ -1364,12 +1545,12 @@ describe('ReactFlightDOMNode', () => {
13641545
'\n'+
13651546
' in Dynamic'+
13661547
(gate(flags=>flags.enableAsyncDebugInfo)
1367-
? ' (file://ReactFlightDOMNode-test.js:1238:27)\n'
1548+
? ' (file://ReactFlightDOMNode-test.js:1419:27)\n'
13681549
: '\n')+
13691550
' in body\n'+
13701551
' in html\n'+
1371-
' in App (file://ReactFlightDOMNode-test.js:1251:25)\n'+
1372-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1552+
' in App (file://ReactFlightDOMNode-test.js:1432:25)\n'+
1553+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13731554
);
13741555
}else{
13751556
expect(
@@ -1378,7 +1559,7 @@ describe('ReactFlightDOMNode', () => {
13781559
'\n'+
13791560
' in body\n'+
13801561
' in html\n'+
1381-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1562+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13821563
);
13831564
}
13841565

@@ -1388,16 +1569,16 @@ describe('ReactFlightDOMNode', () => {
13881569
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13891570
).toBe(
13901571
'\n'+
1391-
' in Dynamic (file://ReactFlightDOMNode-test.js:1238:27)\n'+
1392-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1572+
' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n'+
1573+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
13931574
);
13941575
}else{
13951576
expect(
13961577
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13971578
).toBe(
13981579
''+
13991580
'\n'+
1400-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1581+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
14011582
);
14021583
}
14031584
}else{

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,14 @@ import assign from 'shared/assign';
190190
importnoopfrom'shared/noop';
191191
importgetComponentNameFromTypefrom'shared/getComponentNameFromType';
192192
importisArrayfrom'shared/isArray';
193-
import{SuspenseException,getSuspendedThenable}from'./ReactFizzThenable';
193+
import{
194+
SuspenseException,
195+
getSuspendedThenable,
196+
ensureSuspendableThenableStateDEV,
197+
getSuspendedCallSiteStackDEV,
198+
getSuspendedCallSiteDebugTaskDEV,
199+
setCaptureSuspendedCallSiteDEV,
200+
}from'./ReactFizzThenable';
194201

195202
// Linked list representing the identity of a component given the component/tag name and key.
196203
// The name might be minified but we assume that it's going to be the same generated name. Typically
@@ -355,6 +362,7 @@ const OPEN = 11;
355362
constABORTING=12;
356363
constCLOSING=13;
357364
constCLOSED=14;
365+
constSTALLED_DEV=15;
358366

359367
exportopaquetypeRequest={
360368
destination: null|Destination,
@@ -363,7 +371,7 @@ export opaque type Request = {
363371
+renderState: RenderState,
364372
+rootFormatContext: FormatContext,
365373
+progressiveChunkSize: number,
366-
status: 10|11|12|13|14,
374+
status: 10|11|12|13|14|15,
367375
fatalError: mixed,
368376
nextSegmentId: number,
369377
allPendingTasks: number,// when it reaches zero, we can close the connection.
@@ -1023,6 +1031,89 @@ function pushHaltedAwaitOnComponentStack(
10231031
}
10241032
}
10251033

1034+
// performWork + retryTask without mutation
1035+
functionrerenderStalledTask(request: Request,task: Task): void{
1036+
constprevStatus=request.status;
1037+
request.status=STALLED_DEV;
1038+
1039+
constprevContext=getActiveContext();
1040+
constprevDispatcher=ReactSharedInternals.H;
1041+
ReactSharedInternals.H=HooksDispatcher;
1042+
constprevAsyncDispatcher=ReactSharedInternals.A;
1043+
ReactSharedInternals.A=DefaultAsyncDispatcher;
1044+
1045+
constprevRequest=currentRequest;
1046+
currentRequest=request;
1047+
1048+
constprevGetCurrentStackImpl=ReactSharedInternals.getCurrentStack;
1049+
ReactSharedInternals.getCurrentStack=getCurrentStackInDEV;
1050+
1051+
constprevResumableState=currentResumableState;
1052+
setCurrentResumableState(request.resumableState);
1053+
switchContext(task.context);
1054+
constprevTaskInDEV=currentTaskInDEV;
1055+
setCurrentTaskInDEV(task);
1056+
try{
1057+
retryNode(request,task);
1058+
}catch(x){
1059+
// Suspended again.
1060+
resetHooksState();
1061+
}finally{
1062+
setCurrentTaskInDEV(prevTaskInDEV);
1063+
setCurrentResumableState(prevResumableState);
1064+
1065+
ReactSharedInternals.H =prevDispatcher;
1066+
ReactSharedInternals.A=prevAsyncDispatcher;
1067+
1068+
ReactSharedInternals.getCurrentStack=prevGetCurrentStackImpl;
1069+
if(prevDispatcher===HooksDispatcher){
1070+
// This means that we were in a reentrant work loop. This could happen
1071+
// in a renderer that supports synchronous work like renderToString,
1072+
// when it's called from within another renderer.
1073+
// Normally we don't bother switching the contexts to their root/default
1074+
// values when leaving because we'll likely need the same or similar
1075+
// context again. However, when we're inside a synchronous loop like this
1076+
// we'll to restore the context to what it was before returning.
1077+
switchContext(prevContext);
1078+
}
1079+
currentRequest =prevRequest;
1080+
request.status=prevStatus;
1081+
}
1082+
}
1083+
1084+
functionpushSuspendedCallSiteOnComponentStack(
1085+
request: Request,
1086+
task: Task,
1087+
): void{
1088+
setCaptureSuspendedCallSiteDEV(true);
1089+
constrestoreThenableState=ensureSuspendableThenableStateDEV(
1090+
// refined at the callsite
1091+
((task.thenableState: any): ThenableState),
1092+
);
1093+
try{
1094+
rerenderStalledTask(request,task);
1095+
}finally{
1096+
restoreThenableState();
1097+
setCaptureSuspendedCallSiteDEV(false);
1098+
}
1099+
1100+
const suspendCallSiteStack =getSuspendedCallSiteStackDEV();
1101+
constsuspendCallSiteDebugTask=getSuspendedCallSiteDebugTaskDEV();
1102+
1103+
if(suspendCallSiteStack!==null){
1104+
const ownerStack =task.componentStack;
1105+
task.componentStack={
1106+
// The owner of the suspended call site would be the owner of this task.
1107+
// We need the task itself otherwise we'd miss a frame.
1108+
owner: ownerStack,
1109+
parent: suspendCallSiteStack.parent,
1110+
stack: suspendCallSiteStack.stack,
1111+
type: suspendCallSiteStack.type,
1112+
};
1113+
}
1114+
task.debugTask=suspendCallSiteDebugTask;
1115+
}
1116+
10261117
functionpushServerComponentStack(
10271118
task: Task,
10281119
debugInfo: void|null|ReactDebugInfo,
@@ -2723,7 +2814,12 @@ function renderLazyComponent(
27232814
constinit=lazyComponent._init;
27242815
Component=init(payload);
27252816
}
2726-
if(request.status===ABORTING){
2817+
if(
2818+
request.status===ABORTING&&
2819+
// We're going to discard this render anyway.
2820+
// We just need to reach the point where we suspended in dev.
2821+
(!__DEV__||request.status!==STALLED_DEV)
2822+
){
27272823
// eslint-disable-next-line no-throw-literal
27282824
thrownull;
27292825
}
@@ -4535,12 +4631,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
45354631
debugInfo=node._debugInfo;
45364632
}
45374633
pushHaltedAwaitOnComponentStack(task,debugInfo);
4538-
/*
45394634
if(task.thenableState!==null){
4540-
// TODO: If we were stalled inside use() of a Client Component then we should
4541-
// rerender to get the stack trace from the use() call.
4635+
pushSuspendedCallSiteOnComponentStack(request,task);
45424636
}
4543-
*/
45444637
}
45454638
}
45464639

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [Fizz] Push a stalled use() to the ownerStack/debugTask (#35226) · react/react@41b3e9a · GitHub
Skip to content

Commit 41b3e9a

Browse files
authored
[Fizz] Push a stalled use() to the ownerStack/debugTask (#35226)
1 parent 195fd22 commit 41b3e9a

4 files changed

Lines changed: 527 additions & 27 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 188 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,28 @@ describe('ReactFlightDOMNode', () => {
108108
);
109109
}
110110

111+
/**
112+
* Removes all stackframes not pointing into this file
113+
*/
114+
functionignoreListStack(str){
115+
if(!str){
116+
returnstr;
117+
}
118+
119+
letignoreListedStack='';
120+
constlines=str.split('\n');
121+
122+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
123+
for(constlineoflines){
124+
if(line.indexOf(__filename)===-1){
125+
}else{
126+
ignoreListedStack+='\n'+line.replace(__dirname,'.');
127+
}
128+
}
129+
130+
returnignoreListedStack;
131+
}
132+
111133
functionreadResult(stream){
112134
returnnewPromise((resolve,reject)=>{
113135
letbuffer='';
@@ -784,6 +806,165 @@ describe('ReactFlightDOMNode', () => {
784806
}
785807
});
786808

809+
// @gate enableHalt
810+
it('includes source locations in component and owner stacks for halted Client components',async()=>{
811+
functionSharedComponent({p1, p2, p3}){
812+
use(p1);
813+
use(p2);
814+
use(p3);
815+
return<div>Hello, Dave!</div>;
816+
}
817+
constClientComponentOnTheServer=clientExports(SharedComponent);
818+
constClientComponentOnTheClient=clientExports(
819+
SharedComponent,
820+
123,
821+
'path/to/chunk.js',
822+
);
823+
824+
letresolvePendingPromise;
825+
functionServerComponent(){
826+
constp1=Promise.resolve();
827+
constp2=newPromise(resolve=>{
828+
resolvePendingPromise=value=>{
829+
p2.status='fulfilled';
830+
p2.value=value;
831+
resolve(value);
832+
};
833+
});
834+
constp3=newPromise(()=>{});
835+
returnReactServer.createElement(ClientComponentOnTheClient,{
836+
p1: p1,
837+
p2: p2,
838+
p3: p3,
839+
});
840+
}
841+
842+
functionApp(){
843+
returnReactServer.createElement(
844+
'html',
845+
null,
846+
ReactServer.createElement(
847+
'body',
848+
null,
849+
ReactServer.createElement(
850+
ReactServer.Suspense,
851+
{fallback: 'Loading...'},
852+
ReactServer.createElement(ServerComponent,null),
853+
),
854+
),
855+
);
856+
}
857+
858+
consterrors=[];
859+
constrscStream=awaitserverAct(()=>
860+
ReactServerDOMServer.renderToPipeableStream(
861+
ReactServer.createElement(App,null),
862+
webpackMap,
863+
),
864+
);
865+
866+
constreadable=newStream.PassThrough(streamOptions);
867+
rscStream.pipe(readable);
868+
869+
functionClientRoot({response}){
870+
returnuse(response);
871+
}
872+
873+
constserverConsumerManifest={
874+
moduleMap: {
875+
[webpackMap[ClientComponentOnTheClient.$$id].id]: {
876+
'*': webpackMap[ClientComponentOnTheServer.$$id],
877+
},
878+
},
879+
moduleLoading: webpackModuleLoading,
880+
};
881+
882+
expect(errors).toEqual([]);
883+
884+
functionClientRoot({response}){
885+
returnuse(response);
886+
}
887+
888+
constresponse=ReactServerDOMClient.createFromNodeStream(
889+
readable,
890+
serverConsumerManifest,
891+
);
892+
893+
letcomponentStack;
894+
letownerStack;
895+
896+
constclientAbortController=newAbortController();
897+
898+
constfizzPrerenderStreamResult=ReactDOMFizzStatic.prerender(
899+
React.createElement(ClientRoot,{response}),
900+
{
901+
signal: clientAbortController.signal,
902+
onError(error,errorInfo){
903+
componentStack=errorInfo.componentStack;
904+
ownerStack=React.captureOwnerStack
905+
? React.captureOwnerStack()
906+
: null;
907+
},
908+
},
909+
);
910+
911+
resolvePendingPromise('custom-instrum-resolve');
912+
awaitserverAct(
913+
async()=>
914+
newPromise(resolve=>{
915+
setImmediate(()=>{
916+
clientAbortController.abort();
917+
resolve();
918+
});
919+
}),
920+
);
921+
922+
constfizzPrerenderStream=awaitfizzPrerenderStreamResult;
923+
constprerenderHTML=awaitreadWebResult(fizzPrerenderStream.prelude);
924+
925+
expect(prerenderHTML).toContain('Loading...');
926+
927+
if(__DEV__){
928+
expect(normalizeCodeLocInfo(componentStack)).toBe(
929+
'\n'+
930+
' in SharedComponent (at **)\n'+
931+
' in ServerComponent'+
932+
(gate(flags=>flags.enableAsyncDebugInfo) ? ' (at **)' : '')+
933+
'\n'+
934+
' in Suspense\n'+
935+
' in body\n'+
936+
' in html\n'+
937+
' in App (at **)\n'+
938+
' in ClientRoot (at **)',
939+
);
940+
}else{
941+
expect(normalizeCodeLocInfo(componentStack)).toBe(
942+
'\n'+
943+
' in SharedComponent (at **)\n'+
944+
' in Suspense\n'+
945+
' in body\n'+
946+
' in html\n'+
947+
' in ClientRoot (at **)',
948+
);
949+
}
950+
951+
if(__DEV__){
952+
expect(ignoreListStack(ownerStack)).toBe(
953+
// eslint-disable-next-line react-internal/safe-string-coercion
954+
''+
955+
// The concrete location may change as this test is updated.
956+
// Just make sure they still point at React.use(p2)
957+
(gate(flags=>flags.enableAsyncDebugInfo)
958+
? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)'
959+
: '')+
960+
'\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)'+
961+
'\n at App (file://./ReactFlightDOMNode-test.js:852:25)',
962+
);
963+
}else{
964+
expect(ownerStack).toBeNull();
965+
}
966+
});
967+
787968
// @gate enableHalt
788969
it('includes deeper location for aborted stacks',async()=>{
789970
asyncfunctiongetData(){
@@ -1364,12 +1545,12 @@ describe('ReactFlightDOMNode', () => {
13641545
'\n'+
13651546
' in Dynamic'+
13661547
(gate(flags=>flags.enableAsyncDebugInfo)
1367-
? ' (file://ReactFlightDOMNode-test.js:1238:27)\n'
1548+
? ' (file://ReactFlightDOMNode-test.js:1419:27)\n'
13681549
: '\n')+
13691550
' in body\n'+
13701551
' in html\n'+
1371-
' in App (file://ReactFlightDOMNode-test.js:1251:25)\n'+
1372-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1552+
' in App (file://ReactFlightDOMNode-test.js:1432:25)\n'+
1553+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13731554
);
13741555
}else{
13751556
expect(
@@ -1378,7 +1559,7 @@ describe('ReactFlightDOMNode', () => {
13781559
'\n'+
13791560
' in body\n'+
13801561
' in html\n'+
1381-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1562+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13821563
);
13831564
}
13841565

@@ -1388,16 +1569,16 @@ describe('ReactFlightDOMNode', () => {
13881569
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13891570
).toBe(
13901571
'\n'+
1391-
' in Dynamic (file://ReactFlightDOMNode-test.js:1238:27)\n'+
1392-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1572+
' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n'+
1573+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
13931574
);
13941575
}else{
13951576
expect(
13961577
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13971578
).toBe(
13981579
''+
13991580
'\n'+
1400-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1581+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
14011582
);
14021583
}
14031584
}else{

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,14 @@ import assign from 'shared/assign';
190190
importnoopfrom'shared/noop';
191191
importgetComponentNameFromTypefrom'shared/getComponentNameFromType';
192192
importisArrayfrom'shared/isArray';
193-
import{SuspenseException,getSuspendedThenable}from'./ReactFizzThenable';
193+
import{
194+
SuspenseException,
195+
getSuspendedThenable,
196+
ensureSuspendableThenableStateDEV,
197+
getSuspendedCallSiteStackDEV,
198+
getSuspendedCallSiteDebugTaskDEV,
199+
setCaptureSuspendedCallSiteDEV,
200+
}from'./ReactFizzThenable';
194201

195202
// Linked list representing the identity of a component given the component/tag name and key.
196203
// The name might be minified but we assume that it's going to be the same generated name. Typically
@@ -355,6 +362,7 @@ const OPEN = 11;
355362
constABORTING=12;
356363
constCLOSING=13;
357364
constCLOSED=14;
365+
constSTALLED_DEV=15;
358366

359367
exportopaquetypeRequest={
360368
destination: null|Destination,
@@ -363,7 +371,7 @@ export opaque type Request = {
363371
+renderState: RenderState,
364372
+rootFormatContext: FormatContext,
365373
+progressiveChunkSize: number,
366-
status: 10|11|12|13|14,
374+
status: 10|11|12|13|14|15,
367375
fatalError: mixed,
368376
nextSegmentId: number,
369377
allPendingTasks: number,// when it reaches zero, we can close the connection.
@@ -1023,6 +1031,89 @@ function pushHaltedAwaitOnComponentStack(
10231031
}
10241032
}
10251033

1034+
// performWork + retryTask without mutation
1035+
functionrerenderStalledTask(request: Request,task: Task): void{
1036+
constprevStatus=request.status;
1037+
request.status=STALLED_DEV;
1038+
1039+
constprevContext=getActiveContext();
1040+
constprevDispatcher=ReactSharedInternals.H;
1041+
ReactSharedInternals.H=HooksDispatcher;
1042+
constprevAsyncDispatcher=ReactSharedInternals.A;
1043+
ReactSharedInternals.A=DefaultAsyncDispatcher;
1044+
1045+
constprevRequest=currentRequest;
1046+
currentRequest=request;
1047+
1048+
constprevGetCurrentStackImpl=ReactSharedInternals.getCurrentStack;
1049+
ReactSharedInternals.getCurrentStack=getCurrentStackInDEV;
1050+
1051+
constprevResumableState=currentResumableState;
1052+
setCurrentResumableState(request.resumableState);
1053+
switchContext(task.context);
1054+
constprevTaskInDEV=currentTaskInDEV;
1055+
setCurrentTaskInDEV(task);
1056+
try{
1057+
retryNode(request,task);
1058+
}catch(x){
1059+
// Suspended again.
1060+
resetHooksState();
1061+
}finally{
1062+
setCurrentTaskInDEV(prevTaskInDEV);
1063+
setCurrentResumableState(prevResumableState);
1064+
1065+
ReactSharedInternals.H =prevDispatcher;
1066+
ReactSharedInternals.A=prevAsyncDispatcher;
1067+
1068+
ReactSharedInternals.getCurrentStack=prevGetCurrentStackImpl;
1069+
if(prevDispatcher===HooksDispatcher){
1070+
// This means that we were in a reentrant work loop. This could happen
1071+
// in a renderer that supports synchronous work like renderToString,
1072+
// when it's called from within another renderer.
1073+
// Normally we don't bother switching the contexts to their root/default
1074+
// values when leaving because we'll likely need the same or similar
1075+
// context again. However, when we're inside a synchronous loop like this
1076+
// we'll to restore the context to what it was before returning.
1077+
switchContext(prevContext);
1078+
}
1079+
currentRequest =prevRequest;
1080+
request.status=prevStatus;
1081+
}
1082+
}
1083+
1084+
functionpushSuspendedCallSiteOnComponentStack(
1085+
request: Request,
1086+
task: Task,
1087+
): void{
1088+
setCaptureSuspendedCallSiteDEV(true);
1089+
constrestoreThenableState=ensureSuspendableThenableStateDEV(
1090+
// refined at the callsite
1091+
((task.thenableState: any): ThenableState),
1092+
);
1093+
try{
1094+
rerenderStalledTask(request,task);
1095+
}finally{
1096+
restoreThenableState();
1097+
setCaptureSuspendedCallSiteDEV(false);
1098+
}
1099+
1100+
const suspendCallSiteStack =getSuspendedCallSiteStackDEV();
1101+
constsuspendCallSiteDebugTask=getSuspendedCallSiteDebugTaskDEV();
1102+
1103+
if(suspendCallSiteStack!==null){
1104+
const ownerStack =task.componentStack;
1105+
task.componentStack={
1106+
// The owner of the suspended call site would be the owner of this task.
1107+
// We need the task itself otherwise we'd miss a frame.
1108+
owner: ownerStack,
1109+
parent: suspendCallSiteStack.parent,
1110+
stack: suspendCallSiteStack.stack,
1111+
type: suspendCallSiteStack.type,
1112+
};
1113+
}
1114+
task.debugTask=suspendCallSiteDebugTask;
1115+
}
1116+
10261117
functionpushServerComponentStack(
10271118
task: Task,
10281119
debugInfo: void|null|ReactDebugInfo,
@@ -2723,7 +2814,12 @@ function renderLazyComponent(
27232814
constinit=lazyComponent._init;
27242815
Component=init(payload);
27252816
}
2726-
if(request.status===ABORTING){
2817+
if(
2818+
request.status===ABORTING&&
2819+
// We're going to discard this render anyway.
2820+
// We just need to reach the point where we suspended in dev.
2821+
(!__DEV__||request.status!==STALLED_DEV)
2822+
){
27272823
// eslint-disable-next-line no-throw-literal
27282824
thrownull;
27292825
}
@@ -4535,12 +4631,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
45354631
debugInfo=node._debugInfo;
45364632
}
45374633
pushHaltedAwaitOnComponentStack(task,debugInfo);
4538-
/*
45394634
if(task.thenableState!==null){
4540-
// TODO: If we were stalled inside use() of a Client Component then we should
4541-
// rerender to get the stack trace from the use() call.
4635+
pushSuspendedCallSiteOnComponentStack(request,task);
45424636
}
4543-
*/
45444637
}
45454638
}
45464639

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [Fizz] Push a stalled use() to the ownerStack/debugTask (#35226) · react/react@41b3e9a · GitHub
Skip to content

Commit 41b3e9a

Browse files
authored
[Fizz] Push a stalled use() to the ownerStack/debugTask (#35226)
1 parent 195fd22 commit 41b3e9a

4 files changed

Lines changed: 527 additions & 27 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 188 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,28 @@ describe('ReactFlightDOMNode', () => {
108108
);
109109
}
110110

111+
/**
112+
* Removes all stackframes not pointing into this file
113+
*/
114+
functionignoreListStack(str){
115+
if(!str){
116+
returnstr;
117+
}
118+
119+
letignoreListedStack='';
120+
constlines=str.split('\n');
121+
122+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
123+
for(constlineoflines){
124+
if(line.indexOf(__filename)===-1){
125+
}else{
126+
ignoreListedStack+='\n'+line.replace(__dirname,'.');
127+
}
128+
}
129+
130+
returnignoreListedStack;
131+
}
132+
111133
functionreadResult(stream){
112134
returnnewPromise((resolve,reject)=>{
113135
letbuffer='';
@@ -784,6 +806,165 @@ describe('ReactFlightDOMNode', () => {
784806
}
785807
});
786808

809+
// @gate enableHalt
810+
it('includes source locations in component and owner stacks for halted Client components',async()=>{
811+
functionSharedComponent({p1, p2, p3}){
812+
use(p1);
813+
use(p2);
814+
use(p3);
815+
return<div>Hello, Dave!</div>;
816+
}
817+
constClientComponentOnTheServer=clientExports(SharedComponent);
818+
constClientComponentOnTheClient=clientExports(
819+
SharedComponent,
820+
123,
821+
'path/to/chunk.js',
822+
);
823+
824+
letresolvePendingPromise;
825+
functionServerComponent(){
826+
constp1=Promise.resolve();
827+
constp2=newPromise(resolve=>{
828+
resolvePendingPromise=value=>{
829+
p2.status='fulfilled';
830+
p2.value=value;
831+
resolve(value);
832+
};
833+
});
834+
constp3=newPromise(()=>{});
835+
returnReactServer.createElement(ClientComponentOnTheClient,{
836+
p1: p1,
837+
p2: p2,
838+
p3: p3,
839+
});
840+
}
841+
842+
functionApp(){
843+
returnReactServer.createElement(
844+
'html',
845+
null,
846+
ReactServer.createElement(
847+
'body',
848+
null,
849+
ReactServer.createElement(
850+
ReactServer.Suspense,
851+
{fallback: 'Loading...'},
852+
ReactServer.createElement(ServerComponent,null),
853+
),
854+
),
855+
);
856+
}
857+
858+
consterrors=[];
859+
constrscStream=awaitserverAct(()=>
860+
ReactServerDOMServer.renderToPipeableStream(
861+
ReactServer.createElement(App,null),
862+
webpackMap,
863+
),
864+
);
865+
866+
constreadable=newStream.PassThrough(streamOptions);
867+
rscStream.pipe(readable);
868+
869+
functionClientRoot({response}){
870+
returnuse(response);
871+
}
872+
873+
constserverConsumerManifest={
874+
moduleMap: {
875+
[webpackMap[ClientComponentOnTheClient.$$id].id]: {
876+
'*': webpackMap[ClientComponentOnTheServer.$$id],
877+
},
878+
},
879+
moduleLoading: webpackModuleLoading,
880+
};
881+
882+
expect(errors).toEqual([]);
883+
884+
functionClientRoot({response}){
885+
returnuse(response);
886+
}
887+
888+
constresponse=ReactServerDOMClient.createFromNodeStream(
889+
readable,
890+
serverConsumerManifest,
891+
);
892+
893+
letcomponentStack;
894+
letownerStack;
895+
896+
constclientAbortController=newAbortController();
897+
898+
constfizzPrerenderStreamResult=ReactDOMFizzStatic.prerender(
899+
React.createElement(ClientRoot,{response}),
900+
{
901+
signal: clientAbortController.signal,
902+
onError(error,errorInfo){
903+
componentStack=errorInfo.componentStack;
904+
ownerStack=React.captureOwnerStack
905+
? React.captureOwnerStack()
906+
: null;
907+
},
908+
},
909+
);
910+
911+
resolvePendingPromise('custom-instrum-resolve');
912+
awaitserverAct(
913+
async()=>
914+
newPromise(resolve=>{
915+
setImmediate(()=>{
916+
clientAbortController.abort();
917+
resolve();
918+
});
919+
}),
920+
);
921+
922+
constfizzPrerenderStream=awaitfizzPrerenderStreamResult;
923+
constprerenderHTML=awaitreadWebResult(fizzPrerenderStream.prelude);
924+
925+
expect(prerenderHTML).toContain('Loading...');
926+
927+
if(__DEV__){
928+
expect(normalizeCodeLocInfo(componentStack)).toBe(
929+
'\n'+
930+
' in SharedComponent (at **)\n'+
931+
' in ServerComponent'+
932+
(gate(flags=>flags.enableAsyncDebugInfo) ? ' (at **)' : '')+
933+
'\n'+
934+
' in Suspense\n'+
935+
' in body\n'+
936+
' in html\n'+
937+
' in App (at **)\n'+
938+
' in ClientRoot (at **)',
939+
);
940+
}else{
941+
expect(normalizeCodeLocInfo(componentStack)).toBe(
942+
'\n'+
943+
' in SharedComponent (at **)\n'+
944+
' in Suspense\n'+
945+
' in body\n'+
946+
' in html\n'+
947+
' in ClientRoot (at **)',
948+
);
949+
}
950+
951+
if(__DEV__){
952+
expect(ignoreListStack(ownerStack)).toBe(
953+
// eslint-disable-next-line react-internal/safe-string-coercion
954+
''+
955+
// The concrete location may change as this test is updated.
956+
// Just make sure they still point at React.use(p2)
957+
(gate(flags=>flags.enableAsyncDebugInfo)
958+
? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)'
959+
: '')+
960+
'\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)'+
961+
'\n at App (file://./ReactFlightDOMNode-test.js:852:25)',
962+
);
963+
}else{
964+
expect(ownerStack).toBeNull();
965+
}
966+
});
967+
787968
// @gate enableHalt
788969
it('includes deeper location for aborted stacks',async()=>{
789970
asyncfunctiongetData(){
@@ -1364,12 +1545,12 @@ describe('ReactFlightDOMNode', () => {
13641545
'\n'+
13651546
' in Dynamic'+
13661547
(gate(flags=>flags.enableAsyncDebugInfo)
1367-
? ' (file://ReactFlightDOMNode-test.js:1238:27)\n'
1548+
? ' (file://ReactFlightDOMNode-test.js:1419:27)\n'
13681549
: '\n')+
13691550
' in body\n'+
13701551
' in html\n'+
1371-
' in App (file://ReactFlightDOMNode-test.js:1251:25)\n'+
1372-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1552+
' in App (file://ReactFlightDOMNode-test.js:1432:25)\n'+
1553+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13731554
);
13741555
}else{
13751556
expect(
@@ -1378,7 +1559,7 @@ describe('ReactFlightDOMNode', () => {
13781559
'\n'+
13791560
' in body\n'+
13801561
' in html\n'+
1381-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1562+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13821563
);
13831564
}
13841565

@@ -1388,16 +1569,16 @@ describe('ReactFlightDOMNode', () => {
13881569
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13891570
).toBe(
13901571
'\n'+
1391-
' in Dynamic (file://ReactFlightDOMNode-test.js:1238:27)\n'+
1392-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1572+
' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n'+
1573+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
13931574
);
13941575
}else{
13951576
expect(
13961577
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13971578
).toBe(
13981579
''+
13991580
'\n'+
1400-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1581+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
14011582
);
14021583
}
14031584
}else{

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,14 @@ import assign from 'shared/assign';
190190
importnoopfrom'shared/noop';
191191
importgetComponentNameFromTypefrom'shared/getComponentNameFromType';
192192
importisArrayfrom'shared/isArray';
193-
import{SuspenseException,getSuspendedThenable}from'./ReactFizzThenable';
193+
import{
194+
SuspenseException,
195+
getSuspendedThenable,
196+
ensureSuspendableThenableStateDEV,
197+
getSuspendedCallSiteStackDEV,
198+
getSuspendedCallSiteDebugTaskDEV,
199+
setCaptureSuspendedCallSiteDEV,
200+
}from'./ReactFizzThenable';
194201

195202
// Linked list representing the identity of a component given the component/tag name and key.
196203
// The name might be minified but we assume that it's going to be the same generated name. Typically
@@ -355,6 +362,7 @@ const OPEN = 11;
355362
constABORTING=12;
356363
constCLOSING=13;
357364
constCLOSED=14;
365+
constSTALLED_DEV=15;
358366

359367
exportopaquetypeRequest={
360368
destination: null|Destination,
@@ -363,7 +371,7 @@ export opaque type Request = {
363371
+renderState: RenderState,
364372
+rootFormatContext: FormatContext,
365373
+progressiveChunkSize: number,
366-
status: 10|11|12|13|14,
374+
status: 10|11|12|13|14|15,
367375
fatalError: mixed,
368376
nextSegmentId: number,
369377
allPendingTasks: number,// when it reaches zero, we can close the connection.
@@ -1023,6 +1031,89 @@ function pushHaltedAwaitOnComponentStack(
10231031
}
10241032
}
10251033

1034+
// performWork + retryTask without mutation
1035+
functionrerenderStalledTask(request: Request,task: Task): void{
1036+
constprevStatus=request.status;
1037+
request.status=STALLED_DEV;
1038+
1039+
constprevContext=getActiveContext();
1040+
constprevDispatcher=ReactSharedInternals.H;
1041+
ReactSharedInternals.H=HooksDispatcher;
1042+
constprevAsyncDispatcher=ReactSharedInternals.A;
1043+
ReactSharedInternals.A=DefaultAsyncDispatcher;
1044+
1045+
constprevRequest=currentRequest;
1046+
currentRequest=request;
1047+
1048+
constprevGetCurrentStackImpl=ReactSharedInternals.getCurrentStack;
1049+
ReactSharedInternals.getCurrentStack=getCurrentStackInDEV;
1050+
1051+
constprevResumableState=currentResumableState;
1052+
setCurrentResumableState(request.resumableState);
1053+
switchContext(task.context);
1054+
constprevTaskInDEV=currentTaskInDEV;
1055+
setCurrentTaskInDEV(task);
1056+
try{
1057+
retryNode(request,task);
1058+
}catch(x){
1059+
// Suspended again.
1060+
resetHooksState();
1061+
}finally{
1062+
setCurrentTaskInDEV(prevTaskInDEV);
1063+
setCurrentResumableState(prevResumableState);
1064+
1065+
ReactSharedInternals.H =prevDispatcher;
1066+
ReactSharedInternals.A=prevAsyncDispatcher;
1067+
1068+
ReactSharedInternals.getCurrentStack=prevGetCurrentStackImpl;
1069+
if(prevDispatcher===HooksDispatcher){
1070+
// This means that we were in a reentrant work loop. This could happen
1071+
// in a renderer that supports synchronous work like renderToString,
1072+
// when it's called from within another renderer.
1073+
// Normally we don't bother switching the contexts to their root/default
1074+
// values when leaving because we'll likely need the same or similar
1075+
// context again. However, when we're inside a synchronous loop like this
1076+
// we'll to restore the context to what it was before returning.
1077+
switchContext(prevContext);
1078+
}
1079+
currentRequest =prevRequest;
1080+
request.status=prevStatus;
1081+
}
1082+
}
1083+
1084+
functionpushSuspendedCallSiteOnComponentStack(
1085+
request: Request,
1086+
task: Task,
1087+
): void{
1088+
setCaptureSuspendedCallSiteDEV(true);
1089+
constrestoreThenableState=ensureSuspendableThenableStateDEV(
1090+
// refined at the callsite
1091+
((task.thenableState: any): ThenableState),
1092+
);
1093+
try{
1094+
rerenderStalledTask(request,task);
1095+
}finally{
1096+
restoreThenableState();
1097+
setCaptureSuspendedCallSiteDEV(false);
1098+
}
1099+
1100+
const suspendCallSiteStack =getSuspendedCallSiteStackDEV();
1101+
constsuspendCallSiteDebugTask=getSuspendedCallSiteDebugTaskDEV();
1102+
1103+
if(suspendCallSiteStack!==null){
1104+
const ownerStack =task.componentStack;
1105+
task.componentStack={
1106+
// The owner of the suspended call site would be the owner of this task.
1107+
// We need the task itself otherwise we'd miss a frame.
1108+
owner: ownerStack,
1109+
parent: suspendCallSiteStack.parent,
1110+
stack: suspendCallSiteStack.stack,
1111+
type: suspendCallSiteStack.type,
1112+
};
1113+
}
1114+
task.debugTask=suspendCallSiteDebugTask;
1115+
}
1116+
10261117
functionpushServerComponentStack(
10271118
task: Task,
10281119
debugInfo: void|null|ReactDebugInfo,
@@ -2723,7 +2814,12 @@ function renderLazyComponent(
27232814
constinit=lazyComponent._init;
27242815
Component=init(payload);
27252816
}
2726-
if(request.status===ABORTING){
2817+
if(
2818+
request.status===ABORTING&&
2819+
// We're going to discard this render anyway.
2820+
// We just need to reach the point where we suspended in dev.
2821+
(!__DEV__||request.status!==STALLED_DEV)
2822+
){
27272823
// eslint-disable-next-line no-throw-literal
27282824
thrownull;
27292825
}
@@ -4535,12 +4631,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
45354631
debugInfo=node._debugInfo;
45364632
}
45374633
pushHaltedAwaitOnComponentStack(task,debugInfo);
4538-
/*
45394634
if(task.thenableState!==null){
4540-
// TODO: If we were stalled inside use() of a Client Component then we should
4541-
// rerender to get the stack trace from the use() call.
4635+
pushSuspendedCallSiteOnComponentStack(request,task);
45424636
}
4543-
*/
45444637
}
45454638
}
45464639

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [Fizz] Push a stalled use() to the ownerStack/debugTask (#35226) · react/react@41b3e9a · GitHub
Skip to content

Commit 41b3e9a

Browse files
authored
[Fizz] Push a stalled use() to the ownerStack/debugTask (#35226)
1 parent 195fd22 commit 41b3e9a

4 files changed

Lines changed: 527 additions & 27 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 188 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,28 @@ describe('ReactFlightDOMNode', () => {
108108
);
109109
}
110110

111+
/**
112+
* Removes all stackframes not pointing into this file
113+
*/
114+
functionignoreListStack(str){
115+
if(!str){
116+
returnstr;
117+
}
118+
119+
letignoreListedStack='';
120+
constlines=str.split('\n');
121+
122+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
123+
for(constlineoflines){
124+
if(line.indexOf(__filename)===-1){
125+
}else{
126+
ignoreListedStack+='\n'+line.replace(__dirname,'.');
127+
}
128+
}
129+
130+
returnignoreListedStack;
131+
}
132+
111133
functionreadResult(stream){
112134
returnnewPromise((resolve,reject)=>{
113135
letbuffer='';
@@ -784,6 +806,165 @@ describe('ReactFlightDOMNode', () => {
784806
}
785807
});
786808

809+
// @gate enableHalt
810+
it('includes source locations in component and owner stacks for halted Client components',async()=>{
811+
functionSharedComponent({p1, p2, p3}){
812+
use(p1);
813+
use(p2);
814+
use(p3);
815+
return<div>Hello, Dave!</div>;
816+
}
817+
constClientComponentOnTheServer=clientExports(SharedComponent);
818+
constClientComponentOnTheClient=clientExports(
819+
SharedComponent,
820+
123,
821+
'path/to/chunk.js',
822+
);
823+
824+
letresolvePendingPromise;
825+
functionServerComponent(){
826+
constp1=Promise.resolve();
827+
constp2=newPromise(resolve=>{
828+
resolvePendingPromise=value=>{
829+
p2.status='fulfilled';
830+
p2.value=value;
831+
resolve(value);
832+
};
833+
});
834+
constp3=newPromise(()=>{});
835+
returnReactServer.createElement(ClientComponentOnTheClient,{
836+
p1: p1,
837+
p2: p2,
838+
p3: p3,
839+
});
840+
}
841+
842+
functionApp(){
843+
returnReactServer.createElement(
844+
'html',
845+
null,
846+
ReactServer.createElement(
847+
'body',
848+
null,
849+
ReactServer.createElement(
850+
ReactServer.Suspense,
851+
{fallback: 'Loading...'},
852+
ReactServer.createElement(ServerComponent,null),
853+
),
854+
),
855+
);
856+
}
857+
858+
consterrors=[];
859+
constrscStream=awaitserverAct(()=>
860+
ReactServerDOMServer.renderToPipeableStream(
861+
ReactServer.createElement(App,null),
862+
webpackMap,
863+
),
864+
);
865+
866+
constreadable=newStream.PassThrough(streamOptions);
867+
rscStream.pipe(readable);
868+
869+
functionClientRoot({response}){
870+
returnuse(response);
871+
}
872+
873+
constserverConsumerManifest={
874+
moduleMap: {
875+
[webpackMap[ClientComponentOnTheClient.$$id].id]: {
876+
'*': webpackMap[ClientComponentOnTheServer.$$id],
877+
},
878+
},
879+
moduleLoading: webpackModuleLoading,
880+
};
881+
882+
expect(errors).toEqual([]);
883+
884+
functionClientRoot({response}){
885+
returnuse(response);
886+
}
887+
888+
constresponse=ReactServerDOMClient.createFromNodeStream(
889+
readable,
890+
serverConsumerManifest,
891+
);
892+
893+
letcomponentStack;
894+
letownerStack;
895+
896+
constclientAbortController=newAbortController();
897+
898+
constfizzPrerenderStreamResult=ReactDOMFizzStatic.prerender(
899+
React.createElement(ClientRoot,{response}),
900+
{
901+
signal: clientAbortController.signal,
902+
onError(error,errorInfo){
903+
componentStack=errorInfo.componentStack;
904+
ownerStack=React.captureOwnerStack
905+
? React.captureOwnerStack()
906+
: null;
907+
},
908+
},
909+
);
910+
911+
resolvePendingPromise('custom-instrum-resolve');
912+
awaitserverAct(
913+
async()=>
914+
newPromise(resolve=>{
915+
setImmediate(()=>{
916+
clientAbortController.abort();
917+
resolve();
918+
});
919+
}),
920+
);
921+
922+
constfizzPrerenderStream=awaitfizzPrerenderStreamResult;
923+
constprerenderHTML=awaitreadWebResult(fizzPrerenderStream.prelude);
924+
925+
expect(prerenderHTML).toContain('Loading...');
926+
927+
if(__DEV__){
928+
expect(normalizeCodeLocInfo(componentStack)).toBe(
929+
'\n'+
930+
' in SharedComponent (at **)\n'+
931+
' in ServerComponent'+
932+
(gate(flags=>flags.enableAsyncDebugInfo) ? ' (at **)' : '')+
933+
'\n'+
934+
' in Suspense\n'+
935+
' in body\n'+
936+
' in html\n'+
937+
' in App (at **)\n'+
938+
' in ClientRoot (at **)',
939+
);
940+
}else{
941+
expect(normalizeCodeLocInfo(componentStack)).toBe(
942+
'\n'+
943+
' in SharedComponent (at **)\n'+
944+
' in Suspense\n'+
945+
' in body\n'+
946+
' in html\n'+
947+
' in ClientRoot (at **)',
948+
);
949+
}
950+
951+
if(__DEV__){
952+
expect(ignoreListStack(ownerStack)).toBe(
953+
// eslint-disable-next-line react-internal/safe-string-coercion
954+
''+
955+
// The concrete location may change as this test is updated.
956+
// Just make sure they still point at React.use(p2)
957+
(gate(flags=>flags.enableAsyncDebugInfo)
958+
? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)'
959+
: '')+
960+
'\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)'+
961+
'\n at App (file://./ReactFlightDOMNode-test.js:852:25)',
962+
);
963+
}else{
964+
expect(ownerStack).toBeNull();
965+
}
966+
});
967+
787968
// @gate enableHalt
788969
it('includes deeper location for aborted stacks',async()=>{
789970
asyncfunctiongetData(){
@@ -1364,12 +1545,12 @@ describe('ReactFlightDOMNode', () => {
13641545
'\n'+
13651546
' in Dynamic'+
13661547
(gate(flags=>flags.enableAsyncDebugInfo)
1367-
? ' (file://ReactFlightDOMNode-test.js:1238:27)\n'
1548+
? ' (file://ReactFlightDOMNode-test.js:1419:27)\n'
13681549
: '\n')+
13691550
' in body\n'+
13701551
' in html\n'+
1371-
' in App (file://ReactFlightDOMNode-test.js:1251:25)\n'+
1372-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1552+
' in App (file://ReactFlightDOMNode-test.js:1432:25)\n'+
1553+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13731554
);
13741555
}else{
13751556
expect(
@@ -1378,7 +1559,7 @@ describe('ReactFlightDOMNode', () => {
13781559
'\n'+
13791560
' in body\n'+
13801561
' in html\n'+
1381-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1562+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13821563
);
13831564
}
13841565

@@ -1388,16 +1569,16 @@ describe('ReactFlightDOMNode', () => {
13881569
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13891570
).toBe(
13901571
'\n'+
1391-
' in Dynamic (file://ReactFlightDOMNode-test.js:1238:27)\n'+
1392-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1572+
' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n'+
1573+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
13931574
);
13941575
}else{
13951576
expect(
13961577
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13971578
).toBe(
13981579
''+
13991580
'\n'+
1400-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1581+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
14011582
);
14021583
}
14031584
}else{

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,14 @@ import assign from 'shared/assign';
190190
importnoopfrom'shared/noop';
191191
importgetComponentNameFromTypefrom'shared/getComponentNameFromType';
192192
importisArrayfrom'shared/isArray';
193-
import{SuspenseException,getSuspendedThenable}from'./ReactFizzThenable';
193+
import{
194+
SuspenseException,
195+
getSuspendedThenable,
196+
ensureSuspendableThenableStateDEV,
197+
getSuspendedCallSiteStackDEV,
198+
getSuspendedCallSiteDebugTaskDEV,
199+
setCaptureSuspendedCallSiteDEV,
200+
}from'./ReactFizzThenable';
194201

195202
// Linked list representing the identity of a component given the component/tag name and key.
196203
// The name might be minified but we assume that it's going to be the same generated name. Typically
@@ -355,6 +362,7 @@ const OPEN = 11;
355362
constABORTING=12;
356363
constCLOSING=13;
357364
constCLOSED=14;
365+
constSTALLED_DEV=15;
358366

359367
exportopaquetypeRequest={
360368
destination: null|Destination,
@@ -363,7 +371,7 @@ export opaque type Request = {
363371
+renderState: RenderState,
364372
+rootFormatContext: FormatContext,
365373
+progressiveChunkSize: number,
366-
status: 10|11|12|13|14,
374+
status: 10|11|12|13|14|15,
367375
fatalError: mixed,
368376
nextSegmentId: number,
369377
allPendingTasks: number,// when it reaches zero, we can close the connection.
@@ -1023,6 +1031,89 @@ function pushHaltedAwaitOnComponentStack(
10231031
}
10241032
}
10251033

1034+
// performWork + retryTask without mutation
1035+
functionrerenderStalledTask(request: Request,task: Task): void{
1036+
constprevStatus=request.status;
1037+
request.status=STALLED_DEV;
1038+
1039+
constprevContext=getActiveContext();
1040+
constprevDispatcher=ReactSharedInternals.H;
1041+
ReactSharedInternals.H=HooksDispatcher;
1042+
constprevAsyncDispatcher=ReactSharedInternals.A;
1043+
ReactSharedInternals.A=DefaultAsyncDispatcher;
1044+
1045+
constprevRequest=currentRequest;
1046+
currentRequest=request;
1047+
1048+
constprevGetCurrentStackImpl=ReactSharedInternals.getCurrentStack;
1049+
ReactSharedInternals.getCurrentStack=getCurrentStackInDEV;
1050+
1051+
constprevResumableState=currentResumableState;
1052+
setCurrentResumableState(request.resumableState);
1053+
switchContext(task.context);
1054+
constprevTaskInDEV=currentTaskInDEV;
1055+
setCurrentTaskInDEV(task);
1056+
try{
1057+
retryNode(request,task);
1058+
}catch(x){
1059+
// Suspended again.
1060+
resetHooksState();
1061+
}finally{
1062+
setCurrentTaskInDEV(prevTaskInDEV);
1063+
setCurrentResumableState(prevResumableState);
1064+
1065+
ReactSharedInternals.H =prevDispatcher;
1066+
ReactSharedInternals.A=prevAsyncDispatcher;
1067+
1068+
ReactSharedInternals.getCurrentStack=prevGetCurrentStackImpl;
1069+
if(prevDispatcher===HooksDispatcher){
1070+
// This means that we were in a reentrant work loop. This could happen
1071+
// in a renderer that supports synchronous work like renderToString,
1072+
// when it's called from within another renderer.
1073+
// Normally we don't bother switching the contexts to their root/default
1074+
// values when leaving because we'll likely need the same or similar
1075+
// context again. However, when we're inside a synchronous loop like this
1076+
// we'll to restore the context to what it was before returning.
1077+
switchContext(prevContext);
1078+
}
1079+
currentRequest =prevRequest;
1080+
request.status=prevStatus;
1081+
}
1082+
}
1083+
1084+
functionpushSuspendedCallSiteOnComponentStack(
1085+
request: Request,
1086+
task: Task,
1087+
): void{
1088+
setCaptureSuspendedCallSiteDEV(true);
1089+
constrestoreThenableState=ensureSuspendableThenableStateDEV(
1090+
// refined at the callsite
1091+
((task.thenableState: any): ThenableState),
1092+
);
1093+
try{
1094+
rerenderStalledTask(request,task);
1095+
}finally{
1096+
restoreThenableState();
1097+
setCaptureSuspendedCallSiteDEV(false);
1098+
}
1099+
1100+
const suspendCallSiteStack =getSuspendedCallSiteStackDEV();
1101+
constsuspendCallSiteDebugTask=getSuspendedCallSiteDebugTaskDEV();
1102+
1103+
if(suspendCallSiteStack!==null){
1104+
const ownerStack =task.componentStack;
1105+
task.componentStack={
1106+
// The owner of the suspended call site would be the owner of this task.
1107+
// We need the task itself otherwise we'd miss a frame.
1108+
owner: ownerStack,
1109+
parent: suspendCallSiteStack.parent,
1110+
stack: suspendCallSiteStack.stack,
1111+
type: suspendCallSiteStack.type,
1112+
};
1113+
}
1114+
task.debugTask=suspendCallSiteDebugTask;
1115+
}
1116+
10261117
functionpushServerComponentStack(
10271118
task: Task,
10281119
debugInfo: void|null|ReactDebugInfo,
@@ -2723,7 +2814,12 @@ function renderLazyComponent(
27232814
constinit=lazyComponent._init;
27242815
Component=init(payload);
27252816
}
2726-
if(request.status===ABORTING){
2817+
if(
2818+
request.status===ABORTING&&
2819+
// We're going to discard this render anyway.
2820+
// We just need to reach the point where we suspended in dev.
2821+
(!__DEV__||request.status!==STALLED_DEV)
2822+
){
27272823
// eslint-disable-next-line no-throw-literal
27282824
thrownull;
27292825
}
@@ -4535,12 +4631,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
45354631
debugInfo=node._debugInfo;
45364632
}
45374633
pushHaltedAwaitOnComponentStack(task,debugInfo);
4538-
/*
45394634
if(task.thenableState!==null){
4540-
// TODO: If we were stalled inside use() of a Client Component then we should
4541-
// rerender to get the stack trace from the use() call.
4635+
pushSuspendedCallSiteOnComponentStack(request,task);
45424636
}
4543-
*/
45444637
}
45454638
}
45464639

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [Fizz] Push a stalled use() to the ownerStack/debugTask (#35226) · react/react@41b3e9a · GitHub
Skip to content

Commit 41b3e9a

Browse files
authored
[Fizz] Push a stalled use() to the ownerStack/debugTask (#35226)
1 parent 195fd22 commit 41b3e9a

4 files changed

Lines changed: 527 additions & 27 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 188 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,28 @@ describe('ReactFlightDOMNode', () => {
108108
);
109109
}
110110

111+
/**
112+
* Removes all stackframes not pointing into this file
113+
*/
114+
functionignoreListStack(str){
115+
if(!str){
116+
returnstr;
117+
}
118+
119+
letignoreListedStack='';
120+
constlines=str.split('\n');
121+
122+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
123+
for(constlineoflines){
124+
if(line.indexOf(__filename)===-1){
125+
}else{
126+
ignoreListedStack+='\n'+line.replace(__dirname,'.');
127+
}
128+
}
129+
130+
returnignoreListedStack;
131+
}
132+
111133
functionreadResult(stream){
112134
returnnewPromise((resolve,reject)=>{
113135
letbuffer='';
@@ -784,6 +806,165 @@ describe('ReactFlightDOMNode', () => {
784806
}
785807
});
786808

809+
// @gate enableHalt
810+
it('includes source locations in component and owner stacks for halted Client components',async()=>{
811+
functionSharedComponent({p1, p2, p3}){
812+
use(p1);
813+
use(p2);
814+
use(p3);
815+
return<div>Hello, Dave!</div>;
816+
}
817+
constClientComponentOnTheServer=clientExports(SharedComponent);
818+
constClientComponentOnTheClient=clientExports(
819+
SharedComponent,
820+
123,
821+
'path/to/chunk.js',
822+
);
823+
824+
letresolvePendingPromise;
825+
functionServerComponent(){
826+
constp1=Promise.resolve();
827+
constp2=newPromise(resolve=>{
828+
resolvePendingPromise=value=>{
829+
p2.status='fulfilled';
830+
p2.value=value;
831+
resolve(value);
832+
};
833+
});
834+
constp3=newPromise(()=>{});
835+
returnReactServer.createElement(ClientComponentOnTheClient,{
836+
p1: p1,
837+
p2: p2,
838+
p3: p3,
839+
});
840+
}
841+
842+
functionApp(){
843+
returnReactServer.createElement(
844+
'html',
845+
null,
846+
ReactServer.createElement(
847+
'body',
848+
null,
849+
ReactServer.createElement(
850+
ReactServer.Suspense,
851+
{fallback: 'Loading...'},
852+
ReactServer.createElement(ServerComponent,null),
853+
),
854+
),
855+
);
856+
}
857+
858+
consterrors=[];
859+
constrscStream=awaitserverAct(()=>
860+
ReactServerDOMServer.renderToPipeableStream(
861+
ReactServer.createElement(App,null),
862+
webpackMap,
863+
),
864+
);
865+
866+
constreadable=newStream.PassThrough(streamOptions);
867+
rscStream.pipe(readable);
868+
869+
functionClientRoot({response}){
870+
returnuse(response);
871+
}
872+
873+
constserverConsumerManifest={
874+
moduleMap: {
875+
[webpackMap[ClientComponentOnTheClient.$$id].id]: {
876+
'*': webpackMap[ClientComponentOnTheServer.$$id],
877+
},
878+
},
879+
moduleLoading: webpackModuleLoading,
880+
};
881+
882+
expect(errors).toEqual([]);
883+
884+
functionClientRoot({response}){
885+
returnuse(response);
886+
}
887+
888+
constresponse=ReactServerDOMClient.createFromNodeStream(
889+
readable,
890+
serverConsumerManifest,
891+
);
892+
893+
letcomponentStack;
894+
letownerStack;
895+
896+
constclientAbortController=newAbortController();
897+
898+
constfizzPrerenderStreamResult=ReactDOMFizzStatic.prerender(
899+
React.createElement(ClientRoot,{response}),
900+
{
901+
signal: clientAbortController.signal,
902+
onError(error,errorInfo){
903+
componentStack=errorInfo.componentStack;
904+
ownerStack=React.captureOwnerStack
905+
? React.captureOwnerStack()
906+
: null;
907+
},
908+
},
909+
);
910+
911+
resolvePendingPromise('custom-instrum-resolve');
912+
awaitserverAct(
913+
async()=>
914+
newPromise(resolve=>{
915+
setImmediate(()=>{
916+
clientAbortController.abort();
917+
resolve();
918+
});
919+
}),
920+
);
921+
922+
constfizzPrerenderStream=awaitfizzPrerenderStreamResult;
923+
constprerenderHTML=awaitreadWebResult(fizzPrerenderStream.prelude);
924+
925+
expect(prerenderHTML).toContain('Loading...');
926+
927+
if(__DEV__){
928+
expect(normalizeCodeLocInfo(componentStack)).toBe(
929+
'\n'+
930+
' in SharedComponent (at **)\n'+
931+
' in ServerComponent'+
932+
(gate(flags=>flags.enableAsyncDebugInfo) ? ' (at **)' : '')+
933+
'\n'+
934+
' in Suspense\n'+
935+
' in body\n'+
936+
' in html\n'+
937+
' in App (at **)\n'+
938+
' in ClientRoot (at **)',
939+
);
940+
}else{
941+
expect(normalizeCodeLocInfo(componentStack)).toBe(
942+
'\n'+
943+
' in SharedComponent (at **)\n'+
944+
' in Suspense\n'+
945+
' in body\n'+
946+
' in html\n'+
947+
' in ClientRoot (at **)',
948+
);
949+
}
950+
951+
if(__DEV__){
952+
expect(ignoreListStack(ownerStack)).toBe(
953+
// eslint-disable-next-line react-internal/safe-string-coercion
954+
''+
955+
// The concrete location may change as this test is updated.
956+
// Just make sure they still point at React.use(p2)
957+
(gate(flags=>flags.enableAsyncDebugInfo)
958+
? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)'
959+
: '')+
960+
'\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)'+
961+
'\n at App (file://./ReactFlightDOMNode-test.js:852:25)',
962+
);
963+
}else{
964+
expect(ownerStack).toBeNull();
965+
}
966+
});
967+
787968
// @gate enableHalt
788969
it('includes deeper location for aborted stacks',async()=>{
789970
asyncfunctiongetData(){
@@ -1364,12 +1545,12 @@ describe('ReactFlightDOMNode', () => {
13641545
'\n'+
13651546
' in Dynamic'+
13661547
(gate(flags=>flags.enableAsyncDebugInfo)
1367-
? ' (file://ReactFlightDOMNode-test.js:1238:27)\n'
1548+
? ' (file://ReactFlightDOMNode-test.js:1419:27)\n'
13681549
: '\n')+
13691550
' in body\n'+
13701551
' in html\n'+
1371-
' in App (file://ReactFlightDOMNode-test.js:1251:25)\n'+
1372-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1552+
' in App (file://ReactFlightDOMNode-test.js:1432:25)\n'+
1553+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13731554
);
13741555
}else{
13751556
expect(
@@ -1378,7 +1559,7 @@ describe('ReactFlightDOMNode', () => {
13781559
'\n'+
13791560
' in body\n'+
13801561
' in html\n'+
1381-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1562+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13821563
);
13831564
}
13841565

@@ -1388,16 +1569,16 @@ describe('ReactFlightDOMNode', () => {
13881569
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13891570
).toBe(
13901571
'\n'+
1391-
' in Dynamic (file://ReactFlightDOMNode-test.js:1238:27)\n'+
1392-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1572+
' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n'+
1573+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
13931574
);
13941575
}else{
13951576
expect(
13961577
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13971578
).toBe(
13981579
''+
13991580
'\n'+
1400-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1581+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
14011582
);
14021583
}
14031584
}else{

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,14 @@ import assign from 'shared/assign';
190190
importnoopfrom'shared/noop';
191191
importgetComponentNameFromTypefrom'shared/getComponentNameFromType';
192192
importisArrayfrom'shared/isArray';
193-
import{SuspenseException,getSuspendedThenable}from'./ReactFizzThenable';
193+
import{
194+
SuspenseException,
195+
getSuspendedThenable,
196+
ensureSuspendableThenableStateDEV,
197+
getSuspendedCallSiteStackDEV,
198+
getSuspendedCallSiteDebugTaskDEV,
199+
setCaptureSuspendedCallSiteDEV,
200+
}from'./ReactFizzThenable';
194201

195202
// Linked list representing the identity of a component given the component/tag name and key.
196203
// The name might be minified but we assume that it's going to be the same generated name. Typically
@@ -355,6 +362,7 @@ const OPEN = 11;
355362
constABORTING=12;
356363
constCLOSING=13;
357364
constCLOSED=14;
365+
constSTALLED_DEV=15;
358366

359367
exportopaquetypeRequest={
360368
destination: null|Destination,
@@ -363,7 +371,7 @@ export opaque type Request = {
363371
+renderState: RenderState,
364372
+rootFormatContext: FormatContext,
365373
+progressiveChunkSize: number,
366-
status: 10|11|12|13|14,
374+
status: 10|11|12|13|14|15,
367375
fatalError: mixed,
368376
nextSegmentId: number,
369377
allPendingTasks: number,// when it reaches zero, we can close the connection.
@@ -1023,6 +1031,89 @@ function pushHaltedAwaitOnComponentStack(
10231031
}
10241032
}
10251033

1034+
// performWork + retryTask without mutation
1035+
functionrerenderStalledTask(request: Request,task: Task): void{
1036+
constprevStatus=request.status;
1037+
request.status=STALLED_DEV;
1038+
1039+
constprevContext=getActiveContext();
1040+
constprevDispatcher=ReactSharedInternals.H;
1041+
ReactSharedInternals.H=HooksDispatcher;
1042+
constprevAsyncDispatcher=ReactSharedInternals.A;
1043+
ReactSharedInternals.A=DefaultAsyncDispatcher;
1044+
1045+
constprevRequest=currentRequest;
1046+
currentRequest=request;
1047+
1048+
constprevGetCurrentStackImpl=ReactSharedInternals.getCurrentStack;
1049+
ReactSharedInternals.getCurrentStack=getCurrentStackInDEV;
1050+
1051+
constprevResumableState=currentResumableState;
1052+
setCurrentResumableState(request.resumableState);
1053+
switchContext(task.context);
1054+
constprevTaskInDEV=currentTaskInDEV;
1055+
setCurrentTaskInDEV(task);
1056+
try{
1057+
retryNode(request,task);
1058+
}catch(x){
1059+
// Suspended again.
1060+
resetHooksState();
1061+
}finally{
1062+
setCurrentTaskInDEV(prevTaskInDEV);
1063+
setCurrentResumableState(prevResumableState);
1064+
1065+
ReactSharedInternals.H =prevDispatcher;
1066+
ReactSharedInternals.A=prevAsyncDispatcher;
1067+
1068+
ReactSharedInternals.getCurrentStack=prevGetCurrentStackImpl;
1069+
if(prevDispatcher===HooksDispatcher){
1070+
// This means that we were in a reentrant work loop. This could happen
1071+
// in a renderer that supports synchronous work like renderToString,
1072+
// when it's called from within another renderer.
1073+
// Normally we don't bother switching the contexts to their root/default
1074+
// values when leaving because we'll likely need the same or similar
1075+
// context again. However, when we're inside a synchronous loop like this
1076+
// we'll to restore the context to what it was before returning.
1077+
switchContext(prevContext);
1078+
}
1079+
currentRequest =prevRequest;
1080+
request.status=prevStatus;
1081+
}
1082+
}
1083+
1084+
functionpushSuspendedCallSiteOnComponentStack(
1085+
request: Request,
1086+
task: Task,
1087+
): void{
1088+
setCaptureSuspendedCallSiteDEV(true);
1089+
constrestoreThenableState=ensureSuspendableThenableStateDEV(
1090+
// refined at the callsite
1091+
((task.thenableState: any): ThenableState),
1092+
);
1093+
try{
1094+
rerenderStalledTask(request,task);
1095+
}finally{
1096+
restoreThenableState();
1097+
setCaptureSuspendedCallSiteDEV(false);
1098+
}
1099+
1100+
const suspendCallSiteStack =getSuspendedCallSiteStackDEV();
1101+
constsuspendCallSiteDebugTask=getSuspendedCallSiteDebugTaskDEV();
1102+
1103+
if(suspendCallSiteStack!==null){
1104+
const ownerStack =task.componentStack;
1105+
task.componentStack={
1106+
// The owner of the suspended call site would be the owner of this task.
1107+
// We need the task itself otherwise we'd miss a frame.
1108+
owner: ownerStack,
1109+
parent: suspendCallSiteStack.parent,
1110+
stack: suspendCallSiteStack.stack,
1111+
type: suspendCallSiteStack.type,
1112+
};
1113+
}
1114+
task.debugTask=suspendCallSiteDebugTask;
1115+
}
1116+
10261117
functionpushServerComponentStack(
10271118
task: Task,
10281119
debugInfo: void|null|ReactDebugInfo,
@@ -2723,7 +2814,12 @@ function renderLazyComponent(
27232814
constinit=lazyComponent._init;
27242815
Component=init(payload);
27252816
}
2726-
if(request.status===ABORTING){
2817+
if(
2818+
request.status===ABORTING&&
2819+
// We're going to discard this render anyway.
2820+
// We just need to reach the point where we suspended in dev.
2821+
(!__DEV__||request.status!==STALLED_DEV)
2822+
){
27272823
// eslint-disable-next-line no-throw-literal
27282824
thrownull;
27292825
}
@@ -4535,12 +4631,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
45354631
debugInfo=node._debugInfo;
45364632
}
45374633
pushHaltedAwaitOnComponentStack(task,debugInfo);
4538-
/*
45394634
if(task.thenableState!==null){
4540-
// TODO: If we were stalled inside use() of a Client Component then we should
4541-
// rerender to get the stack trace from the use() call.
4635+
pushSuspendedCallSiteOnComponentStack(request,task);
45424636
}
4543-
*/
45444637
}
45454638
}
45464639

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [Fizz] Push a stalled use() to the ownerStack/debugTask (#35226) · react/react@41b3e9a · GitHub
Skip to content

Commit 41b3e9a

Browse files
authored
[Fizz] Push a stalled use() to the ownerStack/debugTask (#35226)
1 parent 195fd22 commit 41b3e9a

4 files changed

Lines changed: 527 additions & 27 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 188 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,28 @@ describe('ReactFlightDOMNode', () => {
108108
);
109109
}
110110

111+
/**
112+
* Removes all stackframes not pointing into this file
113+
*/
114+
functionignoreListStack(str){
115+
if(!str){
116+
returnstr;
117+
}
118+
119+
letignoreListedStack='';
120+
constlines=str.split('\n');
121+
122+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
123+
for(constlineoflines){
124+
if(line.indexOf(__filename)===-1){
125+
}else{
126+
ignoreListedStack+='\n'+line.replace(__dirname,'.');
127+
}
128+
}
129+
130+
returnignoreListedStack;
131+
}
132+
111133
functionreadResult(stream){
112134
returnnewPromise((resolve,reject)=>{
113135
letbuffer='';
@@ -784,6 +806,165 @@ describe('ReactFlightDOMNode', () => {
784806
}
785807
});
786808

809+
// @gate enableHalt
810+
it('includes source locations in component and owner stacks for halted Client components',async()=>{
811+
functionSharedComponent({p1, p2, p3}){
812+
use(p1);
813+
use(p2);
814+
use(p3);
815+
return<div>Hello, Dave!</div>;
816+
}
817+
constClientComponentOnTheServer=clientExports(SharedComponent);
818+
constClientComponentOnTheClient=clientExports(
819+
SharedComponent,
820+
123,
821+
'path/to/chunk.js',
822+
);
823+
824+
letresolvePendingPromise;
825+
functionServerComponent(){
826+
constp1=Promise.resolve();
827+
constp2=newPromise(resolve=>{
828+
resolvePendingPromise=value=>{
829+
p2.status='fulfilled';
830+
p2.value=value;
831+
resolve(value);
832+
};
833+
});
834+
constp3=newPromise(()=>{});
835+
returnReactServer.createElement(ClientComponentOnTheClient,{
836+
p1: p1,
837+
p2: p2,
838+
p3: p3,
839+
});
840+
}
841+
842+
functionApp(){
843+
returnReactServer.createElement(
844+
'html',
845+
null,
846+
ReactServer.createElement(
847+
'body',
848+
null,
849+
ReactServer.createElement(
850+
ReactServer.Suspense,
851+
{fallback: 'Loading...'},
852+
ReactServer.createElement(ServerComponent,null),
853+
),
854+
),
855+
);
856+
}
857+
858+
consterrors=[];
859+
constrscStream=awaitserverAct(()=>
860+
ReactServerDOMServer.renderToPipeableStream(
861+
ReactServer.createElement(App,null),
862+
webpackMap,
863+
),
864+
);
865+
866+
constreadable=newStream.PassThrough(streamOptions);
867+
rscStream.pipe(readable);
868+
869+
functionClientRoot({response}){
870+
returnuse(response);
871+
}
872+
873+
constserverConsumerManifest={
874+
moduleMap: {
875+
[webpackMap[ClientComponentOnTheClient.$$id].id]: {
876+
'*': webpackMap[ClientComponentOnTheServer.$$id],
877+
},
878+
},
879+
moduleLoading: webpackModuleLoading,
880+
};
881+
882+
expect(errors).toEqual([]);
883+
884+
functionClientRoot({response}){
885+
returnuse(response);
886+
}
887+
888+
constresponse=ReactServerDOMClient.createFromNodeStream(
889+
readable,
890+
serverConsumerManifest,
891+
);
892+
893+
letcomponentStack;
894+
letownerStack;
895+
896+
constclientAbortController=newAbortController();
897+
898+
constfizzPrerenderStreamResult=ReactDOMFizzStatic.prerender(
899+
React.createElement(ClientRoot,{response}),
900+
{
901+
signal: clientAbortController.signal,
902+
onError(error,errorInfo){
903+
componentStack=errorInfo.componentStack;
904+
ownerStack=React.captureOwnerStack
905+
? React.captureOwnerStack()
906+
: null;
907+
},
908+
},
909+
);
910+
911+
resolvePendingPromise('custom-instrum-resolve');
912+
awaitserverAct(
913+
async()=>
914+
newPromise(resolve=>{
915+
setImmediate(()=>{
916+
clientAbortController.abort();
917+
resolve();
918+
});
919+
}),
920+
);
921+
922+
constfizzPrerenderStream=awaitfizzPrerenderStreamResult;
923+
constprerenderHTML=awaitreadWebResult(fizzPrerenderStream.prelude);
924+
925+
expect(prerenderHTML).toContain('Loading...');
926+
927+
if(__DEV__){
928+
expect(normalizeCodeLocInfo(componentStack)).toBe(
929+
'\n'+
930+
' in SharedComponent (at **)\n'+
931+
' in ServerComponent'+
932+
(gate(flags=>flags.enableAsyncDebugInfo) ? ' (at **)' : '')+
933+
'\n'+
934+
' in Suspense\n'+
935+
' in body\n'+
936+
' in html\n'+
937+
' in App (at **)\n'+
938+
' in ClientRoot (at **)',
939+
);
940+
}else{
941+
expect(normalizeCodeLocInfo(componentStack)).toBe(
942+
'\n'+
943+
' in SharedComponent (at **)\n'+
944+
' in Suspense\n'+
945+
' in body\n'+
946+
' in html\n'+
947+
' in ClientRoot (at **)',
948+
);
949+
}
950+
951+
if(__DEV__){
952+
expect(ignoreListStack(ownerStack)).toBe(
953+
// eslint-disable-next-line react-internal/safe-string-coercion
954+
''+
955+
// The concrete location may change as this test is updated.
956+
// Just make sure they still point at React.use(p2)
957+
(gate(flags=>flags.enableAsyncDebugInfo)
958+
? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)'
959+
: '')+
960+
'\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)'+
961+
'\n at App (file://./ReactFlightDOMNode-test.js:852:25)',
962+
);
963+
}else{
964+
expect(ownerStack).toBeNull();
965+
}
966+
});
967+
787968
// @gate enableHalt
788969
it('includes deeper location for aborted stacks',async()=>{
789970
asyncfunctiongetData(){
@@ -1364,12 +1545,12 @@ describe('ReactFlightDOMNode', () => {
13641545
'\n'+
13651546
' in Dynamic'+
13661547
(gate(flags=>flags.enableAsyncDebugInfo)
1367-
? ' (file://ReactFlightDOMNode-test.js:1238:27)\n'
1548+
? ' (file://ReactFlightDOMNode-test.js:1419:27)\n'
13681549
: '\n')+
13691550
' in body\n'+
13701551
' in html\n'+
1371-
' in App (file://ReactFlightDOMNode-test.js:1251:25)\n'+
1372-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1552+
' in App (file://ReactFlightDOMNode-test.js:1432:25)\n'+
1553+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13731554
);
13741555
}else{
13751556
expect(
@@ -1378,7 +1559,7 @@ describe('ReactFlightDOMNode', () => {
13781559
'\n'+
13791560
' in body\n'+
13801561
' in html\n'+
1381-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1562+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13821563
);
13831564
}
13841565

@@ -1388,16 +1569,16 @@ describe('ReactFlightDOMNode', () => {
13881569
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13891570
).toBe(
13901571
'\n'+
1391-
' in Dynamic (file://ReactFlightDOMNode-test.js:1238:27)\n'+
1392-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1572+
' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n'+
1573+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
13931574
);
13941575
}else{
13951576
expect(
13961577
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13971578
).toBe(
13981579
''+
13991580
'\n'+
1400-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1581+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
14011582
);
14021583
}
14031584
}else{

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,14 @@ import assign from 'shared/assign';
190190
importnoopfrom'shared/noop';
191191
importgetComponentNameFromTypefrom'shared/getComponentNameFromType';
192192
importisArrayfrom'shared/isArray';
193-
import{SuspenseException,getSuspendedThenable}from'./ReactFizzThenable';
193+
import{
194+
SuspenseException,
195+
getSuspendedThenable,
196+
ensureSuspendableThenableStateDEV,
197+
getSuspendedCallSiteStackDEV,
198+
getSuspendedCallSiteDebugTaskDEV,
199+
setCaptureSuspendedCallSiteDEV,
200+
}from'./ReactFizzThenable';
194201

195202
// Linked list representing the identity of a component given the component/tag name and key.
196203
// The name might be minified but we assume that it's going to be the same generated name. Typically
@@ -355,6 +362,7 @@ const OPEN = 11;
355362
constABORTING=12;
356363
constCLOSING=13;
357364
constCLOSED=14;
365+
constSTALLED_DEV=15;
358366

359367
exportopaquetypeRequest={
360368
destination: null|Destination,
@@ -363,7 +371,7 @@ export opaque type Request = {
363371
+renderState: RenderState,
364372
+rootFormatContext: FormatContext,
365373
+progressiveChunkSize: number,
366-
status: 10|11|12|13|14,
374+
status: 10|11|12|13|14|15,
367375
fatalError: mixed,
368376
nextSegmentId: number,
369377
allPendingTasks: number,// when it reaches zero, we can close the connection.
@@ -1023,6 +1031,89 @@ function pushHaltedAwaitOnComponentStack(
10231031
}
10241032
}
10251033

1034+
// performWork + retryTask without mutation
1035+
functionrerenderStalledTask(request: Request,task: Task): void{
1036+
constprevStatus=request.status;
1037+
request.status=STALLED_DEV;
1038+
1039+
constprevContext=getActiveContext();
1040+
constprevDispatcher=ReactSharedInternals.H;
1041+
ReactSharedInternals.H=HooksDispatcher;
1042+
constprevAsyncDispatcher=ReactSharedInternals.A;
1043+
ReactSharedInternals.A=DefaultAsyncDispatcher;
1044+
1045+
constprevRequest=currentRequest;
1046+
currentRequest=request;
1047+
1048+
constprevGetCurrentStackImpl=ReactSharedInternals.getCurrentStack;
1049+
ReactSharedInternals.getCurrentStack=getCurrentStackInDEV;
1050+
1051+
constprevResumableState=currentResumableState;
1052+
setCurrentResumableState(request.resumableState);
1053+
switchContext(task.context);
1054+
constprevTaskInDEV=currentTaskInDEV;
1055+
setCurrentTaskInDEV(task);
1056+
try{
1057+
retryNode(request,task);
1058+
}catch(x){
1059+
// Suspended again.
1060+
resetHooksState();
1061+
}finally{
1062+
setCurrentTaskInDEV(prevTaskInDEV);
1063+
setCurrentResumableState(prevResumableState);
1064+
1065+
ReactSharedInternals.H =prevDispatcher;
1066+
ReactSharedInternals.A=prevAsyncDispatcher;
1067+
1068+
ReactSharedInternals.getCurrentStack=prevGetCurrentStackImpl;
1069+
if(prevDispatcher===HooksDispatcher){
1070+
// This means that we were in a reentrant work loop. This could happen
1071+
// in a renderer that supports synchronous work like renderToString,
1072+
// when it's called from within another renderer.
1073+
// Normally we don't bother switching the contexts to their root/default
1074+
// values when leaving because we'll likely need the same or similar
1075+
// context again. However, when we're inside a synchronous loop like this
1076+
// we'll to restore the context to what it was before returning.
1077+
switchContext(prevContext);
1078+
}
1079+
currentRequest =prevRequest;
1080+
request.status=prevStatus;
1081+
}
1082+
}
1083+
1084+
functionpushSuspendedCallSiteOnComponentStack(
1085+
request: Request,
1086+
task: Task,
1087+
): void{
1088+
setCaptureSuspendedCallSiteDEV(true);
1089+
constrestoreThenableState=ensureSuspendableThenableStateDEV(
1090+
// refined at the callsite
1091+
((task.thenableState: any): ThenableState),
1092+
);
1093+
try{
1094+
rerenderStalledTask(request,task);
1095+
}finally{
1096+
restoreThenableState();
1097+
setCaptureSuspendedCallSiteDEV(false);
1098+
}
1099+
1100+
const suspendCallSiteStack =getSuspendedCallSiteStackDEV();
1101+
constsuspendCallSiteDebugTask=getSuspendedCallSiteDebugTaskDEV();
1102+
1103+
if(suspendCallSiteStack!==null){
1104+
const ownerStack =task.componentStack;
1105+
task.componentStack={
1106+
// The owner of the suspended call site would be the owner of this task.
1107+
// We need the task itself otherwise we'd miss a frame.
1108+
owner: ownerStack,
1109+
parent: suspendCallSiteStack.parent,
1110+
stack: suspendCallSiteStack.stack,
1111+
type: suspendCallSiteStack.type,
1112+
};
1113+
}
1114+
task.debugTask=suspendCallSiteDebugTask;
1115+
}
1116+
10261117
functionpushServerComponentStack(
10271118
task: Task,
10281119
debugInfo: void|null|ReactDebugInfo,
@@ -2723,7 +2814,12 @@ function renderLazyComponent(
27232814
constinit=lazyComponent._init;
27242815
Component=init(payload);
27252816
}
2726-
if(request.status===ABORTING){
2817+
if(
2818+
request.status===ABORTING&&
2819+
// We're going to discard this render anyway.
2820+
// We just need to reach the point where we suspended in dev.
2821+
(!__DEV__||request.status!==STALLED_DEV)
2822+
){
27272823
// eslint-disable-next-line no-throw-literal
27282824
thrownull;
27292825
}
@@ -4535,12 +4631,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
45354631
debugInfo=node._debugInfo;
45364632
}
45374633
pushHaltedAwaitOnComponentStack(task,debugInfo);
4538-
/*
45394634
if(task.thenableState!==null){
4540-
// TODO: If we were stalled inside use() of a Client Component then we should
4541-
// rerender to get the stack trace from the use() call.
4635+
pushSuspendedCallSiteOnComponentStack(request,task);
45424636
}
4543-
*/
45444637
}
45454638
}
45464639

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); [Fizz] Push a stalled use() to the ownerStack/debugTask (#35226) · react/react@41b3e9a · GitHub
Skip to content

Commit 41b3e9a

Browse files
authored
[Fizz] Push a stalled use() to the ownerStack/debugTask (#35226)
1 parent 195fd22 commit 41b3e9a

4 files changed

Lines changed: 527 additions & 27 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 188 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,28 @@ describe('ReactFlightDOMNode', () => {
108108
);
109109
}
110110

111+
/**
112+
* Removes all stackframes not pointing into this file
113+
*/
114+
functionignoreListStack(str){
115+
if(!str){
116+
returnstr;
117+
}
118+
119+
letignoreListedStack='';
120+
constlines=str.split('\n');
121+
122+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
123+
for(constlineoflines){
124+
if(line.indexOf(__filename)===-1){
125+
}else{
126+
ignoreListedStack+='\n'+line.replace(__dirname,'.');
127+
}
128+
}
129+
130+
returnignoreListedStack;
131+
}
132+
111133
functionreadResult(stream){
112134
returnnewPromise((resolve,reject)=>{
113135
letbuffer='';
@@ -784,6 +806,165 @@ describe('ReactFlightDOMNode', () => {
784806
}
785807
});
786808

809+
// @gate enableHalt
810+
it('includes source locations in component and owner stacks for halted Client components',async()=>{
811+
functionSharedComponent({p1, p2, p3}){
812+
use(p1);
813+
use(p2);
814+
use(p3);
815+
return<div>Hello, Dave!</div>;
816+
}
817+
constClientComponentOnTheServer=clientExports(SharedComponent);
818+
constClientComponentOnTheClient=clientExports(
819+
SharedComponent,
820+
123,
821+
'path/to/chunk.js',
822+
);
823+
824+
letresolvePendingPromise;
825+
functionServerComponent(){
826+
constp1=Promise.resolve();
827+
constp2=newPromise(resolve=>{
828+
resolvePendingPromise=value=>{
829+
p2.status='fulfilled';
830+
p2.value=value;
831+
resolve(value);
832+
};
833+
});
834+
constp3=newPromise(()=>{});
835+
returnReactServer.createElement(ClientComponentOnTheClient,{
836+
p1: p1,
837+
p2: p2,
838+
p3: p3,
839+
});
840+
}
841+
842+
functionApp(){
843+
returnReactServer.createElement(
844+
'html',
845+
null,
846+
ReactServer.createElement(
847+
'body',
848+
null,
849+
ReactServer.createElement(
850+
ReactServer.Suspense,
851+
{fallback: 'Loading...'},
852+
ReactServer.createElement(ServerComponent,null),
853+
),
854+
),
855+
);
856+
}
857+
858+
consterrors=[];
859+
constrscStream=awaitserverAct(()=>
860+
ReactServerDOMServer.renderToPipeableStream(
861+
ReactServer.createElement(App,null),
862+
webpackMap,
863+
),
864+
);
865+
866+
constreadable=newStream.PassThrough(streamOptions);
867+
rscStream.pipe(readable);
868+
869+
functionClientRoot({response}){
870+
returnuse(response);
871+
}
872+
873+
constserverConsumerManifest={
874+
moduleMap: {
875+
[webpackMap[ClientComponentOnTheClient.$$id].id]: {
876+
'*': webpackMap[ClientComponentOnTheServer.$$id],
877+
},
878+
},
879+
moduleLoading: webpackModuleLoading,
880+
};
881+
882+
expect(errors).toEqual([]);
883+
884+
functionClientRoot({response}){
885+
returnuse(response);
886+
}
887+
888+
constresponse=ReactServerDOMClient.createFromNodeStream(
889+
readable,
890+
serverConsumerManifest,
891+
);
892+
893+
letcomponentStack;
894+
letownerStack;
895+
896+
constclientAbortController=newAbortController();
897+
898+
constfizzPrerenderStreamResult=ReactDOMFizzStatic.prerender(
899+
React.createElement(ClientRoot,{response}),
900+
{
901+
signal: clientAbortController.signal,
902+
onError(error,errorInfo){
903+
componentStack=errorInfo.componentStack;
904+
ownerStack=React.captureOwnerStack
905+
? React.captureOwnerStack()
906+
: null;
907+
},
908+
},
909+
);
910+
911+
resolvePendingPromise('custom-instrum-resolve');
912+
awaitserverAct(
913+
async()=>
914+
newPromise(resolve=>{
915+
setImmediate(()=>{
916+
clientAbortController.abort();
917+
resolve();
918+
});
919+
}),
920+
);
921+
922+
constfizzPrerenderStream=awaitfizzPrerenderStreamResult;
923+
constprerenderHTML=awaitreadWebResult(fizzPrerenderStream.prelude);
924+
925+
expect(prerenderHTML).toContain('Loading...');
926+
927+
if(__DEV__){
928+
expect(normalizeCodeLocInfo(componentStack)).toBe(
929+
'\n'+
930+
' in SharedComponent (at **)\n'+
931+
' in ServerComponent'+
932+
(gate(flags=>flags.enableAsyncDebugInfo) ? ' (at **)' : '')+
933+
'\n'+
934+
' in Suspense\n'+
935+
' in body\n'+
936+
' in html\n'+
937+
' in App (at **)\n'+
938+
' in ClientRoot (at **)',
939+
);
940+
}else{
941+
expect(normalizeCodeLocInfo(componentStack)).toBe(
942+
'\n'+
943+
' in SharedComponent (at **)\n'+
944+
' in Suspense\n'+
945+
' in body\n'+
946+
' in html\n'+
947+
' in ClientRoot (at **)',
948+
);
949+
}
950+
951+
if(__DEV__){
952+
expect(ignoreListStack(ownerStack)).toBe(
953+
// eslint-disable-next-line react-internal/safe-string-coercion
954+
''+
955+
// The concrete location may change as this test is updated.
956+
// Just make sure they still point at React.use(p2)
957+
(gate(flags=>flags.enableAsyncDebugInfo)
958+
? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)'
959+
: '')+
960+
'\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)'+
961+
'\n at App (file://./ReactFlightDOMNode-test.js:852:25)',
962+
);
963+
}else{
964+
expect(ownerStack).toBeNull();
965+
}
966+
});
967+
787968
// @gate enableHalt
788969
it('includes deeper location for aborted stacks',async()=>{
789970
asyncfunctiongetData(){
@@ -1364,12 +1545,12 @@ describe('ReactFlightDOMNode', () => {
13641545
'\n'+
13651546
' in Dynamic'+
13661547
(gate(flags=>flags.enableAsyncDebugInfo)
1367-
? ' (file://ReactFlightDOMNode-test.js:1238:27)\n'
1548+
? ' (file://ReactFlightDOMNode-test.js:1419:27)\n'
13681549
: '\n')+
13691550
' in body\n'+
13701551
' in html\n'+
1371-
' in App (file://ReactFlightDOMNode-test.js:1251:25)\n'+
1372-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1552+
' in App (file://ReactFlightDOMNode-test.js:1432:25)\n'+
1553+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13731554
);
13741555
}else{
13751556
expect(
@@ -1378,7 +1559,7 @@ describe('ReactFlightDOMNode', () => {
13781559
'\n'+
13791560
' in body\n'+
13801561
' in html\n'+
1381-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1562+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13821563
);
13831564
}
13841565

@@ -1388,16 +1569,16 @@ describe('ReactFlightDOMNode', () => {
13881569
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13891570
).toBe(
13901571
'\n'+
1391-
' in Dynamic (file://ReactFlightDOMNode-test.js:1238:27)\n'+
1392-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1572+
' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n'+
1573+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
13931574
);
13941575
}else{
13951576
expect(
13961577
normalizeCodeLocInfo(ownerStack,{preserveLocation: true}),
13971578
).toBe(
13981579
''+
13991580
'\n'+
1400-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1581+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
14011582
);
14021583
}
14031584
}else{

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,14 @@ import assign from 'shared/assign';
190190
importnoopfrom'shared/noop';
191191
importgetComponentNameFromTypefrom'shared/getComponentNameFromType';
192192
importisArrayfrom'shared/isArray';
193-
import{SuspenseException,getSuspendedThenable}from'./ReactFizzThenable';
193+
import{
194+
SuspenseException,
195+
getSuspendedThenable,
196+
ensureSuspendableThenableStateDEV,
197+
getSuspendedCallSiteStackDEV,
198+
getSuspendedCallSiteDebugTaskDEV,
199+
setCaptureSuspendedCallSiteDEV,
200+
}from'./ReactFizzThenable';
194201

195202
// Linked list representing the identity of a component given the component/tag name and key.
196203
// The name might be minified but we assume that it's going to be the same generated name. Typically
@@ -355,6 +362,7 @@ const OPEN = 11;
355362
constABORTING=12;
356363
constCLOSING=13;
357364
constCLOSED=14;
365+
constSTALLED_DEV=15;
358366

359367
exportopaquetypeRequest={
360368
destination: null|Destination,
@@ -363,7 +371,7 @@ export opaque type Request = {
363371
+renderState: RenderState,
364372
+rootFormatContext: FormatContext,
365373
+progressiveChunkSize: number,
366-
status: 10|11|12|13|14,
374+
status: 10|11|12|13|14|15,
367375
fatalError: mixed,
368376
nextSegmentId: number,
369377
allPendingTasks: number,// when it reaches zero, we can close the connection.
@@ -1023,6 +1031,89 @@ function pushHaltedAwaitOnComponentStack(
10231031
}
10241032
}
10251033

1034+
// performWork + retryTask without mutation
1035+
functionrerenderStalledTask(request: Request,task: Task): void{
1036+
constprevStatus=request.status;
1037+
request.status=STALLED_DEV;
1038+
1039+
constprevContext=getActiveContext();
1040+
constprevDispatcher=ReactSharedInternals.H;
1041+
ReactSharedInternals.H=HooksDispatcher;
1042+
constprevAsyncDispatcher=ReactSharedInternals.A;
1043+
ReactSharedInternals.A=DefaultAsyncDispatcher;
1044+
1045+
constprevRequest=currentRequest;
1046+
currentRequest=request;
1047+
1048+
constprevGetCurrentStackImpl=ReactSharedInternals.getCurrentStack;
1049+
ReactSharedInternals.getCurrentStack=getCurrentStackInDEV;
1050+
1051+
constprevResumableState=currentResumableState;
1052+
setCurrentResumableState(request.resumableState);
1053+
switchContext(task.context);
1054+
constprevTaskInDEV=currentTaskInDEV;
1055+
setCurrentTaskInDEV(task);
1056+
try{
1057+
retryNode(request,task);
1058+
}catch(x){
1059+
// Suspended again.
1060+
resetHooksState();
1061+
}finally{
1062+
setCurrentTaskInDEV(prevTaskInDEV);
1063+
setCurrentResumableState(prevResumableState);
1064+
1065+
ReactSharedInternals.H =prevDispatcher;
1066+
ReactSharedInternals.A=prevAsyncDispatcher;
1067+
1068+
ReactSharedInternals.getCurrentStack=prevGetCurrentStackImpl;
1069+
if(prevDispatcher===HooksDispatcher){
1070+
// This means that we were in a reentrant work loop. This could happen
1071+
// in a renderer that supports synchronous work like renderToString,
1072+
// when it's called from within another renderer.
1073+
// Normally we don't bother switching the contexts to their root/default
1074+
// values when leaving because we'll likely need the same or similar
1075+
// context again. However, when we're inside a synchronous loop like this
1076+
// we'll to restore the context to what it was before returning.
1077+
switchContext(prevContext);
1078+
}
1079+
currentRequest =prevRequest;
1080+
request.status=prevStatus;
1081+
}
1082+
}
1083+
1084+
functionpushSuspendedCallSiteOnComponentStack(
1085+
request: Request,
1086+
task: Task,
1087+
): void{
1088+
setCaptureSuspendedCallSiteDEV(true);
1089+
constrestoreThenableState=ensureSuspendableThenableStateDEV(
1090+
// refined at the callsite
1091+
((task.thenableState: any): ThenableState),
1092+
);
1093+
try{
1094+
rerenderStalledTask(request,task);
1095+
}finally{
1096+
restoreThenableState();
1097+
setCaptureSuspendedCallSiteDEV(false);
1098+
}
1099+
1100+
const suspendCallSiteStack =getSuspendedCallSiteStackDEV();
1101+
constsuspendCallSiteDebugTask=getSuspendedCallSiteDebugTaskDEV();
1102+
1103+
if(suspendCallSiteStack!==null){
1104+
const ownerStack =task.componentStack;
1105+
task.componentStack={
1106+
// The owner of the suspended call site would be the owner of this task.
1107+
// We need the task itself otherwise we'd miss a frame.
1108+
owner: ownerStack,
1109+
parent: suspendCallSiteStack.parent,
1110+
stack: suspendCallSiteStack.stack,
1111+
type: suspendCallSiteStack.type,
1112+
};
1113+
}
1114+
task.debugTask=suspendCallSiteDebugTask;
1115+
}
1116+
10261117
functionpushServerComponentStack(
10271118
task: Task,
10281119
debugInfo: void|null|ReactDebugInfo,
@@ -2723,7 +2814,12 @@ function renderLazyComponent(
27232814
constinit=lazyComponent._init;
27242815
Component=init(payload);
27252816
}
2726-
if(request.status===ABORTING){
2817+
if(
2818+
request.status===ABORTING&&
2819+
// We're going to discard this render anyway.
2820+
// We just need to reach the point where we suspended in dev.
2821+
(!__DEV__||request.status!==STALLED_DEV)
2822+
){
27272823
// eslint-disable-next-line no-throw-literal
27282824
thrownull;
27292825
}
@@ -4535,12 +4631,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
45354631
debugInfo=node._debugInfo;
45364632
}
45374633
pushHaltedAwaitOnComponentStack(task,debugInfo);
4538-
/*
45394634
if(task.thenableState!==null){
4540-
// TODO: If we were stalled inside use() of a Client Component then we should
4541-
// rerender to get the stack trace from the use() call.
4635+
pushSuspendedCallSiteOnComponentStack(request,task);
45424636
}
4543-
*/
45444637
}
45454638
}
45464639

0 commit comments

Comments
 (0)