kiru 0.50.8 → 0.51.0-preview.1

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 (76) 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.dom.d.ts +2 -2
  52. package/dist/types.dom.d.ts.map +1 -1
  53. package/dist/types.utils.d.ts +3 -0
  54. package/dist/types.utils.d.ts.map +1 -1
  55. package/dist/utils/vdom.d.ts +2 -1
  56. package/dist/utils/vdom.d.ts.map +1 -1
  57. package/dist/utils/vdom.js +6 -1
  58. package/dist/utils/vdom.js.map +1 -1
  59. package/package.json +17 -1
  60. package/src/constants.ts +1 -0
  61. package/src/reconciler.ts +9 -19
  62. package/src/router/client/index.ts +97 -0
  63. package/src/router/dev/index.ts +63 -0
  64. package/src/router/fileRouter.ts +12 -3
  65. package/src/router/fileRouterController.ts +185 -73
  66. package/src/router/globals.ts +4 -0
  67. package/src/router/head.ts +66 -0
  68. package/src/router/index.ts +7 -0
  69. package/src/router/pageConfig.ts +2 -2
  70. package/src/router/server/index.ts +214 -0
  71. package/src/router/types.internal.ts +6 -2
  72. package/src/router/types.ts +43 -20
  73. package/src/router/utils/index.ts +25 -7
  74. package/src/types.dom.ts +2 -5
  75. package/src/types.utils.ts +4 -0
  76. package/src/utils/vdom.ts +9 -0
