kiru 0.53.0 → 0.54.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 (118) hide show
  1. package/dist/components/derive.d.ts +1 -1
  2. package/dist/components/derive.d.ts.map +1 -1
  3. package/dist/components/derive.js +3 -2
  4. package/dist/components/derive.js.map +1 -1
  5. package/dist/dom.d.ts.map +1 -1
  6. package/dist/dom.js +6 -2
  7. package/dist/dom.js.map +1 -1
  8. package/dist/globals.d.ts +1 -1
  9. package/dist/globals.d.ts.map +1 -1
  10. package/dist/globals.js.map +1 -1
  11. package/dist/hooks/usePromise.d.ts +2 -1
  12. package/dist/hooks/usePromise.d.ts.map +1 -1
  13. package/dist/hooks/usePromise.js +31 -62
  14. package/dist/hooks/usePromise.js.map +1 -1
  15. package/dist/index.js +1 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/router/client/index.d.ts +4 -2
  18. package/dist/router/client/index.d.ts.map +1 -1
  19. package/dist/router/client/index.js +59 -13
  20. package/dist/router/client/index.js.map +1 -1
  21. package/dist/router/constants.d.ts +2 -0
  22. package/dist/router/constants.d.ts.map +1 -0
  23. package/dist/router/constants.js +2 -0
  24. package/dist/router/constants.js.map +1 -0
  25. package/dist/router/context.d.ts +2 -0
  26. package/dist/router/context.d.ts.map +1 -1
  27. package/dist/router/context.js +5 -1
  28. package/dist/router/context.js.map +1 -1
  29. package/dist/router/fileRouterController.d.ts +2 -0
  30. package/dist/router/fileRouterController.d.ts.map +1 -1
  31. package/dist/router/fileRouterController.js +195 -107
  32. package/dist/router/fileRouterController.js.map +1 -1
  33. package/dist/router/globals.d.ts +3 -0
  34. package/dist/router/globals.d.ts.map +1 -1
  35. package/dist/router/globals.js +3 -0
  36. package/dist/router/globals.js.map +1 -1
  37. package/dist/router/guard.d.ts +17 -0
  38. package/dist/router/guard.d.ts.map +1 -0
  39. package/dist/router/guard.js +45 -0
  40. package/dist/router/guard.js.map +1 -0
  41. package/dist/router/head.d.ts.map +1 -1
  42. package/dist/router/head.js +5 -7
  43. package/dist/router/head.js.map +1 -1
  44. package/dist/router/index.d.ts +2 -1
  45. package/dist/router/index.d.ts.map +1 -1
  46. package/dist/router/index.js +2 -1
  47. package/dist/router/index.js.map +1 -1
  48. package/dist/router/{server → ssg}/index.d.ts +4 -3
  49. package/dist/router/ssg/index.d.ts.map +1 -0
  50. package/dist/router/{server → ssg}/index.js +8 -5
  51. package/dist/router/ssg/index.js.map +1 -0
  52. package/dist/router/ssr/index.d.ts +20 -0
  53. package/dist/router/ssr/index.d.ts.map +1 -0
  54. package/dist/router/ssr/index.js +163 -0
  55. package/dist/router/ssr/index.js.map +1 -0
  56. package/dist/router/types.d.ts +42 -16
  57. package/dist/router/types.d.ts.map +1 -1
  58. package/dist/router/types.internal.d.ts +4 -0
  59. package/dist/router/types.internal.d.ts.map +1 -1
  60. package/dist/router/utils/index.d.ts +8 -3
  61. package/dist/router/utils/index.d.ts.map +1 -1
  62. package/dist/router/utils/index.js +38 -6
  63. package/dist/router/utils/index.js.map +1 -1
  64. package/dist/scheduler.d.ts +14 -3
  65. package/dist/scheduler.d.ts.map +1 -1
  66. package/dist/scheduler.js +3 -4
  67. package/dist/scheduler.js.map +1 -1
  68. package/dist/ssr/client.d.ts +1 -1
  69. package/dist/ssr/client.d.ts.map +1 -1
  70. package/dist/ssr/server.d.ts +9 -3
  71. package/dist/ssr/server.d.ts.map +1 -1
  72. package/dist/ssr/server.js +37 -30
  73. package/dist/ssr/server.js.map +1 -1
  74. package/dist/types.d.ts +3 -0
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/utils/format.d.ts +2 -1
  77. package/dist/utils/format.d.ts.map +1 -1
  78. package/dist/utils/format.js +4 -1
  79. package/dist/utils/format.js.map +1 -1
  80. package/dist/utils/index.d.ts +1 -1
  81. package/dist/utils/index.d.ts.map +1 -1
  82. package/dist/utils/index.js +1 -1
  83. package/dist/utils/index.js.map +1 -1
  84. package/dist/utils/promise.d.ts +2 -0
  85. package/dist/utils/promise.d.ts.map +1 -1
  86. package/dist/utils/promise.js +45 -1
  87. package/dist/utils/promise.js.map +1 -1
  88. package/dist/utils/runtime.d.ts +1 -1
  89. package/dist/utils/runtime.js +1 -1
  90. package/package.json +8 -4
  91. package/src/components/derive.ts +5 -3
  92. package/src/dom.ts +5 -1
  93. package/src/globals.ts +1 -1
  94. package/src/hooks/usePromise.ts +57 -77
  95. package/src/index.ts +1 -1
  96. package/src/router/client/index.ts +114 -16
  97. package/src/router/constants.ts +1 -0
  98. package/src/router/context.ts +7 -1
  99. package/src/router/fileRouterController.ts +304 -132
  100. package/src/router/globals.ts +4 -0
  101. package/src/router/guard.ts +72 -0
  102. package/src/router/head.ts +5 -7
  103. package/src/router/index.ts +12 -1
  104. package/src/router/{server → ssg}/index.ts +17 -10
  105. package/src/router/ssr/index.ts +252 -0
  106. package/src/router/types.internal.ts +5 -0
  107. package/src/router/types.ts +53 -16
  108. package/src/router/utils/index.ts +74 -8
  109. package/src/scheduler.ts +20 -3
  110. package/src/ssr/client.ts +1 -1
  111. package/src/ssr/server.ts +58 -34
  112. package/src/types.ts +3 -0
  113. package/src/utils/format.ts +5 -0
  114. package/src/utils/index.ts +1 -1
  115. package/src/utils/promise.ts +70 -1
  116. package/src/utils/runtime.ts +1 -1
  117. package/dist/router/server/index.d.ts.map +0 -1
  118. package/dist/router/server/index.js.map +0 -1
