react-native-ai-debugger 1.0.36 → 1.0.38
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 +3 -0
- package/build/core/executor.d.ts +8 -4
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +292 -144
- package/build/core/executor.js.map +1 -1
- package/build/core/types.d.ts +1 -0
- package/build/core/types.d.ts.map +1 -1
- package/build/index.js +84 -11
- package/build/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,9 @@ This repository includes pre-built [Claude Code skills](https://docs.anthropic.c
|
|
|
47
47
|
| `layout-check` | Verify UI layout against design specs using screenshots and component data |
|
|
48
48
|
| `device-interact` | Automate device interaction: tap, swipe, text input, and element finding |
|
|
49
49
|
| `bundle-check` | Detect and diagnose Metro bundler errors and compilation failures |
|
|
50
|
+
| `native-rebuild` | Rebuild and verify the app after installing native Expo packages |
|
|
51
|
+
|
|
52
|
+
See [`skills/overview.md`](./skills/overview.md) for a decision guide on which skill to use and a recommended workflow.
|
|
50
53
|
|
|
51
54
|
### Installing Skills (Claude Code)
|
|
52
55
|
|
package/build/core/executor.d.ts
CHANGED
|
@@ -65,11 +65,15 @@ export declare function isInspectorActive(): Promise<boolean>;
|
|
|
65
65
|
export declare function getInspectorSelection(): Promise<ExecutionResult>;
|
|
66
66
|
/**
|
|
67
67
|
* Inspect the React component at a specific (x, y) coordinate.
|
|
68
|
-
* Uses the same internal API as React Native's Element Inspector.
|
|
69
68
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
69
|
+
* Works on both Paper and Fabric (New Architecture). Uses a two-step approach
|
|
70
|
+
* because measureInWindow callbacks fire in a future native event loop tick
|
|
71
|
+
* (not microtasks), so awaitPromise cannot be used to collect them:
|
|
72
|
+
*
|
|
73
|
+
* Step 1 — dispatch: walk the fiber tree, call measureInWindow on each host
|
|
74
|
+
* component, store fiber refs and results in app globals.
|
|
75
|
+
* Step 2 — resolve (after 300ms): read the globals, hit-test against target
|
|
76
|
+
* coordinates, return the innermost matching React component.
|
|
73
77
|
*/
|
|
74
78
|
export declare function inspectAtPoint(x: number, y: number, options?: {
|
|
75
79
|
includeProps?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA8R7D,wBAAsB,YAAY,CAC9B,UAAU,EAAE,MAAM,EAClB,YAAY,GAAE,OAAc,EAC5B,OAAO,GAAE,cAAmB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAoF1B;AAGD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,eAAe,CAAC,CAkBjE;AAGD,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAsBhF;AAKD,wBAAsB,SAAS,IAAI,OAAO,CAAC,eAAe,CAAC,CAsI1D;AAgHD;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,GAAE;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,eAAe,CAAC,CAoOhC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,OAAO,GAAE;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkLhC;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,GAAE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkNhC;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,eAAe,CAAC,CA6IhC;AAMD;;;GAGG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,eAAe,CAAC,CAqBvE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAqC1D;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,eAAe,CAAC,CA2EtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE;IAChE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,OAAO,CAAC,eAAe,CAAC,CAyMhC"}
|
package/build/core/executor.js
CHANGED
|
@@ -85,6 +85,58 @@ function validateAndPreprocessExpression(expression) {
|
|
|
85
85
|
expression: cleaned
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Detect if an expression contains multiple statements or declarations.
|
|
90
|
+
* These cannot be wrapped with `return (expr)` — they need block wrapping.
|
|
91
|
+
*
|
|
92
|
+
* Examples:
|
|
93
|
+
* - "console.log('x'); 'result'" → multi-statement (semicolon between statements)
|
|
94
|
+
* - "var x = 1; btoa(x)" → declaration + multi-statement
|
|
95
|
+
* - "var x = 1" → declaration
|
|
96
|
+
* - "JSON.stringify(obj)" → single expression (NOT multi-statement)
|
|
97
|
+
* - "JSON.stringify({a: 'x;y'})" → single expression with semicolon in string
|
|
98
|
+
*/
|
|
99
|
+
function isMultiStatementExpression(expr) {
|
|
100
|
+
const trimmed = expr.trim();
|
|
101
|
+
// Starts with a declaration or statement keyword
|
|
102
|
+
if (/^(var|let|const|function|class|for|while|if|switch|try|do|throw)\b/.test(trimmed)) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
// Check for semicolons that separate statements (not trailing, not inside strings)
|
|
106
|
+
// Remove the trailing semicolon and whitespace first
|
|
107
|
+
const withoutTrailing = trimmed.replace(/;\s*$/, '');
|
|
108
|
+
// Simple heuristic: check if there's a semicolon that's likely between statements
|
|
109
|
+
// Skip semicolons inside string literals by tracking quote state
|
|
110
|
+
let inSingle = false;
|
|
111
|
+
let inDouble = false;
|
|
112
|
+
let inTemplate = false;
|
|
113
|
+
let escaped = false;
|
|
114
|
+
for (let i = 0; i < withoutTrailing.length; i++) {
|
|
115
|
+
const ch = withoutTrailing[i];
|
|
116
|
+
if (escaped) {
|
|
117
|
+
escaped = false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (ch === '\\') {
|
|
121
|
+
escaped = true;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (ch === "'" && !inDouble && !inTemplate) {
|
|
125
|
+
inSingle = !inSingle;
|
|
126
|
+
}
|
|
127
|
+
else if (ch === '"' && !inSingle && !inTemplate) {
|
|
128
|
+
inDouble = !inDouble;
|
|
129
|
+
}
|
|
130
|
+
else if (ch === '`' && !inSingle && !inDouble) {
|
|
131
|
+
inTemplate = !inTemplate;
|
|
132
|
+
}
|
|
133
|
+
else if (ch === ';' && !inSingle && !inDouble && !inTemplate) {
|
|
134
|
+
// Found a semicolon outside of strings — multi-statement
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
88
140
|
// Error patterns that indicate a stale/destroyed context
|
|
89
141
|
const CONTEXT_ERROR_PATTERNS = [
|
|
90
142
|
"cannot find context",
|
|
@@ -94,6 +146,7 @@ const CONTEXT_ERROR_PATTERNS = [
|
|
|
94
146
|
"session closed",
|
|
95
147
|
"context with specified id",
|
|
96
148
|
"no execution context",
|
|
149
|
+
"runningdetached",
|
|
97
150
|
];
|
|
98
151
|
/**
|
|
99
152
|
* Check if an error indicates a stale page context
|
|
@@ -133,7 +186,7 @@ async function attemptQuickReconnect(preferredPort) {
|
|
|
133
186
|
/**
|
|
134
187
|
* Execute expression on a connected app (core implementation without retry)
|
|
135
188
|
*/
|
|
136
|
-
async function executeExpressionCore(expression, awaitPromise) {
|
|
189
|
+
async function executeExpressionCore(expression, awaitPromise, timeoutMs = 10000) {
|
|
137
190
|
const app = getFirstConnectedApp();
|
|
138
191
|
if (!app) {
|
|
139
192
|
return { success: false, error: "No apps connected. Run 'scan_metro' first." };
|
|
@@ -147,10 +200,14 @@ async function executeExpressionCore(expression, awaitPromise) {
|
|
|
147
200
|
return { success: false, error: validation.error };
|
|
148
201
|
}
|
|
149
202
|
const cleanedExpression = validation.expression;
|
|
150
|
-
const TIMEOUT_MS =
|
|
203
|
+
const TIMEOUT_MS = timeoutMs;
|
|
151
204
|
const currentMessageId = getNextMessageId();
|
|
152
205
|
// Wrap expression with global polyfill for Hermes compatibility
|
|
153
|
-
|
|
206
|
+
// Multi-statement expressions (var x = 1; foo(x)) can't use `return (expr)` wrapping
|
|
207
|
+
// because declarations and semicolons are invalid inside parenthesized expressions
|
|
208
|
+
const wrappedExpression = isMultiStatementExpression(cleanedExpression)
|
|
209
|
+
? `(function() { ${GLOBAL_POLYFILL} ${cleanedExpression} })()`
|
|
210
|
+
: `(function() { ${GLOBAL_POLYFILL} return (${cleanedExpression}); })()`;
|
|
154
211
|
return new Promise((resolve) => {
|
|
155
212
|
const timeoutId = setTimeout(() => {
|
|
156
213
|
pendingExecutions.delete(currentMessageId);
|
|
@@ -182,7 +239,7 @@ async function executeExpressionCore(expression, awaitPromise) {
|
|
|
182
239
|
}
|
|
183
240
|
// Execute JavaScript in the connected React Native app with retry logic
|
|
184
241
|
export async function executeInApp(expression, awaitPromise = true, options = {}) {
|
|
185
|
-
const { maxRetries = 2, retryDelayMs = 1000, autoReconnect = true } = options;
|
|
242
|
+
const { maxRetries = 2, retryDelayMs = 1000, autoReconnect = true, timeoutMs = 10000 } = options;
|
|
186
243
|
let lastError;
|
|
187
244
|
let preferredPort;
|
|
188
245
|
// Get preferred port from current connection if available
|
|
@@ -225,7 +282,7 @@ export async function executeInApp(expression, awaitPromise = true, options = {}
|
|
|
225
282
|
return { success: false, error: "WebSocket connection is not open." };
|
|
226
283
|
}
|
|
227
284
|
// Execute the expression
|
|
228
|
-
const result = await executeExpressionCore(expression, awaitPromise);
|
|
285
|
+
const result = await executeExpressionCore(expression, awaitPromise, timeoutMs);
|
|
229
286
|
// Success - return result
|
|
230
287
|
if (result.success) {
|
|
231
288
|
return result;
|
|
@@ -301,6 +358,8 @@ export async function inspectGlobal(objectName) {
|
|
|
301
358
|
return executeInApp(expression, false);
|
|
302
359
|
}
|
|
303
360
|
// Reload the React Native app using __ReactRefresh (Page.reload is not supported by Hermes)
|
|
361
|
+
// Uses fire-and-forget: sends the reload command without waiting for a response,
|
|
362
|
+
// since the JS context is destroyed during reload and would always timeout.
|
|
304
363
|
export async function reloadApp() {
|
|
305
364
|
// Get current connection info before reload
|
|
306
365
|
let app = getFirstConnectedApp();
|
|
@@ -340,35 +399,48 @@ export async function reloadApp() {
|
|
|
340
399
|
}
|
|
341
400
|
}
|
|
342
401
|
const port = app.port;
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
__ReactRefresh.performFullRefresh('mcp-reload');
|
|
351
|
-
return 'Reload triggered via __ReactRefresh.performFullRefresh';
|
|
352
|
-
}
|
|
353
|
-
// Fallback: Try DevSettings if available on global
|
|
354
|
-
if (typeof global !== 'undefined' && global.DevSettings && typeof global.DevSettings.reload === 'function') {
|
|
355
|
-
global.DevSettings.reload();
|
|
356
|
-
return 'Reload triggered via DevSettings';
|
|
357
|
-
}
|
|
358
|
-
return 'Reload not available - make sure app is in development mode with Metro bundler';
|
|
359
|
-
} catch (e) {
|
|
360
|
-
return 'Reload failed: ' + e.message;
|
|
402
|
+
// Fire-and-forget: send reload command via CDP without waiting for response.
|
|
403
|
+
// The JS context is destroyed during reload, so Runtime.evaluate would always timeout.
|
|
404
|
+
const reloadExpression = `(function() {
|
|
405
|
+
try {
|
|
406
|
+
if (typeof __ReactRefresh !== 'undefined' && typeof __ReactRefresh.performFullRefresh === 'function') {
|
|
407
|
+
__ReactRefresh.performFullRefresh('mcp-reload');
|
|
408
|
+
return 'ok';
|
|
361
409
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
410
|
+
if (typeof global !== 'undefined' && global.DevSettings && typeof global.DevSettings.reload === 'function') {
|
|
411
|
+
global.DevSettings.reload();
|
|
412
|
+
return 'ok';
|
|
413
|
+
}
|
|
414
|
+
return 'no-method';
|
|
415
|
+
} catch (e) { return 'error:' + e.message; }
|
|
416
|
+
})()`;
|
|
417
|
+
try {
|
|
418
|
+
if (app.ws.readyState !== WebSocket.OPEN) {
|
|
419
|
+
return { success: false, error: "WebSocket connection is not open." };
|
|
420
|
+
}
|
|
421
|
+
// Send without registering a pending execution — fire and forget
|
|
422
|
+
const messageId = getNextMessageId();
|
|
423
|
+
app.ws.send(JSON.stringify({
|
|
424
|
+
id: messageId,
|
|
425
|
+
method: "Runtime.evaluate",
|
|
426
|
+
params: {
|
|
427
|
+
expression: reloadExpression,
|
|
428
|
+
returnByValue: true,
|
|
429
|
+
awaitPromise: false,
|
|
430
|
+
userGesture: true,
|
|
431
|
+
},
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
return {
|
|
436
|
+
success: false,
|
|
437
|
+
error: `Failed to send reload command: ${error instanceof Error ? error.message : String(error)}`
|
|
438
|
+
};
|
|
367
439
|
}
|
|
368
440
|
// Auto-reconnect after reload
|
|
369
441
|
try {
|
|
370
442
|
// Wait for app to reload (give it time to restart JS context)
|
|
371
|
-
await
|
|
443
|
+
await delay(2000);
|
|
372
444
|
// Close existing connections to this port and cancel any pending auto-reconnections
|
|
373
445
|
// This prevents the dual-reconnection bug where both auto-reconnect and manual reconnect compete
|
|
374
446
|
for (const [key, connectedApp] of connectedApps.entries()) {
|
|
@@ -385,7 +457,7 @@ export async function reloadApp() {
|
|
|
385
457
|
}
|
|
386
458
|
}
|
|
387
459
|
// Small delay to ensure cleanup
|
|
388
|
-
await
|
|
460
|
+
await delay(500);
|
|
389
461
|
// Reconnect to Metro on the same port with auto-reconnection DISABLED
|
|
390
462
|
// We're doing a manual reconnection here, so we don't want the auto-reconnect
|
|
391
463
|
// system to also try reconnecting and compete with us
|
|
@@ -679,7 +751,8 @@ export async function getComponentTree(options = {}) {
|
|
|
679
751
|
return { tree };
|
|
680
752
|
})()
|
|
681
753
|
`;
|
|
682
|
-
|
|
754
|
+
// Use a longer timeout for component tree traversal — large apps can exceed 10s
|
|
755
|
+
const result = await executeInApp(expression, false, { timeoutMs: 30000 });
|
|
683
756
|
// Apply formatting if requested
|
|
684
757
|
if (result.success && result.result) {
|
|
685
758
|
try {
|
|
@@ -862,7 +935,8 @@ export async function getScreenLayout(options = {}) {
|
|
|
862
935
|
};
|
|
863
936
|
})()
|
|
864
937
|
`;
|
|
865
|
-
|
|
938
|
+
// Use a longer timeout for layout traversal — large component trees can exceed 10s
|
|
939
|
+
const result = await executeInApp(expression, false, { timeoutMs: 30000 });
|
|
866
940
|
// Apply TONL formatting if requested
|
|
867
941
|
if (format === 'tonl' && result.success && result.result) {
|
|
868
942
|
try {
|
|
@@ -1389,139 +1463,213 @@ export async function getInspectorSelection() {
|
|
|
1389
1463
|
}
|
|
1390
1464
|
/**
|
|
1391
1465
|
* Inspect the React component at a specific (x, y) coordinate.
|
|
1392
|
-
* Uses the same internal API as React Native's Element Inspector.
|
|
1393
1466
|
*
|
|
1394
|
-
*
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1467
|
+
* Works on both Paper and Fabric (New Architecture). Uses a two-step approach
|
|
1468
|
+
* because measureInWindow callbacks fire in a future native event loop tick
|
|
1469
|
+
* (not microtasks), so awaitPromise cannot be used to collect them:
|
|
1470
|
+
*
|
|
1471
|
+
* Step 1 — dispatch: walk the fiber tree, call measureInWindow on each host
|
|
1472
|
+
* component, store fiber refs and results in app globals.
|
|
1473
|
+
* Step 2 — resolve (after 300ms): read the globals, hit-test against target
|
|
1474
|
+
* coordinates, return the innermost matching React component.
|
|
1397
1475
|
*/
|
|
1398
1476
|
export async function inspectAtPoint(x, y, options = {}) {
|
|
1399
1477
|
const { includeProps = true, includeFrame = true } = options;
|
|
1400
|
-
|
|
1478
|
+
// --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
|
|
1479
|
+
const dispatchExpression = `
|
|
1401
1480
|
(function() {
|
|
1402
|
-
|
|
1481
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1403
1482
|
if (!hook) return { error: 'React DevTools hook not available. Make sure you are running a development build.' };
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1483
|
+
|
|
1484
|
+
var roots = [];
|
|
1485
|
+
if (hook.getFiberRoots) {
|
|
1486
|
+
try { roots = Array.from(hook.getFiberRoots(1) || []); } catch(e) {}
|
|
1487
|
+
}
|
|
1488
|
+
if (roots.length === 0 && hook.renderers) {
|
|
1489
|
+
for (var entry of hook.renderers) {
|
|
1490
|
+
try {
|
|
1491
|
+
var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
|
|
1492
|
+
if (r.length > 0) { roots = r; break; }
|
|
1493
|
+
} catch(e) {}
|
|
1414
1494
|
}
|
|
1415
1495
|
}
|
|
1496
|
+
if (roots.length === 0) return { error: 'No fiber roots found. The app may not have rendered yet.' };
|
|
1416
1497
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
};
|
|
1498
|
+
// Paper: measureInWindow is on stateNode directly.
|
|
1499
|
+
// Fabric: measureInWindow is on stateNode.canonical.publicInstance.
|
|
1500
|
+
function getMeasurable(fiber) {
|
|
1501
|
+
var sn = fiber.stateNode;
|
|
1502
|
+
if (!sn) return null;
|
|
1503
|
+
if (typeof sn.measureInWindow === 'function') return sn;
|
|
1504
|
+
if (sn.canonical && sn.canonical.publicInstance &&
|
|
1505
|
+
typeof sn.canonical.publicInstance.measureInWindow === 'function') {
|
|
1506
|
+
return sn.canonical.publicInstance;
|
|
1507
|
+
}
|
|
1508
|
+
return null;
|
|
1429
1509
|
}
|
|
1430
1510
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1511
|
+
var hostFibers = [];
|
|
1512
|
+
function walkFibers(fiber, depth) {
|
|
1513
|
+
var cur = fiber;
|
|
1514
|
+
while (cur) {
|
|
1515
|
+
if (hostFibers.length >= 500) return;
|
|
1516
|
+
if (typeof cur.type === 'string' && getMeasurable(cur)) hostFibers.push(cur);
|
|
1517
|
+
if (cur.child && depth < 250) walkFibers(cur.child, depth + 1);
|
|
1518
|
+
cur = cur.sibling;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
for (var root of roots) { walkFibers(root.current, 0); }
|
|
1522
|
+
|
|
1523
|
+
if (hostFibers.length === 0) return { error: 'No measurable host components found. App may not be fully rendered.' };
|
|
1524
|
+
|
|
1525
|
+
globalThis.__inspectFibers = hostFibers;
|
|
1526
|
+
globalThis.__inspectMeasurements = new Array(hostFibers.length).fill(null);
|
|
1527
|
+
|
|
1528
|
+
hostFibers.forEach(function(fiber, i) {
|
|
1434
1529
|
try {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
if (!fiber) {
|
|
1441
|
-
resolve({
|
|
1442
|
-
point: { x: ${x}, y: ${y} },
|
|
1443
|
-
error: 'No component found at this point. The coordinates may be outside the app bounds or on a native-only element.'
|
|
1444
|
-
});
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1530
|
+
getMeasurable(fiber).measureInWindow(function(fx, fy, fw, fh) {
|
|
1531
|
+
globalThis.__inspectMeasurements[i] = { x: fx, y: fy, width: fw, height: fh };
|
|
1532
|
+
});
|
|
1533
|
+
} catch(e) {}
|
|
1534
|
+
});
|
|
1447
1535
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1536
|
+
return { count: hostFibers.length };
|
|
1537
|
+
})()
|
|
1538
|
+
`;
|
|
1539
|
+
const dispatchResult = await executeInApp(dispatchExpression, false);
|
|
1540
|
+
if (!dispatchResult.success)
|
|
1541
|
+
return dispatchResult;
|
|
1542
|
+
try {
|
|
1543
|
+
const parsed = JSON.parse(dispatchResult.result || '{}');
|
|
1544
|
+
if (parsed.error)
|
|
1545
|
+
return { success: false, error: parsed.error };
|
|
1546
|
+
}
|
|
1547
|
+
catch { /* ignore parse errors */ }
|
|
1548
|
+
// Wait for native measureInWindow callbacks to fire
|
|
1549
|
+
await delay(300);
|
|
1550
|
+
// --- Step 2: read measurements, hit-test, return result ---
|
|
1551
|
+
const resolveExpression = `
|
|
1552
|
+
(function() {
|
|
1553
|
+
var fibers = globalThis.__inspectFibers;
|
|
1554
|
+
var measurements = globalThis.__inspectMeasurements;
|
|
1555
|
+
globalThis.__inspectFibers = null;
|
|
1556
|
+
globalThis.__inspectMeasurements = null;
|
|
1557
|
+
|
|
1558
|
+
if (!fibers || !measurements) return { error: 'No measurement data available. Run inspect_at_point again.' };
|
|
1559
|
+
|
|
1560
|
+
var targetX = ${x};
|
|
1561
|
+
var targetY = ${y};
|
|
1562
|
+
|
|
1563
|
+
var hits = [];
|
|
1564
|
+
for (var i = 0; i < measurements.length; i++) {
|
|
1565
|
+
var m = measurements[i];
|
|
1566
|
+
if (m && m.width > 0 && m.height > 0 &&
|
|
1567
|
+
targetX >= m.x && targetX <= m.x + m.width &&
|
|
1568
|
+
targetY >= m.y && targetY <= m.y + m.height) {
|
|
1569
|
+
hits.push({ fiber: fibers[i], x: m.x, y: m.y, width: m.width, height: m.height });
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1458
1572
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
path: hierarchy.join(' > ')
|
|
1463
|
-
};
|
|
1464
|
-
|
|
1465
|
-
// Include props if requested (exclude children and functions for cleaner output)
|
|
1466
|
-
if (${includeProps} && viewData?.props) {
|
|
1467
|
-
const props = {};
|
|
1468
|
-
for (const key of Object.keys(viewData.props)) {
|
|
1469
|
-
if (key === 'children') continue;
|
|
1470
|
-
const val = viewData.props[key];
|
|
1471
|
-
if (typeof val === 'function') {
|
|
1472
|
-
props[key] = '[Function]';
|
|
1473
|
-
} else if (typeof val === 'object' && val !== null) {
|
|
1474
|
-
// Shallow serialize objects
|
|
1475
|
-
try {
|
|
1476
|
-
const str = JSON.stringify(val);
|
|
1477
|
-
if (str.length > 200) {
|
|
1478
|
-
props[key] = Array.isArray(val) ? '[Array(' + val.length + ')]' : '[Object]';
|
|
1479
|
-
} else {
|
|
1480
|
-
props[key] = val;
|
|
1481
|
-
}
|
|
1482
|
-
} catch {
|
|
1483
|
-
props[key] = Array.isArray(val) ? '[Array]' : '[Object]';
|
|
1484
|
-
}
|
|
1485
|
-
} else {
|
|
1486
|
-
props[key] = val;
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
if (Object.keys(props).length > 0) result.props = props;
|
|
1490
|
-
}
|
|
1573
|
+
if (hits.length === 0) {
|
|
1574
|
+
return { point: { x: targetX, y: targetY }, error: 'No component found at this point. Coordinates may be outside the app bounds or over a native-only element.' };
|
|
1575
|
+
}
|
|
1491
1576
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1577
|
+
// Smallest area = innermost (most specific) component
|
|
1578
|
+
hits.sort(function(a, b) { return (a.width * a.height) - (b.width * b.height); });
|
|
1579
|
+
var best = hits[0];
|
|
1580
|
+
|
|
1581
|
+
// RN primitives and internal components to skip when surfacing the "element" name.
|
|
1582
|
+
// We want the nearest *custom* component, not a library wrapper.
|
|
1583
|
+
var RN_PRIMITIVES = /^(View|Text|Image|ScrollView|FlatList|SectionList|TextInput|TouchableOpacity|TouchableHighlight|TouchableNativeFeedback|TouchableWithoutFeedback|Pressable|Button|Switch|ActivityIndicator|Modal|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)$/;
|
|
1584
|
+
|
|
1585
|
+
function getNearestNamed(fiber, skipPrimitives) {
|
|
1586
|
+
var cur = fiber;
|
|
1587
|
+
var fallback = null;
|
|
1588
|
+
while (cur) {
|
|
1589
|
+
if (cur.type && typeof cur.type !== 'string') {
|
|
1590
|
+
var name = cur.type.displayName || cur.type.name;
|
|
1591
|
+
if (name) {
|
|
1592
|
+
if (!fallback) fallback = { name: name, fiber: cur };
|
|
1593
|
+
if (!skipPrimitives || !RN_PRIMITIVES.test(name)) {
|
|
1594
|
+
return { name: name, fiber: cur };
|
|
1503
1595
|
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
cur = cur.return;
|
|
1599
|
+
}
|
|
1600
|
+
return fallback;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function buildPath(fiber) {
|
|
1604
|
+
var path = [];
|
|
1605
|
+
var cur = fiber;
|
|
1606
|
+
while (cur) {
|
|
1607
|
+
if (cur.type) {
|
|
1608
|
+
var n = typeof cur.type === 'string'
|
|
1609
|
+
? cur.type
|
|
1610
|
+
: (cur.type.displayName || cur.type.name);
|
|
1611
|
+
if (n) path.unshift(n);
|
|
1612
|
+
}
|
|
1613
|
+
cur = cur.return;
|
|
1614
|
+
}
|
|
1615
|
+
return path.slice(-8).join(' > ');
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// Find nearest custom component (skipping RN primitives) for the element name,
|
|
1619
|
+
// but fall back to the nearest named component if nothing custom is found.
|
|
1620
|
+
var named = getNearestNamed(best.fiber.return || best.fiber, true);
|
|
1621
|
+
var result = {
|
|
1622
|
+
point: { x: targetX, y: targetY },
|
|
1623
|
+
element: named ? named.name : best.fiber.type,
|
|
1624
|
+
nativeElement: best.fiber.type,
|
|
1625
|
+
path: buildPath(best.fiber)
|
|
1626
|
+
};
|
|
1504
1627
|
|
|
1505
|
-
|
|
1628
|
+
if (${includeFrame}) {
|
|
1629
|
+
result.frame = { x: best.x, y: best.y, width: best.width, height: best.height };
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
if (${includeProps} && named && named.fiber.memoizedProps) {
|
|
1633
|
+
var props = {};
|
|
1634
|
+
var keys = Object.keys(named.fiber.memoizedProps);
|
|
1635
|
+
for (var i = 0; i < keys.length; i++) {
|
|
1636
|
+
var key = keys[i];
|
|
1637
|
+
if (key === 'children') continue;
|
|
1638
|
+
var val = named.fiber.memoizedProps[key];
|
|
1639
|
+
if (typeof val === 'function') {
|
|
1640
|
+
props[key] = '[Function]';
|
|
1641
|
+
} else if (typeof val === 'object' && val !== null) {
|
|
1642
|
+
try {
|
|
1643
|
+
var str = JSON.stringify(val);
|
|
1644
|
+
props[key] = str.length > 200
|
|
1645
|
+
? (Array.isArray(val) ? '[Array(' + val.length + ')]' : '[Object]')
|
|
1646
|
+
: val;
|
|
1647
|
+
} catch(e) {
|
|
1648
|
+
props[key] = '[Object]';
|
|
1506
1649
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1650
|
+
} else {
|
|
1651
|
+
props[key] = val;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
if (Object.keys(props).length > 0) result.props = props;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// Hierarchy: custom-named component for each hit, deduped, innermost→outermost
|
|
1658
|
+
var hierarchy = [];
|
|
1659
|
+
for (var j = 0; j < Math.min(hits.length, 15); j++) {
|
|
1660
|
+
var n2 = getNearestNamed(hits[j].fiber.return, true) || getNearestNamed(hits[j].fiber, true);
|
|
1661
|
+
if (n2 && !hierarchy.some(function(h) { return h.name === n2.name; })) {
|
|
1662
|
+
hierarchy.push({
|
|
1663
|
+
name: n2.name,
|
|
1664
|
+
frame: { x: hits[j].x, y: hits[j].y, width: hits[j].width, height: hits[j].height }
|
|
1520
1665
|
});
|
|
1521
1666
|
}
|
|
1522
|
-
}
|
|
1667
|
+
}
|
|
1668
|
+
if (hierarchy.length > 1) result.hierarchy = hierarchy;
|
|
1669
|
+
|
|
1670
|
+
return result;
|
|
1523
1671
|
})()
|
|
1524
1672
|
`;
|
|
1525
|
-
return executeInApp(
|
|
1673
|
+
return executeInApp(resolveExpression, false);
|
|
1526
1674
|
}
|
|
1527
1675
|
//# sourceMappingURL=executor.js.map
|