kiru 0.50.7 → 0.51.0-preview.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 (73) hide show
  1. package/dist/constants.d.ts.map +1 -1
  2. package/dist/constants.js +1 -0
  3. package/dist/constants.js.map +1 -1
  4. package/dist/reconciler.d.ts.map +1 -1
  5. package/dist/reconciler.js +4 -12
  6. package/dist/reconciler.js.map +1 -1
  7. package/dist/router/client/index.d.ts +10 -0
  8. package/dist/router/client/index.d.ts.map +1 -0
  9. package/dist/router/client/index.js +73 -0
  10. package/dist/router/client/index.js.map +1 -0
  11. package/dist/router/dev/index.d.ts +2 -0
  12. package/dist/router/dev/index.d.ts.map +1 -0
  13. package/dist/router/dev/index.js +46 -0
  14. package/dist/router/dev/index.js.map +1 -0
  15. package/dist/router/fileRouter.d.ts +1 -1
  16. package/dist/router/fileRouter.d.ts.map +1 -1
  17. package/dist/router/fileRouter.js +10 -2
  18. package/dist/router/fileRouter.js.map +1 -1
  19. package/dist/router/fileRouterController.d.ts +4 -4
  20. package/dist/router/fileRouterController.d.ts.map +1 -1
  21. package/dist/router/fileRouterController.js +118 -47
  22. package/dist/router/fileRouterController.js.map +1 -1
  23. package/dist/router/globals.d.ts +3 -0
  24. package/dist/router/globals.d.ts.map +1 -1
  25. package/dist/router/globals.js +3 -0
  26. package/dist/router/globals.js.map +1 -1
  27. package/dist/router/head.d.ts +38 -0
  28. package/dist/router/head.d.ts.map +1 -0
  29. package/dist/router/head.js +63 -0
  30. package/dist/router/head.js.map +1 -0
  31. package/dist/router/index.d.ts +5 -0
  32. package/dist/router/index.d.ts.map +1 -1
  33. package/dist/router/index.js +5 -0
  34. package/dist/router/index.js.map +1 -1
  35. package/dist/router/pageConfig.d.ts +1 -1
  36. package/dist/router/pageConfig.d.ts.map +1 -1
  37. package/dist/router/pageConfig.js +1 -1
  38. package/dist/router/pageConfig.js.map +1 -1
  39. package/dist/router/server/index.d.ts +17 -0
  40. package/dist/router/server/index.d.ts.map +1 -0
  41. package/dist/router/server/index.js +160 -0
  42. package/dist/router/server/index.js.map +1 -0
  43. package/dist/router/types.d.ts +35 -11
  44. package/dist/router/types.d.ts.map +1 -1
  45. package/dist/router/types.internal.d.ts +6 -2
  46. package/dist/router/types.internal.d.ts.map +1 -1
  47. package/dist/router/utils/index.d.ts +4 -4
  48. package/dist/router/utils/index.d.ts.map +1 -1
  49. package/dist/router/utils/index.js +18 -5
  50. package/dist/router/utils/index.js.map +1 -1
  51. package/dist/types.utils.d.ts +3 -0
  52. package/dist/types.utils.d.ts.map +1 -1
  53. package/dist/utils/vdom.d.ts +2 -1
  54. package/dist/utils/vdom.d.ts.map +1 -1
  55. package/dist/utils/vdom.js +6 -1
  56. package/dist/utils/vdom.js.map +1 -1
  57. package/package.json +17 -1
  58. package/src/constants.ts +1 -0
  59. package/src/reconciler.ts +9 -19
  60. package/src/router/client/index.ts +97 -0
  61. package/src/router/dev/index.ts +63 -0
  62. package/src/router/fileRouter.ts +12 -3
  63. package/src/router/fileRouterController.ts +185 -73
  64. package/src/router/globals.ts +4 -0
  65. package/src/router/head.ts +66 -0
  66. package/src/router/index.ts +7 -0
  67. package/src/router/pageConfig.ts +2 -2
  68. package/src/router/server/index.ts +214 -0
  69. package/src/router/types.internal.ts +6 -2
  70. package/src/router/types.ts +43 -20
  71. package/src/router/utils/index.ts +224 -206
  72. package/src/types.utils.ts +4 -0
  73. package/src/utils/vdom.ts +9 -0
