@rangojs/router 0.0.0-experimental.0f44aca1

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 (305) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +5214 -0
  5. package/package.json +176 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +220 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +645 -0
  43. package/src/browser/navigation-client.ts +215 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +295 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +550 -0
  48. package/src/browser/prefetch/cache.ts +146 -0
  49. package/src/browser/prefetch/fetch.ts +135 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +42 -0
  52. package/src/browser/prefetch/queue.ts +88 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +360 -0
  55. package/src/browser/react/NavigationProvider.tsx +386 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +431 -0
  79. package/src/browser/scroll-restoration.ts +400 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +538 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +469 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +540 -0
  105. package/src/cache/cf/index.ts +25 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +43 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +275 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +158 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +395 -0
  172. package/src/router/lazy-includes.ts +234 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +248 -0
  175. package/src/router/manifest.ts +267 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +192 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +748 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +316 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1239 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +289 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1002 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +235 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +914 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +102 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +110 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +131 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +365 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +254 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +510 -0
  298. package/src/vite/router-discovery.ts +785 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,162 @@
1
+ "use client";
2
+
3
+ import {
4
+ useContext,
5
+ useState,
6
+ useEffect,
7
+ useRef,
8
+ useOptimistic,
9
+ startTransition,
10
+ } from "react";
11
+ import type { Handle } from "../../handle.js";
12
+ import { getCollectFn } from "../../handle.js";
13
+ import type { HandleData } from "../types.js";
14
+ import { NavigationStoreContext } from "./context.js";
15
+ import { shallowEqual } from "./shallow-equal.js";
16
+
17
+ /**
18
+ * Resolve the collect function for a handle.
19
+ * Handle objects are plain { __brand, $$id } - collect is stored in the registry
20
+ * (populated when createHandle runs on the client).
21
+ */
22
+ function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
23
+ // Look up collect from the registry (populated when the handle module is imported).
24
+ const registered = getCollectFn(handle.$$id);
25
+ if (registered) {
26
+ return registered as (segments: T[][]) => A;
27
+ }
28
+
29
+ // Fall back to default flat collect with a dev warning.
30
+ if (process.env.NODE_ENV !== "production") {
31
+ console.warn(
32
+ `[rsc-router] Handle "${handle.$$id}" was passed as a prop but its collect ` +
33
+ `function could not be resolved. Falling back to flat array. ` +
34
+ `Import the handle module in a client component to register its collect function.`,
35
+ );
36
+ }
37
+ return ((segments: unknown[][]) => segments.flat()) as unknown as (
38
+ segments: T[][],
39
+ ) => A;
40
+ }
41
+
42
+ /**
43
+ * Collect handle data from segments and transform to final value.
44
+ */
45
+ function collectHandle<T, A>(
46
+ handle: Handle<T, A>,
47
+ data: HandleData,
48
+ segmentOrder: string[],
49
+ ): A {
50
+ const collect = resolveCollect(handle);
51
+ const segmentData = data[handle.$$id];
52
+
53
+ if (!segmentData) {
54
+ return collect([]);
55
+ }
56
+
57
+ // Build array of segment arrays in parent -> child order
58
+ const segmentArrays: T[][] = [];
59
+ for (const segmentId of segmentOrder) {
60
+ const entries = segmentData[segmentId];
61
+ if (entries && entries.length > 0) {
62
+ segmentArrays.push(entries as T[]);
63
+ }
64
+ }
65
+
66
+ // Call collect once with all segment data
67
+ return collect(segmentArrays);
68
+ }
69
+
70
+ /**
71
+ * Hook to access collected handle data.
72
+ *
73
+ * Returns the collected value from all route segments that pushed to this handle.
74
+ * Re-renders when handle data changes (navigation, actions).
75
+ *
76
+ * @param handle - The handle to read
77
+ * @param selector - Optional selector for performance (only re-render when selected value changes)
78
+ *
79
+ * @example
80
+ * ```tsx
81
+ * // Get all breadcrumbs
82
+ * const breadcrumbs = useHandle(Breadcrumbs);
83
+ *
84
+ * // With selector - only re-render when last crumb changes
85
+ * const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
86
+ * ```
87
+ */
88
+ export function useHandle<T, A>(handle: Handle<T, A>): A;
89
+ export function useHandle<T, A, S>(
90
+ handle: Handle<T, A>,
91
+ selector: (data: A) => S,
92
+ ): S;
93
+ export function useHandle<T, A, S>(
94
+ handle: Handle<T, A>,
95
+ selector?: (data: A) => S,
96
+ ): A | S {
97
+ const ctx = useContext(NavigationStoreContext);
98
+
99
+ // Initial state from context event controller, or empty fallback without provider.
100
+ const [value, setValue] = useState<A | S>(() => {
101
+ if (!ctx) {
102
+ const collected = collectHandle(handle, {}, []);
103
+ return selector ? selector(collected) : collected;
104
+ }
105
+
106
+ // On client, use event controller state
107
+ const state = ctx.eventController.getHandleState();
108
+ const collected = collectHandle(handle, state.data, state.segmentOrder);
109
+ return selector ? selector(collected) : collected;
110
+ });
111
+ const [optimisticValue, setOptimisticValue] = useOptimistic(value);
112
+
113
+ // Track previous value for shallow comparison
114
+ const prevValueRef = useRef(value);
115
+ prevValueRef.current = value;
116
+
117
+ // Ref keeps the latest selector without re-subscribing on every render.
118
+ const selectorRef = useRef(selector);
119
+ selectorRef.current = selector;
120
+
121
+ // Subscribe to handle data changes (client only)
122
+ useEffect(() => {
123
+ if (!ctx) return;
124
+
125
+ // Sync current state for the (possibly new) handle so that switching
126
+ // handles on an idle page doesn't leave stale data from the old handle.
127
+ const currentHandleState = ctx.eventController.getHandleState();
128
+ const currentCollected = collectHandle(
129
+ handle,
130
+ currentHandleState.data,
131
+ currentHandleState.segmentOrder,
132
+ );
133
+ const currentValue = selectorRef.current
134
+ ? selectorRef.current(currentCollected)
135
+ : currentCollected;
136
+ if (!shallowEqual(currentValue, prevValueRef.current)) {
137
+ prevValueRef.current = currentValue;
138
+ setValue(currentValue);
139
+ }
140
+
141
+ return ctx.eventController.subscribeToHandles(() => {
142
+ const state = ctx.eventController.getHandleState();
143
+ const isAction =
144
+ ctx.eventController.getState().inflightActions.length > 0;
145
+ const collected = collectHandle(handle, state.data, state.segmentOrder);
146
+ const nextValue = selectorRef.current
147
+ ? selectorRef.current(collected)
148
+ : collected;
149
+
150
+ if (!shallowEqual(nextValue, prevValueRef.current)) {
151
+ prevValueRef.current = nextValue;
152
+ startTransition(() => {
153
+ // Skip optimistic update during actions to prevent Suspense fallback
154
+ if (!isAction) setOptimisticValue(nextValue);
155
+ setValue(nextValue);
156
+ });
157
+ }
158
+ });
159
+ }, [handle]);
160
+
161
+ return optimisticValue;
162
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { href, type ValidPaths } from "../../href-client.js";
4
+ import { useMount } from "./use-mount.js";
5
+
6
+ /**
7
+ * Client-side hook for mount-aware URL resolution.
8
+ *
9
+ * Returns an href function that automatically prepends the current
10
+ * include() mount prefix. Inside an include("/shop", ...) scope,
11
+ * the returned function resolves local paths relative to /shop.
12
+ *
13
+ * For absolute paths (outside the current mount), use the bare
14
+ * href() function directly instead.
15
+ *
16
+ * @returns A function that prepends the mount prefix to paths
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * "use client";
21
+ * import { useHref, href } from "@rangojs/router/client";
22
+ *
23
+ * // Inside include("/shop", shopPatterns)
24
+ * function ShopNav() {
25
+ * const href = useHref();
26
+ *
27
+ * return (
28
+ * <>
29
+ * {// Local paths - auto-prefixed with /shop}
30
+ * <Link to={href("/cart")}>Cart</Link>
31
+ * <Link to={href("/product/widget")}>Widget</Link>
32
+ * </>
33
+ * );
34
+ * }
35
+ * ```
36
+ */
37
+ export function useHref(): (path: `/${string}`) => string {
38
+ const mount = useMount();
39
+ return (path: `/${string}`) => href(path as ValidPaths, mount);
40
+ }
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useState,
7
+ useEffect,
8
+ useRef,
9
+ useOptimistic,
10
+ startTransition,
11
+ type Context,
12
+ } from "react";
13
+ import { NavigationStoreContext } from "./context.js";
14
+
15
+ /**
16
+ * Context for Link component to provide its destination URL
17
+ * Used by useLinkStatus to determine if this specific link is pending
18
+ */
19
+ export const LinkContext: Context<string | null> = createContext<string | null>(
20
+ null,
21
+ );
22
+
23
+ /**
24
+ * Link status returned by useLinkStatus hook
25
+ */
26
+ export interface LinkStatus {
27
+ /** Whether navigation to this link's destination is in progress */
28
+ pending: boolean;
29
+ }
30
+
31
+ /**
32
+ * Normalize URL for comparison
33
+ * Handles relative URLs and ensures consistent format
34
+ */
35
+ function normalizeUrl(url: string, origin: string): string {
36
+ try {
37
+ const parsed = new URL(url, origin);
38
+ // Return pathname + search + hash for comparison
39
+ return parsed.pathname + parsed.search + parsed.hash;
40
+ } catch {
41
+ return url;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Check if this link's destination matches the pending navigation URL
47
+ */
48
+ function isPendingFor(
49
+ linkTo: string | null,
50
+ pendingUrl: string | null,
51
+ origin: string,
52
+ ): boolean {
53
+ if (linkTo === null || pendingUrl === null) {
54
+ return false;
55
+ }
56
+ return normalizeUrl(pendingUrl, origin) === normalizeUrl(linkTo, origin);
57
+ }
58
+
59
+ /**
60
+ * Hook to track the pending state of a Link component
61
+ *
62
+ * Must be used inside a Link component. Returns `{ pending: true }`
63
+ * when navigation to this link's destination is in progress.
64
+ *
65
+ * Useful for showing inline loading indicators on individual links.
66
+ *
67
+ * @example
68
+ * ```tsx
69
+ * function LoadingIndicator() {
70
+ * const { pending } = useLinkStatus();
71
+ * return pending ? <Spinner /> : null;
72
+ * }
73
+ *
74
+ * // In your component:
75
+ * <Link to="/dashboard">
76
+ * Dashboard
77
+ * <LoadingIndicator />
78
+ * </Link>
79
+ * ```
80
+ */
81
+ export function useLinkStatus(): LinkStatus {
82
+ const linkTo = useContext(LinkContext);
83
+ const ctx = useContext(NavigationStoreContext);
84
+
85
+ // Get origin for URL normalization (stable across renders)
86
+ const origin =
87
+ typeof window !== "undefined" ? window.location.origin : "http://localhost";
88
+
89
+ // Base state for useOptimistic
90
+ const [basePending, setBasePending] = useState<boolean>(() => {
91
+ if (!ctx || linkTo === null) {
92
+ return false;
93
+ }
94
+ const state = ctx.eventController.getState();
95
+ return isPendingFor(linkTo, state.pendingUrl, origin);
96
+ });
97
+
98
+ const prevPending = useRef(basePending);
99
+
100
+ // useOptimistic allows immediate updates during transitions
101
+ const [pending, setOptimisticPending] = useOptimistic(basePending);
102
+
103
+ useEffect(() => {
104
+ if (!ctx || linkTo === null) {
105
+ return;
106
+ }
107
+
108
+ // Subscribe to navigation state changes
109
+ return ctx.eventController.subscribe(() => {
110
+ const state = ctx.eventController.getState();
111
+ const isPending = isPendingFor(linkTo, state.pendingUrl, origin);
112
+
113
+ if (isPending !== prevPending.current) {
114
+ prevPending.current = isPending;
115
+
116
+ // Use optimistic update for immediate feedback during navigation
117
+ if (state.state !== "idle") {
118
+ startTransition(() => {
119
+ setOptimisticPending(isPending);
120
+ });
121
+ }
122
+
123
+ // Always update base state
124
+ setBasePending(isPending);
125
+ }
126
+ });
127
+ }, [linkTo, origin]);
128
+
129
+ // If not inside a Link, return not pending
130
+ if (linkTo === null) {
131
+ return { pending: false };
132
+ }
133
+
134
+ return { pending };
135
+ }
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import { useContext } from "react";
4
+ import { MountContext } from "./mount-context.js";
5
+
6
+ /**
7
+ * Returns the current include() mount path.
8
+ *
9
+ * Inside `include("/articles", blogPatterns)`, returns "/articles".
10
+ * For nested includes, returns the nearest mount path.
11
+ * At root level (no include), returns "/".
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * "use client";
16
+ * import { useMount, href } from "@rangojs/router/client";
17
+ *
18
+ * function BlogNav({ slug }: { slug: string }) {
19
+ * const mount = useMount(); // "/articles"
20
+ * return (
21
+ * <>
22
+ * <Link to={href("/", mount)}>Blog Home</Link>
23
+ * <Link to={href(`/${slug}`, mount)}>Post</Link>
24
+ * </>
25
+ * );
26
+ * }
27
+ * ```
28
+ */
29
+ export function useMount(): string {
30
+ return useContext(MountContext);
31
+ }
@@ -0,0 +1,99 @@
1
+ "use client";
2
+
3
+ import {
4
+ useContext,
5
+ useState,
6
+ useEffect,
7
+ useOptimistic,
8
+ startTransition,
9
+ useRef,
10
+ } from "react";
11
+ import { NavigationStoreContext } from "./context.js";
12
+ import { shallowEqual } from "./shallow-equal.js";
13
+ import type { PublicNavigationState } from "../types.js";
14
+ import type { DerivedNavigationState } from "../event-controller.js";
15
+
16
+ /**
17
+ * Convert derived state to public version (strips inflightActions)
18
+ */
19
+ function toPublicState(state: DerivedNavigationState): PublicNavigationState {
20
+ const { inflightActions: _, ...publicState } = state;
21
+ return publicState;
22
+ }
23
+
24
+ /**
25
+ * Hook to access reactive navigation state with optional selector for performance.
26
+ *
27
+ * Returns state only. For actions (push, replace, refresh, prefetch),
28
+ * use useRouter() instead.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * const { state, location } = useNavigation();
33
+ * const isLoading = useNavigation(nav => nav.state === 'loading');
34
+ * ```
35
+ */
36
+ export function useNavigation(): PublicNavigationState;
37
+ export function useNavigation<T>(
38
+ selector: (state: PublicNavigationState) => T,
39
+ ): T;
40
+ export function useNavigation<T>(
41
+ selector?: (state: PublicNavigationState) => T,
42
+ ): T | PublicNavigationState {
43
+ const ctx = useContext(NavigationStoreContext);
44
+
45
+ if (!ctx) {
46
+ throw new Error("useNavigation must be used within NavigationProvider");
47
+ }
48
+
49
+ // Base state for useOptimistic
50
+ const [baseValue, setBaseValue] = useState<T | PublicNavigationState>(() => {
51
+ const publicState = toPublicState(ctx.eventController.getState());
52
+ return selector ? selector(publicState) : publicState;
53
+ });
54
+ const prevState = useRef(baseValue);
55
+
56
+ // useOptimistic allows immediate updates during transitions/actions
57
+ const [value, setOptimisticValue] = useOptimistic(baseValue);
58
+
59
+ // Store selector in a ref so the subscription callback always uses the
60
+ // latest selector without re-subscribing on every render (inline functions
61
+ // have a new identity each render). This is event-driven by design: the
62
+ // value updates when the store emits, not when the selector changes.
63
+ // Between events there is nothing new to select from.
64
+ const selectorRef = useRef(selector);
65
+ selectorRef.current = selector;
66
+
67
+ // Subscribe to event controller state changes (only runs on client)
68
+ useEffect(() => {
69
+ // Subscribe to updates from event controller
70
+ return ctx.eventController.subscribe(() => {
71
+ const currentState = ctx.eventController.getState();
72
+ const publicState = toPublicState(currentState);
73
+ const nextSelected = selectorRef.current
74
+ ? selectorRef.current(publicState)
75
+ : publicState;
76
+
77
+ // Check if selected value has changed
78
+ if (!shallowEqual(nextSelected, prevState.current)) {
79
+ prevState.current = nextSelected;
80
+
81
+ // Check if any actions are in progress for optimistic updates
82
+ const hasInflightActions =
83
+ ctx.eventController.getInflightActions().size > 0;
84
+
85
+ if (hasInflightActions || publicState.state !== "idle") {
86
+ // Use optimistic update for immediate feedback during transitions
87
+ startTransition(() => {
88
+ setOptimisticValue(nextSelected);
89
+ });
90
+ }
91
+
92
+ // Always update base state so UI reflects current state
93
+ setBaseValue(nextSelected);
94
+ }
95
+ });
96
+ }, []);
97
+
98
+ return value as T | PublicNavigationState;
99
+ }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { useContext, useState, useEffect, useRef } from "react";
4
+ import { NavigationStoreContext } from "./context.js";
5
+ import { shallowEqual } from "./shallow-equal.js";
6
+
7
+ /**
8
+ * Hook to access the current route params.
9
+ *
10
+ * Returns the merged route params from the matched route.
11
+ * Updates when navigation completes, not during pending navigation.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * // Route: /products/:productId
16
+ * const params = useParams();
17
+ * // { productId: "123" }
18
+ *
19
+ * // With selector
20
+ * const productId = useParams(p => p.productId);
21
+ * ```
22
+ */
23
+ export function useParams(): Record<string, string>;
24
+ export function useParams<T>(
25
+ selector: (params: Record<string, string>) => T,
26
+ ): T;
27
+ export function useParams<T>(
28
+ selector?: (params: Record<string, string>) => T,
29
+ ): T | Record<string, string> {
30
+ const ctx = useContext(NavigationStoreContext);
31
+
32
+ const [value, setValue] = useState<T | Record<string, string>>(() => {
33
+ if (!ctx) {
34
+ return selector ? selector({}) : {};
35
+ }
36
+ const params = ctx.eventController.getParams();
37
+ return selector ? selector(params) : params;
38
+ });
39
+
40
+ const prevValue = useRef(value);
41
+ // Ref keeps the latest selector without re-subscribing. Event-driven by
42
+ // design: value updates on store events, not on selector identity change.
43
+ const selectorRef = useRef(selector);
44
+ selectorRef.current = selector;
45
+
46
+ useEffect(() => {
47
+ if (!ctx) return;
48
+
49
+ const update = () => {
50
+ const params = ctx.eventController.getParams();
51
+ const next = selectorRef.current ? selectorRef.current(params) : params;
52
+
53
+ if (!shallowEqual(next, prevValue.current)) {
54
+ prevValue.current = next;
55
+ setValue(next);
56
+ }
57
+ };
58
+
59
+ update();
60
+
61
+ return ctx.eventController.subscribe(update);
62
+ }, []);
63
+
64
+ return value;
65
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { useContext, useState, useEffect, useRef } from "react";
4
+ import { NavigationStoreContext } from "./context.js";
5
+
6
+ /**
7
+ * Hook to access the current pathname.
8
+ *
9
+ * Returns the committed pathname string (excludes search params and hash).
10
+ * Updates when navigation completes, not during pending navigation.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * const pathname = usePathname();
15
+ * // "/products/123"
16
+ * ```
17
+ */
18
+ export function usePathname(): string {
19
+ const ctx = useContext(NavigationStoreContext);
20
+
21
+ const [pathname, setPathname] = useState<string>(() => {
22
+ if (!ctx) {
23
+ return "/";
24
+ }
25
+ return (ctx.eventController.getState().location as URL).pathname;
26
+ });
27
+
28
+ const prevPathname = useRef(pathname);
29
+
30
+ useEffect(() => {
31
+ if (!ctx) return;
32
+
33
+ const update = () => {
34
+ const next = (ctx.eventController.getState().location as URL).pathname;
35
+ if (next !== prevPathname.current) {
36
+ prevPathname.current = next;
37
+ setPathname(next);
38
+ }
39
+ };
40
+
41
+ update();
42
+
43
+ return ctx.eventController.subscribe(update);
44
+ }, []);
45
+
46
+ return pathname;
47
+ }
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { useContext, useMemo } from "react";
4
+ import { NavigationStoreContext } from "./context.js";
5
+ import { prefetchDirect } from "../prefetch/fetch.js";
6
+ import type { RouterInstance, RouterNavigateOptions } from "../types.js";
7
+
8
+ /**
9
+ * Hook to access router actions (push, replace, refresh, prefetch, back, forward).
10
+ *
11
+ * Returns a STABLE reference that never changes, so components using
12
+ * useRouter() do not re-render on navigation state changes.
13
+ * For reactive navigation state, use useNavigation() instead.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const router = useRouter();
18
+ * router.push("/products");
19
+ * router.replace("/login", { scroll: false });
20
+ * router.prefetch("/dashboard");
21
+ * router.back();
22
+ * ```
23
+ */
24
+ export function useRouter(): RouterInstance {
25
+ const ctx = useContext(NavigationStoreContext);
26
+
27
+ if (!ctx) {
28
+ throw new Error("useRouter must be used within NavigationProvider");
29
+ }
30
+
31
+ // Stable reference: ctx is itself stable (NavigationProvider memoizes with [])
32
+ return useMemo<RouterInstance>(
33
+ () => ({
34
+ push(url: string, options?: RouterNavigateOptions): Promise<void> {
35
+ return ctx.navigate(url, { ...options, replace: false });
36
+ },
37
+
38
+ replace(url: string, options?: RouterNavigateOptions): Promise<void> {
39
+ return ctx.navigate(url, { ...options, replace: true });
40
+ },
41
+
42
+ refresh(): Promise<void> {
43
+ return ctx.refresh();
44
+ },
45
+
46
+ prefetch(url: string): void {
47
+ const segmentState = ctx.store?.getSegmentState();
48
+ if (segmentState) {
49
+ prefetchDirect(url, segmentState.currentSegmentIds, ctx.version);
50
+ }
51
+ },
52
+
53
+ back(): void {
54
+ window.history.back();
55
+ },
56
+
57
+ forward(): void {
58
+ window.history.forward();
59
+ },
60
+ }),
61
+ [],
62
+ );
63
+ }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { useContext, useState, useEffect, useRef } from "react";
4
+ import { NavigationStoreContext } from "./context.js";
5
+ import type { ReadonlyURLSearchParams } from "../types.js";
6
+
7
+ /**
8
+ * Hook to access the current URL search params.
9
+ *
10
+ * Returns a read-only URLSearchParams object from the committed location.
11
+ * Updates when navigation completes, not during pending navigation.
12
+ *
13
+ * Note: During SSR the search params are not available (the server only sends
14
+ * the pathname). The hook returns empty params during SSR and syncs from
15
+ * the browser URL on mount.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * const searchParams = useSearchParams();
20
+ * const query = searchParams.get("q"); // "react"
21
+ * const page = searchParams.get("page"); // "2"
22
+ * ```
23
+ */
24
+ export function useSearchParams(): ReadonlyURLSearchParams {
25
+ const ctx = useContext(NavigationStoreContext);
26
+
27
+ // Always initialize with empty URLSearchParams to match SSR output
28
+ // and avoid hydration mismatch. The useEffect below syncs from
29
+ // the real URL after hydration.
30
+ const [searchParams, setSearchParams] = useState<ReadonlyURLSearchParams>(
31
+ () => new URLSearchParams(),
32
+ );
33
+
34
+ const prevSearch = useRef("");
35
+
36
+ useEffect(() => {
37
+ if (!ctx) return;
38
+
39
+ const update = () => {
40
+ const location = ctx.eventController.getState().location as URL;
41
+ const nextSearch = location.searchParams.toString();
42
+ if (nextSearch !== prevSearch.current) {
43
+ prevSearch.current = nextSearch;
44
+ // Create a snapshot so callers cannot mutate the source URLSearchParams
45
+ setSearchParams(new URLSearchParams(nextSearch));
46
+ }
47
+ };
48
+
49
+ // Sync on mount (picks up search params from browser URL)
50
+ update();
51
+
52
+ return ctx.eventController.subscribe(update);
53
+ }, []);
54
+
55
+ return searchParams;
56
+ }