@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1fa245e2

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 (160) hide show
  1. package/{CLAUDE.md → AGENTS.md} +4 -0
  2. package/README.md +122 -30
  3. package/dist/bin/rango.js +245 -63
  4. package/dist/vite/index.js +859 -418
  5. package/package.json +3 -3
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +32 -0
  8. package/skills/caching/SKILL.md +49 -8
  9. package/skills/document-cache/SKILL.md +2 -2
  10. package/skills/hooks/SKILL.md +33 -31
  11. package/skills/host-router/SKILL.md +218 -0
  12. package/skills/links/SKILL.md +3 -1
  13. package/skills/loader/SKILL.md +72 -22
  14. package/skills/middleware/SKILL.md +2 -0
  15. package/skills/parallel/SKILL.md +126 -0
  16. package/skills/prerender/SKILL.md +112 -70
  17. package/skills/rango/SKILL.md +0 -1
  18. package/skills/route/SKILL.md +34 -4
  19. package/skills/router-setup/SKILL.md +95 -5
  20. package/skills/typesafety/SKILL.md +35 -23
  21. package/src/__internal.ts +92 -0
  22. package/src/bin/rango.ts +18 -0
  23. package/src/browser/app-version.ts +14 -0
  24. package/src/browser/event-controller.ts +5 -0
  25. package/src/browser/link-interceptor.ts +4 -0
  26. package/src/browser/navigation-bridge.ts +114 -18
  27. package/src/browser/navigation-client.ts +126 -44
  28. package/src/browser/navigation-store.ts +43 -8
  29. package/src/browser/navigation-transaction.ts +11 -9
  30. package/src/browser/partial-update.ts +80 -15
  31. package/src/browser/prefetch/cache.ts +166 -27
  32. package/src/browser/prefetch/fetch.ts +52 -39
  33. package/src/browser/prefetch/policy.ts +6 -0
  34. package/src/browser/prefetch/queue.ts +92 -20
  35. package/src/browser/prefetch/resource-ready.ts +77 -0
  36. package/src/browser/react/Link.tsx +70 -14
  37. package/src/browser/react/NavigationProvider.tsx +40 -4
  38. package/src/browser/react/context.ts +7 -2
  39. package/src/browser/react/use-handle.ts +9 -58
  40. package/src/browser/react/use-router.ts +21 -8
  41. package/src/browser/rsc-router.tsx +143 -59
  42. package/src/browser/scroll-restoration.ts +41 -42
  43. package/src/browser/segment-reconciler.ts +6 -1
  44. package/src/browser/server-action-bridge.ts +454 -436
  45. package/src/browser/types.ts +60 -5
  46. package/src/build/generate-manifest.ts +6 -6
  47. package/src/build/generate-route-types.ts +5 -0
  48. package/src/build/route-trie.ts +19 -3
  49. package/src/build/route-types/include-resolution.ts +8 -1
  50. package/src/build/route-types/router-processing.ts +346 -87
  51. package/src/build/route-types/scan-filter.ts +8 -1
  52. package/src/cache/cache-runtime.ts +15 -11
  53. package/src/cache/cache-scope.ts +48 -7
  54. package/src/cache/cf/cf-cache-store.ts +453 -11
  55. package/src/cache/cf/index.ts +5 -1
  56. package/src/cache/document-cache.ts +17 -7
  57. package/src/cache/index.ts +1 -0
  58. package/src/cache/taint.ts +55 -0
  59. package/src/client.rsc.tsx +2 -1
  60. package/src/client.tsx +3 -102
  61. package/src/context-var.ts +72 -2
  62. package/src/debug.ts +2 -2
  63. package/src/handle.ts +40 -0
  64. package/src/handles/breadcrumbs.ts +66 -0
  65. package/src/handles/index.ts +1 -0
  66. package/src/host/index.ts +0 -3
  67. package/src/index.rsc.ts +8 -37
  68. package/src/index.ts +40 -66
  69. package/src/prerender/store.ts +57 -15
  70. package/src/prerender.ts +138 -77
  71. package/src/reverse.ts +22 -1
  72. package/src/route-definition/dsl-helpers.ts +73 -25
  73. package/src/route-definition/helpers-types.ts +10 -6
  74. package/src/route-definition/index.ts +3 -3
  75. package/src/route-definition/redirect.ts +11 -3
  76. package/src/route-definition/resolve-handler-use.ts +149 -0
  77. package/src/route-map-builder.ts +7 -1
  78. package/src/route-types.ts +11 -0
  79. package/src/router/content-negotiation.ts +100 -1
  80. package/src/router/find-match.ts +4 -2
  81. package/src/router/handler-context.ts +108 -25
  82. package/src/router/intercept-resolution.ts +11 -4
  83. package/src/router/lazy-includes.ts +4 -1
  84. package/src/router/loader-resolution.ts +123 -11
  85. package/src/router/logging.ts +5 -2
  86. package/src/router/manifest.ts +9 -3
  87. package/src/router/match-api.ts +125 -190
  88. package/src/router/match-middleware/background-revalidation.ts +30 -2
  89. package/src/router/match-middleware/cache-lookup.ts +88 -16
  90. package/src/router/match-middleware/cache-store.ts +53 -10
  91. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  92. package/src/router/match-middleware/segment-resolution.ts +61 -5
  93. package/src/router/match-result.ts +22 -15
  94. package/src/router/metrics.ts +238 -13
  95. package/src/router/middleware-types.ts +53 -12
  96. package/src/router/middleware.ts +172 -85
  97. package/src/router/navigation-snapshot.ts +182 -0
  98. package/src/router/pattern-matching.ts +20 -5
  99. package/src/router/prerender-match.ts +114 -10
  100. package/src/router/preview-match.ts +30 -102
  101. package/src/router/request-classification.ts +310 -0
  102. package/src/router/revalidation.ts +27 -7
  103. package/src/router/route-snapshot.ts +245 -0
  104. package/src/router/router-context.ts +6 -1
  105. package/src/router/router-interfaces.ts +50 -5
  106. package/src/router/router-options.ts +50 -19
  107. package/src/router/segment-resolution/fresh.ts +200 -19
  108. package/src/router/segment-resolution/helpers.ts +30 -25
  109. package/src/router/segment-resolution/loader-cache.ts +1 -0
  110. package/src/router/segment-resolution/revalidation.ts +429 -301
  111. package/src/router/segment-wrappers.ts +2 -0
  112. package/src/router/trie-matching.ts +20 -2
  113. package/src/router/types.ts +1 -0
  114. package/src/router.ts +88 -15
  115. package/src/rsc/handler.ts +546 -359
  116. package/src/rsc/index.ts +0 -20
  117. package/src/rsc/manifest-init.ts +5 -1
  118. package/src/rsc/progressive-enhancement.ts +25 -8
  119. package/src/rsc/rsc-rendering.ts +35 -43
  120. package/src/rsc/server-action.ts +16 -10
  121. package/src/rsc/ssr-setup.ts +128 -0
  122. package/src/rsc/types.ts +10 -1
  123. package/src/search-params.ts +16 -13
  124. package/src/segment-system.tsx +140 -4
  125. package/src/server/context.ts +148 -16
  126. package/src/server/loader-registry.ts +9 -8
  127. package/src/server/request-context.ts +182 -34
  128. package/src/server.ts +6 -0
  129. package/src/ssr/index.tsx +4 -0
  130. package/src/static-handler.ts +18 -6
  131. package/src/theme/index.ts +4 -13
  132. package/src/types/cache-types.ts +4 -4
  133. package/src/types/handler-context.ts +149 -49
  134. package/src/types/loader-types.ts +36 -9
  135. package/src/types/route-config.ts +17 -8
  136. package/src/types/route-entry.ts +8 -1
  137. package/src/types/segments.ts +2 -5
  138. package/src/urls/path-helper-types.ts +9 -2
  139. package/src/urls/path-helper.ts +48 -13
  140. package/src/urls/pattern-types.ts +12 -0
  141. package/src/urls/response-types.ts +16 -6
  142. package/src/use-loader.tsx +73 -4
  143. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  144. package/src/vite/discovery/discover-routers.ts +23 -5
  145. package/src/vite/discovery/prerender-collection.ts +48 -15
  146. package/src/vite/discovery/state.ts +17 -13
  147. package/src/vite/index.ts +8 -3
  148. package/src/vite/plugin-types.ts +51 -79
  149. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  150. package/src/vite/plugins/expose-action-id.ts +1 -3
  151. package/src/vite/plugins/performance-tracks.ts +88 -0
  152. package/src/vite/plugins/refresh-cmd.ts +127 -0
  153. package/src/vite/plugins/version-plugin.ts +13 -1
  154. package/src/vite/rango.ts +174 -211
  155. package/src/vite/router-discovery.ts +169 -42
  156. package/src/vite/utils/banner.ts +3 -3
  157. package/src/vite/utils/prerender-utils.ts +78 -0
  158. package/src/vite/utils/shared-utils.ts +3 -2
  159. package/skills/testing/SKILL.md +0 -226
  160. package/src/route-definition/route-function.ts +0 -119
