kiru 0.51.0-preview.1 → 0.51.0-preview.2

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 (48) 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 +15 -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/index.d.ts +3 -0
  23. package/dist/router/index.d.ts.map +1 -1
  24. package/dist/router/index.js +6 -0
  25. package/dist/router/index.js.map +1 -1
  26. package/dist/router/server/index.d.ts.map +1 -1
  27. package/dist/router/server/index.js +20 -14
  28. package/dist/router/server/index.js.map +1 -1
  29. package/dist/router/types.d.ts +31 -3
  30. package/dist/router/types.d.ts.map +1 -1
  31. package/dist/router/types.internal.d.ts +9 -8
  32. package/dist/router/types.internal.d.ts.map +1 -1
  33. package/dist/router/utils/index.d.ts +1 -6
  34. package/dist/router/utils/index.d.ts.map +1 -1
  35. package/dist/router/utils/index.js +1 -1
  36. package/dist/router/utils/index.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/constants.ts +1 -0
  39. package/src/router/cache.ts +385 -0
  40. package/src/router/client/index.ts +18 -1
  41. package/src/router/context.ts +8 -0
  42. package/src/router/fileRouterController.ts +140 -47
  43. package/src/router/globals.ts +5 -0
  44. package/src/router/index.ts +8 -0
  45. package/src/router/server/index.ts +30 -14
  46. package/src/router/types.internal.ts +10 -8
  47. package/src/router/types.ts +43 -13
  48. package/src/router/utils/index.ts +1 -1
@@ -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,3 +1,5 @@
1
+ import { createElement } from "../element.js"
2
+
1
3
  export { useFileRouter, type FileRouterContextType } from "./context.js"
2
4
  export * from "./errors.js"
3
5
  export { FileRouter, type FileRouterProps } from "./fileRouter.js"
@@ -11,3 +13,9 @@ export const Head = {
11
13
  Content,
12
14
  Outlet,
13
15
  }
16
+
17
+ export const Body = {
18
+ Outlet: function BodyOutlet() {
19
+ return createElement("kiru-body-outlet")
20
+ },
21
+ }
@@ -1,4 +1,4 @@
1
- import { createElement } from "../../element.js"
1
+ import { createElement, Fragment } from "../../element.js"
2
2
  import { renderToReadableStream } from "../../ssr/server.js"
