kiru 0.53.0 → 0.54.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/globals.d.ts +1 -1
  2. package/dist/globals.d.ts.map +1 -1
  3. package/dist/globals.js.map +1 -1
  4. package/dist/router/client/index.d.ts +4 -2
  5. package/dist/router/client/index.d.ts.map +1 -1
  6. package/dist/router/client/index.js +49 -11
  7. package/dist/router/client/index.js.map +1 -1
  8. package/dist/router/context.d.ts +2 -0
  9. package/dist/router/context.d.ts.map +1 -1
  10. package/dist/router/context.js +5 -1
  11. package/dist/router/context.js.map +1 -1
  12. package/dist/router/fileRouterController.d.ts +2 -0
  13. package/dist/router/fileRouterController.d.ts.map +1 -1
  14. package/dist/router/fileRouterController.js +80 -9
  15. package/dist/router/fileRouterController.js.map +1 -1
  16. package/dist/router/globals.d.ts +3 -0
  17. package/dist/router/globals.d.ts.map +1 -1
  18. package/dist/router/globals.js +3 -0
  19. package/dist/router/globals.js.map +1 -1
  20. package/dist/router/guard.d.ts +17 -0
  21. package/dist/router/guard.d.ts.map +1 -0
  22. package/dist/router/guard.js +45 -0
  23. package/dist/router/guard.js.map +1 -0
  24. package/dist/router/head.d.ts.map +1 -1
  25. package/dist/router/head.js +5 -7
  26. package/dist/router/head.js.map +1 -1
  27. package/dist/router/index.d.ts +2 -1
  28. package/dist/router/index.d.ts.map +1 -1
  29. package/dist/router/index.js +2 -1
  30. package/dist/router/index.js.map +1 -1
  31. package/dist/router/{server → ssg}/index.d.ts +4 -3
  32. package/dist/router/ssg/index.d.ts.map +1 -0
  33. package/dist/router/{server → ssg}/index.js +7 -4
  34. package/dist/router/ssg/index.js.map +1 -0
  35. package/dist/router/ssr/index.d.ts +20 -0
  36. package/dist/router/ssr/index.d.ts.map +1 -0
  37. package/dist/router/ssr/index.js +160 -0
  38. package/dist/router/ssr/index.js.map +1 -0
  39. package/dist/router/types.d.ts +37 -4
  40. package/dist/router/types.d.ts.map +1 -1
  41. package/dist/router/types.internal.d.ts +4 -0
  42. package/dist/router/types.internal.d.ts.map +1 -1
  43. package/dist/router/utils/index.d.ts +8 -3
  44. package/dist/router/utils/index.d.ts.map +1 -1
  45. package/dist/router/utils/index.js +38 -6
  46. package/dist/router/utils/index.js.map +1 -1
  47. package/dist/ssr/client.d.ts +1 -1
  48. package/dist/ssr/client.d.ts.map +1 -1
  49. package/dist/ssr/server.d.ts +1 -2
  50. package/dist/ssr/server.d.ts.map +1 -1
  51. package/dist/ssr/server.js +16 -19
  52. package/dist/ssr/server.js.map +1 -1
  53. package/dist/types.d.ts +3 -0
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/utils/format.d.ts +2 -1
  56. package/dist/utils/format.d.ts.map +1 -1
  57. package/dist/utils/format.js +4 -1
  58. package/dist/utils/format.js.map +1 -1
  59. package/dist/utils/runtime.d.ts +1 -1
  60. package/dist/utils/runtime.js +1 -1
  61. package/package.json +8 -4
  62. package/src/globals.ts +1 -1
  63. package/src/router/client/index.ts +100 -14
  64. package/src/router/context.ts +7 -1
  65. package/src/router/fileRouterController.ts +137 -8
  66. package/src/router/globals.ts +4 -0
  67. package/src/router/guard.ts +72 -0
  68. package/src/router/head.ts +5 -7
  69. package/src/router/index.ts +12 -1
  70. package/src/router/{server → ssg}/index.ts +16 -9
  71. package/src/router/ssr/index.ts +247 -0
  72. package/src/router/types.internal.ts +5 -0
  73. package/src/router/types.ts +48 -4
  74. package/src/router/utils/index.ts +74 -8
  75. package/src/ssr/client.ts +1 -1
  76. package/src/ssr/server.ts +19 -21
  77. package/src/types.ts +3 -0
  78. package/src/utils/format.ts +5 -0
  79. package/src/utils/runtime.ts +1 -1
  80. package/dist/router/server/index.d.ts.map +0 -1
  81. 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"
