react/packages/react-devtools-shared/src/backend/legacy/renderer.js
Justin Grant c88fb49d37
Improve DEV errors if string coercion throws (Temporal.*, Symbol, etc.) (#22064)
* Revise ESLint rules for string coercion

Currently, react uses `'' + value` to coerce mixed values to strings.
This code will throw for Temporal objects or symbols.

To make string-coercion safer and to improve user-facing error messages,
This commit adds a new ESLint rule called `safe-string-coercion`.

This rule has two modes: a production mode and a non-production mode.
* If the `isProductionUserAppCode` option is true, then `'' + value`
  coercions are allowed (because they're faster, although they may
  throw) and `String(value)` coercions are disallowed. Exception:
  when building error messages or running DEV-only code in prod
  files, `String()` should be used because it won't throw.
* If the `isProductionUserAppCode` option is false, then `'' + value`
  coercions are disallowed (because they may throw, and in non-prod
  code it's not worth the risk) and `String(value)` are allowed.

Production mode is used for all files which will be bundled with
developers' userland apps. Non-prod mode is used for all other React
code: tests, DEV blocks, devtools extension, etc.

In production mode, in addiiton to flagging `String(value)` calls,
the rule will also flag `'' + value` or `value + ''` coercions that may
throw. The rule is smart enough to silence itself in the following
"will never throw" cases:
* When the coercion is wrapped in a `typeof` test that restricts to safe
  (non-symbol, non-object) types. Example:
    if (typeof value === 'string' || typeof value === 'number') {
      thisWontReport('' + value);
    }
* When what's being coerced is a unary function result, because unary
   functions never return an object or a symbol.
* When the coerced value is a commonly-used numeric identifier:
  `i`, `idx`, or `lineNumber`.
* When the statement immeidately before the coercion is a DEV-only
  call to a function from shared/CheckStringCoercion.js. This call is a
  no-op in production, but in DEV it will show a console error
  explaining the problem, then will throw right after a long explanatory
  code comment so that debugger users will have an idea what's going on.
  The check function call must be in the following format:
    if (__DEV__) {
      checkXxxxxStringCoercion(value);
    };

Manually disabling the rule is usually not necessary because almost all
prod use of the `'' + value` pattern falls into one of the categories
above. But in the rare cases where the rule isn't smart enough to detect
safe usage (e.g. when a coercion is inside a nested ternary operator),
manually disabling the rule will be needed.

The rule should also be manually disabled in prod error handling code
where `String(value)` should be used for coercions, because it'd be
bad to throw while building an error message or stack trace!

The prod and non-prod modes have differentiated error messages to
explain how to do a proper coercion in that mode.

If a production check call is needed but is missing or incorrect
(e.g. not in a DEV block or not immediately before the coercion), then
a context-sensitive error message will be reported so that developers
can figure out what's wrong and how to fix the problem.

Because string coercions are now handled by the `safe-string-coercion`
rule, the `no-primitive-constructor` rule no longer flags `String()`
usage. It still flags `new String(value)` because that usage is almost
always a bug.

* Add DEV-only string coercion check functions

This commit adds DEV-only functions to check whether coercing
values to strings using the `'' + value` pattern will throw. If it will
throw, these functions will:
1. Display a console error with a friendly error message describing
   the problem and the developer can fix it.
2. Perform the coercion, which will throw. Right before the line where
   the throwing happens, there's a long code comment that will help
   debugger users (or others looking at the exception call stack) figure
   out what happened and how to fix the problem.

One of these check functions should be called before all string coercion
of user-provided values, except when the the coercion is guaranteed not
to throw, e.g.
* if inside a typeof check like `if (typeof value === 'string')`
* if coercing the result of a unary function like `+value` or `value++`
* if coercing a variable named in a whitelist of numeric identifiers:
  `i`, `idx`, or `lineNumber`.

The new `safe-string-coercion` internal ESLint rule enforces that
these check functions are called when they are required.

Only use these check functions in production code that will be bundled
with user apps.  For non-prod code (and for production error-handling
code), use `String(value)` instead which may be a little slower but will
never throw.

* Add failing tests for string coercion

Added failing tests to verify:
* That input, select, and textarea elements with value and defaultValue
  set to Temporal-like objects which will throw when coerced to string
  using the `'' + value` pattern.
* That text elements will throw for Temporal-like objects
* That dangerouslySetInnerHTML will *not* throw for Temporal-like
  objects because this value is not cast to a string before passing to
  the DOM.
* That keys that are Temporal-like objects will throw

All tests above validate the friendly error messages thrown.

* Use `String(value)` for coercion in non-prod files

This commit switches non-production code from `'' + value` (which
throws for Temporal objects and symbols) to instead use `String(value)`
which won't throw for these or other future plus-phobic types.

"Non-produciton code" includes anything not bundled into user apps:
* Tests and test utilities. Note that I didn't change legacy React
  test fixtures because I assumed it was good for those files to
  act just like old React, including coercion behavior.
* Build scripts
* Dev tools package - In addition to switching to `String`, I also
  removed special-case code for coercing symbols which is now
  unnecessary.

* Add DEV-only string coercion checks to prod files

This commit adds DEV-only function calls to to check if string coercion
using `'' + value` will throw, which it will if the value is a Temporal
object or a symbol because those types can't be added with `+`.

If it will throw, then in DEV these checks will show a console error
to help the user undertsand what went wrong and how to fix the
problem. After emitting the console error, the check functions will
retry the coercion which will throw with a call stack that's easy (or
at least easier!) to troubleshoot because the exception happens right
after a long comment explaining the issue. So whether the user is in
a debugger, looking at the browser console, or viewing the in-browser
DEV call stack, it should be easy to understand and fix the problem.

In most cases, the safe-string-coercion ESLint rule is smart enough to
detect when a coercion is safe. But in rare cases (e.g. when a coercion
is inside a ternary) this rule will have to be manually disabled.

This commit also switches error-handling code to use `String(value)`
for coercion, because it's bad to crash when you're trying to build
an error message or a call stack!  Because `String()` is usually
disallowed by the `safe-string-coercion` ESLint rule in production
code, the rule must be disabled when `String()` is used.
2021-09-27 10:05:07 -07:00

1121 lines
32 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 {
ElementTypeClass,
ElementTypeFunction,
ElementTypeRoot,
ElementTypeHostComponent,
ElementTypeOtherOrUnknown,
} from 'react-devtools-shared/src/types';
import {getUID, utfEncodeString, printOperationsArray} from '../../utils';
import {
cleanForBridge,
copyToClipboard,
copyWithDelete,
copyWithRename,
copyWithSet,
} from '../utils';
import {
deletePathInObject,
getDisplayName,
getInObject,
renamePathInObject,
setInObject,
} from 'react-devtools-shared/src/utils';
import {
__DEBUG__,
TREE_OPERATION_ADD,
TREE_OPERATION_REMOVE,
TREE_OPERATION_REORDER_CHILDREN,
} from '../../constants';
import {decorateMany, forceUpdate, restoreMany} from './utils';
import type {
DevToolsHook,
GetFiberIDForNative,
InspectedElementPayload,
InstanceAndStyle,
NativeType,
PathFrame,
PathMatch,
RendererInterface,
} from '../types';
import type {
ComponentFilter,
ElementType,
} from 'react-devtools-shared/src/types';
import type {InspectedElement, SerializedElement} from '../types';
export type InternalInstance = Object;
type LegacyRenderer = Object;
function getData(internalInstance: InternalInstance) {
let displayName = null;
let key = null;
// != used deliberately here to catch undefined and null
if (internalInstance._currentElement != null) {
if (internalInstance._currentElement.key) {
key = String(internalInstance._currentElement.key);
}
const elementType = internalInstance._currentElement.type;
if (typeof elementType === 'string') {
displayName = elementType;
} else if (typeof elementType === 'function') {
displayName = getDisplayName(elementType);
}
}
return {
displayName,
key,
};
}
function getElementType(internalInstance: InternalInstance): ElementType {
// != used deliberately here to catch undefined and null
if (internalInstance._currentElement != null) {
const elementType = internalInstance._currentElement.type;
if (typeof elementType === 'function') {
const publicInstance = internalInstance.getPublicInstance();
if (publicInstance !== null) {
return ElementTypeClass;
} else {
return ElementTypeFunction;
}
} else if (typeof elementType === 'string') {
return ElementTypeHostComponent;
}
}
return ElementTypeOtherOrUnknown;
}
function getChildren(internalInstance: Object): Array<any> {
const children = [];
// If the parent is a native node without rendered children, but with
// multiple string children, then the `element` that gets passed in here is
// a plain value -- a string or number.
if (typeof internalInstance !== 'object') {
// No children
} else if (
internalInstance._currentElement === null ||
internalInstance._currentElement === false
) {
// No children
} else if (internalInstance._renderedComponent) {
const child = internalInstance._renderedComponent;
if (getElementType(child) !== ElementTypeOtherOrUnknown) {
children.push(child);
}
} else if (internalInstance._renderedChildren) {
const renderedChildren = internalInstance._renderedChildren;
for (const name in renderedChildren) {
const child = renderedChildren[name];
if (getElementType(child) !== ElementTypeOtherOrUnknown) {
children.push(child);
}
}
}
// Note: we skip the case where children are just strings or numbers
// because the new DevTools skips over host text nodes anyway.
return children;
}
export function attach(
hook: DevToolsHook,
rendererID: number,
renderer: LegacyRenderer,
global: Object,
): RendererInterface {
const idToInternalInstanceMap: Map<number, InternalInstance> = new Map();
const internalInstanceToIDMap: WeakMap<
InternalInstance,
number,
> = new WeakMap();
const internalInstanceToRootIDMap: WeakMap<
InternalInstance,
number,
> = new WeakMap();
let getInternalIDForNative: GetFiberIDForNative = ((null: any): GetFiberIDForNative);
let findNativeNodeForInternalID: (id: number) => ?NativeType;
if (renderer.ComponentTree) {
getInternalIDForNative = (node, findNearestUnfilteredAncestor) => {
const internalInstance = renderer.ComponentTree.getClosestInstanceFromNode(
node,
);
return internalInstanceToIDMap.get(internalInstance) || null;
};
findNativeNodeForInternalID = (id: number) => {
const internalInstance = idToInternalInstanceMap.get(id);
return renderer.ComponentTree.getNodeFromInstance(internalInstance);
};
} else if (renderer.Mount.getID && renderer.Mount.getNode) {
getInternalIDForNative = (node, findNearestUnfilteredAncestor) => {
// Not implemented.
return null;
};
findNativeNodeForInternalID = (id: number) => {
// Not implemented.
return null;
};
}
function getDisplayNameForFiberID(id: number): string | null {
const internalInstance = idToInternalInstanceMap.get(id);
return internalInstance ? getData(internalInstance).displayName : null;
}
function getID(internalInstance: InternalInstance): number {
if (typeof internalInstance !== 'object' || internalInstance === null) {
throw new Error('Invalid internal instance: ' + internalInstance);
}
if (!internalInstanceToIDMap.has(internalInstance)) {
const id = getUID();
internalInstanceToIDMap.set(internalInstance, id);
idToInternalInstanceMap.set(id, internalInstance);
}
return ((internalInstanceToIDMap.get(internalInstance): any): number);
}
function areEqualArrays(a, b) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
// This is shared mutable state that lets us keep track of where we are.
let parentIDStack = [];
let oldReconcilerMethods = null;
if (renderer.Reconciler) {
// React 15
oldReconcilerMethods = decorateMany(renderer.Reconciler, {
mountComponent(fn, args) {
const internalInstance = args[0];
const hostContainerInfo = args[3];
if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) {
return fn.apply(this, args);
}
if (hostContainerInfo._topLevelWrapper === undefined) {
// SSR
return fn.apply(this, args);
}
const id = getID(internalInstance);
// Push the operation.
const parentID =
parentIDStack.length > 0
? parentIDStack[parentIDStack.length - 1]
: 0;
recordMount(internalInstance, id, parentID);
parentIDStack.push(id);
// Remember the root.
internalInstanceToRootIDMap.set(
internalInstance,
getID(hostContainerInfo._topLevelWrapper),
);
try {
const result = fn.apply(this, args);
parentIDStack.pop();
return result;
} catch (err) {
parentIDStack = [];
throw err;
} finally {
if (parentIDStack.length === 0) {
const rootID = internalInstanceToRootIDMap.get(internalInstance);
if (rootID === undefined) {
throw new Error('Expected to find root ID.');
}
flushPendingEvents(rootID);
}
}
},
performUpdateIfNecessary(fn, args) {
const internalInstance = args[0];
if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) {
return fn.apply(this, args);
}
const id = getID(internalInstance);
parentIDStack.push(id);
const prevChildren = getChildren(internalInstance);
try {
const result = fn.apply(this, args);
const nextChildren = getChildren(internalInstance);
if (!areEqualArrays(prevChildren, nextChildren)) {
// Push the operation
recordReorder(internalInstance, id, nextChildren);
}
parentIDStack.pop();
return result;
} catch (err) {
parentIDStack = [];
throw err;
} finally {
if (parentIDStack.length === 0) {
const rootID = internalInstanceToRootIDMap.get(internalInstance);
if (rootID === undefined) {
throw new Error('Expected to find root ID.');
}
flushPendingEvents(rootID);
}
}
},
receiveComponent(fn, args) {
const internalInstance = args[0];
if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) {
return fn.apply(this, args);
}
const id = getID(internalInstance);
parentIDStack.push(id);
const prevChildren = getChildren(internalInstance);
try {
const result = fn.apply(this, args);
const nextChildren = getChildren(internalInstance);
if (!areEqualArrays(prevChildren, nextChildren)) {
// Push the operation
recordReorder(internalInstance, id, nextChildren);
}
parentIDStack.pop();
return result;
} catch (err) {
parentIDStack = [];
throw err;
} finally {
if (parentIDStack.length === 0) {
const rootID = internalInstanceToRootIDMap.get(internalInstance);
if (rootID === undefined) {
throw new Error('Expected to find root ID.');
}
flushPendingEvents(rootID);
}
}
},
unmountComponent(fn, args) {
const internalInstance = args[0];
if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) {
return fn.apply(this, args);
}
const id = getID(internalInstance);
parentIDStack.push(id);
try {
const result = fn.apply(this, args);
parentIDStack.pop();
// Push the operation.
recordUnmount(internalInstance, id);
return result;
} catch (err) {
parentIDStack = [];
throw err;
} finally {
if (parentIDStack.length === 0) {
const rootID = internalInstanceToRootIDMap.get(internalInstance);
if (rootID === undefined) {
throw new Error('Expected to find root ID.');
}
flushPendingEvents(rootID);
}
}
},
});
}
function cleanup() {
if (oldReconcilerMethods !== null) {
if (renderer.Component) {
restoreMany(renderer.Component.Mixin, oldReconcilerMethods);
} else {
restoreMany(renderer.Reconciler, oldReconcilerMethods);
}
}
oldReconcilerMethods = null;
}
function recordMount(
internalInstance: InternalInstance,
id: number,
parentID: number,
) {
const isRoot = parentID === 0;
if (__DEBUG__) {
console.log(
'%crecordMount()',
'color: green; font-weight: bold;',
id,
getData(internalInstance).displayName,
);
}
if (isRoot) {
// TODO Is this right? For all versions?
const hasOwnerMetadata =
internalInstance._currentElement != null &&
internalInstance._currentElement._owner != null;
pushOperation(TREE_OPERATION_ADD);
pushOperation(id);
pushOperation(ElementTypeRoot);
pushOperation(0); // isProfilingSupported?
pushOperation(hasOwnerMetadata ? 1 : 0);
} else {
const type = getElementType(internalInstance);
const {displayName, key} = getData(internalInstance);
const ownerID =
internalInstance._currentElement != null &&
internalInstance._currentElement._owner != null
? getID(internalInstance._currentElement._owner)
: 0;
const displayNameStringID = getStringID(displayName);
const keyStringID = getStringID(key);
pushOperation(TREE_OPERATION_ADD);
pushOperation(id);
pushOperation(type);
pushOperation(parentID);
pushOperation(ownerID);
pushOperation(displayNameStringID);
pushOperation(keyStringID);
}
}
function recordReorder(
internalInstance: InternalInstance,
id: number,
nextChildren: Array<InternalInstance>,
) {
pushOperation(TREE_OPERATION_REORDER_CHILDREN);
pushOperation(id);
const nextChildIDs = nextChildren.map(getID);
pushOperation(nextChildIDs.length);
for (let i = 0; i < nextChildIDs.length; i++) {
pushOperation(nextChildIDs[i]);
}
}
function recordUnmount(internalInstance: InternalInstance, id: number) {
pendingUnmountedIDs.push(id);
idToInternalInstanceMap.delete(id);
}
function crawlAndRecordInitialMounts(
id: number,
parentID: number,
rootID: number,
) {
if (__DEBUG__) {
console.group('crawlAndRecordInitialMounts() id:', id);
}
const internalInstance = idToInternalInstanceMap.get(id);
if (internalInstance != null) {
internalInstanceToRootIDMap.set(internalInstance, rootID);
recordMount(internalInstance, id, parentID);
getChildren(internalInstance).forEach(child =>
crawlAndRecordInitialMounts(getID(child), id, rootID),
);
}
if (__DEBUG__) {
console.groupEnd();
}
}
function flushInitialOperations() {
// Crawl roots though and register any nodes that mounted before we were injected.
const roots =
renderer.Mount._instancesByReactRootID ||
renderer.Mount._instancesByContainerID;
for (const key in roots) {
const internalInstance = roots[key];
const id = getID(internalInstance);
crawlAndRecordInitialMounts(id, 0, id);
flushPendingEvents(id);
}
}
const pendingOperations: Array<number> = [];
const pendingStringTable: Map<string, number> = new Map();
let pendingUnmountedIDs: Array<number> = [];
let pendingStringTableLength: number = 0;
let pendingUnmountedRootID: number | null = null;
function flushPendingEvents(rootID: number) {
if (
pendingOperations.length === 0 &&
pendingUnmountedIDs.length === 0 &&
pendingUnmountedRootID === null
) {
return;
}
const numUnmountIDs =
pendingUnmountedIDs.length + (pendingUnmountedRootID === null ? 0 : 1);
const operations = new Array(
// Identify which renderer this update is coming from.
2 + // [rendererID, rootFiberID]
// How big is the string table?
1 + // [stringTableLength]
// Then goes the actual string table.
pendingStringTableLength +
// All unmounts are batched in a single message.
// [TREE_OPERATION_REMOVE, removedIDLength, ...ids]
(numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +
// Mount operations
pendingOperations.length,
);
// Identify which renderer this update is coming from.
// This enables roots to be mapped to renderers,
// Which in turn enables fiber properations, states, and hooks to be inspected.
let i = 0;
operations[i++] = rendererID;
operations[i++] = rootID;
// Now fill in the string table.
// [stringTableLength, str1Length, ...str1, str2Length, ...str2, ...]
operations[i++] = pendingStringTableLength;
pendingStringTable.forEach((value, key) => {
operations[i++] = key.length;
const encodedKey = utfEncodeString(key);
for (let j = 0; j < encodedKey.length; j++) {
operations[i + j] = encodedKey[j];
}
i += key.length;
});
if (numUnmountIDs > 0) {
// All unmounts except roots are batched in a single message.
operations[i++] = TREE_OPERATION_REMOVE;
// The first number is how many unmounted IDs we're gonna send.
operations[i++] = numUnmountIDs;
// Fill in the unmounts
for (let j = 0; j < pendingUnmountedIDs.length; j++) {
operations[i++] = pendingUnmountedIDs[j];
}
// The root ID should always be unmounted last.
if (pendingUnmountedRootID !== null) {
operations[i] = pendingUnmountedRootID;
i++;
}
}
// Fill in the rest of the operations.
for (let j = 0; j < pendingOperations.length; j++) {
operations[i + j] = pendingOperations[j];
}
i += pendingOperations.length;
if (__DEBUG__) {
printOperationsArray(operations);
}
// If we've already connected to the frontend, just pass the operations through.
hook.emit('operations', operations);
pendingOperations.length = 0;
pendingUnmountedIDs = [];
pendingUnmountedRootID = null;
pendingStringTable.clear();
pendingStringTableLength = 0;
}
function pushOperation(op: number): void {
if (__DEV__) {
if (!Number.isInteger(op)) {
console.error(
'pushOperation() was called but the value is not an integer.',
op,
);
}
}
pendingOperations.push(op);
}
function getStringID(str: string | null): number {
if (str === null) {
return 0;
}
const existingID = pendingStringTable.get(str);
if (existingID !== undefined) {
return existingID;
}
const stringID = pendingStringTable.size + 1;
pendingStringTable.set(str, stringID);
// The string table total length needs to account
// both for the string length, and for the array item
// that contains the length itself. Hence + 1.
pendingStringTableLength += str.length + 1;
return stringID;
}
let currentlyInspectedElementID: number | null = null;
let currentlyInspectedPaths: Object = {};
// Track the intersection of currently inspected paths,
// so that we can send their data along if the element is re-rendered.
function mergeInspectedPaths(path: Array<string | number>) {
let current = currentlyInspectedPaths;
path.forEach(key => {
if (!current[key]) {
current[key] = {};
}
current = current[key];
});
}
function createIsPathAllowed(key: string) {
// This function helps prevent previously-inspected paths from being dehydrated in updates.
// This is important to avoid a bad user experience where expanded toggles collapse on update.
return function isPathAllowed(path: Array<string | number>): boolean {
let current = currentlyInspectedPaths[key];
if (!current) {
return false;
}
for (let i = 0; i < path.length; i++) {
current = current[path[i]];
if (!current) {
return false;
}
}
return true;
};
}
// Fast path props lookup for React Native style editor.
function getInstanceAndStyle(id: number): InstanceAndStyle {
let instance = null;
let style = null;
const internalInstance = idToInternalInstanceMap.get(id);
if (internalInstance != null) {
instance = internalInstance._instance || null;
const element = internalInstance._currentElement;
if (element != null && element.props != null) {
style = element.props.style || null;
}
}
return {
instance,
style,
};
}
function updateSelectedElement(id: number): void {
const internalInstance = idToInternalInstanceMap.get(id);
if (internalInstance == null) {
console.warn(`Could not find instance with id "${id}"`);
return;
}
switch (getElementType(internalInstance)) {
case ElementTypeClass:
global.$r = internalInstance._instance;
break;
case ElementTypeFunction:
const element = internalInstance._currentElement;
if (element == null) {
console.warn(`Could not find element with id "${id}"`);
return;
}
global.$r = {
props: element.props,
type: element.type,
};
break;
default:
global.$r = null;
break;
}
}
function storeAsGlobal(
id: number,
path: Array<string | number>,
count: number,
): void {
const inspectedElement = inspectElementRaw(id);
if (inspectedElement !== null) {
const value = getInObject(inspectedElement, path);
const key = `$reactTemp${count}`;
window[key] = value;
console.log(key);
console.log(value);
}
}
function copyElementPath(id: number, path: Array<string | number>): void {
const inspectedElement = inspectElementRaw(id);
if (inspectedElement !== null) {
copyToClipboard(getInObject(inspectedElement, path));
}
}
function inspectElement(
requestID: number,
id: number,
path: Array<string | number> | null,
): InspectedElementPayload {
if (currentlyInspectedElementID !== id) {
currentlyInspectedElementID = id;
currentlyInspectedPaths = {};
}
const inspectedElement = inspectElementRaw(id);
if (inspectedElement === null) {
return {
id,
responseID: requestID,
type: 'not-found',
};
}
if (path !== null) {
mergeInspectedPaths(path);
}
// Any time an inspected element has an update,
// we should update the selected $r value as wel.
// Do this before dehyration (cleanForBridge).
updateSelectedElement(id);
inspectedElement.context = cleanForBridge(
inspectedElement.context,
createIsPathAllowed('context'),
);
inspectedElement.props = cleanForBridge(
inspectedElement.props,
createIsPathAllowed('props'),
);
inspectedElement.state = cleanForBridge(
inspectedElement.state,
createIsPathAllowed('state'),
);
return {
id,
responseID: requestID,
type: 'full-data',
value: inspectedElement,
};
}
function inspectElementRaw(id: number): InspectedElement | null {
const internalInstance = idToInternalInstanceMap.get(id);
if (internalInstance == null) {
return null;
}
const {displayName, key} = getData(internalInstance);
const type = getElementType(internalInstance);
let context = null;
let owners = null;
let props = null;
let state = null;
let source = null;
const element = internalInstance._currentElement;
if (element !== null) {
props = element.props;
source = element._source != null ? element._source : null;
let owner = element._owner;
if (owner) {
owners = [];
while (owner != null) {
owners.push({
displayName: getData(owner).displayName || 'Unknown',
id: getID(owner),
key: element.key,
type: getElementType(owner),
});
if (owner._currentElement) {
owner = owner._currentElement._owner;
}
}
}
}
const publicInstance = internalInstance._instance;
if (publicInstance != null) {
context = publicInstance.context || null;
state = publicInstance.state || null;
}
// Not implemented
const errors = [];
const warnings = [];
return {
id,
// Does the current renderer support editable hooks and function props?
canEditHooks: false,
canEditFunctionProps: false,
// Does the current renderer support advanced editing interface?
canEditHooksAndDeletePaths: false,
canEditHooksAndRenamePaths: false,
canEditFunctionPropsDeletePaths: false,
canEditFunctionPropsRenamePaths: false,
// Toggle error boundary did not exist in legacy versions
canToggleError: false,
isErrored: false,
targetErrorBoundaryID: null,
// Suspense did not exist in legacy versions
canToggleSuspense: false,
// Can view component source location.
canViewSource: type === ElementTypeClass || type === ElementTypeFunction,
// Only legacy context exists in legacy versions.
hasLegacyContext: true,
displayName: displayName,
type: type,
key: key != null ? key : null,
// Inspectable properties.
context,
hooks: null,
props,
state,
errors,
warnings,
// List of owners
owners,
// Location of component in source code.
source,
rootType: null,
rendererPackageName: null,
rendererVersion: null,
};
}
function logElementToConsole(id: number): void {
const result = inspectElementRaw(id);
if (result === null) {
console.warn(`Could not find element with id "${id}"`);
return;
}
const supportsGroup = typeof console.groupCollapsed === 'function';
if (supportsGroup) {
console.groupCollapsed(
`[Click to expand] %c<${result.displayName || 'Component'} />`,
// --dom-tag-name-color is the CSS variable Chrome styles HTML elements with in the console.
'color: var(--dom-tag-name-color); font-weight: normal;',
);
}
if (result.props !== null) {
console.log('Props:', result.props);
}
if (result.state !== null) {
console.log('State:', result.state);
}
if (result.context !== null) {
console.log('Context:', result.context);
}
const nativeNode = findNativeNodeForInternalID(id);
if (nativeNode !== null) {
console.log('Node:', nativeNode);
}
if (window.chrome || /firefox/i.test(navigator.userAgent)) {
console.log(
'Right-click any value to save it as a global variable for further inspection.',
);
}
if (supportsGroup) {
console.groupEnd();
}
}
function prepareViewAttributeSource(
id: number,
path: Array<string | number>,
): void {
const inspectedElement = inspectElementRaw(id);
if (inspectedElement !== null) {
window.$attribute = getInObject(inspectedElement, path);
}
}
function prepareViewElementSource(id: number): void {
const internalInstance = idToInternalInstanceMap.get(id);
if (internalInstance == null) {
console.warn(`Could not find instance with id "${id}"`);
return;
}
const element = internalInstance._currentElement;
if (element == null) {
console.warn(`Could not find element with id "${id}"`);
return;
}
global.$type = element.type;
}
function deletePath(
type: 'context' | 'hooks' | 'props' | 'state',
id: number,
hookID: ?number,
path: Array<string | number>,
): void {
const internalInstance = idToInternalInstanceMap.get(id);
if (internalInstance != null) {
const publicInstance = internalInstance._instance;
if (publicInstance != null) {
switch (type) {
case 'context':
deletePathInObject(publicInstance.context, path);
forceUpdate(publicInstance);
break;
case 'hooks':
throw new Error('Hooks not supported by this renderer');
case 'props':
const element = internalInstance._currentElement;
internalInstance._currentElement = {
...element,
props: copyWithDelete(element.props, path),
};
forceUpdate(publicInstance);
break;
case 'state':
deletePathInObject(publicInstance.state, path);
forceUpdate(publicInstance);
break;
}
}
}
}
function renamePath(
type: 'context' | 'hooks' | 'props' | 'state',
id: number,
hookID: ?number,
oldPath: Array<string | number>,
newPath: Array<string | number>,
): void {
const internalInstance = idToInternalInstanceMap.get(id);
if (internalInstance != null) {
const publicInstance = internalInstance._instance;
if (publicInstance != null) {
switch (type) {
case 'context':
renamePathInObject(publicInstance.context, oldPath, newPath);
forceUpdate(publicInstance);
break;
case 'hooks':
throw new Error('Hooks not supported by this renderer');
case 'props':
const element = internalInstance._currentElement;
internalInstance._currentElement = {
...element,
props: copyWithRename(element.props, oldPath, newPath),
};
forceUpdate(publicInstance);
break;
case 'state':
renamePathInObject(publicInstance.state, oldPath, newPath);
forceUpdate(publicInstance);
break;
}
}
}
}
function overrideValueAtPath(
type: 'context' | 'hooks' | 'props' | 'state',
id: number,
hookID: ?number,
path: Array<string | number>,
value: any,
): void {
const internalInstance = idToInternalInstanceMap.get(id);
if (internalInstance != null) {
const publicInstance = internalInstance._instance;
if (publicInstance != null) {
switch (type) {
case 'context':
setInObject(publicInstance.context, path, value);
forceUpdate(publicInstance);
break;
case 'hooks':
throw new Error('Hooks not supported by this renderer');
case 'props':
const element = internalInstance._currentElement;
internalInstance._currentElement = {
...element,
props: copyWithSet(element.props, path, value),
};
forceUpdate(publicInstance);
break;
case 'state':
setInObject(publicInstance.state, path, value);
forceUpdate(publicInstance);
break;
}
}
}
}
// v16+ only features
const getProfilingData = () => {
throw new Error('getProfilingData not supported by this renderer');
};
const handleCommitFiberRoot = () => {
throw new Error('handleCommitFiberRoot not supported by this renderer');
};
const handleCommitFiberUnmount = () => {
throw new Error('handleCommitFiberUnmount not supported by this renderer');
};
const handlePostCommitFiberRoot = () => {
throw new Error('handlePostCommitFiberRoot not supported by this renderer');
};
const overrideError = () => {
throw new Error('overrideError not supported by this renderer');
};
const overrideSuspense = () => {
throw new Error('overrideSuspense not supported by this renderer');
};
const startProfiling = () => {
// Do not throw, since this would break a multi-root scenario where v15 and v16 were both present.
};
const stopProfiling = () => {
// Do not throw, since this would break a multi-root scenario where v15 and v16 were both present.
};
function getBestMatchForTrackedPath(): PathMatch | null {
// Not implemented.
return null;
}
function getPathForElement(id: number): Array<PathFrame> | null {
// Not implemented.
return null;
}
function updateComponentFilters(componentFilters: Array<ComponentFilter>) {
// Not implemented.
}
function setTraceUpdatesEnabled(enabled: boolean) {
// Not implemented.
}
function setTrackedPath(path: Array<PathFrame> | null) {
// Not implemented.
}
function getOwnersList(id: number): Array<SerializedElement> | null {
// Not implemented.
return null;
}
function clearErrorsAndWarnings() {
// Not implemented
}
function clearErrorsForFiberID(id: number) {
// Not implemented
}
function clearWarningsForFiberID(id: number) {
// Not implemented
}
function patchConsoleForStrictMode() {}
function unpatchConsoleForStrictMode() {}
return {
clearErrorsAndWarnings,
clearErrorsForFiberID,
clearWarningsForFiberID,
cleanup,
copyElementPath,
deletePath,
flushInitialOperations,
getBestMatchForTrackedPath,
getDisplayNameForFiberID,
getFiberIDForNative: getInternalIDForNative,
getInstanceAndStyle,
findNativeNodesForFiberID: (id: number) => {
const nativeNode = findNativeNodeForInternalID(id);
return nativeNode == null ? null : [nativeNode];
},
getOwnersList,
getPathForElement,
getProfilingData,
handleCommitFiberRoot,
handleCommitFiberUnmount,
handlePostCommitFiberRoot,
inspectElement,
logElementToConsole,
overrideError,
overrideSuspense,
overrideValueAtPath,
renamePath,
patchConsoleForStrictMode,
prepareViewAttributeSource,
prepareViewElementSource,
renderer,
setTraceUpdatesEnabled,
setTrackedPath,
startProfiling,
stopProfiling,
storeAsGlobal,
unpatchConsoleForStrictMode,
updateComponentFilters,
};
}