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.
- package/LICENSE +21 -0
- package/README.md +366 -0
- package/app.js +2450 -0
- package/assets/icon.svg +54 -0
- package/bin/cli.js +79 -0
- package/bin/open-debugger.sh +9 -0
- package/bin/setup.js +473 -0
- package/index.html +82 -0
- package/main.js +528 -0
- package/package.json +76 -0
- package/preload.js +31 -0
- package/sdk/RNDebugSDK.js +540 -0
- package/src/main/main.js +396 -0
- package/src/main/preload.js +28 -0
- package/src/renderer/app.js +221 -0
- package/src/renderer/components/object-tree.js +245 -0
- package/src/renderer/index.html +111 -0
- package/src/renderer/panels/console.js +248 -0
- package/src/renderer/panels/memory.js +60 -0
- package/src/renderer/panels/network.js +559 -0
- package/src/renderer/panels/performance.js +144 -0
- package/src/renderer/panels/react.js +31 -0
- package/src/renderer/panels/redux.js +159 -0
- package/src/renderer/panels/settings.js +93 -0
- package/src/renderer/panels/sources.js +189 -0
- package/src/renderer/panels/storage.js +134 -0
- package/src/renderer/state.js +132 -0
- package/src/renderer/styles/components.css +145 -0
- package/src/renderer/styles/console.css +73 -0
- package/src/renderer/styles/main.css +229 -0
- package/src/renderer/styles/network.css +242 -0
- package/src/renderer/styles/performance.css +45 -0
- package/src/renderer/styles/redux.css +77 -0
- package/src/renderer/styles/settings.css +63 -0
- package/src/renderer/styles/sources.css +48 -0
- package/src/renderer/styles/storage.css +28 -0
- package/src/renderer/styles/theme-light.css +57 -0
- 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
|
+
}
|