335 lines
11 KiB
JavaScript
335 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {Source} from 'shared/ReactElementType';
|
|
import type {LazyComponent} from 'react/src/ReactLazy';
|
|
|
|
import {
|
|
enableComponentStackLocations,
|
|
disableNativeComponentFrames,
|
|
} from 'shared/ReactFeatureFlags';
|
|
|
|
import {
|
|
REACT_SUSPENSE_TYPE,
|
|
REACT_SUSPENSE_LIST_TYPE,
|
|
REACT_FORWARD_REF_TYPE,
|
|
REACT_MEMO_TYPE,
|
|
REACT_LAZY_TYPE,
|
|
} from 'shared/ReactSymbols';
|
|
|
|
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
|
|
|
|
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
|
|
|
const {ReactCurrentDispatcher} = ReactSharedInternals;
|
|
|
|
let prefix;
|
|
export function describeBuiltInComponentFrame(
|
|
name: string,
|
|
source: void | null | Source,
|
|
ownerFn: void | null | Function,
|
|
): string {
|
|
if (enableComponentStackLocations) {
|
|
if (prefix === undefined) {
|
|
// Extract the VM specific prefix used by each line.
|
|
try {
|
|
throw Error();
|
|
} catch (x) {
|
|
const match = x.stack.trim().match(/\n( *(at )?)/);
|
|
prefix = (match && match[1]) || '';
|
|
}
|
|
}
|
|
// We use the prefix to ensure our stacks line up with native stack frames.
|
|
return '\n' + prefix + name;
|
|
} else {
|
|
let ownerName = null;
|
|
if (__DEV__ && ownerFn) {
|
|
ownerName = ownerFn.displayName || ownerFn.name || null;
|
|
}
|
|
return describeComponentFrame(name, source, ownerName);
|
|
}
|
|
}
|
|
|
|
let reentry = false;
|
|
let componentFrameCache;
|
|
if (__DEV__) {
|
|
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
|
|
componentFrameCache = new PossiblyWeakMap();
|
|
}
|
|
|
|
export function describeNativeComponentFrame(
|
|
fn: Function,
|
|
construct: boolean,
|
|
): string {
|
|
// If something asked for a stack inside a fake render, it should get ignored.
|
|
if (disableNativeComponentFrames || !fn || reentry) {
|
|
return '';
|
|
}
|
|
|
|
if (__DEV__) {
|
|
const frame = componentFrameCache.get(fn);
|
|
if (frame !== undefined) {
|
|
return frame;
|
|
}
|
|
}
|
|
|
|
let control;
|
|
|
|
reentry = true;
|
|
const previousPrepareStackTrace = Error.prepareStackTrace;
|
|
// $FlowFixMe It does accept undefined.
|
|
Error.prepareStackTrace = undefined;
|
|
let previousDispatcher;
|
|
if (__DEV__) {
|
|
previousDispatcher = ReactCurrentDispatcher.current;
|
|
// Set the dispatcher in DEV because this might be call in the render function
|
|
// for warnings.
|
|
ReactCurrentDispatcher.current = null;
|
|
disableLogs();
|
|
}
|
|
try {
|
|
// This should throw.
|
|
if (construct) {
|
|
// Something should be setting the props in the constructor.
|
|
const Fake = function() {
|
|
throw Error();
|
|
};
|
|
// $FlowFixMe
|
|
Object.defineProperty(Fake.prototype, 'props', {
|
|
set: function() {
|
|
// We use a throwing setter instead of frozen or non-writable props
|
|
// because that won't throw in a non-strict mode function.
|
|
throw Error();
|
|
},
|
|
});
|
|
if (typeof Reflect === 'object' && Reflect.construct) {
|
|
// We construct a different control for this case to include any extra
|
|
// frames added by the construct call.
|
|
try {
|
|
Reflect.construct(Fake, []);
|
|
} catch (x) {
|
|
control = x;
|
|
}
|
|
Reflect.construct(fn, [], Fake);
|
|
} else {
|
|
try {
|
|
Fake.call();
|
|
} catch (x) {
|
|
control = x;
|
|
}
|
|
// $FlowFixMe[prop-missing] found when upgrading Flow
|
|
fn.call(Fake.prototype);
|
|
}
|
|
} else {
|
|
try {
|
|
throw Error();
|
|
} catch (x) {
|
|
control = x;
|
|
}
|
|
// TODO(luna): This will currently only throw if the function component
|
|
// tries to access React/ReactDOM/props. We should probably make this throw
|
|
// in simple components too
|
|
fn();
|
|
}
|
|
} catch (sample) {
|
|
// This is inlined manually because closure doesn't do it for us.
|
|
if (sample && control && typeof sample.stack === 'string') {
|
|
// This extracts the first frame from the sample that isn't also in the control.
|
|
// Skipping one frame that we assume is the frame that calls the two.
|
|
const sampleLines = sample.stack.split('\n');
|
|
const controlLines = control.stack.split('\n');
|
|
let s = sampleLines.length - 1;
|
|
let c = controlLines.length - 1;
|
|
while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {
|
|
// We expect at least one stack frame to be shared.
|
|
// Typically this will be the root most one. However, stack frames may be
|
|
// cut off due to maximum stack limits. In this case, one maybe cut off
|
|
// earlier than the other. We assume that the sample is longer or the same
|
|
// and there for cut off earlier. So we should find the root most frame in
|
|
// the sample somewhere in the control.
|
|
c--;
|
|
}
|
|
for (; s >= 1 && c >= 0; s--, c--) {
|
|
// Next we find the first one that isn't the same which should be the
|
|
// frame that called our sample function and the control.
|
|
if (sampleLines[s] !== controlLines[c]) {
|
|
// In V8, the first line is describing the message but other VMs don't.
|
|
// If we're about to return the first line, and the control is also on the same
|
|
// line, that's a pretty good indicator that our sample threw at same line as
|
|
// the control. I.e. before we entered the sample frame. So we ignore this result.
|
|
// This can happen if you passed a class to function component, or non-function.
|
|
if (s !== 1 || c !== 1) {
|
|
do {
|
|
s--;
|
|
c--;
|
|
// We may still have similar intermediate frames from the construct call.
|
|
// The next one that isn't the same should be our match though.
|
|
if (c < 0 || sampleLines[s] !== controlLines[c]) {
|
|
// V8 adds a "new" prefix for native classes. Let's remove it to make it prettier.
|
|
let frame = '\n' + sampleLines[s].replace(' at new ', ' at ');
|
|
|
|
// If our component frame is labeled "<anonymous>"
|
|
// but we have a user-provided "displayName"
|
|
// splice it in to make the stack more readable.
|
|
if (fn.displayName && frame.includes('<anonymous>')) {
|
|
frame = frame.replace('<anonymous>', fn.displayName);
|
|
}
|
|
|
|
if (__DEV__) {
|
|
if (typeof fn === 'function') {
|
|
componentFrameCache.set(fn, frame);
|
|
}
|
|
}
|
|
// Return the line we found.
|
|
return frame;
|
|
}
|
|
} while (s >= 1 && c >= 0);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
reentry = false;
|
|
if (__DEV__) {
|
|
ReactCurrentDispatcher.current = previousDispatcher;
|
|
reenableLogs();
|
|
}
|
|
Error.prepareStackTrace = previousPrepareStackTrace;
|
|
}
|
|
// Fallback to just using the name if we couldn't make it throw.
|
|
const name = fn ? fn.displayName || fn.name : '';
|
|
const syntheticFrame = name ? describeBuiltInComponentFrame(name) : '';
|
|
if (__DEV__) {
|
|
if (typeof fn === 'function') {
|
|
componentFrameCache.set(fn, syntheticFrame);
|
|
}
|
|
}
|
|
return syntheticFrame;
|
|
}
|
|
|
|
const BEFORE_SLASH_RE = /^(.*)[\\\/]/;
|
|
|
|
function describeComponentFrame(
|
|
name: null | string,
|
|
source: void | null | Source,
|
|
ownerName: null | string,
|
|
) {
|
|
let sourceInfo = '';
|
|
if (__DEV__ && source) {
|
|
const path = source.fileName;
|
|
let fileName = path.replace(BEFORE_SLASH_RE, '');
|
|
// In DEV, include code for a common special case:
|
|
// prefer "folder/index.js" instead of just "index.js".
|
|
if (/^index\./.test(fileName)) {
|
|
const match = path.match(BEFORE_SLASH_RE);
|
|
if (match) {
|
|
const pathBeforeSlash = match[1];
|
|
if (pathBeforeSlash) {
|
|
const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');
|
|
fileName = folderName + '/' + fileName;
|
|
}
|
|
}
|
|
}
|
|
sourceInfo = ' (at ' + fileName + ':' + source.lineNumber + ')';
|
|
} else if (ownerName) {
|
|
sourceInfo = ' (created by ' + ownerName + ')';
|
|
}
|
|
return '\n in ' + (name || 'Unknown') + sourceInfo;
|
|
}
|
|
|
|
export function describeClassComponentFrame(
|
|
ctor: Function,
|
|
source: void | null | Source,
|
|
ownerFn: void | null | Function,
|
|
): string {
|
|
if (enableComponentStackLocations) {
|
|
return describeNativeComponentFrame(ctor, true);
|
|
} else {
|
|
return describeFunctionComponentFrame(ctor, source, ownerFn);
|
|
}
|
|
}
|
|
|
|
export function describeFunctionComponentFrame(
|
|
fn: Function,
|
|
source: void | null | Source,
|
|
ownerFn: void | null | Function,
|
|
): string {
|
|
if (enableComponentStackLocations) {
|
|
return describeNativeComponentFrame(fn, false);
|
|
} else {
|
|
if (!fn) {
|
|
return '';
|
|
}
|
|
const name = fn.displayName || fn.name || null;
|
|
let ownerName = null;
|
|
if (__DEV__ && ownerFn) {
|
|
ownerName = ownerFn.displayName || ownerFn.name || null;
|
|
}
|
|
return describeComponentFrame(name, source, ownerName);
|
|
}
|
|
}
|
|
|
|
function shouldConstruct(Component: Function) {
|
|
const prototype = Component.prototype;
|
|
return !!(prototype && prototype.isReactComponent);
|
|
}
|
|
|
|
export function describeUnknownElementTypeFrameInDEV(
|
|
type: any,
|
|
source: void | null | Source,
|
|
ownerFn: void | null | Function,
|
|
): string {
|
|
if (!__DEV__) {
|
|
return '';
|
|
}
|
|
if (type == null) {
|
|
return '';
|
|
}
|
|
if (typeof type === 'function') {
|
|
if (enableComponentStackLocations) {
|
|
return describeNativeComponentFrame(type, shouldConstruct(type));
|
|
} else {
|
|
return describeFunctionComponentFrame(type, source, ownerFn);
|
|
}
|
|
}
|
|
if (typeof type === 'string') {
|
|
return describeBuiltInComponentFrame(type, source, ownerFn);
|
|
}
|
|
switch (type) {
|
|
case REACT_SUSPENSE_TYPE:
|
|
return describeBuiltInComponentFrame('Suspense', source, ownerFn);
|
|
case REACT_SUSPENSE_LIST_TYPE:
|
|
return describeBuiltInComponentFrame('SuspenseList', source, ownerFn);
|
|
}
|
|
if (typeof type === 'object') {
|
|
switch (type.$$typeof) {
|
|
case REACT_FORWARD_REF_TYPE:
|
|
return describeFunctionComponentFrame(type.render, source, ownerFn);
|
|
case REACT_MEMO_TYPE:
|
|
// Memo may contain any component type so we recursively resolve it.
|
|
return describeUnknownElementTypeFrameInDEV(type.type, source, ownerFn);
|
|
case REACT_LAZY_TYPE: {
|
|
const lazyComponent: LazyComponent<any, any> = (type: any);
|
|
const payload = lazyComponent._payload;
|
|
const init = lazyComponent._init;
|
|
try {
|
|
// Lazy may contain any component type so we recursively resolve it.
|
|
return describeUnknownElementTypeFrameInDEV(
|
|
init(payload),
|
|
source,
|
|
ownerFn,
|
|
);
|
|
} catch (x) {}
|
|
}
|
|
}
|
|
}
|
|
return '';
|
|
}
|