@@ -5,6 +5,7 @@ import React, {
5
5
  useCallback,
6
6
  useContext,
7
7
  useEffect,
8
+ useMemo,
8
9
  useRef,
9
10
  type ForwardRefExoticComponent,
10
11
  type RefAttributes,
@@ -32,12 +33,13 @@ export type LinkState =
32
33
  | StateOrGetter<Record<string, unknown>>;
33
34
 
34
35
  import { prefetchDirect, prefetchQueued } from "../prefetch/fetch.js";
36
+ import { getAppVersion } from "../app-version.js";
35
37
  import {
36
38
  observeForPrefetch,
37
39
  unobserveForPrefetch,
38
40
  } from "../prefetch/observer.js";
39
41
 
40
- // Touch device detection for hybrid strategy.
42
+ // Touch device detection for adaptive strategy.
41
43
  // Checked once at module load (Link.tsx is "use client", runs only in browser).
42
44
  const isTouchDevice =
43
45
  typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
@@ -47,14 +49,14 @@ const isTouchDevice =
47
49
  * - "hover": Prefetch on mouse enter (direct, no queue)
48
50
  * - "viewport": Prefetch when link enters viewport (queued, waits for idle)
49
51
  * - "render": Prefetch on component mount regardless of visibility (queued, waits for idle)
50
- * - "hybrid": Hover on pointer devices, viewport on touch devices
52
+ * - "adaptive": Hover on pointer devices, viewport on touch devices
51
53
  * - "none": No prefetching (default)
52
54
  */
53
55
  export type PrefetchStrategy =
54
56
  | "hover"
55
57
  | "viewport"
56
58
  | "render"
57
- | "hybrid"
59
+ | "adaptive"
58
60
  | "none";
59
61
 
60
62
  /**
@@ -80,6 +82,16 @@ export interface LinkProps extends Omit<
80
82
  * Force full document navigation instead of SPA
81
83
  */
82
84
  reloadDocument?: boolean;
85
+ /**
86
+ * Whether to revalidate server data on navigation.
87
+ * Set to `false` to skip the RSC server fetch and only update the URL.
88
+ *
89
+ * Only takes effect when the pathname stays the same (search param / hash changes).
90
+ * If the pathname changes, this option is ignored and a full navigation occurs.
91
+ *
92
+ * @default true
93
+ */
94
+ revalidate?: boolean;
83
95
  /**
84
96
  * Prefetch strategy for the link destination
85
97
  * @default "none"
@@ -170,6 +182,7 @@ export const Link: ForwardRefExoticComponent<
170
182
  replace = false,
171
183
  scroll = true,
172
184
  reloadDocument = false,
185
+ revalidate,
173
186
  prefetch = "none",
174
187
  state,
175
188
  children,
@@ -181,9 +194,19 @@ export const Link: ForwardRefExoticComponent<
181
194
  const ctx = useContext(NavigationStoreContext);
182
195
  const isExternal = isExternalUrl(to);
183
196
 
184
- // Resolve hybrid: viewport on touch devices, hover on pointer devices
197
+ // Auto-prefix with basename for app-local paths.
198
+ // Skip if external, already prefixed, or not a root-relative path.
199
+ const resolvedTo = useMemo(() => {
200
+ if (isExternal) return to;
201
+ const bn = ctx?.basename;
202
+ if (!bn || !to.startsWith("/") || to.startsWith(bn + "/") || to === bn)
203
+ return to;
204
+ return to === "/" ? bn : bn + to;
205
+ }, [to, isExternal, ctx?.basename]);
206
+
207
+ // Resolve adaptive: viewport on touch devices, hover on pointer devices
185
208
  const resolvedStrategy =
186
- prefetch === "hybrid" ? (isTouchDevice ? "viewport" : "hover") : prefetch;
209
+ prefetch === "adaptive" ? (isTouchDevice ? "viewport" : "hover") : prefetch;
187
210
 
188
211
  // Internal ref for viewport observation; merge with forwarded ref
189
212
  const internalRef = useRef<HTMLAnchorElement | null>(null);
@@ -262,17 +285,44 @@ export const Link: ForwardRefExoticComponent<
262
285
  resolvedState = currentState;
263
286
  }
264
287
 
265
- ctx.navigate(to, { replace, scroll, state: resolvedState });
288
+ ctx.navigate(resolvedTo, {
289
+ replace,
290
+ scroll,
291
+ state: resolvedState,
292
+ revalidate,
293
+ });
266
294
  },
267
- [to, isExternal, reloadDocument, replace, scroll, ctx, onClick],
295
+ [
296
+ resolvedTo,
297
+ isExternal,
298
+ reloadDocument,
299
+ replace,
300
+ scroll,
301
+ revalidate,
302
+ ctx,
303
+ onClick,
304
+ ],
268
305
  );
269
306
 
270
307
  const handleMouseEnter = useCallback(() => {
271
- if (resolvedStrategy === "hover" && !isExternal && ctx?.store) {
308
+ if (
309
+ (resolvedStrategy === "hover" || resolvedStrategy === "viewport") &&
310
+ !isExternal &&
311
+ ctx?.store
312
+ ) {
313
+ // For "hover", this is the primary prefetch trigger.
314
+ // For "viewport", this upgrades/prioritizes a potentially queued
315
+ // prefetch — prefetchDirect bypasses the queue, and hasPrefetch
316
+ // deduplicates if the viewport prefetch already completed.
272
317
  const segmentState = ctx.store.getSegmentState();
273
- prefetchDirect(to, segmentState.currentSegmentIds, ctx.version);
318
+ prefetchDirect(
319
+ resolvedTo,
320
+ segmentState.currentSegmentIds,
321
+ getAppVersion(),
322
+ ctx.store.getRouterId?.(),
323
+ );
274
324
  }
275
- }, [resolvedStrategy, to, isExternal, ctx]);
325
+ }, [resolvedStrategy, resolvedTo, isExternal, ctx]);
276
326
 
277
327
  // Viewport/render prefetch: waits for idle before starting,
278
328
  // uses concurrency-limited queue to avoid flooding.
@@ -289,7 +339,12 @@ export const Link: ForwardRefExoticComponent<
289
339
  const triggerPrefetch = () => {
290
340
  if (cancelled) return;
291
341
  const segmentState = ctx.store.getSegmentState();
292
- prefetchQueued(to, segmentState.currentSegmentIds, ctx.version);
342
+ prefetchQueued(
343
+ resolvedTo,
344
+ segmentState.currentSegmentIds,
345
+ getAppVersion(),
346
+ ctx.store.getRouterId?.(),
347
+ );
293
348
  };
294
349
 
295
350
  // Schedule prefetch only when the app is idle (no navigation/streaming).
@@ -328,21 +383,22 @@ export const Link: ForwardRefExoticComponent<
328
383
  unobserveForPrefetch(observedElement);
329
384
  }
330
385
  };
331
- }, [resolvedStrategy, to, isExternal, ctx]);
386
+ }, [resolvedStrategy, resolvedTo, isExternal, ctx]);
332
387
 
333
388
  return (
334
389
  <a
335
390
  ref={setRef}
336
- href={to}
391
+ href={resolvedTo}
337
392
  onClick={handleClick}
338
393
  onMouseEnter={handleMouseEnter}
339
394
  data-link-component
340
395
  data-external={isExternal ? "" : undefined}
341
396
  data-scroll={scroll === false ? "false" : undefined}
342
397
  data-replace={replace ? "true" : undefined}
398
+ data-revalidate={revalidate === false ? "false" : undefined}
343
399
  {...props}
344
400
  >
345
- <LinkContext.Provider value={to}>{children}</LinkContext.Provider>
401
+ <LinkContext.Provider value={resolvedTo}>{children}</LinkContext.Provider>
346
402
  </a>
347
403
  );
348
404
  });
@@ -3,8 +3,10 @@
3
3
  import React, {
4
4
  useState,
5
5
  useEffect,
6
+ useLayoutEffect,
6
7
  useCallback,
7
8
  useMemo,
9
+ useRef,
8
10
  use,
9
11
  type ReactNode,
10
12
  } from "react";
@@ -25,6 +27,7 @@ import { ThemeProvider } from "../../theme/ThemeProvider.js";
25
27
  import { NonceContext } from "./nonce-context.js";
26
28
  import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
27
29
  import { cancelAllPrefetches } from "../prefetch/queue.js";
30
+ import { handleNavigationEnd } from "../scroll-restoration.js";
28
31
 
29
32
  /**
30
33
  * Process handles from an async generator, updating the event controller
@@ -131,9 +134,14 @@ export interface NavigationProviderProps {
131
134
 
132
135
  /**
133
136
  * App version from server payload (stable, immutable).
134
- * Forwarded to prefetch requests for version mismatch detection.
137
+ * Forwarded to context for cache key building.
135
138
  */
136
139
  version?: string;
140
+
141
+ /**
142
+ * URL prefix for all routes (from createRouter({ basename })).
143
+ */
144
+ basename?: string;
137
145
  }
