UNPKG

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
//#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;