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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-core",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "What Framework - The closest framework to vanilla JS",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/dom.js CHANGED
@@ -5,7 +5,6 @@
5
5
  import { effect, batch, untrack, signal, __DEV__, __devtools } from './reactive.js';
6
6
  import { reportError, _injectGetCurrentComponent, shallowEqual } from './components.js';
7
7
  import { _setComponentRef } from './helpers.js';
8
-
9
8
  // SVG elements that need namespace
10
9
  const SVG_ELEMENTS = new Set([
11
10
  'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse',
@@ -74,16 +73,20 @@ function disposeComponent(ctx) {
74
73
  mountedComponents.delete(ctx);
75
74
  }
76
75
 
77
- // Dispose all components and reactive effects attached to a DOM subtree
76
+ // Dispose all components and reactive effects attached to a DOM subtree.
77
+ // Performance: checks _componentCtx / _dispose / _propEffects before walking
78
+ // children, and only checks _commentCtxMap for comment nodes (nodeType 8).
78
79
  export function disposeTree(node) {
79
80
  if (!node) return;
80
81
  if (node._componentCtx) {
81
82
  disposeComponent(node._componentCtx);
82
83
  }
83
- // Check comment node WeakMap for component context
84
- const commentCtx = _commentCtxMap.get(node);
85
- if (commentCtx) {
86
- disposeComponent(commentCtx);
84
+ // Check comment node WeakMap for component context — only for comment nodes
85
+ if (node.nodeType === 8) {
86
+ const commentCtx = _commentCtxMap.get(node);
87
+ if (commentCtx) {
88
+ disposeComponent(commentCtx);
89
+ }
87
90
  }
88
91
  // Dispose reactive function child effects ({() => ...} wrappers)
89
92
  if (node._dispose) {
@@ -95,9 +98,11 @@ export function disposeTree(node) {
95
98
  try { node._propEffects[key](); } catch (e) { /* already disposed */ }
96
99
  }
97
100
  }
98
- if (node.childNodes) {
99
- for (const child of node.childNodes) {
100
- disposeTree(child);
101
+ // Recursively dispose children
102
+ const children = node.childNodes;
103
+ if (children && children.length > 0) {
104
+ for (let i = 0; i < children.length; i++) {
105
+ disposeTree(children[i]);
101
106
  }
102
107
  }
103
108
  }
@@ -216,6 +221,31 @@ export function createDOM(vnode, parent, isSvg) {
216
221
  // --- Component Rendering ---
217
222
  // Components run ONCE. Props are passed as a signal for reactive access.
218
223
 
224
+ // Shared Proxy handler for reactive props — defined once, reused by all components.
225
+ // The Proxy target must be a plain object (not a function) so that ownKeys
226
+ // invariants are satisfied. The propsSignal is stored as target._sig.
227
+ const _propsProxyHandler = {
228
+ get(target, key) {
229
+ if (key === '_sig') return undefined; // hide internal property
230
+ return target._sig()[key];
231
+ },
232
+ has(target, key) {
233
+ if (key === '_sig') return false;
234
+ return key in target._sig();
235
+ },
236
+ ownKeys(target) {
237
+ return Reflect.ownKeys(target._sig());
238
+ },
239
+ getOwnPropertyDescriptor(target, key) {
240
+ if (key === '_sig') return undefined;
241
+ const current = target._sig();
242
+ if (key in current) {
243
+ return { value: current[key], writable: false, enumerable: true, configurable: true };
244
+ }
245
+ return undefined;
246
+ },
247
+ };
248
+
219
249
  const componentStack = [];
220
250
 
221
251
  export function getCurrentComponent() {
@@ -256,6 +286,20 @@ function createComponent(vnode, parent, isSvg) {
256
286
  }
257
287
 
258
288
  // Component context for hooks
289
+ // Error boundary lookup: walk the parent chain once, cache the result.
290
+ const parentCtx = componentStack[componentStack.length - 1] || null;
291
+ let errorBoundary = null;
292
+ if (parentCtx) {
293
+ // Fast path: if parent has an error boundary, use it directly
294
+ errorBoundary = parentCtx._errorBoundary || null;
295
+ if (!errorBoundary) {
296
+ let p = parentCtx._parentCtx;
297
+ while (p) {
298
+ if (p._errorBoundary) { errorBoundary = p._errorBoundary; break; }
299
+ p = p._parentCtx;
300
+ }
301
+ }
302
+ }
259
303
  const ctx = {
260
304
  hooks: [],
261
305
  hookIndex: 0,
@@ -264,15 +308,8 @@ function createComponent(vnode, parent, isSvg) {
264
308
  mounted: false,
265
309
  disposed: false,
266
310
  Component,
267
- _parentCtx: componentStack[componentStack.length - 1] || null,
268
- _errorBoundary: (() => {
269
- let p = componentStack[componentStack.length - 1];
270
- while (p) {
271
- if (p._errorBoundary) return p._errorBoundary;
272
- p = p._parentCtx;
273
- }
274
- return null;
275
- })()
311
+ _parentCtx: parentCtx,
312
+ _errorBoundary: errorBoundary,
276
313
  };
277
314
 
278
315
  // Component boundaries: use comment nodes instead of <span style="display:contents">
@@ -294,35 +331,24 @@ function createComponent(vnode, parent, isSvg) {
294
331
 
295
332
  // Props signal for reactive updates from parent
296
333
  const propsChildren = children.length === 0 ? undefined : children.length === 1 ? children[0] : children;
297
- const propsSignal = signal({ ...props, children: propsChildren });
334
+ // Merge children into props without spreading when possible
335
+ let mergedProps;
336
+ if (propsChildren !== undefined) {
337
+ mergedProps = props ? Object.assign({}, props, { children: propsChildren }) : { children: propsChildren };
338
+ } else {
339
+ mergedProps = props ? Object.assign({}, props) : {};
340
+ }
341
+ const propsSignal = signal(mergedProps);
298
342
  ctx._propsSignal = propsSignal;
299
343
 
300
344
  // Create a reactive props proxy: reading any prop inside an effect
301
345
  // will auto-track the dependency on the propsSignal. This makes prop
302
346
  // access reactive across re-renders without requiring the component
303
347
  // to be re-executed.
304
- const reactiveProps = new Proxy({}, {
305
- get(_, key) {
306
- // Access the signal to create a reactive dependency
307
- const current = propsSignal();
308
- return current[key];
309
- },
310
- has(_, key) {
311
- const current = propsSignal();
312
- return key in current;
313
- },
314
- ownKeys() {
315
- const current = propsSignal();
316
- return Reflect.ownKeys(current);
317
- },
318
- getOwnPropertyDescriptor(_, key) {
319
- const current = propsSignal();
320
- if (key in current) {
321
- return { value: current[key], writable: false, enumerable: true, configurable: true };
322
- }
323
- return undefined;
324
- },
325
- });
348
+ // Reuse shared trap handlers to minimize per-component allocation.
349
+ // Store propsSignal on a plain object target (Proxy invariant: ownKeys must
350
+ // match non-configurable own properties of target; functions have 'prototype').
351
+ const reactiveProps = new Proxy({ _sig: propsSignal }, _propsProxyHandler);
326
352
 
327
353
  // Component runs ONCE — not inside an effect
328
354
  componentStack.push(ctx);
@@ -550,8 +576,9 @@ function createElementFromVNode(vnode, parent, isSvg) {
550
576
  }
551
577
 
552
578
  // Append children
553
- for (const child of children) {
554
- const node = createDOM(child, el, svgContext && tag !== 'foreignObject');
579
+ const isSvgChildren = svgContext && tag !== 'foreignObject';
580
+ for (let i = 0; i < children.length; i++) {
581
+ const node = createDOM(children[i], el, isSvgChildren);
555
582
  if (node) el.appendChild(node);
556
583
  }
557
584
 
@@ -563,16 +590,16 @@ function createElementFromVNode(vnode, parent, isSvg) {
563
590
  // Only applied once for fine-grained (no diffing). Reactive props use effects.
564
591
 
565
592
  function applyProps(el, newProps, oldProps, isSvg) {
566
- newProps = newProps || {};
567
- oldProps = oldProps || {};
593
+ if (!newProps) return;
568
594
 
569
595
  for (const key in newProps) {
570
596
  if (key === 'key' || key === 'children') continue;
571
597
 
572
598
  // Handle ref
573
599
  if (key === 'ref') {
574
- if (typeof newProps.ref === 'function') newProps.ref(el);
575
- else if (newProps.ref) newProps.ref.current = el;
600
+ const ref = newProps.ref;
601
+ if (typeof ref === 'function') ref(el);
602
+ else if (ref) ref.current = el;
576
603
  continue;
577
604
  }
578
605
 
@@ -609,10 +636,13 @@ function setProp(el, key, value, isSvg) {
609
636
  if (old) el.removeEventListener(event, old, useCapture);
610
637
  if (value == null) return;
611
638
  if (!el._events) el._events = {};
639
+ // Single closure per event listener. Uses untrack to prevent accidental
640
+ // signal subscriptions inside event handlers.
612
641
  const wrappedHandler = (e) => {
613
642
  if (!e.nativeEvent) e.nativeEvent = e;
614
- return untrack(() => value(e));
643
+ return untrack(() => wrappedHandler._handler(e));
615
644
  };
645
+ wrappedHandler._handler = value;
616
646
  wrappedHandler._original = value;
617
647
  el._events[storageKey] = wrappedHandler;
618
648
  const eventOpts = value._eventOpts;
package/src/guardrails.js CHANGED
@@ -150,6 +150,8 @@ const VALID_EXPORTS = new Set([
150
150
  // Scheduler
151
151
  'scheduleRead', 'scheduleWrite', 'flushScheduler', 'measure', 'mutate',
152
152
  'useScheduledEffect', 'nextFrame', 'raf', 'onResize', 'onIntersect', 'smoothScrollTo',
153
+ // Text insertion hook (for external text engines)
154
+ '_setTextInsertHook',
153
155
  // Animation
154
156
  'spring', 'tween', 'easings', 'useTransition', 'useGesture', 'useAnimatedValue',
155
157
  'createTransitionClasses', 'cssTransition',
package/src/h.js CHANGED
@@ -8,9 +8,25 @@
8
8
  const EMPTY_OBJ = Object.create(null);
9
9
  const EMPTY_ARR = [];
10
10
 
11
- export function h(tag, props, ...children) {
11
+ export function h(tag, props) {
12
12
  props = props || EMPTY_OBJ;
13
- const flat = flattenChildren(children);
13
+ // Collect children from arguments[2..n] without rest args — avoids array allocation
14
+ // when there are 0-1 children (common case: leaf elements, text nodes).
15
+ const argLen = arguments.length;
16
+ let flat;
17
+ if (argLen <= 2) {
18
+ flat = EMPTY_ARR;
19
+ } else if (argLen === 3) {
20
+ flat = _flattenSingle(arguments[2]);
21
+ } else {
22
+ // Multiple children: flatten all arguments from position 2 onward
23
+ const out = [];
24
+ for (let i = 2; i < argLen; i++) {
25
+ _flattenInto(arguments[i], out);
26
+ }
27
+ flat = out;
28
+ }
29
+
14
30
  const key = props.key ?? null;
15
31
 
16
32
  // Strip key from props passed to component/element
@@ -27,24 +43,33 @@ export function Fragment({ children }) {
27
43
  return children;
28
44
  }
29
45
 
30
- function flattenChildren(children) {
31
- const out = [];
32
- for (let i = 0; i < children.length; i++) {
33
- const child = children[i];
34
- if (child == null || child === false || child === true) continue;
35
- if (Array.isArray(child)) {
36
- out.push(...flattenChildren(child));
37
- } else if (typeof child === 'object' && child._vnode) {
38
- out.push(child);
39
- } else if (typeof child === 'function') {
40
- // Reactive child — preserve function for fine-grained DOM updates
41
- out.push(child);
42
- } else {
43
- // Text node
44
- out.push(String(child));
46
+ // Fast path for single child (most common case)
47
+ function _flattenSingle(child) {
48
+ if (child == null || child === false || child === true) return EMPTY_ARR;
49
+ if (Array.isArray(child)) {
50
+ const out = [];
51
+ _flattenInto(child, out);
52
+ return out;
53
+ }
54
+ if (typeof child === 'object' && child._vnode) return [child];
55
+ if (typeof child === 'function') return [child];
56
+ return [String(child)];
57
+ }
58
+
59
+ // Flatten a child (or array of children) into the output array
60
+ function _flattenInto(child, out) {
61
+ if (child == null || child === false || child === true) return;
62
+ if (Array.isArray(child)) {
63
+ for (let i = 0; i < child.length; i++) {
64
+ _flattenInto(child[i], out);
45
65
  }
66
+ } else if (typeof child === 'object' && child._vnode) {
67
+ out.push(child);
68
+ } else if (typeof child === 'function') {
69
+ out.push(child);
70
+ } else {
71
+ out.push(String(child));
46
72
  }
47
- return out;
48
73
  }
49
74
 
50
75
  // JSX-like tagged template alternative (no build step needed)
package/src/index.js CHANGED
@@ -173,6 +173,9 @@ export {
173
173
  validateImports,
174
174
  } from './guardrails.js';
175
175
 
176
+ // Generic text insertion hook (used by external text engines like what-text)
177
+ export { _setTextInsertHook } from './render.js';
178
+
176
179
  // Agent context (global inspection API)
177
180
  export {
178
181
  installAgentContext,