fusee-framework 2.7.0 → 2.7.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.
Files changed (61) hide show
  1. package/README.md +56 -2
  2. package/bin/create-fusee-linux +0 -0
  3. package/bin/create-fusee-mac-arm64 +0 -0
  4. package/bin/create-fusee-mac-intel +0 -0
  5. package/bin/create-fusee-win.exe +0 -0
  6. package/framework/auto-import-preset.js +20 -12
  7. package/framework/auto-imports.d.ts +68 -39
  8. package/framework/core/compiler.js +310 -310
  9. package/framework/core/component-core.js +445 -0
  10. package/framework/core/component.js +11 -440
  11. package/framework/core/composable.js +29 -29
  12. package/framework/core/h.js +588 -557
  13. package/framework/core/jsx-helpers.js +11 -2
  14. package/framework/core/jsx-runtime.js +73 -73
  15. package/framework/core/rust-compiler/pkg/fusee_compiler_bg.wasm +0 -0
  16. package/framework/core/store.js +333 -333
  17. package/framework/globals.js +42 -0
  18. package/framework/index.js +22 -55
  19. package/framework/router/file-router.js +184 -184
  20. package/framework/server/actions.js +183 -183
  21. package/framework/server/generate-manifest.js +3 -3
  22. package/framework/types/component.d.ts +97 -86
  23. package/framework/types/index.d.ts +118 -117
  24. package/package.json +83 -80
  25. package/framework/core/rust-compiler/Cargo.toml +0 -31
  26. package/framework/core/rust-compiler/README.md +0 -69
  27. package/framework/core/rust-compiler/pkg/README.md +0 -69
  28. package/framework/core/rust-compiler/pkg/fusee_compiler.d.ts +0 -6
  29. package/framework/core/rust-compiler/pkg/fusee_compiler.js +0 -9
  30. package/framework/core/rust-compiler/pkg/fusee_compiler_bg.js +0 -137
  31. package/framework/core/rust-compiler/pkg/fusee_compiler_bg.wasm.d.ts +0 -11
  32. package/framework/core/rust-compiler/pkg/package.json +0 -18
  33. package/framework/core/rust-compiler/src/ast.rs +0 -274
  34. package/framework/core/rust-compiler/src/errors.rs +0 -204
  35. package/framework/core/rust-compiler/src/generator.rs +0 -583
  36. package/framework/core/rust-compiler/src/lexer.rs +0 -414
  37. package/framework/core/rust-compiler/src/lib.rs +0 -87
  38. package/framework/core/rust-compiler/src/main_compiler.rs +0 -165
  39. package/framework/core/rust-compiler/src/parser.rs +0 -516
  40. package/framework/core/rust-compiler/src/ssr_generator.rs +0 -688
  41. package/framework/core/rust-compiler/src/transformer.rs +0 -562
  42. package/framework/core/rust-compiler/src/utils.rs +0 -10
  43. package/framework/engine-go/actions.go +0 -112
  44. package/framework/engine-go/cmd/server/main.go +0 -233
  45. package/framework/engine-go/comet/comet.go +0 -41
  46. package/framework/engine-go/comet/fragment.go +0 -51
  47. package/framework/engine-go/dispatcher.go +0 -120
  48. package/framework/engine-go/elements.go +0 -248
  49. package/framework/engine-go/evalexpr/evalexpr.go +0 -733
  50. package/framework/engine-go/go.mod +0 -3
  51. package/framework/engine-go/hydration.go +0 -43
  52. package/framework/engine-go/load-helpers.go +0 -29
  53. package/framework/engine-go/manifest.go +0 -41
  54. package/framework/engine-go/reactive.go +0 -123
  55. package/framework/engine-go/render.go +0 -635
  56. package/framework/engine-go/reqcontext.go +0 -48
  57. package/framework/engine-go/router.go +0 -85
  58. package/framework/engine-go/rustast/rustast.go +0 -317
  59. package/framework/engine-go/ssr.go +0 -103
  60. package/framework/engine-go/static-assets.go +0 -139
  61. package/framework/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -1,558 +1,589 @@
