reactoradar 1.5.4 → 1.5.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.
Files changed (2) hide show
  1. package/main.js +87 -40
  2. package/package.json +1 -1
package/main.js CHANGED
@@ -19,6 +19,15 @@ const PORTS = {
19
19
  let mainWindow = null;
20
20
  let devtoolsWindow = null; // hosts the embedded CDP DevTools frontend
21
21
 
22
+ // Safe IPC send — prevents "Object has been destroyed" crash
23
+ function _send(channel, ...args) {
24
+ try {
25
+ if (mainWindow && !mainWindow.isDestroyed() && mainWindow.webContents && !mainWindow.webContents.isDestroyed()) {
26
+ mainWindow.webContents.send(channel, ...args);
27
+ }
28
+ } catch {}
29
+ }
30
+
22
31
  // ─── State ────────────────────────────────────────────────────────────────────
23
32
  let reduxClients = new Set();
24
33
  let storageClients = new Set();
@@ -27,8 +36,32 @@ let networkClients = new Set();
27
36
  // ─── Set dock icon ASAP (before app ready) ──────────────────────────────────
28
37
  const _appIcon = nativeImage.createFromPath(path.join(__dirname, 'ReactoRadar.png'));
29
38
 
39
+ // ─── Single Instance Lock ────────────────────────────────────────────────────
40
+ // Prevent multiple ReactoRadar instances from running simultaneously.
41
+ // If a second instance launches, focus the existing window instead.
42
+ const gotLock = app.requestSingleInstanceLock();
43
+ if (!gotLock) {
44
+ // Another instance is already running — show a dialog and quit
45
+ const { dialog } = require('electron');
46
+ app.whenReady().then(() => {
47
+ dialog.showErrorBox(
48
+ 'ReactoRadar is already running',
49
+ 'Another instance of ReactoRadar is already open.\n\nPlease close the existing instance first, or check your system tray / dock.\n\nIf the old version is stuck, run:\n kill $(lsof -ti :9092) \nin your terminal to stop it.'
50
+ );
51
+ app.quit();
52
+ });
53
+ } else {
54
+ app.on('second-instance', () => {
55
+ // Focus the existing window when someone tries to open a second instance
56
+ if (mainWindow) {
57
+ if (mainWindow.isMinimized()) mainWindow.restore();
58
+ mainWindow.focus();
59
+ }
60
+ });
61
+ }
62
+
30
63
  // ─── App lifecycle ────────────────────────────────────────────────────────────
31
- app.whenReady().then(async () => {
64
+ if (gotLock) app.whenReady().then(async () => {
32
65
  nativeTheme.themeSource = 'dark';
33
66
 
34
67
  // Set dock icon on macOS
@@ -42,11 +75,11 @@ app.whenReady().then(async () => {
42
75
  let appVersion;
43
76
  try { appVersion = require('./package.json').version; } catch { appVersion = app.getVersion(); }
44
77
  // Send multiple times to ensure renderer catches it
45
- mainWindow?.webContents.on('did-finish-load', () => {
78
+ mainWindow.webContents.on('did-finish-load', () => {
46
79
  [200, 1000, 3000].forEach(delay => {
47
80
  setTimeout(() => {
48
81
  if (mainWindow && !mainWindow.isDestroyed()) {
49
- mainWindow.webContents.send('app-version', appVersion);
82
+ _send('app-version', appVersion);
50
83
  }
51
84
  }, delay);
52
85
  });
@@ -102,7 +135,7 @@ async function createMainWindow() {
102
135
 
103
136
  // Open the JS Debugger panel (CDP DevTools) in a second window
104
137
  mainWindow.webContents.on('did-finish-load', () => {
105
- mainWindow.webContents.send('ports', PORTS);
138
+ _send('ports', PORTS);
106
139
  });
107
140
  }
108
141
 
@@ -133,7 +166,7 @@ function checkForUpdates() {
133
166
  [500, 2000, 5000].forEach(delay => {
134
167
  setTimeout(() => {
135
168
  if (mainWindow && !mainWindow.isDestroyed()) {
136
- mainWindow.webContents.send('update-available', payload);
169
+ _send('update-available', payload);
137
170
  }
138
171
  }, delay);
139
172
  });
@@ -203,7 +236,7 @@ function fetchCDPTargets(callback) {
203
236
  t.type === 'node' || t.devtoolsFrontendUrl
204
237
  );
205
238
  lastKnownTargets = rnTargets;
206
- mainWindow?.webContents.send('cdp-targets', rnTargets);
239
+ _send('cdp-targets', rnTargets);
207
240
  if (callback) callback(rnTargets);
208
241
  } catch (_) {
209
242
  if (callback) callback([]);
@@ -211,7 +244,7 @@ function fetchCDPTargets(callback) {
211
244
  });
212
245
  }).on('error', () => {
213
246
  lastKnownTargets = [];
214
- mainWindow?.webContents.send('cdp-targets', []);
247
+ _send('cdp-targets', []);
215
248
  if (callback) callback([]);
216
249
  });
217
250
  }
@@ -235,13 +268,13 @@ function startReactDevToolsServer() {
235
268
  reactDTServer.on('error', (err) => {
236
269
  console.warn(`[ReactDT] Server error: ${err.message}`);
237
270
  if (err.code === 'EADDRINUSE') {
238
- mainWindow?.webContents.send('react-dt-status', false);
271
+ _send('react-dt-status', false);
239
272
  }
240
273
  });
241
274
  reactDTServer.on('connection', (ws) => {
242
275
  reactDTClients.add(ws);
243
276
  console.log(`[ReactDT] Client connected (total: ${reactDTClients.size})`);
244
- mainWindow?.webContents.send('react-dt-status', true);
277
+ _send('react-dt-status', true);
245
278
 
246
279
  // Relay messages between all connected clients (frontend ↔ backend)
247
280
  ws.on('message', (data) => {
@@ -256,7 +289,7 @@ function startReactDevToolsServer() {
256
289
  reactDTClients.delete(ws);
257
290
  console.log(`[ReactDT] Client disconnected (total: ${reactDTClients.size})`);
258
291
  if (reactDTClients.size === 0) {
259
- mainWindow?.webContents.send('react-dt-status', false);
292
+ _send('react-dt-status', false);
260
293
  }
261
294
  });
262
295
  });
@@ -270,53 +303,67 @@ function startReactDevToolsServer() {
270
303
  function startBridgeServers() {
271
304
  // Redux Bridge
272
305
  startBridge(PORTS.REDUX_BRIDGE, 'redux', reduxClients, (event) => {
273
- mainWindow?.webContents.send('redux-event', event);
306
+ _send('redux-event', event);
274
307
  });
275
308
 
276
309
  // AsyncStorage Bridge
277
310
  startBridge(PORTS.STORAGE_BRIDGE, 'storage', storageClients, (event) => {
278
- mainWindow?.webContents.send('storage-event', event);
311
+ _send('storage-event', event);
279
312
  });
280
313
 
281
314
  // Network + Console + Perf Bridge (port 9092 carries all types from RNDebugSDK)
282
315
  startBridge(PORTS.NETWORK_BRIDGE, 'network', networkClients, (event) => {
283
316
  if (event.type === 'control') return;
284
317
  if (event.type === 'console') {
285
- mainWindow?.webContents.send('console-event', event);
318
+ _send('console-event', event);
286
319
  } else if (event.type === 'perf') {
287
- mainWindow?.webContents.send('perf-event', event);
320
+ _send('perf-event', event);
288
321
  } else if (event.type === 'ga4') {
289
- mainWindow?.webContents.send('ga4-event', event);
322
+ _send('ga4-event', event);
290
323
  } else {
291
- mainWindow?.webContents.send('network-event', event);
324
+ _send('network-event', event);
292
325
  }
293
326
  });
294
327
  }
295
328
 
296
329
  function startBridge(port, name, clients, onEvent) {
297
- const wss = new WebSocketServer({ port });
298
- wss.on('connection', (ws) => {
299
- clients.add(ws);
300
- console.log(`[${name}] RN app connected`);
301
- mainWindow?.webContents.send(`${name}-connected`, true);
302
-
303
- ws.on('message', (raw) => {
304
- try {
305
- const event = JSON.parse(raw.toString());
306
- onEvent(event);
307
- } catch (e) {
308
- console.warn(`[${name}] Failed to parse message:`, e.message);
330
+ try {
331
+ const wss = new WebSocketServer({ port });
332
+ wss.on('error', (err) => {
333
+ if (err.code === 'EADDRINUSE') {
334
+ console.error(`[${name}] Port ${port} is already in use — another ReactoRadar or debugger may be running.`);
335
+ const { dialog } = require('electron');
336
+ dialog.showErrorBox(
337
+ `Port ${port} is in use`,
338
+ `ReactoRadar cannot start the ${name} bridge because port ${port} is already occupied.\n\nThis usually means an older version of ReactoRadar is still running.\n\nTo fix this, run the following in your terminal:\n kill $(lsof -ti :${port})\n\nThen restart ReactoRadar.`
339
+ );
309
340
  }
310
341
  });
342
+ wss.on('connection', (ws) => {
343
+ clients.add(ws);
344
+ console.log(`[${name}] RN app connected`);
345
+ _send(`${name}-connected`, true);
311
346
 
312
- ws.on('close', () => {
313
- clients.delete(ws);
314
- if (clients.size === 0) {
315
- mainWindow?.webContents.send(`${name}-connected`, false);
316
- }
347
+ ws.on('message', (raw) => {
348
+ try {
349
+ const event = JSON.parse(raw.toString());
350
+ onEvent(event);
351
+ } catch (e) {
352
+ console.warn(`[${name}] Failed to parse message:`, e.message);
353
+ }
354
+ });
355
+
356
+ ws.on('close', () => {
357
+ clients.delete(ws);
358
+ if (clients.size === 0) {
359
+ _send(`${name}-connected`, false);
360
+ }
361
+ });
317
362
  });
318
- });
319
- console.log(`[${name}] Bridge on :${port}`);
363
+ console.log(`[${name}] Bridge on :${port}`);
364
+ } catch (e) {
365
+ console.error(`[${name}] Failed to start bridge on port ${port}:`, e.message);
366
+ }
320
367
  }
321
368
 
322
369
  // ─── IPC from Renderer ────────────────────────────────────────────────────────
@@ -359,7 +406,7 @@ function setupIPC() {
359
406
  if (isNaN(p) || p < 1024 || p > 65535) return;
360
407
  PORTS.METRO = p;
361
408
  fetchCDPTargets();
362
- mainWindow?.webContents.send('ports', PORTS);
409
+ _send('ports', PORTS);
363
410
  });
364
411
 
365
412
  ipcMain.on('set-network-capture', (_, enabled) => {
@@ -503,7 +550,7 @@ function buildMenu() {
503
550
  {
504
551
  label: 'Open JS Debugger (CDP)',
505
552
  accelerator: 'Cmd+D',
506
- click: () => { mainWindow?.webContents.send('trigger-open-cdp'); },
553
+ click: () => { _send('trigger-open-cdp'); },
507
554
  },
508
555
  {
509
556
  label: 'Open React DevTools',
@@ -514,7 +561,7 @@ function buildMenu() {
514
561
  {
515
562
  label: 'Clear All',
516
563
  accelerator: 'Cmd+K',
517
- click: () => { mainWindow?.webContents.send('clear-all-ui'); },
564
+ click: () => { _send('clear-all-ui'); },
518
565
  },
519
566
  { type: 'separator' },
520
567
  {
@@ -527,7 +574,7 @@ function buildMenu() {
527
574
  const next = themes[(idx + 1) % themes.length];
528
575
  nativeTheme.themeSource = next.includes('light') ? 'light' : 'dark';
529
576
  if (mainWindow && !mainWindow.isDestroyed()) {
530
- mainWindow.webContents.send('theme-changed', next);
577
+ _send('theme-changed', next);
531
578
  }
532
579
  },
533
580
  },
@@ -547,7 +594,7 @@ function buildMenu() {
547
594
  {
548
595
  label: 'Find',
549
596
  accelerator: 'Cmd+F',
550
- click: () => { mainWindow?.webContents.send('focus-search'); },
597
+ click: () => { _send('focus-search'); },
551
598
  },
552
599
  ],
553
600
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.5.4",
4
+ "version": "1.5.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": {