@buzola/router 0.0.4 → 0.0.6

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 (38) hide show
  1. package/dist/define/create-page.d.ts +7 -6
  2. package/dist/define/create-page.d.ts.map +1 -1
  3. package/dist/define/create-page.js +33 -120
  4. package/dist/define/create-page.js.map +1 -1
  5. package/dist/engine/loader-state.d.ts +68 -0
  6. package/dist/engine/loader-state.d.ts.map +1 -0
  7. package/dist/engine/loader-state.js +135 -0
  8. package/dist/engine/loader-state.js.map +1 -0
  9. package/dist/engine/route-tree.d.ts +1 -0
  10. package/dist/engine/route-tree.d.ts.map +1 -1
  11. package/dist/engine/route-tree.js +5 -2
  12. package/dist/engine/route-tree.js.map +1 -1
  13. package/dist/engine/router.d.ts +6 -0
  14. package/dist/engine/router.d.ts.map +1 -1
  15. package/dist/engine/router.js +19 -1
  16. package/dist/engine/router.js.map +1 -1
  17. package/dist/engine/types.d.ts +6 -0
  18. package/dist/engine/types.d.ts.map +1 -1
  19. package/dist/react/hooks.d.ts +2 -0
  20. package/dist/react/hooks.d.ts.map +1 -1
  21. package/dist/react/hooks.js +5 -1
  22. package/dist/react/hooks.js.map +1 -1
  23. package/dist/react/link.d.ts +7 -0
  24. package/dist/react/link.d.ts.map +1 -1
  25. package/dist/react/link.js +33 -6
  26. package/dist/react/link.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/__tests__/loader-state.test.ts +339 -0
  29. package/src/__tests__/matcher.test.ts +29 -0
  30. package/src/__tests__/router.test.ts +58 -0
  31. package/src/__tests__/type-safety.test.tsx +24 -0
  32. package/src/define/create-page.tsx +49 -140
  33. package/src/engine/loader-state.ts +178 -0
  34. package/src/engine/route-tree.ts +6 -1
  35. package/src/engine/router.ts +19 -1
  36. package/src/engine/types.ts +6 -0
  37. package/src/react/hooks.ts +5 -1
  38. package/src/react/link.tsx +49 -5
@@ -63,6 +63,8 @@ export interface RouteConfig {
63
63
  component?: RouteComponent
64
64
  /** Schema for validating search params (Standard Schema compatible). */
65
65
  searchSchema?: StandardSchema
66
+ /** Preload function — triggers lazy component import ahead of navigation. */
67
+ preload?: () => void
66
68
  /** Child routes. */
67
69
  children?: RouteConfig[]
68
70
  /** Whether this is a layout route (has outlet but no own path segment). */
@@ -103,6 +105,8 @@ export interface RouteNode {
103
105
  component?: RouteComponent
104
106
  /** Search params schema. */
105
107
  searchSchema?: StandardSchema
108
+ /** Preload function — triggers lazy component import ahead of navigation. */
109
+ preload?: () => void
106
110
  /** Child nodes. */
107
111
  children: RouteNode[]
108
112
  /** Parent node reference. */
@@ -123,6 +127,8 @@ export interface RouterState {
123
127
  matches: RouteMatch[]
124
128
  /** Whether a navigation is in progress. */
125
129
  isPending: boolean
130
+ /** The target URL during a pending navigation. Undefined when not navigating. */
131
+ pendingLocation?: URL
126
132
  /** Navigation state from Navigation API. */
127
133
  navigationState?: unknown
128
134
  }
@@ -115,9 +115,13 @@ export function useRoute() {
115
115
  depth,
116
116
  /** Whether a navigation is pending. */
117
117
  isPending: state.isPending,
118
+ /** The target URL during a pending navigation (with base path stripped). Undefined when not navigating. */
119
+ pendingLocation: state.pendingLocation
120
+ ? router.stripBasePath(state.pendingLocation.pathname)
121
+ : undefined,
118
122
  /** Current pathname (with base path stripped). */
119
123
  pathname: router.stripBasePath(state.location.pathname),
120
- }), [params, matches, depth, state.isPending, state.location.pathname, router])
124
+ }), [params, matches, depth, state.isPending, state.pendingLocation, state.location.pathname, router])
121
125
  }
122
126
 
