fusee-framework 1.6.0 → 1.8.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 (129) hide show
  1. package/README.md +145 -123
  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/CounterSection.js +3 -4
  7. package/app/main.js +50 -62
  8. package/app/main.ts +78 -63
  9. package/app/pages/404.js +16 -0
  10. package/app/pages/[...all].js +35 -0
  11. package/app/pages/[...all].ts +35 -0
  12. package/app/pages/_layout.js +53 -0
  13. package/app/pages/_layout.ts +26 -0
  14. package/app/pages/about.js +23 -0
  15. package/app/pages/about.ts +29 -0
  16. package/app/pages/async-work.js +65 -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 +137 -0
  22. package/app/pages/index.ts +87 -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 +79 -0
  29. package/app/pages/router-example.ts +56 -0
  30. package/app/pages/scroll-demo.ts +102 -0
  31. package/app/pages/test-for.js +65 -0
  32. package/app/pages/test-for.ts +80 -0
  33. package/app/pages/users/[id].js +24 -0
  34. package/app/pages/users/[id].ts +46 -0
  35. package/app/pages/users/index.js +22 -0
  36. package/app/pages/users/index.ts +29 -0
  37. package/app/pages/users.js +43 -0
  38. package/app/pages/users.ts +78 -0
  39. package/app/providers/ChildComponent.js +8 -2
  40. package/app/providers/ChildComponent.ts +47 -47
  41. package/app/providers/GrandchildComponent.js +19 -0
  42. package/app/providers/ParentComponent.js +5 -1
  43. package/app/routes/About.js +1 -1
  44. package/app/routes/About.ts +1 -1
  45. package/app/routes/AuthRoutes.ts +41 -0
  46. package/app/routes/Home.js +9 -0
  47. package/app/routes/NotFound.js +16 -0
  48. package/app/routes/NotFound.ts +16 -0
  49. package/app/routes/RouterExample.js +78 -0
  50. package/app/routes/RouterExample.ts +56 -0
  51. package/app/routes/TestResource.js +47 -0
  52. package/app/routes/WildcardDemo.js +35 -0
  53. package/app/routes/WildcardDemo.ts +35 -0
  54. package/app/services.js +15 -0
  55. package/auto-imports.d.ts +9 -0
  56. package/bin/create-fusee-linux +0 -0
  57. package/bin/create-fusee-mac-arm64 +0 -0
  58. package/bin/create-fusee-mac-intel +0 -0
  59. package/bin/create-fusee-win.exe +0 -0
  60. package/bin/create-fusee.exe +0 -0
  61. package/bin/index.js +38 -0
  62. package/create-fusee-go/cmd/generate.go +72 -0
  63. package/create-fusee-go/cmd/init.go +38 -0
  64. package/create-fusee-go/cmd/root.go +121 -0
  65. package/create-fusee-go/go.mod +10 -0
  66. package/create-fusee-go/go.sum +10 -0
  67. package/create-fusee-go/internal/assets/assets.go +78 -0
  68. package/create-fusee-go/internal/assets/embed/framework/auto-import-preset.js +9 -0
  69. package/create-fusee-go/internal/assets/embed/framework/core/compiler.js +253 -0
  70. package/create-fusee-go/internal/assets/embed/framework/core/component.js +266 -0
  71. package/create-fusee-go/internal/assets/embed/framework/core/composable.js +27 -0
  72. package/create-fusee-go/internal/assets/embed/framework/core/directives.js +875 -0
  73. package/create-fusee-go/internal/assets/embed/framework/core/evaluator.js +153 -0
  74. package/create-fusee-go/internal/assets/embed/framework/core/event-delegation.js +191 -0
  75. package/create-fusee-go/internal/assets/embed/framework/core/signal.js +412 -0
  76. package/create-fusee-go/internal/assets/embed/framework/core/store.js +81 -0
  77. package/create-fusee-go/internal/assets/embed/framework/index.d.ts +3 -0
  78. package/create-fusee-go/internal/assets/embed/framework/index.js +10 -0
  79. package/create-fusee-go/internal/assets/embed/framework/router/file-router.js +177 -0
  80. package/create-fusee-go/internal/assets/embed/framework/router/router.js +462 -0
  81. package/create-fusee-go/internal/assets/embed/framework/types/compiler.d.ts +15 -0
  82. package/create-fusee-go/internal/assets/embed/framework/types/component.d.ts +88 -0
  83. package/create-fusee-go/internal/assets/embed/framework/types/composable.d.ts +14 -0
  84. package/create-fusee-go/internal/assets/embed/framework/types/directives.d.ts +55 -0
  85. package/create-fusee-go/internal/assets/embed/framework/types/evaluator.d.ts +13 -0
  86. package/create-fusee-go/internal/assets/embed/framework/types/event-delegation.d.ts +32 -0
  87. package/create-fusee-go/internal/assets/embed/framework/types/index.d.ts +22 -0
  88. package/create-fusee-go/internal/assets/embed/framework/types/router.d.ts +32 -0
  89. package/create-fusee-go/internal/assets/embed/framework/types/signal.d.ts +79 -0
  90. package/create-fusee-go/internal/assets/embed/framework/types/store.d.ts +8 -0
  91. package/create-fusee-go/internal/assets/templates/Counter.tmpl +26 -0
  92. package/create-fusee-go/internal/assets/templates/component.tmpl +11 -0
  93. package/create-fusee-go/internal/assets/templates/components/Loading.tmpl +24 -0
  94. package/create-fusee-go/internal/assets/templates/index.html.tmpl +15 -0
  95. package/create-fusee-go/internal/assets/templates/main.tmpl +14 -0
  96. package/create-fusee-go/internal/assets/templates/package.json.tmpl +14 -0
  97. package/create-fusee-go/internal/assets/templates/page.tmpl +12 -0
  98. package/create-fusee-go/internal/assets/templates/pages/about.tmpl +20 -0
  99. package/create-fusee-go/internal/assets/templates/pages/index.tmpl +22 -0
  100. package/create-fusee-go/internal/assets/templates/pages/layout.tmpl +18 -0
  101. package/create-fusee-go/internal/assets/templates/tsconfig.json.tmpl +14 -0
  102. package/create-fusee-go/internal/assets/templates/vite.config.js.tmpl +18 -0
  103. package/create-fusee-go/main.go +7 -0
  104. package/framework/auto-import-preset.js +13 -9
  105. package/framework/core/compiler.js +252 -249
  106. package/framework/core/component.js +279 -260
  107. package/framework/core/di.js +187 -0
  108. package/framework/core/directives.js +819 -786
  109. package/framework/core/evaluator.js +153 -70
  110. package/framework/core/event-delegation.js +33 -14
  111. package/framework/core/reconcile.js +151 -0
  112. package/framework/core/signal.js +679 -403
  113. package/framework/core/store.js +322 -81
  114. package/framework/index.js +12 -11
  115. package/framework/router/file-router.js +177 -0
  116. package/framework/router/router.js +554 -80
  117. package/framework/types/component.d.ts +89 -88
  118. package/framework/types/di.d.ts +43 -0
  119. package/framework/types/event-delegation.d.ts +0 -58
  120. package/framework/types/index.d.ts +32 -22
  121. package/framework/types/reconcile.d.ts +7 -0
  122. package/framework/types/router.d.ts +42 -15
  123. package/framework/types/signal.d.ts +147 -78
  124. package/framework/types/store.d.ts +66 -8
  125. package/index.html +638 -626
  126. package/package.json +5 -3
  127. package/tsconfig.json +1 -0
  128. package/vite.config.js +7 -1
  129. /package/{framework → create-fusee-go/internal/assets/embed/framework}/bin/create-fusee.js +0 -0