@@ -0,0 +1,72 @@
1
+ import type { NavigationHook } from "./types"
2
+ import type { GuardModule } from "./types.internal"
3
+
4
+ export type GuardBeforeEach = NavigationHook<
5
+ void | string | Promise<void | string>
6
+ >
7
+ export type GuardAfterEach = NavigationHook<void | Promise<void>>
8
+
9
+ export interface NavGuard {
10
+ beforeEach: GuardBeforeEach
11
+ afterEach: GuardAfterEach
12
+ }
13
+
14
+ export const $NAVGUARD_INTERNAL = Symbol.for("kiru:navguard")
15
+
16
+ export function resolveNavguard(module: GuardModule): NavGuard | null {
17
+ if (
18
+ "guard" in module &&
19
+ $NAVGUARD_INTERNAL in ((module.guard ?? {}) as NavGuardBuilder)
20
+ ) {
21
+ return (module.guard as NavGuardBuilder)[$NAVGUARD_INTERNAL]
22
+ }
23
+ return null
24
+ }
25
+
26
+ export interface NavGuardBuilder {
27
+ beforeEach(...fns: GuardBeforeEach[]): NavGuardBuilder
28
+ afterEach(...fns: GuardAfterEach[]): NavGuardBuilder
29
+ get [$NAVGUARD_INTERNAL](): NavGuard
30
+ }
31
+
32
+ function createNavGuard_impl(
33
+ beforeEach: GuardBeforeEach[],
34
+ afterEach: GuardAfterEach[]
35
+ ): NavGuard {
36
+ return {
37
+ beforeEach: async (ctx, to, from) => {
38
+ for (const fn of beforeEach) {
39
+ const res = await fn(ctx, to, from)
40
+ if (typeof res === "string") {
41
+ return res
42
+ }
43
+ }
44
+ return
45
+ },
46
+ afterEach: async (ctx, to, from) => {
47
+ for (const fn of afterEach) {
48
+ await fn(ctx, to, from)
49
+ }
50
+ },
51
+ }
52
+ }
53
+
54
+ export function createNavGuard(): NavGuardBuilder {
55
+ const beforeEach: GuardBeforeEach[] = []
56
+ const afterEach: GuardAfterEach[] = []
57
+ const guard = createNavGuard_impl(beforeEach, afterEach)
58
+
59
+ return {
60
+ beforeEach(...fns: GuardBeforeEach[]) {
61
+ beforeEach.push(...fns)
62
+ return this
63
+ },
64
+ afterEach(...fns: GuardAfterEach[]) {
65
+ afterEach.push(...fns)
66
+ return this
67
+ },
68
+ get [$NAVGUARD_INTERNAL]() {
69
+ return guard
70
+ },
71
+ }
72
+ }
@@ -1,5 +1,5 @@
1
1
  import { Signal } from "../signals/base.js"