@@ -55,6 +66,7 @@ export class FileRouterController {
55
66
  private historyIndex: number
56
67
  private layouts: FormattedViteImportMap
57
68
  private pages: FormattedViteImportMap<PageModule>
69
+ private guards: FormattedViteImportMap<GuardModule>
58
70
  private pageRouteToConfig?: Map<string, PageConfig>
59
71
  private state: RouterState
60
72
 
@@ -68,6 +80,7 @@ export class FileRouterController {
68
80
  this.historyIndex = 0
69
81
  this.layouts = {}
70
82
  this.pages = {}
83
+ this.guards = {}
71
84
  this.state = {
72
85
  pathname: window.location.pathname,
73
86
  hash: window.location.hash,
@@ -187,6 +200,7 @@ export class FileRouterController {
187
200
  const {
188
201
  pages,
189
202
  layouts,
203
+ guards,
190
204
  dir = "/pages",
191
205
  baseUrl = "/",
192
206
  transition,
@@ -202,6 +216,7 @@ export class FileRouterController {
202
216
  const {
203
217
  pages,
204
218
  layouts,
219
+ guards,
205
220
  page,
206
221
  pageProps,
207
222
  pageLayouts,
@@ -226,6 +241,8 @@ export class FileRouterController {
226
241
  this.currentLayouts.value = pageLayouts.map((l) => l.default)
227
242
  this.pages = pages
228
243
  this.layouts = layouts
244
+ this.guards = (guards ??
245
+ {}) as unknown as FormattedViteImportMap<GuardModule>
229
246
  if (__DEV__) {
230
247
  if (page.config) {
231
248
  this.dev_onPageConfigDefined!(route, page.config)
@@ -273,6 +290,14 @@ export class FileRouterController {
273
290
  normalizedDir,
274
291
  normalizedBaseUrl
275
292
  )
293
+ this.guards = !guards
294
+ ? {}
295
+ : (formatViteImportMap(
296
+ guards as ViteImportMap,
297
+ normalizedDir,
298
+ normalizedBaseUrl
299
+ ) as unknown as FormattedViteImportMap<GuardModule>)
300
+
276
301
  if (__DEV__) {
277
302
  validateRoutes(this.pages)
278
303
  }
@@ -315,10 +340,31 @@ export class FileRouterController {
315
340
  window.history.scrollRestoration = "auto"
316
341
  })
317
342
 
343
+ let ignorePopState = false
344
+
318
345
  window.addEventListener("popstate", (e) => {
319
346
  e.preventDefault()
320
- scrollStack.replace(this.historyIndex, window.scrollX, window.scrollY)
321
347
 
348
+ if (
349
+ !ignorePopState &&
350
+ this.onBeforeLeave(window.location.pathname) === false
351
+ ) {
352
+ ignorePopState = true
353
+ if (e.state !== null) {
354
+ if (e.state.index > this.historyIndex) {
355
+ window.history.go(-1)
356
+ } else if (e.state.index < this.historyIndex) {
357
+ window.history.go(1)
358
+ }
359
+ }
360
+ return
361
+ }
362
+ if (ignorePopState) {
363
+ ignorePopState = false
364
+ return
365
+ }
366
+
367
+ scrollStack.replace(this.historyIndex, window.scrollX, window.scrollY)
322
368
  this.loadRoute().then(() => {
323
369
  if (e.state != null) {
324
370
  this.historyIndex = e.state.index
@@ -331,6 +377,31 @@ export class FileRouterController {
331
377
  })
332
378
  }
333
379
 
380
+ private onBeforeLeave(to: string) {
381
+ const currentPage = this.currentPage.peek()
382
+ if (!currentPage) {
383
+ return true
384
+ }
385
+
386
+ let config = currentPage.config ?? ({} as PageConfig)
387
+ if (__DEV__) {
388
+ if (this.pageRouteToConfig?.has(currentPage.route)) {
389
+ config = this.pageRouteToConfig.get(currentPage.route)!
390
+ }
391
+ }
392
+
393
+ const onBeforeLeave = config.hooks?.onBeforeLeave
394
+ if (onBeforeLeave) {
395
+ return runBeforeLeaveHooks(
396
+ toArray(onBeforeLeave),
397
+ { ...requestContext.current },
398
+ to,
399
+ this.state.pathname
400
+ )
401
+ }
402
+ return true
403
+ }
404
+
334
405
  public getChildren() {
335
406
  const page = this.currentPage.value
336
407
  if (!page) return null
@@ -381,10 +452,38 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
381
452
 
382
453
  const { route, pageEntry, params, routeSegments } = routeMatch
383
454
 
455
+ // Apply beforeEach guards before loading route
456
+ const guardEntries = matchModules(
457
+ this.guards as unknown as FormattedViteImportMap,
458
+ routeSegments
459
+ )
460
+ const guardModules = await Promise.all(
461
+ guardEntries.map(
462
+ (entry) => entry.load() as unknown as Promise<GuardModule>
463
+ )
464
+ )
465
+
466
+ const fromPath = this.state.pathname
467
+ const redirectPath = await runBeforeEachGuards(
468
+ guardModules,
469
+ { ...requestContext.current },
470
+ path,
471
+ fromPath
472
+ )
473
+
474
+ // If redirect was requested, navigate to that path instead
475
+ if (redirectPath !== null) {
476
+ this.state.pathname = path
477
+ return this.navigate(redirectPath, {
478
+ replace: true,
479
+ transition: enableTransition,
480
+ })
481
+ }
482
+
384
483
  fileRouterRoute.current = route
385
484
  const pagePromise = pageEntry.load()
386
485
 
387
- const layoutPromises = matchLayouts(this.layouts, routeSegments).map(
486
+ const layoutPromises = matchModules(this.layouts, routeSegments).map(
388
487
  (layoutEntry) => layoutEntry.load()
389
488
  )
390
489
 
@@ -418,7 +517,23 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
418
517
  }
419
518
  }
420
519
 
421
- const { loader } = config
520
+ const { loader, hooks } = config
521
+
522
+ if (hooks?.onBeforeEnter) {
523
+ const redirectPath = await runBeforeEnterHooks(
524
+ toArray(hooks.onBeforeEnter),
525
+ requestContext,
526
+ path,
527
+ fromPath
528
+ )
529
+ if (redirectPath) {
530
+ this.state.pathname = path
531
+ return this.navigate(redirectPath, {
532
+ replace: true,
533
+ transition: enableTransition,
534
+ })
535
+ }
536
+ }
422
537
 
423
538
  if (loader) {
424
539
  if (loader.mode !== "static" || __DEV__) {
@@ -484,6 +599,15 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
484
599
  this.currentLayouts.value = layouts
485
600
  .filter((m) => typeof m.default === "function")
486
601
  .map((m) => m.default)
602
+
603
+ nextIdle(() => {
604
+ runAfterEachGuards(
605
+ guardModules,
606
+ { ...requestContext.current },
607
+ path,
608
+ fromPath
609
+ )
610
+ })
487
611
  })
488
612
  } catch (error) {
489
613
  console.error("[kiru/router]: Failed to load route component:", error)
@@ -501,7 +625,7 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
501
625
 
502
626
  // Load data from loader (cache check is now done earlier in loadRoute)
503
627
  loader
504
- .load(routerState)
628
+ .load({ ...routerState, context: { ...requestContext.current } })
505
629
  .then(
506
630
  (data) => {
507
631
  // Cache the data if caching is enabled
@@ -570,6 +694,9 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
570
694
  return
571
695
  }
572
696
 
697
+ if (this.onBeforeLeave(prevPath) === false) {
698
+ return
699
+ }
573
700
  this.updateHistoryState(path, options)
574
701
 
575
702
  this.loadRoute(
@@ -603,7 +730,7 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
603
730
  const { pageEntry, route } = routeMatch
604
731
  fileRouterRoute.current = route
605
732
  const pagePromise = pageEntry.load()
606
- const layoutPromises = matchLayouts(this.layouts, route.split("/")).map(
733
+ const layoutPromises = matchModules(this.layouts, route.split("/")).map(
607
734
  (layoutEntry) => layoutEntry.load()
608
735
  )
609
736
  await Promise.all([pagePromise, ...layoutPromises])
@@ -649,13 +776,15 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
649
776
  path
650
777
  )
651
778
  } else {
779
+ const current = scrollStack.get()
780
+
652
781
  // if we've gone back and are now going forward, we need to
653
782
  // truncate the scroll stack so it doesn't just permanently grow.
654
783
  // this should keep it at the same length as the history stack.
655
- const current = scrollStack.get()
656
784
  if (this.historyIndex < window.history.length - 1) {
657
785
  current.length = this.historyIndex
658
786
  }
787
+
659
788
  scrollStack.save([...current, [window.scrollX, window.scrollY]])
660
789
  window.history.pushState(
661
790
  { ...window.history.state, index: ++this.historyIndex },
@@ -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
+ }
@@ -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)
@@ -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
  }