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 +119 -0
- package/package.json +4 -4
- package/src/components.js +1 -1
- package/src/data.js +17 -0
- package/src/dom.js +140 -25
- package/src/hooks.js +5 -0
- package/src/index.js +2 -1
- package/src/reactive.js +37 -3
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.
|
|
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/
|
|
51
|
+
"url": "https://github.com/zvndev/what-fw"
|
|
52
52
|
},
|
|
53
53
|
"bugs": {
|
|
54
|
-
"url": "https://github.com/
|
|
54
|
+
"url": "https://github.com/zvndev/what-fw/issues"
|
|
55
55
|
},
|
|
56
|
-
"homepage": "https://
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 <
|
|
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 <
|
|
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
|
-
|
|
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
|
-
|
|
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?.[
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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'
|
|
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
|
-
|
|
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;
|