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/dist/dom.js CHANGED
@@ -1,890 +1,763 @@
1
- // What Framework - DOM Reconciler
2
- // Surgical DOM updates. Diff props, diff children, patch only what changed.
3
- // Components use <what-c> wrapper elements (display:contents) for clean reconciliation.
4
- // No virtual DOM tree kept in memory — we diff against the live DOM.
5
-
6
1
  import { effect, batch, untrack, signal } from './reactive.js';
7
2
  import { reportError, _injectGetCurrentComponent } from './components.js';
8
3
  import { _setComponentRef } from './helpers.js';
9
-
10
- // Register <what-c> custom element to prevent flash of unstyled content
11
- // Note: style is set in connectedCallback (not constructor) to comply with custom element spec
12
4
  if (typeof customElements !== 'undefined' && !customElements.get('what-c')) {
13
- customElements.define('what-c', class extends HTMLElement {
14
- connectedCallback() {
15
- this.style.display = 'contents';
16
- }
17
- });
18
- }
19
-
20
- // SVG elements that need namespace
5
+ customElements.define('what-c', class extends HTMLElement {
6
+ connectedCallback() {
7
+ this.style.display = 'contents';
8
+ }
9
+ });
10
+ }
21
11
  const SVG_ELEMENTS = new Set([
22
- 'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse',
23
- 'g', 'defs', 'use', 'symbol', 'clipPath', 'mask', 'pattern', 'image',
24
- 'text', 'tspan', 'textPath', 'foreignObject', 'linearGradient', 'radialGradient', 'stop',
25
- 'marker', 'animate', 'animateTransform', 'animateMotion', 'set', 'filter',
26
- 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix',
27
- 'feDiffuseLighting', 'feDisplacementMap', 'feFlood', 'feGaussianBlur', 'feImage',
28
- 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'feSpecularLighting',
29
- 'feTile', 'feTurbulence',
12
+ 'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse',
13
+ 'g', 'defs', 'use', 'symbol', 'clipPath', 'mask', 'pattern', 'image',
14
+ 'text', 'tspan', 'textPath', 'foreignObject', 'linearGradient', 'radialGradient', 'stop',
15
+ 'marker', 'animate', 'animateTransform', 'animateMotion', 'set', 'filter',
16
+ 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix',
17
+ 'feDiffuseLighting', 'feDisplacementMap', 'feFlood', 'feGaussianBlur', 'feImage',
18
+ 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'feSpecularLighting',
19
+ 'feTile', 'feTurbulence',
30
20
  ]);
31
21
  const SVG_NS = 'http://www.w3.org/2000/svg';
32
-
33
- // Track all mounted component contexts for disposal
34
22
  const mountedComponents = new Set();
35
-
36
- // Dispose a component: run effect cleanups, hook cleanups, onCleanup callbacks
23
+ function isDomNode(value) {
24
+ if (!value || typeof value !== 'object') return false;
25
+ if (typeof Node !== 'undefined' && value instanceof Node) return true;
26
+ return typeof value.nodeType === 'number' && typeof value.nodeName === 'string';
27
+ }
28
+ function isVNode(value) {
29
+ return !!value && typeof value === 'object' && (value._vnode === true || 'tag' in value);
30
+ }
37
31
  function disposeComponent(ctx) {
38
- if (ctx.disposed) return;
39
- ctx.disposed = true;
40
-
41
- // Run useEffect cleanup functions
42
- for (const hook of ctx.hooks) {
43
- if (hook && typeof hook === 'object' && 'cleanup' in hook && hook.cleanup) {
44
- try { hook.cleanup(); } catch (e) { console.error('[what] cleanup error:', e); }
45
- }
46
- }
47
-
48
- // Run onCleanup callbacks
49
- if (ctx._cleanupCallbacks) {
50
- for (const fn of ctx._cleanupCallbacks) {
51
- try { fn(); } catch (e) { console.error('[what] onCleanup error:', e); }
52
- }
53
- }
54
-
55
- // Dispose reactive effects
56
- for (const dispose of ctx.effects) {
57
- try { dispose(); } catch (e) { /* effect already disposed */ }
58
- }
59
-
60
- mountedComponents.delete(ctx);
61
- }
62
-
63
- // Dispose all components attached to a DOM subtree
64
- function disposeTree(node) {
65
- if (!node) return;
66
- if (node._componentCtx) {
67
- disposeComponent(node._componentCtx);
68
- }
69
- if (node.childNodes) {
70
- for (const child of node.childNodes) {
71
- disposeTree(child);
72
- }
73
- }
74
- }
75
-
76
- // Mount a component tree into a DOM container
32
+ if (ctx.disposed) return;
33
+ ctx.disposed = true;
34
+ for (const hook of ctx.hooks) {
35
+ if (hook && typeof hook === 'object' && 'cleanup' in hook && hook.cleanup) {
36
+ try { hook.cleanup(); } catch (e) { console.error('[what] cleanup error:', e); }
37
+ }
38
+ }
39
+ if (ctx._cleanupCallbacks) {
40
+ for (const fn of ctx._cleanupCallbacks) {
41
+ try { fn(); } catch (e) { console.error('[what] onCleanup error:', e); }
42
+ }
43
+ }
44
+ for (const dispose of ctx.effects) {
45
+ try { dispose(); } catch (e) { }
46
+ }
47
+ mountedComponents.delete(ctx);
48
+ }
49
+ export function disposeTree(node) {
50
+ if (!node) return;
51
+ if (node._componentCtx) {
52
+ disposeComponent(node._componentCtx);
53
+ }
54
+ if (node._dispose) {
55
+ try { node._dispose(); } catch (e) { }
56
+ }
57
+ if (node.childNodes) {
58
+ for (const child of node.childNodes) {
59
+ disposeTree(child);
60
+ }
61
+ }
62
+ }
77
63
  export function mount(vnode, container) {
78
- if (typeof container === 'string') {
79
- container = document.querySelector(container);
80
- }
81
- disposeTree(container); // Clean up any previous mount
82
- container.textContent = '';
83
- const node = createDOM(vnode, container);
84
- if (node) container.appendChild(node);
85
- return () => {
86
- disposeTree(container);
87
- container.textContent = '';
88
- };
89
- }
90
-
91
- // --- Create DOM from VNode ---
92
-
93
- function createDOM(vnode, parent, isSvg) {
94
- // Null/false/true placeholder comment (preserves child indices for reconciliation)
95
- if (vnode == null || vnode === false || vnode === true) {
96
- return document.createComment('');
97
- }
98
-
99
- // Text
100
- if (typeof vnode === 'string' || typeof vnode === 'number') {
101
- return document.createTextNode(String(vnode));
102
- }
103
-
104
- // Array (fragment)
105
- if (Array.isArray(vnode)) {
106
- const frag = document.createDocumentFragment();
107
- for (const child of vnode) {
108
- const node = createDOM(child, parent, isSvg);
109
- if (node) frag.appendChild(node);
110
- }
111
- return frag;
112
- }
113
-
114
- // Component
115
- if (typeof vnode.tag === 'function') {
116
- return createComponent(vnode, parent, isSvg);
117
- }
118
-
119
- // Detect SVG context: either we're already in SVG, or this tag is an SVG element
120
- const svgContext = isSvg || vnode.tag === 'svg' || SVG_ELEMENTS.has(vnode.tag);
121
-
122
- // HTML or SVG Element
123
- const el = svgContext
124
- ? document.createElementNS(SVG_NS, vnode.tag)
125
- : document.createElement(vnode.tag);
126
-
127
- applyProps(el, vnode.props, {}, svgContext);
128
- for (const child of vnode.children) {
129
- const node = createDOM(child, el, svgContext && vnode.tag !== 'foreignObject');
130
- if (node) el.appendChild(node);
131
- }
132
-
133
- // Store vnode on element for diffing
134
- el._vnode = vnode;
135
- return el;
136
- }
137
-
138
- // --- Component Rendering ---
139
-
64
+ if (typeof container === 'string') {
65
+ container = document.querySelector(container);
66
+ }
67
+ disposeTree(container);
68
+ container.textContent = '';
69
+ const node = createDOM(vnode, container);
70
+ if (node) container.appendChild(node);
71
+ return () => {
72
+ disposeTree(container);
73
+ container.textContent = '';
74
+ };
75
+ }
76
+ export function createDOM(vnode, parent, isSvg) {
77
+ if (vnode == null || vnode === false || vnode === true) {
78
+ return document.createComment('');
79
+ }
80
+ if (typeof vnode === 'string' || typeof vnode === 'number') {
81
+ return document.createTextNode(String(vnode));
82
+ }
83
+ if (isDomNode(vnode)) {
84
+ return vnode;
85
+ }
86
+ if (typeof vnode === 'function') {
87
+ const wrapper = document.createElement('what-c');
88
+ let mounted = false;
89
+ const dispose = effect(() => {
90
+ const val = vnode();
91
+ const vnodes = (val == null || val === false || val === true)
92
+ ? []
93
+ : Array.isArray(val) ? val : [val];
94
+ if (!mounted) {
95
+ mounted = true;
96
+ for (const v of vnodes) {
97
+ const node = createDOM(v, wrapper, parent?._isSvg);
98
+ if (node) wrapper.appendChild(node);
99
+ }
100
+ } else {
101
+ reconcileChildren(wrapper, vnodes);
102
+ }
103
+ });
104
+ wrapper._dispose = dispose;
105
+ return wrapper;
106
+ }
107
+ if (Array.isArray(vnode)) {
108
+ const frag = document.createDocumentFragment();
109
+ for (const child of vnode) {
110
+ const node = createDOM(child, parent, isSvg);
111
+ if (node) frag.appendChild(node);
112
+ }
113
+ return frag;
114
+ }
115
+ if (!isVNode(vnode)) {
116
+ return document.createTextNode(String(vnode));
117
+ }
118
+ if (typeof vnode.tag === 'function') {
119
+ return createComponent(vnode, parent, isSvg);
120
+ }
121
+ const svgContext = isSvg || vnode.tag === 'svg' || SVG_ELEMENTS.has(vnode.tag);
122
+ const el = svgContext
123
+ ? document.createElementNS(SVG_NS, vnode.tag)
124
+ : document.createElement(vnode.tag);
125
+ applyProps(el, vnode.props, {}, svgContext);
126
+ const hasRawHtml = vnode.props && (
127
+ Object.prototype.hasOwnProperty.call(vnode.props, 'dangerouslySetInnerHTML') ||
128
+ Object.prototype.hasOwnProperty.call(vnode.props, 'innerHTML')
129
+ );
130
+ if (!hasRawHtml) {
131
+ for (const child of vnode.children) {
132
+ const node = createDOM(child, el, svgContext && vnode.tag !== 'foreignObject');
133
+ if (node) el.appendChild(node);
134
+ }
135
+ }
136
+ el._vnode = vnode;
137
+ return el;
138
+ }
140
139
  const componentStack = [];