2
- import { isValidTextChild, isVNode } from "../utils/index.js"
2
+ import { isValidTextChild, isVNode, toArray } from "../utils/index.js"
3
3
  import { createElement } from "../element.js"
4
4
  import { __DEV__ } from "../env.js"
5
5
  import { KiruError } from "../error.js"
@@ -12,7 +12,7 @@ const validHeadChildren = ["title", "base", "link", "meta", "style", "script"]
12
12
  function HeadContent({ children }: { children: JSX.Children }): JSX.Element {
13
13
  if (__DEV__) {
14
14
  const n = node.current!
15
- const asArray = Array.isArray(children) ? children : [children]
15
+ const asArray = toArray(children)
16
16
  const invalidNodes = asArray.filter(
17
17
  (c) =>
18
18
  !isVNode(c) ||
@@ -29,16 +29,14 @@ function HeadContent({ children }: { children: JSX.Children }): JSX.Element {
29
29
  }
30
30
  }
31
31
  if ("window" in globalThis) {
32
- const asArray = Array.isArray(children) ? children : [children]
33
- const titleNode = asArray.find(
32
+ const c = toArray(children)
33
+ const titleNode = c.find(
34
34
  (c) => isVNode(c) && c.type === "title"
35
35
  ) as Kiru.VNode
36
36
 
37
37
  if (titleNode) {
38
38
  const props = titleNode.props
39
- const titleChildren = Array.isArray(props.children)
40
- ? props.children
41
- : [props.children]
39
+ const titleChildren = toArray(props.children)
42
40
 
43
41
  document.title = titleChildren
44
42
  .map((c) => (Signal.isSignal(c) ? c.value : c))
@@ -1,9 +1,20 @@
1
1
  import { createElement } from "../element.js"
2
2
  import { __DEV__ } from "../env.js"
3
3
 
4
- export { useFileRouter, type FileRouterContextType } from "./context.js"
4
+ export {
5
+ useRequestContext,
6
+ useFileRouter,
7
+ type FileRouterContextType,
8
+ } from "./context.js"
5
9
  export * from "./errors.js"
6
10
  export { FileRouter, type FileRouterProps } from "./fileRouter.js"
11
+ export {
12
+ createNavGuard,
13
+ type GuardBeforeEach,
14
+ type GuardAfterEach,
15
+ type NavGuard,
16
+ type NavGuardBuilder,
17
+ } from "./guard.js"
7
18
  export * from "./link.js"
8
19
  export * from "./pageConfig.js"
9
20
  export type * from "./types.js"
@@ -1,25 +1,29 @@
1
1
  import { createElement, Fragment } from "../../element.js"
2
-
3
2
  import {
4
- matchLayouts,
3
+ matchModules,
5
4
  matchRoute,
6
5
  match404Route,
7
6
  parseQuery,
8
7
  wrapWithLayouts,
9
8
  } from "../utils/index.js"
10
9
  import { RouterContext } from "../context.js"
11
- import type { PageConfig, PageProps, RouterState } from "../types.js"
12
- import { FormattedViteImportMap, PageModule } from "../types.internal.js"
13
10
  import { __DEV__ } from "../../env.js"
14
11
  import { FileRouterDataLoadError } from "../errors.js"
15
12
  import { renderToString } from "../../renderToString.js"
13
+ import type { PageConfig, PageProps, RouterState } from "../types.js"
14
+ import type {
15
+ FormattedViteImportMap,
16
+ GuardModule,
17
+ PageModule,
18
+ } from "../types.internal.js"
16
19
 
17
20
  export interface RenderContext {
18
- pages: FormattedViteImportMap
21
+ pages: FormattedViteImportMap<PageModule>
19
22
  layouts: FormattedViteImportMap
23
+ guards: FormattedViteImportMap<GuardModule>
20
24
  Document: Kiru.FC
21
25
  registerModule: (moduleId: string) => void
22
- registerPreloadedPageProps: (props: Record<string, unknown>) => void
26
+ registerStaticProps: (props: Record<string, unknown>) => void
23
27
  }
24
28
 
25
29
  export interface RenderResult {
@@ -64,7 +68,7 @@ export async function render(
64
68
 
65
69
  const { pageEntry, routeSegments, params } = routeMatch
66
70
  const is404Route = routeMatch.routeSegments.includes("404")
67
- const layoutEntries = matchLayouts(ctx.layouts, routeSegments)
71
+ const layoutEntries = matchModules(ctx.layouts, routeSegments)
68
72
 
69
73
  ;[pageEntry, ...layoutEntries].forEach((e) => {
70
74
  ctx.registerModule(e.filePath)
@@ -82,7 +86,7 @@ export async function render(
82
86
  const abortController = new AbortController()
83
87
 
84
88
  if (config.loader) {
85
- if (config.loader.mode !== "static" || __DEV__) {
89
+ if (!config.loader.static || __DEV__) {
86
90
  props = { loading: true, data: null, error: null }
87
91
  } else {
88
92
  const routerState: RouterState = {
@@ -99,7 +103,10 @@ export async function render(
99
103
  }, 10000)
100
104
 
101
105
  try {
102
- const data = await config.loader.load(routerState)
106
+ const data = await config.loader.load({
107
+ ...routerState,
108
+ context: {},
109
+ })
103
110
  props = {
104
111
  data,
105
112
  error: null,
@@ -113,7 +120,7 @@ export async function render(
113
120
  }
114
121
  } finally {
115
122
  clearTimeout(timeout)
116
- ctx.registerPreloadedPageProps({ data: props.data, error: props.error })
123
+ ctx.registerStaticProps({ data: props.data, error: props.error })
117
124
  }
118
125
  }
119
126
  }
@@ -0,0 +1,252 @@
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
+ import { createStatefulPromise } from "../../utils/promise.js"
25
+ import { PAGE_DATA_PROMISE_ID } from "../constants.js"
26
+
27
+ export interface SSRRenderContext {
28
+ pages: FormattedViteImportMap<PageModule>
29
+ layouts: FormattedViteImportMap
30
+ guards: FormattedViteImportMap<GuardModule>
31
+ Document: Kiru.FC
32
+ userContext: Kiru.RequestContext
33
+ registerModule: (moduleId: string) => void
34
+ }
35
+
36
+ export interface SSRHttpResponse {
37
+ html: string
38
+ statusCode: number
39
+ headers: Array<[string, string]>
40
+ stream: ReadableStream | null
41
+ }
42
+
43
+ export interface SSRRenderResult {
44
+ httpResponse: SSRHttpResponse | null
45
+ }
46
+
47
+ export async function render(
48
+ url: string,
49
+ ctx: SSRRenderContext
50
+ ): Promise<SSRRenderResult> {
51
+ const extName = path.extname(url)
52
+ if (extName && extName.length > 0) {
53
+ return { httpResponse: null }
54
+ } else if (url.startsWith("/@")) {
55
+ return { httpResponse: null }
56
+ }
57
+
58
+ const u = new URL(url, "http://localhost")
59
+ const pathSegments = u.pathname.split("/").filter(Boolean)
60
+ let routeMatch = matchRoute(ctx.pages, pathSegments)
61
+
62
+ if (!routeMatch) {
63
+ // Try to find a 404 page in parent directories
64
+ const fourOhFourMatch = match404Route(ctx.pages, pathSegments)
65
+ if (fourOhFourMatch) {
66
+ routeMatch = fourOhFourMatch
67
+ } else {
68
+ // Fallback to root 404 or default fallback
69
+ if (url === "/404") {
70
+ if (__DEV__) {
71
+ console.warn(
72
+ "[kiru/router]: No 404 route defined. Using fallback 404 page."
73
+ )
74
+ }
75
+ return {
76
+ httpResponse: {
77
+ statusCode: 404,
78
+ headers: [["Content-Type", "text/html"]],
79
+ html: "<!doctype html><html><head><title>Not Found</title></head><body><h1>404</h1></body></html>",
80
+ stream: null,
81
+ },
82
+ }
83
+ }
84
+ // Recursively render the 404 page
85
+ const notFoundResponse = await render("/404", ctx)
86
+ return {
87
+ httpResponse: {
88
+ html: notFoundResponse.httpResponse?.html ?? "",
89
+ headers: notFoundResponse.httpResponse?.headers ?? [
90
+ ["Content-Type", "text/html"],
91
+ ],
92
+ ...notFoundResponse,
93
+ statusCode: 404,
94
+ stream: null,
95
+ },
96
+ }
97
+ }
98
+ }
99
+ const { pageEntry, routeSegments, params } = routeMatch
100
+ const is404Route = routeMatch.routeSegments.includes("404")
101
+
102
+ const guardEntries = matchModules(ctx.guards, routeSegments)
103
+ const guardModules = await Promise.all(
104
+ guardEntries.map((entry) => entry.load() as unknown as Promise<GuardModule>)
105
+ )
106
+
107
+ const redirectPath = await runBeforeEachGuards(
108
+ guardModules,
109
+ { ...ctx.userContext },
110
+ u.pathname
111
+ )
112
+
113
+ if (redirectPath !== null) {
114
+ return createRedirectResult(redirectPath)
115
+ }
116
+
117
+ const layoutEntries = matchModules(ctx.layouts, routeSegments)
118
+
119
+ // Register all modules for CSS collection
120
+ ;[pageEntry, ...layoutEntries].forEach((e) => {
121
+ ctx.registerModule(e.filePath)
122
+ })
123
+
124
+ const [page, ...layouts] = await Promise.all([
125
+ pageEntry.load(),
126
+ ...layoutEntries.map((layoutEntry) => layoutEntry.load()),
127
+ ])
128
+
129
+ const onBeforeEnter = page.config?.hooks?.onBeforeEnter
130
+ if (onBeforeEnter) {
131
+ const redirectPath = await runBeforeEnterHooks(
132
+ toArray(onBeforeEnter),
133
+ { ...ctx.userContext },
134
+ u.pathname
135
+ )
136
+ if (redirectPath !== null) {
137
+ return createRedirectResult(redirectPath)
138
+ }
139
+ }
140
+
141
+ let documentShell = renderToString(createElement(ctx.Document))
142
+
143
+ if (
144
+ documentShell.includes("</body>") ||
145
+ !documentShell.includes("<kiru-body-outlet>")
146
+ ) {
147
+ throw new Error(
148
+ "[kiru/router]: Document is expected to contain a <Body.Outlet> element. See https://kirujs.dev/docs/api/file-router#general-usage"
149
+ )
150
+ }
151
+
152
+ const query = parseQuery(u.search)
153
+
154
+ let props = {} as PageProps<PageConfig>
155
+ const config = page.config ?? {}
156
+ const abortSignal = new AbortController().signal
157
+
158
+ const routerState: RouterState = {
159
+ params,
160
+ query,
161
+ pathname: u.pathname,
162
+ hash: "",
163
+ signal: abortSignal,
164
+ }
165
+
166
+ let pageDataPromise: Kiru.StatefulPromise<unknown> | null = null
167
+ if (config.loader && !config.loader.static) {
168
+ props = {
169
+ data: null,
170
+ error: null,
171
+ loading: true,
172
+ }
173
+ const promise = config.loader.load({
174
+ ...routerState,
175
+ context: { ...ctx.userContext },
176
+ })
177
+ pageDataPromise = createStatefulPromise(PAGE_DATA_PROMISE_ID, promise)
178
+ }
179
+
180
+ const children = wrapWithLayouts(
181
+ layouts
182
+ .map((layout) => layout.default)
183
+ .filter((l) => typeof l === "function"),
184
+ page.default,
185
+ props
186
+ )
187
+
188
+ const app = createElement(RouterContext.Provider, {
189
+ children: createElement(RequestContext.Provider, {
190
+ children: Fragment({ children }),
191
+ value: ctx.userContext,
192
+ }),
193
+ value: { state: routerState },
194
+ })
195
+
196
+ let { immediate: pageOutletContent, stream } = renderToReadableStream(app, {
197
+ data: pageDataPromise ? [pageDataPromise] : [],
198
+ })
199
+
200
+ const hasHeadContent = pageOutletContent.includes("<kiru-head-content>")
201
+ const hasHeadOutlet = documentShell.includes("<kiru-head-outlet>")
202
+
203
+ if (hasHeadOutlet && hasHeadContent) {
204
+ let [preHeadContent = "", headContentInner = "", postHeadContent = ""] =
205
+ pageOutletContent.split(/<kiru-head-content>|<\/kiru-head-content>/)
206
+
207
+ documentShell = documentShell.replace(
208
+ "<kiru-head-outlet>",
209
+ headContentInner
210
+ )
211
+ pageOutletContent = `${preHeadContent}${postHeadContent}`
212
+ } else if (hasHeadContent) {
213
+ // remove head content element and everything within it
214
+ pageOutletContent = pageOutletContent.replace(
215
+ /<kiru-head-content>(.*?)<\/kiru-head-content>/,
216
+ ""
217
+ )
218
+ } else if (hasHeadOutlet) {
219
+ // remove head outlet element and everything within it
220
+ documentShell = documentShell.replaceAll("<kiru-head-outlet>", "")
221
+ }
222
+
223
+ const [prePageOutlet, postPageOutlet] =
224
+ documentShell.split("<kiru-body-outlet>")
225
+
226
+ const html = `<!DOCTYPE html>${prePageOutlet}<body>${pageOutletContent}</body>${postPageOutlet}`
227
+ const statusCode = is404Route ? 404 : 200
228
+
229
+ queueMicrotask(() => {
230
+ runAfterEachGuards(guardModules, { ...ctx.userContext }, u.pathname)
231
+ })
232
+
233
+ return {
234
+ httpResponse: {
235
+ html,
236
+ statusCode,
237
+ headers: [["Content-Type", "text/html;charset=utf-8"]],
238
+ stream,
239
+ },
240
+ }
241
+ }
242
+
243
+ function createRedirectResult(to: string): SSRRenderResult {
244
+ return {
245
+ httpResponse: {
246
+ statusCode: 302,
247
+ headers: [["Location", to]],
248
+ html: "",
249
+ stream: null,
250
+ },
251
+ }
252
+ }
@@ -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,14 +3,17 @@ 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>
16
+ pagePropsPromise?: Promise<AsyncTaskState<unknown, FileRouterDataLoadError>>
14
17
  pageLayouts: DefaultComponentModule[]
15
18
  params: RouteParams
16
19
  query: RouteQuery
@@ -40,6 +43,14 @@ export interface FileRouterConfig {
40
43
  * ```
41
44
  */
42
45
  layouts: Record<string, unknown>
46
+ /**
47
+ * The import map to use for loading nav guards
48
+ * @example
49
+ * ```tsx
50
+ * <FileRouter config={{ guards: import.meta.glob("/∗∗/guard.{ts,js}"), ... }} />
51
+ * ```
52
+ */
53
+ guards?: Record<string, unknown>
43
54
 
44
55
  /**
45
56
  * The base url to use as a prefix for route matching
@@ -98,28 +109,40 @@ export interface RouterState {
98
109
  signal: AbortSignal
99
110
  }
100
111
 
101
- type PageDataLoaderContext = RouterState & {}
102
-
103
112
  export interface PageDataLoaderCacheConfig {
104
113
  type: "memory" | "localStorage" | "sessionStorage"
105
114
  ttl: number
106
115
  }
107
116
 
117
+ interface LoaderContext extends RouterState {
118
+ /**
119
+ * The request context - in SSR, this is the data from the server
120
+ * that's passed to the `renderPage` function
121
+ * @example
122
+ * ```ts
123
+ * // server.ts
124
+ * renderPage({ url, context: { test: 123 } })
125
+ *
126
+ * // page.tsx
127
+ * loader: {
128
+ * load: ({ context }) => context.test
129
+ * }
130
+ * ```
131
+ */
132
+ context: Kiru.RequestContext
133
+ }
134
+
108
135
  export type PageDataLoaderConfig<T = unknown> = {
109
136
  /**
110
137
  * The function to load the page data
111
138
  */
112
- load: (context: PageDataLoaderContext) => Promise<T>
139
+ load: (context: LoaderContext) => Promise<T>
113
140
  } & (
114
141
  | {
115
142
  /**
116
- * The mode to use for the page data loader
117
- * @default "client"
118
- * @description
119
- * - **static**: The page data is loaded at build time and never updated
120
- * - **client**: The page data is loaded upon navigation and updated on subsequent navigations
143
+ * Indicates that the page data should only be loaded at build time
121
144
  */
122
- mode?: "client"
145
+ static?: false
123
146
  /**
124
147
  * Enable transitions when swapping between "load", "error" and "data" states
125
148
  */
@@ -139,16 +162,28 @@ export type PageDataLoaderConfig<T = unknown> = {
139
162
  }
140
163
  | {
141
164
  /**
142
- * The mode to use for the page data loader
143
- * @default "client"
144
- * @description
145
- * - **static**: The page data is loaded at build time and never updated
146
- * - **client**: The page data is loaded upon navigation and updated on subsequent navigations
165
+ * Indicates that the page data should only be loaded at build time
147
166
  */
148
- mode: "static"
167
+ static: true
149
168
  }
150
169
  )
151
170
 
171
+ export type NavigationHook<T> = (
172
+ context: Kiru.RequestContext,
173
+ to: string,
174
+ from: string
175
+ ) => T
176
+
177
+ export type OnBeforeEnterHook = NavigationHook<
178
+ string | void | Promise<string | void>
179
+ >
180
+ export type OnBeforeLeaveHook = NavigationHook<false | void>
181
+
182
+ interface PageContextHooks {
183
+ onBeforeEnter?: OnBeforeEnterHook | OnBeforeEnterHook[]
184
+ onBeforeLeave?: OnBeforeLeaveHook | OnBeforeLeaveHook[]
185
+ }
186
+
152
187
  export interface PageConfig<T = unknown> {
153
188
  /**
154
189
  * The loader configuration for this page
@@ -159,6 +194,8 @@ export interface PageConfig<T = unknown> {
159
194
  * returned, a page will be generated
160
195
  */
161
196
  generateStaticParams?: () => RouteParams[] | Promise<RouteParams[]>
197
+
198
+ hooks?: PageContextHooks
162
199
  }
163
200
 
164
201
  export type PageProps<T extends PageConfig<any>> = T extends PageConfig<infer U>