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 +77 -54
- package/main.js +87 -40
- package/package.json +1 -1
- package/sdk/RNDebugSDK.js +1 -1
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 (!
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
78
|
+
mainWindow.webContents.on('did-finish-load', () => {
|
|
46
79
|
[200, 1000, 3000].forEach(delay => {
|
|
47
80
|
setTimeout(() => {
|
|
48
81
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
306
|
+
_send('redux-event', event);
|
|
274
307
|
});
|
|
275
308
|
|
|
276
309
|
// AsyncStorage Bridge
|
|
277
310
|
startBridge(PORTS.STORAGE_BRIDGE, 'storage', storageClients, (event) => {
|
|
278
|
-
|
|
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
|
-
|
|
318
|
+
_send('console-event', event);
|
|
286
319
|
} else if (event.type === 'perf') {
|
|
287
|
-
|
|
320
|
+
_send('perf-event', event);
|
|
288
321
|
} else if (event.type === 'ga4') {
|
|
289
|
-
|
|
322
|
+
_send('ga4-event', event);
|
|
290
323
|
} else {
|
|
291
|
-
|
|
324
|
+
_send('network-event', event);
|
|
292
325
|
}
|
|
293
326
|
});
|
|
294
327
|
}
|
|
295
328
|
|
|
296
329
|
function startBridge(port, name, clients, onEvent) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: () => {
|
|
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: () => {
|
|
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
|
-
|
|
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: () => {
|
|
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
|
+
"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 ───────────────────────────────────────────────────────────────────
|