1
- import { effect, batch, signal, microEffect } from '../core/signal.js'
2
- import { createEventHandler, registerDelegatedEvent, isDelegatedEvent } from '../core/event-delegation.js'
3
- import { resolveElementProperty } from '../core/element-props.js'
4
-
5
- export { effect as _effect, batch as _batch }
6
-
7
- /** Reports whether a function should be evaluated as a plain getter. */
8
- function isGetter(v) {
9
- return typeof v === 'function' && !v.isSignal
10
- }
11
-
12
- const SENSITIVE_ATTRS = new Set(['href', 'src', 'srcset', 'formaction', 'xlink:href', 'data'])
13
- const DANGEROUS_SCHEME = /^(javascript|data|vbscript|file):/i
14
-
15
- /** Sanitizes URL-bearing DOM properties before they reach the browser. */
16
- function sanitize(name, value) {
17
- if (SENSITIVE_ATTRS.has(name.toLowerCase())) {
18
- const trimmed = String(value).trim()
19
- if (DANGEROUS_SCHEME.test(trimmed)) {
20
- console.warn(`[fusée] Blocked potential XSS on "${name}": "${value}"`)
21
- return 'about:blank'
22
- }
23
- }
24
- return value
25
- }
26
-
27
- /** Creates a framework node for an element and mounts its child nodes. */
28
- export function h(tag, props = {}, children = [], isStatic = false) {
29
- const el = document.createElement(tag)
30
- const effects = []
31
-
32
- if (!isStatic) {
33
- _applyProps(el, props, effects)
34
- } else {
35
- _applyStaticProps(el, props)
36
- }
37
-
38
- const childNodes = _mountChildren(el, children, effects)
39
-
40
- return {
41
- node: el,
42
- effects,
43
- props,
44
- children: childNodes,
45
- _tag: tag,
46
- }
47
- }
48
-
49
- /** Creates a text node that optionally updates from a reactive getter. */
50
- export function hText(value) {
51
- const node = document.createTextNode('')
52
- const effects = []
53
-
54
- if (typeof value === 'function') {
55
- const cleanup = microEffect(() => {
56
- node.textContent = String(value() ?? '')
57
- })
58
- effects.push(cleanup)
59
- } else {
60
- node.textContent = String(value ?? '')
61
- }
62
-
63
- return { node, effects }
64
- }
65
-
66
- /** Creates a reactive conditional anchored by a comment node. */
67
- export function hIf(branches) {
68
- const anchor = document.createComment('f-if')
69
- const effects = []
70
-
71
- let currentNodes = []
72
- let currentBranch = -1
73
-
74
- const cleanup = effect(() => {
75
- let matchIdx = -1
76
- for (let i = 0; i < branches.length; i++) {
77
- const [cond] = branches[i]
78
- if (cond === null || cond()) { matchIdx = i; break }
79
- }
80
-
81
- if (matchIdx === currentBranch) return
82
-
83
- _unmountNodes(currentNodes)
84
- currentNodes = []
85
- currentBranch = matchIdx
86
-
87
- if (matchIdx === -1) return
88
-
89
- const [, getChildren] = branches[matchIdx]
90
- const children = getChildren()
91
- const parent = anchor.parentNode
92
-
93
- if (parent) {
94
- for (const fnode of children) {
95
- parent.insertBefore(fnode.node, anchor)
96
- currentNodes.push(fnode)
97
- }
98
- }
99
- })
100
-
101
- effects.push(cleanup)
102
-
103
- return {
104
- node: anchor,
105
- effects,
106
- _ifBranchNodes: () => currentNodes,
107
- _isIf: true,
108
- }
109
- }
110
-
111
- /** Creates a keyed reactive list anchored by a comment node. */
112
- export function hFor(sourceGetter, renderItem, keyFn) {
113
- const anchor = document.createComment('f-for')
114
- const effects = []
115
-
116
- let keyedMap = new Map()
117
- let oldKeys = []
118
- let _keyEvalCurrent = null
119
- const keyEvaluatorGetter = () => _keyEvalCurrent
120
- keyEvaluatorGetter.isSignal = true
121
-
122
- const cleanup = effect(() => {
123
- const list = sourceGetter() ?? []
124
- const parent = anchor.parentNode
125
-
126
- if (!parent) {
127
- return
128
- }
129
-
130
- const newKeys = new Array(list.length)
131
- for (let i = 0; i < list.length; i++) {
132
- _keyEvalCurrent = list[i]
133
- newKeys[i] = keyFn(keyEvaluatorGetter, i)
134
- }
135
-
136
- if (oldKeys.length === newKeys.length) {
137
- let identical = true
138
- for (let i = 0; i < newKeys.length; i++) {
139
- if (oldKeys[i] !== newKeys[i]) { identical = false; break }
140
- }
141
- if (identical) {
142
- for (let i = 0; i < list.length; i++) {
143
- const entry = keyedMap.get(newKeys[i])
144
- if (entry.itemSignal() !== list[i]) {
145
- entry.itemSignal(list[i])
146
- }
147
- }
148
- return
149
- }
150
- }
151
-
152
- if (oldKeys.length === newKeys.length) {
153
- let diffA = -1, diffB = -1, diffCount = 0
154
- for (let i = 0; i < newKeys.length; i++) {
155
- if (oldKeys[i] !== newKeys[i]) {
156
- if (diffCount === 0) {
157
- diffA = i
158
- } else if (diffCount === 1) {
159
- diffB = i
160
- }
161
- diffCount++
162
- if (diffCount > 2)
163
- break
164
- }
165
- }
166
- if (diffCount === 2 && oldKeys[diffA] === newKeys[diffB] && oldKeys[diffB] === newKeys[diffA]) {
167
- const entryA = keyedMap.get(oldKeys[diffA])
168
- const entryB = keyedMap.get(oldKeys[diffB])
169
- const nodeA = entryA.fnode.node
170
- const nodeB = entryB.fnode.node
171
- const refA = nodeA.nextSibling
172
- const parentEl = nodeA.parentNode
173
- parentEl.insertBefore(nodeA, nodeB.nextSibling)
174
- parentEl.insertBefore(nodeB, refA)
175
- if (entryA.itemSignal() !== list[diffA]) {
176
- entryA.itemSignal(list[diffA])
177
- }
178
- if (entryB.itemSignal() !== list[diffB]) {
179
- entryB.itemSignal(list[diffB])
180
- }
181
- oldKeys = newKeys
182
- return
183
- }
184
- }
185
-
186
- const newMap = new Map()
187
-
188
- for (let i = 0; i < list.length; i++) {
189
- const item = list[i]
190
- const key = newKeys[i]
191
-
192
- const exists = keyedMap.get(key)
193
- if (exists) {
194
- if (exists.itemSignal() !== item) {
195
- exists.itemSignal(item)
196
- }
197
- newMap.set(key, exists)
198
- } else {
199
- const itemSignal = signal(item)
200
- const fnode = renderItem(itemSignal, i)
201
- newMap.set(key, { fnode, key, itemSignal })
202
- }
203
- }
204
-
205
- for (const [key, entry] of keyedMap) {
206
- if (!newMap.has(key)) {
207
- _unmountNodes([entry.fnode])
208
- }
209
- }
210
-
211
- const newEntries = newKeys.map(k => newMap.get(k))
212
- const oldIndexOf = new Map()
213
- oldKeys.forEach((k, i) => oldIndexOf.set(k, i))
214
-
215
- const posMap = newEntries.map(entry => {
216
- const idx = oldIndexOf.get(entry.key)
217
- return idx === undefined ? -1 : idx
218
- })
219
-
220
- const lisIndices = _getLIS(posMap)
221
- let lisPtr = lisIndices.length - 1
222
-
223
- for (let i = newEntries.length - 1; i >= 0; i--) {
224
- const entry = newEntries[i]
225
- const ref = i === newEntries.length - 1 ? anchor : newEntries[i + 1].fnode.node
226
- const inLis = lisPtr >= 0 && i === lisIndices[lisPtr]
227
-
228
- if (inLis) {
229
- lisPtr--
230
- if (!entry.fnode.node.parentNode) {
231
- parent.insertBefore(entry.fnode.node, ref)
232
- }
233
- } else if (entry.fnode.node.nextSibling !== ref || !entry.fnode.node.parentNode) {
234
- parent.insertBefore(entry.fnode.node, ref)
235
- }
236
- }
237
-
238
- keyedMap = newMap
239
- oldKeys = newKeys
240
- })
241
-
242
- effects.push(cleanup)
243
-
244
- return {
245
- node: anchor,
246
- effects,
247
- _isFor: true,
248
- _getNodes: () => [...keyedMap.values()].map(e => e.fnode),
249
- }
250
- }
251
-
252
- function _getLIS(arr) {
253
- const p = arr.slice()
254
- const result = []
255
-
256
- for (let i = 0; i < arr.length; i++) {
257
- if (arr[i] === -1) continue
258
-
259
- if (result.length === 0 || arr[result[result.length - 1]] < arr[i]) {
260
- p[i] = result.length ? result[result.length - 1] : -1
261
- result.push(i)
262
- continue
263
- }
264
-
265
- let lo = 0, hi = result.length - 1
266
- while (lo < hi) {
267
- const mid = (lo + hi) >> 1
268
- if (arr[result[mid]] < arr[i]) lo = mid + 1
269
- else hi = mid
270
- }
271
- if (arr[i] < arr[result[lo]]) {
272
- p[i] = lo > 0 ? result[lo - 1] : -1
273
- result[lo] = i
274
- }
275
- }
276
-
277
- let u = result.length
278
- let v = u > 0 ? result[u - 1] : -1
279
- const seq = new Array(u)
280
- while (u-- > 0) {
281
- seq[u] = v
282
- v = p[v]
283
- }
284
- return seq
285
- }
286
-
287
- /** Marks a slots object whose outlets must stay native `<slot>` elements (shadow DOM). */
288
- export const NATIVE_SLOTS = Symbol.for('fusee.nativeSlots')
289
-
290
- export function hSlot(slots, name, fallback = []) {
291
- if (slots?.[NATIVE_SLOTS]) {
292
- return h('slot', name === 'default' ? {} : { name }, fallback)
293
- }
294
-
295
- const anchor = document.createComment(`slot:${name}`)
296
- const effects = []
297
- const slotFn = slots?.[name]
298
- const nodes = typeof slotFn === 'function' ? slotFn() : fallback
299
- return {
300
- node: anchor,
301
- effects,
302
- _slotNodes: nodes,
303
- _isSlot: true,
304
- }
305
- }
306
-
307
- export function createComponent(name, ComponentFn, rawProps = {}, rawSlots = {}) {
308
- if (!ComponentFn) {
309
- console.warn(`[fusée] Component "${name}" is not registered`)
310
- const placeholder = document.createComment(`missing:${name}`)
311
- return { node: placeholder, effects: [] }
312
- }
313
-
314
- const resolvedProps = _resolveComponentProps(rawProps)
315
- const listeners = {}
316
- const slots = {}
317
-
318
- for (const [key, value] of Object.entries(rawProps)) {
319
- if (key.startsWith('on:')) {
320
- listeners[key.slice(3)] = value
321
- }
322
- }
323
-
324
- for (const [slotName, slotFn] of Object.entries(rawSlots)) {
325
- slots[slotName] = slotFn
326
- }
327
-
328
- const container = document.createElement('div')
329
- container.setAttribute('data-fusee-component', name)
330
-
331
- const api = ComponentFn(resolvedProps, { listeners, slots })
332
- api.render(container)
333
- const root = container.firstElementChild ?? container
334
-
335
- const effects = []
336
- effects.push(() => api.unmount())
337
-
338
- return { node: root, effects, _component: api }
339
- }
340
-
341
- function _applyProps(el, props, effects) {
342
- for (const key in props) {
343
- const value = props[key]
344
- if (key.startsWith('@')) {
345
- const eventName = key.slice(1)
346
- const { handler, modifiers = [] } = value
347
- const handlerState = { timeoutId: null, throttleTimeoutId: null, lastRun: 0 }
348
- const wrappedHandler = createEventHandler(handler, modifiers, null, null, handlerState)
349
-
350
- let cleanup
351
- if (isDelegatedEvent(eventName)) {
352
- cleanup = registerDelegatedEvent(el, eventName, wrappedHandler, { modifiers, handlerState })
353
- } else {
354
- el.addEventListener(eventName, wrappedHandler)
355
- cleanup = () => el.removeEventListener(eventName, wrappedHandler)
356
- }
357
- effects.push(cleanup)
358
- continue
359
- }
360
-
361
- if (key === 'f-show') {
362
- const cleanup = effect(() => {
363
- el.style.display = value() ? '' : 'none'
364
- })
365
- effects.push(cleanup)
366
- continue
367
- }
368
-
369
- if (key === 'f-html') {
370
- const cleanup = effect(() => {
371
- el.innerHTML = String(value() ?? '')
372
- })
373
- effects.push(cleanup)
374
- continue
375
- }
376
-
377
- if (key === 'f-model') {
378
- const sig = value()
379
- const cleanup = effect(() => {
380
- const v = typeof sig === 'function' ? sig() : sig
381
- if (el.value !== String(v ?? '')) el.value = String(v ?? '')
382
- })
383
- effects.push(cleanup)
384
- continue
385
- }
386
-
387
- if (key === 'f-ref') {
388
- el.dispatchEvent(new CustomEvent('fusee:ref', {
389
- detail: { name: value },
390
- bubbles: true,
391
- }))
392
- continue
393
- }
394
-
395
- if (key === 'f-once') continue
396
-
397
- if (typeof value !== 'function') {
398
- _applyAttr(el, key, value)
399
- continue
400
- }
401
-
402
- const attrName = key
403
- const getter = value
404
- const cleanup = effect(() => {
405
- _applyAttr(el, attrName, getter())
406
- })
407
- effects.push(cleanup)
408
- }
409
- }
410
-
411
- function _applyStaticProps(el, props) {
412
- for (const key in props) {
413
- const value = props[key]
414
- if (key.startsWith('@') || key.startsWith('f-'))
415
- continue
416
- if (typeof value !== 'function')
417
- _applyAttr(el, key, value)
418
- }
419
- }
420
-
421
- function _applyAttr(el, name, value) {
422
- if (name === 'class') {
423
- const cls = _resolveClass(value)
424
- if (cls) el.className = cls
425
- else el.removeAttribute('class')
426
- return
427
- }
428
-
429
- if (name === 'style') {
430
- if (value !== null && typeof value === 'object') {
431
- for (const k in value) el.style[k] = value[k]
432
- } else {
433
- el.style.cssText = String(value ?? '')
434
- }
435
- return
436
- }
437
-
438
- const elementProp = resolveElementProperty(el, name, value)
439
- if (elementProp) {
440
- el[elementProp] = value
441
- return
442
- }
443
-
444
- if (name in el && typeof el[name] !== 'undefined' && name !== 'type') {
445
- try {
446
- el[name] = value
447
- return
448
- } catch (_) {
449
- return
450
- }
451
- }
452
-
453
- if (typeof value === 'boolean') {
454
- value ? el.setAttribute(name, '') : el.removeAttribute(name)
455
- return
456
- }
457
-
458
- if (value == null) {
459
- el.removeAttribute(name)
460
- return
461
- }
462
-
463
- el.setAttribute(name, sanitize(name, String(value)))
464
- }
465
-
466
- function _resolveClass(value) {
467
- if (typeof value === 'string')
468
- return value
469
- if (Array.isArray(value))
470
- return value.filter(Boolean).join(' ')
471
- if (value && typeof value === 'object')
472
- return Object.entries(value).filter(([, v]) => !!v).map(([k]) => k).join(' ')
473
- return String(value ?? '')
474
- }
475
-
476
- function _mountChildren(parent, children, parentEffects) {
477
- const mounted = []
478
-
479
- for (const fnode of children) {
480
- if (!fnode || !fnode.node) continue
481
-
482
- parent.appendChild(fnode.node)
483
-
484
- if (fnode._isSlot && fnode._slotNodes) {
485
- for (const sn of fnode._slotNodes) {
486
- if (sn?.node) parent.insertBefore(sn.node, null)
487
- }
488
- }
489
-
490
- if (fnode.effects) parentEffects.push(...fnode.effects)
491
- mounted.push(fnode)
492
- }
493
-
494
- return mounted
495
- }
496
-
497
- function _unmountNodes(fnodes) {
498
- for (const fnode of fnodes) {
499
- if (!fnode) continue
500
-
501
- if (fnode.effects) {
502
- for (let i = fnode.effects.length - 1; i >= 0; i--) {
503
- const cleanup = fnode.effects[i]
504
- if (typeof cleanup === 'function') cleanup()
505
- }
506
- }
507
-
508
- if (fnode.node?.parentNode) {
509
- fnode.node.parentNode.removeChild(fnode.node)
510
- }
511
-
512
- if (fnode._ifBranchNodes) {
513
- _unmountNodes(fnode._ifBranchNodes())
514
- }
515
-
516
- if (fnode._getNodes) {
517
- _unmountNodes(fnode._getNodes())
518
- }
519
- }
520
- }
521
-
522
- function _resolveComponentProps(rawProps) {
523
- const resolved = {}
524
-
525
- for (const [key, value] of Object.entries(rawProps)) {
526
- if (key.startsWith('on:')) continue
527
-
528
- if (isGetter(value)) {
529
- Object.defineProperty(resolved, key, {
530
- get: value,
531
- enumerable: true,
532
- configurable: true,
533
- })
534
- } else {
535
- resolved[key] = value
536
- }
537
- }
538
-
539
- return resolved
540
- }
541
-
542
- export function mount(renderFn, ctx, components, container) {
543
- const fnodes = renderFn(ctx, components)
544
- const effects = []
545
-
546
- for (const fnode of fnodes) {
547
- if (!fnode?.node) continue
548
- container.appendChild(fnode.node)
549
- if (fnode.effects) effects.push(...fnode.effects)
550
- }
551
-
552
- return {
553
- unmount() {
554
- _unmountNodes(fnodes)
555
- container.innerHTML = ''
556
- }
557
- }
1
+ import { effect, batch, signal, microEffect, untrack } from '../core/signal.js'
2
+ import { createEventHandler, registerDelegatedEvent, isDelegatedEvent } from '../core/event-delegation.js'
3
+ import { resolveElementProperty } from '../core/element-props.js'
4
+
5
+ export { effect as _effect, batch as _batch }
6
+
7
+ /** Reports whether a function should be evaluated as a plain getter. */
8
+ function isGetter(v) {
9
+ return typeof v === 'function' && !v.isSignal
10
+ }
11
+
12
+ const SENSITIVE_ATTRS = new Set(['href', 'src', 'srcset', 'formaction', 'xlink:href', 'data'])
13
+ const DANGEROUS_SCHEME = /^(javascript|data|vbscript|file):/i
14
+
15
+ /** Sanitizes URL-bearing DOM properties before they reach the browser. */
16
+ function sanitize(name, value) {
17
+ if (SENSITIVE_ATTRS.has(name.toLowerCase())) {
18
+ const trimmed = String(value).trim()
19
+ if (DANGEROUS_SCHEME.test(trimmed)) {
20
+ console.warn(`[fusée] Blocked potential XSS on "${name}": "${value}"`)
21
+ return 'about:blank'
22
+ }
23
+ }
24
+ return value
25
+ }
26
+
27
+ /** Creates a framework node for an element and mounts its child nodes. */
28
+ export function h(tag, props = {}, children = [], isStatic = false) {
29
+ const el = document.createElement(tag)
30
+ const effects = []
31
+
32
+ if (!isStatic) {
33
+ _applyProps(el, props, effects)
34
+ } else {
35
+ _applyStaticProps(el, props)
36
+ }
37
+
38
+ const childNodes = _mountChildren(el, children, effects)
39
+
40
+ return {
41
+ node: el,
42
+ effects,
43
+ props,
44
+ children: childNodes,
45
+ _tag: tag,
46
+ }
47
+ }
48
+
49
+ /** Creates a text node that optionally updates from a reactive getter. */
50
+ export function hText(value) {
51
+ const node = document.createTextNode('')
52
+ const effects = []
53
+
54
+ if (typeof value === 'function') {
55
+ const cleanup = microEffect(() => {
56
+ node.textContent = String(value() ?? '')
57
+ })
58
+ effects.push(cleanup)
59
+ } else {
60
+ node.textContent = String(value ?? '')
61
+ }
62
+
63
+ return { node, effects }
64
+ }
65
+
66
+ /** Creates a reactive conditional anchored by a comment node. */
67
+ export function hIf(branches) {
68
+ const anchor = document.createComment('f-if')
69
+ const effects = []
70
+
71
+ let currentNodes = []
72
+ let currentBranch = -1
73
+ const mounted = _createMountTracker()
74
+
75
+ const cleanup = effect(() => {
76
+ mounted.track()
77
+ let matchIdx = -1
78
+ for (let i = 0; i < branches.length; i++) {
79
+ const [cond] = branches[i]
80
+ if (cond === null || cond()) { matchIdx = i; break }
81
+ }
82
+
83
+ const parent = anchor.parentNode
84
+ if (!parent) return mounted.defer()
85
+
86
+ if (matchIdx === currentBranch) return
87
+
88
+ _unmountNodes(currentNodes)
89
+ currentNodes = []
90
+ currentBranch = matchIdx
91
+
92
+ if (matchIdx === -1) return
93
+
94
+ const [, getChildren] = branches[matchIdx]
95
+ const children = untrack(getChildren)
96
+
97
+ for (const fnode of children) {
98
+ if (!fnode?.node) continue
99
+ insertFNode(parent, fnode, anchor)
100
+ currentNodes.push(fnode)
101
+ }
102
+ })
103
+
104
+ effects.push(cleanup)
105
+
106
+ return {
107
+ node: anchor,
108
+ effects,
109
+ _ifBranchNodes: () => currentNodes,
110
+ _isIf: true,
111
+ _afterInsert: mounted.resume,
112
+ }
113
+ }
114
+
115
+ /** Creates a keyed reactive list anchored by a comment node. */
116
+ export function hFor(sourceGetter, renderItem, keyFn) {
117
+ const anchor = document.createComment('f-for')
118
+ const effects = []
119
+
120
+ let keyedMap = new Map()
121
+ let oldKeys = []
122
+ let _keyEvalCurrent = null
123
+ const keyEvaluatorGetter = () => _keyEvalCurrent
124
+ keyEvaluatorGetter.isSignal = true
125
+ const mounted = _createMountTracker()
126
+
127
+ const cleanup = effect(() => {
128
+ mounted.track()
129
+ const list = sourceGetter() ?? []
130
+ const parent = anchor.parentNode
131
+
132
+ if (!parent) return mounted.defer()
133
+
134
+ const newKeys = new Array(list.length)
135
+ for (let i = 0; i < list.length; i++) {
136
+ _keyEvalCurrent = list[i]
137
+ const key = keyFn(keyEvaluatorGetter, i)
138
+ // Loops without :key compile to a key of undefined; key them by position, as SSR does.
139
+ newKeys[i] = key === undefined ? i : key
140
+ }
141
+
142
+ if (oldKeys.length === newKeys.length) {
143
+ let identical = true
144
+ for (let i = 0; i < newKeys.length; i++) {
145
+ if (oldKeys[i] !== newKeys[i]) { identical = false; break }
146
+ }
147
+ if (identical) {
148
+ for (let i = 0; i < list.length; i++) {
149
+ _setItem(keyedMap.get(newKeys[i]), list[i])
150
+ }
151
+ return
152
+ }
153
+ }
154
+
155
+ if (oldKeys.length === newKeys.length) {
156
+ let diffA = -1, diffB = -1, diffCount = 0
157
+ for (let i = 0; i < newKeys.length; i++) {
158
+ if (oldKeys[i] !== newKeys[i]) {
159
+ if (diffCount === 0) {
160
+ diffA = i
161
+ } else if (diffCount === 1) {
162
+ diffB = i
163
+ }
164
+ diffCount++
165
+ if (diffCount > 2)
166
+ break
167
+ }
168
+ }
169
+ if (diffCount === 2 && oldKeys[diffA] === newKeys[diffB] && oldKeys[diffB] === newKeys[diffA]) {
170
+ const entryA = keyedMap.get(oldKeys[diffA])
171
+ const entryB = keyedMap.get(oldKeys[diffB])
172
+ const nodeA = entryA.fnode.node
173
+ const nodeB = entryB.fnode.node
174
+ const refA = nodeA.nextSibling
175
+ const parentEl = nodeA.parentNode
176
+ parentEl.insertBefore(nodeA, nodeB.nextSibling)
177
+ parentEl.insertBefore(nodeB, refA)
178
+ // The two rows traded places: each keeps its key and receives the item now at its new index.
179
+ _setItem(entryA, list[diffB])
180
+ _setItem(entryB, list[diffA])
181
+ oldKeys = newKeys
182
+ return
183
+ }
184
+ }
185
+
186
+ const newMap = new Map()
187
+
188
+ for (let i = 0; i < list.length; i++) {
189
+ const item = list[i]
190
+ const key = newKeys[i]
191
+
192
+ const exists = keyedMap.get(key)
193
+ if (exists) {
194
+ _setItem(exists, item)
195
+ newMap.set(key, exists)
196
+ } else {
197
+ const itemSignal = signal(item)
198
+ const fnode = untrack(() => renderItem(itemSignal, i))
199
+ newMap.set(key, { fnode, key, itemSignal })
200
+ }
201
+ }
202
+
203
+ for (const [key, entry] of keyedMap) {
204
+ if (!newMap.has(key)) {
205
+ _unmountNodes([entry.fnode])
206
+ }
207
+ }
208
+
209
+ const newEntries = newKeys.map(k => newMap.get(k))
210
+ const oldIndexOf = new Map()
211
+ oldKeys.forEach((k, i) => oldIndexOf.set(k, i))
212
+
213
+ const posMap = newEntries.map(entry => {
214
+ const idx = oldIndexOf.get(entry.key)
215
+ return idx === undefined ? -1 : idx
216
+ })
217
+
218
+ const lisIndices = _getLIS(posMap)
219
+ let lisPtr = lisIndices.length - 1
220
+
221
+ for (let i = newEntries.length - 1; i >= 0; i--) {
222
+ const entry = newEntries[i]
223
+ const ref = i === newEntries.length - 1 ? anchor : newEntries[i + 1].fnode.node
224
+ const inLis = lisPtr >= 0 && i === lisIndices[lisPtr]
225
+
226
+ if (inLis) {
227
+ lisPtr--
228
+ if (!entry.fnode.node.parentNode) {
229
+ insertFNode(parent, entry.fnode, ref)
230
+ }
231
+ } else if (entry.fnode.node.nextSibling !== ref || !entry.fnode.node.parentNode) {
232
+ insertFNode(parent, entry.fnode, ref)
233
+ }
234
+ }
235
+
236
+ keyedMap = newMap
237
+ oldKeys = newKeys
238
+ })
239
+
240
+ effects.push(cleanup)
241
+
242
+ return {
243
+ node: anchor,
244
+ effects,
245
+ _isFor: true,
246
+ _getNodes: () => [...keyedMap.values()].map(e => e.fnode),
247
+ _afterInsert: mounted.resume,
248
+ }
249
+ }
250
+
251
+ /** Updates a keyed row's item signal without subscribing the list effect to it. */
252
+ function _setItem(entry, item) {
253
+ if (untrack(entry.itemSignal) !== item) entry.itemSignal(item)
254
+ }
255
+
256
+ /**
257
+ * hIf/hFor are built before their anchor has a parent (children are created before the
258
+ * element that holds them), so their first render waits until insertFNode() places the
259
+ * anchor and calls _afterInsert.
260
+ */
261
+ function _createMountTracker() {
262
+ const tick = signal(0)
263
+ let pending = false
264
+ return {
265
+ track: () => tick(),
266
+ defer: () => { pending = true },
267
+ resume: () => {
268
+ if (!pending) return
269
+ pending = false
270
+ tick(untrack(tick) + 1)
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Inserts a framework node (and slot nodes it carries) before ref, then lets anchor-based
277
+ * nodes (hIf/hFor) render now that they have a parent.
278
+ */
279
+ export function insertFNode(parent, fnode, ref = null) {
280
+ parent.insertBefore(fnode.node, ref)
281
+ if (fnode._isSlot && fnode._slotNodes) {
282
+ for (const sn of fnode._slotNodes) {
283
+ if (sn?.node) insertFNode(parent, sn, ref)
284
+ }
285
+ }
286
+ if (typeof fnode._afterInsert === 'function') fnode._afterInsert()
287
+ }
288
+
289
+ function _getLIS(arr) {
290
+ const p = arr.slice()
291
+ const result = []
292
+
293
+ for (let i = 0; i < arr.length; i++) {
294
+ if (arr[i] === -1) continue
295
+
296
+ if (result.length === 0 || arr[result[result.length - 1]] < arr[i]) {
297
+ p[i] = result.length ? result[result.length - 1] : -1
298
+ result.push(i)
299
+ continue
300
+ }
301
+
302
+ let lo = 0, hi = result.length - 1
303
+ while (lo < hi) {
304
+ const mid = (lo + hi) >> 1
305
+ if (arr[result[mid]] < arr[i]) lo = mid + 1
306
+ else hi = mid
307
+ }
308
+ if (arr[i] < arr[result[lo]]) {
309
+ p[i] = lo > 0 ? result[lo - 1] : -1
310
+ result[lo] = i
311
+ }
312
+ }
313
+
314
+ let u = result.length
315
+ let v = u > 0 ? result[u - 1] : -1
316
+ const seq = new Array(u)
317
+ while (u-- > 0) {
318
+ seq[u] = v
319
+ v = p[v]
320
+ }
321
+ return seq
322
+ }
323
+
324
+ /** Marks a slots object whose outlets must stay native `<slot>` elements (shadow DOM). */
325
+ export const NATIVE_SLOTS = Symbol.for('fusee.nativeSlots')
326
+
327
+ export function hSlot(slots, name, fallback = []) {
328
+ if (slots?.[NATIVE_SLOTS]) {
329
+ return h('slot', name === 'default' ? {} : { name }, fallback)
330
+ }
331
+
332
+ const anchor = document.createComment(`slot:${name}`)
333
+ const effects = []
334
+ const slotFn = slots?.[name]
335
+ const nodes = typeof slotFn === 'function' ? slotFn() : fallback
336
+ return {
337
+ node: anchor,
338
+ effects,
339
+ _slotNodes: nodes,
340
+ _isSlot: true,
341
+ }
342
+ }
343
+
344
+ export function createComponent(name, ComponentFn, rawProps = {}, rawSlots = {}) {
345
+ if (!ComponentFn) {
346
+ console.warn(`[fusée] Component "${name}" is not registered`)
347
+ const placeholder = document.createComment(`missing:${name}`)
348
+ return { node: placeholder, effects: [] }
349
+ }
350
+
351
+ const resolvedProps = _resolveComponentProps(rawProps)
352
+ const listeners = {}
353
+ const slots = {}
354
+
355
+ for (const [key, value] of Object.entries(rawProps)) {
356
+ if (key.startsWith('on:')) {
357
+ listeners[key.slice(3)] = value
358
+ }
359
+ }
360
+
361
+ for (const [slotName, slotFn] of Object.entries(rawSlots)) {
362
+ slots[slotName] = slotFn
363
+ }
364
+
365
+ const container = document.createElement('div')
366
+ container.setAttribute('data-fusee-component', name)
367
+
368
+ const api = ComponentFn(resolvedProps, { listeners, slots })
369
+ api.render(container)
370
+ const root = container.firstElementChild ?? container
371
+
372
+ const effects = []
373
+ effects.push(() => api.unmount())
374
+
375
+ return { node: root, effects, _component: api }
376
+ }
377
+
378
+ function _applyProps(el, props, effects) {
379
+ for (const key in props) {
380
+ const value = props[key]
381
+ if (key.startsWith('@')) {
382
+ const eventName = key.slice(1)
383
+ const { handler, modifiers = [] } = value
384
+ const handlerState = { timeoutId: null, throttleTimeoutId: null, lastRun: 0 }
385
+ const wrappedHandler = createEventHandler(handler, modifiers, null, null, handlerState)
386
+
387
+ let cleanup
388
+ if (isDelegatedEvent(eventName)) {
389
+ cleanup = registerDelegatedEvent(el, eventName, wrappedHandler, { modifiers, handlerState })
390
+ } else {
391
+ el.addEventListener(eventName, wrappedHandler)
392
+ cleanup = () => el.removeEventListener(eventName, wrappedHandler)
393
+ }
394
+ effects.push(cleanup)
395
+ continue
396
+ }
397
+
398
+ if (key === 'f-show') {
399
+ const cleanup = effect(() => {
400
+ el.style.display = value() ? '' : 'none'
401
+ })
402
+ effects.push(cleanup)
403
+ continue
404
+ }
405
+
406
+ if (key === 'f-html') {
407
+ const cleanup = effect(() => {
408
+ el.innerHTML = String(value() ?? '')
409
+ })
410
+ effects.push(cleanup)
411
+ continue
412
+ }
413
+
414
+ if (key === 'f-model') {
415
+ const sig = value()
416
+ const cleanup = effect(() => {
417
+ const v = typeof sig === 'function' ? sig() : sig
418
+ if (el.value !== String(v ?? '')) el.value = String(v ?? '')
419
+ })
420
+ effects.push(cleanup)
421
+ continue
422
+ }
423
+
424
+ if (key === 'f-ref') {
425
+ el.dispatchEvent(new CustomEvent('fusee:ref', {
426
+ detail: { name: value },
427
+ bubbles: true,
428
+ }))
429
+ continue
430
+ }
431
+
432
+ if (key === 'f-once') continue
433
+
434
+ if (typeof value !== 'function') {
435
+ _applyAttr(el, key, value)
436
+ continue
437
+ }
438
+
439
+ const attrName = key
440
+ const getter = value
441
+ const cleanup = effect(() => {
442
+ _applyAttr(el, attrName, getter())
443
+ })
444
+ effects.push(cleanup)
445
+ }
446
+ }
447
+
448
+ function _applyStaticProps(el, props) {
449
+ for (const key in props) {
450
+ const value = props[key]
451
+ if (key.startsWith('@') || key.startsWith('f-'))
452
+ continue
453
+ if (typeof value !== 'function')
454
+ _applyAttr(el, key, value)
455
+ }
456
+ }
457
+
458
+ function _applyAttr(el, name, value) {
459
+ if (name === 'class') {
460
+ const cls = _resolveClass(value)
461
+ if (cls) el.className = cls
462
+ else el.removeAttribute('class')
463
+ return
464
+ }
465
+
466
+ if (name === 'style') {
467
+ if (value !== null && typeof value === 'object') {
468
+ for (const k in value) el.style[k] = value[k]
469
+ } else {
470
+ el.style.cssText = String(value ?? '')
471
+ }
472
+ return
473
+ }
474
+
475
+ const elementProp = resolveElementProperty(el, name, value)
476
+ if (elementProp) {
477
+ el[elementProp] = value
478
+ return
479
+ }
480
+
481
+ if (name in el && typeof el[name] !== 'undefined' && name !== 'type') {
482
+ try {
483
+ el[name] = value
484
+ return
485
+ } catch (_) {
486
+ return
487
+ }
488
+ }
489
+
490
+ if (typeof value === 'boolean') {
491
+ value ? el.setAttribute(name, '') : el.removeAttribute(name)
492
+ return
493
+ }
494
+
495
+ if (value == null) {
496
+ el.removeAttribute(name)
497
+ return
498
+ }
499
+
500
+ el.setAttribute(name, sanitize(name, String(value)))
501
+ }
502
+
503
+ function _resolveClass(value) {
504
+ if (typeof value === 'string')
505
+ return value
506
+ if (Array.isArray(value))
507
+ return value.filter(Boolean).join(' ')
508
+ if (value && typeof value === 'object')
509
+ return Object.entries(value).filter(([, v]) => !!v).map(([k]) => k).join(' ')
510
+ return String(value ?? '')
511
+ }
512
+
513
+ function _mountChildren(parent, children, parentEffects) {
514
+ const mounted = []
515
+
516
+ for (const fnode of children) {
517
+ if (!fnode || !fnode.node) continue
518
+
519
+ insertFNode(parent, fnode)
520
+
521
+ if (fnode.effects) parentEffects.push(...fnode.effects)
522
+ mounted.push(fnode)
523
+ }
524
+
525
+ return mounted
526
+ }
527
+
528
+ function _unmountNodes(fnodes) {
529
+ for (const fnode of fnodes) {
530
+ if (!fnode) continue
531
+
532
+ if (fnode.effects) {
533
+ for (let i = fnode.effects.length - 1; i >= 0; i--) {
534
+ const cleanup = fnode.effects[i]
535
+ if (typeof cleanup === 'function') cleanup()
536
+ }
537
+ }
538
+
539
+ if (fnode.node?.parentNode) {
540
+ fnode.node.parentNode.removeChild(fnode.node)
541
+ }
542
+
543
+ if (fnode._ifBranchNodes) {
544
+ _unmountNodes(fnode._ifBranchNodes())
545
+ }
546
+
547
+ if (fnode._getNodes) {
548
+ _unmountNodes(fnode._getNodes())
549
+ }
550
+ }
551
+ }
552
+
553
+ function _resolveComponentProps(rawProps) {
554
+ const resolved = {}
555
+
556
+ for (const [key, value] of Object.entries(rawProps)) {
557
+ if (key.startsWith('on:')) continue
558
+
559
+ if (isGetter(value)) {
560
+ Object.defineProperty(resolved, key, {
561
+ get: value,
562
+ enumerable: true,
563
+ configurable: true,
564
+ })
565
+ } else {
566
+ resolved[key] = value
567
+ }
568
+ }
569
+
570
+ return resolved
571
+ }
572
+
573
+ export function mount(renderFn, ctx, components, container) {
574
+ const fnodes = renderFn(ctx, components)
575
+ const effects = []
576
+
577
+ for (const fnode of fnodes) {
578
+ if (!fnode?.node) continue
579
+ insertFNode(container, fnode)
580
+ if (fnode.effects) effects.push(...fnode.effects)
581
+ }
582
+
583
+ return {
584
+ unmount() {
585
+ _unmountNodes(fnodes)
586
+ container.innerHTML = ''
587
+ }
588
+ }
558
589
  }