@@ -0,0 +1,214 @@
1
+ import { createElement } from "../../element.js"
2
+ import { renderToReadableStream } from "../../ssr/server.js"
3
+ import {
4
+ matchLayouts,
5
+ matchRoute,
6
+ match404Route,
7
+ parseQuery,
8
+ wrapWithLayouts,
9
+ } from "../utils/index.js"
10
+ import { RouterContext } from "../context.js"
11
+ import type { PageConfig, PageProps, RouterState } from "../types.js"
12
+ import type { Readable } from "node:stream"
13
+ import { FormattedViteImportMap, PageModule } from "../types.internal.js"
14
+ import { __DEV__ } from "../../env.js"
15
+ import { FileRouterDataLoadError } from "../errors.js"
16
+
17
+ export interface RenderContext {
18
+ pages: FormattedViteImportMap
19
+ layouts: FormattedViteImportMap
20
+ Document: Kiru.FC
21
+ registerModule: (moduleId: string) => void
22
+ registerPreloadedPageProps: (props: Record<string, unknown>) => void
23
+ }
24
+
25
+ export interface RenderResult {
26
+ status: number
27
+ immediate: string
28
+ stream: Readable | null
29
+ }
30
+
31
+ export async function render(
32
+ url: string,
33
+ ctx: RenderContext,
34
+ result?: RenderResult
35
+ ): Promise<RenderResult> {
36
+ const u = new URL(url, "http://localhost")
37
+ const pathSegments = u.pathname.split("/").filter(Boolean)
38
+ let routeMatch = matchRoute(ctx.pages, pathSegments)
39
+
40
+ if (!routeMatch) {
41
+ // Try to find a 404 page in parent directories
42
+ const fourOhFourMatch = match404Route(ctx.pages, pathSegments)
43
+ if (fourOhFourMatch) {
44
+ routeMatch = fourOhFourMatch
45
+ } else {
46
+ // Fallback to root 404 or default fallback
47
+ if (url === "/404" && result) {
48
+ if (__DEV__) {
49
+ console.warn(
50
+ "[kiru/router]: No 404 route defined. Using fallback 404 page."
51
+ )
52
+ }
53
+ return {
54
+ status: 404,
55
+ immediate:
56
+ "<!doctype html><html><head><title>Not Found</title></head><body><h1>404</h1></body></html>",
57
+ stream: null,
58
+ }
59
+ }
60
+ return render("/404", ctx, {
61
+ ...(result ?? {}),
62
+ immediate: "",
63
+ stream: null,
64
+ status: 404,
65
+ })
66
+ }
67
+ }
68
+
69
+ const { pageEntry, routeSegments, params } = routeMatch
70
+ const is404Route = routeMatch.routeSegments.includes("404")
71
+ const layoutEntries = matchLayouts(ctx.layouts, routeSegments)
72
+
73
+ if (__DEV__) {
74
+ ;[pageEntry, ...layoutEntries].forEach((e) => {
75
+ ctx.registerModule(e.filePath!)
76
+ })
77
+ }
78
+
79
+ const [page, ...layouts] = await Promise.all([
80
+ pageEntry.load() as unknown as Promise<PageModule>,
81
+ ...layoutEntries.map((layoutEntry) => layoutEntry.load()),
82
+ ])
83
+
84
+ const query = parseQuery(u.search)
85
+
86
+ let props = {} as PageProps<PageConfig>
87
+ const config = page.config ?? {}
88
+ const abortController = new AbortController()
89
+
90
+ if (config.loader) {
91
+ if (config.loader.mode !== "static" || __DEV__) {
92
+ props = { loading: true, data: null, error: null }
93
+ } else {
94
+ const routerState: RouterState = {
95
+ path: u.pathname,
96
+ params,
97
+ query,
98
+ signal: abortController.signal,
99
+ }
100
+ const timeout = setTimeout(() => {
101
+ abortController.abort(
102
+ "[kiru/router]: Page data loading timed out after 10 seconds"
103
+ )
104
+ }, 10000)
105
+
106
+ try {
107
+ const data = await config.loader.load(routerState)
108
+ props = {
109
+ data,
110
+ error: null,
111
+ loading: false,
112
+ }
113
+ } catch (error) {
114
+ props = {
115
+ error: new FileRouterDataLoadError(error),
116
+ loading: false,
117
+ data: null,
118
+ }
119
+ } finally {
120
+ clearTimeout(timeout)
121
+ ctx.registerPreloadedPageProps({ data: props.data, error: props.error })
122
+ }
123
+ }
124
+ }
125
+
126
+ const children = wrapWithLayouts(
127
+ layouts
128
+ .map((layout) => layout.default)
129
+ .filter((l) => typeof l === "function"),
130
+ page.default,
131
+ props
132
+ )
133
+
134
+ const app = createElement(RouterContext.Provider, {
135
+ children: createElement(ctx.Document, { children }),
136
+ value: {
137
+ state: {
138
+ params,
139
+ query,
140
+ path: u.pathname,
141
+ signal: abortController.signal, // Server-side signal (not abortable)
142
+ } as RouterState,
143
+ },
144
+ })
145
+
146
+ let { immediate, stream } = renderToReadableStream(app)
147
+ const hasHeadOutlet = immediate.includes("<kiru-head-outlet>")
148
+ const hasHeadContent = immediate.includes("<kiru-head-content>")
149
+
150
+ if (hasHeadOutlet && hasHeadContent) {
151
+ let [preHeadContent = "", headContentInner = "", postHeadContent = ""] =
152
+ immediate.split(/<kiru-head-content>|<\/kiru-head-content>/)
153
+
154
+ preHeadContent = preHeadContent.replace(
155
+ "<kiru-head-outlet>",
156
+ headContentInner
157
+ )
158
+ immediate = `${preHeadContent}${postHeadContent}`
159
+ } else if (hasHeadContent) {
160
+ // remove head content element and everything within it
161
+ immediate = immediate.replace(
162
+ /<kiru-head-content>(.*?)<\/kiru-head-content>/,
163
+ ""
164
+ )
165
+ } else if (hasHeadOutlet) {
166
+ // remove head outlet element and everything within it
167
+ immediate = immediate.replaceAll("<kiru-head-outlet>", "")
168
+ }
169
+
170
+ // console.log("immediate", immediate)
171
+
172
+ return {
173
+ status: is404Route ? 404 : result?.status ?? 200,
174
+ immediate: "<!doctype html>" + immediate,
175
+ stream,
176
+ }
177
+ }
178
+
179
+ export async function generateStaticPaths(pages: FormattedViteImportMap) {
180
+ const results: Record<string, string> = {}
181
+ const entries = Object.values(pages)
182
+ for (const entry of entries) {
183
+ // Build a clean URL path excluding group segments like (articles)
184
+ const urlSegments = entry.segments.filter(
185
+ (s) => !(s.startsWith("(") && s.endsWith(")"))
186
+ )
187
+
188
+ const basePath = "/" + urlSegments.join("/")
189
+ // if (basePath.endsWith("/404")) continue
190
+
191
+ const hasDynamic = urlSegments.some((s) => s.startsWith(":"))
192
+ if (!hasDynamic) {
193
+ results[basePath === "" ? "/" : basePath] = entry.filePath
194
+ continue
195
+ }
196
+ try {
197
+ const mod: PageModule = await entry.load()
198
+ const gen = mod?.config?.generateStaticParams
199
+ if (!gen) continue
200
+ const paramsList = await gen()
201
+ if (!Array.isArray(paramsList)) continue
202
+
203
+ for (const params of paramsList) {
204
+ let p = basePath
205
+ for (const key in params) {
206
+ const value = params[key]
207
+ p = p.replace(`:${key}*`, value).replace(`:${key}`, value)
208
+ }
209
+ results[p] = entry.filePath
210
+ }
211
+ } catch {}
212
+ }
213
+ return results
214
+ }
@@ -5,8 +5,12 @@ export interface DefaultComponentModule {
5
5
  }
