reactoradar 1.2.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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/app.js +2450 -0
  4. package/assets/icon.svg +54 -0
  5. package/bin/cli.js +79 -0
  6. package/bin/open-debugger.sh +9 -0
  7. package/bin/setup.js +473 -0
  8. package/index.html +82 -0
  9. package/main.js +528 -0
  10. package/package.json +76 -0
  11. package/preload.js +31 -0
  12. package/sdk/RNDebugSDK.js +540 -0
  13. package/src/main/main.js +396 -0
  14. package/src/main/preload.js +28 -0
  15. package/src/renderer/app.js +221 -0
  16. package/src/renderer/components/object-tree.js +245 -0
  17. package/src/renderer/index.html +111 -0
  18. package/src/renderer/panels/console.js +248 -0
  19. package/src/renderer/panels/memory.js +60 -0
  20. package/src/renderer/panels/network.js +559 -0
  21. package/src/renderer/panels/performance.js +144 -0
  22. package/src/renderer/panels/react.js +31 -0
  23. package/src/renderer/panels/redux.js +159 -0
  24. package/src/renderer/panels/settings.js +93 -0
  25. package/src/renderer/panels/sources.js +189 -0
  26. package/src/renderer/panels/storage.js +134 -0
  27. package/src/renderer/state.js +132 -0
  28. package/src/renderer/styles/components.css +145 -0
  29. package/src/renderer/styles/console.css +73 -0
  30. package/src/renderer/styles/main.css +229 -0
  31. package/src/renderer/styles/network.css +242 -0
  32. package/src/renderer/styles/performance.css +45 -0
  33. package/src/renderer/styles/redux.css +77 -0
  34. package/src/renderer/styles/settings.css +63 -0
  35. package/src/renderer/styles/sources.css +48 -0
  36. package/src/renderer/styles/storage.css +28 -0
  37. package/src/renderer/styles/theme-light.css +57 -0
  38. package/styles.css +1308 -0