138
146
 
139
147
  /**
@@ -166,6 +174,7 @@ export function NavigationProvider({
166
174
  initialTheme,
167
175
  warmupEnabled,
168
176
  version,
177
+ basename,
169
178
  }: NavigationProviderProps): ReactNode {
170
179
  // Track current payload for rendering (this triggers re-renders)
171
180
  const [payload, setPayload] = useState(initialPayload);
@@ -195,6 +204,7 @@ export function NavigationProvider({
195
204
  navigate,
196
205
  refresh,
197
206
  version,
207
+ basename,
198
208
  }),
199
209
  [],
200
210
  );
@@ -286,24 +296,50 @@ export function NavigationProvider({
286
296
  };
287
297
  }, [warmupEnabled]);
288
298
 
289
- // Cancel speculative prefetches when navigation starts.
290
- // Viewport/render prefetches should not compete with navigation fetches.
299
+ // Cancel non-matching prefetches when navigation starts.
300
+ // Frees connections so the navigation fetch isn't competing with
301
+ // speculative prefetches. The prefetch matching the navigation target
302
+ // is kept alive so it can be reused via consumeInflightPrefetch.
291
303
  useEffect(() => {
292
304
  let wasIdle = true;
293
305
  const unsub = eventController.subscribe(() => {
294
306
  const state = eventController.getState();
295
307
  const isIdle = state.state === "idle" && !state.isStreaming;
296
308
  if (wasIdle && !isIdle) {
297
- cancelAllPrefetches();
309
+ cancelAllPrefetches(state.pendingUrl);
298
310
  }
299
311
  wasIdle = isIdle;
300
312
  });
301
313
  return unsub;
302
314
  }, [eventController]);
303
315
 
316
+ // Pending scroll action to apply after React commits
317
+ const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
318
+
319
+ // Apply scroll after React commits the new content to the DOM
320
+ useLayoutEffect(() => {
321
+ const scrollAction = pendingScrollRef.current;
322
+ if (!scrollAction) return;
323
+ pendingScrollRef.current = undefined;
324
+
325
+ if (scrollAction.enabled === false) return;
326
+
327
+ handleNavigationEnd({
328
+ restore: scrollAction.restore,
329
+ scroll: scrollAction.enabled,
330
+ isStreaming: scrollAction.isStreaming,
331
+ });
332
+ });
333
+
304
334
  // Subscribe to UI updates (for re-rendering the tree)
305
335
  useEffect(() => {
306
336
  const unsubscribe = store.onUpdate((update) => {
337
+ // Capture scroll intent — it will be applied in useLayoutEffect
338
+ // after React commits this state update to the DOM.
339
+ // Always assign (even undefined) to clear stale scroll from prior navigations,
340
+ // so server actions or error updates don't accidentally replay old scroll.
341
+ pendingScrollRef.current = update.scroll;
342
+
307
343
  setPayload({
308
344
  root: update.root,
309
345
  metadata: update.metadata,
@@ -43,10 +43,15 @@ export interface NavigationStoreContextValue {
43
43
  refresh: () => Promise<void>;
44
44
 
45
45
  /**
46
- * App version from server payload (stable, immutable).
47
- * Used in prefetch requests for version mismatch detection.
46
+ * App version from the initial server payload.
48
47
  */
