@plastic-js/plastic 1.0.1

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.
@@ -0,0 +1,1058 @@
1
+ import {
2
+ batch, createComputed, createSignal, createTree, effect, isComputed, isSignal, isTree, runUntracked, toRaw,
3
+ } from './reactivity.js'
4
+ import {
5
+ flattenChildren, isEventProp, normalizeTextNodeValue, toClassMap, toClassTokens,
6
+ } from './utils.js'
7
+ import { getCurrentComputation, setCurrentComputation } from './computation-context.js'
8
+ import { createControlFlow } from './control-flow.js'
9
+ import { isMergedProps, mergeProps } from './merge-props.js'
10
+
11
+ const Fragment = Symbol('Fragment')
12
+ const OWNER = Symbol('owner')
13
+ const COMPONENT_DESCRIPTOR = Symbol('component-descriptor')
14
+ const PENDING_DESCRIPTORS = Symbol('pending-descriptors')
15
+
16
+ // ============ Owner & Lifecycle Management ============
17
+ // Global context for effect scoping and cleanup tracking
18
+ let currentOwner = null
19
+
20
+ const CONTEXT_ID = Symbol('context-id')
21
+
22
+ const getCurrentOwner = ()=> currentOwner
23
+
24
+ const createOwner = (parent = null)=> {
25
+ const owner = {
26
+ parent,
27
+ children: new Set(),
28
+ cleanups: [],
29
+ effects: [],
30
+ contexts: new Map(),
31
+ refs: [],
32
+ mounts: [],
33
+ mounted: false,
34
+ }
35
+ if (parent){
36
+ parent.children.add(owner)
37
+ }
38
+ return owner
39
+ }
40
+
41
+ const runOwnerMounts = (owner)=> {
42
+ runUntracked(()=> {
43
+ owner.children.forEach(child=> runOwnerMounts(child))
44
+ owner.refs.forEach((fn)=> {
45
+ fn()
46
+ })
47
+ owner.mounts.forEach((fn)=> {
48
+ fn()
49
+ })
50
+ owner.mounted = true
51
+ })
52
+ }
53
+
54
+ const runWithOwner = (owner, fn)=> {
55
+ const prev = currentOwner
56
+ currentOwner = owner
57
+ try {
58
+ return fn()
59
+ } finally {
60
+ currentOwner = prev
61
+ }
62
+ }
63
+
64
+ const renderInOwner = (owner, result)=> runWithOwner(owner, ()=> node2Element(result))
65
+
66
+ // Reactive child updates can insert plain DOM wrappers that contain mounted
67
+ // component roots deeper in the subtree, so walk the inserted nodes and run
68
+ // any deferred owner mounts we find.
69
+ const mountOwnedSubtree = (node)=> {
70
+ if (!(node instanceof Node)){
71
+ return
72
+ }
73
+
74
+ const stack = [node]
75
+ while (stack.length){
76
+ const current = stack.pop()
77
+ const owner = current[OWNER]
78
+ if (owner && !owner.mounted && current.isConnected){
79
+ runOwnerMounts(owner)
80
+ }
81
+
82
+ for (const child of current.childNodes){
83
+ stack.push(child)
84
+ }
85
+ }
86
+ }
87
+
88
+ const disposeOwner = (owner)=> {
89
+ if (owner.parent){
90
+ owner.parent.children.delete(owner)
91
+ }
92
+
93
+ // Dispose all child owners recursively
94
+ owner.children.forEach(child=> disposeOwner(child))
95
+ owner.children.clear()
96
+
97
+ // Stop owner-bound effects for this scope during unmount.
98
+ ;[...owner.effects].reverse().forEach((stop)=> {
99
+ if (typeof stop === 'function'){
100
+ stop()
101
+ }
102
+ })
103
+ owner.effects.length = 0
104
+
105
+ ;[...owner.cleanups].reverse().forEach((cleanup)=> {
106
+ if (typeof cleanup === 'function'){
107
+ cleanup()
108
+ }
109
+ })
110
+ owner.cleanups.length = 0
111
+ owner.refs.length = 0
112
+ owner.mounts.length = 0
113
+ }
114
+
115
+ const flushCleanups = (list)=> {
116
+ runUntracked(()=> {
117
+ [...list].reverse().forEach((l)=> {
118
+ l()
119
+ })
120
+ list.length = 0
121
+ })
122
+ }
123
+
124
+ // Create a binding effect that integrates with the owner system
125
+ const createBindingEffect = (runner)=> {
126
+ const owner = currentOwner
127
+ // effect-level cleanups (run before each re-execution)
128
+ const local = []
129
+
130
+ const stop = effect(()=> {
131
+ // Run effect-level cleanups in reverse order, untracked to avoid accidental dependency registration
132
+ flushCleanups(local)
133
+
134
+ // Set up computation context for onCleanup within the effect
135
+ const prevComp = getCurrentComputation()
136
+ setCurrentComputation({ cleanups: local })
137
+ // Restore the owner captured at creation time so that:
138
+ // 1. appendChild defers component-descriptor children (requires currentOwner != null)
139
+ // 2. createOwner() chains new owners under the correct parent on re-fires
140
+ const prevOwner = currentOwner
141
+ currentOwner = owner
142
+ try {
143
+ runner()
144
+ } finally {
145
+ setCurrentComputation(prevComp)
146
+ currentOwner = prevOwner
147
+ }
148
+ })
149
+
150
+ if (!owner){
151
+ return stop
152
+ }
153
+ // Register effect and its disposal in the owner
154
+ owner.effects.push(stop)
155
+ owner.cleanups.push(()=> {
156
+ // Run remaining local cleanups, untracked to avoid accidental dependency registration
157
+ flushCleanups(local)
158
+ })
159
+
160
+ return stop
161
+ }
162
+
163
+ const registerCleanup = (fn)=> {
164
+ const currentComputation = getCurrentComputation()
165
+ if (!currentOwner && !currentComputation){
166
+ throw new Error('registerCleanup must be called within a component or effect scope')
167
+ }
168
+ if (currentComputation){
169
+ currentComputation.cleanups.push(fn)
170
+ } else {
171
+ currentOwner.cleanups.push(fn)
172
+ }
173
+ }
174
+
175
+ // Public API: onMount - register function to run after mount
176
+ const onMount = (fn)=> {
177
+ if (!currentOwner){
178
+ throw new Error('onMount must be called within a component scope')
179
+ }
180
+ currentOwner.mounts.push(fn)
181
+ }
182
+
183
+ const onUnmount = (fn)=> {
184
+ if (!currentOwner){
185
+ throw new Error('onUnmount must be called within a component scope')
186
+ }
187
+ currentOwner.cleanups.push(fn)
188
+ }
189
+
190
+ const createContext = (defaultValue)=> {
191
+ const context = {
192
+ [CONTEXT_ID]: Symbol('context'),
193
+ defaultValue,
194
+ }
195
+
196
+ context.Provider = ({ value, children })=> {
197
+ if (!currentOwner){
198
+ throw new Error('Context Provider must be rendered within a component scope')
199
+ }
200
+
201
+ currentOwner.contexts.set(context[CONTEXT_ID], value)
202
+
203
+ if (typeof children === 'function'){
204
+ return children()
205
+ }
206
+
207
+ return children ?? null
208
+ }
209
+
210
+ return context
211
+ }
212
+
213
+ const useContext = (context)=> {
214
+ if (!context || typeof context !== 'object' || !(CONTEXT_ID in context)){
215
+ throw new Error('useContext requires a context created by createContext')
216
+ }
217
+
218
+ if (!currentOwner){
219
+ throw new Error('useContext must be called within a component scope')
220
+ }
221
+
222
+ let owner = currentOwner
223
+ while (owner){
224
+ if (owner.contexts.has(context[CONTEXT_ID])){
225
+ return owner.contexts.get(context[CONTEXT_ID])
226
+ }
227
+ owner = owner.parent
228
+ }
229
+
230
+ return context.defaultValue
231
+ }
232
+
233
+ const createComponentDescriptor = (tag, props, children)=> ({
234
+ [COMPONENT_DESCRIPTOR]: true,
235
+ tag,
236
+ props,
237
+ children,
238
+ instance: null,
239
+ })
240
+
241
+ const isComponentDescriptor = value=> value != null && typeof value === 'object' && value[COMPONENT_DESCRIPTOR] === true
242
+
243
+ const isReactivePrimitive = value=> value != null && (isSignal(value) || isComputed(value))
244
+ const isReactive = value=> isReactivePrimitive(value) || typeof value === 'function'
245
+ const createPlaceholder = ()=> document.createComment('null')
246
+ const isSupportedEvent = (element, eventName)=> `on${eventName}` in element
247
+ const isBooleanDomProp = (element, key)=> key in element && typeof element[key] === 'boolean'
248
+
249
+ // JSX uses camelCase for some props whose corresponding DOM property is all-lowercase.
250
+ // Normalise the key before any DOM access so the property lookup and setAttribute
251
+ // calls use the name the browser actually exposes.
252
+ const JSX_PROP_MAP = {
253
+ autoComplete: 'autocomplete',
254
+ autoFocus: 'autofocus',
255
+ autoPlay: 'autoplay',
256
+ encType: 'enctype',
257
+ hrefLang: 'hreflang',
258
+ }
259
+
260
+ const MAX_REACTIVE_RESOLVE_STEPS = 16
261
+
262
+ const resolveReactiveValue = (value)=> {
263
+ // In practice this loop resolves in at most 2 steps. The known chains are:
264
+ // signal → primitive (1 step)
265
+ // computed → primitive (1 step)
266
+ // function → primitive (1 step)
267
+ // signal → tree (1 step, tree is an object so the loop stops)
268
+ // signal → function → value (2 steps)
269
+ // computed → function → value (2 steps)
270
+ // signal→signal is forbidden by createSignal; signal→computed triggers a warning.
271
+ // The loop is kept rather than hard-coding 2 steps because a function returning
272
+ // a function (e.g. const fn = () => () => 'red'; <div style={fn} />) is a grey
273
+ // area that the framework does not explicitly forbid, and silently mis-resolving
274
+ // it would be worse than the negligible cost of an extra iteration.
275
+ let resolved = value
276
+ let steps = 0
277
+
278
+ while (steps < MAX_REACTIVE_RESOLVE_STEPS){
279
+ if (resolved == null){
280
+ break
281
+ }
282
+
283
+ if (isSignal(resolved) || isComputed(resolved)){
284
+ resolved = resolved()
285
+ steps++
286
+ continue
287
+ }
288
+
289
+ if (typeof resolved === 'function'){
290
+ resolved = resolved()
291
+ steps++
292
+ continue
293
+ }
294
+
295
+ break
296
+ }
297
+
298
+ return resolved
299
+ }
300
+
301
+ const createReactiveTextNode = (reactiveValue)=> {
302
+ const textNode = document.createTextNode('')
303
+ let prev = textNode.data
304
+
305
+ createBindingEffect(()=> {
306
+ const next = normalizeTextNodeValue(resolveReactiveValue(reactiveValue))
307
+ if (prev === next){
308
+ return
309
+ }
310
+ textNode.data = next
311
+ prev = next
312
+ })
313
+
314
+ return textNode
315
+ }
316
+
317
+ const materializeComponentDescriptor = (descriptor)=> {
318
+ if (descriptor.instance instanceof Node){
319
+ return descriptor.instance
320
+ }
321
+
322
+ const owner = createOwner(currentOwner)
323
+ let componentProps = descriptor.props ?? {}
324
+
325
+ // Legacy `h(Comp, props, ...children)` carries variadic children alongside
326
+ // props. Compiled JSX always packs children into the proxy itself, leaving
327
+ // descriptor.children empty. Layer any extra variadic children on top via
328
+ // mergeProps so the proxy contract holds for both call shapes.
329
+ if (descriptor.children && descriptor.children.length > 0){
330
+ const kids = descriptor.children.length === 1 ? descriptor.children[0] : descriptor.children
331
+ componentProps = mergeProps(componentProps, { children: kids })
332
+ }
333
+
334
+ const result = runUntracked(()=> runWithOwner(owner, ()=> descriptor.tag(componentProps)))
335
+ const normalized = runUntracked(()=> renderInOwner(owner, result))
336
+
337
+ if (normalized instanceof Node){
338
+ normalized[OWNER] = owner
339
+ }
340
+
341
+ descriptor.instance = normalized
342
+ return normalized
343
+ }
344
+
345
+ const createReactiveChildNode = (reactiveValue)=> {
346
+ const start = document.createComment('dynamic-start')
347
+ const end = document.createComment('dynamic-end')
348
+ const fragment = document.createDocumentFragment()
349
+ fragment.append(start, end)
350
+ let mountedNodes = []
351
+
352
+ createBindingEffect(()=> {
353
+ const nextNode = node2Element(resolveReactiveValue(reactiveValue))
354
+ // When the reactive value produced an array, node2Element returns a
355
+ // fragment with deferred component/thunk children. Flush them now before
356
+ // insertBefore drains the fragment — once drained, PENDING_DESCRIPTORS is
357
+ // unreachable. currentOwner is correctly set here because createBindingEffect
358
+ // restores the owner captured at creation time.
359
+ if (nextNode instanceof DocumentFragment && nextNode[PENDING_DESCRIPTORS]){
360
+ flushPendingDescriptors(nextNode)
361
+ }
362
+ const parent = end.parentNode
363
+ if (!parent){
364
+ return
365
+ }
366
+
367
+ mountedNodes.forEach((node)=> {
368
+ if (node.parentNode){
369
+ node.parentNode.removeChild(node)
370
+ }
371
+ })
372
+ mountedNodes = []
373
+
374
+ if (nextNode instanceof DocumentFragment){
375
+ mountedNodes = [...nextNode.childNodes]
376
+ } else {
377
+ mountedNodes = [nextNode]
378
+ }
379
+
380
+ parent.insertBefore(nextNode, end)
381
+ if (start.isConnected){
382
+ mountedNodes.forEach(node=> mountOwnedSubtree(node))
383
+ }
384
+ })
385
+
386
+ // The fragment is drained the moment it's appended into the parent, so the
387
+ // start/end markers and mountedNodes become free-standing children of that
388
+ // parent. Without this cleanup, disposing the owner stops the binding effect
389
+ // but leaves the DOM nodes behind (visible as the fragment-root disposer
390
+ // regression). Only register if we're inside an owner/computation scope —
391
+ // some standalone render helpers materialize without one.
392
+ if (currentOwner || getCurrentComputation()){
393
+ registerCleanup(()=> {
394
+ mountedNodes.forEach((node)=> {
395
+ if (node.parentNode){
396
+ node.parentNode.removeChild(node)
397
+ }
398
+ })
399
+ mountedNodes = []
400
+ if (start.parentNode){
401
+ start.parentNode.removeChild(start)
402
+ }
403
+ if (end.parentNode){
404
+ end.parentNode.removeChild(end)
405
+ }
406
+ })
407
+ }
408
+
409
+ return fragment
410
+ }
411
+
412
+ const applyClassNameMap = (element, classNameMap)=> {
413
+ if (!(classNameMap instanceof Map)){
414
+ const temp = new Map()
415
+ Object.entries(classNameMap).forEach(([className, enabled])=> {
416
+ temp.set(className, enabled)
417
+ })
418
+ classNameMap = temp
419
+ }
420
+ classNameMap.forEach((enabled, className)=> {
421
+ // if should be enabled but isn't, add it
422
+ if (enabled && !element.classList.contains(className)){
423
+ element.classList.add(className)
424
+ }
425
+
426
+ // if shouldn't be enabled but is, remove it
427
+ if (!enabled && element.classList.contains(className)){
428
+ element.classList.remove(className)
429
+ }
430
+ })
431
+ }
432
+
433
+ // Apply a className value to an element. Always reconciles against the
434
+ // element's current classList so re-runs (e.g. via an enclosing binding
435
+ // effect) drop tokens that disappeared from the new value.
436
+ // NOTE: `value` must already be unwrapped before calling this function.
437
+ // Do not pass signals, computeds, or accessor thunks — callers are
438
+ // responsible for resolving reactive values via `resolveReactiveValue` first.
439
+ const applyClassProp = (element, value)=> {
440
+ const expectedClass = toClassMap(value)
441
+ const actualClass = new Set(element.classList)
442
+ const shouldRemove = [...actualClass].filter(className=> !expectedClass.has(className))
443
+ const combinedClassMap = new Map([...expectedClass, ...shouldRemove.map(className=> [className, false])])
444
+ applyClassNameMap(element, combinedClassMap)
445
+ }
446
+ const applyStyleObject = (element, styles, prevStyles = {})=> {
447
+ const nextStyles = styles ?? {}
448
+
449
+ Object.keys(prevStyles).forEach((property)=> {
450
+ if (nextStyles[property] != null && nextStyles[property] !== false){
451
+ return
452
+ }
453
+
454
+ clearStyleKey(element, property)
455
+ delete prevStyles[property]
456
+ })
457
+
458
+ Object.entries(nextStyles).forEach(([property, value])=> {
459
+ if (value == null || value === false){
460
+ return
461
+ }
462
+
463
+ const nextValue = String(value)
464
+ if (prevStyles[property] === nextValue){
465
+ return
466
+ }
467
+
468
+ if (property.startsWith('--')){
469
+ element.style.setProperty(property, nextValue)
470
+ } else {
471
+ element.style[property] = nextValue
472
+ }
473
+
474
+ prevStyles[property] = nextValue
475
+ })
476
+
477
+ return prevStyles
478
+ }
479
+
480
+ const clearStyleKey = (element, key)=> {
481
+ if (key.startsWith('--')){
482
+ element.style.removeProperty(key)
483
+ } else {
484
+ element.style[key] = ''
485
+ }
486
+ }
487
+
488
+ // NOTE: `value` must already be unwrapped before calling this function.
489
+ // Do not pass signals, computeds, or accessor thunks — callers are
490
+ // responsible for resolving reactive values via `resolveReactiveValue` first.
491
+ const applyStyleProp = (element, value, prevValue)=> {
492
+ if (value == null || value === false){
493
+ element.style.cssText = ''
494
+ return undefined
495
+ }
496
+
497
+ if (typeof value === 'string'){
498
+ element.style.cssText = value
499
+ return value
500
+ }
501
+
502
+ if (typeof value === 'object'){
503
+ return applyStyleObject(element, value, typeof prevValue === 'string' ? undefined : prevValue)
504
+ }
505
+
506
+ return prevValue
507
+ }
508
+
509
+ const clearDomProp = (element, key)=> {
510
+ if (key in element){
511
+ try {
512
+ element[key] = ''
513
+ } catch {
514
+ // Some DOM properties (for example HTMLInputElement.form) are readonly.
515
+ }
516
+ }
517
+
518
+ element.removeAttribute(key)
519
+ }
520
+
521
+ // Apply a prop value directly to the DOM. Reactive props reuse this same path inside effects.
522
+ const setDomProp = (element, key, value)=> {
523
+ if (isBooleanDomProp(element, key)){
524
+ const next = Boolean(value)
525
+ const prev = element[key]
526
+ const hasAttribute = element.hasAttribute(key)
527
+ if (prev === next && (next && hasAttribute || !next && !hasAttribute)){
528
+ return
529
+ }
530
+
531
+ element[key] = next
532
+
533
+ if (next){
534
+ element.setAttribute(key, '')
535
+ return
536
+ }
537
+
538
+ element.removeAttribute(key)
539
+ return
540
+ }
541
+
542
+ if (value == null || value === false){
543
+ // Skip removal for aria-* when value is false — some ATs distinguish
544
+ // aria-hidden="false" (visible) from a missing attribute (also visible but unreliable),
545
+ // so we must keep the attribute present even when falsey.
546
+ if (!(value === false && typeof key === 'string' && key.startsWith('aria-'))){
547
+ const prev = key in element ? element[key] : ''
548
+ if (prev === '' && !element.hasAttribute(key)){
549
+ return
550
+ }
551
+ clearDomProp(element, key)
552
+ return
553
+ }
554
+ }
555
+
556
+ if (key in element){
557
+ const prev = element[key]
558
+ if (prev === value){
559
+ return
560
+ }
561
+ try {
562
+ element[key] = value
563
+ return
564
+ } catch {
565
+ // Fall through to attribute writes for readonly DOM properties.
566
+ }
567
+ }
568
+
569
+ const next = String(value)
570
+ const prev = element.getAttribute(key)
571
+ if (prev === next){
572
+ return
573
+ }
574
+ element.setAttribute(key, next)
575
+ }
576
+
577
+ const applyCommonAttribute = (element, key, source)=> {
578
+ if (isReactive(source)){
579
+ createBindingEffect(()=> {
580
+ setDomProp(element, key, resolveReactiveValue(source))
581
+ })
582
+ return
583
+ }
584
+ setDomProp(element, key, source)
585
+ }
586
+
587
+ const applyRefProp = (element, ref)=> {
588
+ if (typeof ref !== 'function'){
589
+ return
590
+ }
591
+
592
+ const assignRef = value=> ref(value)
593
+
594
+ const owner = currentOwner
595
+
596
+ if (owner && !owner.mounted) {
597
+ owner.refs.push(() => assignRef(element))
598
+ } else {
599
+ assignRef(element)
600
+ }
601
+
602
+ if (owner || getCurrentComputation()){
603
+ registerCleanup(()=> {
604
+ assignRef(null)
605
+ })
606
+ }
607
+ }
608
+
609
+ const disposeBindings = (bindings)=> {
610
+ ;[...bindings].reverse().forEach((dispose)=> {
611
+ if (typeof dispose === 'function'){
612
+ dispose()
613
+ }
614
+ })
615
+ bindings.length = 0
616
+ }
617
+
618
+ // Bind a single non-event, non-special prop key to the DOM element. Each
619
+ // individual prop runs inside its own binding effect so a signal change in
620
+ // one prop's getter only re-writes that one attribute. For plain (non-proxy)
621
+ // props the inner effect runs once with no dependencies and is essentially
622
+ // free. The value is resolved through `resolveReactiveValue` so signals and
623
+ // zero-arg accessor thunks (used widely by ark-plastic / zag adapters) are
624
+ // unwrapped before being applied to the DOM.
625
+ const bindReactiveProp = (element, props, key)=> {
626
+ let prevStyleValue
627
+ const stop = createBindingEffect(()=> {
628
+ const value = resolveReactiveValue(props[key])
629
+ if (key === 'className' || key === 'class'){
630
+ applyClassProp(element, value)
631
+ return
632
+ }
633
+ if (key === 'style'){
634
+ prevStyleValue = applyStyleProp(element, value, prevStyleValue)
635
+ return
636
+ }
637
+ const domKey = JSX_PROP_MAP[key] ?? key
638
+ setDomProp(element, domKey, value)
639
+ })
640
+
641
+ return ()=> {
642
+ stop?.()
643
+ if (key === 'className' || key === 'class'){
644
+ element.removeAttribute('class')
645
+ return
646
+ }
647
+ if (key === 'style'){
648
+ // Only clear style properties that Plastic itself set, preserving any
649
+ // inline styles written directly to the DOM by third-party libraries
650
+ // (e.g. Zag's pointer-events management via assignPointerEventToLayers).
651
+ if (prevStyleValue && typeof prevStyleValue === 'object'){
652
+ Object.keys(prevStyleValue).forEach(prop=> clearStyleKey(element, prop))
653
+ } else if (typeof prevStyleValue === 'string'){
654
+ element.style.cssText = ''
655
+ }
656
+ return
657
+ }
658
+ const domKey = JSX_PROP_MAP[key] ?? key
659
+ clearDomProp(element, domKey)
660
+ }
661
+ }
662
+
663
+ // Attach a single listener that resolves the current handler from `props` at
664
+ // dispatch time. This makes handlers reactive without re-attaching listeners:
665
+ // when a parent's signal changes the handler reference, the next event read
666
+ // sees the new function via the proxy.
667
+ const bindReactiveEvent = (element, props, key)=> {
668
+ const eventName = key.slice(2).toLowerCase()
669
+ if (!isSupportedEvent(element, eventName)){
670
+ return ()=> {}
671
+ }
672
+ const listener = (...args)=> {
673
+ const handler = props[key]
674
+ if (typeof handler === 'function'){
675
+ handler(...args)
676
+ }
677
+ }
678
+ element.addEventListener(eventName, listener)
679
+ if (currentOwner || getCurrentComputation()){
680
+ registerCleanup(()=> {
681
+ element.removeEventListener(eventName, listener)
682
+ })
683
+ }
684
+
685
+ return ()=> {
686
+ element.removeEventListener(eventName, listener)
687
+ }
688
+ }
689
+
690
+ // Apply a props object (plain object or mergeProps proxy) to a DOM element.
691
+ // Each prop gets its own binding effect so a change to one signal only
692
+ // re-writes that one attribute. The enclosing binding effect tracks
693
+ // `Reflect.ownKeys(props)`, so when a dynamic spread source adds or removes
694
+ // keys later we tear down the previous bindings and rebuild them from the
695
+ // current key set.
696
+ const applyProps = (element, props = {})=> {
697
+ createBindingEffect(()=> {
698
+ const bindings = []
699
+ registerCleanup(()=> {
700
+ disposeBindings(bindings)
701
+ })
702
+
703
+ for (const key of Reflect.ownKeys(props)){
704
+ if (typeof key === 'symbol' || key === 'children' || key === 'key'){
705
+ continue
706
+ }
707
+ if (key === 'classList'){
708
+ throw new Error('classList prop is not supported. Use className instead.')
709
+ }
710
+ if (key === 'ref'){
711
+ const ref = props[key]
712
+ applyRefProp(element, ref)
713
+ bindings.push(()=> {
714
+ if (typeof ref === 'function'){
715
+ ref(null)
716
+ }
717
+ })
718
+ continue
719
+ }
720
+ if (isEventProp(key)){
721
+ bindings.push(bindReactiveEvent(element, props, key))
722
+ continue
723
+ }
724
+ bindings.push(bindReactiveProp(element, props, key))
725
+ }
726
+ })
727
+ return element
728
+ }
729
+
730
+ // Normalize any JSX return value into a DOM node that can be appended safely.
731
+ const node2Element = (node)=> {
732
+ if (node === null || node === undefined){
733
+ return createPlaceholder()
734
+ }
735
+ if (isComponentDescriptor(node)){
736
+ return materializeComponentDescriptor(node)
737
+ }
738
+ if (isReactivePrimitive(node)){
739
+ return createReactiveTextNode(node)
740
+ }
741
+ if (typeof node === 'function'){
742
+ return createReactiveChildNode(node)
743
+ }
744
+ if (typeof node === 'string' || typeof node === 'number'){
745
+ return document.createTextNode(String(node))
746
+ }
747
+ if (node instanceof Node){
748
+ flushPendingDescriptors(node)
749
+ return node
750
+ }
751
+ if (Array.isArray(node)){
752
+ const fragment = document.createDocumentFragment()
753
+ appendChildren(fragment, node)
754
+ // Do NOT flush here — the caller (appendChild) will transfer any pending
755
+ // descriptors to the real parent element before draining the fragment, so
756
+ // flushPendingDescriptors runs later with the correct owner active.
757
+ return fragment
758
+ }
759
+ return createPlaceholder()
760
+ }
761
+
762
+ const materializeNode = node=> node2Element(node)
763
+
764
+ // Walk an Element subtree and materialize any component descriptors that were
765
+ // deferred by appendChild during eager native-tag construction. Materialization
766
+ // happens with the *current* owner active, so when this is invoked from inside
767
+ // a component's renderInOwner pass, deferred children correctly chain their
768
+ // owner under that component (e.g. <Provider><div><Label/></div></Provider> —
769
+ // Label's owner.parent becomes the Provider's owner, and useContext walks find
770
+ // the Provider value).
771
+ const flushPendingDescriptors = (root)=> {
772
+ if (!(root instanceof Element) && !(root instanceof DocumentFragment)){
773
+ return
774
+ }
775
+ const stack = [root]
776
+ while (stack.length){
777
+ const node = stack.pop()
778
+ const pending = node[PENDING_DESCRIPTORS]
779
+ if (pending){
780
+ node[PENDING_DESCRIPTORS] = undefined
781
+ pending.forEach(({ placeholder, descriptor })=> {
782
+ if (!placeholder.parentNode){
783
+ return
784
+ }
785
+ const materialized = node2Element(descriptor)
786
+ if (materialized instanceof DocumentFragment && materialized[PENDING_DESCRIPTORS]){
787
+ flushPendingDescriptors(materialized)
788
+ }
789
+ placeholder.parentNode.replaceChild(materialized, placeholder)
790
+ })
791
+ }
792
+ for (const child of node.childNodes){
793
+ if (child instanceof Element){
794
+ stack.push(child)
795
+ }
796
+ }
797
+ }
798
+ }
799
+
800
+ // Ignore empty JSX children and append everything else after normalization.
801
+ const appendChild = (parent, child)=> {
802
+ if (child == null){
803
+ return parent
804
+ }
805
+
806
+ // Defer component-descriptor and reactive-thunk children until the
807
+ // surrounding component owner is active. JS evaluates h() arguments eagerly,
808
+ // so without this the inner component would materialize (or the reactive
809
+ // binding would capture its owner) under the *outer* component's owner,
810
+ // missing any context that the wrapping component sets in its body.
811
+ // Thunks (`typeof child === 'function') are reactive accessors injected by
812
+ // the babel reactive transform; they create a binding that captures
813
+ // currentOwner, so deferring them here ensures createReactiveChildNode runs
814
+ // later during flushPendingDescriptors with the correct owner active.
815
+ // Only defer when we're already inside a component scope (currentOwner set)
816
+ // — direct h() usage at the top of a script expects synchronous
817
+ // materialization.
818
+ if ((isComponentDescriptor(child) || typeof child === 'function') && currentOwner != null && (parent instanceof Element || parent instanceof DocumentFragment)){
819
+ const placeholder = document.createComment('pending')
820
+ parent.appendChild(placeholder)
821
+ const list = parent[PENDING_DESCRIPTORS] ?? (parent[PENDING_DESCRIPTORS] = [])
822
+ list.push({ placeholder, descriptor: child })
823
+ return parent
824
+ }
825
+
826
+ // When a native element is appended inside a component scope, it may carry
827
+ // pending component descriptors in its subtree that were deferred during
828
+ // eager h() construction. Flushing them now (via node2Element → flushPendingDescriptors)
829
+ // would materialize those descriptors under the current owner, which is
830
+ // the *outer* component — not the provider that will be set up later.
831
+ // Instead, bubble all pending descriptors from the element's subtree up to
832
+ // the parent so they get flushed by flushPendingDescriptors with the
833
+ // correct owner once the surrounding component finishes rendering.
834
+ if (child instanceof Element && currentOwner != null && (parent instanceof Element || parent instanceof DocumentFragment)){
835
+ const stack = [child]
836
+ while (stack.length){
837
+ const node = stack.pop()
838
+ const pending = node[PENDING_DESCRIPTORS]
839
+ if (pending){
840
+ node[PENDING_DESCRIPTORS] = undefined
841
+ const list = parent[PENDING_DESCRIPTORS] ?? (parent[PENDING_DESCRIPTORS] = [])
842
+ list.push(...pending)
843
+ }
844
+ for (const grandchild of node.childNodes){
845
+ if (grandchild instanceof Element) stack.push(grandchild)
846
+ }
847
+ }
848
+ parent.appendChild(child)
849
+ return parent
850
+ }
851
+
852
+ const childNode = node2Element(child)
853
+ // When the child resolved to a fragment that carries deferred component
854
+ // descriptors, transfer them to the real parent before draining so
855
+ // flushPendingDescriptors can find and materialize them later with the
856
+ // correct owner active (see the array branch in node2Element).
857
+ if (childNode instanceof DocumentFragment && childNode[PENDING_DESCRIPTORS] && (parent instanceof Element || parent instanceof DocumentFragment)){
858
+ const list = parent[PENDING_DESCRIPTORS] ?? (parent[PENDING_DESCRIPTORS] = [])
859
+ list.push(...childNode[PENDING_DESCRIPTORS])
860
+ childNode[PENDING_DESCRIPTORS] = undefined
861
+ }
862
+ parent.appendChild(childNode)
863
+ return parent
864
+ }
865
+
866
+ const appendChildren = (parent, children)=> {
867
+ // Flatten first so nested array children from conditionals or loops work naturally.
868
+ flattenChildren(children).forEach((child)=> {
869
+ appendChild(parent, child)
870
+ })
871
+ return parent
872
+ }
873
+
874
+ const {
875
+ mountDynamic,
876
+ Either,
877
+ True,
878
+ False,
879
+ Match,
880
+ Case,
881
+ Default,
882
+ Loop,
883
+ Portal,
884
+ } = createControlFlow({
885
+ createOwner,
886
+ runOwnerMounts,
887
+ runWithOwner,
888
+ disposeOwner,
889
+ createBindingEffect,
890
+ renderInOwner,
891
+ getCurrentOwner,
892
+ registerCleanup,
893
+ batch,
894
+ appendChild,
895
+ flushPendingDescriptors,
896
+ })
897
+
898
+ // Thin runtime helper for <Dynamic component={tag} ...props />.
899
+ // `component` is treated as the tag argument for `h`.
900
+
901
+ const Dynamic = ({ component, ...props })=> {
902
+ // Resolve signals/computed directly; also resolve zero-arg accessor thunks produced
903
+ // by the Babel reactive plugin when `component` is a dynamic expression.
904
+ let dynamicTag = component
905
+ if (isReactivePrimitive(component) || typeof component === 'function' && component.length === 0){
906
+ dynamicTag = resolveReactiveValue(component)
907
+ }
908
+ return h(dynamicTag, props)
909
+ }
910
+
911
+ const SVG_TAGS = new Set(['svg', 'animate', 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'defs', 'desc', 'ellipse', 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence', 'filter', 'foreignObject', 'g', 'image', 'line', 'linearGradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', 'rect', 'set', 'stop', 'switch', 'symbol', 'text', 'textPath', 'tspan', 'use', 'view'])
912
+
913
+ const h = (tag, props, ...children)=> {
914
+ const nextProps = props || {}
915
+
916
+ if (tag === Fragment){
917
+ // Fragments produce a DocumentFragment so no wrapper element is introduced.
918
+ const propChildren = nextProps.children ?? []
919
+ const normalizedPropChildren = Array.isArray(propChildren) ? propChildren : [propChildren]
920
+ const mergedChildren = [...normalizedPropChildren, ...children]
921
+ const fragment = document.createDocumentFragment()
922
+ appendChildren(fragment, mergedChildren)
923
+ return fragment
924
+ }
925
+
926
+ if (typeof tag === 'function'){
927
+ // Compiled JSX always packs children into the props proxy and passes no
928
+ // variadic children; preserve the proxy as-is so component bodies read
929
+ // `props.children` reactively. Legacy handwritten `h(Comp, props, ...kids)`
930
+ // callers pass kids separately — forward them to the descriptor so
931
+ // materializeComponentDescriptor can layer them into the proxy via
932
+ // `mergeProps`.
933
+ return createComponentDescriptor(tag, nextProps, children)
934
+ }
935
+
936
+ if (typeof tag !== 'string'){
937
+ throw new Error('Only static string tags and Fragment are supported.')
938
+ }
939
+
940
+ // Native tags create real DOM elements directly without a virtual DOM layer.
941
+ const element = SVG_TAGS.has(tag) ? document.createElementNS('http://www.w3.org/2000/svg', tag) : document.createElement(tag)
942
+ applyProps(element, nextProps)
943
+
944
+ const propChildren = nextProps.children ?? []
945
+ const normalizedPropChildren = Array.isArray(propChildren) ? propChildren : [propChildren]
946
+ const mergedChildren = [...normalizedPropChildren, ...children]
947
+ appendChildren(element, mergedChildren)
948
+ return element
949
+ }
950
+
951
+ const jsx = (tag, props, key)=> {
952
+ if (key === undefined){
953
+ return h(tag, props)
954
+ }
955
+ // Layer key on without flattening the proxy.
956
+ return h(tag, mergeProps(props, { key }))
957
+ }
958
+ const jsxs = jsx
959
+ // jsxDEV is the development-mode variant used by automatic JSX transforms; the extra
960
+ // debug arguments (isStaticChildren, source, self) are unused at runtime.
961
+ const jsxDEV = (tag, props, key) => jsx(tag, props, key)
962
+
963
+ // Render by appending the normalized root node into the target container.
964
+ // Returns a disposer function that cleans up all effects and listeners.
965
+ const renderApp = (container, node)=> {
966
+ const appNode = node2Element(node)
967
+ container.appendChild(appNode)
968
+ // Get the owner from the node (set by h() when rendering components)
969
+ const owner = appNode[OWNER]
970
+ // Execute root onMount callbacks if owner exists
971
+ if (owner){
972
+ runOwnerMounts(owner)
973
+ }
974
+
975
+ // Return a disposer function
976
+ let disposed = false
977
+ const dispose = ()=> {
978
+ if (disposed){ return }
979
+ disposed = true
980
+ if (owner){
981
+ disposeOwner(owner)
982
+ }
983
+ if (appNode.parentNode === container){
984
+ container.removeChild(appNode)
985
+ }
986
+ }
987
+
988
+ return dispose
989
+ }
990
+
991
+ export {
992
+ // Public API
993
+ Fragment,
994
+ h,
995
+ jsx,
996
+ jsxDEV,
997
+ jsxs,
998
+ onMount,
999
+ onUnmount,
1000
+ createContext,
1001
+ useContext,
1002
+ renderApp,
1003
+ // Internal signal primitives (framework internals/tests)
1004
+ createComputed,
1005
+ createSignal,
1006
+ createTree,
1007
+ toRaw,
1008
+ isTree,
1009
+ // Owner / lifecycle internals
1010
+ createOwner,
1011
+ runOwnerMounts,
1012
+ runWithOwner,
1013
+ disposeOwner,
1014
+ createBindingEffect,
1015
+ registerCleanup,
1016
+ // Reactive helpers
1017
+ isReactivePrimitive,
1018
+ isReactive,
1019
+ // DOM helpers
1020
+ createPlaceholder,
1021
+ flattenChildren,
1022
+ isEventProp,
1023
+ isSupportedEvent,
1024
+ isBooleanDomProp,
1025
+ JSX_PROP_MAP,
1026
+ normalizeTextNodeValue,
1027
+ createReactiveTextNode,
1028
+ toClassTokens,
1029
+ toClassMap,
1030
+ applyClassNameMap,
1031
+ applyClassProp,
1032
+ applyStyleObject,
1033
+ clearStyleKey,
1034
+ applyStyleProp,
1035
+ applyRefProp,
1036
+ clearDomProp,
1037
+ setDomProp,
1038
+ applyCommonAttribute,
1039
+ applyProps,
1040
+ materializeNode,
1041
+ node2Element,
1042
+ appendChild,
1043
+ appendChildren,
1044
+ // Control flow
1045
+ mountDynamic,
1046
+ Either,
1047
+ True,
1048
+ False,
1049
+ Match,
1050
+ Case,
1051
+ Default,
1052
+ Loop,
1053
+ Portal,
1054
+ Dynamic,
1055
+ // Reactive props proxy
1056
+ isMergedProps,
1057
+ mergeProps,
1058
+ }