package/index.html ADDED
@@ -0,0 +1,82 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self' http://localhost:* ws://localhost:*; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com;">
6
+ <title>ReactoRadar</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=Syne:wght@700;800&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="styles.css">
10
+ </head>
11
+ <body>
12
+ <div id="app">
13
+
14
+ <!-- ── TITLE BAR (logo + device status + actions in one line) ── -->
15
+ <header id="titlebar">
16
+ <div class="titlebar-drag"></div>
17
+ <div class="logo">Reacto<span>Radar</span></div>
18
+ <span class="title-sep">—</span>
19
+ <div id="deviceStatus" class="device-status waiting">
20
+ <span class="device-dot"></span>
21
+ <span id="deviceText">Waiting for device...</span>
22
+ </div>
23
+ <div class="titlebar-actions">
24
+ <button class="tb-btn" id="btnClear" title="Clear active tab (⌘K clears all)">Clear</button>
25
+ <button class="tb-btn primary" id="btnCDP" title="Open JS Debugger (⌘D)">JS Debugger ↗</button>
26
+ </div>
27
+ </header>
28
+
29
+ <!-- ── SIDEBAR ── -->
30
+ <nav id="sidebar">
31
+ <button class="nav-btn active" data-panel="console" title="Console">
32
+ <svg viewBox="0 0 20 20"><path d="M3 5h14M3 10h8M3 15h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
33
+ <span>Console</span>
34
+ </button>
35
+ <button class="nav-btn" data-panel="network" title="Network">
36
+ <svg viewBox="0 0 20 20"><circle cx="10" cy="10" r="7" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M10 3c-2 2.5-2 11.5 0 14M10 3c2 2.5 2 11.5 0 14M3 10h14" stroke="currentColor" stroke-width="1.5"/></svg>
37
+ <span>Network</span>
38
+ </button>
39
+ <button class="nav-btn" data-panel="redux" title="Redux">
40
+ <svg viewBox="0 0 20 20"><rect x="3" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5" fill="none"/><rect x="11" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5" fill="none"/><rect x="7" y="11" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
41
+ <span>Redux</span>
42
+ </button>
43
+ <button class="nav-btn" data-panel="storage" title="Application">
44
+ <svg viewBox="0 0 20 20"><ellipse cx="10" cy="6" rx="7" ry="3" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M3 6v4c0 1.66 3.13 3 7 3s7-1.34 7-3V6" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M3 10v4c0 1.66 3.13 3 7 3s7-1.34 7-3v-4" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
45
+ <span>App</span>
46
+ </button>
47
+ <button class="nav-btn" data-panel="memory" title="Memory">
48
+ <svg viewBox="0 0 20 20"><rect x="3" y="8" width="3" height="8" rx="0.5" stroke="currentColor" stroke-width="1.2" fill="none"/><rect x="8.5" y="4" width="3" height="12" rx="0.5" stroke="currentColor" stroke-width="1.2" fill="none"/><rect x="14" y="6" width="3" height="10" rx="0.5" stroke="currentColor" stroke-width="1.2" fill="none"/></svg>
49
+ <span>Memory</span>
50
+ </button>
51
+ <button class="nav-btn" data-panel="performance" title="Performance">
52
+ <svg viewBox="0 0 20 20"><polyline points="2,16 6,10 10,13 14,5 18,8" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
53
+ <span>Perf</span>
54
+ </button>
55
+ <button class="nav-btn" data-panel="react" title="React Tree">
56
+ <svg viewBox="0 0 20 20"><circle cx="10" cy="10" r="2" fill="currentColor"/><ellipse cx="10" cy="10" rx="8" ry="3.5" stroke="currentColor" stroke-width="1.5" fill="none"/><ellipse cx="10" cy="10" rx="8" ry="3.5" stroke="currentColor" stroke-width="1.5" fill="none" transform="rotate(60 10 10)"/><ellipse cx="10" cy="10" rx="8" ry="3.5" stroke="currentColor" stroke-width="1.5" fill="none" transform="rotate(120 10 10)"/></svg>
57
+ <span>React</span>
58
+ </button>
59
+ <div class="nav-spacer"></div>
60
+ <button class="nav-btn" data-panel="settings" title="Settings">
61
+ <svg viewBox="0 0 20 20"><path d="M10 13a3 3 0 100-6 3 3 0 000 6z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M17.4 11c.2-.6.2-1.4 0-2l1.3-1-1.7-3-1.6.5c-.5-.4-1-.7-1.6-.9L13.4 3H10.6l-.4 1.6c-.6.2-1.1.5-1.6.9L7 5 5.3 8l1.3 1c-.2.6-.2 1.4 0 2L5.3 12 7 15l1.6-.5c.5.4 1 .7 1.6.9l.4 1.6h2.8l.4-1.6c.6-.2 1.1-.5 1.6-.9l1.6.5 1.7-3-1.3-1z" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
62
+ <span>Settings</span>
63
+ </button>
64
+ </nav>
65
+
66
+ <!-- ── PANELS ── -->
67
+ <main id="content">
68
+ <div id="panel-console" class="panel active"></div>
69
+ <div id="panel-network" class="panel"></div>
70
+ <div id="panel-performance" class="panel"></div>
71
+ <div id="panel-memory" class="panel"></div>
72
+ <div id="panel-redux" class="panel"></div>
73
+ <div id="panel-storage" class="panel"></div>
74
+ <div id="panel-react" class="panel"></div>
75
+ <div id="panel-settings" class="panel"></div>
76
+ </main>
77
+
78
+ </div>
79
+
80
+ <script src="app.js"></script>
81
+ </body>
82
+ </html>
package/main.js ADDED
@@ -0,0 +1,528 @@
1
+ 'use strict';
2
+
3
+ const { app, BrowserWindow, ipcMain, Menu, shell, nativeTheme, nativeImage } = require('electron');
4
+ const path = require('path');
5
+ const http = require('http');
6
+ const https = require('https');
7
+ const { WebSocketServer, WebSocket } = require('ws');
8
+
9
+ // ─── Ports ────────────────────────────────────────────────────────────────────
10
+ const PORTS = {
11
+ METRO: 8081, // Metro bundler (CDP proxy lives here)
12
+ REACT_DT: 8097, // react-devtools-core server port
13
+ REDUX_BRIDGE: 9090, // our custom Redux WS bridge
14
+ STORAGE_BRIDGE:9091, // AsyncStorage WS bridge
15
+ NETWORK_BRIDGE:9092, // Network intercept WS bridge
16
+ };
17
+
18
+ // ─── Windows ──────────────────────────────────────────────────────────────────
19
+ let mainWindow = null;
20
+ let devtoolsWindow = null; // hosts the embedded CDP DevTools frontend
21
+
22
+ // ─── State ────────────────────────────────────────────────────────────────────
23
+ let reduxClients = new Set();
24
+ let storageClients = new Set();
25
+ let networkClients = new Set();
26
+
27
+ // ─── App lifecycle ────────────────────────────────────────────────────────────
28
+ app.whenReady().then(async () => {
29
+ // Theme will be set by renderer via IPC once it reads localStorage
30
+ nativeTheme.themeSource = 'dark';
31
+
32
+ // Set dock icon on macOS
33
+ if (process.platform === 'darwin') {
34
+ try {
35
+ const iconPath = path.join(__dirname, 'ReactoRadar.png');
36
+ const icon = nativeImage.createFromPath(iconPath);
37
+ if (!icon.isEmpty()) {
38
+ app.dock.setIcon(icon);
39
+ }
40
+ } catch (e) {
41
+ console.warn('[Icon] Failed to set dock icon:', e.message);
42
+ }
43
+ }
44
+
45
+ await createMainWindow();
46
+
47
+ // Check for updates (non-blocking)
48
+ checkForUpdates();
49
+ startBridgeServers();
50
+ startReactDevToolsServer();
51
+ setupMetroCDPProxy();
52
+ setupIPC();
53
+ buildMenu();
54
+ });
55
+
56
+ app.on('window-all-closed', () => {
57
+ if (process.platform !== 'darwin') app.quit();
58
+ });
59
+
60
+ app.on('before-quit', () => {
61
+ // Close all WS servers gracefully
62
+ if (reactDTServer) {
63
+ reactDTServer.close();
64
+ reactDTClients.forEach(ws => ws.close());
65
+ reactDTClients.clear();
66
+ }
67
+ });
68
+
69
+ app.on('activate', () => {
70
+ if (BrowserWindow.getAllWindows().length === 0) createMainWindow();
71
+ });
72
+
73
+ // ─── Main Window ──────────────────────────────────────────────────────────────
74
+ async function createMainWindow() {
75
+ mainWindow = new BrowserWindow({
76
+ width: 1400,
77
+ height: 900,
78
+ minWidth: 900,
79
+ minHeight: 600,
80
+ titleBarStyle: 'hiddenInset',
81
+ backgroundColor: '#0a0b0e',
82
+ vibrancy: 'under-window',
83
+ visualEffectState: 'active',
84
+ webPreferences: {
85
+ nodeIntegration: false,
86
+ contextIsolation: true,
87
+ preload: path.join(__dirname, 'preload.js'),
88
+ },
89
+ icon: nativeImage.createFromPath(path.join(__dirname, 'ReactoRadar.png')),
90
+ });
91
+
92
+ mainWindow.loadFile(path.join(__dirname, 'index.html'));
93
+
94
+ // Open the JS Debugger panel (CDP DevTools) in a second window
95
+ mainWindow.webContents.on('did-finish-load', () => {
96
+ mainWindow.webContents.send('ports', PORTS);
97
+ });
98
+ }
99
+
100
+ // ─── Update Checker ──────────────────────────────────────────────────────────
101
+ function checkForUpdates() {
102
+ const currentVersion = require('./package.json').version;
103
+ https.get('https://registry.npmjs.org/reactoradar/latest', (res) => {
104
+ let data = '';
105
+ res.on('data', d => data += d);
106
+ res.on('end', () => {
107
+ try {
108
+ const latest = JSON.parse(data).version;
109
+ if (latest && latest !== currentVersion) {
110
+ // Notify the renderer to show an update banner
111
+ mainWindow?.webContents.send('update-available', { current: currentVersion, latest });
112
+ console.log(`[Update] New version available: ${latest} (current: ${currentVersion})`);
113
+ }
114
+ } catch {}
115
+ });
116
+ }).on('error', () => {}); // Silently fail — update check is optional
117
+ }
118
+
119
+ // ─── CDP DevTools Window (JS breakpoints, Sources, Console) ──────────────────
120
+ let lastKnownTargets = [];
121
+
122
+ function openCDPWindow(target) {
123
+ if (devtoolsWindow && !devtoolsWindow.isDestroyed()) {
124
+ devtoolsWindow.focus();
125
+ return;
126
+ }
127
+
128
+ // Build the frontend URL from Metro's provided devtoolsFrontendUrl
129
+ // Metro /json/list returns: { devtoolsFrontendUrl: "/debugger-frontend/rn_fusebox.html?ws=...", ... }
130
+ let frontendUrl;
131
+ if (target.devtoolsFrontendUrl) {
132
+ // Metro provides the exact path — use it
133
+ frontendUrl = `http://localhost:${PORTS.METRO}${target.devtoolsFrontendUrl}`;
134
+ } else if (target.webSocketDebuggerUrl) {
135
+ // Fallback: construct URL manually with rn_fusebox (RN 0.76+) or rn_inspector (older)
136
+ const wsUrl = target.webSocketDebuggerUrl;
137
+ frontendUrl = `http://localhost:${PORTS.METRO}/debugger-frontend/rn_fusebox.html?ws=${encodeURIComponent(wsUrl)}&sources.hide_add_folder=true`;
138
+ } else {
139
+ console.warn('[CDP] No usable target URL');
140
+ return;
141
+ }
142
+
143
+ const titleSuffix = target.deviceName ? ` — ${target.deviceName}` : '';
144
+ devtoolsWindow = new BrowserWindow({
145
+ width: 1200,
146
+ height: 800,
147
+ titleBarStyle: 'hiddenInset',
148
+ backgroundColor: '#0a0b0e',
149
+ title: `JS Debugger${titleSuffix}`,
150
+ webPreferences: {
151
+ nodeIntegration: false,
152
+ contextIsolation: true,
153
+ sandbox: true,
154
+ },
155
+ });
156
+
157
+ console.log(`[CDP] Loading DevTools: ${frontendUrl}`);
158
+ devtoolsWindow.loadURL(frontendUrl);
159
+
160
+ devtoolsWindow.on('closed', () => { devtoolsWindow = null; });
161
+ }
162
+
163
+ // ─── Metro CDP — fetch targets on demand (no continuous polling) ──────────────
164
+ // Continuous polling causes Metro's dev-middleware WebSocket to crash with
165
+ // "readyState 3 (CLOSED)" when connections are opened/closed rapidly.
166
+ // Instead, we fetch targets only when the user needs them.
167
+ function fetchCDPTargets(callback) {
168
+ http.get(`http://localhost:${PORTS.METRO}/json/list`, (res) => {
169
+ let data = '';
170
+ res.on('data', d => data += d);
171
+ res.on('end', () => {
172
+ try {
173
+ const targets = JSON.parse(data);
174
+ const rnTargets = targets.filter(t =>
175
+ t.type === 'node' || t.devtoolsFrontendUrl
176
+ );
177
+ lastKnownTargets = rnTargets;
178
+ mainWindow?.webContents.send('cdp-targets', rnTargets);
179
+ if (callback) callback(rnTargets);
180
+ } catch (_) {
181
+ if (callback) callback([]);
182
+ }
183
+ });
184
+ }).on('error', () => {
185
+ lastKnownTargets = [];
186
+ mainWindow?.webContents.send('cdp-targets', []);
187
+ if (callback) callback([]);
188
+ });
189
+ }
190
+
191
+ function setupMetroCDPProxy() {
192
+ // Single fetch after app starts (not continuous polling)
193
+ setTimeout(() => fetchCDPTargets(), 3000);
194
+ }
195
+
196
+ // ─── React DevTools Relay Server (Component Tree + Profiler) ─────────────────
197
+ // React Native automatically connects to ws://localhost:8097 in dev mode.
198
+ // We run a simple WS relay on that port. When a standalone react-devtools
199
+ // window connects (via `npx react-devtools`) or when the RN app connects,
200
+ // we track the connection and relay messages between frontend ↔ backend.
201
+ let reactDTServer = null;
202
+ let reactDTClients = new Set();
203
+
204
+ function startReactDevToolsServer() {
205
+ try {
206
+ reactDTServer = new WebSocketServer({ port: PORTS.REACT_DT });
207
+ reactDTServer.on('error', (err) => {
208
+ console.warn(`[ReactDT] Server error: ${err.message}`);
209
+ if (err.code === 'EADDRINUSE') {
210
+ mainWindow?.webContents.send('react-dt-status', false);
211
+ }
212
+ });
213
+ reactDTServer.on('connection', (ws) => {
214
+ reactDTClients.add(ws);
215
+ console.log(`[ReactDT] Client connected (total: ${reactDTClients.size})`);
216
+ mainWindow?.webContents.send('react-dt-status', true);
217
+
218
+ // Relay messages between all connected clients (frontend ↔ backend)
219
+ ws.on('message', (data) => {
220
+ reactDTClients.forEach((client) => {
221
+ if (client !== ws && client.readyState === WebSocket.OPEN) {
222
+ client.send(data);
223
+ }
224
+ });
225
+ });
226
+
227
+ ws.on('close', () => {
228
+ reactDTClients.delete(ws);
229
+ console.log(`[ReactDT] Client disconnected (total: ${reactDTClients.size})`);
230
+ if (reactDTClients.size === 0) {
231
+ mainWindow?.webContents.send('react-dt-status', false);
232
+ }
233
+ });
234
+ });
235
+ console.log(`[ReactDT] Relay server on :${PORTS.REACT_DT}`);
236
+ } catch (e) {
237
+ console.warn('[ReactDT] Failed to start relay server:', e.message);
238
+ }
239
+ }
240
+
241
+ // ─── Bridge Servers (Redux, Storage, Network) ─────────────────────────────────
242
+ function startBridgeServers() {
243
+ // Redux Bridge
244
+ startBridge(PORTS.REDUX_BRIDGE, 'redux', reduxClients, (event) => {
245
+ mainWindow?.webContents.send('redux-event', event);
246
+ });
247
+
248
+ // AsyncStorage Bridge
249
+ startBridge(PORTS.STORAGE_BRIDGE, 'storage', storageClients, (event) => {
250
+ mainWindow?.webContents.send('storage-event', event);
251
+ });
252
+
253
+ // Network + Console + Perf Bridge (port 9092 carries all types from RNDebugSDK)
254
+ startBridge(PORTS.NETWORK_BRIDGE, 'network', networkClients, (event) => {
255
+ if (event.type === 'control') return;
256
+ if (event.type === 'console') {
257
+ mainWindow?.webContents.send('console-event', event);
258
+ } else if (event.type === 'perf') {
259
+ mainWindow?.webContents.send('perf-event', event);
260
+ } else {
261
+ mainWindow?.webContents.send('network-event', event);
262
+ }
263
+ });
264
+ }
265
+
266
+ function startBridge(port, name, clients, onEvent) {
267
+ const wss = new WebSocketServer({ port });
268
+ wss.on('connection', (ws) => {
269
+ clients.add(ws);
270
+ console.log(`[${name}] RN app connected`);
271
+ mainWindow?.webContents.send(`${name}-connected`, true);
272
+
273
+ ws.on('message', (raw) => {
274
+ try {
275
+ const event = JSON.parse(raw.toString());
276
+ onEvent(event);
277
+ } catch (e) {
278
+ console.warn(`[${name}] Failed to parse message:`, e.message);
279
+ }
280
+ });
281
+
282
+ ws.on('close', () => {
283
+ clients.delete(ws);
284
+ if (clients.size === 0) {
285
+ mainWindow?.webContents.send(`${name}-connected`, false);
286
+ }
287
+ });
288
+ });
289
+ console.log(`[${name}] Bridge on :${port}`);
290
+ }
291
+
292
+ // ─── IPC from Renderer ────────────────────────────────────────────────────────
293
+ function setupIPC() {
294
+ ipcMain.on('open-cdp-target', (_, wsUrl) => {
295
+ // Always fetch fresh targets, then open
296
+ fetchCDPTargets((targets) => {
297
+ if (wsUrl && targets.length > 0) {
298
+ const target = targets.find(t => t.webSocketDebuggerUrl === wsUrl) || targets[0];
299
+ openCDPWindow(target);
300
+ } else if (targets.length > 0) {
301
+ const target = targets.find(t =>
302
+ t.reactNative?.capabilities?.prefersFuseboxFrontend
303
+ ) || targets[0];
304
+ openCDPWindow(target);
305
+ }
306
+ });
307
+ });
308
+
309
+ ipcMain.on('open-react-devtools', () => {
310
+ // Open standalone react-devtools window
311
+ const rdtWin = new BrowserWindow({
312
+ width: 1100,
313
+ height: 700,
314
+ titleBarStyle: 'hiddenInset',
315
+ backgroundColor: '#0a0b0e',
316
+ title: 'React DevTools',
317
+ webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true },
318
+ });
319
+ // Metro serves the React DevTools frontend at /debugger-ui
320
+ rdtWin.loadURL(`http://localhost:${PORTS.METRO}/debugger-ui`);
321
+ });
322
+
323
+ // clear-all is handled by renderer via clear-all-ui IPC from menu
324
+
325
+ ipcMain.on('set-network-capture', (_, enabled) => {
326
+ // Broadcast to connected RN apps so they can stop/start intercepting
327
+ networkClients.forEach(ws => {
328
+ if (ws.readyState === WebSocket.OPEN) {
329
+ ws.send(JSON.stringify({ type: 'control', action: 'set-network-capture', enabled }));
330
+ }
331
+ });
332
+ });
333
+
334
+ // Track the RN project root — detected from Metro or setup
335
+ let _rnProjectRoot = null;
336
+
337
+ ipcMain.handle('read-source-file', async (_, filepath) => {
338
+ const fs = require('fs');
339
+ try {
340
+ // Try absolute path first
341
+ if (path.isAbsolute(filepath) && fs.existsSync(filepath)) {
342
+ return fs.readFileSync(filepath, 'utf8');
343
+ }
344
+
345
+ // Find the RN project root by checking where Metro is running
346
+ if (!_rnProjectRoot) {
347
+ // Look for common project paths
348
+ const candidates = [];
349
+ // Check Metro's cwd by looking at the source map paths
350
+ const home = process.env.HOME || '';
351
+ // Scan for directories containing package.json with react-native
352
+ // Dynamically find RN project directories
353
+ const searchDirs = [];
354
+ // Scan home directory for common RN project patterns
355
+ try {
356
+ const homeItems = require('fs').readdirSync(home);
357
+ homeItems.forEach(dir => {
358
+ const full = path.join(home, dir);
359
+ try {
360
+ const sub = require('fs').readdirSync(full);
361
+ sub.forEach(s => {
362
+ const projDir = path.join(full, s);
363
+ if (require('fs').existsSync(path.join(projDir, 'package.json')) &&
364
+ require('fs').existsSync(path.join(projDir, 'node_modules', 'react-native'))) {
365
+ searchDirs.push(projDir);
366
+ }
367
+ // One level deeper (e.g., ~/Company/branch/project)
368
+ try {
369
+ require('fs').readdirSync(projDir).forEach(ss => {
370
+ const deep = path.join(projDir, ss);
371
+ if (require('fs').existsSync(path.join(deep, 'package.json')) &&
372
+ require('fs').existsSync(path.join(deep, 'node_modules', 'react-native'))) {
373
+ searchDirs.push(deep);
374
+ }
375
+ });
376
+ } catch {}
377
+ });
378
+ } catch {}
379
+ });
380
+ } catch {}
381
+ // Also try to detect from Metro's /json endpoint
382
+ try {
383
+ const result = require('child_process').execSync(
384
+ "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.*||'",
385
+ { encoding: 'utf8', timeout: 3000 }
386
+ ).trim();
387
+ if (result && fs.existsSync(result)) candidates.unshift(result);
388
+ } catch {}
389
+
390
+ candidates.push(...searchDirs);
391
+ for (const dir of candidates) {
392
+ const full = path.join(dir, filepath);
393
+ if (fs.existsSync(full)) {
394
+ _rnProjectRoot = dir;
395
+ break;
396
+ }
397
+ }
398
+ }
399
+
400
+ if (_rnProjectRoot) {
401
+ const full = path.join(_rnProjectRoot, filepath);
402
+ if (fs.existsSync(full)) return fs.readFileSync(full, 'utf8');
403
+ }
404
+
405
+ // Last resort: search recursively from home
406
+ const homeSearch = path.join(process.env.HOME || '', filepath);
407
+ if (fs.existsSync(homeSearch)) return fs.readFileSync(homeSearch, 'utf8');
408
+
409
+ return null;
410
+ } catch (e) {
411
+ return null;
412
+ }
413
+ });
414
+
415
+ ipcMain.on('set-stack-trace-capture', (_, enabled) => {
416
+ networkClients.forEach(ws => {
417
+ if (ws.readyState === WebSocket.OPEN) {
418
+ ws.send(JSON.stringify({ type: 'control', action: 'set-stack-trace', enabled }));
419
+ }
420
+ });
421
+ });
422
+
423
+ ipcMain.on('set-network-throttle', (_, profile) => {
424
+ // Broadcast throttle config to connected RN apps
425
+ networkClients.forEach(ws => {
426
+ if (ws.readyState === WebSocket.OPEN) {
427
+ ws.send(JSON.stringify({ type: 'control', action: 'set-throttle', profile }));
428
+ }
429
+ });
430
+ });
431
+
432
+ ipcMain.on('open-external', (_, url) => {
433
+ if (url && typeof url === 'string' && (url.startsWith('https://') || url.startsWith('http://'))) {
434
+ shell.openExternal(url);
435
+ }
436
+ });
437
+
438
+ ipcMain.on('set-theme', (_, theme) => {
439
+ nativeTheme.themeSource = theme === 'light' ? 'light' : 'dark';
440
+ const bg = theme === 'light' ? '#f5f6f8' : '#0a0b0e';
441
+ if (mainWindow && !mainWindow.isDestroyed()) {
442
+ mainWindow.setBackgroundColor(bg);
443
+ }
444
+ });
445
+ }
446
+
447
+ // ─── macOS App Menu ───────────────────────────────────────────────────────────
448
+ function buildMenu() {
449
+ const template = [
450
+ {
451
+ label: app.name,
452
+ submenu: [
453
+ { role: 'about' },
454
+ { type: 'separator' },
455
+ { role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' },
456
+ { type: 'separator' },
457
+ { role: 'quit' },
458
+ ],
459
+ },
460
+ {
461
+ label: 'Debugger',
462
+ submenu: [
463
+ {
464
+ label: 'Open JS Debugger (CDP)',
465
+ accelerator: 'Cmd+D',
466
+ click: () => { mainWindow?.webContents.send('trigger-open-cdp'); },
467
+ },
468
+ {
469
+ label: 'Open React DevTools',
470
+ accelerator: 'Cmd+R',
471
+ click: () => { ipcMain.emit('open-react-devtools'); },
472
+ },
473
+ { type: 'separator' },
474
+ {
475
+ label: 'Clear All',
476
+ accelerator: 'Cmd+K',
477
+ click: () => { mainWindow?.webContents.send('clear-all-ui'); },
478
+ },
479
+ { type: 'separator' },
480
+ {
481
+ label: 'Next Theme',
482
+ accelerator: 'Cmd+Shift+T',
483
+ click: () => {
484
+ const themes = ['dark','light','monokai','dracula','solarized-dark','solarized-light','nord','github-dark','one-dark'];
485
+ const current = nativeTheme.themeSource || 'dark';
486
+ const idx = themes.indexOf(current);
487
+ const next = themes[(idx + 1) % themes.length];
488
+ nativeTheme.themeSource = next.includes('light') ? 'light' : 'dark';
489
+ if (mainWindow && !mainWindow.isDestroyed()) {
490
+ mainWindow.webContents.send('theme-changed', next);
491
+ }
492
+ },
493
+ },
494
+ ],
495
+ },
496
+ {
497
+ label: 'Edit',
498
+ submenu: [
499
+ { role: 'undo' },
500
+ { role: 'redo' },
501
+ { type: 'separator' },
502
+ { role: 'cut' },
503
+ { role: 'copy' },
504
+ { role: 'paste' },
505
+ { role: 'selectAll' },
506
+ ],
507
+ },
508
+ {
509
+ label: 'View',
510
+ submenu: [
511
+ { role: 'reload' }, { role: 'forceReload' },
512
+ { type: 'separator' },
513
+ { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' },
514
+ { type: 'separator' },
515
+ { role: 'togglefullscreen' },
516
+ ],
517
+ },
518
+ {
519
+ label: 'Window',
520
+ submenu: [
521
+ { role: 'minimize' }, { role: 'zoom' },
522
+ { type: 'separator' },
523
+ { role: 'front' },
524
+ ],
525
+ },
526
+ ];
527
+ Menu.setApplicationMenu(Menu.buildFromTemplate(template));
528
+ }
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "reactoradar",
3
+ "productName": "ReactoRadar",
4
+ "version": "1.2.3",
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
+ "main": "main.js",
7
+ "bin": {
8
+ "reactoradar": "./bin/cli.js",
9
+ "rn-debugger-app": "./bin/cli.js",
10
+ "rn-debugger-setup": "./bin/setup.js"
11
+ },
12
+ "keywords": [
13
+ "react-native",
14
+ "debugger",
15
+ "devtools",
16
+ "electron",
17
+ "redux",
18
+ "network-inspector",
19
+ "hermes",
20
+ "flipper-alternative",
21
+ "react-native-debugger",
22
+ "macos"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/sharanagouda/react-native-debugger.git"
27
+ },
28
+ "homepage": "https://github.com/sharanagouda/react-native-debugger#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/sharanagouda/react-native-debugger/issues"
31
+ },
32
+ "license": "MIT",
33
+ "author": "sharanagouda",
34
+ "files": [
35
+ "main.js",
36
+ "preload.js",
37
+ "index.html",
38
+ "app.js",
39
+ "styles.css",
40
+ "sdk/",
41
+ "bin/",
42
+ "assets/",
43
+ "src/"
44
+ ],
45
+ "scripts": {
46
+ "start": "unset ELECTRON_RUN_AS_NODE && electron .",
47
+ "setup": "node bin/setup.js",
48
+ "unsetup": "node bin/setup.js --uninstall",
49
+ "build": "electron-builder --mac",
50
+ "pack": "electron-builder --mac --dir"
51
+ },
52
+ "dependencies": {
53
+ "react-devtools-core": "^5.3.1",
54
+ "ws": "^8.17.0"
55
+ },
56
+ "devDependencies": {
57
+ "electron": "^35.7.5",
58
+ "electron-builder": "^24.13.0",
59
+ "electron-devtools-installer": "^3.2.0"
60
+ },
61
+ "build": {
62
+ "appId": "com.yourteam.rn-debugger",
63
+ "productName": "ReactoRadar",
64
+ "mac": {
65
+ "category": "public.app-category.developer-tools",
66
+ "icon": "ReactoRadar.icns",
67
+ "target": [
68
+ "dmg",
69
+ "zip"
70
+ ]
71
+ },
72
+ "directories": {
73
+ "output": "dist"
74
+ }
75
+ }
76
+ }