react-native-ai-debugger 1.8.0 → 1.8.1
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/build/core/appDetection.d.ts +7 -0
- package/build/core/appDetection.d.ts.map +1 -1
- package/build/core/appDetection.js +38 -17
- package/build/core/appDetection.js.map +1 -1
- package/build/core/componentSearch.d.ts +38 -0
- package/build/core/componentSearch.d.ts.map +1 -0
- package/build/core/componentSearch.js +419 -0
- package/build/core/componentSearch.js.map +1 -0
- package/build/core/componentTree.d.ts +12 -0
- package/build/core/componentTree.d.ts.map +1 -0
- package/build/core/componentTree.js +275 -0
- package/build/core/componentTree.js.map +1 -0
- package/build/core/connection.d.ts +1 -0
- package/build/core/connection.d.ts.map +1 -1
- package/build/core/connection.js +97 -88
- package/build/core/connection.js.map +1 -1
- package/build/core/debugGlobals.d.ts +5 -0
- package/build/core/debugGlobals.d.ts.map +1 -0
- package/build/core/debugGlobals.js +300 -0
- package/build/core/debugGlobals.js.map +1 -0
- package/build/core/executor.d.ts +7 -206
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +10 -4333
- package/build/core/executor.js.map +1 -1
- package/build/core/index.d.ts +3 -2
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +3 -2
- package/build/core/index.js.map +1 -1
- package/build/core/inspector.d.ts +50 -0
- package/build/core/inspector.d.ts.map +1 -0
- package/build/core/inspector.js +604 -0
- package/build/core/inspector.js.map +1 -0
- package/build/core/jsExecute.d.ts +27 -0
- package/build/core/jsExecute.d.ts.map +1 -0
- package/build/core/jsExecute.js +453 -0
- package/build/core/jsExecute.js.map +1 -0
- package/build/core/pressables.d.ts +44 -0
- package/build/core/pressables.d.ts.map +1 -0
- package/build/core/pressables.js +1480 -0
- package/build/core/pressables.js.map +1 -0
- package/build/core/register.d.ts +8 -0
- package/build/core/register.d.ts.map +1 -0
- package/build/core/register.js +216 -0
- package/build/core/register.js.map +1 -0
- package/build/core/screenLayout.d.ts +52 -0
- package/build/core/screenLayout.d.ts.map +1 -0
- package/build/core/screenLayout.js +828 -0
- package/build/core/screenLayout.js.map +1 -0
- package/build/core/toolHelpers.d.ts +24 -0
- package/build/core/toolHelpers.d.ts.map +1 -0
- package/build/core/toolHelpers.js +118 -0
- package/build/core/toolHelpers.js.map +1 -0
- package/build/core/types.d.ts +11 -6
- package/build/core/types.d.ts.map +1 -1
- package/build/index.d.ts +2 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +37 -4440
- package/build/index.js.map +1 -1
- package/build/tools/accountTools.d.ts +2 -0
- package/build/tools/accountTools.d.ts.map +1 -1
- package/build/tools/accountTools.js +5 -0
- package/build/tools/accountTools.js.map +1 -1
- package/build/tools/bundleTools.d.ts +3 -0
- package/build/tools/bundleTools.d.ts.map +1 -0
- package/build/tools/bundleTools.js +368 -0
- package/build/tools/bundleTools.js.map +1 -0
- package/build/tools/componentTools.d.ts +3 -0
- package/build/tools/componentTools.d.ts.map +1 -0
- package/build/tools/componentTools.js +639 -0
- package/build/tools/componentTools.js.map +1 -0
- package/build/tools/connectionTools.d.ts +3 -0
- package/build/tools/connectionTools.d.ts.map +1 -0
- package/build/tools/connectionTools.js +564 -0
- package/build/tools/connectionTools.js.map +1 -0
- package/build/tools/deviceTools.d.ts +3 -0
- package/build/tools/deviceTools.d.ts.map +1 -0
- package/build/tools/deviceTools.js +296 -0
- package/build/tools/deviceTools.js.map +1 -0
- package/build/tools/executionTools.d.ts +3 -0
- package/build/tools/executionTools.d.ts.map +1 -0
- package/build/tools/executionTools.js +173 -0
- package/build/tools/executionTools.js.map +1 -0
- package/build/tools/interactionTools.d.ts +3 -0
- package/build/tools/interactionTools.d.ts.map +1 -0
- package/build/tools/interactionTools.js +414 -0
- package/build/tools/interactionTools.js.map +1 -0
- package/build/tools/logTools.d.ts +3 -0
- package/build/tools/logTools.d.ts.map +1 -0
- package/build/tools/logTools.js +371 -0
- package/build/tools/logTools.js.map +1 -0
- package/build/tools/metaTools.d.ts +7 -0
- package/build/tools/metaTools.d.ts.map +1 -0
- package/build/tools/metaTools.js +205 -0
- package/build/tools/metaTools.js.map +1 -0
- package/build/tools/networkTools.d.ts +3 -0
- package/build/tools/networkTools.d.ts.map +1 -0
- package/build/tools/networkTools.js +478 -0
- package/build/tools/networkTools.js.map +1 -0
- package/build/tools/reduxTools.d.ts +3 -0
- package/build/tools/reduxTools.d.ts.map +1 -0
- package/build/tools/reduxTools.js +83 -0
- package/build/tools/reduxTools.js.map +1 -0
- package/build/tools/screenshotTools.d.ts +3 -0
- package/build/tools/screenshotTools.d.ts.map +1 -0
- package/build/tools/screenshotTools.js +606 -0
- package/build/tools/screenshotTools.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { executeInApp } from "./jsExecute.js";
|
|
2
|
+
function formatTreeToTonl(node, indent = 0) {
|
|
3
|
+
const prefix = " ".repeat(indent);
|
|
4
|
+
let result = `${prefix}${node.component}`;
|
|
5
|
+
// Add props inline if present
|
|
6
|
+
if (node.props && Object.keys(node.props).length > 0) {
|
|
7
|
+
const propsStr = Object.entries(node.props)
|
|
8
|
+
.map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`)
|
|
9
|
+
.join(",");
|
|
10
|
+
result += ` (${propsStr})`;
|
|
11
|
+
}
|
|
12
|
+
// Add layout inline if present
|
|
13
|
+
if (node.layout && Object.keys(node.layout).length > 0) {
|
|
14
|
+
const layoutStr = Object.entries(node.layout)
|
|
15
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
16
|
+
.join(",");
|
|
17
|
+
result += ` [${layoutStr}]`;
|
|
18
|
+
}
|
|
19
|
+
result += "\n";
|
|
20
|
+
// Recurse children
|
|
21
|
+
if (node.children && node.children.length > 0) {
|
|
22
|
+
for (const child of node.children) {
|
|
23
|
+
result += formatTreeToTonl(child, indent + 1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
// Ultra-compact structure-only tree format (just component names, indented)
|
|
29
|
+
function formatTreeStructureOnly(node, indent = 0) {
|
|
30
|
+
const prefix = " ".repeat(indent);
|
|
31
|
+
let result = `${prefix}${node.component}\n`;
|
|
32
|
+
if (node.children && node.children.length > 0) {
|
|
33
|
+
for (const child of node.children) {
|
|
34
|
+
result += formatTreeStructureOnly(child, indent + 1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
export async function getComponentTree(options = {}) {
|
|
40
|
+
const { includeProps = false, includeStyles = false, hideInternals = true, format = "tonl", structureOnly = false, focusedOnly = false, device } = options;
|
|
41
|
+
// Use lower default depth for structureOnly to keep output compact (~2-5KB)
|
|
42
|
+
// Full mode uses higher depth since TONL format handles it better
|
|
43
|
+
// focusedOnly mode uses moderate depth since we're already filtering to active screen
|
|
44
|
+
const maxDepth = options.maxDepth ?? 5000;
|
|
45
|
+
const expression = `
|
|
46
|
+
(function() {
|
|
47
|
+
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
48
|
+
if (!hook) return { error: 'React DevTools hook not found. Make sure you are running a development build.' };
|
|
49
|
+
|
|
50
|
+
// Try to get fiber roots (renderer ID is usually 1)
|
|
51
|
+
let roots = [];
|
|
52
|
+
if (hook.getFiberRoots) {
|
|
53
|
+
roots = [...(hook.getFiberRoots(1) || [])];
|
|
54
|
+
}
|
|
55
|
+
if (roots.length === 0 && hook.renderers) {
|
|
56
|
+
// Try all renderers
|
|
57
|
+
for (const [id] of hook.renderers) {
|
|
58
|
+
const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
|
|
59
|
+
if (r.length > 0) {
|
|
60
|
+
roots = r;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (roots.length === 0) return { error: 'No fiber roots found. The app may not have rendered yet.' };
|
|
66
|
+
|
|
67
|
+
const maxDepth = ${maxDepth};
|
|
68
|
+
const includeProps = ${includeProps};
|
|
69
|
+
const includeStyles = ${includeStyles};
|
|
70
|
+
const hideInternals = ${hideInternals};
|
|
71
|
+
const focusedOnly = ${focusedOnly};
|
|
72
|
+
|
|
73
|
+
// Internal RN components to hide
|
|
74
|
+
const internalPatterns = /^(RCT|RNS|Animated\\(|AnimatedComponent|VirtualizedList|CellRenderer|ScrollViewContext|PerformanceLoggerContext|RootTagContext|HeaderShownContext|HeaderHeightContext|HeaderBackContext|SafeAreaFrameContext|SafeAreaInsetsContext|VirtualizedListContext|VirtualizedListCellContextProvider|StaticContainer|DelayedFreeze|Freeze|Suspender|DebugContainer|MaybeNestedStack|SceneView|NavigationContent|PreventRemoveProvider|EnsureSingleNavigator)/;
|
|
75
|
+
|
|
76
|
+
// Screen component patterns - user's actual screens (strict matching)
|
|
77
|
+
// Only match *Screen and *Page to avoid false positives like BottomTabView
|
|
78
|
+
const screenPatterns = /^[A-Z][a-zA-Z0-9]*(Screen|Page)$/;
|
|
79
|
+
|
|
80
|
+
// Navigation/internal screen patterns to SKIP (these look like screens but are framework components)
|
|
81
|
+
const internalScreenPatterns = /^(MaybeScreen|Screen$|ScreenContainer|ScreenStack|SceneView|Background$)/;
|
|
82
|
+
|
|
83
|
+
// Provider/wrapper patterns to skip when finding focused screen
|
|
84
|
+
const wrapperPatterns = /^(App|AppContainer|Provider|Context|SafeArea|Gesture|Theme|Redux|Root|Navigator|Stack|Tab|Drawer|Navigation|Container|Wrapper|Layout|ErrorBoundary|Suspense|PersistGate|LinkingContext|AppState|View|Fragment|NativeStack|BottomTab|Screen$)/i;
|
|
85
|
+
|
|
86
|
+
// Global overlay patterns - stop traversing into these subtrees
|
|
87
|
+
// Be specific to avoid blocking BottomSheetDrawer, PortalProvider, etc.
|
|
88
|
+
const overlayPatterns = /^(BottomSheet$|BottomSheetGlobal|Modal$|Toast$|Snackbar$|Dialog$|Overlay$|Popup$|MyToast$|PaywallModal$|FullScreenBannerModal$)/i;
|
|
89
|
+
|
|
90
|
+
// Navigation container patterns - skip traversing into these (screens inside are nav screens, not focused content)
|
|
91
|
+
const navContainerPatterns = /^(RootNavigation|NativeStackNavigator|BottomTabNavigator|DrawerNavigator|TabNavigator|StackNavigator)/;
|
|
92
|
+
|
|
93
|
+
function getComponentName(fiber) {
|
|
94
|
+
if (!fiber || !fiber.type) return null;
|
|
95
|
+
if (typeof fiber.type === 'string') return fiber.type; // Host component (View, Text, etc.)
|
|
96
|
+
return fiber.type.displayName || fiber.type.name || null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function shouldHide(name) {
|
|
100
|
+
if (!hideInternals || !name) return false;
|
|
101
|
+
return internalPatterns.test(name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function extractLayoutStyles(style) {
|
|
105
|
+
try {
|
|
106
|
+
if (!style) return null;
|
|
107
|
+
const merged = Array.isArray(style)
|
|
108
|
+
? Object.assign({}, ...style.filter(Boolean).map(s => {
|
|
109
|
+
try { return typeof s === 'object' ? s : {}; }
|
|
110
|
+
catch { return {}; }
|
|
111
|
+
}))
|
|
112
|
+
: (typeof style === 'object' ? style : {});
|
|
113
|
+
|
|
114
|
+
const layout = {};
|
|
115
|
+
const layoutKeys = [
|
|
116
|
+
'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
117
|
+
'paddingHorizontal', 'paddingVertical',
|
|
118
|
+
'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
|
|
119
|
+
'marginHorizontal', 'marginVertical',
|
|
120
|
+
'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
|
|
121
|
+
'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink',
|
|
122
|
+
'justifyContent', 'alignItems', 'alignSelf', 'alignContent',
|
|
123
|
+
'position', 'top', 'bottom', 'left', 'right',
|
|
124
|
+
'gap', 'rowGap', 'columnGap',
|
|
125
|
+
'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth'
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
for (const key of layoutKeys) {
|
|
129
|
+
if (merged[key] !== undefined) layout[key] = merged[key];
|
|
130
|
+
}
|
|
131
|
+
return Object.keys(layout).length > 0 ? layout : null;
|
|
132
|
+
} catch { return null; }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function walkFiber(fiber, depth) {
|
|
136
|
+
if (!fiber || depth > maxDepth) return null;
|
|
137
|
+
|
|
138
|
+
const name = getComponentName(fiber);
|
|
139
|
+
|
|
140
|
+
// Skip anonymous/internal components unless they have meaningful children
|
|
141
|
+
if (!name || shouldHide(name)) {
|
|
142
|
+
// Still traverse children
|
|
143
|
+
let child = fiber.child;
|
|
144
|
+
const children = [];
|
|
145
|
+
while (child) {
|
|
146
|
+
const childResult = walkFiber(child, depth);
|
|
147
|
+
if (childResult) children.push(childResult);
|
|
148
|
+
child = child.sibling;
|
|
149
|
+
}
|
|
150
|
+
// Return first meaningful child or null
|
|
151
|
+
return children.length === 1 ? children[0] : (children.length > 1 ? { component: '(Fragment)', children } : null);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const node = { component: name };
|
|
155
|
+
|
|
156
|
+
// Include props if requested (excluding children and style for cleaner output)
|
|
157
|
+
if (includeProps && fiber.memoizedProps) {
|
|
158
|
+
const props = {};
|
|
159
|
+
for (const key of Object.keys(fiber.memoizedProps)) {
|
|
160
|
+
if (key === 'children' || key === 'style') continue;
|
|
161
|
+
try {
|
|
162
|
+
const val = fiber.memoizedProps[key];
|
|
163
|
+
if (typeof val === 'function') {
|
|
164
|
+
props[key] = '[Function]';
|
|
165
|
+
} else if (typeof val === 'object' && val !== null) {
|
|
166
|
+
props[key] = Array.isArray(val) ? '[Array]' : '[Object]';
|
|
167
|
+
} else {
|
|
168
|
+
props[key] = val;
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
props[key] = '[Animated Value]';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (Object.keys(props).length > 0) node.props = props;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Include layout styles if requested
|
|
178
|
+
try {
|
|
179
|
+
if (includeStyles && fiber.memoizedProps?.style) {
|
|
180
|
+
const layout = extractLayoutStyles(fiber.memoizedProps.style);
|
|
181
|
+
if (layout) node.layout = layout;
|
|
182
|
+
}
|
|
183
|
+
} catch { /* animated style — skip */ }
|
|
184
|
+
|
|
185
|
+
// Traverse children
|
|
186
|
+
let child = fiber.child;
|
|
187
|
+
const children = [];
|
|
188
|
+
while (child) {
|
|
189
|
+
const childResult = walkFiber(child, depth + 1);
|
|
190
|
+
if (childResult) children.push(childResult);
|
|
191
|
+
child = child.sibling;
|
|
192
|
+
}
|
|
193
|
+
if (children.length > 0) node.children = children;
|
|
194
|
+
|
|
195
|
+
return node;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Find focused screen if requested
|
|
199
|
+
function findFocusedScreen(fiber, depth = 0) {
|
|
200
|
+
if (!fiber || depth > 5000) return null;
|
|
201
|
+
|
|
202
|
+
const name = getComponentName(fiber);
|
|
203
|
+
|
|
204
|
+
// Skip overlays (BottomSheet, Modal, Toast, etc.) - don't traverse into them
|
|
205
|
+
if (name && overlayPatterns.test(name)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Skip navigation containers - screens inside are nav screens, not focused content
|
|
210
|
+
if (name && navContainerPatterns.test(name)) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check if this is a user's screen component (not framework internals)
|
|
215
|
+
if (name && screenPatterns.test(name) && !wrapperPatterns.test(name) && !internalScreenPatterns.test(name)) {
|
|
216
|
+
return fiber;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Search children
|
|
220
|
+
let child = fiber.child;
|
|
221
|
+
while (child) {
|
|
222
|
+
const found = findFocusedScreen(child, depth + 1);
|
|
223
|
+
if (found) return found;
|
|
224
|
+
child = child.sibling;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let startFiber = roots[0].current;
|
|
231
|
+
let focusedScreenName = null;
|
|
232
|
+
|
|
233
|
+
if (focusedOnly) {
|
|
234
|
+
const focused = findFocusedScreen(roots[0].current);
|
|
235
|
+
if (focused) {
|
|
236
|
+
startFiber = focused;
|
|
237
|
+
focusedScreenName = getComponentName(focused);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const tree = walkFiber(startFiber, 0);
|
|
242
|
+
|
|
243
|
+
if (focusedOnly && focusedScreenName) {
|
|
244
|
+
return { focusedScreen: focusedScreenName, tree };
|
|
245
|
+
}
|
|
246
|
+
return { tree };
|
|
247
|
+
})()
|
|
248
|
+
`;
|
|
249
|
+
// Use a longer timeout for component tree traversal — large apps can exceed 10s
|
|
250
|
+
const result = await executeInApp(expression, false, { timeoutMs: 30000 }, device);
|
|
251
|
+
// Apply formatting if requested
|
|
252
|
+
if (result.success && result.result) {
|
|
253
|
+
try {
|
|
254
|
+
const parsed = JSON.parse(result.result);
|
|
255
|
+
if (parsed.tree) {
|
|
256
|
+
const prefix = parsed.focusedScreen ? `Focused: ${parsed.focusedScreen}\n\n` : "";
|
|
257
|
+
// Structure-only mode: ultra-compact format with just component names
|
|
258
|
+
if (structureOnly) {
|
|
259
|
+
const structure = formatTreeStructureOnly(parsed.tree);
|
|
260
|
+
return { success: true, result: prefix + structure };
|
|
261
|
+
}
|
|
262
|
+
// TONL format: compact with props/layout
|
|
263
|
+
if (format === "tonl") {
|
|
264
|
+
const tonl = formatTreeToTonl(parsed.tree);
|
|
265
|
+
return { success: true, result: prefix + tonl };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// If parsing fails, return original result
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=componentTree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"componentTree.js","sourceRoot":"","sources":["../../src/core/componentTree.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,6 +23,7 @@ export declare function clearReconnectionSuppression(): void;
|
|
|
23
23
|
* Tries with Origin first, falls back to without on quick failure.
|
|
24
24
|
*/
|
|
25
25
|
export declare function createWebSocketWithOriginFallback(url: string, timeoutMs?: number): Promise<WebSocket>;
|
|
26
|
+
export declare function getWebSocketStateName(state: number): string;
|
|
26
27
|
/**
|
|
27
28
|
* Purge stale connections for scanned ports.
|
|
28
29
|
* When Metro restarts with a different app, /json returns new device IDs.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/core/connection.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,IAAI,CAAC;AAC3B,OAAO,EAAE,UAAU,EAAE,YAAY,EAAoB,YAAY,EAAkB,cAAc,EAAsB,sBAAsB,EAAmB,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAwF1M;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAI3C;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE/D;AAED;;GAEG;AACH,wBAAgB,4BAA4B,IAAI,IAAI,CAEnD;AAYD;;;;;;;GAOG;AACH,wBAAgB,iCAAiC,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CA6DnG;
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/core/connection.ts"],"names":[],"mappings":"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"}
|
package/build/core/connection.js
CHANGED
|
@@ -167,7 +167,7 @@ function findAppKeyForDevice(device) {
|
|
|
167
167
|
return null;
|
|
168
168
|
}
|
|
169
169
|
// Helper to convert WebSocket readyState to readable name
|
|
170
|
-
function getWebSocketStateName(state) {
|
|
170
|
+
export function getWebSocketStateName(state) {
|
|
171
171
|
switch (state) {
|
|
172
172
|
case WebSocket.CONNECTING: return "CONNECTING";
|
|
173
173
|
case WebSocket.OPEN: return "OPEN";
|
|
@@ -1132,8 +1132,11 @@ export function getConnectedAppByDevice(device) {
|
|
|
1132
1132
|
const names = matches.map(a => a.deviceInfo.deviceName || a.deviceInfo.title).join(", ");
|
|
1133
1133
|
throw new UserInputError(`Multiple devices match "${device}": ${names}. Be more specific (use the full device name for an exact match).`);
|
|
1134
1134
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1135
|
+
if (allOpenNames.length === 0) {
|
|
1136
|
+
throw new UserInputError(`No connected device matches "${device}". No devices are currently connected — run scan_metro to discover and connect to Metro servers.`);
|
|
1137
|
+
}
|
|
1138
|
+
const quoted = allOpenNames.map(n => `"${n}"`).join(", ");
|
|
1139
|
+
throw new UserInputError(`No connected device matches "${device}". Connected devices: ${quoted}. Retry with one of these names (substring match, case-insensitive).`);
|
|
1137
1140
|
}
|
|
1138
1141
|
// Check if any app is connected with an OPEN WebSocket
|
|
1139
1142
|
export function hasConnectedApp() {
|
|
@@ -1309,30 +1312,32 @@ async function findFirstMetroPort() {
|
|
|
1309
1312
|
*/
|
|
1310
1313
|
export async function ensureConnection(options = {}) {
|
|
1311
1314
|
const { port, healthCheck = true, forceRefresh = false } = options;
|
|
1312
|
-
let app = getFirstConnectedApp();
|
|
1313
1315
|
let wasReconnected = false;
|
|
1314
|
-
//
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1316
|
+
// forceRefresh: close every currently-connected app, then fall through to
|
|
1317
|
+
// single-device fresh connect. Full multi-device re-discovery is scan_metro's
|
|
1318
|
+
// job — ensureConnection only fresh-connects one when nothing is open.
|
|
1319
|
+
if (forceRefresh) {
|
|
1320
|
+
for (const { key, app } of getConnectedApps()) {
|
|
1321
|
+
cancelReconnectionTimer(key);
|
|
1322
|
+
try {
|
|
1323
|
+
app.ws.close();
|
|
1324
|
+
}
|
|
1325
|
+
catch { /* ignore */ }
|
|
1326
|
+
connectedApps.delete(key);
|
|
1323
1327
|
}
|
|
1324
|
-
connectedApps.delete(appKey);
|
|
1325
|
-
app = null;
|
|
1326
1328
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
+
let openApps = getConnectedApps()
|
|
1330
|
+
.filter(a => a.isConnected)
|
|
1331
|
+
.map(a => a.app);
|
|
1332
|
+
// Nothing connected — establish a single-device connection from scratch.
|
|
1333
|
+
if (openApps.length === 0) {
|
|
1329
1334
|
const targetPort = port ?? await findFirstMetroPort();
|
|
1330
1335
|
if (!targetPort) {
|
|
1331
1336
|
return {
|
|
1332
1337
|
connected: false,
|
|
1333
1338
|
wasReconnected: false,
|
|
1334
1339
|
healthCheckPassed: false,
|
|
1335
|
-
|
|
1340
|
+
connectionInfos: [],
|
|
1336
1341
|
error: "No Metro server found. Make sure Metro bundler is running.",
|
|
1337
1342
|
};
|
|
1338
1343
|
}
|
|
@@ -1343,7 +1348,7 @@ export async function ensureConnection(options = {}) {
|
|
|
1343
1348
|
connected: false,
|
|
1344
1349
|
wasReconnected: false,
|
|
1345
1350
|
healthCheckPassed: false,
|
|
1346
|
-
|
|
1351
|
+
connectionInfos: [],
|
|
1347
1352
|
error: `No debuggable devices found on port ${targetPort}. Make sure the app is running.`,
|
|
1348
1353
|
};
|
|
1349
1354
|
}
|
|
@@ -1355,32 +1360,23 @@ export async function ensureConnection(options = {}) {
|
|
|
1355
1360
|
clearReconnectionSuppression();
|
|
1356
1361
|
const connectResult = await connectToDevice(mainDevice, targetPort);
|
|
1357
1362
|
wasReconnected = true;
|
|
1358
|
-
// connectToDevice resolves with a status string for non-throw
|
|
1359
|
-
// outcomes including "Skipped X (stale CDP target …)". Surface
|
|
1360
|
-
// that explicitly instead of falling through to the generic
|
|
1361
|
-
// "Connection succeeded but app is not available" below.
|
|
1362
1363
|
if (connectResult.includes("stale CDP target")) {
|
|
1363
1364
|
return {
|
|
1364
1365
|
connected: false,
|
|
1365
1366
|
wasReconnected: false,
|
|
1366
1367
|
healthCheckPassed: false,
|
|
1367
|
-
|
|
1368
|
+
connectionInfos: [],
|
|
1368
1369
|
error: `${connectResult}. The CDP page advertised by Metro is no longer responsive — restart the React Native app, then retry.`,
|
|
1369
1370
|
};
|
|
1370
1371
|
}
|
|
1371
|
-
// Race window:
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
app = getFirstConnectedApp();
|
|
1377
|
-
for (let i = 0; !app && i < 5; i++) {
|
|
1378
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1379
|
-
app = getFirstConnectedApp();
|
|
1372
|
+
// Race window: settle briefly and retry getting the open app list.
|
|
1373
|
+
for (let i = 0; i < 5 && openApps.length === 0; i++) {
|
|
1374
|
+
if (i > 0)
|
|
1375
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1376
|
+
openApps = getConnectedApps().filter(a => a.isConnected).map(a => a.app);
|
|
1380
1377
|
}
|
|
1381
1378
|
}
|
|
1382
1379
|
catch (error) {
|
|
1383
|
-
// Ensure we always have a meaningful error message
|
|
1384
1380
|
let errorMessage;
|
|
1385
1381
|
if (error instanceof Error) {
|
|
1386
1382
|
errorMessage = error.message;
|
|
@@ -1395,82 +1391,95 @@ export async function ensureConnection(options = {}) {
|
|
|
1395
1391
|
connected: false,
|
|
1396
1392
|
wasReconnected: false,
|
|
1397
1393
|
healthCheckPassed: false,
|
|
1398
|
-
|
|
1394
|
+
connectionInfos: [],
|
|
1399
1395
|
error: `Connection failed: ${errorMessage}`,
|
|
1400
1396
|
};
|
|
1401
1397
|
}
|
|
1402
1398
|
}
|
|
1403
|
-
if (
|
|
1399
|
+
if (openApps.length === 0) {
|
|
1404
1400
|
return {
|
|
1405
1401
|
connected: false,
|
|
1406
1402
|
wasReconnected: false,
|
|
1407
1403
|
healthCheckPassed: false,
|
|
1408
|
-
|
|
1404
|
+
connectionInfos: [],
|
|
1409
1405
|
error: "Connection succeeded but app is not available",
|
|
1410
1406
|
};
|
|
1411
1407
|
}
|
|
1412
|
-
//
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
app.
|
|
1425
|
-
|
|
1426
|
-
catch {
|
|
1427
|
-
// Ignore
|
|
1428
|
-
}
|
|
1429
|
-
connectedApps.delete(appKey);
|
|
1430
|
-
// Re-fetch devices and reconnect
|
|
1431
|
-
const devices = await fetchDevices(targetPort);
|
|
1432
|
-
const mainDevice = selectMainDevice(devices);
|
|
1433
|
-
if (mainDevice) {
|
|
1408
|
+
// Per-device health-check; reconnect failed devices individually so a dead
|
|
1409
|
+
// Android doesn't take down a healthy iOS report and vice versa.
|
|
1410
|
+
const perApp = [];
|
|
1411
|
+
for (const candidate of openApps) {
|
|
1412
|
+
let app = candidate;
|
|
1413
|
+
let healthy = true;
|
|
1414
|
+
if (healthCheck) {
|
|
1415
|
+
healthy = await runQuickHealthCheck(app);
|
|
1416
|
+
if (!healthy && !wasReconnected) {
|
|
1417
|
+
const failedTitle = app.deviceInfo.title;
|
|
1418
|
+
console.error(`[execbro] Health check failed for ${failedTitle}, attempting reconnection...`);
|
|
1419
|
+
const appKey = `${app.port}-${app.deviceInfo.id}`;
|
|
1420
|
+
const appPort = app.port;
|
|
1421
|
+
cancelReconnectionTimer(appKey);
|
|
1434
1422
|
try {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1423
|
+
app.ws.close();
|
|
1424
|
+
}
|
|
1425
|
+
catch { /* ignore */ }
|
|
1426
|
+
connectedApps.delete(appKey);
|
|
1427
|
+
const devices = await fetchDevices(appPort);
|
|
1428
|
+
const mainDevice = selectMainDevice(devices);
|
|
1429
|
+
if (mainDevice) {
|
|
1430
|
+
try {
|
|
1431
|
+
await connectToDevice(mainDevice, appPort);
|
|
1432
|
+
wasReconnected = true;
|
|
1433
|
+
const fresh = getConnectedApps()
|
|
1434
|
+
.filter(a => a.isConnected)
|
|
1435
|
+
.map(a => a.app);
|
|
1436
|
+
const replacement = fresh.find(a => a.deviceInfo.id === mainDevice.id);
|
|
1437
|
+
if (replacement) {
|
|
1438
|
+
app = replacement;
|
|
1439
|
+
healthy = await runQuickHealthCheck(app);
|
|
1440
|
+
}
|
|
1441
|
+
else {
|
|
1442
|
+
healthy = false;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
catch {
|
|
1446
|
+
healthy = false;
|
|
1441
1447
|
}
|
|
1442
1448
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
healthCheckPassed = false;
|
|
1449
|
+
else {
|
|
1450
|
+
healthy = false;
|
|
1446
1451
|
}
|
|
1447
1452
|
}
|
|
1448
1453
|
}
|
|
1454
|
+
perApp.push({ app, healthy });
|
|
1449
1455
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
return {
|
|
1461
|
-
connected: isConnected,
|
|
1462
|
-
wasReconnected,
|
|
1463
|
-
healthCheckPassed,
|
|
1464
|
-
connectionInfo: app ? {
|
|
1456
|
+
const connectionInfos = perApp.map(({ app, healthy }) => {
|
|
1457
|
+
const appKey = `${app.port}-${app.deviceInfo.id}`;
|
|
1458
|
+
const connectionState = getConnectionState(appKey);
|
|
1459
|
+
const contextHealth = getContextHealth(appKey);
|
|
1460
|
+
let uptime = "unknown";
|
|
1461
|
+
if (connectionState?.lastConnectedTime) {
|
|
1462
|
+
uptime = formatDuration(Date.now() - connectionState.lastConnectedTime.getTime());
|
|
1463
|
+
}
|
|
1464
|
+
return {
|
|
1465
|
+
deviceName: app.deviceInfo.deviceName || app.deviceInfo.title,
|
|
1465
1466
|
deviceTitle: app.deviceInfo.title,
|
|
1467
|
+
platform: app.platform,
|
|
1466
1468
|
port: app.port,
|
|
1467
1469
|
uptime,
|
|
1468
1470
|
contextId: contextHealth?.contextId ?? null,
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1471
|
+
healthCheckPassed: healthy,
|
|
1472
|
+
};
|
|
1473
|
+
});
|
|
1474
|
+
const anyOpen = perApp.some(r => r.app.ws.readyState === WebSocket.OPEN);
|
|
1475
|
+
const allHealthy = perApp.every(r => r.healthy);
|
|
1476
|
+
return {
|
|
1477
|
+
connected: anyOpen,
|
|
1478
|
+
wasReconnected,
|
|
1479
|
+
healthCheckPassed: allHealthy,
|
|
1480
|
+
connectionInfos,
|
|
1481
|
+
...(!anyOpen && {
|
|
1482
|
+
error: "App became unavailable after reconnection attempt. Try running scan_metro then ensure_connection."
|
|
1474
1483
|
}),
|
|
1475
1484
|
};
|
|
1476
1485
|
}
|