reactoradar 1.6.10 → 1.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,6 +53,17 @@
53
53
  | **React** | Component tree and props inspector via `react-devtools-core` relay |
54
54
  | **Settings** | 9 color themes, font family/size, configurable panel visibility with drag-to-reorder, Metro port config, keyboard shortcuts, auto-update, support link |
55
55
 
56
+ ### What's New in v1.6.11
57
+
58
+ - **Auto-clear on reconnect** — All tabs reset automatically when the RN app relaunches (fresh session, no stale data)
59
+ - **No more `[object Object]`** — Safe string serialization everywhere in Console, Network, and Redux panels
60
+ - **SDK auto-detects platform** — Android emulator (`10.0.2.2`) and iOS simulator (`127.0.0.1`) detected at runtime. No manual HOST editing.
61
+ - **Setup auto-patches legacy `createStore`** — Detects `const middleware = []` pattern and wires `reduxMiddleware` automatically
62
+ - **SDK hardened** — BigInt/circular-safe JSON, Redux state >1MB truncated, binary response guard, reconnect backoff, console try/catch
63
+ - **Version rollback** — Settings > Version History shows all releases with download/install buttons
64
+ - **Panel-per-file architecture** — Each panel in its own file under `panels/`. Safer, easier to maintain.
65
+ - **Null guards everywhere** — All panel init functions, badge updates, and DOM access null-safe
66
+
56
67
  ### What's New in v1.6.0
57
68
 
58
69
  - **Auto-Update** — `.dmg` builds auto-download updates from GitHub Releases. Settings shows "Restart & Update" when ready.
@@ -134,7 +145,7 @@ Console, Network, Redux, GA4, AsyncStorage data flows automatically. No config n
134
145
  | Android real device (USB) | `10.0.2.2` | `adb reverse` tunnels over USB (auto-configured) |
135
146
  | iOS real device (USB/WiFi) | Mac's LAN IP | Auto-detected. Device must be on same WiFi as Mac. |
136
147
 
137
- `npx reactoradar setup` auto-detects your platform and sets the correct HOST.
148
+ The SDK auto-detects the platform at runtime. For iOS real devices, set `HOST_OVERRIDE` in `src/debug/RNDebugSDK.js` to your Mac's LAN IP. `npx reactoradar setup` handles this automatically.
138
149
 
139
150
  ### Uninstall
140
151
 
@@ -146,7 +157,7 @@ npx reactoradar remove
146
157
 
147
158
  | ReactoRadar | React Native | Engine | Architecture |
148
159
  |---|---|---|---|
149
- | v1.6+ | 0.74 — 0.81+ | Hermes | Old & New Architecture |
160
+ | v1.6.11+ | 0.74 — 0.81+ | Hermes | Old & New Architecture |
150
161
 
151
162
  ## Network Inspector
152
163
 
@@ -203,13 +214,26 @@ export const store = configureStore({
203
214
  });
204
215
  ```
205
216
 
206
- **Legacy Redux (createStore):**
217
+ **Legacy Redux (createStore with middleware array):**
218
+ ```js
219
+ const middleware = [];
220
+ // ... your existing middleware (saga, thunk, etc.)
221
+ if (__DEV__) {
222
+ try {
223
+ const { reduxMiddleware } = require('./debug/RNDebugSDK');
224
+ if (reduxMiddleware) middleware.push(reduxMiddleware);
225
+ } catch {}
226
+ }
227
+ const store = createStore(rootReducer, applyMiddleware(...middleware));
228
+ ```
229
+
230
+ **Legacy Redux (createStore without middleware):**
207
231
  ```js
208
232
  import { reduxEnhancer } from '../debug/RNDebugSDK';
209
233
  const store = createStore(reducer, __DEV__ ? reduxEnhancer : undefined);
