@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.
- package/LICENSE +21 -0
- package/README.md +442 -0
- package/package.json +78 -0
- package/src/computation-context.js +11 -0
- package/src/control-flow.js +367 -0
- package/src/index.js +87 -0
- package/src/jsx-runtime.js +1058 -0
- package/src/merge-props.js +245 -0
- package/src/reactivity.js +408 -0
- package/src/router.js +919 -0
- package/src/split-props.js +42 -0
- package/src/utils.js +51 -0
|
@@ -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
|
+
}
|