mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
[Flight] Normalize Stack Using Fake Evals (#30401)
Stacked on https://github.com/facebook/react/pull/30400 and https://github.com/facebook/react/pull/30369 Previously we were using fake evals to recreate a stack for console replaying and thrown errors. However, for owner stacks we just used the raw string that came from the server. This means that the format of the owner stack could include different formats. Like Spidermonkey format for the client components and V8 for the server components. This means that this stack can't be parsed natively by the browser like when printing them as error like in https://github.com/facebook/react/pull/30289. Additionally, since there's no source file registered with that name and no source mapping url, it can't be source mapped. Before: <img width="1329" alt="before-firefox" src="https://github.com/user-attachments/assets/cbe03f9c-96ac-48fb-b58f-f3a224a774f4"> Instead, we need to create a fake stack like we do for the other things. That way when it's printed as an Error it gets source mapped. It also means that the format is consistently in the native format of the current browser. After: <img width="753" alt="after-firefox" src="https://github.com/user-attachments/assets/b436f1f5-ca37-4203-b29f-df9828c9fad3"> So this is nice because you can just take the result from `captureOwnerStack()` and append it to an `Error` stack and print it natively. E.g. this is what React DevTools will do. If you want to parse and present it yourself though it's a bit awkward though. The `captureOwnerStack()` API now includes a bunch of `rsc://React/` URLs. These don't really have any direct connection to the source map. Only the browser knows this connection from the eval. You basically have to strip the prefix and then manually pass the remainder to your own `findSourceMapURL`. Another awkward part is that since Safari doesn't support eval sourceURL exposed into `error.stack` - it means that `captureOwnerStack()` get an empty location for server components since the fake eval doesn't work there. That's not a big deal since these stacks are already broken even for client modules for many because the `eval-source-map` strategy in Webpack doesn't work in Safari for this same reason. A lot of this refactoring is just clarifying that there's three kind of ReactComponentInfo fields: - `stack` - The raw stack as described on the original server. - `debugStack` - The Error object containing the stack as represented in the current client as fake evals. - `debugTask` - The same thing as `debugStack` but described in terms of a native `console.createTask`.
This commit is contained in:
committed by
GitHub
parent
4676d0a0c3
commit
e8e8ea4cc5
+88
-9
@@ -689,11 +689,21 @@ function createElement(
|
||||
value: null,
|
||||
});
|
||||
if (enableOwnerStacks) {
|
||||
let normalizedStackTrace: null | Error = null;
|
||||
if (stack !== null) {
|
||||
// We create a fake stack and then create an Error object inside of it.
|
||||
// This means that the stack trace is now normalized into the native format
|
||||
// of the browser and the stack frames will have been registered with
|
||||
// source mapping information.
|
||||
// This can unfortunately happen within a user space callstack which will
|
||||
// remain on the stack.
|
||||
normalizedStackTrace = createFakeJSXCallStackInDEV(response, stack);
|
||||
}
|
||||
Object.defineProperty(element, '_debugStack', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: stack,
|
||||
value: normalizedStackTrace,
|
||||
});
|
||||
|
||||
let task: null | ConsoleTask = null;
|
||||
@@ -724,6 +734,12 @@ function createElement(
|
||||
writable: true,
|
||||
value: task,
|
||||
});
|
||||
|
||||
// This owner should ideally have already been initialized to avoid getting
|
||||
// user stack frames on the stack.
|
||||
if (owner !== null) {
|
||||
initializeFakeStack(response, owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,9 +768,9 @@ function createElement(
|
||||
};
|
||||
if (enableOwnerStacks) {
|
||||
// $FlowFixMe[cannot-write]
|
||||
erroredComponent.stack = element._debugStack;
|
||||
erroredComponent.debugStack = element._debugStack;
|
||||
// $FlowFixMe[cannot-write]
|
||||
erroredComponent.task = element._debugTask;
|
||||
erroredComponent.debugTask = element._debugTask;
|
||||
}
|
||||
erroredChunk._debugInfo = [erroredComponent];
|
||||
}
|
||||
@@ -915,9 +931,9 @@ function waitForReference<T>(
|
||||
};
|
||||
if (enableOwnerStacks) {
|
||||
// $FlowFixMe[cannot-write]
|
||||
erroredComponent.stack = element._debugStack;
|
||||
erroredComponent.debugStack = element._debugStack;
|
||||
// $FlowFixMe[cannot-write]
|
||||
erroredComponent.task = element._debugTask;
|
||||
erroredComponent.debugTask = element._debugTask;
|
||||
}
|
||||
const chunkDebugInfo: ReactDebugInfo =
|
||||
chunk._debugInfo || (chunk._debugInfo = []);
|
||||
@@ -2001,16 +2017,23 @@ function initializeFakeTask(
|
||||
response: Response,
|
||||
debugInfo: ReactComponentInfo | ReactAsyncInfo,
|
||||
): null | ConsoleTask {
|
||||
if (!supportsCreateTask || typeof debugInfo.stack !== 'string') {
|
||||
if (!supportsCreateTask) {
|
||||
return null;
|
||||
}
|
||||
const componentInfo: ReactComponentInfo = (debugInfo: any); // Refined
|
||||
const stack: string = debugInfo.stack;
|
||||
const cachedEntry = componentInfo.task;
|
||||
const cachedEntry = componentInfo.debugTask;
|
||||
if (cachedEntry !== undefined) {
|
||||
return cachedEntry;
|
||||
}
|
||||
|
||||
if (typeof debugInfo.stack !== 'string') {
|
||||
// If this is an error, we should've really already initialized the task.
|
||||
// If it's null, we can't initialize a task.
|
||||
return null;
|
||||
}
|
||||
|
||||
const stack = debugInfo.stack;
|
||||
|
||||
const ownerTask =
|
||||
componentInfo.owner == null
|
||||
? null
|
||||
@@ -2034,10 +2057,63 @@ function initializeFakeTask(
|
||||
componentTask = ownerTask.run(callStack);
|
||||
}
|
||||
// $FlowFixMe[cannot-write]: We consider this part of initialization.
|
||||
componentInfo.task = componentTask;
|
||||
componentInfo.debugTask = componentTask;
|
||||
return componentTask;
|
||||
}
|
||||
|
||||
const createFakeJSXCallStack = {
|
||||
'react-stack-bottom-frame': function (
|
||||
response: Response,
|
||||
stack: string,
|
||||
): Error {
|
||||
const callStackForError = buildFakeCallStack(
|
||||
response,
|
||||
stack,
|
||||
fakeJSXCallSite,
|
||||
);
|
||||
return callStackForError();
|
||||
},
|
||||
};
|
||||
|
||||
const createFakeJSXCallStackInDEV: (
|
||||
response: Response,
|
||||
stack: string,
|
||||
) => Error = __DEV__
|
||||
? // We use this technique to trick minifiers to preserve the function name.
|
||||
(createFakeJSXCallStack['react-stack-bottom-frame'].bind(
|
||||
createFakeJSXCallStack,
|
||||
): any)
|
||||
: (null: any);
|
||||
|
||||
/** @noinline */
|
||||
function fakeJSXCallSite() {
|
||||
// This extra call frame represents the JSX creation function. We always pop this frame
|
||||
// off before presenting so it needs to be part of the stack.
|
||||
return new Error('react-stack-top-frame');
|
||||
}
|
||||
|
||||
function initializeFakeStack(
|
||||
response: Response,
|
||||
debugInfo: ReactComponentInfo | ReactAsyncInfo,
|
||||
): void {
|
||||
const cachedEntry = debugInfo.debugStack;
|
||||
if (cachedEntry !== undefined) {
|
||||
return;
|
||||
}
|
||||
if (typeof debugInfo.stack === 'string') {
|
||||
// $FlowFixMe[cannot-write]
|
||||
// $FlowFixMe[prop-missing]
|
||||
debugInfo.debugStack = createFakeJSXCallStackInDEV(
|
||||
response,
|
||||
debugInfo.stack,
|
||||
);
|
||||
}
|
||||
if (debugInfo.owner != null) {
|
||||
// Initialize any owners not yet initialized.
|
||||
initializeFakeStack(response, debugInfo.owner);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDebugInfo(
|
||||
response: Response,
|
||||
id: number,
|
||||
@@ -2054,6 +2130,8 @@ function resolveDebugInfo(
|
||||
// render phase so we're not inside a user space stack at this point. If we waited
|
||||
// to initialize it when we need it, we might be inside user code.
|
||||
initializeFakeTask(response, debugInfo);
|
||||
initializeFakeStack(response, debugInfo);
|
||||
|
||||
const chunk = getChunk(response, id);
|
||||
const chunkDebugInfo: ReactDebugInfo =
|
||||
chunk._debugInfo || (chunk._debugInfo = []);
|
||||
@@ -2096,6 +2174,7 @@ function resolveConsoleEntry(
|
||||
);
|
||||
if (owner != null) {
|
||||
const task = initializeFakeTask(response, owner);
|
||||
initializeFakeStack(response, owner);
|
||||
if (task !== null) {
|
||||
task.run(callStack);
|
||||
return;
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ function normalizeCodeLocInfo(str) {
|
||||
|
||||
function normalizeComponentInfo(debugInfo) {
|
||||
if (typeof debugInfo.stack === 'string') {
|
||||
const {task, ...copy} = debugInfo;
|
||||
const {debugTask, debugStack, ...copy} = debugInfo;
|
||||
copy.stack = normalizeCodeLocInfo(debugInfo.stack);
|
||||
if (debugInfo.owner) {
|
||||
copy.owner = normalizeComponentInfo(debugInfo.owner);
|
||||
|
||||
+3
-3
@@ -1858,14 +1858,14 @@ describe('ReactUpdates', () => {
|
||||
|
||||
let error = null;
|
||||
let ownerStack = null;
|
||||
let nativeStack = null;
|
||||
let debugStack = null;
|
||||
const originalConsoleError = console.error;
|
||||
console.error = e => {
|
||||
error = e;
|
||||
ownerStack = gate(flags => flags.enableOwnerStacks)
|
||||
? React.captureOwnerStack()
|
||||
: null;
|
||||
nativeStack = new Error().stack;
|
||||
debugStack = new Error().stack;
|
||||
Scheduler.log('stop');
|
||||
};
|
||||
try {
|
||||
@@ -1879,7 +1879,7 @@ describe('ReactUpdates', () => {
|
||||
|
||||
expect(error).toContain('Maximum update depth exceeded');
|
||||
// The currently executing effect should be on the native stack
|
||||
expect(nativeStack).toContain('at myEffect');
|
||||
expect(debugStack).toContain('at myEffect');
|
||||
if (gate(flags => flags.enableOwnerStacks)) {
|
||||
expect(ownerStack).toContain('at App');
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -2015,7 +2015,7 @@ function createChildReconciler(
|
||||
if (typeof debugInfo[i].stack === 'string') {
|
||||
throwFiber._debugOwner = (debugInfo[i]: any);
|
||||
if (enableOwnerStacks) {
|
||||
throwFiber._debugTask = debugInfo[i].task;
|
||||
throwFiber._debugTask = debugInfo[i].debugTask;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -165,17 +165,13 @@ export function getOwnerStackByFiberInDev(workInProgress: Fiber): string {
|
||||
info += '\n' + debugStack;
|
||||
}
|
||||
}
|
||||
} else if (typeof owner.stack === 'string') {
|
||||
} else if (owner.debugStack != null) {
|
||||
// Server Component
|
||||
// The Server Component stack can come from a different VM that formats it different.
|
||||
// Likely V8. Since Chrome based browsers support createTask which is going to use
|
||||
// another code path anyway. I.e. this is likely NOT a V8 based browser.
|
||||
// This will cause some of the stack to have different formatting.
|
||||
// TODO: Normalize server component stacks to the client formatting.
|
||||
const ownerStack: string = owner.stack;
|
||||
const ownerStack: Error = owner.debugStack;
|
||||
owner = owner.owner;
|
||||
if (owner && ownerStack !== '') {
|
||||
info += '\n' + ownerStack;
|
||||
if (owner && ownerStack) {
|
||||
// TODO: Should we stash this somewhere for caching purposes?
|
||||
info += '\n' + formatOwnerStack(ownerStack);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ function filterDebugStack(error: Error): string {
|
||||
// To keep things light we exclude the entire trace in this case.
|
||||
return '';
|
||||
}
|
||||
const frames = stack.split('\n').slice(1);
|
||||
const frames = stack.split('\n').slice(1); // Pop the JSX frame.
|
||||
return frames.filter(isNotExternal).join('\n');
|
||||
}
|
||||
|
||||
|
||||
+19
-8
@@ -159,21 +159,32 @@ export function getOwnerStackByComponentStackNodeInDev(
|
||||
componentStack;
|
||||
|
||||
while (owner) {
|
||||
let debugStack: void | null | string | Error = owner.stack;
|
||||
if (typeof debugStack !== 'string' && debugStack != null) {
|
||||
// Stash the formatted stack so that we can avoid redoing the filtering.
|
||||
// $FlowFixMe[cannot-write]: This has been refined to a ComponentStackNode.
|
||||
owner.stack = debugStack = formatOwnerStack(debugStack);
|
||||
let ownerStack: ?string = null;
|
||||
if (owner.debugStack != null) {
|
||||
// Server Component
|
||||
// TODO: Should we stash this somewhere for caching purposes?
|
||||
ownerStack = formatOwnerStack(owner.debugStack);
|
||||
owner = owner.owner;
|
||||
} else if (owner.stack != null) {
|
||||
// Client Component
|
||||
const node: ComponentStackNode = (owner: any);
|
||||
if (typeof owner.stack !== 'string') {
|
||||
ownerStack = node.stack = formatOwnerStack(owner.stack);
|
||||
} else {
|
||||
ownerStack = owner.stack;
|
||||
}
|
||||
owner = owner.owner;
|
||||
} else {
|
||||
owner = owner.owner;
|
||||
}
|
||||
owner = owner.owner;
|
||||
// If we don't actually print the stack if there is no owner of this JSX element.
|
||||
// In a real app it's typically not useful since the root app is always controlled
|
||||
// by the framework. These also tend to have noisy stacks because they're not rooted
|
||||
// in a React render but in some imperative bootstrapping code. It could be useful
|
||||
// if the element was created in module scope. E.g. hoisted. We could add a a single
|
||||
// stack frame for context for example but it doesn't say much if that's a wrapper.
|
||||
if (owner && debugStack) {
|
||||
info += '\n' + debugStack;
|
||||
if (owner && ownerStack) {
|
||||
info += '\n' + ownerStack;
|
||||
}
|
||||
}
|
||||
return info;
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ function filterDebugStack(error: Error): string {
|
||||
// To keep things light we exclude the entire trace in this case.
|
||||
return '';
|
||||
}
|
||||
const frames = stack.split('\n').slice(1);
|
||||
const frames = stack.split('\n').slice(1); // Pop the JSX frame.
|
||||
return frames.filter(isNotExternal).join('\n');
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -859,17 +859,17 @@ function pushServerComponentStack(
|
||||
if (typeof componentInfo.name !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (enableOwnerStacks && componentInfo.stack === undefined) {
|
||||
if (enableOwnerStacks && componentInfo.debugStack === undefined) {
|
||||
continue;
|
||||
}
|
||||
task.componentStack = {
|
||||
parent: task.componentStack,
|
||||
type: componentInfo,
|
||||
owner: componentInfo.owner,
|
||||
stack: componentInfo.stack,
|
||||
stack: enableOwnerStacks ? componentInfo.debugStack : null,
|
||||
};
|
||||
if (enableOwnerStacks) {
|
||||
task.debugTask = (componentInfo.task: any);
|
||||
task.debugTask = (componentInfo.debugTask: any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
// TODO: Make this configurable on the Request.
|
||||
const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\<anonymous\>\)/;
|
||||
|
||||
function isNotExternal(stackFrame: string): boolean {
|
||||
return !externalRegExp.test(stackFrame);
|
||||
}
|
||||
|
||||
function filterDebugStack(error: Error): string {
|
||||
// Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
|
||||
// to save bandwidth even in DEV. We'll also replay these stacks on the client so by
|
||||
// stripping them early we avoid that overhead. Otherwise we'd normally just rely on
|
||||
// the DevTools or framework's ignore lists to filter them out.
|
||||
let stack = error.stack;
|
||||
if (stack.startsWith('Error: react-stack-top-frame\n')) {
|
||||
// V8's default formatting prefixes with the error message which we
|
||||
// don't want/need.
|
||||
stack = stack.slice(29);
|
||||
}
|
||||
let idx = stack.indexOf('react-stack-bottom-frame');
|
||||
if (idx !== -1) {
|
||||
idx = stack.lastIndexOf('\n', idx);
|
||||
}
|
||||
if (idx !== -1) {
|
||||
// Cut off everything after the bottom frame since it'll be internals.
|
||||
stack = stack.slice(0, idx);
|
||||
}
|
||||
const frames = stack.split('\n').slice(1); // Pop the JSX frame.
|
||||
return frames.filter(isNotExternal).join('\n');
|
||||
}
|
||||
|
||||
export function formatOwnerStack(ownerStackTrace: Error): string {
|
||||
return filterDebugStack(ownerStackTrace);
|
||||
}
|
||||
+25
-14
@@ -351,7 +351,7 @@ type Task = {
|
||||
thenableState: ThenableState | null,
|
||||
environmentName: string, // DEV-only. Used to track if the environment for this task changed.
|
||||
debugOwner: null | ReactComponentInfo, // DEV-only
|
||||
debugStack: null | string, // DEV-only
|
||||
debugStack: null | Error, // DEV-only
|
||||
debugTask: null | ConsoleTask, // DEV-only
|
||||
};
|
||||
|
||||
@@ -972,7 +972,12 @@ function callWithDebugContextInDEV<A, T>(
|
||||
};
|
||||
if (enableOwnerStacks) {
|
||||
// $FlowFixMe[cannot-write]
|
||||
componentDebugInfo.stack = task.debugStack;
|
||||
componentDebugInfo.stack =
|
||||
task.debugStack === null ? null : filterDebugStack(task.debugStack);
|
||||
// $FlowFixMe[cannot-write]
|
||||
componentDebugInfo.debugStack = task.debugStack;
|
||||
// $FlowFixMe[cannot-write]
|
||||
componentDebugInfo.debugTask = task.debugTask;
|
||||
}
|
||||
const debugTask = task.debugTask;
|
||||
// We don't need the async component storage context here so we only set the
|
||||
@@ -1029,7 +1034,12 @@ function renderFunctionComponent<Props>(
|
||||
}: ReactComponentInfo);
|
||||
if (enableOwnerStacks) {
|
||||
// $FlowFixMe[cannot-write]
|
||||
componentDebugInfo.stack = task.debugStack;
|
||||
componentDebugInfo.stack =
|
||||
task.debugStack === null ? null : filterDebugStack(task.debugStack);
|
||||
// $FlowFixMe[cannot-write]
|
||||
componentDebugInfo.debugStack = task.debugStack;
|
||||
// $FlowFixMe[cannot-write]
|
||||
componentDebugInfo.debugTask = task.debugTask;
|
||||
}
|
||||
// We outline this model eagerly so that we can refer to by reference as an owner.
|
||||
// If we had a smarter way to dedupe we might not have to do this if there ends up
|
||||
@@ -1419,7 +1429,7 @@ function renderClientElement(
|
||||
key,
|
||||
props,
|
||||
task.debugOwner,
|
||||
task.debugStack,
|
||||
task.debugStack === null ? null : filterDebugStack(task.debugStack),
|
||||
validated,
|
||||
]
|
||||
: [REACT_ELEMENT_TYPE, type, key, props, task.debugOwner]
|
||||
@@ -1598,7 +1608,7 @@ function createTask(
|
||||
implicitSlot: boolean,
|
||||
abortSet: Set<Task>,
|
||||
debugOwner: null | ReactComponentInfo, // DEV-only
|
||||
debugStack: null | string, // DEV-only
|
||||
debugStack: null | Error, // DEV-only
|
||||
debugTask: null | ConsoleTask, // DEV-only
|
||||
): Task {
|
||||
request.pendingChunks++;
|
||||
@@ -2205,10 +2215,7 @@ function renderModelDestructive(
|
||||
if (__DEV__) {
|
||||
task.debugOwner = element._owner;
|
||||
if (enableOwnerStacks) {
|
||||
task.debugStack =
|
||||
!element._debugStack || typeof element._debugStack === 'string'
|
||||
? element._debugStack
|
||||
: filterDebugStack(element._debugStack);
|
||||
task.debugStack = element._debugStack;
|
||||
task.debugTask = element._debugTask;
|
||||
}
|
||||
// TODO: Pop this. Since we currently don't have a point where we can pop the stack
|
||||
@@ -2507,10 +2514,11 @@ function renderModelDestructive(
|
||||
if (__DEV__) {
|
||||
if (
|
||||
// TODO: We don't currently have a brand check on ReactComponentInfo. Reconsider.
|
||||
typeof value.task === 'object' &&
|
||||
value.task !== null &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof value.task.run === 'function' &&
|
||||
((typeof value.debugTask === 'object' &&
|
||||
value.debugTask !== null &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof value.debugTask.run === 'function') ||
|
||||
value.debugStack instanceof Error) &&
|
||||
typeof value.name === 'string' &&
|
||||
typeof value.env === 'string' &&
|
||||
value.owner !== undefined &&
|
||||
@@ -2520,7 +2528,10 @@ function renderModelDestructive(
|
||||
) {
|
||||
// This looks like a ReactComponentInfo. We can't serialize the ConsoleTask object so we
|
||||
// need to omit it before serializing.
|
||||
const componentDebugInfo: Omit<ReactComponentInfo, 'task'> = {
|
||||
const componentDebugInfo: Omit<
|
||||
ReactComponentInfo,
|
||||
'debugTask' | 'debugStack',
|
||||
> = {
|
||||
name: value.name,
|
||||
env: value.env,
|
||||
owner: (value: any).owner,
|
||||
|
||||
@@ -13,6 +13,8 @@ import {describeBuiltInComponentFrame} from 'shared/ReactComponentStackFrame';
|
||||
|
||||
import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {formatOwnerStack} from '../ReactFlightOwnerStack';
|
||||
|
||||
export function getOwnerStackByComponentInfoInDev(
|
||||
componentInfo: ReactComponentInfo,
|
||||
): string {
|
||||
@@ -34,12 +36,13 @@ export function getOwnerStackByComponentInfoInDev(
|
||||
let owner: void | null | ReactComponentInfo = componentInfo;
|
||||
|
||||
while (owner) {
|
||||
if (typeof owner.stack === 'string') {
|
||||
const ownerStack: ?Error = owner.debugStack;
|
||||
if (ownerStack != null) {
|
||||
// Server Component
|
||||
const ownerStack: string = owner.stack;
|
||||
owner = owner.owner;
|
||||
if (owner && ownerStack !== '') {
|
||||
info += '\n' + ownerStack;
|
||||
if (owner) {
|
||||
// TODO: Should we stash this somewhere for caching purposes?
|
||||
info += '\n' + formatOwnerStack(ownerStack);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
|
||||
@@ -183,7 +183,9 @@ export type ReactComponentInfo = {
|
||||
+env?: string,
|
||||
+owner?: null | ReactComponentInfo,
|
||||
+stack?: null | string,
|
||||
+task?: null | ConsoleTask,
|
||||
// Stashed Data for the Specific Execution Environment. Not part of the transport protocol
|
||||
+debugStack?: null | Error,
|
||||
+debugTask?: null | ConsoleTask,
|
||||
};
|
||||
|
||||
export type ReactAsyncInfo = {
|
||||
|
||||
Reference in New Issue
Block a user