what-core 0.5.1 → 0.5.4

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/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # what-core
2
+
3
+ The reactive engine behind [What Framework](https://whatfw.com). Provides signals, fine-grained reactivity, components, hooks, and DOM rendering -- all without a virtual DOM diffing step.
4
+
5
+ Most users should install [`what-framework`](https://www.npmjs.com/package/what-framework) instead. `what-core` is the internal engine consumed by other What packages.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install what-core
11
+ ```
12
+
13
+ ## Reactive Primitives
14
+
15
+ ```js
16
+ import { signal, computed, effect, batch, untrack } from 'what-core';
17
+
18
+ const count = signal(0);
19
+
20
+ // Read
21
+ count(); // 0
22
+
23
+ // Write
24
+ count.set(5);
25
+ count.set(c => c + 1);
26
+
27
+ // Derived value
28
+ const doubled = computed(() => count() * 2);
29
+
30
+ // Side effects
31
+ effect(() => {
32
+ console.log('Count:', count());
33
+ });
34
+
35
+ // Batch updates (effects run once at the end)
36
+ batch(() => {
37
+ a.set(1);
38
+ b.set(2);
39
+ });
40
+
41
+ // Read without subscribing
42
+ untrack(() => someSignal());
43
+ count.peek();
44
+ ```
45
+
46
+ ## Hooks
47
+
48
+ React-compatible hooks backed by signals internally.
49
+
50
+ ```js
51
+ import {
52
+ useState, useEffect, useMemo, useCallback,
53
+ useRef, useReducer, useContext, createContext,
54
+ onMount, onCleanup,
55
+ } from 'what-core';
56
+
57
+ const [count, setCount] = useState(0);
58
+
59
+ useEffect(() => {
60
+ const id = setInterval(tick, 1000);
61
+ return () => clearInterval(id);
62
+ }, []);
63
+ ```
64
+
65
+ ## Components
66
+
67
+ ```js
68
+ import { h, mount, Fragment, memo, lazy, Suspense, ErrorBoundary, Show, For } from 'what-core';
69
+
70
+ function Counter() {
71
+ const count = signal(0);
72
+ return h('button', { onclick: () => count.set(c => c + 1) }, () => count());
73
+ }
74
+
75
+ mount(h(Counter), '#app');
76
+ ```
77
+
78
+ ## Additional Modules
79
+
80
+ | Export path | Contents |
81
+ |---|---|
82
+ | `what-core` | Signals, hooks, components, store, forms, data fetching, animation, a11y, skeleton loaders |
83
+ | `what-core/render` | Fine-grained rendering primitives (`template`, `insert`, `spread`, `delegateEvents`) |
84
+ | `what-core/jsx-runtime` | JSX automatic runtime |
85
+ | `what-core/testing` | Test utilities |
86
+
87
+ ## API Overview
88
+
89
+ **Reactivity** -- `signal`, `computed`, `effect`, `batch`, `untrack`, `flushSync`, `createRoot`
90
+
91
+ **Rendering** -- `h`, `Fragment`, `html`, `mount`, `template`, `insert`, `spread`, `delegateEvents`
92
+
93
+ **Hooks** -- `useState`, `useSignal`, `useComputed`, `useEffect`, `useMemo`, `useCallback`, `useRef`, `useContext`, `useReducer`, `createContext`, `onMount`, `onCleanup`, `createResource`
94
+
95
+ **Components** -- `memo`, `lazy`, `Suspense`, `ErrorBoundary`, `Show`, `For`, `Switch`, `Match`, `Island`, `Portal`
96
+
97
+ **Store** -- `createStore`, `derived`, `atom`
98
+
99
+ **Data Fetching** -- `useSWR`, `useQuery`, `useInfiniteQuery`, `invalidateQueries`, `prefetchQuery`
100
+
101
+ **Forms** -- `useForm`, `useField`, `rules`, `zodResolver`, `Input`, `Select`, `Checkbox`, `ErrorMessage`
102
+
103
+ **Animation** -- `spring`, `tween`, `easings`, `useGesture`, `useTransition`
104
+
105
+ **Accessibility** -- `useFocusTrap`, `FocusTrap`, `announce`, `SkipLink`, `useRovingTabIndex`, `VisuallyHidden`, `useId`
106
+
107
+ **Scheduler** -- `scheduleRead`, `scheduleWrite`, `measure`, `mutate`, `onResize`, `onIntersect`
108
+
109
+ **Head** -- `Head`, `clearHead`
110
+
111
+ ## Links
112
+
113
+ - [Documentation](https://whatfw.com)
114
+ - [GitHub](https://github.com/zvndev/what-fw)
115
+ - [Benchmarks](https://benchmarks.whatfw.com)
116
+
117
+ ## License
118
+
119
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-core",
3
- "version": "0.5.1",
3
+ "version": "0.5.4",
4
4
  "description": "What Framework - The closest framework to vanilla JS",
5
5
  "type": "module",
6
6
  "main": "dist/what.js",
@@ -48,10 +48,10 @@
48
48
  "license": "MIT",
49
49
  "repository": {
50
50
  "type": "git",
51
- "url": "https://github.com/aspect/what-fw"
51
+ "url": "https://github.com/zvndev/what-fw"
52
52
  },
53
53
  "bugs": {
54
- "url": "https://github.com/aspect/what-fw/issues"
54
+ "url": "https://github.com/zvndev/what-fw/issues"
55
55
  },
56
- "homepage": "https://github.com/aspect/what-fw#readme"
56
+ "homepage": "https://whatfw.com"
57
57
  }
package/src/components.js CHANGED
@@ -50,7 +50,7 @@ export function memo(Component, areEqual) {
50
50
  let _getCurrentComponent = null;
51
51
  export function _injectGetCurrentComponent(fn) { _getCurrentComponent = fn; }
52
52
 
53
- function shallowEqual(a, b) {
53
+ export function shallowEqual(a, b) {
54
54
  if (a === b) return true;
55
55
  const keysA = Object.keys(a);
56
56
  const keysB = Object.keys(b);
package/src/data.js CHANGED
@@ -636,3 +636,20 @@ export function clearCache() {
636
636
  lastFetchTimestamps.clear();
637
637
  inFlightRequests.clear();
638
638
  }
639
+
640
+ /**
641
+ * Get a snapshot of all cache entries for devtools.
642
+ * @internal
643
+ */
644
+ export function __getCacheSnapshot() {
645
+ const entries = [];
646
+ for (const [key, sig] of cacheSignals) {
647
+ entries.push({
648
+ key,
649
+ data: sig.peek(),
650
+ error: errorSignals.has(key) ? errorSignals.get(key).peek() : null,
651
+ isValidating: validatingSignals.has(key) ? validatingSignals.get(key).peek() : false,
652
+ });
653
+ }
654
+ return entries;
655
+ }
package/src/dom.js CHANGED
@@ -3,8 +3,8 @@
3
3
  // Components use <what-c> wrapper elements (display:contents) for clean reconciliation.
4
4
  // No virtual DOM tree kept in memory — we diff against the live DOM.
5
5
 
6
- import { effect, batch, untrack, signal } from './reactive.js';
7
- import { reportError, _injectGetCurrentComponent } from './components.js';
6
+ import { effect, batch, untrack, signal, __DEV__, __devtools } from './reactive.js';
7
+ import { reportError, _injectGetCurrentComponent, shallowEqual } from './components.js';
8
8
  import { _setComponentRef } from './helpers.js';
9
9
 
10
10
  // Register <what-c> custom element to prevent flash of unstyled content
@@ -14,6 +14,36 @@ if (typeof customElements !== 'undefined' && !customElements.get('what-c')) {
14
14
  connectedCallback() {
15
15
  this.style.display = 'contents';
16
16
  }
17
+ // display:contents elements don't generate a layout box — getBoundingClientRect()
18
+ // returns zeros, offsetWidth/Height return 0. React libraries (react-draggable,
19
+ // react-colorful, etc.) traverse parentNode and call getBoundingClientRect() on
20
+ // what they expect to be a layout container. Since <what-c> is layout-invisible,
21
+ // delegate to the nearest ancestor that has a real box.
22
+ _layoutParent() {
23
+ let el = this.parentElement;
24
+ while (el && el.tagName === 'WHAT-C') el = el.parentElement;
25
+ return el;
26
+ }
27
+ getBoundingClientRect() {
28
+ const p = this._layoutParent();
29
+ return p ? p.getBoundingClientRect() : super.getBoundingClientRect();
30
+ }
31
+ get offsetWidth() {
32
+ const p = this._layoutParent();
33
+ return p ? p.offsetWidth : 0;
34
+ }
35
+ get offsetHeight() {
36
+ const p = this._layoutParent();
37
+ return p ? p.offsetHeight : 0;
38
+ }
39
+ get clientWidth() {
40
+ const p = this._layoutParent();
41
+ return p ? p.clientWidth : 0;
42
+ }
43
+ get clientHeight() {
44
+ const p = this._layoutParent();
45
+ return p ? p.clientHeight : 0;
46
+ }
17
47
  });
18
48
  }
19
49
 
@@ -67,6 +97,7 @@ function disposeComponent(ctx) {
67
97
  try { dispose(); } catch (e) { /* effect already disposed */ }
68
98
  }
69
99
 
100
+ if (__DEV__ && __devtools?.onComponentUnmount) __devtools.onComponentUnmount(ctx);
70
101
  mountedComponents.delete(ctx);
71
102
  }
72
103
 
@@ -160,6 +191,11 @@ export function createDOM(vnode, parent, isSvg) {
160
191
  return document.createTextNode(String(vnode));
161
192
  }
162
193
 
194
+ // Portal (string-tagged vnodes from helpers.js Portal or react-compat createPortal)
195
+ if (vnode.tag === '__portal') {
196
+ return createPortalDOM(vnode, parent);
197
+ }
198
+
163
199
  // Component
164
200
  if (typeof vnode.tag === 'function') {
165
201
  return createComponent(vnode, parent, isSvg);
@@ -208,7 +244,20 @@ export function getComponentStack() {
208
244
  }
209
245
 
210
246
  function createComponent(vnode, parent, isSvg) {
211
- const { tag: Component, props, children } = vnode;
247
+ let { tag: Component, props, children } = vnode;
248
+
249
+ // Class component detection — ES6 classes can't be called without `new`.
250
+ // React compat layer wraps class components in createElement, but some
251
+ // library-internal components may bypass that path. Detect and wrap here.
252
+ if (typeof Component === 'function' &&
253
+ (Component.prototype?.isReactComponent || Component.prototype?.render)) {
254
+ const ClassComp = Component;
255
+ Component = function ClassComponentBridge(props) {
256
+ const instance = new ClassComp(props);
257
+ return instance.render();
258
+ };
259
+ Component.displayName = ClassComp.displayName || ClassComp.name || 'ClassComponent';
260
+ }
212
261
 
213
262
  // Handle special boundary components
214
263
  if (Component === '__errorBoundary' || vnode.tag === '__errorBoundary') {
@@ -217,8 +266,8 @@ function createComponent(vnode, parent, isSvg) {
217
266
  if (Component === '__suspense' || vnode.tag === '__suspense') {
218
267
  return createSuspenseBoundary(vnode, parent);
219
268
  }
220
- if (Component === '__portal' || vnode.tag === '__portal') {
221
- return createPortal(vnode, parent);
269
+ if (Component === '__portal' || vnode.tag === '__portal') { // Now also handled in createDOM directly
270
+ return createPortalDOM(vnode, parent);
222
271
  }
223
272
 
224
273
  // Component context for hooks
@@ -256,9 +305,12 @@ function createComponent(vnode, parent, isSvg) {
256
305
 
257
306
  // Track for disposal
258
307
  mountedComponents.add(ctx);
308
+ if (__DEV__ && __devtools?.onComponentMount) __devtools.onComponentMount(ctx);
259
309
 
260
310
  // Props signal for reactive updates from parent
261
- const propsSignal = signal({ ...props, children });
311
+ // Match React's children semantics: 0→undefined, 1→single child, N→array
312
+ const propsChildren = children.length === 0 ? undefined : children.length === 1 ? children[0] : children;
313
+ const propsSignal = signal({ ...props, children: propsChildren });
262
314
  ctx._propsSignal = propsSignal;
263
315
 
264
316
  // Reactive render: re-renders when signals used inside change
@@ -280,7 +332,9 @@ function createComponent(vnode, parent, isSvg) {
280
332
  return;
281
333
  }
282
334
 
283
- componentStack.pop();
335
+ // Keep ctx on componentStack while creating/reconciling children
336
+ // so child components' _parentCtx correctly points to this component.
337
+ // This is essential for context propagation (useContext walks _parentCtx).
284
338
 
285
339
  const vnodes = Array.isArray(result) ? result : [result];
286
340
 
@@ -306,6 +360,8 @@ function createComponent(vnode, parent, isSvg) {
306
360
  // Update: reconcile children inside wrapper
307
361
  reconcileChildren(wrapper, vnodes);
308
362
  }
363
+
364
+ componentStack.pop();
309
365
  });
310
366
 
311
367
  ctx.effects.push(dispose);
@@ -343,7 +399,6 @@ function createErrorBoundary(vnode, parent) {
343
399
  vnodes = children;
344
400
  }
345
401
 
346
- componentStack.pop();
347
402
  vnodes = Array.isArray(vnodes) ? vnodes : [vnodes];
348
403
 
349
404
  if (wrapper.childNodes.length === 0) {
@@ -354,6 +409,8 @@ function createErrorBoundary(vnode, parent) {
354
409
  } else {
355
410
  reconcileChildren(wrapper, vnodes);
356
411
  }
412
+
413
+ componentStack.pop();
357
414
  });
358
415
 
359
416
  boundaryCtx.effects.push(dispose);
@@ -381,6 +438,8 @@ function createSuspenseBoundary(vnode, parent) {
381
438
  const vnodes = isLoading ? [fallback] : children;
382
439
  const normalized = Array.isArray(vnodes) ? vnodes : [vnodes];
383
440
 
441
+ componentStack.push(boundaryCtx);
442
+
384
443
  if (wrapper.childNodes.length === 0) {
385
444
  for (const v of normalized) {
386
445
  const node = createDOM(v, wrapper);
@@ -389,6 +448,8 @@ function createSuspenseBoundary(vnode, parent) {
389
448
  } else {
390
449
  reconcileChildren(wrapper, normalized);
391
450
  }
451
+
452
+ componentStack.pop();
392
453
  });
393
454
 
394
455
  boundaryCtx.effects.push(dispose);
@@ -396,7 +457,7 @@ function createSuspenseBoundary(vnode, parent) {
396
457
  }
397
458
 
398
459
  // Portal component handler — renders children into a different DOM container
399
- function createPortal(vnode, parent) {
460
+ function createPortalDOM(vnode, parent) {
400
461
  const { container } = vnode.props;
401
462
  const children = vnode.children;
402
463
 
@@ -494,9 +555,33 @@ function reconcileUnkeyed(parent, oldNodes, newVNodes, beforeMarker) {
494
555
 
495
556
  // Keyed reconciliation with LIS algorithm for O(n log n) minimal moves
496
557
  function reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker) {
558
+ const newLen = newVNodes.length;
559
+ const oldLen = oldNodes.length;
560
+
561
+ // --- Fast path: same-position keys (covers "update N items in-place") ---
562
+ // If same length and all keys match at the same index, skip LIS entirely.
563
+ // Just patch each node in-place — O(n) with zero DOM moves.
564
+ if (newLen === oldLen && newLen > 0) {
565
+ let allMatch = true;
566
+ for (let i = 0; i < newLen; i++) {
567
+ const newKey = newVNodes[i]?.key;
568
+ const oldKey = oldNodes[i]?._vnode?.key;
569
+ if (newKey == null || newKey !== oldKey) {
570
+ allMatch = false;
571
+ break;
572
+ }
573
+ }
574
+ if (allMatch) {
575
+ for (let i = 0; i < newLen; i++) {
576
+ patchNode(parent, oldNodes[i], newVNodes[i]);
577
+ }
578
+ return;
579
+ }
580
+ }
581
+
497
582
  // Build old key -> { node, index } map
498
583
  const oldKeyMap = new Map();
499
- for (let i = 0; i < oldNodes.length; i++) {
584
+ for (let i = 0; i < oldLen; i++) {
500
585
  const node = oldNodes[i];
501
586
  const key = node._vnode?.key;
502
587
  if (key != null) {
@@ -505,7 +590,6 @@ function reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker) {
505
590
  }
506
591
 
507
592
  const newNodes = [];
508
- const newLen = newVNodes.length;
509
593
 
510
594
  // First pass: match keys and find reusable nodes
511
595
  const sources = new Array(newLen).fill(-1); // Maps new index to old index
@@ -522,7 +606,7 @@ function reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker) {
522
606
  }
523
607
 
524
608
  // Remove nodes that aren't reused
525
- for (let i = 0; i < oldNodes.length; i++) {
609
+ for (let i = 0; i < oldLen; i++) {
526
610
  if (!reused.has(i) && oldNodes[i]?.parentNode) {
527
611
  disposeTree(oldNodes[i]);
528
612
  oldNodes[i].parentNode.removeChild(oldNodes[i]);
@@ -794,7 +878,14 @@ function patchNode(parent, domNode, vnode) {
794
878
  if (domNode._componentCtx && !domNode._componentCtx.disposed
795
879
  && domNode._componentCtx.Component === vnode.tag) {
796
880
  // Same component — update props reactively, let its effect re-render
797
- domNode._componentCtx._propsSignal.set({ ...vnode.props, children: vnode.children });
881
+ const ch = vnode.children;
882
+ const patchChildren = ch.length === 0 ? undefined : ch.length === 1 ? ch[0] : ch;
883
+ const nextProps = { ...vnode.props, children: patchChildren };
884
+ // Skip signal update if props haven't changed (shallow compare)
885
+ const prevProps = domNode._componentCtx._propsSignal.peek();
886
+ if (!shallowEqual(prevProps, nextProps)) {
887
+ domNode._componentCtx._propsSignal.set(nextProps);
888
+ }
798
889
  domNode._vnode = vnode; // Keep vnode current for keyed reconciliation
799
890
  return domNode;
800
891
  }
@@ -905,23 +996,40 @@ function applyProps(el, newProps, oldProps, isSvg) {
905
996
  }
906
997
 
907
998
  function setProp(el, key, value, isSvg) {
908
- // Event handlers: onClick -> click
999
+ // Event handlers: onClick -> click, onFocusCapture -> focus (capture phase)
909
1000
  // Wrap in untrack so signal reads in handlers don't create subscriptions
910
1001
  if (key.startsWith('on') && key.length > 2) {
911
- const event = key.slice(2).toLowerCase();
1002
+ let eventName = key.slice(2);
1003
+ // React-style capture phase: onClickCapture → click in capture phase
1004
+ let useCapture = false;
1005
+ if (eventName.endsWith('Capture')) {
1006
+ eventName = eventName.slice(0, -7);
1007
+ useCapture = true;
1008
+ }
1009
+ const event = eventName.toLowerCase();
1010
+ // Use a combined key for storage so capture/bubble don't conflict
1011
+ const storageKey = useCapture ? event + '_capture' : event;
912
1012
  // Store handler for removal
913
- const old = el._events?.[event];
1013
+ const old = el._events?.[storageKey];
914
1014
  // Skip re-wrapping if same handler function
915
1015
  if (old && old._original === value) return;
916
- if (old) el.removeEventListener(event, old);
1016
+ if (old) el.removeEventListener(event, old, useCapture);
1017
+ // If handler is null/undefined, just remove the old one and bail
1018
+ if (value == null) return;
917
1019
  if (!el._events) el._events = {};
918
- // Wrap handler to untrack signal reads
919
- const wrappedHandler = (e) => untrack(() => value(e));
1020
+ // Wrap handler to untrack signal reads.
1021
+ // Add nativeEvent for React compat React synthetic events have
1022
+ // e.nativeEvent pointing to the actual DOM event. Libraries like
1023
+ // react-colorful, cmdk, and @floating-ui/react check this property.
1024
+ const wrappedHandler = (e) => {
1025
+ if (!e.nativeEvent) e.nativeEvent = e;
1026
+ return untrack(() => value(e));
1027
+ };
920
1028
  wrappedHandler._original = value;
921
- el._events[event] = wrappedHandler;
1029
+ el._events[storageKey] = wrappedHandler;
922
1030
  // Check for _eventOpts (once/capture/passive from compiler)
923
1031
  const eventOpts = value._eventOpts;
924
- el.addEventListener(event, wrappedHandler, eventOpts || undefined);
1032
+ el.addEventListener(event, wrappedHandler, eventOpts || useCapture || undefined);
925
1033
  return;
926
1034
  }
927
1035
 
@@ -1003,10 +1111,17 @@ function setProp(el, key, value, isSvg) {
1003
1111
 
1004
1112
  function removeProp(el, key, oldValue) {
1005
1113
  if (key.startsWith('on') && key.length > 2) {
1006
- const event = key.slice(2).toLowerCase();
1007
- if (el._events?.[event]) {
1008
- el.removeEventListener(event, el._events[event]);
1009
- delete el._events[event];
1114
+ let eventName = key.slice(2);
1115
+ let useCapture = false;
1116
+ if (eventName.endsWith('Capture')) {
1117
+ eventName = eventName.slice(0, -7);
1118
+ useCapture = true;
1119
+ }
1120
+ const event = eventName.toLowerCase();
1121
+ const storageKey = useCapture ? event + '_capture' : event;
1122
+ if (el._events?.[storageKey]) {
1123
+ el.removeEventListener(event, el._events[storageKey], useCapture);
1124
+ delete el._events[storageKey];
1010
1125
  }
1011
1126
  return;
1012
1127
  }
package/src/hooks.js CHANGED
@@ -192,6 +192,11 @@ export function createContext(defaultValue) {
192
192
  }
193
193
  return children;
194
194
  },
195
+ // React-compatible Consumer: <Context.Consumer>{value => ...}</Context.Consumer>
196
+ Consumer: ({ children }) => {
197
+ const value = useContext(context);
198
+ return typeof children === 'function' ? children(value) : children;
199
+ },
195
200
  };
196
201
  return context;
197
202
  }
package/src/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // The closest framework to vanilla JS.
3
3
 
4
4
  // Reactive primitives
5
- export { signal, computed, effect, memo as signalMemo, batch, untrack, flushSync, createRoot } from './reactive.js';
5
+ export { signal, computed, effect, memo as signalMemo, batch, untrack, flushSync, createRoot, __setDevToolsHooks } from './reactive.js';
6
6
 
7
7
  // Fine-grained rendering primitives
8
8
  export { template, insert, mapArray, spread, setProp, delegateEvents, on, classList } from './render.js';
@@ -132,6 +132,7 @@ export {
132
132
  setQueryData,
133
133
  getQueryData,
134
134
  clearCache,
135
+ __getCacheSnapshot,
135
136
  } from './data.js';
136
137
 
137
138
  // Form utilities
package/src/reactive.js CHANGED
@@ -2,7 +2,18 @@
2
2
  // Signals + Effects: fine-grained reactivity without virtual DOM overhead
3
3
 
4
4
  // Dev-mode flag — build tools can dead-code-eliminate when false
5
- export const __DEV__ = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production' || true;
5
+ export const __DEV__ = typeof process !== 'undefined'
6
+ ? process.env?.NODE_ENV !== 'production'
7
+ : true;
8
+
9
+ // DevTools hooks — set by what-devtools when installed.
10
+ // These are no-ops in production (dead-code eliminated with __DEV__).
11
+ export let __devtools = null;
12
+
13
+ /** @internal Install devtools hooks. Called by what-devtools. */
14
+ export function __setDevToolsHooks(hooks) {
15
+ if (__DEV__) __devtools = hooks;
16
+ }
6
17
 
7
18
  let currentEffect = null;
8
19
  let currentRoot = null;
@@ -13,7 +24,7 @@ let pendingEffects = [];
13
24
  // A reactive value. Reading inside an effect auto-tracks the dependency.
14
25
  // Writing triggers only the effects that depend on this signal.
15
26
 
16
- export function signal(initial) {
27
+ export function signal(initial, debugName) {
17
28
  let value = initial;
18
29
  const subs = new Set();
19
30
 
@@ -31,6 +42,7 @@ export function signal(initial) {
31
42
  const nextVal = typeof args[0] === 'function' ? args[0](value) : args[0];
32
43
  if (Object.is(value, nextVal)) return;
33
44
  value = nextVal;
45
+ if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);
34
46
  notify(subs);
35
47
  }
36
48
 
@@ -38,6 +50,7 @@ export function signal(initial) {
38
50
  const nextVal = typeof next === 'function' ? next(value) : next;
39
51
  if (Object.is(value, nextVal)) return;
40
52
  value = nextVal;
53
+ if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);
41
54
  notify(subs);
42
55
  };
43
56
 
@@ -48,6 +61,14 @@ export function signal(initial) {
48
61
  };
49
62
 
50
63
  sig._signal = true;
64
+ if (__DEV__) {
65
+ sig._subs = subs;
66
+ if (debugName) sig._debugName = debugName;
67
+ }
68
+
69
+ // Notify devtools of signal creation
70
+ if (__DEV__ && __devtools) __devtools.onSignalCreate(sig);
71
+
51
72
  return sig;
52
73
  }
53
74
 
@@ -130,7 +151,7 @@ export function batch(fn) {
130
151
  // --- Internals ---
131
152
 
132
153
  function _createEffect(fn, lazy) {
133
- return {
154
+ const e = {
134
155
  fn,
135
156
  deps: [], // array of subscriber sets (cheaper than Set for typical 1-3 deps)
136
157
  lazy: lazy || false,
@@ -139,6 +160,8 @@ function _createEffect(fn, lazy) {
139
160
  _pending: false,
140
161
  _stable: false, // stable effects skip cleanup/re-subscribe on re-run
141
162
  };
163
+ if (__DEV__ && __devtools) __devtools.onEffectCreate(e);
164
+ return e;
142
165
  }
143
166
 
144
167
  function _runEffect(e) {
@@ -158,9 +181,13 @@ function _runEffect(e) {
158
181
  try {
159
182
  const result = e.fn();
160
183
  if (typeof result === 'function') e._cleanup = result;
184
+ } catch (err) {
185
+ if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
186
+ if (__DEV__) console.warn('[what] Error in stable effect:', err);
161
187
  } finally {
162
188
  currentEffect = prev;
163
189
  }
190
+ if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);
164
191
  return;
165
192
  }
166
193
 
@@ -168,6 +195,7 @@ function _runEffect(e) {
168
195
  // Run effect cleanup from previous run
169
196
  if (e._cleanup) {
170
197
  try { e._cleanup(); } catch (err) {
198
+ if (__devtools?.onError) __devtools.onError(err, { type: 'effect-cleanup', effect: e });
171
199
  if (__DEV__) console.warn('[what] Error in effect cleanup:', err);
172
200
  }
173
201
  e._cleanup = null;
@@ -180,13 +208,18 @@ function _runEffect(e) {
180
208
  if (typeof result === 'function') {
181
209
  e._cleanup = result;
182
210
  }
211
+ } catch (err) {
212
+ if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
213
+ throw err;
183
214
  } finally {
184
215
  currentEffect = prev;
185
216
  }
217
+ if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);
186
218
  }
187
219
 
188
220
  function _disposeEffect(e) {
189
221
  e.disposed = true;
222
+ if (__DEV__ && __devtools) __devtools.onEffectDispose(e);
190
223
  cleanup(e);
191
224
  // Run cleanup on dispose
192
225
  if (e._cleanup) {
@@ -220,6 +253,7 @@ function notify(subs) {
220
253
  e._cleanup = result;
221
254
  }
222
255
  } catch (err) {
256
+ if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
223
257
  if (__DEV__) console.warn('[what] Error in stable effect:', err);
224
258
  } finally {
225
259
  currentEffect = prev;