fusee-framework 1.6.0 → 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 (112) hide show
  1. package/README.md +29 -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/CounterSection.js +3 -4
  7. package/app/main.js +29 -62
  8. package/app/main.ts +72 -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 +26 -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/composables.js +158 -0
  17. package/app/pages/composables.ts +158 -0
  18. package/app/pages/dynamic.js +40 -0
  19. package/app/pages/dynamic.ts +40 -0
  20. package/app/pages/index.js +117 -0
  21. package/app/pages/index.ts +78 -0
  22. package/app/pages/login.ts +21 -0
  23. package/app/pages/objects/[id].js +56 -0
  24. package/app/pages/objects/[id].ts +56 -0
  25. package/app/pages/posts/[category]/[slug].js +56 -0
  26. package/app/pages/posts/[category]/[slug].ts +56 -0
  27. package/app/pages/router-example.js +56 -0
  28. package/app/pages/router-example.ts +56 -0
  29. package/app/pages/test-for.js +65 -0
  30. package/app/pages/test-for.ts +80 -0
  31. package/app/pages/users/[id].js +24 -0
  32. package/app/pages/users/[id].ts +46 -0
  33. package/app/pages/users/index.js +22 -0
  34. package/app/pages/users/index.ts +29 -0
  35. package/app/pages/users.js +43 -0
  36. package/app/pages/users.ts +78 -0
  37. package/app/routes/About.js +1 -1
  38. package/app/routes/About.ts +1 -1
  39. package/app/routes/AuthRoutes.ts +41 -0
  40. package/app/routes/Home.js +9 -0
  41. package/app/routes/NotFound.js +16 -0
  42. package/app/routes/NotFound.ts +16 -0
  43. package/app/routes/RouterExample.js +56 -0
  44. package/app/routes/RouterExample.ts +56 -0
  45. package/app/routes/WildcardDemo.js +35 -0
  46. package/app/routes/WildcardDemo.ts +35 -0
  47. package/auto-imports.d.ts +1 -0
  48. package/bin/create-fusee-linux +0 -0
  49. package/bin/create-fusee-mac-arm64 +0 -0
  50. package/bin/create-fusee-mac-intel +0 -0
  51. package/bin/create-fusee-win.exe +0 -0
  52. package/bin/create-fusee.exe +0 -0
  53. package/bin/index.js +38 -0
  54. package/create-fusee-go/cmd/generate.go +72 -0
  55. package/create-fusee-go/cmd/init.go +38 -0
  56. package/create-fusee-go/cmd/root.go +121 -0
  57. package/create-fusee-go/go.mod +10 -0
  58. package/create-fusee-go/go.sum +10 -0
  59. package/create-fusee-go/internal/assets/assets.go +78 -0
  60. package/create-fusee-go/internal/assets/embed/framework/auto-import-preset.js +9 -0
  61. package/create-fusee-go/internal/assets/embed/framework/core/compiler.js +253 -0
  62. package/create-fusee-go/internal/assets/embed/framework/core/component.js +266 -0
  63. package/create-fusee-go/internal/assets/embed/framework/core/composable.js +27 -0
  64. package/create-fusee-go/internal/assets/embed/framework/core/directives.js +875 -0
  65. package/create-fusee-go/internal/assets/embed/framework/core/evaluator.js +153 -0
  66. package/create-fusee-go/internal/assets/embed/framework/core/event-delegation.js +191 -0
  67. package/create-fusee-go/internal/assets/embed/framework/core/signal.js +412 -0
  68. package/create-fusee-go/internal/assets/embed/framework/core/store.js +81 -0
  69. package/create-fusee-go/internal/assets/embed/framework/index.d.ts +3 -0
  70. package/create-fusee-go/internal/assets/embed/framework/index.js +10 -0
  71. package/create-fusee-go/internal/assets/embed/framework/router/file-router.js +177 -0
  72. package/create-fusee-go/internal/assets/embed/framework/router/router.js +462 -0
  73. package/create-fusee-go/internal/assets/embed/framework/types/compiler.d.ts +15 -0
  74. package/create-fusee-go/internal/assets/embed/framework/types/component.d.ts +88 -0
  75. package/create-fusee-go/internal/assets/embed/framework/types/composable.d.ts +14 -0
  76. package/create-fusee-go/internal/assets/embed/framework/types/directives.d.ts +55 -0
  77. package/create-fusee-go/internal/assets/embed/framework/types/evaluator.d.ts +13 -0
  78. package/create-fusee-go/internal/assets/embed/framework/types/event-delegation.d.ts +32 -0
  79. package/create-fusee-go/internal/assets/embed/framework/types/index.d.ts +22 -0
  80. package/create-fusee-go/internal/assets/embed/framework/types/router.d.ts +32 -0
  81. package/create-fusee-go/internal/assets/embed/framework/types/signal.d.ts +79 -0
  82. package/create-fusee-go/internal/assets/embed/framework/types/store.d.ts +8 -0
  83. package/create-fusee-go/internal/assets/templates/Counter.tmpl +26 -0
  84. package/create-fusee-go/internal/assets/templates/component.tmpl +11 -0
  85. package/create-fusee-go/internal/assets/templates/components/Loading.tmpl +24 -0
  86. package/create-fusee-go/internal/assets/templates/index.html.tmpl +15 -0
  87. package/create-fusee-go/internal/assets/templates/main.tmpl +14 -0
  88. package/create-fusee-go/internal/assets/templates/package.json.tmpl +14 -0
  89. package/create-fusee-go/internal/assets/templates/page.tmpl +12 -0
  90. package/create-fusee-go/internal/assets/templates/pages/about.tmpl +20 -0
  91. package/create-fusee-go/internal/assets/templates/pages/index.tmpl +22 -0
  92. package/create-fusee-go/internal/assets/templates/pages/layout.tmpl +18 -0
  93. package/create-fusee-go/internal/assets/templates/tsconfig.json.tmpl +14 -0
  94. package/create-fusee-go/internal/assets/templates/vite.config.js.tmpl +18 -0
  95. package/create-fusee-go/main.go +7 -0
  96. package/framework/auto-import-preset.js +1 -1
  97. package/framework/core/compiler.js +252 -249
  98. package/framework/core/component.js +265 -260
  99. package/framework/core/directives.js +875 -786
  100. package/framework/core/evaluator.js +153 -70
  101. package/framework/core/event-delegation.js +33 -14
  102. package/framework/core/signal.js +412 -403
  103. package/framework/index.js +10 -11
  104. package/framework/router/file-router.js +177 -0
  105. package/framework/router/router.js +462 -80
  106. package/framework/types/event-delegation.d.ts +0 -58
  107. package/framework/types/router.d.ts +32 -15
  108. package/framework/types/signal.d.ts +1 -0
  109. package/index.html +638 -626
  110. package/package.json +5 -3
  111. package/tsconfig.json +1 -0
  112. /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,40 +1,13 @@
