reactoradar 1.5.4 → 1.5.6

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/bin/setup.js CHANGED
@@ -166,10 +166,18 @@ async function install(projectDir) {
166
166
  const sdkDestDir = path.join(projectDir, 'src', 'debug');
167
167
  const sdkDest = path.join(sdkDestDir, 'RNDebugSDK.js');
168
168
 
169
- if (!dirExists(path.join(projectDir, 'src'))) {
170
- fs.mkdirSync(path.join(projectDir, 'src'), { recursive: true });
169
+ if (!fileExists(sdkSrc)) {
170
+ err('SDK source file not found at: ' + sdkSrc);
171
+ err('This may be a corrupted installation. Try: npm cache clean --force && npx reactoradar@latest setup');
172
+ process.exit(1);
173
+ }
174
+
175
+ try {
176
+ fs.mkdirSync(sdkDestDir, { recursive: true });
177
+ } catch (e) {
178
+ err('Failed to create directory ' + sdkDestDir + ': ' + e.message);
179
+ process.exit(1);
171
180
  }
172
- fs.mkdirSync(sdkDestDir, { recursive: true });
173
181
 
174
182
  // Read SDK, patch HOST
175
183
  let sdkContent = fs.readFileSync(sdkSrc, 'utf8');
@@ -177,7 +185,18 @@ async function install(projectDir) {
177
185
  /const HOST = '[^']+';/,
178
186
  `const HOST = '${host}';`
179
187
  );
180
- fs.writeFileSync(sdkDest, sdkContent);
188
+ try {
189
+ fs.writeFileSync(sdkDest, sdkContent);
190
+ } catch (e) {
191
+ err('Failed to write SDK file to ' + sdkDest + ': ' + e.message);
192
+ process.exit(1);
193
+ }
194
+
195
+ // Verify the file was actually written
196
+ if (!fileExists(sdkDest)) {
197
+ err('SDK file was not created at ' + sdkDest + ' — check directory permissions');
198
+ process.exit(1);
199
+ }
181
200
  log('Copied RNDebugSDK.js →', C.dim + 'src/debug/RNDebugSDK.js' + C.reset);
182
201
 
183
202
  // 4. Patch entry file
@@ -207,56 +226,60 @@ ${SDK_MARKER_END}
207
226
  }
208
227
  }
209
228
 
