fusee-framework 1.5.3 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +30 -13
  2. package/app/benchmark/benchmark.js +633 -0
  3. package/app/benchmark/index.html +102 -0
  4. package/app/benchmark/styles.css +249 -0
  5. package/app/components/Counter.js +17 -7
  6. package/app/components/Counter.ts +1 -1
  7. package/app/components/CounterSection.js +3 -4
  8. package/app/main.js +29 -62
  9. package/app/main.ts +72 -63
  10. package/app/pages/404.js +16 -0
  11. package/app/pages/[...all].js +35 -0
  12. package/app/pages/[...all].ts +35 -0
  13. package/app/pages/_layout.js +26 -0
  14. package/app/pages/_layout.ts +26 -0
  15. package/app/pages/about.js +23 -0
  16. package/app/pages/about.ts +29 -0
  17. package/app/pages/composables.js +158 -0
  18. package/app/pages/composables.ts +158 -0
  19. package/app/pages/dynamic.js +40 -0
  20. package/app/pages/dynamic.ts +40 -0
  21. package/app/pages/index.js +117 -0
  22. package/app/pages/index.ts +78 -0
  23. package/app/pages/login.ts +21 -0
  24. package/app/pages/objects/[id].js +56 -0
  25. package/app/pages/objects/[id].ts +56 -0
  26. package/app/pages/posts/[category]/[slug].js +56 -0
  27. package/app/pages/posts/[category]/[slug].ts +56 -0
  28. package/app/pages/router-example.js +56 -0
  29. package/app/pages/router-example.ts +56 -0
  30. package/app/pages/test-for.js +65 -0
  31. package/app/pages/test-for.ts +80 -0
  32. package/app/pages/users/[id].js +24 -0
  33. package/app/pages/users/[id].ts +46 -0
  34. package/app/pages/users/index.js +22 -0
  35. package/app/pages/users/index.ts +29 -0
  36. package/app/pages/users.js +43 -0
  37. package/app/pages/users.ts +78 -0
  38. package/app/routes/About.js +1 -1
  39. package/app/routes/About.ts +1 -1
  40. package/app/routes/AuthRoutes.ts +41 -0
  41. package/app/routes/Home.js +9 -0
  42. package/app/routes/NotFound.js +16 -0
  43. package/app/routes/NotFound.ts +16 -0
  44. package/app/routes/RouterExample.js +56 -0
  45. package/app/routes/RouterExample.ts +56 -0
  46. package/app/routes/WildcardDemo.js +35 -0
  47. package/app/routes/WildcardDemo.ts +35 -0
  48. package/auto-imports.d.ts +1 -0
  49. package/bin/create-fusee-linux +0 -0
  50. package/bin/create-fusee-mac-arm64 +0 -0
  51. package/bin/create-fusee-mac-intel +0 -0
  52. package/bin/create-fusee-win.exe +0 -0
  53. package/bin/create-fusee.exe +0 -0
  54. package/bin/index.js +38 -0
  55. package/create-fusee-go/cmd/generate.go +72 -0
  56. package/create-fusee-go/cmd/init.go +38 -0
  57. package/create-fusee-go/cmd/root.go +121 -0
  58. package/create-fusee-go/go.mod +10 -0
  59. package/create-fusee-go/go.sum +10 -0
  60. package/create-fusee-go/internal/assets/assets.go +78 -0
  61. package/create-fusee-go/internal/assets/embed/framework/auto-import-preset.js +9 -0
  62. package/create-fusee-go/internal/assets/embed/framework/core/compiler.js +253 -0
  63. package/create-fusee-go/internal/assets/embed/framework/core/component.js +266 -0
  64. package/create-fusee-go/internal/assets/embed/framework/core/composable.js +27 -0
  65. package/create-fusee-go/internal/assets/embed/framework/core/directives.js +875 -0
  66. package/create-fusee-go/internal/assets/embed/framework/core/evaluator.js +153 -0
  67. package/create-fusee-go/internal/assets/embed/framework/core/event-delegation.js +191 -0
  68. package/create-fusee-go/internal/assets/embed/framework/core/signal.js +412 -0
  69. package/create-fusee-go/internal/assets/embed/framework/core/store.js +81 -0
  70. package/create-fusee-go/internal/assets/embed/framework/index.d.ts +3 -0
  71. package/create-fusee-go/internal/assets/embed/framework/index.js +10 -0
  72. package/create-fusee-go/internal/assets/embed/framework/router/file-router.js +177 -0
  73. package/create-fusee-go/internal/assets/embed/framework/router/router.js +462 -0
  74. package/create-fusee-go/internal/assets/embed/framework/types/compiler.d.ts +15 -0
  75. package/create-fusee-go/internal/assets/embed/framework/types/component.d.ts +88 -0
  76. package/create-fusee-go/internal/assets/embed/framework/types/composable.d.ts +14 -0
  77. package/create-fusee-go/internal/assets/embed/framework/types/directives.d.ts +55 -0
  78. package/create-fusee-go/internal/assets/embed/framework/types/evaluator.d.ts +13 -0
  79. package/create-fusee-go/internal/assets/embed/framework/types/event-delegation.d.ts +32 -0
  80. package/create-fusee-go/internal/assets/embed/framework/types/index.d.ts +22 -0
  81. package/create-fusee-go/internal/assets/embed/framework/types/router.d.ts +32 -0
  82. package/create-fusee-go/internal/assets/embed/framework/types/signal.d.ts +79 -0
  83. package/create-fusee-go/internal/assets/embed/framework/types/store.d.ts +8 -0
  84. package/create-fusee-go/internal/assets/templates/Counter.tmpl +26 -0
  85. package/create-fusee-go/internal/assets/templates/component.tmpl +11 -0
  86. package/create-fusee-go/internal/assets/templates/components/Loading.tmpl +24 -0
  87. package/create-fusee-go/internal/assets/templates/index.html.tmpl +15 -0
  88. package/create-fusee-go/internal/assets/templates/main.tmpl +14 -0
  89. package/create-fusee-go/internal/assets/templates/package.json.tmpl +14 -0
  90. package/create-fusee-go/internal/assets/templates/page.tmpl +12 -0
  91. package/create-fusee-go/internal/assets/templates/pages/about.tmpl +20 -0
  92. package/create-fusee-go/internal/assets/templates/pages/index.tmpl +22 -0
  93. package/create-fusee-go/internal/assets/templates/pages/layout.tmpl +18 -0
  94. package/create-fusee-go/internal/assets/templates/tsconfig.json.tmpl +14 -0
  95. package/create-fusee-go/internal/assets/templates/vite.config.js.tmpl +18 -0
  96. package/create-fusee-go/main.go +7 -0
  97. package/framework/auto-import-preset.js +1 -1
  98. package/framework/core/compiler.js +252 -249
  99. package/framework/core/component.js +265 -260
  100. package/framework/core/directives.js +875 -632
  101. package/framework/core/evaluator.js +153 -70
  102. package/framework/core/event-delegation.js +191 -0
  103. package/framework/core/signal.js +412 -404
  104. package/framework/index.js +10 -11
  105. package/framework/router/file-router.js +177 -0
  106. package/framework/router/router.js +462 -80
  107. package/framework/types/component.d.ts +0 -4
  108. package/framework/types/event-delegation.d.ts +32 -0
  109. package/framework/types/index.d.ts +3 -0
  110. package/framework/types/router.d.ts +32 -15
  111. package/framework/types/signal.d.ts +1 -0
  112. package/index.html +638 -563
  113. package/package.json +5 -3
  114. package/tsconfig.json +1 -0
  115. /package/{framework → create-fusee-go/internal/assets/embed/framework}/bin/create-fusee.js +0 -0
