reactoradar 1.6.7 → 1.6.8

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 CHANGED
@@ -136,6 +136,8 @@ document.addEventListener('keydown', (e) => {
136
136
  }
137
137
  });
138
138
 
139
+ // Global filter removed — each panel has its own search input
140
+
139
141
  // ─── Clear (each panel has its own clear button now) ─────────────────────────
140
142
 
141
143
  function clearActiveTab() {
@@ -236,28 +238,28 @@ function clearAll() {
236
238
  if (ga4Detail) ga4Detail.innerHTML = '';
237
239
  // Native logs
238
240
  _nativeState.logs = [];
239
- const nativeList2 = $('nativeLogList');
240
- if (nativeList2) nativeList2.innerHTML = '';
241
+ const nativeList = $('nativeLogList');
242
+ if (nativeList) nativeList.innerHTML = '';
241
243
  // Performance
242
244
  perfState.fps = [];
243
245
  perfState.jsThread = [];
244
246
  perfState.uiThread = [];
245
247
  perfState.data = [];
246
- const perfFPS2 = $('perfFPS'); if (perfFPS2) perfFPS2.textContent = '—';
247
- const perfJS2 = $('perfJS'); if (perfJS2) perfJS2.textContent = '—';
248
- const perfUI2 = $('perfUI'); if (perfUI2) perfUI2.textContent = '—';
248
+ const perfFPS = $('perfFPS'); if (perfFPS) perfFPS.textContent = '—';
249
+ const perfJS = $('perfJS'); if (perfJS) perfJS.textContent = '—';
250
+ const perfUI = $('perfUI'); if (perfUI) perfUI.textContent = '—';
249
251
  clearPerfCanvas('perfFPSCanvas');
250
252
  clearPerfCanvas('perfJSCanvas');
251
253
  clearPerfCanvas('perfUICanvas');
252
254
  // Memory
253
- const memHU2 = $('memHeapUsed'); if (memHU2) memHU2.textContent = '—';
254
- const memHT2 = $('memHeapTotal'); if (memHT2) memHT2.textContent = '—';
255
- const memN2 = $('memNative'); if (memN2) memN2.textContent = '—';
255
+ const memHU = $('memHeapUsed'); if (memHU) memHU.textContent = '—';
256
+ const memHT = $('memHeapTotal'); if (memHT) memHT.textContent = '—';
257
+ const memN = $('memNative'); if (memN) memN.textContent = '—';
256
258
  // Badges
257
- $('cBadge').textContent = '0';
258
- $('nBadge').textContent = '0';
259
- $('rBadge').textContent = '0';
260
- $('sBadge').textContent = '0';
259
+ const cB = $('cBadge'); if (cB) cB.textContent = '0';
260
+ const nB = $('nBadge'); if (nB) nB.textContent = '0';
261
+ const rB = $('rBadge'); if (rB) rB.textContent = '0';
262
+ const sB = $('sBadge'); if (sB) sB.textContent = '0';
261
263
  if ($('ga4Badge')) $('ga4Badge').textContent = '0';
262
264
  if ($('nativeBadge')) $('nativeBadge').textContent = '0';
263
265
  // Re-render all
@@ -281,13 +283,8 @@ function freeMemory() {
281
283
  if (state.console.logs.length > 200) {
282
284
  state.console.logs = state.console.logs.slice(-200);
283
285
  }
284
- // Trim Redux history (keep actions and states in sync — they must have same length)
285
- if (state.redux.actions.length > 50) {
286
- state.redux.actions = state.redux.actions.slice(-50);
287
- state.redux.states = state.redux.states.slice(-50);
288
- state.redux.actions.forEach((a, i) => a.index = i);
289
- state.redux.selected = -1;
290
- }
286
+ // Drop full Redux state snapshots (keep action metadata)
287
+ state.redux.states = [];
291
288
  // Drop storage values (keep keys for reference)
292
289
  for (const k in state.storage.entries) {
293
290
  state.storage.entries[k] = null;
@@ -327,11 +324,13 @@ function updateDeviceBanner(service, connected) {
327
324
  }
328
325
  }
329
326
 
327
+
330
328
  function takeScreenshot() {
331
329
  const btn = $('btnScreenshot');
332
330
  if (!btn) return;
333
331
  const origText = btn.innerHTML;
334
332
  btn.innerHTML = '<span style="opacity:0.6">Saving...</span>';
333
+ // Use Electron's native capturePage — always works, no DOM rendering issues
335
334
  window.electronAPI?.captureScreenshot();
336
335
  btn.innerHTML = '<span style="color:var(--green)">Saved!</span>';
337
336
  setTimeout(() => { btn.innerHTML = origText; }, 2000);
package/init.js CHANGED
@@ -5,7 +5,8 @@
5
5
 
6
6
  // ─── CDP Button ───────────────────────────────────────────────────────────────
7
7
  $('btnCDP')?.addEventListener('click', () => {
8
- window.electronAPI?.openCDPTarget(null);
8
+ // Tell main process to open the CDP DevTools window with the best available target
9
+ window.electronAPI?.openCDPTarget(null); // null = use latest known target
9
10
  });
10
11
 
11
12
  // ─── Screenshot Button ────────────────────────────────────────────────────────
@@ -37,7 +38,6 @@ if (window.electronAPI) {
37
38
  window.electronAPI.on('network-event', handleNetworkEvent);
38
39
  window.electronAPI.on('storage-event', handleStorageEvent);
39
40
 
40
- window.electronAPI.on('console-event', addConsoleLog);
41
41
  window.electronAPI.on('ga4-event', handleGA4Event);
42
42
 
43
43
  window.electronAPI.on('perf-event', event => {
@@ -66,6 +66,7 @@ if (window.electronAPI) {
66
66
 
67
67
  // Cmd+F — focus the search input for the active panel
68
68
  function _handleFind() {
69
+ // If network detail is open, focus the detail search
69
70
  if (state.activePanel === 'network' && state.network.selectedId) {
70
71
  const wrap = $('detailSearchWrap');
71
72
  const input = $('detailSearchInput');
@@ -88,12 +89,14 @@ if (window.electronAPI) {
88
89
  const el = $(inputId);
89
90
  if (el) { el.focus(); el.select(); }
90
91
  }
92
+ // Also show/focus Console bottom find bar
91
93
  if (state.activePanel === 'console') {
92
94
  const bar = $('consoleFindBar');
93
95
  if (bar) { bar.style.display = 'flex'; $('consoleFindInput')?.focus(); }
94
96
  }
95
97
  }
96
98
  window.electronAPI.on('focus-search', _handleFind);
99
+ // Direct keyboard fallback — Electron menu accelerators can miss in some contexts
97
100
  document.addEventListener('keydown', (e) => {
98
101
  if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
99
102
  e.preventDefault();
@@ -104,6 +107,7 @@ if (window.electronAPI) {
104
107
  window.electronAPI.on('app-version', (version, isPackaged) => {
105
108
  state._appVersion = version;
106
109
  state._isPackaged = !!isPackaged;
110
+ // Update anywhere the version is displayed
107
111
  document.querySelectorAll('#aboutVersion').forEach(el => el.textContent = 'v' + version);
108
112
  });
109
113
 
@@ -121,21 +125,27 @@ if (window.electronAPI) {
121
125
  window.electronAPI?.openCDPTarget(null);
122
126
  });
123
127
 
128
+ // Theme toggle from menu shortcut (Cmd+Shift+T)
124
129
  window.electronAPI.on('theme-changed', theme => {
125
130
  document.documentElement.setAttribute('data-theme', theme);
126
131
  setStoredTheme(theme);
127
132
  document.querySelectorAll('#themeSwitcher .theme-card')
128
133
  .forEach(b => b.classList.toggle('active', b.dataset.theme === theme));
129
134
  });
135
+
136
+ // Console event IPC
137
+ window.electronAPI.on('console-event', addConsoleLog);
130
138
  }
131
139
 
132
140
  // ─── Memory Monitor ──────────────────────────────────────────────────────────
141
+ // Check memory usage periodically and warn user before it causes blank screen
133
142
  let _memoryWarningShown = false;
134
143
  setInterval(() => {
135
144
  if (!window.performance || !performance.memory) return;
136
145
  const used = performance.memory.usedJSHeapSize;
137
146
  const limit = performance.memory.jsHeapSizeLimit;
138
147
  const pct = used / limit;
148
+ // Warn at 70% usage
139
149
  if (pct > 0.7 && !_memoryWarningShown) {
140
150
  _memoryWarningShown = true;
141
151
  const banner = document.createElement('div');
@@ -147,6 +157,7 @@ setInterval(() => {
147
157
  + `<button class="memory-warn-btn" id="memWarnDismiss">Dismiss</button>`;
148
158
  document.body.prepend(banner);
149
159
  $('memWarnClear')?.addEventListener('click', () => {
160
+ // Clear all panel data
150
161
  state.console.logs = []; _consolePending = [];
151
162
  _lastLogMsg = ''; _lastLogRow = null; _lastLogCount = 1;
152
163
  $('cBadge').textContent = '0'; renderConsole();
@@ -158,17 +169,19 @@ setInterval(() => {
158
169
  });
159
170
  $('memWarnDismiss')?.addEventListener('click', () => { banner.remove(); });
160
171
  }
172
+ // Reset flag when memory drops
161
173
  if (pct < 0.5) _memoryWarningShown = false;
162
- }, 30000);
174
+ }, 30000); // Check every 30 seconds
175
+
176
+ // Apply saved theme + font size + font family + app name on load
163
177
 
164
- // ─────────────────────────────────────────────────────────────────────────────
165
- // Apply saved settings on load
166
- // ─────────────────────────────────────────────────────────────────────────────
167
178
  applyTheme(getStoredTheme());
168
179
  applyFontSize(getStoredFontSize());
169
180
  applyFontFamily(getStoredFontFamily());
170
181
  applyAppName(getStoredAppName());
171
182
  applyTabVisibility();
183
+
184
+ // Send stored metro port to backend
172
185
  window.electronAPI?.setMetroPort(getStoredMetroPort());
173
186
 
174
187
  // ─────────────────────────────────────────────────────────────────────────────
@@ -182,6 +195,5 @@ initMemoryPanel();
182
195
  initReduxPanel();
183
196
  initStoragePanel();
184
197
  initReactPanel();
185
- initSourcesPanel();
186
198
  initNativeLogsPanel();
187
199
  initSettingsPanel();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.6.7",
4
+ "version": "1.6.8",
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/panels/console.js CHANGED
@@ -1,5 +1,4 @@
1
- // ─── Console Panel ─────────────────────────────────────────────────────────
2
-
1
+ // ─── Console Panel + Shared Renderers ─────────────────────────────────────
3
2
  // Load saved log level filters from localStorage
4
3
  function getStoredLogLevels() {
5
4
  try {
@@ -14,6 +13,7 @@ function setStoredLogLevels(levels) {
14
13
 
15
14
  function initConsolePanel() {
16
15
  const panel = $('panel-console');
16
+ if (!panel) return;
17
17
  const levels = getStoredLogLevels();
18
18
  state.console.levelFilters = levels;
19
19
  state.console.showRedux = !!levels.redux;
@@ -321,10 +321,10 @@ function flushConsoleBatch() {
321
321
  }
322
322
  }
323
323
 
324
- // NOTE: console-event IPC listener is registered in init.js (not here)
325
- // to keep all IPC registrations in one place and avoid duplicate listener issues.
326
324
 
327
- // ─── Shared Object Tree Renderer (Chrome DevTools-like) ──────────────────────
325
+ // NOTE: console-event IPC listener is registered in init.js
326
+
327
+ // ─── Object Tree Renderer (Chrome DevTools-like) ─────────────────────────────
328
328
  // Builds interactive, collapsible DOM nodes for objects/arrays.
329
329
 
330
330
  // Collect all entries for an object: own data properties + prototype getter values.
@@ -709,7 +709,7 @@ function buildLogRow(l) {
709
709
  return div;
710
710
  }
711
711
 
712
- // ─── Shared Context Menu ─────────────────────────────────────────────────────
712
+ // ─── Shared context menu helper ──────────────────────────────────────────────
713
713
  function showContextMenu(e, items) {
714
714
  document.querySelectorAll('.ctx-menu').forEach(el => el.remove());
715
715
  const menu = document.createElement('div');
@@ -786,3 +786,4 @@ function renderConsole() {
786
786
  list.appendChild(frag);
787
787
  list.scrollTop = list.scrollHeight;
788
788
  }
789
+
package/panels/ga4.js CHANGED
@@ -1,10 +1,12 @@
1
+ // ─── GA4 Events Panel ──────────────────────────────────────────────────────
1
2
  // ─────────────────────────────────────────────────────────────────────────────
2
- // GA4 EVENT INSPECTOR — extracted from app.js
3
+ // GA4 EVENT INSPECTOR
3
4
  // ─────────────────────────────────────────────────────────────────────────────
4
5
  const ga4State = { events: [], selected: -1, searchFilter: '', sortDir: 'desc' };
5
6
 
6
7
  function initGA4Panel() {
7
8
  const panel = $('panel-ga4');
9
+ if (!panel) return;
8
10
  panel.innerHTML = `
9
11
  <div class="panel-toolbar">
10
12
  <span class="panel-label">GA4 Events</span>
@@ -326,3 +328,4 @@ function renderGA4Summary() {
326
328
  summary.appendChild(chip);
327
329
  });
328
330
  }
331
+
package/panels/native.js CHANGED
@@ -1,4 +1,7 @@
1
1
  // ─── Native Logs Panel ─────────────────────────────────────────────────────
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // NATIVE LOGS PANEL
4
+ // ─────────────────────────────────────────────────────────────────────────────
2
5
  const _nativeState = { logs: [], connected: false, platform: null, levelFilter: 'all', searchFilter: '' };
3
6
  const MAX_NATIVE_LOGS = 2000;
4
7
 
@@ -254,3 +257,4 @@ function _renderNativeLogs() {
254
257
  list.innerHTML = '';
255
258
  _nativeState.logs.forEach(log => _appendNativeLog(log));
256
259
  }
260
+
package/panels/network.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // ─── Network Panel ─────────────────────────────────────────────────────────
2
-
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // NETWORK PANEL (Chrome DevTools-style)
4
+ // ─────────────────────────────────────────────────────────────────────────────
3
5
  const NET_COLS = [
4
6
  { key: 'name', label: 'Name', width: 380, min: 150 },
5
7
  { key: 'status', label: 'Status', width: 60, min: 40 },
@@ -12,6 +14,7 @@ const NET_COLS = [
12
14
 
13
15
  function initNetworkPanel() {
14
16
  const panel = $('panel-network');
17
+ if (!panel) return;
15
18
  panel.innerHTML = `
16
19
  <div class="panel-toolbar">
17
20
  <span class="panel-label">Network</span>
@@ -966,3 +969,4 @@ function buildCurlCommand(r) {
966
969
  }
967
970
  return cmd;
968
971
  }
972
+
@@ -1,8 +1,12 @@
1
- // ─── Performance + Memory Panel ────────────────────────────────────────────
1
+ // ─── Performance + Memory Panels ───────────────────────────────────────────
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // PERFORMANCE PANEL — FPS, render timing, JS thread
4
+ // ─────────────────────────────────────────────────────────────────────────────
2
5
  const perfState = { fps: [], jsThread: [], uiThread: [], recording: false, data: [] };
3
6
 
4
7
  function initPerformancePanel() {
5
8
  const panel = $('panel-performance');
9
+ if (!panel) return;
6
10
  panel.innerHTML = `
7
11
  <div class="panel-toolbar">
8
12
  <span class="panel-label">Performance</span>
@@ -130,9 +134,12 @@ function handlePerfEvent(event) {
130
134
  }
131
135
  }
132
136
 
133
- // ─── Memory Panel ────────────────────────────────────────────────────────────
137
+ // ─────────────────────────────────────────────────────────────────────────────
138
+ // MEMORY PANEL — Heap snapshot summary via Hermes CDP
139
+ // ─────────────────────────────────────────────────────────────────────────────
134
140
  function initMemoryPanel() {
135
141
  const panel = $('panel-memory');
142
+ if (!panel) return;
136
143
  panel.innerHTML = `
137
144
  <div class="panel-toolbar">
138
145
  <span class="panel-label">Memory</span>
package/panels/react.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // ─── React Tree Panel ──────────────────────────────────────────────────────
2
2
  function initReactPanel() {
3
3
  const panel = $('panel-react');
4
+ if (!panel) return;
4
5
  panel.innerHTML = `
5
6
  <div class="panel-toolbar">
6
7
  <span class="panel-label">React Tree</span>
@@ -19,3 +20,4 @@ function initReactPanel() {
19
20
  window.electronAPI?.openReactDevTools();
20
21
  });
21
22
  }
23
+
package/panels/redux.js CHANGED
@@ -1,8 +1,10 @@
1
+ // ─── Redux Panel ───────────────────────────────────────────────────────────
1
2
  // ─────────────────────────────────────────────────────────────────────────────
2
- // REDUX PANEL — extracted from app.js
3
+ // REDUX PANEL
3
4
  // ─────────────────────────────────────────────────────────────────────────────
4
5
  function initReduxPanel() {
5
6
  const panel = $('panel-redux');
7
+ if (!panel) return;
6
8
  panel.innerHTML = `
7
9
  <div class="panel-toolbar">
8
10
  <span class="panel-label">Redux</span>
@@ -436,3 +438,4 @@ function renderRedux() {
436
438
  selEl.scrollIntoView({ block: 'nearest', behavior: 'auto' });
437
439
  }
438
440
  }
441
+
@@ -1,6 +1,4 @@
1
- // ─── Settings Panel ────────────────────────────────────────────────────────
2
-
3
- // ─── Theme helpers ───────────────────────────────────────────────────────────
1
+ // ─── Settings Panel + Shared Utilities ─────────────────────────────────────
4
2
  function getStoredTheme() {
5
3
  try { return localStorage.getItem('rn-debug-theme') || 'dark'; } catch { return 'dark'; }
6
4
  }
@@ -303,6 +301,7 @@ function applyFontSize(size) {
303
301
 
304
302
  function initSettingsPanel() {
305
303
  const panel = $('panel-settings');
304
+ if (!panel) return;
306
305
  const current = getStoredTheme();
307
306
  const currentSize = getStoredFontSize();
308
307
  panel.innerHTML = `
@@ -594,13 +593,12 @@ function _loadVersionHistory() {
594
593
  } catch { /* skip bad date */ }
595
594
  }
596
595
 
597
- // Build action buttons based on install type
596
+ // Build action button based on install type
598
597
  let actionHtml = '';
599
598
  if (isCurrent) {
600
599
  actionHtml = '<span class="version-installed">Installed</span>';
601
600
  } else if (isPackaged) {
602
- // Show download dropdown with .dmg and .zip links
603
- actionHtml = '<div class="version-dl-wrap"><button class="version-install-btn" title="Download this version">Download ▾</button></div>';
601
+ actionHtml = '<button class="version-install-btn" title="Download .dmg for this version">Download</button>';
604
602
  } else {
605
603
  actionHtml = `<button class="version-npm-btn" title="Copy npm install command">npx @${esc(r.version)}</button>`;
606
604
  }
@@ -615,57 +613,14 @@ function _loadVersionHistory() {
615
613
  <button class="version-notes-btn" title="View release notes">Notes</button>
616
614
  </div>`;
617
615
 
618
- // DMG/ZIP download dropdown
619
- const dlWrap = row.querySelector('.version-dl-wrap');
616
+ // DMG download button — opens the .dmg asset or release page
620
617
  const installBtn = row.querySelector('.version-install-btn');
621
- if (installBtn && dlWrap) {
622
- installBtn.addEventListener('click', (e) => {
623
- e.stopPropagation();
624
- // Remove any existing dropdown
625
- document.querySelectorAll('.version-dl-menu').forEach(m => m.remove());
626
-
627
- const menu = document.createElement('div');
628
- menu.className = 'version-dl-menu';
629
-
630
- // .dmg link
631
- if (r.dmgUrl) {
632
- const dmgItem = document.createElement('div');
633
- dmgItem.className = 'version-dl-item';
634
- dmgItem.innerHTML = `<span class="version-dl-icon">💿</span><div><div class="version-dl-name">.dmg Installer</div><div class="version-dl-hint">macOS installer (Apple Silicon)</div></div>`;
635
- dmgItem.addEventListener('click', () => { window.electronAPI.openExternal(r.dmgUrl); menu.remove(); });
636
- menu.appendChild(dmgItem);
637
- }
638
-
639
- // .zip link
640
- if (r.zipUrl) {
641
- const zipItem = document.createElement('div');
642
- zipItem.className = 'version-dl-item';
643
- zipItem.innerHTML = `<span class="version-dl-icon">📦</span><div><div class="version-dl-name">.zip Archive</div><div class="version-dl-hint">Portable zip archive</div></div>`;
644
- zipItem.addEventListener('click', () => { window.electronAPI.openExternal(r.zipUrl); menu.remove(); });
645
- menu.appendChild(zipItem);
646
- }
647
-
648
- // GitHub release page fallback
649
- if (r.htmlUrl) {
650
- const ghItem = document.createElement('div');
651
- ghItem.className = 'version-dl-item';
652
- ghItem.innerHTML = `<span class="version-dl-icon">🔗</span><div><div class="version-dl-name">GitHub Release</div><div class="version-dl-hint">View all assets on GitHub</div></div>`;
653
- ghItem.addEventListener('click', () => { window.electronAPI.openExternal(r.htmlUrl); menu.remove(); });
654
- menu.appendChild(ghItem);
655
- }
656
-
657
- // No assets fallback
658
- if (!r.dmgUrl && !r.zipUrl && !r.htmlUrl) {
659
- menu.innerHTML = '<div style="padding:8px 12px;color:var(--text-dim);font-size:10px">No downloads available</div>';
618
+ if (installBtn) {
619
+ installBtn.addEventListener('click', () => {
620
+ const url = r.dmgUrl || r.htmlUrl || '';
621
+ if (url) {
622
+ window.electronAPI.openExternal(url);
660
623
  }
661
-
662
- dlWrap.appendChild(menu);
663
-
664
- // Close on outside click
665
- setTimeout(() => {
666
- const close = (ev) => { if (!menu.contains(ev.target) && ev.target !== installBtn) { menu.remove(); document.removeEventListener('click', close); } };
667
- document.addEventListener('click', close);
668
- }, 0);
669
624
  });
670
625
  }
671
626
 
@@ -707,7 +662,8 @@ function _loadVersionHistory() {
707
662
  });
708
663
  }
709
664
 
710
- // ─── Update Banner ───────────────────────────────────────────────────────────
665
+
666
+ // ─── Update Banner & Changelog (shared, used by IPC handlers) ──────────────
711
667
  // Reusable — called from IPC handler AND from initSettingsPanel
712
668
  function _applyUpdateBanner() {
713
669
  const info = state._updateAvailable;
@@ -830,3 +786,6 @@ async function _showChangelog(version) {
830
786
  if (body) body.innerHTML = '<div style="color:var(--red);padding:20px;text-align:center">Could not fetch release notes</div>';
831
787
  }
832
788
  }
789
+
790
+
791
+
package/panels/sources.js CHANGED
@@ -1,6 +1,10 @@
1
1
  // ─── Sources Panel ─────────────────────────────────────────────────────────
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // SOURCES PANEL (placeholder — use JS Debugger button for breakpoints)
4
+ // ─────────────────────────────────────────────────────────────────────────────
2
5
  function initSourcesPanel() {
3
6
  const panel = $('panel-sources');
7
+ if (!panel) return;
4
8
  panel.innerHTML = `
5
9
  <div class="panel-toolbar">
6
10
  <span class="panel-label">Sources</span>
@@ -280,3 +284,6 @@ async function loadSourceFile(filepath) {
280
284
  function updateSourcesPanel(targets) {
281
285
  // No-op: file list is populated by fetchSourceFileList from Metro source map
282
286
  }
287
+
288
+ // ─────────────────────────────────────────────────────────────────────────────
289
+ // PERFORMANCE PANEL — FPS, render timing, JS thread
package/panels/storage.js CHANGED
@@ -1,8 +1,10 @@
1
+ // ─── AsyncStorage Panel ────────────────────────────────────────────────────
1
2
  // ─────────────────────────────────────────────────────────────────────────────
2
- // ASYNC STORAGE PANEL — extracted from app.js
3
+ // ASYNC STORAGE PANEL
3
4
  // ─────────────────────────────────────────────────────────────────────────────
4
5
  function initStoragePanel() {
5
6
  const panel = $('panel-storage');
7
+ if (!panel) return;
6
8
  panel.innerHTML = `
7
9
  <div class="panel-toolbar">
8
10
  <span class="panel-label">AsyncStorage</span>
@@ -183,3 +185,7 @@ function formatSize(bytes) {
183
185
  if (bytes < 1024) return `${bytes}b`;
184
186
  return `${(bytes/1024).toFixed(1)}kb`;
185
187
  }
188
+
189
+ // ─────────────────────────────────────────────────────────────────────────────
190
+ // REACT TREE PANEL
191
+ // ─────────────────────────────────────────────────────────────────────────────
package/sdk/RNDebugSDK.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * watchAsyncStorage(); // call once early in app
15
15
  */
16
16
 
17
- if (!__DEV__) {
17
+ if (typeof __DEV__ === 'undefined' || !__DEV__) {
18
18
  module.exports = { reduxEnhancer: x => x, reduxMiddleware: () => next => action => next(action), watchAsyncStorage: () => {} };
19
19
  } else {
20
20
 
@@ -80,29 +80,38 @@ function _shouldIntercept() {
80
80
 
81
81
  // ─── WebSocket Factory ────────────────────────────────────────────────────────
82
82
  function makeChannel(port, name, onMessage) {
83
- let ws = null, queue = [], connected = false;
83
+ let ws = null, queue = [], connected = false, retryDelay = 2000;
84
84
 
85
85
  function connect() {
86
+ ws = null;
87
+ connected = false;
86
88
  try {
87
89
  ws = new WebSocket(`ws://${HOST}:${port}`);
88
90
  ws.onopen = () => {
89
91
  connected = true;
90
- queue.forEach(m => ws.send(m));
91
- queue = [];
92
+ retryDelay = 2000;
93
+ const pending = queue.splice(0);
94
+ for (const m of pending) {
95
+ try { if (ws.readyState === WebSocket.OPEN) ws.send(m); else { queue.push(m); break; } }
96
+ catch { queue.push(m); break; }
97
+ }
92
98
  };
93
99
  ws.onmessage = (evt) => {
94
100
  if (onMessage) {
95
101
  try { onMessage(JSON.parse(evt.data)); } catch {}
96
102
  }
97
103
  };
98
- ws.onclose = () => { connected = false; setTimeout(connect, 2000); };
104
+ ws.onclose = () => { connected = false; setTimeout(connect, retryDelay); retryDelay = Math.min(retryDelay * 1.5, 30000); };
99
105
  ws.onerror = () => {};
100
- } catch { setTimeout(connect, 2000); }
106
+ } catch { setTimeout(connect, retryDelay); retryDelay = Math.min(retryDelay * 1.5, 30000); }
101
107
  }
102
108
 
103
109
  function send(obj) {
104
- const msg = JSON.stringify({ ...obj, ts: Date.now() });
105
- if (connected && ws?.readyState === WebSocket.OPEN) ws.send(msg);
110
+ let msg;
111
+ try {
112
+ msg = JSON.stringify({ ...obj, ts: Date.now() }, (_, v) => typeof v === 'bigint' ? v.toString() : v);
113
+ } catch { return; }
114
+ if (connected && ws?.readyState === WebSocket.OPEN) { try { ws.send(msg); } catch {} }
106
115
  else { queue.push(msg); if (queue.length > 300) queue.shift(); }
107
116
  }
108
117
 
@@ -186,21 +195,25 @@ function _extractCaller() {
186
195
  }
187
196
 
188
197
  LEVELS.forEach(level => {
189
- _console[level] = console[level].bind(console);
198
+ const orig = console[level];
199
+ if (typeof orig !== 'function') return;
200
+ _console[level] = orig.bind(console);
190
201
  console[level] = (...args) => {
191
- _console[level](...args);
192
- // Skip interception when SDK is paused or debugger is attached
193
- // This prevents double-logging and message queue deadlocks with CDP
194
- if (!_shouldIntercept()) return;
195
- const structuredArgs = args.map(serializeArg);
196
- const message = args.map(a => {
197
- if (typeof a === 'string') return a;
198
- try { return JSON.stringify(a, null, 2); } catch { return String(a); }
199
- }).join(' ');
200
- // Stack trace capture controlled by toggle (disabled by default for performance)
201
- // When enabled: captures for all levels. When disabled: skips entirely.
202
- const caller = _stackTraceEnabled ? _extractCaller() : '';
203
- mainCh.send({ type: 'console', level, message, args: structuredArgs, caller });
202
+ try { _console[level](...args); } catch {}
203
+ try {
204
+ // Skip interception when SDK is paused or debugger is attached
205
+ // This prevents double-logging and message queue deadlocks with CDP
206
+ if (!_shouldIntercept()) return;
207
+ const structuredArgs = args.map(serializeArg);
208
+ const message = args.map(a => {
209
+ if (typeof a === 'string') return a;
210
+ try { return JSON.stringify(a, null, 2); } catch { return String(a); }
211
+ }).join(' ');
212
+ // Stack trace capture controlled by toggle (disabled by default for performance)
213
+ // When enabled: captures for all levels. When disabled: skips entirely.
214
+ const caller = _stackTraceEnabled ? _extractCaller() : '';
215
+ mainCh.send({ type: 'console', level, message, args: structuredArgs, caller });
216
+ } catch {} // end of interception try
204
217
  };
205
218
  });
206
219
 
@@ -250,17 +263,31 @@ global.fetch = async (input, init = {}) => {
250
263
  const t0 = Date.now();
251
264
  try {
252
265
  const resp = await _fetch(input, init);
253
- const clone = resp.clone();
254
- clone.text().then(body => {
255
- if (!_networkCaptureEnabled) return;
256
- let parsed = body;
257
- try { parsed = JSON.parse(body); } catch {}
258
- const rHeaders = {};
259
- clone.headers?.forEach?.((v, k) => { rHeaders[k] = v; });
260
- mainCh.send({ type: 'network', phase: 'response', id, url, method,
261
- status: resp.status, statusText: resp.statusText,
262
- duration: Date.now() - t0, responseHeaders: rHeaders, responseBody: parsed });
263
- }).catch(() => {});
266
+ try {
267
+ const contentType = resp.headers?.get?.('content-type') || '';
268
+ const contentLen = parseInt(resp.headers?.get?.('content-length') || '0', 10);
269
+ if (contentLen > 1_000_000 || /image|video|audio|octet-stream|font/i.test(contentType)) {
270
+ const rHeaders = {};
271
+ resp.headers?.forEach?.((v, k) => { rHeaders[k] = v; });
272
+ mainCh.send({ type: 'network', phase: 'response', id, url: String(typeof input === 'string' ? input : input?.url || ''),
273
+ method, status: resp.status, statusText: resp.statusText, duration: Date.now() - t0,
274
+ responseHeaders: rHeaders, responseBody: `[Binary ${contentType} — ${contentLen} bytes]` });
275
+ } else {
276
+ try {
277
+ const clone = resp.clone();
278
+ clone.text().then(body => {
279
+ if (!_networkCaptureEnabled) return;
280
+ let parsed = body;
281
+ try { parsed = JSON.parse(body); } catch {}
282
+ const rHeaders = {};
283
+ clone.headers?.forEach?.((v, k) => { rHeaders[k] = v; });
284
+ mainCh.send({ type: 'network', phase: 'response', id, url, method,
285
+ status: resp.status, statusText: resp.statusText,
286
+ duration: Date.now() - t0, responseHeaders: rHeaders, responseBody: parsed });
287
+ }).catch(() => {});
288
+ } catch {} // clone failed — skip capture
289
+ }
290
+ } catch {} // header access failed
264
291
  return resp;
265
292
  } catch (err) {
266
293
  mainCh.send({ type: 'network', phase: 'error', id, url, method,
@@ -292,6 +319,7 @@ global.fetch = async (input, init = {}) => {
292
319
  meta.method = (method || 'GET').toUpperCase();
293
320
  meta.url = String(url);
294
321
  meta.t0 = Date.now();
322
+ meta.sent = false;
295
323
  return _open.apply(xhr, arguments);
296
324
  };
297
325
 
@@ -452,13 +480,33 @@ function reduxEnhancer(createStore) {
452
480
  let actionCount = 0;
453
481
 
454
482
  // Send initial state
455
- reduxCh.send({ type: 'redux', action: { type: '@@INIT' }, nextState: store.getState(), index: actionCount++ });
483
+ try {
484
+ const initState = store.getState();
485
+ let safeInit;
486
+ try {
487
+ const s = JSON.stringify(initState, (_, v) => typeof v === 'bigint' ? v.toString() : v);
488
+ safeInit = s.length > 1_000_000 ? { __truncated: true, sizeBytes: s.length, keys: Object.keys(initState) } : JSON.parse(s);
489
+ } catch { safeInit = { __error: 'State not serializable' }; }
490
+ reduxCh.send({ type: 'redux', action: { type: '@@INIT' }, nextState: safeInit, index: actionCount++ });
491
+ } catch {}
456
492
 
457
493
  const origDispatch = store.dispatch;
458
494
  store.dispatch = (action) => {
459
495
  const result = origDispatch(action);
460
- const nextState = store.getState();
461
- reduxCh.send({ type: 'redux', action, nextState, index: actionCount++ });
496
+ try {
497
+ const nextState = store.getState();
498
+ const safeAction = typeof action === 'function'
499
+ ? { type: `[Function: ${action.name || 'thunk'}]` }
500
+ : (action || { type: '@@UNKNOWN' });
501
+ let safeState;
502
+ try {
503
+ const s = JSON.stringify(nextState, (_, v) => typeof v === 'bigint' ? v.toString() : v);
504
+ safeState = s.length > 1_000_000
505
+ ? { __truncated: true, sizeBytes: s.length, keys: Object.keys(nextState) }
506
+ : JSON.parse(s);
507
+ } catch { safeState = { __error: 'State not serializable' }; }
508
+ reduxCh.send({ type: 'redux', action: safeAction, nextState: safeState, index: actionCount++ });
509
+ } catch {}
462
510
  return result;
463
511
  };
464
512
  return store;
@@ -467,9 +515,23 @@ function reduxEnhancer(createStore) {
467
515
 
468
516
  // ─── Redux Toolkit middleware (alternative) ───────────────────────────────────
469
517
  // If you use RTK configureStore, add this to middleware array instead:
518
+ let _mwActionCount = 0;
470
519
  const reduxMiddleware = store => next => action => {
471
520
  const result = next(action);
472
- reduxCh.send({ type: 'redux', action, nextState: store.getState() });
521
+ try {
522
+ const nextState = store.getState();
523
+ const safeAction = typeof action === 'function'
524
+ ? { type: `[Function: ${action.name || 'thunk'}]` }
525
+ : (action || { type: '@@UNKNOWN' });
526
+ let safeState;
527
+ try {
528
+ const s = JSON.stringify(nextState, (_, v) => typeof v === 'bigint' ? v.toString() : v);
529
+ safeState = s.length > 1_000_000
530
+ ? { __truncated: true, sizeBytes: s.length, keys: Object.keys(nextState) }
531
+ : JSON.parse(s);
532
+ } catch { safeState = { __error: 'State not serializable' }; }
533
+ reduxCh.send({ type: 'redux', action: safeAction, nextState: safeState, index: _mwActionCount++ });
534
+ } catch {}
473
535
  return result;
474
536
  };
475
537
 
@@ -510,7 +572,7 @@ function watchAsyncStorage() {
510
572
  RNAsyncStorage.mergeItem = async (key, value, ...rest) => {
511
573
  const result = await _mergeItem(key, value, ...rest);
512
574
  // Read back merged value
513
- RNAsyncStorage.getItem(key).then(v => storageCh.send({ type: 'storage', action: 'set', key, value: v }));
575
+ RNAsyncStorage.getItem(key).then(v => storageCh.send({ type: 'storage', action: 'set', key, value: v })).catch(() => {});
514
576
  return result;
515
577
  };
516
578