| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | * |
| 7 | * @flow |
| 8 | */ |
| 9 | |
| 10 | import { |
| 11 | REACT_ELEMENT_TYPE, |
| 12 | REACT_FORWARD_REF_TYPE, |
| 13 | REACT_LAZY_TYPE, |
| 14 | REACT_MEMO_TYPE, |
| 15 | REACT_SUSPENSE_TYPE, |
| 16 | REACT_SUSPENSE_LIST_TYPE, |
| 17 | REACT_VIEW_TRANSITION_TYPE, |
| 18 | } from 'shared/ReactSymbols'; |
| 19 | |
| 20 | import type {LazyComponent} from 'react/src/ReactLazy'; |
| 21 | |
| 22 | import isArray from 'shared/isArray'; |
| 23 | import getPrototypeOf from 'shared/getPrototypeOf'; |
| 24 | |
| 25 | import {enableViewTransition} from 'shared/ReactFeatureFlags'; |
| 26 | |
| 27 | // Used for DEV messages to keep track of which parent rendered some props, |
| 28 | // in case they error. |
| 29 | export const jsxPropsParents: WeakMap<any, any> = new WeakMap(); |
| 30 | export const jsxChildrenParents: WeakMap<any, any> = new WeakMap(); |
| 31 | |
| 32 | function isObjectPrototype(object: any): boolean { |
| 33 | if (!object) { |
| 34 | return false; |
| 35 | } |
| 36 | const ObjectPrototype = Object.prototype; |
| 37 | if (object === ObjectPrototype) { |
| 38 | return true; |
| 39 | } |
| 40 | // It might be an object from a different Realm which is |
| 41 | // still just a plain simple object. |
| 42 | if (getPrototypeOf(object)) { |
| 43 | return false; |
| 44 | } |
| 45 | const names = Object.getOwnPropertyNames(object); |
| 46 | for (let i = 0; i < names.length; i++) { |
| 47 | if (!(names[i] in ObjectPrototype)) { |
| 48 | return false; |
| 49 | } |
| 50 | } |
| 51 | return true; |
| 52 | } |
| 53 | |
| 54 | export function isGetter(object: any, name: string): boolean { |
| 55 | const ObjectPrototype = Object.prototype; |
| 56 | if (object === ObjectPrototype || object === null) { |
| 57 | return false; |
| 58 | } |
| 59 | const descriptor = Object.getOwnPropertyDescriptor(object, name); |
| 60 | if (descriptor === undefined) { |
| 61 | return isGetter(getPrototypeOf(object), name); |
| 62 | } |
| 63 | return typeof descriptor.get === 'function'; |
| 64 | } |
| 65 | |
| 66 | export function isSimpleObject(object: any): boolean { |
| 67 | if (!isObjectPrototype(getPrototypeOf(object))) { |
| 68 | return false; |
| 69 | } |
| 70 | const names = Object.getOwnPropertyNames(object); |
| 71 | for (let i = 0; i < names.length; i++) { |
| 72 | const descriptor = Object.getOwnPropertyDescriptor(object, names[i]); |
| 73 | if (!descriptor) { |
| 74 | return false; |
| 75 | } |
| 76 | if (!descriptor.enumerable) { |
| 77 | if ( |
| 78 | (names[i] === 'key' || names[i] === 'ref') && |
| 79 | typeof descriptor.get === 'function' |
| 80 | ) { |
| 81 | // React adds key and ref getters to props objects to issue warnings. |
| 82 | // Those getters will not be transferred to the client, but that's ok, |
| 83 | // so we'll special case them. |
| 84 | continue; |
| 85 | } |
| 86 | return false; |
| 87 | } |
| 88 | } |
| 89 | return true; |
| 90 | } |
| 91 | |
| 92 | export function objectName(object: mixed): string { |
| 93 | // $FlowFixMe[method-unbinding] |
| 94 | const name = Object.prototype.toString.call(object); |
| 95 | // Extract 'Object' from '[object Object]': |
| 96 | return name.slice(8, name.length - 1); |
| 97 | } |
| 98 | |
| 99 | function describeKeyForErrorMessage(key: string): string { |
| 100 | const encodedKey = JSON.stringify(key); |
| 101 | return '"' + key + '"' === encodedKey ? key : encodedKey; |
| 102 | } |
| 103 | |
| 104 | export function describeValueForErrorMessage(value: mixed): string { |
| 105 | switch (typeof value) { |
| 106 | case 'string': { |
| 107 | return JSON.stringify( |
| 108 | value.length <= 10 ? value : value.slice(0, 10) + '...', |
| 109 | ); |
| 110 | } |
| 111 | case 'object': { |
| 112 | if (isArray(value)) { |
| 113 | return '[...]'; |
| 114 | } |
| 115 | if (value !== null && value.$$typeof === CLIENT_REFERENCE_TAG) { |
| 116 | return describeClientReference(value); |
| 117 | } |
| 118 | const name = objectName(value); |
| 119 | if (name === 'Object') { |
| 120 | return '{...}'; |
| 121 | } |
| 122 | return name; |
| 123 | } |
| 124 | case 'function': { |
| 125 | if ((value as any).$$typeof === CLIENT_REFERENCE_TAG) { |
| 126 | return describeClientReference(value); |
| 127 | } |
| 128 | const name = (value as any).displayName || value.name; |
| 129 | return name ? 'function ' + name : 'function'; |
| 130 | } |
| 131 | default: |
| 132 | // eslint-disable-next-line react-internal/safe-string-coercion |
| 133 | return String(value); |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | function describeElementType(type: any): string { |
| 138 | if (typeof type === 'string') { |
| 139 | return type; |
| 140 | } |
| 141 | switch (type) { |
| 142 | case REACT_SUSPENSE_TYPE: |
| 143 | return 'Suspense'; |
| 144 | case REACT_SUSPENSE_LIST_TYPE: |
| 145 | return 'SuspenseList'; |
| 146 | case REACT_VIEW_TRANSITION_TYPE: |
| 147 | if (enableViewTransition) { |
| 148 | return 'ViewTransition'; |
| 149 | } |
| 150 | } |
| 151 | if (typeof type === 'object') { |
| 152 | switch (type.$$typeof) { |
| 153 | case REACT_FORWARD_REF_TYPE: |
| 154 | return describeElementType(type.render); |
| 155 | case REACT_MEMO_TYPE: |
| 156 | return describeElementType(type.type); |
| 157 | case REACT_LAZY_TYPE: { |
| 158 | const lazyComponent: LazyComponent<any, any> = type as any; |
| 159 | const payload = lazyComponent._payload; |
| 160 | const init = lazyComponent._init; |
| 161 | try { |
| 162 | // Lazy may contain any component type so we recursively resolve it. |
| 163 | return describeElementType(init(payload)); |
| 164 | } catch (x) {} |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | return ''; |
| 169 | } |
| 170 | |
| 171 | const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference'); |
| 172 | |
| 173 | function describeClientReference(ref: any) { |
| 174 | return 'client'; |
| 175 | } |
| 176 | |
| 177 | export function describeObjectForErrorMessage( |
| 178 | objectOrArray: {+[key: string | number]: mixed, ...} | $ReadOnlyArray<mixed>, |
| 179 | expandedName?: string, |
| 180 | ): string { |
| 181 | const objKind = objectName(objectOrArray); |
| 182 | if (objKind !== 'Object' && objKind !== 'Array') { |
| 183 | return objKind; |
| 184 | } |
| 185 | let str = ''; |
| 186 | let start = -1; |
| 187 | let length = 0; |
| 188 | if (isArray(objectOrArray)) { |
| 189 | if (__DEV__ && jsxChildrenParents.has(objectOrArray)) { |
| 190 | // Print JSX Children |
| 191 | const type = jsxChildrenParents.get(objectOrArray); |
| 192 | str = '<' + describeElementType(type) + '>'; |
| 193 | const array: $ReadOnlyArray<mixed> = objectOrArray; |
| 194 | for (let i = 0; i < array.length; i++) { |
| 195 | const value = array[i]; |
| 196 | let substr; |
| 197 | if (typeof value === 'string') { |
| 198 | substr = value; |
| 199 | } else if (typeof value === 'object' && value !== null) { |
| 200 | substr = '{' + describeObjectForErrorMessage(value) + '}'; |
| 201 | } else { |
| 202 | substr = '{' + describeValueForErrorMessage(value) + '}'; |
| 203 | } |
| 204 | if ('' + i === expandedName) { |
| 205 | start = str.length; |
| 206 | length = substr.length; |
| 207 | str += substr; |
| 208 | } else if (substr.length < 15 && str.length + substr.length < 40) { |
| 209 | str += substr; |
| 210 | } else { |
| 211 | str += '{...}'; |
| 212 | } |
| 213 | } |
| 214 | str += '</' + describeElementType(type) + '>'; |
| 215 | } else { |
| 216 | // Print Array |
| 217 | str = '['; |
| 218 | const array: $ReadOnlyArray<mixed> = objectOrArray; |
| 219 | for (let i = 0; i < array.length; i++) { |
| 220 | if (i > 0) { |
| 221 | str += ', '; |
| 222 | } |
| 223 | const value = array[i]; |
| 224 | let substr; |
| 225 | if (typeof value === 'object' && value !== null) { |
| 226 | substr = describeObjectForErrorMessage(value); |
| 227 | } else { |
| 228 | substr = describeValueForErrorMessage(value); |
| 229 | } |
| 230 | if ('' + i === expandedName) { |
| 231 | start = str.length; |
| 232 | length = substr.length; |
| 233 | str += substr; |
| 234 | } else if (substr.length < 10 && str.length + substr.length < 40) { |
| 235 | str += substr; |
| 236 | } else { |
| 237 | str += '...'; |
| 238 | } |
| 239 | } |
| 240 | str += ']'; |
| 241 | } |
| 242 | } else { |
| 243 | if (objectOrArray.$$typeof === REACT_ELEMENT_TYPE) { |
| 244 | str = '<' + describeElementType(objectOrArray.type) + '/>'; |
| 245 | } else if (objectOrArray.$$typeof === CLIENT_REFERENCE_TAG) { |
| 246 | return describeClientReference(objectOrArray); |
| 247 | } else if (__DEV__ && jsxPropsParents.has(objectOrArray)) { |
| 248 | // Print JSX |
| 249 | const type = jsxPropsParents.get(objectOrArray); |
| 250 | str = '<' + (describeElementType(type) || '...'); |
| 251 | const object: {+[key: string | number]: mixed, ...} = objectOrArray; |
| 252 | const names = Object.keys(object); |
| 253 | for (let i = 0; i < names.length; i++) { |
| 254 | str += ' '; |
| 255 | const name = names[i]; |
| 256 | str += describeKeyForErrorMessage(name) + '='; |
| 257 | const value = object[name]; |
| 258 | let substr; |
| 259 | if ( |
| 260 | name === expandedName && |
| 261 | typeof value === 'object' && |
| 262 | value !== null |
| 263 | ) { |
| 264 | substr = describeObjectForErrorMessage(value); |
| 265 | } else { |
| 266 | substr = describeValueForErrorMessage(value); |
| 267 | } |
| 268 | if (typeof value !== 'string') { |
| 269 | substr = '{' + substr + '}'; |
| 270 | } |
| 271 | if (name === expandedName) { |
| 272 | start = str.length; |
| 273 | length = substr.length; |
| 274 | str += substr; |
| 275 | } else if (substr.length < 10 && str.length + substr.length < 40) { |
| 276 | str += substr; |
| 277 | } else { |
| 278 | str += '...'; |
| 279 | } |
| 280 | } |
| 281 | str += '>'; |
| 282 | } else { |
| 283 | // Print Object |
| 284 | str = '{'; |
| 285 | const object: {+[key: string | number]: mixed, ...} = objectOrArray; |
| 286 | const names = Object.keys(object); |
| 287 | for (let i = 0; i < names.length; i++) { |
| 288 | if (i > 0) { |
| 289 | str += ', '; |
| 290 | } |
| 291 | const name = names[i]; |
| 292 | str += describeKeyForErrorMessage(name) + ': '; |
| 293 | const value = object[name]; |
| 294 | let substr; |
| 295 | if (typeof value === 'object' && value !== null) { |
| 296 | substr = describeObjectForErrorMessage(value); |
| 297 | } else { |
| 298 | substr = describeValueForErrorMessage(value); |
| 299 | } |
| 300 | if (name === expandedName) { |
| 301 | start = str.length; |
| 302 | length = substr.length; |
| 303 | str += substr; |
| 304 | } else if (substr.length < 10 && str.length + substr.length < 40) { |
| 305 | str += substr; |
| 306 | } else { |
| 307 | str += '...'; |
| 308 | } |
| 309 | } |
| 310 | str += '}'; |
| 311 | } |
| 312 | } |
| 313 | if (expandedName === undefined) { |
| 314 | return str; |
| 315 | } |
| 316 | if (start > -1 && length > 0) { |
| 317 | const highlight = ' '.repeat(start) + '^'.repeat(length); |
| 318 | return '\n ' + str + '\n ' + highlight; |
| 319 | } |
| 320 | return '\n ' + str; |
| 321 | } |