210
234
  ```
211
235
 
212
- > **Note:** The import path is relative from your store file to `src/debug/RNDebugSDK`. Run `npx reactoradar setup` to auto-detect the correct path.
236
+ > **Note:** `npx reactoradar setup` auto-detects your store file and patches it. If it can't, follow the examples above. The import path is relative from your store file to `src/debug/RNDebugSDK`.
213
237
 
214
238
  ## Settings
215
239
 
@@ -293,7 +317,8 @@ const store = createStore(reducer, __DEV__ ? reduxEnhancer : undefined);
293
317
  | Network tab empty | Run Metro with `--reset-cache` |
294
318
  | Blank screen after long use | Click "Clear All Data" on the memory warning banner, or restart the app |
295
319
  | Redux shows "No actions dispatched" | Verify `reduxMiddleware` is wired in your store. Run `npx reactoradar setup` to auto-detect. |
296
- | Real device not connecting | Ensure HOST in `src/debug/RNDebugSDK.js` matches your Mac's LAN IP. Re-run `npx reactoradar setup`. |
320
+ | Android emulator not connecting | Run `adb reverse tcp:9090 tcp:9090 && adb reverse tcp:9091 tcp:9091 && adb reverse tcp:9092 tcp:9092`. Re-run after emulator restart. |
321
+ | Real device not connecting | Set `HOST_OVERRIDE` in `src/debug/RNDebugSDK.js` to your Mac's LAN IP. Re-run `npx reactoradar setup`. |
297
322
  | `XHRInterceptor.js` warning | Set `networking: false` in ReactotronConfig.js |
298
323
  | GA4 events not showing | Restart Metro with `--reset-cache` after setup |
299
324
  | Port conflict | Run `kill $(lsof -ti :9092)` to free the port, then restart |
package/app.js CHANGED
@@ -46,6 +46,14 @@ const esc = s => s == null ? '' : String(s)
46
46
  .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
47
47
  const ts = ms => new Date(ms).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
48
48
 
49
+ // Safe string conversion — never returns [object Object]
50
+ function safeStr(val) {
51
+ if (val == null) return '';
52
+ if (typeof val === 'string') return val;
53
+ if (typeof val === 'number' || typeof val === 'boolean') return String(val);
54
+ try { return JSON.stringify(val); } catch { return '[Complex object]'; }
55
+ }
56
+
49
57
 
50
58
  function pretty(val) {
51
59
  if (val == null) return '';
@@ -64,10 +72,12 @@ function syntaxHighlight(json) {
64
72
  }
65
73
 
66
74
  function renderJSON(val) {
75
+ if (val == null) return '<span style="color:var(--text-dim)">Empty response</span>';
67
76
  try {
68
77
  const str = typeof val === 'string' ? val : JSON.stringify(val, null, 2);
78
+ if (!str || str === '{}' || str === '""') return '<span style="color:var(--text-dim)">Empty response body</span>';
69
79
  return syntaxHighlight(esc(str));
70
- } catch { return esc(typeof val === 'object' ? JSON.stringify(val) : String(val)); }
80
+ } catch { try { return esc(JSON.stringify(val)); } catch { return esc('[Unserializable data]'); } }
71
81
  }
72
82
 
73
83
  function tryURL(url) { try { return new URL(url); } catch { return null; } }
package/bin/setup.js CHANGED
@@ -383,30 +383,9 @@ ${SDK_MARKER_END}
383
383
  } else {
384
384
  log('Redux store already has RNDebugSDK wired correctly — skipping');
385
385
  }
386
- } else if (storeContent.includes('configureStore')) {
387
- // RTK configureStore
388
- // Try to add middleware to configureStore
389
- if (storeContent.includes('middleware:') || storeContent.includes('middleware :')) {
390
- warn('Redux store found at', C.bold + storeFile + C.reset, '— has custom middleware');
391
- console.log(C.dim + ' Add manually to your middleware:' + C.reset);
392
- console.log(C.dim + ` import { reduxMiddleware } from '${relSDK}';` + C.reset);
393
- console.log(C.dim + ' middleware: (getDefault) => __DEV__' + C.reset);
394
- console.log(C.dim + ' ? getDefault().concat(reduxMiddleware)' + C.reset);
395
- console.log(C.dim + ' : getDefault(),' + C.reset);
396
- } else {
397
- // Add middleware field to configureStore
398
- const patched = storeContent.replace(
399
- /(configureStore\s*\(\s*\{)/,
400
- `$1\n middleware: (getDefaultMiddleware) =>\n __DEV__\n ? getDefaultMiddleware().concat(require('${relSDK}').reduxMiddleware)\n : getDefaultMiddleware(),`
401
- );
402
- if (patched !== storeContent) {
403
- fs.writeFileSync(storePath, patched);
404
- log('Patched', C.bold + storeFile + C.reset, '— Redux middleware wired');
405
- } else {
406
- warn('Could not auto-patch', storeFile, '— wire Redux manually');
407
- }
408
- }
409
- } else if (storeContent.includes('createStore')) {
386
+ } else if (/createStore\s*\(/.test(storeContent)) {
387
+ // Legacy createStore (check this BEFORE configureStore — a file may have both
388
+ // if the user named their wrapper function "configureStore" but uses Redux's createStore inside)
410
389
  // Legacy createStore — try to auto-patch by adding reduxMiddleware to middleware array
411
390
  let patched = storeContent;
412
391
  let didPatch = false;
@@ -449,9 +428,30 @@ ${SDK_MARKER_END}
449
428
  console.log(C.dim + ' Could not auto-patch. Add manually:' + C.reset);
450
429
  console.log(C.dim + ` if (__DEV__) { try { const { reduxMiddleware } = require('${relSDK}'); middleware.push(reduxMiddleware); } catch {} }` + C.reset);
451
430
  console.log(C.dim + ' Add this BEFORE the createStore() call in your middleware setup.' + C.reset);
431
+ }
432
+ } else if (/configureStore\s*\(\s*\{/.test(storeContent)) {
433
+ // RTK configureStore({ ... }) — actual Redux Toolkit usage
434
+ if (storeContent.includes('middleware:') || storeContent.includes('middleware :')) {
435
+ warn('Redux store found at', C.bold + storeFile + C.reset, '— has custom middleware');
436
+ console.log(C.dim + ' Add manually to your middleware:' + C.reset);
437
+ console.log(C.dim + ` import { reduxMiddleware } from '${relSDK}';` + C.reset);
438
+ console.log(C.dim + ' middleware: (getDefault) => __DEV__' + C.reset);
439
+ console.log(C.dim + ' ? getDefault().concat(reduxMiddleware)' + C.reset);
440
+ console.log(C.dim + ' : getDefault(),' + C.reset);
441
+ } else {
442
+ const patched = storeContent.replace(
443
+ /(configureStore\s*\(\s*\{)/,
444
+ `$1\n middleware: (getDefaultMiddleware) =>\n __DEV__\n ? getDefaultMiddleware().concat(require('${relSDK}').reduxMiddleware)\n : getDefaultMiddleware(),`
445
+ );
446
+ if (patched !== storeContent) {
447
+ fs.writeFileSync(storePath, patched);
448
+ log('Patched', C.bold + storeFile + C.reset, '— Redux middleware wired (RTK)');
449
+ } else {
450
+ warn('Could not auto-patch', storeFile, '— wire Redux manually');
451
+ }
452
452
  }
453
- }
454
- } else {
453
+ }
454
+ } else {
455
455
  warn('Redux detected but store file not found automatically');
456
456
  console.log(C.dim + ' Add to your store setup:' + C.reset);
457
457
  console.log(C.dim + ' import { reduxMiddleware } from \'./src/debug/RNDebugSDK\';' + C.reset);
package/init.js CHANGED
@@ -47,21 +47,32 @@ if (window.electronAPI) {
47
47
 
48
48
  window.electronAPI.on('clear-all-ui', clearAll);
49
49
 
50
- // When all device bridges disconnect, release heavy memory but keep logs visible.
51
- // Debounced to avoid data loss during hot reloads or flaky connections.
50
+ // Device disconnect debounced freeMemory. Reconnect clearAll (fresh session).
52
51
  let _disconnectTimer = null;
52
+ let _wasDisconnected = false;
53
+
53
54
  window.electronAPI.on('device-all-disconnected', () => {
55
+ _wasDisconnected = true;
54
56
  clearTimeout(_disconnectTimer);
55
57
  _disconnectTimer = setTimeout(() => {
56
58
  console.log('[App] All devices disconnected — freeing memory');
57
59
  freeMemory();
58
60
  }, 3000);
59
61
  });
60
- // Cancel pending free if a device reconnects
61
- const _cancelDisconnectTimer = () => { clearTimeout(_disconnectTimer); _disconnectTimer = null; };
62
- window.electronAPI.on('redux-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('redux', on); });
63
- window.electronAPI.on('network-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('network', on); });
64
- window.electronAPI.on('storage-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('storage', on); });
62
+
63
+ const _handleReconnect = () => {
64
+ clearTimeout(_disconnectTimer);
65
+ _disconnectTimer = null;
66
+ if (_wasDisconnected) {
67
+ _wasDisconnected = false;
68
+ console.log('[App] Device reconnected — clearing old session data');
69
+ clearAll();
70
+ }
71
+ };
72
+
73
+ window.electronAPI.on('redux-connected', on => { if (on) _handleReconnect(); updateDeviceBanner('redux', on); });
74
+ window.electronAPI.on('network-connected', on => { if (on) _handleReconnect(); updateDeviceBanner('network', on); });
75
+ window.electronAPI.on('storage-connected', on => { if (on) _handleReconnect(); updateDeviceBanner('storage', on); });
65
76
  window.electronAPI.on('react-dt-status', on => { updateDeviceBanner('reactDT', on); });
66
77
 
67
78
  // Cmd+F — focus the search input for the active panel
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.6.10",
4
+ "version": "1.6.11",
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
@@ -401,7 +401,8 @@ function primitivePreview(val) {
401
401
  if (typeof val === 'number' || typeof val === 'boolean') return String(val);
402
402
  if (Array.isArray(val)) return `Array(${val.length})`;
403
403
  if (typeof val === 'object') return `{...}`;
404
- return String(val);
404
+ if (typeof val === 'function') return `[Function: ${val.name || 'anonymous'}]`;
405
+ return safeStr(val);
405
406
  }
406
407
 
407
408
  function createTreeNode(key, val, startCollapsed) {
@@ -505,7 +506,7 @@ function _safeStr(val) {
505
506
  if (val === undefined) return 'undefined';
506
507
  if (typeof val === 'string') return val;
507
508
  if (typeof val === 'number' || typeof val === 'boolean') return String(val);
508
- try { return JSON.stringify(val, null, 2); } catch { return String(val); }
509
+ try { return JSON.stringify(val, null, 2); } catch { return '[Complex object]'; }
509
510
  }
510
511
 
511
512
  function createPrimitiveSpan(val) {
@@ -561,7 +562,7 @@ function buildLogBody(logEntry) {
561
562
  });
562
563
  } else if (logEntry.message != null) {
563
564
  // Legacy / flat message — try to parse JSON objects out of it
564
- const msg = String(logEntry.message);
565
+ const msg = safeStr(logEntry.message);
565
566
  // Try parsing the whole message as JSON
566
567
  try {
567
568
  const parsed = JSON.parse(msg);
@@ -691,7 +692,7 @@ function buildLogRow(l) {
691
692
  items.push({ label: 'Copy as JSON', action: () => {
692
693
  const json = l.args.map(a => {
693
694
  if (a.t === 'object' || a.t === 'array') return JSON.stringify(a.v, null, 2);
694
- return String(a.v);
695
+ return safeStr(a.v);
695
696
  }).join(' ');
696
697
  navigator.clipboard.writeText(json);
697
698
  }});
package/panels/network.js CHANGED
@@ -203,13 +203,13 @@ function initNetworkPanel() {
203
203
  method: r.method || 'GET',
204
204
  url: r.url || '',
205
205
  headers: Object.entries(r.requestHeaders || {}).map(([n, v]) => ({ name: n, value: v })),
206
- postData: r.requestBody ? { mimeType: 'application/json', text: typeof r.requestBody === 'object' ? JSON.stringify(r.requestBody) : String(r.requestBody) } : undefined,
206
+ postData: r.requestBody ? { mimeType: 'application/json', text: typeof r.requestBody === 'object' ? JSON.stringify(r.requestBody) : safeStr(r.requestBody) } : undefined,
207
207
  },
208
208
  response: {
209
209
  status: r.status || 0,
210
210
  statusText: r.statusText || '',
211
211
  headers: Object.entries(r.responseHeaders || {}).map(([n, v]) => ({ name: n, value: v })),
212
- content: { size: -1, mimeType: 'application/json', text: r.responseBody ? (typeof r.responseBody === 'object' ? JSON.stringify(r.responseBody) : String(r.responseBody)) : '' },
212
+ content: { size: -1, mimeType: 'application/json', text: r.responseBody ? (typeof r.responseBody === 'object' ? JSON.stringify(r.responseBody) : safeStr(r.responseBody)) : '' },
213
213
  },
214
214
  timings: { send: 0, wait: r.duration || 0, receive: 0 },
215
215
  };
@@ -835,7 +835,7 @@ function renderNetDetailContent(r) {
835
835
  if (!keys.length) return `<div class="section-label">${title}</div><span style="color:var(--text-dim)">none</span>`;
836
836
  return `<div class="section-label">${title}</div><div class="kv-grid">${keys.map(k => {
837
837
  let val = h[k];
838
- if (val && typeof val === 'object') { try { val = JSON.stringify(val); } catch { val = String(val); } }
838
+ if (val && typeof val === 'object') { try { val = JSON.stringify(val); } catch { val = '[Complex object]'; } }
839
839
  return `<span class="kv-key">${esc(k)}</span><span class="kv-val">${esc(val)}</span>`;
840
840
  }).join('')}</div>`;
841
841
  };
@@ -870,6 +870,7 @@ function renderNetDetailContent(r) {
870
870
  const isErrStatus = _isHttpError(r);
871
871
  if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
872
872
  if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
873
+ if (r.responseBody == null || r.responseBody === '') { body.innerHTML = '<span style="color:var(--text-dim)">Empty response body</span>'; return; }
873
874
  // Render as collapsible JSON tree with right-click copy
874
875
  const val = r.responseBody;
875
876
  let treeData = val;
@@ -896,13 +897,14 @@ function renderNetDetailContent(r) {
896
897
  });
897
898
  } else {
898
899
  body.innerHTML = isErrStatus
899
- ? `<span style="color:var(--red)">${esc(String(r.responseBody))}</span>`
900
+ ? `<span style="color:var(--red)">${esc(safeStr(r.responseBody))}</span>`
900
901
  : '<span style="color:var(--text-dim)">No preview available</span>';
901
902
  }
902
903
  } else if (tab === 'response') {
903
904
  const isErrStatus = _isHttpError(r);
904
905
  if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
905
906
  if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
907
+ if (r.responseBody == null || r.responseBody === '') { body.innerHTML = '<span style="color:var(--text-dim)">Empty response body</span>'; return; }
906
908
  if (isErrStatus) {
907
909
  const errBanner = document.createElement('div');
908
910
  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';