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
@@ -2,9 +2,15 @@ import { Signal } from "../signals/base.js"
2
2
  import { watch } from "../signals/watch.js"
3
3
  import { __DEV__ } from "../env.js"
4
4
  import { flushSync, nextIdle } from "../scheduler.js"
5
+ import { toArray } from "../utils/format.js"
5
6
  import { ReloadOptions, type FileRouterContextType } from "./context.js"
6
7
  import { FileRouterDataLoadError } from "./errors.js"
7
- import { fileRouterInstance, fileRouterRoute, routerCache } from "./globals.js"
8
+ import {
9
+ fileRouterInstance,
10
+ fileRouterRoute,
11
+ requestContext,
12
+ routerCache,
13
+ } from "./globals.js"
8
14
  import type {
9
15
  FileRouterConfig,
10
16
  PageConfig,
@@ -17,17 +23,22 @@ import type {
17
23
  CurrentPage,
18
24
  DevtoolsInterface,
19
25
  FormattedViteImportMap,
26
+ GuardModule,
20
27
  PageModule,
21
28
  ViteImportMap,
22
29
  } from "./types.internal.js"
23
30
  import {
24
31
  formatViteImportMap,
25
- matchLayouts,
32
+ matchModules,
26
33
  matchRoute,
27
34
  match404Route,
28
35
  normalizePrefixPath,
29
36
  parseQuery,
30
37
  wrapWithLayouts,
38
+ runAfterEachGuards,
39
+ runBeforeEachGuards,
40
+ runBeforeEnterHooks,
41
+ runBeforeLeaveHooks,
31
42
  } from "./utils/index.js"
32
43
  import { RouterCache, type CacheKey } from "./cache.js"
33
44
  import { scrollStack } from "./scrollStack.js"
@@ -36,6 +47,19 @@ interface PageConfigWithLoader<T = unknown> extends PageConfig {
36
47
  loader: PageDataLoaderConfig<T>
37
48
  }
38
49
 
50
+ interface LoadRouteOptions {
51
+ path?: string
52
+ transition?: boolean
53
+ isStatic404?: boolean
54
+ onPaint?: () => void
55
+ }
56
+
57
+ let transitionId = 0
58
+ let currentTransition = null as null | {
59
+ transition: ViewTransition
60
+ id: number
61
+ }
62
+
39
63
  export class FileRouterController {
40
64
  public contextValue: FileRouterContextType
41
65
  public devtools?: DevtoolsInterface
@@ -55,6 +79,7 @@ export class FileRouterController {
55
79
  private historyIndex: number
56
80
  private layouts: FormattedViteImportMap
57
81
  private pages: FormattedViteImportMap<PageModule>
82
+ private guards: FormattedViteImportMap<GuardModule>
58
83
  private pageRouteToConfig?: Map<string, PageConfig>
59
84
  private state: RouterState
60
85
 
@@ -68,6 +93,7 @@ export class FileRouterController {
68
93
  this.historyIndex = 0
69
94
  this.layouts = {}
70
95
  this.pages = {}
96
+ this.guards = {}
71
97
  this.state = {
72
98
  pathname: window.location.pathname,
73
99
  hash: window.location.hash,
@@ -79,7 +105,7 @@ export class FileRouterController {
79
105
  this.contextValue = {
80
106
  invalidate: async (...paths: string[]) => {
81
107
  if (this.invalidate(...paths)) {
82
- return this.loadRoute(void 0, void 0, true)
108
+ return this.loadRoute()
83
109
  }
84
110
  },
85
111
  get state() {
@@ -91,7 +117,7 @@ export class FileRouterController {
91
117
  if (options?.invalidate ?? true) {
92
118
  this.invalidate(this.state.pathname)
93
119
  }
94
- return this.loadRoute(void 0, void 0, options?.transition)
120
+ return this.loadRoute({ transition: options?.transition })
95
121
  },
96
122
  setQuery: this.setQuery.bind(this),
97
123
  setHash: this.setHash.bind(this),
@@ -112,12 +138,11 @@ export class FileRouterController {
112
138
  if (curPage?.route === existing.route && loader) {
113
139
  const p = this.currentPageProps.value
114
140
  const transition =
115
- (loader.mode !== "static" && loader.transition) ??
116
- this.enableTransitions
141
+ (!loader.static && loader.transition) ?? this.enableTransitions
117
142
 
118
143
  // Check cache first if caching is enabled
119
144
  let cachedData = null
120
- if (loader.mode !== "static" && loader.cache) {
145
+ if (!loader.static && loader.cache) {
121
146
  const cacheKey: CacheKey = {
122
147
  path: this.state.pathname,
123
148
  params: this.state.params,
@@ -134,9 +159,11 @@ export class FileRouterController {
134
159
  error: null,
135
160
  loading: false,
136
161
  }
137
- handleStateTransition(this.state.signal, transition, () => {
138
- this.currentPageProps.value = props
139
- })
162
+ handleStateTransition(
163
+ transition,
164
+ transitionId,
165
+ () => (this.currentPageProps.value = props)
166
+ )
140
167
  } else {
141
168
  // No cached data - show loading state and load data
142
169
  const props = {
@@ -145,13 +172,14 @@ export class FileRouterController {
145
172
  data: null,
146
173
  error: null,
147
174
  }
148
- handleStateTransition(this.state.signal, transition, () => {
149
- this.currentPageProps.value = props
150
- })
175
+ handleStateTransition(
176
+ transition,
177
+ transitionId,
178
+ () => (this.currentPageProps.value = props)
179
+ )
151
180
 
152
181
  this.loadRouteData(
153
182
  config as PageConfigWithLoader,
154
- props,
155
183
  this.state,
156
184
  transition
157
185
  )
@@ -187,6 +215,7 @@ export class FileRouterController {
187
215
  const {
188
216
  pages,
189
217
  layouts,
218
+ guards,
190
219
  dir = "/pages",
191
220
  baseUrl = "/",
192
221
  transition,
@@ -198,12 +227,38 @@ export class FileRouterController {
198
227
  normalizePrefixPath(baseUrl),
199
228
  ]
200
229
 
201
- if (preloaded) {
230
+ if (!preloaded) {
231
+ this.pages = formatViteImportMap(
232
+ pages as ViteImportMap,
233
+ normalizedDir,
234
+ normalizedBaseUrl
235
+ )
236
+
237
+ this.layouts = formatViteImportMap(
238
+ layouts as ViteImportMap,
239
+ normalizedDir,
240
+ normalizedBaseUrl
241
+ )
242
+ this.guards = !guards
243
+ ? {}
244
+ : (formatViteImportMap(
245
+ guards as ViteImportMap,
246
+ normalizedDir,
247
+ normalizedBaseUrl
248
+ ) as unknown as FormattedViteImportMap<GuardModule>)
249
+
250
+ if (__DEV__) {
251
+ validateRoutes(this.pages)
252
+ }
253
+ this.loadRoute()
254
+ } else {
202
255
  const {
203
256
  pages,
204
257
  layouts,
258
+ guards,
205
259
  page,
206
260
  pageProps,
261
+ pagePropsPromise,
207
262
  pageLayouts,
208
263
  route,
209
264
  params,
@@ -226,57 +281,49 @@ export class FileRouterController {
226
281
  this.currentLayouts.value = pageLayouts.map((l) => l.default)
227
282
  this.pages = pages
228
283
  this.layouts = layouts
284
+ this.guards = (guards ??
285
+ {}) as unknown as FormattedViteImportMap<GuardModule>
229
286
  if (__DEV__) {
287
+ validateRoutes(this.pages)
230
288
  if (page.config) {
231
289
  this.dev_onPageConfigDefined!(route, page.config)
232
290
  }
233
291
  }
234
- if (__DEV__) {
235
- validateRoutes(this.pages)
236
- }
292
+
237
293
  const loader = page.config?.loader
238
- if (
294
+ const transition =
295
+ (!loader?.static && loader?.transition) ?? this.enableTransitions
296
+
297
+ if (loader && pagePropsPromise) {
298
+ const prevState = this.state
299
+ pagePropsPromise.then(({ data, error }) => {
300
+ if (this.state !== prevState) return
301
+
302
+ handleStateTransition(
303
+ transition,
304
+ transitionId,
305
+ () =>
306
+ (this.currentPageProps.value = { loading: false, data, error })
307
+ )
308
+ })
309
+ } else if (
239
310
  loader &&
240
- ((loader.mode !== "static" && pageProps.loading === true) || __DEV__)
311
+ ((!loader.static && pageProps.loading === true) || __DEV__)
241
312
  ) {
242
313
  if (cacheData === null) {
243
- this.loadRouteData(
244
- page.config as PageConfigWithLoader,
245
- pageProps,
246
- this.state
247
- )
314
+ this.loadRouteData(page.config as PageConfigWithLoader, this.state)
248
315
  } else {
249
316
  nextIdle(() => {
250
- const props = {
251
- ...pageProps,
252
- data: cacheData.value,
253
- error: null,
254
- loading: false,
255
- }
256
- // @ts-ignore
257
- const transition = loader.transition ?? this.enableTransitions
258
- handleStateTransition(this.state.signal, transition, () => {
259
- this.currentPageProps.value = props
317
+ handleStateTransition(transition, transitionId, () => {
318
+ this.currentPageProps.value = {
319
+ data: cacheData.value,
320
+ error: null,
321
+ loading: false,
322
+ }
260
323
  })
261
324
  })
262
325
  }
263
326
  }
264
- } else {
265
- this.pages = formatViteImportMap(
266
- pages as ViteImportMap,
267
- normalizedDir,
268
- normalizedBaseUrl
269
- )
270
-
271
- this.layouts = formatViteImportMap(
272
- layouts as ViteImportMap,
273
- normalizedDir,
274
- normalizedBaseUrl
275
- )
276
- if (__DEV__) {
277
- validateRoutes(this.pages)
278
- }
279
- this.loadRoute()
280
327
  }
281
328
 
282
329
  window.history.scrollRestoration = "manual"
@@ -315,22 +362,74 @@ export class FileRouterController {
315
362
  window.history.scrollRestoration = "auto"
316
363
  })
317
364
 
365
+ let ignorePopState = false
366
+
318
367
  window.addEventListener("popstate", (e) => {
319
368
  e.preventDefault()
369
+
370
+ if (
371
+ !ignorePopState &&
372
+ this.onBeforeLeave(window.location.pathname) === false
373
+ ) {
374
+ ignorePopState = true
375
+ if (e.state !== null) {
376
+ if (e.state.index > this.historyIndex) {
377
+ window.history.go(-1)
378
+ } else if (e.state.index < this.historyIndex) {
379
+ window.history.go(1)
380
+ }
381
+ }
382
+ return
383
+ }
384
+ if (ignorePopState) {
385
+ ignorePopState = false
386
+ return
387
+ }
388
+
320
389
  scrollStack.replace(this.historyIndex, window.scrollX, window.scrollY)
321
390
 
322
- this.loadRoute().then(() => {
323
- if (e.state != null) {
391
+ // prep 'on painted' callback for scroll-to-offset action
392
+ // this will fire once the page has rendered but before (loader?) kicks off.
393
+ let onPaint
394
+ if (e.state != null) {
395
+ onPaint = () => {
324
396
  this.historyIndex = e.state.index
325
397
  const offset = scrollStack.getItem(e.state.index)
326
398
  if (offset !== undefined) {
327
399
  window.scrollTo(...offset)
328
400
  }
329
401
  }
330
- })
402
+ }
403
+
404
+ this.loadRoute({ onPaint })
331
405
  })
332
406
  }
333
407
 
408
+ private onBeforeLeave(to: string) {
409
+ const currentPage = this.currentPage.peek()
410
+ if (!currentPage) {
411
+ return true
412
+ }
413
+
414
+ let config = currentPage.config ?? ({} as PageConfig)
415
+ if (__DEV__) {
416
+ if (this.pageRouteToConfig?.has(currentPage.route)) {
417
+ config = this.pageRouteToConfig.get(currentPage.route)!
418
+ }
419
+ }
420
+
421
+ const onBeforeLeave = config.hooks?.onBeforeLeave
422
+ if (onBeforeLeave) {
423
+ return runBeforeLeaveHooks(
424
+ toArray(onBeforeLeave),
425
+ { ...requestContext.current },
426
+ to,
427
+ this.state.pathname
428
+ )
429
+ }
430
+ return true
431
+ }
432
+
334
433
  public getChildren() {
335
434
  const page = this.currentPage.value
336
435
  if (!page) return null
@@ -351,12 +450,14 @@ export class FileRouterController {
351
450
  fileRouterInstance.current = null
352
451
  }
353
452
 
354
- private async loadRoute(
355
- path: string = window.location.pathname,
356
- props: Record<string, unknown> = {},
357
- enableTransition = this.enableTransitions,
358
- isStatic404 = false
359
- ): Promise<void> {
453
+ private async loadRoute(options?: LoadRouteOptions): Promise<void> {
454
+ const {
455
+ transition: enableTransition = this.enableTransitions,
456
+ isStatic404 = false,
457
+ path = window.location.pathname,
458
+ onPaint,
459
+ } = options ?? {}
460
+
360
461
  this.abortController?.abort()
361
462
  const signal = (this.abortController = new AbortController()).signal
362
463
 
@@ -381,10 +482,38 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
381
482
 
382
483
  const { route, pageEntry, params, routeSegments } = routeMatch
383
484
 
485
+ // Apply beforeEach guards before loading route
486
+ const guardEntries = matchModules(
487
+ this.guards as unknown as FormattedViteImportMap,
488
+ routeSegments
489
+ )
490
+ const guardModules = await Promise.all(
491
+ guardEntries.map(
492
+ (entry) => entry.load() as unknown as Promise<GuardModule>
493
+ )
494
+ )
495
+
496
+ const fromPath = this.state.pathname
497
+ const redirectPath = await runBeforeEachGuards(
498
+ guardModules,
499
+ { ...requestContext.current },
500
+ path,
501
+ fromPath
502
+ )
503
+
504
+ // If redirect was requested, navigate to that path instead
505
+ if (redirectPath !== null) {
506
+ this.state.pathname = path
507
+ return this.navigate(redirectPath, {
508
+ replace: true,
509
+ transition: enableTransition,
510
+ })
511
+ }
512
+
384
513
  fileRouterRoute.current = route
385
514
  const pagePromise = pageEntry.load()
386
515
 
387
- const layoutPromises = matchLayouts(this.layouts, routeSegments).map(
516
+ const layoutPromises = matchModules(this.layouts, routeSegments).map(
388
517
  (layoutEntry) => layoutEntry.load()
389
518
  )
390
519
 
@@ -418,62 +547,76 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
418
547
  }
419
548
  }
420
549
 
421
- const { loader } = config
550
+ const { loader, hooks } = config
422
551
 
423
- if (loader) {
424
- if (loader.mode !== "static" || __DEV__) {
425
- // Check cache first if caching is enabled
426
- let cachedData = null
427
- if (loader.mode !== "static" && loader.cache) {
428
- const cacheKey: CacheKey = {
429
- path: routerState.pathname,
430
- params: routerState.params,
431
- query: routerState.query,
432
- }
433
- cachedData = routerCache.current!.get(cacheKey, loader.cache)
434
- }
552
+ if (hooks?.onBeforeEnter) {
553
+ const redirectPath = await runBeforeEnterHooks(
554
+ toArray(hooks.onBeforeEnter),
555
+ requestContext,
556
+ path,
557
+ fromPath
558
+ )
559
+ if (redirectPath !== null) {
560
+ this.state.pathname = path
561
+ return this.navigate(redirectPath, {
562
+ replace: true,
563
+ transition: enableTransition,
564
+ })
565
+ }
566
+ }
435
567
 
436
- if (cachedData !== null) {
437
- // Use cached data immediately - no loading state needed
438
- props = {
439
- ...props,
440
- data: cachedData.value,
441
- error: null,
442
- loading: false,
443
- } satisfies PageProps<PageConfig<unknown>>
444
- } else {
445
- // No cached data - show loading state and load data
446
- props = {
447
- ...props,
448
- loading: true,
568
+ let props: Record<string, unknown> = {}
569
+ if (!!loader) {
570
+ props = {
571
+ data: null,
572
+ error: null,
573
+ loading: true,
574
+ }
575
+ }
576
+
577
+ if (loader?.static && !__DEV__) {
578
+ const staticProps = page.__KIRU_STATIC_PROPS__?.[path]
579
+ if (!staticProps) {
580
+ // 404
581
+ return this.loadRoute({
582
+ path,
583
+ transition: enableTransition,
584
+ isStatic404: true,
585
+ })
586
+ }
587
+ const { data, error } = staticProps
588
+ props = error
589
+ ? {
449
590
  data: null,
591
+ error: new FileRouterDataLoadError(error),
592
+ loading: false,
593
+ }
594
+ : {
595
+ data: data,
450
596
  error: null,
451
- } satisfies PageProps<PageConfig<unknown>>
452
-
453
- this.loadRouteData(
454
- config as PageConfigWithLoader,
455
- props,
456
- routerState,
457
- enableTransition
458
- )
459
- }
460
- } else {
461
- const staticProps = page.__KIRU_STATIC_PROPS__?.[path]
462
- if (!staticProps) {
463
- return this.loadRoute(path, props, enableTransition, true)
464
- }
597
+ loading: false,
598
+ }
599
+ } else if (!loader?.static && loader?.cache) {
600
+ const cacheKey: CacheKey = {
601
+ path: routerState.pathname,
602
+ params: routerState.params,
603
+ query: routerState.query,
604
+ }
605
+ const cachedData = routerCache.current!.get(cacheKey, loader.cache)
465
606
 
466
- const { data, error } = staticProps
607
+ if (cachedData !== null) {
467
608
  props = {
468
- ...props,
469
- data: data,
470
- error: error ? new FileRouterDataLoadError(error) : null,
609
+ data: cachedData.value,
610
+ error: null,
471
611
  loading: false,
472
- } as PageProps<PageConfig<unknown>>
612
+ } satisfies PageProps<PageConfig<unknown>>
473
613
  }
474
614
  }
475
615
 
476
- return handleStateTransition(signal, enableTransition, () => {
616
+ // loader transition must use the same id as page transition in order to prevent skipping it.
617
+ let tId = transitionId++
618
+
619
+ return await handleStateTransition(enableTransition, tId, () => {
477
620
  this.state = routerState
478
621
  this.currentPage.value = {
479
622
  component: page.default,
@@ -484,6 +627,25 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
484
627
  this.currentLayouts.value = layouts
485
628
  .filter((m) => typeof m.default === "function")
486
629
  .map((m) => m.default)
630
+
631
+ nextIdle(() => {
632
+ runAfterEachGuards(
633
+ guardModules,
634
+ { ...requestContext.current },
635
+ path,
636
+ fromPath
637
+ )
638
+ if (props.loading) {
639
+ this.loadRouteData(
640
+ config as PageConfigWithLoader,
641
+ routerState,
642
+ enableTransition,
643
+ tId
644
+ ).then(() => signal.aborted || onPaint?.())
645
+ } else {
646
+ onPaint?.()
647
+ }
648
+ })
487
649
  })
488
650
  } catch (error) {
489
651
  console.error("[kiru/router]: Failed to load route component:", error)
@@ -493,19 +655,19 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
493
655
 
494
656
  private async loadRouteData(
495
657
  config: PageConfigWithLoader,
496
- props: Record<string, unknown>,
497
658
  routerState: RouterState,
498
- enableTransition = this.enableTransitions
659
+ enableTransition = this.enableTransitions,
660
+ id = transitionId
499
661
  ) {
500
662
  const { loader } = config
501
663
 
502
664
  // Load data from loader (cache check is now done earlier in loadRoute)
503
- loader
504
- .load(routerState)
665
+ return loader
666
+ .load({ ...routerState, context: { ...requestContext.current } })
505
667
  .then(
506
668
  (data) => {
507
669
  // Cache the data if caching is enabled
508
- if (loader.mode !== "static" && loader.cache) {
670
+ if (!loader.static && loader.cache) {
509
671
  const cacheKey: CacheKey = {
510
672
  path: routerState.pathname,
511
673
  params: routerState.params,
@@ -531,14 +693,13 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
531
693
  if (routerState.signal.aborted) return
532
694
 
533
695
  const transition =
534
- (loader.mode !== "static" && loader.transition) ?? enableTransition
696
+ (!loader.static && loader.transition) ?? enableTransition
535
697
 
536
- handleStateTransition(routerState.signal, transition, () => {
537
- this.currentPageProps.value = {
538
- ...props,
539
- ...state,
540
- } satisfies PageProps<PageConfig<unknown>>
541
- })
698
+ return handleStateTransition(
699
+ transition,
700
+ id,
701
+ () => (this.currentPageProps.value = state)
702
+ )
542
703
  })
543
704
  }
544
705
 
@@ -566,17 +727,17 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
566
727
  const url = new URL(path, "http://localhost")
567
728
  const { hash: nextHash, pathname: nextPath } = url
568
729
  const { hash: prevHash, pathname: prevPath } = this.state
569
- if (nextHash === prevHash && nextPath === prevPath) {
730
+ if (
731
+ (nextHash === prevHash && nextPath === prevPath) ||
732
+ this.onBeforeLeave(prevPath) === false
733
+ ) {
570
734
  return
571
735
  }
572
736
 
573
737
  this.updateHistoryState(path, options)
574
738
 
575
- this.loadRoute(
576
- void 0,
577
- void 0,
578
- options?.transition ?? this.enableTransitions
579
- ).then(() => {
739
+ const transition = options?.transition ?? this.enableTransitions
740
+ this.loadRoute({ transition }).then(() => {
580
741
  if (nextHash !== prevHash) {
581
742
  window.dispatchEvent(new HashChangeEvent("hashchange"))
582
743
  }
@@ -603,7 +764,7 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
603
764
  const { pageEntry, route } = routeMatch
604
765
  fileRouterRoute.current = route
605
766
  const pagePromise = pageEntry.load()
606
- const layoutPromises = matchLayouts(this.layouts, route.split("/")).map(
767
+ const layoutPromises = matchModules(this.layouts, route.split("/")).map(
607
768
  (layoutEntry) => layoutEntry.load()
608
769
  )
609
770
  await Promise.all([pagePromise, ...layoutPromises])
@@ -649,13 +810,15 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
649
810
  path
650
811
  )
651
812
  } else {
813
+ const current = scrollStack.get()
814
+
652
815
  // if we've gone back and are now going forward, we need to
653
816
  // truncate the scroll stack so it doesn't just permanently grow.
654
817
  // this should keep it at the same length as the history stack.
655
- const current = scrollStack.get()
656
818
  if (this.historyIndex < window.history.length - 1) {
657
819
  current.length = this.historyIndex
658
820
  }
821
+
659
822
  scrollStack.save([...current, [window.scrollX, window.scrollY]])
660
823
  window.history.pushState(
661
824
  { ...window.history.state, index: ++this.historyIndex },
@@ -685,23 +848,32 @@ function buildQueryString(
685
848
  }
686
849
 
687
850
  async function handleStateTransition(
688
- signal: AbortSignal,
689
851
  enableTransition: boolean,
852
+ id: number,
690
853
  callback: () => void
691
854
  ) {
855
+ if (currentTransition) {
856
+ const { id: currentId, transition } = currentTransition
857
+ // for cross-page navigations, we skip any existing transitions.
858
+ // otherwise (eg. loaders), we wait for the existing transition to finish
859
+ if (id !== currentId) {
860
+ transition.skipTransition()
861
+ }
862
+ await transition.finished
863
+ }
692
864
  if (!enableTransition || typeof document.startViewTransition !== "function") {
693
865
  return new Promise<void>((resolve) => {
694
866
  callback()
695
867
  nextIdle(resolve)
696
868
  })
697
869
  }
698
- const vt = document.startViewTransition(() => {
870
+ const transition = document.startViewTransition(() => {
699
871
  callback()
700
872
  flushSync()
701
873
  })
702
-
703
- signal.addEventListener("abort", () => vt.skipTransition())
704
- await vt.ready
874
+ currentTransition = { id, transition }
875
+ await transition.finished
876
+ currentTransition = null
705
877
  }
706
878
 
707
879
  function validateRoutes(pageMap: FormattedViteImportMap) {
@@ -12,3 +12,7 @@ export const fileRouterRoute = {
12
12
  export const routerCache = {
13
13
  current: null as RouterCache | null,
14
14
  }
15
+
16
+ export const requestContext = {
17
+ current: {} as Kiru.RequestContext,
18
+ }