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
@@ -1,80 +1,462 @@
1
- let _routes = []
2
- let _outlet = null
3
- let _currentInstance = null
4
-
5
- export function createRouter(routes) {
6
- _routes = routes
7
-
8
- const handler = () => _resolve()
9
- window.addEventListener('hashchange', handler)
10
-
11
- if (document.readyState === 'loading') {
12
- window.addEventListener('DOMContentLoaded', handler, { once: true })
13
- } else {
14
- setTimeout(() => {
15
- if (!_currentInstance) _resolve()
16
- }, 0)
17
- }
18
-
19
- return {
20
- navigate,
21
- destroy() {
22
- window.removeEventListener('hashchange', handler)
23
- window.removeEventListener('DOMContentLoaded', handler)
24
- if (_currentInstance) {
25
- try {
26
- _currentInstance.unmount()
27
- } catch { }
28
- }
29
- _routes = []
30
- _outlet = null
31
- _currentInstance = null
32
- }
33
- }
34
- }
35
-
36
- export function navigate(path) {
37
- window.location.hash = path
38
- }
39
-
40
- export function mountOutlet(el) {
41
- _outlet = el
42
- _resolve()
43
- }
44
-
45
- function _resolve() {
46
- if (!_outlet || _routes.length === 0) {
47
- return
48
- }
49
-
50
- const hash = window.location.hash
51
- const path = hash.startsWith('#') ? hash.slice(1) || '/' : '/'
52
-
53
- const matched = _routes.find(r => _matchPath(r.path, path))
54
-
55
- if (!matched) {
56
- _outlet.innerHTML = `<p style="color:red">[framework] No route matched for "${path}"</p>`
57
- return
58
- }
59
-
60
- if (_currentInstance) {
61
- try { _currentInstance.unmount() } catch { }
62
- }
63
-
64
- _outlet.innerHTML = ''
65
-
66
- const componentFn = matched.component
67
- _currentInstance = componentFn()
68
- _currentInstance.render(_outlet)
69
- }
70
-
71
- function _matchPath(routePath, actualPath) {
72
- const routeParts = routePath.split('/').filter(Boolean)
73
- const actualParts = actualPath.split('/').filter(Boolean)
74
-
75
- if (routeParts.length !== actualParts.length) return false
76
-
77
- return routeParts.every((part, i) =>
78
- part.startsWith(':') || part === actualParts[i]
79
- )
80
- }
1
+ import { signal } from '../core/signal.js'
2
+
3
+ export const currentRoute = signal('/')
4
+ export const routeParams = signal({})
5
+ export const matchedRoutes = signal([])
6
+
7
+ let _routes = []
8
+ let _rootOutlet = null
9
+ let _clickHandler = null
10
+ let _activeChain = []
11
+ let _routeCache = new Map()
12
+ let _cacheMaxSize = 100
13
+ let _routerViewTimeout = 10000
14
+
15
+ function _getPath() {
16
+ return window.location.pathname || '/'
17
+ }
18
+
19
+ function _getRoutePaths(routePath) {
20
+ return Array.isArray(routePath) ? routePath : [routePath]
21
+ }
22
+
23
+ function _updateRoute() {
24
+ currentRoute(_getPath())
25
+ }
26
+
27
+ function _matchSegments(routePath, urlSegments) {
28
+ const cleanPath = routePath.startsWith('/') ? routePath.slice(1) : routePath
29
+
30
+ if (cleanPath === '') {
31
+ return { matched: true, consumed: 0, params: {} }
32
+ }
33
+
34
+ if (cleanPath === '*') {
35
+ return { matched: true, consumed: urlSegments.length, params: {} }
36
+ }
37
+
38
+ const routeSegs = cleanPath.split('/').filter(Boolean)
39
+ const hasWildcard = routeSegs[routeSegs.length - 1] === '*'
40
+ const checkSegs = hasWildcard ? routeSegs.slice(0, -1) : routeSegs
41
+
42
+ if (hasWildcard) {
43
+ if (urlSegments.length < checkSegs.length) return null
44
+ }
45
+
46
+ if (urlSegments.length < checkSegs.length) return null
47
+
48
+ const params = {}
49
+ for (let i = 0; i < checkSegs.length; i++) {
50
+ const rSeg = checkSegs[i]
51
+ const uSeg = urlSegments[i]
52
+ if (rSeg.startsWith(':')) {
53
+ params[rSeg.slice(1)] = uSeg
54
+ } else if (rSeg !== uSeg) {
55
+ return null
56
+ }
57
+ }
58
+
59
+ const consumed = hasWildcard ? urlSegments.length : checkSegs.length
60
+ return { matched: true, consumed, params }
61
+ }
62
+
63
+ function _matchRouteTree(routes, urlSegments) {
64
+ for (const route of routes) {
65
+ const paths = _getRoutePaths(route.path)
66
+ for (const path of paths) {
67
+ const result = _matchSegments(path, urlSegments)
68
+ if (!result) continue
69
+
70
+ const remaining = urlSegments.slice(result.consumed)
71
+
72
+ if (route.children && route.children.length > 0) {
73
+ const childChain = _matchRouteTree(route.children, remaining)
74
+ if (childChain) {
75
+ return [
76
+ { route, params: result.params, matchedSegments: urlSegments.slice(0, result.consumed), matchedPath: path },
77
+ ...childChain
78
+ ]
79
+ }
80
+ continue
81
+ }
82
+
83
+ if (remaining.length === 0 || path === '*' || path.endsWith('/*')) {
84
+ return [{ route, params: result.params, matchedSegments: urlSegments.slice(0, result.consumed), matchedPath: path }]
85
+ }
86
+ }
87
+ }
88
+
89
+ return null
90
+ }
91
+
92
+ function _matchSingleFlat(routePath, actualPath) {
93
+ if (routePath === '*') return true
94
+
95
+ const routeParts = routePath.split('/').filter(Boolean)
96
+ const actualParts = actualPath.split('/').filter(Boolean)
97
+
98
+ const hasWildcard = routeParts.length > 0 && routeParts[routeParts.length - 1] === '*'
99
+
100
+ if (hasWildcard) {
101
+ const baseParts = routeParts.slice(0, -1)
102
+ if (actualParts.length < baseParts.length) return false
103
+ return baseParts.every((part, i) => part.startsWith(':') || part === actualParts[i])
104
+ }
105
+
106
+ if (routeParts.length !== actualParts.length) return false
107
+ return routeParts.every((part, i) => part.startsWith(':') || part === actualParts[i])
108
+ }
109
+
110
+ function _matchFlat(routePath, actualPath) {
111
+ const paths = _getRoutePaths(routePath)
112
+ return paths.some(p => _matchSingleFlat(p, actualPath))
113
+ }
114
+
115
+ function _findMatchingPath(routePath, actualPath) {
116
+ const paths = _getRoutePaths(routePath)
117
+ return paths.find(p => _matchSingleFlat(p, actualPath)) || null
118
+ }
119
+
120
+ function _extractParamsFromPath(singlePath, actualPath) {
121
+ const params = {}
122
+ if (singlePath === '*') return params
123
+
124
+ const routeParts = singlePath.split('/').filter(Boolean)
125
+ const actualParts = actualPath.split('/').filter(Boolean)
126
+
127
+ const hasWildcard = routeParts.length > 0 && routeParts[routeParts.length - 1] === '*'
128
+ const checkLen = hasWildcard ? routeParts.length - 1 : routeParts.length
129
+
130
+ for (let i = 0; i < checkLen; i++) {
131
+ if (routeParts[i] && routeParts[i].startsWith(':')) {
132
+ params[routeParts[i].slice(1)] = actualParts[i] || ''
133
+ }
134
+ }
135
+ return params
136
+ }
137
+
138
+ function _extractParamsFlat(routePath, actualPath) {
139
+ const matchingPath = _findMatchingPath(routePath, actualPath)
140
+ if (!matchingPath) return {}
141
+ return _extractParamsFromPath(matchingPath, actualPath)
142
+ }
143
+
144
+ function _findMatchingChain(path) {
145
+ if (_routeCache.has(path)) {
146
+ const cached = _routeCache.get(path)
147
+ _routeCache.delete(path)
148
+ _routeCache.set(path, cached)
149
+ return cached
150
+ }
151
+
152
+ const urlSegments = path.split('/').filter(Boolean)
153
+ const hasNested = _routes.some(r => r.children && r.children.length > 0)
154
+
155
+ let chain = null
156
+
157
+ if (hasNested) {
158
+ chain = _matchRouteTree(_routes, urlSegments)
159
+ if (chain) {
160
+ _cacheResult(path, chain)
161
+ return chain
162
+ }
163
+ }
164
+
165
+ const paths = _routes.filter(r => {
166
+ const routePaths = _getRoutePaths(r.path)
167
+ return !routePaths.includes('*') && !routePaths.some(p => p.endsWith('/*')) && !r.children
168
+ })
169
+ const exactMatch = paths.find(r => _matchFlat(r.path, path))
170
+ if (exactMatch) {
171
+ const matchedPath = _findMatchingPath(exactMatch.path, path)
172
+ chain = [{ route: exactMatch, params: _extractParamsFlat(exactMatch.path, path), matchedSegments: urlSegments, matchedPath }]
173
+ _cacheResult(path, chain)
174
+ return chain
175
+ }
176
+
177
+ const wildcardPaths = _routes.filter(r => {
178
+ const routePaths = _getRoutePaths(r.path)
179
+ return !r.children && routePaths.some(p => p.endsWith('/*'))
180
+ })
181
+ const wildcardMatch = wildcardPaths.find(r => _matchFlat(r.path, path))
182
+ if (wildcardMatch) {
183
+ const matchedPath = _findMatchingPath(wildcardMatch.path, path)
184
+ chain = [{ route: wildcardMatch, params: _extractParamsFlat(wildcardMatch.path, path), matchedSegments: urlSegments, matchedPath }]
185
+ _cacheResult(path, chain)
186
+ return chain
187
+ }
188
+
189
+ const catchAll = _routes.find(r => {
190
+ const routePaths = _getRoutePaths(r.path)
191
+ return routePaths.includes('*')
192
+ })
193
+ if (catchAll) {
194
+ chain = [{ route: catchAll, params: {}, matchedSegments: urlSegments, matchedPath: '*' }]
195
+ _cacheResult(path, chain)
196
+ return chain
197
+ }
198
+
199
+ return null
200
+ }
201
+
202
+ function _cacheResult(path, chain) {
203
+ if (_routeCache.size >= _cacheMaxSize) {
204
+ const firstKey = _routeCache.keys().next().value
205
+ _routeCache.delete(firstKey)
206
+ }
207
+ _routeCache.set(path, chain)
208
+ }
209
+
210
+ function _clearRouteCache() {
211
+ _routeCache.clear()
212
+ }
213
+
214
+ function _unmountFromLevel(level) {
215
+ for (let i = _activeChain.length - 1; i >= level; i--) {
216
+ const entry = _activeChain[i]
217
+ if (entry && entry.instance) {
218
+ try { entry.instance.unmount() } catch { }
219
+ }
220
+ }
221
+ _activeChain.length = level
222
+ }
223
+
224
+ function _getRoutePathCount(route) {
225
+ return Array.isArray(route.path) ? route.path.length : 1
226
+ }
227
+
228
+ function _renderChain(chain) {
229
+ try {
230
+ let reuseUntil = 0
231
+ for (let i = 0; i < Math.min(_activeChain.length, chain.length); i++) {
232
+ const active = _activeChain[i]
233
+ const incoming = chain[i]
234
+
235
+ if (
236
+ active.route === incoming.route &&
237
+ _segmentsEqual(active.matchedSegments, incoming.matchedSegments)
238
+ ) {
239
+ reuseUntil = i + 1
240
+ }
241
+ else if (
242
+ active.route === incoming.route &&
243
+ _getRoutePathCount(active.route) > 1
244
+ ) {
245
+ reuseUntil = i + 1
246
+ }
247
+ else {
248
+ break
249
+ }
250
+ }
251
+
252
+ _unmountFromLevel(reuseUntil)
253
+
254
+ const allParams = {}
255
+ for (const entry of chain) {
256
+ Object.assign(allParams, entry.params)
257
+ }
258
+ routeParams(allParams)
259
+ matchedRoutes(chain.map(e => e.route))
260
+ _renderFromLevel(chain, reuseUntil)
261
+ } catch (error) {
262
+ console.error('[framework] Error rendering route chain:', error)
263
+ _unmountFromLevel(0)
264
+ if (_rootOutlet) {
265
+ _rootOutlet.innerHTML = `<p style="color:red">[framework] Error rendering route: ${error.message}</p>`
266
+ }
267
+ }
268
+ }
269
+
270
+ function _renderFromLevel(chain, level) {
271
+ if (level >= chain.length) return
272
+
273
+ const entry = chain[level]
274
+ const outlet = _getOutletForLevel(level)
275
+
276
+ if (!outlet) {
277
+ _waitForRouterView(level, () => _renderFromLevel(chain, level))
278
+ return
279
+ }
280
+
281
+ outlet.innerHTML = ''
282
+
283
+ const componentFn = entry.route.component
284
+ const instance = componentFn()
285
+ instance.render(outlet)
286
+
287
+ _activeChain[level] = {
288
+ route: entry.route,
289
+ matchedSegments: entry.matchedSegments,
290
+ params: entry.params,
291
+ instance,
292
+ outlet
293
+ }
294
+
295
+ if (level + 1 < chain.length) _scheduleNextLevel(chain, level + 1)
296
+ }
297
+
298
+ function _scheduleNextLevel(chain, level) {
299
+ const outlet = _getOutletForLevel(level)
300
+ if (outlet) {
301
+ _renderFromLevel(chain, level)
302
+ return
303
+ }
304
+
305
+ _waitForRouterView(level, () => _renderFromLevel(chain, level))
306
+ }
307
+
308
+ function _getOutletForLevel(level) {
309
+ if (level === 0) return _rootOutlet
310
+
311
+ const parentEntry = _activeChain[level - 1]
312
+ if (!parentEntry || !parentEntry.outlet) return null
313
+
314
+ return parentEntry.outlet.querySelector('[data-router-view]')
315
+ }
316
+
317
+ function _waitForRouterView(level, callback) {
318
+ const parentEntry = _activeChain[level - 1]
319
+ if (!parentEntry || !parentEntry.outlet) return
320
+
321
+ const parentOutlet = parentEntry.outlet
322
+
323
+ const existing = parentOutlet.querySelector('[data-router-view]')
324
+ if (existing) {
325
+ callback()
326
+ return
327
+ }
328
+
329
+ const observer = new MutationObserver(() => {
330
+ const el = parentOutlet.querySelector('[data-router-view]')
331
+ if (el) {
332
+ observer.disconnect()
333
+ callback()
334
+ }
335
+ })
336
+
337
+ observer.observe(parentOutlet, { childList: true, subtree: true })
338
+
339
+ setTimeout(() => observer.disconnect(), _routerViewTimeout)
340
+ }
341
+
342
+ function _segmentsEqual(a, b) {
343
+ if (a.length !== b.length) return false
344
+ for (let i = 0; i < a.length; i++) {
345
+ if (a[i] !== b[i]) return false
346
+ }
347
+ return true
348
+ }
349
+
350
+ function _resolveRoute() {
351
+ if (!_rootOutlet || _routes.length === 0) return
352
+
353
+ const path = _getPath()
354
+ const chain = _findMatchingChain(path)
355
+
356
+ if (!chain) {
357
+ _unmountFromLevel(0)
358
+ _rootOutlet.innerHTML = `<p style="color:red">[framework] No route matched for "${path}"</p>`
359
+ routeParams({})
360
+ matchedRoutes([])
361
+ return
362
+ }
363
+
364
+ _renderChain(chain)
365
+ }
366
+
367
+ export function navigate(path) {
368
+ if (path === _getPath()) return
369
+ window.history.pushState({}, '', path)
370
+ _updateRoute()
371
+ _resolveRoute()
372
+ }
373
+
374
+ export function mountOutlet(el) {
375
+ _rootOutlet = el
376
+ _resolveRoute()
377
+ }
378
+
379
+ function _isInternalLink(anchor) {
380
+ const href = anchor.getAttribute('href')
381
+ if (!href) return false
382
+ if (href.startsWith('#')) return false
383
+ if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {
384
+ const url = new URL(href, window.location.origin)
385
+ return url.origin === window.location.origin
386
+ }
387
+ return href.startsWith('/')
388
+ }
389
+
390
+ function _getAnchorFromEvent(e) {
391
+ if (e.composedPath) {
392
+ return e.composedPath().find(el => el.tagName === 'A')
393
+ }
394
+ return e.target.tagName === 'A' ? e.target : null
395
+ }
396
+
397
+ function _handleLinkClick(e) {
398
+ const anchor = _getAnchorFromEvent(e)
399
+ if (!anchor) return
400
+
401
+ if (anchor.hasAttribute('f-link') || _isInternalLink(anchor)) {
402
+ e.preventDefault()
403
+ const href = anchor.getAttribute('href')
404
+ if (href && href !== _getPath()) {
405
+ navigate(href)
406
+ }
407
+ }
408
+ }
409
+
410
+ function _setupPopstateHandler() {
411
+ const handler = () => {
412
+ _updateRoute()
413
+ _resolveRoute()
414
+ }
415
+ window.addEventListener('popstate', handler)
416
+ return handler
417
+ }
418
+
419
+ function _setupClickHandler() {
420
+ _clickHandler = _handleLinkClick
421
+ document.addEventListener('click', _clickHandler)
422
+ return _clickHandler
423
+ }
424
+
425
+ function _setupInitialRoute() {
426
+ if (document.readyState === 'loading') {
427
+ window.addEventListener('DOMContentLoaded', () => {
428
+ _updateRoute()
429
+ _resolveRoute()
430
+ }, { once: true })
431
+ } else {
432
+ setTimeout(() => {
433
+ if (_activeChain.length === 0) _resolveRoute()
434
+ }, 0)
435
+ }
436
+ }
437
+
438
+ export function createRouter(routes, options = {}) {
439
+ _routes = routes
440
+ _activeChain = []
441
+ _cacheMaxSize = options.cacheSize || 100
442
+ _routerViewTimeout = options.routerViewTimeout || 10000
443
+
444
+ _updateRoute()
445
+
446
+ const popstateHandler = _setupPopstateHandler()
447
+ _setupClickHandler()
448
+ _setupInitialRoute()
449
+
450
+ return {
451
+ navigate,
452
+ destroy() {
453
+ window.removeEventListener('popstate', popstateHandler)
454
+ document.removeEventListener('click', _clickHandler)
455
+ _unmountFromLevel(0)
456
+ _routes = []
457
+ _rootOutlet = null
458
+ _activeChain = []
459
+ _clearRouteCache()
460
+ }
461
+ }
462
+ }
@@ -1,9 +1,5 @@
1
- // ─── Component Types ─────────────────────────────────────────────────────────
2
-
3
1
  export type ComponentProps = Record<string, any>