6
6
 
7
7
  export interface PageModule {
8
- default: DefaultComponentModule
8
+ default: Kiru.FC
9
9
  config?: PageConfig
10
+ __KIRU_STATIC_PROPS__?: Record<
11
+ string,
12
+ { data: unknown; error: string | null }
13
+ >
10
14
  }
11
15
 
12
16
  export interface ViteImportMap {
@@ -18,7 +22,7 @@ export interface FormattedViteImportMap {
18
22
  load: () => Promise<DefaultComponentModule>
19
23
  specificity: number
20
24
  segments: string[]
21
- filePath?: string
25
+ filePath: string
22
26
  }
23
27
  }
24
28
 
@@ -1,5 +1,21 @@
1
1
  import { AsyncTaskState } from "../types.utils"
2
2
  import { FileRouterDataLoadError } from "./errors"
3
+ import {
4
+ DefaultComponentModule,
5
+ FormattedViteImportMap,
6
+ PageModule,
7
+ } from "./types.internal"
8
+
9
+ export interface FileRouterPreloadConfig {
10
+ pages: FormattedViteImportMap
11
+ layouts: FormattedViteImportMap
12
+ page: PageModule
13
+ pageProps: Record<string, unknown>
14
+ pageLayouts: DefaultComponentModule[]
15
+ params: RouteParams
16
+ query: RouteQuery
17
+ route: string
18
+ }
3
19
 
4
20
  export interface FileRouterConfig {
5
21
  /**
@@ -35,6 +51,12 @@ export interface FileRouterConfig {
35
51
  * @default false
36
52
  */
37
53
  transition?: boolean
54
+
55
+ /**
56
+ * Used for generated entry point files
57
+ * @internal
58
+ */
59
+ preloaded?: FileRouterPreloadConfig
38
60
  }
39
61
 
