kiru 0.52.4 → 0.54.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 (202) hide show
  1. package/dist/appContext.d.ts.map +1 -1
  2. package/dist/appContext.js +14 -11
  3. package/dist/appContext.js.map +1 -1
  4. package/dist/components/derive.d.ts +20 -0
  5. package/dist/components/derive.d.ts.map +1 -0
  6. package/dist/components/derive.js +57 -0
  7. package/dist/components/derive.js.map +1 -0
  8. package/dist/components/errorBoundary.d.ts +1 -1
  9. package/dist/components/errorBoundary.d.ts.map +1 -1
  10. package/dist/components/index.d.ts +1 -1
  11. package/dist/components/index.d.ts.map +1 -1
  12. package/dist/components/index.js +1 -1
  13. package/dist/components/index.js.map +1 -1
  14. package/dist/components/memo.d.ts +6 -3
  15. package/dist/components/memo.d.ts.map +1 -1
  16. package/dist/components/memo.js.map +1 -1
  17. package/dist/constants.d.ts +3 -3
  18. package/dist/constants.d.ts.map +1 -1
  19. package/dist/constants.js +3 -3
  20. package/dist/constants.js.map +1 -1
  21. package/dist/dom.d.ts.map +1 -1
  22. package/dist/dom.js +69 -66
  23. package/dist/dom.js.map +1 -1
  24. package/dist/element.d.ts +2 -2
  25. package/dist/element.d.ts.map +1 -1
  26. package/dist/element.js +20 -32
  27. package/dist/element.js.map +1 -1
  28. package/dist/globalContext.d.ts +6 -1
  29. package/dist/globalContext.d.ts.map +1 -1
  30. package/dist/globalContext.js +14 -2
  31. package/dist/globalContext.js.map +1 -1
  32. package/dist/globals.d.ts +1 -1
  33. package/dist/globals.d.ts.map +1 -1
  34. package/dist/globals.js.map +1 -1
  35. package/dist/hooks/usePromise.d.ts +4 -9
  36. package/dist/hooks/usePromise.d.ts.map +1 -1
  37. package/dist/hooks/usePromise.js +25 -28
  38. package/dist/hooks/usePromise.js.map +1 -1
  39. package/dist/hooks/useRef.d.ts +2 -2
  40. package/dist/hooks/useRef.d.ts.map +1 -1
  41. package/dist/jsx.d.ts +1 -1
  42. package/dist/jsx.d.ts.map +1 -1
  43. package/dist/reconciler.d.ts +1 -1
  44. package/dist/reconciler.d.ts.map +1 -1
  45. package/dist/reconciler.js +57 -117
  46. package/dist/reconciler.js.map +1 -1
  47. package/dist/recursiveRender.d.ts +4 -3
  48. package/dist/recursiveRender.d.ts.map +1 -1
  49. package/dist/recursiveRender.js +20 -19
  50. package/dist/recursiveRender.js.map +1 -1
  51. package/dist/renderToString.js +2 -2
  52. package/dist/renderToString.js.map +1 -1
  53. package/dist/router/client/index.d.ts +4 -2
  54. package/dist/router/client/index.d.ts.map +1 -1
  55. package/dist/router/client/index.js +49 -11
  56. package/dist/router/client/index.js.map +1 -1
  57. package/dist/router/context.d.ts +2 -0
  58. package/dist/router/context.d.ts.map +1 -1
  59. package/dist/router/context.js +5 -1
  60. package/dist/router/context.js.map +1 -1
  61. package/dist/router/fileRouterController.d.ts +3 -1
  62. package/dist/router/fileRouterController.d.ts.map +1 -1
  63. package/dist/router/fileRouterController.js +80 -9
  64. package/dist/router/fileRouterController.js.map +1 -1
  65. package/dist/router/globals.d.ts +3 -0
  66. package/dist/router/globals.d.ts.map +1 -1
  67. package/dist/router/globals.js +3 -0
  68. package/dist/router/globals.js.map +1 -1
  69. package/dist/router/guard.d.ts +17 -0
  70. package/dist/router/guard.d.ts.map +1 -0
  71. package/dist/router/guard.js +45 -0
  72. package/dist/router/guard.js.map +1 -0
  73. package/dist/router/head.d.ts.map +1 -1
  74. package/dist/router/head.js +5 -7
  75. package/dist/router/head.js.map +1 -1
  76. package/dist/router/index.d.ts +3 -2
  77. package/dist/router/index.d.ts.map +1 -1
  78. package/dist/router/index.js +2 -1
  79. package/dist/router/index.js.map +1 -1
  80. package/dist/router/{server → ssg}/index.d.ts +4 -3
  81. package/dist/router/ssg/index.d.ts.map +1 -0
  82. package/dist/router/{server → ssg}/index.js +7 -4
  83. package/dist/router/ssg/index.js.map +1 -0
  84. package/dist/router/ssr/index.d.ts +20 -0
  85. package/dist/router/ssr/index.d.ts.map +1 -0
  86. package/dist/router/ssr/index.js +160 -0
  87. package/dist/router/ssr/index.js.map +1 -0
  88. package/dist/router/types.d.ts +37 -4
  89. package/dist/router/types.d.ts.map +1 -1
  90. package/dist/router/types.internal.d.ts +4 -0
  91. package/dist/router/types.internal.d.ts.map +1 -1
  92. package/dist/router/utils/index.d.ts +9 -4
  93. package/dist/router/utils/index.d.ts.map +1 -1
  94. package/dist/router/utils/index.js +38 -6
  95. package/dist/router/utils/index.js.map +1 -1
  96. package/dist/signals/base.d.ts +1 -3
  97. package/dist/signals/base.d.ts.map +1 -1
  98. package/dist/signals/base.js +3 -3
  99. package/dist/signals/base.js.map +1 -1
  100. package/dist/signals/computed.d.ts +4 -2
  101. package/dist/signals/computed.d.ts.map +1 -1
  102. package/dist/signals/computed.js +29 -6
  103. package/dist/signals/computed.js.map +1 -1
  104. package/dist/signals/for.d.ts +10 -0
  105. package/dist/signals/for.d.ts.map +1 -0
  106. package/dist/signals/for.js +7 -0
  107. package/dist/signals/for.js.map +1 -0
  108. package/dist/signals/index.d.ts +1 -1
  109. package/dist/signals/index.js +1 -1
  110. package/dist/signals/types.d.ts +1 -1
  111. package/dist/signals/types.d.ts.map +1 -1
  112. package/dist/ssr/client.d.ts +1 -1
  113. package/dist/ssr/client.d.ts.map +1 -1
  114. package/dist/ssr/server.d.ts +1 -2
  115. package/dist/ssr/server.d.ts.map +1 -1
  116. package/dist/ssr/server.js +26 -29
  117. package/dist/ssr/server.js.map +1 -1
  118. package/dist/types.d.ts +13 -11
  119. package/dist/types.d.ts.map +1 -1
  120. package/dist/types.dom.d.ts +33 -32
  121. package/dist/types.dom.d.ts.map +1 -1
  122. package/dist/types.utils.d.ts +5 -1
  123. package/dist/types.utils.d.ts.map +1 -1
  124. package/dist/utils/format.d.ts +4 -3
  125. package/dist/utils/format.d.ts.map +1 -1
  126. package/dist/utils/format.js +8 -4
  127. package/dist/utils/format.js.map +1 -1
  128. package/dist/utils/index.d.ts +0 -1
  129. package/dist/utils/index.d.ts.map +1 -1
  130. package/dist/utils/index.js +0 -1
  131. package/dist/utils/index.js.map +1 -1
  132. package/dist/utils/promise.d.ts +16 -0
  133. package/dist/utils/promise.d.ts.map +1 -0
  134. package/dist/utils/promise.js +14 -0
  135. package/dist/utils/promise.js.map +1 -0
  136. package/dist/utils/runtime.d.ts +11 -2
  137. package/dist/utils/runtime.d.ts.map +1 -1
  138. package/dist/utils/runtime.js +26 -2
  139. package/dist/utils/runtime.js.map +1 -1
  140. package/dist/utils/vdom.d.ts +4 -4
  141. package/dist/utils/vdom.d.ts.map +1 -1
  142. package/dist/utils/vdom.js +18 -17
  143. package/dist/utils/vdom.js.map +1 -1
  144. package/dist/vNode.d.ts +4 -0
  145. package/dist/vNode.d.ts.map +1 -0
  146. package/dist/vNode.js +22 -0
  147. package/dist/vNode.js.map +1 -0
  148. package/package.json +8 -4
  149. package/src/appContext.ts +15 -13
  150. package/src/components/derive.ts +121 -0
  151. package/src/components/index.ts +1 -1
  152. package/src/components/memo.ts +3 -2
  153. package/src/constants.ts +4 -4
  154. package/src/dom.ts +71 -66
  155. package/src/element.ts +22 -35
  156. package/src/globalContext.ts +24 -3
  157. package/src/globals.ts +1 -1
  158. package/src/hooks/usePromise.ts +32 -41
  159. package/src/hooks/useRef.ts +2 -2
  160. package/src/reconciler.ts +87 -125
  161. package/src/recursiveRender.ts +25 -23
  162. package/src/renderToString.ts +3 -3
  163. package/src/router/client/index.ts +100 -14
  164. package/src/router/context.ts +7 -1
  165. package/src/router/fileRouterController.ts +137 -8
  166. package/src/router/globals.ts +4 -0
  167. package/src/router/guard.ts +72 -0
  168. package/src/router/head.ts +5 -7
  169. package/src/router/index.ts +12 -1
  170. package/src/router/{server → ssg}/index.ts +16 -9
  171. package/src/router/ssr/index.ts +247 -0
  172. package/src/router/types.internal.ts +5 -0
  173. package/src/router/types.ts +48 -4
  174. package/src/router/utils/index.ts +74 -8
  175. package/src/signals/base.ts +3 -3
  176. package/src/signals/computed.ts +43 -6
  177. package/src/signals/for.ts +25 -0
  178. package/src/signals/index.ts +1 -1
  179. package/src/signals/types.ts +5 -1
  180. package/src/ssr/client.ts +1 -1
  181. package/src/ssr/server.ts +31 -33
  182. package/src/types.dom.ts +40 -40
  183. package/src/types.ts +14 -11
  184. package/src/types.utils.ts +11 -1
  185. package/src/utils/format.ts +12 -4
  186. package/src/utils/index.ts +0 -2
  187. package/src/utils/promise.ts +26 -0
  188. package/src/utils/runtime.ts +30 -2
  189. package/src/utils/vdom.ts +20 -23
  190. package/src/vNode.ts +30 -0
  191. package/dist/components/suspense.d.ts +0 -24
  192. package/dist/components/suspense.d.ts.map +0 -1
  193. package/dist/components/suspense.js +0 -36
  194. package/dist/components/suspense.js.map +0 -1
  195. package/dist/router/server/index.d.ts.map +0 -1
  196. package/dist/router/server/index.js.map +0 -1
  197. package/dist/signals/jsx.d.ts +0 -17
  198. package/dist/signals/jsx.d.ts.map +0 -1
  199. package/dist/signals/jsx.js +0 -11
  200. package/dist/signals/jsx.js.map +0 -1
  201. package/src/components/suspense.ts +0 -72
  202. package/src/signals/jsx.ts +0 -46
