what-devtools 0.6.0 → 0.6.2
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/dist/index.js +348 -0
- package/dist/index.js.map +7 -0
- package/dist/index.min.js +2 -0
- package/dist/index.min.js.map +7 -0
- package/dist/panel.js +476 -0
- package/dist/panel.js.map +7 -0
- package/dist/panel.min.js +2 -0
- package/dist/panel.min.js.map +7 -0
- package/index.d.ts +22 -0
- package/package.json +19 -7
- package/panel.d.ts +2 -0
- package/src/DevPanel.jsx +129 -356
- package/src/index.js +72 -9
package/src/DevPanel.jsx
CHANGED
|
@@ -1,402 +1,175 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What Framework DevPanel
|
|
3
3
|
*
|
|
4
|
-
* A
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Signal count, effect count, component count
|
|
9
|
-
* - Recent errors with structured output
|
|
10
|
-
* - Real-time signal watcher
|
|
11
|
-
* - Health indicator (green/yellow/red)
|
|
12
|
-
* - Toggle via Ctrl+Shift+D / Cmd+Shift+D
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* import { DevPanel } from 'what-devtools/panel';
|
|
16
|
-
* // Add to your app:
|
|
17
|
-
* <DevPanel />
|
|
18
|
-
*
|
|
19
|
-
* Works WITHOUT MCP devtools connected.
|
|
4
|
+
* A small floating UI panel for browser-based devtools tests and local debugging.
|
|
5
|
+
* It is intentionally implemented without JSX so the devtools package does not
|
|
6
|
+
* depend on compiler fragment behavior to render its own diagnostics UI.
|
|
20
7
|
*/
|
|
21
8
|
|
|
22
|
-
import {
|
|
9
|
+
import { onCleanup } from 'what-core';
|
|
23
10
|
import { subscribe, getSnapshot, getErrors, installDevTools } from './index.js';
|
|
24
11
|
|
|
12
|
+
const MONO = 'ui-monospace,SFMono-Regular,Menlo,monospace';
|
|
13
|
+
|
|
25
14
|
export function DevPanel() {
|
|
26
|
-
// Auto-install devtools if not already done
|
|
27
15
|
installDevTools();
|
|
28
16
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
if (typeof document === 'undefined') return null;
|
|
18
|
+
|
|
19
|
+
let activeTab = 'signals';
|
|
20
|
+
let isOpen = false;
|
|
21
|
+
|
|
22
|
+
const root = document.createDocumentFragment();
|
|
23
|
+
const toggle = document.createElement('button');
|
|
24
|
+
toggle.type = 'button';
|
|
25
|
+
toggle.textContent = 'W';
|
|
26
|
+
toggle.title = 'What Framework DevTools (Ctrl+Shift+D)';
|
|
27
|
+
toggle.setAttribute('style',
|
|
28
|
+
'position:fixed;bottom:12px;right:12px;z-index:99999;width:36px;height:36px;' +
|
|
29
|
+
'border-radius:8px;border:1px solid #2a2a4a;background:linear-gradient(135deg,#2563eb,#1d4ed8);' +
|
|
30
|
+
`color:#fff;font-weight:800;font-size:14px;cursor:pointer;font-family:${MONO};` +
|
|
31
|
+
'box-shadow:0 4px 12px rgba(37,99,235,0.3);'
|
|
32
|
+
);
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
const panel = document.createElement('div');
|
|
35
|
+
panel.setAttribute('style',
|
|
36
|
+
'position:fixed;bottom:0;right:0;width:380px;max-height:55vh;z-index:99998;' +
|
|
37
|
+
`font-family:${MONO};font-size:12px;background:#1a1a2e;color:#e0e0e0;` +
|
|
38
|
+
'border:1px solid #2a2a4a;border-radius:12px 0 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.3);' +
|
|
39
|
+
'display:none;flex-direction:column;overflow:hidden;'
|
|
40
|
+
);
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
root.append(toggle, panel);
|
|
43
|
+
|
|
44
|
+
function setOpen(next) {
|
|
45
|
+
isOpen = next;
|
|
46
|
+
panel.style.display = isOpen ? 'flex' : 'none';
|
|
47
|
+
if (isOpen) renderPanel();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
toggle.addEventListener('click', () => setOpen(!isOpen));
|
|
47
51
|
|
|
48
|
-
// Keyboard shortcut: Ctrl+Shift+D / Cmd+Shift+D
|
|
49
52
|
const onKeyDown = (e) => {
|
|
50
53
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'D') {
|
|
51
54
|
e.preventDefault();
|
|
52
|
-
|
|
55
|
+
setOpen(!isOpen);
|
|
53
56
|
}
|
|
54
57
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
document.addEventListener('keydown', onKeyDown);
|
|
59
|
+
|
|
60
|
+
const unsub = subscribe(() => {
|
|
61
|
+
if (isOpen) renderPanel();
|
|
62
|
+
});
|
|
63
|
+
const interval = setInterval(() => {
|
|
64
|
+
if (isOpen) renderPanel();
|
|
65
|
+
}, 500);
|
|
58
66
|
|
|
59
67
|
onCleanup(() => {
|
|
60
68
|
unsub();
|
|
61
69
|
clearInterval(interval);
|
|
62
|
-
|
|
63
|
-
document.removeEventListener('keydown', onKeyDown);
|
|
64
|
-
}
|
|
70
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
65
71
|
});
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const errs = recentErrors();
|
|
71
|
-
const recentErrCount = errs.filter(
|
|
72
|
-
(e) => Date.now() - e.timestamp < 30000
|
|
73
|
-
).length;
|
|
74
|
-
|
|
75
|
-
if (recentErrCount > 0) return { color: '#ef4444', label: 'Errors detected' };
|
|
76
|
-
if (data.effects.length > 100) return { color: '#eab308', label: 'Many effects' };
|
|
77
|
-
if (data.signals.length > 200) return { color: '#eab308', label: 'Many signals' };
|
|
78
|
-
return { color: '#22c55e', label: 'Healthy' };
|
|
79
|
-
};
|
|
73
|
+
function renderPanel() {
|
|
74
|
+
panel.replaceChildren(renderHeader(), renderTabs(), renderContent());
|
|
75
|
+
}
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'background:#1a1a2e;color:#e0e0e0;border:1px solid #2a2a4a;' +
|
|
85
|
-
'border-radius:12px 0 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.3);' +
|
|
86
|
-
'display:flex;flex-direction:column;overflow:hidden;';
|
|
77
|
+
function renderHeader() {
|
|
78
|
+
const header = document.createElement('div');
|
|
79
|
+
header.setAttribute('style', 'display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #2a2a4a;background:#16163a;');
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
'padding:6px 10px;border:none;background:' +
|
|
92
|
-
(isActive ? '#2a2a4a' : 'transparent') +
|
|
93
|
-
';color:' +
|
|
94
|
-
(isActive ? '#fff' : '#6a6a8a') +
|
|
95
|
-
';cursor:pointer;font-family:inherit;font-size:11px;font-weight:600;border-radius:4px;'
|
|
96
|
-
);
|
|
97
|
-
};
|
|
81
|
+
const title = document.createElement('span');
|
|
82
|
+
title.textContent = 'What DevTools';
|
|
83
|
+
title.setAttribute('style', 'font-weight:700;font-size:12px;color:#818cf8;');
|
|
98
84
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const recentErrs = errs.slice(-3).reverse();
|
|
85
|
+
const close = document.createElement('button');
|
|
86
|
+
close.type = 'button';
|
|
87
|
+
close.textContent = 'x';
|
|
88
|
+
close.setAttribute('style', 'background:none;border:none;color:#6a6a8a;cursor:pointer;font-size:14px;');
|
|
89
|
+
close.addEventListener('click', () => setOpen(false));
|
|
105
90
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;padding:8px;background:#0d0d1a;border-radius:6px;">
|
|
110
|
-
<div
|
|
111
|
-
style={() =>
|
|
112
|
-
'width:10px;height:10px;border-radius:50%;background:' +
|
|
113
|
-
getHealth().color +
|
|
114
|
-
';'
|
|
115
|
-
}
|
|
116
|
-
/>
|
|
117
|
-
<span style={() => 'color:' + getHealth().color + ';font-weight:600;'}>
|
|
118
|
-
{() => getHealth().label}
|
|
119
|
-
</span>
|
|
120
|
-
</div>
|
|
91
|
+
header.append(title, close);
|
|
92
|
+
return header;
|
|
93
|
+
}
|
|
121
94
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
<div style="text-align:center;padding:8px;background:#0d0d1a;border-radius:6px;">
|
|
141
|
-
<div style="font-size:18px;font-weight:700;color:#34d399;">
|
|
142
|
-
{() => snapshot().components.length}
|
|
143
|
-
</div>
|
|
144
|
-
<div style="font-size:10px;color:#6a6a8a;margin-top:2px;">
|
|
145
|
-
Components
|
|
146
|
-
</div>
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
95
|
+
function renderTabs() {
|
|
96
|
+
const tabs = document.createElement('div');
|
|
97
|
+
tabs.setAttribute('style', 'display:flex;gap:2px;padding:6px 8px;border-bottom:1px solid #2a2a4a;flex-wrap:wrap;');
|
|
98
|
+
for (const tab of ['signals', 'effects', 'components', 'errors']) {
|
|
99
|
+
const button = document.createElement('button');
|
|
100
|
+
button.type = 'button';
|
|
101
|
+
button.textContent = tabLabel(tab);
|
|
102
|
+
button.setAttribute('style', tabStyle(tab));
|
|
103
|
+
button.addEventListener('click', () => {
|
|
104
|
+
activeTab = tab;
|
|
105
|
+
renderPanel();
|
|
106
|
+
});
|
|
107
|
+
tabs.append(button);
|
|
108
|
+
}
|
|
109
|
+
return tabs;
|
|
110
|
+
}
|
|
149
111
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
<div style="font-size:11px;font-weight:600;color:#f87171;margin-bottom:6px;">
|
|
158
|
-
Recent Errors ({errs.length})
|
|
159
|
-
</div>
|
|
160
|
-
{recent.map((e, i) => (
|
|
161
|
-
<div
|
|
162
|
-
key={i}
|
|
163
|
-
style="padding:6px 8px;background:#1c0a0a;border:1px solid #3b1219;border-radius:4px;margin-bottom:4px;font-size:11px;color:#fca5a5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"
|
|
164
|
-
title={e.message}
|
|
165
|
-
>
|
|
166
|
-
<span style="color:#6a6a8a;">
|
|
167
|
-
[{e.type}]
|
|
168
|
-
</span>{' '}
|
|
169
|
-
{e.message}
|
|
170
|
-
</div>
|
|
171
|
-
))}
|
|
172
|
-
</div>
|
|
173
|
-
);
|
|
174
|
-
}}
|
|
175
|
-
</div>
|
|
176
|
-
);
|
|
177
|
-
};
|
|
112
|
+
function tabLabel(tab) {
|
|
113
|
+
const snapshot = getSnapshot();
|
|
114
|
+
if (tab === 'signals') return `Signals (${snapshot.signals.length})`;
|
|
115
|
+
if (tab === 'effects') return `Effects (${snapshot.effects.length})`;
|
|
116
|
+
if (tab === 'components') return `Components (${snapshot.components.length})`;
|
|
117
|
+
return `Errors (${getErrors().length})`;
|
|
118
|
+
}
|
|
178
119
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
return (
|
|
188
|
-
<div style="padding:8px;">
|
|
189
|
-
{data.signals.map((s) => (
|
|
190
|
-
<div
|
|
191
|
-
key={s.id}
|
|
192
|
-
style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-bottom:1px solid #2a2a4a;"
|
|
193
|
-
>
|
|
194
|
-
<span style="color:#818cf8;">{s.name}</span>
|
|
195
|
-
<span style="color:#a0a0c0;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
|
196
|
-
{formatValue(s.value)}
|
|
197
|
-
</span>
|
|
198
|
-
</div>
|
|
199
|
-
))}
|
|
200
|
-
</div>
|
|
201
|
-
);
|
|
202
|
-
};
|
|
120
|
+
function tabStyle(tab) {
|
|
121
|
+
const selected = activeTab === tab;
|
|
122
|
+
return 'padding:6px 10px;border:none;background:' + (selected ? '#2a2a4a' : 'transparent') +
|
|
123
|
+
';color:' + (selected ? '#fff' : '#6a6a8a') +
|
|
124
|
+
`;cursor:pointer;font-family:${MONO};font-size:11px;font-weight:600;border-radius:4px;`;
|
|
125
|
+
}
|
|
203
126
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
);
|
|
127
|
+
function renderContent() {
|
|
128
|
+
const content = document.createElement('div');
|
|
129
|
+
content.setAttribute('style', 'overflow-y:auto;flex:1;padding:8px;');
|
|
130
|
+
const snapshot = getSnapshot();
|
|
131
|
+
|
|
132
|
+
if (activeTab === 'signals') {
|
|
133
|
+
renderRows(content, snapshot.signals, (signal) => [signal.name, formatValue(signal.value)], '#818cf8');
|
|
134
|
+
} else if (activeTab === 'effects') {
|
|
135
|
+
renderRows(content, snapshot.effects, (effect) => [effect.name, `runs: ${effect.runCount || 0}`], '#fbbf24');
|
|
136
|
+
} else if (activeTab === 'components') {
|
|
137
|
+
renderRows(content, snapshot.components, (component) => [`<${component.name} />`, ''], '#34d399');
|
|
138
|
+
} else {
|
|
139
|
+
renderRows(content, getErrors(), (error) => [`[${error.type}]`, error.message], '#f87171');
|
|
211
140
|
}
|
|
212
|
-
return (
|
|
213
|
-
<div style="padding:8px;">
|
|
214
|
-
{data.effects.map((e) => (
|
|
215
|
-
<div
|
|
216
|
-
key={e.id}
|
|
217
|
-
style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-bottom:1px solid #2a2a4a;"
|
|
218
|
-
>
|
|
219
|
-
<span style="color:#fbbf24;">{e.name}</span>
|
|
220
|
-
<span style="color:#6a6a8a;font-size:10px;">
|
|
221
|
-
runs: {e.runCount || 0}
|
|
222
|
-
</span>
|
|
223
|
-
</div>
|
|
224
|
-
))}
|
|
225
|
-
</div>
|
|
226
|
-
);
|
|
227
|
-
};
|
|
228
141
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return (
|
|
234
|
-
<div style="padding:12px;color:#4a4a6a;">No components tracked</div>
|
|
235
|
-
);
|
|
142
|
+
if (!content.childNodes.length) {
|
|
143
|
+
content.textContent = `No ${activeTab} tracked`;
|
|
144
|
+
content.style.color = '#4a4a6a';
|
|
145
|
+
content.style.padding = '12px';
|
|
236
146
|
}
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
{data.components.map((c) => (
|
|
240
|
-
<div
|
|
241
|
-
key={c.id}
|
|
242
|
-
style="padding:4px 8px;border-bottom:1px solid #2a2a4a;"
|
|
243
|
-
>
|
|
244
|
-
<span style="color:#34d399;">
|
|
245
|
-
<{c.name} />
|
|
246
|
-
</span>
|
|
247
|
-
</div>
|
|
248
|
-
))}
|
|
249
|
-
</div>
|
|
250
|
-
);
|
|
251
|
-
};
|
|
147
|
+
return content;
|
|
148
|
+
}
|
|
252
149
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
150
|
+
function renderRows(parent, rows, mapRow, color) {
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
const [leftText, rightText] = mapRow(row);
|
|
153
|
+
const item = document.createElement('div');
|
|
154
|
+
item.setAttribute('style', 'display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-bottom:1px solid #2a2a4a;gap:12px;');
|
|
155
|
+
const left = document.createElement('span');
|
|
156
|
+
left.textContent = leftText;
|
|
157
|
+
left.setAttribute('style', `color:${color};`);
|
|
158
|
+
const right = document.createElement('span');
|
|
159
|
+
right.textContent = rightText;
|
|
160
|
+
right.setAttribute('style', 'color:#a0a0c0;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;');
|
|
161
|
+
item.append(left, right);
|
|
162
|
+
parent.append(item);
|
|
260
163
|
}
|
|
261
|
-
|
|
262
|
-
<div style="padding:8px;">
|
|
263
|
-
{errs
|
|
264
|
-
.slice()
|
|
265
|
-
.reverse()
|
|
266
|
-
.map((e, i) => (
|
|
267
|
-
<div
|
|
268
|
-
key={i}
|
|
269
|
-
style="padding:8px;background:#0d0d1a;border:1px solid #2a2a4a;border-radius:6px;margin-bottom:6px;"
|
|
270
|
-
>
|
|
271
|
-
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
|
|
272
|
-
<span style="color:#f87171;font-weight:600;font-size:11px;">
|
|
273
|
-
[{e.type}]
|
|
274
|
-
</span>
|
|
275
|
-
<span style="color:#4a4a6a;font-size:10px;">
|
|
276
|
-
{new Date(e.timestamp).toLocaleTimeString()}
|
|
277
|
-
</span>
|
|
278
|
-
</div>
|
|
279
|
-
<div style="color:#e0e0e0;font-size:11px;white-space:pre-wrap;word-break:break-all;">
|
|
280
|
-
{e.message}
|
|
281
|
-
</div>
|
|
282
|
-
{e.stack ? (
|
|
283
|
-
<div style="color:#4a4a6a;font-size:10px;margin-top:4px;white-space:pre-wrap;max-height:60px;overflow:hidden;">
|
|
284
|
-
{e.stack.split('\n').slice(0, 3).join('\n')}
|
|
285
|
-
</div>
|
|
286
|
-
) : null}
|
|
287
|
-
</div>
|
|
288
|
-
))}
|
|
289
|
-
</div>
|
|
290
|
-
);
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
return (
|
|
294
|
-
<>
|
|
295
|
-
{/* Toggle button with health indicator */}
|
|
296
|
-
<button
|
|
297
|
-
onclick={() => isOpen((v) => !v)}
|
|
298
|
-
style={() => {
|
|
299
|
-
const health = getHealth();
|
|
300
|
-
return (
|
|
301
|
-
'position:fixed;bottom:12px;right:12px;z-index:99999;width:36px;height:36px;' +
|
|
302
|
-
'border-radius:8px;border:1px solid #2a2a4a;' +
|
|
303
|
-
'background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;' +
|
|
304
|
-
'font-weight:800;font-size:14px;cursor:pointer;' +
|
|
305
|
-
'font-family:ui-monospace,monospace;' +
|
|
306
|
-
'box-shadow:0 4px 12px rgba(37,99,235,0.3),' +
|
|
307
|
-
'0 0 0 2px ' + health.color + ';'
|
|
308
|
-
);
|
|
309
|
-
}}
|
|
310
|
-
title="What Framework DevTools (Ctrl+Shift+D)"
|
|
311
|
-
>
|
|
312
|
-
W
|
|
313
|
-
</button>
|
|
314
|
-
|
|
315
|
-
{/* Panel -- conditionally rendered */}
|
|
316
|
-
{() =>
|
|
317
|
-
isOpen() ? (
|
|
318
|
-
<div style={PANEL_STYLE}>
|
|
319
|
-
{/* Header */}
|
|
320
|
-
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #2a2a4a;background:#16163a;">
|
|
321
|
-
<div style="display:flex;align-items:center;gap:8px;">
|
|
322
|
-
<span style="font-weight:700;font-size:12px;color:#818cf8;">
|
|
323
|
-
What DevTools
|
|
324
|
-
</span>
|
|
325
|
-
<div
|
|
326
|
-
style={() =>
|
|
327
|
-
'width:8px;height:8px;border-radius:50%;background:' +
|
|
328
|
-
getHealth().color +
|
|
329
|
-
';'
|
|
330
|
-
}
|
|
331
|
-
title={() => getHealth().label}
|
|
332
|
-
/>
|
|
333
|
-
</div>
|
|
334
|
-
<button
|
|
335
|
-
onclick={() => isOpen(false)}
|
|
336
|
-
style="background:none;border:none;color:#6a6a8a;cursor:pointer;font-size:14px;"
|
|
337
|
-
>
|
|
338
|
-
x
|
|
339
|
-
</button>
|
|
340
|
-
</div>
|
|
341
|
-
|
|
342
|
-
{/* Tabs */}
|
|
343
|
-
<div style="display:flex;gap:2px;padding:6px 8px;border-bottom:1px solid #2a2a4a;flex-wrap:wrap;">
|
|
344
|
-
<button
|
|
345
|
-
style={tabStyle('overview')}
|
|
346
|
-
onclick={() => activeTab('overview')}
|
|
347
|
-
>
|
|
348
|
-
Overview
|
|
349
|
-
</button>
|
|
350
|
-
<button
|
|
351
|
-
style={tabStyle('signals')}
|
|
352
|
-
onclick={() => activeTab('signals')}
|
|
353
|
-
>
|
|
354
|
-
Signals ({() => snapshot().signals.length})
|
|
355
|
-
</button>
|
|
356
|
-
<button
|
|
357
|
-
style={tabStyle('effects')}
|
|
358
|
-
onclick={() => activeTab('effects')}
|
|
359
|
-
>
|
|
360
|
-
Effects ({() => snapshot().effects.length})
|
|
361
|
-
</button>
|
|
362
|
-
<button
|
|
363
|
-
style={tabStyle('components')}
|
|
364
|
-
onclick={() => activeTab('components')}
|
|
365
|
-
>
|
|
366
|
-
Components ({() => snapshot().components.length})
|
|
367
|
-
</button>
|
|
368
|
-
<button
|
|
369
|
-
style={tabStyle('errors')}
|
|
370
|
-
onclick={() => activeTab('errors')}
|
|
371
|
-
>
|
|
372
|
-
Errors ({() => recentErrors().length})
|
|
373
|
-
</button>
|
|
374
|
-
</div>
|
|
164
|
+
}
|
|
375
165
|
|
|
376
|
-
|
|
377
|
-
<div style="overflow-y:auto;flex:1;">
|
|
378
|
-
{() => {
|
|
379
|
-
const tab = activeTab();
|
|
380
|
-
if (tab === 'overview') return renderOverview();
|
|
381
|
-
if (tab === 'signals') return renderSignals();
|
|
382
|
-
if (tab === 'effects') return renderEffects();
|
|
383
|
-
if (tab === 'components') return renderComponents();
|
|
384
|
-
if (tab === 'errors') return renderErrors();
|
|
385
|
-
return renderOverview();
|
|
386
|
-
}}
|
|
387
|
-
</div>
|
|
388
|
-
</div>
|
|
389
|
-
) : null
|
|
390
|
-
}
|
|
391
|
-
</>
|
|
392
|
-
);
|
|
166
|
+
return root;
|
|
393
167
|
}
|
|
394
168
|
|
|
395
169
|
function formatValue(value) {
|
|
396
170
|
if (value === null) return 'null';
|
|
397
171
|
if (value === undefined) return 'undefined';
|
|
398
|
-
if (typeof value === 'string')
|
|
399
|
-
return `"${value.length > 30 ? value.slice(0, 30) + '...' : value}"`;
|
|
172
|
+
if (typeof value === 'string') return `"${value.length > 30 ? value.slice(0, 30) + '...' : value}"`;
|
|
400
173
|
if (typeof value === 'object') {
|
|
401
174
|
try {
|
|
402
175
|
const str = JSON.stringify(value);
|
package/src/index.js
CHANGED
|
@@ -31,6 +31,10 @@ const subsToSignalId = new WeakMap();
|
|
|
31
31
|
const errors = [];
|
|
32
32
|
const MAX_ERRORS = 100;
|
|
33
33
|
|
|
34
|
+
// Hydration mismatch log (capped at 50)
|
|
35
|
+
const hydrationMismatches = [];
|
|
36
|
+
const MAX_HYDRATION_MISMATCHES = 50;
|
|
37
|
+
|
|
34
38
|
// Event listeners for the DevPanel
|
|
35
39
|
const listeners = new Set();
|
|
36
40
|
|
|
@@ -226,12 +230,15 @@ export function unregisterEffect(e) {
|
|
|
226
230
|
/**
|
|
227
231
|
* Capture a runtime error.
|
|
228
232
|
*/
|
|
229
|
-
function captureError(err, context) {
|
|
233
|
+
export function captureError(err, typeOrContext, context) {
|
|
234
|
+
const resolvedContext = typeof typeOrContext === 'string'
|
|
235
|
+
? { ...(context || {}), type: typeOrContext }
|
|
236
|
+
: (typeOrContext || context || {});
|
|
230
237
|
const entry = {
|
|
231
238
|
message: err?.message || String(err),
|
|
232
239
|
stack: err?.stack || null,
|
|
233
|
-
type:
|
|
234
|
-
effectId:
|
|
240
|
+
type: resolvedContext?.type || 'unknown',
|
|
241
|
+
effectId: resolvedContext?.effect?._devId || null,
|
|
235
242
|
timestamp: Date.now(),
|
|
236
243
|
};
|
|
237
244
|
errors.push(entry);
|
|
@@ -314,6 +321,7 @@ export function getSnapshot(opts = {}) {
|
|
|
314
321
|
effects: effectList,
|
|
315
322
|
components: componentList,
|
|
316
323
|
errors: errors.slice(),
|
|
324
|
+
hydrationMismatches: hydrationMismatches.slice(),
|
|
317
325
|
};
|
|
318
326
|
}
|
|
319
327
|
|
|
@@ -328,6 +336,32 @@ export function getErrors(opts = {}) {
|
|
|
328
336
|
return errors.slice();
|
|
329
337
|
}
|
|
330
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Get captured hydration mismatches.
|
|
341
|
+
* @param {object} [opts]
|
|
342
|
+
* @param {number} [opts.since] - Only mismatches after this timestamp
|
|
343
|
+
*/
|
|
344
|
+
export function getHydrationMismatches(opts = {}) {
|
|
345
|
+
const { since } = opts;
|
|
346
|
+
if (since) return hydrationMismatches.filter(m => m.timestamp > since);
|
|
347
|
+
return hydrationMismatches.slice();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Reset devtools registries and captured logs.
|
|
352
|
+
*/
|
|
353
|
+
export function resetDevTools() {
|
|
354
|
+
signals.clear();
|
|
355
|
+
effects.clear();
|
|
356
|
+
components.clear();
|
|
357
|
+
errors.length = 0;
|
|
358
|
+
hydrationMismatches.length = 0;
|
|
359
|
+
listeners.clear();
|
|
360
|
+
signalId = 0;
|
|
361
|
+
effectId = 0;
|
|
362
|
+
componentId = 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
331
365
|
/**
|
|
332
366
|
* Install devtools. Call once at app startup.
|
|
333
367
|
* Wires into what-core's __DEV__ hooks and exposes `window.__WHAT_DEVTOOLS__`.
|
|
@@ -341,10 +375,24 @@ export function installDevTools(core) {
|
|
|
341
375
|
const hooks = {
|
|
342
376
|
onSignalCreate: (sig) => registerSignal(sig),
|
|
343
377
|
onSignalUpdate: (sig) => notifySignalUpdate(sig),
|
|
378
|
+
onSignalDispose: (sig) => unregisterSignal(sig),
|
|
344
379
|
onEffectCreate: (e) => registerEffect(e),
|
|
345
380
|
onEffectDispose: (e) => unregisterEffect(e),
|
|
346
381
|
onEffectRun: (e) => trackEffectRun(e),
|
|
347
382
|
onError: (err, context) => captureError(err, context),
|
|
383
|
+
onHydrationMismatch: (info) => {
|
|
384
|
+
const entry = {
|
|
385
|
+
type: 'hydration_mismatch',
|
|
386
|
+
component: info.component,
|
|
387
|
+
expected: info.expected,
|
|
388
|
+
actual: info.actual,
|
|
389
|
+
mismatchCount: info.mismatchCount,
|
|
390
|
+
timestamp: Date.now(),
|
|
391
|
+
};
|
|
392
|
+
hydrationMismatches.push(entry);
|
|
393
|
+
if (hydrationMismatches.length > MAX_HYDRATION_MISMATCHES) hydrationMismatches.shift();
|
|
394
|
+
emit('hydration:mismatch', entry);
|
|
395
|
+
},
|
|
348
396
|
onComponentMount: (ctx) => {
|
|
349
397
|
const name = ctx.Component?.displayName || ctx.Component?.name || 'Anonymous';
|
|
350
398
|
const parentDevId = ctx._parentCtx?._devId || null;
|
|
@@ -362,11 +410,13 @@ export function installDevTools(core) {
|
|
|
362
410
|
if (typeof window !== 'undefined') window.__WHAT_CORE__ = core;
|
|
363
411
|
} else {
|
|
364
412
|
try {
|
|
365
|
-
import('what-core').then(mod => {
|
|
413
|
+
import('what-core/devtools').then(mod => {
|
|
366
414
|
if (mod.__setDevToolsHooks) mod.__setDevToolsHooks(hooks);
|
|
367
|
-
if (typeof window !== 'undefined') window.
|
|
368
|
-
}).catch(() =>
|
|
369
|
-
} catch {
|
|
415
|
+
if (typeof window !== 'undefined') window.__WHAT_CORE_DEVTOOLS__ = mod;
|
|
416
|
+
}).catch((error) => warnDevToolsImportFailure(error));
|
|
417
|
+
} catch (error) {
|
|
418
|
+
warnDevToolsImportFailure(error);
|
|
419
|
+
}
|
|
370
420
|
}
|
|
371
421
|
|
|
372
422
|
if (typeof window !== 'undefined') {
|
|
@@ -375,13 +425,26 @@ export function installDevTools(core) {
|
|
|
375
425
|
get effects() { return getSnapshot().effects; },
|
|
376
426
|
get components() { return getSnapshot().components; },
|
|
377
427
|
get errors() { return getErrors(); },
|
|
428
|
+
get hydrationMismatches() { return getHydrationMismatches(); },
|
|
378
429
|
getSnapshot,
|
|
379
430
|
getErrors,
|
|
431
|
+
getHydrationMismatches,
|
|
380
432
|
subscribe,
|
|
381
433
|
safeSerialize,
|
|
382
|
-
|
|
434
|
+
captureError,
|
|
435
|
+
resetDevTools,
|
|
436
|
+
_registries: { signals, effects, components, errors, hydrationMismatches },
|
|
383
437
|
};
|
|
384
438
|
}
|
|
385
439
|
}
|
|
386
440
|
|
|
387
|
-
export { signals, effects, components, errors };
|
|
441
|
+
export { signals, effects, components, errors, hydrationMismatches };
|
|
442
|
+
|
|
443
|
+
function warnDevToolsImportFailure(error) {
|
|
444
|
+
const isDev = typeof process === 'undefined' || process.env?.NODE_ENV !== 'production';
|
|
445
|
+
if (!isDev || typeof console === 'undefined') return;
|
|
446
|
+
console.warn(
|
|
447
|
+
'[what-devtools] Could not import what-core/devtools. Pass installDevTools({ __setDevToolsHooks }) or verify package subpath exports.',
|
|
448
|
+
error
|
|
449
|
+
);
|
|
450
|
+
}
|