react/packages/shared/ReactSerializationErrors.js

291 lines
8.5 KiB
JavaScript

/**
* 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
*/
import {
REACT_ELEMENT_TYPE,
REACT_FORWARD_REF_TYPE,
REACT_LAZY_TYPE,
REACT_MEMO_TYPE,
REACT_SUSPENSE_TYPE,
REACT_SUSPENSE_LIST_TYPE,
} from 'shared/ReactSymbols';
import type {LazyComponent} from 'react/src/ReactLazy';
import isArray from 'shared/isArray';
// Used for DEV messages to keep track of which parent rendered some props,
// in case they error.
export const jsxPropsParents: WeakMap<any, any> = new WeakMap();
export const jsxChildrenParents: WeakMap<any, any> = new WeakMap();
function isObjectPrototype(object: any): boolean {
if (!object) {
return false;
}
const ObjectPrototype = Object.prototype;
if (object === ObjectPrototype) {
return true;
}
// It might be an object from a different Realm which is
// still just a plain simple object.
if (Object.getPrototypeOf(object)) {
return false;
}
const names = Object.getOwnPropertyNames(object);
for (let i = 0; i < names.length; i++) {
if (!(names[i] in ObjectPrototype)) {
return false;
}
}
return true;
}
export function isSimpleObject(object: any): boolean {
if (!isObjectPrototype(Object.getPrototypeOf(object))) {
return false;
}
const names = Object.getOwnPropertyNames(object);
for (let i = 0; i < names.length; i++) {
const descriptor = Object.getOwnPropertyDescriptor(object, names[i]);
if (!descriptor) {
return false;
}
if (!descriptor.enumerable) {
if (
(names[i] === 'key' || names[i] === 'ref') &&
typeof descriptor.get === 'function'
) {
// React adds key and ref getters to props objects to issue warnings.
// Those getters will not be transferred to the client, but that's ok,
// so we'll special case them.
continue;
}
return false;
}
}
return true;
}
export function objectName(object: mixed): string {
// $FlowFixMe[method-unbinding]
const name = Object.prototype.toString.call(object);
return name.replace(/^\[object (.*)\]$/, function (m, p0) {
return p0;
});
}
function describeKeyForErrorMessage(key: string): string {
const encodedKey = JSON.stringify(key);
return '"' + key + '"' === encodedKey ? key : encodedKey;
}
export function describeValueForErrorMessage(value: mixed): string {
switch (typeof value) {
case 'string': {
return JSON.stringify(
value.length <= 10 ? value : value.slice(0, 10) + '...',
);
}
case 'object': {
if (isArray(value)) {
return '[...]';
}
const name = objectName(value);
if (name === 'Object') {
return '{...}';
}
return name;
}
case 'function':
return 'function';
default:
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value);
}
}
function describeElementType(type: any): string {
if (typeof type === 'string') {
return type;
}
switch (type) {
case REACT_SUSPENSE_TYPE:
return 'Suspense';
case REACT_SUSPENSE_LIST_TYPE:
return 'SuspenseList';
}
if (typeof type === 'object') {
switch (type.$$typeof) {
case REACT_FORWARD_REF_TYPE:
return describeElementType(type.render);
case REACT_MEMO_TYPE:
return describeElementType(type.type);
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 describeElementType(init(payload));
} catch (x) {}
}
}
}
return '';
}
export function describeObjectForErrorMessage(
objectOrArray: {+[key: string | number]: mixed, ...} | $ReadOnlyArray<mixed>,
expandedName?: string,
): string {
const objKind = objectName(objectOrArray);
if (objKind !== 'Object' && objKind !== 'Array') {
return objKind;
}
let str = '';
let start = -1;
let length = 0;
if (isArray(objectOrArray)) {
if (__DEV__ && jsxChildrenParents.has(objectOrArray)) {
// Print JSX Children
const type = jsxChildrenParents.get(objectOrArray);
str = '<' + describeElementType(type) + '>';
const array: $ReadOnlyArray<mixed> = objectOrArray;
for (let i = 0; i < array.length; i++) {
const value = array[i];
let substr;
if (typeof value === 'string') {
substr = value;
} else if (typeof value === 'object' && value !== null) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
substr = '{' + describeObjectForErrorMessage(value) + '}';
} else {
substr = '{' + describeValueForErrorMessage(value) + '}';
}
if ('' + i === expandedName) {
start = str.length;
length = substr.length;
str += substr;
} else if (substr.length < 15 && str.length + substr.length < 40) {
str += substr;
} else {
str += '{...}';
}
}
str += '</' + describeElementType(type) + '>';
} else {
// Print Array
str = '[';
const array: $ReadOnlyArray<mixed> = objectOrArray;
for (let i = 0; i < array.length; i++) {
if (i > 0) {
str += ', ';
}
const value = array[i];
let substr;
if (typeof value === 'object' && value !== null) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
substr = describeObjectForErrorMessage(value);
} else {
substr = describeValueForErrorMessage(value);
}
if ('' + i === expandedName) {
start = str.length;
length = substr.length;
str += substr;
} else if (substr.length < 10 && str.length + substr.length < 40) {
str += substr;
} else {
str += '...';
}
}
str += ']';
}
} else {
if (objectOrArray.$$typeof === REACT_ELEMENT_TYPE) {
str = '<' + describeElementType(objectOrArray.type) + '/>';
} else if (__DEV__ && jsxPropsParents.has(objectOrArray)) {
// Print JSX
const type = jsxPropsParents.get(objectOrArray);
str = '<' + (describeElementType(type) || '...');
const object: {+[key: string | number]: mixed, ...} = objectOrArray;
const names = Object.keys(object);
for (let i = 0; i < names.length; i++) {
str += ' ';
const name = names[i];
str += describeKeyForErrorMessage(name) + '=';
const value = object[name];
let substr;
if (
name === expandedName &&
typeof value === 'object' &&
value !== null
) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
substr = describeObjectForErrorMessage(value);
} else {
substr = describeValueForErrorMessage(value);
}
if (typeof value !== 'string') {
substr = '{' + substr + '}';
}
if (name === expandedName) {
start = str.length;
length = substr.length;
str += substr;
} else if (substr.length < 10 && str.length + substr.length < 40) {
str += substr;
} else {
str += '...';
}
}
str += '>';
} else {
// Print Object
str = '{';
const object: {+[key: string | number]: mixed, ...} = objectOrArray;
const names = Object.keys(object);
for (let i = 0; i < names.length; i++) {
if (i > 0) {
str += ', ';
}
const name = names[i];
str += describeKeyForErrorMessage(name) + ': ';
const value = object[name];
let substr;
if (typeof value === 'object' && value !== null) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
substr = describeObjectForErrorMessage(value);
} else {
substr = describeValueForErrorMessage(value);
}
if (name === expandedName) {
start = str.length;
length = substr.length;
str += substr;
} else if (substr.length < 10 && str.length + substr.length < 40) {
str += substr;
} else {
str += '...';
}
}
str += '}';
}
}
if (expandedName === undefined) {
return str;
}
if (start > -1 && length > 0) {
const highlight = ' '.repeat(start) + '^'.repeat(length);
return '\n ' + str + '\n ' + highlight;
}
return '\n ' + str;
}