kiru 0.50.6 → 0.50.7

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.
@@ -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
+ }