40
62
  export interface RouteParams {
@@ -69,37 +91,38 @@ export interface RouterState {
69
91
 
70
92
  type PageDataLoaderContext = RouterState & {}
71
93
 
72
- export interface PageDataLoaderConfig<T = unknown> {
94
+ export type PageDataLoaderConfig<T = unknown> = {
73
95
  /**
74
96
  * The function to load the page data
75
97
  */
76
98
  load: (context: PageDataLoaderContext) => Promise<T>
99
+
100
+ /**
101
+ * The mode to use for the page data loader
102
+ * @default "client"
103
+ * @description
104
+ * - **static**: The page data is loaded at build time and never updated
105
+ * - **client**: The page data is loaded upon navigation and updated on subsequent navigations
106
+ */
107
+ mode?: "static" | "client"
77
108
  /**
78
- * Enable transitions when swapping between "load", "error" and "data" states
109
+ * Enable transitions when swapping between "load", "error" and "data" states (only when mode is "client")
79
110
  */
80
111
  transition?: boolean
81
112
  }
82
113
 
83
- export interface PageConfig {
114
+ export interface PageConfig<T = unknown> {
84
115
  /**
85
116
  * The loader configuration for this page
86
117
  */
87
- loader?: PageDataLoaderConfig
88
- // title?: string
89
- // description?: string
90
- // meta?: Record<string, string>
118
+ loader?: PageDataLoaderConfig<T>
119
+ /**
120
+ * Generate static params for this page. For each params
121
+ * returned, a page will be generated
122
+ */
123
+ generateStaticParams?: () => RouteParams[] | Promise<RouteParams[]>
91
124
  }
92
125
 
93
- export type PageProps<T extends PageConfig> =
94
- T["loader"] extends PageDataLoaderConfig
95
- ? AsyncTaskState<
96
- Awaited<ReturnType<T["loader"]["load"]>>,
97
- FileRouterDataLoadError
98
- >
99
- : {}
100
-
101
- export interface ErrorPageProps {
102
- source?: {
103
- path: string
104
- }
105
- }
126
+ export type PageProps<T extends PageConfig<any>> = T extends PageConfig<infer U>
127
+ ? AsyncTaskState<U, FileRouterDataLoadError>
128
+ : {}
@@ -5,11 +5,11 @@ import type {
5
5
  RouteMatch,
6
6
  ViteImportMap,
7
7
  } from "../types.internal"
8
- import { PageConfig, PageProps } from "../types.js"
9
8
 
10
9
  export {
11
10
  formatViteImportMap,
12
11
  matchRoute,
12
+ match404Route,
13
13
  matchLayouts,
14
14
  normalizePrefixPath,
15
15
  parseQuery,
@@ -24,7 +24,11 @@ function formatViteImportMap(
24
24
  return Object.keys(map).reduce<FormattedViteImportMap>((acc, key) => {
25
25
  const dirIndex = key.indexOf(dir)
26
26
  if (dirIndex === -1) {
27
- console.warn(`[kiru/router]: File "${key}" does not start with "${dir}".`)
27
+ if (__DEV__) {
28
+ console.warn(
29
+ `[kiru/router]: File "${key}" does not start with "${dir}".`
30
+ )
31
+ }
28
32
  return acc
29
33
  }
30
34
 
@@ -61,10 +65,7 @@ function formatViteImportMap(
61
65
  load: map[key],
62
66
  specificity,
63
67
  segments,
64
- }
65
-
66
- if (__DEV__) {
67
- value.filePath = key
68
+ filePath: key,
68
69
  }
69
70
 
70
71
  return {
@@ -142,6 +143,23 @@ function matchRoute(
142
143
  return matches[0] || null
143
144
  }
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
+
145
163
  function matchLayouts(
146
164
  layouts: FormattedViteImportMap,
147
165
  routeSegments: string[]
@@ -197,7 +215,7 @@ function parseQuery(
197
215
  function wrapWithLayouts(
198
216
  layouts: Kiru.FC[],
199
217
  page: Kiru.FC,
200
- props: PageProps<PageConfig>
218
+ props: Record<string, unknown>
201
219
  ) {
202
220
  return layouts.reduceRight(
203
221
  (children, Layout) => createElement(Layout, { children }),
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 BaseEvent<T extends Element = Element>
368
+ interface BaseEventHandler<T extends Element = Element>
369
369
  extends DOMEvent<Event, T> {}
370
370
 
371
371
  interface AnimationEvent<T extends Element = Element>
@@ -413,10 +413,6 @@ 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
-
420
416
  type ClipboardEventHandler<T extends Element = Element> = EventHandler<
421
417
  ClipboardEvent<T>
422
418
  >
@@ -832,6 +828,7 @@ interface HtmlElementAttributes {
832
828
  content?: string
833
829
  httpEquiv?: string
834
830
  name?: string
831
+ property?: string
835
832
  }
836
833
  meter: {
837
834
  value?: string | number
@@ -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