@@ -1,206 +1,224 @@
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
- }
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
+
9
+ export {
10
+ formatViteImportMap,
11
+ matchRoute,
12
+ match404Route,
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
+ if (__DEV__) {
28
+ console.warn(
29
+ `[kiru/router]: File "${key}" does not start with "${dir}".`
30
+ )
31
+ }
32
+ return acc
33
+ }
34
+
35
+ let specificity = 0
36
+ let k = key.slice(dirIndex + dir.length)
37
+ while (k.startsWith("/")) {
38
+ k = k.slice(1)
39
+ }
40
+ const segments: string[] = []
41
+ const parts = k.split("/").slice(0, -1)
42
+
43
+ for (let i = 0; i < parts.length; i++) {
44
+ const part = parts[i]
45
+ if (part.startsWith("[...") && part.endsWith("]")) {
46
+ if (i !== parts.length - 1) {
47
+ throw new Error(
48
+ `[kiru/router]: Catchall must be the folder name. Got "${key}"`
49
+ )
50
+ }
51
+ segments.push(`:${part.slice(4, -1)}*`)
52
+ specificity += 1
53
+ break
54
+ }
55
+ if (part.startsWith("[") && part.endsWith("]")) {
56
+ segments.push(`:${part.slice(1, -1)}`)
57
+ specificity += 10
58
+ continue
59
+ }
60
+ specificity += 100
61
+ segments.push(part)
62
+ }
63
+
64
+ const value: FormattedViteImportMap[string] = {
65
+ load: map[key],
66
+ specificity,
67
+ segments,
68
+ filePath: key,
69
+ }
70
+
71
+ return {
72
+ ...acc,
73
+ [baseUrl + segments.join("/")]: value,
74
+ }
75
+ }, {})
76
+ }
77
+
78
+ function matchRoute(
79
+ pages: FormattedViteImportMap,
80
+ pathSegments: string[]
81
+ ): RouteMatch | null {
82
+ const matches: RouteMatch[] = []
83
+ outer: for (const [route, pageEntry] of Object.entries(pages)) {
84
+ const routeSegments = pageEntry.segments
85
+ const pathMatchingSegments = routeSegments.filter(
86
+ (seg) => !seg.startsWith("(") && !seg.endsWith(")")
87
+ )
88
+
89
+ const params: Record<string, string> = {}
90
+ let hasCatchall = false
91
+
92
+ // Check if route matches
93
+ for (
94
+ let i = 0;
95
+ i < pathMatchingSegments.length && i < pathSegments.length;
96
+ i++
97
+ ) {
98
+ const routeSeg = pathMatchingSegments[i]
99
+
100
+ if (routeSeg.startsWith(":")) {
101
+ const key = routeSeg.slice(1)
102
+
103
+ if (routeSeg.endsWith("*")) {
104
+ // Catchall route - matches remaining segments
105
+ hasCatchall = true
106
+ const catchallKey = key.slice(0, -1) // Remove the *
107
+ params[catchallKey] = pathSegments.slice(i).join("/")
108
+ break
109
+ } else {
110
+ // Regular dynamic segment
111
+ if (i >= pathSegments.length) {
112
+ continue outer
113
+ }
114
+ params[key] = pathSegments[i]
115
+ }
116
+ } else {
117
+ // Static segment
118
+ if (routeSeg !== pathSegments[i]) {
119
+ continue outer
120
+ }
121
+ }
122
+ }
123
+
124
+ // For non-catchall routes, ensure exact length match
125
+ if (!hasCatchall && pathMatchingSegments.length !== pathSegments.length) {
126
+ continue
127
+ }
128
+
129
+ matches.push({
130
+ route,
131
+ pageEntry,
132
+ params,
133
+ routeSegments,
134
+ })
135
+ }
136
+
137
+ // Sort by specificity (highest first) and return the best match
138
+ if (matches.length === 0) {
139
+ return null
140
+ }
141
+
142
+ matches.sort((a, b) => b.pageEntry.specificity - a.pageEntry.specificity)
143
+ return matches[0] || null
144
+ }
145
+
146
+ function match404Route(
147
+ pages: FormattedViteImportMap,
148
+ pathSegments: string[]
149
+ ): RouteMatch | null {
150
+ // Try to find a 404 page at each parent directory level
151
+ // Start from the deepest level and work up to root
152
+ for (let i = pathSegments.length; i >= 0; i--) {
153
+ const parentSegments = pathSegments.slice(0, i)
154
+ const fourOhFourSegments = [...parentSegments, "404"]
155
+ const match = matchRoute(pages, fourOhFourSegments)
156
+ if (match) {
157
+ return match
158
+ }
159
+ }
160
+ return null
161
+ }
162
+
163
+ function matchLayouts(
164
+ layouts: FormattedViteImportMap,
165
+ routeSegments: string[]
166
+ ) {
167
+ return ["/", ...routeSegments].reduce((acc, _, i) => {
168
+ const layoutPath = "/" + routeSegments.slice(0, i).join("/")
169
+ const layout = layouts[layoutPath]
170
+
171
+ if (!layout) {
172
+ return acc
173
+ }
174
+
175
+ return [...acc, layout]
176
+ }, [] as FormattedViteImportMap[string][])
177
+ }
178
+
179
+ function normalizePrefixPath(path: string) {
180
+ while (path.startsWith(".")) {
181
+ path = path.slice(1)
182
+ }
183
+ path = `/${path}/`
184
+ while (path.startsWith("//")) {
185
+ path = path.slice(1)
186
+ }
187
+ while (path.endsWith("//")) {
188
+ path = path.slice(0, -1)
189
+ }
190
+ return path
191
+ }
192
+
193
+ function parseQuery(
194
+ search: string
195
+ ): Record<string, string | string[] | undefined> {
196
+ const params = new URLSearchParams(search)
197
+ const query: Record<string, string | string[] | undefined> = {}
198
+
199
+ for (const [key, value] of params.entries()) {
200
+ if (query[key]) {
201
+ // Convert to array if multiple values
202
+ if (Array.isArray(query[key])) {
203
+ ;(query[key] as string[]).push(value)
204
+ } else {
205
+ query[key] = [query[key] as string, value]
206
+ }
207
+ } else {
208
+ query[key] = value
209
+ }
210
+ }
211
+
212
+ return query
213
+ }
214
+
215
+ function wrapWithLayouts(
216
+ layouts: Kiru.FC[],
217
+ page: Kiru.FC,
218
+ props: Record<string, unknown>
219
+ ) {
220
+ return layouts.reduceRight(
221
+ (children, Layout) => createElement(Layout, { children }),
222
+ createElement(page, props)
223
+ )
224
+ }
@@ -61,3 +61,7 @@ export type AsyncTaskState<T, E extends Error = Error> =
61
61
  error: E
62
62
  loading: false
63
63
  }
64
+
65
+ export type Guard<T, K extends keyof T> = {
66
+ [P in K]: T[P]
67
+ }
package/src/utils/vdom.ts CHANGED
@@ -17,6 +17,7 @@ export {
17
17
  cloneVNode,
18
18
  isVNodeDeleted,
19
19
  isVNode,
20
+ isValidTextChild,
20
21
  isExoticType,
21
22
  isFragment,
22
23
  isLazy,
@@ -54,6 +55,14 @@ function isVNode(thing: unknown): thing is Kiru.VNode {
54
55
  return typeof thing === "object" && thing !== null && "type" in thing
55
56
  }
56
57
 
58
+ function isValidTextChild(thing: unknown): thing is string | number | bigint {
59
+ return (
60
+ (typeof thing === "string" && thing !== "") ||
61
+ typeof thing === "number" ||
62
+ typeof thing === "bigint"
63
+ )
64
+ }
65
+
57
66
  function isExoticType(type: Kiru.VNode["type"]): type is Kiru.ExoticSymbol {
58
67
  return (
59
68
  type === $FRAGMENT || type === $CONTEXT_PROVIDER || type === $ERROR_BOUNDARY