react-native-ai-debugger 1.0.30 → 1.0.32
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 +49 -18
- package/build/core/httpServer.d.ts.map +1 -1
- package/build/core/httpServer.js +1105 -15
- package/build/core/httpServer.js.map +1 -1
- package/build/core/state.d.ts.map +1 -1
- package/build/core/state.js +2 -2
- package/build/core/state.js.map +1 -1
- package/build/index.js +17 -38
- package/build/index.js.map +1 -1
- package/package.json +1 -1
package/build/core/httpServer.js
CHANGED
|
@@ -3,6 +3,8 @@ import { logBuffer, networkBuffer, bundleErrorBuffer, connectedApps } from "./st
|
|
|
3
3
|
import { listAndroidDevices, androidScreenshot, androidGetScreenSize, androidTap } from "./android.js";
|
|
4
4
|
import { listIOSSimulators, iosScreenshot, iosTap } from "./ios.js";
|
|
5
5
|
import { recognizeText, inferIOSDevicePixelRatio } from "./ocr.js";
|
|
6
|
+
import { getAllConnectionStates, getContextHealth } from "./connectionState.js";
|
|
7
|
+
import { executeInApp, getComponentTree, listDebugGlobals, inspectGlobal } from "./executor.js";
|
|
6
8
|
const DEFAULT_HTTP_PORT = 3456;
|
|
7
9
|
const MAX_PORT_ATTEMPTS = 20;
|
|
8
10
|
// Store the active port for querying via MCP tool
|
|
@@ -181,7 +183,11 @@ function htmlTemplate(title, content, refreshInterval = 3000) {
|
|
|
181
183
|
<a href="/" ${title === 'Dashboard' ? 'class="active"' : ''}>Dashboard</a>
|
|
182
184
|
<a href="/logs" ${title === 'Logs' ? 'class="active"' : ''}>Logs</a>
|
|
183
185
|
<a href="/network" ${title === 'Network' ? 'class="active"' : ''}>Network</a>
|
|
186
|
+
<a href="/bundle-errors" ${title === 'Bundle Errors' ? 'class="active"' : ''}>Errors</a>
|
|
184
187
|
<a href="/apps" ${title === 'Apps' ? 'class="active"' : ''}>Apps</a>
|
|
188
|
+
<a href="/repl" ${title === 'REPL' ? 'class="active"' : ''}>REPL</a>
|
|
189
|
+
<a href="/component-tree" ${title === 'Component Tree' ? 'class="active"' : ''}>Components</a>
|
|
190
|
+
<a href="/globals" ${title === 'Globals' ? 'class="active"' : ''}>Globals</a>
|
|
185
191
|
<a href="/tap-verifier" ${title === 'Tap Verifier' ? 'class="active"' : ''}>Tap Verifier</a>
|
|
186
192
|
</nav>
|
|
187
193
|
<div id="content">${content}</div>
|
|
@@ -470,21 +476,996 @@ function renderApps() {
|
|
|
470
476
|
if (apps.length === 0) {
|
|
471
477
|
return htmlTemplate('Apps', '<div class="empty">No apps connected. Use scan_metro to connect to a running Metro server.</div>');
|
|
472
478
|
}
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
<
|
|
479
|
+
const connectionStates = getAllConnectionStates();
|
|
480
|
+
const appsHtml = apps.map(app => {
|
|
481
|
+
const state = connectionStates.get(app.id);
|
|
482
|
+
const health = getContextHealth(app.id);
|
|
483
|
+
let uptimeStr = '-';
|
|
484
|
+
if (state?.lastConnectedTime) {
|
|
485
|
+
const uptimeMs = Date.now() - state.lastConnectedTime.getTime();
|
|
486
|
+
const uptimeSec = Math.floor(uptimeMs / 1000);
|
|
487
|
+
if (uptimeSec < 60)
|
|
488
|
+
uptimeStr = `${uptimeSec}s`;
|
|
489
|
+
else if (uptimeSec < 3600)
|
|
490
|
+
uptimeStr = `${Math.floor(uptimeSec / 60)}m ${uptimeSec % 60}s`;
|
|
491
|
+
else
|
|
492
|
+
uptimeStr = `${Math.floor(uptimeSec / 3600)}h ${Math.floor((uptimeSec % 3600) / 60)}m`;
|
|
493
|
+
}
|
|
494
|
+
const healthStatus = health?.isStale ? 'stale' : 'healthy';
|
|
495
|
+
const healthClass = health?.isStale ? 'health-stale' : 'health-ok';
|
|
496
|
+
// Get gaps for this specific app
|
|
497
|
+
const appGaps = state?.connectionGaps || [];
|
|
498
|
+
const recentAppGaps = appGaps.slice(-3);
|
|
499
|
+
const gapsHtml = recentAppGaps.length > 0 ? `
|
|
500
|
+
<div class="app-detail" style="margin-top: 8px;">
|
|
501
|
+
<strong>Recent Gaps:</strong>
|
|
502
|
+
${recentAppGaps.map(gap => {
|
|
503
|
+
const duration = gap.durationMs ? `${Math.round(gap.durationMs / 1000)}s` : 'ongoing';
|
|
504
|
+
return `<div style="color: #d29922; font-size: 12px; margin-left: 8px;">• ${escapeHtml(gap.reason)} (${duration})</div>`;
|
|
505
|
+
}).join('')}
|
|
506
|
+
</div>
|
|
507
|
+
` : '';
|
|
508
|
+
return `
|
|
509
|
+
<div class="app-card">
|
|
510
|
+
<h3>${escapeHtml(app.deviceInfo.title)}</h3>
|
|
511
|
+
<span class="app-status ${app.connected ? 'connected' : 'disconnected'}">
|
|
512
|
+
${app.connected ? 'Connected' : 'Disconnected'}
|
|
513
|
+
</span>
|
|
514
|
+
<span class="app-status ${healthClass}" style="margin-left: 8px;">
|
|
515
|
+
Context: ${healthStatus}
|
|
516
|
+
</span>
|
|
517
|
+
<div class="app-detail">Device: ${escapeHtml(app.deviceInfo.deviceName)}</div>
|
|
518
|
+
<div class="app-detail">Metro Port: ${app.port}</div>
|
|
519
|
+
<div class="app-detail">Uptime: ${uptimeStr}</div>
|
|
520
|
+
<div class="app-detail">ID: ${escapeHtml(app.id)}</div>
|
|
521
|
+
${gapsHtml}
|
|
522
|
+
</div>
|
|
523
|
+
`;
|
|
524
|
+
}).join('');
|
|
525
|
+
return htmlTemplate('Apps', `
|
|
526
|
+
<style>
|
|
527
|
+
.health-ok { background: #238636; color: white; }
|
|
528
|
+
.health-stale { background: #d29922; color: #333; }
|
|
529
|
+
</style>
|
|
530
|
+
<h1>Connected Apps</h1>
|
|
531
|
+
${appsHtml}
|
|
532
|
+
`);
|
|
533
|
+
}
|
|
534
|
+
function renderBundleErrors() {
|
|
535
|
+
const errors = bundleErrorBuffer.get();
|
|
536
|
+
const status = bundleErrorBuffer.getStatus();
|
|
537
|
+
const statusText = status.hasError ? 'Build Failed' : 'Build OK';
|
|
538
|
+
let content = `
|
|
539
|
+
<h1>Bundle Errors</h1>
|
|
540
|
+
<div class="stats">
|
|
541
|
+
<div class="stat">
|
|
542
|
+
<div class="stat-value" style="color: ${status.hasError ? '#f85149' : '#3fb950'};">${statusText}</div>
|
|
543
|
+
<div class="stat-label">Build Status</div>
|
|
544
|
+
</div>
|
|
545
|
+
<div class="stat">
|
|
546
|
+
<div class="stat-value">${errors.length}</div>
|
|
547
|
+
<div class="stat-label">Errors</div>
|
|
548
|
+
</div>
|
|
549
|
+
${status.lastBuildTimestamp ? `
|
|
550
|
+
<div class="stat">
|
|
551
|
+
<div class="stat-value">${formatTime(status.lastBuildTimestamp)}</div>
|
|
552
|
+
<div class="stat-label">Last Build</div>
|
|
553
|
+
</div>
|
|
554
|
+
` : ''}
|
|
555
|
+
</div>
|
|
556
|
+
`;
|
|
557
|
+
if (errors.length === 0) {
|
|
558
|
+
content += '<div class="empty" style="margin-top: 20px;">No bundle errors. Your app is building successfully!</div>';
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
const errorsHtml = errors.map((error, index) => {
|
|
562
|
+
const location = error.line ? `Line ${error.line}${error.column ? `:${error.column}` : ''}` : '';
|
|
563
|
+
return `
|
|
564
|
+
<div class="error-card" style="background: #161b22; border: 1px solid #f85149; border-radius: 8px; padding: 16px; margin-bottom: 12px;">
|
|
565
|
+
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
|
|
566
|
+
<strong style="color: #f85149;">Error ${index + 1}</strong>
|
|
567
|
+
${location ? `<span style="color: #8b949e; font-size: 12px;">${location}</span>` : ''}
|
|
568
|
+
</div>
|
|
569
|
+
${error.file ? `<div style="color: #58a6ff; font-size: 13px; margin-bottom: 8px; font-family: monospace;">${escapeHtml(error.file)}</div>` : ''}
|
|
570
|
+
<pre style="margin: 0; padding: 12px; background: #0d1117; border-radius: 4px; overflow-x: auto; white-space: pre-wrap;"><code style="color: #f85149;">${escapeHtml(error.message)}</code></pre>
|
|
571
|
+
${error.codeFrame ? `
|
|
572
|
+
<div style="margin-top: 12px;">
|
|
573
|
+
<div style="color: #8b949e; font-size: 11px; margin-bottom: 4px;">Code Frame:</div>
|
|
574
|
+
<pre style="margin: 0; padding: 12px; background: #0d1117; border-radius: 4px; overflow-x: auto;"><code class="language-javascript">${escapeHtml(error.codeFrame)}</code></pre>
|
|
575
|
+
</div>
|
|
576
|
+
` : ''}
|
|
577
|
+
</div>
|
|
578
|
+
`;
|
|
579
|
+
}).join('');
|
|
580
|
+
content += `<div style="margin-top: 20px;">${errorsHtml}</div>`;
|
|
581
|
+
}
|
|
582
|
+
return htmlTemplate('Bundle Errors', content);
|
|
583
|
+
}
|
|
584
|
+
function renderRepl() {
|
|
585
|
+
return `<!DOCTYPE html>
|
|
586
|
+
<html lang="en">
|
|
587
|
+
<head>
|
|
588
|
+
<meta charset="UTF-8">
|
|
589
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
590
|
+
<title>REPL - RN Debugger</title>
|
|
591
|
+
<style>
|
|
592
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
593
|
+
body {
|
|
594
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
595
|
+
background: #0d1117;
|
|
596
|
+
color: #c9d1d9;
|
|
597
|
+
padding: 20px;
|
|
598
|
+
line-height: 1.5;
|
|
599
|
+
}
|
|
600
|
+
nav {
|
|
601
|
+
background: #161b22;
|
|
602
|
+
padding: 12px 20px;
|
|
603
|
+
margin: -20px -20px 20px -20px;
|
|
604
|
+
border-bottom: 1px solid #30363d;
|
|
605
|
+
display: flex;
|
|
606
|
+
gap: 20px;
|
|
607
|
+
align-items: center;
|
|
608
|
+
}
|
|
609
|
+
nav a {
|
|
610
|
+
color: #58a6ff;
|
|
611
|
+
text-decoration: none;
|
|
612
|
+
padding: 6px 12px;
|
|
613
|
+
border-radius: 6px;
|
|
614
|
+
transition: background 0.2s;
|
|
615
|
+
}
|
|
616
|
+
nav a:hover { background: #21262d; }
|
|
617
|
+
nav a.active { background: #388bfd; color: white; }
|
|
618
|
+
.logo { font-weight: 600; color: #f0f6fc; margin-right: auto; }
|
|
619
|
+
h1 { margin-bottom: 16px; font-size: 1.5em; }
|
|
620
|
+
.repl-container {
|
|
621
|
+
display: flex;
|
|
622
|
+
flex-direction: column;
|
|
623
|
+
gap: 16px;
|
|
624
|
+
height: calc(100vh - 140px);
|
|
625
|
+
}
|
|
626
|
+
.input-section {
|
|
627
|
+
display: flex;
|
|
628
|
+
flex-direction: column;
|
|
629
|
+
gap: 8px;
|
|
630
|
+
}
|
|
631
|
+
.input-section label {
|
|
632
|
+
color: #8b949e;
|
|
633
|
+
font-size: 13px;
|
|
634
|
+
}
|
|
635
|
+
#codeInput {
|
|
636
|
+
width: 100%;
|
|
637
|
+
height: 150px;
|
|
638
|
+
padding: 12px;
|
|
639
|
+
font-family: 'SF Mono', Consolas, monospace;
|
|
640
|
+
font-size: 14px;
|
|
641
|
+
background: #161b22;
|
|
642
|
+
color: #c9d1d9;
|
|
643
|
+
border: 1px solid #30363d;
|
|
644
|
+
border-radius: 8px;
|
|
645
|
+
resize: vertical;
|
|
646
|
+
}
|
|
647
|
+
#codeInput:focus {
|
|
648
|
+
outline: none;
|
|
649
|
+
border-color: #58a6ff;
|
|
650
|
+
}
|
|
651
|
+
.controls {
|
|
652
|
+
display: flex;
|
|
653
|
+
gap: 12px;
|
|
654
|
+
align-items: center;
|
|
655
|
+
}
|
|
656
|
+
.btn {
|
|
657
|
+
padding: 8px 16px;
|
|
658
|
+
border: none;
|
|
659
|
+
border-radius: 6px;
|
|
660
|
+
font-size: 14px;
|
|
661
|
+
font-weight: 500;
|
|
662
|
+
cursor: pointer;
|
|
663
|
+
transition: background 0.2s;
|
|
664
|
+
}
|
|
665
|
+
.btn-primary {
|
|
666
|
+
background: #238636;
|
|
667
|
+
color: white;
|
|
668
|
+
}
|
|
669
|
+
.btn-primary:hover { background: #2ea043; }
|
|
670
|
+
.btn-primary:disabled { background: #21262d; color: #6e7681; cursor: not-allowed; }
|
|
671
|
+
.btn-secondary {
|
|
672
|
+
background: #21262d;
|
|
673
|
+
color: #c9d1d9;
|
|
674
|
+
}
|
|
675
|
+
.btn-secondary:hover { background: #30363d; }
|
|
676
|
+
.checkbox-label {
|
|
677
|
+
display: flex;
|
|
678
|
+
align-items: center;
|
|
679
|
+
gap: 6px;
|
|
680
|
+
color: #8b949e;
|
|
681
|
+
font-size: 13px;
|
|
682
|
+
}
|
|
683
|
+
.output-section {
|
|
684
|
+
flex: 1;
|
|
685
|
+
display: flex;
|
|
686
|
+
flex-direction: column;
|
|
687
|
+
gap: 8px;
|
|
688
|
+
min-height: 0;
|
|
689
|
+
}
|
|
690
|
+
.output-header {
|
|
691
|
+
display: flex;
|
|
692
|
+
justify-content: space-between;
|
|
693
|
+
align-items: center;
|
|
694
|
+
}
|
|
695
|
+
.output-header label {
|
|
696
|
+
color: #8b949e;
|
|
697
|
+
font-size: 13px;
|
|
698
|
+
}
|
|
699
|
+
#output {
|
|
700
|
+
flex: 1;
|
|
701
|
+
padding: 12px;
|
|
702
|
+
font-family: 'SF Mono', Consolas, monospace;
|
|
703
|
+
font-size: 13px;
|
|
704
|
+
background: #161b22;
|
|
705
|
+
color: #c9d1d9;
|
|
706
|
+
border: 1px solid #30363d;
|
|
707
|
+
border-radius: 8px;
|
|
708
|
+
overflow: auto;
|
|
709
|
+
white-space: pre-wrap;
|
|
710
|
+
word-break: break-word;
|
|
711
|
+
}
|
|
712
|
+
.output-success { color: #3fb950; }
|
|
713
|
+
.output-error { color: #f85149; }
|
|
714
|
+
.history-section {
|
|
715
|
+
margin-top: 16px;
|
|
716
|
+
}
|
|
717
|
+
.history-section h3 {
|
|
718
|
+
color: #8b949e;
|
|
719
|
+
font-size: 13px;
|
|
720
|
+
margin-bottom: 8px;
|
|
721
|
+
}
|
|
722
|
+
.history-list {
|
|
723
|
+
display: flex;
|
|
724
|
+
flex-wrap: wrap;
|
|
725
|
+
gap: 8px;
|
|
726
|
+
}
|
|
727
|
+
.history-item {
|
|
728
|
+
padding: 4px 10px;
|
|
729
|
+
background: #21262d;
|
|
730
|
+
border-radius: 4px;
|
|
731
|
+
font-size: 12px;
|
|
732
|
+
font-family: monospace;
|
|
733
|
+
cursor: pointer;
|
|
734
|
+
max-width: 200px;
|
|
735
|
+
overflow: hidden;
|
|
736
|
+
text-overflow: ellipsis;
|
|
737
|
+
white-space: nowrap;
|
|
738
|
+
}
|
|
739
|
+
.history-item:hover { background: #30363d; }
|
|
740
|
+
</style>
|
|
741
|
+
</head>
|
|
742
|
+
<body>
|
|
743
|
+
<nav>
|
|
744
|
+
<span class="logo">RN Debugger</span>
|
|
745
|
+
<a href="/">Dashboard</a>
|
|
746
|
+
<a href="/logs">Logs</a>
|
|
747
|
+
<a href="/network">Network</a>
|
|
748
|
+
<a href="/bundle-errors">Errors</a>
|
|
749
|
+
<a href="/apps">Apps</a>
|
|
750
|
+
<a href="/repl" class="active">REPL</a>
|
|
751
|
+
<a href="/component-tree">Components</a>
|
|
752
|
+
<a href="/globals">Globals</a>
|
|
753
|
+
<a href="/tap-verifier">Tap Verifier</a>
|
|
754
|
+
</nav>
|
|
755
|
+
<h1>JavaScript REPL</h1>
|
|
756
|
+
<div class="repl-container">
|
|
757
|
+
<div class="input-section">
|
|
758
|
+
<label>Enter JavaScript expression to execute in the app:</label>
|
|
759
|
+
<textarea id="codeInput" placeholder="// Example: get current navigation state
|
|
760
|
+
global.__REACT_NAVIGATION__?.current?.getRootState()
|
|
761
|
+
|
|
762
|
+
// Or inspect React DevTools hook
|
|
763
|
+
Object.keys(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ || {})"></textarea>
|
|
764
|
+
<div class="controls">
|
|
765
|
+
<button class="btn btn-primary" id="executeBtn" onclick="executeCode()">Execute</button>
|
|
766
|
+
<button class="btn btn-secondary" onclick="clearOutput()">Clear Output</button>
|
|
767
|
+
<label class="checkbox-label">
|
|
768
|
+
<input type="checkbox" id="awaitPromise" checked>
|
|
769
|
+
Await Promises
|
|
770
|
+
</label>
|
|
771
|
+
</div>
|
|
772
|
+
</div>
|
|
773
|
+
<div class="output-section">
|
|
774
|
+
<div class="output-header">
|
|
775
|
+
<label>Output:</label>
|
|
776
|
+
<span id="execTime" style="color: #6e7681; font-size: 12px;"></span>
|
|
777
|
+
</div>
|
|
778
|
+
<div id="output"><span style="color: #6e7681;">Output will appear here...</span></div>
|
|
779
|
+
</div>
|
|
780
|
+
<div class="history-section">
|
|
781
|
+
<h3>History</h3>
|
|
782
|
+
<div class="history-list" id="historyList"></div>
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
<script>
|
|
786
|
+
const MAX_HISTORY = 10;
|
|
787
|
+
let history = JSON.parse(localStorage.getItem('repl-history') || '[]');
|
|
788
|
+
|
|
789
|
+
function updateHistoryUI() {
|
|
790
|
+
const list = document.getElementById('historyList');
|
|
791
|
+
list.innerHTML = history.map((item, i) =>
|
|
792
|
+
'<div class="history-item" onclick="loadHistory(' + i + ')" title="' + item.replace(/"/g, '"') + '">' + item.slice(0, 50) + (item.length > 50 ? '...' : '') + '</div>'
|
|
793
|
+
).join('');
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function loadHistory(index) {
|
|
797
|
+
document.getElementById('codeInput').value = history[index];
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function addToHistory(code) {
|
|
801
|
+
// Remove if already exists
|
|
802
|
+
history = history.filter(h => h !== code);
|
|
803
|
+
// Add to front
|
|
804
|
+
history.unshift(code);
|
|
805
|
+
// Limit size
|
|
806
|
+
history = history.slice(0, MAX_HISTORY);
|
|
807
|
+
localStorage.setItem('repl-history', JSON.stringify(history));
|
|
808
|
+
updateHistoryUI();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function executeCode() {
|
|
812
|
+
const code = document.getElementById('codeInput').value.trim();
|
|
813
|
+
if (!code) return;
|
|
814
|
+
|
|
815
|
+
const output = document.getElementById('output');
|
|
816
|
+
const execTime = document.getElementById('execTime');
|
|
817
|
+
const btn = document.getElementById('executeBtn');
|
|
818
|
+
const awaitPromise = document.getElementById('awaitPromise').checked;
|
|
819
|
+
|
|
820
|
+
btn.disabled = true;
|
|
821
|
+
btn.textContent = 'Executing...';
|
|
822
|
+
output.innerHTML = '<span style="color: #8b949e;">Executing...</span>';
|
|
823
|
+
execTime.textContent = '';
|
|
824
|
+
|
|
825
|
+
const startTime = Date.now();
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
const res = await fetch('/api/execute', {
|
|
829
|
+
method: 'POST',
|
|
830
|
+
headers: { 'Content-Type': 'application/json' },
|
|
831
|
+
body: JSON.stringify({ expression: code, awaitPromise })
|
|
832
|
+
});
|
|
833
|
+
const data = await res.json();
|
|
834
|
+
const elapsed = Date.now() - startTime;
|
|
835
|
+
execTime.textContent = elapsed + 'ms';
|
|
836
|
+
|
|
837
|
+
if (data.success) {
|
|
838
|
+
output.innerHTML = '<span class="output-success">' + formatOutput(data.result) + '</span>';
|
|
839
|
+
addToHistory(code);
|
|
840
|
+
} else {
|
|
841
|
+
output.innerHTML = '<span class="output-error">Error: ' + escapeHtml(data.error || 'Unknown error') + '</span>';
|
|
842
|
+
}
|
|
843
|
+
} catch (err) {
|
|
844
|
+
output.innerHTML = '<span class="output-error">Request failed: ' + escapeHtml(err.message) + '</span>';
|
|
845
|
+
} finally {
|
|
846
|
+
btn.disabled = false;
|
|
847
|
+
btn.textContent = 'Execute';
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function formatOutput(result) {
|
|
852
|
+
if (result === undefined || result === 'undefined') return 'undefined';
|
|
853
|
+
if (result === null || result === 'null') return 'null';
|
|
854
|
+
try {
|
|
855
|
+
const parsed = typeof result === 'string' ? JSON.parse(result) : result;
|
|
856
|
+
return escapeHtml(JSON.stringify(parsed, null, 2));
|
|
857
|
+
} catch {
|
|
858
|
+
return escapeHtml(String(result));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function escapeHtml(str) {
|
|
863
|
+
return String(str)
|
|
864
|
+
.replace(/&/g, '&')
|
|
865
|
+
.replace(/</g, '<')
|
|
866
|
+
.replace(/>/g, '>')
|
|
867
|
+
.replace(/"/g, '"');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function clearOutput() {
|
|
871
|
+
document.getElementById('output').innerHTML = '<span style="color: #6e7681;">Output will appear here...</span>';
|
|
872
|
+
document.getElementById('execTime').textContent = '';
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Keyboard shortcut: Ctrl/Cmd + Enter to execute
|
|
876
|
+
document.getElementById('codeInput').addEventListener('keydown', (e) => {
|
|
877
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
878
|
+
e.preventDefault();
|
|
879
|
+
executeCode();
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// Init history
|
|
884
|
+
updateHistoryUI();
|
|
885
|
+
</script>
|
|
886
|
+
</body>
|
|
887
|
+
</html>`;
|
|
888
|
+
}
|
|
889
|
+
function renderComponentTree() {
|
|
890
|
+
return `<!DOCTYPE html>
|
|
891
|
+
<html lang="en">
|
|
892
|
+
<head>
|
|
893
|
+
<meta charset="UTF-8">
|
|
894
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
895
|
+
<title>Component Tree - RN Debugger</title>
|
|
896
|
+
<style>
|
|
897
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
898
|
+
body {
|
|
899
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
900
|
+
background: #0d1117;
|
|
901
|
+
color: #c9d1d9;
|
|
902
|
+
padding: 20px;
|
|
903
|
+
line-height: 1.5;
|
|
904
|
+
}
|
|
905
|
+
nav {
|
|
906
|
+
background: #161b22;
|
|
907
|
+
padding: 12px 20px;
|
|
908
|
+
margin: -20px -20px 20px -20px;
|
|
909
|
+
border-bottom: 1px solid #30363d;
|
|
910
|
+
display: flex;
|
|
911
|
+
gap: 20px;
|
|
912
|
+
align-items: center;
|
|
913
|
+
}
|
|
914
|
+
nav a {
|
|
915
|
+
color: #58a6ff;
|
|
916
|
+
text-decoration: none;
|
|
917
|
+
padding: 6px 12px;
|
|
918
|
+
border-radius: 6px;
|
|
919
|
+
transition: background 0.2s;
|
|
920
|
+
}
|
|
921
|
+
nav a:hover { background: #21262d; }
|
|
922
|
+
nav a.active { background: #388bfd; color: white; }
|
|
923
|
+
.logo { font-weight: 600; color: #f0f6fc; margin-right: auto; }
|
|
924
|
+
h1 { margin-bottom: 16px; font-size: 1.5em; }
|
|
925
|
+
.controls {
|
|
926
|
+
display: flex;
|
|
927
|
+
gap: 12px;
|
|
928
|
+
align-items: center;
|
|
929
|
+
margin-bottom: 16px;
|
|
930
|
+
flex-wrap: wrap;
|
|
931
|
+
}
|
|
932
|
+
.btn {
|
|
933
|
+
padding: 8px 16px;
|
|
934
|
+
border: none;
|
|
935
|
+
border-radius: 6px;
|
|
936
|
+
font-size: 14px;
|
|
937
|
+
font-weight: 500;
|
|
938
|
+
cursor: pointer;
|
|
939
|
+
transition: background 0.2s;
|
|
940
|
+
}
|
|
941
|
+
.btn-primary {
|
|
942
|
+
background: #238636;
|
|
943
|
+
color: white;
|
|
944
|
+
}
|
|
945
|
+
.btn-primary:hover { background: #2ea043; }
|
|
946
|
+
.btn-primary:disabled { background: #21262d; color: #6e7681; cursor: not-allowed; }
|
|
947
|
+
.btn-secondary {
|
|
948
|
+
background: #21262d;
|
|
949
|
+
color: #c9d1d9;
|
|
950
|
+
}
|
|
951
|
+
.btn-secondary:hover { background: #30363d; }
|
|
952
|
+
.checkbox-label {
|
|
953
|
+
display: flex;
|
|
954
|
+
align-items: center;
|
|
955
|
+
gap: 6px;
|
|
956
|
+
color: #8b949e;
|
|
957
|
+
font-size: 13px;
|
|
958
|
+
}
|
|
959
|
+
#searchInput {
|
|
960
|
+
padding: 8px 12px;
|
|
961
|
+
border: 1px solid #30363d;
|
|
962
|
+
border-radius: 6px;
|
|
963
|
+
background: #161b22;
|
|
964
|
+
color: #c9d1d9;
|
|
965
|
+
font-size: 14px;
|
|
966
|
+
width: 200px;
|
|
967
|
+
}
|
|
968
|
+
#searchInput:focus {
|
|
969
|
+
outline: none;
|
|
970
|
+
border-color: #58a6ff;
|
|
971
|
+
}
|
|
972
|
+
.tree-container {
|
|
973
|
+
background: #161b22;
|
|
974
|
+
border: 1px solid #30363d;
|
|
975
|
+
border-radius: 8px;
|
|
976
|
+
padding: 16px;
|
|
977
|
+
overflow: auto;
|
|
978
|
+
max-height: calc(100vh - 220px);
|
|
979
|
+
}
|
|
980
|
+
.tree-content {
|
|
981
|
+
font-family: 'SF Mono', Consolas, monospace;
|
|
982
|
+
font-size: 13px;
|
|
983
|
+
white-space: pre;
|
|
984
|
+
line-height: 1.6;
|
|
985
|
+
}
|
|
986
|
+
.tree-content .component { color: #7ee787; }
|
|
987
|
+
.tree-content .props { color: #d2a8ff; }
|
|
988
|
+
.tree-content .layout { color: #79c0ff; }
|
|
989
|
+
.loading { color: #8b949e; text-align: center; padding: 40px; }
|
|
990
|
+
.error { color: #f85149; padding: 20px; }
|
|
991
|
+
.focused-screen {
|
|
992
|
+
background: #388bfd33;
|
|
993
|
+
padding: 8px 12px;
|
|
994
|
+
border-radius: 6px;
|
|
995
|
+
margin-bottom: 12px;
|
|
996
|
+
font-size: 14px;
|
|
997
|
+
}
|
|
998
|
+
.focused-screen strong { color: #58a6ff; }
|
|
999
|
+
.stats {
|
|
1000
|
+
color: #8b949e;
|
|
1001
|
+
font-size: 12px;
|
|
1002
|
+
margin-bottom: 12px;
|
|
1003
|
+
}
|
|
1004
|
+
</style>
|
|
1005
|
+
</head>
|
|
1006
|
+
<body>
|
|
1007
|
+
<nav>
|
|
1008
|
+
<span class="logo">RN Debugger</span>
|
|
1009
|
+
<a href="/">Dashboard</a>
|
|
1010
|
+
<a href="/logs">Logs</a>
|
|
1011
|
+
<a href="/network">Network</a>
|
|
1012
|
+
<a href="/bundle-errors">Errors</a>
|
|
1013
|
+
<a href="/apps">Apps</a>
|
|
1014
|
+
<a href="/repl">REPL</a>
|
|
1015
|
+
<a href="/component-tree" class="active">Components</a>
|
|
1016
|
+
<a href="/globals">Globals</a>
|
|
1017
|
+
<a href="/tap-verifier">Tap Verifier</a>
|
|
1018
|
+
</nav>
|
|
1019
|
+
<h1>React Component Tree</h1>
|
|
1020
|
+
<div class="controls">
|
|
1021
|
+
<button class="btn btn-primary" id="refreshBtn" onclick="loadTree()">Refresh</button>
|
|
1022
|
+
<input type="text" id="searchInput" placeholder="Filter components..." oninput="filterTree()">
|
|
1023
|
+
<label class="checkbox-label">
|
|
1024
|
+
<input type="checkbox" id="focusedOnly" checked onchange="loadTree()">
|
|
1025
|
+
Focused Screen Only
|
|
1026
|
+
</label>
|
|
1027
|
+
<label class="checkbox-label">
|
|
1028
|
+
<input type="checkbox" id="structureOnly" checked onchange="loadTree()">
|
|
1029
|
+
Structure Only (Compact)
|
|
1030
|
+
</label>
|
|
1031
|
+
<label class="checkbox-label">
|
|
1032
|
+
<input type="checkbox" id="includeProps" onchange="loadTree()">
|
|
1033
|
+
Include Props
|
|
1034
|
+
</label>
|
|
1035
|
+
</div>
|
|
1036
|
+
<div id="focusedInfo"></div>
|
|
1037
|
+
<div id="stats" class="stats"></div>
|
|
1038
|
+
<div class="tree-container">
|
|
1039
|
+
<div class="tree-content" id="treeContent">
|
|
1040
|
+
<div class="loading">Click "Refresh" to load the component tree...</div>
|
|
482
1041
|
</div>
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
1042
|
+
</div>
|
|
1043
|
+
<script>
|
|
1044
|
+
let fullTree = '';
|
|
1045
|
+
|
|
1046
|
+
async function loadTree() {
|
|
1047
|
+
const content = document.getElementById('treeContent');
|
|
1048
|
+
const focusedInfo = document.getElementById('focusedInfo');
|
|
1049
|
+
const stats = document.getElementById('stats');
|
|
1050
|
+
const btn = document.getElementById('refreshBtn');
|
|
1051
|
+
|
|
1052
|
+
const focusedOnly = document.getElementById('focusedOnly').checked;
|
|
1053
|
+
const structureOnly = document.getElementById('structureOnly').checked;
|
|
1054
|
+
const includeProps = document.getElementById('includeProps').checked;
|
|
1055
|
+
|
|
1056
|
+
btn.disabled = true;
|
|
1057
|
+
btn.textContent = 'Loading...';
|
|
1058
|
+
content.innerHTML = '<div class="loading">Loading component tree...</div>';
|
|
1059
|
+
focusedInfo.innerHTML = '';
|
|
1060
|
+
stats.textContent = '';
|
|
1061
|
+
|
|
1062
|
+
try {
|
|
1063
|
+
const params = new URLSearchParams({
|
|
1064
|
+
focusedOnly: focusedOnly.toString(),
|
|
1065
|
+
structureOnly: structureOnly.toString(),
|
|
1066
|
+
includeProps: includeProps.toString(),
|
|
1067
|
+
maxDepth: structureOnly ? '50' : '100'
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
const res = await fetch('/api/component-tree?' + params);
|
|
1071
|
+
const data = await res.json();
|
|
1072
|
+
|
|
1073
|
+
if (data.success) {
|
|
1074
|
+
fullTree = data.result || '';
|
|
1075
|
+
|
|
1076
|
+
// Check for focused screen info
|
|
1077
|
+
const lines = fullTree.split('\\n');
|
|
1078
|
+
if (lines[0] && lines[0].startsWith('Focused:')) {
|
|
1079
|
+
focusedInfo.innerHTML = '<div class="focused-screen"><strong>' + escapeHtml(lines[0]) + '</strong></div>';
|
|
1080
|
+
fullTree = lines.slice(2).join('\\n');
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Count components
|
|
1084
|
+
const lineCount = fullTree.split('\\n').filter(l => l.trim()).length;
|
|
1085
|
+
stats.textContent = lineCount + ' components';
|
|
1086
|
+
|
|
1087
|
+
displayTree(fullTree);
|
|
1088
|
+
} else {
|
|
1089
|
+
content.innerHTML = '<div class="error">Error: ' + escapeHtml(data.error || 'Unknown error') + '</div>';
|
|
1090
|
+
}
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
content.innerHTML = '<div class="error">Request failed: ' + escapeHtml(err.message) + '</div>';
|
|
1093
|
+
} finally {
|
|
1094
|
+
btn.disabled = false;
|
|
1095
|
+
btn.textContent = 'Refresh';
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function displayTree(tree) {
|
|
1100
|
+
const content = document.getElementById('treeContent');
|
|
1101
|
+
// Syntax highlight the tree
|
|
1102
|
+
const highlighted = tree
|
|
1103
|
+
.split('\\n')
|
|
1104
|
+
.map(line => {
|
|
1105
|
+
// Component names (at start of line after indentation)
|
|
1106
|
+
let result = line.replace(/^(\\s*)(\\S+)/, '$1<span class="component">$2</span>');
|
|
1107
|
+
// Props in parentheses
|
|
1108
|
+
result = result.replace(/\\(([^)]+)\\)/g, '<span class="props">($1)</span>');
|
|
1109
|
+
// Layout in brackets
|
|
1110
|
+
result = result.replace(/\\[([^\\]]+)\\]/g, '<span class="layout">[$1]</span>');
|
|
1111
|
+
return result;
|
|
1112
|
+
})
|
|
1113
|
+
.join('\\n');
|
|
1114
|
+
content.innerHTML = highlighted || '<div class="loading">No components found</div>';
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function filterTree() {
|
|
1118
|
+
const filter = document.getElementById('searchInput').value.toLowerCase();
|
|
1119
|
+
if (!filter) {
|
|
1120
|
+
displayTree(fullTree);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const filtered = fullTree
|
|
1125
|
+
.split('\\n')
|
|
1126
|
+
.filter(line => line.toLowerCase().includes(filter))
|
|
1127
|
+
.join('\\n');
|
|
1128
|
+
|
|
1129
|
+
displayTree(filtered || 'No matching components');
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function escapeHtml(str) {
|
|
1133
|
+
return String(str)
|
|
1134
|
+
.replace(/&/g, '&')
|
|
1135
|
+
.replace(/</g, '<')
|
|
1136
|
+
.replace(/>/g, '>');
|
|
1137
|
+
}
|
|
1138
|
+
</script>
|
|
1139
|
+
</body>
|
|
1140
|
+
</html>`;
|
|
1141
|
+
}
|
|
1142
|
+
function renderGlobals() {
|
|
1143
|
+
return `<!DOCTYPE html>
|
|
1144
|
+
<html lang="en">
|
|
1145
|
+
<head>
|
|
1146
|
+
<meta charset="UTF-8">
|
|
1147
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1148
|
+
<title>Debug Globals - RN Debugger</title>
|
|
1149
|
+
<style>
|
|
1150
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1151
|
+
body {
|
|
1152
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1153
|
+
background: #0d1117;
|
|
1154
|
+
color: #c9d1d9;
|
|
1155
|
+
padding: 20px;
|
|
1156
|
+
line-height: 1.5;
|
|
1157
|
+
}
|
|
1158
|
+
nav {
|
|
1159
|
+
background: #161b22;
|
|
1160
|
+
padding: 12px 20px;
|
|
1161
|
+
margin: -20px -20px 20px -20px;
|
|
1162
|
+
border-bottom: 1px solid #30363d;
|
|
1163
|
+
display: flex;
|
|
1164
|
+
gap: 20px;
|
|
1165
|
+
align-items: center;
|
|
1166
|
+
}
|
|
1167
|
+
nav a {
|
|
1168
|
+
color: #58a6ff;
|
|
1169
|
+
text-decoration: none;
|
|
1170
|
+
padding: 6px 12px;
|
|
1171
|
+
border-radius: 6px;
|
|
1172
|
+
transition: background 0.2s;
|
|
1173
|
+
}
|
|
1174
|
+
nav a:hover { background: #21262d; }
|
|
1175
|
+
nav a.active { background: #388bfd; color: white; }
|
|
1176
|
+
.logo { font-weight: 600; color: #f0f6fc; margin-right: auto; }
|
|
1177
|
+
h1 { margin-bottom: 16px; font-size: 1.5em; }
|
|
1178
|
+
.controls {
|
|
1179
|
+
display: flex;
|
|
1180
|
+
gap: 12px;
|
|
1181
|
+
align-items: center;
|
|
1182
|
+
margin-bottom: 16px;
|
|
1183
|
+
}
|
|
1184
|
+
.btn {
|
|
1185
|
+
padding: 8px 16px;
|
|
1186
|
+
border: none;
|
|
1187
|
+
border-radius: 6px;
|
|
1188
|
+
font-size: 14px;
|
|
1189
|
+
font-weight: 500;
|
|
1190
|
+
cursor: pointer;
|
|
1191
|
+
transition: background 0.2s;
|
|
1192
|
+
}
|
|
1193
|
+
.btn-primary {
|
|
1194
|
+
background: #238636;
|
|
1195
|
+
color: white;
|
|
1196
|
+
}
|
|
1197
|
+
.btn-primary:hover { background: #2ea043; }
|
|
1198
|
+
.btn-primary:disabled { background: #21262d; color: #6e7681; cursor: not-allowed; }
|
|
1199
|
+
.category {
|
|
1200
|
+
background: #161b22;
|
|
1201
|
+
border: 1px solid #30363d;
|
|
1202
|
+
border-radius: 8px;
|
|
1203
|
+
margin-bottom: 16px;
|
|
1204
|
+
overflow: hidden;
|
|
1205
|
+
}
|
|
1206
|
+
.category-header {
|
|
1207
|
+
padding: 12px 16px;
|
|
1208
|
+
background: #21262d;
|
|
1209
|
+
cursor: pointer;
|
|
1210
|
+
display: flex;
|
|
1211
|
+
justify-content: space-between;
|
|
1212
|
+
align-items: center;
|
|
1213
|
+
}
|
|
1214
|
+
.category-header:hover { background: #30363d; }
|
|
1215
|
+
.category-title {
|
|
1216
|
+
font-weight: 600;
|
|
1217
|
+
color: #58a6ff;
|
|
1218
|
+
}
|
|
1219
|
+
.category-count {
|
|
1220
|
+
color: #8b949e;
|
|
1221
|
+
font-size: 13px;
|
|
1222
|
+
}
|
|
1223
|
+
.category-content {
|
|
1224
|
+
padding: 12px 16px;
|
|
1225
|
+
display: none;
|
|
1226
|
+
}
|
|
1227
|
+
.category.expanded .category-content { display: block; }
|
|
1228
|
+
.global-item {
|
|
1229
|
+
padding: 8px 12px;
|
|
1230
|
+
margin: 4px 0;
|
|
1231
|
+
background: #0d1117;
|
|
1232
|
+
border-radius: 4px;
|
|
1233
|
+
font-family: 'SF Mono', Consolas, monospace;
|
|
1234
|
+
font-size: 13px;
|
|
1235
|
+
cursor: pointer;
|
|
1236
|
+
display: flex;
|
|
1237
|
+
justify-content: space-between;
|
|
1238
|
+
align-items: center;
|
|
1239
|
+
}
|
|
1240
|
+
.global-item:hover { background: #21262d; }
|
|
1241
|
+
.global-name { color: #7ee787; }
|
|
1242
|
+
.inspect-btn {
|
|
1243
|
+
padding: 4px 8px;
|
|
1244
|
+
background: #388bfd;
|
|
1245
|
+
color: white;
|
|
1246
|
+
border: none;
|
|
1247
|
+
border-radius: 4px;
|
|
1248
|
+
font-size: 11px;
|
|
1249
|
+
cursor: pointer;
|
|
1250
|
+
}
|
|
1251
|
+
.inspect-btn:hover { background: #58a6ff; }
|
|
1252
|
+
.loading { color: #8b949e; text-align: center; padding: 40px; }
|
|
1253
|
+
.error { color: #f85149; padding: 20px; }
|
|
1254
|
+
.modal {
|
|
1255
|
+
display: none;
|
|
1256
|
+
position: fixed;
|
|
1257
|
+
top: 0;
|
|
1258
|
+
left: 0;
|
|
1259
|
+
right: 0;
|
|
1260
|
+
bottom: 0;
|
|
1261
|
+
background: rgba(0, 0, 0, 0.8);
|
|
1262
|
+
z-index: 1000;
|
|
1263
|
+
padding: 40px;
|
|
1264
|
+
overflow: auto;
|
|
1265
|
+
}
|
|
1266
|
+
.modal.visible { display: block; }
|
|
1267
|
+
.modal-content {
|
|
1268
|
+
max-width: 800px;
|
|
1269
|
+
margin: 0 auto;
|
|
1270
|
+
background: #161b22;
|
|
1271
|
+
border: 1px solid #30363d;
|
|
1272
|
+
border-radius: 8px;
|
|
1273
|
+
overflow: hidden;
|
|
1274
|
+
}
|
|
1275
|
+
.modal-header {
|
|
1276
|
+
padding: 12px 16px;
|
|
1277
|
+
background: #21262d;
|
|
1278
|
+
display: flex;
|
|
1279
|
+
justify-content: space-between;
|
|
1280
|
+
align-items: center;
|
|
1281
|
+
}
|
|
1282
|
+
.modal-title {
|
|
1283
|
+
font-weight: 600;
|
|
1284
|
+
color: #58a6ff;
|
|
1285
|
+
font-family: monospace;
|
|
1286
|
+
}
|
|
1287
|
+
.close-btn {
|
|
1288
|
+
background: none;
|
|
1289
|
+
border: none;
|
|
1290
|
+
color: #8b949e;
|
|
1291
|
+
font-size: 24px;
|
|
1292
|
+
cursor: pointer;
|
|
1293
|
+
}
|
|
1294
|
+
.close-btn:hover { color: #f85149; }
|
|
1295
|
+
.modal-body {
|
|
1296
|
+
padding: 16px;
|
|
1297
|
+
max-height: 70vh;
|
|
1298
|
+
overflow: auto;
|
|
1299
|
+
}
|
|
1300
|
+
.modal-body pre {
|
|
1301
|
+
margin: 0;
|
|
1302
|
+
padding: 12px;
|
|
1303
|
+
background: #0d1117;
|
|
1304
|
+
border-radius: 4px;
|
|
1305
|
+
overflow-x: auto;
|
|
1306
|
+
font-size: 12px;
|
|
1307
|
+
}
|
|
1308
|
+
.prop-item {
|
|
1309
|
+
padding: 8px;
|
|
1310
|
+
border-bottom: 1px solid #21262d;
|
|
1311
|
+
}
|
|
1312
|
+
.prop-item:last-child { border-bottom: none; }
|
|
1313
|
+
.prop-name { color: #d2a8ff; }
|
|
1314
|
+
.prop-type { color: #8b949e; font-size: 11px; margin-left: 8px; }
|
|
1315
|
+
.prop-value { color: #79c0ff; font-family: monospace; font-size: 12px; margin-top: 4px; }
|
|
1316
|
+
</style>
|
|
1317
|
+
</head>
|
|
1318
|
+
<body>
|
|
1319
|
+
<nav>
|
|
1320
|
+
<span class="logo">RN Debugger</span>
|
|
1321
|
+
<a href="/">Dashboard</a>
|
|
1322
|
+
<a href="/logs">Logs</a>
|
|
1323
|
+
<a href="/network">Network</a>
|
|
1324
|
+
<a href="/bundle-errors">Errors</a>
|
|
1325
|
+
<a href="/apps">Apps</a>
|
|
1326
|
+
<a href="/repl">REPL</a>
|
|
1327
|
+
<a href="/component-tree">Components</a>
|
|
1328
|
+
<a href="/globals" class="active">Globals</a>
|
|
1329
|
+
<a href="/tap-verifier">Tap Verifier</a>
|
|
1330
|
+
</nav>
|
|
1331
|
+
<h1>Debug Globals Explorer</h1>
|
|
1332
|
+
<div class="controls">
|
|
1333
|
+
<button class="btn btn-primary" id="refreshBtn" onclick="loadGlobals()">Refresh</button>
|
|
1334
|
+
</div>
|
|
1335
|
+
<div id="content">
|
|
1336
|
+
<div class="loading">Click "Refresh" to discover debug globals...</div>
|
|
1337
|
+
</div>
|
|
1338
|
+
|
|
1339
|
+
<div class="modal" id="inspectModal" onclick="closeModal(event)">
|
|
1340
|
+
<div class="modal-content" onclick="event.stopPropagation()">
|
|
1341
|
+
<div class="modal-header">
|
|
1342
|
+
<span class="modal-title" id="modalTitle">Global</span>
|
|
1343
|
+
<button class="close-btn" onclick="closeModal()">×</button>
|
|
1344
|
+
</div>
|
|
1345
|
+
<div class="modal-body" id="modalBody">
|
|
1346
|
+
Loading...
|
|
1347
|
+
</div>
|
|
1348
|
+
</div>
|
|
1349
|
+
</div>
|
|
1350
|
+
|
|
1351
|
+
<script>
|
|
1352
|
+
let globalsData = null;
|
|
1353
|
+
|
|
1354
|
+
async function loadGlobals() {
|
|
1355
|
+
const content = document.getElementById('content');
|
|
1356
|
+
const btn = document.getElementById('refreshBtn');
|
|
1357
|
+
|
|
1358
|
+
btn.disabled = true;
|
|
1359
|
+
btn.textContent = 'Loading...';
|
|
1360
|
+
content.innerHTML = '<div class="loading">Scanning for debug globals...</div>';
|
|
1361
|
+
|
|
1362
|
+
try {
|
|
1363
|
+
const res = await fetch('/api/globals');
|
|
1364
|
+
const data = await res.json();
|
|
1365
|
+
|
|
1366
|
+
if (data.success && data.result) {
|
|
1367
|
+
globalsData = typeof data.result === 'string' ? JSON.parse(data.result) : data.result;
|
|
1368
|
+
renderGlobals(globalsData);
|
|
1369
|
+
} else {
|
|
1370
|
+
content.innerHTML = '<div class="error">Error: ' + escapeHtml(data.error || 'Unknown error') + '</div>';
|
|
1371
|
+
}
|
|
1372
|
+
} catch (err) {
|
|
1373
|
+
content.innerHTML = '<div class="error">Request failed: ' + escapeHtml(err.message) + '</div>';
|
|
1374
|
+
} finally {
|
|
1375
|
+
btn.disabled = false;
|
|
1376
|
+
btn.textContent = 'Refresh';
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function renderGlobals(categories) {
|
|
1381
|
+
const content = document.getElementById('content');
|
|
1382
|
+
const html = Object.entries(categories)
|
|
1383
|
+
.filter(([_, items]) => items && items.length > 0)
|
|
1384
|
+
.map(([category, items]) => {
|
|
1385
|
+
const itemsHtml = items.map(name =>
|
|
1386
|
+
'<div class="global-item">' +
|
|
1387
|
+
'<span class="global-name">' + escapeHtml(name) + '</span>' +
|
|
1388
|
+
'<button class="inspect-btn" onclick="inspectGlobal(\\'' + escapeHtml(name).replace(/'/g, "\\\\'") + '\\')">Inspect</button>' +
|
|
1389
|
+
'</div>'
|
|
1390
|
+
).join('');
|
|
1391
|
+
return '<div class="category" onclick="toggleCategory(this)">' +
|
|
1392
|
+
'<div class="category-header">' +
|
|
1393
|
+
'<span class="category-title">' + escapeHtml(category) + '</span>' +
|
|
1394
|
+
'<span class="category-count">' + items.length + ' items</span>' +
|
|
1395
|
+
'</div>' +
|
|
1396
|
+
'<div class="category-content" onclick="event.stopPropagation()">' + itemsHtml + '</div>' +
|
|
1397
|
+
'</div>';
|
|
1398
|
+
}).join('');
|
|
1399
|
+
|
|
1400
|
+
if (!html) {
|
|
1401
|
+
content.innerHTML = '<div class="loading">No debug globals found. Make sure your app has debugging tools enabled (e.g., React DevTools, Apollo Client, Redux).</div>';
|
|
1402
|
+
} else {
|
|
1403
|
+
content.innerHTML = html;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function toggleCategory(el) {
|
|
1408
|
+
el.classList.toggle('expanded');
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async function inspectGlobal(name) {
|
|
1412
|
+
const modal = document.getElementById('inspectModal');
|
|
1413
|
+
const title = document.getElementById('modalTitle');
|
|
1414
|
+
const body = document.getElementById('modalBody');
|
|
1415
|
+
|
|
1416
|
+
title.textContent = name;
|
|
1417
|
+
body.innerHTML = '<div class="loading">Loading...</div>';
|
|
1418
|
+
modal.classList.add('visible');
|
|
1419
|
+
|
|
1420
|
+
try {
|
|
1421
|
+
const res = await fetch('/api/globals/' + encodeURIComponent(name));
|
|
1422
|
+
const data = await res.json();
|
|
1423
|
+
|
|
1424
|
+
if (data.success && data.result) {
|
|
1425
|
+
const result = typeof data.result === 'string' ? JSON.parse(data.result) : data.result;
|
|
1426
|
+
if (result.error) {
|
|
1427
|
+
body.innerHTML = '<div class="error">' + escapeHtml(result.error) + '</div>';
|
|
1428
|
+
} else {
|
|
1429
|
+
const propsHtml = Object.entries(result).map(([key, info]) => {
|
|
1430
|
+
const typeInfo = typeof info === 'object' && info !== null ? info : { type: typeof info, value: info };
|
|
1431
|
+
return '<div class="prop-item">' +
|
|
1432
|
+
'<span class="prop-name">' + escapeHtml(key) + '</span>' +
|
|
1433
|
+
'<span class="prop-type">' + escapeHtml(typeInfo.type || 'unknown') + (typeInfo.callable ? ' (callable)' : '') + '</span>' +
|
|
1434
|
+
(typeInfo.preview || typeInfo.value !== undefined ?
|
|
1435
|
+
'<div class="prop-value">' + escapeHtml(String(typeInfo.preview || typeInfo.value)).slice(0, 200) + '</div>' : '') +
|
|
1436
|
+
'</div>';
|
|
1437
|
+
}).join('');
|
|
1438
|
+
body.innerHTML = propsHtml || '<div class="loading">Empty object</div>';
|
|
1439
|
+
}
|
|
1440
|
+
} else {
|
|
1441
|
+
body.innerHTML = '<div class="error">Error: ' + escapeHtml(data.error || 'Unknown error') + '</div>';
|
|
1442
|
+
}
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
body.innerHTML = '<div class="error">Request failed: ' + escapeHtml(err.message) + '</div>';
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function closeModal(event) {
|
|
1449
|
+
if (!event || event.target === document.getElementById('inspectModal')) {
|
|
1450
|
+
document.getElementById('inspectModal').classList.remove('visible');
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function escapeHtml(str) {
|
|
1455
|
+
return String(str)
|
|
1456
|
+
.replace(/&/g, '&')
|
|
1457
|
+
.replace(/</g, '<')
|
|
1458
|
+
.replace(/>/g, '>')
|
|
1459
|
+
.replace(/"/g, '"');
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Close modal on Escape key
|
|
1463
|
+
document.addEventListener('keydown', (e) => {
|
|
1464
|
+
if (e.key === 'Escape') closeModal();
|
|
1465
|
+
});
|
|
1466
|
+
</script>
|
|
1467
|
+
</body>
|
|
1468
|
+
</html>`;
|
|
488
1469
|
}
|
|
489
1470
|
function renderTapVerifier() {
|
|
490
1471
|
return `<!DOCTYPE html>
|
|
@@ -1042,6 +2023,26 @@ function createRequestHandler() {
|
|
|
1042
2023
|
res.end(renderTapVerifier());
|
|
1043
2024
|
return;
|
|
1044
2025
|
}
|
|
2026
|
+
if (url === "/bundle-errors") {
|
|
2027
|
+
res.setHeader("Content-Type", "text/html");
|
|
2028
|
+
res.end(renderBundleErrors());
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
if (url === "/repl") {
|
|
2032
|
+
res.setHeader("Content-Type", "text/html");
|
|
2033
|
+
res.end(renderRepl());
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
if (url === "/component-tree") {
|
|
2037
|
+
res.setHeader("Content-Type", "text/html");
|
|
2038
|
+
res.end(renderComponentTree());
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
if (url === "/globals") {
|
|
2042
|
+
res.setHeader("Content-Type", "text/html");
|
|
2043
|
+
res.end(renderGlobals());
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
1045
2046
|
// JSON API endpoints
|
|
1046
2047
|
res.setHeader("Content-Type", "application/json");
|
|
1047
2048
|
if (url === "/api/logs" || url === "/api/logs/") {
|
|
@@ -1076,6 +2077,86 @@ function createRequestHandler() {
|
|
|
1076
2077
|
};
|
|
1077
2078
|
res.end(JSON.stringify(status, null, 2));
|
|
1078
2079
|
}
|
|
2080
|
+
else if (url === "/api/connection-status") {
|
|
2081
|
+
// Connection health API endpoint
|
|
2082
|
+
const states = {};
|
|
2083
|
+
const health = {};
|
|
2084
|
+
for (const [appKey] of connectedApps.entries()) {
|
|
2085
|
+
const state = getAllConnectionStates().get(appKey);
|
|
2086
|
+
const contextHealth = getContextHealth(appKey);
|
|
2087
|
+
if (state)
|
|
2088
|
+
states[appKey] = state;
|
|
2089
|
+
if (contextHealth)
|
|
2090
|
+
health[appKey] = contextHealth;
|
|
2091
|
+
}
|
|
2092
|
+
res.end(JSON.stringify({ states, health }, null, 2));
|
|
2093
|
+
}
|
|
2094
|
+
else if (url === "/api/execute" && req.method === "POST") {
|
|
2095
|
+
// REPL execute API endpoint
|
|
2096
|
+
let body = '';
|
|
2097
|
+
req.on('data', chunk => { body += chunk; });
|
|
2098
|
+
req.on('end', async () => {
|
|
2099
|
+
try {
|
|
2100
|
+
const data = JSON.parse(body);
|
|
2101
|
+
const { expression, awaitPromise = true } = data;
|
|
2102
|
+
if (!expression || typeof expression !== 'string') {
|
|
2103
|
+
res.end(JSON.stringify({ success: false, error: 'expression is required' }));
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
const result = await executeInApp(expression, awaitPromise);
|
|
2107
|
+
res.end(JSON.stringify(result, null, 2));
|
|
2108
|
+
}
|
|
2109
|
+
catch (err) {
|
|
2110
|
+
res.end(JSON.stringify({ success: false, error: String(err) }));
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
else if (url === "/api/component-tree") {
|
|
2116
|
+
// Component tree API endpoint
|
|
2117
|
+
const maxDepth = parseInt(params.get('maxDepth') || '50', 10);
|
|
2118
|
+
const focusedOnly = params.get('focusedOnly') === 'true';
|
|
2119
|
+
const structureOnly = params.get('structureOnly') === 'true';
|
|
2120
|
+
const includeProps = params.get('includeProps') === 'true';
|
|
2121
|
+
try {
|
|
2122
|
+
const result = await getComponentTree({
|
|
2123
|
+
maxDepth,
|
|
2124
|
+
focusedOnly,
|
|
2125
|
+
structureOnly,
|
|
2126
|
+
includeProps
|
|
2127
|
+
});
|
|
2128
|
+
res.end(JSON.stringify(result, null, 2));
|
|
2129
|
+
}
|
|
2130
|
+
catch (err) {
|
|
2131
|
+
res.end(JSON.stringify({ success: false, error: String(err) }));
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
else if (url === "/api/globals") {
|
|
2135
|
+
// Debug globals list API endpoint
|
|
2136
|
+
try {
|
|
2137
|
+
const result = await listDebugGlobals();
|
|
2138
|
+
res.end(JSON.stringify(result, null, 2));
|
|
2139
|
+
}
|
|
2140
|
+
catch (err) {
|
|
2141
|
+
res.end(JSON.stringify({ success: false, error: String(err) }));
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
else if (url.startsWith("/api/globals/")) {
|
|
2145
|
+
// Inspect specific global API endpoint
|
|
2146
|
+
const globalName = decodeURIComponent(url.replace("/api/globals/", ""));
|
|
2147
|
+
if (!globalName) {
|
|
2148
|
+
res.end(JSON.stringify({ success: false, error: 'Global name required' }));
|
|
2149
|
+
}
|
|
2150
|
+
else {
|
|
2151
|
+
try {
|
|
2152
|
+
const result = await inspectGlobal(globalName);
|
|
2153
|
+
res.end(JSON.stringify(result, null, 2));
|
|
2154
|
+
}
|
|
2155
|
+
catch (err) {
|
|
2156
|
+
res.end(JSON.stringify({ success: false, error: String(err) }));
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
1079
2160
|
else if (url === "/api/tap-verifier/devices") {
|
|
1080
2161
|
const platform = params.get('platform') || 'android';
|
|
1081
2162
|
try {
|
|
@@ -1296,7 +2377,11 @@ function createRequestHandler() {
|
|
|
1296
2377
|
"/": "Dashboard",
|
|
1297
2378
|
"/logs": "Console logs (colored)",
|
|
1298
2379
|
"/network": "Network requests",
|
|
1299
|
-
"/
|
|
2380
|
+
"/bundle-errors": "Bundle/compilation errors",
|
|
2381
|
+
"/apps": "Connected apps with connection health",
|
|
2382
|
+
"/repl": "JavaScript REPL for code execution",
|
|
2383
|
+
"/component-tree": "React component tree viewer",
|
|
2384
|
+
"/globals": "Debug globals explorer",
|
|
1300
2385
|
"/tap-verifier": "Tap coordinate verification tool"
|
|
1301
2386
|
},
|
|
1302
2387
|
api: {
|
|
@@ -1305,6 +2390,11 @@ function createRequestHandler() {
|
|
|
1305
2390
|
"/api/network": "All captured network requests (JSON)",
|
|
1306
2391
|
"/api/bundle-errors": "Metro bundle/compilation errors (JSON)",
|
|
1307
2392
|
"/api/apps": "Connected React Native apps (JSON)",
|
|
2393
|
+
"/api/connection-status": "Connection states and context health for all apps",
|
|
2394
|
+
"/api/execute": "Execute JavaScript in the app (POST: expression, awaitPromise?)",
|
|
2395
|
+
"/api/component-tree": "Get React component tree (query: maxDepth, focusedOnly, structureOnly, includeProps)",
|
|
2396
|
+
"/api/globals": "List available debug globals",
|
|
2397
|
+
"/api/globals/:name": "Inspect a specific global object",
|
|
1308
2398
|
"/api/tap-verifier/devices": "List available devices (query: platform=android|ios)",
|
|
1309
2399
|
"/api/tap-verifier/screen-size": "Get device screen size (query: platform, deviceId)",
|
|
1310
2400
|
"/api/tap-verifier/screenshot": "Get device screenshot as base64 (query: platform, deviceId)",
|