reactoradar 1.6.4 → 1.6.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 +282 -18
- package/main.js +85 -9
- package/package.json +1 -1
- package/preload.js +2 -1
- package/styles.css +90 -0
package/app.js
CHANGED
|
@@ -194,23 +194,37 @@ function clearActiveTab() {
|
|
|
194
194
|
const memHT = $('memHeapTotal'); if (memHT) memHT.textContent = '—';
|
|
195
195
|
const memN = $('memNative'); if (memN) memN.textContent = '—';
|
|
196
196
|
break;
|
|
197
|
+
case 'native':
|
|
198
|
+
_nativeState.logs = [];
|
|
199
|
+
if ($('nativeBadge')) $('nativeBadge').textContent = '0';
|
|
200
|
+
const nativeList = $('nativeLogList');
|
|
201
|
+
if (nativeList) nativeList.innerHTML = '';
|
|
202
|
+
break;
|
|
197
203
|
default:
|
|
198
204
|
break;
|
|
199
205
|
}
|
|
200
206
|
}
|
|
201
207
|
|
|
202
|
-
// Clear all (used by IPC clear-all-ui from menu Cmd+K)
|
|
208
|
+
// Clear all (used by IPC clear-all-ui from menu Cmd+K, and on device disconnect)
|
|
203
209
|
function clearAll() {
|
|
210
|
+
// Cancel pending render batches
|
|
211
|
+
if (_consoleRAF) { cancelAnimationFrame(_consoleRAF); _consoleRAF = null; }
|
|
212
|
+
if (_netRAF) { cancelAnimationFrame(_netRAF); _netRAF = null; }
|
|
213
|
+
if (_storageRAF) { cancelAnimationFrame(_storageRAF); _storageRAF = null; }
|
|
214
|
+
// Console
|
|
204
215
|
state.console.logs = [];
|
|
205
216
|
_consolePending = [];
|
|
206
217
|
_lastLogMsg = ''; _lastLogRow = null; _lastLogCount = 1;
|
|
218
|
+
// Network
|
|
207
219
|
state.network.requests = {};
|
|
208
220
|
state.network.order = [];
|
|
209
221
|
state.network.selectedId = null;
|
|
210
222
|
closeNetDetail();
|
|
223
|
+
// Redux
|
|
211
224
|
state.redux.actions = [];
|
|
212
225
|
state.redux.states = [];
|
|
213
226
|
state.redux.selected = -1;
|
|
227
|
+
// Storage
|
|
214
228
|
state.storage.entries = {};
|
|
215
229
|
state.storage.keys = [];
|
|
216
230
|
state.storage.selected = null;
|
|
@@ -226,6 +240,21 @@ function clearAll() {
|
|
|
226
240
|
_nativeState.logs = [];
|
|
227
241
|
const nativeList = $('nativeLogList');
|
|
228
242
|
if (nativeList) nativeList.innerHTML = '';
|
|
243
|
+
// Performance
|
|
244
|
+
perfState.fps = [];
|
|
245
|
+
perfState.jsThread = [];
|
|
246
|
+
perfState.uiThread = [];
|
|
247
|
+
perfState.data = [];
|
|
248
|
+
const perfFPS = $('perfFPS'); if (perfFPS) perfFPS.textContent = '—';
|
|
249
|
+
const perfJS = $('perfJS'); if (perfJS) perfJS.textContent = '—';
|
|
250
|
+
const perfUI = $('perfUI'); if (perfUI) perfUI.textContent = '—';
|
|
251
|
+
clearPerfCanvas('perfFPSCanvas');
|
|
252
|
+
clearPerfCanvas('perfJSCanvas');
|
|
253
|
+
clearPerfCanvas('perfUICanvas');
|
|
254
|
+
// Memory
|
|
255
|
+
const memHU = $('memHeapUsed'); if (memHU) memHU.textContent = '—';
|
|
256
|
+
const memHT = $('memHeapTotal'); if (memHT) memHT.textContent = '—';
|
|
257
|
+
const memN = $('memNative'); if (memN) memN.textContent = '—';
|
|
229
258
|
// Badges
|
|
230
259
|
$('cBadge').textContent = '0';
|
|
231
260
|
$('nBadge').textContent = '0';
|
|
@@ -241,6 +270,42 @@ function clearAll() {
|
|
|
241
270
|
if (typeof renderGA4List === 'function') { renderGA4List(); renderGA4Summary(); }
|
|
242
271
|
}
|
|
243
272
|
|
|
273
|
+
// Free heavy in-memory data without clearing the visible UI.
|
|
274
|
+
// Called on device disconnect and app quit to reduce memory footprint
|
|
275
|
+
// while keeping logs/network/redux visible for inspection.
|
|
276
|
+
function freeMemory() {
|
|
277
|
+
// Drop response/request bodies from network requests (biggest memory hog)
|
|
278
|
+
for (const id of state.network.order) {
|
|
279
|
+
const r = state.network.requests[id];
|
|
280
|
+
if (r) { r.responseBody = null; r.requestBody = null; }
|
|
281
|
+
}
|
|
282
|
+
// Trim console logs to a small tail (keep last 200 for reference)
|
|
283
|
+
if (state.console.logs.length > 200) {
|
|
284
|
+
state.console.logs = state.console.logs.slice(-200);
|
|
285
|
+
}
|
|
286
|
+
// Drop full Redux state snapshots (keep action metadata)
|
|
287
|
+
state.redux.states = [];
|
|
288
|
+
// Drop storage values (keep keys for reference)
|
|
289
|
+
for (const k in state.storage.entries) {
|
|
290
|
+
state.storage.entries[k] = null;
|
|
291
|
+
}
|
|
292
|
+
// Trim GA4 events
|
|
293
|
+
if (ga4State.events.length > 200) {
|
|
294
|
+
ga4State.events = ga4State.events.slice(-200);
|
|
295
|
+
}
|
|
296
|
+
// Trim native logs
|
|
297
|
+
if (_nativeState.logs.length > 200) {
|
|
298
|
+
_nativeState.logs = _nativeState.logs.slice(-200);
|
|
299
|
+
}
|
|
300
|
+
// Drop performance timeline data
|
|
301
|
+
perfState.data = [];
|
|
302
|
+
perfState.fps = [];
|
|
303
|
+
perfState.jsThread = [];
|
|
304
|
+
perfState.uiThread = [];
|
|
305
|
+
// Flush pending console batch
|
|
306
|
+
_consolePending = [];
|
|
307
|
+
}
|
|
308
|
+
|
|
244
309
|
// ─── CDP Button ───────────────────────────────────────────────────────────────
|
|
245
310
|
$('btnCDP')?.addEventListener('click', () => {
|
|
246
311
|
// Tell main process to open the CDP DevTools window with the best available target
|
|
@@ -294,13 +359,25 @@ if (window.electronAPI) {
|
|
|
294
359
|
handleMemoryEvent(event);
|
|
295
360
|
});
|
|
296
361
|
|
|
297
|
-
window.electronAPI.on('redux-connected', on => { updateDeviceBanner('redux', on); });
|
|
298
|
-
window.electronAPI.on('network-connected', on => { updateDeviceBanner('network', on); });
|
|
299
|
-
window.electronAPI.on('storage-connected', on => { updateDeviceBanner('storage', on); });
|
|
300
|
-
window.electronAPI.on('react-dt-status', on => { updateDeviceBanner('reactDT', on); });
|
|
301
|
-
|
|
302
362
|
window.electronAPI.on('clear-all-ui', clearAll);
|
|
303
363
|
|
|
364
|
+
// When all device bridges disconnect, release heavy memory but keep logs visible.
|
|
365
|
+
// Debounced to avoid data loss during hot reloads or flaky connections.
|
|
366
|
+
let _disconnectTimer = null;
|
|
367
|
+
window.electronAPI.on('device-all-disconnected', () => {
|
|
368
|
+
clearTimeout(_disconnectTimer);
|
|
369
|
+
_disconnectTimer = setTimeout(() => {
|
|
370
|
+
console.log('[App] All devices disconnected — freeing memory');
|
|
371
|
+
freeMemory();
|
|
372
|
+
}, 3000);
|
|
373
|
+
});
|
|
374
|
+
// Cancel pending free if a device reconnects
|
|
375
|
+
const _cancelDisconnectTimer = () => { clearTimeout(_disconnectTimer); _disconnectTimer = null; };
|
|
376
|
+
window.electronAPI.on('redux-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('redux', on); });
|
|
377
|
+
window.electronAPI.on('network-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('network', on); });
|
|
378
|
+
window.electronAPI.on('storage-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('storage', on); });
|
|
379
|
+
window.electronAPI.on('react-dt-status', on => { updateDeviceBanner('reactDT', on); });
|
|
380
|
+
|
|
304
381
|
// Cmd+F — focus the search input for the active panel
|
|
305
382
|
function _handleFind() {
|
|
306
383
|
// If network detail is open, focus the detail search
|
|
@@ -341,8 +418,9 @@ if (window.electronAPI) {
|
|
|
341
418
|
}
|
|
342
419
|
});
|
|
343
420
|
|
|
344
|
-
window.electronAPI.on('app-version', (version) => {
|
|
421
|
+
window.electronAPI.on('app-version', (version, isPackaged) => {
|
|
345
422
|
state._appVersion = version;
|
|
423
|
+
state._isPackaged = !!isPackaged;
|
|
346
424
|
// Update anywhere the version is displayed
|
|
347
425
|
document.querySelectorAll('#aboutVersion').forEach(el => el.textContent = 'v' + version);
|
|
348
426
|
});
|
|
@@ -426,16 +504,19 @@ function _applyUpdateBanner() {
|
|
|
426
504
|
}
|
|
427
505
|
|
|
428
506
|
async function _showChangelog(version) {
|
|
507
|
+
if (!version || typeof version !== 'string') return;
|
|
508
|
+
|
|
429
509
|
// Remove existing modal
|
|
430
510
|
$('changelogModal')?.remove();
|
|
431
511
|
|
|
512
|
+
const safeVersion = esc(version);
|
|
432
513
|
const modal = document.createElement('div');
|
|
433
514
|
modal.id = 'changelogModal';
|
|
434
515
|
modal.className = 'changelog-modal-overlay';
|
|
435
516
|
modal.innerHTML = `
|
|
436
517
|
<div class="changelog-modal">
|
|
437
518
|
<div class="changelog-header">
|
|
438
|
-
<span class="changelog-title">What's New in v${
|
|
519
|
+
<span class="changelog-title">What's New in v${safeVersion}</span>
|
|
439
520
|
<button class="changelog-close" id="changelogClose">×</button>
|
|
440
521
|
</div>
|
|
441
522
|
<div class="changelog-body" id="changelogBody">
|
|
@@ -452,6 +533,11 @@ async function _showChangelog(version) {
|
|
|
452
533
|
try {
|
|
453
534
|
const notes = await window.electronAPI?.fetchChangelog(version);
|
|
454
535
|
const body = $('changelogBody');
|
|
536
|
+
if (!body) return;
|
|
537
|
+
if (!notes || typeof notes !== 'string') {
|
|
538
|
+
body.innerHTML = '<div style="color:var(--text-dim);padding:20px;text-align:center">No release notes available.</div>';
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
455
541
|
if (body && notes) {
|
|
456
542
|
// Simple markdown-like rendering
|
|
457
543
|
body.innerHTML = notes
|
|
@@ -814,6 +900,41 @@ window.electronAPI?.on('console-event', addConsoleLog);
|
|
|
814
900
|
// ─── Object Tree Renderer (Chrome DevTools-like) ─────────────────────────────
|
|
815
901
|
// Builds interactive, collapsible DOM nodes for objects/arrays.
|
|
816
902
|
|
|
903
|
+
// Collect all entries for an object: own data properties + prototype getter values.
|
|
904
|
+
// Getter-derived keys use the clean name (e.g. "deliveryId") and skip backing
|
|
905
|
+
// fields (e.g. "_deliveryId") so the log output mirrors the model's public API.
|
|
906
|
+
function collectEntries(val) {
|
|
907
|
+
if (Array.isArray(val)) return val.map((v, i) => [i, v]);
|
|
908
|
+
|
|
909
|
+
const result = {};
|
|
910
|
+
const getterKeys = new Set();
|
|
911
|
+
|
|
912
|
+
// 1. Walk prototype chain and invoke getters
|
|
913
|
+
let proto = Object.getPrototypeOf(val);
|
|
914
|
+
while (proto && proto !== Object.prototype) {
|
|
915
|
+
const descs = Object.getOwnPropertyDescriptors(proto);
|
|
916
|
+
for (const [k, desc] of Object.entries(descs)) {
|
|
917
|
+
if (k === 'constructor') continue;
|
|
918
|
+
if (desc.get && !(k in result)) {
|
|
919
|
+
try { result[k] = desc.get.call(val); } catch { /* skip broken getters */ }
|
|
920
|
+
getterKeys.add(k);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
proto = Object.getPrototypeOf(proto);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// 2. Add own data properties, but skip backing fields whose getter is present.
|
|
927
|
+
// Convention: getter "foo" backs "_foo"; if "foo" was collected, skip "_foo".
|
|
928
|
+
const ownKeys = Object.keys(val);
|
|
929
|
+
for (const k of ownKeys) {
|
|
930
|
+
const clean = k.startsWith('_') ? k.slice(1) : null;
|
|
931
|
+
if (clean && getterKeys.has(clean)) continue; // skip _backing field
|
|
932
|
+
if (!(k in result)) result[k] = val[k];
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return Object.entries(result);
|
|
936
|
+
}
|
|
937
|
+
|
|
817
938
|
function objPreview(val, maxLen) {
|
|
818
939
|
maxLen = maxLen || 80;
|
|
819
940
|
if (val === null) return 'null';
|
|
@@ -831,16 +952,16 @@ function objPreview(val, maxLen) {
|
|
|
831
952
|
return `(${val.length}) [${items.join(', ')}${suffix}]`;
|
|
832
953
|
}
|
|
833
954
|
if (typeof val === 'object') {
|
|
834
|
-
const
|
|
835
|
-
if (
|
|
955
|
+
const entries = collectEntries(val);
|
|
956
|
+
if (entries.length === 0) return '{}';
|
|
836
957
|
const items = [];
|
|
837
958
|
let len = 2;
|
|
838
|
-
for (let i = 0; i <
|
|
839
|
-
const s = `${
|
|
959
|
+
for (let i = 0; i < entries.length && len < maxLen; i++) {
|
|
960
|
+
const s = `${entries[i][0]}: ${primitivePreview(entries[i][1])}`;
|
|
840
961
|
len += s.length + 2;
|
|
841
962
|
items.push(s);
|
|
842
963
|
}
|
|
843
|
-
const suffix = items.length <
|
|
964
|
+
const suffix = items.length < entries.length ? ', ...' : '';
|
|
844
965
|
return `{${items.join(', ')}${suffix}}`;
|
|
845
966
|
}
|
|
846
967
|
return primitivePreview(val);
|
|
@@ -909,7 +1030,7 @@ function createTreeNode(key, val, startCollapsed) {
|
|
|
909
1030
|
function populateChildren() {
|
|
910
1031
|
if (populated) return;
|
|
911
1032
|
populated = true;
|
|
912
|
-
const entries =
|
|
1033
|
+
const entries = collectEntries(val);
|
|
913
1034
|
entries.forEach(([k, v]) => {
|
|
914
1035
|
children.appendChild(createTreeNode(k, v, true));
|
|
915
1036
|
});
|
|
@@ -3247,10 +3368,18 @@ function initNativeLogsPanel() {
|
|
|
3247
3368
|
<div class="native-log-list" id="nativeLogList"></div>
|
|
3248
3369
|
</div>`;
|
|
3249
3370
|
|
|
3250
|
-
// Connect buttons
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3371
|
+
// Connect buttons — auto-enable tab when user clicks connect
|
|
3372
|
+
function _enableNativeTab() {
|
|
3373
|
+
const vis = getTabVisibility();
|
|
3374
|
+
if (!vis['native']) {
|
|
3375
|
+
vis['native'] = true;
|
|
3376
|
+
setTabVisibility(vis);
|
|
3377
|
+
applyTabVisibility();
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
$('nativeConnectAndroid')?.addEventListener('click', () => { _enableNativeTab(); window.electronAPI?.startNativeLogs('android'); });
|
|
3381
|
+
$('nativeConnectIOSSim')?.addEventListener('click', () => { _enableNativeTab(); window.electronAPI?.startNativeLogs('ios-sim'); });
|
|
3382
|
+
$('nativeConnectIOSDevice')?.addEventListener('click', () => { _enableNativeTab(); window.electronAPI?.startNativeLogs('ios-device'); });
|
|
3254
3383
|
$('nativeDisconnect')?.addEventListener('click', () => window.electronAPI?.stopNativeLogs());
|
|
3255
3384
|
|
|
3256
3385
|
// Clear buttons (toolbar + logs area)
|
|
@@ -3860,6 +3989,27 @@ function initSettingsPanel() {
|
|
|
3860
3989
|
<b style="color:var(--text)">5.</b> <code style="color:var(--accent);background:var(--bg3);padding:1px 5px;border-radius:3px">npx reactoradar remove</code> to uninstall
|
|
3861
3990
|
</div>
|
|
3862
3991
|
</div>
|
|
3992
|
+
<div class="settings-section">
|
|
3993
|
+
<div class="settings-section-title">Version History</div>
|
|
3994
|
+
<div class="settings-hint" style="margin-bottom:4px">Roll back to a previous version if you notice issues.</div>
|
|
3995
|
+
<div class="settings-hint rollback-steps" id="rollbackSteps" style="margin-bottom:10px;line-height:1.8;font-size:10px">
|
|
3996
|
+
<b style="color:var(--text)">How to roll back:</b><br/>
|
|
3997
|
+
<span id="rollbackDmgSteps" style="display:none">
|
|
3998
|
+
<b style="color:var(--text)">1.</b> Click <b>Download</b> on the version you want<br/>
|
|
3999
|
+
<b style="color:var(--text)">2.</b> Open the downloaded <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">.dmg</code> file<br/>
|
|
4000
|
+
<b style="color:var(--text)">3.</b> Drag the app to Applications (replace existing)<br/>
|
|
4001
|
+
<b style="color:var(--text)">4.</b> Relaunch ReactoRadar
|
|
4002
|
+
</span>
|
|
4003
|
+
<span id="rollbackNpmSteps" style="display:none">
|
|
4004
|
+
<b style="color:var(--text)">1.</b> Run <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">npx reactoradar@<version></code> e.g. <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">npx reactoradar@1.6.4</code><br/>
|
|
4005
|
+
<b style="color:var(--text)">2.</b> Or pin globally: <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">npm i -g reactoradar@1.6.4</code><br/>
|
|
4006
|
+
<b style="color:var(--text)">3.</b> Run <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">reactoradar</code> to launch
|
|
4007
|
+
</span>
|
|
4008
|
+
</div>
|
|
4009
|
+
<div id="versionHistoryList" class="version-history-list">
|
|
4010
|
+
<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Loading versions...</div>
|
|
4011
|
+
</div>
|
|
4012
|
+
</div>
|
|
3863
4013
|
</div>
|
|
3864
4014
|
</div>
|
|
3865
4015
|
</div>`;
|
|
@@ -3966,6 +4116,120 @@ function initSettingsPanel() {
|
|
|
3966
4116
|
|
|
3967
4117
|
// Apply update banner if update info arrived before settings panel was created
|
|
3968
4118
|
_applyUpdateBanner();
|
|
4119
|
+
|
|
4120
|
+
// Fetch and render version history for rollback
|
|
4121
|
+
_loadVersionHistory();
|
|
4122
|
+
}
|
|
4123
|
+
|
|
4124
|
+
function _loadVersionHistory() {
|
|
4125
|
+
const container = $('versionHistoryList');
|
|
4126
|
+
if (!container) return;
|
|
4127
|
+
if (!window.electronAPI || typeof window.electronAPI.fetchReleases !== 'function') {
|
|
4128
|
+
container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Version history not available.</div>';
|
|
4129
|
+
return;
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
// Show appropriate rollback steps based on install type
|
|
4133
|
+
const isPackaged = !!state._isPackaged;
|
|
4134
|
+
const dmgSteps = $('rollbackDmgSteps');
|
|
4135
|
+
const npmSteps = $('rollbackNpmSteps');
|
|
4136
|
+
if (dmgSteps) dmgSteps.style.display = isPackaged ? '' : 'none';
|
|
4137
|
+
if (npmSteps) npmSteps.style.display = isPackaged ? 'none' : '';
|
|
4138
|
+
|
|
4139
|
+
window.electronAPI.fetchReleases().then(releases => {
|
|
4140
|
+
if (!Array.isArray(releases) || releases.length === 0) {
|
|
4141
|
+
container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Could not load versions.</div>';
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
const currentVersion = state._appVersion || '';
|
|
4146
|
+
container.innerHTML = '';
|
|
4147
|
+
|
|
4148
|
+
releases.forEach(r => {
|
|
4149
|
+
if (!r || !r.version) return; // skip malformed entries
|
|
4150
|
+
|
|
4151
|
+
const isCurrent = r.version === currentVersion;
|
|
4152
|
+
const row = document.createElement('div');
|
|
4153
|
+
row.className = 'version-row' + (isCurrent ? ' version-current' : '');
|
|
4154
|
+
|
|
4155
|
+
// Safe date formatting
|
|
4156
|
+
let dateStr = '';
|
|
4157
|
+
if (r.date) {
|
|
4158
|
+
try {
|
|
4159
|
+
const d = new Date(r.date);
|
|
4160
|
+
if (!isNaN(d.getTime())) {
|
|
4161
|
+
dateStr = d.toLocaleDateString('en', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
4162
|
+
}
|
|
4163
|
+
} catch { /* skip bad date */ }
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
// Build action button based on install type
|
|
4167
|
+
let actionHtml = '';
|
|
4168
|
+
if (isCurrent) {
|
|
4169
|
+
actionHtml = '<span class="version-installed">Installed</span>';
|
|
4170
|
+
} else if (isPackaged) {
|
|
4171
|
+
actionHtml = '<button class="version-install-btn" title="Download .dmg for this version">Download</button>';
|
|
4172
|
+
} else {
|
|
4173
|
+
actionHtml = `<button class="version-npm-btn" title="Copy npm install command">npx @${esc(r.version)}</button>`;
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
row.innerHTML = `
|
|
4177
|
+
<div class="version-info">
|
|
4178
|
+
<span class="version-tag">v${esc(r.version)}${r.prerelease ? ' <span class="version-pre">pre</span>' : ''}${isCurrent ? ' <span class="version-badge">current</span>' : ''}</span>
|
|
4179
|
+
<span class="version-date">${esc(dateStr)}</span>
|
|
4180
|
+
</div>
|
|
4181
|
+
<div class="version-actions">
|
|
4182
|
+
${actionHtml}
|
|
4183
|
+
<button class="version-notes-btn" title="View release notes">Notes</button>
|
|
4184
|
+
</div>`;
|
|
4185
|
+
|
|
4186
|
+
// DMG download button — opens the .dmg asset or release page
|
|
4187
|
+
const installBtn = row.querySelector('.version-install-btn');
|
|
4188
|
+
if (installBtn) {
|
|
4189
|
+
installBtn.addEventListener('click', () => {
|
|
4190
|
+
const url = r.dmgUrl || r.htmlUrl || '';
|
|
4191
|
+
if (url) {
|
|
4192
|
+
window.electronAPI.openExternal(url);
|
|
4193
|
+
}
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
// NPM copy button — copies the npx command to clipboard
|
|
4198
|
+
const npmBtn = row.querySelector('.version-npm-btn');
|
|
4199
|
+
if (npmBtn) {
|
|
4200
|
+
npmBtn.addEventListener('click', () => {
|
|
4201
|
+
const cmd = `npx reactoradar@${r.version}`;
|
|
4202
|
+
navigator.clipboard.writeText(cmd).then(() => {
|
|
4203
|
+
const orig = npmBtn.textContent;
|
|
4204
|
+
npmBtn.textContent = 'Copied!';
|
|
4205
|
+
npmBtn.style.color = 'var(--green)';
|
|
4206
|
+
setTimeout(() => { npmBtn.textContent = orig; npmBtn.style.color = ''; }, 2000);
|
|
4207
|
+
}).catch(() => {});
|
|
4208
|
+
});
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
// Notes button — show changelog in modal
|
|
4212
|
+
const notesBtn = row.querySelector('.version-notes-btn');
|
|
4213
|
+
if (notesBtn) {
|
|
4214
|
+
notesBtn.addEventListener('click', () => {
|
|
4215
|
+
if (r.version && typeof _showChangelog === 'function') {
|
|
4216
|
+
_showChangelog(r.version);
|
|
4217
|
+
}
|
|
4218
|
+
});
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
container.appendChild(row);
|
|
4222
|
+
});
|
|
4223
|
+
|
|
4224
|
+
// If no rows were rendered (all entries were malformed)
|
|
4225
|
+
if (container.children.length === 0) {
|
|
4226
|
+
container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">No versions found.</div>';
|
|
4227
|
+
}
|
|
4228
|
+
}).catch(() => {
|
|
4229
|
+
if (container) {
|
|
4230
|
+
container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Could not load versions. Check your internet connection.</div>';
|
|
4231
|
+
}
|
|
4232
|
+
});
|
|
3969
4233
|
}
|
|
3970
4234
|
|
|
3971
4235
|
// ─── Memory Monitor ──────────────────────────────────────────────────────────
|
package/main.js
CHANGED
|
@@ -35,6 +35,7 @@ function _send(channel, ...args) {
|
|
|
35
35
|
let reduxClients = new Set();
|
|
36
36
|
let storageClients = new Set();
|
|
37
37
|
let networkClients = new Set();
|
|
38
|
+
const _bridgeServers = []; // track bridge WSS instances for cleanup on quit
|
|
38
39
|
|
|
39
40
|
// ─── Set dock icon ASAP (before app ready) ──────────────────────────────────
|
|
40
41
|
const _appIcon = nativeImage.createFromPath(path.join(__dirname, 'ReactoRadar.png'));
|
|
@@ -74,15 +75,16 @@ if (gotLock) app.whenReady().then(async () => {
|
|
|
74
75
|
|
|
75
76
|
await createMainWindow();
|
|
76
77
|
|
|
77
|
-
// Send version to renderer — try package.json, fallback to app.getVersion()
|
|
78
|
+
// Send version + install type to renderer — try package.json, fallback to app.getVersion()
|
|
78
79
|
let appVersion;
|
|
79
80
|
try { appVersion = require('./package.json').version; } catch { appVersion = app.getVersion(); }
|
|
81
|
+
const isPackaged = app.isPackaged;
|
|
80
82
|
// Send multiple times to ensure renderer catches it (covers race conditions)
|
|
81
83
|
mainWindow.webContents.on('did-finish-load', () => {
|
|
82
84
|
// Send immediately + retries
|
|
83
|
-
_send('app-version', appVersion);
|
|
85
|
+
_send('app-version', appVersion, isPackaged);
|
|
84
86
|
[500, 2000, 5000].forEach(delay => {
|
|
85
|
-
setTimeout(() => _send('app-version', appVersion), delay);
|
|
87
|
+
setTimeout(() => _send('app-version', appVersion, isPackaged), delay);
|
|
86
88
|
});
|
|
87
89
|
});
|
|
88
90
|
|
|
@@ -102,12 +104,27 @@ app.on('window-all-closed', () => {
|
|
|
102
104
|
|
|
103
105
|
app.on('before-quit', () => {
|
|
104
106
|
_forceQuit = true;
|
|
107
|
+
// Free renderer memory before shutdown (logs are not cleared — user may still see them briefly)
|
|
108
|
+
_send('device-all-disconnected');
|
|
109
|
+
// Close CDP DevTools window if open
|
|
110
|
+
if (devtoolsWindow && !devtoolsWindow.isDestroyed()) {
|
|
111
|
+
devtoolsWindow.destroy();
|
|
112
|
+
devtoolsWindow = null;
|
|
113
|
+
}
|
|
105
114
|
// Close all WS servers gracefully
|
|
106
115
|
if (reactDTServer) {
|
|
107
116
|
reactDTServer.close();
|
|
108
117
|
reactDTClients.forEach(ws => ws.close());
|
|
109
118
|
reactDTClients.clear();
|
|
110
119
|
}
|
|
120
|
+
// Close bridge servers and disconnect all clients
|
|
121
|
+
_bridgeServers.forEach(wss => {
|
|
122
|
+
wss.clients.forEach(ws => ws.close());
|
|
123
|
+
wss.close();
|
|
124
|
+
});
|
|
125
|
+
reduxClients.clear();
|
|
126
|
+
storageClients.clear();
|
|
127
|
+
networkClients.clear();
|
|
111
128
|
});
|
|
112
129
|
|
|
113
130
|
app.on('activate', () => {
|
|
@@ -336,6 +353,10 @@ function startReactDevToolsServer() {
|
|
|
336
353
|
});
|
|
337
354
|
});
|
|
338
355
|
|
|
356
|
+
ws.on('error', (err) => {
|
|
357
|
+
console.warn(`[ReactDT] Client error:`, err.message);
|
|
358
|
+
});
|
|
359
|
+
|
|
339
360
|
ws.on('close', () => {
|
|
340
361
|
reactDTClients.delete(ws);
|
|
341
362
|
console.log(`[ReactDT] Client disconnected (total: ${reactDTClients.size})`);
|
|
@@ -380,6 +401,7 @@ function startBridgeServers() {
|
|
|
380
401
|
function startBridge(port, name, clients, onEvent) {
|
|
381
402
|
try {
|
|
382
403
|
const wss = new WebSocketServer({ port });
|
|
404
|
+
_bridgeServers.push(wss);
|
|
383
405
|
wss.on('error', (err) => {
|
|
384
406
|
if (err.code === 'EADDRINUSE') {
|
|
385
407
|
console.error(`[${name}] Port ${port} is already in use — another ReactoRadar or debugger may be running.`);
|
|
@@ -404,10 +426,19 @@ function startBridge(port, name, clients, onEvent) {
|
|
|
404
426
|
}
|
|
405
427
|
});
|
|
406
428
|
|
|
429
|
+
ws.on('error', (err) => {
|
|
430
|
+
console.warn(`[${name}] Client error:`, err.message);
|
|
431
|
+
});
|
|
432
|
+
|
|
407
433
|
ws.on('close', () => {
|
|
408
434
|
clients.delete(ws);
|
|
409
435
|
if (clients.size === 0) {
|
|
410
436
|
_send(`${name}-connected`, false);
|
|
437
|
+
// When every bridge has zero clients, tell the renderer to clear old data
|
|
438
|
+
if (reduxClients.size === 0 && storageClients.size === 0 && networkClients.size === 0) {
|
|
439
|
+
console.log('[Bridge] All device connections closed — sending clear signal');
|
|
440
|
+
_send('device-all-disconnected');
|
|
441
|
+
}
|
|
411
442
|
}
|
|
412
443
|
});
|
|
413
444
|
});
|
|
@@ -589,6 +620,9 @@ function setupIPC() {
|
|
|
589
620
|
});
|
|
590
621
|
|
|
591
622
|
ipcMain.handle('fetch-changelog', async (_, version) => {
|
|
623
|
+
if (!version || typeof version !== 'string' || !/^[\d]+\.[\d]+\.[\d]+/.test(version)) {
|
|
624
|
+
return 'Invalid version.';
|
|
625
|
+
}
|
|
592
626
|
return new Promise((resolve) => {
|
|
593
627
|
https.get(`https://api.github.com/repos/sharanagouda/reactoradar/releases/tags/v${version}`, {
|
|
594
628
|
headers: { 'User-Agent': 'ReactoRadar', 'Accept': 'application/vnd.github.v3+json' }
|
|
@@ -603,6 +637,44 @@ function setupIPC() {
|
|
|
603
637
|
});
|
|
604
638
|
});
|
|
605
639
|
|
|
640
|
+
// Fetch all releases for version history / rollback
|
|
641
|
+
ipcMain.handle('fetch-releases', async () => {
|
|
642
|
+
return new Promise((resolve) => {
|
|
643
|
+
https.get('https://api.github.com/repos/sharanagouda/reactoradar/releases?per_page=20', {
|
|
644
|
+
headers: { 'User-Agent': 'ReactoRadar', 'Accept': 'application/vnd.github.v3+json' }
|
|
645
|
+
}, (res) => {
|
|
646
|
+
let data = '';
|
|
647
|
+
res.on('data', d => data += d);
|
|
648
|
+
res.on('end', () => {
|
|
649
|
+
try {
|
|
650
|
+
const releases = JSON.parse(data);
|
|
651
|
+
if (!Array.isArray(releases)) { resolve([]); return; }
|
|
652
|
+
const mapped = [];
|
|
653
|
+
for (const r of releases) {
|
|
654
|
+
if (!r || typeof r !== 'object') continue;
|
|
655
|
+
const tag = r.tag_name || '';
|
|
656
|
+
const version = tag.replace(/^v/, '');
|
|
657
|
+
if (!version) continue; // skip entries with no version
|
|
658
|
+
const assets = Array.isArray(r.assets) ? r.assets : [];
|
|
659
|
+
mapped.push({
|
|
660
|
+
version,
|
|
661
|
+
tag,
|
|
662
|
+
name: r.name || tag || version,
|
|
663
|
+
date: r.published_at || null,
|
|
664
|
+
prerelease: !!r.prerelease,
|
|
665
|
+
body: r.body || '',
|
|
666
|
+
dmgUrl: (assets.find(a => a && a.name && a.name.endsWith('.dmg')) || {}).browser_download_url || '',
|
|
667
|
+
zipUrl: (assets.find(a => a && a.name && a.name.endsWith('.zip')) || {}).browser_download_url || '',
|
|
668
|
+
htmlUrl: r.html_url || '',
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
resolve(mapped);
|
|
672
|
+
} catch { resolve([]); }
|
|
673
|
+
});
|
|
674
|
+
}).on('error', () => resolve([]));
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
606
678
|
ipcMain.on('install-update', () => {
|
|
607
679
|
if (autoUpdater) {
|
|
608
680
|
autoUpdater.quitAndInstall(false, true);
|
|
@@ -654,10 +726,9 @@ function setupIPC() {
|
|
|
654
726
|
});
|
|
655
727
|
|
|
656
728
|
ipcMain.on('start-native-logs', (_, platform) => {
|
|
657
|
-
// Kill existing process
|
|
729
|
+
// Kill existing process
|
|
658
730
|
if (_nativeLogProcess) {
|
|
659
|
-
try {
|
|
660
|
-
try { _nativeLogProcess.kill(); } catch {}
|
|
731
|
+
try { _nativeLogProcess.kill('SIGTERM'); } catch {}
|
|
661
732
|
_nativeLogProcess = null;
|
|
662
733
|
}
|
|
663
734
|
|
|
@@ -665,9 +736,9 @@ function setupIPC() {
|
|
|
665
736
|
let cmd, args;
|
|
666
737
|
|
|
667
738
|
if (platform === 'android') {
|
|
668
|
-
// adb logcat —
|
|
739
|
+
// adb logcat — show only new logs from now (not historical buffer)
|
|
669
740
|
cmd = 'adb';
|
|
670
|
-
args = ['logcat', '-v', 'threadtime', '*:W']; //
|
|
741
|
+
args = ['logcat', '-v', 'threadtime', '-T', '1', '*:W']; // -T 1 = last 1 line then real-time
|
|
671
742
|
} else if (platform === 'ios-sim') {
|
|
672
743
|
// xcrun simctl for iOS Simulator — use syslog style for parseable output
|
|
673
744
|
cmd = 'xcrun';
|
|
@@ -682,9 +753,10 @@ function setupIPC() {
|
|
|
682
753
|
}
|
|
683
754
|
|
|
684
755
|
try {
|
|
685
|
-
_nativeLogProcess = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe']
|
|
756
|
+
_nativeLogProcess = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
686
757
|
|
|
687
758
|
_send('native-status', { connected: true, platform });
|
|
759
|
+
console.log(`[NativeLogs] Started ${cmd} ${args.join(' ')} (pid: ${_nativeLogProcess.pid})`);
|
|
688
760
|
|
|
689
761
|
let buffer = '';
|
|
690
762
|
_nativeLogProcess.stdout.on('data', (chunk) => {
|
|
@@ -703,6 +775,10 @@ function setupIPC() {
|
|
|
703
775
|
if (text) _send('native-log', { level: 'error', message: text, source: 'stderr', ts: Date.now() });
|
|
704
776
|
});
|
|
705
777
|
|
|
778
|
+
// Guard against stream errors (broken pipe, etc.)
|
|
779
|
+
_nativeLogProcess.stdout.on('error', () => {});
|
|
780
|
+
_nativeLogProcess.stderr.on('error', () => {});
|
|
781
|
+
|
|
706
782
|
_nativeLogProcess.on('close', (code) => {
|
|
707
783
|
_nativeLogProcess = null;
|
|
708
784
|
_send('native-status', { connected: false, error: code ? `Process exited with code ${code}` : 'Disconnected' });
|
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.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/preload.js
CHANGED
|
@@ -11,7 +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
|
+
'native-log', 'native-status', 'device-all-disconnected',
|
|
15
15
|
];
|
|
16
16
|
if (allowed.includes(channel)) {
|
|
17
17
|
ipcRenderer.removeAllListeners(channel);
|
|
@@ -36,4 +36,5 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|
|
36
36
|
stopNativeLogs: () => ipcRenderer.send('stop-native-logs'),
|
|
37
37
|
detectNativePlatform: () => ipcRenderer.invoke('detect-native-platform'),
|
|
38
38
|
fetchChangelog: (version) => ipcRenderer.invoke('fetch-changelog', version),
|
|
39
|
+
fetchReleases: () => ipcRenderer.invoke('fetch-releases'),
|
|
39
40
|
});
|
package/styles.css
CHANGED
|
@@ -1320,6 +1320,96 @@ mark { background: rgba(79,172,255,.2); color: var(--accent); border-radius: 2px
|
|
|
1320
1320
|
text-decoration: underline;
|
|
1321
1321
|
}
|
|
1322
1322
|
|
|
1323
|
+
/* Version History */
|
|
1324
|
+
.version-history-list {
|
|
1325
|
+
max-height: 300px;
|
|
1326
|
+
overflow-y: auto;
|
|
1327
|
+
border: 1px solid var(--border);
|
|
1328
|
+
border-radius: 6px;
|
|
1329
|
+
background: var(--bg2);
|
|
1330
|
+
}
|
|
1331
|
+
.version-history-list::-webkit-scrollbar { width: 3px; }
|
|
1332
|
+
.version-history-list::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
|
|
1333
|
+
.version-row {
|
|
1334
|
+
display: flex;
|
|
1335
|
+
align-items: center;
|
|
1336
|
+
justify-content: space-between;
|
|
1337
|
+
padding: 8px 12px;
|
|
1338
|
+
border-bottom: 1px solid var(--border);
|
|
1339
|
+
font-size: 11px;
|
|
1340
|
+
transition: background 0.15s;
|
|
1341
|
+
}
|
|
1342
|
+
.version-row:last-child { border-bottom: none; }
|
|
1343
|
+
.version-row:hover { background: var(--bg3); }
|
|
1344
|
+
.version-row.version-current { background: color-mix(in srgb, var(--accent) 8%, transparent); }
|
|
1345
|
+
.version-info { display: flex; flex-direction: column; gap: 2px; }
|
|
1346
|
+
.version-tag { color: var(--text); font-weight: 600; font-size: 12px; }
|
|
1347
|
+
.version-date { color: var(--text-dim); font-size: 10px; }
|
|
1348
|
+
.version-badge {
|
|
1349
|
+
display: inline-block;
|
|
1350
|
+
background: var(--green);
|
|
1351
|
+
color: #000;
|
|
1352
|
+
font-size: 9px;
|
|
1353
|
+
font-weight: 700;
|
|
1354
|
+
padding: 1px 5px;
|
|
1355
|
+
border-radius: 3px;
|
|
1356
|
+
margin-left: 6px;
|
|
1357
|
+
vertical-align: middle;
|
|
1358
|
+
text-transform: uppercase;
|
|
1359
|
+
}
|
|
1360
|
+
.version-pre {
|
|
1361
|
+
display: inline-block;
|
|
1362
|
+
background: var(--yellow);
|
|
1363
|
+
color: #000;
|
|
1364
|
+
font-size: 9px;
|
|
1365
|
+
font-weight: 700;
|
|
1366
|
+
padding: 1px 5px;
|
|
1367
|
+
border-radius: 3px;
|
|
1368
|
+
margin-left: 4px;
|
|
1369
|
+
vertical-align: middle;
|
|
1370
|
+
}
|
|
1371
|
+
.version-actions { display: flex; gap: 6px; align-items: center; }
|
|
1372
|
+
.version-installed {
|
|
1373
|
+
font-size: 10px;
|
|
1374
|
+
color: var(--green);
|
|
1375
|
+
font-weight: 600;
|
|
1376
|
+
}
|
|
1377
|
+
.version-install-btn {
|
|
1378
|
+
background: var(--accent);
|
|
1379
|
+
color: #fff;
|
|
1380
|
+
border: none;
|
|
1381
|
+
border-radius: 4px;
|
|
1382
|
+
padding: 3px 10px;
|
|
1383
|
+
font-size: 10px;
|
|
1384
|
+
font-weight: 600;
|
|
1385
|
+
cursor: pointer;
|
|
1386
|
+
transition: opacity 0.15s;
|
|
1387
|
+
}
|
|
1388
|
+
.version-install-btn:hover { opacity: 0.85; }
|
|
1389
|
+
.version-notes-btn {
|
|
1390
|
+
background: transparent;
|
|
1391
|
+
color: var(--text-mid);
|
|
1392
|
+
border: 1px solid var(--border);
|
|
1393
|
+
border-radius: 4px;
|
|
1394
|
+
padding: 3px 8px;
|
|
1395
|
+
font-size: 10px;
|
|
1396
|
+
cursor: pointer;
|
|
1397
|
+
transition: color 0.15s, border-color 0.15s;
|
|
1398
|
+
}
|
|
1399
|
+
.version-npm-btn {
|
|
1400
|
+
background: transparent;
|
|
1401
|
+
color: var(--accent);
|
|
1402
|
+
border: 1px solid var(--accent);
|
|
1403
|
+
border-radius: 4px;
|
|
1404
|
+
padding: 3px 8px;
|
|
1405
|
+
font-size: 10px;
|
|
1406
|
+
font-family: var(--font-mono, monospace);
|
|
1407
|
+
cursor: pointer;
|
|
1408
|
+
transition: background 0.15s, color 0.15s;
|
|
1409
|
+
}
|
|
1410
|
+
.version-npm-btn:hover { background: var(--accent); color: #fff; }
|
|
1411
|
+
.version-notes-btn:hover { color: var(--accent); border-color: var(--accent); }
|
|
1412
|
+
|
|
1323
1413
|
/* ─────────────────────────────────────────────────────────────────────────────
|
|
1324
1414
|
SOURCES PANEL
|
|
1325
1415
|
───────────────────────────────────────────────────────────────────────────── */
|