react-native-components-inspector
Version:
react native components inspector can used to click the component and open the code in the editor
444 lines (435 loc) • 15.7 kB
JavaScript
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
const react_native = __toESM(require("react-native"));
const react = __toESM(require("react"));
const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
//#region src/utils/stringifyPropsForDisplay.ts
function stringifyPropsForDisplay(value, options) {
const maxDepth = options?.maxDepth ?? 2;
const maxKeys = options?.maxKeys ?? 20;
const maxString = options?.maxString ?? 2e3;
const seen = new WeakSet();
function truncateString(input) {
if (input.length <= maxString) return input;
return input.slice(0, maxString) + "…";
}
function format(val, depth) {
if (val === null) return "null";
const type = typeof val;
if (type === "string") return JSON.stringify(truncateString(val));
if (type === "number" || type === "boolean") return String(val);
if (type === "bigint") return String(val) + "n";
if (type === "undefined") return "undefined";
if (type === "function") return `[Function ${val.name || "anonymous"}]`;
if (type === "symbol") return String(val);
if (depth > maxDepth) return "…";
if (Array.isArray(val)) {
const items = [];
const len = Math.min(val.length, maxKeys);
for (let i = 0; i < len; i++) items.push(format(val[i], depth + 1));
if (val.length > len) items.push("…");
return `[${items.join(", ")}]`;
}
if (type === "object") {
if (seen.has(val)) return "[Circular]";
seen.add(val);
const keys = Object.keys(val);
const shown = keys.slice(0, maxKeys);
const parts = [];
for (const key of shown) try {
parts.push(`${key}: ${format(val[key], depth + 1)}`);
} catch {
parts.push(`${key}: [Unable to read]`);
}
if (keys.length > shown.length) parts.push("…");
return `{ ${parts.join(", ")} }`;
}
try {
return JSON.stringify(val);
} catch {
return String(val);
}
}
try {
return format(value, 0);
} catch (e) {
return "[Unserializable props]";
}
}
//#endregion
//#region src/utils/constants.ts
const BLOCKED_COMPONENTS = [
"createanimatedcomponent",
"moetext",
"text",
"view",
"scrollview",
"flatlist",
"sectionlist",
"virtualizedlist",
"image",
"touchableopacity",
"touchablehighlight",
"pressable",
"button"
];
//#endregion
//#region src/devServer.ts
function getDevServerBaseURL() {
const scriptURL = react_native.NativeModules?.SourceCode?.scriptURL;
if (!scriptURL) return null;
try {
const url = new URL(scriptURL);
return `${url.protocol}//${url.hostname}${url.port ? `:${url.port}` : ""}`;
} catch {
return null;
}
}
//#endregion
//#region src/components/useInspector.ts
function useInspector(params) {
const { wrapperRef, isInspectorEnabled, blockComponents } = params;
const [highlightInfo, setHighlightInfo] = (0, react.useState)(null);
const [popoverInfo, setPopoverInfo] = (0, react.useState)(null);
const baseURL = getDevServerBaseURL();
const isBlockedComponent = (componentName) => {
return blockComponents.some((blockedComponent) => componentName.toLowerCase().includes(blockedComponent));
};
const openInEditor = (file) => {
if (!baseURL || !file) return;
fetch(`${baseURL}/__open-in-editor?file=${encodeURIComponent(file)}`).catch(() => {});
};
const findComponentAtPoint = (0, react.useCallback)((pageXInput, pageYInput) => {
if (!wrapperRef.current) return;
wrapperRef.current.measure((fx, fy, width, height, pageX, pageY) => {
if (!__DEV__) return;
const localX = pageXInput - pageX;
const localY = pageYInput - pageY;
const onInspectorData = (data) => {
try {
const frame = data?.frame;
const source = getSourceFromNodeClosestFiber(data);
const style = data?.props?.style;
const ancestorsSources = [];
try {
let node = data?.closestInstance?.return;
while (node && ancestorsSources.length < 5) {
const aSource = getSourceFromNodeFiber(node);
const displayName = node?.elementType?.displayName || node?.type?.displayName || node?.elementType?.name || node?.type?.name || (typeof node?.elementType === "string" ? node.elementType : void 0);
ancestorsSources.push({
name: displayName,
source: aSource
});
node = node.return;
}
} catch {}
if (frame && typeof frame.left === "number" && typeof frame.top === "number") {
setHighlightInfo({
x: Math.max(0, frame.left - pageX),
y: Math.max(0, frame.top - pageY),
width: Math.max(0, frame.width ?? 0),
height: Math.max(0, frame.height ?? 0)
});
const componentKey = source.file || "";
const canOpenInEditor = source && !isBlockedComponent(componentKey);
setPopoverInfo({
frame,
props: data?.props,
hierarchy: data?.hierarchy,
selectedIndex: data?.selectedIndex,
source,
style,
ancestorsSources: !canOpenInEditor ? ancestorsSources : void 0
});
if (canOpenInEditor) {
const fileForOpen = String(source.file || "");
openInEditor(`${fileForOpen}:${source.line}:${source.column}`);
} else {
const filteredAncestorsSources = [...new Set(ancestorsSources.map((item) => item.source?.file ? `${item.source.file}:${item.source.line}:${item.source.column}` : "").filter((item) => !!item && !item.includes("node_modules")))];
if (filteredAncestorsSources.length) openInEditor(filteredAncestorsSources[0]);
else {
console.log("\x1B[31m[Inspector] __inspectorSource 非期望跳转组件:\x1B[0m", source.file);
console.log("可能是以下组件之一:\n", filteredAncestorsSources.join("\n"));
}
}
}
} catch (e) {
console.warn("[Inspector] log error:", e);
}
return false;
};
try {
const getInspectorDataForViewAtPoint = require("react-native/Libraries/Inspector/getInspectorDataForViewAtPoint");
if (getInspectorDataForViewAtPoint) {
getInspectorDataForViewAtPoint(wrapperRef.current, localX, localY, onInspectorData);
return;
}
} catch {}
try {
const RNPI = require("react-native/Libraries/ReactNative/ReactNativePrivateInterface");
const FabricUIManager = RNPI?.FabricUIManager;
const UIManager = RNPI?.UIManager;
const rootTag = (0, react_native.findNodeHandle)(wrapperRef.current);
if (rootTag == null) {
console.warn("[Inspector] rootTag not found");
return;
}
if (FabricUIManager?.getInspectorDataForViewAtPoint) FabricUIManager.getInspectorDataForViewAtPoint(rootTag, localX, localY, onInspectorData);
else if (UIManager?.getInspectorDataForViewAtPoint) UIManager.getInspectorDataForViewAtPoint(rootTag, localX, localY, onInspectorData);
else console.warn("[Inspector] getInspectorDataForViewAtPoint is not available. RN version/arch may differ.");
} catch (error) {
console.warn("[Inspector] fallback private call failed:", error);
}
});
}, [wrapperRef]);
const panResponder = (0, react.useMemo)(() => react_native.PanResponder.create({
onStartShouldSetPanResponder: () => isInspectorEnabled,
onMoveShouldSetPanResponder: () => false,
onStartShouldSetPanResponderCapture: () => isInspectorEnabled,
onMoveShouldSetPanResponderCapture: () => false,
onPanResponderTerminationRequest: () => false,
onShouldBlockNativeResponder: () => true,
onPanResponderGrant: (event) => {
if (!isInspectorEnabled) return;
const { pageX, pageY } = event.nativeEvent;
findComponentAtPoint(pageX, pageY);
}
}), [isInspectorEnabled, findComponentAtPoint]);
const reset = (0, react.useCallback)(() => {
setHighlightInfo(null);
setPopoverInfo(null);
}, []);
return {
panHandlers: panResponder.panHandlers,
highlightInfo,
popoverInfo,
reset
};
}
const getSourceFromNodeClosestFiber = (node) => {
return node?.closestInstance?.memoizedProps?.__inspectorSource || node?.closestInstance?.props?.__inspectorSource || node?.closestInstance?.pendingProps?.__inspectorSource;
};
const getSourceFromNodeFiber = (node) => {
return node?.memoizedProps?.__inspectorSource || node?.props?.__inspectorSource || node?.pendingProps?.__inspectorSource;
};
//#endregion
//#region src/components/InspectorHighlight.tsx
const InspectorHighlight = ({ x, y, width, height }) => {
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
style: [styles$2.highlight, {
left: x,
top: y,
width,
height
}],
pointerEvents: "none"
});
};
const styles$2 = react_native.StyleSheet.create({ highlight: {
position: "absolute",
borderWidth: 2,
borderColor: "#ff0000",
backgroundColor: "rgba(255, 0, 0, 0.2)",
zIndex: 9999
} });
//#endregion
//#region src/components/InspectorPopover.tsx
const formatSourceLoc = (src) => {
if (!src?.file) return "N/A";
const line = src.line != null ? `:${src.line}` : "";
const col = src.column != null ? `:${src.column}` : "";
return `${src.file}${line}${col}`;
};
const InspectorPopover = ({ x, y, title, sizeText, propsText, source, styleText, ancestorsSources }) => {
const showAncestors = Array.isArray(ancestorsSources) && ancestorsSources.length > 0;
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
style: [styles$1.popover, {
left: x,
top: y
}],
pointerEvents: "none",
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
style: styles$1.popoverTitle,
children: title
}), showAncestors ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
style: styles$1.section,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
style: styles$1.sectionTitle,
children: "Ancestor Sources (up to 5)"
}), ancestorsSources.map((item, idx) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
style: styles$1.popoverText,
numberOfLines: 2,
children: `${idx + 1}. ${item.name || "Anonymous"} -> ${formatSourceLoc(item.source)}`
}, idx))]
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
style: styles$1.section,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
style: styles$1.sectionTitle,
children: "Source"
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
style: styles$1.popoverText,
numberOfLines: 2,
children: formatSourceLoc(source)
})]
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
style: styles$1.section,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
style: styles$1.sectionTitle,
children: "Size"
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
style: styles$1.popoverText,
children: sizeText
})]
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
style: styles$1.section,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
style: styles$1.sectionTitle,
children: "Style"
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
numberOfLines: 3,
style: styles$1.popoverCode,
children: styleText || "N/A"
})]
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
style: styles$1.section,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
style: styles$1.sectionTitle,
children: "Props"
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
numberOfLines: 3,
style: styles$1.popoverCode,
children: propsText || "N/A"
})]
})
] })]
});
};
const styles$1 = react_native.StyleSheet.create({
popover: {
position: "absolute",
backgroundColor: "#fff",
borderRadius: 6,
padding: 8,
maxWidth: 300,
borderColor: "rgba(0,0,0,0.12)",
borderWidth: react_native.StyleSheet.hairlineWidth,
shadowColor: "#000",
shadowOpacity: .15,
shadowRadius: 8,
shadowOffset: {
width: 0,
height: 4
},
zIndex: 1e4
},
popoverTitle: {
fontWeight: "600",
marginBottom: 4
},
popoverText: { color: "#333" },
section: { marginTop: 8 },
sectionTitle: {
fontWeight: "600",
marginBottom: 4
},
popoverCode: { color: "#666" },
openBtn: {
marginTop: 8,
alignSelf: "flex-start",
paddingVertical: 6,
paddingHorizontal: 10,
backgroundColor: "#1677ff",
borderRadius: 4
},
openBtnText: {
color: "#fff",
fontWeight: "600"
}
});
//#endregion
//#region src/components/InspectorWrapper.tsx
const InspectorWrapper = (0, react.memo)((props) => {
const { children, blockComponents = BLOCKED_COMPONENTS } = props;
const wrapperRef = (0, react.useRef)(null);
const [isInspectorEnabled, setIsInspectorEnabled] = (0, react.useState)(false);
const { panHandlers, highlightInfo, popoverInfo, reset } = useInspector({
wrapperRef,
isInspectorEnabled,
blockComponents
});
(0, react.useEffect)(() => {
if (__DEV__) react_native.DevSettings.addMenuItem("Toggle Inspector", () => {
setIsInspectorEnabled((prev) => !prev);
reset();
});
}, [reset]);
if (!__DEV__) return children;
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
style: styles.container,
ref: wrapperRef,
...isInspectorEnabled ? panHandlers : {},
children: [
children,
isInspectorEnabled && highlightInfo && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InspectorHighlight, {
x: highlightInfo.x,
y: highlightInfo.y,
width: highlightInfo.width,
height: highlightInfo.height
}),
isInspectorEnabled && highlightInfo && popoverInfo?.frame && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InspectorPopover, {
x: highlightInfo.x,
y: highlightInfo.y + highlightInfo.height + 6,
title: popoverInfo.hierarchy?.[popoverInfo.selectedIndex ?? 0]?.name || "Component",
sizeText: `${Math.round(popoverInfo.frame.width)}×${Math.round(popoverInfo.frame.height)}`,
propsText: popoverInfo.props ? stringifyPropsForDisplay(popoverInfo.props, {
maxDepth: 2,
maxKeys: 20,
maxString: 300
}) : void 0,
source: popoverInfo.source,
styleText: popoverInfo.style ? stringifyPropsForDisplay(popoverInfo.style, {
maxDepth: 2,
maxKeys: 20,
maxString: 300
}) : void 0,
ancestorsSources: popoverInfo.ancestorsSources
})
]
});
});
const styles = react_native.StyleSheet.create({
container: { flex: 1 },
touchableContainer: { flex: 1 }
});
//#endregion
exports.BLOCKED_COMPONENTS = BLOCKED_COMPONENTS;
exports.InspectorHighlight = InspectorHighlight;
exports.InspectorPopover = InspectorPopover;
exports.InspectorWrapper = InspectorWrapper;
exports.getDevServerBaseURL = getDevServerBaseURL;
exports.stringifyPropsForDisplay = stringifyPropsForDisplay;
exports.useInspector = useInspector;