@buzola/router 0.0.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 (114) hide show
  1. package/dist/__tests__/create-page.test.d.ts +2 -0
  2. package/dist/__tests__/create-page.test.d.ts.map +1 -0
  3. package/dist/__tests__/create-page.test.js +457 -0
  4. package/dist/__tests__/create-page.test.js.map +1 -0
  5. package/dist/__tests__/define-routes.test.d.ts +2 -0
  6. package/dist/__tests__/define-routes.test.d.ts.map +1 -0
  7. package/dist/__tests__/define-routes.test.js +42 -0
  8. package/dist/__tests__/define-routes.test.js.map +1 -0
  9. package/dist/__tests__/matcher.test.d.ts +2 -0
  10. package/dist/__tests__/matcher.test.d.ts.map +1 -0
  11. package/dist/__tests__/matcher.test.js +108 -0
  12. package/dist/__tests__/matcher.test.js.map +1 -0
  13. package/dist/__tests__/router.test.d.ts +2 -0
  14. package/dist/__tests__/router.test.d.ts.map +1 -0
  15. package/dist/__tests__/router.test.js +327 -0
  16. package/dist/__tests__/router.test.js.map +1 -0
  17. package/dist/__tests__/type-safety.test.d.ts +15 -0
  18. package/dist/__tests__/type-safety.test.d.ts.map +1 -0
  19. package/dist/__tests__/type-safety.test.js +163 -0
  20. package/dist/__tests__/type-safety.test.js.map +1 -0
  21. package/dist/__tests__/types.test.d.ts +2 -0
  22. package/dist/__tests__/types.test.d.ts.map +1 -0
  23. package/dist/__tests__/types.test.js +22 -0
  24. package/dist/__tests__/types.test.js.map +1 -0
  25. package/dist/define/create-page.d.ts +87 -0
  26. package/dist/define/create-page.d.ts.map +1 -0
  27. package/dist/define/create-page.js +305 -0
  28. package/dist/define/create-page.js.map +1 -0
  29. package/dist/define/define-routes.d.ts +34 -0
  30. package/dist/define/define-routes.d.ts.map +1 -0
  31. package/dist/define/define-routes.js +53 -0
  32. package/dist/define/define-routes.js.map +1 -0
  33. package/dist/engine/matcher.d.ts +8 -0
  34. package/dist/engine/matcher.d.ts.map +1 -0
  35. package/dist/engine/matcher.js +79 -0
  36. package/dist/engine/matcher.js.map +1 -0
  37. package/dist/engine/navigation-adapter.d.ts +13 -0
  38. package/dist/engine/navigation-adapter.d.ts.map +1 -0
  39. package/dist/engine/navigation-adapter.js +156 -0
  40. package/dist/engine/navigation-adapter.js.map +1 -0
  41. package/dist/engine/route-tree.d.ts +21 -0
  42. package/dist/engine/route-tree.d.ts.map +1 -0
  43. package/dist/engine/route-tree.js +95 -0
  44. package/dist/engine/route-tree.js.map +1 -0
  45. package/dist/engine/router.d.ts +162 -0
  46. package/dist/engine/router.d.ts.map +1 -0
  47. package/dist/engine/router.js +400 -0
  48. package/dist/engine/router.js.map +1 -0
  49. package/dist/engine/schema.d.ts +46 -0
  50. package/dist/engine/schema.d.ts.map +1 -0
  51. package/dist/engine/schema.js +110 -0
  52. package/dist/engine/schema.js.map +1 -0
  53. package/dist/engine/types.d.ts +183 -0
  54. package/dist/engine/types.d.ts.map +1 -0
  55. package/dist/engine/types.js +2 -0
  56. package/dist/engine/types.js.map +1 -0
  57. package/dist/engine/utils.d.ts +7 -0
  58. package/dist/engine/utils.d.ts.map +1 -0
  59. package/dist/engine/utils.js +17 -0
  60. package/dist/engine/utils.js.map +1 -0
  61. package/dist/index.d.ts +14 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +12 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/react/context.d.ts +21 -0
  66. package/dist/react/context.d.ts.map +1 -0
  67. package/dist/react/context.js +7 -0
  68. package/dist/react/context.js.map +1 -0
  69. package/dist/react/error-boundary.d.ts +24 -0
  70. package/dist/react/error-boundary.d.ts.map +1 -0
  71. package/dist/react/error-boundary.js +37 -0
  72. package/dist/react/error-boundary.js.map +1 -0
  73. package/dist/react/hooks.d.ts +85 -0
  74. package/dist/react/hooks.d.ts.map +1 -0
  75. package/dist/react/hooks.js +161 -0
  76. package/dist/react/hooks.js.map +1 -0
  77. package/dist/react/index.d.ts +12 -0
  78. package/dist/react/index.d.ts.map +1 -0
  79. package/dist/react/index.js +7 -0
  80. package/dist/react/index.js.map +1 -0
  81. package/dist/react/link.d.ts +42 -0
  82. package/dist/react/link.d.ts.map +1 -0
  83. package/dist/react/link.js +38 -0
  84. package/dist/react/link.js.map +1 -0
  85. package/dist/react/outlet.d.ts +13 -0
  86. package/dist/react/outlet.d.ts.map +1 -0
  87. package/dist/react/outlet.js +46 -0
  88. package/dist/react/outlet.js.map +1 -0
  89. package/dist/react/provider.d.ts +22 -0
  90. package/dist/react/provider.d.ts.map +1 -0
  91. package/dist/react/provider.js +37 -0
  92. package/dist/react/provider.js.map +1 -0
  93. package/package.json +29 -0
  94. package/src/__tests__/create-page.test.ts +588 -0
  95. package/src/__tests__/matcher.test.ts +127 -0
  96. package/src/__tests__/router.test.ts +424 -0
  97. package/src/__tests__/type-safety.test.tsx +207 -0
  98. package/src/define/create-page.tsx +472 -0
  99. package/src/engine/matcher.ts +88 -0
  100. package/src/engine/navigation-adapter.ts +191 -0
  101. package/src/engine/route-tree.ts +129 -0
  102. package/src/engine/router.ts +471 -0
  103. package/src/engine/schema.ts +130 -0
  104. package/src/engine/types.ts +203 -0
  105. package/src/engine/utils.ts +16 -0
  106. package/src/env.d.ts +120 -0
  107. package/src/index.ts +61 -0
  108. package/src/react/context.ts +24 -0
  109. package/src/react/error-boundary.tsx +56 -0
  110. package/src/react/hooks.ts +211 -0
  111. package/src/react/index.ts +16 -0
  112. package/src/react/link.tsx +93 -0
  113. package/src/react/outlet.tsx +67 -0
  114. package/src/react/provider.tsx +60 -0
