@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30

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 (297) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +883 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4655 -747
  5. package/package.json +78 -50
  6. package/skills/cache-guide/SKILL.md +262 -0
  7. package/skills/caching/SKILL.md +54 -25
  8. package/skills/composability/SKILL.md +172 -0
  9. package/skills/debug-manifest/SKILL.md +12 -8
  10. package/skills/document-cache/SKILL.md +23 -21
  11. package/skills/fonts/SKILL.md +167 -0
  12. package/skills/hooks/SKILL.md +390 -63
  13. package/skills/host-router/SKILL.md +218 -0
  14. package/skills/intercept/SKILL.md +133 -10
  15. package/skills/layout/SKILL.md +102 -5
  16. package/skills/links/SKILL.md +239 -0
  17. package/skills/loader/SKILL.md +366 -29
  18. package/skills/middleware/SKILL.md +173 -36
  19. package/skills/mime-routes/SKILL.md +128 -0
  20. package/skills/parallel/SKILL.md +80 -3
  21. package/skills/prerender/SKILL.md +643 -0
  22. package/skills/rango/SKILL.md +86 -16
  23. package/skills/response-routes/SKILL.md +411 -0
  24. package/skills/route/SKILL.md +227 -14
  25. package/skills/router-setup/SKILL.md +225 -32
  26. package/skills/tailwind/SKILL.md +129 -0
  27. package/skills/theme/SKILL.md +12 -11
  28. package/skills/typesafety/SKILL.md +401 -75
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +10 -4
  31. package/src/bin/rango.ts +321 -0
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +87 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +20 -4
  38. package/src/browser/logging.ts +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +201 -553
  41. package/src/browser/navigation-client.ts +124 -71
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +267 -317
  46. package/src/browser/prefetch/cache.ts +146 -0
  47. package/src/browser/prefetch/fetch.ts +135 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +42 -0
  50. package/src/browser/prefetch/queue.ts +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +173 -73
  53. package/src/browser/react/NavigationProvider.tsx +138 -27
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +37 -0
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +49 -65
  65. package/src/browser/react/use-href.tsx +20 -188
  66. package/src/browser/react/use-link-status.ts +6 -5
  67. package/src/browser/react/use-mount.ts +31 -0
  68. package/src/browser/react/use-navigation.ts +27 -78
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +111 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +83 -0
  79. package/src/browser/server-action-bridge.ts +504 -584
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +92 -57
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +438 -0
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +35 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +10 -15
  114. package/src/client.tsx +114 -135
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +34 -19
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +165 -0
  124. package/src/host/errors.ts +97 -0
  125. package/src/host/index.ts +53 -0
  126. package/src/host/pattern-matcher.ts +214 -0
  127. package/src/host/router.ts +352 -0
  128. package/src/host/testing.ts +79 -0
  129. package/src/host/types.ts +146 -0
  130. package/src/host/utils.ts +25 -0
  131. package/src/href-client.ts +135 -49
  132. package/src/index.rsc.ts +182 -17
  133. package/src/index.ts +238 -24
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +27 -142
  136. package/src/loader.ts +27 -10
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +37 -0
  140. package/src/prerender/store.ts +185 -0
  141. package/src/prerender.ts +463 -0
  142. package/src/reverse.ts +330 -0
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +9 -11
  145. package/src/route-definition/dsl-helpers.ts +934 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1388
  151. package/src/route-map-builder.ts +241 -112
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +70 -9
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +158 -0
  158. package/src/router/handler-context.ts +371 -81
  159. package/src/router/intercept-resolution.ts +395 -0
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +155 -32
  164. package/src/router/match-api.ts +620 -0
  165. package/src/router/match-context.ts +5 -3
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +382 -9
  169. package/src/router/match-middleware/cache-store.ts +51 -22
  170. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  171. package/src/router/match-middleware/segment-resolution.ts +24 -6
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -29
  174. package/src/router/metrics.ts +235 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +324 -367
  178. package/src/router/pattern-matching.ts +321 -30
  179. package/src/router/prerender-match.ts +400 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1241 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -0
  192. package/src/router/segment-wrappers.ts +289 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +239 -0
  197. package/src/router/types.ts +77 -3
  198. package/src/router.ts +688 -3656
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +786 -760
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +5 -25
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +235 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +40 -14
  215. package/src/search-params.ts +230 -0
  216. package/src/segment-system.tsx +57 -61
  217. package/src/server/context.ts +202 -51
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +422 -70
  223. package/src/server.ts +36 -120
  224. package/src/ssr/index.tsx +157 -26
  225. package/src/static-handler.ts +114 -0
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +687 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1577
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -726
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -782
  261. package/src/vite/plugin-types.ts +131 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +254 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -3
  289. package/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/href.ts +0 -255
  294. package/src/vite/expose-handle-id.ts +0 -209
  295. package/src/vite/expose-loader-id.ts +0 -357
  296. package/src/vite/expose-location-state-id.ts +0 -177
  297. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -9,27 +9,34 @@ import {
9
9
  startTransition,
10
10
  } from "react";
