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 +30 -5
- package/app.js +11 -1
- package/bin/setup.js +26 -26
- package/init.js +18 -7
- package/package.json +1 -1
- package/panels/console.js +5 -4
- package/panels/network.js +6 -4
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
|
-
|
|
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`.
|
|
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
|
-
|
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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(
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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) :
|
|
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) :
|
|
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 =
|
|
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(
|
|
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';
|