@rangojs/router 0.0.0-experimental.1b930379 → 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 (136) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +76 -18
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +558 -319
  5. package/package.json +16 -15
  6. package/skills/cache-guide/SKILL.md +32 -0
  7. package/skills/caching/SKILL.md +45 -4
  8. package/skills/links/SKILL.md +3 -1
  9. package/skills/loader/SKILL.md +53 -43
  10. package/skills/middleware/SKILL.md +2 -0
  11. package/skills/parallel/SKILL.md +126 -0
  12. package/skills/prerender/SKILL.md +110 -68
  13. package/skills/route/SKILL.md +31 -0
  14. package/skills/router-setup/SKILL.md +87 -2
  15. package/skills/typesafety/SKILL.md +10 -0
  16. package/src/__internal.ts +1 -1
  17. package/src/browser/app-version.ts +14 -0
  18. package/src/browser/event-controller.ts +5 -0
  19. package/src/browser/navigation-bridge.ts +19 -13
  20. package/src/browser/navigation-client.ts +115 -58
  21. package/src/browser/navigation-store.ts +43 -8
  22. package/src/browser/navigation-transaction.ts +11 -9
  23. package/src/browser/partial-update.ts +80 -15
  24. package/src/browser/prefetch/cache.ts +57 -5
  25. package/src/browser/prefetch/fetch.ts +38 -23
  26. package/src/browser/prefetch/queue.ts +92 -20
  27. package/src/browser/prefetch/resource-ready.ts +77 -0
  28. package/src/browser/react/Link.tsx +53 -9
  29. package/src/browser/react/NavigationProvider.tsx +40 -4
  30. package/src/browser/react/context.ts +7 -2
  31. package/src/browser/react/use-handle.ts +9 -58
  32. package/src/browser/react/use-router.ts +21 -8
  33. package/src/browser/rsc-router.tsx +134 -59
  34. package/src/browser/scroll-restoration.ts +41 -42
  35. package/src/browser/segment-reconciler.ts +6 -1
  36. package/src/browser/server-action-bridge.ts +8 -6
  37. package/src/browser/types.ts +36 -5
  38. package/src/build/generate-manifest.ts +6 -6
  39. package/src/build/generate-route-types.ts +3 -0
  40. package/src/build/route-types/include-resolution.ts +8 -1
  41. package/src/build/route-types/router-processing.ts +223 -74
  42. package/src/build/route-types/scan-filter.ts +8 -1
  43. package/src/cache/cache-runtime.ts +15 -11
  44. package/src/cache/cache-scope.ts +48 -7
  45. package/src/cache/cf/cf-cache-store.ts +453 -11
  46. package/src/cache/cf/index.ts +5 -1
  47. package/src/cache/document-cache.ts +17 -7
  48. package/src/cache/index.ts +1 -0
  49. package/src/cache/taint.ts +55 -0
  50. package/src/client.tsx +2 -56
  51. package/src/context-var.ts +72 -2
  52. package/src/debug.ts +2 -2
  53. package/src/handle.ts +40 -0
  54. package/src/index.rsc.ts +3 -1
  55. package/src/index.ts +8 -0
  56. package/src/prerender/store.ts +5 -4
  57. package/src/prerender.ts +138 -77
  58. package/src/reverse.ts +22 -1
  59. package/src/route-definition/dsl-helpers.ts +73 -25
  60. package/src/route-definition/helpers-types.ts +10 -6
  61. package/src/route-definition/index.ts +3 -0
  62. package/src/route-definition/redirect.ts +11 -3
  63. package/src/route-definition/resolve-handler-use.ts +149 -0
  64. package/src/route-map-builder.ts +7 -1
  65. package/src/route-types.ts +11 -0
  66. package/src/router/content-negotiation.ts +100 -1
  67. package/src/router/find-match.ts +4 -2
  68. package/src/router/handler-context.ts +79 -23
  69. package/src/router/intercept-resolution.ts +11 -4
  70. package/src/router/lazy-includes.ts +4 -1
  71. package/src/router/loader-resolution.ts +122 -10
  72. package/src/router/logging.ts +5 -2
  73. package/src/router/manifest.ts +9 -3
  74. package/src/router/match-api.ts +124 -189
  75. package/src/router/match-middleware/background-revalidation.ts +30 -2
  76. package/src/router/match-middleware/cache-lookup.ts +88 -16
  77. package/src/router/match-middleware/cache-store.ts +53 -10
  78. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  79. package/src/router/match-middleware/segment-resolution.ts +61 -5
  80. package/src/router/match-result.ts +22 -6
  81. package/src/router/metrics.ts +6 -1
  82. package/src/router/middleware-types.ts +6 -8
  83. package/src/router/middleware.ts +4 -6
  84. package/src/router/navigation-snapshot.ts +182 -0
  85. package/src/router/prerender-match.ts +110 -10
  86. package/src/router/preview-match.ts +30 -102
  87. package/src/router/request-classification.ts +310 -0
  88. package/src/router/route-snapshot.ts +245 -0
  89. package/src/router/router-context.ts +6 -1
  90. package/src/router/router-interfaces.ts +36 -4
  91. package/src/router/router-options.ts +37 -11
  92. package/src/router/segment-resolution/fresh.ts +183 -20
  93. package/src/router/segment-resolution/helpers.ts +29 -24
  94. package/src/router/segment-resolution/loader-cache.ts +1 -0
  95. package/src/router/segment-resolution/revalidation.ts +412 -297
  96. package/src/router/segment-wrappers.ts +2 -0
  97. package/src/router/types.ts +1 -0
  98. package/src/router.ts +59 -6
  99. package/src/rsc/handler.ts +460 -368
  100. package/src/rsc/manifest-init.ts +5 -1
  101. package/src/rsc/progressive-enhancement.ts +4 -0
  102. package/src/rsc/rsc-rendering.ts +5 -0
  103. package/src/rsc/server-action.ts +2 -0
  104. package/src/rsc/ssr-setup.ts +2 -2
  105. package/src/rsc/types.ts +8 -1
  106. package/src/segment-system.tsx +140 -4
  107. package/src/server/context.ts +140 -14
  108. package/src/server/loader-registry.ts +9 -8
  109. package/src/server/request-context.ts +144 -18
  110. package/src/ssr/index.tsx +4 -0
  111. package/src/static-handler.ts +18 -6
  112. package/src/types/cache-types.ts +4 -4
  113. package/src/types/handler-context.ts +137 -33
  114. package/src/types/loader-types.ts +36 -9
  115. package/src/types/route-entry.ts +8 -1
  116. package/src/types/segments.ts +2 -0
  117. package/src/urls/path-helper-types.ts +9 -2
  118. package/src/urls/path-helper.ts +48 -13
  119. package/src/urls/pattern-types.ts +12 -0
  120. package/src/urls/response-types.ts +16 -6
  121. package/src/use-loader.tsx +73 -4
  122. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  123. package/src/vite/discovery/discover-routers.ts +5 -1
  124. package/src/vite/discovery/prerender-collection.ts +14 -1
  125. package/src/vite/discovery/state.ts +13 -6
  126. package/src/vite/index.ts +4 -0
  127. package/src/vite/plugin-types.ts +51 -79
  128. package/src/vite/plugins/expose-action-id.ts +1 -3
  129. package/src/vite/plugins/performance-tracks.ts +88 -0
  130. package/src/vite/plugins/refresh-cmd.ts +88 -26
  131. package/src/vite/plugins/version-plugin.ts +13 -1
  132. package/src/vite/rango.ts +163 -211
  133. package/src/vite/router-discovery.ts +153 -42
  134. package/src/vite/utils/banner.ts +3 -3
  135. package/src/vite/utils/prerender-utils.ts +18 -0
  136. package/src/vite/utils/shared-utils.ts +3 -2
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Resource Readiness
3
+ *
4
+ * Utilities to defer speculative prefetches until critical resources
5
+ * (viewport images) have finished loading. Prevents prefetch fetch()
6
+ * calls from competing with images for the browser's connection pool.
7
+ */
8
+
9
+ /**
10
+ * Resolve when all in-viewport images have finished loading.
11
+ * Returns immediately if no images are pending.
12
+ *
13
+ * Only checks images that exist at call time — does not observe
14
+ * dynamically added images. For SPA navigations where new images
15
+ * appear after render, call this after the navigation settles.
16
+ */
17
+ export function waitForViewportImages(): Promise<void> {
18
+ if (typeof document === "undefined") return Promise.resolve();
19
+
20
+ const pending = Array.from(document.querySelectorAll("img")).filter((img) => {
21
+ if (img.complete) return false;
22
+ const rect = img.getBoundingClientRect();
23
+ return (
24
+ rect.bottom > 0 &&
25
+ rect.right > 0 &&
26
+ rect.top < window.innerHeight &&
27
+ rect.left < window.innerWidth
28
+ );
29
+ });
30
+
31
+ if (pending.length === 0) return Promise.resolve();
32
+
33
+ return new Promise((resolve) => {
34
+ const settled = new Set<HTMLImageElement>();
35
+
36
+ const settle = (img: HTMLImageElement) => {
37
+ if (settled.has(img)) return;
38
+ settled.add(img);
39
+ if (settled.size >= pending.length) resolve();
40
+ };
41
+
42
+ for (const img of pending) {
43
+ img.addEventListener("load", () => settle(img), { once: true });
44
+ img.addEventListener("error", () => settle(img), { once: true });
45
+ // Re-check: image may have completed between the initial filter
46
+ // and listener attachment. settle() is idempotent per image, so
47
+ // a queued load event firing afterward is harmless.
48
+ if (img.complete) settle(img);
49
+ }
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Resolve after the given number of milliseconds.
55
+ */
56
+ export function wait(ms: number): Promise<void> {
57
+ return new Promise((resolve) => setTimeout(resolve, ms));
58
+ }
59
+
60
+ /**
61
+ * Resolve when the browser has an idle main-thread moment.
62
+ * Uses requestIdleCallback where available, falls back to setTimeout.
63
+ *
64
+ * This is a scheduling hint, not an asset-loaded detector — combine
65
+ * with waitForViewportImages() for full resource readiness.
66
+ */
67
+ export function waitForIdle(timeout = 200): Promise<void> {
68
+ if (typeof window !== "undefined" && "requestIdleCallback" in window) {
69
+ return new Promise((resolve) => {
70
+ window.requestIdleCallback(() => resolve(), { timeout });
71
+ });
72
+ }
73
+
74
+ return new Promise((resolve) => {
75
+ setTimeout(resolve, 0);
76
+ });
77
+ }
@@ -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,6 +33,7 @@ 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,
@@ -192,6 +194,16 @@ export const Link: ForwardRefExoticComponent<
192
194
  const ctx = useContext(NavigationStoreContext);
193
195
  const isExternal = isExternalUrl(to);
194
196
 
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
+
195
207
  // Resolve adaptive: viewport on touch devices, hover on pointer devices
196
208
  const resolvedStrategy =
197
209
  prefetch === "adaptive" ? (isTouchDevice ? "viewport" : "hover") : prefetch;
@@ -273,17 +285,44 @@ export const Link: ForwardRefExoticComponent<
273
285
  resolvedState = currentState;
274
286
  }
275
287
 
276
- ctx.navigate(to, { replace, scroll, state: resolvedState, revalidate });
288
+ ctx.navigate(resolvedTo, {
289
+ replace,
290
+ scroll,
291
+ state: resolvedState,
292
+ revalidate,
293
+ });
277
294
  },
278
- [to, isExternal, reloadDocument, replace, scroll, revalidate, ctx, onClick],
295
+ [
296
+ resolvedTo,
297
+ isExternal,
298
+ reloadDocument,
299
+ replace,
300
+ scroll,
301
+ revalidate,
302
+ ctx,
303
+ onClick,
304
+ ],
279
305
  );
