reactoradar 1.5.3 → 1.5.5
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 +83 -26
- package/main.js +111 -43
- package/package.json +1 -1
- package/styles.css +11 -6
package/app.js
CHANGED
|
@@ -209,7 +209,7 @@ if (window.electronAPI) {
|
|
|
209
209
|
const btn = $('btnCDP');
|
|
210
210
|
if (btn) {
|
|
211
211
|
const hasCDP = targets?.length > 0;
|
|
212
|
-
const port = getStoredMetroPort();
|
|
212
|
+
const port = state.ports?.METRO || getStoredMetroPort();
|
|
213
213
|
btn.textContent = hasCDP
|
|
214
214
|
? `JS Debugger (:${port}) [${targets.length}] ↗`
|
|
215
215
|
: `JS Debugger (:${port}) ↗`;
|
|
@@ -268,21 +268,7 @@ if (window.electronAPI) {
|
|
|
268
268
|
window.electronAPI.on('update-available', ({ current, latest }) => {
|
|
269
269
|
// Show in settings only, not as a banner
|
|
270
270
|
state._updateAvailable = { current, latest };
|
|
271
|
-
|
|
272
|
-
if (el) el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${latest} available</span>`;
|
|
273
|
-
// Add update button in settings if not already there
|
|
274
|
-
if (!$('updateBtn')) {
|
|
275
|
-
const aboutEl = document.querySelector('.settings-about');
|
|
276
|
-
if (aboutEl) {
|
|
277
|
-
const btn = document.createElement('div');
|
|
278
|
-
btn.style.cssText = 'margin-top:10px';
|
|
279
|
-
btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px">Download v' + latest + '</button>';
|
|
280
|
-
aboutEl.appendChild(btn);
|
|
281
|
-
$('updateBtn')?.addEventListener('click', () => {
|
|
282
|
-
window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger/releases');
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
}
|
|
271
|
+
_applyUpdateBanner();
|
|
286
272
|
});
|
|
287
273
|
|
|
288
274
|
window.electronAPI.on('trigger-open-cdp', () => {
|
|
@@ -299,6 +285,31 @@ if (window.electronAPI) {
|
|
|
299
285
|
}
|
|
300
286
|
|
|
301
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
|
+
|
|
302
313
|
function updateDeviceBanner(service, connected) {
|
|
303
314
|
state.connections[service] = connected;
|
|
304
315
|
const el = $('deviceStatus');
|
|
@@ -1273,9 +1284,13 @@ function renderNetwork() {
|
|
|
1273
1284
|
rows.appendChild(frag);
|
|
1274
1285
|
}
|
|
1275
1286
|
|
|
1287
|
+
function _isHttpError(r) {
|
|
1288
|
+
return r.phase === 'error' || (r.status && r.status >= 400);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1276
1291
|
function buildNetRow(r, wfMin, wfRange) {
|
|
1277
1292
|
const row = document.createElement('div');
|
|
1278
|
-
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' : '');
|
|
1279
1294
|
row.dataset.id = r.id;
|
|
1280
1295
|
|
|
1281
1296
|
const urlObj = tryURL(r.url);
|
|
@@ -1291,7 +1306,9 @@ function buildNetRow(r, wfMin, wfRange) {
|
|
|
1291
1306
|
const method = r.method || '?';
|
|
1292
1307
|
const mClass = ['GET','POST','PUT','PATCH','DELETE'].includes(method) ? `m-${method}` : 'm-other';
|
|
1293
1308
|
const fullPath = urlObj ? urlObj.pathname + urlObj.search : r.url || '';
|
|
1294
|
-
|
|
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>`;
|
|
1295
1312
|
row.appendChild(nameCell);
|
|
1296
1313
|
|
|
1297
1314
|
// Status
|
|
@@ -1301,7 +1318,13 @@ function buildNetRow(r, wfMin, wfRange) {
|
|
|
1301
1318
|
statusCell.style.width = NET_COLS[1].width + 'px';
|
|
1302
1319
|
let statusStr = '...', sCls = 's-pending';
|
|
1303
1320
|
if (r.phase === 'error') { statusStr = 'ERR'; sCls = 's-err'; }
|
|
1304
|
-
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
|
+
}
|
|
1305
1328
|
statusCell.className += ` ${sCls}`;
|
|
1306
1329
|
statusCell.textContent = statusStr;
|
|
1307
1330
|
row.appendChild(statusCell);
|
|
@@ -1350,6 +1373,7 @@ function buildNetRow(r, wfMin, wfRange) {
|
|
|
1350
1373
|
const width = Math.max(2, ((r.duration || 50) / wfRange) * 100);
|
|
1351
1374
|
let barCls = 'pending';
|
|
1352
1375
|
if (r.phase === 'error') barCls = 'err';
|
|
1376
|
+
else if (r.status && r.status >= 400) barCls = 'err';
|
|
1353
1377
|
else if (r.status) barCls = `s${Math.floor(r.status/100)}`;
|
|
1354
1378
|
wfCell.innerHTML = `<div class="wf-bar ${barCls}" style="left:${left}%;width:${width}%"></div>`;
|
|
1355
1379
|
}
|
|
@@ -1416,8 +1440,12 @@ function renderNetDetailTabs(r) {
|
|
|
1416
1440
|
}
|
|
1417
1441
|
|
|
1418
1442
|
function renderNetDetailContent(r) {
|
|
1419
|
-
|
|
1443
|
+
let body = $('netDetailContent');
|
|
1420
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;
|
|
1421
1449
|
const tab = r._tab || 'headers';
|
|
1422
1450
|
|
|
1423
1451
|
if (tab === 'headers') {
|
|
@@ -1436,7 +1464,7 @@ function renderNetDetailContent(r) {
|
|
|
1436
1464
|
<div class="kv-grid">
|
|
1437
1465
|
<span class="kv-key">Request URL</span><span class="kv-val">${esc(r.url)}</span>
|
|
1438
1466
|
<span class="kv-key">Method</span><span class="kv-val">${esc(r.method)}</span>
|
|
1439
|
-
<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>
|
|
1440
1468
|
</div>
|
|
1441
1469
|
${renderH('Response Headers', rsH)}
|
|
1442
1470
|
${renderH('Request Headers', rqH)}`;
|
|
@@ -1460,16 +1488,27 @@ function renderNetDetailContent(r) {
|
|
|
1460
1488
|
}
|
|
1461
1489
|
}
|
|
1462
1490
|
} else if (tab === 'preview') {
|
|
1463
|
-
|
|
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; }
|
|
1464
1493
|
if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
|
|
1465
1494
|
// Render as collapsible JSON tree with right-click copy
|
|
1466
1495
|
const val = r.responseBody;
|
|
1467
1496
|
let treeData = val;
|
|
1468
1497
|
if (typeof val === 'string') {
|
|
1469
|
-
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
|
+
}
|
|
1470
1502
|
}
|
|
1471
1503
|
if (treeData && typeof treeData === 'object') {
|
|
1472
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
|
+
}
|
|
1473
1512
|
body.appendChild(createTreeNode(null, treeData, false));
|
|
1474
1513
|
// Right-click on preview to copy the whole object or clicked node value
|
|
1475
1514
|
body.addEventListener('contextmenu', (e) => {
|
|
@@ -1477,12 +1516,27 @@ function renderNetDetailContent(r) {
|
|
|
1477
1516
|
showPreviewCopyMenu(e, treeData);
|
|
1478
1517
|
});
|
|
1479
1518
|
} else {
|
|
1480
|
-
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>';
|
|
1481
1522
|
}
|
|
1482
1523
|
} else if (tab === 'response') {
|
|
1483
|
-
|
|
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; }
|
|
1484
1526
|
if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
|
|
1485
|
-
|
|
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
|
+
}
|
|
1486
1540
|
}
|
|
1487
1541
|
}
|
|
1488
1542
|
|
|
@@ -2433,6 +2487,9 @@ function initSettingsPanel() {
|
|
|
2433
2487
|
setStoredFontSize(size);
|
|
2434
2488
|
applyFontSize(size);
|
|
2435
2489
|
});
|
|
2490
|
+
|
|
2491
|
+
// Apply update banner if update info arrived before settings panel was created
|
|
2492
|
+
_applyUpdateBanner();
|
|
2436
2493
|
}
|
|
2437
2494
|
|
|
2438
2495
|
// Apply saved theme + font size + app name on load
|
package/main.js
CHANGED
|
@@ -19,6 +19,15 @@ const PORTS = {
|
|
|
19
19
|
let mainWindow = null;
|
|
20
20
|
let devtoolsWindow = null; // hosts the embedded CDP DevTools frontend
|
|
21
21
|
|
|
22
|
+
// Safe IPC send — prevents "Object has been destroyed" crash
|
|
23
|
+
function _send(channel, ...args) {
|
|
24
|
+
try {
|
|
25
|
+
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.webContents && !mainWindow.webContents.isDestroyed()) {
|
|
26
|
+
mainWindow.webContents.send(channel, ...args);
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
23
32
|
let reduxClients = new Set();
|
|
24
33
|
let storageClients = new Set();
|
|
@@ -27,8 +36,32 @@ let networkClients = new Set();
|
|
|
27
36
|
// ─── Set dock icon ASAP (before app ready) ──────────────────────────────────
|
|
28
37
|
const _appIcon = nativeImage.createFromPath(path.join(__dirname, 'ReactoRadar.png'));
|
|
29
38
|
|
|
39
|
+
// ─── Single Instance Lock ────────────────────────────────────────────────────
|
|
40
|
+
// Prevent multiple ReactoRadar instances from running simultaneously.
|
|
41
|
+
// If a second instance launches, focus the existing window instead.
|
|
42
|
+
const gotLock = app.requestSingleInstanceLock();
|
|
43
|
+
if (!gotLock) {
|
|
44
|
+
// Another instance is already running — show a dialog and quit
|
|
45
|
+
const { dialog } = require('electron');
|
|
46
|
+
app.whenReady().then(() => {
|
|
47
|
+
dialog.showErrorBox(
|
|
48
|
+
'ReactoRadar is already running',
|
|
49
|
+
'Another instance of ReactoRadar is already open.\n\nPlease close the existing instance first, or check your system tray / dock.\n\nIf the old version is stuck, run:\n kill $(lsof -ti :9092) \nin your terminal to stop it.'
|
|
50
|
+
);
|
|
51
|
+
app.quit();
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
app.on('second-instance', () => {
|
|
55
|
+
// Focus the existing window when someone tries to open a second instance
|
|
56
|
+
if (mainWindow) {
|
|
57
|
+
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
58
|
+
mainWindow.focus();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
30
63
|
// ─── App lifecycle ────────────────────────────────────────────────────────────
|
|
31
|
-
app.whenReady().then(async () => {
|
|
64
|
+
if (gotLock) app.whenReady().then(async () => {
|
|
32
65
|
nativeTheme.themeSource = 'dark';
|
|
33
66
|
|
|
34
67
|
// Set dock icon on macOS
|
|
@@ -42,11 +75,11 @@ app.whenReady().then(async () => {
|
|
|
42
75
|
let appVersion;
|
|
43
76
|
try { appVersion = require('./package.json').version; } catch { appVersion = app.getVersion(); }
|
|
44
77
|
// Send multiple times to ensure renderer catches it
|
|
45
|
-
mainWindow
|
|
78
|
+
mainWindow.webContents.on('did-finish-load', () => {
|
|
46
79
|
[200, 1000, 3000].forEach(delay => {
|
|
47
80
|
setTimeout(() => {
|
|
48
81
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
49
|
-
|
|
82
|
+
_send('app-version', appVersion);
|
|
50
83
|
}
|
|
51
84
|
}, delay);
|
|
52
85
|
});
|
|
@@ -102,11 +135,23 @@ async function createMainWindow() {
|
|
|
102
135
|
|
|
103
136
|
// Open the JS Debugger panel (CDP DevTools) in a second window
|
|
104
137
|
mainWindow.webContents.on('did-finish-load', () => {
|
|
105
|
-
|
|
138
|
+
_send('ports', PORTS);
|
|
106
139
|
});
|
|
107
140
|
}
|
|
108
141
|
|
|
109
142
|
// ─── Update Checker ──────────────────────────────────────────────────────────
|
|
143
|
+
function _semverCompare(a, b) {
|
|
144
|
+
// Returns 1 if a > b, -1 if a < b, 0 if equal
|
|
145
|
+
const pa = (a || '').split('.').map(Number);
|
|
146
|
+
const pb = (b || '').split('.').map(Number);
|
|
147
|
+
for (let i = 0; i < 3; i++) {
|
|
148
|
+
const va = pa[i] || 0, vb = pb[i] || 0;
|
|
149
|
+
if (va > vb) return 1;
|
|
150
|
+
if (va < vb) return -1;
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
110
155
|
function checkForUpdates() {
|
|
111
156
|
const currentVersion = require('./package.json').version;
|
|
112
157
|
https.get('https://registry.npmjs.org/reactoradar/latest', (res) => {
|
|
@@ -115,9 +160,16 @@ function checkForUpdates() {
|
|
|
115
160
|
res.on('end', () => {
|
|
116
161
|
try {
|
|
117
162
|
const latest = JSON.parse(data).version;
|
|
118
|
-
if (latest && latest
|
|
119
|
-
//
|
|
120
|
-
|
|
163
|
+
if (latest && _semverCompare(latest, currentVersion) > 0) {
|
|
164
|
+
// Send with retries to ensure renderer catches it after did-finish-load
|
|
165
|
+
const payload = { current: currentVersion, latest };
|
|
166
|
+
[500, 2000, 5000].forEach(delay => {
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
169
|
+
_send('update-available', payload);
|
|
170
|
+
}
|
|
171
|
+
}, delay);
|
|
172
|
+
});
|
|
121
173
|
console.log(`[Update] New version available: ${latest} (current: ${currentVersion})`);
|
|
122
174
|
}
|
|
123
175
|
} catch {}
|
|
@@ -184,7 +236,7 @@ function fetchCDPTargets(callback) {
|
|
|
184
236
|
t.type === 'node' || t.devtoolsFrontendUrl
|
|
185
237
|
);
|
|
186
238
|
lastKnownTargets = rnTargets;
|
|
187
|
-
|
|
239
|
+
_send('cdp-targets', rnTargets);
|
|
188
240
|
if (callback) callback(rnTargets);
|
|
189
241
|
} catch (_) {
|
|
190
242
|
if (callback) callback([]);
|
|
@@ -192,7 +244,7 @@ function fetchCDPTargets(callback) {
|
|
|
192
244
|
});
|
|
193
245
|
}).on('error', () => {
|
|
194
246
|
lastKnownTargets = [];
|
|
195
|
-
|
|
247
|
+
_send('cdp-targets', []);
|
|
196
248
|
if (callback) callback([]);
|
|
197
249
|
});
|
|
198
250
|
}
|
|
@@ -216,13 +268,13 @@ function startReactDevToolsServer() {
|
|
|
216
268
|
reactDTServer.on('error', (err) => {
|
|
217
269
|
console.warn(`[ReactDT] Server error: ${err.message}`);
|
|
218
270
|
if (err.code === 'EADDRINUSE') {
|
|
219
|
-
|
|
271
|
+
_send('react-dt-status', false);
|
|
220
272
|
}
|
|
221
273
|
});
|
|
222
274
|
reactDTServer.on('connection', (ws) => {
|
|
223
275
|
reactDTClients.add(ws);
|
|
224
276
|
console.log(`[ReactDT] Client connected (total: ${reactDTClients.size})`);
|
|
225
|
-
|
|
277
|
+
_send('react-dt-status', true);
|
|
226
278
|
|
|
227
279
|
// Relay messages between all connected clients (frontend ↔ backend)
|
|
228
280
|
ws.on('message', (data) => {
|
|
@@ -237,7 +289,7 @@ function startReactDevToolsServer() {
|
|
|
237
289
|
reactDTClients.delete(ws);
|
|
238
290
|
console.log(`[ReactDT] Client disconnected (total: ${reactDTClients.size})`);
|
|
239
291
|
if (reactDTClients.size === 0) {
|
|
240
|
-
|
|
292
|
+
_send('react-dt-status', false);
|
|
241
293
|
}
|
|
242
294
|
});
|
|
243
295
|
});
|
|
@@ -251,53 +303,67 @@ function startReactDevToolsServer() {
|
|
|
251
303
|
function startBridgeServers() {
|
|
252
304
|
// Redux Bridge
|
|
253
305
|
startBridge(PORTS.REDUX_BRIDGE, 'redux', reduxClients, (event) => {
|
|
254
|
-
|
|
306
|
+
_send('redux-event', event);
|
|
255
307
|
});
|
|
256
308
|
|
|
257
309
|
// AsyncStorage Bridge
|
|
258
310
|
startBridge(PORTS.STORAGE_BRIDGE, 'storage', storageClients, (event) => {
|
|
259
|
-
|
|
311
|
+
_send('storage-event', event);
|
|
260
312
|
});
|
|
261
313
|
|
|
262
314
|
// Network + Console + Perf Bridge (port 9092 carries all types from RNDebugSDK)
|
|
263
315
|
startBridge(PORTS.NETWORK_BRIDGE, 'network', networkClients, (event) => {
|
|
264
316
|
if (event.type === 'control') return;
|
|
265
317
|
if (event.type === 'console') {
|
|
266
|
-
|
|
318
|
+
_send('console-event', event);
|
|
267
319
|
} else if (event.type === 'perf') {
|
|
268
|
-
|
|
320
|
+
_send('perf-event', event);
|
|
269
321
|
} else if (event.type === 'ga4') {
|
|
270
|
-
|
|
322
|
+
_send('ga4-event', event);
|
|
271
323
|
} else {
|
|
272
|
-
|
|
324
|
+
_send('network-event', event);
|
|
273
325
|
}
|
|
274
326
|
});
|
|
275
327
|
}
|
|
276
328
|
|
|
277
329
|
function startBridge(port, name, clients, onEvent) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
} catch (e) {
|
|
289
|
-
console.warn(`[${name}] Failed to parse message:`, e.message);
|
|
330
|
+
try {
|
|
331
|
+
const wss = new WebSocketServer({ port });
|
|
332
|
+
wss.on('error', (err) => {
|
|
333
|
+
if (err.code === 'EADDRINUSE') {
|
|
334
|
+
console.error(`[${name}] Port ${port} is already in use — another ReactoRadar or debugger may be running.`);
|
|
335
|
+
const { dialog } = require('electron');
|
|
336
|
+
dialog.showErrorBox(
|
|
337
|
+
`Port ${port} is in use`,
|
|
338
|
+
`ReactoRadar cannot start the ${name} bridge because port ${port} is already occupied.\n\nThis usually means an older version of ReactoRadar is still running.\n\nTo fix this, run the following in your terminal:\n kill $(lsof -ti :${port})\n\nThen restart ReactoRadar.`
|
|
339
|
+
);
|
|
290
340
|
}
|
|
291
341
|
});
|
|
342
|
+
wss.on('connection', (ws) => {
|
|
343
|
+
clients.add(ws);
|
|
344
|
+
console.log(`[${name}] RN app connected`);
|
|
345
|
+
_send(`${name}-connected`, true);
|
|
292
346
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
347
|
+
ws.on('message', (raw) => {
|
|
348
|
+
try {
|
|
349
|
+
const event = JSON.parse(raw.toString());
|
|
350
|
+
onEvent(event);
|
|
351
|
+
} catch (e) {
|
|
352
|
+
console.warn(`[${name}] Failed to parse message:`, e.message);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
ws.on('close', () => {
|
|
357
|
+
clients.delete(ws);
|
|
358
|
+
if (clients.size === 0) {
|
|
359
|
+
_send(`${name}-connected`, false);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
298
362
|
});
|
|
299
|
-
|
|
300
|
-
|
|
363
|
+
console.log(`[${name}] Bridge on :${port}`);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
console.error(`[${name}] Failed to start bridge on port ${port}:`, e.message);
|
|
366
|
+
}
|
|
301
367
|
}
|
|
302
368
|
|
|
303
369
|
// ─── IPC from Renderer ────────────────────────────────────────────────────────
|
|
@@ -336,9 +402,11 @@ function setupIPC() {
|
|
|
336
402
|
// clear-all is handled by renderer via clear-all-ui IPC from menu
|
|
337
403
|
|
|
338
404
|
ipcMain.on('set-metro-port', (_, port) => {
|
|
339
|
-
|
|
405
|
+
const p = parseInt(port);
|
|
406
|
+
if (isNaN(p) || p < 1024 || p > 65535) return;
|
|
407
|
+
PORTS.METRO = p;
|
|
340
408
|
fetchCDPTargets();
|
|
341
|
-
|
|
409
|
+
_send('ports', PORTS);
|
|
342
410
|
});
|
|
343
411
|
|
|
344
412
|
ipcMain.on('set-network-capture', (_, enabled) => {
|
|
@@ -482,7 +550,7 @@ function buildMenu() {
|
|
|
482
550
|
{
|
|
483
551
|
label: 'Open JS Debugger (CDP)',
|
|
484
552
|
accelerator: 'Cmd+D',
|
|
485
|
-
click: () => {
|
|
553
|
+
click: () => { _send('trigger-open-cdp'); },
|
|
486
554
|
},
|
|
487
555
|
{
|
|
488
556
|
label: 'Open React DevTools',
|
|
@@ -493,7 +561,7 @@ function buildMenu() {
|
|
|
493
561
|
{
|
|
494
562
|
label: 'Clear All',
|
|
495
563
|
accelerator: 'Cmd+K',
|
|
496
|
-
click: () => {
|
|
564
|
+
click: () => { _send('clear-all-ui'); },
|
|
497
565
|
},
|
|
498
566
|
{ type: 'separator' },
|
|
499
567
|
{
|
|
@@ -506,7 +574,7 @@ function buildMenu() {
|
|
|
506
574
|
const next = themes[(idx + 1) % themes.length];
|
|
507
575
|
nativeTheme.themeSource = next.includes('light') ? 'light' : 'dark';
|
|
508
576
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
509
|
-
|
|
577
|
+
_send('theme-changed', next);
|
|
510
578
|
}
|
|
511
579
|
},
|
|
512
580
|
},
|
|
@@ -526,7 +594,7 @@ function buildMenu() {
|
|
|
526
594
|
{
|
|
527
595
|
label: 'Find',
|
|
528
596
|
accelerator: 'Cmd+F',
|
|
529
|
-
click: () => {
|
|
597
|
+
click: () => { _send('focus-search'); },
|
|
530
598
|
},
|
|
531
599
|
],
|
|
532
600
|
},
|
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.5",
|
|
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/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; }
|