@@ -0,0 +1,247 @@
1
+ import path from "path"
2
+ import { createElement, Fragment } from "../../element.js"
3
+ import { __DEV__ } from "../../env.js"
4
+ import { renderToString } from "../../renderToString.js"
5
+ import { renderToReadableStream } from "../../ssr/server.js"
6
+ import { toArray } from "../../utils/format.js"
7
+ import {
8
+ matchModules,
9
+ matchRoute,
10
+ match404Route,
11
+ parseQuery,
12
+ wrapWithLayouts,
13
+ runBeforeEachGuards,
14
+ runAfterEachGuards,
15
+ runBeforeEnterHooks,
16
+ } from "../utils/index.js"
17
+ import { RouterContext, RequestContext } from "../context.js"
18
+ import type { PageConfig, PageProps, RouterState } from "../types.js"
19
+ import type {
20
+ FormattedViteImportMap,
21
+ GuardModule,
22
+ PageModule,
23
+ } from "../types.internal.js"
24
+
25
+ export interface SSRRenderContext {
26
+ pages: FormattedViteImportMap<PageModule>
27
+ layouts: FormattedViteImportMap
28
+ guards: FormattedViteImportMap<GuardModule>
29
+ Document: Kiru.FC
30
+ userContext: Kiru.RequestContext
31
+ registerModule: (moduleId: string) => void
32
+ }
33
+
34
+ export interface SSRHttpResponse {
35
+ html: string
36
+ statusCode: number
37
+ headers: Array<[string, string]>
38
+ stream: ReadableStream | null
39
+ }
40
+
41
+ export interface SSRRenderResult {
42
+ httpResponse: SSRHttpResponse | null
43
+ }
44
+
45
+ export async function render(
46
+ url: string,
47
+ ctx: SSRRenderContext
48
+ ): Promise<SSRRenderResult> {
49
+ const extName = path.extname(url)
50
+ if (extName && extName.length > 0) {
51
+ return {
52
+ httpResponse: null,
53
+ }
54
+ } else if (url.startsWith("/@")) {
55
+ return {
56
+ httpResponse: null,
57
+ }
58
+ }
59
+ const u = new URL(url, "http://localhost")
60
+ const pathSegments = u.pathname.split("/").filter(Boolean)
61
+ let routeMatch = matchRoute(ctx.pages, pathSegments)
62
+
63
+ if (!routeMatch) {
64
+ // Try to find a 404 page in parent directories
65
+ const fourOhFourMatch = match404Route(ctx.pages, pathSegments)
66
+ if (fourOhFourMatch) {
67
+ routeMatch = fourOhFourMatch
68
+ } else {
69
+ // Fallback to root 404 or default fallback
70
+ if (url === "/404") {
71
+ if (__DEV__) {
72
+ console.warn(
73
+ "[kiru/router]: No 404 route defined. Using fallback 404 page."
74
+ )
75
+ }
76
+ return {
77
+ httpResponse: {
78
+ statusCode: 404,
79
+ headers: [["Content-Type", "text/html"]],
80
+ html: "<!doctype html><html><head><title>Not Found</title></head><body><h1>404</h1></body></html>",
81
+ stream: null,
82
+ },
83
+ }
84
+ }
85
+ // Recursively render the 404 page
86
+ const notFoundResponse = await render("/404", ctx)
87
+ return {
88
+ httpResponse: {
89
+ html: notFoundResponse.httpResponse?.html ?? "",
90
+ headers: notFoundResponse.httpResponse?.headers ?? [
91
+ ["Content-Type", "text/html"],
92
+ ],
93
+ ...notFoundResponse,
94
+ statusCode: 404,
95
+ stream: null,
96
+ },
97
+ }
98
+ }
99
+ }
100
+ const { pageEntry, routeSegments, params } = routeMatch
101
+ const is404Route = routeMatch.routeSegments.includes("404")
102
+
103
+ const guardEntries = matchModules(ctx.guards, routeSegments)
104
+ const guardModules = await Promise.all(
105
+ guardEntries.map((entry) => entry.load() as unknown as Promise<GuardModule>)
106
+ )
107
+
108
+ const redirectPath = await runBeforeEachGuards(
109
+ guardModules,
110
+ { ...ctx.userContext },
111
+ u.pathname
112
+ )
113
+
114
+ if (redirectPath !== null) {
115
+ return createRedirectResult(redirectPath)
116
+ }
117
+
118
+ const layoutEntries = matchModules(ctx.layouts, routeSegments)
119
+
120
+ // Register all modules for CSS collection
121
+ ;[pageEntry, ...layoutEntries].forEach((e) => {
122
+ ctx.registerModule(e.filePath)
123
+ })
124
+
125
+ const [page, ...layouts] = await Promise.all([
126
+ pageEntry.load(),
127
+ ...layoutEntries.map((layoutEntry) => layoutEntry.load()),
128
+ ])
129
+
130
+ const onBeforeEnter = page.config?.hooks?.onBeforeEnter
131
+ if (onBeforeEnter) {
132
+ const redirectPath = await runBeforeEnterHooks(
133
+ toArray(onBeforeEnter),
134
+ { ...ctx.userContext },
135
+ u.pathname
136
+ )
137
+ if (redirectPath) {
138
+ return createRedirectResult(redirectPath)
139
+ }
140
+ }
141
+
142
+ const query = parseQuery(u.search)
143
+
144
+ let props = {} as PageProps<PageConfig>
145
+ const config = page.config ?? {}
146
+ const abortController = new AbortController()
147
+
148
+ // PageConfig loaders don't run on the server
149
+ if (config.loader) {
150
+ props = {
151
+ data: null,
152
+ error: null,
153
+ loading: true,
154
+ }
155
+ }
156
+
157
+ let documentShell = renderToString(createElement(ctx.Document))
158
+
159
+ if (
160
+ documentShell.includes("</body>") ||
161
+ !documentShell.includes("<kiru-body-outlet>")
162
+ ) {
163
+ throw new Error(
164
+ "[kiru/router]: Document is expected to contain a <Body.Outlet> element. See https://kirujs.dev/docs/api/file-router#general-usage"
165
+ )
166
+ }
167
+
168
+ const children = wrapWithLayouts(
169
+ layouts
170
+ .map((layout) => layout.default)
171
+ .filter((l) => typeof l === "function"),
172
+ page.default,
173
+ props
174
+ )
175
+
176
+ const routerContextValue = {
177
+ state: {
178
+ params,
179
+ query,
180
+ pathname: u.pathname,
181
+ hash: "",
182
+ signal: abortController.signal,
183
+ } satisfies RouterState,
184
+ }
185
+
186
+ const app = createElement(RouterContext.Provider, {
187
+ children: createElement(RequestContext.Provider, {
188
+ children: Fragment({ children }),
189
+ value: ctx.userContext,
190
+ }),
191
+ value: routerContextValue,
192
+ })
193
+
194
+ let { immediate: pageOutletContent, stream } = renderToReadableStream(app)
195
+ const hasHeadContent = pageOutletContent.includes("<kiru-head-content>")
196
+ const hasHeadOutlet = documentShell.includes("<kiru-head-outlet>")
197
+
198
+ if (hasHeadOutlet && hasHeadContent) {
199
+ let [preHeadContent = "", headContentInner = "", postHeadContent = ""] =
200
+ pageOutletContent.split(/<kiru-head-content>|<\/kiru-head-content>/)
201
+
202
+ documentShell = documentShell.replace(
203
+ "<kiru-head-outlet>",
204
+ headContentInner
205
+ )
206
+ pageOutletContent = `${preHeadContent}${postHeadContent}`
207
+ } else if (hasHeadContent) {
208
+ // remove head content element and everything within it
209
+ pageOutletContent = pageOutletContent.replace(
210
+ /<kiru-head-content>(.*?)<\/kiru-head-content>/,
211
+ ""
212
+ )
213
+ } else if (hasHeadOutlet) {
214
+ // remove head outlet element and everything within it
215
+ documentShell = documentShell.replaceAll("<kiru-head-outlet>", "")
216
+ }
217
+
218
+ const [prePageOutlet, postPageOutlet] =
219
+ documentShell.split("<kiru-body-outlet>")
220
+
221
+ const html = `<!DOCTYPE html>${prePageOutlet}<body>${pageOutletContent}</body>${postPageOutlet}`
222
+ const statusCode = is404Route ? 404 : 200
223
+
224
+ queueMicrotask(() => {
225
+ runAfterEachGuards(guardModules, { ...ctx.userContext }, u.pathname)
226
+ })
227
+
228
+ return {
229
+ httpResponse: {
230
+ html,
231
+ statusCode,
232
+ headers: [["Content-Type", "text/html;charset=utf-8"]],
233
+ stream,
234
+ },
235
+ }
236
+ }
237
+
238
+ function createRedirectResult(to: string): SSRRenderResult {
239
+ return {
240
+ httpResponse: {
241
+ statusCode: 302,
242
+ headers: [["Location", to]],
243
+ html: "",
244
+ stream: null,
245
+ },
246
+ }
247
+ }
@@ -1,5 +1,6 @@
1
1
  import type { FileRouterContextType } from "./context"
