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.
- package/README.md +29 -13
- package/app/benchmark/benchmark.js +633 -0
- package/app/benchmark/index.html +102 -0
- package/app/benchmark/styles.css +249 -0
- package/app/components/Counter.js +17 -7
- package/app/components/CounterSection.js +3 -4
- package/app/main.js +29 -62
- package/app/main.ts +72 -63
- package/app/pages/404.js +16 -0
- package/app/pages/[...all].js +35 -0
- package/app/pages/[...all].ts +35 -0
- package/app/pages/_layout.js +26 -0
- package/app/pages/_layout.ts +26 -0
- package/app/pages/about.js +23 -0
- package/app/pages/about.ts +29 -0
- package/app/pages/composables.js +158 -0
- package/app/pages/composables.ts +158 -0
- package/app/pages/dynamic.js +40 -0
- package/app/pages/dynamic.ts +40 -0
- package/app/pages/index.js +117 -0
- package/app/pages/index.ts +78 -0
- package/app/pages/login.ts +21 -0
- package/app/pages/objects/[id].js +56 -0
- package/app/pages/objects/[id].ts +56 -0
- package/app/pages/posts/[category]/[slug].js +56 -0
- package/app/pages/posts/[category]/[slug].ts +56 -0
- package/app/pages/router-example.js +56 -0
- package/app/pages/router-example.ts +56 -0
- package/app/pages/test-for.js +65 -0
- package/app/pages/test-for.ts +80 -0
- package/app/pages/users/[id].js +24 -0
- package/app/pages/users/[id].ts +46 -0
- package/app/pages/users/index.js +22 -0
- package/app/pages/users/index.ts +29 -0
- package/app/pages/users.js +43 -0
- package/app/pages/users.ts +78 -0
- package/app/routes/About.js +1 -1
- package/app/routes/About.ts +1 -1
- package/app/routes/AuthRoutes.ts +41 -0
- package/app/routes/Home.js +9 -0
- package/app/routes/NotFound.js +16 -0
- package/app/routes/NotFound.ts +16 -0
- package/app/routes/RouterExample.js +56 -0
- package/app/routes/RouterExample.ts +56 -0
- package/app/routes/WildcardDemo.js +35 -0
- package/app/routes/WildcardDemo.ts +35 -0
- package/auto-imports.d.ts +1 -0
- package/bin/create-fusee-linux +0 -0
- package/bin/create-fusee-mac-arm64 +0 -0
- package/bin/create-fusee-mac-intel +0 -0
- package/bin/create-fusee-win.exe +0 -0
- package/bin/create-fusee.exe +0 -0
- package/bin/index.js +38 -0
- package/create-fusee-go/cmd/generate.go +72 -0
- package/create-fusee-go/cmd/init.go +38 -0
- package/create-fusee-go/cmd/root.go +121 -0
- package/create-fusee-go/go.mod +10 -0
- package/create-fusee-go/go.sum +10 -0
- package/create-fusee-go/internal/assets/assets.go +78 -0
- package/create-fusee-go/internal/assets/embed/framework/auto-import-preset.js +9 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler.js +253 -0
- package/create-fusee-go/internal/assets/embed/framework/core/component.js +266 -0
- package/create-fusee-go/internal/assets/embed/framework/core/composable.js +27 -0
- package/create-fusee-go/internal/assets/embed/framework/core/directives.js +875 -0
- package/create-fusee-go/internal/assets/embed/framework/core/evaluator.js +153 -0
- package/create-fusee-go/internal/assets/embed/framework/core/event-delegation.js +191 -0
- package/create-fusee-go/internal/assets/embed/framework/core/signal.js +412 -0
- package/create-fusee-go/internal/assets/embed/framework/core/store.js +81 -0
- package/create-fusee-go/internal/assets/embed/framework/index.d.ts +3 -0
- package/create-fusee-go/internal/assets/embed/framework/index.js +10 -0
- package/create-fusee-go/internal/assets/embed/framework/router/file-router.js +177 -0
- package/create-fusee-go/internal/assets/embed/framework/router/router.js +462 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler.d.ts +15 -0
- package/create-fusee-go/internal/assets/embed/framework/types/component.d.ts +88 -0
- package/create-fusee-go/internal/assets/embed/framework/types/composable.d.ts +14 -0
- package/create-fusee-go/internal/assets/embed/framework/types/directives.d.ts +55 -0
- package/create-fusee-go/internal/assets/embed/framework/types/evaluator.d.ts +13 -0
- package/create-fusee-go/internal/assets/embed/framework/types/event-delegation.d.ts +32 -0
- package/create-fusee-go/internal/assets/embed/framework/types/index.d.ts +22 -0
- package/create-fusee-go/internal/assets/embed/framework/types/router.d.ts +32 -0
- package/create-fusee-go/internal/assets/embed/framework/types/signal.d.ts +79 -0
- package/create-fusee-go/internal/assets/embed/framework/types/store.d.ts +8 -0
- package/create-fusee-go/internal/assets/templates/Counter.tmpl +26 -0
- package/create-fusee-go/internal/assets/templates/component.tmpl +11 -0
- package/create-fusee-go/internal/assets/templates/components/Loading.tmpl +24 -0
- package/create-fusee-go/internal/assets/templates/index.html.tmpl +15 -0
- package/create-fusee-go/internal/assets/templates/main.tmpl +14 -0
- package/create-fusee-go/internal/assets/templates/package.json.tmpl +14 -0
- package/create-fusee-go/internal/assets/templates/page.tmpl +12 -0
- package/create-fusee-go/internal/assets/templates/pages/about.tmpl +20 -0
- package/create-fusee-go/internal/assets/templates/pages/index.tmpl +22 -0
- package/create-fusee-go/internal/assets/templates/pages/layout.tmpl +18 -0
- package/create-fusee-go/internal/assets/templates/tsconfig.json.tmpl +14 -0
- package/create-fusee-go/internal/assets/templates/vite.config.js.tmpl +18 -0
- package/create-fusee-go/main.go +7 -0
- package/framework/auto-import-preset.js +1 -1
- package/framework/core/compiler.js +252 -249
- package/framework/core/component.js +265 -260
- package/framework/core/directives.js +875 -786
- package/framework/core/evaluator.js +153 -70
- package/framework/core/event-delegation.js +33 -14
- package/framework/core/signal.js +412 -403
- package/framework/index.js +10 -11
- package/framework/router/file-router.js +177 -0
- package/framework/router/router.js +462 -80
- package/framework/types/event-delegation.d.ts +0 -58
- package/framework/types/router.d.ts +32 -15
- package/framework/types/signal.d.ts +1 -0
- package/index.html +638 -626
- package/package.json +5 -3
- package/tsconfig.json +1 -0
- /package/{framework → create-fusee-go/internal/assets/embed/framework}/bin/create-fusee.js +0 -0
|
@@ -1,80 +1,462 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export
|
|
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
|
|