@@ -1,250 +1,253 @@
1
- import { effect, batch } from './signal.js'
2
- import { processDirectives } from './directives.js'
3
- import { parseSlots } from './component.js'
4
- import {
5
- evaluateExpression,
6
- parseInterpolation,
7
- sanitizeAttr,
8
- MUSTACHE_RE
9
- } from './evaluator.js'
10
-
11
- export function mountTemplate(template, container, context, components) {
12
- const effects = []
13
- let processed = template.replace(/(<[a-zA-Z0-9-]+\s[^>]*?)\s:([a-zA-Z][a-zA-Z0-9-]*)=/g, '$1 data-bind-$2=')
14
-
15
- for (const name of Object.keys(components)) {
16
- const reWithSlot = new RegExp(
17
- `\\{\\{\\s*${name}(\\s[^}]*?|\\s*)\\}\\}([\\s\\S]*?)\\{\\{\\s*\\/${name}\\s*\\}\\}`,
18
- 'g'
19
- )
20
- const reNoSlot = new RegExp(`\\{\\{\\s*${name}(\\s+.*?|\\s*)\\}\\}`, 'g')
21
-
22
- processed = processed.replace(reWithSlot, (match, propsStr, slotContent) => {
23
- const attrs = parseComponentAttrs(propsStr || '')
24
- const encodedSlot = encodeURIComponent(slotContent.trim())
25
- return `<div data-component="${name}"${attrs} data-slot="${encodedSlot}"></div>`
26
- })
27
-
28
- processed = processed.replace(reNoSlot, (match, propsStr) => {
29
- const attrs = parseComponentAttrs(propsStr || '')
30
- return `<div data-component="${name}"${attrs}></div>`
31
- })
32
- }
33
-
34
- container.innerHTML = processed
35
- compileNode(container, context, components, effects)
36
-
37
- return { effects }
38
- }
39
-
40
- function parseComponentAttrs(propsStr) {
41
- let attrs = ''
42
- if (!propsStr || !propsStr.trim()) return attrs
43
-
44
- const attrRegex = /(@?:?[a-zA-Z0-9-]+)="([^"]*)"/g
45
- let attrMatch
46
- while ((attrMatch = attrRegex.exec(propsStr)) !== null) {
47
- const attrName = attrMatch[1]
48
- const attrValue = attrMatch[2]
49
-
50
- if (attrName.startsWith('@')) {
51
- attrs += ` data-on-${attrName.slice(1).toLowerCase()}="${attrValue}"`
52
- } else if (attrName.startsWith(':')) {
53
- attrs += ` data-bind-prop-${attrName.slice(1).toLowerCase()}="${attrValue}"`
54
- } else {
55
- attrs += ` data-prop-${attrName.toLowerCase()}="${attrValue}"`
56
- }
57
- }
58
- return attrs
59
- }
60
-
61
- export function compileNode(node, context, components, effects) {
62
- if (node.nodeType === 1 && node.hasAttribute('f-once')) {
63
- node.removeAttribute('f-once')
64
- const temporaryEffects = []
65
-
66
- processDirectives(node, context, components, temporaryEffects)
67
- processBindingAttrs(node, context, temporaryEffects)
68
- processMustaches(node, context, temporaryEffects)
69
- bindComponents(node, components, context, temporaryEffects)
70
-
71
- let child = node.firstChild
72
- while (child) {
73
- compileOnce(child, context, components, temporaryEffects)
74
- child = child.nextSibling
75
- }
76
-
77
- for (const cleanup of temporaryEffects) if (typeof cleanup === 'function') cleanup()
78
- return
79
- }
80
-
81
- if (node.nodeType === 1) {
82
- if (processDirectives(node, context, components, effects)) return
83
- processBindingAttrs(node, context, effects)
84
- bindComponents(node, components, context, effects)
85
- } else if (node.nodeType === 3) {
86
- processMustaches(node, context, effects)
87
- }
88
-
89
- let child = node.firstChild
90
- while (child) {
91
- compileNode(child, context, components, effects)
92
- child = child.nextSibling
93
- }
94
- }
95
-
96
- function compileOnce(node, context, components, effects) {
97
- if (node.nodeType === 1) {
98
- if (processDirectives(node, context, components, effects)) return
99
- processBindingAttrs(node, context, effects)
100
- bindComponents(node, components, context, effects)
101
- } else if (node.nodeType === 3) {
102
- processMustaches(node, context, effects)
103
- }
104
-
105
- let child = node.firstChild
106
- while (child) {
107
- compileOnce(child, context, components, effects)
108
- child = child.nextSibling
109
- }
110
- }
111
-
112
- function processMustaches(node, context, effects) {
113
- if (node.nodeType !== 3) return
114
- const text = node.textContent
115
- if (!MUSTACHE_RE.test(text)) return
116
-
117
- const parts = parseInterpolation(text)
118
- const fragment = document.createDocumentFragment()
119
-
120
- for (const part of parts) {
121
- if (part.type === 'static') {
122
- fragment.appendChild(document.createTextNode(part.value))
123
- } else {
124
- const reactiveNode = document.createTextNode('')
125
- fragment.appendChild(reactiveNode)
126
- const key = part.key
127
- const e = effect(() => {
128
- const resolved = evaluateExpression(key, context)
129
- reactiveNode.textContent = String(resolved ?? '')
130
- })
131
- effects.push(e)
132
- }
133
- }
134
- node.parentNode.replaceChild(fragment, node)
135
- }
136
-
137
- function processBindingAttrs(el, context, effects) {
138
- if (el.nodeType !== 1) return
139
- for (const attr of [...el.attributes]) {
140
- if (attr.name.startsWith('data-bind-') && !attr.name.startsWith('data-bind-prop-')) {
141
- const realAttr = attr.name.slice(10)
142
- const expr = attr.value
143
- el.removeAttribute(attr.name)
144
-
145
- const e = effect(() => {
146
- let resolved = evaluateExpression(expr, context)
147
- if (realAttr === 'class') {
148
- if (!el._staticClass) el._staticClass = el.getAttribute('class') || ''
149
- let dynamicClass = ''
150
- if (Array.isArray(resolved)) dynamicClass = resolved.filter(Boolean).join(' ')
151
- else if (resolved !== null && typeof resolved === 'object') {
152
- dynamicClass = Object.entries(resolved).filter(([_, v]) => !!v).map(([k]) => k).join(' ')
153
- } else dynamicClass = String(resolved ?? '')
154
- el.className = (el._staticClass + ' ' + dynamicClass).trim()
155
- } else if (realAttr === 'style') {
156
- if (resolved !== null && typeof resolved === 'object') {
157
- for (const k in resolved) el.style[k] = resolved[k]
158
- } else el.style.cssText = String(resolved ?? '')
159
- } else {
160
- if (typeof resolved === 'boolean') {
161
- resolved ? el.setAttribute(realAttr, '') : el.removeAttribute(realAttr)
162
- } else el.setAttribute(realAttr, sanitizeAttr(realAttr, String(resolved)))
163
- }
164
- })
165
- effects.push(e)
166
- continue
167
- }
168
-
169
- if (MUSTACHE_RE.test(attr.value)) {
170
- const attrName = attr.name
171
- const parts = parseInterpolation(attr.value)
172
- el.removeAttribute(attrName)
173
- const e = effect(() => {
174
- let result = ''
175
- for (const part of parts) {
176
- result += part.type === 'static' ? part.value : String(evaluateExpression(part.key, context) ?? '')
177
- }
178
- el.setAttribute(attrName, sanitizeAttr(attrName, result))
179
- })
180
- effects.push(e)
181
- }
182
- }
183
- }
184
-
185
- function bindComponents(el, components, context, effects) {
186
- if (el.nodeType !== 1) return
187
- const name = el.getAttribute('data-component')
188
- if (!name) return
189
-
190
- el.removeAttribute('data-component')
191
-
192
- const ComponentFn = components[name]
193
- if (!ComponentFn) return
194
-
195
- const props = {}
196
- const listeners = {}
197
-
198
- for (const attr of [...el.attributes]) {
199
- const attrName = attr.name.toLowerCase()
200
-
201
- if (attrName.startsWith('data-prop-')) {
202
- props[attrName.slice(10)] = attr.value
203
-
204
- } else if (attrName.startsWith('data-bind-prop-')) {
205
- const propName = attrName.slice(15)
206
- const expression = attr.value.trim()
207
-
208
- if (expression in context) {
209
- Object.defineProperty(props, propName, {
210
- get() {
211
- const val = context[expression]
212
- return typeof val === 'function' && val.isSignal ? val() : val
213
- },
214
- enumerable: true,
215
- configurable: true
216
- })
217
- } else {
218
- Object.defineProperty(props, propName, {
219
- get() { return evaluateExpression(expression, context) },
220
- enumerable: true,
221
- configurable: true
222
- })
223
- }
224
-
225
- } else if (attrName.startsWith('data-on-')) {
226
- const eventName = attrName.slice(8)
227
- const handlerExpr = attr.value.trim()
228
-
229
- listeners[eventName] = (...args) => {
230
- const handler = context[handlerExpr]
231
- if (typeof handler === 'function') {
232
- handler(...args)
233
- } else {
234
- evaluateExpression(handlerExpr, context)
235
- }
236
- }
237
- }
238
- }
239
-
240
- const rawSlot = el.getAttribute('data-slot')
241
- const slots = rawSlot ? parseSlots(decodeURIComponent(rawSlot)) : {}
242
- el.removeAttribute('data-slot')
243
-
244
- const childComponent = ComponentFn(props, { listeners, slots, parent: context._instance })
245
- childComponent.render(el)
246
-
247
- if (effects) {
248
- effects.push(() => childComponent.unmount())
249
- }
1
+ import { effect, batch } from './signal.js'
2
+ import { processDirectives } from './directives.js'
3
+ import { parseSlots } from './component.js'
4
+ import {
5
+ evaluateExpression,
6
+ parseInterpolation,
7
+ sanitizeAttr,
8
+ MUSTACHE_RE
9
+ } from './evaluator.js'
10
+
11
+ export function mountTemplate(template, container, context, components) {
12
+ const effects = []
13
+ let processed = template.replace(/(<[a-zA-Z0-9-]+\s[^>]*?)\s:([a-zA-Z][a-zA-Z0-9-]*)=/g, '$1 data-bind-$2=')
14
+
15
+ for (const name of Object.keys(components)) {
16
+ const reWithSlot = new RegExp(
17
+ `\\{\\{\\s*${name}(\\s[^}]*?|\\s*)\\}\\}([\\s\\S]*?)\\{\\{\\s*\\/${name}\\s*\\}\\}`,
18
+ 'g'
19
+ )
20
+ const reNoSlot = new RegExp(`\\{\\{\\s*${name}(\\s+.*?|\\s*)\\}\\}`, 'g')
21
+
22
+ processed = processed.replace(reWithSlot, (match, propsStr, slotContent) => {
23
+ const attrs = parseComponentAttrs(propsStr || '')
24
+ const encodedSlot = encodeURIComponent(slotContent.trim())
25
+ return `<div data-component="${name}"${attrs} data-slot="${encodedSlot}"></div>`
26
+ })
27
+
28
+ processed = processed.replace(reNoSlot, (match, propsStr) => {
29
+ const attrs = parseComponentAttrs(propsStr || '')
30
+ return `<div data-component="${name}"${attrs}></div>`
31
+ })
32
+ }
33
+
34
+ container.innerHTML = processed
35
+ compileNode(container, context, components, effects)
36
+
37
+ return { effects }
38
+ }
39
+
40
+ function parseComponentAttrs(propsStr) {
41
+ let attrs = ''
42
+ if (!propsStr || !propsStr.trim()) return attrs
43
+
44
+ const attrRegex = /(@?:?[a-zA-Z0-9-]+)="([^"]*)"/g
45
+ let attrMatch
46
+ while ((attrMatch = attrRegex.exec(propsStr)) !== null) {
47
+ const attrName = attrMatch[1]
48
+ const attrValue = attrMatch[2]
49
+
50
+ if (attrName.startsWith('@')) {
51
+ attrs += ` data-on-${attrName.slice(1).toLowerCase()}="${attrValue}"`
52
+ } else if (attrName.startsWith(':')) {
53
+ attrs += ` data-bind-prop-${attrName.slice(1).toLowerCase()}="${attrValue}"`
54
+ } else {
55
+ attrs += ` data-prop-${attrName.toLowerCase()}="${attrValue}"`
56
+ }
57
+ }
58
+ return attrs
59
+ }
60
+
61
+ export function compileNode(node, context, components, effects) {
62
+ if (node.nodeType === 1 && node.hasAttribute('f-once')) {
63
+ node.removeAttribute('f-once')
64
+ const temporaryEffects = []
65
+
66
+ processDirectives(node, context, components, temporaryEffects)
67
+ processBindingAttrs(node, context, temporaryEffects)
68
+ processMustaches(node, context, temporaryEffects)
69
+ bindComponents(node, components, context, temporaryEffects)
70
+
71
+ let child = node.firstChild
72
+ while (child) {
73
+ compileOnce(child, context, components, temporaryEffects)
74
+ child = child.nextSibling
75
+ }
76
+
77
+ for (let i = temporaryEffects.length - 1; i >= 0; i--) {
78
+ const cleanup = temporaryEffects[i]
79
+ if (typeof cleanup === 'function') cleanup()
80
+ }
81
+ return
82
+ }
83
+
84
+ if (node.nodeType === 1) {
85
+ if (processDirectives(node, context, components, effects)) return
86
+ processBindingAttrs(node, context, effects)
87
+ bindComponents(node, components, context, effects)
88
+ } else if (node.nodeType === 3) {
89
+ processMustaches(node, context, effects)
90
+ }
91
+
92
+ let child = node.firstChild
93
+ while (child) {
94
+ compileNode(child, context, components, effects)
95
+ child = child.nextSibling
96
+ }
97
+ }
98
+
99
+ function compileOnce(node, context, components, effects) {
100
+ if (node.nodeType === 1) {
101
+ if (processDirectives(node, context, components, effects)) return
102
+ processBindingAttrs(node, context, effects)
103
+ bindComponents(node, components, context, effects)
104
+ } else if (node.nodeType === 3) {
105
+ processMustaches(node, context, effects)
106
+ }
107
+
108
+ let child = node.firstChild
109
+ while (child) {
110
+ compileOnce(child, context, components, effects)
111
+ child = child.nextSibling
112
+ }
113
+ }
114
+
115
+ function processMustaches(node, context, effects) {
116
+ if (node.nodeType !== 3) return
117
+ const text = node.textContent
118
+ if (!MUSTACHE_RE.test(text)) return
119
+
120
+ const parts = parseInterpolation(text)
121
+ const fragment = document.createDocumentFragment()
122
+
123
+ for (const part of parts) {
124
+ if (part.type === 'static') {
125
+ fragment.appendChild(document.createTextNode(part.value))
126
+ } else {
127
+ const reactiveNode = document.createTextNode('')
128
+ fragment.appendChild(reactiveNode)
129
+ const key = part.key
130
+ const e = effect(() => {
131
+ const resolved = evaluateExpression(key, context)
132
+ reactiveNode.textContent = String(resolved ?? '')
133
+ })
134
+ effects.push(e)
135
+ }
136
+ }
137
+ node.parentNode.replaceChild(fragment, node)
138
+ }
139
+
140
+ function processBindingAttrs(el, context, effects) {
141
+ if (el.nodeType !== 1) return
142
+ for (const attr of [...el.attributes]) {
143
+ if (attr.name.startsWith('data-bind-') && !attr.name.startsWith('data-bind-prop-')) {
144
+ const realAttr = attr.name.slice(10)
145
+ const expr = attr.value
146
+ el.removeAttribute(attr.name)
147
+
148
+ const e = effect(() => {
149
+ let resolved = evaluateExpression(expr, context)
150
+ if (realAttr === 'class') {
151
+ if (!el._staticClass) el._staticClass = el.getAttribute('class') || ''
152
+ let dynamicClass = ''
153
+ if (Array.isArray(resolved)) dynamicClass = resolved.filter(Boolean).join(' ')
154
+ else if (resolved !== null && typeof resolved === 'object') {
155
+ dynamicClass = Object.entries(resolved).filter(([_, v]) => !!v).map(([k]) => k).join(' ')
156
+ } else dynamicClass = String(resolved ?? '')
157
+ el.className = (el._staticClass + ' ' + dynamicClass).trim()
158
+ } else if (realAttr === 'style') {
159
+ if (resolved !== null && typeof resolved === 'object') {
160
+ for (const k in resolved) el.style[k] = resolved[k]
161
+ } else el.style.cssText = String(resolved ?? '')
162
+ } else {
163
+ if (typeof resolved === 'boolean') {
164
+ resolved ? el.setAttribute(realAttr, '') : el.removeAttribute(realAttr)
165
+ } else el.setAttribute(realAttr, sanitizeAttr(realAttr, String(resolved)))
166
+ }
167
+ })
168
+ effects.push(e)
169
+ continue
170
+ }
171
+
172
+ if (MUSTACHE_RE.test(attr.value)) {
173
+ const attrName = attr.name
174
+ const parts = parseInterpolation(attr.value)
175
+ el.removeAttribute(attrName)
176
+ const e = effect(() => {
177
+ let result = ''
178
+ for (const part of parts) {
179
+ result += part.type === 'static' ? part.value : String(evaluateExpression(part.key, context) ?? '')
180
+ }
181
+ el.setAttribute(attrName, sanitizeAttr(attrName, result))
182
+ })
183
+ effects.push(e)
184
+ }
185
+ }
186
+ }
187
+
188
+ function bindComponents(el, components, context, effects) {
189
+ if (el.nodeType !== 1) return
190
+ const name = el.getAttribute('data-component')
191
+ if (!name) return
192
+
193
+ el.removeAttribute('data-component')
194
+
195
+ const ComponentFn = components[name]
196
+ if (!ComponentFn) return
197
+
198
+ const props = {}
199
+ const listeners = {}
200
+
201
+ for (const attr of [...el.attributes]) {
202
+ const attrName = attr.name.toLowerCase()
203
+
204
+ if (attrName.startsWith('data-prop-')) {
205
+ props[attrName.slice(10)] = attr.value
206
+
207
+ } else if (attrName.startsWith('data-bind-prop-')) {
208
+ const propName = attrName.slice(15)
209
+ const expression = attr.value.trim()
210
+
211
+ if (expression in context) {
212
+ Object.defineProperty(props, propName, {
213
+ get() {
214
+ const val = context[expression]
215
+ return typeof val === 'function' && val.isSignal ? val() : val
216
+ },
217
+ enumerable: true,
218
+ configurable: true
219
+ })
220
+ } else {
221
+ Object.defineProperty(props, propName, {
222
+ get() { return evaluateExpression(expression, context) },
223
+ enumerable: true,
224
+ configurable: true
225
+ })
226
+ }
227
+
228
+ } else if (attrName.startsWith('data-on-')) {
229
+ const eventName = attrName.slice(8)
230
+ const handlerExpr = attr.value.trim()
231
+
232
+ listeners[eventName] = (...args) => {
233
+ const handler = context[handlerExpr]
234
+ if (typeof handler === 'function') {
235
+ handler(...args)
236
+ } else {
237
+ evaluateExpression(handlerExpr, context)
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ const rawSlot = el.getAttribute('data-slot')
244
+ const slots = rawSlot ? parseSlots(decodeURIComponent(rawSlot)) : {}
245
+ el.removeAttribute('data-slot')
246
+
247
+ const childComponent = ComponentFn(props, { listeners, slots, parent: context._instance })
248
+ childComponent.render(el)
249
+
250
+ if (effects) {
251
+ effects.push(() => childComponent.unmount())
252
+ }
250
253
  }