reactoradar 1.5.0 → 1.5.3

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
@@ -205,15 +205,19 @@ if (window.electronAPI) {
205
205
  window.electronAPI.on('ports', ports => { state.ports = ports; });
206
206
 
207
207
  window.electronAPI.on('cdp-targets', targets => {
208
- const hasCDP = targets?.length > 0;
209
- $('btnCDP').textContent = hasCDP
210
- ? `JS Debugger (${targets.length}) ↗`
211
- : 'JS Debugger ↗';
212
- $('btnCDP').style.opacity = hasCDP ? '1' : '0.5';
213
- if (hasCDP) {
214
- $('btnCDP').onclick = () => window.electronAPI.openCDPTarget(targets[0].webSocketDebuggerUrl);
208
+ state.cdpTargets = targets;
209
+ const btn = $('btnCDP');
210
+ if (btn) {
211
+ const hasCDP = targets?.length > 0;
212
+ const port = getStoredMetroPort();
213
+ btn.textContent = hasCDP
214
+ ? `JS Debugger (:${port}) [${targets.length}] ↗`
215
+ : `JS Debugger (:${port}) ↗`;
216
+ btn.style.opacity = hasCDP ? '1' : '0.5';
217
+ if (hasCDP) {
218
+ btn.onclick = () => window.electronAPI.openCDPTarget(targets[0].webSocketDebuggerUrl);
219
+ }
215
220
  }
216
-
217
221
  });
218
222
 
219
223
  window.electronAPI.on('redux-event', handleReduxEvent);
@@ -257,8 +261,8 @@ if (window.electronAPI) {
257
261
 
258
262
  window.electronAPI.on('app-version', (version) => {
259
263
  state._appVersion = version;
260
- const el = $('aboutVersion');
261
- if (el) el.textContent = 'v' + version;
264
+ // Update anywhere the version is displayed
265
+ document.querySelectorAll('#aboutVersion').forEach(el => el.textContent = 'v' + version);
262
266
  });
263
267
 
264
268
  window.electronAPI.on('update-available', ({ current, latest }) => {
@@ -2149,9 +2153,9 @@ function initReactPanel() {
2149
2153
  <div class="react-connect-hint" id="reactHint">
2150
2154
  <div class="icon" style="font-size:40px;opacity:.2">⚛️</div>
2151
2155
  <div class="label">React DevTools</div>
2152
- <div class="hint">Launches as a separate window connected to your app</div>
2153
- <div class="hint">React Native auto-connects on port <code>8097</code> in dev mode</div>
2154
- <button class="btn-launch" id="btnReactDT">Open React DevTools ↗</button>
2156
+ <div class="hint">Opens as a separate window connected to your app via port 8097</div>
2157
+ <div class="hint" style="margin-top:8px;color:var(--yellow)">Note: The RN inspector overlay won't work while React DevTools is connected. Close the DevTools window to use the built-in inspector.</div>
2158
+ <button class="btn-launch" id="btnReactDT" style="margin-top:12px">Open React DevTools ↗</button>
2155
2159
  </div>
2156
2160
  </div>`;
2157
2161
 
@@ -2182,6 +2186,12 @@ function getStoredAppName() {
2182
2186
  function setStoredAppName(n) {
2183
2187
  try { localStorage.setItem('rn-debug-appname', n); } catch {}
2184
2188
  }
2189
+ function getStoredMetroPort() {
2190
+ try { return parseInt(localStorage.getItem('rn-debug-metro-port')) || 8081; } catch { return 8081; }
2191
+ }
2192
+ function setStoredMetroPort(p) {
2193
+ try { localStorage.setItem('rn-debug-metro-port', String(p)); } catch {}
2194
+ }
2185
2195
  function applyAppName(name) {
2186
2196
  const logo = document.querySelector('.logo');
2187
2197
  if (logo) {
@@ -2280,10 +2290,11 @@ function initSettingsPanel() {
2280
2290
  </div>
2281
2291
  </div>
2282
2292
  <div class="settings-row">
2283
- <div>
2284
- <div class="settings-label">Metro Bundler</div>
2285
- <div class="settings-hint">CDP target discovery on :8081</div>
2293
+ <div style="display:flex;flex-direction:column;gap:2px">
2294
+ <div class="settings-label">Metro Bundler Port</div>
2295
+ <div class="settings-hint">Port for CDP target discovery (default: 8081)</div>
2286
2296
  </div>
2297
+ <input id="metroPortInput" type="number" class="net-search-input" style="width:70px;text-align:center" value="${getStoredMetroPort()}" />
2287
2298
  </div>
2288
2299
  </div>
2289
2300
  <div class="settings-section">
@@ -2334,6 +2345,7 @@ function initSettingsPanel() {
2334
2345
  <div class="about-links" style="display:flex;gap:16px;justify-content:center">
2335
2346
  <span class="about-link" id="linkGithub">GitHub</span>
2336
2347
  <span class="about-link" id="linkDocs">Documentation</span>
2348
+ <span class="about-link" id="linkLinkedIn">Developer LinkedIn</span>
2337
2349
  </div>
2338
2350
  </div>
2339
2351
  </div>
@@ -2383,6 +2395,9 @@ function initSettingsPanel() {
2383
2395
  $('linkDocs')?.addEventListener('click', () => {
2384
2396
  window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger#readme');
2385
2397
  });
2398
+ $('linkLinkedIn')?.addEventListener('click', () => {
2399
+ window.electronAPI?.openExternal('https://www.linkedin.com/in/sharanagoudamk/');
2400
+ });
2386
2401
 
2387
2402
  // App name
2388
2403
  $('appNameInput').addEventListener('change', (e) => {
@@ -2396,6 +2411,15 @@ function initSettingsPanel() {
2396
2411
  applyAppName('ReactoRadar');
2397
2412
  });
2398
2413
 
2414
+ // Metro Port
2415
+ $('metroPortInput')?.addEventListener('change', (e) => {
2416
+ let port = parseInt(e.target.value.trim());
2417
+ if (isNaN(port) || port < 1024 || port > 65535) port = 8081;
2418
+ e.target.value = port;
2419
+ setStoredMetroPort(port);
2420
+ window.electronAPI?.setMetroPort(port);
2421
+ });
2422
+
2399
2423
  // Font size controls
2400
2424
  $('fontSizeDown').addEventListener('click', () => {
2401
2425
  let size = getStoredFontSize();
@@ -2416,6 +2440,9 @@ applyTheme(getStoredTheme());
2416
2440
  applyFontSize(getStoredFontSize());
2417
2441
  applyAppName(getStoredAppName());
2418
2442
 
2443
+ // Send stored metro port to backend
2444
+ window.electronAPI?.setMetroPort(getStoredMetroPort());
2445
+
2419
2446
  // ─────────────────────────────────────────────────────────────────────────────
2420
2447
  // SOURCES PANEL — CDP-based file browser + breakpoints
2421
2448
  // ─────────────────────────────────────────────────────────────────────────────
@@ -2667,7 +2694,8 @@ async function loadSourceFile(filepath) {
2667
2694
  // Strategy 2: Fetch from Metro
2668
2695
  if (!source) {
2669
2696
  try {
2670
- const resp = await fetch(`http://localhost:8081/${filepath}?platform=ios&dev=true`);
2697
+ const port = getStoredMetroPort();
2698
+ const resp = await fetch(`http://localhost:${port}/${filepath}?platform=ios&dev=true`);
2671
2699
  if (resp.ok) source = await resp.text();
2672
2700
  } catch {}
2673
2701
  }
package/main.js CHANGED
@@ -38,16 +38,25 @@ app.whenReady().then(async () => {
38
38
 
39
39
  await createMainWindow();
40
40
 
41
- // Send version to renderer (delay to ensure IPC listeners are registered)
42
- const appVersion = require('./package.json').version;
41
+ // Send version to renderer try package.json, fallback to app.getVersion()
42
+ let appVersion;
43
+ try { appVersion = require('./package.json').version; } catch { appVersion = app.getVersion(); }
44
+ // Send multiple times to ensure renderer catches it
43
45
  mainWindow?.webContents.on('did-finish-load', () => {
44
- setTimeout(() => mainWindow?.webContents.send('app-version', appVersion), 500);
46
+ [200, 1000, 3000].forEach(delay => {
47
+ setTimeout(() => {
48
+ if (mainWindow && !mainWindow.isDestroyed()) {
49
+ mainWindow.webContents.send('app-version', appVersion);
50
+ }
51
+ }, delay);
52
+ });
45
53
  });
46
54
 
47
55
  // Check for updates (non-blocking)
48
56
  checkForUpdates();
49
57
  startBridgeServers();
50
- startReactDevToolsServer();
58
+ // React DevTools relay NOT started by default — it blocks RN's built-in inspector.
59
+ // Started on-demand when user clicks React tab or Cmd+R.
51
60
  setupMetroCDPProxy();
52
61
  setupIPC();
53
62
  buildMenu();
@@ -309,6 +318,8 @@ function setupIPC() {
309
318
  });
310
319
 
311
320
  ipcMain.on('open-react-devtools', () => {
321
+ // Start the relay server if not already running
322
+ if (!reactDTServer) startReactDevToolsServer();
312
323
  // Open standalone react-devtools window
313
324
  const rdtWin = new BrowserWindow({
314
325
  width: 1100,
@@ -324,6 +335,12 @@ function setupIPC() {
324
335
 
325
336
  // clear-all is handled by renderer via clear-all-ui IPC from menu
326
337
 
338
+ ipcMain.on('set-metro-port', (_, port) => {
339
+ PORTS.METRO = port;
340
+ fetchCDPTargets();
341
+ mainWindow?.webContents.send('ports', PORTS);
342
+ });
343
+
327
344
  ipcMain.on('set-network-capture', (_, enabled) => {
328
345
  // Broadcast to connected RN apps so they can stop/start intercepting
329
346
  networkClients.forEach(ws => {
@@ -383,7 +400,7 @@ function setupIPC() {
383
400
  // Also try to detect from Metro's /json endpoint
384
401
  try {
385
402
  const result = require('child_process').execSync(
386
- "lsof -i :8081 -t 2>/dev/null | head -1 | xargs -I{} lsof -p {} -Fn 2>/dev/null | grep '^n/' | grep 'node_modules' | head -1 | sed 's|^n||;s|/node_modules.*||'",
403
+ `lsof -i :${PORTS.METRO} -t 2>/dev/null | head -1 | xargs -I{} lsof -p {} -Fn 2>/dev/null | grep '^n/' | grep 'node_modules' | head -1 | sed 's|^n||;s|/node_modules.*||'`,
387
404
  { encoding: 'utf8', timeout: 3000 }
388
405
  ).trim();
389
406
  if (result && fs.existsSync(result)) candidates.unshift(result);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.5.0",
4
+ "version": "1.5.3",
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
@@ -26,6 +26,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
26
26
  setNetworkCapture: (enabled) => ipcRenderer.send('set-network-capture', enabled),
27
27
  setStackTraceCapture: (enabled) => ipcRenderer.send('set-stack-trace-capture', enabled),
28
28
  setNetworkThrottle: (profile) => ipcRenderer.send('set-network-throttle', profile),
29
+ setMetroPort: (port) => ipcRenderer.send('set-metro-port', port),
29
30
  readSourceFile: (filepath) => ipcRenderer.invoke('read-source-file', filepath),
30
31
  openExternal: (url) => ipcRenderer.send('open-external', url),
31
32
  });
package/sdk/RNDebugSDK.js CHANGED
@@ -34,6 +34,49 @@ let _stackTraceEnabled = false; // Disabled by default for performance
34
34
  let _throttleProfile = 'none'; // 'none', 'fast3g', 'slow3g', 'offline'
35
35
  const THROTTLE_DELAYS = { none: 0, fast3g: 500, slow3g: 2000, offline: -1 };
36
36
 
37
+ // ─── SDK Pause/Resume (allows inspector to work without SDK interference) ────
38
+ // When paused, console/fetch/XHR interception is disabled so the RN inspector
39
+ // and CDP debugger can work without conflicts. Controlled via the debugger app.
40
+ let _sdkPaused = false;
41
+
42
+ function _isSDKActive() {
43
+ return !_sdkPaused;
44
+ }
45
+
46
+ // ─── Debugger Detection ──────────────────────────────────────────────────────
47
+ // Detect if a CDP debugger (Chrome DevTools / Hermes inspector) is attached.
48
+ // When detected, we back off our patches to avoid conflicts with the inspector.
49
+ let _debuggerDetected = false;
50
+ let _debuggerCheckInterval = null;
51
+
52
+ function _checkDebuggerAttached() {
53
+ // Method 1: Check if Hermes debugger globals are set
54
+ const hermesDebugger = !!(global.__DEBUGGER_CONNECTED__ || global.__HERMES_DEBUGGER_CONNECTED__);
55
+ // Method 2: Check React DevTools hook for debugger attachment
56
+ const rdtHook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
57
+ const rdtDebugger = !!(rdtHook && rdtHook._debuggerAttached);
58
+ // Method 3: Check if CDP is connected via the inspector agent
59
+ const inspectorConnected = !!(global.__inspectorGlobalObject || global.__inspector);
60
+
61
+ const wasDetected = _debuggerDetected;
62
+ _debuggerDetected = hermesDebugger || rdtDebugger || inspectorConnected;
63
+
64
+ if (_debuggerDetected && !wasDetected) {
65
+ _console.log('[RNDebugSDK] Debugger detected — SDK interception paused to avoid inspector conflicts. Use the ReactoRadar app to resume.');
66
+ } else if (!_debuggerDetected && wasDetected) {
67
+ _console.log('[RNDebugSDK] Debugger disconnected — SDK interception resumed.');
68
+ }
69
+ }
70
+
71
+ // Check periodically (every 3s) — lightweight, no performance impact
72
+ _debuggerCheckInterval = setInterval(_checkDebuggerAttached, 3000);
73
+ // Also check once immediately after a short delay (debugger may attach during startup)
74
+ setTimeout(_checkDebuggerAttached, 1000);
75
+
76
+ function _shouldIntercept() {
77
+ return _isSDKActive() && !_debuggerDetected;
78
+ }
79
+
37
80
  // ─── WebSocket Factory ────────────────────────────────────────────────────────
38
81
  function makeChannel(port, name, onMessage) {
39
82
  let ws = null, queue = [], connected = false;
@@ -72,6 +115,21 @@ const mainCh = makeChannel(PORTS.NETWORK_AND_CONSOLE, 'main', (msg) => {
72
115
  if (msg.action === 'set-network-capture') _networkCaptureEnabled = !!msg.enabled;
73
116
  if (msg.action === 'set-throttle') _throttleProfile = msg.profile || 'none';
74
117
  if (msg.action === 'set-stack-trace') _stackTraceEnabled = !!msg.enabled;
118
+ // Pause/Resume SDK interception (allows inspector to work)
119
+ if (msg.action === 'pause-sdk') {
120
+ _sdkPaused = true;
121
+ _console.log('[RNDebugSDK] SDK paused — inspector/debugger can now inspect the app freely.');
122
+ mainCh.send({ type: 'control', action: 'sdk-status', paused: true });
123
+ }
124
+ if (msg.action === 'resume-sdk') {
125
+ _sdkPaused = false;
126
+ _console.log('[RNDebugSDK] SDK resumed — interception re-enabled.');
127
+ mainCh.send({ type: 'control', action: 'sdk-status', paused: false });
128
+ }
129
+ // Query current status
130
+ if (msg.action === 'query-sdk-status') {
131
+ mainCh.send({ type: 'control', action: 'sdk-status', paused: _sdkPaused, debuggerDetected: _debuggerDetected });
132
+ }
75
133
  }
76
134
  });
77
135
  const reduxCh = makeChannel(PORTS.REDUX, 'redux');
@@ -130,6 +188,9 @@ LEVELS.forEach(level => {
130
188
  _console[level] = console[level].bind(console);
131
189
  console[level] = (...args) => {
132
190
  _console[level](...args);
191
+ // Skip interception when SDK is paused or debugger is attached
192
+ // This prevents double-logging and message queue deadlocks with CDP
193
+ if (!_shouldIntercept()) return;
133
194
  const structuredArgs = args.map(serializeArg);
134
195
  const message = args.map(a => {
135
196
  if (typeof a === 'string') return a;
@@ -167,6 +228,10 @@ function _flattenHeaders(h) {
167
228
  // ─── Fetch Intercept ─────────────────────────────────────────────────────────
168
229
  const _fetch = global.fetch;
169
230
  global.fetch = async (input, init = {}) => {
231
+ // When SDK is paused or debugger is attached, pass through without interception
232
+ // This prevents racing with CDP's own Fetch.enable domain
233
+ if (!_shouldIntercept()) return _fetch(input, init);
234
+
170
235
  // Throttle: simulate slow network or offline
171
236
  const delay = THROTTLE_DELAYS[_throttleProfile] || 0;
172
237
  if (delay === -1) return Promise.reject(new TypeError('Network request failed (offline throttle)'));
@@ -236,24 +301,24 @@ global.fetch = async (input, init = {}) => {
236
301
  return _setHeader.apply(xhr, arguments);
237
302
  };
238
303
 
239
- // Wrap send
240
- const _send = xhr.send.bind(xhr);
241
- xhr.send = function(body) {
242
- if (_networkCaptureEnabled && !meta.sent) {
243
- meta.sent = true;
244
- let reqBody = null;
245
- if (body != null) {
246
- try { reqBody = typeof body === 'string' ? body : JSON.parse(JSON.stringify(body)); } catch { reqBody = String(body); }
247
- }
248
- mainCh.send({ type: 'network', phase: 'request', id: meta.id, url: meta.url,
249
- method: meta.method, requestHeaders: meta.headers, requestBody: reqBody });
250
- }
251
- return _send.apply(xhr, arguments);
252
- };
304
+ // Wrap send
305
+ const _send = xhr.send.bind(xhr);
306
+ xhr.send = function(body) {
307
+ if (_shouldIntercept() && _networkCaptureEnabled && !meta.sent) {
308
+ meta.sent = true;
309
+ let reqBody = null;
310
+ if (body != null) {
311
+ try { reqBody = typeof body === 'string' ? body : JSON.parse(JSON.stringify(body)); } catch { reqBody = String(body); }
312
+ }
313
+ mainCh.send({ type: 'network', phase: 'request', id: meta.id, url: meta.url,
314
+ method: meta.method, requestHeaders: meta.headers, requestBody: reqBody });
315
+ }
316
+ return _send.apply(xhr, arguments);
317
+ };
253
318
 
254
319
  // Listen for completion
255
- xhr.addEventListener('readystatechange', function() {
256
- if (xhr.readyState !== 4 || !meta.sent || !_networkCaptureEnabled) return;
320
+ xhr.addEventListener('readystatechange', function() {
321
+ if (xhr.readyState !== 4 || !meta.sent || !_shouldIntercept() || !_networkCaptureEnabled) return;
257
322
  try {
258
323
  const duration = Date.now() - meta.t0;
259
324
  if (xhr.status > 0) {
@@ -310,17 +375,8 @@ global.fetch = async (input, init = {}) => {
310
375
  _console.log('[RNDebugSDK] XHR constructor wrapped for network capture');
311
376
  }
312
377
 
313
- // Wrap immediately if available
314
- if (global.XMLHttpRequest) wrapXHR();
315
-
316
- // Also wrap after RN polyfills set up (they replace global.XMLHttpRequest)
317
- [0, 50, 200, 500].forEach(delay => {
318
- setTimeout(() => {
319
- if (global.XMLHttpRequest && !global.XMLHttpRequest.__dbgWrapped) {
320
- wrapXHR();
321
- }
322
- }, delay);
323
- });
378
+ // Wrap immediately if available
379
+ if (global.XMLHttpRequest) wrapXHR();
324
380
  })();
325
381
 
326
382
  // ─── Axios Interceptor (belt-and-suspenders with XHR patch) ──────────────────
@@ -334,8 +390,8 @@ setTimeout(() => {
334
390
  function addDbgInterceptors(instance) {
335
391
  if (!instance || !instance.interceptors || instance.__dbgInt) return;
336
392
  instance.__dbgInt = true;
337
- instance.interceptors.request.use(config => {
338
- if (!_networkCaptureEnabled) return config;
393
+ instance.interceptors.request.use(config => {
394
+ if (!_shouldIntercept() || !_networkCaptureEnabled) return config;
339
395
  const id = `ax-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
340
396
  config._dbgId = id;
341
397
  config._dbgT0 = Date.now();
@@ -348,9 +404,10 @@ setTimeout(() => {
348
404
  mainCh.send({ type:'network', phase:'request', id, url, method:(config.method||'GET').toUpperCase(), requestHeaders:h, requestBody:body });
349
405
  return config;
350
406
  }, e => Promise.reject(e));
351
- instance.interceptors.response.use(resp => {
352
- const c = resp.config || {};
353
- if (!c._dbgId) return resp;
407
+ instance.interceptors.response.use(resp => {
408
+ if (!_shouldIntercept() || !_networkCaptureEnabled) return resp;
409
+ const c = resp.config || {};
410
+ if (!c._dbgId) return resp;
354
411
  const url = c.baseURL ? c.baseURL.replace(/\/+$/,'') + '/' + (c.url||'').replace(/^\/+/,'') : (c.url||'');
355
412
  const dur = c._dbgT0 ? Date.now() - c._dbgT0 : 0;
356
413
  const rh = {};
@@ -361,9 +418,10 @@ setTimeout(() => {
361
418
  mainCh.send({ type:'network', phase:'response', id:c._dbgId, url, method:(c.method||'GET').toUpperCase(),
362
419
  status:resp.status, statusText:resp.statusText, duration:dur, responseHeaders:rh, responseBody:body });
363
420
  return resp;
364
- }, err => {
365
- const c = err?.config || {};
366
- if (c._dbgId) {
421
+ }, err => {
422
+ if (!_shouldIntercept() || !_networkCaptureEnabled) return Promise.reject(err);
423
+ const c = err?.config || {};
424
+ if (c._dbgId) {
367
425
  const url = c.baseURL ? c.baseURL.replace(/\/+$/,'') + '/' + (c.url||'').replace(/^\/+/,'') : (c.url||'');
368
426
  const dur = c._dbgT0 ? Date.now() - c._dbgT0 : 0;
369
427
  const r = err?.response;