Track Stack of JSX Calls (#29032)

This is the first step to experimenting with a new type of stack traces
behind the `enableOwnerStacks` flag - in DEV only.

The idea is to generate stacks that are more like if the JSX was a
direct call even though it's actually a lazy call. Not only can you see
which exact JSX call line number generated the erroring component but if
that's inside an abstraction function, which function called that
function and if it's a component, which component generated that
component. For this to make sense it really need to be the "owner" stack
rather than the parent stack like we do for other component stacks. On
one hand it has more precise information but on the other hand it also
loses context. For most types of problems the owner stack is the most
useful though since it tells you which component rendered this
component.

The problem with the platform in its current state is that there's two
ways to deal with stacks:

1) `new Error().stack` 
2) `console.createTask()`

The nice thing about `new Error().stack` is that we can extract the
frames and piece them together in whatever way we want. That is great
for constructing custom UIs like error dialogs. Unfortunately, we can't
take custom stacks and set them in the native UIs like Chrome DevTools.

The nice thing about `console.createTask()` is that the resulting stacks
are natively integrated into the Chrome DevTools in the console and the
breakpoint debugger. They also automatically follow source mapping and
ignoreLists. The downside is that there's no way to extract the async
stack outside the native UI itself so this information cannot be used
for custom UIs like errors dialogs. It also means we can't collect this
on the server and then pass it to the client for server components.

The solution here is that we use both techniques and collect both an
`Error` object and a `Task` object for every JSX call.

The main concern about this approach is the performance so that's the
main thing to test. It's certainly too slow for production but it might
also be too slow even for DEV.

This first PR doesn't actually use the stacks yet. It just collects them
as the first step. The next step is to start utilizing this information
in error printing etc.