4
-
5
2
  export type EmptyProps = Record<string, never>
6
-
7
3
  export type PropSchema = string[] | Record<string, PropConfig>
8
4
 
9
5
  export type PropConfig = {
@@ -0,0 +1,32 @@
1
+ export const DELEGATED_EVENTS: Set<string>
2
+
3
+ export function isDelegatedEvent(eventName: string): boolean
4
+
5
+ export interface DelegatedEventOptions {
6
+ modifiers?: string[]
7
+ context?: Record<string, any>
8
+ expr?: string
9
+ }
10
+
11
+ export function registerDelegatedEvent(
12
+ element: Element,
13
+ eventType: string,
14
+ handler: (event: Event) => void,
15
+ options?: DelegatedEventOptions
16
+ ): () => void
17
+
18
+ export function createEventHandler(
19
+ handler: Function | string,
20
+ modifiers: string[],
21
+ context: Record<string, any>,
22
+ expr: string
23
+ ): (event: Event) => void
24
+
25
+ export interface EventHandlerSyntax {
26
+ delegated: `@${string}`
27
+ native: `on:${string}`
28
+ }
29
+
30
+ declare global {
31
+ const isDelegatedEvent: typeof import('./event-delegation').isDelegatedEvent
32
+ }
@@ -5,6 +5,7 @@ export * from './router'
5
5
  export * from './compiler'
6
6
  export * from './directives'
7
7
  export * from './store'
8
+ export * from './event-delegation'
8
9
 
9
10
  declare global {
10
11
  type Signal<T = any> = import('./signal').Signal<T>
@@ -13,7 +14,9 @@ declare global {
13
14
  type Composable<T extends (...args: any[]) => any> = import('./composable').Composable<T>
14
15
  type DirectiveBinding<V = any> = import('./directives').DirectiveBinding<V>
15
16
  type DirectiveHooks<V = any> = import('./directives').DirectiveHooks<V>
17
+ type DelegatedEventOptions = import('./event-delegation').DelegatedEventOptions
16
18
  const defineStore: typeof import('./store').defineStore
19
+ const isDelegatedEvent: typeof import('./event-delegation').isDelegatedEvent
17
20
  }
18
21
 
19
22
  export as namespace Fusee
@@ -1,15 +1,32 @@
1
- import type { ComponentApi } from './component'
2
-
3
- export type Route = {
4
- path: string
5
- component: () => ComponentApi
6
- }
7
-
8
- export type Router = {
9
- navigate: (path: string) => void
10
- destroy: () => void
11
- }
12
-
13
- export declare function createRouter(routes: Route[]): Router
14
- export declare function navigate(path: string): void
15
- export declare function mountOutlet(el: HTMLElement): void
1
+ import type { ComponentApi, ComponentFactory } from './component'
2
+ import type { Signal } from './signal'
3
+
4
+ export type Route = {
5
+ path: string | string[]
6
+ component: ComponentFactory<any>
7
+ children?: Route[]
8
+ }
9
+
10
+ export type Router = {
11
+ navigate: (path: string) => void
12
+ destroy: () => void
13
+ }
14
+
15
+ export type RouteParams = Record<string, string>
16
+
17
+ export declare const currentRoute: Signal<string>
18
+ export declare const routeParams: Signal<RouteParams>
19
+ export declare const matchedRoutes: Signal<Route[]>
20
+
21
+ export declare function createRouter(routes: Route[], options?: { cacheSize?: number; routerViewTimeout?: number }): Router
22
+ export declare function navigate(path: string): void
23
+ export declare function mountOutlet(el: HTMLElement): void
24
+
25
+ export type FileRouterOptions = {
26
+ loadingComponent?: ComponentFactory<any>
27
+ }
28
+
29
+ export declare function generateRoutes(
30
+ globResults: Record<string, () => Promise<any>>,
31
+ options?: FileRouterOptions
32
+ ): Route[]
@@ -73,6 +73,7 @@ export declare function watch<T extends any[]>(
73
73
  options?: WatchOptions
74
74
  ): () => void
75
75
  export declare function inspect(...args: any[]): (() => void) | void
76
+ export declare function onCleanup(fn: () => void): void
76
77
 
77
78
  declare function setEffectHook(fn: (eff: any) => void): void
78
79