what-devtools 0.10.0 → 0.11.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-devtools",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Dev tools for What Framework — signal inspector, component tree, effect graph",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -20,7 +20,7 @@
20
20
  "inspector"
21
21
  ],
22
22
  "peerDependencies": {
23
- "what-core": "^0.10.0"
23
+ "what-core": "^0.11.1"
24
24
  },
25
25
  "author": "ZVN DEV (https://zvndev.com)",
26
26
  "license": "MIT",
package/src/DevPanel.jsx CHANGED
@@ -20,12 +20,25 @@
20
20
  */
21
21
 
22
22
  import { signal, effect, onCleanup } from 'what-core';
23
- import { subscribe, getSnapshot, getErrors, installDevTools } from './index.js';
23
+ import { subscribe, getSnapshot, getErrors, installDevTools, _suppressDevtools } from './index.js';
24
24
 
25
25
  export function DevPanel() {
26
26
  // Auto-install devtools if not already done
27
27
  installDevTools();
28
28
 
29
+ // The panel's ENTIRE body runs with devtools registration suppressed —
30
+ // the panel must not appear in its own signal/effect lists. More
31
+ // importantly: if panel-internal effects registered, every panel re-render
32
+ // would emit effect:created events, the subscribe() callback below would
33
+ // write `snapshot`, that write would re-render the panel, creating more
34
+ // effects → an unbounded feedback loop that crashes the page.
35
+ // The two bindings that (re)create DOM subtrees after mount (the isOpen
36
+ // panel toggle and the tab-content switch) carry their own _suppressDevtools
37
+ // wrappers, because their re-runs happen outside this bracket.
38
+ return _suppressDevtools(() => DevPanelBody());
39
+ }
40
+
41
+ function DevPanelBody() {
29
42
  const isOpen = signal(false);
30
43
  const activeTab = signal('overview');
31
44
  const snapshot = signal(getSnapshot());
@@ -291,7 +304,11 @@ export function DevPanel() {
291
304
  };
292
305
 
293
306
  return (
294
- <>
307
+ // display:contents wrapper instead of a fragment — the babel plugin
308
+ // currently miscompiles top-level fragments whose element children carry
309
+ // event handlers (it references _el$N bindings it never emits). The
310
+ // wrapper is layout-neutral; both children are position:fixed anyway.
311
+ <div style="display:contents">
295
312
  {/* Toggle button with health indicator */}
296
313
  <button
297
314
  onclick={() => isOpen((v) => !v)}
@@ -312,9 +329,11 @@ export function DevPanel() {
312
329
  W
313
330
  </button>
314
331
 
315
- {/* Panel -- conditionally rendered */}
332
+ {/* Panel -- conditionally rendered. _suppressDevtools: opening the
333
+ panel instantiates this whole subtree (dozens of bindings) — none
334
+ of them may register with devtools (see DevPanel docblock). */}
316
335
  {() =>
317
- isOpen() ? (
336
+ _suppressDevtools(() => isOpen() ? (
318
337
  <div style={PANEL_STYLE}>
319
338
  {/* Header */}
320
339
  <div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #2a2a4a;background:#16163a;">
@@ -373,9 +392,10 @@ export function DevPanel() {
373
392
  </button>
374
393
  </div>
375
394
 
376
- {/* Content */}
395
+ {/* Content. _suppressDevtools: tab switches and snapshot
396
+ updates rebuild this subtree — keep it out of devtools. */}
377
397
  <div style="overflow-y:auto;flex:1;">
378
- {() => {
398
+ {() => _suppressDevtools(() => {
379
399
  const tab = activeTab();
380
400
  if (tab === 'overview') return renderOverview();
381
401
  if (tab === 'signals') return renderSignals();
@@ -383,12 +403,12 @@ export function DevPanel() {
383
403
  if (tab === 'components') return renderComponents();
384
404
  if (tab === 'errors') return renderErrors();
385
405
  return renderOverview();
386
- }}
406
+ })}
387
407
  </div>
388
408
  </div>
389
- ) : null
409
+ ) : null)
390
410
  }
391
- </>
411
+ </div>
392
412
  );
393
413
  }
394
414
 
package/src/index.js CHANGED
@@ -19,6 +19,11 @@ let signalId = 0;
19
19
  let effectId = 0;
20
20
  let componentId = 0;
21
21
 
22
+ // what-core's installSignalReadGuardrail, captured when the core module is
23
+ // wired up in installDevTools(). Dev-only: warns when a signal is coerced
24
+ // to a string/number without being called (e.g. `Total: ${count}`).
25
+ let coreSignalReadGuardrail = null;
26
+
22
27
  // Registries
23
28
  const signals = new Map(); // id → { name, ref, createdAt, internal }
24
29
  const effects = new Map(); // id → { name, createdAt, depSignalIds, runCount, lastRunAt }
@@ -84,6 +89,29 @@ function attributeToComponent() {
84
89
  return null;
85
90
  }
86
91
 
92
+ // --- Self-tracking suppression ---
93
+ // Devtools UI rendered INSIDE the inspected app (the DevPanel) must not
94
+ // register its own signals/effects: panel-internal registrations emit
95
+ // devtools events, the panel reacts by updating its snapshot signal, the
96
+ // resulting re-render creates more effects, which emit again — an unbounded
97
+ // feedback loop that wedges and crashes the page. Re-entrant (counter).
98
+ let suppressDepth = 0;
99
+
100
+ /**
101
+ * Run fn with devtools registration suppressed (exception-safe).
102
+ * Signals/effects/components created inside fn are invisible to devtools.
103
+ * Does NOT affect what-core's reactive dependency tracking.
104
+ * @internal Used by DevPanel; exported for devtools-adjacent UIs and tests.
105
+ */
106
+ export function _suppressDevtools(fn) {
107
+ suppressDepth++;
108
+ try {
109
+ return fn();
110
+ } finally {
111
+ suppressDepth--;
112
+ }
113
+ }
114
+
87
115
  // Error log (capped at 100)
88
116
  const errors = [];
89
117
  const MAX_ERRORS = 100;
@@ -184,7 +212,7 @@ export function safeSerialize(value, depth = 0, seen) {
184
212
  * Called from reactive.js __DEV__ hooks.
185
213
  */
186
214
  export function registerSignal(sig, name) {
187
- if (!installed) return;
215
+ if (!installed || suppressDepth > 0) return;
188
216
  const id = ++signalId;
189
217
  const entry = {
190
218
  id,
@@ -200,6 +228,11 @@ export function registerSignal(sig, name) {
200
228
  sig._devId = id;
201
229
  // Reverse lookup for O(1) effect dep resolution
202
230
  if (sig._subs) subsToSignalId.set(sig._subs, id);
231
+ // Dev guardrail: warn when this signal is string/number-coerced without
232
+ // being called (catches `Total: ${count}` — should be `${count()}`).
233
+ if (coreSignalReadGuardrail) {
234
+ try { coreSignalReadGuardrail(sig, entry.name); } catch {}
235
+ }
203
236
  emit('signal:created', entry);
204
237
  return id;
205
238
  }
@@ -232,7 +265,7 @@ export function unregisterSignal(sig) {
232
265
  * Register an effect with the devtools.
233
266
  */
234
267
  export function registerEffect(e, name) {
235
- if (!installed) return;
268
+ if (!installed || suppressDepth > 0) return;
236
269
  const id = ++effectId;
237
270
  const entry = {
238
271
  id,
@@ -322,7 +355,7 @@ function captureError(err, context) {
322
355
  * (see what_components / what_explain) so agents re-query before using IDs.
323
356
  */
324
357
  export function registerComponent(name, element, parentDevId) {
325
- if (!installed) return;
358
+ if (!installed || suppressDepth > 0) return;
326
359
  const id = ++componentId;
327
360
  const entry = {
328
361
  id,
@@ -457,6 +490,11 @@ export function installDevTools(core) {
457
490
  // Wire into what-core's reactive system
458
491
  function installInto(mod) {
459
492
  if (!mod) return;
493
+ // Capture the guardrail BEFORE hooks go live so the very first
494
+ // registrations (and the pre-install drain below) get wrapped too.
495
+ if (typeof mod.installSignalReadGuardrail === 'function') {
496
+ coreSignalReadGuardrail = mod.installSignalReadGuardrail;
497
+ }
460
498
  if (mod.__setDevToolsHooks) mod.__setDevToolsHooks(hooks);
461
499
  if (typeof window !== 'undefined') window.__WHAT_CORE__ = mod;
462
500