what-core 0.6.2 → 0.8.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 +321 -182
- package/dist/index.js.map +3 -3
- package/dist/index.min.js +5 -5
- 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 +263 -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 -48
- package/src/guardrails.js +2 -0
- package/src/h.js +43 -18
- package/src/index.js +3 -0
- package/src/reactive.js +176 -101
- package/src/render.js +131 -35
package/src/render.js
CHANGED
|
@@ -2,11 +2,21 @@
|
|
|
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
|
-
|
|
8
7
|
export { effect, untrack };
|
|
9
8
|
|
|
9
|
+
// --- Generic text insertion hook ---
|
|
10
|
+
// External text engines (e.g., what-text) register a callback here via
|
|
11
|
+
// _setTextInsertHook(). When null (default), zero cost — no module loaded,
|
|
12
|
+
// no branch taken. The hook receives (parentElement, textString) on every
|
|
13
|
+
// dynamic text insertion and update.
|
|
14
|
+
let _onTextInsert = null;
|
|
15
|
+
|
|
16
|
+
export function _setTextInsertHook(fn) {
|
|
17
|
+
_onTextInsert = typeof fn === 'function' ? fn : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
// --- _$createComponent(Component, props, children) ---
|
|
11
21
|
// Internal compiler target for component instantiation. The compiler emits calls
|
|
12
22
|
// to this function instead of h() — keeping h() out of compiled output entirely.
|
|
@@ -15,7 +25,13 @@ export { effect, untrack };
|
|
|
15
25
|
export function _$createComponent(Component, props, children) {
|
|
16
26
|
if (children && children.length > 0) {
|
|
17
27
|
const mergedChildren = children.length === 1 ? children[0] : children;
|
|
18
|
-
|
|
28
|
+
// Mutate props in place when possible to avoid object spread allocation.
|
|
29
|
+
// Compiled output creates a fresh props object per call, so mutation is safe.
|
|
30
|
+
if (props) {
|
|
31
|
+
props.children = mergedChildren;
|
|
32
|
+
} else {
|
|
33
|
+
props = { children: mergedChildren };
|
|
34
|
+
}
|
|
19
35
|
}
|
|
20
36
|
// Build a VNode-like object and pass to createDOM which handles component execution
|
|
21
37
|
return createDOM({ tag: Component, props: props || {}, children: children || [], key: null, _vnode: true });
|
|
@@ -88,14 +104,10 @@ function _$templateImpl(html) {
|
|
|
88
104
|
if (tableInfo) {
|
|
89
105
|
const t = document.createElement('template');
|
|
90
106
|
t.innerHTML = tableInfo.wrap + trimmed + tableInfo.unwrap;
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
node = node.firstChild;
|
|
96
|
-
}
|
|
97
|
-
return node.cloneNode(true);
|
|
98
|
-
};
|
|
107
|
+
// Pre-navigate to the target element once — avoids per-clone traversal.
|
|
108
|
+
let target = t.content.firstChild;
|
|
109
|
+
for (let i = 0; i < tableInfo.depth; i++) target = target.firstChild;
|
|
110
|
+
return () => target.cloneNode(true);
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
const t = document.createElement('template');
|
|
@@ -156,13 +168,58 @@ export function svgTemplate(html) {
|
|
|
156
168
|
|
|
157
169
|
export function insert(parent, child, marker) {
|
|
158
170
|
if (typeof child === 'function') {
|
|
159
|
-
|
|
171
|
+
// Fast path: if the first evaluation returns a string/number, optimistically
|
|
172
|
+
// create a text node for direct updates. If the value type changes later
|
|
173
|
+
// (e.g., text -> vnode), fall back to full reconcileInsert.
|
|
174
|
+
const first = child();
|
|
175
|
+
const t = typeof first;
|
|
176
|
+
if (t === 'string' || t === 'number') {
|
|
177
|
+
const textNode = document.createTextNode(String(first));
|
|
178
|
+
const m = marker || null;
|
|
179
|
+
if (m) parent.insertBefore(textNode, m);
|
|
180
|
+
else parent.appendChild(textNode);
|
|
181
|
+
if (_onTextInsert) _onTextInsert(parent, String(first));
|
|
182
|
+
let current = textNode;
|
|
183
|
+
let isTextFastPath = true;
|
|
184
|
+
effect(() => {
|
|
185
|
+
const val = child();
|
|
186
|
+
const vt = typeof val;
|
|
187
|
+
if (isTextFastPath && (vt === 'string' || vt === 'number')) {
|
|
188
|
+
// Fast path: still text — update data directly (no allocations)
|
|
189
|
+
const str = String(val);
|
|
190
|
+
if (textNode.data !== str) textNode.data = str;
|
|
191
|
+
if (_onTextInsert) _onTextInsert(parent, str);
|
|
192
|
+
} else {
|
|
193
|
+
// Type changed — fall back to full reconcile
|
|
194
|
+
isTextFastPath = false;
|
|
195
|
+
current = reconcileInsert(parent, val, current, m);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return textNode;
|
|
199
|
+
}
|
|
200
|
+
// General path for non-text reactive children (first value was null/vnode/array)
|
|
201
|
+
let current = first != null ? reconcileInsert(parent, first, null, marker || null) : null;
|
|
160
202
|
effect(() => {
|
|
161
203
|
current = reconcileInsert(parent, child(), current, marker || null);
|
|
162
204
|
});
|
|
163
205
|
return current;
|
|
164
206
|
}
|
|
165
207
|
|
|
208
|
+
// Static text: create text node directly, skip reconcileInsert overhead
|
|
209
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
210
|
+
const textNode = document.createTextNode(String(child));
|
|
211
|
+
if (marker) parent.insertBefore(textNode, marker);
|
|
212
|
+
else parent.appendChild(textNode);
|
|
213
|
+
return textNode;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Static DOM node: insert directly, skip reconcileInsert overhead
|
|
217
|
+
if (child != null && typeof child === 'object' && child.nodeType > 0) {
|
|
218
|
+
if (marker) parent.insertBefore(child, marker);
|
|
219
|
+
else parent.appendChild(child);
|
|
220
|
+
return child;
|
|
221
|
+
}
|
|
222
|
+
|
|
166
223
|
return reconcileInsert(parent, child, null, marker || null);
|
|
167
224
|
}
|
|
168
225
|
|
|
@@ -176,10 +233,12 @@ function isVNode(value) {
|
|
|
176
233
|
return !!value && typeof value === 'object' && (value._vnode === true || 'tag' in value);
|
|
177
234
|
}
|
|
178
235
|
|
|
236
|
+
// Check if parent is an SVG element. Cached typeof check avoids repeated lookups.
|
|
237
|
+
const _hasSVGElement = typeof SVGElement !== 'undefined';
|
|
179
238
|
function isSvgParent(parent) {
|
|
180
|
-
return
|
|
239
|
+
return _hasSVGElement
|
|
181
240
|
&& parent instanceof SVGElement
|
|
182
|
-
&& parent.tagName
|
|
241
|
+
&& parent.tagName !== 'foreignObject';
|
|
183
242
|
}
|
|
184
243
|
|
|
185
244
|
function asNodeArray(value) {
|
|
@@ -252,10 +311,26 @@ function reconcileInsert(parent, value, current, marker) {
|
|
|
252
311
|
if ((typeof value === 'string' || typeof value === 'number')
|
|
253
312
|
&& current && !Array.isArray(current) && current.nodeType === 3) {
|
|
254
313
|
const text = String(value);
|
|
255
|
-
if (current.
|
|
314
|
+
if (current.data !== text) current.data = text;
|
|
256
315
|
return current;
|
|
257
316
|
}
|
|
258
317
|
|
|
318
|
+
// Fast path: single DOM node value with single current node — skip array allocations
|
|
319
|
+
if (typeof value === 'object' && value !== null && value.nodeType > 0 && !Array.isArray(value)) {
|
|
320
|
+
if (value === current) return current;
|
|
321
|
+
if (current && !Array.isArray(current) && current.nodeType > 0) {
|
|
322
|
+
// Replace single node with single node
|
|
323
|
+
if (current.parentNode === parent) {
|
|
324
|
+
disposeTree(current);
|
|
325
|
+
parent.replaceChild(value, current);
|
|
326
|
+
} else {
|
|
327
|
+
if (targetMarker) parent.insertBefore(value, targetMarker);
|
|
328
|
+
else parent.appendChild(value);
|
|
329
|
+
}
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
259
334
|
const newNodes = valuesToNodes(value, parent, []);
|
|
260
335
|
const oldNodes = asNodeArray(current);
|
|
261
336
|
|
|
@@ -263,10 +338,17 @@ function reconcileInsert(parent, value, current, marker) {
|
|
|
263
338
|
return current;
|
|
264
339
|
}
|
|
265
340
|
|
|
266
|
-
|
|
341
|
+
// Remove old nodes not in the new set. For small arrays (typical case),
|
|
342
|
+
// linear scan is faster than Set allocation + hashing.
|
|
343
|
+
const newLen = newNodes.length;
|
|
267
344
|
for (let i = 0; i < oldNodes.length; i++) {
|
|
268
345
|
const oldNode = oldNodes[i];
|
|
269
|
-
if (
|
|
346
|
+
if (oldNode.parentNode !== parent) continue;
|
|
347
|
+
let found = false;
|
|
348
|
+
for (let j = 0; j < newLen; j++) {
|
|
349
|
+
if (newNodes[j] === oldNode) { found = true; break; }
|
|
350
|
+
}
|
|
351
|
+
if (!found) {
|
|
270
352
|
disposeTree(oldNode);
|
|
271
353
|
parent.removeChild(oldNode);
|
|
272
354
|
}
|
|
@@ -318,7 +400,9 @@ export function mapArray(source, mapFn, options) {
|
|
|
318
400
|
} else {
|
|
319
401
|
reconcileList(parent, endMarker, items, newItems, mappedNodes, disposeFns, mapFn);
|
|
320
402
|
}
|
|
321
|
-
items
|
|
403
|
+
// Save a snapshot of items for next diff. Use slice() to defend against
|
|
404
|
+
// in-place mutation, but skip for empty arrays (common clear case).
|
|
405
|
+
items = newItems.length > 0 ? newItems.slice() : newItems;
|
|
322
406
|
});
|
|
323
407
|
|
|
324
408
|
return endMarker;
|
|
@@ -330,13 +414,21 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
|
|
|
330
414
|
const oldLen = oldItems.length;
|
|
331
415
|
|
|
332
416
|
if (newLen === 0) {
|
|
333
|
-
// Fast path: clear all —
|
|
417
|
+
// Fast path: clear all — dispose reactive scopes first (handles effects/cleanups),
|
|
418
|
+
// then remove DOM nodes. createRoot disposal handles all tracked effects; we only
|
|
419
|
+
// need disposeTree for nodes with additional reactive bindings outside createRoot.
|
|
334
420
|
if (oldLen > 0) {
|
|
335
421
|
for (let i = 0; i < oldLen; i++) {
|
|
336
|
-
disposeFns[i]
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
422
|
+
if (disposeFns[i]) disposeFns[i]();
|
|
423
|
+
}
|
|
424
|
+
for (let i = oldLen - 1; i >= 0; i--) {
|
|
425
|
+
const node = mappedNodes[i];
|
|
426
|
+
if (node) {
|
|
427
|
+
// Only walk subtree if the node has reactive state not tracked by createRoot
|
|
428
|
+
if (node._componentCtx || node._dispose || node._propEffects) {
|
|
429
|
+
disposeTree(node);
|
|
430
|
+
}
|
|
431
|
+
if (node.parentNode === parent) parent.removeChild(node);
|
|
340
432
|
}
|
|
341
433
|
}
|
|
342
434
|
mappedNodes.length = 0;
|
|
@@ -350,7 +442,7 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
|
|
|
350
442
|
const frag = document.createDocumentFragment();
|
|
351
443
|
for (let i = 0; i < newLen; i++) {
|
|
352
444
|
const item = newItems[i];
|
|
353
|
-
const node =
|
|
445
|
+
const node = _createItemScope(dispose => {
|
|
354
446
|
disposeFns[i] = dispose;
|
|
355
447
|
return mapFn(item, i);
|
|
356
448
|
});
|
|
@@ -407,7 +499,7 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
|
|
|
407
499
|
for (let i = start; i <= newEnd; i++) {
|
|
408
500
|
const item = newItems[i];
|
|
409
501
|
const idx = i;
|
|
410
|
-
newMapped[i] =
|
|
502
|
+
newMapped[i] = _createItemScope(dispose => {
|
|
411
503
|
newDispose[idx] = dispose;
|
|
412
504
|
return mapFn(item, idx);
|
|
413
505
|
});
|
|
@@ -492,7 +584,7 @@ function _reconcileMiddle(parent, endMarker, oldItems, newItems, mappedNodes, di
|
|
|
492
584
|
if (!newMapped[i]) {
|
|
493
585
|
const item = newItems[i];
|
|
494
586
|
const idx = i;
|
|
495
|
-
newMapped[i] =
|
|
587
|
+
newMapped[i] = _createItemScope(dispose => {
|
|
496
588
|
newDispose[idx] = dispose;
|
|
497
589
|
return mapFn(item, idx);
|
|
498
590
|
});
|
|
@@ -571,13 +663,17 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
571
663
|
// --- Fast path: clear all ---
|
|
572
664
|
if (newLen === 0) {
|
|
573
665
|
if (oldLen > 0) {
|
|
574
|
-
//
|
|
575
|
-
// Without this, cleanup callbacks leak.
|
|
666
|
+
// Dispose reactive scopes first, then remove DOM nodes.
|
|
576
667
|
for (let i = 0; i < oldLen; i++) {
|
|
577
|
-
disposeFns[i]
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
668
|
+
if (disposeFns[i]) disposeFns[i]();
|
|
669
|
+
}
|
|
670
|
+
for (let i = oldLen - 1; i >= 0; i--) {
|
|
671
|
+
const node = mappedNodes[i];
|
|
672
|
+
if (node) {
|
|
673
|
+
if (node._componentCtx || node._dispose || node._propEffects) {
|
|
674
|
+
disposeTree(node);
|
|
675
|
+
}
|
|
676
|
+
if (node.parentNode === parent) parent.removeChild(node);
|
|
581
677
|
}
|
|
582
678
|
}
|
|
583
679
|
mappedNodes.length = 0;
|
|
@@ -602,7 +698,7 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
602
698
|
} else {
|
|
603
699
|
accessor = item; // raw mode: pass item directly
|
|
604
700
|
}
|
|
605
|
-
const node =
|
|
701
|
+
const node = _createItemScope(dispose => {
|
|
606
702
|
disposeFns[idx] = dispose;
|
|
607
703
|
return mapFn(accessor, idx);
|
|
608
704
|
});
|
|
@@ -678,7 +774,7 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
678
774
|
} else {
|
|
679
775
|
accessor = item;
|
|
680
776
|
}
|
|
681
|
-
newMapped[i] =
|
|
777
|
+
newMapped[i] = _createItemScope(dispose => {
|
|
682
778
|
newDispose[idx] = dispose;
|
|
683
779
|
return mapFn(accessor, idx);
|
|
684
780
|
});
|
|
@@ -747,7 +843,7 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
747
843
|
} else {
|
|
748
844
|
accessor = item;
|
|
749
845
|
}
|
|
750
|
-
newMapped[i] =
|
|
846
|
+
newMapped[i] = _createItemScope(dispose => {
|
|
751
847
|
newDispose[idx] = dispose;
|
|
752
848
|
return mapFn(accessor, idx);
|
|
753
849
|
});
|