@@ -0,0 +1,875 @@
1
+ import { signal, effect, batch } from './signal.js'
2
+ import { evaluateExpression } from './evaluator.js'
3
+ import { compileNode } from './compiler.js'
4
+ import { registerDelegatedEvent, createEventHandler, isDelegatedEvent } from './event-delegation.js'
5
+ import { currentRoute } from '../router/router.js'
6
+
7
+ const customDirectives = new Map()
8
+
9
+ export function directive(name, definition) {
10
+ if (typeof name !== 'string' || !name.trim()) {
11
+ console.error('[framework] directive() requires a non-empty name')
12
+ return
13
+ }
14
+ if (!definition || typeof definition !== 'object') {
15
+ console.error(`[framework] directive("${name}") requires a definition object`)
16
+ return
17
+ }
18
+ customDirectives.set(name, definition)
19
+ }
20
+
21
+ export function processCustomDirectives(el, context, effects) {
22
+ if (el.nodeType !== 1 || !el.hasAttribute || !el.attributes) return
23
+
24
+ for (const attr of [...el.attributes]) {
25
+ const match = attr.name.match(/^f-([a-zA-Z0-9-]+)(?::([a-zA-Z0-9-]+))?(?:\.(.+))?$/)
26
+
27
+ if (!match) continue
28
+
29
+ const directiveName = match[1]
30
+ const arg = match[2] || null
31
+ const modifiersStr = match[3]
32
+ const modifiers = modifiersStr ? modifiersStr.split('.') : []
33
+
34
+ const definition = customDirectives.get(directiveName)
35
+ if (!definition) {
36
+ continue
37
+ }
38
+
39
+ el.removeAttribute(attr.name)
40
+
41
+ const value = evaluateExpression(attr.value, context)
42
+ const expression = attr.value
43
+
44
+ const binding = {
45
+ value,
46
+ expression,
47
+ arg,
48
+ modifiers
49
+ }
50
+
51
+ if (typeof definition.mounted === 'function') {
52
+ definition.mounted(el, binding)
53
+ }
54
+
55
+ if (typeof definition.updated === 'function') {
56
+ effects.push(effect(() => {
57
+ const newValue = evaluateExpression(expression, context)
58
+
59
+ if (!Object.is(binding.value, newValue)) {
60
+ binding.value = newValue
61
+ definition.updated(el, binding)
62
+ }
63
+ }))
64
+ }
65
+
66
+ if (typeof definition.unmounted === 'function') {
67
+ effects.push(() => {
68
+ definition.unmounted(el, binding)
69
+ })
70
+ }
71
+ }
72
+ }
73
+
74
+ export function processDirectives(root, context, components, effects) {
75
+ if (processFor(root, context, components, effects)) return true
76
+ if (processIf(root, context, components, effects)) return true
77
+ if (processIs(root, context, components, effects)) return true
78
+
79
+ processShow(root, context, effects)
80
+ processModel(root, context, effects)
81
+ processRefs(root, context)
82
+ processHtml(root, context, effects)
83
+ processText(root, context, effects)
84
+ processEvents(root, context, effects)
85
+ processCustomDirectives(root, context, effects)
86
+ processClassList(root, context, effects)
87
+ processLinks(root, effects)
88
+ processCloak(root)
89
+
90
+ return false
91
+ }
92
+
93
+ export function processRefs(el, context) {
94
+ if (el.nodeType !== 1 || !el.hasAttribute('f-ref')) return
95
+
96
+ const refName = el.getAttribute('f-ref')
97
+ el.removeAttribute('f-ref')
98
+
99
+ const refVar = context[refName]
100
+ if (typeof refVar === 'function' && refVar.isSignal && !refVar.readonly) {
101
+ refVar(el)
102
+ } else {
103
+ const reason = refVar?.readonly ? 'it is read-only (computed)' : 'it is not a signal'
104
+ console.warn(`[framework] f-ref requires a writable signal. Target "${refName}" is invalid: ${reason}`)
105
+ }
106
+ }
107
+
108
+ export function processModel(el, context, effects) {
109
+ if (el.nodeType !== 1 || !el.hasAttribute('f-model')) return
110
+
111
+ const expr = el.getAttribute('f-model')
112
+ el.removeAttribute('f-model')
113
+
114
+ const signalMethod = context[expr]
115
+ if (typeof signalMethod === 'function' && signalMethod.isSignal && !signalMethod.readonly) {
116
+ effects.push(effect(() => {
117
+ const val = signalMethod()
118
+ if (el.type === 'checkbox') {
119
+ el.checked = !!val
120
+ } else {
121
+ el.value = val ?? ''
122
+ }
123
+ }))
124
+
125
+ const eventName = (el.tagName === 'SELECT' || el.type === 'checkbox' || el.type === 'radio') ? 'change' : 'input'
126
+ const handler = () => {
127
+ const newVal = el.type === 'checkbox' ? el.checked : el.value
128
+ batch(() => signalMethod(newVal))
129
+ }
130
+ el.addEventListener(eventName, handler)
131
+ effects.push(() => el.removeEventListener(eventName, handler))
132
+ } else {
133
+ const reason = signalMethod?.readonly ? 'it is read-only (computed)' : 'it is not a signal'
134
+ console.warn(`[framework] f-model requires a writable signal. Expression "${expr}" is invalid: ${reason}`)
135
+ }
136
+ }
137
+
138
+ export function processFor(el, context, components, effects) {
139
+ if (el.nodeType !== 1 || !el.hasAttribute('f-for')) return false
140
+
141
+ const expr = el.getAttribute('f-for')
142
+ el.removeAttribute('f-for')
143
+
144
+ const match = expr.match(/^\s*(?:(?:\(\s*([a-zA-Z_$][0-9a-zA-Z_$]*)(?:\s*,\s*([a-zA-Z_$][0-9a-zA-Z_$]*))?(?:\s*,\s*([a-zA-Z_$][0-9a-zA-Z_$]*))?\s*\))|([a-zA-Z_$][0-9a-zA-Z_$]*))\s+in\s+(.+)\s*$/)
145
+ if (!match) {
146
+ console.warn(`[framework] Invalid f-for expression: "${expr}"`)
147
+ return false
148
+ }
149
+
150
+ const itemName = match[4] || match[1]
151
+ const keyName = match[2]
152
+ const indexName = match[3]
153
+ const arrayExpr = match[5]
154
+
155
+ const keyExpr = el.getAttribute('data-bind-key') || el.getAttribute(':key')
156
+ if (keyExpr) {
157
+ el.removeAttribute('data-bind-key')
158
+ el.removeAttribute(':key')
159
+ }
160
+
161
+ const anchor = document.createComment('f-for')
162
+ const parent = el.parentNode
163
+ parent.insertBefore(anchor, el)
164
+
165
+ const templateNode = el.cloneNode(true)
166
+ parent.removeChild(el)
167
+
168
+ let cache = new Map()
169
+ let previousOrder = []
170
+
171
+ effects.push(effect(() => {
172
+ const rawItems = evaluateExpression(arrayExpr, context)
173
+ let items = []
174
+
175
+ if (Array.isArray(rawItems)) {
176
+ items = rawItems.map((item, index) => ({ value: item, rawIndex: index }))
177
+ } else if (typeof rawItems === 'number' && !isNaN(rawItems)) {
178
+ items = Array.from({ length: Math.max(0, Math.floor(rawItems)) }, (_, i) => ({ value: i + 1, rawIndex: i }))
179
+ } else if (rawItems && typeof rawItems === 'object') {
180
+ items = Object.entries(rawItems).map(([key, value], index) => ({ value, key, rawIndex: index }))
181
+ }
182
+
183
+ const nextCache = new Map()
184
+ const nextOrder = []
185
+
186
+ const newItemsWithKeys = items.map((item, index) => {
187
+ const itemContext = Object.create(context)
188
+ itemContext[itemName] = item.value
189
+ if (keyName) itemContext[keyName] = item.hasOwnProperty('key') ? item.key : item.rawIndex
190
+ if (indexName) itemContext[indexName] = index
191
+
192
+ const key = keyExpr ? evaluateExpression(keyExpr, itemContext) : item.rawIndex
193
+ return { item, key }
194
+ })
195
+
196
+ const parentNode = anchor.parentNode
197
+ let currentCursor = anchor.nextSibling
198
+
199
+ const isClear = items.length === 0 && previousOrder.length > 0
200
+ if (isClear) {
201
+ const nodesToRemove = []
202
+ let node = anchor.nextSibling
203
+ while (node) {
204
+ const next = node.nextSibling
205
+ nodesToRemove.push(node)
206
+ node = next
207
+ }
208
+ if (nodesToRemove.length > 0) {
209
+ const fragment = document.createDocumentFragment()
210
+ nodesToRemove.forEach(n => fragment.appendChild(n))
211
+ }
212
+ for (const entry of cache.values()) {
213
+ for (let i = entry.effects.length - 1; i >= 0; i--) {
214
+ const cleanup = entry.effects[i]
215
+ if (typeof cleanup === 'function') cleanup()
216
+ }
217
+ }
218
+ cache.clear()
219
+ previousOrder = []
220
+ return
221
+ }
222
+
223
+ const fragment = document.createDocumentFragment()
224
+ const nodesToInsert = []
225
+
226
+ newItemsWithKeys.forEach(({ item, key }, index) => {
227
+ let entry = cache.get(key)
228
+
229
+ if (entry) {
230
+ batch(() => {
231
+ if (typeof entry.context[itemName] === 'function' && entry.context[itemName].isSignal) {
232
+ entry.context[itemName](item.value)
233
+ }
234
+ if (keyName && typeof entry.context[keyName] === 'function' && entry.context[keyName].isSignal) {
235
+ entry.context[keyName](item.hasOwnProperty('key') ? item.key : item.rawIndex)
236
+ }
237
+ if (indexName && typeof entry.context[indexName] === 'function' && entry.context[indexName].isSignal) {
238
+ entry.context[indexName](index)
239
+ }
240
+ })
241
+
242
+ if (entry.node !== currentCursor) {
243
+ parentNode.insertBefore(entry.node, currentCursor)
244
+ } else {
245
+ currentCursor = currentCursor.nextSibling
246
+ }
247
+ cache.delete(key)
248
+ } else {
249
+ const itemContext = Object.create(context)
250
+ itemContext[itemName] = signal(item.value)
251
+ if (keyName) itemContext[keyName] = signal(item.hasOwnProperty('key') ? item.key : item.rawIndex)
252
+ if (indexName) itemContext[indexName] = signal(index)
253
+
254
+ const clone = templateNode.cloneNode(true)
255
+ const childEffects = []
256
+ compileNode(clone, itemContext, components, childEffects)
257
+
258
+ fragment.appendChild(clone)
259
+ entry = { node: clone, effects: childEffects, context: itemContext }
260
+ nodesToInsert.push({ node: clone, effects: childEffects, context: itemContext, key })
261
+ }
262
+
263
+ nextCache.set(key, entry)
264
+ nextOrder.push(entry.node)
265
+ })
266
+
267
+ if (fragment.childNodes.length > 0) {
268
+ parentNode.insertBefore(fragment, currentCursor)
269
+ nodesToInsert.forEach(({ node, effects, context, key }) => {
270
+ nextCache.set(key, { node, effects, context })
271
+ })
272
+ }
273
+
274
+ const removedEntries = []
275
+ for (const [_, entry] of cache.entries()) {
276
+ removedEntries.push(entry)
277
+ }
278
+ if (removedEntries.length > 0) {
279
+ const removeFragment = document.createDocumentFragment()
280
+ removedEntries.forEach(entry => {
281
+ for (let i = entry.effects.length - 1; i >= 0; i--) {
282
+ const cleanup = entry.effects[i]
283
+ if (typeof cleanup === 'function') cleanup()
284
+ }
285
+ if (entry.node.parentNode) {
286
+ removeFragment.appendChild(entry.node)
287
+ }
288
+ })
289
+ }
290
+
291
+ cache = nextCache
292
+ previousOrder = nextOrder
293
+ }))
294
+
295
+ effects.push(() => {
296
+ for (const entry of cache.values()) {
297
+ for (let i = entry.effects.length - 1; i >= 0; i--) {
298
+ const cleanup = entry.effects[i]
299
+ if (typeof cleanup === 'function') cleanup()
300
+ }
301
+ }
302
+ cache.clear()
303
+ })
304
+
305
+ return true
306
+ }
307
+
308
+ export function processIf(el, context, components, effects) {
309
+ if (el.nodeType !== 1 || !el.hasAttribute('f-if')) return false
310
+
311
+ const parent = el.parentNode
312
+ if (!parent) return false
313
+
314
+ const group = []
315
+ let curr = el
316
+
317
+ while (curr) {
318
+ if (curr.nodeType === 1) {
319
+ const type = curr.hasAttribute('f-if') ? 'if' :
320
+ curr.hasAttribute('f-elif') ? 'elif' :
321
+ curr.hasAttribute('f-else-if') ? 'elif' :
322
+ curr.hasAttribute('f-else') ? 'else' : null
323
+
324
+ if (type) {
325
+ const expr = type === 'else' ? 'true' : curr.getAttribute(`f-${type === 'elif' && curr.hasAttribute('f-else-if') ? 'else-if' : type}`)
326
+ group.push({ node: curr, expr, type, isElement: true })
327
+
328
+ curr.removeAttribute('f-if')
329
+ curr.removeAttribute('f-elif')
330
+ curr.removeAttribute('f-else-if')
331
+ curr.removeAttribute('f-else')
332
+
333
+ if (type === 'else') break
334
+ } else {
335
+ break
336
+ }
337
+ } else if (curr.nodeType === 3 && curr.textContent.trim() === '' || curr.nodeType === 8) {
338
+ group.push({ node: curr, isElement: false })
339
+ } else {
340
+ break
341
+ }
342
+ curr = curr.nextSibling
343
+ }
344
+
345
+ const templates = group
346
+ .filter(item => item.isElement)
347
+ .map(item => ({
348
+ template: item.node.cloneNode(true),
349
+ expr: item.expr,
350
+ type: item.type
351
+ }))
352
+
353
+ const anchor = document.createComment('f-condition-anchor')
354
+ parent.insertBefore(anchor, el)
355
+
356
+ for (const item of group) item.node.remove()
357
+
358
+ let activeIndex = -1
359
+ let activeNode = null
360
+ let activeCleanup = []
361
+
362
+ effects.push(effect(() => {
363
+ let nextIndex = -1
364
+ for (let i = 0; i < templates.length; i++) {
365
+ const isMatch = templates[i].type === 'else' ? true : evaluateExpression(templates[i].expr, context)
366
+ if (isMatch) {
367
+ nextIndex = i
368
+ break
369
+ }
370
+ }
371
+
372
+ if (nextIndex !== activeIndex) {
373
+ for (let i = activeCleanup.length - 1; i >= 0; i--) {
374
+ const cleanup = activeCleanup[i]
375
+ if (typeof cleanup === 'function') cleanup()
376
+ }
377
+ activeCleanup = []
378
+ if (activeNode) {
379
+ activeNode.remove()
380
+ activeNode = null
381
+ }
382
+
383
+ if (nextIndex !== -1) {
384
+ activeNode = templates[nextIndex].template.cloneNode(true)
385
+ compileNode(activeNode, context, components, activeCleanup)
386
+ anchor.parentNode.insertBefore(activeNode, anchor.nextSibling)
387
+ }
388
+ activeIndex = nextIndex
389
+ }
390
+ }))
391
+
392
+ effects.push(() => {
393
+ for (let i = activeCleanup.length - 1; i >= 0; i--) {
394
+ const cleanup = activeCleanup[i]
395
+ if (typeof cleanup === 'function') cleanup()
396
+ }
397
+ activeCleanup = []
398
+ })
399
+
400
+ return true
401
+ }
402
+
403
+ export function processIs(el, context, components, effects) {
404
+ if (el.nodeType !== 1 || !el.hasAttribute('f-is')) return false
405
+
406
+ const expr = el.getAttribute('f-is')
407
+ const keepAlive = processKeepAlive(el)
408
+
409
+ el.removeAttribute('f-is')
410
+
411
+ const anchor = document.createComment('f-keepAlive-anchor')
412
+ el.parentNode.insertBefore(anchor, el)
413
+ const templateNode = el.cloneNode(true)
414
+ el.remove()
415
+
416
+ const cache = new Map()
417
+ let activeKey = null
418
+ let activeCleanup = []
419
+
420
+ effects.push(effect(() => {
421
+ const rawValue = evaluateExpression(expr, context)
422
+
423
+ if (rawValue === activeKey) return
424
+
425
+ if (activeKey !== null) {
426
+ const currentEntry = cache.get(activeKey)
427
+ if (currentEntry) {
428
+ if (keepAlive) {
429
+ currentEntry.node.remove()
430
+ } else {
431
+ for (let i = activeCleanup.length - 1; i >= 0; i--) {
432
+ const cleanup = activeCleanup[i]
433
+ if (typeof cleanup === 'function') cleanup()
434
+ }
435
+ currentEntry.node.remove()
436
+ cache.delete(activeKey)
437
+ activeCleanup = []
438
+ }
439
+ }
440
+ }
441
+
442
+ if (!rawValue) {
443
+ activeKey = null
444
+ return
445
+ }
446
+
447
+ let entry = cache.get(rawValue)
448
+
449
+ if (entry && keepAlive) {
450
+ anchor.parentNode.insertBefore(entry.node, anchor.nextSibling)
451
+ activeCleanup = entry.cleanup
452
+ } else {
453
+ let ComponentFn = typeof rawValue === 'string' ? components[rawValue] : rawValue
454
+
455
+ if (typeof ComponentFn !== 'function') {
456
+ console.warn(`[framework] f-is: Component "${rawValue}" not found or invalid.`)
457
+ return
458
+ }
459
+
460
+ const compEl = templateNode.cloneNode(true)
461
+ compEl._isDynamicComponent = true
462
+
463
+ const tempName = '__dynamic_component__'
464
+ compEl.setAttribute('data-component', tempName)
465
+
466
+ anchor.parentNode.insertBefore(compEl, anchor.nextSibling)
467
+
468
+ const childCleanup = []
469
+ const dynamicComponents = { ...components, [tempName]: ComponentFn }
470
+
471
+ compileNode(compEl, context, dynamicComponents, childCleanup)
472
+
473
+ entry = { node: compEl, cleanup: childCleanup }
474
+ cache.set(rawValue, entry)
475
+ activeCleanup = childCleanup
476
+ }
477
+
478
+ activeKey = rawValue
479
+ }))
480
+
481
+ effects.push(() => {
482
+ for (const entry of cache.values()) {
483
+ for (let i = entry.cleanup.length - 1; i >= 0; i--) {
484
+ const cleanup = entry.cleanup[i]
485
+ if (typeof cleanup === 'function') cleanup()
486
+ }
487
+ entry.node.remove()
488
+ }
489
+ cache.clear()
490
+ })
491
+
492
+ return true
493
+ }
494
+
495
+ function processKeepAlive(el) {
496
+ if (el.nodeType !== 1) return false
497
+ const hasKeepAlive = el.hasAttribute('f-keepAlive')
498
+ if (hasKeepAlive) el.removeAttribute('f-keepAlive')
499
+ return hasKeepAlive
500
+ }
501
+
502
+ export function processShow(el, context, effects) {
503
+ if (el.nodeType !== 1 || !el.hasAttribute('f-show')) return
504
+
505
+ const expr = el.getAttribute('f-show')
506
+ el.removeAttribute('f-show')
507
+ const originalDisplay = el.style.display === 'none' ? '' : el.style.display
508
+
509
+ effects.push(effect(() => {
510
+ const val = evaluateExpression(expr, context)
511
+ el.style.display = val ? originalDisplay : 'none'
512
+ }))
513
+ }
514
+
515
+ export function processHtml(el, context, effects) {
516
+ if (el.nodeType !== 1 || !el.hasAttribute('f-html')) return
517
+
518
+ const expr = el.getAttribute('f-html')
519
+ el.removeAttribute('f-html')
520
+
521
+ const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
522
+ if (isDev) {
523
+ console.warn(`[framework] f-html can expose the site to XSS. Use only with trusted content.`)
524
+ }
525
+
526
+ effects.push(effect(() => {
527
+ const val = evaluateExpression(expr, context)
528
+ el.innerHTML = String(val ?? '')
529
+ }))
530
+ }
531
+
532
+ export function processEvents(el, context, effects) {
533
+ if (el.nodeType !== 1 || !el.attributes) return
534
+
535
+ for (const attr of [...el.attributes]) {
536
+ if (attr.name.startsWith('on:')) {
537
+ const fullEventName = attr.name.slice(3)
538
+ const parts = fullEventName.split('.')
539
+ const eventName = parts[0]
540
+ const modifiers = parts.slice(1)
541
+ const expr = attr.value
542
+
543
+ el.removeAttribute(attr.name)
544
+
545
+ let timeoutId
546
+ let lastRun = 0
547
+ let throttleTimeoutId
548
+
549
+ let debounceMs = 0
550
+ let throttleMs = 0
551
+
552
+ const parseDuration = (idx) => {
553
+ const next = modifiers[idx + 1]
554
+ if (next && /^\d/.test(next)) {
555
+ if (next.endsWith('ms')) return parseInt(next) || 250
556
+ if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
557
+ }
558
+ return 250
559
+ }
560
+
561
+ const debounceIdx = modifiers.indexOf('debounce')
562
+ if (debounceIdx > -1) debounceMs = parseDuration(debounceIdx)
563
+
564
+ const throttleIdx = modifiers.indexOf('throttle')
565
+ if (throttleIdx > -1) throttleMs = parseDuration(throttleIdx)
566
+
567
+ const executeLogic = (e) => {
568
+ batch(() => {
569
+ const potentialFn = context[expr]
570
+ if (typeof potentialFn === 'function' && !potentialFn.isSignal) {
571
+ potentialFn(e)
572
+ } else {
573
+ evaluateExpression(expr, context, { $event: e }, false)
574
+ }
575
+ })
576
+ }
577
+
578
+ const handler = (e) => {
579
+ if (modifiers.includes('prevent')) e.preventDefault()
580
+ if (modifiers.includes('stop')) e.stopPropagation()
581
+ if (modifiers.includes('self') && e.target !== el) return
582
+
583
+ if (e instanceof KeyboardEvent) {
584
+ const key = e.key.toLowerCase()
585
+ const keyModifiers = modifiers.filter(m => ['enter', 'escape', 'tab', 'space', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(m))
586
+ if (keyModifiers.length > 0) {
587
+ const match = keyModifiers.some(m => {
588
+ if (m === 'escape') return key === 'escape' || key === 'esc'
589
+ if (m === 'space') return key === ' ' || key === 'spacebar'
590
+ return key === m
591
+ })
592
+ if (!match) return
593
+ }
594
+ }
595
+
596
+ if (throttleMs > 0) {
597
+ const now = Date.now()
598
+ const remaining = throttleMs - (now - lastRun)
599
+
600
+ if (remaining <= 0) {
601
+ clearTimeout(throttleTimeoutId)
602
+ lastRun = now
603
+ executeLogic(e)
604
+ } else {
605
+ clearTimeout(throttleTimeoutId)
606
+ throttleTimeoutId = setTimeout(() => {
607
+ lastRun = Date.now()
608
+ executeLogic(e)
609
+ }, remaining)
610
+ }
611
+ return
612
+ }
613
+
614
+ if (debounceMs > 0) {
615
+ clearTimeout(timeoutId)
616
+ timeoutId = setTimeout(() => executeLogic(e), debounceMs)
617
+ } else {
618
+ executeLogic(e)
619
+ }
620
+ }
621
+
622
+ const targetNode = modifiers.includes('window') ? window : (modifiers.includes('document') ? document : el)
623
+ const options = {
624
+ once: modifiers.includes('once'),
625
+ capture: modifiers.includes('capture') || modifiers.includes('away')
626
+ }
627
+
628
+ targetNode.addEventListener(eventName, handler, options)
629
+ if (effects) {
630
+ effects.push(() => {
631
+ targetNode.removeEventListener(eventName, handler, options)
632
+ if (timeoutId) clearTimeout(timeoutId)
633
+ if (throttleTimeoutId) clearTimeout(throttleTimeoutId)
634
+ })
635
+ }
636
+ continue
637
+ }
638
+
639
+ if (attr.name.startsWith('@')) {
640
+ const fullEventName = attr.name.slice(1)
641
+ const parts = fullEventName.split('.')
642
+ const eventName = parts[0]
643
+ const modifiers = parts.slice(1)
644
+ const expr = attr.value
645
+
646
+ el.removeAttribute(attr.name)
647
+
648
+ const targetNode = modifiers.includes('window') ? window : (modifiers.includes('document') ? document : null)
649
+
650
+ if (targetNode) {
651
+ const handler = createEventHandler(
652
+ typeof context[expr] === 'function' && !context[expr].isSignal
653
+ ? context[expr]
654
+ : expr,
655
+ modifiers,
656
+ context,
657
+ expr
658
+ )
659
+
660
+ const options = {
661
+ once: modifiers.includes('once'),
662
+ capture: modifiers.includes('capture') || modifiers.includes('away')
663
+ }
664
+
665
+ targetNode.addEventListener(eventName, handler, options)
666
+ if (effects) {
667
+ effects.push(() => {
668
+ targetNode.removeEventListener(eventName, handler, options)
669
+ })
670
+ }
671
+ } else if (isDelegatedEvent(eventName) && !modifiers.includes('capture') && !modifiers.includes('away')) {
672
+ const potentialFn = context[expr]
673
+ const isFunction = typeof potentialFn === 'function' && !potentialFn.isSignal
674
+
675
+ const handlerState = {
676
+ timeoutId: null,
677
+ throttleTimeoutId: null,
678
+ lastRun: 0
679
+ }
680
+
681
+ const wrappedHandler = createEventHandler(
682
+ isFunction ? potentialFn : expr,
683
+ modifiers,
684
+ context,
685
+ expr,
686
+ handlerState
687
+ )
688
+
689
+ const cleanup = registerDelegatedEvent(el, eventName, wrappedHandler, {
690
+ modifiers,
691
+ context,
692
+ expr,
693
+ handlerState
694
+ })
695
+
696
+ if (effects) {
697
+ effects.push(cleanup)
698
+ }
699
+ } else {
700
+ let timeoutId
701
+ let lastRun = 0
702
+ let throttleTimeoutId
703
+
704
+ let debounceMs = 0
705
+ let throttleMs = 0
706
+
707
+ const parseDuration = (idx) => {
708
+ const next = modifiers[idx + 1]
709
+ if (next && /^\d/.test(next)) {
710
+ if (next.endsWith('ms')) return parseInt(next) || 250
711
+ if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
712
+ }
713
+ return 250
714
+ }
715
+
716
+ const debounceIdx = modifiers.indexOf('debounce')
717
+ if (debounceIdx > -1) debounceMs = parseDuration(debounceIdx)
718
+
719
+ const throttleIdx = modifiers.indexOf('throttle')
720
+ if (throttleIdx > -1) throttleMs = parseDuration(throttleIdx)
721
+
722
+ const executeLogic = (e) => {
723
+ batch(() => {
724
+ const potentialFn = context[expr]
725
+ if (typeof potentialFn === 'function' && !potentialFn.isSignal) {
726
+ potentialFn(e)
727
+ } else {
728
+ evaluateExpression(expr, context, { $event: e }, false)
729
+ }
730
+ })
731
+ }
732
+
733
+ const handler = (e) => {
734
+ if (modifiers.includes('prevent')) e.preventDefault()
735
+ if (modifiers.includes('stop')) e.stopPropagation()
736
+ if (modifiers.includes('self') && e.target !== el) return
737
+
738
+ if (e instanceof KeyboardEvent) {
739
+ const key = e.key.toLowerCase()
740
+ const keyModifiers = modifiers.filter(m => ['enter', 'escape', 'tab', 'space', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(m))
741
+ if (keyModifiers.length > 0) {
742
+ const match = keyModifiers.some(m => {
743
+ if (m === 'escape') return key === 'escape' || key === 'esc'
744
+ if (m === 'space') return key === ' ' || key === 'spacebar'
745
+ return key === m
746
+ })
747
+ if (!match) return
748
+ }
749
+ }
750
+
751
+ if (throttleMs > 0) {
752
+ const now = Date.now()
753
+ const remaining = throttleMs - (now - lastRun)
754
+
755
+ if (remaining <= 0) {
756
+ clearTimeout(throttleTimeoutId)
757
+ lastRun = now
758
+ executeLogic(e)
759
+ } else {
760
+ clearTimeout(throttleTimeoutId)
761
+ throttleTimeoutId = setTimeout(() => {
762
+ lastRun = Date.now()
763
+ executeLogic(e)
764
+ }, remaining)
765
+ }
766
+ return
767
+ }
768
+
769
+ if (debounceMs > 0) {
770
+ clearTimeout(timeoutId)
771
+ timeoutId = setTimeout(() => executeLogic(e), debounceMs)
772
+ } else {
773
+ executeLogic(e)
774
+ }
775
+ }
776
+
777
+ const options = {
778
+ once: modifiers.includes('once'),
779
+ capture: modifiers.includes('capture') || modifiers.includes('away')
780
+ }
781
+
782
+ el.addEventListener(eventName, handler, options)
783
+ if (effects) {
784
+ effects.push(() => {
785
+ el.removeEventListener(eventName, handler, options)
786
+ if (timeoutId) clearTimeout(timeoutId)
787
+ if (throttleTimeoutId) clearTimeout(throttleTimeoutId)
788
+ })
789
+ }
790
+ }
791
+ }
792
+ }
793
+ }
794
+
795
+ export function processText(el, context, effects) {
796
+ if (el.nodeType !== 1 || !el.hasAttribute('f-text')) return
797
+ const expr = el.getAttribute('f-text')
798
+ el.removeAttribute('f-text')
799
+ effects.push(effect(() => {
800
+ const val = evaluateExpression(expr, context)
801
+ el.textContent = String(val ?? '')
802
+ }))
803
+ }
804
+
805
+ export function processCloak(el) {
806
+ if (el.nodeType !== 1) return
807
+ if (el.hasAttribute('f-cloak')) el.removeAttribute('f-cloak')
808
+
809
+ const els = el.querySelectorAll('[f-cloak]')
810
+ for (const e of els) e.removeAttribute('f-cloak')
811
+ }
812
+
813
+ export function processClassList(el, context, effects) {
814
+ if (el.nodeType !== 1 || !el.hasAttribute('f-classList')) return
815
+
816
+ const expr = el.getAttribute('f-classList')
817
+ el.removeAttribute('f-classList')
818
+
819
+ const appliedClasses = new Set()
820
+
821
+ const e = effect(() => {
822
+ const result = evaluateExpression(expr, context)
823
+
824
+ if (!result || typeof result !== 'object') {
825
+ console.warn(`[framework] f-classList must evaluate to an object, got: ${typeof result}`)
826
+ return
827
+ }
828
+
829
+ for (const [className, shouldApply] of Object.entries(result)) {
830
+ const isApplied = appliedClasses.has(className)
831
+
832
+ if (shouldApply && !isApplied) {
833
+ el.classList.add(className)
834
+ appliedClasses.add(className)
835
+ } else if (!shouldApply && isApplied) {
836
+ el.classList.remove(className)
837
+ appliedClasses.delete(className)
838
+ }
839
+ }
840
+
841
+ const currentClasses = new Set(Object.keys(result))
842
+ for (const className of [...appliedClasses]) {
843
+ if (!currentClasses.has(className)) {
844
+ el.classList.remove(className)
845
+ appliedClasses.delete(className)
846
+ }
847
+ }
848
+ })
849
+
850
+ effects.push(e)
851
+ }
852
+
853
+ export function processLinks(el, effects) {
854
+ if (el.nodeType !== 1) return
855
+
856
+ const links = el.hasAttribute('f-link') ? [el] : el.querySelectorAll('[f-link]')
857
+
858
+ for (const link of links) {
859
+ if (link.tagName !== 'A') continue
860
+
861
+ const href = link.getAttribute('href')
862
+ if (!href) continue
863
+
864
+ effects.push(effect(() => {
865
+ const currentPath = currentRoute()
866
+ const isActive = currentPath === href || currentPath.startsWith(href + '/')
867
+
868
+ if (isActive) {
869
+ link.classList.add('active')
870
+ } else {
871
+ link.classList.remove('active')
872
+ }
873
+ }))
874
+ }
875
+ }