3
3
  import {
4
4
  matchLayouts,
@@ -72,7 +72,7 @@ export async function render(
72
72
 
73
73
  if (__DEV__) {
74
74
  ;[pageEntry, ...layoutEntries].forEach((e) => {
75
- ctx.registerModule(e.filePath!)
75
+ ctx.registerModule(e.absolutePath!)
76
76
  })
77
77
  }
78
78
 
@@ -131,8 +131,21 @@ export async function render(
131
131
  props
132
132
  )
133
133
 
134
+ let { immediate: documentShell } = renderToReadableStream(
135
+ createElement(ctx.Document)
136
+ )
137
+
138
+ if (
139
+ documentShell.includes("</body>") ||
140
+ !documentShell.includes("<kiru-body-outlet>")
141
+ ) {
142
+ throw new Error(
143
+ "[kiru/router]: Document is expected to contain a <Body.Outlet> element. See https://kirujs.dev/docs/api/file-router#ssg"
144
+ )
145
+ }
146
+
134
147
  const app = createElement(RouterContext.Provider, {
135
- children: createElement(ctx.Document, { children }),
148
+ children: Fragment({ children }),
136
149
  value: {
137
150
  state: {
138
151
  params,
@@ -143,35 +156,38 @@ export async function render(
143
156
  },
144
157
  })
145
158
 
146
- let { immediate, stream } = renderToReadableStream(app)
147
- const hasHeadOutlet = immediate.includes("<kiru-head-outlet>")
148
- const hasHeadContent = immediate.includes("<kiru-head-content>")
159
+ let { immediate: pageOutletContent, stream } = renderToReadableStream(app)
160
+ const hasHeadContent = pageOutletContent.includes("<kiru-head-content>")
161
+ const hasHeadOutlet = documentShell.includes("<kiru-head-outlet>")
149
162
 
150
163
  if (hasHeadOutlet && hasHeadContent) {
151
164
  let [preHeadContent = "", headContentInner = "", postHeadContent = ""] =
152
- immediate.split(/<kiru-head-content>|<\/kiru-head-content>/)
165
+ pageOutletContent.split(/<kiru-head-content>|<\/kiru-head-content>/)
153
166
 
154
- preHeadContent = preHeadContent.replace(
167
+ documentShell = documentShell.replace(
155
168
  "<kiru-head-outlet>",
156
169
  headContentInner
157
170
  )
158
- immediate = `${preHeadContent}${postHeadContent}`
171
+ pageOutletContent = `${preHeadContent}${postHeadContent}`
159
172
  } else if (hasHeadContent) {
160
173
  // remove head content element and everything within it
161
- immediate = immediate.replace(
174
+ pageOutletContent = pageOutletContent.replace(
162
175
  /<kiru-head-content>(.*?)<\/kiru-head-content>/,
163
176
  ""
164
177
  )
165
178
  } else if (hasHeadOutlet) {
166
179
  // remove head outlet element and everything within it
167
- immediate = immediate.replaceAll("<kiru-head-outlet>", "")
180
+ documentShell = documentShell.replaceAll("<kiru-head-outlet>", "")
168
181
  }
169
182
 
183
+ const [prePageOutlet, postPageOutlet] =
184
+ documentShell.split("<kiru-body-outlet>")
185
+
170
186
  // console.log("immediate", immediate)
171
187
 
172
188
  return {
173
189
  status: is404Route ? 404 : result?.status ?? 200,
174
- immediate: "<!doctype html>" + immediate,
190
+ immediate: `<!doctype html>${prePageOutlet}<body>${pageOutletContent}</body>${postPageOutlet}`,
175
191
  stream,
176
192
  }
177
193
  }
@@ -190,7 +206,7 @@ export async function generateStaticPaths(pages: FormattedViteImportMap) {
190
206
 
191
207
  const hasDynamic = urlSegments.some((s) => s.startsWith(":"))
192
208
  if (!hasDynamic) {
193
- results[basePath === "" ? "/" : basePath] = entry.filePath
209
+ results[basePath === "" ? "/" : basePath] = entry.absolutePath
194
210
  continue
195
211
  }
196
212
  try {
@@ -206,7 +222,7 @@ export async function generateStaticPaths(pages: FormattedViteImportMap) {
206
222
  const value = params[key]
207
223
  p = p.replace(`:${key}*`, value).replace(`:${key}`, value)
208
224
  }
209
- results[p] = entry.filePath
225
+ results[p] = entry.absolutePath
210
226
  }
211
227
  } catch {}
212
228
  }
@@ -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
  }
@@ -15,6 +15,7 @@ export interface FileRouterPreloadConfig {
15
15
  params: RouteParams
16
16
  query: RouteQuery
17
17
  route: string
18
+ cacheData: null | { value: unknown }
18
19
  }
19
20
 
20
21
  export interface FileRouterConfig {
@@ -91,25 +92,54 @@ export interface RouterState {
91
92
 
92
93
  type PageDataLoaderContext = RouterState & {}
93
94
 
95
+ export interface PageDataLoaderCacheConfig {
96
+ type: "memory" | "localStorage" | "sessionStorage"
97
+ ttl: number
98
+ }
99
+
94
100
  export type PageDataLoaderConfig<T = unknown> = {
95
101
  /**
96
102
  * The function to load the page data
97
103
  */
98
104
  load: (context: PageDataLoaderContext) => Promise<T>
105
+ } & (
106
+ | {
107
+ /**
108
+ * The mode to use for the page data loader
109
+ * @default "client"
110
+ * @description
111
+ * - **static**: The page data is loaded at build time and never updated
112
+ * - **client**: The page data is loaded upon navigation and updated on subsequent navigations
113
+ */
114
+ mode?: "client"
115
+ /**
116
+ * Enable transitions when swapping between "load", "error" and "data" states
117
+ */
118
+ transition?: boolean
99
119
 
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"
108
- /**
109
- * Enable transitions when swapping between "load", "error" and "data" states (only when mode is "client")
110
- */
111
- transition?: boolean
112
- }
120
+ /**
121
+ * Configure caching for this loader
122
+ * @example
123
+ * ```ts
124
+ * cache: {
125
+ * type: "memory", // or "localStorage" / "sessionStorage"
126
+ * ttl: 1000 * 60 * 5, // 5 minutes
127
+ }
128
+ * ```
129
+ */
130
+ cache?: PageDataLoaderCacheConfig
131
+ }
132
+ | {
133
+ /**
134
+ * The mode to use for the page data loader
135
+ * @default "client"
136
+ * @description
137
+ * - **static**: The page data is loaded at build time and never updated
138
+ * - **client**: The page data is loaded upon navigation and updated on subsequent navigations
139
+ */
140
+ mode: "static"
141
+ }
142
+ )
113
143
 
114
144
  export interface PageConfig<T = unknown> {
115
145
  /**
@@ -65,7 +65,7 @@ function formatViteImportMap(
65
65
  load: map[key],
66
66
  specificity,
67
67
  segments,
68
- filePath: key,
68
+ absolutePath: key,
69
69
  }
70
70
 
71
71
  return {