reactoradar 1.5.2 → 1.5.4
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/app.js +125 -40
- package/main.js +35 -5
- package/package.json +1 -1
- package/preload.js +1 -0
- package/sdk/RNDebugSDK.js +93 -35
- package/styles.css +11 -6
package/app.js
CHANGED
|
@@ -205,15 +205,19 @@ if (window.electronAPI) {
|
|
|
205
205
|
window.electronAPI.on('ports', ports => { state.ports = ports; });
|
|
206
206
|
|
|
207
207
|
window.electronAPI.on('cdp-targets', targets => {
|
|
208
|
-
|
|
209
|
-
$('btnCDP')
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
208
|
+
state.cdpTargets = targets;
|
|
209
|
+
const btn = $('btnCDP');
|
|
210
|
+
if (btn) {
|
|
211
|
+
const hasCDP = targets?.length > 0;
|
|
212
|
+
const port = state.ports?.METRO || getStoredMetroPort();
|
|
213
|
+
btn.textContent = hasCDP
|
|
214
|
+
? `JS Debugger (:${port}) [${targets.length}] ↗`
|
|
215
|
+
: `JS Debugger (:${port}) ↗`;
|
|
216
|
+
btn.style.opacity = hasCDP ? '1' : '0.5';
|
|
217
|
+
if (hasCDP) {
|
|
218
|
+
btn.onclick = () => window.electronAPI.openCDPTarget(targets[0].webSocketDebuggerUrl);
|
|
219
|
+
}
|
|
215
220
|
}
|
|
216
|
-
|
|
217
221
|
});
|
|
218
222
|
|
|
219
223
|
window.electronAPI.on('redux-event', handleReduxEvent);
|
|
@@ -264,21 +268,7 @@ if (window.electronAPI) {
|
|
|
264
268
|
window.electronAPI.on('update-available', ({ current, latest }) => {
|
|
265
269
|
// Show in settings only, not as a banner
|
|
266
270
|
state._updateAvailable = { current, latest };
|
|
267
|
-
|
|
268
|
-
if (el) el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${latest} available</span>`;
|
|
269
|
-
// Add update button in settings if not already there
|
|
270
|
-
if (!$('updateBtn')) {
|
|
271
|
-
const aboutEl = document.querySelector('.settings-about');
|
|
272
|
-
if (aboutEl) {
|
|
273
|
-
const btn = document.createElement('div');
|
|
274
|
-
btn.style.cssText = 'margin-top:10px';
|
|
275
|
-
btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px">Download v' + latest + '</button>';
|
|
276
|
-
aboutEl.appendChild(btn);
|
|
277
|
-
$('updateBtn')?.addEventListener('click', () => {
|
|
278
|
-
window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger/releases');
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
271
|
+
_applyUpdateBanner();
|
|
282
272
|
});
|
|
283
273
|
|
|
284
274
|
window.electronAPI.on('trigger-open-cdp', () => {
|
|
@@ -295,6 +285,31 @@ if (window.electronAPI) {
|
|
|
295
285
|
}
|
|
296
286
|
|
|
297
287
|
// ─── Device Connection Status (inline in titlebar) ───────────────────────────
|
|
288
|
+
// Reusable — called from IPC handler AND from initSettingsPanel
|
|
289
|
+
function _applyUpdateBanner() {
|
|
290
|
+
const info = state._updateAvailable;
|
|
291
|
+
if (!info) return;
|
|
292
|
+
const { current, latest } = info;
|
|
293
|
+
const el = $('aboutVersion');
|
|
294
|
+
if (el && !el.dataset.updateApplied) {
|
|
295
|
+
el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${latest} available</span>`;
|
|
296
|
+
el.dataset.updateApplied = '1';
|
|
297
|
+
}
|
|
298
|
+
// Add update button in settings if not already there
|
|
299
|
+
if (!$('updateBtn')) {
|
|
300
|
+
const aboutEl = document.querySelector('.settings-about');
|
|
301
|
+
if (aboutEl) {
|
|
302
|
+
const btn = document.createElement('div');
|
|
303
|
+
btn.style.cssText = 'margin-top:10px';
|
|
304
|
+
btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Download v' + latest + '</button>';
|
|
305
|
+
aboutEl.appendChild(btn);
|
|
306
|
+
$('updateBtn')?.addEventListener('click', () => {
|
|
307
|
+
window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger/releases');
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
298
313
|
function updateDeviceBanner(service, connected) {
|
|
299
314
|
state.connections[service] = connected;
|
|
300
315
|
const el = $('deviceStatus');
|
|
@@ -1269,9 +1284,13 @@ function renderNetwork() {
|
|
|
1269
1284
|
rows.appendChild(frag);
|
|
1270
1285
|
}
|
|
1271
1286
|
|
|
1287
|
+
function _isHttpError(r) {
|
|
1288
|
+
return r.phase === 'error' || (r.status && r.status >= 400);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1272
1291
|
function buildNetRow(r, wfMin, wfRange) {
|
|
1273
1292
|
const row = document.createElement('div');
|
|
1274
|
-
row.className = 'net-row' + (r.id === state.network.selectedId ? ' selected' : '') + (r
|
|
1293
|
+
row.className = 'net-row' + (r.id === state.network.selectedId ? ' selected' : '') + (_isHttpError(r) ? ' error' : '');
|
|
1275
1294
|
row.dataset.id = r.id;
|
|
1276
1295
|
|
|
1277
1296
|
const urlObj = tryURL(r.url);
|
|
@@ -1287,7 +1306,9 @@ function buildNetRow(r, wfMin, wfRange) {
|
|
|
1287
1306
|
const method = r.method || '?';
|
|
1288
1307
|
const mClass = ['GET','POST','PUT','PATCH','DELETE'].includes(method) ? `m-${method}` : 'm-other';
|
|
1289
1308
|
const fullPath = urlObj ? urlObj.pathname + urlObj.search : r.url || '';
|
|
1290
|
-
|
|
1309
|
+
const isErr = _isHttpError(r);
|
|
1310
|
+
const pathCls = isErr ? ' net-path-error' : '';
|
|
1311
|
+
nameCell.innerHTML = `<span class="method-badge ${mClass}">${method}</span> <span class="net-path${pathCls}" title="${esc(r.url)}">${esc(fullPath)}</span><span class="net-host">${esc(host)}</span>`;
|
|
1291
1312
|
row.appendChild(nameCell);
|
|
1292
1313
|
|
|
1293
1314
|
// Status
|
|
@@ -1297,7 +1318,13 @@ function buildNetRow(r, wfMin, wfRange) {
|
|
|
1297
1318
|
statusCell.style.width = NET_COLS[1].width + 'px';
|
|
1298
1319
|
let statusStr = '...', sCls = 's-pending';
|
|
1299
1320
|
if (r.phase === 'error') { statusStr = 'ERR'; sCls = 's-err'; }
|
|
1300
|
-
else if (r.status) {
|
|
1321
|
+
else if (r.status) {
|
|
1322
|
+
statusStr = String(r.status);
|
|
1323
|
+
const group = Math.floor(r.status / 100);
|
|
1324
|
+
// 1xx info, 2xx success, 3xx redirect, 4xx client error, 5xx server error
|
|
1325
|
+
if (group >= 4) sCls = 's-err';
|
|
1326
|
+
else sCls = `s-${group}`;
|
|
1327
|
+
}
|
|
1301
1328
|
statusCell.className += ` ${sCls}`;
|
|
1302
1329
|
statusCell.textContent = statusStr;
|
|
1303
1330
|
row.appendChild(statusCell);
|
|
@@ -1346,6 +1373,7 @@ function buildNetRow(r, wfMin, wfRange) {
|
|
|
1346
1373
|
const width = Math.max(2, ((r.duration || 50) / wfRange) * 100);
|
|
1347
1374
|
let barCls = 'pending';
|
|
1348
1375
|
if (r.phase === 'error') barCls = 'err';
|
|
1376
|
+
else if (r.status && r.status >= 400) barCls = 'err';
|
|
1349
1377
|
else if (r.status) barCls = `s${Math.floor(r.status/100)}`;
|
|
1350
1378
|
wfCell.innerHTML = `<div class="wf-bar ${barCls}" style="left:${left}%;width:${width}%"></div>`;
|
|
1351
1379
|
}
|
|
@@ -1412,8 +1440,12 @@ function renderNetDetailTabs(r) {
|
|
|
1412
1440
|
}
|
|
1413
1441
|
|
|
1414
1442
|
function renderNetDetailContent(r) {
|
|
1415
|
-
|
|
1443
|
+
let body = $('netDetailContent');
|
|
1416
1444
|
if (!body) return;
|
|
1445
|
+
// Clone-replace to remove all stale event listeners (prevents contextmenu leak)
|
|
1446
|
+
const fresh = body.cloneNode(false);
|
|
1447
|
+
body.parentNode.replaceChild(fresh, body);
|
|
1448
|
+
body = fresh;
|
|
1417
1449
|
const tab = r._tab || 'headers';
|
|
1418
1450
|
|
|
1419
1451
|
if (tab === 'headers') {
|
|
@@ -1432,7 +1464,7 @@ function renderNetDetailContent(r) {
|
|
|
1432
1464
|
<div class="kv-grid">
|
|
1433
1465
|
<span class="kv-key">Request URL</span><span class="kv-val">${esc(r.url)}</span>
|
|
1434
1466
|
<span class="kv-key">Method</span><span class="kv-val">${esc(r.method)}</span>
|
|
1435
|
-
<span class="kv-key">Status</span><span class="kv-val ${r.status ? 's-' + Math.floor(r.status/100) : 's-pending'}">${r.status || 'Pending'} ${r.statusText || ''}</span>
|
|
1467
|
+
<span class="kv-key">Status</span><span class="kv-val ${r.phase === 'error' ? 's-err' : r.status ? (r.status >= 400 ? 's-err' : 's-' + Math.floor(r.status/100)) : 's-pending'}">${r.phase === 'error' ? (r.status || 'ERR') : (r.status || 'Pending')} ${r.statusText || (r.phase === 'error' ? r.error || 'Network Error' : '')}</span>
|
|
1436
1468
|
</div>
|
|
1437
1469
|
${renderH('Response Headers', rsH)}
|
|
1438
1470
|
${renderH('Request Headers', rqH)}`;
|
|
@@ -1456,16 +1488,27 @@ function renderNetDetailContent(r) {
|
|
|
1456
1488
|
}
|
|
1457
1489
|
}
|
|
1458
1490
|
} else if (tab === 'preview') {
|
|
1459
|
-
|
|
1491
|
+
const isErrStatus = _isHttpError(r);
|
|
1492
|
+
if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
|
|
1460
1493
|
if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
|
|
1461
1494
|
// Render as collapsible JSON tree with right-click copy
|
|
1462
1495
|
const val = r.responseBody;
|
|
1463
1496
|
let treeData = val;
|
|
1464
1497
|
if (typeof val === 'string') {
|
|
1465
|
-
try { treeData = JSON.parse(val); } catch {
|
|
1498
|
+
try { treeData = JSON.parse(val); } catch {
|
|
1499
|
+
body.innerHTML = `<span style="color:${isErrStatus ? 'var(--red)' : 'inherit'}">${esc(val)}</span>`;
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1466
1502
|
}
|
|
1467
1503
|
if (treeData && typeof treeData === 'object') {
|
|
1468
1504
|
body.innerHTML = '';
|
|
1505
|
+
// Show error status banner above the response body
|
|
1506
|
+
if (isErrStatus) {
|
|
1507
|
+
const errBanner = document.createElement('div');
|
|
1508
|
+
errBanner.style.cssText = 'color:var(--red);font-weight:600;padding:4px 0 8px;font-size:11px;border-bottom:1px solid rgba(255,94,114,.15);margin-bottom:8px';
|
|
1509
|
+
errBanner.textContent = `${r.status || 'ERR'} ${r.statusText || r.error || 'Error'}`;
|
|
1510
|
+
body.appendChild(errBanner);
|
|
1511
|
+
}
|
|
1469
1512
|
body.appendChild(createTreeNode(null, treeData, false));
|
|
1470
1513
|
// Right-click on preview to copy the whole object or clicked node value
|
|
1471
1514
|
body.addEventListener('contextmenu', (e) => {
|
|
@@ -1473,12 +1516,27 @@ function renderNetDetailContent(r) {
|
|
|
1473
1516
|
showPreviewCopyMenu(e, treeData);
|
|
1474
1517
|
});
|
|
1475
1518
|
} else {
|
|
1476
|
-
body.innerHTML =
|
|
1519
|
+
body.innerHTML = isErrStatus
|
|
1520
|
+
? `<span style="color:var(--red)">${esc(String(r.responseBody))}</span>`
|
|
1521
|
+
: '<span style="color:var(--text-dim)">No preview available</span>';
|
|
1477
1522
|
}
|
|
1478
1523
|
} else if (tab === 'response') {
|
|
1479
|
-
|
|
1524
|
+
const isErrStatus = _isHttpError(r);
|
|
1525
|
+
if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
|
|
1480
1526
|
if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
|
|
1481
|
-
|
|
1527
|
+
if (isErrStatus) {
|
|
1528
|
+
const errBanner = document.createElement('div');
|
|
1529
|
+
errBanner.style.cssText = 'color:var(--red);font-weight:600;padding:4px 0 8px;font-size:11px;border-bottom:1px solid rgba(255,94,114,.15);margin-bottom:8px';
|
|
1530
|
+
errBanner.textContent = `${r.status || 'ERR'} ${r.statusText || r.error || 'Error'}`;
|
|
1531
|
+
body.innerHTML = '';
|
|
1532
|
+
body.appendChild(errBanner);
|
|
1533
|
+
const raw = document.createElement('div');
|
|
1534
|
+
raw.style.color = 'var(--red)';
|
|
1535
|
+
raw.innerHTML = renderJSON(r.responseBody);
|
|
1536
|
+
body.appendChild(raw);
|
|
1537
|
+
} else {
|
|
1538
|
+
body.innerHTML = renderJSON(r.responseBody);
|
|
1539
|
+
}
|
|
1482
1540
|
}
|
|
1483
1541
|
}
|
|
1484
1542
|
|
|
@@ -2149,9 +2207,9 @@ function initReactPanel() {
|
|
|
2149
2207
|
<div class="react-connect-hint" id="reactHint">
|
|
2150
2208
|
<div class="icon" style="font-size:40px;opacity:.2">⚛️</div>
|
|
2151
2209
|
<div class="label">React DevTools</div>
|
|
2152
|
-
<div class="hint">
|
|
2153
|
-
<div class="hint">React
|
|
2154
|
-
<button class="btn-launch" id="btnReactDT">Open React DevTools ↗</button>
|
|
2210
|
+
<div class="hint">Opens as a separate window connected to your app via port 8097</div>
|
|
2211
|
+
<div class="hint" style="margin-top:8px;color:var(--yellow)">Note: The RN inspector overlay won't work while React DevTools is connected. Close the DevTools window to use the built-in inspector.</div>
|
|
2212
|
+
<button class="btn-launch" id="btnReactDT" style="margin-top:12px">Open React DevTools ↗</button>
|
|
2155
2213
|
</div>
|
|
2156
2214
|
</div>`;
|
|
2157
2215
|
|
|
@@ -2182,6 +2240,12 @@ function getStoredAppName() {
|
|
|
2182
2240
|
function setStoredAppName(n) {
|
|
2183
2241
|
try { localStorage.setItem('rn-debug-appname', n); } catch {}
|
|
2184
2242
|
}
|
|
2243
|
+
function getStoredMetroPort() {
|
|
2244
|
+
try { return parseInt(localStorage.getItem('rn-debug-metro-port')) || 8081; } catch { return 8081; }
|
|
2245
|
+
}
|
|
2246
|
+
function setStoredMetroPort(p) {
|
|
2247
|
+
try { localStorage.setItem('rn-debug-metro-port', String(p)); } catch {}
|
|
2248
|
+
}
|
|
2185
2249
|
function applyAppName(name) {
|
|
2186
2250
|
const logo = document.querySelector('.logo');
|
|
2187
2251
|
if (logo) {
|
|
@@ -2280,10 +2344,11 @@ function initSettingsPanel() {
|
|
|
2280
2344
|
</div>
|
|
2281
2345
|
</div>
|
|
2282
2346
|
<div class="settings-row">
|
|
2283
|
-
<div>
|
|
2284
|
-
<div class="settings-label">Metro Bundler</div>
|
|
2285
|
-
<div class="settings-hint">CDP target discovery
|
|
2347
|
+
<div style="display:flex;flex-direction:column;gap:2px">
|
|
2348
|
+
<div class="settings-label">Metro Bundler Port</div>
|
|
2349
|
+
<div class="settings-hint">Port for CDP target discovery (default: 8081)</div>
|
|
2286
2350
|
</div>
|
|
2351
|
+
<input id="metroPortInput" type="number" class="net-search-input" style="width:70px;text-align:center" value="${getStoredMetroPort()}" />
|
|
2287
2352
|
</div>
|
|
2288
2353
|
</div>
|
|
2289
2354
|
<div class="settings-section">
|
|
@@ -2334,6 +2399,7 @@ function initSettingsPanel() {
|
|
|
2334
2399
|
<div class="about-links" style="display:flex;gap:16px;justify-content:center">
|
|
2335
2400
|
<span class="about-link" id="linkGithub">GitHub</span>
|
|
2336
2401
|
<span class="about-link" id="linkDocs">Documentation</span>
|
|
2402
|
+
<span class="about-link" id="linkLinkedIn">Developer LinkedIn</span>
|
|
2337
2403
|
</div>
|
|
2338
2404
|
</div>
|
|
2339
2405
|
</div>
|
|
@@ -2383,6 +2449,9 @@ function initSettingsPanel() {
|
|
|
2383
2449
|
$('linkDocs')?.addEventListener('click', () => {
|
|
2384
2450
|
window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger#readme');
|
|
2385
2451
|
});
|
|
2452
|
+
$('linkLinkedIn')?.addEventListener('click', () => {
|
|
2453
|
+
window.electronAPI?.openExternal('https://www.linkedin.com/in/sharanagoudamk/');
|
|
2454
|
+
});
|
|
2386
2455
|
|
|
2387
2456
|
// App name
|
|
2388
2457
|
$('appNameInput').addEventListener('change', (e) => {
|
|
@@ -2396,6 +2465,15 @@ function initSettingsPanel() {
|
|
|
2396
2465
|
applyAppName('ReactoRadar');
|
|
2397
2466
|
});
|
|
2398
2467
|
|
|
2468
|
+
// Metro Port
|
|
2469
|
+
$('metroPortInput')?.addEventListener('change', (e) => {
|
|
2470
|
+
let port = parseInt(e.target.value.trim());
|
|
2471
|
+
if (isNaN(port) || port < 1024 || port > 65535) port = 8081;
|
|
2472
|
+
e.target.value = port;
|
|
2473
|
+
setStoredMetroPort(port);
|
|
2474
|
+
window.electronAPI?.setMetroPort(port);
|
|
2475
|
+
});
|
|
2476
|
+
|
|
2399
2477
|
// Font size controls
|
|
2400
2478
|
$('fontSizeDown').addEventListener('click', () => {
|
|
2401
2479
|
let size = getStoredFontSize();
|
|
@@ -2409,6 +2487,9 @@ function initSettingsPanel() {
|
|
|
2409
2487
|
setStoredFontSize(size);
|
|
2410
2488
|
applyFontSize(size);
|
|
2411
2489
|
});
|
|
2490
|
+
|
|
2491
|
+
// Apply update banner if update info arrived before settings panel was created
|
|
2492
|
+
_applyUpdateBanner();
|
|
2412
2493
|
}
|
|
2413
2494
|
|
|
2414
2495
|
// Apply saved theme + font size + app name on load
|
|
@@ -2416,6 +2497,9 @@ applyTheme(getStoredTheme());
|
|
|
2416
2497
|
applyFontSize(getStoredFontSize());
|
|
2417
2498
|
applyAppName(getStoredAppName());
|
|
2418
2499
|
|
|
2500
|
+
// Send stored metro port to backend
|
|
2501
|
+
window.electronAPI?.setMetroPort(getStoredMetroPort());
|
|
2502
|
+
|
|
2419
2503
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2420
2504
|
// SOURCES PANEL — CDP-based file browser + breakpoints
|
|
2421
2505
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -2667,7 +2751,8 @@ async function loadSourceFile(filepath) {
|
|
|
2667
2751
|
// Strategy 2: Fetch from Metro
|
|
2668
2752
|
if (!source) {
|
|
2669
2753
|
try {
|
|
2670
|
-
const
|
|
2754
|
+
const port = getStoredMetroPort();
|
|
2755
|
+
const resp = await fetch(`http://localhost:${port}/${filepath}?platform=ios&dev=true`);
|
|
2671
2756
|
if (resp.ok) source = await resp.text();
|
|
2672
2757
|
} catch {}
|
|
2673
2758
|
}
|
package/main.js
CHANGED
|
@@ -55,7 +55,8 @@ app.whenReady().then(async () => {
|
|
|
55
55
|
// Check for updates (non-blocking)
|
|
56
56
|
checkForUpdates();
|
|
57
57
|
startBridgeServers();
|
|
58
|
-
|
|
58
|
+
// React DevTools relay NOT started by default — it blocks RN's built-in inspector.
|
|
59
|
+
// Started on-demand when user clicks React tab or Cmd+R.
|
|
59
60
|
setupMetroCDPProxy();
|
|
60
61
|
setupIPC();
|
|
61
62
|
buildMenu();
|
|
@@ -106,6 +107,18 @@ async function createMainWindow() {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// ─── Update Checker ──────────────────────────────────────────────────────────
|
|
110
|
+
function _semverCompare(a, b) {
|
|
111
|
+
// Returns 1 if a > b, -1 if a < b, 0 if equal
|
|
112
|
+
const pa = (a || '').split('.').map(Number);
|
|
113
|
+
const pb = (b || '').split('.').map(Number);
|
|
114
|
+
for (let i = 0; i < 3; i++) {
|
|
115
|
+
const va = pa[i] || 0, vb = pb[i] || 0;
|
|
116
|
+
if (va > vb) return 1;
|
|
117
|
+
if (va < vb) return -1;
|
|
118
|
+
}
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
109
122
|
function checkForUpdates() {
|
|
110
123
|
const currentVersion = require('./package.json').version;
|
|
111
124
|
https.get('https://registry.npmjs.org/reactoradar/latest', (res) => {
|
|
@@ -114,9 +127,16 @@ function checkForUpdates() {
|
|
|
114
127
|
res.on('end', () => {
|
|
115
128
|
try {
|
|
116
129
|
const latest = JSON.parse(data).version;
|
|
117
|
-
if (latest && latest
|
|
118
|
-
//
|
|
119
|
-
|
|
130
|
+
if (latest && _semverCompare(latest, currentVersion) > 0) {
|
|
131
|
+
// Send with retries to ensure renderer catches it after did-finish-load
|
|
132
|
+
const payload = { current: currentVersion, latest };
|
|
133
|
+
[500, 2000, 5000].forEach(delay => {
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
136
|
+
mainWindow.webContents.send('update-available', payload);
|
|
137
|
+
}
|
|
138
|
+
}, delay);
|
|
139
|
+
});
|
|
120
140
|
console.log(`[Update] New version available: ${latest} (current: ${currentVersion})`);
|
|
121
141
|
}
|
|
122
142
|
} catch {}
|
|
@@ -317,6 +337,8 @@ function setupIPC() {
|
|
|
317
337
|
});
|
|
318
338
|
|
|
319
339
|
ipcMain.on('open-react-devtools', () => {
|
|
340
|
+
// Start the relay server if not already running
|
|
341
|
+
if (!reactDTServer) startReactDevToolsServer();
|
|
320
342
|
// Open standalone react-devtools window
|
|
321
343
|
const rdtWin = new BrowserWindow({
|
|
322
344
|
width: 1100,
|
|
@@ -332,6 +354,14 @@ function setupIPC() {
|
|
|
332
354
|
|
|
333
355
|
// clear-all is handled by renderer via clear-all-ui IPC from menu
|
|
334
356
|
|
|
357
|
+
ipcMain.on('set-metro-port', (_, port) => {
|
|
358
|
+
const p = parseInt(port);
|
|
359
|
+
if (isNaN(p) || p < 1024 || p > 65535) return;
|
|
360
|
+
PORTS.METRO = p;
|
|
361
|
+
fetchCDPTargets();
|
|
362
|
+
mainWindow?.webContents.send('ports', PORTS);
|
|
363
|
+
});
|
|
364
|
+
|
|
335
365
|
ipcMain.on('set-network-capture', (_, enabled) => {
|
|
336
366
|
// Broadcast to connected RN apps so they can stop/start intercepting
|
|
337
367
|
networkClients.forEach(ws => {
|
|
@@ -391,7 +421,7 @@ function setupIPC() {
|
|
|
391
421
|
// Also try to detect from Metro's /json endpoint
|
|
392
422
|
try {
|
|
393
423
|
const result = require('child_process').execSync(
|
|
394
|
-
|
|
424
|
+
`lsof -i :${PORTS.METRO} -t 2>/dev/null | head -1 | xargs -I{} lsof -p {} -Fn 2>/dev/null | grep '^n/' | grep 'node_modules' | head -1 | sed 's|^n||;s|/node_modules.*||'`,
|
|
395
425
|
{ encoding: 'utf8', timeout: 3000 }
|
|
396
426
|
).trim();
|
|
397
427
|
if (result && fs.existsSync(result)) candidates.unshift(result);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reactoradar",
|
|
3
3
|
"productName": "ReactoRadar",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.4",
|
|
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": {
|
package/preload.js
CHANGED
|
@@ -26,6 +26,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|
|
26
26
|
setNetworkCapture: (enabled) => ipcRenderer.send('set-network-capture', enabled),
|
|
27
27
|
setStackTraceCapture: (enabled) => ipcRenderer.send('set-stack-trace-capture', enabled),
|
|
28
28
|
setNetworkThrottle: (profile) => ipcRenderer.send('set-network-throttle', profile),
|
|
29
|
+
setMetroPort: (port) => ipcRenderer.send('set-metro-port', port),
|
|
29
30
|
readSourceFile: (filepath) => ipcRenderer.invoke('read-source-file', filepath),
|
|
30
31
|
openExternal: (url) => ipcRenderer.send('open-external', url),
|
|
31
32
|
});
|
package/sdk/RNDebugSDK.js
CHANGED
|
@@ -34,6 +34,49 @@ let _stackTraceEnabled = false; // Disabled by default for performance
|
|
|
34
34
|
let _throttleProfile = 'none'; // 'none', 'fast3g', 'slow3g', 'offline'
|
|
35
35
|
const THROTTLE_DELAYS = { none: 0, fast3g: 500, slow3g: 2000, offline: -1 };
|
|
36
36
|
|
|
37
|
+
// ─── SDK Pause/Resume (allows inspector to work without SDK interference) ────
|
|
38
|
+
// When paused, console/fetch/XHR interception is disabled so the RN inspector
|
|
39
|
+
// and CDP debugger can work without conflicts. Controlled via the debugger app.
|
|
40
|
+
let _sdkPaused = false;
|
|
41
|
+
|
|
42
|
+
function _isSDKActive() {
|
|
43
|
+
return !_sdkPaused;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Debugger Detection ──────────────────────────────────────────────────────
|
|
47
|
+
// Detect if a CDP debugger (Chrome DevTools / Hermes inspector) is attached.
|
|
48
|
+
// When detected, we back off our patches to avoid conflicts with the inspector.
|
|
49
|
+
let _debuggerDetected = false;
|
|
50
|
+
let _debuggerCheckInterval = null;
|
|
51
|
+
|
|
52
|
+
function _checkDebuggerAttached() {
|
|
53
|
+
// Method 1: Check if Hermes debugger globals are set
|
|
54
|
+
const hermesDebugger = !!(global.__DEBUGGER_CONNECTED__ || global.__HERMES_DEBUGGER_CONNECTED__);
|
|
55
|
+
// Method 2: Check React DevTools hook for debugger attachment
|
|
56
|
+
const rdtHook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
57
|
+
const rdtDebugger = !!(rdtHook && rdtHook._debuggerAttached);
|
|
58
|
+
// Method 3: Check if CDP is connected via the inspector agent
|
|
59
|
+
const inspectorConnected = !!(global.__inspectorGlobalObject || global.__inspector);
|
|
60
|
+
|
|
61
|
+
const wasDetected = _debuggerDetected;
|
|
62
|
+
_debuggerDetected = hermesDebugger || rdtDebugger || inspectorConnected;
|
|
63
|
+
|
|
64
|
+
if (_debuggerDetected && !wasDetected) {
|
|
65
|
+
_console.log('[RNDebugSDK] Debugger detected — SDK interception paused to avoid inspector conflicts. Use the ReactoRadar app to resume.');
|
|
66
|
+
} else if (!_debuggerDetected && wasDetected) {
|
|
67
|
+
_console.log('[RNDebugSDK] Debugger disconnected — SDK interception resumed.');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check periodically (every 3s) — lightweight, no performance impact
|
|
72
|
+
_debuggerCheckInterval = setInterval(_checkDebuggerAttached, 3000);
|
|
73
|
+
// Also check once immediately after a short delay (debugger may attach during startup)
|
|
74
|
+
setTimeout(_checkDebuggerAttached, 1000);
|
|
75
|
+
|
|
76
|
+
function _shouldIntercept() {
|
|
77
|
+
return _isSDKActive() && !_debuggerDetected;
|
|
78
|
+
}
|
|
79
|
+
|
|
37
80
|
// ─── WebSocket Factory ────────────────────────────────────────────────────────
|
|
38
81
|
function makeChannel(port, name, onMessage) {
|
|
39
82
|
let ws = null, queue = [], connected = false;
|
|
@@ -72,6 +115,21 @@ const mainCh = makeChannel(PORTS.NETWORK_AND_CONSOLE, 'main', (msg) => {
|
|
|
72
115
|
if (msg.action === 'set-network-capture') _networkCaptureEnabled = !!msg.enabled;
|
|
73
116
|
if (msg.action === 'set-throttle') _throttleProfile = msg.profile || 'none';
|
|
74
117
|
if (msg.action === 'set-stack-trace') _stackTraceEnabled = !!msg.enabled;
|
|
118
|
+
// Pause/Resume SDK interception (allows inspector to work)
|
|
119
|
+
if (msg.action === 'pause-sdk') {
|
|
120
|
+
_sdkPaused = true;
|
|
121
|
+
_console.log('[RNDebugSDK] SDK paused — inspector/debugger can now inspect the app freely.');
|
|
122
|
+
mainCh.send({ type: 'control', action: 'sdk-status', paused: true });
|
|
123
|
+
}
|
|
124
|
+
if (msg.action === 'resume-sdk') {
|
|
125
|
+
_sdkPaused = false;
|
|
126
|
+
_console.log('[RNDebugSDK] SDK resumed — interception re-enabled.');
|
|
127
|
+
mainCh.send({ type: 'control', action: 'sdk-status', paused: false });
|
|
128
|
+
}
|
|
129
|
+
// Query current status
|
|
130
|
+
if (msg.action === 'query-sdk-status') {
|
|
131
|
+
mainCh.send({ type: 'control', action: 'sdk-status', paused: _sdkPaused, debuggerDetected: _debuggerDetected });
|
|
132
|
+
}
|
|
75
133
|
}
|
|
76
134
|
});
|
|
77
135
|
const reduxCh = makeChannel(PORTS.REDUX, 'redux');
|
|
@@ -130,6 +188,9 @@ LEVELS.forEach(level => {
|
|
|
130
188
|
_console[level] = console[level].bind(console);
|
|
131
189
|
console[level] = (...args) => {
|
|
132
190
|
_console[level](...args);
|
|
191
|
+
// Skip interception when SDK is paused or debugger is attached
|
|
192
|
+
// This prevents double-logging and message queue deadlocks with CDP
|
|
193
|
+
if (!_shouldIntercept()) return;
|
|
133
194
|
const structuredArgs = args.map(serializeArg);
|
|
134
195
|
const message = args.map(a => {
|
|
135
196
|
if (typeof a === 'string') return a;
|
|
@@ -167,6 +228,10 @@ function _flattenHeaders(h) {
|
|
|
167
228
|
// ─── Fetch Intercept ─────────────────────────────────────────────────────────
|
|
168
229
|
const _fetch = global.fetch;
|
|
169
230
|
global.fetch = async (input, init = {}) => {
|
|
231
|
+
// When SDK is paused or debugger is attached, pass through without interception
|
|
232
|
+
// This prevents racing with CDP's own Fetch.enable domain
|
|
233
|
+
if (!_shouldIntercept()) return _fetch(input, init);
|
|
234
|
+
|
|
170
235
|
// Throttle: simulate slow network or offline
|
|
171
236
|
const delay = THROTTLE_DELAYS[_throttleProfile] || 0;
|
|
172
237
|
if (delay === -1) return Promise.reject(new TypeError('Network request failed (offline throttle)'));
|
|
@@ -236,24 +301,24 @@ global.fetch = async (input, init = {}) => {
|
|
|
236
301
|
return _setHeader.apply(xhr, arguments);
|
|
237
302
|
};
|
|
238
303
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
304
|
+
// Wrap send
|
|
305
|
+
const _send = xhr.send.bind(xhr);
|
|
306
|
+
xhr.send = function(body) {
|
|
307
|
+
if (_shouldIntercept() && _networkCaptureEnabled && !meta.sent) {
|
|
308
|
+
meta.sent = true;
|
|
309
|
+
let reqBody = null;
|
|
310
|
+
if (body != null) {
|
|
311
|
+
try { reqBody = typeof body === 'string' ? body : JSON.parse(JSON.stringify(body)); } catch { reqBody = String(body); }
|
|
312
|
+
}
|
|
313
|
+
mainCh.send({ type: 'network', phase: 'request', id: meta.id, url: meta.url,
|
|
314
|
+
method: meta.method, requestHeaders: meta.headers, requestBody: reqBody });
|
|
315
|
+
}
|
|
316
|
+
return _send.apply(xhr, arguments);
|
|
317
|
+
};
|
|
253
318
|
|
|
254
319
|
// Listen for completion
|
|
255
|
-
|
|
256
|
-
|
|
320
|
+
xhr.addEventListener('readystatechange', function() {
|
|
321
|
+
if (xhr.readyState !== 4 || !meta.sent || !_shouldIntercept() || !_networkCaptureEnabled) return;
|
|
257
322
|
try {
|
|
258
323
|
const duration = Date.now() - meta.t0;
|
|
259
324
|
if (xhr.status > 0) {
|
|
@@ -310,17 +375,8 @@ global.fetch = async (input, init = {}) => {
|
|
|
310
375
|
_console.log('[RNDebugSDK] XHR constructor wrapped for network capture');
|
|
311
376
|
}
|
|
312
377
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// Also wrap after RN polyfills set up (they replace global.XMLHttpRequest)
|
|
317
|
-
[0, 50, 200, 500].forEach(delay => {
|
|
318
|
-
setTimeout(() => {
|
|
319
|
-
if (global.XMLHttpRequest && !global.XMLHttpRequest.__dbgWrapped) {
|
|
320
|
-
wrapXHR();
|
|
321
|
-
}
|
|
322
|
-
}, delay);
|
|
323
|
-
});
|
|
378
|
+
// Wrap immediately if available
|
|
379
|
+
if (global.XMLHttpRequest) wrapXHR();
|
|
324
380
|
})();
|
|
325
381
|
|
|
326
382
|
// ─── Axios Interceptor (belt-and-suspenders with XHR patch) ──────────────────
|
|
@@ -334,8 +390,8 @@ setTimeout(() => {
|
|
|
334
390
|
function addDbgInterceptors(instance) {
|
|
335
391
|
if (!instance || !instance.interceptors || instance.__dbgInt) return;
|
|
336
392
|
instance.__dbgInt = true;
|
|
337
|
-
|
|
338
|
-
|
|
393
|
+
instance.interceptors.request.use(config => {
|
|
394
|
+
if (!_shouldIntercept() || !_networkCaptureEnabled) return config;
|
|
339
395
|
const id = `ax-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
|
|
340
396
|
config._dbgId = id;
|
|
341
397
|
config._dbgT0 = Date.now();
|
|
@@ -348,9 +404,10 @@ setTimeout(() => {
|
|
|
348
404
|
mainCh.send({ type:'network', phase:'request', id, url, method:(config.method||'GET').toUpperCase(), requestHeaders:h, requestBody:body });
|
|
349
405
|
return config;
|
|
350
406
|
}, e => Promise.reject(e));
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
407
|
+
instance.interceptors.response.use(resp => {
|
|
408
|
+
if (!_shouldIntercept() || !_networkCaptureEnabled) return resp;
|
|
409
|
+
const c = resp.config || {};
|
|
410
|
+
if (!c._dbgId) return resp;
|
|
354
411
|
const url = c.baseURL ? c.baseURL.replace(/\/+$/,'') + '/' + (c.url||'').replace(/^\/+/,'') : (c.url||'');
|
|
355
412
|
const dur = c._dbgT0 ? Date.now() - c._dbgT0 : 0;
|
|
356
413
|
const rh = {};
|
|
@@ -361,9 +418,10 @@ setTimeout(() => {
|
|
|
361
418
|
mainCh.send({ type:'network', phase:'response', id:c._dbgId, url, method:(c.method||'GET').toUpperCase(),
|
|
362
419
|
status:resp.status, statusText:resp.statusText, duration:dur, responseHeaders:rh, responseBody:body });
|
|
363
420
|
return resp;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
421
|
+
}, err => {
|
|
422
|
+
if (!_shouldIntercept() || !_networkCaptureEnabled) return Promise.reject(err);
|
|
423
|
+
const c = err?.config || {};
|
|
424
|
+
if (c._dbgId) {
|
|
367
425
|
const url = c.baseURL ? c.baseURL.replace(/\/+$/,'') + '/' + (c.url||'').replace(/^\/+/,'') : (c.url||'');
|
|
368
426
|
const dur = c._dbgT0 ? Date.now() - c._dbgT0 : 0;
|
|
369
427
|
const r = err?.response;
|
package/styles.css
CHANGED
|
@@ -741,7 +741,11 @@ body {
|
|
|
741
741
|
}
|
|
742
742
|
.net-row:hover { background: var(--bg3); }
|
|
743
743
|
.net-row.selected { background: var(--bg4); }
|
|
744
|
-
.net-row.error { background: rgba(255,94,114,.
|
|
744
|
+
.net-row.error { background: rgba(255,94,114,.06); border-bottom-color: rgba(255,94,114,.10); }
|
|
745
|
+
.net-row.error:hover { background: rgba(255,94,114,.10); }
|
|
746
|
+
.net-row.error .net-path { color: var(--red); }
|
|
747
|
+
.net-row.error .net-host { color: rgba(255,94,114,.6); }
|
|
748
|
+
.net-path-error { color: var(--red) !important; }
|
|
745
749
|
.net-cell {
|
|
746
750
|
padding: 6px 8px;
|
|
747
751
|
overflow: hidden;
|
|
@@ -757,12 +761,13 @@ body {
|
|
|
757
761
|
.net-cell-name .method-badge { flex-shrink: 0; vertical-align: middle; }
|
|
758
762
|
|
|
759
763
|
.net-status { font-weight: 700; }
|
|
760
|
-
.s-
|
|
761
|
-
.s-
|
|
762
|
-
.s-
|
|
763
|
-
.s-
|
|
764
|
+
.s-1 { color: var(--text-mid); } /* 1xx informational */
|
|
765
|
+
.s-2 { color: var(--green); } /* 2xx success */
|
|
766
|
+
.s-3 { color: var(--yellow); } /* 3xx redirect */
|
|
767
|
+
.s-4 { color: var(--red); } /* 4xx client error */
|
|
768
|
+
.s-5 { color: var(--red); } /* 5xx server error */
|
|
764
769
|
.s-pending { color: var(--text-dim); }
|
|
765
|
-
.s-err { color: var(--red); }
|
|
770
|
+
.s-err { color: var(--red); font-weight: 700; }
|
|
766
771
|
|
|
767
772
|
.net-type { color: var(--text-dim); font-size: 10px; }
|
|
768
773
|
.net-initiator { color: var(--text-dim); font-size: 10px; }
|