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
@@ -4,3 +4,10 @@ export { FileRouter, type FileRouterProps } from "./fileRouter.js"
4
4
  export * from "./link.js"
5
5
  export * from "./pageConfig.js"
6
6
  export type * from "./types.js"
7
+
8
+ import { Content, Outlet } from "./head.js"
9
+
10
+ export const Head = {
11
+ Content,
12
+ Outlet,
13
+ }
@@ -2,8 +2,8 @@ import { __DEV__ } from "../env.js"
2
2
  import { fileRouterInstance } from "./globals.js"
3
3
  import type { PageConfig } from "./types"
4
4
 
5
- export function definePageConfig<T extends PageConfig>(config: T): T {
6
- if (__DEV__) {
5
+ export function definePageConfig<T>(config: PageConfig<T>): PageConfig<T> {
6
+ if (__DEV__ && "window" in globalThis) {
7
7
  const filePath = window.__kiru?.HMRContext?.getCurrentFilePath()
8
8
  const fileRouter = fileRouterInstance.current
9
9
  if (filePath && fileRouter) {
@@ -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
+ : {}