@pyreon/runtime-dom 0.24.5 → 0.24.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -9
- package/src/delegate.ts +0 -98
- package/src/devtools.ts +0 -339
- package/src/env.d.ts +0 -6
- package/src/hydrate.ts +0 -450
- package/src/hydration-debug.ts +0 -129
- package/src/index.ts +0 -83
- package/src/keep-alive-entry.ts +0 -3
- package/src/keep-alive.ts +0 -83
- package/src/manifest.ts +0 -236
- package/src/mount.ts +0 -597
- package/src/nodes.ts +0 -896
- package/src/props.ts +0 -474
- package/src/template.ts +0 -523
- package/src/tests/callback-ref-unmount.browser.test.ts +0 -62
- package/src/tests/callback-ref-unmount.test.ts +0 -52
- package/src/tests/compiler-integration.test.tsx +0 -508
- package/src/tests/coverage-gaps.test.ts +0 -3183
- package/src/tests/coverage.test.ts +0 -1140
- package/src/tests/ctx-stack-growth-repro.test.tsx +0 -158
- package/src/tests/dev-gate-pattern.test.ts +0 -46
- package/src/tests/dev-gate-treeshake.test.ts +0 -256
- package/src/tests/error-boundary-stack-leak-repro.test.tsx +0 -133
- package/src/tests/fanout-repro.test.tsx +0 -219
- package/src/tests/hydration-integration.test.tsx +0 -540
- package/src/tests/keyed-array-in-for-batched-toggle.browser.test.ts +0 -140
- package/src/tests/lifecycle-integration.test.tsx +0 -342
- package/src/tests/lis-prepend.browser.test.ts +0 -99
- package/src/tests/manifest-snapshot.test.ts +0 -85
- package/src/tests/mount.test.ts +0 -3529
- package/src/tests/native-markers.test.ts +0 -19
- package/src/tests/props.test.ts +0 -581
- package/src/tests/reactive-props.test.ts +0 -270
- package/src/tests/real-world-integration.test.tsx +0 -714
- package/src/tests/rs-collapse-dyn-h.browser.test.ts +0 -303
- package/src/tests/rs-collapse-dyn.browser.test.ts +0 -316
- package/src/tests/rs-collapse-h.browser.test.ts +0 -152
- package/src/tests/rs-collapse-h.test.ts +0 -237
- package/src/tests/rs-collapse.browser.test.ts +0 -128
- package/src/tests/runtime-dom.browser.test.ts +0 -409
- package/src/tests/setup.ts +0 -3
- package/src/tests/show-context.test.ts +0 -270
- package/src/tests/show-of-for-batched-toggle.browser.test.ts +0 -122
- package/src/tests/ssr-xss-round-trip.browser.test.ts +0 -93
- package/src/tests/style-key-removal.browser.test.ts +0 -54
- package/src/tests/style-key-removal.test.ts +0 -88
- package/src/tests/template.test.ts +0 -383
- package/src/tests/transition-timeout-leak.test.ts +0 -126
- package/src/tests/transition.test.ts +0 -568
- package/src/tests/verified-correct-probes.test.ts +0 -56
- package/src/transition-entry.ts +0 -7
- package/src/transition-group.ts +0 -350
- package/src/transition.ts +0 -245
package/src/mount.ts
DELETED
|
@@ -1,597 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ComponentFn,
|
|
3
|
-
ForProps,
|
|
4
|
-
NativeItem,
|
|
5
|
-
PortalProps,
|
|
6
|
-
RefProp,
|
|
7
|
-
VNode,
|
|
8
|
-
VNodeChild,
|
|
9
|
-
} from '@pyreon/core'
|
|
10
|
-
import {
|
|
11
|
-
dispatchToErrorBoundary,
|
|
12
|
-
EMPTY_PROPS,
|
|
13
|
-
ForSymbol,
|
|
14
|
-
Fragment,
|
|
15
|
-
makeReactiveProps,
|
|
16
|
-
PortalSymbol,
|
|
17
|
-
propagateError,
|
|
18
|
-
reportError,
|
|
19
|
-
runWithHooks,
|
|
20
|
-
} from '@pyreon/core'
|
|
21
|
-
import { effectScope, renderEffect, runUntracked, setCurrentScope } from '@pyreon/reactivity'
|
|
22
|
-
import { registerComponent, unregisterComponent } from './devtools'
|
|
23
|
-
import { mountFor, mountKeyedList, mountReactive } from './nodes'
|
|
24
|
-
import { applyProps } from './props'
|
|
25
|
-
|
|
26
|
-
// Dev-mode gate: see `pyreon/no-process-dev-gate` lint rule for why this
|
|
27
|
-
// uses `import.meta.env.DEV` instead of `typeof process !== 'undefined'`.
|
|
28
|
-
const __DEV__ = process.env.NODE_ENV !== 'production'
|
|
29
|
-
|
|
30
|
-
// Dev-time counter sink — see packages/internals/perf-harness for contract.
|
|
31
|
-
const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
|
|
32
|
-
|
|
33
|
-
type Cleanup = () => void
|
|
34
|
-
const noop: Cleanup = () => {
|
|
35
|
-
/* noop */
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// When > 0, we're mounting children inside an element — child cleanups can skip
|
|
39
|
-
// DOM removal (parent element removal handles it). This avoids allocating a
|
|
40
|
-
// removeChild closure for every nested element that has no reactive work.
|
|
41
|
-
let _elementDepth = 0
|
|
42
|
-
|
|
43
|
-
// Stack tracking which component is currently being mounted (depth-first order).
|
|
44
|
-
// Used to infer parent/child relationships for DevTools.
|
|
45
|
-
// Only allocated in dev — production mounts skip devtools entirely.
|
|
46
|
-
let _mountingStack: string[] | undefined
|
|
47
|
-
if (__DEV__) _mountingStack = []
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Mount a single child into `parent`, inserting before `anchor` (null = append).
|
|
51
|
-
* Returns a cleanup that removes the node(s) and disposes all reactive effects.
|
|
52
|
-
*
|
|
53
|
-
* This function is the hot path — all child types are handled inline to avoid
|
|
54
|
-
* function call overhead in tight render loops (1000+ calls per list render).
|
|
55
|
-
*/
|
|
56
|
-
export function mountChild(
|
|
57
|
-
child: VNodeChild | VNodeChild[] | (() => VNodeChild | VNodeChild[]),
|
|
58
|
-
parent: Node,
|
|
59
|
-
anchor: Node | null = null,
|
|
60
|
-
): Cleanup {
|
|
61
|
-
if (__DEV__) _countSink.__pyreon_count__?.('runtime.mountChild')
|
|
62
|
-
// Reactive accessor — function that reads signals
|
|
63
|
-
if (typeof child === 'function') {
|
|
64
|
-
const sample = runUntracked(() => (child as () => VNodeChild | VNodeChild[])())
|
|
65
|
-
if (isKeyedArray(sample)) {
|
|
66
|
-
const prevDepth = _elementDepth
|
|
67
|
-
_elementDepth = 0
|
|
68
|
-
const cleanup = mountKeyedList(child as () => VNode[], parent, anchor, (v, p, a) =>
|
|
69
|
-
mountChild(v, p, a),
|
|
70
|
-
)
|
|
71
|
-
_elementDepth = prevDepth
|
|
72
|
-
return cleanup
|
|
73
|
-
}
|
|
74
|
-
// Text fast path: reactive string/number/boolean — update text.data in-place
|
|
75
|
-
if (typeof sample === 'string' || typeof sample === 'number' || typeof sample === 'boolean') {
|
|
76
|
-
const text = document.createTextNode(sample === false ? '' : String(sample))
|
|
77
|
-
parent.insertBefore(text, anchor)
|
|
78
|
-
const dispose = renderEffect(() => {
|
|
79
|
-
const v = (child as () => unknown)()
|
|
80
|
-
const next = v == null || v === false ? '' : String(v as string | number)
|
|
81
|
-
if (next !== text.data) text.data = next
|
|
82
|
-
})
|
|
83
|
-
if (_elementDepth > 0) return dispose
|
|
84
|
-
return () => {
|
|
85
|
-
dispose()
|
|
86
|
-
const p = text.parentNode
|
|
87
|
-
if (p && (p as Element).isConnected !== false) p.removeChild(text)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
const prevDepth = _elementDepth
|
|
91
|
-
_elementDepth = 0
|
|
92
|
-
const cleanup = mountReactive(child as () => VNodeChild, parent, anchor, mountChild)
|
|
93
|
-
_elementDepth = prevDepth
|
|
94
|
-
return cleanup
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Array of children (e.g. from .map())
|
|
98
|
-
if (Array.isArray(child)) return mountChildren(child, parent, anchor)
|
|
99
|
-
|
|
100
|
-
// Nothing to render
|
|
101
|
-
if (child == null || child === false) return noop
|
|
102
|
-
|
|
103
|
-
// Primitive — text node (static, no reactive effects to tear down).
|
|
104
|
-
if (typeof child !== 'object') {
|
|
105
|
-
parent.insertBefore(document.createTextNode(String(child)), anchor)
|
|
106
|
-
return noop
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// NativeItem — pre-built DOM element from _tpl() or createTemplate().
|
|
110
|
-
if ((child as unknown as NativeItem).__isNative) {
|
|
111
|
-
const native = child as unknown as NativeItem
|
|
112
|
-
parent.insertBefore(native.el, anchor)
|
|
113
|
-
if (!native.cleanup) {
|
|
114
|
-
if (_elementDepth > 0) return noop
|
|
115
|
-
return () => {
|
|
116
|
-
const p = native.el.parentNode
|
|
117
|
-
if (p && (p as Element).isConnected !== false) p.removeChild(native.el)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (_elementDepth > 0) return native.cleanup
|
|
121
|
-
return () => {
|
|
122
|
-
native.cleanup?.()
|
|
123
|
-
const p = native.el.parentNode
|
|
124
|
-
if (p && (p as Element).isConnected !== false) p.removeChild(native.el)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// VNode — element, component, fragment, For, Portal
|
|
129
|
-
const vnode = child as VNode
|
|
130
|
-
|
|
131
|
-
if (vnode.type === Fragment) return mountChildren(vnode.children ?? [], parent, anchor)
|
|
132
|
-
|
|
133
|
-
if (vnode.type === (ForSymbol as unknown as string)) {
|
|
134
|
-
// Compiler wraps `<For each={signal}>` in `_rp(() => signal())` →
|
|
135
|
-
// `props.each` is a getter that returns the resolved array, not the
|
|
136
|
-
// function. Destructuring eagerly captures the array and breaks
|
|
137
|
-
// reactivity + crashes mountFor (which calls `source()`). Read each
|
|
138
|
-
// lazily so the _rp getter fires inside mountFor's effect, preserving
|
|
139
|
-
// signal tracking. User-written `each={() => fn()}` (already a
|
|
140
|
-
// function, not _rp-wrapped) still works because props.each is the
|
|
141
|
-
// function itself.
|
|
142
|
-
const props = vnode.props as unknown as ForProps<unknown>
|
|
143
|
-
const initialEach = props.each as unknown
|
|
144
|
-
const source: () => unknown[] =
|
|
145
|
-
typeof initialEach === 'function'
|
|
146
|
-
? (initialEach as () => unknown[])
|
|
147
|
-
: (() => props.each as unknown as unknown[])
|
|
148
|
-
const prevDepth = _elementDepth
|
|
149
|
-
_elementDepth = 0
|
|
150
|
-
const cleanup = mountFor(
|
|
151
|
-
source as () => unknown[],
|
|
152
|
-
props.by,
|
|
153
|
-
props.children,
|
|
154
|
-
parent,
|
|
155
|
-
anchor,
|
|
156
|
-
mountChild,
|
|
157
|
-
)
|
|
158
|
-
_elementDepth = prevDepth
|
|
159
|
-
return cleanup
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (vnode.type === (PortalSymbol as unknown as string)) {
|
|
163
|
-
const { target, children } = vnode.props as unknown as PortalProps
|
|
164
|
-
if (__DEV__ && !target) {
|
|
165
|
-
console.warn('[Pyreon] <Portal> received a falsy `target`. Provide a valid DOM element.')
|
|
166
|
-
return noop
|
|
167
|
-
}
|
|
168
|
-
if (__DEV__ && !(target instanceof Node)) {
|
|
169
|
-
console.warn(
|
|
170
|
-
`[Pyreon] <Portal> target must be a DOM node. Received ${typeof target}. ` +
|
|
171
|
-
'Use document.getElementById() or a ref to get the target element.',
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
return mountChild(children, target, null)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (typeof vnode.type === 'function') {
|
|
178
|
-
return mountComponent(vnode as VNode & { type: ComponentFn }, parent, anchor)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (__DEV__ && typeof vnode.type !== 'string') {
|
|
182
|
-
console.warn(
|
|
183
|
-
`[Pyreon] Invalid VNode type: expected a string tag or component function, ` +
|
|
184
|
-
`received ${typeof vnode.type} (${String(vnode.type)}). ` +
|
|
185
|
-
`This usually means you passed an object or class instead of a component function.`,
|
|
186
|
-
)
|
|
187
|
-
return noop
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return mountElement(vnode, parent, anchor)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ─── Element ─────────────────────────────────────────────────────────────────
|
|
194
|
-
|
|
195
|
-
// Void elements that cannot have children
|
|
196
|
-
const VOID_ELEMENTS = new Set([
|
|
197
|
-
'area',
|
|
198
|
-
'base',
|
|
199
|
-
'br',
|
|
200
|
-
'col',
|
|
201
|
-
'embed',
|
|
202
|
-
'hr',
|
|
203
|
-
'img',
|
|
204
|
-
'input',
|
|
205
|
-
'link',
|
|
206
|
-
'meta',
|
|
207
|
-
'param',
|
|
208
|
-
'source',
|
|
209
|
-
'track',
|
|
210
|
-
'wbr',
|
|
211
|
-
])
|
|
212
|
-
|
|
213
|
-
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
214
|
-
const MATHML_NS = 'http://www.w3.org/1998/Math/MathML'
|
|
215
|
-
|
|
216
|
-
// Tags that require namespace-aware creation
|
|
217
|
-
const SVG_TAGS = new Set([
|
|
218
|
-
'svg',
|
|
219
|
-
'circle',
|
|
220
|
-
'ellipse',
|
|
221
|
-
'line',
|
|
222
|
-
'path',
|
|
223
|
-
'polygon',
|
|
224
|
-
'polyline',
|
|
225
|
-
'rect',
|
|
226
|
-
'g',
|
|
227
|
-
'defs',
|
|
228
|
-
'symbol',
|
|
229
|
-
'use',
|
|
230
|
-
'text',
|
|
231
|
-
'tspan',
|
|
232
|
-
'textPath',
|
|
233
|
-
'image',
|
|
234
|
-
'clipPath',
|
|
235
|
-
'mask',
|
|
236
|
-
'pattern',
|
|
237
|
-
'marker',
|
|
238
|
-
'linearGradient',
|
|
239
|
-
'radialGradient',
|
|
240
|
-
'stop',
|
|
241
|
-
'filter',
|
|
242
|
-
'feBlend',
|
|
243
|
-
'feColorMatrix',
|
|
244
|
-
'feComponentTransfer',
|
|
245
|
-
'feComposite',
|
|
246
|
-
'feConvolveMatrix',
|
|
247
|
-
'feDiffuseLighting',
|
|
248
|
-
'feDisplacementMap',
|
|
249
|
-
'feFlood',
|
|
250
|
-
'feGaussianBlur',
|
|
251
|
-
'feImage',
|
|
252
|
-
'feMerge',
|
|
253
|
-
'feMergeNode',
|
|
254
|
-
'feMorphology',
|
|
255
|
-
'feOffset',
|
|
256
|
-
'feSpecularLighting',
|
|
257
|
-
'feTile',
|
|
258
|
-
'feTurbulence',
|
|
259
|
-
'animate',
|
|
260
|
-
'animateMotion',
|
|
261
|
-
'animateTransform',
|
|
262
|
-
'set',
|
|
263
|
-
'desc',
|
|
264
|
-
'title',
|
|
265
|
-
'metadata',
|
|
266
|
-
'foreignObject',
|
|
267
|
-
])
|
|
268
|
-
|
|
269
|
-
const MATHML_TAGS = new Set([
|
|
270
|
-
'math',
|
|
271
|
-
'mi',
|
|
272
|
-
'mo',
|
|
273
|
-
'mn',
|
|
274
|
-
'ms',
|
|
275
|
-
'mtext',
|
|
276
|
-
'mspace',
|
|
277
|
-
'mrow',
|
|
278
|
-
'mfrac',
|
|
279
|
-
'msqrt',
|
|
280
|
-
'mroot',
|
|
281
|
-
'msub',
|
|
282
|
-
'msup',
|
|
283
|
-
'msubsup',
|
|
284
|
-
'munder',
|
|
285
|
-
'mover',
|
|
286
|
-
'munderover',
|
|
287
|
-
'mtable',
|
|
288
|
-
'mtr',
|
|
289
|
-
'mtd',
|
|
290
|
-
'mpadded',
|
|
291
|
-
'mphantom',
|
|
292
|
-
'menclose',
|
|
293
|
-
])
|
|
294
|
-
|
|
295
|
-
/** Track SVG context depth — children of <svg> inherit the SVG namespace. */
|
|
296
|
-
let _svgDepth = 0
|
|
297
|
-
let _mathmlDepth = 0
|
|
298
|
-
|
|
299
|
-
function createElementWithNS(tag: string): Element {
|
|
300
|
-
if (_svgDepth > 0 || SVG_TAGS.has(tag)) return document.createElementNS(SVG_NS, tag)
|
|
301
|
-
if (_mathmlDepth > 0 || MATHML_TAGS.has(tag)) return document.createElementNS(MATHML_NS, tag)
|
|
302
|
-
return document.createElement(tag)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function mountElement(vnode: VNode, parent: Node, anchor: Node | null): Cleanup {
|
|
306
|
-
const tag = vnode.type as string
|
|
307
|
-
const el = createElementWithNS(tag)
|
|
308
|
-
const isSvg = tag === 'svg'
|
|
309
|
-
const isMathml = tag === 'math'
|
|
310
|
-
if (isSvg) _svgDepth++
|
|
311
|
-
if (isMathml) _mathmlDepth++
|
|
312
|
-
|
|
313
|
-
if (__DEV__ && (vnode.children?.length ?? 0) > 0 && VOID_ELEMENTS.has(vnode.type as string)) {
|
|
314
|
-
console.warn(
|
|
315
|
-
`[Pyreon] <${vnode.type as string}> is a void element and cannot have children. ` +
|
|
316
|
-
'Children passed to void elements will be ignored by the browser.',
|
|
317
|
-
)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Skip applyProps entirely when props is the shared empty sentinel (identity check — no allocation)
|
|
321
|
-
const props = vnode.props
|
|
322
|
-
const propCleanup: Cleanup | null = props !== EMPTY_PROPS ? applyProps(el, props) : null
|
|
323
|
-
|
|
324
|
-
// Mount children inside element context — nested elements can skip DOM removal closures
|
|
325
|
-
_elementDepth++
|
|
326
|
-
const childCleanup = mountChildren(vnode.children ?? [], el, null)
|
|
327
|
-
_elementDepth--
|
|
328
|
-
if (isSvg) _svgDepth--
|
|
329
|
-
if (isMathml) _mathmlDepth--
|
|
330
|
-
|
|
331
|
-
parent.insertBefore(el, anchor)
|
|
332
|
-
|
|
333
|
-
// Populate ref after the element is in the DOM
|
|
334
|
-
const ref = props.ref as RefProp<Element> | null | undefined
|
|
335
|
-
if (ref) {
|
|
336
|
-
if (typeof ref === 'function') ref(el)
|
|
337
|
-
else ref.current = el
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (!propCleanup && childCleanup === noop && !ref) {
|
|
341
|
-
if (_elementDepth > 0) return noop
|
|
342
|
-
return () => {
|
|
343
|
-
const p = el.parentNode
|
|
344
|
-
if (p && (p as Element).isConnected !== false) p.removeChild(el)
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (_elementDepth > 0) {
|
|
349
|
-
if (!ref && !propCleanup) return childCleanup
|
|
350
|
-
if (!ref && propCleanup)
|
|
351
|
-
return () => {
|
|
352
|
-
propCleanup()
|
|
353
|
-
childCleanup()
|
|
354
|
-
}
|
|
355
|
-
const refToClean = ref
|
|
356
|
-
return () => {
|
|
357
|
-
if (refToClean) {
|
|
358
|
-
if (typeof refToClean === 'function') refToClean(null)
|
|
359
|
-
else refToClean.current = null
|
|
360
|
-
}
|
|
361
|
-
if (propCleanup) propCleanup()
|
|
362
|
-
childCleanup()
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return () => {
|
|
367
|
-
if (ref) {
|
|
368
|
-
if (typeof ref === 'function') ref(null)
|
|
369
|
-
else ref.current = null
|
|
370
|
-
}
|
|
371
|
-
if (propCleanup) propCleanup()
|
|
372
|
-
childCleanup()
|
|
373
|
-
const p = el.parentNode
|
|
374
|
-
if (p && (p as Element).isConnected !== false) p.removeChild(el)
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// ─── Component ───────────────────────────────────────────────────────────────
|
|
379
|
-
|
|
380
|
-
function mountComponent(
|
|
381
|
-
vnode: VNode & { type: ComponentFn },
|
|
382
|
-
parent: Node,
|
|
383
|
-
anchor: Node | null,
|
|
384
|
-
): Cleanup {
|
|
385
|
-
const scope = effectScope()
|
|
386
|
-
setCurrentScope(scope)
|
|
387
|
-
|
|
388
|
-
let hooks: ReturnType<typeof runWithHooks>['hooks']
|
|
389
|
-
let output: VNodeChild
|
|
390
|
-
|
|
391
|
-
const componentName = (vnode.type.name || 'Anonymous') as string
|
|
392
|
-
|
|
393
|
-
// Devtools: generate ID + track parent/child hierarchy (dev only).
|
|
394
|
-
// In production, compId/devParentId are never assigned — Vite tree-shakes
|
|
395
|
-
// all __DEV__ blocks + the devtools module import to zero bytes.
|
|
396
|
-
let compId: string | undefined
|
|
397
|
-
let devParentId: string | null | undefined
|
|
398
|
-
if (__DEV__) {
|
|
399
|
-
compId = `${componentName}-${Math.random().toString(36).slice(2, 9)}`
|
|
400
|
-
devParentId = _mountingStack![_mountingStack!.length - 1] ?? null
|
|
401
|
-
_mountingStack!.push(compId)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Merge vnode.children into props.children if not already set
|
|
405
|
-
const children = vnode.children ?? []
|
|
406
|
-
const rawProps =
|
|
407
|
-
children.length > 0 && (vnode.props as Record<string, unknown>).children === undefined
|
|
408
|
-
? {
|
|
409
|
-
...vnode.props,
|
|
410
|
-
children: children.length === 1 ? children[0] : children,
|
|
411
|
-
}
|
|
412
|
-
: vnode.props
|
|
413
|
-
|
|
414
|
-
// Convert compiler-emitted () => expr wrappers into getter properties.
|
|
415
|
-
// This makes component props reactive — reading props.state inside an
|
|
416
|
-
// effect/computed tracks the underlying signals.
|
|
417
|
-
const mergedProps =
|
|
418
|
-
rawProps === EMPTY_PROPS ? rawProps : makeReactiveProps(rawProps as Record<string, unknown>)
|
|
419
|
-
|
|
420
|
-
try {
|
|
421
|
-
const result = runWithHooks(vnode.type, mergedProps)
|
|
422
|
-
hooks = result.hooks
|
|
423
|
-
output = result.vnode
|
|
424
|
-
} catch (err) {
|
|
425
|
-
if (__DEV__) _mountingStack!.pop()
|
|
426
|
-
setCurrentScope(null)
|
|
427
|
-
scope.stop()
|
|
428
|
-
reportError({
|
|
429
|
-
component: componentName,
|
|
430
|
-
phase: 'setup',
|
|
431
|
-
error: err,
|
|
432
|
-
timestamp: Date.now(),
|
|
433
|
-
props: vnode.props as Record<string, unknown>,
|
|
434
|
-
})
|
|
435
|
-
const handled = dispatchToErrorBoundary(err)
|
|
436
|
-
if (!handled) {
|
|
437
|
-
console.error(`[Pyreon] <${componentName}> threw during setup:`, err)
|
|
438
|
-
}
|
|
439
|
-
if (__DEV__ && !handled) {
|
|
440
|
-
const overlay = document.createElement('pre')
|
|
441
|
-
overlay.style.cssText =
|
|
442
|
-
'color:#e53e3e;background:#fff5f5;padding:12px;border:2px solid #e53e3e;border-radius:6px;font-size:12px;margin:4px;font-family:monospace;white-space:pre-wrap;word-break:break-word'
|
|
443
|
-
const e = err as Error
|
|
444
|
-
overlay.textContent = `[${componentName}] ${e.message ?? err}\n${e.stack ?? ''}`
|
|
445
|
-
parent.insertBefore(overlay, anchor)
|
|
446
|
-
return () => overlay.remove()
|
|
447
|
-
}
|
|
448
|
-
return noop
|
|
449
|
-
} finally {
|
|
450
|
-
setCurrentScope(null)
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (__DEV__ && output != null && typeof output === 'object') {
|
|
454
|
-
if (output instanceof Promise) {
|
|
455
|
-
console.warn(
|
|
456
|
-
`[Pyreon] Component <${componentName}> returned a Promise. ` +
|
|
457
|
-
'Components must be synchronous — use lazy() + Suspense for async loading, ' +
|
|
458
|
-
'or fetch data in onMount and store it in a signal.',
|
|
459
|
-
)
|
|
460
|
-
} else if (!('type' in output) && !Array.isArray(output) && !(output as any).__isNative) {
|
|
461
|
-
// Objects without `type` that are NOT arrays (valid VNodeChild[])
|
|
462
|
-
// and NOT NativeItems (from _tpl()) are invalid. Arrays come from
|
|
463
|
-
// Fragment returns, NativeItems come from compiled templates.
|
|
464
|
-
console.warn(
|
|
465
|
-
`[Pyreon] Component <${componentName}> returned an invalid value. Components must return a VNode, string, null, function, or array.`,
|
|
466
|
-
)
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (hooks.update) {
|
|
471
|
-
for (const fn of hooks.update) scope.addUpdateHook(fn)
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
let subtreeCleanup: Cleanup = noop
|
|
475
|
-
try {
|
|
476
|
-
subtreeCleanup = output != null ? mountChild(output, parent, anchor) : noop
|
|
477
|
-
} catch (err) {
|
|
478
|
-
if (__DEV__) _mountingStack!.pop()
|
|
479
|
-
scope.stop()
|
|
480
|
-
const handled = propagateError(err, hooks) || dispatchToErrorBoundary(err)
|
|
481
|
-
if (!handled) {
|
|
482
|
-
reportError({
|
|
483
|
-
component: componentName,
|
|
484
|
-
phase: 'render',
|
|
485
|
-
error: err,
|
|
486
|
-
timestamp: Date.now(),
|
|
487
|
-
props: vnode.props as Record<string, unknown>,
|
|
488
|
-
})
|
|
489
|
-
console.error(`[Pyreon] <${componentName}> threw during render:`, err)
|
|
490
|
-
}
|
|
491
|
-
return noop
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (__DEV__) {
|
|
495
|
-
_mountingStack!.pop()
|
|
496
|
-
const firstEl = parent instanceof Element ? parent.firstElementChild : null
|
|
497
|
-
registerComponent(compId!, componentName, firstEl, devParentId!)
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Fire onMount hooks inline — effects created inside are tracked by the scope.
|
|
501
|
-
// Lazy-allocate mountCleanups only when an onMount callback returns a cleanup fn.
|
|
502
|
-
let mountCleanups: Cleanup[] | null = null
|
|
503
|
-
if (hooks.mount) {
|
|
504
|
-
for (const fn of hooks.mount) {
|
|
505
|
-
try {
|
|
506
|
-
let cleanup: (() => void) | undefined
|
|
507
|
-
scope.runInScope(() => {
|
|
508
|
-
cleanup = fn() as (() => void) | undefined
|
|
509
|
-
})
|
|
510
|
-
if (cleanup) {
|
|
511
|
-
if (mountCleanups === null) mountCleanups = []
|
|
512
|
-
mountCleanups.push(cleanup)
|
|
513
|
-
}
|
|
514
|
-
} catch (err) {
|
|
515
|
-
console.error(`[Pyreon] Error in onMount hook of <${componentName}>:`, err)
|
|
516
|
-
reportError({ component: componentName, phase: 'mount', error: err, timestamp: Date.now() })
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return () => {
|
|
522
|
-
if (__DEV__) unregisterComponent(compId!)
|
|
523
|
-
scope.stop()
|
|
524
|
-
subtreeCleanup()
|
|
525
|
-
if (hooks.unmount) {
|
|
526
|
-
for (const fn of hooks.unmount) {
|
|
527
|
-
try {
|
|
528
|
-
fn()
|
|
529
|
-
} catch (err) {
|
|
530
|
-
console.error(`[Pyreon] Error in onUnmount hook of <${componentName}>:`, err)
|
|
531
|
-
reportError({
|
|
532
|
-
component: componentName,
|
|
533
|
-
phase: 'unmount',
|
|
534
|
-
error: err,
|
|
535
|
-
timestamp: Date.now(),
|
|
536
|
-
})
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
if (mountCleanups) for (const fn of mountCleanups) fn()
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// ─── Children ────────────────────────────────────────────────────────────────
|
|
545
|
-
|
|
546
|
-
function mountChildren(children: VNodeChild[], parent: Node, anchor: Node | null): Cleanup {
|
|
547
|
-
if (children.length === 0) return noop
|
|
548
|
-
|
|
549
|
-
// 1-child fast path
|
|
550
|
-
if (children.length === 1) {
|
|
551
|
-
const c = children[0] as VNodeChild
|
|
552
|
-
if (c !== undefined) {
|
|
553
|
-
if (anchor === null && (typeof c === 'string' || typeof c === 'number')) {
|
|
554
|
-
;(parent as HTMLElement).textContent = String(c)
|
|
555
|
-
return noop
|
|
556
|
-
}
|
|
557
|
-
return mountChild(c, parent, anchor)
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// 2-child fast path — avoids .map() allocation (covers <tr><td/><td/></tr>)
|
|
562
|
-
if (children.length === 2) {
|
|
563
|
-
const c0 = children[0] as VNodeChild
|
|
564
|
-
const c1 = children[1] as VNodeChild
|
|
565
|
-
if (c0 !== undefined && c1 !== undefined) {
|
|
566
|
-
const d0 = mountChild(c0, parent, anchor)
|
|
567
|
-
const d1 = mountChild(c1, parent, anchor)
|
|
568
|
-
if (d0 === noop && d1 === noop) return noop
|
|
569
|
-
if (d0 === noop) return d1
|
|
570
|
-
if (d1 === noop) return d0
|
|
571
|
-
return () => {
|
|
572
|
-
d0()
|
|
573
|
-
d1()
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const cleanups = children.map((c) => mountChild(c, parent, anchor))
|
|
579
|
-
return () => {
|
|
580
|
-
for (const fn of cleanups) fn()
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// ─── Keyed array detection ────────────────────────────────────────────────────
|
|
585
|
-
|
|
586
|
-
/** Returns true if value is a non-empty array of VNodes that all carry keys. */
|
|
587
|
-
function isKeyedArray(value: unknown): value is VNode[] {
|
|
588
|
-
if (!Array.isArray(value) || value.length === 0) return false
|
|
589
|
-
return value.every(
|
|
590
|
-
(v) =>
|
|
591
|
-
v !== null &&
|
|
592
|
-
typeof v === 'object' &&
|
|
593
|
-
!Array.isArray(v) &&
|
|
594
|
-
(v as VNode).key !== null &&
|
|
595
|
-
(v as VNode).key !== undefined,
|
|
596
|
-
)
|
|
597
|
-
}
|