2
2
  import type { PageConfig } from "./types"
3
+ import type { NavGuardBuilder } from "./guard"
3
4
 
4
5
  export interface CurrentPage {
5
6
  component: Kiru.FC<any>
@@ -20,6 +21,10 @@ export interface PageModule {
20
21
  >
21
22
  }
22
23
 
24
+ export interface GuardModule {
25
+ guard: NavGuardBuilder
26
+ }
27
+
23
28
  export interface ViteImportMap {
24
29
  [fp: string]: () => Promise<DefaultComponentModule>
25
30
  }
@@ -3,12 +3,14 @@ import type { FileRouterDataLoadError } from "./errors"
3
3
  import type {
4
4
  DefaultComponentModule,
5
5
  FormattedViteImportMap,
6
+ GuardModule,
6
7
  PageModule,
7
8
  } from "./types.internal"
8
9
 
9
10
  export interface FileRouterPreloadConfig {
10
- pages: FormattedViteImportMap
11
+ pages: FormattedViteImportMap<PageModule>
11
12
  layouts: FormattedViteImportMap
13
+ guards?: FormattedViteImportMap<GuardModule>
12
14
  page: PageModule
13
15
  pageProps: Record<string, unknown>
14
16
  pageLayouts: DefaultComponentModule[]
@@ -40,6 +42,14 @@ export interface FileRouterConfig {
40
42
  * ```
41
43
  */
42
44
  layouts: Record<string, unknown>
45
+ /**
46
+ * The import map to use for loading nav guards
47
+ * @example
48
+ * ```tsx
49
+ * <FileRouter config={{ guards: import.meta.glob("/∗∗/guard.{ts,js}"), ... }} />
50
+ * ```
51
+ */
52
+ guards?: Record<string, unknown>
43
53
 
44
54
  /**
45
55
  * The base url to use as a prefix for route matching
@@ -98,18 +108,34 @@ export interface RouterState {
98
108
  signal: AbortSignal
99
109
  }
100
110
 
101
- type PageDataLoaderContext = RouterState & {}
102
-
103
111
  export interface PageDataLoaderCacheConfig {
104
112
  type: "memory" | "localStorage" | "sessionStorage"
105
113
  ttl: number
106
114
  }
107
115
 
116
+ interface LoaderContext extends RouterState {
117
+ /**
118
+ * The request context - in SSR, this is the data from the server
119
+ * that's passed to the `renderPage` function
120
+ * @example
121
+ * ```ts
122
+ * // server.ts
123
+ * renderPage({ url, context: { test: 123 } })
124
+ *
125
+ * // page.tsx
126
+ * loader: {
127
+ * load: ({ context }) => context.test
128
+ * }
129
+ * ```
130
+ */
131
+ context: Kiru.RequestContext
132
+ }
133
+
108
134
  export type PageDataLoaderConfig<T = unknown> = {
109
135
  /**
110
136
  * The function to load the page data
111
137
  */
112
- load: (context: PageDataLoaderContext) => Promise<T>
138
+ load: (context: LoaderContext) => Promise<T>
113
139
  } & (
114
140
  | {
115
141
  /**
@@ -149,6 +175,22 @@ export type PageDataLoaderConfig<T = unknown> = {
149
175
  }
150
176
  )
151
177
 
178
+ export type NavigationHook<T> = (
179
+ context: Kiru.RequestContext,
180
+ to: string,
181
+ from: string
182
+ ) => T
183
+
184
+ export type OnBeforeEnterHook = NavigationHook<
185
+ string | void | Promise<string | void>
186
+ >
187
+ export type OnBeforeLeaveHook = NavigationHook<false | void>
188
+
189
+ interface PageContextHooks {
190
+ onBeforeEnter?: OnBeforeEnterHook | OnBeforeEnterHook[]
191
+ onBeforeLeave?: OnBeforeLeaveHook | OnBeforeLeaveHook[]
192
+ }
193
+
152
194
  export interface PageConfig<T = unknown> {
153
195
  /**
154
196
  * The loader configuration for this page
@@ -159,6 +201,8 @@ export interface PageConfig<T = unknown> {
159
201
  * returned, a page will be generated
160
202
  */
161
203
  generateStaticParams?: () => RouteParams[] | Promise<RouteParams[]>
204
+
205
+ hooks?: PageContextHooks
162
206
  }
163
207
 
164
208
  export type PageProps<T extends PageConfig<any>> = T extends PageConfig<infer U>
@@ -1,19 +1,27 @@
1
1
  import { createElement } from "../../element.js"
2
2
  import { __DEV__ } from "../../env.js"
3
+ import { resolveNavguard } from "../guard.js"
3
4
  import type {
5
+ DefaultComponentModule,
4
6
  FormattedViteImportMap,
7
+ GuardModule,
5
8
  RouteMatch,
6
9
  ViteImportMap,
7
10
  } from "../types.internal"
11
+ import { OnBeforeEnterHook, OnBeforeLeaveHook } from "../types.js"
8
12
 
9
13
  export {
10
14
  formatViteImportMap,
11
15
  matchRoute,
12
16
  match404Route,
13
- matchLayouts,
17
+ matchModules,
14
18
  normalizePrefixPath,
15
19
  parseQuery,
16
20
  wrapWithLayouts,
21
+ runBeforeLeaveHooks,
22
+ runBeforeEnterHooks,
23
+ runBeforeEachGuards,
24
+ runAfterEachGuards,
17
25
  }
18
26
 
19
27
  function formatViteImportMap(
@@ -177,20 +185,20 @@ function match404Route(
177
185
  return null
178
186
  }
179
187
 
180
- function matchLayouts(
181
- layouts: FormattedViteImportMap,
188
+ function matchModules<T = DefaultComponentModule>(
189
+ modules: FormattedViteImportMap<T>,
182
190
  routeSegments: string[]
183
191
  ) {
184
192
  return ["/", ...routeSegments].reduce((acc, _, i) => {
185
- const layoutPath = "/" + routeSegments.slice(0, i).join("/")
186
- const layout = layouts[layoutPath]
193
+ const modulePath = "/" + routeSegments.slice(0, i).join("/")
194
+ const module = modules[modulePath]
187
195
 
188
- if (!layout) {
196
+ if (!module) {
189
197
  return acc
190
198
  }
191
199
 
192
- return [...acc, layout]
193
- }, [] as FormattedViteImportMap[string][])
200
+ return [...acc, module]
201
+ }, [] as FormattedViteImportMap<T>[string][])
194
202
  }
195
203
 
196
204
  function normalizePrefixPath(path: string) {
@@ -239,3 +247,61 @@ function wrapWithLayouts(
239
247
  createElement(page, props)
240
248
  )
241
249
  }
250
+
251
+ function runBeforeLeaveHooks(
252
+ hooks: OnBeforeLeaveHook[],
253
+ context: Kiru.RequestContext,
254
+ to: string,
255
+ from: string = to
256
+ ): false | void {
257
+ for (const hook of hooks) {
258
+ const res = hook(context, to, from)
259
+ if (res === false) {
260
+ return false
261
+ }
262
+ }
263
+ }
264
+
265
+ async function runBeforeEnterHooks(
266
+ hooks: OnBeforeEnterHook[],
267
+ context: Kiru.RequestContext,
268
+ to: string,
269
+ from: string = to
270
+ ) {
271
+ for (const hook of hooks) {
272
+ const result = await hook(context, to, from)
273
+ if (typeof result === "string") {
274
+ return result
275
+ }
276
+ }
277
+
278
+ return null
279
+ }
280
+
281
+ async function runBeforeEachGuards(
282
+ guardModules: GuardModule[],
283
+ context: Kiru.RequestContext,
284
+ to: string,
285
+ from: string = to
286
+ ): Promise<string | null> {
287
+ const beforeHooks = guardModules
288
+ .map((guardModule) => resolveNavguard(guardModule)?.beforeEach)
289
+ .filter((x) => typeof x === "function")
290
+
291
+ return runBeforeEnterHooks(beforeHooks, context, to, from)
292
+ }
293
+
294
+ async function runAfterEachGuards(
295
+ guardModules: GuardModule[],
296
+ context: Kiru.RequestContext,
297
+ to: string,
298
+ from: string = to
299
+ ): Promise<void> {
300
+ const afterHooks = guardModules
301
+ .map((guardModule) => resolveNavguard(guardModule)?.afterEach)
302
+ .filter((x) => typeof x === "function")
303
+
304
+ for (const hook of afterHooks) {
305
+ await hook(context, to, from)
306
+ }
307
+ }
@@ -125,16 +125,16 @@ export class Signal<T> {
125
125
  return () => this.$subs!.delete(cb)
126
126
  }
127
127
 
128
- notify(options?: { filter?: (sub: Function | Kiru.VNode) => boolean }) {
128
+ notify(filter?: (sub: SignalSubscriber) => boolean) {
129
129
  if (__DEV__) {
130
130
  return signalSubsMap.get(this.$id)?.forEach((sub) => {
131
- if (options?.filter && !options.filter(sub)) return
131
+ if (filter && !filter(sub)) return
132
132
  const { $value, $prevValue } = latest(this)
133
133
  return sub($value, $prevValue)
134
134
  })
135
135
  }
136
136
  this.$subs!.forEach((sub) => {
137
- if (options?.filter && !options.filter(sub)) return
137
+ if (filter && !filter(sub)) return
138
138
  return sub(this.$value, this.$prevValue)
139
139
  })
140
140
  }
@@ -1,6 +1,6 @@
1
1
  import { __DEV__ } from "../env.js"
2
2
  import { $HMR_ACCEPT } from "../constants.js"
3
- import { useHook } from "../hooks/utils.js"
3
+ import { depsRequireChange, useHook } from "../hooks/utils.js"
4
4
  import { latest } from "../utils/index.js"
5
5
  import { effectQueue, signalSubsMap } from "./globals.js"
6
6
  import { executeWithTracking } from "./effect.js"
@@ -69,6 +69,16 @@ export class ComputedSignal<T> extends Signal<T> {
69
69
  Signal.dispose(signal)
70
70
  }
71
71
 
72
+ static updateGetter<T>(signal: ComputedSignal<T>, getter: (prev?: T) => T) {
73
+ const $computed = latest(signal)
74
+ $computed.$getter = getter
75
+ $computed.$isDirty = true
76
+
77
+ ComputedSignal.run($computed)
78
+ if (Object.is($computed.$value, $computed.$prevValue)) return
79
+ $computed.notify()
80
+ }
81
+
72
82
  private static stop<T>(computed: ComputedSignal<T>) {
73
83
  const { $id, $unsubs } = latest(computed)
74
84
 
@@ -103,20 +113,35 @@ export class ComputedSignal<T> extends Signal<T> {
103
113
  }
104
114
  }
105
115
 
106
- export const computed = <T>(
116
+ export function computed<T>(
107
117
  getter: (prev?: T) => T,
108
118
  displayName?: string
109
- ): ComputedSignal<T> => {
119
+ ): ComputedSignal<T> {
110
120
  return new ComputedSignal(getter, displayName)
111
121
  }
112
122
 
113
- export const useComputed = <T>(
123
+ export function useComputed<T>(
124
+ getter: (prev?: T) => T,
125
+ displayName?: string
126
+ ): ComputedSignal<T>
127
+
128
+ export function useComputed<T>(
114
129
  getter: (prev?: T) => T,
130
+ deps?: unknown[],
115
131
  displayName?: string
116
- ) => {
132
+ ): ComputedSignal<T>
133
+
134
+ export function useComputed<T>(
135
+ getter: (prev?: T) => T,
136
+ depsOrDisplayName?: string | unknown[],
137
+ displayName?: string
138
+ ) {
117
139
  return useHook(
118
140
  "useComputedSignal",
119
- { signal: null! as ComputedSignal<T> },
141
+ {
142
+ signal: null! as ComputedSignal<T>,
143
+ deps: void 0 as unknown[] | undefined,
144
+ },
120
145
  ({ hook, isInit, isHMR }) => {
121
146
  if (__DEV__) {
122
147
  hook.dev = {
@@ -134,8 +159,20 @@ export const useComputed = <T>(
134
159
  }
135
160
  }
136
161
  if (isInit) {
162
+ if (typeof depsOrDisplayName === "string") {
163
+ displayName = depsOrDisplayName
164
+ depsOrDisplayName = void 0
165
+ }
166
+ hook.deps = depsOrDisplayName
137
167
  hook.cleanup = () => ComputedSignal.dispose(hook.signal)
138
168
  hook.signal = computed(getter, displayName)
169
+ } else if (
170
+ hook.deps &&
171
+ typeof depsOrDisplayName !== "string" &&
172
+ depsRequireChange(hook.deps, depsOrDisplayName)
173
+ ) {
174
+ hook.deps = depsOrDisplayName
175
+ ComputedSignal.updateGetter(hook.signal, getter)
139
176
  }
140
177
 
141
178
  return hook.signal
@@ -0,0 +1,25 @@
1
+ import type { Signal } from "./base.js"
2
+
3
+ type InferArraySignalItemType<T extends Signal<any[]>> = T extends Signal<
4
+ infer V
5
+ >
6
+ ? V extends Array<infer W>
7
+ ? W
8
+ : never
9
+ : never
10
+
11
+ type ForProps<T extends Signal<any[]>, U = InferArraySignalItemType<T>> = {
12
+ each: T
13
+ fallback?: JSX.Element
14
+ children: (value: U, index: number, array: U[]) => JSX.Element
15
+ }
16
+
17
+ export function For<T extends Signal<any[]>>({
18
+ each,
19
+ fallback,
20
+ children,
21
+ }: ForProps<T>) {
22
+ const items = each.value
23
+ if (items.length === 0) return fallback
24
+ return items.map(children)
25
+ }
@@ -8,5 +8,5 @@ export { Signal, signal, useSignal } from "./base.js"
8
8
  export { ComputedSignal, computed, useComputed } from "./computed.js"
9
9
  export { WatchEffect, watch, useWatch } from "./watch.js"
10
10
  export { unwrap, tick } from "./utils.js"
11
- export * from "./jsx.js"
11
+ export * from "./for.js"
12
12
  export * from "./types.js"
@@ -6,5 +6,9 @@ export type ReadonlySignal<T> = Signal<T> & {
6
6
  export type SignalSubscriber<T = unknown> = (value: T, prevValue?: T) => void
7
7
 
8
8
  export type SignalValues<T extends readonly Signal<unknown>[]> = {
9
- [I in keyof T]: T[I] extends Signal<infer V> ? V : never
9
+ [I in keyof T]: T[I] extends Signal<infer V>
10
+ ? V extends Kiru.StatefulPromise<infer P>
11
+ ? P
12
+ : V
13
+ : never
10
14
  }
package/src/ssr/client.ts CHANGED
@@ -10,7 +10,7 @@ interface HydrationAppContextOptions extends AppContextOptions {
10
10
  * - "dynamic": SSR with lazy promise hydration
11
11
  * @default "dynamic"
12
12
  */
13
- hydrationMode?: "static" | "dynamic"
13
+ hydrationMode?: Kiru.HydrationMode
14
14
  }
15
15
 
16
16
  export function hydrate(