1
- // ─── Event Delegation Types ────────────────────────────────────────────────
2
-
3
- /**
4
- * Set of standard events that support delegation.
5
- * These events bubble and are commonly used in UI interactions.
6
- */
7
1
  export const DELEGATED_EVENTS: Set<string>
8
2
 
9
- /**
10
- * Check if an event type is delegated.
11
- * @param eventName - The event name to check (e.g., 'click', 'keydown')
12
- * @returns True if the event can be delegated
13
- */
14
3
  export function isDelegatedEvent(eventName: string): boolean
15
4
 
16
- /**
17
- * Options for registering a delegated event.
18
- */
19
5
  export interface DelegatedEventOptions {
20
- /** Event modifiers (e.g., 'prevent', 'stop', 'debounce', 'throttle') */
21
6
  modifiers?: string[]
22
- /** Component context for evaluating expressions */
23
7
  context?: Record<string, any>
24
- /** Expression string to evaluate */
25
8
  expr?: string
26
9
  }
27
10
 
28
- /**
29
- * Register a delegated event handler for an element.
30
- * Creates a single document-level listener if not already present.
31
- *
32
- * @param element - The DOM element to register
33
- * @param eventType - The event type (e.g., 'click', 'input')
34
- * @param handler - The event handler function
35
- * @param options - Optional configuration
36
- * @returns Cleanup function to remove the handler
37
- */
38
11
  export function registerDelegatedEvent(
39
12
  element: Element,
40
13
  eventType: string,
@@ -42,16 +15,6 @@ export function registerDelegatedEvent(
42
15
  options?: DelegatedEventOptions
43
16
  ): () => void
44
17
 
45
- /**
46
- * Create a wrapped event handler with modifier support.
47
- * Supports: prevent, stop, self, debounce, throttle, keyboard modifiers.
48
- *
49
- * @param handler - The original handler (function or expression)
50
- * @param modifiers - Array of modifier names
51
- * @param context - Component context for evaluating expressions
52
- * @param expr - Expression string (when handler is a string)
53
- * @returns Wrapped handler function
54
- */
55
18
  export function createEventHandler(
56
19
  handler: Function | string,
57
20
  modifiers: string[],
@@ -59,29 +22,8 @@ export function createEventHandler(
59
22
  expr: string
60
23
  ): (event: Event) => void
61
24
 
62
- /**
63
- * Event handler syntax preference.
64
- * Use @ prefix for delegated events (optimized, single document listener).
65
- * Use on: prefix for native events (individual element listeners, full event control).
66
- *
67
- * @example
68
- * // Delegated - efficient for many elements
69
- * <button @click="handleClick">Click</button>
70
- *
71
- * @example
72
- * // Native - full control including stopPropagation
73
- * <button on:click="handleClick">Click</button>
74
- *
75
- * @example
76
- * // With modifiers
77
- * <form @submit.prevent="handleSubmit">Submit</form>
78
- * <input @keydown.enter="handleEnter" />
79
- * <input @input.debounce="handleInput" />
80
- */
81
25
  export interface EventHandlerSyntax {
82
- /** Delegated event: @click, @input, @keydown, etc. */
83
26
  delegated: `@${string}`
84
- /** Native event: on:click, on:input, on:custom, etc. */
85
27
  native: `on:${string}`
86
28
  }
87
29
 
@@ -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