diff --git a/packages/vscode-extension/lib/wrapper.js b/packages/vscode-extension/lib/wrapper.js index 75a83710d..007bf4cf4 100644 --- a/packages/vscode-extension/lib/wrapper.js +++ b/packages/vscode-extension/lib/wrapper.js @@ -4,11 +4,11 @@ const { useContext, useState, useEffect, useRef, useCallback } = require("react" const { LogBox, AppRegistry, + Dimensions, RootTagContext, View, - Dimensions, Linking, - findNodeHandle, + findNodeHandle } = require("react-native"); const { storybookPreview } = require("./storybook_helper"); @@ -22,7 +22,7 @@ let navigationHistory = new Map(); const InternalImports = { get PREVIEW_APP_KEY() { return require("./preview").PREVIEW_APP_KEY; - }, + } }; const RNInternals = { @@ -69,6 +69,130 @@ function useAgentListener(agent, eventName, listener, deps = []) { }, [agent, ...deps]); } +function obtainGetInspectorDataForInstance() { + const renderers = Array.from(window.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers?.values()); + if (!renderers) { + return undefined; + } + for (const renderer of renderers) { + if (renderer.rendererConfig?.getInspectorDataForInstance) { + return renderer.rendererConfig.getInspectorDataForInstance; + } + } + return undefined; +}; + +function createStackElement( + frame, name, source +) { + return { + componentName: name, + source: { + fileName: source.fileName, + line0Based: source.lineNumber - 1, + column0Based: source.columnNumber - 1, + }, + frame, + }; +}; + +// Returns an array of promises which resolve to elements of the components hierarchy stack. +function traverseComponentsTreeUp(startNode) { + const getInspectorDataForInstance = obtainGetInspectorDataForInstance(); + + if (!getInspectorDataForInstance) { + console.warn("Failed to obtain getInspectorDataForInstance renderer method."); + return []; + } + + const stackPromises = []; + let node = startNode; + + // Optimization: we break after reaching fiber node corresponding to OffscreenComponent (with tag 22). + // https://github.com/facebook/react/blob/c3570b158d087eb4e3ee5748c4bd9360045c8a26/packages/react-reconciler/src/ReactWorkTags.js#L62 + while (node && node.tag !== 22) { + const data = getInspectorDataForInstance(node); + + if (data.hierarchy && data.hierarchy.length > 0) { + stackPromises.push(new Promise((resolve, reject) => { + const item = data.hierarchy[data.hierarchy.length - 1]; + const inspectorData = item.getInspectorData(findNodeHandle); + + if (!inspectorData.source) { + resolve(undefined); + return; + } + + try { + inspectorData.measure((_x, _y, viewWidth, viewHeight, pageX, pageY) => { + const stackElementFrame = { + x: pageX, + y: pageY, + width: viewWidth, + height: viewHeight + }; + + const stackElement = createStackElement(stackElementFrame, item.name, inspectorData.source); + resolve(stackElement); + }); + } catch (e) { + reject(e); + } + })); + }; + + node = node.return; + } + + return stackPromises; +}; + +function getInspectorDataForCoordinates(mainContainerRef, x, y, requestStack, callback) { + const { width: screenWidth, height: screenHeight } = Dimensions.get("screen"); + + function scaleFrame(frame) { + return { + x: frame.x / screenWidth, + y: frame.y / screenHeight, + width: frame.width / screenWidth, + height: frame.height / screenHeight + }; + }; + + RNInternals.getInspectorDataForViewAtPoint( + mainContainerRef.current, + x * screenWidth, + y * screenHeight, + (viewData) => { + const frame = viewData.frame; + const scaledFrame = scaleFrame({ + x: frame.left, + y: frame.top, + width: frame.width, + height: frame.height + }); + + if (!requestStack) { + callback({ frame: scaledFrame }); + return; + } + + Promise.all(traverseComponentsTreeUp(viewData.closestInstance, [])) + .then((stack) => stack.filter(Boolean)) + .then((stack) => + stack.map((stackElement) => ({ + ...stackElement, + frame: scaleFrame(stackElement.frame) + })) + ).then((scaledStack) => + callback({ + frame: scaledFrame, + stack: scaledStack + })); + } + ); +}; + export function AppWrapper({ children, initialProps, fabric }) { const rootTag = useContext(RootTagContext); const [devtoolsAgent, setDevtoolsAgent] = useState(null); @@ -180,69 +304,20 @@ export function AppWrapper({ children, initialProps, fabric }) { devtoolsAgent, "RNIDE_inspect", (payload) => { - const getInspectorDataForViewAtPoint = RNInternals.getInspectorDataForViewAtPoint; - const { width, height } = Dimensions.get("screen"); - - getInspectorDataForViewAtPoint( - mainContainerRef.current, - payload.x * width, - payload.y * height, - (viewData) => { - const frame = viewData.frame; - const scaledFrame = { - x: frame.left / width, - y: frame.top / height, - width: frame.width / width, - height: frame.height / height, - }; - let stackPromise = Promise.resolve(undefined); - if (payload.requestStack) { - stackPromise = Promise.all( - viewData.hierarchy.reverse().map((item) => { - const inspectorData = item.getInspectorData((arg) => findNodeHandle(arg)); - const framePromise = new Promise((resolve, reject) => { - try { - inspectorData.measure((_x, _y, viewWidth, viewHeight, pageX, pageY) => { - resolve({ - x: pageX / width, - y: pageY / height, - width: viewWidth / width, - height: viewHeight / height, - }); - }); - } catch (e) { - reject(e); - } - }); - - return framePromise - .catch(() => undefined) - .then((frame) => { - return inspectorData.source - ? { - componentName: item.name, - source: { - fileName: inspectorData.source.fileName, - line0Based: inspectorData.source.lineNumber - 1, - column0Based: inspectorData.source.columnNumber - 1, - }, - frame, - } - : undefined; - }); - }) - ).then((stack) => stack?.filter(Boolean)); - } - stackPromise.then((stack) => { - devtoolsAgent._bridge.send("RNIDE_inspectData", { - id: payload.id, - frame: scaledFrame, - stack: stack, - }); + const { id, x, y, requestStack } = payload; + + getInspectorDataForCoordinates( + mainContainerRef, + x, + y, + requestStack, + (inspectorData) => { + devtoolsAgent._bridge.send("RNIDE_inspectData", { + id, + ...inspectorData }); - } - ); - }, + }); + }, [mainContainerRef] );