11
11
  import type { Handle } from "../../handle.js";
12
+ import { getCollectFn } from "../../handle.js";
12
13
  import type { HandleData } from "../types.js";
13
14
  import { NavigationStoreContext } from "./context.js";
15
+ import { shallowEqual } from "./shallow-equal.js";
14
16
 
15
17
  /**
16
- * SSR module-level state.
17
- * Populated by initHandleDataSync before React renders.
18
- * Used by useState initializer during SSR.
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).
19
21
  */
20
- let ssrHandleData: HandleData = {};
21
- let ssrSegmentOrder: string[] = [];
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
+ }
22
28
 
23
- /**
24
- * Filter segment IDs to only include routes and layouts.
25
- * Excludes parallels (contain .@) and loaders (contain D followed by digit).
26
- */
27
- function filterSegmentOrder(matched: string[]): string[] {
28
- return matched.filter((id) => {
29
- if (id.includes(".@")) return false;
30
- if (/D\d+\./.test(id)) return false;
31
- return true;
32
- });
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;
33
40
  }
34
41
 
35
42
  /**
@@ -38,15 +45,16 @@ function filterSegmentOrder(matched: string[]): string[] {
38
45
  function collectHandle<T, A>(
39
46
  handle: Handle<T, A>,
40
47
  data: HandleData,
41
- segmentOrder: string[]
48
+ segmentOrder: string[],
42
49
  ): A {
50
+ const collect = resolveCollect(handle);
43
51
  const segmentData = data[handle.$$id];
44
52
 
45
53
  if (!segmentData) {
46
- return handle.collect([]);
54
+ return collect([]);
47
55
  }
48
56
 
49
- // Build array of segment arrays in parent child order
57
+ // Build array of segment arrays in parent -> child order
50
58
  const segmentArrays: T[][] = [];
51
59
  for (const segmentId of segmentOrder) {
52
60
  const entries = segmentData[segmentId];
@@ -56,46 +64,7 @@ function collectHandle<T, A>(
56
64
  }
57
65
 
58
66
  // Call collect once with all segment data
59
- return handle.collect(segmentArrays);
60
- }
61
-
62
- /**
63
- * Shallow equality check for selector results.
64
- */
65
- function shallowEqual<T>(a: T, b: T): boolean {
66
- if (Object.is(a, b)) return true;
67
- if (
68
- typeof a !== "object" ||
69
- a === null ||
70
- typeof b !== "object" ||
71
- b === null
72
- ) {
73
- return false;
74
- }
75
- const keysA = Object.keys(a);
76
- const keysB = Object.keys(b);
77
- if (keysA.length !== keysB.length) return false;
78
- for (const key of keysA) {
79
- if (
80
- !Object.hasOwn(b, key) ||
81
- !Object.is((a as any)[key], (b as any)[key])
82
- ) {
83
- return false;
84
- }
85
- }
86
- return true;
87
- }
88
-
89
- /**
90
- * Initialize handle data synchronously for SSR.
91
- * Called before rendering to populate state for useState initializer.
92
- *
93
- * @param data - Handle data from RSC payload
94
- * @param matched - Segment order for reduction
95
- */
96
- export function initHandleDataSync(data: HandleData, matched?: string[]): void {
97
- ssrHandleData = data;
98
- ssrSegmentOrder = filterSegmentOrder(matched ?? []);
67
+ return collect(segmentArrays);
99
68
  }
100
69
 