280
306
 
281
307
  const handleMouseEnter = useCallback(() => {
282
- 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.
283
317
  const segmentState = ctx.store.getSegmentState();
284
- prefetchDirect(to, segmentState.currentSegmentIds, ctx.version);
318
+ prefetchDirect(
319
+ resolvedTo,
320
+ segmentState.currentSegmentIds,
321
+ getAppVersion(),
322
+ ctx.store.getRouterId?.(),
323
+ );
285
324
  }
286
- }, [resolvedStrategy, to, isExternal, ctx]);
325
+ }, [resolvedStrategy, resolvedTo, isExternal, ctx]);
287
326
 
288
327
  // Viewport/render prefetch: waits for idle before starting,
289
328
  // uses concurrency-limited queue to avoid flooding.
@@ -300,7 +339,12 @@ export const Link: ForwardRefExoticComponent<
300
339
  const triggerPrefetch = () => {
301
340
  if (cancelled) return;
302
341
  const segmentState = ctx.store.getSegmentState();
303
- prefetchQueued(to, segmentState.currentSegmentIds, ctx.version);
342
+ prefetchQueued(
343
+ resolvedTo,
344
+ segmentState.currentSegmentIds,
345
+ getAppVersion(),
346
+ ctx.store.getRouterId?.(),
347
+ );
304
348
  };
305
349
 
306
350
  // Schedule prefetch only when the app is idle (no navigation/streaming).
@@ -339,12 +383,12 @@ export const Link: ForwardRefExoticComponent<
339
383
  unobserveForPrefetch(observedElement);
340
384
  }
341
385
  };
342
- }, [resolvedStrategy, to, isExternal, ctx]);
386
+ }, [resolvedStrategy, resolvedTo, isExternal, ctx]);
343
387
 
344
388
  return (
345
389
  <a
346
390
  ref={setRef}
347
- href={to}
391
+ href={resolvedTo}
348
392
  onClick={handleClick}
349
393
  onMouseEnter={handleMouseEnter}
350
394
  data-link-component
@@ -354,7 +398,7 @@ export const Link: ForwardRefExoticComponent<
354
398
  data-revalidate={revalidate === false ? "false" : undefined}
355
399
  {...props}
356
400
  >
357
- <LinkContext.Provider value={to}>{children}</LinkContext.Provider>
401
+ <LinkContext.Provider value={resolvedTo}>{children}</LinkContext.Provider>
358
402
  </a>
359
403
  );
360
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
  }