49
48
  version: string | undefined;
49
+
50
+ /**
51
+ * URL prefix for all routes (from createRouter({ basename })).
52
+ * Used by Link and useRouter() to auto-prefix app-local paths.
53
+ */
54
+ basename: string | undefined;
50
55
  }
51
56
 
52
57
  /**
@@ -9,64 +9,11 @@ import {
9
9
  startTransition,
10
10
  } from "react";
11
11
  import type { Handle } from "../../handle.js";
12
- import { getCollectFn } from "../../handle.js";
12
+ import { collectHandleData } from "../../handle.js";
13
13
  import type { HandleData } from "../types.js";
14
14
  import { NavigationStoreContext } from "./context.js";
15
15
  import { shallowEqual } from "./shallow-equal.js";
16
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
17
  /**
71
18
  * Hook to access collected handle data.
72
19
  *
@@ -99,13 +46,13 @@ export function useHandle<T, A, S>(
99
46
  // Initial state from context event controller, or empty fallback without provider.
100
47
  const [value, setValue] = useState<A | S>(() => {
101
48
  if (!ctx) {
102
- const collected = collectHandle(handle, {}, []);
49
+ const collected = collectHandleData(handle, {}, []);
103
50
  return selector ? selector(collected) : collected;
104
51
  }
105
52
 
106
53
  // On client, use event controller state
107
54
  const state = ctx.eventController.getHandleState();
108
- const collected = collectHandle(handle, state.data, state.segmentOrder);
55
+ const collected = collectHandleData(handle, state.data, state.segmentOrder);
109
56
  return selector ? selector(collected) : collected;
110
57
  });
111
58
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
@@ -125,7 +72,7 @@ export function useHandle<T, A, S>(
125
72
  // Sync current state for the (possibly new) handle so that switching
126
73
  // handles on an idle page doesn't leave stale data from the old handle.
127
74
  const currentHandleState = ctx.eventController.getHandleState();
128
- const currentCollected = collectHandle(
75
+ const currentCollected = collectHandleData(
129
76
  handle,
130
77
  currentHandleState.data,
131
78
  currentHandleState.segmentOrder,
@@ -142,7 +89,11 @@ export function useHandle<T, A, S>(
142
89
  const state = ctx.eventController.getHandleState();
143
90
  const isAction =
144
91
  ctx.eventController.getState().inflightActions.length > 0;
145
- const collected = collectHandle(handle, state.data, state.segmentOrder);
92
+ const collected = collectHandleData(
93
+ handle,
94
+ state.data,
95
+ state.segmentOrder,
96
+ );
146
97
  const nextValue = selectorRef.current
147
98
  ? selectorRef.current(collected)
148
99
  : collected;
@@ -3,6 +3,7 @@
3
3
  import { useContext, useMemo } from "react";
4
4
  import { NavigationStoreContext } from "./context.js";
5
5
  import { prefetchDirect } from "../prefetch/fetch.js";
6
+ import { getAppVersion } from "../app-version.js";
6
7
  import type { RouterInstance, RouterNavigateOptions } from "../types.js";
7
8
 
8
9
  /**
@@ -29,14 +30,22 @@ export function useRouter(): RouterInstance {
29
30
  }
30
31
 
31
32
  // Stable reference: ctx is itself stable (NavigationProvider memoizes with [])
32
- return useMemo<RouterInstance>(
33
- () => ({
33
+ return useMemo<RouterInstance>(() => {
34
+ /** Prefix a root-relative path with basename if not already prefixed. */
35
+ function withBasename(url: string): string {
36
+ const bn = ctx!.basename;
37
+ if (!bn || !url.startsWith("/") || url.startsWith(bn + "/") || url === bn)
38
+ return url;
39
+ return url === "/" ? bn : bn + url;
40
+ }
41
+
42
+ return {
34
43
  push(url: string, options?: RouterNavigateOptions): Promise<void> {
35
- return ctx.navigate(url, { ...options, replace: false });
44
+ return ctx.navigate(withBasename(url), { ...options, replace: false });
36
45
  },
37
46
 
38
47
  replace(url: string, options?: RouterNavigateOptions): Promise<void> {
39
- return ctx.navigate(url, { ...options, replace: true });
48
+ return ctx.navigate(withBasename(url), { ...options, replace: true });
40
49
  },
41
50
 
42
51
  refresh(): Promise<void> {
@@ -46,7 +55,12 @@ export function useRouter(): RouterInstance {
46
55
  prefetch(url: string): void {
47
56
  const segmentState = ctx.store?.getSegmentState();
48
57
  if (segmentState) {
49
- prefetchDirect(url, segmentState.currentSegmentIds, ctx.version);
58
+ prefetchDirect(
59
+ withBasename(url),
60
+ segmentState.currentSegmentIds,
61
+ getAppVersion(),
62
+ ctx.store?.getRouterId?.(),
63
+ );
50
64
  }
51
65
  },
52
66
 
@@ -57,7 +71,6 @@ export function useRouter(): RouterInstance {
57
71
  forward(): void {
58
72
  window.history.forward();
59
73
  },
60
- }),
61
- [],
62
- );
74
+ };
75
+ }, []);
63
76
  }