210
- // 5. Detect and wire Redux
211
- info('Checking for Redux...');
212
- const hasRedux = allDeps['@reduxjs/toolkit'] || allDeps['redux'];
213
- if (hasRedux) {
214
- const storeFile = findStoreFile(projectDir);
215
- if (storeFile) {
216
- const storePath = path.join(projectDir, storeFile);
217
- const storeContent = fs.readFileSync(storePath, 'utf8');
218
-
219
- if (storeContent.includes('RNDebugSDK')) {
220
- log('Redux store already has RNDebugSDK wired — skipping');
221
- } else if (storeContent.includes('configureStore')) {
222
- // RTK configureStore
223
- const patchedStore = `${SDK_MARKER_START}\nimport { reduxMiddleware } from '../debug/RNDebugSDK';\n${SDK_MARKER_END}\n` + storeContent;
224
-
225
- // Try to add middleware to configureStore
226
- if (storeContent.includes('middleware:') || storeContent.includes('middleware :')) {
227
- warn('Redux store found at', C.bold + storeFile + C.reset, '— has custom middleware');
228
- console.log(C.dim + ' Add manually to your middleware:' + C.reset);
229
- console.log(C.dim + ' import { reduxMiddleware } from \'./src/debug/RNDebugSDK\';' + C.reset);
230
- console.log(C.dim + ' middleware: (getDefault) => __DEV__' + C.reset);
231
- console.log(C.dim + ' ? getDefault().concat(reduxMiddleware)' + C.reset);
232
- console.log(C.dim + ' : getDefault(),' + C.reset);
233
- } else {
234
- // Add middleware field to configureStore
235
- const patched = storeContent.replace(
236
- /(configureStore\s*\(\s*\{)/,
237
- `$1\n middleware: (getDefaultMiddleware) =>\n __DEV__\n ? getDefaultMiddleware().concat(require('./src/debug/RNDebugSDK').reduxMiddleware)\n : getDefaultMiddleware(),`
238
- );
239
- if (patched !== storeContent) {
240
- fs.writeFileSync(storePath, patched);
241
- log('Patched', C.bold + storeFile + C.reset, '— Redux middleware wired');
242
- } else {
243
- warn('Could not auto-patch', storeFile, '— wire Redux manually');
244
- }
245
- }
246
- } else if (storeContent.includes('createStore')) {
247
- warn('Legacy createStore found at', C.bold + storeFile + C.reset);
248
- console.log(C.dim + ' Add manually:' + C.reset);
249
- console.log(C.dim + ' import { reduxEnhancer } from \'./src/debug/RNDebugSDK\';' + C.reset);
250
- console.log(C.dim + ' const store = createStore(reducer, __DEV__ ? reduxEnhancer : undefined);' + C.reset);
251
- }
252
- } else {
253
- warn('Redux detected but store file not found automatically');
254
- console.log(C.dim + ' Add to your store setup:' + C.reset);
255
- console.log(C.dim + ' import { reduxMiddleware } from \'./src/debug/RNDebugSDK\';' + C.reset);
256
- }
257
- } else {
258
- log('No Redux detected skipping');
259
- }
229
+ // 5. Detect and wire Redux
230
+ info('Checking for Redux...');
231
+ const hasRedux = allDeps['@reduxjs/toolkit'] || allDeps['redux'];
232
+ if (hasRedux) {
233
+ const storeFile = findStoreFile(projectDir);
234
+ if (storeFile) {
235
+ const storePath = path.join(projectDir, storeFile);
236
+ const storeContent = fs.readFileSync(storePath, 'utf8');
237
+
238
+ // Compute relative path from store file to SDK
239
+ const storeDir = path.dirname(path.join(projectDir, storeFile));
240
+ let relSDK = path.relative(storeDir, path.join(projectDir, 'src', 'debug', 'RNDebugSDK'))
241
+ .replace(/\\/g, '/'); // Windows compat
242
+ if (!relSDK.startsWith('.')) relSDK = './' + relSDK;
243
+
244
+ if (storeContent.includes('RNDebugSDK')) {
245
+ log('Redux store already has RNDebugSDK wired — skipping');
246
+ } else if (storeContent.includes('configureStore')) {
247
+ // RTK configureStore
248
+ // Try to add middleware to configureStore
249
+ if (storeContent.includes('middleware:') || storeContent.includes('middleware :')) {
250
+ warn('Redux store found at', C.bold + storeFile + C.reset, '— has custom middleware');
251
+ console.log(C.dim + ' Add manually to your middleware:' + C.reset);
252
+ console.log(C.dim + ` import { reduxMiddleware } from '${relSDK}';` + C.reset);
253
+ console.log(C.dim + ' middleware: (getDefault) => __DEV__' + C.reset);
254
+ console.log(C.dim + ' ? getDefault().concat(reduxMiddleware)' + C.reset);
255
+ console.log(C.dim + ' : getDefault(),' + C.reset);
256
+ } else {
257
+ // Add middleware field to configureStore
258
+ const patched = storeContent.replace(
259
+ /(configureStore\s*\(\s*\{)/,
260
+ `$1\n middleware: (getDefaultMiddleware) =>\n __DEV__\n ? getDefaultMiddleware().concat(require('${relSDK}').reduxMiddleware)\n : getDefaultMiddleware(),`
261
+ );
262
+ if (patched !== storeContent) {
263
+ fs.writeFileSync(storePath, patched);
264
+ log('Patched', C.bold + storeFile + C.reset, '— Redux middleware wired');
265
+ } else {
266
+ warn('Could not auto-patch', storeFile, '— wire Redux manually');
267
+ }
268
+ }
269
+ } else if (storeContent.includes('createStore')) {
270
+ warn('Legacy createStore found at', C.bold + storeFile + C.reset);
271
+ console.log(C.dim + ' Add manually:' + C.reset);
272
+ console.log(C.dim + ` import { reduxEnhancer } from '${relSDK}';` + C.reset);
273
+ console.log(C.dim + ' const store = createStore(reducer, __DEV__ ? reduxEnhancer : undefined);' + C.reset);
274
+ }
275
+ } else {
276
+ warn('Redux detected but store file not found automatically');
277
+ console.log(C.dim + ' Add to your store setup:' + C.reset);
278
+ console.log(C.dim + ' import { reduxMiddleware } from \'./src/debug/RNDebugSDK\';' + C.reset);
279
+ }
280
+ } else {
281
+ log('No Redux detected — skipping');
282
+ }
260
283
 
261
284
  // 6. adb reverse for Android
262
285
  if (platform.hasAndroidEmu || platform.hasAndroidDevice) {
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.6",
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/sdk/RNDebugSDK.js CHANGED
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  if (!__DEV__) {
18
- module.exports = { reduxEnhancer: x => x, watchAsyncStorage: () => {} };
18
+ module.exports = { reduxEnhancer: x => x, reduxMiddleware: () => next => action => next(action), watchAsyncStorage: () => {} };
19
19
  } else {
20
20
 
21
21
  // ─── Config ───────────────────────────────────────────────────────────────────