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/dist/index.js +313 -182
- package/dist/index.js.map +3 -3
- package/dist/index.min.js +6 -6
- package/dist/index.min.js.map +3 -3
- package/dist/jsx-dev-runtime.js +37 -16
- package/dist/jsx-dev-runtime.js.map +2 -2
- package/dist/jsx-dev-runtime.min.js +1 -1
- package/dist/jsx-dev-runtime.min.js.map +3 -3
- package/dist/jsx-runtime.js +37 -16
- package/dist/jsx-runtime.js.map +2 -2
- package/dist/jsx-runtime.min.js +1 -1
- package/dist/jsx-runtime.min.js.map +3 -3
- package/dist/render.js +257 -194
- package/dist/render.js.map +3 -3
- package/dist/render.min.js +1 -1
- package/dist/render.min.js.map +3 -3
- package/dist/testing.js +174 -149
- package/dist/testing.js.map +2 -2
- package/dist/testing.min.js +1 -1
- package/dist/testing.min.js.map +3 -3
- package/package.json +1 -1
- package/src/dom.js +78 -47
- package/src/h.js +43 -18
- package/src/reactive.js +176 -101
- package/src/render.js +121 -34
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
|
-
|
|
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
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
227
|
+
return _hasSVGElement
|
|
181
228
|
&& parent instanceof SVGElement
|
|
182
|
-
&& parent.tagName
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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 —
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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 =
|
|
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] =
|
|
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] =
|
|
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
|
-
//
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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 =
|
|
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] =
|
|
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] =
|
|
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)) {
|