kiru 0.51.0-preview.1 → 0.51.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 (62) 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/router/cache.d.ts +71 -0
  5. package/dist/router/cache.d.ts.map +1 -0
  6. package/dist/router/cache.js +325 -0
  7. package/dist/router/cache.js.map +1 -0
  8. package/dist/router/client/index.d.ts.map +1 -1
  9. package/dist/router/client/index.js +62 -1
  10. package/dist/router/client/index.js.map +1 -1
  11. package/dist/router/context.d.ts +8 -0
  12. package/dist/router/context.d.ts.map +1 -1
  13. package/dist/router/context.js.map +1 -1
  14. package/dist/router/fileRouterController.d.ts +1 -0
  15. package/dist/router/fileRouterController.d.ts.map +1 -1
  16. package/dist/router/fileRouterController.js +124 -36
  17. package/dist/router/fileRouterController.js.map +1 -1
  18. package/dist/router/globals.d.ts +4 -0
  19. package/dist/router/globals.d.ts.map +1 -1
  20. package/dist/router/globals.js +3 -0
  21. package/dist/router/globals.js.map +1 -1
  22. package/dist/router/head.d.ts +4 -36
  23. package/dist/router/head.d.ts.map +1 -1
  24. package/dist/router/head.js +33 -53
  25. package/dist/router/head.js.map +1 -1
  26. package/dist/router/index.d.ts +57 -3
  27. package/dist/router/index.d.ts.map +1 -1
  28. package/dist/router/index.js +67 -3
  29. package/dist/router/index.js.map +1 -1
  30. package/dist/router/server/index.d.ts +1 -3
  31. package/dist/router/server/index.d.ts.map +1 -1
  32. package/dist/router/server/index.js +23 -20
  33. package/dist/router/server/index.js.map +1 -1
  34. package/dist/router/types.d.ts +31 -3
  35. package/dist/router/types.d.ts.map +1 -1
  36. package/dist/router/types.internal.d.ts +9 -8
  37. package/dist/router/types.internal.d.ts.map +1 -1
  38. package/dist/router/utils/index.d.ts +1 -6
  39. package/dist/router/utils/index.d.ts.map +1 -1
  40. package/dist/router/utils/index.js +1 -1
  41. package/dist/router/utils/index.js.map +1 -1
  42. package/dist/types.dom.d.ts +2 -1
  43. package/dist/types.dom.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/constants.ts +1 -0
  46. package/src/router/cache.ts +385 -0
  47. package/src/router/client/index.ts +81 -1
  48. package/src/router/context.ts +8 -0
  49. package/src/router/fileRouterController.ts +140 -47
  50. package/src/router/globals.ts +5 -0
  51. package/src/router/head.ts +44 -54
  52. package/src/router/index.ts +70 -3
  53. package/src/router/server/index.ts +33 -24
  54. package/src/router/types.internal.ts +10 -8
  55. package/src/router/types.ts +43 -13
  56. package/src/router/utils/index.ts +1 -1
  57. package/src/types.dom.ts +5 -1
  58. package/dist/router/dev/index.d.ts +0 -2
  59. package/dist/router/dev/index.d.ts.map +0 -1
  60. package/dist/router/dev/index.js +0 -46
  61. package/dist/router/dev/index.js.map +0 -1
  62. package/src/router/dev/index.ts +0 -63
@@ -1,9 +1,9 @@
1
1
  import { Signal } from "../signals/base.js"
2
- import { flushSync } from "../scheduler.js"
2
+ import { flushSync, nextIdle } from "../scheduler.js"
3
3
  import { __DEV__ } from "../env.js"
4
4
  import { type FileRouterContextType } from "./context.js"
5
5
  import { FileRouterDataLoadError } from "./errors.js"
