mirror of
https://github.com/zebrajr/react.git
synced 2025-12-07 00:20:28 +01:00
1008 lines
29 KiB
JavaScript
1008 lines
29 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, copyWithSet} from '../utils';
|
|
import {getDisplayName, getInObject} 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 {Owner, InspectedElement} 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 = '' + 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(
|
|
id: number,
|
|
path?: Array<string | number>,
|
|
): InspectedElementPayload {
|
|
if (currentlyInspectedElementID !== id) {
|
|
currentlyInspectedElementID = id;
|
|
currentlyInspectedPaths = {};
|
|
}
|
|
|
|
const inspectedElement = inspectElementRaw(id);
|
|
if (inspectedElement === null) {
|
|
return {
|
|
id,
|
|
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,
|
|
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),
|
|
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;
|
|
}
|
|
|
|
return {
|
|
id,
|
|
|
|
// Hooks did not exist in legacy versions
|
|
canEditHooks: false,
|
|
|
|
// Does the current renderer support editable function props?
|
|
canEditFunctionProps: true,
|
|
|
|
// 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,
|
|
|
|
// 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 setInProps(id: number, path: Array<string | number>, value: any) {
|
|
const internalInstance = idToInternalInstanceMap.get(id);
|
|
if (internalInstance != null) {
|
|
const element = internalInstance._currentElement;
|
|
internalInstance._currentElement = {
|
|
...element,
|
|
props: copyWithSet(element.props, path, value),
|
|
};
|
|
forceUpdate(internalInstance._instance);
|
|
}
|
|
}
|
|
|
|
function setInState(id: number, path: Array<string | number>, value: any) {
|
|
const internalInstance = idToInternalInstanceMap.get(id);
|
|
if (internalInstance != null) {
|
|
const publicInstance = internalInstance._instance;
|
|
if (publicInstance != null) {
|
|
setIn(publicInstance.state, path, value);
|
|
forceUpdate(publicInstance);
|
|
}
|
|
}
|
|
}
|
|
|
|
function setInContext(id: number, path: Array<string | number>, value: any) {
|
|
const internalInstance = idToInternalInstanceMap.get(id);
|
|
if (internalInstance != null) {
|
|
const publicInstance = internalInstance._instance;
|
|
if (publicInstance != null) {
|
|
setIn(publicInstance.context, path, value);
|
|
forceUpdate(publicInstance);
|
|
}
|
|
}
|
|
}
|
|
|
|
function setIn(obj: Object, path: Array<string | number>, value: any) {
|
|
const last = path.pop();
|
|
const parent = path.reduce(
|
|
// $FlowFixMe
|
|
(reduced, attr) => (reduced ? reduced[attr] : null),
|
|
obj,
|
|
);
|
|
if (parent) {
|
|
// $FlowFixMe
|
|
parent[last] = value;
|
|
}
|
|
}
|
|
|
|
// 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 overrideSuspense = () => {
|
|
throw new Error('overrideSuspense not supported by this renderer');
|
|
};
|
|
const setInHook = () => {
|
|
throw new Error('setInHook 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<Owner> | null {
|
|
// Not implemented.
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
cleanup,
|
|
copyElementPath,
|
|
flushInitialOperations,
|
|
getBestMatchForTrackedPath,
|
|
getDisplayNameForFiberID,
|
|
getFiberIDForNative: getInternalIDForNative,
|
|
getInstanceAndStyle,
|
|
findNativeNodesForFiberID: (id: number) => {
|
|
const nativeNode = findNativeNodeForInternalID(id);
|
|
return nativeNode == null ? null : [nativeNode];
|
|
},
|
|
getOwnersList,
|
|
getPathForElement,
|
|
getProfilingData,
|
|
handleCommitFiberRoot,
|
|
handleCommitFiberUnmount,
|
|
inspectElement,
|
|
logElementToConsole,
|
|
overrideSuspense,
|
|
prepareViewAttributeSource,
|
|
prepareViewElementSource,
|
|
renderer,
|
|
setInContext,
|
|
setInHook,
|
|
setInProps,
|
|
setInState,
|
|
setTraceUpdatesEnabled,
|
|
setTrackedPath,
|
|
startProfiling,
|
|
stopProfiling,
|
|
storeAsGlobal,
|
|
updateComponentFilters,
|
|
};
|
|
}
|