reactoradar 1.6.1 → 1.6.3
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 +7 -7
- package/app.js +366 -24
- package/bin/setup.js +1 -1
- package/index.html +7 -2
- package/main.js +164 -7
- package/package.json +6 -6
- package/preload.js +4 -0
- package/styles.css +46 -1
package/README.md
CHANGED
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
|
|
28
28
|
### Console — Interactive Log Viewer
|
|
29
29
|
<p align="center">
|
|
30
|
-
<img src="https://raw.githubusercontent.com/sharanagouda/
|
|
30
|
+
<img src="https://raw.githubusercontent.com/sharanagouda/reactoradar/main/screenshots/consoleLogs.png" alt="Console Panel" width="800" />
|
|
31
31
|
</p>
|
|
32
32
|
|
|
33
33
|
*Collapsible object trees, multi-select level filters, log grouping, search, export as JSON, right-click to copy*
|
|
34
34
|
|
|
35
35
|
### Network — Chrome DevTools-style Inspector
|
|
36
36
|
<p align="center">
|
|
37
|
-
<img src="https://raw.githubusercontent.com/sharanagouda/
|
|
37
|
+
<img src="https://raw.githubusercontent.com/sharanagouda/reactoradar/main/screenshots/networkLogs.png" alt="Network Panel" width="800" />
|
|
38
38
|
</p>
|
|
39
39
|
|
|
40
40
|
*Resizable/sortable columns, slow API highlights, export as HAR, stats bar, hide unwanted URLs, throttling*
|
|
@@ -85,7 +85,7 @@ npx reactoradar # Launch the debugger
|
|
|
85
85
|
|
|
86
86
|
### Option B: Download .dmg
|
|
87
87
|
|
|
88
|
-
1. Download from [Releases](https://github.com/sharanagouda/
|
|
88
|
+
1. Download from [Releases](https://github.com/sharanagouda/reactoradar/releases)
|
|
89
89
|
2. Drag **ReactoRadar** to Applications
|
|
90
90
|
3. Install the SDK: `npx reactoradar setup` from your RN project
|
|
91
91
|
4. Open ReactoRadar from Applications
|
|
@@ -101,8 +101,8 @@ npm install -g reactoradar
|
|
|
101
101
|
### Option D: Build from source
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
git clone https://github.com/sharanagouda/
|
|
105
|
-
cd
|
|
104
|
+
git clone https://github.com/sharanagouda/reactoradar.git
|
|
105
|
+
cd reactoradar
|
|
106
106
|
npm install
|
|
107
107
|
npm start # dev mode
|
|
108
108
|
npm run build # build .dmg
|
|
@@ -314,8 +314,8 @@ If ReactoRadar helps your workflow, consider supporting development:
|
|
|
314
314
|
Contributions welcome! Fork → branch → PR.
|
|
315
315
|
|
|
316
316
|
```bash
|
|
317
|
-
git clone https://github.com/sharanagouda/
|
|
318
|
-
cd
|
|
317
|
+
git clone https://github.com/sharanagouda/reactoradar.git
|
|
318
|
+
cd reactoradar
|
|
319
319
|
npm install
|
|
320
320
|
npm start
|
|
321
321
|
```
|
package/app.js
CHANGED
|
@@ -203,6 +203,7 @@ function clearActiveTab() {
|
|
|
203
203
|
function clearAll() {
|
|
204
204
|
state.console.logs = [];
|
|
205
205
|
_consolePending = [];
|
|
206
|
+
_lastLogMsg = ''; _lastLogRow = null; _lastLogCount = 1;
|
|
206
207
|
state.network.requests = {};
|
|
207
208
|
state.network.order = [];
|
|
208
209
|
state.network.selectedId = null;
|
|
@@ -213,14 +214,31 @@ function clearAll() {
|
|
|
213
214
|
state.storage.entries = {};
|
|
214
215
|
state.storage.keys = [];
|
|
215
216
|
state.storage.selected = null;
|
|
217
|
+
// GA4
|
|
218
|
+
ga4State.events = [];
|
|
219
|
+
ga4State.selected = -1;
|
|
220
|
+
ga4State.searchFilter = '';
|
|
221
|
+
const ga4Search = $('ga4Search');
|
|
222
|
+
if (ga4Search) ga4Search.value = '';
|
|
223
|
+
const ga4Detail = $('ga4Detail');
|
|
224
|
+
if (ga4Detail) ga4Detail.innerHTML = '';
|
|
225
|
+
// Native logs
|
|
226
|
+
_nativeState.logs = [];
|
|
227
|
+
const nativeList = $('nativeLogList');
|
|
228
|
+
if (nativeList) nativeList.innerHTML = '';
|
|
229
|
+
// Badges
|
|
216
230
|
$('cBadge').textContent = '0';
|
|
217
231
|
$('nBadge').textContent = '0';
|
|
218
232
|
$('rBadge').textContent = '0';
|
|
219
233
|
$('sBadge').textContent = '0';
|
|
234
|
+
if ($('ga4Badge')) $('ga4Badge').textContent = '0';
|
|
235
|
+
if ($('nativeBadge')) $('nativeBadge').textContent = '0';
|
|
236
|
+
// Re-render all
|
|
220
237
|
renderConsole();
|
|
221
238
|
renderNetwork();
|
|
222
239
|
renderRedux();
|
|
223
240
|
renderStorage();
|
|
241
|
+
if (typeof renderGA4List === 'function') { renderGA4List(); renderGA4Summary(); }
|
|
224
242
|
}
|
|
225
243
|
|
|
226
244
|
// ─── CDP Button ───────────────────────────────────────────────────────────────
|
|
@@ -384,7 +402,7 @@ function _applyUpdateBanner() {
|
|
|
384
402
|
btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Download v' + latest + '</button>';
|
|
385
403
|
aboutEl.appendChild(btn);
|
|
386
404
|
$('updateBtn')?.addEventListener('click', () => {
|
|
387
|
-
window.electronAPI?.openExternal('https://github.com/sharanagouda/
|
|
405
|
+
window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar/releases');
|
|
388
406
|
});
|
|
389
407
|
}
|
|
390
408
|
}
|
|
@@ -658,6 +676,10 @@ const MAX_CONSOLE_LOGS = 5000;
|
|
|
658
676
|
|
|
659
677
|
function addConsoleLog(event) {
|
|
660
678
|
state.console.logs.push(event);
|
|
679
|
+
// Cap in-memory logs to prevent memory leak
|
|
680
|
+
if (state.console.logs.length > MAX_CONSOLE_LOGS) {
|
|
681
|
+
state.console.logs = state.console.logs.slice(-MAX_CONSOLE_LOGS);
|
|
682
|
+
}
|
|
661
683
|
_consolePending.push(event);
|
|
662
684
|
|
|
663
685
|
// Batch DOM updates via rAF — only one paint per frame
|
|
@@ -2038,9 +2060,15 @@ function initGA4Panel() {
|
|
|
2038
2060
|
$('ga4Clear').addEventListener('click', () => {
|
|
2039
2061
|
ga4State.events = [];
|
|
2040
2062
|
ga4State.selected = -1;
|
|
2063
|
+
ga4State.searchFilter = '';
|
|
2064
|
+
const search = $('ga4Search');
|
|
2065
|
+
if (search) search.value = '';
|
|
2041
2066
|
$('ga4Badge').textContent = '0';
|
|
2042
2067
|
renderGA4List();
|
|
2043
2068
|
renderGA4Summary();
|
|
2069
|
+
// Clear detail pane
|
|
2070
|
+
const detail = $('ga4Detail');
|
|
2071
|
+
if (detail) detail.innerHTML = '<div class="ga4-detail-empty" style="color:var(--text-dim);padding:20px;text-align:center;font-size:11px">Select an event to view details</div>';
|
|
2044
2072
|
});
|
|
2045
2073
|
|
|
2046
2074
|
$('ga4SortBtn').addEventListener('click', () => {
|
|
@@ -2154,7 +2182,7 @@ function renderGA4List() {
|
|
|
2154
2182
|
const row = document.createElement('div');
|
|
2155
2183
|
row.className = 'ga4-row' + (e.index === ga4State.selected ? ' selected' : '');
|
|
2156
2184
|
|
|
2157
|
-
const time = new Date(e.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
|
|
2185
|
+
const time = new Date(e.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
2158
2186
|
|
|
2159
2187
|
const evtColor = _ga4EventColor(e.name);
|
|
2160
2188
|
const colorStyle = evtColor ? `color:${evtColor}` : '';
|
|
@@ -2184,12 +2212,15 @@ function renderGA4List() {
|
|
|
2184
2212
|
}
|
|
2185
2213
|
|
|
2186
2214
|
function renderGA4Detail(e) {
|
|
2187
|
-
|
|
2215
|
+
let detail = $('ga4Detail');
|
|
2188
2216
|
if (!detail) return;
|
|
2189
2217
|
|
|
2190
|
-
const time = new Date(e.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
|
|
2218
|
+
const time = new Date(e.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
2191
2219
|
|
|
2192
|
-
|
|
2220
|
+
// Clone-replace to remove stale event listeners
|
|
2221
|
+
const fresh = detail.cloneNode(false);
|
|
2222
|
+
detail.parentNode.replaceChild(fresh, detail);
|
|
2223
|
+
detail = fresh;
|
|
2193
2224
|
|
|
2194
2225
|
// Header info
|
|
2195
2226
|
const header = document.createElement('div');
|
|
@@ -2748,8 +2779,8 @@ function initStoragePanel() {
|
|
|
2748
2779
|
<input id="storageSearch" class="net-search-input" placeholder="Filter keys..." />
|
|
2749
2780
|
</div>
|
|
2750
2781
|
</div>
|
|
2751
|
-
<div class="storage-layout">
|
|
2752
|
-
<div class="storage-keys">
|
|
2782
|
+
<div class="storage-layout" id="storageLayout">
|
|
2783
|
+
<div class="storage-keys" id="storageKeysPane">
|
|
2753
2784
|
<div class="panel-toolbar" style="height:32px">
|
|
2754
2785
|
<span style="font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px">Keys</span>
|
|
2755
2786
|
</div>
|
|
@@ -2757,10 +2788,11 @@ function initStoragePanel() {
|
|
|
2757
2788
|
<div class="empty-state" id="storageEmpty">
|
|
2758
2789
|
<div class="icon">💾</div>
|
|
2759
2790
|
<div class="label">No storage data</div>
|
|
2760
|
-
<div class="hint">
|
|
2791
|
+
<div class="hint">AsyncStorage data will appear here</div>
|
|
2761
2792
|
</div>
|
|
2762
2793
|
</div>
|
|
2763
2794
|
</div>
|
|
2795
|
+
<div class="storage-resize-handle" id="storageResizeHandle"></div>
|
|
2764
2796
|
<div class="storage-value-view">
|
|
2765
2797
|
<div class="storage-value-toolbar">
|
|
2766
2798
|
<span style="font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px">Value</span>
|
|
@@ -2776,6 +2808,35 @@ function initStoragePanel() {
|
|
|
2776
2808
|
state.storage.searchFilter = e.target.value.toLowerCase().trim();
|
|
2777
2809
|
renderStorage();
|
|
2778
2810
|
});
|
|
2811
|
+
|
|
2812
|
+
// Drag resize handle for key list width
|
|
2813
|
+
const handle = $('storageResizeHandle');
|
|
2814
|
+
const layout = $('storageLayout');
|
|
2815
|
+
const keysPane = $('storageKeysPane');
|
|
2816
|
+
if (handle && layout && keysPane) {
|
|
2817
|
+
let dragging = false;
|
|
2818
|
+
let startX = 0;
|
|
2819
|
+
let startW = 0;
|
|
2820
|
+
handle.addEventListener('mousedown', (e) => {
|
|
2821
|
+
e.preventDefault();
|
|
2822
|
+
dragging = true;
|
|
2823
|
+
startX = e.clientX;
|
|
2824
|
+
startW = keysPane.offsetWidth;
|
|
2825
|
+
document.body.style.cursor = 'col-resize';
|
|
2826
|
+
document.body.style.userSelect = 'none';
|
|
2827
|
+
});
|
|
2828
|
+
document.addEventListener('mousemove', (e) => {
|
|
2829
|
+
if (!dragging) return;
|
|
2830
|
+
const newW = Math.max(120, Math.min(600, startW + (e.clientX - startX)));
|
|
2831
|
+
layout.style.gridTemplateColumns = `${newW}px 4px 1fr`;
|
|
2832
|
+
});
|
|
2833
|
+
document.addEventListener('mouseup', () => {
|
|
2834
|
+
if (!dragging) return;
|
|
2835
|
+
dragging = false;
|
|
2836
|
+
document.body.style.cursor = '';
|
|
2837
|
+
document.body.style.userSelect = '';
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2779
2840
|
}
|
|
2780
2841
|
|
|
2781
2842
|
let _storageRAF = null;
|
|
@@ -2851,7 +2912,7 @@ function renderStorage() {
|
|
|
2851
2912
|
}
|
|
2852
2913
|
|
|
2853
2914
|
function renderStorageValue() {
|
|
2854
|
-
|
|
2915
|
+
let body = $('storageValueBody');
|
|
2855
2916
|
const keyLabel = $('storageSelectedKey');
|
|
2856
2917
|
if (!body) return;
|
|
2857
2918
|
const { selected, entries } = state.storage;
|
|
@@ -2861,7 +2922,29 @@ function renderStorageValue() {
|
|
|
2861
2922
|
return;
|
|
2862
2923
|
}
|
|
2863
2924
|
if (keyLabel) keyLabel.textContent = selected;
|
|
2864
|
-
|
|
2925
|
+
// Clone-replace to remove stale event listeners
|
|
2926
|
+
const fresh = body.cloneNode(false);
|
|
2927
|
+
body.parentNode.replaceChild(fresh, body);
|
|
2928
|
+
body = fresh;
|
|
2929
|
+
|
|
2930
|
+
let val = entries[selected];
|
|
2931
|
+
// Try to parse JSON strings into objects for tree display
|
|
2932
|
+
if (typeof val === 'string') {
|
|
2933
|
+
try { val = JSON.parse(val); } catch {}
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
if (val && typeof val === 'object') {
|
|
2937
|
+
body.appendChild(createTreeNode(null, val, false));
|
|
2938
|
+
body.addEventListener('contextmenu', (e) => {
|
|
2939
|
+
e.preventDefault();
|
|
2940
|
+
showContextMenu(e, [
|
|
2941
|
+
{ label: 'Copy Value', action: () => navigator.clipboard.writeText(JSON.stringify(val, null, 2)) },
|
|
2942
|
+
{ label: 'Copy Key', action: () => navigator.clipboard.writeText(selected) },
|
|
2943
|
+
]);
|
|
2944
|
+
});
|
|
2945
|
+
} else {
|
|
2946
|
+
body.innerHTML = renderJSON(val);
|
|
2947
|
+
}
|
|
2865
2948
|
}
|
|
2866
2949
|
|
|
2867
2950
|
function formatSize(bytes) {
|
|
@@ -2872,6 +2955,257 @@ function formatSize(bytes) {
|
|
|
2872
2955
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2873
2956
|
// REACT TREE PANEL
|
|
2874
2957
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2958
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2959
|
+
// NATIVE LOGS PANEL
|
|
2960
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2961
|
+
const _nativeState = { logs: [], connected: false, platform: null, levelFilter: 'all', searchFilter: '' };
|
|
2962
|
+
const MAX_NATIVE_LOGS = 2000;
|
|
2963
|
+
|
|
2964
|
+
function initNativeLogsPanel() {
|
|
2965
|
+
const panel = $('panel-native');
|
|
2966
|
+
if (!panel) return;
|
|
2967
|
+
panel.innerHTML = `
|
|
2968
|
+
<div class="panel-toolbar">
|
|
2969
|
+
<span class="panel-label">Native Logs</span>
|
|
2970
|
+
<span class="badge" id="nativeBadge">0</span>
|
|
2971
|
+
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
2972
|
+
<span class="native-status" id="nativeStatus">Detecting...</span>
|
|
2973
|
+
<button class="panel-clear-btn" id="nativeClear">Clear</button>
|
|
2974
|
+
</div>
|
|
2975
|
+
</div>
|
|
2976
|
+
<div class="native-connect-panel" id="nativeConnectPanel">
|
|
2977
|
+
<div class="native-hero">
|
|
2978
|
+
<div style="font-size:36px;opacity:0.15;margin-bottom:12px">📱</div>
|
|
2979
|
+
<div style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:6px">Native Logs</div>
|
|
2980
|
+
<div style="font-size:11px;color:var(--text-dim);max-width:420px;line-height:1.7;margin-bottom:20px">
|
|
2981
|
+
Stream native crash logs, errors, and warnings directly in ReactoRadar.<br/>
|
|
2982
|
+
No need to open Android Studio or Xcode.
|
|
2983
|
+
</div>
|
|
2984
|
+
<div class="native-platform-cards">
|
|
2985
|
+
<div class="native-card" id="nativeCardAndroid">
|
|
2986
|
+
<div class="native-card-icon">🤖</div>
|
|
2987
|
+
<div class="native-card-title">Android</div>
|
|
2988
|
+
<div class="native-card-hint">Requires: <code>adb</code> in PATH (Android SDK)</div>
|
|
2989
|
+
<div class="native-card-prereq">
|
|
2990
|
+
<div class="native-prereq-step"><b>Prerequisites:</b></div>
|
|
2991
|
+
<div class="native-prereq-step">1. Enable <b>Developer Options</b> on device<br/><span style="color:var(--text-dim);font-size:9px">Settings → About Phone → Tap Build Number 7 times</span></div>
|
|
2992
|
+
<div class="native-prereq-step">2. Enable <b>USB Debugging</b><br/><span style="color:var(--text-dim);font-size:9px">Settings → Developer Options → USB Debugging → ON</span></div>
|
|
2993
|
+
<div class="native-prereq-step">3. Connect device via USB and accept the prompt</div>
|
|
2994
|
+
<div class="native-prereq-step">4. Verify: run <code>adb devices</code> in terminal</div>
|
|
2995
|
+
</div>
|
|
2996
|
+
<div id="nativeAndroidStatus" class="native-detect-status"></div>
|
|
2997
|
+
<button class="native-connect-btn" id="nativeConnectAndroid">Connect Android</button>
|
|
2998
|
+
</div>
|
|
2999
|
+
<div class="native-card" id="nativeCardIOS">
|
|
3000
|
+
<div class="native-card-icon">🍎</div>
|
|
3001
|
+
<div class="native-card-title">iOS</div>
|
|
3002
|
+
<div class="native-card-hint">Simulator or USB device</div>
|
|
3003
|
+
<div class="native-card-prereq">
|
|
3004
|
+
<div class="native-prereq-step"><b>Simulator:</b></div>
|
|
3005
|
+
<div class="native-prereq-step">Requires Xcode Command Line Tools<br/><code>xcode-select --install</code></div>
|
|
3006
|
+
<div class="native-prereq-step" style="margin-top:6px"><b>Real Device (USB):</b></div>
|
|
3007
|
+
<div class="native-prereq-step">1. Install: <code>brew install libimobiledevice</code></div>
|
|
3008
|
+
<div class="native-prereq-step">2. Connect device, tap <b>Trust</b> on the prompt</div>
|
|
3009
|
+
<div class="native-prereq-step">3. Verify: <code>idevice_id -l</code> shows device UDID</div>
|
|
3010
|
+
</div>
|
|
3011
|
+
<div id="nativeIOSStatus" class="native-detect-status"></div>
|
|
3012
|
+
<div style="display:flex;gap:6px;margin-top:8px">
|
|
3013
|
+
<button class="native-connect-btn" id="nativeConnectIOSSim">Simulator</button>
|
|
3014
|
+
<button class="native-connect-btn" id="nativeConnectIOSDevice">USB Device</button>
|
|
3015
|
+
</div>
|
|
3016
|
+
</div>
|
|
3017
|
+
</div>
|
|
3018
|
+
</div>
|
|
3019
|
+
</div>
|
|
3020
|
+
<div class="native-logs-area" id="nativeLogsArea" style="display:none">
|
|
3021
|
+
<div class="native-filter-bar">
|
|
3022
|
+
<input id="nativeSearch" class="net-search-input" placeholder="Filter logs..." />
|
|
3023
|
+
<div class="native-level-filters" id="nativeLevelFilters">
|
|
3024
|
+
<button class="net-status-btn active" data-level="all">All</button>
|
|
3025
|
+
<button class="net-status-btn" data-level="fatal">Fatal</button>
|
|
3026
|
+
<button class="net-status-btn" data-level="error">Error</button>
|
|
3027
|
+
<button class="net-status-btn" data-level="warn">Warn</button>
|
|
3028
|
+
<button class="net-status-btn" data-level="info">Info</button>
|
|
3029
|
+
<button class="net-status-btn" data-level="debug">Debug</button>
|
|
3030
|
+
</div>
|
|
3031
|
+
<div style="margin-left:auto;display:flex;gap:6px;align-items:center">
|
|
3032
|
+
<button class="panel-clear-btn" id="nativeLogsClear">Clear</button>
|
|
3033
|
+
<button class="panel-clear-btn" id="nativeDisconnect" style="color:var(--red)">Disconnect</button>
|
|
3034
|
+
</div>
|
|
3035
|
+
</div>
|
|
3036
|
+
<div class="native-log-list" id="nativeLogList"></div>
|
|
3037
|
+
</div>`;
|
|
3038
|
+
|
|
3039
|
+
// Connect buttons
|
|
3040
|
+
$('nativeConnectAndroid')?.addEventListener('click', () => window.electronAPI?.startNativeLogs('android'));
|
|
3041
|
+
$('nativeConnectIOSSim')?.addEventListener('click', () => window.electronAPI?.startNativeLogs('ios-sim'));
|
|
3042
|
+
$('nativeConnectIOSDevice')?.addEventListener('click', () => window.electronAPI?.startNativeLogs('ios-device'));
|
|
3043
|
+
$('nativeDisconnect')?.addEventListener('click', () => window.electronAPI?.stopNativeLogs());
|
|
3044
|
+
|
|
3045
|
+
// Clear buttons (toolbar + logs area)
|
|
3046
|
+
$('nativeClear')?.addEventListener('click', _clearNativeLogs);
|
|
3047
|
+
$('nativeLogsClear')?.addEventListener('click', _clearNativeLogs);
|
|
3048
|
+
|
|
3049
|
+
// Level filter
|
|
3050
|
+
$('nativeLevelFilters')?.addEventListener('click', (e) => {
|
|
3051
|
+
const btn = e.target.closest('.net-status-btn');
|
|
3052
|
+
if (!btn) return;
|
|
3053
|
+
$('nativeLevelFilters').querySelectorAll('.net-status-btn').forEach(b => b.classList.remove('active'));
|
|
3054
|
+
btn.classList.add('active');
|
|
3055
|
+
_nativeState.levelFilter = btn.dataset.level;
|
|
3056
|
+
_renderNativeLogs();
|
|
3057
|
+
});
|
|
3058
|
+
|
|
3059
|
+
// Search
|
|
3060
|
+
$('nativeSearch')?.addEventListener('input', (e) => {
|
|
3061
|
+
_nativeState.searchFilter = e.target.value.toLowerCase().trim();
|
|
3062
|
+
_renderNativeLogs();
|
|
3063
|
+
});
|
|
3064
|
+
|
|
3065
|
+
// IPC: receive native logs
|
|
3066
|
+
window.electronAPI?.on('native-log', (log) => {
|
|
3067
|
+
if (!isTabEnabled('native')) return;
|
|
3068
|
+
_nativeState.logs.push(log);
|
|
3069
|
+
if (_nativeState.logs.length > MAX_NATIVE_LOGS) {
|
|
3070
|
+
_nativeState.logs = _nativeState.logs.slice(-MAX_NATIVE_LOGS);
|
|
3071
|
+
}
|
|
3072
|
+
$('nativeBadge').textContent = _nativeState.logs.length;
|
|
3073
|
+
_appendNativeLog(log);
|
|
3074
|
+
});
|
|
3075
|
+
|
|
3076
|
+
// IPC: connection status
|
|
3077
|
+
window.electronAPI?.on('native-status', (status) => {
|
|
3078
|
+
_nativeState.connected = status.connected;
|
|
3079
|
+
_nativeState.platform = status.platform || null;
|
|
3080
|
+
const statusEl = $('nativeStatus');
|
|
3081
|
+
const connectPanel = $('nativeConnectPanel');
|
|
3082
|
+
const logsArea = $('nativeLogsArea');
|
|
3083
|
+
|
|
3084
|
+
if (status.connected) {
|
|
3085
|
+
if (statusEl) { statusEl.textContent = `Connected (${status.platform})`; statusEl.style.color = 'var(--green)'; }
|
|
3086
|
+
if (connectPanel) connectPanel.style.display = 'none';
|
|
3087
|
+
if (logsArea) logsArea.style.display = 'flex';
|
|
3088
|
+
} else {
|
|
3089
|
+
if (statusEl) {
|
|
3090
|
+
statusEl.textContent = status.error || 'Not connected';
|
|
3091
|
+
statusEl.style.color = status.error ? 'var(--red)' : 'var(--text-dim)';
|
|
3092
|
+
}
|
|
3093
|
+
if (connectPanel) connectPanel.style.display = 'flex';
|
|
3094
|
+
if (logsArea) logsArea.style.display = 'none';
|
|
3095
|
+
}
|
|
3096
|
+
});
|
|
3097
|
+
|
|
3098
|
+
// Auto-detect platform and auto-connect
|
|
3099
|
+
_autoDetectNative();
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
function _clearNativeLogs() {
|
|
3103
|
+
_nativeState.logs = [];
|
|
3104
|
+
if ($('nativeBadge')) $('nativeBadge').textContent = '0';
|
|
3105
|
+
const list = $('nativeLogList');
|
|
3106
|
+
if (list) list.innerHTML = '';
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
async function _autoDetectNative() {
|
|
3110
|
+
const statusEl = $('nativeStatus');
|
|
3111
|
+
try {
|
|
3112
|
+
const result = await window.electronAPI?.detectNativePlatform();
|
|
3113
|
+
if (!result) { if (statusEl) { statusEl.textContent = 'Detection unavailable'; statusEl.style.color = 'var(--text-dim)'; } return; }
|
|
3114
|
+
|
|
3115
|
+
// Update card statuses
|
|
3116
|
+
const androidStatus = $('nativeAndroidStatus');
|
|
3117
|
+
const iosStatus = $('nativeIOSStatus');
|
|
3118
|
+
if (androidStatus) {
|
|
3119
|
+
if (result.android) { androidStatus.innerHTML = '<span style="color:var(--green)">Device detected</span>'; }
|
|
3120
|
+
else if (result.adbPath) { androidStatus.innerHTML = '<span style="color:var(--orange)">adb found — no device connected</span>'; }
|
|
3121
|
+
else { androidStatus.innerHTML = '<span style="color:var(--text-dim)">adb not found</span>'; }
|
|
3122
|
+
}
|
|
3123
|
+
if (iosStatus) {
|
|
3124
|
+
const parts = [];
|
|
3125
|
+
if (result.iosSim) parts.push('<span style="color:var(--green)">Simulator running</span>');
|
|
3126
|
+
if (result.iosDevice) parts.push('<span style="color:var(--green)">USB device detected</span>');
|
|
3127
|
+
if (!parts.length) parts.push('<span style="color:var(--text-dim)">No device detected</span>');
|
|
3128
|
+
iosStatus.innerHTML = parts.join(' · ');
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
// Show detection result — user clicks Connect to start
|
|
3132
|
+
if (result.android || result.iosSim || result.iosDevice) {
|
|
3133
|
+
const detected = [result.android ? 'Android' : '', result.iosSim ? 'iOS Sim' : '', result.iosDevice ? 'iOS Device' : ''].filter(Boolean).join(', ');
|
|
3134
|
+
if (statusEl) { statusEl.textContent = `Detected: ${detected} — click Connect to start`; statusEl.style.color = 'var(--accent)'; }
|
|
3135
|
+
} else {
|
|
3136
|
+
if (statusEl) { statusEl.textContent = 'No device detected'; statusEl.style.color = 'var(--text-dim)'; }
|
|
3137
|
+
}
|
|
3138
|
+
} catch {
|
|
3139
|
+
if (statusEl) { statusEl.textContent = 'Detection failed'; statusEl.style.color = 'var(--text-dim)'; }
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
function _appendNativeLog(log) {
|
|
3144
|
+
const list = $('nativeLogList');
|
|
3145
|
+
if (!list) return;
|
|
3146
|
+
|
|
3147
|
+
// Check filters
|
|
3148
|
+
if (_nativeState.levelFilter !== 'all' && log.level !== _nativeState.levelFilter) return;
|
|
3149
|
+
if (_nativeState.searchFilter && !log.message?.toLowerCase().includes(_nativeState.searchFilter) && !log.tag?.toLowerCase().includes(_nativeState.searchFilter)) return;
|
|
3150
|
+
|
|
3151
|
+
const isExpandable = log.level === 'error' || log.level === 'fatal' || (log.message || '').length > 200;
|
|
3152
|
+
const row = document.createElement('div');
|
|
3153
|
+
row.className = `native-log-row native-${log.level || 'info'}`;
|
|
3154
|
+
|
|
3155
|
+
const time = log.time || new Date(log.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
3156
|
+
|
|
3157
|
+
// Header line (always visible)
|
|
3158
|
+
const header = document.createElement('div');
|
|
3159
|
+
header.className = 'native-log-header';
|
|
3160
|
+
header.innerHTML = `<span class="native-log-time">${esc(time)}</span>`
|
|
3161
|
+
+ `<span class="native-log-level">${esc((log.level || 'info').toUpperCase())}</span>`
|
|
3162
|
+
+ (log.tag ? `<span class="native-log-tag">${esc(log.tag)}</span>` : '')
|
|
3163
|
+
+ `<span class="native-log-preview">${esc((log.message || '').split('\\n')[0].slice(0, 200))}</span>`;
|
|
3164
|
+
row.appendChild(header);
|
|
3165
|
+
|
|
3166
|
+
// Expandable full message (for errors and long messages)
|
|
3167
|
+
if (isExpandable) {
|
|
3168
|
+
const fullMsg = document.createElement('div');
|
|
3169
|
+
fullMsg.className = 'native-log-full';
|
|
3170
|
+
fullMsg.style.display = 'none';
|
|
3171
|
+
fullMsg.textContent = log.message || '';
|
|
3172
|
+
row.appendChild(fullMsg);
|
|
3173
|
+
|
|
3174
|
+
header.style.cursor = 'pointer';
|
|
3175
|
+
header.addEventListener('click', () => {
|
|
3176
|
+
const open = fullMsg.style.display !== 'none';
|
|
3177
|
+
fullMsg.style.display = open ? 'none' : 'block';
|
|
3178
|
+
row.classList.toggle('expanded', !open);
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// Right-click to copy
|
|
3183
|
+
row.addEventListener('contextmenu', (e) => {
|
|
3184
|
+
e.preventDefault();
|
|
3185
|
+
showContextMenu(e, [
|
|
3186
|
+
{ label: 'Copy Message', action: () => navigator.clipboard.writeText(log.message || '') },
|
|
3187
|
+
{ label: 'Copy Raw Line', action: () => navigator.clipboard.writeText(log.raw || log.message || '') },
|
|
3188
|
+
...(log.tag ? [{ label: `Copy Tag (${log.tag})`, action: () => navigator.clipboard.writeText(log.tag) }] : []),
|
|
3189
|
+
]);
|
|
3190
|
+
});
|
|
3191
|
+
|
|
3192
|
+
list.appendChild(row);
|
|
3193
|
+
|
|
3194
|
+
// Cap DOM rows
|
|
3195
|
+
while (list.children.length > 1000) list.firstChild.remove();
|
|
3196
|
+
|
|
3197
|
+
// Auto-scroll if near bottom
|
|
3198
|
+
const atBottom = (list.scrollHeight - list.scrollTop - list.clientHeight) < 150;
|
|
3199
|
+
if (atBottom) list.scrollTop = list.scrollHeight;
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
function _renderNativeLogs() {
|
|
3203
|
+
const list = $('nativeLogList');
|
|
3204
|
+
if (!list) return;
|
|
3205
|
+
list.innerHTML = '';
|
|
3206
|
+
_nativeState.logs.forEach(log => _appendNativeLog(log));
|
|
3207
|
+
}
|
|
3208
|
+
|
|
2875
3209
|
function initReactPanel() {
|
|
2876
3210
|
const panel = $('panel-react');
|
|
2877
3211
|
panel.innerHTML = `
|
|
@@ -2969,24 +3303,25 @@ function _updateHiddenBadge() {
|
|
|
2969
3303
|
|
|
2970
3304
|
// ─── Tab Visibility ──────────────────────────────────────────────────────────
|
|
2971
3305
|
const TAB_CONFIG = [
|
|
2972
|
-
{ id: 'console', label: 'Console',
|
|
2973
|
-
{ id: 'network', label: 'Network',
|
|
2974
|
-
{ id: 'redux', label: 'Redux',
|
|
2975
|
-
{ id: 'ga4', label: 'GA4 Events',
|
|
2976
|
-
{ id: 'storage', label: '
|
|
2977
|
-
{ id: 'memory', label: 'Memory',
|
|
2978
|
-
{ id: 'performance', label: 'Performance',
|
|
2979
|
-
{ id: 'react', label: 'React Tree',
|
|
3306
|
+
{ id: 'console', label: 'Console', icon: '🖥', essential: true },
|
|
3307
|
+
{ id: 'network', label: 'Network', icon: '📡', essential: true },
|
|
3308
|
+
{ id: 'redux', label: 'Redux', icon: '🔲', essential: false },
|
|
3309
|
+
{ id: 'ga4', label: 'GA4 Events', icon: '📊', essential: false },
|
|
3310
|
+
{ id: 'storage', label: 'AsyncStorage', icon: '💾', essential: false },
|
|
3311
|
+
{ id: 'memory', label: 'Memory', icon: '🧠', essential: false, defaultHidden: true },
|
|
3312
|
+
{ id: 'performance', label: 'Performance', icon: '⚡', essential: false, defaultHidden: true },
|
|
3313
|
+
{ id: 'react', label: 'React Tree', icon: '⚛️', essential: false },
|
|
3314
|
+
{ id: 'native', label: 'Native Logs', icon: '📱', essential: false, defaultHidden: true },
|
|
2980
3315
|
];
|
|
2981
3316
|
function getTabVisibility() {
|
|
2982
3317
|
try {
|
|
2983
3318
|
const saved = JSON.parse(localStorage.getItem('rn-debug-tab-visibility') || '{}');
|
|
2984
3319
|
const result = {};
|
|
2985
|
-
TAB_CONFIG.forEach(t => { result[t.id] = saved[t.id] !== undefined ? saved[t.id] :
|
|
3320
|
+
TAB_CONFIG.forEach(t => { result[t.id] = saved[t.id] !== undefined ? saved[t.id] : !t.defaultHidden; });
|
|
2986
3321
|
return result;
|
|
2987
3322
|
} catch {
|
|
2988
3323
|
const result = {};
|
|
2989
|
-
TAB_CONFIG.forEach(t => { result[t.id] =
|
|
3324
|
+
TAB_CONFIG.forEach(t => { result[t.id] = !t.defaultHidden; });
|
|
2990
3325
|
return result;
|
|
2991
3326
|
}
|
|
2992
3327
|
}
|
|
@@ -2996,7 +3331,13 @@ function setTabVisibility(vis) {
|
|
|
2996
3331
|
function getTabOrder() {
|
|
2997
3332
|
try {
|
|
2998
3333
|
const saved = JSON.parse(localStorage.getItem('rn-debug-tab-order') || '[]');
|
|
2999
|
-
if (saved.length
|
|
3334
|
+
if (saved.length) {
|
|
3335
|
+
// Merge: keep saved order, append any new tabs not in saved list
|
|
3336
|
+
const allIds = TAB_CONFIG.map(t => t.id);
|
|
3337
|
+
const merged = saved.filter(id => allIds.includes(id));
|
|
3338
|
+
allIds.forEach(id => { if (!merged.includes(id)) merged.push(id); });
|
|
3339
|
+
return merged;
|
|
3340
|
+
}
|
|
3000
3341
|
} catch {}
|
|
3001
3342
|
return TAB_CONFIG.map(t => t.id);
|
|
3002
3343
|
}
|
|
@@ -3353,10 +3694,10 @@ function initSettingsPanel() {
|
|
|
3353
3694
|
|
|
3354
3695
|
// About links
|
|
3355
3696
|
$('linkGithub')?.addEventListener('click', () => {
|
|
3356
|
-
window.electronAPI?.openExternal('https://github.com/sharanagouda/
|
|
3697
|
+
window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar');
|
|
3357
3698
|
});
|
|
3358
3699
|
$('linkDocs')?.addEventListener('click', () => {
|
|
3359
|
-
window.electronAPI?.openExternal('https://github.com/sharanagouda/
|
|
3700
|
+
window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar#readme');
|
|
3360
3701
|
});
|
|
3361
3702
|
$('linkLinkedIn')?.addEventListener('click', () => {
|
|
3362
3703
|
window.electronAPI?.openExternal('https://www.linkedin.com/in/sharanagoudamk/');
|
|
@@ -3463,7 +3804,7 @@ applyTabVisibility();
|
|
|
3463
3804
|
window.electronAPI?.setMetroPort(getStoredMetroPort());
|
|
3464
3805
|
|
|
3465
3806
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
3466
|
-
// SOURCES PANEL —
|
|
3807
|
+
// SOURCES PANEL (placeholder — use JS Debugger button for breakpoints)
|
|
3467
3808
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
3468
3809
|
function initSourcesPanel() {
|
|
3469
3810
|
const panel = $('panel-sources');
|
|
@@ -3944,4 +4285,5 @@ initMemoryPanel();
|
|
|
3944
4285
|
initReduxPanel();
|
|
3945
4286
|
initStoragePanel();
|
|
3946
4287
|
initReactPanel();
|
|
4288
|
+
initNativeLogsPanel();
|
|
3947
4289
|
initSettingsPanel();
|
package/bin/setup.js
CHANGED
|
@@ -465,7 +465,7 @@ ${SDK_MARKER_END}
|
|
|
465
465
|
console.log(' 1. Start the debugger: ' + C.cyan + 'open "/Applications/ReactoRadar.app"' + C.reset);
|
|
466
466
|
} else {
|
|
467
467
|
console.log(' 1. Start the debugger: ' + C.cyan + 'npx reactoradar' + C.reset);
|
|
468
|
-
console.log(' ' + C.dim + '(or download .dmg from https://github.com/sharanagouda/
|
|
468
|
+
console.log(' ' + C.dim + '(or download .dmg from https://github.com/sharanagouda/reactoradar/releases)' + C.reset);
|
|
469
469
|
}
|
|
470
470
|
console.log(' 2. Run your RN app: ' + C.cyan + 'npx react-native start --reset-cache' + C.reset);
|
|
471
471
|
console.log(' 3. Console, Network, Storage auto-connect');
|
package/index.html
CHANGED
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
<svg viewBox="0 0 20 20"><path d="M10 2v16M6 6l4-4 4 4M3 18h14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><circle cx="5" cy="13" r="2" stroke="currentColor" stroke-width="1.2" fill="none"/><circle cx="10" cy="9" r="2" stroke="currentColor" stroke-width="1.2" fill="none"/><circle cx="15" cy="12" r="2" stroke="currentColor" stroke-width="1.2" fill="none"/></svg>
|
|
48
48
|
<span>GA4</span>
|
|
49
49
|
</button>
|
|
50
|
-
<button class="nav-btn" data-panel="storage" title="
|
|
50
|
+
<button class="nav-btn" data-panel="storage" title="AsyncStorage">
|
|
51
51
|
<svg viewBox="0 0 20 20"><ellipse cx="10" cy="6" rx="7" ry="3" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M3 6v4c0 1.66 3.13 3 7 3s7-1.34 7-3V6" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M3 10v4c0 1.66 3.13 3 7 3s7-1.34 7-3v-4" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
|
|
52
|
-
<span>
|
|
52
|
+
<span>Async<br/>Storage</span>
|
|
53
53
|
</button>
|
|
54
54
|
<button class="nav-btn" data-panel="memory" title="Memory">
|
|
55
55
|
<svg viewBox="0 0 20 20"><rect x="3" y="8" width="3" height="8" rx="0.5" stroke="currentColor" stroke-width="1.2" fill="none"/><rect x="8.5" y="4" width="3" height="12" rx="0.5" stroke="currentColor" stroke-width="1.2" fill="none"/><rect x="14" y="6" width="3" height="10" rx="0.5" stroke="currentColor" stroke-width="1.2" fill="none"/></svg>
|
|
@@ -59,6 +59,10 @@
|
|
|
59
59
|
<svg viewBox="0 0 20 20"><polyline points="2,16 6,10 10,13 14,5 18,8" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
60
60
|
<span>Perf</span>
|
|
61
61
|
</button>
|
|
62
|
+
<button class="nav-btn" data-panel="native" title="Native Logs">
|
|
63
|
+
<svg viewBox="0 0 20 20"><path d="M4 4h12v12H4z" stroke="currentColor" stroke-width="1.5" fill="none" rx="2"/><path d="M7 8h6M7 11h4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><circle cx="14" cy="14" r="3" fill="var(--red)" stroke="currentColor" stroke-width="1"/><path d="M13 13l2 2M15 13l-2 2" stroke="#fff" stroke-width="1" stroke-linecap="round"/></svg>
|
|
64
|
+
<span>Native</span>
|
|
65
|
+
</button>
|
|
62
66
|
<button class="nav-btn" data-panel="react" title="React Tree">
|
|
63
67
|
<svg viewBox="0 0 20 20"><circle cx="10" cy="10" r="2" fill="currentColor"/><ellipse cx="10" cy="10" rx="8" ry="3.5" stroke="currentColor" stroke-width="1.5" fill="none"/><ellipse cx="10" cy="10" rx="8" ry="3.5" stroke="currentColor" stroke-width="1.5" fill="none" transform="rotate(60 10 10)"/><ellipse cx="10" cy="10" rx="8" ry="3.5" stroke="currentColor" stroke-width="1.5" fill="none" transform="rotate(120 10 10)"/></svg>
|
|
64
68
|
<span>React</span>
|
|
@@ -80,6 +84,7 @@
|
|
|
80
84
|
<div id="panel-redux" class="panel"></div>
|
|
81
85
|
<div id="panel-storage" class="panel"></div>
|
|
82
86
|
<div id="panel-react" class="panel"></div>
|
|
87
|
+
<div id="panel-native" class="panel"></div>
|
|
83
88
|
<div id="panel-settings" class="panel"></div>
|
|
84
89
|
</main>
|
|
85
90
|
|
package/main.js
CHANGED
|
@@ -20,6 +20,7 @@ const PORTS = {
|
|
|
20
20
|
// ─── Windows ──────────────────────────────────────────────────────────────────
|
|
21
21
|
let mainWindow = null;
|
|
22
22
|
let devtoolsWindow = null; // hosts the embedded CDP DevTools frontend
|
|
23
|
+
let _forceQuit = false;
|
|
23
24
|
|
|
24
25
|
// Safe IPC send — prevents "Object has been destroyed" crash
|
|
25
26
|
function _send(channel, ...args) {
|
|
@@ -76,14 +77,12 @@ if (gotLock) app.whenReady().then(async () => {
|
|
|
76
77
|
// Send version to renderer — try package.json, fallback to app.getVersion()
|
|
77
78
|
let appVersion;
|
|
78
79
|
try { appVersion = require('./package.json').version; } catch { appVersion = app.getVersion(); }
|
|
79
|
-
// Send multiple times to ensure renderer catches it
|
|
80
|
+
// Send multiple times to ensure renderer catches it (covers race conditions)
|
|
80
81
|
mainWindow.webContents.on('did-finish-load', () => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
}, delay);
|
|
82
|
+
// Send immediately + retries
|
|
83
|
+
_send('app-version', appVersion);
|
|
84
|
+
[500, 2000, 5000].forEach(delay => {
|
|
85
|
+
setTimeout(() => _send('app-version', appVersion), delay);
|
|
87
86
|
});
|
|
88
87
|
});
|
|
89
88
|
|
|
@@ -102,6 +101,7 @@ app.on('window-all-closed', () => {
|
|
|
102
101
|
});
|
|
103
102
|
|
|
104
103
|
app.on('before-quit', () => {
|
|
104
|
+
_forceQuit = true;
|
|
105
105
|
// Close all WS servers gracefully
|
|
106
106
|
if (reactDTServer) {
|
|
107
107
|
reactDTServer.close();
|
|
@@ -139,6 +139,26 @@ async function createMainWindow() {
|
|
|
139
139
|
mainWindow.webContents.on('did-finish-load', () => {
|
|
140
140
|
_send('ports', PORTS);
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
// Close confirmation dialog
|
|
144
|
+
mainWindow.on('close', (e) => {
|
|
145
|
+
if (_forceQuit) return;
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
dialog.showMessageBox(mainWindow, {
|
|
148
|
+
type: 'question',
|
|
149
|
+
buttons: ['Quit', 'Cancel'],
|
|
150
|
+
defaultId: 1,
|
|
151
|
+
title: 'Close ReactoRadar',
|
|
152
|
+
message: 'Are you sure you want to quit?',
|
|
153
|
+
detail: 'Active debug sessions will be disconnected.',
|
|
154
|
+
}).then(({ response }) => {
|
|
155
|
+
if (response === 0) {
|
|
156
|
+
_forceQuit = true;
|
|
157
|
+
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.close();
|
|
158
|
+
app.quit();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
// ─── Update Checker ──────────────────────────────────────────────────────────
|
|
@@ -589,6 +609,143 @@ function setupIPC() {
|
|
|
589
609
|
}
|
|
590
610
|
});
|
|
591
611
|
|
|
612
|
+
// ─── Native Log Streaming ──────────────────────────────────────────────────
|
|
613
|
+
let _nativeLogProcess = null;
|
|
614
|
+
|
|
615
|
+
// Auto-detect which native platform is available
|
|
616
|
+
ipcMain.handle('detect-native-platform', () => {
|
|
617
|
+
const { execSync } = require('child_process');
|
|
618
|
+
function tryCmd(cmd) { try { return execSync(cmd, { encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 5000 }).trim(); } catch { return ''; } }
|
|
619
|
+
|
|
620
|
+
const result = { android: false, iosSim: false, iosDevice: false, adbPath: false };
|
|
621
|
+
|
|
622
|
+
// Check adb
|
|
623
|
+
const adbCheck = tryCmd('which adb');
|
|
624
|
+
result.adbPath = !!adbCheck;
|
|
625
|
+
if (adbCheck) {
|
|
626
|
+
const devices = tryCmd('adb devices');
|
|
627
|
+
result.android = devices.includes('emulator') || /\b[A-Z0-9]{6,}\s+device\b/i.test(devices);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Check iOS simulator
|
|
631
|
+
const simCheck = tryCmd('xcrun simctl list devices booted 2>/dev/null');
|
|
632
|
+
result.iosSim = simCheck.includes('Booted');
|
|
633
|
+
|
|
634
|
+
// Check iOS device
|
|
635
|
+
const idevice = tryCmd('idevice_id -l 2>/dev/null');
|
|
636
|
+
result.iosDevice = !!(idevice && idevice.trim().length > 0);
|
|
637
|
+
|
|
638
|
+
return result;
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
ipcMain.on('start-native-logs', (_, platform) => {
|
|
642
|
+
// Kill existing process group
|
|
643
|
+
if (_nativeLogProcess) {
|
|
644
|
+
try { process.kill(-_nativeLogProcess.pid); } catch {}
|
|
645
|
+
try { _nativeLogProcess.kill(); } catch {}
|
|
646
|
+
_nativeLogProcess = null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const { spawn } = require('child_process');
|
|
650
|
+
let cmd, args;
|
|
651
|
+
|
|
652
|
+
if (platform === 'android') {
|
|
653
|
+
// adb logcat — filter for app-relevant logs
|
|
654
|
+
cmd = 'adb';
|
|
655
|
+
args = ['logcat', '-v', 'threadtime', '*:W']; // Warnings and above
|
|
656
|
+
} else if (platform === 'ios-sim') {
|
|
657
|
+
// xcrun simctl for iOS Simulator — use syslog style for parseable output
|
|
658
|
+
cmd = 'xcrun';
|
|
659
|
+
args = ['simctl', 'spawn', 'booted', 'log', 'stream', '--style', 'syslog', '--level', 'error'];
|
|
660
|
+
} else if (platform === 'ios-device') {
|
|
661
|
+
// idevicesyslog for real iOS device
|
|
662
|
+
cmd = 'idevicesyslog';
|
|
663
|
+
args = [];
|
|
664
|
+
} else {
|
|
665
|
+
_send('native-status', { connected: false, error: `Unknown platform: ${platform}` });
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
_nativeLogProcess = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], detached: true });
|
|
671
|
+
|
|
672
|
+
_send('native-status', { connected: true, platform });
|
|
673
|
+
|
|
674
|
+
let buffer = '';
|
|
675
|
+
_nativeLogProcess.stdout.on('data', (chunk) => {
|
|
676
|
+
buffer += chunk.toString();
|
|
677
|
+
const lines = buffer.split('\n');
|
|
678
|
+
buffer = lines.pop(); // keep incomplete line
|
|
679
|
+
lines.forEach(line => {
|
|
680
|
+
if (!line.trim()) return;
|
|
681
|
+
const parsed = _parseNativeLog(line, platform);
|
|
682
|
+
if (parsed) _send('native-log', parsed);
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
_nativeLogProcess.stderr.on('data', (chunk) => {
|
|
687
|
+
const text = chunk.toString().trim();
|
|
688
|
+
if (text) _send('native-log', { level: 'error', message: text, source: 'stderr', ts: Date.now() });
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
_nativeLogProcess.on('close', (code) => {
|
|
692
|
+
_nativeLogProcess = null;
|
|
693
|
+
_send('native-status', { connected: false, error: code ? `Process exited with code ${code}` : 'Disconnected' });
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
_nativeLogProcess.on('error', (err) => {
|
|
697
|
+
_nativeLogProcess = null;
|
|
698
|
+
_send('native-status', { connected: false, error: `Failed to start ${cmd}: ${err.message}. Is it installed?` });
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
} catch (e) {
|
|
702
|
+
_send('native-status', { connected: false, error: e.message });
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
ipcMain.on('stop-native-logs', () => {
|
|
707
|
+
if (_nativeLogProcess) {
|
|
708
|
+
try { _nativeLogProcess.kill(); } catch {}
|
|
709
|
+
_nativeLogProcess = null;
|
|
710
|
+
_send('native-status', { connected: false });
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
// Clean up on quit
|
|
715
|
+
app.on('before-quit', () => {
|
|
716
|
+
if (_nativeLogProcess) { try { _nativeLogProcess.kill(); } catch {} }
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
function _parseNativeLog(line, platform) {
|
|
720
|
+
if (platform === 'android') {
|
|
721
|
+
// Android logcat format: "06-05 10:30:45.123 1234 5678 E TAG: message"
|
|
722
|
+
const m = line.match(/^\d{2}-\d{2}\s+(\d{2}:\d{2}:\d{2})\.\d+\s+\d+\s+\d+\s+([VDIWEF])\s+([^:]+):\s*(.*)/);
|
|
723
|
+
if (m) {
|
|
724
|
+
const levelMap = { V: 'verbose', D: 'debug', I: 'info', W: 'warn', E: 'error', F: 'fatal' };
|
|
725
|
+
return { ts: Date.now(), time: m[1], level: levelMap[m[2]] || 'info', tag: m[3].trim(), message: m[4], raw: line };
|
|
726
|
+
}
|
|
727
|
+
return { ts: Date.now(), level: 'info', message: line, raw: line };
|
|
728
|
+
}
|
|
729
|
+
if (platform === 'ios-sim' || platform === 'ios-device') {
|
|
730
|
+
// syslog style: "2026-06-05 10:30:45.123456+0530 localhost process[pid]: (subsystem) [category] <Level>: message"
|
|
731
|
+
const m1 = line.match(/(\d{2}:\d{2}:\d{2})\.\d+[^\s]*\s+\S+\s+(\S+)\[\d+\].*?<(\w+)>:\s*(.*)/);
|
|
732
|
+
if (m1) {
|
|
733
|
+
const levelMap = { Notice: 'info', Info: 'info', Default: 'info', Debug: 'debug', Error: 'error', Fault: 'fatal' };
|
|
734
|
+
return { ts: Date.now(), time: m1[1], level: levelMap[m1[3]] || 'info', tag: m1[2], message: m1[4], raw: line };
|
|
735
|
+
}
|
|
736
|
+
// idevicesyslog format: "Jun 5 10:30:45 iPhone MyApp(libsystem)[123] <Error>: message"
|
|
737
|
+
const m2 = line.match(/\w+\s+\d+\s+(\d{2}:\d{2}:\d{2})\s+\S+\s+(\S+?)[\[(].*?<(\w+)>:\s*(.*)/);
|
|
738
|
+
if (m2) {
|
|
739
|
+
const levelMap = { Notice: 'info', Info: 'info', Debug: 'debug', Warning: 'warn', Error: 'error', Critical: 'fatal' };
|
|
740
|
+
return { ts: Date.now(), time: m2[1], level: levelMap[m2[3]] || 'info', tag: m2[2], message: m2[4], raw: line };
|
|
741
|
+
}
|
|
742
|
+
// Fallback
|
|
743
|
+
const timeMatch = line.match(/(\d{2}:\d{2}:\d{2})/);
|
|
744
|
+
return { ts: Date.now(), time: timeMatch ? timeMatch[1] : '', level: 'info', message: line, raw: line };
|
|
745
|
+
}
|
|
746
|
+
return { ts: Date.now(), level: 'info', message: line, raw: line };
|
|
747
|
+
}
|
|
748
|
+
|
|
592
749
|
ipcMain.on('set-theme', (_, theme) => {
|
|
593
750
|
nativeTheme.themeSource = theme === 'light' ? 'light' : 'dark';
|
|
594
751
|
const bg = theme === 'light' ? '#f5f6f8' : '#0a0b0e';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reactoradar",
|
|
3
3
|
"productName": "ReactoRadar",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.3",
|
|
5
5
|
"description": "macOS debugger for React Native — Console, Sources, Network, Performance, Memory, Redux, AsyncStorage, React tree. Supports RN 0.74+ with Hermes and New Architecture.",
|
|
6
6
|
"main": "main.js",
|
|
7
7
|
"bin": {
|
|
@@ -18,16 +18,16 @@
|
|
|
18
18
|
"network-inspector",
|
|
19
19
|
"hermes",
|
|
20
20
|
"flipper-alternative",
|
|
21
|
-
"
|
|
21
|
+
"reactoradar",
|
|
22
22
|
"macos"
|
|
23
23
|
],
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/sharanagouda/
|
|
26
|
+
"url": "git+https://github.com/sharanagouda/reactoradar.git"
|
|
27
27
|
},
|
|
28
|
-
"homepage": "https://github.com/sharanagouda/
|
|
28
|
+
"homepage": "https://github.com/sharanagouda/reactoradar#readme",
|
|
29
29
|
"bugs": {
|
|
30
|
-
"url": "https://github.com/sharanagouda/
|
|
30
|
+
"url": "https://github.com/sharanagouda/reactoradar/issues"
|
|
31
31
|
},
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"author": "sharanagouda",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
{
|
|
67
67
|
"provider": "github",
|
|
68
68
|
"owner": "sharanagouda",
|
|
69
|
-
"repo": "
|
|
69
|
+
"repo": "reactoradar"
|
|
70
70
|
}
|
|
71
71
|
],
|
|
72
72
|
"mac": {
|
package/preload.js
CHANGED
|
@@ -11,6 +11,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|
|
11
11
|
'ports', 'cdp-targets', 'redux-event', 'storage-event', 'network-event',
|
|
12
12
|
'console-event', 'perf-event', 'ga4-event', 'redux-connected', 'storage-connected', 'network-connected',
|
|
13
13
|
'react-dt-status', 'trigger-open-cdp', 'clear-all-ui', 'theme-changed', 'update-available', 'update-downloaded', 'app-version', 'focus-search',
|
|
14
|
+
'native-log', 'native-status',
|
|
14
15
|
];
|
|
15
16
|
if (allowed.includes(channel)) {
|
|
16
17
|
ipcRenderer.removeAllListeners(channel);
|
|
@@ -31,4 +32,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|
|
31
32
|
openExternal: (url) => ipcRenderer.send('open-external', url),
|
|
32
33
|
installUpdate: () => ipcRenderer.send('install-update'),
|
|
33
34
|
captureScreenshot: () => ipcRenderer.send('capture-screenshot'),
|
|
35
|
+
startNativeLogs: (platform) => ipcRenderer.send('start-native-logs', platform),
|
|
36
|
+
stopNativeLogs: () => ipcRenderer.send('stop-native-logs'),
|
|
37
|
+
detectNativePlatform: () => ipcRenderer.invoke('detect-native-platform'),
|
|
34
38
|
});
|
package/styles.css
CHANGED
|
@@ -1039,7 +1039,9 @@ body {
|
|
|
1039
1039
|
/* ─────────────────────────────────────────────────────────────────────────────
|
|
1040
1040
|
ASYNC STORAGE PANEL
|
|
1041
1041
|
───────────────────────────────────────────────────────────────────────────── */
|
|
1042
|
-
.storage-layout { display: grid; grid-template-columns: 240px 1fr; height: 100%; }
|
|
1042
|
+
.storage-layout { display: grid; grid-template-columns: 240px 4px 1fr; height: 100%; }
|
|
1043
|
+
.storage-resize-handle { background: var(--border); cursor: col-resize; transition: background 0.1s; }
|
|
1044
|
+
.storage-resize-handle:hover { background: var(--accent); }
|
|
1043
1045
|
.storage-keys { display: flex; flex-direction: column; border-right: 1px solid var(--border); overflow: hidden; }
|
|
1044
1046
|
.storage-keys-list { flex: 1; overflow-y: auto; }
|
|
1045
1047
|
.storage-keys-list::-webkit-scrollbar { width: 3px; }
|
|
@@ -1637,6 +1639,49 @@ mark { background: rgba(79,172,255,.2); color: var(--accent); border-radius: 2px
|
|
|
1637
1639
|
.net-hidden-unhide { font-size: 9px; background: transparent; border: 1px solid var(--border); color: var(--accent); border-radius: 3px; padding: 2px 6px; cursor: pointer; flex-shrink: 0; }
|
|
1638
1640
|
.net-hidden-unhide:hover { background: rgba(61,136,255,.1); }
|
|
1639
1641
|
|
|
1642
|
+
/* ── Native Logs Panel ─────────────────────────────────────────────────────── */
|
|
1643
|
+
.native-connect-panel { flex: 1; display: flex; align-items: center; justify-content: center; overflow-y: auto; padding: 24px; }
|
|
1644
|
+
.native-hero { text-align: center; max-width: 520px; width: 100%; }
|
|
1645
|
+
.native-platform-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 16px; }
|
|
1646
|
+
.native-card { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 16px 18px; text-align: left; display: flex; flex-direction: column; }
|
|
1647
|
+
.native-card-icon { font-size: 22px; margin-bottom: 8px; }
|
|
1648
|
+
.native-card-title { font-size: 12px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
|
|
1649
|
+
.native-card-hint { font-size: 10px; color: var(--text-dim); margin-bottom: 12px; }
|
|
1650
|
+
.native-card-hint code { background: var(--bg3); padding: 1px 4px; border-radius: 3px; color: var(--accent); }
|
|
1651
|
+
.native-card-prereq { font-size: 10px; color: var(--text-mid); line-height: 1.8; flex: 1; }
|
|
1652
|
+
.native-prereq-step { margin-bottom: 3px; padding-left: 4px; }
|
|
1653
|
+
.native-prereq-step code { background: var(--bg3); padding: 1px 4px; border-radius: 3px; color: var(--accent); font-size: 10px; }
|
|
1654
|
+
.native-connect-btn { margin-top: 12px; padding: 7px 18px; border-radius: 6px; border: 1px solid var(--accent); background: transparent; color: var(--accent); font-size: 11px; font-weight: 600; cursor: pointer; transition: all 0.12s; }
|
|
1655
|
+
.native-connect-btn:hover { background: var(--accent); color: #fff; }
|
|
1656
|
+
.native-status { font-size: 10px; color: var(--text-dim); }
|
|
1657
|
+
.native-logs-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
1658
|
+
.native-filter-bar { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-bottom: 1px solid var(--border); background: var(--bg2); }
|
|
1659
|
+
.native-level-filters { display: flex; gap: 4px; }
|
|
1660
|
+
.native-log-list { flex: 1; overflow-y: auto; font-size: 11px; line-height: 1.5; }
|
|
1661
|
+
.native-log-row { display: flex; gap: 8px; padding: 3px 12px; border-bottom: 1px solid var(--border); font-family: inherit; align-items: baseline; }
|
|
1662
|
+
.native-log-row:hover { background: var(--bg3); }
|
|
1663
|
+
.native-log-time { color: var(--text-dim); font-size: 10px; flex-shrink: 0; width: 65px; }
|
|
1664
|
+
.native-log-level { font-size: 9px; font-weight: 700; flex-shrink: 0; width: 42px; text-align: center; padding: 1px 0; border-radius: 3px; }
|
|
1665
|
+
.native-log-tag { color: var(--accent); font-size: 10px; font-weight: 600; flex-shrink: 0; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1666
|
+
.native-log-header { display: flex; gap: 8px; align-items: baseline; }
|
|
1667
|
+
.native-log-preview { color: var(--text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
1668
|
+
.native-log-full { display: none; padding: 6px 12px 8px 85px; font-size: 11px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; color: var(--text); background: var(--bg2); border-top: 1px solid var(--border); user-select: text; -webkit-user-select: text; }
|
|
1669
|
+
.native-log-row.expanded { background: var(--bg2); }
|
|
1670
|
+
.native-log-row.expanded .native-log-preview { white-space: normal; }
|
|
1671
|
+
.native-error .native-log-full, .native-fatal .native-log-full { color: var(--red); }
|
|
1672
|
+
.native-detect-status { font-size: 10px; margin: 8px 0 4px; min-height: 14px; }
|
|
1673
|
+
.native-verbose .native-log-level { color: var(--text-dim); background: var(--bg3); }
|
|
1674
|
+
.native-debug .native-log-level { color: var(--text-mid); background: var(--bg3); }
|
|
1675
|
+
.native-info .native-log-level { color: var(--green); background: rgba(61,214,140,.1); }
|
|
1676
|
+
.native-warn .native-log-level { color: var(--orange); background: rgba(255,165,0,.1); }
|
|
1677
|
+
.native-warn { background: rgba(255,165,0,.03); }
|
|
1678
|
+
.native-error .native-log-level { color: var(--red); background: rgba(255,94,114,.1); }
|
|
1679
|
+
.native-error { background: rgba(255,94,114,.04); }
|
|
1680
|
+
.native-error .native-log-msg { color: var(--red); }
|
|
1681
|
+
.native-fatal .native-log-level { color: #fff; background: var(--red); }
|
|
1682
|
+
.native-fatal { background: rgba(255,94,114,.08); }
|
|
1683
|
+
.native-fatal .native-log-msg { color: var(--red); font-weight: 700; }
|
|
1684
|
+
|
|
1640
1685
|
/* ── Support Button ────────────────────────────────────────────────────────── */
|
|
1641
1686
|
.support-btn { background: linear-gradient(135deg, #ff813f, #ff5e72); color: #fff; border: none; padding: 8px 20px; border-radius: 8px; font-size: 12px; font-weight: 700; cursor: pointer; transition: all 0.15s; letter-spacing: 0.3px; }
|
|
1642
1687
|
.support-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(255,94,114,.3); }
|