For RSC we pass the stack along across over the wire. This can be
concatenated on the client following the owner path to create an owner
stack leading back into the server. We'll later use this information to
restore fake frames on the client for native integration. Since this
information quickly gets pretty heavy if we include all frames, we strip
out the top frame. We also strip out everything below the functions that
call into user space in the Flight runtime. To do this we need to figure
out the frames that represents calling out into user space. The
resulting stack is typically just the one frame inside the owner
component's JSX callsite. I also eagerly strip out things we expect to
be ignoreList:ed anyway - such as `node_modules` and Node.js internals.
This commit is contained in:
Sebastian Markbåge
2024-05-09 12:23:05 -04:00
committed by GitHub
parent 04b058868c
commit 151cce3740
17 changed files with 483 additions and 70 deletions
+20
View File
@@ -43,6 +43,7 @@ import {
enablePostpone,
enableRefAsProp,
enableFlightReadableStream,
enableOwnerStacks,
} from 'shared/ReactFeatureFlags';
import {
@@ -563,6 +564,7 @@ function createElement(
key: mixed,
props: mixed,
owner: null | ReactComponentInfo, // DEV-only
stack: null | string, // DEV-only
): React$Element<any> {
let element: any;
if (__DEV__ && enableRefAsProp) {
@@ -623,6 +625,23 @@ function createElement(
writable: true,
value: null,
});
if (enableOwnerStacks) {
Object.defineProperty(element, '_debugStack', {
configurable: false,
enumerable: false,
writable: true,
value: {stack: stack},
});
Object.defineProperty(element, '_debugTask', {
configurable: false,
enumerable: false,
writable: true,
value: null,
});
}
// TODO: We should be freezing the element but currently, we might write into
// _debugInfo later. We could move it into _store which remains mutable.
Object.freeze(element.props);
}
return element;
}
@@ -1003,6 +1022,7 @@ function parseModelTuple(
tuple[2],
tuple[3],
__DEV__ ? (tuple: any)[4] : null,
__DEV__ && enableOwnerStacks ? (tuple: any)[5] : null,
);
}
return value;
+127 -24
View File
@@ -21,12 +21,24 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') {
function normalizeCodeLocInfo(str) {
return (
str &&
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
return '\n in ' + name + (/\d/.test(m) ? ' (at **)' : '');
str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
})
);
}
function getDebugInfo(obj) {
const debugInfo = obj._debugInfo;
if (debugInfo) {
for (let i = 0; i < debugInfo.length; i++) {
if (typeof debugInfo[i].stack === 'string') {
debugInfo[i].stack = normalizeCodeLocInfo(debugInfo[i].stack);
}
}
}
return debugInfo;
}
const heldValues = [];
let finalizationCallback;
function FinalizationRegistryMock(callback) {
@@ -221,8 +233,19 @@ describe('ReactFlight', () => {
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport);
const greeting = rootModel.greeting;
expect(greeting._debugInfo).toEqual(
__DEV__ ? [{name: 'Greeting', env: 'Server', owner: null}] : undefined,
expect(getDebugInfo(greeting)).toEqual(
__DEV__
? [
{
name: 'Greeting',
env: 'Server',
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
},
]
: undefined,
);
ReactNoop.render(greeting);
});
@@ -248,8 +271,19 @@ describe('ReactFlight', () => {
await act(async () => {
const promise = ReactNoopFlightClient.read(transport);
expect(promise._debugInfo).toEqual(
__DEV__ ? [{name: 'Greeting', env: 'Server', owner: null}] : undefined,
expect(getDebugInfo(promise)).toEqual(
__DEV__
? [
{
name: 'Greeting',
env: 'Server',
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
},
]
: undefined,
);
ReactNoop.render(await promise);
});
@@ -2233,9 +2267,11 @@ describe('ReactFlight', () => {
return <span>!</span>;
}
const lazy = React.lazy(async () => ({
default: <ThirdPartyLazyComponent />,
}));
const lazy = React.lazy(async function myLazy() {
return {
default: <ThirdPartyLazyComponent />,
};
});
function ThirdPartyComponent() {
return <span>stranger</span>;
@@ -2269,31 +2305,61 @@ describe('ReactFlight', () => {
await act(async () => {
const promise = ReactNoopFlightClient.read(transport);
expect(promise._debugInfo).toEqual(
expect(getDebugInfo(promise)).toEqual(
__DEV__
? [{name: 'ServerComponent', env: 'Server', owner: null}]
? [
{
name: 'ServerComponent',
env: 'Server',
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
},
]
: undefined,
);
const result = await promise;
const thirdPartyChildren = await result.props.children[1];
// We expect the debug info to be transferred from the inner stream to the outer.
expect(thirdPartyChildren[0]._debugInfo).toEqual(
expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
__DEV__
? [{name: 'ThirdPartyComponent', env: 'third-party', owner: null}]
? [
{
name: 'ThirdPartyComponent',
env: 'third-party',
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
},
]
: undefined,
);
expect(thirdPartyChildren[1]._debugInfo).toEqual(
expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
__DEV__
? [{name: 'ThirdPartyLazyComponent', env: 'third-party', owner: null}]
? [
{
name: 'ThirdPartyLazyComponent',
env: 'third-party',
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in myLazy (at **)\n in lazyInitializer (at **)'
: undefined,
},
]
: undefined,
);
expect(thirdPartyChildren[2]._debugInfo).toEqual(
expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
__DEV__
? [
{
name: 'ThirdPartyFragmentComponent',
env: 'third-party',
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
},
]
: undefined,
@@ -2357,24 +2423,47 @@ describe('ReactFlight', () => {
await act(async () => {
const promise = ReactNoopFlightClient.read(transport);
expect(promise._debugInfo).toEqual(
expect(getDebugInfo(promise)).toEqual(
__DEV__
? [{name: 'ServerComponent', env: 'Server', owner: null}]
? [
{
name: 'ServerComponent',
env: 'Server',
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
},
]
: undefined,
);
const result = await promise;
const thirdPartyFragment = await result.props.children;
expect(thirdPartyFragment._debugInfo).toEqual(
__DEV__ ? [{name: 'Keyed', env: 'Server', owner: null}] : undefined,
expect(getDebugInfo(thirdPartyFragment)).toEqual(
__DEV__
? [
{
name: 'Keyed',
env: 'Server',
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in ServerComponent (at **)'
: undefined,
},
]
: undefined,
);
// We expect the debug info to be transferred from the inner stream to the outer.
expect(thirdPartyFragment.props.children._debugInfo).toEqual(
expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual(
__DEV__
? [
{
name: 'ThirdPartyAsyncIterableComponent',
env: 'third-party',
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
},
]
: undefined,
@@ -2467,10 +2556,24 @@ describe('ReactFlight', () => {
// We've rendered down to the span.
expect(greeting.type).toBe('span');
if (__DEV__) {
const greetInfo = {name: 'Greeting', env: 'Server', owner: null};
expect(greeting._debugInfo).toEqual([
const greetInfo = {
name: 'Greeting',
env: 'Server',
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
};
expect(getDebugInfo(greeting)).toEqual([
greetInfo,
{name: 'Container', env: 'Server', owner: greetInfo},
{
name: 'Container',
env: 'Server',
owner: greetInfo,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Greeting (at **)'
: undefined,
},
]);
// The owner that created the span was the outer server component.
// We expect the debug info to be referentially equal to the owner.
@@ -263,7 +263,7 @@ describe('ReactFlightDOMEdge', () => {
const serializedContent = await readResult(stream1);
expect(serializedContent.length).toBeLessThan(400);
expect(serializedContent.length).toBeLessThan(410);
expect(timesRendered).toBeLessThan(5);
const model = await ReactServerDOMClient.createFromReadableStream(stream2, {
@@ -296,7 +296,7 @@ describe('ReactFlightDOMEdge', () => {
const [stream1, stream2] = passThrough(stream).tee();
const serializedContent = await readResult(stream1);
expect(serializedContent.length).toBeLessThan(400);
expect(serializedContent.length).toBeLessThan(__DEV__ ? 590 : 400);
expect(timesRendered).toBeLessThan(5);
const model = await ReactServerDOMClient.createFromReadableStream(stream2, {
@@ -324,7 +324,7 @@ describe('ReactFlightDOMEdge', () => {
<ServerComponent recurse={20} />,
);
const serializedContent = await readResult(stream);
const expectedDebugInfoSize = __DEV__ ? 64 * 20 : 0;
const expectedDebugInfoSize = __DEV__ ? 300 * 20 : 0;
expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize);
});
@@ -742,10 +742,18 @@ describe('ReactFlightDOMEdge', () => {
// We've rendered down to the span.
expect(greeting.type).toBe('span');
if (__DEV__) {
const greetInfo = {name: 'Greeting', env: 'Server', owner: null};
const greetInfo = expect.objectContaining({
name: 'Greeting',
env: 'Server',
owner: null,
});
expect(lazyWrapper._debugInfo).toEqual([
greetInfo,
{name: 'Container', env: 'Server', owner: greetInfo},
expect.objectContaining({
name: 'Container',
env: 'Server',
owner: greetInfo,
}),
]);
// The owner that created the span was the outer server component.
// We expect the debug info to be referentially equal to the owner.
+210 -38
View File
@@ -17,6 +17,7 @@ import {
enableTaint,
enableRefAsProp,
enableServerComponentLogs,
enableOwnerStacks,
} from 'shared/ReactFeatureFlags';
import {enableFlightReadableStream} from 'shared/ReactFeatureFlags';
@@ -123,6 +124,98 @@ import binaryToComparableString from 'shared/binaryToComparableString';
import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
// TODO: Make this configurable on the Request.
const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\<anonymous\>\)/;
let callComponentFrame: null | string = null;
let callIteratorFrame: null | string = null;
let callLazyInitFrame: null | string = null;
function isNotExternal(stackFrame: string): boolean {
return !externalRegExp.test(stackFrame);
}
function initCallComponentFrame(): string {
// Extract the stack frame of the callComponentInDEV function.
const error = callComponentInDEV(Error, 'react-stack-top-frame', {});
const stack = error.stack;
const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0;
const endIdx = stack.indexOf('\n', startIdx);
if (endIdx === -1) {
return stack.slice(startIdx);
}
return stack.slice(startIdx, endIdx);
}
function initCallIteratorFrame(): string {
// Extract the stack frame of the callIteratorInDEV function.
try {
(callIteratorInDEV: any)({next: null});
return '';
} catch (error) {
const stack = error.stack;
const startIdx = stack.startsWith('TypeError: ')
? stack.indexOf('\n') + 1
: 0;
const endIdx = stack.indexOf('\n', startIdx);
if (endIdx === -1) {
return stack.slice(startIdx);
}
return stack.slice(startIdx, endIdx);
}
}
function initCallLazyInitFrame(): string {
// Extract the stack frame of the callLazyInitInDEV function.
const error = callLazyInitInDEV({
$$typeof: REACT_LAZY_TYPE,
_init: Error,
_payload: 'react-stack-top-frame',
});
const stack = error.stack;
const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0;
const endIdx = stack.indexOf('\n', startIdx);
if (endIdx === -1) {
return stack.slice(startIdx);
}
return stack.slice(startIdx, endIdx);
}
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);
}
const frames = stack.split('\n').slice(1);
if (callComponentFrame === null) {
callComponentFrame = initCallComponentFrame();
}
let lastFrameIdx = frames.indexOf(callComponentFrame);
if (lastFrameIdx === -1) {
if (callLazyInitFrame === null) {
callLazyInitFrame = initCallLazyInitFrame();
}
lastFrameIdx = frames.indexOf(callLazyInitFrame);
if (lastFrameIdx === -1) {
if (callIteratorFrame === null) {
callIteratorFrame = initCallIteratorFrame();
}
lastFrameIdx = frames.indexOf(callIteratorFrame);
}
}
if (lastFrameIdx !== -1) {
// Cut off everything after our "callComponent" slot since it'll be Flight internals.
frames.length = lastFrameIdx;
}
return frames.filter(isNotExternal).join('\n');
}
initAsyncDebugInfo();
function patchConsole(consoleInst: typeof console, methodName: string) {
@@ -146,10 +239,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
// Extract the stack. Not all console logs print the full stack but they have at
// least the line it was called from. We could optimize transfer by keeping just
// one stack frame but keeping it simple for now and include all frames.
let stack = new Error().stack;
if (stack.startsWith('Error: \n')) {
stack = stack.slice(8);
}
let stack = filterDebugStack(new Error('react-stack-top-frame'));
const firstLine = stack.indexOf('\n');
if (firstLine === -1) {
stack = '';
@@ -621,6 +711,20 @@ function serializeReadableStream(
return serializeByValueID(streamTask.id);
}
// This indirect exists so we can exclude its stack frame in DEV (and anything below it).
/** @noinline */
function callIteratorInDEV(
iterator: $AsyncIterator<ReactClientValue, ReactClientValue, void>,
progress: (
entry:
| {done: false, +value: ReactClientValue, ...}
| {done: true, +value: ReactClientValue, ...},
) => void,
error: (reason: mixed) => void,
) {
iterator.next().then(progress, error);
}
function serializeAsyncIterable(
request: Request,
task: Task,
@@ -697,7 +801,11 @@ function serializeAsyncIterable(
request.pendingChunks++;
tryStreamTask(request, streamTask);
enqueueFlush(request);
iterator.next().then(progress, error);
if (__DEV__) {
callIteratorInDEV(iterator, progress, error);
} else {
iterator.next().then(progress, error);
}
} catch (x) {
error(x);
return;
@@ -731,7 +839,11 @@ function serializeAsyncIterable(
}
}
request.abortListeners.add(error);
iterator.next().then(progress, error);
if (__DEV__) {
callIteratorInDEV(iterator, progress, error);
} else {
iterator.next().then(progress, error);
}
return serializeByValueID(streamTask.id);
}
@@ -809,13 +921,49 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
return lazyType;
}
// This indirect exists so we can exclude its stack frame in DEV (and anything below it).
/** @noinline */
function callComponentInDEV<Props, R>(
Component: (p: Props, arg: void) => R,
props: Props,
componentDebugInfo: ReactComponentInfo,
): R {
// The secondArg is always undefined in Server Components since refs error early.
const secondArg = undefined;
setCurrentOwner(componentDebugInfo);
try {
if (supportsComponentStorage) {
// Run the component in an Async Context that tracks the current owner.
return componentStorage.run(
componentDebugInfo,
Component,
props,
secondArg,
);
} else {
return Component(props, secondArg);
}
} finally {
setCurrentOwner(null);
}
}
// This indirect exists so we can exclude its stack frame in DEV (and anything below it).
/** @noinline */
function callLazyInitInDEV(lazy: LazyComponent<any, any>): any {
const payload = lazy._payload;
const init = lazy._init;
return init(payload);
}
function renderFunctionComponent<Props>(
request: Request,
task: Task,
key: null | string,
Component: (p: Props, arg: void) => any,
props: Props,
owner: null | ReactComponentInfo,
owner: null | ReactComponentInfo, // DEV-only
stack: null | string, // DEV-only
): ReactJSONValue {
// Reset the task's thenable state before continuing, so that if a later
// component suspends we can reuse the same task object. If the same
@@ -823,8 +971,6 @@ function renderFunctionComponent<Props>(
const prevThenableState = task.thenableState;
task.thenableState = null;
// The secondArg is always undefined in Server Components since refs error early.
const secondArg = undefined;
let result;
let componentDebugInfo: ReactComponentInfo;
@@ -850,6 +996,9 @@ function renderFunctionComponent<Props>(
env: request.environmentName,
owner: owner,
};
if (enableOwnerStacks) {
(componentDebugInfo: any).stack = stack;
}
// 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
// being no references to this as an owner.
@@ -857,24 +1006,11 @@ function renderFunctionComponent<Props>(
emitDebugChunk(request, componentDebugID, componentDebugInfo);
}
prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
setCurrentOwner(componentDebugInfo);
try {
if (supportsComponentStorage) {
// Run the component in an Async Context that tracks the current owner.
result = componentStorage.run(
componentDebugInfo,
Component,
props,
secondArg,
);
} else {
result = Component(props, secondArg);
}
} finally {
setCurrentOwner(null);
}
result = callComponentInDEV(Component, props, componentDebugInfo);
} else {
prepareToUseHooksForComponent(prevThenableState, null);
// The secondArg is always undefined in Server Components since refs error early.
const secondArg = undefined;
result = Component(props, secondArg);
}
if (typeof result === 'object' && result !== null) {
@@ -1093,6 +1229,7 @@ function renderClientElement(
key: null | string,
props: any,
owner: null | ReactComponentInfo, // DEV-only
stack: null | string, // DEV-only
): ReactJSONValue {
// We prepend the terminal client element that actually gets serialized with
// the keys of any Server Components which are not serialized.
@@ -1103,7 +1240,9 @@ function renderClientElement(
key = keyPath + ',' + key;
}
const element = __DEV__
? [REACT_ELEMENT_TYPE, type, key, props, owner]
? enableOwnerStacks
? [REACT_ELEMENT_TYPE, type, key, props, owner, stack]
: [REACT_ELEMENT_TYPE, type, key, props, owner]
: [REACT_ELEMENT_TYPE, type, key, props];
if (task.implicitSlot && key !== null) {
// The root Server Component had no key so it was in an implicit slot.
@@ -1151,6 +1290,7 @@ function renderElement(
ref: mixed,
props: any,
owner: null | ReactComponentInfo, // DEV only
stack: null | string, // DEV only
): ReactJSONValue {
if (ref !== null && ref !== undefined) {
// When the ref moves to the regular props object this will implicitly
@@ -1171,13 +1311,21 @@ function renderElement(
if (typeof type === 'function') {
if (isClientReference(type) || isTemporaryReference(type)) {
// This is a reference to a Client Component.
return renderClientElement(task, type, key, props, owner);
return renderClientElement(task, type, key, props, owner, stack);
}
// This is a Server Component.
return renderFunctionComponent(request, task, key, type, props, owner);
return renderFunctionComponent(
request,
task,
key,
type,
props,
owner,
stack,
);
} else if (typeof type === 'string') {
// This is a host element. E.g. HTML.
return renderClientElement(task, type, key, props, owner);
return renderClientElement(task, type, key, props, owner, stack);
} else if (typeof type === 'symbol') {
if (type === REACT_FRAGMENT_TYPE && key === null) {
// For key-less fragments, we add a small optimization to avoid serializing
@@ -1198,17 +1346,22 @@ function renderElement(
}
// This might be a built-in React component. We'll let the client decide.
// Any built-in works as long as its props are serializable.
return renderClientElement(task, type, key, props, owner);
return renderClientElement(task, type, key, props, owner, stack);
} else if (type != null && typeof type === 'object') {
if (isClientReference(type)) {
// This is a reference to a Client Component.
return renderClientElement(task, type, key, props, owner);
return renderClientElement(task, type, key, props, owner, stack);
}
switch (type.$$typeof) {
case REACT_LAZY_TYPE: {
const payload = type._payload;
const init = type._init;
const wrappedType = init(payload);
let wrappedType;
if (__DEV__) {
wrappedType = callLazyInitInDEV(type);
} else {
const payload = type._payload;
const init = type._init;
wrappedType = init(payload);
}
return renderElement(
request,
task,
@@ -1217,6 +1370,7 @@ function renderElement(
ref,
props,
owner,
stack,
);
}
case REACT_FORWARD_REF_TYPE: {
@@ -1227,10 +1381,20 @@ function renderElement(
type.render,
props,
owner,
stack,
);
}
case REACT_MEMO_TYPE: {
return renderElement(request, task, type.type, key, ref, props, owner);
return renderElement(
request,
task,
type.type,
key,
ref,
props,
owner,
stack,
);
}
}
}
@@ -1822,6 +1986,9 @@ function renderModelDestructive(
ref,
props,
__DEV__ ? element._owner : null,
__DEV__ && enableOwnerStacks
? filterDebugStack(element._debugStack)
: null,
);
}
case REACT_LAZY_TYPE: {
@@ -1830,9 +1997,14 @@ function renderModelDestructive(
task.thenableState = null;
const lazy: LazyComponent<any, any> = (value: any);
const payload = lazy._payload;
const init = lazy._init;
const resolvedModel = init(payload);
let resolvedModel;
if (__DEV__) {
resolvedModel = callLazyInitInDEV(lazy);
} else {
const payload = lazy._payload;
const init = lazy._init;
resolvedModel = init(payload);
}
if (__DEV__) {
const debugInfo: ?ReactDebugInfo = lazy._debugInfo;
if (debugInfo) {
+74 -2
View File
@@ -13,6 +13,7 @@ import {
getIteratorFn,
REACT_ELEMENT_TYPE,
REACT_FRAGMENT_TYPE,
REACT_LAZY_TYPE,
} from 'shared/ReactSymbols';
import {checkKeyStringCoercion} from 'shared/CheckStringCoercion';
import isValidElementType from 'shared/isValidElementType';
@@ -23,6 +24,7 @@ import {
disableStringRefs,
disableDefaultPropsExceptForClasses,
enableFastJSX,
enableOwnerStacks,
} from 'shared/ReactFeatureFlags';
import {checkPropStringCoercion} from 'shared/CheckStringCoercion';
import {ClassComponent} from 'react-reconciler/src/ReactWorkTags';
@@ -30,6 +32,34 @@ import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFrom
const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference');
const createTask =
// eslint-disable-next-line react-internal/no-production-logging
__DEV__ && enableOwnerStacks && console.createTask
? // eslint-disable-next-line react-internal/no-production-logging
console.createTask
: () => null;
function getTaskName(type) {
if (type === REACT_FRAGMENT_TYPE) {
return '<>';
}
if (
typeof type === 'object' &&
type !== null &&
type.$$typeof === REACT_LAZY_TYPE
) {
// We don't want to eagerly initialize the initializer in DEV mode so we can't
// call it to extract the type so we don't know the type of this component.
return '<...>';
}
try {
const name = getComponentNameFromType(type);
return name ? '<' + name + '>' : '<...>';
} catch (x) {
return '<...>';
}
}
function getOwner() {
if (__DEV__ || !disableStringRefs) {
const dispatcher = ReactSharedInternals.A;
@@ -194,7 +224,17 @@ function elementRefGetterWithDeprecationWarning() {
* indicating filename, line number, and/or other information.
* @internal
*/
function ReactElement(type, key, _ref, self, source, owner, props) {
function ReactElement(
type,
key,
_ref,
self,
source,
owner,
props,
debugStack,
debugTask,
) {
let ref;
if (enableRefAsProp) {
// When enableRefAsProp is on, ignore whatever was passed as the ref
@@ -311,6 +351,20 @@ function ReactElement(type, key, _ref, self, source, owner, props) {
writable: true,
value: null,
});
if (enableOwnerStacks) {
Object.defineProperty(element, '_debugStack', {
configurable: false,
enumerable: false,
writable: true,
value: debugStack,
});
Object.defineProperty(element, '_debugTask', {
configurable: false,
enumerable: false,
writable: true,
value: debugTask,
});
}
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
@@ -404,7 +458,17 @@ export function jsxProd(type, config, maybeKey) {
}
}
return ReactElement(type, key, ref, undefined, undefined, getOwner(), props);
return ReactElement(
type,
key,
ref,
undefined,
undefined,
getOwner(),
props,
undefined,
undefined,
);
}
// While `jsxDEV` should never be called when running in production, we do
@@ -652,6 +716,8 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
source,
getOwner(),
props,
__DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined,
__DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined,
);
if (type === REACT_FRAGMENT_TYPE) {
@@ -842,6 +908,8 @@ export function createElement(type, config, children) {
undefined,
getOwner(),
props,
__DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined,
__DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined,
);
if (type === REACT_FRAGMENT_TYPE) {
@@ -862,6 +930,8 @@ export function cloneAndReplaceKey(oldElement, newKey) {
undefined,
!__DEV__ && disableStringRefs ? undefined : oldElement._owner,
oldElement.props,
__DEV__ && enableOwnerStacks ? oldElement._debugStack : undefined,
__DEV__ && enableOwnerStacks ? oldElement._debugTask : undefined,
);
}
@@ -973,6 +1043,8 @@ export function cloneElement(element, config, children) {
undefined,
owner,
props,
__DEV__ && enableOwnerStacks ? element._debugStack : undefined,
__DEV__ && enableOwnerStacks ? element._debugTask : undefined,
);
for (let i = 2; i < arguments.length; i++) {
+9
View File
@@ -7,6 +7,12 @@
* @flow
*/
import type {ReactDebugInfo} from './ReactTypes';
interface ConsoleTask {
run<T>(f: () => T): T;
}
export type ReactElement = {
$$typeof: any,
type: any,
@@ -18,4 +24,7 @@ export type ReactElement = {
// __DEV__
_store: {validated: boolean, ...},
_debugInfo: null | ReactDebugInfo,
_debugStack: Error,
_debugTask: null | ConsoleTask,
};
+2
View File
@@ -125,6 +125,8 @@ export const enableEarlyReturnForPropDiffing = false;
export const enableAddPropertiesFastPath = false;
export const enableOwnerStacks = __EXPERIMENTAL__;
/**
* Enables an expiration time for retry lanes to avoid starvation.
*/
+1
View File
@@ -182,6 +182,7 @@ export type ReactComponentInfo = {
+name?: string,
+env?: string,
+owner?: null | ReactComponentInfo,
+stack?: null | string,
};
export type ReactAsyncInfo = {
@@ -100,5 +100,7 @@ export const enableReactTestRendererWarning = false;
export const disableLegacyMode = false;
export const disableDOMTestUtils = false;
export const enableOwnerStacks = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -107,6 +107,8 @@ export const enableAddPropertiesFastPath = false;
export const renameElementSymbol = true;
export const enableOwnerStacks = __EXPERIMENTAL__;
// Profiling Only
export const enableProfilerTimer = __PROFILE__;
export const enableProfilerCommitHooks = __PROFILE__;
@@ -98,5 +98,7 @@ export const enableRenderableContext = true;
export const enableReactTestRendererWarning = true;
export const disableDefaultPropsExceptForClasses = true;
export const enableOwnerStacks = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -93,5 +93,7 @@ export const enableAddPropertiesFastPath = false;
export const renameElementSymbol = false;
export const enableOwnerStacks = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -93,5 +93,7 @@ export const enableAddPropertiesFastPath = false;
export const renameElementSymbol = false;
export const enableOwnerStacks = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -123,5 +123,7 @@ export const disableLegacyMode = __EXPERIMENTAL__;
export const disableDOMTestUtils = false;
export const enableEarlyReturnForPropDiffing = false;
export const enableOwnerStacks = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -49,6 +49,11 @@ module.exports = function (babel) {
errorMsgExpressions
);
if (errorMsgLiteral === 'react-stack-top-frame') {
// This is a special case for generating stack traces.
return;
}
let prodErrorId = errorMap[errorMsgLiteral];
if (prodErrorId === undefined) {
// There is no error code for this message. Add an inline comment
+4
View File
@@ -50,6 +50,10 @@ module.exports = {
return;
}
const errorMessage = nodeToErrorTemplate(errorMessageNode);
if (errorMessage === 'react-stack-top-frame') {
// This is a special case for generating stack traces.
return;
}
if (errorMessages.has(errorMessage)) {
return;
}
+6 -1
View File
@@ -292,9 +292,14 @@ function lazyRequireFunctionExports(moduleName) {
// If this export is a function, return a wrapper function that lazily
// requires the implementation from the current module cache.
if (typeof originalModule[prop] === 'function') {
return function () {
const wrapper = function () {
return jest.requireActual(moduleName)[prop].apply(this, arguments);
};
// We use this to trick the filtering of Flight to exclude this frame.
Object.defineProperty(wrapper, 'name', {
value: '(<anonymous>)',
});
return wrapper;
} else {
return originalModule[prop];
}