6
- import { fileRouterInstance, fileRouterRoute } from "./globals.js"
6
+ import { fileRouterInstance, fileRouterRoute, routerCache } from "./globals.js"
7
7
  import type {
8
8
  FileRouterConfig,
9
9
  PageConfig,
@@ -26,6 +26,7 @@ import {
26
26
  parseQuery,
27
27
  wrapWithLayouts,
28
28
  } from "./utils/index.js"
29
+ import { RouterCache, type CacheKey } from "./cache.js"
29
30
 
30
31
  interface PageConfigWithLoader<T = unknown> extends PageConfig {
31
32
  loader: PageDataLoaderConfig<T>
@@ -34,7 +35,7 @@ interface PageConfigWithLoader<T = unknown> extends PageConfig {
34
35
  export class FileRouterController {
35
36
  public contextValue: FileRouterContextType
36
37
  private enableTransitions: boolean
37
- private pages: FormattedViteImportMap
38
+ private pages: FormattedViteImportMap<PageModule>
38
39
  private layouts: FormattedViteImportMap
39
40
  private abortController: AbortController
40
41
  private currentPage: Signal<{
@@ -53,6 +54,7 @@ export class FileRouterController {
53
54
  private pageRouteToConfig?: Map<string, PageConfig>
54
55
 
55
56
  constructor() {
57
+ routerCache.current ??= new RouterCache()
56
58
  this.enableTransitions = false
57
59
  this.pages = {}
58
60
  this.layouts = {}
@@ -68,6 +70,7 @@ export class FileRouterController {
68
70
  }
69
71
  const __this = this
70
72
  this.contextValue = {
73
+ invalidate: this.invalidate.bind(this),
71
74
  get state() {
72
75
  return __this.state
73
76
  },
@@ -114,11 +117,12 @@ export class FileRouterController {
114
117
  route,
115
118
  params,
116
119
  query,
120
+ cacheData,
117
121
  } = preloaded
118
122
  this.state = {
119
123
  params,
120
124
  query,
121
- path: route,
125
+ path: window.location.pathname,
122
126
  signal: this.abortController.signal,
123
127
  }
124
128
  this.currentPage.value = {
@@ -139,20 +143,30 @@ export class FileRouterController {
139
143
  validateRoutes(this.pages)
140
144
  }
141
145
  const loader = page.config?.loader
142
- if (__DEV__) {
143
- if (loader) {
146
+ if (loader && loader.mode !== "static" && pageProps.loading === true) {
147
+ if (cacheData === null) {
144
148
  this.loadRouteData(
145
149
  page.config as PageConfigWithLoader,
146
150
  pageProps,
147
151
  this.state
148
152
  )
153
+ } else {
154
+ nextIdle(() => {
155
+ const props = {
156
+ ...pageProps,
157
+ data: cacheData.value,
158
+ error: null,
159
+ loading: false,
160
+ }
161
+ let transition = this.enableTransitions
162
+ if (loader.transition !== undefined) {
163
+ transition = loader.transition
164
+ }
165
+ handleStateTransition(this.state.signal, transition, () => {
166
+ this.currentPageProps.value = props
167
+ })
168
+ })
149
169
  }
150
- } else if (loader && loader.mode !== "static") {
151
- this.loadRouteData(
152
- page.config as PageConfigWithLoader,
153
- pageProps,
154
- this.state
155
- )
156
170
  }
157
171
  } else {
158
172
  this.pages = formatViteImportMap(
@@ -182,28 +196,55 @@ export class FileRouterController {
182
196
  return
183
197
  }
184
198
  const curPage = this.currentPage.value
185
- if (curPage?.route === existing.route && config.loader) {
199
+ const loader = config.loader
200
+ if (curPage?.route === existing.route && loader) {
186
201
  const p = this.currentPageProps.value
187
202
  let transition = this.enableTransitions
188
- if (config.loader.transition !== undefined) {
189
- transition = config.loader.transition
203
+ if (loader.mode !== "static" && loader.transition !== undefined) {
204
+ transition = loader.transition
190
205
  }
191
- const props = {
192
- ...p,
193
- loading: true,
194
- data: null,
195
- error: null,
206
+
207
+ // Check cache first if caching is enabled
208
+ let cachedData = null
209
+ if (loader.mode !== "static" && loader.cache) {
210
+ const cacheKey: CacheKey = {
211
+ path: this.state.path,
212
+ params: this.state.params,
213
+ query: this.state.query,
214
+ }
215
+ cachedData = routerCache.current!.get(cacheKey, loader.cache)
196
216
  }
197
- handleStateTransition(this.state.signal, transition, () => {
198
- this.currentPageProps.value = props
199
- })
200
217
 
201
- this.loadRouteData(
202
- config as PageConfigWithLoader,
203
- props,
204
- this.state,
205
- transition
206
- )
218
+ if (cachedData !== null) {
219
+ // Use cached data immediately - no loading state needed
220
+ const props = {
221
+ ...p,
222
+ data: cachedData.value,
223
+ error: null,
224
+ loading: false,
225
+ }
226
+ handleStateTransition(this.state.signal, transition, () => {
227
+ this.currentPageProps.value = props
228
+ })
229
+ } else {
230
+ // No cached data - show loading state and load data
231
+ const props = {
232
+ ...p,
233
+ loading: true,
234
+ data: null,
235
+ error: null,
236
+ }
237
+ handleStateTransition(this.state.signal, transition, () => {
238
+ this.currentPageProps.value = props
239
+ })
240
+
241
+ this.loadRouteData(
242
+ config as PageConfigWithLoader,
243
+ props,
244
+ this.state,
245
+ transition
246
+ )
247
+ }
207
248
  }
208
249
 
209
250
  this.pageRouteToConfig?.set(existing.route, config)
@@ -269,7 +310,7 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
269
310
  )
270
311
 
271
312
  const [page, ...layouts] = await Promise.all([
272
- pagePromise as Promise<PageModule>,
313
+ pagePromise,
273
314
  ...layoutPromises,
274
315
  ])
275
316
 
@@ -301,19 +342,41 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
301
342
 
302
343
  if (loader) {
303
344
  if (loader.mode !== "static" || __DEV__) {
304
- props = {
305
- ...props,
306
- loading: true,
307
- data: null,
308
- error: null,
309
- } satisfies PageProps<PageConfig<unknown>>
345
+ // Check cache first if caching is enabled
346
+ let cachedData = null
347
+ if (loader.mode !== "static" && loader.cache) {
348
+ const cacheKey: CacheKey = {
349
+ path: routerState.path,
350
+ params: routerState.params,
351
+ query: routerState.query,
352
+ }
353
+ cachedData = routerCache.current!.get(cacheKey, loader.cache)
354
+ }
310
355
 
311
- this.loadRouteData(
312
- config as PageConfigWithLoader,
313
- props,
314
- routerState,
315
- enableTransition
316
- )
356
+ if (cachedData !== null) {
357
+ // Use cached data immediately - no loading state needed
358
+ props = {
359
+ ...props,
360
+ data: cachedData.value,
361
+ error: null,
362
+ loading: false,
363
+ } satisfies PageProps<PageConfig<unknown>>
364
+ } else {
365
+ // No cached data - show loading state and load data
366
+ props = {
367
+ ...props,
368
+ loading: true,
369
+ data: null,
370
+ error: null,
371
+ } satisfies PageProps<PageConfig<unknown>>
372
+
373
+ this.loadRouteData(
374
+ config as PageConfigWithLoader,
375
+ props,
376
+ routerState,
377
+ enableTransition
378
+ )
379
+ }
317
380
  } else {
318
381
  const staticProps = page.__KIRU_STATIC_PROPS__?.[path]
319
382
  if (!staticProps) {
@@ -355,15 +418,28 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
355
418
  enableTransition = this.enableTransitions
356
419
  ) {
357
420
  const { loader } = config
421
+
422
+ // Load data from loader (cache check is now done earlier in loadRoute)
358
423
  loader
359
424
  .load(routerState)
360
425
  .then(
361
- (data) =>
362
- ({
426
+ (data) => {
427
+ // Cache the data if caching is enabled
428
+ if (loader.mode !== "static" && loader.cache) {
429
+ const cacheKey: CacheKey = {
430
+ path: routerState.path,
431
+ params: routerState.params,
432
+ query: routerState.query,
433
+ }
434
+ routerCache.current!.set(cacheKey, data, loader.cache)
435
+ }
436
+
437
+ return {
363
438
  data,
364
439
  error: null,
365
440
  loading: false,
366
- } satisfies PageProps<PageConfig<unknown>>),
441
+ } satisfies PageProps<PageConfig<unknown>>
442
+ },
367
443
  (error) =>
368
444
  ({
369
445
  data: null,
@@ -375,7 +451,7 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
375
451
  if (routerState.signal.aborted) return
376
452
 
377
453
  let transition = enableTransition
378
- if (loader.transition !== undefined) {
454
+ if (loader.mode !== "static" && loader.transition !== undefined) {
379
455
  transition = loader.transition
380
456
  }
381
457
 
@@ -388,6 +464,23 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
388
464
  })
389
465
  }
390
466
 
467
+ private invalidate(...paths: string[]) {
468
+ // Invalidate cache entries
469
+ routerCache.current!.invalidate(...paths)
470
+
471
+ // Check if current page matches any invalidated paths
472
+ const currentPath = this.state.path
473
+ const shouldRefresh = routerCache.current!.pathMatchesPattern(
474
+ currentPath,
475
+ paths
476
+ )
477
+
478
+ if (shouldRefresh) {
479
+ // Refresh the current page to get fresh data
480
+ this.loadRoute(currentPath, {}, this.enableTransitions)
481
+ }
482
+ }
483
+
391
484
  private async navigate(
392
485
  path: string,
393
486
  options?: {
@@ -482,7 +575,7 @@ function validateRoutes(pageMap: FormattedViteImportMap) {
482
575
  let warning = "[kiru/router]: Route conflicts detected:\n"
483
576
  warning += routeConflicts
484
577
  .map(([route1, route2]) => {
485
- return ` - "${route1.filePath}" conflicts with "${route2.filePath}"\n`
578
+ return ` - "${route1.absolutePath}" conflicts with "${route2.absolutePath}"\n`
486
579
  })
487
580
  .join("")
488
581
  warning += "Routes are ordered by specificity (higher specificity wins)"
@@ -1,3 +1,4 @@
1
+ import type { RouterCache } from "./cache"
1
2
  import type { FileRouterController } from "./fileRouterController"
2
3
 
3
4
  export const fileRouterInstance = {
@@ -7,3 +8,7 @@ export const fileRouterInstance = {
7
8
  export const fileRouterRoute = {
8
9
  current: null as string | null,
9
10
  }
11
+
12
+ export const routerCache = {
13
+ current: null as RouterCache | null,
14
+ }
@@ -1,66 +1,56 @@
1
- import { createElement } from "../index.js"
2
1
  import { Signal } from "../signals/base.js"
3
2
  import { isValidTextChild, isVNode } from "../utils/index.js"
3
+ import { createElement } from "../element.js"
4
+ import { __DEV__ } from "../env.js"
5
+ import { KiruError } from "../error.js"
6
+ import { node } from "../globals.js"
4
7
 
5
- export { Content, Outlet }
8
+ export { HeadContent, HeadOutlet }
6
9
 
7
- /**
8
- * Used with SSG. Renders content to the document head via a corresponding `<Head.Outlet>` component placed in your `document.tsx`.
9
- * @example
10
- * // src/pages/index.tsx
11
- * export default function Index() {
12
- * return (
13
- * <div>
14
- * <Head.Content>
15
- * <title>My App - Home</title>
16
- * </Head.Content>
17
- * <h1>Home</h1>
18
- * </div>
19
- * )
20
- }
21
- */
22
- function Content({ children }: { children: JSX.Children }) {
23
- if ("window" in globalThis) {
24
- ;(Array.isArray(children) ? children : [children])
25
- .filter(isVNode)
26
- .forEach(({ type, props }) => {
27
- switch (type) {
28
- case "title":
29
- const title = (
30
- Array.isArray(props.children) ? props.children : [props.children]
31
- )
32
- .map((c) => (Signal.isSignal(c) ? c.value : c))
33
- .filter(isValidTextChild)
34
- .join("")
35
- return (document.title = title)
36
- case "meta":
37
- return document
38
- .querySelector(`meta[name="${props.name}"]`)
39
- ?.setAttribute("content", String(props.content))
40
- }
10
+ const validHeadChildren = ["title", "base", "link", "meta", "style", "script"]
11
+
12
+ function HeadContent({ children }: { children: JSX.Children }): JSX.Element {
13
+ if (__DEV__) {
14
+ const n = node.current!
15
+ const asArray = Array.isArray(children) ? children : [children]
16
+ const invalidNodes = asArray.filter(
17
+ (c) =>
18
+ !isVNode(c) ||
19
+ typeof c.type !== "string" ||
20
+ !validHeadChildren.includes(c.type)
21
+ )
22
+ if (invalidNodes.length) {
23
+ throw new KiruError({
24
+ message: `[kiru/router]: <Head.Content> only accepts title, base, link, meta, style and script elements as children. Received: ${invalidNodes.map(
25
+ (n) => (isVNode(n) ? `<${n.type.toString()}>` : `"${n}"`)
26
+ )}`,
27
+ vNode: n,
41
28
  })
29
+ }
30
+ }
31
+ if ("window" in globalThis) {
32
+ const asArray = Array.isArray(children) ? children : [children]
33
+ const titleNode = asArray.find(
34
+ (c) => isVNode(c) && c.type === "title"
35
+ ) as Kiru.VNode
36
+
37
+ if (titleNode) {
38
+ const props = titleNode.props
39
+ const titleChildren = Array.isArray(props.children)
40
+ ? props.children
41
+ : [props.children]
42
+
43
+ document.title = titleChildren
44
+ .map((c) => (Signal.isSignal(c) ? c.value : c))
45
+ .filter(isValidTextChild)
46
+ .join("")
47
+ }
48
+
42
49
  return null
43
50
  }
44
51
  return createElement("kiru-head-content", { children })
45
52
  }
46
53
 
47
- /**
48
- * Used with SSG. Renders content to the document head from a `<Head>` component in the currently rendered page.
49
- * @example
50
- * // document.tsx
51
- * export default function Document() {
52
- * return (
53
- * <html lang="en">
54
- * <head>
55
- * <meta charset="utf-8" />
56
- * <meta name="viewport" content="width=device-width, initial-scale=1" />
57
- * <Head.Outlet />
58
- * </head>
59
- * <body>{children}</body>
60
- * </html>
61
- * )
62
- }
63
- */
64
- function Outlet() {
54
+ function HeadOutlet(): JSX.Element {
65
55
  return createElement("kiru-head-outlet")
66
56
  }
@@ -1,3 +1,6 @@
1
+ import { createElement } from "../element.js"
2
+ import { __DEV__ } from "../env.js"
3
+
1
4
  export { useFileRouter, type FileRouterContextType } from "./context.js"
2
5
  export * from "./errors.js"
3
6
  export { FileRouter, type FileRouterProps } from "./fileRouter.js"
@@ -5,9 +8,73 @@ export * from "./link.js"
5
8
  export * from "./pageConfig.js"
6
9
  export type * from "./types.js"
7
10
 
8
- import { Content, Outlet } from "./head.js"
11
+ import { HeadContent, HeadOutlet } from "./head.js"
9
12
 
10
13
  export const Head = {
11
- Content,
12
- Outlet,
14
+ /**
15
+ * - During SSG, renders content to the document head via a corresponding `<Head.Outlet>` component placed in your `document.tsx`.
16
+ * - During CSR, updates document title.
17
+ * @example
18
+ * // src/pages/index.tsx
19
+ * export default function Index() {
20
+ * return (
21
+ * <div>
22
+ * <Head.Content>
23
+ * <title>My App - Home</title>
24
+ * </Head.Content>
25
+ * <h1>Home</h1>
26
+ * </div>
27
+ * )
28
+ }
29
+ */
30
+ Content: HeadContent,
31
+ /**
32
+ * Used with SSG. Renders content to the document head from a `<Head>` component in the currently rendered page.
33
+ * @example
34
+ * // src/pages/document.tsx
35
+ * export default function Document() {
36
+ * return (
37
+ * <html lang="en">
38
+ * <head>
39
+ * <meta charset="utf-8" />
40
+ * <meta name="viewport" content="width=device-width, initial-scale=1" />
41
+ * <Head.Outlet />
42
+ * </head>
43
+ * <Body.Outlet />
44
+ * </html>
45
+ * )
46
+ }
47
+ */
48
+ Outlet: HeadOutlet,
49
+ }
50
+
51
+ export const Body = {
52
+ /**
53
+ * Used with SSG 'document' files. Renders content to the document body via a corresponding `<Body.Outlet>` component placed in your `document.tsx`.
54
+ * @example
55
+ * // src/pages/document.tsx
56
+ * export default function Document() {
57
+ * return (
58
+ * <html lang="en">
59
+ * <head>
60
+ * <meta charset="utf-8" />
61
+ * <meta name="viewport" content="width=device-width, initial-scale=1" />
62
+ * <Head.Outlet />
63
+ * </head>
64
+ * <Body.Outlet />
65
+ * </html>
66
+ * )
67
+ }
68
+ */
69
+ Outlet: BodyOutlet,
70
+ }
71
+
72
+ function BodyOutlet() {
73
+ return createElement("kiru-body-outlet")
74
+ }
75
+
76
+ if (__DEV__) {
77
+ ;(Head.Content as Kiru.FC).displayName = "Kiru.Head.Content"
78
+ ;(Head.Outlet as Kiru.FC).displayName = "Kiru.Head.Outlet"
79
+ ;(Body.Outlet as Kiru.FC).displayName = "Kiru.Body.Outlet"
13
80
  }
@@ -1,5 +1,5 @@
1
- import { createElement } from "../../element.js"
2
- import { renderToReadableStream } from "../../ssr/server.js"
1
+ import { createElement, Fragment } from "../../element.js"
2
+
3
3
  import {
4
4
  matchLayouts,
5
5
  matchRoute,
@@ -9,10 +9,10 @@ import {
9
9
  } from "../utils/index.js"
10
10
  import { RouterContext } from "../context.js"
11
11
  import type { PageConfig, PageProps, RouterState } from "../types.js"
12
- import type { Readable } from "node:stream"
13
12
  import { FormattedViteImportMap, PageModule } from "../types.internal.js"
14
13
  import { __DEV__ } from "../../env.js"
15
14
  import { FileRouterDataLoadError } from "../errors.js"
15
+ import { renderToString } from "../../renderToString.js"
16
16
 
17
17
  export interface RenderContext {
18
18
  pages: FormattedViteImportMap
@@ -24,8 +24,7 @@ export interface RenderContext {
24
24
 
25
25
  export interface RenderResult {
26
26
  status: number
27
- immediate: string
28
- stream: Readable | null
27
+ body: string
29
28
  }
30
29
 
31
30
  export async function render(
@@ -52,15 +51,12 @@ export async function render(
52
51
  }
53
52
  return {
54
53
  status: 404,
55
- immediate:
56
- "<!doctype html><html><head><title>Not Found</title></head><body><h1>404</h1></body></html>",
57
- stream: null,
54
+ body: "<!doctype html><html><head><title>Not Found</title></head><body><h1>404</h1></body></html>",
58
55
  }
59
56
  }
60
57
  return render("/404", ctx, {
61
58
  ...(result ?? {}),
62
- immediate: "",
63
- stream: null,
59
+ body: "",
64
60
  status: 404,
65
61
  })
66
62
  }
@@ -72,7 +68,7 @@ export async function render(
72
68
 
73
69
  if (__DEV__) {
74
70
  ;[pageEntry, ...layoutEntries].forEach((e) => {
75
- ctx.registerModule(e.filePath!)
71
+ ctx.registerModule(e.absolutePath!)
76
72
  })
77
73
  }
78
74
 
@@ -131,8 +127,19 @@ export async function render(
131
127
  props
132
128
  )
133
129
 
130
+ let documentShell = renderToString(createElement(ctx.Document))
131
+
132
+ if (
133
+ documentShell.includes("</body>") ||
134
+ !documentShell.includes("<kiru-body-outlet>")
135
+ ) {
136
+ throw new Error(
137
+ "[kiru/router]: Document is expected to contain a <Body.Outlet> element. See https://kirujs.dev/docs/api/file-router#ssg"
138
+ )
139
+ }
140
+
134
141
  const app = createElement(RouterContext.Provider, {
135
- children: createElement(ctx.Document, { children }),
142
+ children: Fragment({ children }),
136
143
  value: {
137
144
  state: {
138
145
  params,
@@ -143,36 +150,38 @@ export async function render(
143
150
  },
144
151
  })
145
152
 
146
- let { immediate, stream } = renderToReadableStream(app)
147
- const hasHeadOutlet = immediate.includes("<kiru-head-outlet>")
148
- const hasHeadContent = immediate.includes("<kiru-head-content>")
153
+ let pageOutletContent = renderToString(app)
154
+ const hasHeadContent = pageOutletContent.includes("<kiru-head-content>")
155
+ const hasHeadOutlet = documentShell.includes("<kiru-head-outlet>")
149
156
 
150
157
  if (hasHeadOutlet && hasHeadContent) {
151
158
  let [preHeadContent = "", headContentInner = "", postHeadContent = ""] =
152
- immediate.split(/<kiru-head-content>|<\/kiru-head-content>/)
159
+ pageOutletContent.split(/<kiru-head-content>|<\/kiru-head-content>/)
153
160
 
154
- preHeadContent = preHeadContent.replace(
161
+ documentShell = documentShell.replace(
155
162
  "<kiru-head-outlet>",
156
163
  headContentInner
157
164
  )
158
- immediate = `${preHeadContent}${postHeadContent}`
165
+ pageOutletContent = `${preHeadContent}${postHeadContent}`
159
166
  } else if (hasHeadContent) {
160
167
  // remove head content element and everything within it
161
- immediate = immediate.replace(
168
+ pageOutletContent = pageOutletContent.replace(
162
169
  /<kiru-head-content>(.*?)<\/kiru-head-content>/,
163
170
  ""
164
171
  )
165
172
  } else if (hasHeadOutlet) {
166
173
  // remove head outlet element and everything within it
167
- immediate = immediate.replaceAll("<kiru-head-outlet>", "")
174
+ documentShell = documentShell.replaceAll("<kiru-head-outlet>", "")
168
175
  }
169
176
 
177
+ const [prePageOutlet, postPageOutlet] =
178
+ documentShell.split("<kiru-body-outlet>")
179
+
170
180
  // console.log("immediate", immediate)
171
181
 
172
182
  return {
173
183
  status: is404Route ? 404 : result?.status ?? 200,
174
- immediate: "<!doctype html>" + immediate,
175
- stream,
184
+ body: `<!doctype html>${prePageOutlet}<body>${pageOutletContent}</body>${postPageOutlet}`,
176
185
  }
177
186
  }
178
187
 
@@ -190,7 +199,7 @@ export async function generateStaticPaths(pages: FormattedViteImportMap) {
190
199
 
191
200
  const hasDynamic = urlSegments.some((s) => s.startsWith(":"))
192
201
  if (!hasDynamic) {
193
- results[basePath === "" ? "/" : basePath] = entry.filePath
202
+ results[basePath === "" ? "/" : basePath] = entry.absolutePath
194
203
  continue
195
204
  }
196
205
  try {
@@ -206,7 +215,7 @@ export async function generateStaticPaths(pages: FormattedViteImportMap) {
206
215
  const value = params[key]
207
216
  p = p.replace(`:${key}*`, value).replace(`:${key}`, value)
208
217
  }
209
- results[p] = entry.filePath
218
+ results[p] = entry.absolutePath
210
219
  }
211
220
  } catch {}
212
221
  }
@@ -17,18 +17,20 @@ export interface ViteImportMap {
17
17
  [fp: string]: () => Promise<DefaultComponentModule>
18
18
  }
19
19
 
20
- export interface FormattedViteImportMap {
21
- [key: string]: {
22
- load: () => Promise<DefaultComponentModule>
23
- specificity: number
24
- segments: string[]
25
- filePath: string
26
- }
20
+ export interface FormattedViteImportMapEntry<T = DefaultComponentModule> {
21
+ load: () => Promise<T>
22
+ specificity: number
23
+ segments: string[]
24
+ absolutePath: string
25
+ }
26
+
27
+ export interface FormattedViteImportMap<T = DefaultComponentModule> {
28
+ [key: string]: FormattedViteImportMapEntry<T>
27
29
  }
28
30
 
29
31
  export interface RouteMatch {
30
32
  route: string
31
- pageEntry: FormattedViteImportMap[string]
33
+ pageEntry: FormattedViteImportMapEntry<PageModule>
32
34
  params: Record<string, string>
33
35
  routeSegments: string[]
34
36
  }