101
70
  /**
@@ -119,19 +88,18 @@ export function initHandleDataSync(data: HandleData, matched?: string[]): void {
119
88
  export function useHandle<T, A>(handle: Handle<T, A>): A;
120
89
  export function useHandle<T, A, S>(
121
90
  handle: Handle<T, A>,
122
- selector: (data: A) => S
91
+ selector: (data: A) => S,
123
92
  ): S;
124
93
  export function useHandle<T, A, S>(
125
94
  handle: Handle<T, A>,
126
- selector?: (data: A) => S
95
+ selector?: (data: A) => S,
127
96
  ): A | S {
128
97
  const ctx = useContext(NavigationStoreContext);
129
98
 
130
- // Initial state from SSR module state or event controller
99
+ // Initial state from context event controller, or empty fallback without provider.
131
100
  const [value, setValue] = useState<A | S>(() => {
132
- // During SSR, use module-level state
133
- if (typeof document === "undefined" || !ctx) {
134
- const collected = collectHandle(handle, ssrHandleData, ssrSegmentOrder);
101
+ if (!ctx) {
102
+ const collected = collectHandle(handle, {}, []);
135
103
  return selector ? selector(collected) : collected;
136
104
  }
137
105
 
@@ -146,7 +114,7 @@ export function useHandle<T, A, S>(
146
114
  const prevValueRef = useRef(value);
147
115
  prevValueRef.current = value;
148
116
 
149
- // Memoize selector ref
117
+ // Ref keeps the latest selector without re-subscribing on every render.
150
118
  const selectorRef = useRef(selector);
151
119
  selectorRef.current = selector;
152
120
 
@@ -154,6 +122,22 @@ export function useHandle<T, A, S>(
154
122
  useEffect(() => {
155
123
  if (!ctx) return;
156
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
+
157
141
  return ctx.eventController.subscribeToHandles(() => {
158
142
  const state = ctx.eventController.getHandleState();
159
143
  const isAction =
@@ -1,208 +1,40 @@
1
1
  "use client";
2
2
 
3
- import { useContext } from "react";
4
- import { createHref, type ScopedHrefFunction } from "../../href.js";
5
- import type { UrlPatterns } from "../../urls.js";
6
- import { HrefContext, type HrefContextValue } from "../../href-context.js";
7
-
8
- // Re-export for backwards compatibility
9
- export { HrefContext, type HrefContextValue } from "../../href-context.js";
10
-
11
- /**
12
- * Resolution priority for href:
13
- * 1. Path-based (/blog/:slug) → Use directly
14
- * 2. Absolute name (shop.cart) → Global lookup (has dot separator)
15
- * 3. Local name (index) → Prepend current name prefix, then lookup
16
- */
17
- function resolveRouteName(
18
- name: string,
19
- routeMap: Record<string, string>,
20
- currentRoutePrefix?: string
21
- ): string | undefined {
22
- // 1. Path-based - starts with /
23
- if (name.startsWith("/")) {
24
- return name;
25
- }
26
-
27
- // 2. Absolute name - already has a dot (e.g., "shop.cart")
28
- if (name.includes(".")) {
29
- return routeMap[name];
30
- }
31
-
32
- // 3. Local name - try with current prefix first, then fall back to direct lookup
33
- if (currentRoutePrefix) {
34
- // Extract the prefix from current route name
35
- // e.g., "blog.posts.detail" → prefix is "blog.posts"
36
- const lastDot = currentRoutePrefix.lastIndexOf(".");
37
- const prefix = lastDot > 0 ? currentRoutePrefix.substring(0, lastDot) : currentRoutePrefix;
38
-
39
- // Try prefixed name
40
- const prefixedName = `${prefix}.${name}`;
41
- if (routeMap[prefixedName] !== undefined) {
42
- return routeMap[prefixedName];
43
- }
44
-
45
- // If current route is a nested include, try parent prefixes
46
- // e.g., for "blog.posts.detail", try "blog.posts.index", then "blog.index"
47
- let currentPrefix = prefix;
48
- while (currentPrefix.includes(".")) {
49
- const parentDot = currentPrefix.lastIndexOf(".");
50
- currentPrefix = currentPrefix.substring(0, parentDot);
51
- const parentPrefixedName = `${currentPrefix}.${name}`;
52
- if (routeMap[parentPrefixedName] !== undefined) {
53
- return routeMap[parentPrefixedName];
54
- }
55
- }
56
- }
57
-
58
- // Fall back to direct lookup (route without prefix)
59
- return routeMap[name];
60
- }
3
+ import { href, type ValidPaths } from "../../href-client.js";
4
+ import { useMount } from "./use-mount.js";
61
5
 
