what-core 0.6.1 → 0.7.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
@@ -2,7 +2,7 @@
2
2
  // Solid-style rendering: components run once, signals create individual DOM effects.
3
3
  // No VDOM diffing — direct DOM manipulation with surgical signal-driven updates.
4
4
 
5
- import { effect, untrack, createRoot, signal, __DEV__ } from './reactive.js';
5
+ import { effect, untrack, createRoot, _createItemScope, signal, __DEV__ } from './reactive.js';
6
6
  import { createDOM, disposeTree, getCurrentComponent, getComponentStack } from './dom.js';
7
7
 
8
8
  export { effect, untrack };
@@ -15,7 +15,13 @@ export { effect, untrack };
15
15
  export function _$createComponent(Component, props, children) {
16
16
  if (children && children.length > 0) {
17
17
  const mergedChildren = children.length === 1 ? children[0] : children;
18
- props = props ? { ...props, children: mergedChildren } : { children: mergedChildren };
18
+ // Mutate props in place when possible to avoid object spread allocation.
19
+ // Compiled output creates a fresh props object per call, so mutation is safe.
20
+ if (props) {
21
+ props.children = mergedChildren;
22
+ } else {
23
+ props = { children: mergedChildren };
24
+ }
19
25
  }
20
26
  // Build a VNode-like object and pass to createDOM which handles component execution
21
27
  return createDOM({ tag: Component, props: props || {}, children: children || [], key: null, _vnode: true });
@@ -88,14 +94,10 @@ function _$templateImpl(html) {
88
94
  if (tableInfo) {
89
95
  const t = document.createElement('template');
90
96
  t.innerHTML = tableInfo.wrap + trimmed + tableInfo.unwrap;
91
- // Navigate down through the wrapper to reach the actual element
92
- return () => {
93
- let node = t.content.firstChild;
94
- for (let i = 0; i < tableInfo.depth; i++) {
95
- node = node.firstChild;
96
- }
97
- return node.cloneNode(true);
98
- };
97
+ // Pre-navigate to the target element once avoids per-clone traversal.
98
+ let target = t.content.firstChild;
99
+ for (let i = 0; i < tableInfo.depth; i++) target = target.firstChild;
100
+ return () => target.cloneNode(true);
99
101
  }
100
102
 
101
103
  const t = document.createElement('template');
@@ -156,13 +158,56 @@ export function svgTemplate(html) {
156
158
 
157
159
  export function insert(parent, child, marker) {
158
160
  if (typeof child === 'function') {
159
- let current = null;
161
+ // Fast path: if the first evaluation returns a string/number, optimistically
162
+ // create a text node for direct updates. If the value type changes later
163
+ // (e.g., text -> vnode), fall back to full reconcileInsert.
164
+ const first = child();
165
+ const t = typeof first;
166
+ if (t === 'string' || t === 'number') {
167
+ const textNode = document.createTextNode(String(first));
168
+ const m = marker || null;
169
+ if (m) parent.insertBefore(textNode, m);
170
+ else parent.appendChild(textNode);
171
+ let current = textNode;
172
+ let isTextFastPath = true;
173
+ effect(() => {
174
+ const val = child();
175
+ const vt = typeof val;
176
+ if (isTextFastPath && (vt === 'string' || vt === 'number')) {
177
+ // Fast path: still text — update data directly (no allocations)
178
+ const str = String(val);
179
+ if (textNode.data !== str) textNode.data = str;
180
+ } else {
181
+ // Type changed — fall back to full reconcile
182
+ isTextFastPath = false;
183
+ current = reconcileInsert(parent, val, current, m);
184
+ }
185
+ });
186
+ return textNode;
187
+ }
188
+ // General path for non-text reactive children (first value was null/vnode/array)
189
+ let current = first != null ? reconcileInsert(parent, first, null, marker || null) : null;
160
190
  effect(() => {
161
191
  current = reconcileInsert(parent, child(), current, marker || null);
162
192
  });
163
193
  return current;
164
194
  }
165
195
 
196
+ // Static text: create text node directly, skip reconcileInsert overhead
197
+ if (typeof child === 'string' || typeof child === 'number') {
198
+ const textNode = document.createTextNode(String(child));
199
+ if (marker) parent.insertBefore(textNode, marker);
200
+ else parent.appendChild(textNode);
201
+ return textNode;
202
+ }
203
+
204
+ // Static DOM node: insert directly, skip reconcileInsert overhead
205
+ if (child != null && typeof child === 'object' && child.nodeType > 0) {
206
+ if (marker) parent.insertBefore(child, marker);
207
+ else parent.appendChild(child);
208
+ return child;
209
+ }
210
+
166
211
  return reconcileInsert(parent, child, null, marker || null);
167
212
  }
168
213
 
@@ -176,10 +221,12 @@ function isVNode(value) {
176
221
  return !!value && typeof value === 'object' && (value._vnode === true || 'tag' in value);
177
222
  }
178
223
 
224
+ // Check if parent is an SVG element. Cached typeof check avoids repeated lookups.
225
+ const _hasSVGElement = typeof SVGElement !== 'undefined';
179
226
  function isSvgParent(parent) {
180
- return typeof SVGElement !== 'undefined'
227
+ return _hasSVGElement
181
228
  && parent instanceof SVGElement
182
- && parent.tagName.toLowerCase() !== 'foreignobject';
229
+ && parent.tagName !== 'foreignObject';
183
230
  }
184
231
 
185
232
  function asNodeArray(value) {
@@ -252,10 +299,26 @@ function reconcileInsert(parent, value, current, marker) {
252
299
  if ((typeof value === 'string' || typeof value === 'number')
253
300
  && current && !Array.isArray(current) && current.nodeType === 3) {
254
301
  const text = String(value);
255
- if (current.textContent !== text) current.textContent = text;
302
+ if (current.data !== text) current.data = text;
256
303
  return current;
257
304
  }
258
305
 
306
+ // Fast path: single DOM node value with single current node — skip array allocations
307
+ if (typeof value === 'object' && value !== null && value.nodeType > 0 && !Array.isArray(value)) {
308
+ if (value === current) return current;
309
+ if (current && !Array.isArray(current) && current.nodeType > 0) {
310
+ // Replace single node with single node
311
+ if (current.parentNode === parent) {
312
+ disposeTree(current);
313
+ parent.replaceChild(value, current);
314
+ } else {
315
+ if (targetMarker) parent.insertBefore(value, targetMarker);
316
+ else parent.appendChild(value);
317
+ }
318
+ return value;
319
+ }
320
+ }
321
+
259
322
  const newNodes = valuesToNodes(value, parent, []);
260
323
  const oldNodes = asNodeArray(current);
261
324
 
@@ -263,10 +326,17 @@ function reconcileInsert(parent, value, current, marker) {
263
326
  return current;
264
327
  }
265
328
 
266
- const keep = new Set(newNodes);
329
+ // Remove old nodes not in the new set. For small arrays (typical case),
330
+ // linear scan is faster than Set allocation + hashing.
331
+ const newLen = newNodes.length;
267
332
  for (let i = 0; i < oldNodes.length; i++) {
268
333
  const oldNode = oldNodes[i];
269
- if (!keep.has(oldNode) && oldNode.parentNode === parent) {
334
+ if (oldNode.parentNode !== parent) continue;
335
+ let found = false;
336
+ for (let j = 0; j < newLen; j++) {
337
+ if (newNodes[j] === oldNode) { found = true; break; }
338
+ }
339
+ if (!found) {
270
340
  disposeTree(oldNode);
271
341
  parent.removeChild(oldNode);
272
342
  }
@@ -318,7 +388,9 @@ export function mapArray(source, mapFn, options) {
318
388
  } else {
319
389
  reconcileList(parent, endMarker, items, newItems, mappedNodes, disposeFns, mapFn);
320
390
  }
321
- items = newItems.slice();
391
+ // Save a snapshot of items for next diff. Use slice() to defend against
392
+ // in-place mutation, but skip for empty arrays (common clear case).
393
+ items = newItems.length > 0 ? newItems.slice() : newItems;
322
394
  });
323
395
 
324
396
  return endMarker;
@@ -330,13 +402,21 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
330
402
  const oldLen = oldItems.length;
331
403
 
332
404
  if (newLen === 0) {
333
- // Fast path: clear all — remove only this list's nodes, not all parent content
405
+ // Fast path: clear all — dispose reactive scopes first (handles effects/cleanups),
406
+ // then remove DOM nodes. createRoot disposal handles all tracked effects; we only
407
+ // need disposeTree for nodes with additional reactive bindings outside createRoot.
334
408
  if (oldLen > 0) {
335
409
  for (let i = 0; i < oldLen; i++) {
336
- disposeFns[i]?.();
337
- if (mappedNodes[i]?.parentNode === parent) {
338
- disposeTree(mappedNodes[i]);
339
- parent.removeChild(mappedNodes[i]);
410
+ if (disposeFns[i]) disposeFns[i]();
411
+ }
412
+ for (let i = oldLen - 1; i >= 0; i--) {
413
+ const node = mappedNodes[i];
414
+ if (node) {
415
+ // Only walk subtree if the node has reactive state not tracked by createRoot
416
+ if (node._componentCtx || node._dispose || node._propEffects) {
417
+ disposeTree(node);
418
+ }
419
+ if (node.parentNode === parent) parent.removeChild(node);
340
420
  }
341
421
  }
342
422
  mappedNodes.length = 0;
@@ -350,7 +430,7 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
350
430
  const frag = document.createDocumentFragment();
351
431
  for (let i = 0; i < newLen; i++) {
352
432
  const item = newItems[i];
353
- const node = createRoot(dispose => {
433
+ const node = _createItemScope(dispose => {
354
434
  disposeFns[i] = dispose;
355
435
  return mapFn(item, i);
356
436
  });
@@ -407,7 +487,7 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
407
487
  for (let i = start; i <= newEnd; i++) {
408
488
  const item = newItems[i];
409
489
  const idx = i;
410
- newMapped[i] = createRoot(dispose => {
490
+ newMapped[i] = _createItemScope(dispose => {
411
491
  newDispose[idx] = dispose;
412
492
  return mapFn(item, idx);
413
493
  });
@@ -492,7 +572,7 @@ function _reconcileMiddle(parent, endMarker, oldItems, newItems, mappedNodes, di
492
572
  if (!newMapped[i]) {
493
573
  const item = newItems[i];
494
574
  const idx = i;
495
- newMapped[i] = createRoot(dispose => {
575
+ newMapped[i] = _createItemScope(dispose => {
496
576
  newDispose[idx] = dispose;
497
577
  return mapFn(item, idx);
498
578
  });
@@ -571,13 +651,17 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
571
651
  // --- Fast path: clear all ---
572
652
  if (newLen === 0) {
573
653
  if (oldLen > 0) {
574
- // Call dispose functions to run cleanup callbacks (onCleanup, effect cleanups).
575
- // Without this, cleanup callbacks leak.
654
+ // Dispose reactive scopes first, then remove DOM nodes.
576
655
  for (let i = 0; i < oldLen; i++) {
577
- disposeFns[i]?.();
578
- if (mappedNodes[i]?.parentNode === parent) {
579
- disposeTree(mappedNodes[i]);
580
- parent.removeChild(mappedNodes[i]);
656
+ if (disposeFns[i]) disposeFns[i]();
657
+ }
658
+ for (let i = oldLen - 1; i >= 0; i--) {
659
+ const node = mappedNodes[i];
660
+ if (node) {
661
+ if (node._componentCtx || node._dispose || node._propEffects) {
662
+ disposeTree(node);
663
+ }
664
+ if (node.parentNode === parent) parent.removeChild(node);
581
665
  }
582
666
  }
583
667
  mappedNodes.length = 0;
@@ -602,7 +686,7 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
602
686
  } else {
603
687
  accessor = item; // raw mode: pass item directly
604
688
  }
605
- const node = createRoot(dispose => {
689
+ const node = _createItemScope(dispose => {
606
690
  disposeFns[idx] = dispose;
607
691
  return mapFn(accessor, idx);
608
692
  });
@@ -678,7 +762,7 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
678
762
  } else {
679
763
  accessor = item;
680
764
  }
681
- newMapped[i] = createRoot(dispose => {
765
+ newMapped[i] = _createItemScope(dispose => {
682
766
  newDispose[idx] = dispose;
683
767
  return mapFn(accessor, idx);
684
768
  });
@@ -747,7 +831,7 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
747
831
  } else {
748
832
  accessor = item;
749
833
  }
750
- newMapped[i] = createRoot(dispose => {
834
+ newMapped[i] = _createItemScope(dispose => {
751
835
  newDispose[idx] = dispose;
752
836
  return mapFn(accessor, idx);
753
837
  });
@@ -865,6 +949,9 @@ export function setProp(el, key, value) {
865
949
  return;
866
950
  }
867
951
 
952
+ // Key prop — no-op, WhatFW has no virtual DOM (defense in depth, issue #6)
953
+ if (key === 'key') return;
954
+
868
955
  // Sanitize URL attributes — reject dangerous protocols
869
956
  if (URL_ATTRS.has(key) || URL_ATTRS.has(key.toLowerCase())) {
870
957
  if (!isSafeUrl(value)) {