141
-
142
140
  export function getCurrentComponent() {
143
- return componentStack[componentStack.length - 1];
141
+ return componentStack[componentStack.length - 1];
144
142
  }
145
-
146
- // Inject into components.js and helpers.js to avoid circular imports
147
143
  _injectGetCurrentComponent(getCurrentComponent);
148
144
  _setComponentRef(getCurrentComponent);
149
-
150
145
  export function getComponentStack() {
151
- return componentStack;
146
+ return componentStack;
152
147
  }
153
-
154
148
  function createComponent(vnode, parent, isSvg) {
155
- const { tag: Component, props, children } = vnode;
156
-
157
- // Handle special boundary components
158
- if (Component === '__errorBoundary' || vnode.tag === '__errorBoundary') {
159
- return createErrorBoundary(vnode, parent);
160
- }
161
- if (Component === '__suspense' || vnode.tag === '__suspense') {
162
- return createSuspenseBoundary(vnode, parent);
163
- }
164
- if (Component === '__portal' || vnode.tag === '__portal') {
165
- return createPortal(vnode, parent);
166
- }
167
-
168
- // Component context for hooks
169
- const ctx = {
170
- hooks: [],
171
- hookIndex: 0,
172
- effects: [],
173
- cleanups: [],
174
- mounted: false,
175
- disposed: false,
176
- Component, // Store for identity check in patchNode
177
- _parentCtx: componentStack[componentStack.length - 1] || null,
178
- // Inherit error boundary from parent context chain
179
- _errorBoundary: (() => {
180
- let p = componentStack[componentStack.length - 1];
181
- while (p) {
182
- if (p._errorBoundary) return p._errorBoundary;
183
- p = p._parentCtx;
184
- }
185
- return null;
186
- })()
187
- };
188
-
189
- // Wrapper element: <what-c display:contents> for HTML, <g> for SVG
190
- // Note: <what-c> custom element sets display:contents in its constructor
191
- let wrapper;
192
- if (isSvg) {
193
- wrapper = document.createElementNS(SVG_NS, 'g');
194
- } else {
195
- wrapper = document.createElement('what-c');
196
- }
197
- wrapper._componentCtx = ctx;
198
- wrapper._isSvg = !!isSvg;
199
- ctx._wrapper = wrapper;
200
-
201
- // Track for disposal
202
- mountedComponents.add(ctx);
203
-
204
- // Props signal for reactive updates from parent
205
- const propsSignal = signal({ ...props, children });
206
- ctx._propsSignal = propsSignal;
207
-
208
- // Reactive render: re-renders when signals used inside change
209
- const dispose = effect(() => {
210
- if (ctx.disposed) return;
211
- ctx.hookIndex = 0;
212
-
213
- componentStack.push(ctx);
214
-
215
- let result;
216
- try {
217
- result = Component(propsSignal());
218
- } catch (error) {
219
- componentStack.pop();
220
- if (!reportError(error, ctx)) {
221
- console.error('[what] Uncaught error in component:', Component.name || 'Anonymous', error);
222
- throw error;
223
- }
224
- return;
225
- }
226
-
227
- componentStack.pop();
228
-
229
- const vnodes = Array.isArray(result) ? result : [result];
230
-
231
- if (!ctx.mounted) {
232
- // Initial mount
233
- ctx.mounted = true;
234
-
235
- // Run onMount callbacks after DOM is ready
236
- if (ctx._mountCallbacks) {
237
- queueMicrotask(() => {
238
- if (ctx.disposed) return;
239
- for (const fn of ctx._mountCallbacks) {
240
- try { fn(); } catch (e) { console.error('[what] onMount error:', e); }
241
- }
242
- });
243
- }
244
-
245
- for (const v of vnodes) {
246
- const node = createDOM(v, wrapper, isSvg);
247
- if (node) wrapper.appendChild(node);
248
- }
249
- } else {
250
- // Update: reconcile children inside wrapper
251
- reconcileChildren(wrapper, vnodes);
252
- }
253
- });
254
-
255
- ctx.effects.push(dispose);
256
- return wrapper;
257
- }
258
-
259
- // Error boundary component handler
149
+ const { tag: Component, props, children } = vnode;
150
+ if (Component === '__errorBoundary' || vnode.tag === '__errorBoundary') {
151
+ return createErrorBoundary(vnode, parent);
152
+ }
153
+ if (Component === '__suspense' || vnode.tag === '__suspense') {
154
+ return createSuspenseBoundary(vnode, parent);
155
+ }
156
+ if (Component === '__portal' || vnode.tag === '__portal') {
157
+ return createPortal(vnode, parent);
158
+ }
159
+ const ctx = {
160
+ hooks: [],
161
+ hookIndex: 0,
162
+ effects: [],
163
+ cleanups: [],
164
+ mounted: false,
165
+ disposed: false,
166
+ Component,
167
+ _parentCtx: componentStack[componentStack.length - 1] || null,
168
+ _errorBoundary: (() => {
169
+ let p = componentStack[componentStack.length - 1];
170
+ while (p) {
171
+ if (p._errorBoundary) return p._errorBoundary;
172
+ p = p._parentCtx;
173
+ }
174
+ return null;
175
+ })()
176
+ };
177
+ let wrapper;
178
+ if (isSvg) {
179
+ wrapper = document.createElementNS(SVG_NS, 'g');
180
+ } else {
181
+ wrapper = document.createElement('what-c');
182
+ }
183
+ wrapper._componentCtx = ctx;
184
+ wrapper._isSvg = !!isSvg;
185
+ ctx._wrapper = wrapper;
186
+ mountedComponents.add(ctx);
187
+ const propsSignal = signal({ ...props, children });
188
+ ctx._propsSignal = propsSignal;
189
+ const dispose = effect(() => {
190
+ if (ctx.disposed) return;
191
+ ctx.hookIndex = 0;
192
+ componentStack.push(ctx);
193
+ let result;
194
+ try {
195
+ result = Component(propsSignal());
196
+ } catch (error) {
197
+ componentStack.pop();
198
+ if (!reportError(error, ctx)) {
199
+ console.error('[what] Uncaught error in component:', Component.name || 'Anonymous', error);
200
+ throw error;
201
+ }
202
+ return;
203
+ }
204
+ componentStack.pop();
205
+ const vnodes = Array.isArray(result) ? result : [result];
206
+ if (!ctx.mounted) {
207
+ ctx.mounted = true;
208
+ if (ctx._mountCallbacks) {
209
+ queueMicrotask(() => {
210
+ if (ctx.disposed) return;
211
+ for (const fn of ctx._mountCallbacks) {
212
+ try { fn(); } catch (e) { console.error('[what] onMount error:', e); }
213
+ }
214
+ });
215
+ }
216
+ for (const v of vnodes) {
217
+ const node = createDOM(v, wrapper, isSvg);
218
+ if (node) wrapper.appendChild(node);
219
+ }
220
+ } else {
221
+ reconcileChildren(wrapper, vnodes);
222
+ }
223
+ });
224
+ ctx.effects.push(dispose);
225
+ wrapper._vnode = vnode;
226
+ return wrapper;
227
+ }
260
228
  function createErrorBoundary(vnode, parent) {
261
- const { errorState, handleError, fallback, reset } = vnode.props;
262
- const children = vnode.children;
263
-
264
- const wrapper = document.createElement('what-c');
265
- wrapper.style.display = 'contents';
266
-
267
- // Create a boundary context so child components can find this boundary via _parentCtx chain
268
- const boundaryCtx = {
269
- hooks: [], hookIndex: 0, effects: [], cleanups: [],
270
- mounted: false, disposed: false,
271
- _parentCtx: componentStack[componentStack.length - 1] || null,
272
- _errorBoundary: handleError,
273
- };
274
- wrapper._componentCtx = boundaryCtx;
275
-
276
- const dispose = effect(() => {
277
- const error = errorState();
278
-
279
- // Push boundary context so child components inherit _errorBoundary via _parentCtx
280
- componentStack.push(boundaryCtx);
281
-
282
- let vnodes;
283
- if (error) {
284
- vnodes = typeof fallback === 'function' ? [fallback({ error, reset })] : [fallback];
285
- } else {
286
- vnodes = children;
287
- }
288
-
289
- componentStack.pop();
290
- vnodes = Array.isArray(vnodes) ? vnodes : [vnodes];
291
-
292
- if (wrapper.childNodes.length === 0) {
293
- for (const v of vnodes) {
294
- const node = createDOM(v, wrapper);
295
- if (node) wrapper.appendChild(node);
296
- }
297
- } else {
298
- reconcileChildren(wrapper, vnodes);
299
- }
300
- });
301
-
302
- boundaryCtx.effects.push(dispose);
303
- return wrapper;
304
- }
305
-
306
- // Suspense boundary component handler
229
+ const { errorState, handleError, fallback, reset } = vnode.props;
230
+ const children = vnode.children;
231
+ const wrapper = document.createElement('what-c');
232
+ wrapper.style.display = 'contents';
233
+ const boundaryCtx = {
234
+ hooks: [], hookIndex: 0, effects: [], cleanups: [],
235
+ mounted: false, disposed: false,
236
+ _parentCtx: componentStack[componentStack.length - 1] || null,
237
+ _errorBoundary: handleError,
238
+ };
239
+ wrapper._componentCtx = boundaryCtx;
240
+ const dispose = effect(() => {
241
+ const error = errorState();
242
+ componentStack.push(boundaryCtx);
243
+ let vnodes;
244
+ if (error) {
245
+ vnodes = typeof fallback === 'function' ? [fallback({ error, reset })] : [fallback];
246
+ } else {
247
+ vnodes = children;
248
+ }
249
+ componentStack.pop();
250
+ vnodes = Array.isArray(vnodes) ? vnodes : [vnodes];
251
+ if (wrapper.childNodes.length === 0) {
252
+ for (const v of vnodes) {
253
+ const node = createDOM(v, wrapper);
254
+ if (node) wrapper.appendChild(node);
255
+ }
256
+ } else {
257
+ reconcileChildren(wrapper, vnodes);
258
+ }
259
+ });
260
+ boundaryCtx.effects.push(dispose);
261
+ return wrapper;
262
+ }
307
263
  function createSuspenseBoundary(vnode, parent) {
308
- const { boundary, fallback, loading } = vnode.props;
309
- const children = vnode.children;
310
-
311
- const wrapper = document.createElement('what-c');
312
- wrapper.style.display = 'contents';
313
-
314
- // Create a boundary context to store the dispose function for cleanup
315
- const boundaryCtx = {
316
- hooks: [], hookIndex: 0, effects: [], cleanups: [],
317
- mounted: false, disposed: false,
318
- _parentCtx: componentStack[componentStack.length - 1] || null,
319
- };
320
- wrapper._componentCtx = boundaryCtx;
321
-
322
- const dispose = effect(() => {
323
- const isLoading = loading();
324
- const vnodes = isLoading ? [fallback] : children;
325
- const normalized = Array.isArray(vnodes) ? vnodes : [vnodes];
326
-
327
- if (wrapper.childNodes.length === 0) {
328
- for (const v of normalized) {
329
- const node = createDOM(v, wrapper);
330
- if (node) wrapper.appendChild(node);
331
- }
332
- } else {
333
- reconcileChildren(wrapper, normalized);
334
- }
335
- });
336
-
337
- boundaryCtx.effects.push(dispose);
338
- return wrapper;
339
- }
340
-
341
- // Portal component handler — renders children into a different DOM container
264
+ const { boundary, fallback, loading } = vnode.props;
265
+ const children = vnode.children;
266
+ const wrapper = document.createElement('what-c');
267
+ wrapper.style.display = 'contents';
268
+ const boundaryCtx = {
269
+ hooks: [], hookIndex: 0, effects: [], cleanups: [],
270
+ mounted: false, disposed: false,
271
+ _parentCtx: componentStack[componentStack.length - 1] || null,
272
+ };
273
+ wrapper._componentCtx = boundaryCtx;
274
+ const dispose = effect(() => {
275
+ const isLoading = loading();
276
+ const vnodes = isLoading ? [fallback] : children;
277
+ const normalized = Array.isArray(vnodes) ? vnodes : [vnodes];
278
+ if (wrapper.childNodes.length === 0) {
279
+ for (const v of normalized) {
280
+ const node = createDOM(v, wrapper);
281
+ if (node) wrapper.appendChild(node);
282
+ }
283
+ } else {
284
+ reconcileChildren(wrapper, normalized);
285
+ }
286
+ });
287
+ boundaryCtx.effects.push(dispose);
288
+ return wrapper;
289
+ }
342
290
  function createPortal(vnode, parent) {
343
- const { container } = vnode.props;
344
- const children = vnode.children;
345
-
346
- if (!container) {
347
- console.warn('[what] Portal: target container not found');
348
- return document.createComment('portal:empty');
349
- }
350
-
351
- // Create a boundary context for cleanup
352
- const portalCtx = {
353
- hooks: [], hookIndex: 0, effects: [], cleanups: [],
354
- mounted: false, disposed: false,
355
- _parentCtx: componentStack[componentStack.length - 1] || null,
356
- };
357
-
358
- // Placeholder in the original tree for reconciliation
359
- const placeholder = document.createComment('portal');
360
- placeholder._componentCtx = portalCtx;
361
-
362
- // Render children into the target container
363
- const portalNodes = [];
364
- for (const child of children) {
365
- const node = createDOM(child, container);
366
- if (node) {
367
- container.appendChild(node);
368
- portalNodes.push(node);
369
- }
370
- }
371
-
372
- // Register cleanup to remove portal nodes when placeholder is disposed
373
- portalCtx._cleanupCallbacks = [() => {
374
- for (const node of portalNodes) {
375
- disposeTree(node);
376
- if (node.parentNode) node.parentNode.removeChild(node);
377
- }
378
- }];
379
-
380
- return placeholder;
381
- }
382
-
383
- // --- Reconciliation ---
384
- // Diff old DOM nodes against new VNodes, patch in place.
385
- // Uses keyed reconciliation with LIS (Longest Increasing Subsequence) for minimal DOM moves.
386
-
291
+ const { container } = vnode.props;
292
+ const children = vnode.children;
293
+ if (!container) {
294
+ console.warn('[what] Portal: target container not found');
295
+ return document.createComment('portal:empty');
296
+ }
297
+ const portalCtx = {
298
+ hooks: [], hookIndex: 0, effects: [], cleanups: [],
299
+ mounted: false, disposed: false,
300
+ _parentCtx: componentStack[componentStack.length - 1] || null,
301
+ };
302
+ const placeholder = document.createComment('portal');
303
+ placeholder._componentCtx = portalCtx;
304
+ const portalNodes = [];
305
+ for (const child of children) {
306
+ const node = createDOM(child, container);
307
+ if (node) {
308
+ container.appendChild(node);
309
+ portalNodes.push(node);
310
+ }
311
+ }
312
+ portalCtx._cleanupCallbacks = [() => {
313
+ for (const node of portalNodes) {
314
+ disposeTree(node);
315
+ if (node.parentNode) node.parentNode.removeChild(node);
316
+ }
317
+ }];
318
+ return placeholder;
319
+ }
387
320
  function reconcile(parent, oldNodes, newVNodes, beforeMarker) {
388
- if (!parent) return;
389
-
390
- const hasKeys = newVNodes.some(v => v && typeof v === 'object' && v.key != null);
391
-
392
- if (hasKeys) {
393
- reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker);
394
- } else {
395
- reconcileUnkeyed(parent, oldNodes, newVNodes, beforeMarker);
396
- }
397
- }
398
-
399
- // Unkeyed reconciliation (index-based, fast for static lists)
321
+ if (!parent) return;
322
+ const hasKeys = newVNodes.some(v => v && typeof v === 'object' && v.key != null);
323
+ if (hasKeys) {
324
+ reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker);
325
+ } else {
326
+ reconcileUnkeyed(parent, oldNodes, newVNodes, beforeMarker);
327
+ }
328
+ }
400
329
  function reconcileUnkeyed(parent, oldNodes, newVNodes, beforeMarker) {
401
- const maxLen = Math.max(oldNodes.length, newVNodes.length);
402
- const newNodes = [];
403
-
404
- for (let i = 0; i < maxLen; i++) {
405
- const oldNode = oldNodes[i];
406
- const newVNode = newVNodes[i];
407
-
408
- if (i >= newVNodes.length) {
409
- // Remove extra old nodes
410
- if (oldNode && oldNode.parentNode) {
411
- disposeTree(oldNode);
412
- oldNode.parentNode.removeChild(oldNode);
413
- }
414
- continue;
415
- }
416
-
417
- if (i >= oldNodes.length) {
418
- // Append new nodes
419
- const node = createDOM(newVNode, parent);
420
- if (node) {
421
- const ref = getInsertionRef(oldNodes, beforeMarker);
422
- parent.insertBefore(node, ref);
423
- newNodes.push(node);
424
- }
425
- continue;
426
- }
427
-
428
- // Patch existing node
429
- const patched = patchNode(parent, oldNode, newVNode);
430
- newNodes.push(patched);
431
- }
432
-
433
- // Update the reference array
434
- oldNodes.length = 0;
435
- oldNodes.push(...newNodes);
436
- }
437
-
438
- // Keyed reconciliation with LIS algorithm for O(n log n) minimal moves
330
+ const maxLen = Math.max(oldNodes.length, newVNodes.length);
331
+ const newNodes = [];
332
+ for (let i = 0; i < maxLen; i++) {
333
+ const oldNode = oldNodes[i];
334
+ const newVNode = newVNodes[i];
335
+ if (i >= newVNodes.length) {
336
+ if (oldNode && oldNode.parentNode) {
337
+ disposeTree(oldNode);
338
+ oldNode.parentNode.removeChild(oldNode);
339
+ }
340
+ continue;
341
+ }
342
+ if (i >= oldNodes.length) {
343
+ const node = createDOM(newVNode, parent);
344
+ if (node) {
345
+ const ref = getInsertionRef(oldNodes, beforeMarker);
346
+ parent.insertBefore(node, ref);
347
+ newNodes.push(node);
348
+ }
349
+ continue;
350
+ }
351
+ const patched = patchNode(parent, oldNode, newVNode);
352
+ newNodes.push(patched);
353
+ }
354
+ oldNodes.length = 0;
355
+ oldNodes.push(...newNodes);
356
+ }
439
357
  function reconcileKeyed(parent, oldNodes, newVNodes, beforeMarker) {
440
- // Build old key -> { node, index } map
441
- const oldKeyMap = new Map();
442
- for (let i = 0; i < oldNodes.length; i++) {
443
- const node = oldNodes[i];
444
- const key = node._vnode?.key;
445
- if (key != null) {
446
- oldKeyMap.set(key, { node, index: i });
447
- }
448
- }
449
-
450
- const newNodes = [];
451
- const newLen = newVNodes.length;
452
-
453
- // First pass: match keys and find reusable nodes
454
- const sources = new Array(newLen).fill(-1); // Maps new index to old index
455
- const reused = new Set();
456
-
457
- for (let i = 0; i < newLen; i++) {
458
- const vnode = newVNodes[i];
459
- const key = vnode?.key;
460
- if (key != null && oldKeyMap.has(key)) {
461
- const { node: oldNode, index: oldIndex } = oldKeyMap.get(key);
462
- sources[i] = oldIndex;
463
- reused.add(oldIndex);
464
- }
465
- }
466
-
467
- // Remove nodes that aren't reused
468
- for (let i = 0; i < oldNodes.length; i++) {
469
- if (!reused.has(i) && oldNodes[i]?.parentNode) {
470
- disposeTree(oldNodes[i]);
471
- oldNodes[i].parentNode.removeChild(oldNodes[i]);
472
- }
473
- }
474
-
475
- // Find LIS (Longest Increasing Subsequence) of old indices.
476
- // The LIS tells us which reused nodes are already in correct relative order
477
- // and don't need to be moved. Only nodes NOT in the LIS need DOM moves.
478
- //
479
- // Step 1: Filter out -1 entries (new nodes with no old counterpart).
480
- // Step 2: Compute LIS on the filtered array. Result: indices into the filtered array.
481
- // Step 3: Map filtered-array indices back to original sources[] indices (new-VNode indices).
482
- // For each LIS index `lis[i]`, we find the `lis[i]`-th non-negative entry in sources[]
483
- // and return its position in the original sources array.
484
- // Build filteredToOriginal map in one O(n) pass instead of O(n²) nested loop
485
- const filtered = [];
486
- const filteredToOriginal = [];
487
- for (let j = 0; j < sources.length; j++) {
488
- if (sources[j] !== -1) {
489
- filteredToOriginal.push(j);
490
- filtered.push(sources[j]);
491
- }
492
- }
493
- const lis = longestIncreasingSubsequence(filtered);
494
- const lisSet = new Set(lis.map(i => filteredToOriginal[i]));
495
-
496
- // Build new nodes array and move/create as needed
497
- let lastInserted = beforeMarker?.nextSibling || null;
498
-
499
- // Process in reverse order for correct insertion
500
- for (let i = newLen - 1; i >= 0; i--) {
501
- const vnode = newVNodes[i];
502
- const key = vnode?.key;
503
- const oldEntry = key != null ? oldKeyMap.get(key) : null;
504
-
505
- if (oldEntry && sources[i] !== -1) {
506
- // Reuse existing node
507
- const oldNode = oldEntry.node;
508
- // Patch props/children
509
- const patched = patchNode(parent, oldNode, vnode);
510
- newNodes[i] = patched;
511
-
512
- // Move if not in LIS
513
- if (!lisSet.has(i) && patched.parentNode) {
514
- parent.insertBefore(patched, lastInserted);
515
- }
516
- lastInserted = patched;
517
- } else {
518
- // Create new node
519
- const node = createDOM(vnode, parent);
520
- if (node) {
521
- parent.insertBefore(node, lastInserted);
522
- lastInserted = node;
523
- }
524
- newNodes[i] = node;
525
- }
526
- }
527
-
528
- // Update the reference array
529
- oldNodes.length = 0;
530
- oldNodes.push(...newNodes.filter(Boolean));
531
- }
532
-
533
- // Longest Increasing Subsequence - O(n log n)
534
- // Returns indices of elements that form the LIS
358
+ const oldKeyMap = new Map();
359
+ for (let i = 0; i < oldNodes.length; i++) {
360
+ const node = oldNodes[i];
361
+ const key = node._vnode?.key;
362
+ if (key != null) {
363
+ oldKeyMap.set(key, { node, index: i });
364
+ }
365
+ }
366
+ const newNodes = [];
367
+ const newLen = newVNodes.length;
368
+ const sources = new Array(newLen).fill(-1);
369
+ const reused = new Set();
370
+ for (let i = 0; i < newLen; i++) {
371
+ const vnode = newVNodes[i];
372
+ const key = vnode?.key;
373
+ if (key != null && oldKeyMap.has(key)) {
374
+ const { node: oldNode, index: oldIndex } = oldKeyMap.get(key);
375
+ sources[i] = oldIndex;
376
+ reused.add(oldIndex);
377
+ }
378
+ }
379
+ for (let i = 0; i < oldNodes.length; i++) {
380
+ if (!reused.has(i) && oldNodes[i]?.parentNode) {
381
+ disposeTree(oldNodes[i]);
382
+ oldNodes[i].parentNode.removeChild(oldNodes[i]);
383
+ }
384
+ }
385
+ const filtered = [];
386
+ const filteredToOriginal = [];
387
+ for (let j = 0; j < sources.length; j++) {
388
+ if (sources[j] !== -1) {
389
+ filteredToOriginal.push(j);
390
+ filtered.push(sources[j]);
391
+ }
392
+ }
393
+ const lis = longestIncreasingSubsequence(filtered);
394
+ const lisSet = new Set(lis.map(i => filteredToOriginal[i]));
395
+ let lastInserted = beforeMarker?.nextSibling || null;
396
+ for (let i = newLen - 1; i >= 0; i--) {
397
+ const vnode = newVNodes[i];
398
+ const key = vnode?.key;
399
+ const oldEntry = key != null ? oldKeyMap.get(key) : null;
400
+ if (oldEntry && sources[i] !== -1) {
401
+ const oldNode = oldEntry.node;
402
+ const patched = patchNode(parent, oldNode, vnode);
403
+ newNodes[i] = patched;
404
+ if (!lisSet.has(i) && patched.parentNode) {
405
+ parent.insertBefore(patched, lastInserted);
406
+ }
407
+ lastInserted = patched;
408
+ } else {
409
+ const node = createDOM(vnode, parent);
410
+ if (node) {
411
+ parent.insertBefore(node, lastInserted);
412
+ lastInserted = node;
413
+ }
414
+ newNodes[i] = node;
415
+ }
416
+ }
417
+ oldNodes.length = 0;
418
+ oldNodes.push(...newNodes.filter(Boolean));
419
+ }
535
420
  function longestIncreasingSubsequence(arr) {
536
- if (arr.length === 0) return [];
537
-
538
- const n = arr.length;
539
- const dp = new Array(n).fill(1); // Length of LIS ending at i
540
- const parent = new Array(n).fill(-1); // Parent index for reconstruction
541
- const tails = [0]; // Indices of smallest tail elements
542
-
543
- for (let i = 1; i < n; i++) {
544
- if (arr[i] > arr[tails[tails.length - 1]]) {
545
- parent[i] = tails[tails.length - 1];
546
- tails.push(i);
547
- } else {
548
- // Binary search for the smallest element >= arr[i]
549
- let lo = 0, hi = tails.length - 1;
550
- while (lo < hi) {
551
- const mid = (lo + hi) >> 1;
552
- if (arr[tails[mid]] < arr[i]) lo = mid + 1;
553
- else hi = mid;
554
- }
555
- if (arr[i] < arr[tails[lo]]) {
556
- if (lo > 0) parent[i] = tails[lo - 1];
557
- tails[lo] = i;
558
- }
559
- }
560
- }
561
-
562
- // Reconstruct LIS
563
- const result = [];
564
- let k = tails[tails.length - 1];
565
- while (k !== -1) {
566
- result.push(k);
567
- k = parent[k];
568
- }
569
- return result.reverse();
570
- }
571
-
421
+ if (arr.length === 0) return [];
422
+ const n = arr.length;
423
+ const dp = new Array(n).fill(1);
424
+ const parent = new Array(n).fill(-1);
425
+ const tails = [0];
426
+ for (let i = 1; i < n; i++) {
427
+ if (arr[i] > arr[tails[tails.length - 1]]) {
428
+ parent[i] = tails[tails.length - 1];
429
+ tails.push(i);
430
+ } else {
431
+ let lo = 0, hi = tails.length - 1;
432
+ while (lo < hi) {
433
+ const mid = (lo + hi) >> 1;
434
+ if (arr[tails[mid]] < arr[i]) lo = mid + 1;
435
+ else hi = mid;
436
+ }
437
+ if (arr[i] < arr[tails[lo]]) {
438
+ if (lo > 0) parent[i] = tails[lo - 1];
439
+ tails[lo] = i;
440
+ }
441
+ }
442
+ }
443
+ const result = [];
444
+ let k = tails[tails.length - 1];
445
+ while (k !== -1) {
446
+ result.push(k);
447
+ k = parent[k];
448
+ }
449
+ return result.reverse();
450
+ }
572
451
  function getInsertionRef(nodes, marker) {
573
- if (nodes.length > 0) {
574
- const last = nodes[nodes.length - 1];
575
- return last.nextSibling;
576
- }
577
- return marker ? marker.nextSibling : null;
578
- }
579
-
580
- // Helper: clean up array marker range (startMarker .. endMarker) and return a clean replacement node
452
+ if (nodes.length > 0) {
453
+ const last = nodes[nodes.length - 1];
454
+ return last.nextSibling;
455
+ }
456
+ return marker ? marker.nextSibling : null;
457
+ }
581
458
  function cleanupArrayMarkers(parent, startMarker) {
582
- const endMarker = startMarker._arrayEnd;
583
- if (!endMarker) return null;
584
- // Remove all nodes between start and end markers
585
- let node = startMarker.nextSibling;
586
- while (node && node !== endMarker) {
587
- const next = node.nextSibling;
588
- disposeTree(node);
589
- parent.removeChild(node);
590
- node = next;
591
- }
592
- // Remove end marker
593
- if (endMarker.parentNode) parent.removeChild(endMarker);
594
- return startMarker;
595
- }
596
-
459
+ const endMarker = startMarker._arrayEnd;
460
+ if (!endMarker) return null;
461
+ let node = startMarker.nextSibling;
462
+ while (node && node !== endMarker) {
463
+ const next = node.nextSibling;
464
+ disposeTree(node);
465
+ parent.removeChild(node);
466
+ node = next;
467
+ }
468
+ if (endMarker.parentNode) parent.removeChild(endMarker);
469
+ return startMarker;
470
+ }
597
471
  function patchNode(parent, domNode, vnode) {
598
- // Null/removed keep placeholder or replace with one
599
- if (vnode == null || vnode === false || vnode === true) {
600
- // Handle array marker cleanup
601
- if (domNode && domNode.nodeType === 8 && domNode._arrayEnd) {
602
- cleanupArrayMarkers(parent, domNode);
603
- const placeholder = document.createComment('');
604
- parent.replaceChild(placeholder, domNode);
605
- return placeholder;
606
- }
607
- if (domNode && domNode.nodeType === 8 && !domNode._componentCtx) {
608
- return domNode; // already a placeholder comment
609
- }
610
- const placeholder = document.createComment('');
611
- if (domNode && domNode.parentNode) {
612
- disposeTree(domNode);
613
- parent.replaceChild(placeholder, domNode);
614
- }
615
- return placeholder;
616
- }
617
-
618
- // Text
619
- if (typeof vnode === 'string' || typeof vnode === 'number') {
620
- const text = String(vnode);
621
- // Clean up array markers if transitioning from array to text
622
- if (domNode && domNode.nodeType === 8 && domNode._arrayEnd) {
623
- cleanupArrayMarkers(parent, domNode);
624
- const newNode = document.createTextNode(text);
625
- parent.replaceChild(newNode, domNode);
626
- return newNode;
627
- }
628
- if (domNode.nodeType === 3) {
629
- if (domNode.textContent !== text) domNode.textContent = text;
630
- return domNode;
631
- }
632
- const newNode = document.createTextNode(text);
633
- disposeTree(domNode);
634
- parent.replaceChild(newNode, domNode);
635
- return newNode;
636
- }
637
-
638
- // Array — use marker comments to bracket the range (DocumentFragment empties on append)
639
- if (Array.isArray(vnode)) {
640
- // If domNode is already an array marker, reconcile contents in place
641
- if (domNode && domNode.nodeType === 8 && domNode._arrayEnd) {
642
- const endMarker = domNode._arrayEnd;
643
- // Collect existing children between markers
644
- const oldChildren = [];
645
- let node = domNode.nextSibling;
646
- while (node && node !== endMarker) {
647
- oldChildren.push(node);
648
- node = node.nextSibling;
649
- }
650
- // Reconcile the array contents
651
- const maxLen = Math.max(oldChildren.length, vnode.length);
652
- for (let i = 0; i < maxLen; i++) {
653
- if (i >= vnode.length) {
654
- // Remove extra old nodes
655
- if (oldChildren[i]?.parentNode) {
656
- disposeTree(oldChildren[i]);
657
- parent.removeChild(oldChildren[i]);
658
- }
659
- } else if (i >= oldChildren.length) {
660
- // Append new nodes before end marker
661
- const newNode = createDOM(vnode[i], parent);
662
- if (newNode) parent.insertBefore(newNode, endMarker);
663
- } else {
664
- // Patch existing
665
- patchNode(parent, oldChildren[i], vnode[i]);
666
- }
667
- }
668
- return domNode;
669
- }
670
- // Fresh array: create markers
671
- const startMarker = document.createComment('[');
672
- const endMarker = document.createComment(']');
673
- disposeTree(domNode);
674
- parent.replaceChild(endMarker, domNode);
675
- parent.insertBefore(startMarker, endMarker);
676
- for (const v of vnode) {
677
- const node = createDOM(v, parent);
678
- if (node) parent.insertBefore(node, endMarker);
679
- }
680
- startMarker._arrayEnd = endMarker;
681
- return startMarker;
682
- }
683
-
684
- // Component
685
- if (typeof vnode.tag === 'function') {
686
- // Check if old node is a component wrapper for the same component
687
- if (domNode._componentCtx && !domNode._componentCtx.disposed
688
- && domNode._componentCtx.Component === vnode.tag) {
689
- // Same component — update props reactively, let its effect re-render
690
- domNode._componentCtx._propsSignal.set({ ...vnode.props, children: vnode.children });
691
- return domNode;
692
- }
693
- // Different component or not a component — dispose old, create new
694
- disposeTree(domNode);
695
- const node = createComponent(vnode, parent);
696
- parent.replaceChild(node, domNode);
697
- return node;
698
- }
699
-
700
- // Element: same tag? Patch props + children
701
- if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag) {
702
- const oldProps = domNode._vnode?.props || {};
703
- applyProps(domNode, vnode.props, oldProps);
704
- reconcileChildren(domNode, vnode.children);
705
- domNode._vnode = vnode;
706
- return domNode;
707
- }
708
-
709
- // Different tag: replace entirely
710
- const newNode = createDOM(vnode, parent);
711
- disposeTree(domNode);
712
- parent.replaceChild(newNode, domNode);
713
- return newNode;
714
- }
715
-
472
+ if (vnode == null || vnode === false || vnode === true) {
473
+ if (domNode && domNode.nodeType === 8 && domNode._arrayEnd) {
474
+ cleanupArrayMarkers(parent, domNode);
475
+ const placeholder = document.createComment('');
476
+ parent.replaceChild(placeholder, domNode);
477
+ return placeholder;
478
+ }
479
+ if (domNode && domNode.nodeType === 8 && !domNode._componentCtx) {
480
+ return domNode;
481
+ }
482
+ const placeholder = document.createComment('');
483
+ if (domNode && domNode.parentNode) {
484
+ disposeTree(domNode);
485
+ parent.replaceChild(placeholder, domNode);
486
+ }
487
+ return placeholder;
488
+ }
489
+ if (typeof vnode === 'function') {
490
+ const wrapper = document.createElement('what-c');
491
+ let mounted = false;
492
+ const dispose = effect(() => {
493
+ const val = vnode();
494
+ const vnodes = (val == null || val === false || val === true)
495
+ ? []
496
+ : Array.isArray(val) ? val : [val];
497
+ if (!mounted) {
498
+ mounted = true;
499
+ for (const v of vnodes) {
500
+ const node = createDOM(v, wrapper);
501
+ if (node) wrapper.appendChild(node);
502
+ }
503
+ } else {
504
+ reconcileChildren(wrapper, vnodes);
505
+ }
506
+ });
507
+ wrapper._dispose = dispose;
508
+ if (domNode && domNode.parentNode) {
509
+ disposeTree(domNode);
510
+ parent.replaceChild(wrapper, domNode);
511
+ }
512
+ return wrapper;
513
+ }
514
+ if (isDomNode(vnode)) {
515
+ if (domNode === vnode) return domNode;
516
+ if (domNode && domNode.parentNode) {
517
+ disposeTree(domNode);
518
+ parent.replaceChild(vnode, domNode);
519
+ }
520
+ return vnode;
521
+ }
522
+ if (typeof vnode === 'string' || typeof vnode === 'number') {
523
+ const text = String(vnode);
524
+ if (domNode && domNode.nodeType === 8 && domNode._arrayEnd) {
525
+ cleanupArrayMarkers(parent, domNode);
526
+ const newNode = document.createTextNode(text);
527
+ parent.replaceChild(newNode, domNode);
528
+ return newNode;
529
+ }
530
+ if (domNode.nodeType === 3) {
531
+ if (domNode.textContent !== text) domNode.textContent = text;
532
+ return domNode;
533
+ }
534
+ const newNode = document.createTextNode(text);
535
+ disposeTree(domNode);
536
+ parent.replaceChild(newNode, domNode);
537
+ return newNode;
538
+ }
539
+ if (Array.isArray(vnode)) {
540
+ if (domNode && domNode.nodeType === 8 && domNode._arrayEnd) {
541
+ const endMarker = domNode._arrayEnd;
542
+ const oldChildren = [];
543
+ let node = domNode.nextSibling;
544
+ while (node && node !== endMarker) {
545
+ oldChildren.push(node);
546
+ node = node.nextSibling;
547
+ }
548
+ const maxLen = Math.max(oldChildren.length, vnode.length);
549
+ for (let i = 0; i < maxLen; i++) {
550
+ if (i >= vnode.length) {
551
+ if (oldChildren[i]?.parentNode) {
552
+ disposeTree(oldChildren[i]);
553
+ parent.removeChild(oldChildren[i]);
554
+ }
555
+ } else if (i >= oldChildren.length) {
556
+ const newNode = createDOM(vnode[i], parent);
557
+ if (newNode) parent.insertBefore(newNode, endMarker);
558
+ } else {
559
+ patchNode(parent, oldChildren[i], vnode[i]);
560
+ }
561
+ }
562
+ return domNode;
563
+ }
564
+ const startMarker = document.createComment('[');
565
+ const endMarker = document.createComment(']');
566
+ disposeTree(domNode);
567
+ parent.replaceChild(endMarker, domNode);
568
+ parent.insertBefore(startMarker, endMarker);
569
+ for (const v of vnode) {
570
+ const node = createDOM(v, parent);
571
+ if (node) parent.insertBefore(node, endMarker);
572
+ }
573
+ startMarker._arrayEnd = endMarker;
574
+ return startMarker;
575
+ }
576
+ if (!isVNode(vnode)) {
577
+ const text = String(vnode);
578
+ if (domNode.nodeType === 3) {
579
+ if (domNode.textContent !== text) domNode.textContent = text;
580
+ return domNode;
581
+ }
582
+ const newNode = document.createTextNode(text);
583
+ disposeTree(domNode);
584
+ parent.replaceChild(newNode, domNode);
585
+ return newNode;
586
+ }
587
+ if (typeof vnode.tag === 'function') {
588
+ if (domNode._componentCtx && !domNode._componentCtx.disposed
589
+ && domNode._componentCtx.Component === vnode.tag) {
590
+ domNode._componentCtx._propsSignal.set({ ...vnode.props, children: vnode.children });
591
+ domNode._vnode = vnode;
592
+ return domNode;
593
+ }
594
+ disposeTree(domNode);
595
+ const node = createComponent(vnode, parent);
596
+ parent.replaceChild(node, domNode);
597
+ return node;
598
+ }
599
+ if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag) {
600
+ const oldProps = domNode._vnode?.props || {};
601
+ const nextProps = vnode.props || {};
602
+ const hadRawHtml = Object.prototype.hasOwnProperty.call(oldProps, 'dangerouslySetInnerHTML')
603
+ || Object.prototype.hasOwnProperty.call(oldProps, 'innerHTML');
604
+ const hasRawHtml = Object.prototype.hasOwnProperty.call(nextProps, 'dangerouslySetInnerHTML')
605
+ || Object.prototype.hasOwnProperty.call(nextProps, 'innerHTML');
606
+ if (hasRawHtml && !hadRawHtml) {
607
+ for (const child of Array.from(domNode.childNodes)) {
608
+ disposeTree(child);
609
+ }
610
+ }
611
+ applyProps(domNode, nextProps, oldProps);
612
+ if (!hasRawHtml) {
613
+ reconcileChildren(domNode, vnode.children);
614
+ }
615
+ domNode._vnode = vnode;
616
+ return domNode;
617
+ }
618
+ const newNode = createDOM(vnode, parent);
619
+ disposeTree(domNode);
620
+ parent.replaceChild(newNode, domNode);
621
+ return newNode;
622
+ }
716
623
  function reconcileChildren(parent, newChildVNodes) {
717
- const oldChildren = Array.from(parent.childNodes);
718
-
719
- // Check for keyed children
720
- const hasKeys = newChildVNodes.some(v => v && typeof v === 'object' && v.key != null);
721
-
722
- if (hasKeys) {
723
- // Use keyed reconciliation
724
- reconcileKeyed(parent, oldChildren, newChildVNodes, null);
725
- } else {
726
- // Unkeyed reconciliation
727
- const maxLen = Math.max(oldChildren.length, newChildVNodes.length);
728
-
729
- for (let i = 0; i < maxLen; i++) {
730
- if (i >= newChildVNodes.length) {
731
- // Remove extra
732
- if (oldChildren[i]?.parentNode) {
733
- disposeTree(oldChildren[i]);
734
- parent.removeChild(oldChildren[i]);
735
- }
736
- continue;
737
- }
738
-
739
- if (i >= oldChildren.length) {
740
- // Append new
741
- const node = createDOM(newChildVNodes[i], parent);
742
- if (node) parent.appendChild(node);
743
- continue;
744
- }
745
-
746
- patchNode(parent, oldChildren[i], newChildVNodes[i]);
747
- }
748
- }
749
- }
750
-
751
- // --- Prop Diffing ---
752
- // Only touch DOM for props that actually changed.
753
-
624
+ const oldChildren = Array.from(parent.childNodes);
625
+ const hasKeys = newChildVNodes.some(v => v && typeof v === 'object' && v.key != null);
626
+ if (hasKeys) {
627
+ reconcileKeyed(parent, oldChildren, newChildVNodes, null);
628
+ } else {
629
+ const maxLen = Math.max(oldChildren.length, newChildVNodes.length);
630
+ for (let i = 0; i < maxLen; i++) {
631
+ if (i >= newChildVNodes.length) {
632
+ if (oldChildren[i]?.parentNode) {
633
+ disposeTree(oldChildren[i]);
634
+ parent.removeChild(oldChildren[i]);
635
+ }
636
+ continue;
637
+ }
638
+ if (i >= oldChildren.length) {
639
+ const node = createDOM(newChildVNodes[i], parent);
640
+ if (node) parent.appendChild(node);
641
+ continue;
642
+ }
643
+ patchNode(parent, oldChildren[i], newChildVNodes[i]);
644
+ }
645
+ }
646
+ }
754
647
  function applyProps(el, newProps, oldProps, isSvg) {
755
- newProps = newProps || {};
756
- oldProps = oldProps || {};
757
-
758
- // Remove old props not in new
759
- for (const key in oldProps) {
760
- if (key === 'key' || key === 'ref' || key === 'children') continue;
761
- if (!(key in newProps)) {
762
- removeProp(el, key, oldProps[key]);
763
- }
764
- }
765
-
766
- // Set new/changed props
767
- for (const key in newProps) {
768
- if (key === 'key' || key === 'ref' || key === 'children') continue;
769
- if (newProps[key] !== oldProps[key]) {
770
- setProp(el, key, newProps[key], isSvg);
771
- }
772
- }
773
-
774
- // Handle ref
775
- if (newProps.ref && newProps.ref !== oldProps.ref) {
776
- if (typeof newProps.ref === 'function') newProps.ref(el);
777
- else newProps.ref.current = el;
778
- }
779
- }
780
-
648
+ newProps = newProps || {};
649
+ oldProps = oldProps || {};
650
+ for (const key in oldProps) {
651
+ if (key === 'key' || key === 'ref' || key === 'children') continue;
652
+ if (!(key in newProps)) {
653
+ removeProp(el, key, oldProps[key]);
654
+ }
655
+ }
656
+ for (const key in newProps) {
657
+ if (key === 'key' || key === 'ref' || key === 'children') continue;
658
+ if (newProps[key] !== oldProps[key]) {
659
+ setProp(el, key, newProps[key], isSvg);
660
+ }
661
+ }
662
+ if (newProps.ref && newProps.ref !== oldProps.ref) {
663
+ if (typeof newProps.ref === 'function') newProps.ref(el);
664
+ else newProps.ref.current = el;
665
+ }
666
+ }
781
667
  function setProp(el, key, value, isSvg) {
782
- // Event handlers: onClick -> click
783
- // Wrap in untrack so signal reads in handlers don't create subscriptions
784
- if (key.startsWith('on') && key.length > 2) {
785
- const event = key.slice(2).toLowerCase();
786
- // Store handler for removal
787
- const old = el._events?.[event];
788
- // Skip re-wrapping if same handler function
789
- if (old && old._original === value) return;
790
- if (old) el.removeEventListener(event, old);
791
- if (!el._events) el._events = {};
792
- // Wrap handler to untrack signal reads
793
- const wrappedHandler = (e) => untrack(() => value(e));
794
- wrappedHandler._original = value;
795
- el._events[event] = wrappedHandler;
796
- // Check for _eventOpts (once/capture/passive from compiler)
797
- const eventOpts = value._eventOpts;
798
- el.addEventListener(event, wrappedHandler, eventOpts || undefined);
799
- return;
800
- }
801
-
802
- // className / class
803
- if (key === 'className' || key === 'class') {
804
- if (isSvg) {
805
- el.setAttribute('class', value || '');
806
- } else {
807
- el.className = value || '';
808
- }
809
- return;
810
- }
811
-
812
- // Style object track previous style to remove stale properties
813
- if (key === 'style') {
814
- if (typeof value === 'string') {
815
- el.style.cssText = value;
816
- el._prevStyle = null;
817
- } else if (typeof value === 'object') {
818
- // Remove old style properties not in new style
819
- const oldStyle = el._prevStyle || {};
820
- for (const prop in oldStyle) {
821
- if (!(prop in value)) el.style[prop] = '';
822
- }
823
- for (const prop in value) {
824
- el.style[prop] = value[prop] ?? '';
825
- }
826
- el._prevStyle = { ...value };
827
- }
828
- return;
829
- }
830
-
831
- // dangerouslySetInnerHTML
832
- if (key === 'dangerouslySetInnerHTML') {
833
- el.innerHTML = value.__html;
834
- return;
835
- }
836
-
837
- // Boolean attributes
838
- if (typeof value === 'boolean') {
839
- if (value) el.setAttribute(key, '');
840
- else el.removeAttribute(key);
841
- return;
842
- }
843
-
844
- // data-* and aria-* as attributes
845
- if (key.startsWith('data-') || key.startsWith('aria-')) {
846
- el.setAttribute(key, value);
847
- return;
848
- }
849
-
850
- // SVG: always use setAttribute (SVG properties don't work as DOM properties)
851
- if (isSvg) {
852
- if (value === false || value == null) {
853
- el.removeAttribute(key);
854
- } else {
855
- el.setAttribute(key, value === true ? '' : String(value));
856
- }
857
- return;
858
- }
859
-
860
- // Default: set as property if it exists, otherwise attribute
861
- if (key in el) {
862
- el[key] = value;
863
- } else {
864
- el.setAttribute(key, value);
865
- }
866
- }
867
-
668
+ if (key.startsWith('on') && key.length > 2) {
669
+ const event = key.slice(2).toLowerCase();
670
+ const old = el._events?.[event];
671
+ if (old && old._original === value) return;
672
+ if (old) el.removeEventListener(event, old);
673
+ if (!el._events) el._events = {};
674
+ const wrappedHandler = (e) => untrack(() => value(e));
675
+ wrappedHandler._original = value;
676
+ el._events[event] = wrappedHandler;
677
+ const eventOpts = value._eventOpts;
678
+ el.addEventListener(event, wrappedHandler, eventOpts || undefined);
679
+ return;
680
+ }
681
+ if (key === 'className' || key === 'class') {
682
+ if (isSvg) {
683
+ el.setAttribute('class', value || '');
684
+ } else {
685
+ el.className = value || '';
686
+ }
687
+ return;
688
+ }
689
+ if (key === 'style') {
690
+ if (typeof value === 'string') {
691
+ el.style.cssText = value;
692
+ el._prevStyle = null;
693
+ } else if (typeof value === 'object') {
694
+ const oldStyle = el._prevStyle || {};
695
+ for (const prop in oldStyle) {
696
+ if (!(prop in value)) el.style[prop] = '';
697
+ }
698
+ for (const prop in value) {
699
+ el.style[prop] = value[prop] ?? '';
700
+ }
701
+ el._prevStyle = { ...value };
702
+ }
703
+ return;
704
+ }
705
+ if (key === 'dangerouslySetInnerHTML') {
706
+ el.innerHTML = value?.__html ?? '';
707
+ return;
708
+ }
709
+ if (key === 'innerHTML') {
710
+ if (value && typeof value === 'object' && '__html' in value) {
711
+ el.innerHTML = value.__html ?? '';
712
+ } else {
713
+ el.innerHTML = value ?? '';
714
+ }
715
+ return;
716
+ }
717
+ if (typeof value === 'boolean') {
718
+ if (value) el.setAttribute(key, '');
719
+ else el.removeAttribute(key);
720
+ return;
721
+ }
722
+ if (key.startsWith('data-') || key.startsWith('aria-')) {
723
+ el.setAttribute(key, value);
724
+ return;
725
+ }
726
+ if (isSvg) {
727
+ if (value === false || value == null) {
728
+ el.removeAttribute(key);
729
+ } else {
730
+ el.setAttribute(key, value === true ? '' : String(value));
731
+ }
732
+ return;
733
+ }
734
+ if (key in el) {
735
+ el[key] = value;
736
+ } else {
737
+ el.setAttribute(key, value);
738
+ }
739
+ }
868
740
  function removeProp(el, key, oldValue) {
869
- if (key.startsWith('on') && key.length > 2) {
870
- const event = key.slice(2).toLowerCase();
871
- if (el._events?.[event]) {
872
- el.removeEventListener(event, el._events[event]);
873
- delete el._events[event];
874
- }
875
- return;
876
- }
877
-
878
- if (key === 'className' || key === 'class') {
879
- el.className = '';
880
- return;
881
- }
882
-
883
- if (key === 'style') {
884
- el.style.cssText = '';
885
- el._prevStyle = null;
886
- return;
887
- }
888
-
889
- el.removeAttribute(key);
741
+ if (key.startsWith('on') && key.length > 2) {
742
+ const event = key.slice(2).toLowerCase();
743
+ if (el._events?.[event]) {
744
+ el.removeEventListener(event, el._events[event]);
745
+ delete el._events[event];
746
+ }
747
+ return;
748
+ }
749
+ if (key === 'className' || key === 'class') {
750
+ el.className = '';
751
+ return;
752
+ }
753
+ if (key === 'style') {
754
+ el.style.cssText = '';
755
+ el._prevStyle = null;
756
+ return;
757
+ }
758
+ if (key === 'dangerouslySetInnerHTML' || key === 'innerHTML') {
759
+ el.innerHTML = '';
760
+ return;
890
761
  }
762
+ el.removeAttribute(key);
763
+ }