@@ -0,0 +1,16 @@
1
+ export { BuzolaProvider } from './provider'
2
+ export type { BuzolaProviderProps } from './provider'
3
+
4
+ export { Outlet } from './outlet'
5
+ export type { OutletProps } from './outlet'
6
+
7
+ export { Link } from './link'
8
+ export type { LinkProps } from './link'
9
+
10
+ export { ErrorBoundary } from './error-boundary'
11
+
12
+ export { useBlocker, useInvalidate, useNavigate, useNavigationState, useParams, useRoute, useRouter, useRouterState, useSearchParams } from './hooks'
13
+ export type { BlockerState } from './hooks'
14
+
15
+ export { RouteContext, RouterContext } from './context'
16
+ export type { RouteContextValue } from './context'
@@ -0,0 +1,93 @@
1
+ import React, { useCallback } from 'react'
2
+ import type { EffectivePageParams, NavigateOptions, RegisteredPage } from '../engine/types.js'
3
+ import { useRouter, useRouterState } from './hooks.js'
4
+
5
+ // ─── Link props type — always type-safe via page ID ──────────────────────────
6
+
7
+ type LinkPropsBase = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & {
8
+ /** Replace the current history entry instead of pushing. */
9
+ replace?: boolean
10
+ /** State to associate with the navigation entry. */
11
+ state?: unknown
12
+ /** Use View Transitions API for this navigation. */
13
+ viewTransition?: boolean
14
+ /**
15
+ * Whether to reset scroll position after navigation.
16
+ * Default is `true`. Set to `false` to preserve current scroll position.
17
+ */
18
+ resetScroll?: boolean
19
+ /**
20
+ * Use exact matching for active detection.
21
+ * When `true` (default), `data-active` is set only when the current path equals the link target.
22
+ * When `false`, `data-active` is also set when the current path starts with the link target.
23
+ */
24
+ activeExact?: boolean
25
+ }
26
+
27
+ export type LinkProps<P extends RegisteredPage = RegisteredPage> = [keyof EffectivePageParams<P>] extends [never]
28
+ ? LinkPropsBase & { to: P; params?: never }
29
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
30
+ : {} extends EffectivePageParams<P> ? LinkPropsBase & { to: P; params?: EffectivePageParams<P> }
31
+ : LinkPropsBase & { to: P; params: EffectivePageParams<P> }
32
+
33
+ /**
34
+ * Type-safe link component.
35
+ * Uses page IDs for navigation — the router looks up the route pattern from the page registry.
36
+ * For simple navigations, renders a native <a> and lets the Navigation API handle it.
37
+ * Uses programmatic navigation when state, replace, resetScroll, or viewTransition is needed.
38
+ *
39
+ * Sets `data-active` attribute when the link target matches the current URL.
40
+ */
41
+ export function Link<P extends RegisteredPage>(props: LinkProps<P>): React.ReactElement {
42
+ const {
43
+ to,
44
+ params,
45
+ replace,
46
+ state,
47
+ viewTransition,
48
+ resetScroll,
49
+ activeExact = true,
50
+ onClick,
51
+ children,
52
+ ...rest
53
+ } = props as LinkPropsBase & { to: string; params?: Record<string, string> }
54
+
55
+ const router = useRouter()
56
+ const routerState = useRouterState()
57
+
58
+ const href = router.buildPagePath(to, params)
59
+ const needsProgrammaticNav = state !== undefined || replace || viewTransition || resetScroll === false
60
+
61
+ // Active detection: compare route paths (without basePath)
62
+ const currentPath = router.stripBasePath(routerState.location.pathname)
63
+ const targetPath = router.stripBasePath(href.split('?')[0])
64
+ const isExact = currentPath === targetPath
65
+ const isPrefix = targetPath !== '/' && currentPath.startsWith(targetPath + '/')
66
+ const isActive = isExact || (!activeExact && isPrefix)
67
+
68
+ const handleClick = useCallback(
69
+ (e: React.MouseEvent<HTMLAnchorElement>) => {
70
+ onClick?.(e)
71
+ if (e.defaultPrevented) return
72
+
73
+ // Only handle left clicks without modifier keys
74
+ if (e.button !== 0 || e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
75
+
76
+ if (needsProgrammaticNav) {
77
+ e.preventDefault()
78
+ router.navigateToPage(
79
+ to,
80
+ params,
81
+ { replace, state, viewTransition, resetScroll } as NavigateOptions,
82
+ )
83
+ }
84
+ },
85
+ [onClick, needsProgrammaticNav, router, to, params, replace, state, viewTransition, resetScroll],
86
+ )
87
+
88
+ return (
89
+ <a href={href} onClick={handleClick} data-active={isActive ? '' : undefined} {...rest}>
90
+ {children}
91
+ </a>
92
+ )
93
+ }
@@ -0,0 +1,67 @@
1
+ import React, { Suspense, use, useMemo } from 'react'
2
+ import { RouteContext, type RouteContextValue } from './context.js'
3
+ import { ErrorBoundary } from './error-boundary.js'
4
+
5
+ export interface OutletProps {
6
+ /** Fallback to show while lazy components load. */
7
+ fallback?: React.ReactNode
8
+ /** Fallback to show when a route component throws an error. */
9
+ errorFallback?: React.ReactNode | ((error: Error) => React.ReactNode)
10
+ }
11
+
12
+ /**
13
+ * Renders the component for the current depth in the route match chain.
14
+ * Each Outlet increments the depth and renders the next matched route's component.
15
+ */
16
+ export function Outlet({ fallback, errorFallback }: OutletProps): React.ReactElement | null {
17
+ const routeContext = use(RouteContext)
18
+ if (!routeContext) {
19
+ throw new Error('<Outlet /> must be used within a <BuzolaProvider>')
20
+ }
21
+
22
+ const { state, depth, matches } = routeContext
23
+
24
+ // Find the next match that has a component, skipping componentless nodes
25
+ // (e.g. directory layout nodes without a layout.tsx file).
26
+ let effectiveDepth = depth
27
+ let match = matches[effectiveDepth]
28
+ while (match && !match.node.component) {
29
+ effectiveDepth++
30
+ match = matches[effectiveDepth]
31
+ }
32
+
33
+ const nextContext = useMemo<RouteContextValue | null>(() => {
34
+ if (!match) return null
35
+ // Merge params from all skipped matches and the effective match
36
+ let mergedParams = { ...routeContext.params }
37
+ for (let i = depth; i <= effectiveDepth; i++) {
38
+ if (matches[i]) {
39
+ mergedParams = { ...mergedParams, ...matches[i].params }
40
+ }
41
+ }
42
+ return {
43
+ state,
44
+ depth: effectiveDepth + 1,
45
+ matches,
46
+ params: mergedParams,
47
+ }
48
+ }, [state, depth, effectiveDepth, matches, match, routeContext.params])
49
+
50
+ if (!match) {
51
+ return null
52
+ }
53
+
54
+ const Component = match.node.component!
55
+
56
+ return (
57
+ <ErrorBoundary fallback={errorFallback} resetKey={match.node.id}>
58
+ <Suspense fallback={fallback ?? null}>
59
+ {nextContext && (
60
+ <RouteContext value={nextContext}>
61
+ <Component />
62
+ </RouteContext>
63
+ )}
64
+ </Suspense>
65
+ </ErrorBoundary>
66
+ )
67
+ }
@@ -0,0 +1,60 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
2
+ import { createBrowserNavigationAdapter } from '../engine/navigation-adapter.js'
3
+ import { Router } from '../engine/router.js'
4
+ import type { RouteNode } from '../engine/types.js'
5
+ import { RouteContext, type RouteContextValue, RouterContext } from './context.js'
6
+ import { Outlet } from './outlet.js'
7
+
8
+ export type BuzolaProviderProps =
9
+ & {
10
+ children?: React.ReactNode
11
+ }
12
+ & (
13
+ | { router: Router; routes?: never; persistentParams?: never; pageRegistry?: never }
14
+ | {
15
+ router?: never
16
+ routes: RouteNode[]
17
+ persistentParams?: () => Record<string, string>
18
+ pageRegistry?: Record<string, string>
19
+ }
20
+ )
21
+
22
+ /**
23
+ * Root provider for Buzola router.
24
+ * Subscribes to router state and provides context to the React tree.
25
+ */
26
+ export function BuzolaProvider({ routes, children, ...props }: BuzolaProviderProps): React.ReactElement {
27
+ const routerRef = useRef<Router | undefined>(props.router)
28
+ if (!routerRef.current) {
29
+ routerRef.current = new Router({
30
+ routes: routes!,
31
+ adapter: createBrowserNavigationAdapter(),
32
+ persistentParams: props.persistentParams,
33
+ pageRegistry: props.pageRegistry,
34
+ })
35
+ }
36
+ const router = routerRef.current
37
+ const subscribe = useCallback((cb: () => void) => router.subscribe(cb), [router])
38
+ const getSnapshot = useCallback(() => router.getState(), [router])
39
+ const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
40
+
41
+ // Start navigation interception
42
+ useEffect(() => {
43
+ return router.start()
44
+ }, [router])
45
+
46
+ const routeContextValue = useMemo<RouteContextValue>(() => ({
47
+ state,
48
+ depth: 0,
49
+ matches: state.matches,
50
+ params: {},
51
+ }), [state])
52
+
53
+ return (
54
+ <RouterContext value={router}>
55
+ <RouteContext value={routeContextValue}>
56
+ {children ?? <Outlet />}
57
+ </RouteContext>
58
+ </RouterContext>
59
+ )
60
+ }