62
6
  /**
63
- * Type for href function returned by useHref
64
- */
65
- export type HrefFn = {
66
- /**
67
- * Generate a URL from a route name
68
- *
69
- * @param name - Route name (local or absolute) or path-based URL
70
- * @param params - Optional params for dynamic segments
71
- * @returns The resolved URL
72
- *
73
- * @example
74
- * ```tsx
75
- * const href = useHref();
76
- *
77
- * // Local name (resolved with current prefix)
78
- * href("index") // → "/blog" (if inside blog patterns)
79
- * href("post", { slug: "hello" }) // → "/blog/hello"
80
- *
81
- * // Absolute name (direct lookup)
82
- * href("shop.cart") // → "/shop/cart"
83
- *
84
- * // Path-based (used directly)
85
- * href("/about") // → "/about"
86
- * ```
87
- */
88
- (name: string, params?: Record<string, string>): string;
89
- };
90
-
91
- /**
92
- * Client-side hook for resolving route names with current name prefix.
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.
93
12
  *
94
- * Resolution priority:
95
- * 1. Path-based (`/blog/:slug`) Use directly
96
- * 2. Absolute name (`shop.cart`) → Global lookup (contains dot)
97
- * 3. Local name (`index`) → Prepend current name prefix, then lookup
13
+ * For absolute paths (outside the current mount), use the bare
14
+ * href() function directly instead.
98
15
  *
99
- * @typeParam TPatterns - Optional patterns type for type-safe local route names
100
- * @returns A function to generate URLs from route names
16
+ * @returns A function that prepends the mount prefix to paths
101
17
  *
102
18
  * @example
103
19
  * ```tsx
104
20
  * "use client";
105
- * import { useHref } from "@rangojs/router/client";
106
- * import { blogPatterns } from "../urls/blog";
21
+ * import { useHref, href } from "@rangojs/router/client";
107
22
  *
108
- * function BlogNav() {
109
- * // Type-safe: knows "index" | "post" are valid local names
110
- * const href = useHref<typeof blogPatterns>();
23
+ * // Inside include("/shop", shopPatterns)
24
+ * function ShopNav() {
25
+ * const href = useHref();
111
26
  *
112
27
  * return (
113
28
  * <>
114
- * {/* Local names - type-safe, resolved with current name prefix *\/}
115
- * <Link href={href("index")}>Blog Home</Link>
116
- * <Link href={href("post", { slug: "hello" })}>Post</Link>
117
- *
118
- * {/* Absolute names - explicit prefix *\/}
119
- * <Link href={href("shop.cart")}>Cart</Link>
120
- *
121
- * {/* Path-based - always works *\/}
122
- * <Link href={href("/about")}>About</Link>
29
+ * {// Local paths - auto-prefixed with /shop}
30
+ * <Link to={href("/cart")}>Cart</Link>
31
+ * <Link to={href("/product/widget")}>Widget</Link>
123
32
  * </>
124
33
  * );
125
34
  * }
126
35
  * ```
127
36
  */
