kiru 0.50.6 → 0.50.8
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/dist/router/context.d.ts +4 -0
- package/dist/router/context.d.ts.map +1 -1
- package/dist/router/context.js.map +1 -1
- package/dist/router/fileRouter.js +1 -1
- package/dist/router/fileRouter.js.map +1 -1
- package/dist/router/fileRouterController.d.ts +3 -4
- package/dist/router/fileRouterController.d.ts.map +1 -1
- package/dist/router/fileRouterController.js +33 -159
- package/dist/router/fileRouterController.js.map +1 -1
- package/dist/router/link.d.ts +9 -0
- package/dist/router/link.d.ts.map +1 -1
- package/dist/router/link.js +21 -3
- package/dist/router/link.js.map +1 -1
- package/dist/router/types.internal.d.ts +14 -0
- package/dist/router/types.internal.d.ts.map +1 -1
- package/dist/router/utils/index.d.ts +15 -0
- package/dist/router/utils/index.d.ts.map +1 -0
- package/dist/router/utils/index.js +148 -0
- package/dist/router/utils/index.js.map +1 -0
- package/dist/types.dom.d.ts +2 -1
- package/dist/types.dom.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/router/context.ts +8 -0
- package/src/router/fileRouter.ts +1 -1
- package/src/router/fileRouterController.ts +47 -216
- package/src/router/link.ts +39 -2
- package/src/router/types.internal.ts +16 -0
- package/src/router/utils/index.ts +206 -0
- package/src/types.dom.ts +5 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Signal } from "../signals/base.js"
|
|
2
2
|
import { flushSync } from "../scheduler.js"
|
|
3
3
|
import { __DEV__ } from "../env.js"
|
|
4
|
-
import { createElement } from "../element.js"
|
|
5
4
|
import { type FileRouterContextType } from "./context.js"
|
|
6
5
|
import { FileRouterDataLoadError } from "./errors.js"
|
|
7
6
|
import { fileRouterInstance } from "./globals.js"
|
|
@@ -14,26 +13,26 @@ import type {
|
|
|
14
13
|
RouterState,
|
|
15
14
|
} from "./types.js"
|
|
16
15
|
import type {
|
|
17
|
-
|
|
16
|
+
FormattedViteImportMap,
|
|
18
17
|
PageModule,
|
|
19
18
|
ViteImportMap,
|
|
20
19
|
} from "./types.internal.js"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
20
|
+
import {
|
|
21
|
+
formatViteImportMap,
|
|
22
|
+
matchLayouts,
|
|
23
|
+
matchRoute,
|
|
24
|
+
normalizePrefixPath,
|
|
25
|
+
parseQuery,
|
|
26
|
+
wrapWithLayouts,
|
|
27
|
+
} from "./utils/index.js"
|
|
30
28
|
|
|
31
29
|
export class FileRouterController {
|
|
30
|
+
public contextValue: FileRouterContextType
|
|
32
31
|
private enableTransitions: boolean
|
|
33
32
|
private pages: FormattedViteImportMap
|
|
34
33
|
private layouts: FormattedViteImportMap
|
|
35
34
|
private abortController: AbortController
|
|
36
|
-
private
|
|
35
|
+
private routeState: Signal<{
|
|
37
36
|
component: Kiru.FC<any>
|
|
38
37
|
config?: PageConfig
|
|
39
38
|
route: string
|
|
@@ -41,7 +40,6 @@ export class FileRouterController {
|
|
|
41
40
|
private currentPageProps: Signal<PageProps<PageConfig>>
|
|
42
41
|
private currentLayouts: Signal<Kiru.FC[]>
|
|
43
42
|
private state: RouterState
|
|
44
|
-
private contextValue: FileRouterContextType
|
|
45
43
|
private cleanups: (() => void)[] = []
|
|
46
44
|
private filePathToPageRoute?: Map<
|
|
47
45
|
string,
|
|
@@ -55,7 +53,7 @@ export class FileRouterController {
|
|
|
55
53
|
this.pages = {}
|
|
56
54
|
this.layouts = {}
|
|
57
55
|
this.abortController = new AbortController()
|
|
58
|
-
this.
|
|
56
|
+
this.routeState = new Signal(null)
|
|
59
57
|
this.currentPageProps = new Signal({})
|
|
60
58
|
this.currentLayouts = new Signal([])
|
|
61
59
|
this.state = {
|
|
@@ -64,18 +62,17 @@ export class FileRouterController {
|
|
|
64
62
|
query: {},
|
|
65
63
|
signal: this.abortController.signal,
|
|
66
64
|
}
|
|
67
|
-
|
|
68
65
|
const __this = this
|
|
69
66
|
this.contextValue = {
|
|
70
67
|
get state() {
|
|
71
68
|
return __this.state
|
|
72
69
|
},
|
|
73
70
|
navigate: this.navigate.bind(this),
|
|
74
|
-
|
|
71
|
+
prefetchRouteModules: this.prefetchRouteModules.bind(this),
|
|
75
72
|
reload: (options?: { transition?: boolean }) =>
|
|
76
73
|
this.loadRoute(void 0, void 0, options?.transition),
|
|
74
|
+
setQuery: this.setQuery.bind(this),
|
|
77
75
|
}
|
|
78
|
-
|
|
79
76
|
if (__DEV__) {
|
|
80
77
|
this.filePathToPageRoute = new Map()
|
|
81
78
|
this.pageRouteToConfig = new Map()
|
|
@@ -119,7 +116,7 @@ export class FileRouterController {
|
|
|
119
116
|
this.filePathToPageRoute?.set(fp, { route, config })
|
|
120
117
|
return
|
|
121
118
|
}
|
|
122
|
-
const curPage = this.
|
|
119
|
+
const curPage = this.routeState.value
|
|
123
120
|
if (curPage?.route === existing.route && config.loader) {
|
|
124
121
|
const p = this.currentPageProps.value
|
|
125
122
|
let transition = this.enableTransitions
|
|
@@ -142,104 +139,20 @@ export class FileRouterController {
|
|
|
142
139
|
this.pageRouteToConfig?.set(existing.route, config)
|
|
143
140
|
}
|
|
144
141
|
|
|
145
|
-
public getContextValue() {
|
|
146
|
-
return this.contextValue
|
|
147
|
-
}
|
|
148
|
-
|
|
149
142
|
public getChildren() {
|
|
150
|
-
const page = this.
|
|
151
|
-
|
|
152
|
-
layouts = this.currentLayouts.value
|
|
143
|
+
const page = this.routeState.value
|
|
144
|
+
if (!page) return null
|
|
153
145
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return layouts.reduceRight(
|
|
157
|
-
(children, Layout) => createElement(Layout, { children }),
|
|
158
|
-
createElement(page.component, props)
|
|
159
|
-
)
|
|
160
|
-
}
|
|
146
|
+
const props = this.currentPageProps.value,
|
|
147
|
+
layouts = this.currentLayouts.value
|
|
161
148
|
|
|
162
|
-
return
|
|
149
|
+
return wrapWithLayouts(layouts, page.component, props)
|
|
163
150
|
}
|
|
164
151
|
|
|
165
152
|
public dispose() {
|
|
166
153
|
this.cleanups.forEach((cleanup) => cleanup())
|
|
167
154
|
}
|
|
168
155
|
|
|
169
|
-
private matchRoute(pathSegments: string[]) {
|
|
170
|
-
const matches: Array<{
|
|
171
|
-
route: string
|
|
172
|
-
pageEntry: FormattedViteImportMap[string]
|
|
173
|
-
params: Record<string, string>
|
|
174
|
-
routeSegments: string[]
|
|
175
|
-
}> = []
|
|
176
|
-
|
|
177
|
-
// Find all matching routes
|
|
178
|
-
outer: for (const [route, pageEntry] of Object.entries(this.pages)) {
|
|
179
|
-
const routeSegments = pageEntry.segments
|
|
180
|
-
const pathMatchingSegments = routeSegments.filter(
|
|
181
|
-
(seg) => !seg.startsWith("(") && !seg.endsWith(")")
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
const params: Record<string, string> = {}
|
|
185
|
-
let hasCatchall = false
|
|
186
|
-
|
|
187
|
-
// Check if route matches
|
|
188
|
-
for (
|
|
189
|
-
let i = 0;
|
|
190
|
-
i < pathMatchingSegments.length && i < pathSegments.length;
|
|
191
|
-
i++
|
|
192
|
-
) {
|
|
193
|
-
const routeSeg = pathMatchingSegments[i]
|
|
194
|
-
|
|
195
|
-
if (routeSeg.startsWith(":")) {
|
|
196
|
-
const key = routeSeg.slice(1)
|
|
197
|
-
|
|
198
|
-
if (routeSeg.endsWith("*")) {
|
|
199
|
-
// Catchall route - matches remaining segments
|
|
200
|
-
hasCatchall = true
|
|
201
|
-
const catchallKey = key.slice(0, -1) // Remove the *
|
|
202
|
-
params[catchallKey] = pathSegments.slice(i).join("/")
|
|
203
|
-
break
|
|
204
|
-
} else {
|
|
205
|
-
// Regular dynamic segment
|
|
206
|
-
if (i >= pathSegments.length) {
|
|
207
|
-
continue outer
|
|
208
|
-
}
|
|
209
|
-
params[key] = pathSegments[i]
|
|
210
|
-
}
|
|
211
|
-
} else {
|
|
212
|
-
// Static segment
|
|
213
|
-
if (routeSeg !== pathSegments[i]) {
|
|
214
|
-
continue outer
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// For non-catchall routes, ensure exact length match
|
|
220
|
-
if (!hasCatchall && pathMatchingSegments.length !== pathSegments.length) {
|
|
221
|
-
continue
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
matches.push({
|
|
225
|
-
route,
|
|
226
|
-
pageEntry,
|
|
227
|
-
params,
|
|
228
|
-
routeSegments,
|
|
229
|
-
})
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Sort by specificity (highest first) and return the best match
|
|
233
|
-
if (matches.length === 0) {
|
|
234
|
-
return null
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
matches.sort((a, b) => b.pageEntry.specificity - a.pageEntry.specificity)
|
|
238
|
-
const bestMatch = matches[0]
|
|
239
|
-
|
|
240
|
-
return bestMatch
|
|
241
|
-
}
|
|
242
|
-
|
|
243
156
|
private async loadRoute(
|
|
244
157
|
path: string = window.location.pathname,
|
|
245
158
|
props: PageProps<PageConfig> = {},
|
|
@@ -250,10 +163,10 @@ export class FileRouterController {
|
|
|
250
163
|
|
|
251
164
|
try {
|
|
252
165
|
const pathSegments = path.split("/").filter(Boolean)
|
|
253
|
-
const routeMatch = this.
|
|
166
|
+
const routeMatch = matchRoute(this.pages, pathSegments)
|
|
254
167
|
|
|
255
168
|
if (!routeMatch) {
|
|
256
|
-
const _404 = this.
|
|
169
|
+
const _404 = matchRoute(this.pages, ["404"])
|
|
257
170
|
if (!_404) {
|
|
258
171
|
if (__DEV__) {
|
|
259
172
|
console.error(
|
|
@@ -275,23 +188,16 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
|
|
|
275
188
|
this.currentRoute = route
|
|
276
189
|
const pagePromise = pageEntry.load()
|
|
277
190
|
|
|
278
|
-
const layoutPromises =
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (!layout) {
|
|
283
|
-
return acc
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return [...acc, layout.load()]
|
|
287
|
-
}, [] as Promise<DefaultComponentModule>[])
|
|
191
|
+
const layoutPromises = matchLayouts(this.layouts, routeSegments).map(
|
|
192
|
+
(layoutEntry) => layoutEntry.load()
|
|
193
|
+
)
|
|
288
194
|
|
|
289
|
-
const query = parseQuery(window.location.search)
|
|
290
195
|
const [page, ...layouts] = await Promise.all([
|
|
291
196
|
pagePromise,
|
|
292
197
|
...layoutPromises,
|
|
293
198
|
])
|
|
294
199
|
|
|
200
|
+
const query = parseQuery(window.location.search)
|
|
295
201
|
this.currentRoute = null
|
|
296
202
|
if (signal.aborted) return
|
|
297
203
|
|
|
@@ -322,7 +228,7 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
|
|
|
322
228
|
|
|
323
229
|
this.state = routerState
|
|
324
230
|
handleStateTransition(signal, enableTransition, () => {
|
|
325
|
-
this.
|
|
231
|
+
this.routeState.value = {
|
|
326
232
|
component: page.default,
|
|
327
233
|
config,
|
|
328
234
|
route: "/" + routeSegments.join("/"),
|
|
@@ -334,7 +240,7 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
|
|
|
334
240
|
})
|
|
335
241
|
} catch (error) {
|
|
336
242
|
console.error("[kiru/router]: Failed to load route component:", error)
|
|
337
|
-
this.
|
|
243
|
+
this.routeState.value = null
|
|
338
244
|
}
|
|
339
245
|
}
|
|
340
246
|
|
|
@@ -382,10 +288,28 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
|
|
|
382
288
|
) {
|
|
383
289
|
const f = options?.replace ? "replaceState" : "pushState"
|
|
384
290
|
window.history[f]({}, "", path)
|
|
385
|
-
window.dispatchEvent(new PopStateEvent("popstate", { state: {} }))
|
|
386
291
|
return this.loadRoute(path, options?.props, options?.transition)
|
|
387
292
|
}
|
|
388
293
|
|
|
294
|
+
private async prefetchRouteModules(path: string) {
|
|
295
|
+
try {
|
|
296
|
+
const routeMatch = matchRoute(this.pages, path.split("/").filter(Boolean))
|
|
297
|
+
if (!routeMatch) {
|
|
298
|
+
throw new Error(`No route defined (path: ${path}).`)
|
|
299
|
+
}
|
|
300
|
+
const { pageEntry, route } = routeMatch
|
|
301
|
+
this.currentRoute = route
|
|
302
|
+
const pagePromise = pageEntry.load()
|
|
303
|
+
const layoutPromises = matchLayouts(this.layouts, route.split("/")).map(
|
|
304
|
+
(layoutEntry) => layoutEntry.load()
|
|
305
|
+
)
|
|
306
|
+
await Promise.all([pagePromise, ...layoutPromises])
|
|
307
|
+
this.currentRoute = null
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.error("[kiru/router]: Failed to prefetch route:", error)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
389
313
|
private setQuery(query: RouteQuery) {
|
|
390
314
|
const queryString = buildQueryString(query)
|
|
391
315
|
const newUrl = `${this.state.path}${queryString ? `?${queryString}` : ""}`
|
|
@@ -395,28 +319,6 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
|
|
|
395
319
|
}
|
|
396
320
|
}
|
|
397
321
|
|
|
398
|
-
function parseQuery(
|
|
399
|
-
search: string
|
|
400
|
-
): Record<string, string | string[] | undefined> {
|
|
401
|
-
const params = new URLSearchParams(search)
|
|
402
|
-
const query: Record<string, string | string[] | undefined> = {}
|
|
403
|
-
|
|
404
|
-
for (const [key, value] of params.entries()) {
|
|
405
|
-
if (query[key]) {
|
|
406
|
-
// Convert to array if multiple values
|
|
407
|
-
if (Array.isArray(query[key])) {
|
|
408
|
-
;(query[key] as string[]).push(value)
|
|
409
|
-
} else {
|
|
410
|
-
query[key] = [query[key] as string, value]
|
|
411
|
-
}
|
|
412
|
-
} else {
|
|
413
|
-
query[key] = value
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
return query
|
|
418
|
-
}
|
|
419
|
-
|
|
420
322
|
function buildQueryString(
|
|
421
323
|
query: Record<string, string | string[] | undefined>
|
|
422
324
|
): string {
|
|
@@ -435,77 +337,6 @@ function buildQueryString(
|
|
|
435
337
|
return params.toString()
|
|
436
338
|
}
|
|
437
339
|
|
|
438
|
-
function formatViteImportMap(
|
|
439
|
-
map: ViteImportMap,
|
|
440
|
-
dir: string,
|
|
441
|
-
baseUrl: string
|
|
442
|
-
): FormattedViteImportMap {
|
|
443
|
-
return Object.keys(map).reduce<FormattedViteImportMap>((acc, key) => {
|
|
444
|
-
const dirIndex = key.indexOf(dir)
|
|
445
|
-
if (dirIndex === -1) {
|
|
446
|
-
return acc
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
let specificity = 0
|
|
450
|
-
let k = key.slice(dirIndex + dir.length)
|
|
451
|
-
while (k.startsWith("/")) {
|
|
452
|
-
k = k.slice(1)
|
|
453
|
-
}
|
|
454
|
-
const segments: string[] = []
|
|
455
|
-
const parts = k.split("/").slice(0, -1)
|
|
456
|
-
|
|
457
|
-
for (let i = 0; i < parts.length; i++) {
|
|
458
|
-
const part = parts[i]
|
|
459
|
-
if (part.startsWith("[...") && part.endsWith("]")) {
|
|
460
|
-
if (i !== parts.length - 1) {
|
|
461
|
-
throw new Error(
|
|
462
|
-
`[kiru/router]: Catchall must be the folder name. Got "${key}"`
|
|
463
|
-
)
|
|
464
|
-
}
|
|
465
|
-
segments.push(`:${part.slice(4, -1)}*`)
|
|
466
|
-
specificity += 1
|
|
467
|
-
break
|
|
468
|
-
}
|
|
469
|
-
if (part.startsWith("[") && part.endsWith("]")) {
|
|
470
|
-
segments.push(`:${part.slice(1, -1)}`)
|
|
471
|
-
specificity += 10
|
|
472
|
-
continue
|
|
473
|
-
}
|
|
474
|
-
specificity += 100
|
|
475
|
-
segments.push(part)
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const value: FormattedViteImportMap[string] = {
|
|
479
|
-
load: map[key],
|
|
480
|
-
specificity,
|
|
481
|
-
segments,
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (__DEV__) {
|
|
485
|
-
value.filePath = key
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return {
|
|
489
|
-
...acc,
|
|
490
|
-
[baseUrl + segments.join("/")]: value,
|
|
491
|
-
}
|
|
492
|
-
}, {})
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function normalizePrefixPath(path: string) {
|
|
496
|
-
while (path.startsWith(".")) {
|
|
497
|
-
path = path.slice(1)
|
|
498
|
-
}
|
|
499
|
-
path = `/${path}/`
|
|
500
|
-
while (path.startsWith("//")) {
|
|
501
|
-
path = path.slice(1)
|
|
502
|
-
}
|
|
503
|
-
while (path.endsWith("//")) {
|
|
504
|
-
path = path.slice(0, -1)
|
|
505
|
-
}
|
|
506
|
-
return path
|
|
507
|
-
}
|
|
508
|
-
|
|
509
340
|
function handleStateTransition(
|
|
510
341
|
signal: AbortSignal,
|
|
511
342
|
enableTransition: boolean,
|
package/src/router/link.ts
CHANGED
|
@@ -6,26 +6,57 @@ import { useFileRouter } from "./context.js"
|
|
|
6
6
|
export interface LinkProps extends ElementProps<"a"> {
|
|
7
7
|
/**
|
|
8
8
|
* The path to navigate to
|
|
9
|
+
* @example
|
|
10
|
+
* <Link to="/about">About</Link>
|
|
9
11
|
*/
|
|
10
12
|
to: string
|
|
11
13
|
/**
|
|
12
14
|
* Whether to replace the current history entry
|
|
15
|
+
* @default false
|
|
13
16
|
*/
|
|
14
17
|
replace?: boolean
|
|
15
18
|
/**
|
|
16
19
|
* Whether to trigger a view transition
|
|
20
|
+
* @default false (overrides transition from config)
|
|
17
21
|
*/
|
|
18
22
|
transition?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* Whether to prefetch the route's javascript dependencies when hovered or focused
|
|
25
|
+
* @default true
|
|
26
|
+
*/
|
|
27
|
+
prefetchJs?: boolean
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
export const Link: Kiru.FC<LinkProps> = ({
|
|
22
31
|
to,
|
|
23
32
|
onclick,
|
|
33
|
+
onmouseover,
|
|
34
|
+
onfocus,
|
|
24
35
|
replace,
|
|
25
36
|
transition,
|
|
37
|
+
prefetchJs,
|
|
26
38
|
...props
|
|
27
39
|
}) => {
|
|
28
|
-
const { navigate } = useFileRouter()
|
|
40
|
+
const { navigate, prefetchRouteModules } = useFileRouter()
|
|
41
|
+
|
|
42
|
+
const handleMouseOver = useCallback(
|
|
43
|
+
(e: Kiru.MouseEvent<HTMLAnchorElement>) => {
|
|
44
|
+
if (prefetchJs !== false) {
|
|
45
|
+
prefetchRouteModules(to)
|
|
46
|
+
}
|
|
47
|
+
onmouseover?.(e)
|
|
48
|
+
},
|
|
49
|
+
[onmouseover]
|
|
50
|
+
)
|
|
51
|
+
const handleFocus = useCallback(
|
|
52
|
+
(e: Kiru.FocusEvent<HTMLAnchorElement>) => {
|
|
53
|
+
if (prefetchJs !== false) {
|
|
54
|
+
prefetchRouteModules(to)
|
|
55
|
+
}
|
|
56
|
+
onfocus?.(e)
|
|
57
|
+
},
|
|
58
|
+
[onfocus]
|
|
59
|
+
)
|
|
29
60
|
|
|
30
61
|
const handleClick = useCallback(
|
|
31
62
|
(e: Kiru.MouseEvent<HTMLAnchorElement>) => {
|
|
@@ -37,5 +68,11 @@ export const Link: Kiru.FC<LinkProps> = ({
|
|
|
37
68
|
[to, navigate, onclick, replace]
|
|
38
69
|
)
|
|
39
70
|
|
|
40
|
-
return createElement("a", {
|
|
71
|
+
return createElement("a", {
|
|
72
|
+
href: to,
|
|
73
|
+
onclick: handleClick,
|
|
74
|
+
onmouseover: handleMouseOver,
|
|
75
|
+
onfocus: handleFocus,
|
|
76
|
+
...props,
|
|
77
|
+
})
|
|
41
78
|
}
|
|
@@ -12,3 +12,19 @@ export interface PageModule {
|
|
|
12
12
|
export interface ViteImportMap {
|
|
13
13
|
[fp: string]: () => Promise<DefaultComponentModule>
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
export interface FormattedViteImportMap {
|
|
17
|
+
[key: string]: {
|
|
18
|
+
load: () => Promise<DefaultComponentModule>
|
|
19
|
+
specificity: number
|
|
20
|
+
segments: string[]
|
|
21
|
+
filePath?: string
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RouteMatch {
|
|
26
|
+
route: string
|
|
27
|
+
pageEntry: FormattedViteImportMap[string]
|
|
28
|
+
params: Record<string, string>
|
|
29
|
+
routeSegments: string[]
|
|
30
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createElement } from "../../element.js"
|
|
2
|
+
import { __DEV__ } from "../../env.js"
|
|
3
|
+
import type {
|
|
4
|
+
FormattedViteImportMap,
|
|
5
|
+
RouteMatch,
|
|
6
|
+
ViteImportMap,
|
|
7
|
+
} from "../types.internal"
|
|
8
|
+
import { PageConfig, PageProps } from "../types.js"
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
formatViteImportMap,
|
|
12
|
+
matchRoute,
|
|
13
|
+
matchLayouts,
|
|
14
|
+
normalizePrefixPath,
|
|
15
|
+
parseQuery,
|
|
16
|
+
wrapWithLayouts,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatViteImportMap(
|
|
20
|
+
map: ViteImportMap,
|
|
21
|
+
dir: string,
|
|
22
|
+
baseUrl: string
|
|
23
|
+
): FormattedViteImportMap {
|
|
24
|
+
return Object.keys(map).reduce<FormattedViteImportMap>((acc, key) => {
|
|
25
|
+
const dirIndex = key.indexOf(dir)
|
|
26
|
+
if (dirIndex === -1) {
|
|
27
|
+
console.warn(`[kiru/router]: File "${key}" does not start with "${dir}".`)
|
|
28
|
+
return acc
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let specificity = 0
|
|
32
|
+
let k = key.slice(dirIndex + dir.length)
|
|
33
|
+
while (k.startsWith("/")) {
|
|
34
|
+
k = k.slice(1)
|
|
35
|
+
}
|
|
36
|
+
const segments: string[] = []
|
|
37
|
+
const parts = k.split("/").slice(0, -1)
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < parts.length; i++) {
|
|
40
|
+
const part = parts[i]
|
|
41
|
+
if (part.startsWith("[...") && part.endsWith("]")) {
|
|
42
|
+
if (i !== parts.length - 1) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`[kiru/router]: Catchall must be the folder name. Got "${key}"`
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
segments.push(`:${part.slice(4, -1)}*`)
|
|
48
|
+
specificity += 1
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
if (part.startsWith("[") && part.endsWith("]")) {
|
|
52
|
+
segments.push(`:${part.slice(1, -1)}`)
|
|
53
|
+
specificity += 10
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
specificity += 100
|
|
57
|
+
segments.push(part)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const value: FormattedViteImportMap[string] = {
|
|
61
|
+
load: map[key],
|
|
62
|
+
specificity,
|
|
63
|
+
segments,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (__DEV__) {
|
|
67
|
+
value.filePath = key
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...acc,
|
|
72
|
+
[baseUrl + segments.join("/")]: value,
|
|
73
|
+
}
|
|
74
|
+
}, {})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function matchRoute(
|
|
78
|
+
pages: FormattedViteImportMap,
|
|
79
|
+
pathSegments: string[]
|
|
80
|
+
): RouteMatch | null {
|
|
81
|
+
const matches: RouteMatch[] = []
|
|
82
|
+
outer: for (const [route, pageEntry] of Object.entries(pages)) {
|
|
83
|
+
const routeSegments = pageEntry.segments
|
|
84
|
+
const pathMatchingSegments = routeSegments.filter(
|
|
85
|
+
(seg) => !seg.startsWith("(") && !seg.endsWith(")")
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const params: Record<string, string> = {}
|
|
89
|
+
let hasCatchall = false
|
|
90
|
+
|
|
91
|
+
// Check if route matches
|
|
92
|
+
for (
|
|
93
|
+
let i = 0;
|
|
94
|
+
i < pathMatchingSegments.length && i < pathSegments.length;
|
|
95
|
+
i++
|
|
96
|
+
) {
|
|
97
|
+
const routeSeg = pathMatchingSegments[i]
|
|
98
|
+
|
|
99
|
+
if (routeSeg.startsWith(":")) {
|
|
100
|
+
const key = routeSeg.slice(1)
|
|
101
|
+
|
|
102
|
+
if (routeSeg.endsWith("*")) {
|
|
103
|
+
// Catchall route - matches remaining segments
|
|
104
|
+
hasCatchall = true
|
|
105
|
+
const catchallKey = key.slice(0, -1) // Remove the *
|
|
106
|
+
params[catchallKey] = pathSegments.slice(i).join("/")
|
|
107
|
+
break
|
|
108
|
+
} else {
|
|
109
|
+
// Regular dynamic segment
|
|
110
|
+
if (i >= pathSegments.length) {
|
|
111
|
+
continue outer
|
|
112
|
+
}
|
|
113
|
+
params[key] = pathSegments[i]
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
// Static segment
|
|
117
|
+
if (routeSeg !== pathSegments[i]) {
|
|
118
|
+
continue outer
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// For non-catchall routes, ensure exact length match
|
|
124
|
+
if (!hasCatchall && pathMatchingSegments.length !== pathSegments.length) {
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
matches.push({
|
|
129
|
+
route,
|
|
130
|
+
pageEntry,
|
|
131
|
+
params,
|
|
132
|
+
routeSegments,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Sort by specificity (highest first) and return the best match
|
|
137
|
+
if (matches.length === 0) {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
matches.sort((a, b) => b.pageEntry.specificity - a.pageEntry.specificity)
|
|
142
|
+
return matches[0] || null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function matchLayouts(
|
|
146
|
+
layouts: FormattedViteImportMap,
|
|
147
|
+
routeSegments: string[]
|
|
148
|
+
) {
|
|
149
|
+
return ["/", ...routeSegments].reduce((acc, _, i) => {
|
|
150
|
+
const layoutPath = "/" + routeSegments.slice(0, i).join("/")
|
|
151
|
+
const layout = layouts[layoutPath]
|
|
152
|
+
|
|
153
|
+
if (!layout) {
|
|
154
|
+
return acc
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return [...acc, layout]
|
|
158
|
+
}, [] as FormattedViteImportMap[string][])
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizePrefixPath(path: string) {
|
|
162
|
+
while (path.startsWith(".")) {
|
|
163
|
+
path = path.slice(1)
|
|
164
|
+
}
|
|
165
|
+
path = `/${path}/`
|
|
166
|
+
while (path.startsWith("//")) {
|
|
167
|
+
path = path.slice(1)
|
|
168
|
+
}
|
|
169
|
+
while (path.endsWith("//")) {
|
|
170
|
+
path = path.slice(0, -1)
|
|
171
|
+
}
|
|
172
|
+
return path
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseQuery(
|
|
176
|
+
search: string
|
|
177
|
+
): Record<string, string | string[] | undefined> {
|
|
178
|
+
const params = new URLSearchParams(search)
|
|
179
|
+
const query: Record<string, string | string[] | undefined> = {}
|
|
180
|
+
|
|
181
|
+
for (const [key, value] of params.entries()) {
|
|
182
|
+
if (query[key]) {
|
|
183
|
+
// Convert to array if multiple values
|
|
184
|
+
if (Array.isArray(query[key])) {
|
|
185
|
+
;(query[key] as string[]).push(value)
|
|
186
|
+
} else {
|
|
187
|
+
query[key] = [query[key] as string, value]
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
query[key] = value
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return query
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function wrapWithLayouts(
|
|
198
|
+
layouts: Kiru.FC[],
|
|
199
|
+
page: Kiru.FC,
|
|
200
|
+
props: PageProps<PageConfig>
|
|
201
|
+
) {
|
|
202
|
+
return layouts.reduceRight(
|
|
203
|
+
(children, Layout) => createElement(Layout, { children }),
|
|
204
|
+
createElement(page, props)
|
|
205
|
+
)
|
|
206
|
+
}
|
package/src/types.dom.ts
CHANGED
|
@@ -365,7 +365,7 @@ declare global {
|
|
|
365
365
|
bivarianceHack(event: E): void
|
|
366
366
|
}["bivarianceHack"]
|
|
367
367
|
|
|
368
|
-
interface
|
|
368
|
+
interface BaseEvent<T extends Element = Element>
|
|
369
369
|
extends DOMEvent<Event, T> {}
|
|
370
370
|
|
|
371
371
|
interface AnimationEvent<T extends Element = Element>
|
|
@@ -413,6 +413,10 @@ declare global {
|
|
|
413
413
|
interface WheelEvent<T extends Element = Element>
|
|
414
414
|
extends DOMEvent<NativeWheelEvent, T> {}
|
|
415
415
|
|
|
416
|
+
type BaseEventHandler<T extends Element = Element> = EventHandler<
|
|
417
|
+
BaseEvent<T>
|
|
418
|
+
>
|
|
419
|
+
|
|
416
420
|
type ClipboardEventHandler<T extends Element = Element> = EventHandler<
|
|
417
421
|
ClipboardEvent<T>
|
|
418
422
|
>
|