mirror of
https://github.com/facebook/react.git
synced 2026-02-22 03:42:05 +00:00
[Fizz] Push a stalled use() to the ownerStack/debugTask (#35226)
This commit is contained in:
committed by
GitHub
parent
195fd2286b
commit
41b3e9a670
@@ -108,6 +108,28 @@ describe('ReactFlightDOMNode', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all stackframes not pointing into this file
|
||||||
|
*/
|
||||||
|
function ignoreListStack(str) {
|
||||||
|
if (!str) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ignoreListedStack = '';
|
||||||
|
const lines = str.split('\n');
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.indexOf(__filename) === -1) {
|
||||||
|
} else {
|
||||||
|
ignoreListedStack += '\n' + line.replace(__dirname, '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ignoreListedStack;
|
||||||
|
}
|
||||||
|
|
||||||
function readResult(stream) {
|
function readResult(stream) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
@@ -784,6 +806,165 @@ describe('ReactFlightDOMNode', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @gate enableHalt
|
||||||
|
it('includes source locations in component and owner stacks for halted Client components', async () => {
|
||||||
|
function SharedComponent({p1, p2, p3}) {
|
||||||
|
use(p1);
|
||||||
|
use(p2);
|
||||||
|
use(p3);
|
||||||
|
return <div>Hello, Dave!</div>;
|
||||||
|
}
|
||||||
|
const ClientComponentOnTheServer = clientExports(SharedComponent);
|
||||||
|
const ClientComponentOnTheClient = clientExports(
|
||||||
|
SharedComponent,
|
||||||
|
123,
|
||||||
|
'path/to/chunk.js',
|
||||||
|
);
|
||||||
|
|
||||||
|
let resolvePendingPromise;
|
||||||
|
function ServerComponent() {
|
||||||
|
const p1 = Promise.resolve();
|
||||||
|
const p2 = new Promise(resolve => {
|
||||||
|
resolvePendingPromise = value => {
|
||||||
|
p2.status = 'fulfilled';
|
||||||
|
p2.value = value;
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const p3 = new Promise(() => {});
|
||||||
|
return ReactServer.createElement(ClientComponentOnTheClient, {
|
||||||
|
p1: p1,
|
||||||
|
p2: p2,
|
||||||
|
p3: p3,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return ReactServer.createElement(
|
||||||
|
'html',
|
||||||
|
null,
|
||||||
|
ReactServer.createElement(
|
||||||
|
'body',
|
||||||
|
null,
|
||||||
|
ReactServer.createElement(
|
||||||
|
ReactServer.Suspense,
|
||||||
|
{fallback: 'Loading...'},
|
||||||
|
ReactServer.createElement(ServerComponent, null),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
const rscStream = await serverAct(() =>
|
||||||
|
ReactServerDOMServer.renderToPipeableStream(
|
||||||
|
ReactServer.createElement(App, null),
|
||||||
|
webpackMap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const readable = new Stream.PassThrough(streamOptions);
|
||||||
|
rscStream.pipe(readable);
|
||||||
|
|
||||||
|
function ClientRoot({response}) {
|
||||||
|
return use(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverConsumerManifest = {
|
||||||
|
moduleMap: {
|
||||||
|
[webpackMap[ClientComponentOnTheClient.$$id].id]: {
|
||||||
|
'*': webpackMap[ClientComponentOnTheServer.$$id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
moduleLoading: webpackModuleLoading,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(errors).toEqual([]);
|
||||||
|
|
||||||
|
function ClientRoot({response}) {
|
||||||
|
return use(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = ReactServerDOMClient.createFromNodeStream(
|
||||||
|
readable,
|
||||||
|
serverConsumerManifest,
|
||||||
|
);
|
||||||
|
|
||||||
|
let componentStack;
|
||||||
|
let ownerStack;
|
||||||
|
|
||||||
|
const clientAbortController = new AbortController();
|
||||||
|
|
||||||
|
const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
|
||||||
|
React.createElement(ClientRoot, {response}),
|
||||||
|
{
|
||||||
|
signal: clientAbortController.signal,
|
||||||
|
onError(error, errorInfo) {
|
||||||
|
componentStack = errorInfo.componentStack;
|
||||||
|
ownerStack = React.captureOwnerStack
|
||||||
|
? React.captureOwnerStack()
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
resolvePendingPromise('custom-instrum-resolve');
|
||||||
|
await serverAct(
|
||||||
|
async () =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
setImmediate(() => {
|
||||||
|
clientAbortController.abort();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fizzPrerenderStream = await fizzPrerenderStreamResult;
|
||||||
|
const prerenderHTML = await readWebResult(fizzPrerenderStream.prelude);
|
||||||
|
|
||||||
|
expect(prerenderHTML).toContain('Loading...');
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
expect(normalizeCodeLocInfo(componentStack)).toBe(
|
||||||
|
'\n' +
|
||||||
|
' in SharedComponent (at **)\n' +
|
||||||
|
' in ServerComponent' +
|
||||||
|
(gate(flags => flags.enableAsyncDebugInfo) ? ' (at **)' : '') +
|
||||||
|
'\n' +
|
||||||
|
' in Suspense\n' +
|
||||||
|
' in body\n' +
|
||||||
|
' in html\n' +
|
||||||
|
' in App (at **)\n' +
|
||||||
|
' in ClientRoot (at **)',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
expect(normalizeCodeLocInfo(componentStack)).toBe(
|
||||||
|
'\n' +
|
||||||
|
' in SharedComponent (at **)\n' +
|
||||||
|
' in Suspense\n' +
|
||||||
|
' in body\n' +
|
||||||
|
' in html\n' +
|
||||||
|
' in ClientRoot (at **)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
expect(ignoreListStack(ownerStack)).toBe(
|
||||||
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||||
|
'' +
|
||||||
|
// The concrete location may change as this test is updated.
|
||||||
|
// Just make sure they still point at React.use(p2)
|
||||||
|
(gate(flags => flags.enableAsyncDebugInfo)
|
||||||
|
? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)'
|
||||||
|
: '') +
|
||||||
|
'\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)' +
|
||||||
|
'\n at App (file://./ReactFlightDOMNode-test.js:852:25)',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
expect(ownerStack).toBeNull();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// @gate enableHalt
|
// @gate enableHalt
|
||||||
it('includes deeper location for aborted stacks', async () => {
|
it('includes deeper location for aborted stacks', async () => {
|
||||||
async function getData() {
|
async function getData() {
|
||||||
@@ -1364,12 +1545,12 @@ describe('ReactFlightDOMNode', () => {
|
|||||||
'\n' +
|
'\n' +
|
||||||
' in Dynamic' +
|
' in Dynamic' +
|
||||||
(gate(flags => flags.enableAsyncDebugInfo)
|
(gate(flags => flags.enableAsyncDebugInfo)
|
||||||
? ' (file://ReactFlightDOMNode-test.js:1238:27)\n'
|
? ' (file://ReactFlightDOMNode-test.js:1419:27)\n'
|
||||||
: '\n') +
|
: '\n') +
|
||||||
' in body\n' +
|
' in body\n' +
|
||||||
' in html\n' +
|
' in html\n' +
|
||||||
' in App (file://ReactFlightDOMNode-test.js:1251:25)\n' +
|
' in App (file://ReactFlightDOMNode-test.js:1432:25)\n' +
|
||||||
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
|
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
expect(
|
expect(
|
||||||
@@ -1378,7 +1559,7 @@ describe('ReactFlightDOMNode', () => {
|
|||||||
'\n' +
|
'\n' +
|
||||||
' in body\n' +
|
' in body\n' +
|
||||||
' in html\n' +
|
' in html\n' +
|
||||||
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
|
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1388,8 +1569,8 @@ describe('ReactFlightDOMNode', () => {
|
|||||||
normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
|
normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
|
||||||
).toBe(
|
).toBe(
|
||||||
'\n' +
|
'\n' +
|
||||||
' in Dynamic (file://ReactFlightDOMNode-test.js:1238:27)\n' +
|
' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n' +
|
||||||
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
|
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
expect(
|
expect(
|
||||||
@@ -1397,7 +1578,7 @@ describe('ReactFlightDOMNode', () => {
|
|||||||
).toBe(
|
).toBe(
|
||||||
'' +
|
'' +
|
||||||
'\n' +
|
'\n' +
|
||||||
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
|
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
107
packages/react-server/src/ReactFizzServer.js
vendored
107
packages/react-server/src/ReactFizzServer.js
vendored
@@ -190,7 +190,14 @@ import assign from 'shared/assign';
|
|||||||
import noop from 'shared/noop';
|
import noop from 'shared/noop';
|
||||||
import getComponentNameFromType from 'shared/getComponentNameFromType';
|
import getComponentNameFromType from 'shared/getComponentNameFromType';
|
||||||
import isArray from 'shared/isArray';
|
import isArray from 'shared/isArray';
|
||||||
import {SuspenseException, getSuspendedThenable} from './ReactFizzThenable';
|
import {
|
||||||
|
SuspenseException,
|
||||||
|
getSuspendedThenable,
|
||||||
|
ensureSuspendableThenableStateDEV,
|
||||||
|
getSuspendedCallSiteStackDEV,
|
||||||
|
getSuspendedCallSiteDebugTaskDEV,
|
||||||
|
setCaptureSuspendedCallSiteDEV,
|
||||||
|
} from './ReactFizzThenable';
|
||||||
|
|
||||||
// Linked list representing the identity of a component given the component/tag name and key.
|
// Linked list representing the identity of a component given the component/tag name and key.
|
||||||
// The name might be minified but we assume that it's going to be the same generated name. Typically
|
// 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;
|
|||||||
const ABORTING = 12;
|
const ABORTING = 12;
|
||||||
const CLOSING = 13;
|
const CLOSING = 13;
|
||||||
const CLOSED = 14;
|
const CLOSED = 14;
|
||||||
|
const STALLED_DEV = 15;
|
||||||
|
|
||||||
export opaque type Request = {
|
export opaque type Request = {
|
||||||
destination: null | Destination,
|
destination: null | Destination,
|
||||||
@@ -363,7 +371,7 @@ export opaque type Request = {
|
|||||||
+renderState: RenderState,
|
+renderState: RenderState,
|
||||||
+rootFormatContext: FormatContext,
|
+rootFormatContext: FormatContext,
|
||||||
+progressiveChunkSize: number,
|
+progressiveChunkSize: number,
|
||||||
status: 10 | 11 | 12 | 13 | 14,
|
status: 10 | 11 | 12 | 13 | 14 | 15,
|
||||||
fatalError: mixed,
|
fatalError: mixed,
|
||||||
nextSegmentId: number,
|
nextSegmentId: number,
|
||||||
allPendingTasks: number, // when it reaches zero, we can close the connection.
|
allPendingTasks: number, // when it reaches zero, we can close the connection.
|
||||||
@@ -1023,6 +1031,89 @@ function pushHaltedAwaitOnComponentStack(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// performWork + retryTask without mutation
|
||||||
|
function rerenderStalledTask(request: Request, task: Task): void {
|
||||||
|
const prevStatus = request.status;
|
||||||
|
request.status = STALLED_DEV;
|
||||||
|
|
||||||
|
const prevContext = getActiveContext();
|
||||||
|
const prevDispatcher = ReactSharedInternals.H;
|
||||||
|
ReactSharedInternals.H = HooksDispatcher;
|
||||||
|
const prevAsyncDispatcher = ReactSharedInternals.A;
|
||||||
|
ReactSharedInternals.A = DefaultAsyncDispatcher;
|
||||||
|
|
||||||
|
const prevRequest = currentRequest;
|
||||||
|
currentRequest = request;
|
||||||
|
|
||||||
|
const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;
|
||||||
|
ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
|
||||||
|
|
||||||
|
const prevResumableState = currentResumableState;
|
||||||
|
setCurrentResumableState(request.resumableState);
|
||||||
|
switchContext(task.context);
|
||||||
|
const prevTaskInDEV = currentTaskInDEV;
|
||||||
|
setCurrentTaskInDEV(task);
|
||||||
|
try {
|
||||||
|
retryNode(request, task);
|
||||||
|
} catch (x) {
|
||||||
|
// Suspended again.
|
||||||
|
resetHooksState();
|
||||||
|
} finally {
|
||||||
|
setCurrentTaskInDEV(prevTaskInDEV);
|
||||||
|
setCurrentResumableState(prevResumableState);
|
||||||
|
|
||||||
|
ReactSharedInternals.H = prevDispatcher;
|
||||||
|
ReactSharedInternals.A = prevAsyncDispatcher;
|
||||||
|
|
||||||
|
ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;
|
||||||
|
if (prevDispatcher === HooksDispatcher) {
|
||||||
|
// This means that we were in a reentrant work loop. This could happen
|
||||||
|
// in a renderer that supports synchronous work like renderToString,
|
||||||
|
// when it's called from within another renderer.
|
||||||
|
// Normally we don't bother switching the contexts to their root/default
|
||||||
|
// values when leaving because we'll likely need the same or similar
|
||||||
|
// context again. However, when we're inside a synchronous loop like this
|
||||||
|
// we'll to restore the context to what it was before returning.
|
||||||
|
switchContext(prevContext);
|
||||||
|
}
|
||||||
|
currentRequest = prevRequest;
|
||||||
|
request.status = prevStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushSuspendedCallSiteOnComponentStack(
|
||||||
|
request: Request,
|
||||||
|
task: Task,
|
||||||
|
): void {
|
||||||
|
setCaptureSuspendedCallSiteDEV(true);
|
||||||
|
const restoreThenableState = ensureSuspendableThenableStateDEV(
|
||||||
|
// refined at the callsite
|
||||||
|
((task.thenableState: any): ThenableState),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
rerenderStalledTask(request, task);
|
||||||
|
} finally {
|
||||||
|
restoreThenableState();
|
||||||
|
setCaptureSuspendedCallSiteDEV(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const suspendCallSiteStack = getSuspendedCallSiteStackDEV();
|
||||||
|
const suspendCallSiteDebugTask = getSuspendedCallSiteDebugTaskDEV();
|
||||||
|
|
||||||
|
if (suspendCallSiteStack !== null) {
|
||||||
|
const ownerStack = task.componentStack;
|
||||||
|
task.componentStack = {
|
||||||
|
// The owner of the suspended call site would be the owner of this task.
|
||||||
|
// We need the task itself otherwise we'd miss a frame.
|
||||||
|
owner: ownerStack,
|
||||||
|
parent: suspendCallSiteStack.parent,
|
||||||
|
stack: suspendCallSiteStack.stack,
|
||||||
|
type: suspendCallSiteStack.type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
task.debugTask = suspendCallSiteDebugTask;
|
||||||
|
}
|
||||||
|
|
||||||
function pushServerComponentStack(
|
function pushServerComponentStack(
|
||||||
task: Task,
|
task: Task,
|
||||||
debugInfo: void | null | ReactDebugInfo,
|
debugInfo: void | null | ReactDebugInfo,
|
||||||
@@ -2723,7 +2814,12 @@ function renderLazyComponent(
|
|||||||
const init = lazyComponent._init;
|
const init = lazyComponent._init;
|
||||||
Component = init(payload);
|
Component = init(payload);
|
||||||
}
|
}
|
||||||
if (request.status === ABORTING) {
|
if (
|
||||||
|
request.status === ABORTING &&
|
||||||
|
// We're going to discard this render anyway.
|
||||||
|
// We just need to reach the point where we suspended in dev.
|
||||||
|
(!__DEV__ || request.status !== STALLED_DEV)
|
||||||
|
) {
|
||||||
// eslint-disable-next-line no-throw-literal
|
// eslint-disable-next-line no-throw-literal
|
||||||
throw null;
|
throw null;
|
||||||
}
|
}
|
||||||
@@ -4535,12 +4631,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
|||||||
debugInfo = node._debugInfo;
|
debugInfo = node._debugInfo;
|
||||||
}
|
}
|
||||||
pushHaltedAwaitOnComponentStack(task, debugInfo);
|
pushHaltedAwaitOnComponentStack(task, debugInfo);
|
||||||
/*
|
|
||||||
if (task.thenableState !== null) {
|
if (task.thenableState !== null) {
|
||||||
// TODO: If we were stalled inside use() of a Client Component then we should
|
pushSuspendedCallSiteOnComponentStack(request, task);
|
||||||
// rerender to get the stack trace from the use() call.
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
137
packages/react-server/src/ReactFizzThenable.js
vendored
137
packages/react-server/src/ReactFizzThenable.js
vendored
@@ -7,20 +7,19 @@
|
|||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Corresponds to ReactFiberWakeable and ReactFlightWakeable modules. Generally,
|
// Corresponds to ReactFiberThenable and ReactFlightThenable modules. Generally,
|
||||||
// changes to one module should be reflected in the others.
|
// changes to one module should be reflected in the others.
|
||||||
|
|
||||||
// TODO: Rename this module and the corresponding Fiber one to "Thenable"
|
|
||||||
// instead of "Wakeable". Or some other more appropriate name.
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Thenable,
|
Thenable,
|
||||||
PendingThenable,
|
PendingThenable,
|
||||||
FulfilledThenable,
|
FulfilledThenable,
|
||||||
RejectedThenable,
|
RejectedThenable,
|
||||||
} from 'shared/ReactTypes';
|
} from 'shared/ReactTypes';
|
||||||
|
import type {ComponentStackNode} from './ReactFizzComponentStack';
|
||||||
|
|
||||||
import noop from 'shared/noop';
|
import noop from 'shared/noop';
|
||||||
|
import {currentTaskInDEV} from './ReactFizzCurrentTask';
|
||||||
|
|
||||||
export opaque type ThenableState = Array<Thenable<any>>;
|
export opaque type ThenableState = Array<Thenable<any>>;
|
||||||
|
|
||||||
@@ -126,6 +125,9 @@ export function trackUsedThenable<T>(
|
|||||||
// get captured by the work loop, log a warning, because that means
|
// get captured by the work loop, log a warning, because that means
|
||||||
// something in userspace must have caught it.
|
// something in userspace must have caught it.
|
||||||
suspendedThenable = thenable;
|
suspendedThenable = thenable;
|
||||||
|
if (__DEV__ && shouldCaptureSuspendedCallSite) {
|
||||||
|
captureSuspendedCallSite();
|
||||||
|
}
|
||||||
throw SuspenseException;
|
throw SuspenseException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,3 +165,130 @@ export function getSuspendedThenable(): Thenable<mixed> {
|
|||||||
suspendedThenable = null;
|
suspendedThenable = null;
|
||||||
return thenable;
|
return thenable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shouldCaptureSuspendedCallSite: boolean = false;
|
||||||
|
export function setCaptureSuspendedCallSiteDEV(capture: boolean): void {
|
||||||
|
if (!__DEV__) {
|
||||||
|
// eslint-disable-next-line react-internal/prod-error-codes
|
||||||
|
throw new Error(
|
||||||
|
'setCaptureSuspendedCallSiteDEV was called in a production environment. ' +
|
||||||
|
'This is a bug in React.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
shouldCaptureSuspendedCallSite = capture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEV-only
|
||||||
|
let suspendedCallSiteStack: ComponentStackNode | null = null;
|
||||||
|
let suspendedCallSiteDebugTask: ConsoleTask | null = null;
|
||||||
|
function captureSuspendedCallSite(): void {
|
||||||
|
// This is currently only used when aborting in Fizz.
|
||||||
|
// You can only abort the render in Fizz and Flight.
|
||||||
|
// In Fiber we only track suspended use via DevTools.
|
||||||
|
// In Flight, we track suspended use via async debug info.
|
||||||
|
const currentTask = currentTaskInDEV;
|
||||||
|
if (currentTask === null) {
|
||||||
|
// eslint-disable-next-line react-internal/prod-error-codes -- not a prod error
|
||||||
|
throw new Error(
|
||||||
|
'Expected to have a current task when tracking a suspend call site. ' +
|
||||||
|
'This is a bug in React.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const currentComponentStack = currentTask.componentStack;
|
||||||
|
if (currentComponentStack === null) {
|
||||||
|
// eslint-disable-next-line react-internal/prod-error-codes -- not a prod error
|
||||||
|
throw new Error(
|
||||||
|
'Expected to have a component stack on the current task when ' +
|
||||||
|
'tracking a suspended call site. This is a bug in React.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
suspendedCallSiteStack = {
|
||||||
|
parent: currentComponentStack.parent,
|
||||||
|
type: currentComponentStack.type,
|
||||||
|
owner: currentComponentStack.owner,
|
||||||
|
stack: Error('react-stack-top-frame'),
|
||||||
|
};
|
||||||
|
// TODO: If this is used in error handlers, the ConsoleTask stack
|
||||||
|
// will just be this debugTask + the stack of the abort() call which usually means
|
||||||
|
// it's just this debugTask.
|
||||||
|
// Ideally we'd be able to reconstruct the owner ConsoleTask as well.
|
||||||
|
// The stack of the debugTask would not point to the suspend location anyway.
|
||||||
|
// The focus is really on callsite which should be used in captureOwnerStack().
|
||||||
|
suspendedCallSiteDebugTask = currentTask.debugTask;
|
||||||
|
}
|
||||||
|
export function getSuspendedCallSiteStackDEV(): ComponentStackNode | null {
|
||||||
|
if (__DEV__) {
|
||||||
|
if (suspendedCallSiteStack === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const callSite = suspendedCallSiteStack;
|
||||||
|
suspendedCallSiteStack = null;
|
||||||
|
return callSite;
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line react-internal/prod-error-codes
|
||||||
|
throw new Error(
|
||||||
|
'getSuspendedCallSiteDEV was called in a production environment. ' +
|
||||||
|
'This is a bug in React.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSuspendedCallSiteDebugTaskDEV(): ConsoleTask | null {
|
||||||
|
if (__DEV__) {
|
||||||
|
if (suspendedCallSiteDebugTask === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const debugTask = suspendedCallSiteDebugTask;
|
||||||
|
suspendedCallSiteDebugTask = null;
|
||||||
|
return debugTask;
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line react-internal/prod-error-codes
|
||||||
|
throw new Error(
|
||||||
|
'getSuspendedCallSiteDebugTaskDEV was called in a production environment. ' +
|
||||||
|
'This is a bug in React.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureSuspendableThenableStateDEV(
|
||||||
|
thenableState: ThenableState,
|
||||||
|
): () => void {
|
||||||
|
if (__DEV__) {
|
||||||
|
const lastThenable = thenableState[thenableState.length - 1];
|
||||||
|
// Reset the last thenable back to pending.
|
||||||
|
switch (lastThenable.status) {
|
||||||
|
case 'fulfilled':
|
||||||
|
const previousThenableValue = lastThenable.value;
|
||||||
|
// $FlowIgnore[method-unbinding] We rebind .then immediately.
|
||||||
|
const previousThenableThen = lastThenable.then.bind(lastThenable);
|
||||||
|
delete lastThenable.value;
|
||||||
|
delete (lastThenable: any).status;
|
||||||
|
// We'll call .then again if we resuspend. Since we potentially corrupted
|
||||||
|
// the internal state of unknown classes, we need to diffuse the potential
|
||||||
|
// crash by replacing the .then method with a noop.
|
||||||
|
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
|
||||||
|
lastThenable.then = noop;
|
||||||
|
return () => {
|
||||||
|
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
|
||||||
|
lastThenable.then = previousThenableThen;
|
||||||
|
lastThenable.value = previousThenableValue;
|
||||||
|
lastThenable.status = 'fulfilled';
|
||||||
|
};
|
||||||
|
case 'rejected':
|
||||||
|
const previousThenableReason = lastThenable.reason;
|
||||||
|
delete lastThenable.reason;
|
||||||
|
delete (lastThenable: any).status;
|
||||||
|
return () => {
|
||||||
|
lastThenable.reason = previousThenableReason;
|
||||||
|
lastThenable.status = 'rejected';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return noop;
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line react-internal/prod-error-codes
|
||||||
|
throw new Error(
|
||||||
|
'ensureSuspendableThenableStateDEV was called in a production environment. ' +
|
||||||
|
'This is a bug in React.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
import {AsyncLocalStorage} from 'node:async_hooks';
|
||||||
|
|
||||||
let act;
|
let act;
|
||||||
let React;
|
let React;
|
||||||
@@ -27,10 +28,43 @@ function normalizeCodeLocInfo(str) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all stackframes not pointing into this file
|
||||||
|
*/
|
||||||
|
function ignoreListStack(str) {
|
||||||
|
if (!str) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ignoreListedStack = '';
|
||||||
|
const lines = str.split('\n');
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.indexOf(__filename) === -1) {
|
||||||
|
} else {
|
||||||
|
ignoreListedStack += '\n' + line.replace(__dirname, '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ignoreListedStack;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTask = new AsyncLocalStorage({defaultValue: null});
|
||||||
|
|
||||||
describe('ReactServer', () => {
|
describe('ReactServer', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
|
||||||
|
console.createTask = jest.fn(taskName => {
|
||||||
|
return {
|
||||||
|
run: taskFn => {
|
||||||
|
const parentTask = currentTask.getStore() || '';
|
||||||
|
return currentTask.run(parentTask + '\n' + taskName, taskFn);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
act = require('internal-test-utils').act;
|
act = require('internal-test-utils').act;
|
||||||
React = require('react');
|
React = require('react');
|
||||||
ReactNoopServer = require('react-noop-renderer/server');
|
ReactNoopServer = require('react-noop-renderer/server');
|
||||||
@@ -49,29 +83,67 @@ describe('ReactServer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('has Owner Stacks in DEV when aborted', async () => {
|
it('has Owner Stacks in DEV when aborted', async () => {
|
||||||
function Component({promise}) {
|
const Context = React.createContext(null);
|
||||||
React.use(promise);
|
|
||||||
|
function Component({p1, p2, p3}) {
|
||||||
|
const context = React.use(Context);
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error('Missing context');
|
||||||
|
}
|
||||||
|
React.use(p1);
|
||||||
|
React.use(p2);
|
||||||
|
React.use(p3);
|
||||||
return <div>Hello, Dave!</div>;
|
return <div>Hello, Dave!</div>;
|
||||||
}
|
}
|
||||||
function App({promise}) {
|
function Indirection({p1, p2, p3}) {
|
||||||
return <Component promise={promise} />;
|
return (
|
||||||
|
<div>
|
||||||
|
<Component p1={p1} p2={p2} p3={p3} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function App({p1, p2, p3}) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<Indirection p1={p1} p2={p2} p3={p3} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let caughtError;
|
let caughtError;
|
||||||
let componentStack;
|
let componentStack;
|
||||||
let ownerStack;
|
let ownerStack;
|
||||||
|
let task;
|
||||||
|
const resolvedPromise = Promise.resolve('one');
|
||||||
|
resolvedPromise.status = 'fulfilled';
|
||||||
|
resolvedPromise.value = 'one';
|
||||||
|
let resolvePendingPromise;
|
||||||
|
const pendingPromise = new Promise(resolve => {
|
||||||
|
resolvePendingPromise = value => {
|
||||||
|
pendingPromise.status = 'fulfilled';
|
||||||
|
pendingPromise.value = value;
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const hangingPromise = new Promise(() => {});
|
||||||
const result = ReactNoopServer.render(
|
const result = ReactNoopServer.render(
|
||||||
<App promise={new Promise(() => {})} />,
|
<Context value="provided">
|
||||||
|
<App p1={resolvedPromise} p2={pendingPromise} p3={hangingPromise} />
|
||||||
|
</Context>,
|
||||||
{
|
{
|
||||||
onError: (error, errorInfo) => {
|
onError: (error, errorInfo) => {
|
||||||
caughtError = error;
|
caughtError = error;
|
||||||
componentStack = errorInfo.componentStack;
|
componentStack = errorInfo.componentStack;
|
||||||
ownerStack = __DEV__ ? React.captureOwnerStack() : null;
|
ownerStack = __DEV__ ? React.captureOwnerStack() : null;
|
||||||
|
task = currentTask.getStore();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
resolvePendingPromise('two');
|
||||||
result.abort();
|
result.abort();
|
||||||
});
|
});
|
||||||
expect(caughtError).toEqual(
|
expect(caughtError).toEqual(
|
||||||
@@ -80,10 +152,35 @@ describe('ReactServer', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(normalizeCodeLocInfo(componentStack)).toEqual(
|
expect(normalizeCodeLocInfo(componentStack)).toEqual(
|
||||||
'\n in Component (at **)' + '\n in App (at **)',
|
'\n in Component (at **)' +
|
||||||
);
|
'\n in div' +
|
||||||
expect(normalizeCodeLocInfo(ownerStack)).toEqual(
|
'\n in Indirection (at **)' +
|
||||||
__DEV__ ? '\n in App (at **)' : null,
|
'\n in div' +
|
||||||
|
'\n in section' +
|
||||||
|
'\n in App (at **)',
|
||||||
);
|
);
|
||||||
|
if (__DEV__) {
|
||||||
|
// The concrete location may change as this test is updated.
|
||||||
|
// Just make sure they still point at the same code
|
||||||
|
if (gate(flags => flags.enableAsyncDebugInfo)) {
|
||||||
|
expect(ignoreListStack(ownerStack)).toEqual(
|
||||||
|
'' +
|
||||||
|
// Pointing at React.use(p2)
|
||||||
|
'\n at Component (./ReactServer-test.js:94:13)' +
|
||||||
|
'\n at Indirection (./ReactServer-test.js:101:44)' +
|
||||||
|
'\n at App (./ReactServer-test.js:109:46)',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
expect(ignoreListStack(ownerStack)).toEqual(
|
||||||
|
'' +
|
||||||
|
'\n at Indirection (./ReactServer-test.js:101:44)' +
|
||||||
|
'\n at App (./ReactServer-test.js:109:46)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(task).toEqual('\n<Component>');
|
||||||
|
} else {
|
||||||
|
expect(ownerStack).toBeNull();
|
||||||
|
expect(task).toEqual(undefined);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user