reactoradar 1.6.6 → 1.6.8

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.
@@ -0,0 +1,289 @@
1
+ // ─── Sources Panel ─────────────────────────────────────────────────────────
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // SOURCES PANEL (placeholder — use JS Debugger button for breakpoints)
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ function initSourcesPanel() {
6
+ const panel = $('panel-sources');
7
+ if (!panel) return;
8
+ panel.innerHTML = `
9
+ <div class="panel-toolbar">
10
+ <span class="panel-label">Sources</span>
11
+ <div class="ml-auto" style="display:flex;gap:6px">
12
+ <button class="tb-btn" id="btnOpenSourcesExt" title="Open in separate DevTools window">Breakpoints ↗</button>
13
+ </div>
14
+ </div>
15
+ <div class="sources-layout">
16
+ <div class="sources-sidebar" id="sourcesSidebar">
17
+ <div class="panel-toolbar" style="height:32px">
18
+ <input id="sourcesSearch" class="net-search-input" style="width:100%" placeholder="Search files..." />
19
+ </div>
20
+ <div class="scroll-area sources-file-list" id="sourcesFileList">
21
+ <div class="empty-state" id="sourcesEmpty">
22
+ <div class="icon" style="font-size:28px;opacity:.2">&lt;/&gt;</div>
23
+ <div class="label">Waiting for Metro...</div>
24
+ <div class="hint">Source files will load when Metro is running</div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ <div class="sources-editor" id="sourcesEditor">
29
+ <div class="panel-toolbar" style="height:32px">
30
+ <span id="sourcesFileName" style="font-size:10px;color:var(--accent)"></span>
31
+ <span id="sourcesLineInfo" style="font-size:10px;color:var(--text-dim);margin-left:auto"></span>
32
+ </div>
33
+ <div class="scroll-area sources-code" id="sourcesCode">
34
+ <span style="color:var(--text-dim);padding:20px;display:block">Select a file to view its source</span>
35
+ </div>
36
+ </div>
37
+ </div>`;
38
+
39
+ // Open JS Debugger for breakpoints
40
+ $('btnOpenSourcesExt').addEventListener('click', () => {
41
+ window.electronAPI?.openCDPTarget(null);
42
+ });
43
+
44
+ // Search filter for file tree
45
+ $('sourcesSearch').addEventListener('input', (e) => {
46
+ const term = e.target.value.toLowerCase().trim();
47
+ document.querySelectorAll('#sourcesFileList .src-tree-file').forEach(row => {
48
+ const filepath = row.dataset.file || '';
49
+ const match = !term || filepath.toLowerCase().includes(term);
50
+ row.style.display = match ? '' : 'none';
51
+ });
52
+ // Show/hide folder nodes based on whether they have visible children
53
+ document.querySelectorAll('#sourcesFileList .src-tree-folder').forEach(folder => {
54
+ const visibleFiles = folder.querySelectorAll('.src-tree-file:not([style*="display: none"])');
55
+ folder.style.display = (!term || visibleFiles.length > 0) ? '' : 'none';
56
+ // Auto-expand folders when searching
57
+ if (term && visibleFiles.length > 0) {
58
+ const children = folder.querySelector('.src-tree-children');
59
+ const arrow = folder.querySelector('.src-tree-arrow');
60
+ if (children) children.style.display = 'block';
61
+ if (arrow) { arrow.textContent = '\u25BC'; arrow.classList.add('expanded'); }
62
+ }
63
+ });
64
+ });
65
+
66
+ // Fetch the source map / bundle modules list from Metro
67
+ fetchSourceFileList();
68
+ }
69
+
70
+ async function fetchSourceFileList() {
71
+ if (!window.electronAPI?.getSourceFileList) {
72
+ console.log('[Sources] electronAPI.getSourceFileList not available, retrying...');
73
+ setTimeout(fetchSourceFileList, 5000);
74
+ return;
75
+ }
76
+ try {
77
+ console.log('[Sources] Fetching file list from Metro...');
78
+ const result = await window.electronAPI.getSourceFileList();
79
+ console.log('[Sources] Got result:', result?.files?.length, 'files, root:', result?.root?.slice(-30));
80
+ if (result?.files && result.files.length > 0) {
81
+ state._sourcesRoot = result.root;
82
+ // Limit to 500 files max to avoid DOM overload
83
+ const files = result.files.length > 500 ? result.files.slice(0, 500) : result.files;
84
+ renderSourceFileList(files);
85
+ console.log('[Sources] Rendered', files.length, 'files');
86
+ } else {
87
+ console.log('[Sources] No files, retrying in 5s...');
88
+ setTimeout(fetchSourceFileList, 5000);
89
+ }
90
+ } catch (e) {
91
+ console.log('[Sources] Error:', e?.message || e);
92
+ setTimeout(fetchSourceFileList, 5000);
93
+ }
94
+ }
95
+
96
+ function renderSourceFileList(files) {
97
+ const list = $('sourcesFileList');
98
+ const empty = $('sourcesEmpty');
99
+ if (!list) return;
100
+ if (!files.length) return;
101
+ if (empty) empty.style.display = 'none';
102
+ list.querySelectorAll('.src-tree-node').forEach(e => e.remove());
103
+
104
+ // Build folder tree from file paths
105
+ const tree = {};
106
+ files.forEach(filepath => {
107
+ const parts = filepath.split('/').filter(Boolean);
108
+ let node = tree;
109
+ parts.forEach((part, i) => {
110
+ if (i === parts.length - 1) {
111
+ // File leaf
112
+ node[part] = filepath; // string = file
113
+ } else {
114
+ // Folder
115
+ if (!node[part] || typeof node[part] === 'string') node[part] = {};
116
+ node = node[part];
117
+ }
118
+ });
119
+ });
120
+
121
+ // Render tree recursively
122
+ const frag = document.createDocumentFragment();
123
+
124
+ // Project folders first, node_modules last
125
+ const topKeys = Object.keys(tree).sort((a, b) => {
126
+ if (a === 'node_modules') return 1;
127
+ if (b === 'node_modules') return -1;
128
+ return a.localeCompare(b);
129
+ });
130
+
131
+ topKeys.forEach(key => {
132
+ frag.appendChild(buildSourceTreeNode(key, tree[key], 0));
133
+ });
134
+ list.appendChild(frag);
135
+ }
136
+
137
+ function buildSourceTreeNode(name, value, depth) {
138
+ if (typeof value === 'string') {
139
+ // File leaf
140
+ const row = document.createElement('div');
141
+ row.className = 'src-tree-node src-tree-file';
142
+ row.dataset.file = value;
143
+ row.style.paddingLeft = (12 + depth * 16) + 'px';
144
+ const isNM = value.includes('node_modules');
145
+ const ext = name.split('.').pop();
146
+ const iconColor = ext === 'tsx' || ext === 'ts' ? '#3178c6'
147
+ : ext === 'jsx' || ext === 'js' ? '#f0db4f'
148
+ : ext === 'json' ? '#a0a0a0'
149
+ : ext === 'css' ? '#264de4'
150
+ : 'var(--text-dim)';
151
+ row.innerHTML = `<span class="src-file-icon" style="color:${iconColor}">●</span><span class="src-file-name" style="color:${isNM ? 'var(--text-dim)' : 'var(--text-bright)'}">${esc(name)}</span>`;
152
+ row.addEventListener('click', () => {
153
+ const fileList = $('sourcesFileList');
154
+ fileList.querySelectorAll('.src-tree-file').forEach(el => el.classList.remove('selected'));
155
+ row.classList.add('selected');
156
+ loadSourceFile(value);
157
+ });
158
+ // Search filter support
159
+ const searchInput = $('sourcesSearch');
160
+ if (searchInput && searchInput.value) {
161
+ const term = searchInput.value.toLowerCase();
162
+ if (!name.toLowerCase().includes(term) && !value.toLowerCase().includes(term)) {
163
+ row.style.display = 'none';
164
+ }
165
+ }
166
+ return row;
167
+ }
168
+
169
+ // Folder node
170
+ const container = document.createElement('div');
171
+ container.className = 'src-tree-node src-tree-folder';
172
+
173
+ const header = document.createElement('div');
174
+ header.className = 'src-tree-folder-header';
175
+ header.style.paddingLeft = (8 + depth * 16) + 'px';
176
+
177
+ const arrow = document.createElement('span');
178
+ arrow.className = 'src-tree-arrow';
179
+ arrow.textContent = '\u25B6';
180
+
181
+ const folderName = document.createElement('span');
182
+ folderName.className = 'src-folder-name';
183
+ const isNM = name === 'node_modules';
184
+ folderName.style.color = isNM ? 'var(--text-dim)' : 'var(--text)';
185
+ folderName.textContent = name;
186
+
187
+ header.appendChild(arrow);
188
+ header.appendChild(folderName);
189
+ container.appendChild(header);
190
+
191
+ const children = document.createElement('div');
192
+ children.className = 'src-tree-children';
193
+ // Start all folders collapsed
194
+ children.style.display = 'none';
195
+
196
+ // Sort: folders first, then files
197
+ const entries = Object.entries(value).sort((a, b) => {
198
+ const aIsFolder = typeof a[1] === 'object';
199
+ const bIsFolder = typeof b[1] === 'object';
200
+ if (aIsFolder !== bIsFolder) return aIsFolder ? -1 : 1;
201
+ return a[0].localeCompare(b[0]);
202
+ });
203
+
204
+ let populated = false;
205
+ function populate() {
206
+ if (populated) return;
207
+ populated = true;
208
+ entries.forEach(([childName, childValue]) => {
209
+ children.appendChild(buildSourceTreeNode(childName, childValue, depth + 1));
210
+ });
211
+ }
212
+
213
+ // Folders start collapsed — populate lazily on first expand
214
+ header.addEventListener('click', () => {
215
+ const isOpen = children.style.display !== 'none';
216
+ if (!isOpen) {
217
+ populate();
218
+ children.style.display = 'block';
219
+ arrow.textContent = '\u25BC';
220
+ arrow.classList.add('expanded');
221
+ } else {
222
+ children.style.display = 'none';
223
+ arrow.textContent = '\u25B6';
224
+ arrow.classList.remove('expanded');
225
+ }
226
+ });
227
+
228
+ container.appendChild(children);
229
+ return container;
230
+ }
231
+
232
+ async function loadSourceFile(filepath) {
233
+ const codeEl = $('sourcesCode');
234
+ const nameEl = $('sourcesFileName');
235
+ const lineEl = $('sourcesLineInfo');
236
+ if (!codeEl) return;
237
+ if (nameEl) nameEl.textContent = filepath.split('/').pop();
238
+ if (lineEl) lineEl.textContent = filepath;
239
+ codeEl.innerHTML = '<span style="color:var(--text-dim)">Loading...</span>';
240
+
241
+ let source = null;
242
+ const root = state._sourcesRoot || '';
243
+ const fullPath = root ? `${root}/${filepath}` : filepath;
244
+
245
+ // Strategy 1: Read from disk via IPC (most reliable)
246
+ if (window.electronAPI?.readSourceFile) {
247
+ source = await window.electronAPI.readSourceFile(fullPath);
248
+ }
249
+
250
+ // Strategy 2: Fetch from Metro
251
+ if (!source) {
252
+ try {
253
+ const port = getStoredMetroPort();
254
+ const resp = await fetch(`http://localhost:${port}/${filepath}?platform=ios&dev=true`);
255
+ if (resp.ok) source = await resp.text();
256
+ } catch {}
257
+ }
258
+
259
+ if (!source) {
260
+ codeEl.innerHTML = `<span style="color:var(--text-dim);padding:20px;display:block">Could not load: ${esc(filepath)}</span>`;
261
+ return;
262
+ }
263
+
264
+ // Render with line numbers
265
+ const lines = source.split('\n');
266
+ if (lineEl) lineEl.textContent = `${filepath} (${lines.length} lines)`;
267
+ codeEl.innerHTML = '';
268
+ const pre = document.createElement('pre');
269
+ pre.className = 'source-pre';
270
+ lines.forEach((line, i) => {
271
+ const lineDiv = document.createElement('div');
272
+ lineDiv.className = 'source-line';
273
+ lineDiv.innerHTML = `<span class="source-line-num">${i + 1}</span><span class="source-line-code">${syntaxHighlight(esc(line))}</span>`;
274
+ pre.appendChild(lineDiv);
275
+ });
276
+ codeEl.appendChild(pre);
277
+ }
278
+
279
+ // Called from cdp-targets IPC handler (no longer opens external window)
280
+
281
+ // Called from cdp-targets IPC handler (shared, no duplicate registration)
282
+ // Sources panel uses Metro source map for file tree — CDP targets are only
283
+ // used for the "Breakpoints" button, not for the file list.
284
+ function updateSourcesPanel(targets) {
285
+ // No-op: file list is populated by fetchSourceFileList from Metro source map
286
+ }
287
+
288
+ // ─────────────────────────────────────────────────────────────────────────────
289
+ // PERFORMANCE PANEL — FPS, render timing, JS thread
@@ -0,0 +1,191 @@
1
+ // ─── AsyncStorage Panel ────────────────────────────────────────────────────
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // ASYNC STORAGE PANEL
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ function initStoragePanel() {
6
+ const panel = $('panel-storage');
7
+ if (!panel) return;
8
+ panel.innerHTML = `
9
+ <div class="panel-toolbar">
10
+ <span class="panel-label">AsyncStorage</span>
11
+ <span class="badge" id="sBadge">0</span>
12
+ <div class="ml-auto">
13
+ <input id="storageSearch" class="net-search-input" placeholder="Filter keys..." />
14
+ </div>
15
+ </div>
16
+ <div class="storage-layout" id="storageLayout">
17
+ <div class="storage-keys" id="storageKeysPane">
18
+ <div class="panel-toolbar" style="height:32px">
19
+ <span style="font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px">Keys</span>
20
+ </div>
21
+ <div class="scroll-area storage-keys-list" id="storageKeyList">
22
+ <div class="empty-state" id="storageEmpty">
23
+ <div class="icon">💾</div>
24
+ <div class="label">No storage data</div>
25
+ <div class="hint">AsyncStorage data will appear here</div>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ <div class="storage-resize-handle" id="storageResizeHandle"></div>
30
+ <div class="storage-value-view">
31
+ <div class="storage-value-toolbar">
32
+ <span style="font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px">Value</span>
33
+ <span id="storageSelectedKey" style="font-size:11px;color:var(--accent);margin-left:8px"></span>
34
+ </div>
35
+ <div class="storage-value-body" id="storageValueBody">
36
+ <span style="color:var(--text-dim)">Select a key to view its value</span>
37
+ </div>
38
+ </div>
39
+ </div>`;
40
+
41
+ $('storageSearch').addEventListener('input', (e) => {
42
+ state.storage.searchFilter = e.target.value.toLowerCase().trim();
43
+ renderStorage();
44
+ });
45
+
46
+ // Drag resize handle for key list width
47
+ const handle = $('storageResizeHandle');
48
+ const layout = $('storageLayout');
49
+ const keysPane = $('storageKeysPane');
50
+ if (handle && layout && keysPane) {
51
+ let dragging = false;
52
+ let startX = 0;
53
+ let startW = 0;
54
+ handle.addEventListener('mousedown', (e) => {
55
+ e.preventDefault();
56
+ dragging = true;
57
+ startX = e.clientX;
58
+ startW = keysPane.offsetWidth;
59
+ document.body.style.cursor = 'col-resize';
60
+ document.body.style.userSelect = 'none';
61
+ });
62
+ document.addEventListener('mousemove', (e) => {
63
+ if (!dragging) return;
64
+ const newW = Math.max(120, Math.min(600, startW + (e.clientX - startX)));
65
+ layout.style.gridTemplateColumns = `${newW}px 4px 1fr`;
66
+ });
67
+ document.addEventListener('mouseup', () => {
68
+ if (!dragging) return;
69
+ dragging = false;
70
+ document.body.style.cursor = '';
71
+ document.body.style.userSelect = '';
72
+ });
73
+ }
74
+ }
75
+
76
+ let _storageRAF = null;
77
+
78
+ function handleStorageEvent(event) {
79
+ if (event.type !== 'storage') return;
80
+ if (!isTabEnabled('storage')) return;
81
+ const { key, value, action } = event;
82
+ if (action === 'set' || action === 'snapshot') {
83
+ if (action === 'snapshot' && typeof key === 'object') {
84
+ // Skip if data hasn't changed
85
+ const newKeys = Object.keys(key).slice().sort().join(',');
86
+ const oldKeys = state.storage.keys.slice().sort().join(',');
87
+ if (newKeys === oldKeys) {
88
+ // Check if values changed
89
+ let same = true;
90
+ for (const [k, v] of Object.entries(key)) {
91
+ if (state.storage.entries[k] !== v) { same = false; break; }
92
+ }
93
+ if (same) return; // No changes, skip re-render
94
+ }
95
+ Object.entries(key).forEach(([k, v]) => {
96
+ state.storage.entries[k] = v;
97
+ if (!state.storage.keys.includes(k)) state.storage.keys.push(k);
98
+ });
99
+ } else {
100
+ if (state.storage.entries[key] === value) return; // No change
101
+ state.storage.entries[key] = value;
102
+ if (!state.storage.keys.includes(key)) state.storage.keys.push(key);
103
+ }
104
+ } else if (action === 'remove') {
105
+ if (!(key in state.storage.entries)) return; // Already removed
106
+ delete state.storage.entries[key];
107
+ state.storage.keys = state.storage.keys.filter(k => k !== key);
108
+ if (state.storage.selected === key) state.storage.selected = null;
109
+ }
110
+ $('sBadge').textContent = state.storage.keys.length;
111
+ // Debounce render via rAF
112
+ if (!_storageRAF) {
113
+ _storageRAF = requestAnimationFrame(() => {
114
+ _storageRAF = null;
115
+ renderStorage();
116
+ });
117
+ }
118
+ }
119
+
120
+ function renderStorage() {
121
+ const list = $('storageKeyList');
122
+ const empty = $('storageEmpty');
123
+ if (!list) return;
124
+
125
+ const { searchFilter } = state.storage;
126
+ const visible = state.storage.keys.filter(k =>
127
+ !searchFilter || k.toLowerCase().includes(searchFilter)
128
+ );
129
+
130
+ empty.style.display = visible.length ? 'none' : 'flex';
131
+ list.querySelectorAll('.storage-key-row').forEach(e => e.remove());
132
+
133
+ const frag = document.createDocumentFragment();
134
+ visible.forEach(k => {
135
+ const div = document.createElement('div');
136
+ const val = state.storage.entries[k] || '';
137
+ div.className = 'storage-key-row entry' + (k === state.storage.selected ? ' selected' : '');
138
+ div.innerHTML = `
139
+ <span class="key-name">${highlight(esc(k), searchFilter)}</span>
140
+ <span class="key-size">${formatSize(val.length)}</span>`;
141
+ div.onclick = () => { state.storage.selected = k; renderStorage(); renderStorageValue(); };
142
+ frag.appendChild(div);
143
+ });
144
+ list.appendChild(frag);
145
+ renderStorageValue();
146
+ }
147
+
148
+ function renderStorageValue() {
149
+ let body = $('storageValueBody');
150
+ const keyLabel = $('storageSelectedKey');
151
+ if (!body) return;
152
+ const { selected, entries } = state.storage;
153
+ if (!selected) {
154
+ body.innerHTML = '<span style="color:var(--text-dim)">Select a key</span>';
155
+ if (keyLabel) keyLabel.textContent = '';
156
+ return;
157
+ }
158
+ if (keyLabel) keyLabel.textContent = selected;
159
+ // Clone-replace to remove stale event listeners
160
+ const fresh = body.cloneNode(false);
161
+ body.parentNode.replaceChild(fresh, body);
162
+ body = fresh;
163
+
164
+ let val = entries[selected];
165
+ // Try to parse JSON strings into objects for tree display
166
+ if (typeof val === 'string') {
167
+ try { val = JSON.parse(val); } catch {}
168
+ }
169
+
170
+ if (val && typeof val === 'object') {
171
+ body.appendChild(createTreeNode(null, val, false));
172
+ body.addEventListener('contextmenu', (e) => {
173
+ e.preventDefault();
174
+ showContextMenu(e, [
175
+ { label: 'Copy Value', action: () => navigator.clipboard.writeText(JSON.stringify(val, null, 2)) },
176
+ { label: 'Copy Key', action: () => navigator.clipboard.writeText(selected) },
177
+ ]);
178
+ });
179
+ } else {
180
+ body.innerHTML = renderJSON(val);
181
+ }
182
+ }
183
+
184
+ function formatSize(bytes) {
185
+ if (bytes < 1024) return `${bytes}b`;
186
+ return `${(bytes/1024).toFixed(1)}kb`;
187
+ }
188
+
189
+ // ─────────────────────────────────────────────────────────────────────────────
190
+ // REACT TREE PANEL
191
+ // ─────────────────────────────────────────────────────────────────────────────