128
- export function useHref<
129
- TPatterns extends UrlPatterns<any, any> = UrlPatterns<any, Record<string, string>>
130
- >(): TPatterns extends UrlPatterns<any, infer TRoutes>
131
- ? ScopedHrefFunction<TRoutes>
132
- : HrefFn {
133
- const context = useContext(HrefContext);
134
-
135
- if (!context) {
136
- // Return a function that warns and returns the name as-is
137
- return ((name: string, _params?: Record<string, string>) => {
138
- if (process.env.NODE_ENV !== "production") {
139
- console.warn(
140
- "[useHref] HrefContext not found. Make sure HrefProvider is mounted. Returning name as-is."
141
- );
142
- }
143
- return name;
144
- }) as any;
145
- }
146
-
147
- const { routeMap, routeName } = context;
148
-
149
- return ((name: string, params?: Record<string, string>) => {
150
- // Path-based - return directly (optionally with param substitution)
151
- if (name.startsWith("/")) {
152
- if (params) {
153
- // Substitute params in path-based URL
154
- return name.replace(/:([^/]+)/g, (_, key) => {
155
- const value = params[key];
156
- if (value === undefined) {
157
- throw new Error(`Missing param "${key}" for path "${name}"`);
158
- }
159
- return encodeURIComponent(value);
160
- });
161
- }
162
- return name;
163
- }
164
-
165
- // Resolve route name
166
- const pattern = resolveRouteName(name, routeMap, routeName);
167
-
168
- if (pattern === undefined) {
169
- throw new Error(
170
- `Unknown route: "${name}"${routeName ? ` (current route: ${routeName})` : ""}`
171
- );
172
- }
173
-
174
- // If no params, return pattern directly
175
- if (!params) {
176
- return pattern;
177
- }
178
-
179
- // Substitute params
180
- return pattern.replace(/:([^/]+)/g, (_, key) => {
181
- const value = params[key];
182
- if (value === undefined) {
183
- throw new Error(`Missing param "${key}" for route "${name}"`);
184
- }
185
- return encodeURIComponent(value);
186
- });
187
- }) as any;
188
- }
189
-
190
- /**
191
- * Provider component for href context
192
- * Used internally by NavigationProvider to pass route map from RSC metadata
193
- */
194
- export function HrefProvider({
195
- routeMap,
196
- routeName,
197
- children,
198
- }: {
199
- routeMap: Record<string, string>;
200
- routeName?: string;
201
- children: React.ReactNode;
202
- }): React.ReactElement {
203
- return (
204
- <HrefContext.Provider value={{ routeMap, routeName }}>
205
- {children}
206
- </HrefContext.Provider>
207
- );
37
+ export function useHref(): (path: `/${string}`) => string {
38
+ const mount = useMount();
39
+ return (path: `/${string}`) => href(path as ValidPaths, mount);
208
40
  }
@@ -16,7 +16,9 @@ import { NavigationStoreContext } from "./context.js";
16
16
  * Context for Link component to provide its destination URL
17
17
  * Used by useLinkStatus to determine if this specific link is pending
18
18
  */
19
- export const LinkContext: Context<string | null> = createContext<string | null>(null);
19
+ export const LinkContext: Context<string | null> = createContext<string | null>(
20
+ null,
21
+ );
20
22
 
21
23
  /**
22
24
  * Link status returned by useLinkStatus hook
@@ -46,7 +48,7 @@ function normalizeUrl(url: string, origin: string): string {
46
48
  function isPendingFor(
47
49
  linkTo: string | null,
48
50
  pendingUrl: string | null,
49
- origin: string
51
+ origin: string,
50
52
  ): boolean {
51
53
  if (linkTo === null || pendingUrl === null) {
52
54
  return false;
@@ -81,9 +83,8 @@ export function useLinkStatus(): LinkStatus {
81
83
  const ctx = useContext(NavigationStoreContext);
82
84
 
83
85
  // Get origin for URL normalization (stable across renders)
84
- const origin = typeof window !== "undefined"
85
- ? window.location.origin
86
- : "http://localhost";
86
+ const origin =
87
+ typeof window !== "undefined" ? window.location.origin : "http://localhost";
87
88
 
88
89
  // Base state for useOptimistic
89
90
  const [basePending, setBasePending] = useState<boolean>(() => {
@@ -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
+ }
@@ -9,44 +9,10 @@ import {
9
9
  useRef,
10
10
  } from "react";
11
11
  import { NavigationStoreContext } from "./context.js";
12
- import type { PublicNavigationState, NavigateOptions } from "../types.js";
12
+ import { shallowEqual } from "./shallow-equal.js";
13
+ import type { PublicNavigationState } from "../types.js";
13
14
  import type { DerivedNavigationState } from "../event-controller.js";
14
15
 
15
- /**
16
- * Shallow equality check for selector results
17
- */
18
- function shallowEqual<T>(a: T, b: T): boolean {
19
- if (Object.is(a, b)) return true;
20
- if (
21
- typeof a !== "object" ||
22
- a === null ||
23
- typeof b !== "object" ||
24
- b === null
25
- ) {
26
- return false;
27
- }
28
- const keysA = Object.keys(a);
29
- const keysB = Object.keys(b);
30
- if (keysA.length !== keysB.length) return false;
31
- for (const key of keysA) {
32
- if (
33
- !Object.hasOwn(b, key) ||
34
- !Object.is((a as any)[key], (b as any)[key])
35
- ) {
36
- return false;
37
- }
38
- }
39
- return true;
40
- }
41
-
42
- // SSR-safe default state (public version without internal properties)
43
- const SSR_DEFAULT_STATE: PublicNavigationState = {
44
- state: "idle",
45
- isStreaming: false,
46
- location: new URL("/", "http://localhost"),
47
- pendingUrl: null,
48
- };
49
-
50
16
  /**
51
17
  * Convert derived state to public version (strips inflightActions)
52
18
  */
@@ -55,49 +21,33 @@ function toPublicState(state: DerivedNavigationState): PublicNavigationState {
55
21
  return publicState;
56
22
  }
57
23
 
58
- // No-op functions for SSR
59
- const noopNavigate = async () => {};
60
- const noopRefresh = async () => {};
61
-
62
24
  /**
63
- * Navigation methods returned by useNavigation
64
- */
65
- export interface NavigationMethods {
66
- navigate: (url: string, options?: NavigateOptions) => Promise<void>;
67
- refresh: () => Promise<void>;
68
- }
69
-
70
- /**
71
- * Full value returned when no selector is provided
72
- */
73
- export type NavigationValue = PublicNavigationState & NavigationMethods;
74
-
75
- /**
76
- * Hook to access navigation state with optional selector for performance
25
+ * Hook to access reactive navigation state with optional selector for performance.
77
26
  *
78
- * Uses the event controller for reactive state management.
79
- * State is derived from source of truth (currentNavigation, inflightActions).
27
+ * Returns state only. For actions (push, replace, refresh, prefetch),
28
+ * use useRouter() instead.
80
29
  *
81
30
  * @example
82
31
  * ```tsx
83
- * const state = useNavigation(nav => nav.state);
32
+ * const { state, location } = useNavigation();
84
33
  * const isLoading = useNavigation(nav => nav.state === 'loading');
85
34
  * ```
86
35
  */
87
- export function useNavigation(): NavigationValue;
36
+ export function useNavigation(): PublicNavigationState;
88
37
  export function useNavigation<T>(
89
- selector: (state: PublicNavigationState) => T
38
+ selector: (state: PublicNavigationState) => T,
90
39
  ): T;
91
40
  export function useNavigation<T>(
92
- selector?: (state: PublicNavigationState) => T
93
- ): T | NavigationValue {
41
+ selector?: (state: PublicNavigationState) => T,
42
+ ): T | PublicNavigationState {
94
43
  const ctx = useContext(NavigationStoreContext);
95
44
 
45
+ if (!ctx) {
46
+ throw new Error("useNavigation must be used within NavigationProvider");
47
+ }
48
+
96
49
  // Base state for useOptimistic
97
50
  const [baseValue, setBaseValue] = useState<T | PublicNavigationState>(() => {
98
- if (typeof document === "undefined" || !ctx) {
99
- return selector ? selector(SSR_DEFAULT_STATE) : SSR_DEFAULT_STATE;
100
- }
101
51
  const publicState = toPublicState(ctx.eventController.getState());
102
52
  return selector ? selector(publicState) : publicState;
103
53
  });
@@ -106,15 +56,23 @@ export function useNavigation<T>(
106
56
  // useOptimistic allows immediate updates during transitions/actions
107
57
  const [value, setOptimisticValue] = useOptimistic(baseValue);
108
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
+
109
67
  // Subscribe to event controller state changes (only runs on client)
110
68
  useEffect(() => {
111
- if (!ctx) return;
112
-
113
69
  // Subscribe to updates from event controller
114
70
  return ctx.eventController.subscribe(() => {
115
71
  const currentState = ctx.eventController.getState();
116
72
  const publicState = toPublicState(currentState);
117
- const nextSelected = selector ? selector(publicState) : publicState;
73
+ const nextSelected = selectorRef.current
74
+ ? selectorRef.current(publicState)
75
+ : publicState;
118
76
 
119
77
  // Check if selected value has changed
120
78
  if (!shallowEqual(nextSelected, prevState.current)) {
@@ -135,16 +93,7 @@ export function useNavigation<T>(
135
93
  setBaseValue(nextSelected);
136
94
  }
137
95
  });
138
- }, [selector]);
139
-
140
- // If no selector, include navigation methods
141
- if (!selector) {
142
- return {
143
- ...(value as PublicNavigationState),
144
- navigate: ctx?.navigate ?? noopNavigate,
145
- refresh: ctx?.refresh ?? noopRefresh,
146
- };
147
- }
96
+ }, []);
148
97
 
149
- return value as T;
98
+ return value as T | PublicNavigationState;
150
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
+ }