123
127
  /**
@@ -1,4 +1,4 @@
1
- import React, { useCallback } from 'react'
1
+ import React, { useCallback, useEffect, useRef } from 'react'
2
2
  import type { EffectivePageParams, NavigateOptions, RegisteredPage } from '../engine/types.js'
3
3
  import { useRouter, useRouterState } from './hooks.js'
4
4
 
@@ -27,6 +27,13 @@ type LinkPropsBase = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>
27
27
  * When `true`, clones the single child element and merges `href`, `onClick`, and `data-active` props into it.
28
28
  */
29
29
  asChild?: boolean
30
+ /**
31
+ * Prefetch strategy for lazy-loaded route code.
32
+ * - `'intent'` — preload on mouseEnter/focus (default)
33
+ * - `'render'` — preload as soon as the Link renders
34
+ * - `'none'` — disable prefetching
35
+ */
36
+ prefetch?: 'intent' | 'render' | 'none'
30
37
  }
31
38
 
32
39
  export type LinkProps<P extends RegisteredPage = RegisteredPage> = [keyof EffectivePageParams<P>] extends [never]
@@ -53,13 +60,17 @@ export function Link<P extends RegisteredPage>(props: LinkProps<P>): React.React
53
60
  resetScroll,
54
61
  activeExact = true,
55
62
  asChild,
63
+ prefetch = 'intent',
56
64
  onClick,
65
+ onMouseEnter,
66
+ onFocus,
57
67
  children,
58
68
  ...rest
59
69
  } = props as LinkPropsBase & { to: string; params?: Record<string, string> }
60
70
 
61
71
  const router = useRouter()
62
72
  const routerState = useRouterState()
73
+ const preloadedRef = useRef(false)
63
74
 
64
75
  const href = router.buildPagePath(to, params)
65
76
  const needsProgrammaticNav = state !== undefined || replace || viewTransition || resetScroll === false
@@ -71,6 +82,33 @@ export function Link<P extends RegisteredPage>(props: LinkProps<P>): React.React
71
82
  const isPrefix = targetPath !== '/' && currentPath.startsWith(targetPath + '/')
72
83
  const isActive = isExact || (!activeExact && isPrefix)
73
84
 
85
+ const doPreload = useCallback(() => {
86
+ if (preloadedRef.current) return
87
+ preloadedRef.current = true
88
+ router.preload(to, params)
89
+ }, [router, to, params])
90
+
91
+ // Prefetch on render
92
+ useEffect(() => {
93
+ if (prefetch === 'render') doPreload()
94
+ }, [prefetch, doPreload])
95
+
96
+ const handleMouseEnter = useCallback(
97
+ (e: React.MouseEvent<HTMLAnchorElement>) => {
98
+ onMouseEnter?.(e)
99
+ if (prefetch === 'intent') doPreload()
100
+ },
101
+ [onMouseEnter, prefetch, doPreload],
102
+ )
103
+
104
+ const handleFocus = useCallback(
105
+ (e: React.FocusEvent<HTMLAnchorElement>) => {
106
+ onFocus?.(e)
107
+ if (prefetch === 'intent') doPreload()
108
+ },
109
+ [onFocus, prefetch, doPreload],
110
+ )
111
+
74
112
  const handleClick = useCallback(
75
113
  (e: React.MouseEvent<HTMLAnchorElement>) => {
76
114
  onClick?.(e)
@@ -91,18 +129,24 @@ export function Link<P extends RegisteredPage>(props: LinkProps<P>): React.React
91
129
  [onClick, needsProgrammaticNav, router, to, params, replace, state, viewTransition, resetScroll],
92
130
  )
93
131
 
132
+ const commonProps = {
133
+ href,
134
+ onClick: handleClick,
135
+ onMouseEnter: handleMouseEnter,
136
+ onFocus: handleFocus,
137
+ 'data-active': isActive ? '' : undefined,
138
+ }
139
+
94
140
  if (asChild) {
95
141
  const child = React.Children.only(children) as React.ReactElement<any>
96
142
  return React.cloneElement(child, {
97
- href,
98
- onClick: handleClick,
99
- 'data-active': isActive ? '' : undefined,
143
+ ...commonProps,
100
144
  ...rest,
101
145
  })
102
146
  }
103
147
 
104
148
  return (
105
- <a href={href} onClick={handleClick} data-active={isActive ? '' : undefined} {...rest}>
149
+ <a {...commonProps} {...rest}>
106
150
  {children}
107
151
  </a>
108
152
  )