what-core 0.4.1 → 0.5.0

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/src/render.js CHANGED
@@ -3,6 +3,9 @@
3
3
  // No VDOM diffing — direct DOM manipulation with surgical signal-driven updates.
4
4
 
5
5
  import { effect, untrack, createRoot, signal } from './reactive.js';
6
+ import { createDOM, disposeTree } from './dom.js';
7
+
8
+ export { effect, untrack };
6
9
 
7
10
  // --- template(html) ---
8
11
  // Pre-parse HTML string into a <template> element. Returns a factory function
@@ -22,72 +25,124 @@ export function template(html) {
22
25
  // - array → insert each element
23
26
 
24
27
  export function insert(parent, child, marker) {
25
- if (child == null || typeof child === 'boolean') return;
28
+ if (typeof child === 'function') {
29
+ let current = null;
30
+ effect(() => {
31
+ current = reconcileInsert(parent, child(), current, marker || null);
32
+ });
33
+ return current;
34
+ }
35
+
36
+ return reconcileInsert(parent, child, null, marker || null);
37
+ }
38
+
39
+ function isDomNode(value) {
40
+ if (!value || typeof value !== 'object') return false;
41
+ if (typeof Node !== 'undefined' && value instanceof Node) return true;
42
+ return typeof value.nodeType === 'number' && typeof value.nodeName === 'string';
43
+ }
44
+
45
+ function isVNode(value) {
46
+ return !!value && typeof value === 'object' && (value._vnode === true || 'tag' in value);
47
+ }
26
48
 
27
- if (typeof child === 'string' || typeof child === 'number') {
28
- const textNode = document.createTextNode(String(child));
29
- parent.insertBefore(textNode, marker || null);
30
- return textNode;
49
+ function isSvgParent(parent) {
50
+ return typeof SVGElement !== 'undefined'
51
+ && parent instanceof SVGElement
52
+ && parent.tagName.toLowerCase() !== 'foreignobject';
53
+ }
54
+
55
+ function asNodeArray(value) {
56
+ if (value == null) return [];
57
+ return Array.isArray(value) ? value : [value];
58
+ }
59
+
60
+ function valuesToNodes(value, parent, out) {
61
+ if (value == null || typeof value === 'boolean') return out;
62
+
63
+ if (Array.isArray(value)) {
64
+ for (let i = 0; i < value.length; i++) {
65
+ valuesToNodes(value[i], parent, out);
66
+ }
67
+ return out;
31
68
  }
32
69
 
33
- if (typeof child === 'function') {
34
- // Reactive expression — create micro-effect
35
- let currentNode = document.createTextNode('');
36
- parent.insertBefore(currentNode, marker || null);
70
+ if (typeof value === 'string' || typeof value === 'number') {
71
+ out.push(document.createTextNode(String(value)));
72
+ return out;
73
+ }
37
74
 
38
- effect(() => {
39
- const value = child();
40
- if (value instanceof Node) {
41
- // Function returned a DOM node — replace text node with it
42
- if (currentNode !== value) {
43
- parent.replaceChild(value, currentNode);
44
- currentNode = value;
45
- }
46
- } else if (Array.isArray(value)) {
47
- // Function returned array — handle dynamic lists
48
- _insertArray(parent, value, currentNode, marker);
49
- } else {
50
- // Primitive — update text content
51
- const text = value == null || typeof value === 'boolean' ? '' : String(value);
52
- if (currentNode.nodeType === 3) {
53
- if (currentNode.textContent !== text) currentNode.textContent = text;
54
- } else {
55
- const textNode = document.createTextNode(text);
56
- parent.replaceChild(textNode, currentNode);
57
- currentNode = textNode;
58
- }
75
+ if (isDomNode(value)) {
76
+ out.push(value);
77
+ return out;
78
+ }
79
+
80
+ if (isVNode(value)) {
81
+ out.push(createDOM(value, parent, isSvgParent(parent)));
82
+ return out;
83
+ }
84
+
85
+ out.push(document.createTextNode(String(value)));
86
+ return out;
87
+ }
88
+
89
+ function sameNodeArray(a, b) {
90
+ if (a.length !== b.length) return false;
91
+ for (let i = 0; i < a.length; i++) {
92
+ if (a[i] !== b[i]) return false;
93
+ }
94
+ return true;
95
+ }
96
+
97
+ function reconcileInsert(parent, value, current, marker) {
98
+ const targetMarker = marker || null;
99
+
100
+ if (value == null || typeof value === 'boolean') {
101
+ const oldNodes = asNodeArray(current);
102
+ for (let i = 0; i < oldNodes.length; i++) {
103
+ const oldNode = oldNodes[i];
104
+ if (oldNode.parentNode === parent) {
105
+ disposeTree(oldNode);
106
+ parent.removeChild(oldNode);
59
107
  }
60
- });
108
+ }
109
+ return null;
110
+ }
61
111
 
62
- return currentNode;
112
+ if ((typeof value === 'string' || typeof value === 'number')
113
+ && current && !Array.isArray(current) && current.nodeType === 3) {
114
+ const text = String(value);
115
+ if (current.textContent !== text) current.textContent = text;
116
+ return current;
63
117
  }
64
118
 
65
- if (child instanceof Node) {
66
- parent.insertBefore(child, marker || null);
67
- return child;
119
+ const newNodes = valuesToNodes(value, parent, []);
120
+ const oldNodes = asNodeArray(current);
121
+
122
+ if (sameNodeArray(oldNodes, newNodes)) {
123
+ return current;
68
124
  }
69
125
 
70
- if (Array.isArray(child)) {
71
- const nodes = [];
72
- for (let i = 0; i < child.length; i++) {
73
- const node = insert(parent, child[i], marker);
74
- if (node) nodes.push(node);
126
+ const keep = new Set(newNodes);
127
+ for (let i = 0; i < oldNodes.length; i++) {
128
+ const oldNode = oldNodes[i];
129
+ if (!keep.has(oldNode) && oldNode.parentNode === parent) {
130
+ disposeTree(oldNode);
131
+ parent.removeChild(oldNode);
75
132
  }
76
- return nodes;
77
133
  }
78
- }
79
134
 
80
- function _insertArray(parent, arr, currentNode, marker) {
81
- // Simple case: replace placeholder with array nodes
82
- const frag = document.createDocumentFragment();
83
- for (let i = 0; i < arr.length; i++) {
84
- if (arr[i] instanceof Node) {
85
- frag.appendChild(arr[i]);
86
- } else if (arr[i] != null && typeof arr[i] !== 'boolean') {
87
- frag.appendChild(document.createTextNode(String(arr[i])));
135
+ let ref = targetMarker;
136
+ for (let i = newNodes.length - 1; i >= 0; i--) {
137
+ const node = newNodes[i];
138
+ if (node.parentNode !== parent || node.nextSibling !== ref) {
139
+ parent.insertBefore(node, ref);
88
140
  }
141
+ ref = node;
89
142
  }
90
- parent.replaceChild(frag, currentNode);
143
+
144
+ if (newNodes.length === 0) return null;
145
+ return newNodes.length === 1 ? newNodes[0] : newNodes;
91
146
  }
92
147
 
93
148
  // --- mapArray(source, mapFn, options?) ---
@@ -650,6 +705,14 @@ export function spread(el, props) {
650
705
  function setPropDirect(el, key, value) {
651
706
  if (key === 'class' || key === 'className') {
652
707
  el.className = value || '';
708
+ } else if (key === 'dangerouslySetInnerHTML') {
709
+ el.innerHTML = value?.__html ?? '';
710
+ } else if (key === 'innerHTML') {
711
+ if (value && typeof value === 'object' && '__html' in value) {
712
+ el.innerHTML = value.__html ?? '';
713
+ } else {
714
+ el.innerHTML = value ?? '';
715
+ }
653
716
  } else if (key === 'style') {
654
717
  if (typeof value === 'string') {
655
718
  el.style.cssText = value;
package/src/store.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Lightweight global state management. Signal-based, type-safe, ergonomic.
3
3
  // Like Zustand meets signals — define a store, use it anywhere.
4
4
 
5
- import { signal, computed, batch } from './reactive.js';
5
+ import { signal, computed, batch, __DEV__ } from './reactive.js';
6
6
 
7
7
  // --- storeComputed ---
8
8
  // Marker wrapper to explicitly tag a function as a computed in createStore.
@@ -57,6 +57,11 @@ export function createStore(definition) {
57
57
  // Use explicit _storeComputed marker instead of function.length heuristic
58
58
  for (const [key, value] of Object.entries(definition)) {
59
59
  if (typeof value === 'function' && value._storeComputed) {
60
+ if (__DEV__ && value.length === 0) {
61
+ console.warn(
62
+ `[what] derived() for "${key}" should accept the state parameter, e.g. derived(state => ...).`
63
+ );
64
+ }
60
65
  // Computed: explicitly marked with storeComputed()
61
66
  computeds[key] = value;
62
67
  } else if (typeof value === 'function') {