react-native-ai-devtools 1.4.17 → 1.4.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -22
- package/build/__tests__/integration/tools.test.js +9 -10
- package/build/__tests__/integration/tools.test.js.map +1 -1
- package/build/core/connection.d.ts +8 -0
- package/build/core/connection.d.ts.map +1 -1
- package/build/core/connection.js +77 -5
- package/build/core/connection.js.map +1 -1
- package/build/core/executor.d.ts +8 -6
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +447 -112
- package/build/core/executor.js.map +1 -1
- package/build/core/index.d.ts +18 -20
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +16 -20
- package/build/core/index.js.map +1 -1
- package/build/core/ios.d.ts +20 -7
- package/build/core/ios.d.ts.map +1 -1
- package/build/core/ios.js +453 -339
- package/build/core/ios.js.map +1 -1
- package/build/core/telemetry.d.ts +1 -1
- package/build/core/telemetry.d.ts.map +1 -1
- package/build/core/telemetry.js +5 -1
- package/build/core/telemetry.js.map +1 -1
- package/build/index.js +65 -130
- package/build/index.js.map +1 -1
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +159 -101
- package/build/pro/tap.js.map +1 -1
- package/package.json +2 -3
- package/build/core/httpServer.d.ts +0 -14
- package/build/core/httpServer.d.ts.map +0 -1
- package/build/core/httpServer.js +0 -2459
- package/build/core/httpServer.js.map +0 -1
- package/build/core/httpServerProcess.d.ts +0 -25
- package/build/core/httpServerProcess.d.ts.map +0 -1
- package/build/core/httpServerProcess.js +0 -153
- package/build/core/httpServerProcess.js.map +0 -1
- package/build/httpServerStandalone.d.ts +0 -7
- package/build/httpServerStandalone.d.ts.map +0 -1
- package/build/httpServerStandalone.js +0 -31
- package/build/httpServerStandalone.js.map +0 -1
package/build/core/executor.js
CHANGED
|
@@ -532,15 +532,125 @@ function formatTreeStructureOnly(node, indent = 0) {
|
|
|
532
532
|
return result;
|
|
533
533
|
}
|
|
534
534
|
function formatScreenLayoutToTonl(elements) {
|
|
535
|
-
|
|
535
|
+
// Check if elements have tree structure (parentIndex)
|
|
536
|
+
const hasTree = elements.some(el => el.parentIndex !== undefined);
|
|
537
|
+
if (hasTree) {
|
|
538
|
+
return formatScreenLayoutTree(elements);
|
|
539
|
+
}
|
|
540
|
+
// Flat format fallback
|
|
541
|
+
const lines = ["#elements{component,path,depth,frame,layout,id}"];
|
|
536
542
|
for (const el of elements) {
|
|
543
|
+
const frame = el.frame
|
|
544
|
+
? `${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)}`
|
|
545
|
+
: "";
|
|
537
546
|
const layout = el.layout
|
|
538
547
|
? Object.entries(el.layout)
|
|
539
548
|
.map(([k, v]) => `${k}:${v}`)
|
|
540
549
|
.join(";")
|
|
541
550
|
: "";
|
|
542
551
|
const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
|
|
543
|
-
lines.push(`${el.component}|${el.path}|${el.depth}|${layout}|${id}`);
|
|
552
|
+
lines.push(`${el.component}|${el.path}|${el.depth}|${frame}|${layout}|${id}`);
|
|
553
|
+
}
|
|
554
|
+
return lines.join("\n");
|
|
555
|
+
}
|
|
556
|
+
function formatScreenLayoutTree(elements, extended = false) {
|
|
557
|
+
// Build index: originalIndex -> element index in the filtered array
|
|
558
|
+
const indexMap = new Map();
|
|
559
|
+
for (let i = 0; i < elements.length; i++) {
|
|
560
|
+
if (elements[i].originalIndex !== undefined) {
|
|
561
|
+
indexMap.set(elements[i].originalIndex, i);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Build children lists
|
|
565
|
+
const children = new Map();
|
|
566
|
+
const roots = [];
|
|
567
|
+
for (let i = 0; i < elements.length; i++) {
|
|
568
|
+
const el = elements[i];
|
|
569
|
+
const parentOrigIdx = el.parentIndex;
|
|
570
|
+
if (parentOrigIdx === undefined || parentOrigIdx === -1 || !indexMap.has(parentOrigIdx)) {
|
|
571
|
+
roots.push(i);
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
const parentIdx = indexMap.get(parentOrigIdx);
|
|
575
|
+
if (!children.has(parentIdx))
|
|
576
|
+
children.set(parentIdx, []);
|
|
577
|
+
children.get(parentIdx).push(i);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Collapse wrapper chains: if a child has the same frame as its parent,
|
|
581
|
+
// it's just a wrapper — promote its children and text to the parent
|
|
582
|
+
function sameFrame(a, b) {
|
|
583
|
+
if (!a.frame || !b.frame)
|
|
584
|
+
return false;
|
|
585
|
+
return Math.abs(a.frame.x - b.frame.x) < 1 &&
|
|
586
|
+
Math.abs(a.frame.y - b.frame.y) < 1 &&
|
|
587
|
+
Math.abs(a.frame.width - b.frame.width) < 1 &&
|
|
588
|
+
Math.abs(a.frame.height - b.frame.height) < 1;
|
|
589
|
+
}
|
|
590
|
+
function collapseNode(parentIdx) {
|
|
591
|
+
const kids = children.get(parentIdx);
|
|
592
|
+
if (!kids)
|
|
593
|
+
return;
|
|
594
|
+
const newKids = [];
|
|
595
|
+
for (const kidIdx of kids) {
|
|
596
|
+
if (sameFrame(elements[parentIdx], elements[kidIdx])) {
|
|
597
|
+
// Collapse: inherit text if parent has none
|
|
598
|
+
if (!elements[parentIdx].text && elements[kidIdx].text) {
|
|
599
|
+
elements[parentIdx].text = elements[kidIdx].text;
|
|
600
|
+
}
|
|
601
|
+
// Promote grandchildren to parent
|
|
602
|
+
const grandKids = children.get(kidIdx);
|
|
603
|
+
if (grandKids) {
|
|
604
|
+
newKids.push(...grandKids);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
newKids.push(kidIdx);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (newKids.length > 0) {
|
|
612
|
+
children.set(parentIdx, newKids);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
children.delete(parentIdx);
|
|
616
|
+
}
|
|
617
|
+
// Recurse into the (possibly new) children
|
|
618
|
+
const updatedKids = children.get(parentIdx);
|
|
619
|
+
if (updatedKids) {
|
|
620
|
+
for (const kid of updatedKids) {
|
|
621
|
+
collapseNode(kid);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
for (const root of roots) {
|
|
626
|
+
collapseNode(root);
|
|
627
|
+
}
|
|
628
|
+
const lines = [];
|
|
629
|
+
function printNode(idx, indent) {
|
|
630
|
+
const el = elements[idx];
|
|
631
|
+
const prefix = " ".repeat(indent);
|
|
632
|
+
const frame = el.frame
|
|
633
|
+
? ` (${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`
|
|
634
|
+
: "";
|
|
635
|
+
const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
|
|
636
|
+
const idStr = id ? ` [${id}]` : "";
|
|
637
|
+
// Only show text on leaf nodes (no meaningful children in tree)
|
|
638
|
+
// to avoid repeating children's text on every parent
|
|
639
|
+
const isLeaf = !children.has(idx);
|
|
640
|
+
const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
|
|
641
|
+
const layoutStr = extended && el.layout
|
|
642
|
+
? ` {${Object.entries(el.layout).map(([k, v]) => `${k}:${v}`).join("; ")}}`
|
|
643
|
+
: "";
|
|
644
|
+
lines.push(`${prefix}${el.component}${frame}${idStr}${textStr}${layoutStr}`);
|
|
645
|
+
const kids = children.get(idx);
|
|
646
|
+
if (kids) {
|
|
647
|
+
for (const kid of kids) {
|
|
648
|
+
printNode(kid, indent + 1);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
for (const root of roots) {
|
|
653
|
+
printNode(root, 0);
|
|
544
654
|
}
|
|
545
655
|
return lines.join("\n");
|
|
546
656
|
}
|
|
@@ -572,7 +682,7 @@ export async function getComponentTree(options = {}) {
|
|
|
572
682
|
// Use lower default depth for structureOnly to keep output compact (~2-5KB)
|
|
573
683
|
// Full mode uses higher depth since TONL format handles it better
|
|
574
684
|
// focusedOnly mode uses moderate depth since we're already filtering to active screen
|
|
575
|
-
const maxDepth = options.maxDepth ??
|
|
685
|
+
const maxDepth = options.maxDepth ?? 5000;
|
|
576
686
|
const expression = `
|
|
577
687
|
(function() {
|
|
578
688
|
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
@@ -717,7 +827,7 @@ export async function getComponentTree(options = {}) {
|
|
|
717
827
|
|
|
718
828
|
// Find focused screen if requested
|
|
719
829
|
function findFocusedScreen(fiber, depth = 0) {
|
|
720
|
-
if (!fiber || depth >
|
|
830
|
+
if (!fiber || depth > 5000) return null;
|
|
721
831
|
|
|
722
832
|
const name = getComponentName(fiber);
|
|
723
833
|
|
|
@@ -793,33 +903,57 @@ export async function getComponentTree(options = {}) {
|
|
|
793
903
|
return result;
|
|
794
904
|
}
|
|
795
905
|
/**
|
|
796
|
-
* Get layout
|
|
797
|
-
*
|
|
906
|
+
* Get layout data for visible components on the current screen.
|
|
907
|
+
* Uses measureInWindow to get actual screen positions and filters
|
|
908
|
+
* to only components within the viewport.
|
|
909
|
+
*
|
|
910
|
+
* Two-step approach (same as inspectAtPoint):
|
|
911
|
+
* Step 1: Walk fiber tree, dispatch measureInWindow on host components
|
|
912
|
+
* Step 2: After 300ms, read measurements, filter by viewport, build results
|
|
798
913
|
*/
|
|
799
914
|
export async function getScreenLayout(options = {}) {
|
|
800
|
-
const {
|
|
801
|
-
const
|
|
915
|
+
const { extended = false, summary = false, device } = options;
|
|
916
|
+
const maxDepth = 5000;
|
|
917
|
+
const componentsOnly = true;
|
|
918
|
+
const shortPath = true;
|
|
919
|
+
// --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
|
|
920
|
+
const dispatchExpression = `
|
|
802
921
|
(function() {
|
|
803
|
-
|
|
922
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
804
923
|
if (!hook) return { error: 'React DevTools hook not found.' };
|
|
805
924
|
|
|
806
|
-
|
|
925
|
+
var roots = [];
|
|
807
926
|
if (hook.getFiberRoots) {
|
|
808
|
-
roots =
|
|
927
|
+
roots = Array.from(hook.getFiberRoots(1) || []);
|
|
809
928
|
}
|
|
810
929
|
if (roots.length === 0 && hook.renderers) {
|
|
811
|
-
for (
|
|
812
|
-
|
|
930
|
+
for (var entry of hook.renderers) {
|
|
931
|
+
var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
|
|
813
932
|
if (r.length > 0) { roots = r; break; }
|
|
814
933
|
}
|
|
815
934
|
}
|
|
816
935
|
if (roots.length === 0) return { error: 'No fiber roots found.' };
|
|
817
936
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
937
|
+
function getMeasurable(fiber) {
|
|
938
|
+
var sn = fiber.stateNode;
|
|
939
|
+
if (!sn) return null;
|
|
940
|
+
if (typeof sn.measureInWindow === 'function') return sn;
|
|
941
|
+
if (sn.canonical && sn.canonical.publicInstance &&
|
|
942
|
+
typeof sn.canonical.publicInstance.measureInWindow === 'function') {
|
|
943
|
+
return sn.canonical.publicInstance;
|
|
944
|
+
}
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Collect host fibers with their metadata
|
|
949
|
+
var hostFibers = [];
|
|
950
|
+
var fiberMeta = [];
|
|
951
|
+
var componentsOnlyMode = ${componentsOnly};
|
|
952
|
+
|
|
953
|
+
// RN internals and primitives — skip these when looking for meaningful component names
|
|
954
|
+
// Only filter internal components that can't be written as JSX.
|
|
955
|
+
// Keep all user-facing components (View, Text, ScrollView, Modal, etc.)
|
|
956
|
+
var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|RCT.*|RNS.*|RNC.*|ViewManagerAdapter_.*|VirtualizedList.*|CellRenderer.*|FrameSizeProvider.*|MaybeScreenContainer|MaybeScreen|Navigation.*|Screen$|ScreenStack|ScreenContainer|ScreenContentWrapper|SceneView|DelayedFreeze|Freeze|Suspender|DebugContainer|StaticContainer|SafeAreaProvider.*|SafeAreaFrameContext|SafeAreaInsetsContext|ExpoRoot|ExpoRootComponent|GestureHandler.*|NativeViewGestureHandler|GestureDetector|PanGestureHandler|Reanimated.*|BottomTabNavigator|TabLayout|RouteNode|Route$|KeyboardProvider|PortalProviderComponent|BottomSheetModalProviderWrapper|ThemeContext|ThemeProvider|TextAncestorContext|PressabilityDebugView|TouchableHighlightImpl|StatusBarOverlay|BottomSheetHostingContainerComponent|BottomSheetGestureHandlersProvider|BottomSheetBackdropContainerComponent|BottomSheetContainerComponent|BottomSheetDraggableViewComponent|BottomSheetHandleContainerComponent|BottomSheetBackgroundContainerComponent|DebuggingOverlay|InspectorDeferred|Inspector|InspectorOverlay|InspectorPanel|StyleInspector|BoxInspector|BoxContainer|ElementBox|BorderBox|InspectorPanelButton)$/;
|
|
823
957
|
|
|
824
958
|
function getComponentName(fiber) {
|
|
825
959
|
if (!fiber || !fiber.type) return null;
|
|
@@ -827,8 +961,196 @@ export async function getScreenLayout(options = {}) {
|
|
|
827
961
|
return fiber.type.displayName || fiber.type.name || null;
|
|
828
962
|
}
|
|
829
963
|
|
|
830
|
-
|
|
831
|
-
|
|
964
|
+
// Find the first measurable host descendant of a fiber
|
|
965
|
+
function findFirstHost(fiber, depth) {
|
|
966
|
+
if (!fiber || depth > 20) return null;
|
|
967
|
+
if (typeof fiber.type === 'string' && getMeasurable(fiber)) return fiber;
|
|
968
|
+
var child = fiber.child;
|
|
969
|
+
while (child) {
|
|
970
|
+
var found = findFirstHost(child, depth + 1);
|
|
971
|
+
if (found) return found;
|
|
972
|
+
child = child.sibling;
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Extract text content from a fiber subtree.
|
|
978
|
+
// When a fiber has a string child, return it without recursing
|
|
979
|
+
// (avoids duplication from Text > RCTText having the same string).
|
|
980
|
+
function collectText(fiber, d) {
|
|
981
|
+
if (!fiber || d > 30) return '';
|
|
982
|
+
var props = fiber.memoizedProps;
|
|
983
|
+
if (props) {
|
|
984
|
+
var ch = props.children;
|
|
985
|
+
// Leaf text — return without recursing into children fibers
|
|
986
|
+
if (typeof ch === 'string') return ch;
|
|
987
|
+
if (typeof ch === 'number') return String(ch);
|
|
988
|
+
if (Array.isArray(ch)) {
|
|
989
|
+
var inline = [];
|
|
990
|
+
for (var ci = 0; ci < ch.length; ci++) {
|
|
991
|
+
if (typeof ch[ci] === 'string') inline.push(ch[ci]);
|
|
992
|
+
else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
|
|
993
|
+
}
|
|
994
|
+
if (inline.length > 0) return inline.join('');
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
// No direct text — collect from child fibers (siblings = adjacent elements)
|
|
998
|
+
var parts = [];
|
|
999
|
+
var child = fiber.child;
|
|
1000
|
+
while (child) {
|
|
1001
|
+
var t = collectText(child, d + 1);
|
|
1002
|
+
if (t) parts.push(t);
|
|
1003
|
+
child = child.sibling;
|
|
1004
|
+
}
|
|
1005
|
+
return parts.join(' ').trim();
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (componentsOnlyMode) {
|
|
1009
|
+
// componentsOnly: walk tree looking for meaningful custom components,
|
|
1010
|
+
// measure their first host child, track parent for tree output
|
|
1011
|
+
function walkComponents(fiber, depth, path, parentIdx) {
|
|
1012
|
+
if (!fiber || depth > ${maxDepth}) return;
|
|
1013
|
+
var name = getComponentName(fiber);
|
|
1014
|
+
var isHost = typeof fiber.type === 'string';
|
|
1015
|
+
var isMeaningful = name && !isHost && !RN_PRIMITIVES.test(name);
|
|
1016
|
+
|
|
1017
|
+
var myIdx = parentIdx;
|
|
1018
|
+
if (isMeaningful) {
|
|
1019
|
+
var host = findFirstHost(fiber, 0);
|
|
1020
|
+
if (host) {
|
|
1021
|
+
myIdx = hostFibers.length;
|
|
1022
|
+
// Extract text from this component's subtree
|
|
1023
|
+
var text = collectText(fiber, 0);
|
|
1024
|
+
hostFibers.push(host);
|
|
1025
|
+
fiberMeta.push({
|
|
1026
|
+
hostName: typeof host.type === 'string' ? host.type : '',
|
|
1027
|
+
customName: name,
|
|
1028
|
+
depth: depth,
|
|
1029
|
+
path: path.concat([name]),
|
|
1030
|
+
parentIndex: parentIdx,
|
|
1031
|
+
text: text ? text.slice(0, 80) : null
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
var child = fiber.child;
|
|
1037
|
+
while (child) {
|
|
1038
|
+
var childName = getComponentName(child);
|
|
1039
|
+
walkComponents(child, depth + 1, childName ? path.concat([childName]) : path, myIdx);
|
|
1040
|
+
child = child.sibling;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
walkComponents(roots[0].current, 0, [], -1);
|
|
1044
|
+
} else {
|
|
1045
|
+
// Default mode: collect all host fibers with ancestor info
|
|
1046
|
+
function walkFibers(fiber, depth, path) {
|
|
1047
|
+
if (!fiber || depth > ${maxDepth}) return;
|
|
1048
|
+
var name = getComponentName(fiber);
|
|
1049
|
+
var isHost = typeof fiber.type === 'string';
|
|
1050
|
+
|
|
1051
|
+
if (name && isHost && getMeasurable(fiber)) {
|
|
1052
|
+
// Find nearest meaningful custom component ancestor for display
|
|
1053
|
+
var customName = null;
|
|
1054
|
+
var fallbackName = null;
|
|
1055
|
+
var cur = fiber.return;
|
|
1056
|
+
while (cur) {
|
|
1057
|
+
if (cur.type && typeof cur.type !== 'string') {
|
|
1058
|
+
var cName = cur.type.displayName || cur.type.name || null;
|
|
1059
|
+
if (cName) {
|
|
1060
|
+
if (!fallbackName) fallbackName = cName;
|
|
1061
|
+
if (!RN_PRIMITIVES.test(cName)) {
|
|
1062
|
+
customName = cName;
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
cur = cur.return;
|
|
1068
|
+
}
|
|
1069
|
+
if (!customName) customName = fallbackName;
|
|
1070
|
+
|
|
1071
|
+
hostFibers.push(fiber);
|
|
1072
|
+
fiberMeta.push({
|
|
1073
|
+
hostName: name,
|
|
1074
|
+
customName: customName,
|
|
1075
|
+
depth: depth,
|
|
1076
|
+
path: path.slice()
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
var child = fiber.child;
|
|
1081
|
+
while (child) {
|
|
1082
|
+
var childName = getComponentName(child);
|
|
1083
|
+
walkFibers(child, depth + 1, childName ? path.concat([childName]) : path);
|
|
1084
|
+
child = child.sibling;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
walkFibers(roots[0].current, 0, []);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (hostFibers.length === 0) return { error: 'No measurable host components found.' };
|
|
1091
|
+
|
|
1092
|
+
// Store fibers and metadata globally for step 2
|
|
1093
|
+
globalThis.__layoutFibers = hostFibers;
|
|
1094
|
+
globalThis.__layoutMeta = fiberMeta;
|
|
1095
|
+
globalThis.__layoutMeasurements = new Array(hostFibers.length).fill(null);
|
|
1096
|
+
|
|
1097
|
+
// Dispatch measureInWindow on all host fibers
|
|
1098
|
+
for (var i = 0; i < hostFibers.length; i++) {
|
|
1099
|
+
try {
|
|
1100
|
+
(function(idx) {
|
|
1101
|
+
getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
|
|
1102
|
+
globalThis.__layoutMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
|
|
1103
|
+
});
|
|
1104
|
+
})(i);
|
|
1105
|
+
} catch(e) {}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return { count: hostFibers.length };
|
|
1109
|
+
})()
|
|
1110
|
+
`;
|
|
1111
|
+
const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
|
|
1112
|
+
if (!dispatchResult.success)
|
|
1113
|
+
return dispatchResult;
|
|
1114
|
+
try {
|
|
1115
|
+
const parsed = JSON.parse(dispatchResult.result || "{}");
|
|
1116
|
+
if (parsed.error)
|
|
1117
|
+
return { success: false, error: parsed.error };
|
|
1118
|
+
}
|
|
1119
|
+
catch {
|
|
1120
|
+
/* ignore */
|
|
1121
|
+
}
|
|
1122
|
+
// Wait for measureInWindow callbacks
|
|
1123
|
+
await delay(300);
|
|
1124
|
+
// --- Step 2: read measurements, filter visible, build results ---
|
|
1125
|
+
const resolveExpression = `
|
|
1126
|
+
(function() {
|
|
1127
|
+
var fibers = globalThis.__layoutFibers;
|
|
1128
|
+
var meta = globalThis.__layoutMeta;
|
|
1129
|
+
var measurements = globalThis.__layoutMeasurements;
|
|
1130
|
+
globalThis.__layoutFibers = null;
|
|
1131
|
+
globalThis.__layoutMeta = null;
|
|
1132
|
+
globalThis.__layoutMeasurements = null;
|
|
1133
|
+
|
|
1134
|
+
if (!fibers || !measurements || !meta) {
|
|
1135
|
+
return { error: 'No measurement data. Run get_screen_layout again.' };
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
var componentsOnly = ${componentsOnly};
|
|
1139
|
+
var shortPath = ${shortPath};
|
|
1140
|
+
var summaryMode = ${summary};
|
|
1141
|
+
var pathSegments = 3;
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
// Get viewport dimensions from the first root view measurement
|
|
1145
|
+
// or use a generous default
|
|
1146
|
+
var viewportW = 9999, viewportH = 9999;
|
|
1147
|
+
for (var v = 0; v < measurements.length; v++) {
|
|
1148
|
+
if (measurements[v] && measurements[v].x === 0 && measurements[v].y === 0 &&
|
|
1149
|
+
measurements[v].width > 0 && measurements[v].height > 0) {
|
|
1150
|
+
viewportW = measurements[v].width;
|
|
1151
|
+
viewportH = measurements[v].height;
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
832
1154
|
}
|
|
833
1155
|
|
|
834
1156
|
function formatPath(pathArray) {
|
|
@@ -838,22 +1160,14 @@ export async function getScreenLayout(options = {}) {
|
|
|
838
1160
|
return '... > ' + pathArray.slice(-pathSegments).join(' > ');
|
|
839
1161
|
}
|
|
840
1162
|
|
|
841
|
-
function extractAllStyles(style) {
|
|
842
|
-
if (!style) return null;
|
|
843
|
-
const merged = Array.isArray(style)
|
|
844
|
-
? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
|
|
845
|
-
: (typeof style === 'object' ? style : {});
|
|
846
|
-
return Object.keys(merged).length > 0 ? merged : null;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
1163
|
function extractLayoutStyles(style) {
|
|
850
1164
|
if (!style) return null;
|
|
851
|
-
|
|
852
|
-
? Object.assign({}
|
|
1165
|
+
var merged = Array.isArray(style)
|
|
1166
|
+
? Object.assign.apply(null, [{}].concat(style.filter(Boolean).map(function(s) { return typeof s === 'object' ? s : {}; })))
|
|
853
1167
|
: (typeof style === 'object' ? style : {});
|
|
854
1168
|
|
|
855
|
-
|
|
856
|
-
|
|
1169
|
+
var layout = {};
|
|
1170
|
+
var layoutKeys = [
|
|
857
1171
|
'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
858
1172
|
'paddingHorizontal', 'paddingVertical',
|
|
859
1173
|
'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
|
|
@@ -867,76 +1181,103 @@ export async function getScreenLayout(options = {}) {
|
|
|
867
1181
|
'backgroundColor', 'borderColor', 'borderRadius'
|
|
868
1182
|
];
|
|
869
1183
|
|
|
870
|
-
for (
|
|
871
|
-
if (merged[
|
|
1184
|
+
for (var k = 0; k < layoutKeys.length; k++) {
|
|
1185
|
+
if (merged[layoutKeys[k]] !== undefined) layout[layoutKeys[k]] = merged[layoutKeys[k]];
|
|
872
1186
|
}
|
|
873
1187
|
return Object.keys(layout).length > 0 ? layout : null;
|
|
874
1188
|
}
|
|
875
1189
|
|
|
876
|
-
|
|
1190
|
+
var elements = [];
|
|
877
1191
|
|
|
878
|
-
|
|
879
|
-
|
|
1192
|
+
for (var i = 0; i < measurements.length; i++) {
|
|
1193
|
+
var m = measurements[i];
|
|
1194
|
+
if (!m) continue;
|
|
880
1195
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (name && (!componentsOnly || !isHost)) {
|
|
886
|
-
const style = fiber.memoizedProps?.style;
|
|
887
|
-
const layout = extractLayoutStyles(style);
|
|
888
|
-
|
|
889
|
-
// Get text content if it's a Text component
|
|
890
|
-
let textContent = null;
|
|
891
|
-
if (name === 'Text' || name === 'RCTText') {
|
|
892
|
-
const children = fiber.memoizedProps?.children;
|
|
893
|
-
if (typeof children === 'string') textContent = children;
|
|
894
|
-
else if (typeof children === 'number') textContent = String(children);
|
|
895
|
-
}
|
|
1196
|
+
// Filter: only visible within viewport (with some tolerance for partial visibility)
|
|
1197
|
+
if (m.width <= 0 || m.height <= 0) continue;
|
|
1198
|
+
if (m.x + m.width < 0 || m.y + m.height < 0) continue;
|
|
1199
|
+
if (m.x > viewportW || m.y > viewportH) continue;
|
|
896
1200
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
if (layout) element.layout = layout;
|
|
904
|
-
if (textContent) element.text = textContent.slice(0, 100);
|
|
905
|
-
|
|
906
|
-
// Include key props for identification
|
|
907
|
-
if (fiber.memoizedProps) {
|
|
908
|
-
const identifiers = {};
|
|
909
|
-
if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
|
|
910
|
-
if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
|
|
911
|
-
if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
|
|
912
|
-
if (fiber.key) identifiers.key = fiber.key;
|
|
913
|
-
if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
|
|
914
|
-
}
|
|
1201
|
+
var fiber = fibers[i];
|
|
1202
|
+
var info = meta[i];
|
|
1203
|
+
|
|
1204
|
+
var displayName = componentsOnly ? info.customName : info.hostName;
|
|
1205
|
+
if (!displayName) continue;
|
|
1206
|
+
if (componentsOnly && !info.customName) continue;
|
|
915
1207
|
|
|
916
|
-
|
|
1208
|
+
var style = fiber.memoizedProps ? fiber.memoizedProps.style : null;
|
|
1209
|
+
var layout = extractLayoutStyles(style);
|
|
1210
|
+
|
|
1211
|
+
// Get text content — use pre-collected text from step 1, or fall back to host fiber
|
|
1212
|
+
var textContent = info.text || null;
|
|
1213
|
+
if (!textContent && (info.hostName === 'RCTText' || info.hostName === 'Text')) {
|
|
1214
|
+
var children = fiber.memoizedProps ? fiber.memoizedProps.children : null;
|
|
1215
|
+
if (typeof children === 'string') textContent = children;
|
|
1216
|
+
else if (typeof children === 'number') textContent = String(children);
|
|
917
1217
|
}
|
|
918
1218
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1219
|
+
var element = {
|
|
1220
|
+
component: displayName,
|
|
1221
|
+
path: formatPath(info.path),
|
|
1222
|
+
depth: info.depth,
|
|
1223
|
+
frame: { x: m.x, y: m.y, width: m.width, height: m.height },
|
|
1224
|
+
originalIndex: i
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
if (info.parentIndex !== undefined) element.parentIndex = info.parentIndex;
|
|
1228
|
+
if (layout) element.layout = layout;
|
|
1229
|
+
if (textContent) element.text = textContent.slice(0, 100);
|
|
1230
|
+
|
|
1231
|
+
// Identifiers
|
|
1232
|
+
if (fiber.memoizedProps) {
|
|
1233
|
+
var identifiers = {};
|
|
1234
|
+
if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
|
|
1235
|
+
if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
|
|
1236
|
+
if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
|
|
1237
|
+
if (fiber.key) identifiers.key = fiber.key;
|
|
1238
|
+
if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
|
|
925
1239
|
}
|
|
1240
|
+
|
|
1241
|
+
elements.push(element);
|
|
926
1242
|
}
|
|
927
1243
|
|
|
928
|
-
|
|
1244
|
+
// In componentsOnly mode, remove full-screen wrapper components
|
|
1245
|
+
// and re-parent their children to collapse the wrapper chain
|
|
1246
|
+
if (componentsOnly && elements.length > 0) {
|
|
1247
|
+
var filtered = [];
|
|
1248
|
+
// Map: originalIndex -> resolved parent for skipped wrappers
|
|
1249
|
+
var reparent = {};
|
|
1250
|
+
|
|
1251
|
+
for (var fi = 0; fi < elements.length; fi++) {
|
|
1252
|
+
var el = elements[fi];
|
|
1253
|
+
var fr = el.frame;
|
|
1254
|
+
var isFullScreen = fr && fr.x === 0 && fr.y === 0 &&
|
|
1255
|
+
Math.abs(fr.width - viewportW) < 2 && Math.abs(fr.height - viewportH) < 2;
|
|
1256
|
+
|
|
1257
|
+
if (isFullScreen) {
|
|
1258
|
+
// Skip this wrapper, map its originalIndex to its parent
|
|
1259
|
+
reparent[el.originalIndex] = el.parentIndex;
|
|
1260
|
+
} else {
|
|
1261
|
+
// Resolve parent through any skipped wrappers
|
|
1262
|
+
var resolvedParent = el.parentIndex;
|
|
1263
|
+
while (resolvedParent !== undefined && resolvedParent !== -1 && reparent[resolvedParent] !== undefined) {
|
|
1264
|
+
resolvedParent = reparent[resolvedParent];
|
|
1265
|
+
}
|
|
1266
|
+
el.parentIndex = resolvedParent;
|
|
1267
|
+
filtered.push(el);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
elements = filtered;
|
|
1271
|
+
}
|
|
929
1272
|
|
|
930
|
-
// Summary mode: return counts by component name
|
|
931
1273
|
if (summaryMode) {
|
|
932
|
-
|
|
933
|
-
for (
|
|
934
|
-
counts[
|
|
1274
|
+
var counts = {};
|
|
1275
|
+
for (var j = 0; j < elements.length; j++) {
|
|
1276
|
+
counts[elements[j].component] = (counts[elements[j].component] || 0) + 1;
|
|
935
1277
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
.map(([name, count]) => ({ component: name, count }));
|
|
1278
|
+
var sorted = Object.keys(counts).map(function(name) {
|
|
1279
|
+
return { component: name, count: counts[name] };
|
|
1280
|
+
}).sort(function(a, b) { return b.count - a.count; });
|
|
940
1281
|
return {
|
|
941
1282
|
totalElements: elements.length,
|
|
942
1283
|
uniqueComponents: sorted.length,
|
|
@@ -945,15 +1286,15 @@ export async function getScreenLayout(options = {}) {
|
|
|
945
1286
|
}
|
|
946
1287
|
|
|
947
1288
|
return {
|
|
1289
|
+
viewport: { width: viewportW, height: viewportH },
|
|
948
1290
|
totalElements: elements.length,
|
|
949
1291
|
elements: elements
|
|
950
1292
|
};
|
|
951
1293
|
})()
|
|
952
1294
|
`;
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
if (format === "tonl" && result.success && result.result) {
|
|
1295
|
+
const result = await executeInApp(resolveExpression, false, { timeoutMs: 30000 }, device);
|
|
1296
|
+
// Format output as tree
|
|
1297
|
+
if (result.success && result.result) {
|
|
957
1298
|
try {
|
|
958
1299
|
const parsed = JSON.parse(result.result);
|
|
959
1300
|
if (parsed.components) {
|
|
@@ -962,9 +1303,8 @@ export async function getScreenLayout(options = {}) {
|
|
|
962
1303
|
return { success: true, result: tonl };
|
|
963
1304
|
}
|
|
964
1305
|
else if (parsed.elements) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
return { success: true, result: tonl };
|
|
1306
|
+
const tree = formatScreenLayoutTree(parsed.elements, extended);
|
|
1307
|
+
return { success: true, result: tree };
|
|
968
1308
|
}
|
|
969
1309
|
}
|
|
970
1310
|
catch {
|
|
@@ -1280,8 +1620,6 @@ export async function findComponents(pattern, options = {}) {
|
|
|
1280
1620
|
child = child.sibling;
|
|
1281
1621
|
}
|
|
1282
1622
|
} catch(e) {
|
|
1283
|
-
// Skip nodes that throw (e.g. Reanimated animated components)
|
|
1284
|
-
// Still try to traverse children if possible
|
|
1285
1623
|
try {
|
|
1286
1624
|
var child = fiber.child;
|
|
1287
1625
|
while (child && results.length < maxResults) {
|
|
@@ -1294,7 +1632,6 @@ export async function findComponents(pattern, options = {}) {
|
|
|
1294
1632
|
|
|
1295
1633
|
search(roots[0].current, [], 0);
|
|
1296
1634
|
|
|
1297
|
-
// Summary mode: just return counts by component name
|
|
1298
1635
|
if (summaryMode) {
|
|
1299
1636
|
const counts = {};
|
|
1300
1637
|
for (const r of results) {
|
|
@@ -1319,18 +1656,15 @@ export async function findComponents(pattern, options = {}) {
|
|
|
1319
1656
|
})()
|
|
1320
1657
|
`;
|
|
1321
1658
|
const result = await executeInApp(expression, false, {}, device);
|
|
1322
|
-
// Apply TONL formatting if requested
|
|
1323
1659
|
if (format === "tonl" && result.success && result.result) {
|
|
1324
1660
|
try {
|
|
1325
1661
|
const parsed = JSON.parse(result.result);
|
|
1326
1662
|
if (parsed.components) {
|
|
1327
1663
|
if (parsed.totalMatches !== undefined) {
|
|
1328
|
-
// Summary mode
|
|
1329
1664
|
const tonl = formatSummaryToTonl(parsed.components, parsed.totalMatches);
|
|
1330
1665
|
return { success: true, result: `pattern: ${parsed.pattern}\n${tonl}` };
|
|
1331
1666
|
}
|
|
1332
1667
|
else {
|
|
1333
|
-
// Full list mode
|
|
1334
1668
|
const tonl = formatFoundComponentsToTonl(parsed.components);
|
|
1335
1669
|
return { success: true, result: `pattern: ${parsed.pattern}\nfound: ${parsed.found}\n${tonl}` };
|
|
1336
1670
|
}
|
|
@@ -1387,7 +1721,7 @@ export async function pressElement(options) {
|
|
|
1387
1721
|
}
|
|
1388
1722
|
|
|
1389
1723
|
function extractText(fiber, depth) {
|
|
1390
|
-
if (!fiber || depth >
|
|
1724
|
+
if (!fiber || depth > 5000) return '';
|
|
1391
1725
|
var parts = [];
|
|
1392
1726
|
var props = fiber.memoizedProps;
|
|
1393
1727
|
if (props) {
|
|
@@ -1656,7 +1990,7 @@ export async function pressElement(options) {
|
|
|
1656
1990
|
var hostFiber = null;
|
|
1657
1991
|
var fallbackHost = null;
|
|
1658
1992
|
function findHostChild(fiber, depth) {
|
|
1659
|
-
if (!fiber || depth >
|
|
1993
|
+
if (!fiber || depth > 5000 || hostFiber) return;
|
|
1660
1994
|
if (fiber.tag === 5 && fiber.stateNode) {
|
|
1661
1995
|
var hostType = typeof fiber.type === 'string' ? fiber.type : '';
|
|
1662
1996
|
if (target.isInput && (hostType.indexOf('TextInput') !== -1 || hostType.indexOf('textinput') !== -1)) {
|
|
@@ -1806,7 +2140,7 @@ export async function isInspectorActive(device) {
|
|
|
1806
2140
|
if (roots.length === 0) return false;
|
|
1807
2141
|
|
|
1808
2142
|
function findComponent(fiber, targetName, depth = 0) {
|
|
1809
|
-
if (!fiber || depth >
|
|
2143
|
+
if (!fiber || depth > 5000) return null;
|
|
1810
2144
|
const name = fiber.type?.displayName || fiber.type?.name;
|
|
1811
2145
|
if (name === targetName) return fiber;
|
|
1812
2146
|
let child = fiber.child;
|
|
@@ -1848,28 +2182,29 @@ export async function getInspectorSelection(device) {
|
|
|
1848
2182
|
}
|
|
1849
2183
|
if (roots.length === 0) return { error: 'No fiber roots found.' };
|
|
1850
2184
|
|
|
1851
|
-
// Find InspectorPanel
|
|
1852
|
-
function
|
|
1853
|
-
if (!fiber || depth >
|
|
2185
|
+
// Find all InspectorPanel instances (apps with modals may have multiple)
|
|
2186
|
+
function findAllPanels(fiber, targetName, depth, results) {
|
|
2187
|
+
if (!fiber || depth > 5000) return;
|
|
1854
2188
|
const name = fiber.type?.displayName || fiber.type?.name;
|
|
1855
|
-
if (name === targetName)
|
|
2189
|
+
if (name === targetName) results.push(fiber);
|
|
1856
2190
|
let child = fiber.child;
|
|
1857
2191
|
while (child) {
|
|
1858
|
-
|
|
1859
|
-
if (found) return found;
|
|
2192
|
+
findAllPanels(child, targetName, depth + 1, results);
|
|
1860
2193
|
child = child.sibling;
|
|
1861
2194
|
}
|
|
1862
|
-
return null;
|
|
1863
2195
|
}
|
|
1864
2196
|
|
|
1865
|
-
const
|
|
1866
|
-
|
|
2197
|
+
const panels = [];
|
|
2198
|
+
findAllPanels(roots[0].current, 'InspectorPanel', 0, panels);
|
|
2199
|
+
if (panels.length === 0) {
|
|
1867
2200
|
return {
|
|
1868
2201
|
error: 'Element Inspector is not active.',
|
|
1869
2202
|
hint: 'Use toggle_element_inspector to enable the inspector, then tap an element to select it.'
|
|
1870
2203
|
};
|
|
1871
2204
|
}
|
|
1872
2205
|
|
|
2206
|
+
// Prefer the panel that has an active selection
|
|
2207
|
+
const panelFiber = panels.find(p => p.memoizedProps.hierarchy?.length > 0) || panels[0];
|
|
1873
2208
|
const props = panelFiber.memoizedProps;
|
|
1874
2209
|
if (!props.hierarchy || props.hierarchy.length === 0) {
|
|
1875
2210
|
return {
|
|
@@ -2028,7 +2363,7 @@ export async function inspectAtPoint(x, y, options = {}) {
|
|
|
2028
2363
|
|
|
2029
2364
|
// RN primitives and internal components to skip when surfacing the "element" name.
|
|
2030
2365
|
// We want the nearest *custom* component, not a library wrapper.
|
|
2031
|
-
var RN_PRIMITIVES = /^(View|Text|Image|ScrollView|FlatList|SectionList|TextInput|TouchableOpacity|TouchableHighlight|TouchableNativeFeedback|TouchableWithoutFeedback|Pressable|Button|Switch|ActivityIndicator|
|
|
2366
|
+
var RN_PRIMITIVES = /^(View|Text|Image|ScrollView|FlatList|SectionList|TextInput|TouchableOpacity|TouchableHighlight|TouchableNativeFeedback|TouchableWithoutFeedback|Pressable|Button|Switch|ActivityIndicator|SafeAreaView|KeyboardAvoidingView|Animated\\(.*|withAnimated.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|VirtualizedList.*|CellRenderer.*|FrameSizeProvider|MaybeScreenContainer|RCT.*|RNS.*|Navigation.*|Screen$|ScreenStack|ScreenContainer|ScreenContentWrapper|SceneView|DelayedFreeze|Freeze|Suspender|DebugContainer|StaticContainer|Expo.*|LinearGradient|ViewManagerAdapter_.*|Svg.*|Defs|Path|Rect|Circle|G|Line|Polygon|Polyline|Ellipse|ClipPath|GestureHandler.*|NativeViewGestureHandler|Reanimated.*|BottomTabNavigator|TabLayout|RouteNode|Route$|MaybeScreen|SafeAreaProvider.*|GestureDetector|PanGestureHandler|DropShadow|BlurView|MaskedView.*)$/;
|
|
2032
2367
|
|
|
2033
2368
|
function getNearestNamed(fiber, skipPrimitives) {
|
|
2034
2369
|
var cur = fiber;
|