@rangojs/router 0.20.0 → 0.22.0

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 (331) hide show
  1. package/dist/bin/rango.js +39 -15
  2. package/dist/types/browser/event-controller.d.ts +49 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-store.d.ts +9 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  6. package/dist/types/browser/partial-update.d.ts +2 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  8. package/dist/types/browser/rango-state.d.ts +3 -1
  9. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  10. package/dist/types/browser/react/context.d.ts +25 -0
  11. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  12. package/dist/types/browser/react/location-state.d.ts +7 -2
  13. package/dist/types/browser/rsc-router.d.ts +13 -2
  14. package/dist/types/browser/types.d.ts +10 -0
  15. package/dist/types/build/generate-manifest.d.ts +2 -0
  16. package/dist/types/cache/background-task.d.ts +22 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  18. package/dist/types/cache/cache-policy.d.ts +17 -0
  19. package/dist/types/cache/cache-scope.d.ts +5 -6
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  22. package/dist/types/cache/document-cache.d.ts +8 -5
  23. package/dist/types/cache/handle-snapshot.d.ts +59 -50
  24. package/dist/types/cache/index.d.ts +3 -1
  25. package/dist/types/cache/invalidation-order.d.ts +14 -0
  26. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  27. package/dist/types/cache/shell-snapshot.d.ts +29 -4
  28. package/dist/types/cache/store-scope.d.ts +22 -0
  29. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  30. package/dist/types/cache/types.d.ts +84 -24
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
  32. package/dist/types/client-urls/navigation.d.ts +7 -1
  33. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  34. package/dist/types/errors.d.ts +6 -0
  35. package/dist/types/handles/deferred-resolution.d.ts +18 -1
  36. package/dist/types/internal-suspense-audit.d.ts +1 -0
  37. package/dist/types/loader-store.d.ts +4 -8
  38. package/dist/types/outlet-provider.d.ts +2 -1
  39. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  40. package/dist/types/prerender/cloudflare.d.ts +49 -0
  41. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  42. package/dist/types/prerender/index.d.ts +12 -0
  43. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  44. package/dist/types/prerender/on-demand.d.ts +272 -0
  45. package/dist/types/prerender/producer-guard.d.ts +33 -0
  46. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  47. package/dist/types/prerender/warm-request.d.ts +65 -0
  48. package/dist/types/prerender/warm.d.ts +65 -0
  49. package/dist/types/prerender/writable-store.d.ts +170 -0
  50. package/dist/types/prerender.d.ts +27 -0
  51. package/dist/types/reverse.d.ts +1 -1
  52. package/dist/types/route-content-wrapper.d.ts +3 -1
  53. package/dist/types/router/error-handling.d.ts +2 -0
  54. package/dist/types/router/handler-context.d.ts +1 -1
  55. package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
  56. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  57. package/dist/types/router/pattern-matching.d.ts +2 -0
  58. package/dist/types/router/prefetch-default.d.ts +4 -4
  59. package/dist/types/router/prerender-match.d.ts +23 -1
  60. package/dist/types/router/route-trie-builder.d.ts +8 -1
  61. package/dist/types/router/router-context.d.ts +1 -0
  62. package/dist/types/router/router-interfaces.d.ts +32 -1
  63. package/dist/types/router/router-options.d.ts +39 -5
  64. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  65. package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  67. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  68. package/dist/types/router/trie-matching.d.ts +2 -0
  69. package/dist/types/router-versions.d.ts +39 -0
  70. package/dist/types/rsc/handler-context.d.ts +5 -0
  71. package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
  72. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  73. package/dist/types/rsc/shell-capture.d.ts +14 -1
  74. package/dist/types/rsc/shell-serve.d.ts +2 -2
  75. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  76. package/dist/types/rsc/types.d.ts +12 -6
  77. package/dist/types/segment-boundary-content.d.ts +17 -0
  78. package/dist/types/segment-loader-promise.d.ts +2 -6
  79. package/dist/types/server/build-version-table.d.ts +53 -0
  80. package/dist/types/server/context.d.ts +12 -4
  81. package/dist/types/server/handle-store.d.ts +89 -46
  82. package/dist/types/server/request-context.d.ts +84 -9
  83. package/dist/types/suspense-audit-react.d.ts +26 -0
  84. package/dist/types/suspense-audit.d.ts +157 -0
  85. package/dist/types/testing/action-form.d.ts +34 -0
  86. package/dist/types/testing/build-versions.d.ts +32 -0
  87. package/dist/types/testing/dom.entry.d.ts +1 -1
  88. package/dist/types/testing/flight.entry.d.ts +3 -1
  89. package/dist/types/testing/index.d.ts +6 -0
  90. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  91. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  92. package/dist/types/testing/location-state-key.d.ts +5 -1
  93. package/dist/types/testing/render-route.d.ts +77 -6
  94. package/dist/types/testing/serve-shell-request.d.ts +55 -4
  95. package/dist/types/testing/shell-status.d.ts +14 -6
  96. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  97. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  98. package/dist/types/thenable-status.d.ts +6 -0
  99. package/dist/types/types/handler-context.d.ts +15 -5
  100. package/dist/types/types/route-entry.d.ts +5 -0
  101. package/dist/types/urls/path-helper.d.ts +18 -0
  102. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  103. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  104. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  105. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  106. package/dist/types/vite/discovery/state.d.ts +25 -0
  107. package/dist/types/vite/encryption-key.d.ts +18 -0
  108. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  109. package/dist/types/vite/plugin-types.d.ts +27 -0
  110. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  111. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  112. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  113. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  114. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  115. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  116. package/dist/types/vite/router-discovery.d.ts +6 -0
  117. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  118. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  119. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  120. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  121. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  122. package/dist/vite/index.js +1707 -931
  123. package/package.json +12 -3
  124. package/skills/cache-guide/SKILL.md +102 -37
  125. package/skills/caching/SKILL.md +56 -9
  126. package/skills/catalog.json +1 -1
  127. package/skills/client-urls/SKILL.md +16 -0
  128. package/skills/cloudflare/SKILL.md +133 -1
  129. package/skills/comparison/references/framework-comparison.md +17 -13
  130. package/skills/deployment-caching/SKILL.md +33 -12
  131. package/skills/document-cache/SKILL.md +16 -14
  132. package/skills/hooks/SKILL.md +25 -0
  133. package/skills/hooks/handle-and-actions.md +5 -0
  134. package/skills/hooks/navigation.md +38 -0
  135. package/skills/hooks/state.md +213 -11
  136. package/skills/hooks/urls.md +40 -1
  137. package/skills/host-router/SKILL.md +31 -0
  138. package/skills/intercept/SKILL.md +6 -0
  139. package/skills/loader/SKILL.md +25 -5
  140. package/skills/middleware/SKILL.md +2 -0
  141. package/skills/migrate-nextjs/SKILL.md +82 -9
  142. package/skills/ppr/SKILL.md +150 -48
  143. package/skills/prerender/SKILL.md +651 -10
  144. package/skills/rango/SKILL.md +9 -7
  145. package/skills/response-routes/SKILL.md +5 -5
  146. package/skills/route/SKILL.md +29 -0
  147. package/skills/router-setup/SKILL.md +15 -1
  148. package/skills/testing/SKILL.md +32 -27
  149. package/skills/testing/cache-prerender.md +18 -17
  150. package/skills/testing/client-components.md +193 -8
  151. package/skills/testing/handles.md +2 -0
  152. package/skills/testing/loader.md +1 -1
  153. package/skills/testing/middleware.md +1 -1
  154. package/skills/testing/render-handler.md +18 -18
  155. package/skills/testing/serve-shell-request.md +167 -22
  156. package/skills/testing/server-actions.md +17 -17
  157. package/skills/testing/server-tree.md +1 -1
  158. package/skills/typesafety/generated-files-and-cli.md +1 -1
  159. package/skills/use-cache/SKILL.md +25 -10
  160. package/skills/vercel/SKILL.md +5 -3
  161. package/skills/view-transitions/SKILL.md +9 -4
  162. package/src/bin/rango.ts +1 -1
  163. package/src/browser/event-controller.ts +119 -2
  164. package/src/browser/history-state.ts +39 -17
  165. package/src/browser/link-interceptor.ts +2 -2
  166. package/src/browser/navigation-bridge.ts +104 -41
  167. package/src/browser/navigation-store.ts +26 -0
  168. package/src/browser/navigation-transaction.ts +24 -12
  169. package/src/browser/network-error-handler.ts +2 -0
  170. package/src/browser/partial-update.ts +58 -14
  171. package/src/browser/prefetch/default-strategy.ts +22 -42
  172. package/src/browser/rango-state.ts +3 -1
  173. package/src/browser/react/Link.tsx +37 -43
  174. package/src/browser/react/NavigationProvider.tsx +85 -1
  175. package/src/browser/react/context.ts +30 -0
  176. package/src/browser/react/location-state-shared.ts +200 -34
  177. package/src/browser/react/location-state.ts +28 -77
  178. package/src/browser/react/use-action.ts +4 -1
  179. package/src/browser/react/use-handle.ts +3 -0
  180. package/src/browser/react/use-link-status.ts +4 -3
  181. package/src/browser/react/use-navigation.ts +4 -1
  182. package/src/browser/react/use-params.ts +4 -1
  183. package/src/browser/react/use-pathname.ts +4 -1
  184. package/src/browser/react/use-search-params.ts +5 -3
  185. package/src/browser/react/use-segments.ts +36 -54
  186. package/src/browser/rsc-router.tsx +93 -31
  187. package/src/browser/segment-reconciler.ts +3 -3
  188. package/src/browser/server-action-bridge.ts +43 -7
  189. package/src/browser/types.ts +11 -0
  190. package/src/build/generate-manifest.ts +17 -0
  191. package/src/build/merge-full-manifests.ts +5 -0
  192. package/src/build/route-trie.ts +1 -0
  193. package/src/cache/background-task.ts +61 -0
  194. package/src/cache/cache-key-utils.ts +91 -12
  195. package/src/cache/cache-policy.ts +11 -2
  196. package/src/cache/cache-runtime.ts +23 -23
  197. package/src/cache/cache-scope.ts +48 -36
  198. package/src/cache/cf/cf-cache-store.ts +91 -66
  199. package/src/cache/cf/cf-cache-types.ts +12 -4
  200. package/src/cache/document-cache.ts +35 -14
  201. package/src/cache/handle-snapshot.ts +118 -86
  202. package/src/cache/index.ts +5 -0
  203. package/src/cache/invalidation-order.ts +53 -0
  204. package/src/cache/memory-segment-store.ts +52 -1
  205. package/src/cache/shell-snapshot.ts +52 -5
  206. package/src/cache/store-scope.ts +36 -0
  207. package/src/cache/tag-invalidation.ts +7 -5
  208. package/src/cache/types.ts +90 -24
  209. package/src/cache/vercel/vercel-cache-store.ts +68 -26
  210. package/src/client-urls/client-root.tsx +1 -0
  211. package/src/client-urls/navigation.ts +10 -0
  212. package/src/client-urls/optimistic-location.ts +10 -4
  213. package/src/client.tsx +40 -3
  214. package/src/errors.ts +13 -6
  215. package/src/handles/deferred-resolution.ts +36 -0
  216. package/src/internal-suspense-audit.ts +12 -0
  217. package/src/loader-store.ts +4 -9
  218. package/src/outlet-provider.tsx +25 -1
  219. package/src/prerender/build-shell-capture.ts +11 -9
  220. package/src/prerender/cloudflare.ts +130 -0
  221. package/src/prerender/create-prerender-trigger.ts +780 -0
  222. package/src/prerender/index.ts +39 -0
  223. package/src/prerender/memory-prerender-store.ts +115 -0
  224. package/src/prerender/on-demand.ts +339 -0
  225. package/src/prerender/producer-guard.ts +66 -0
  226. package/src/prerender/shell-manifest-key.ts +12 -17
  227. package/src/prerender/warm-request.ts +154 -0
  228. package/src/prerender/warm.ts +193 -0
  229. package/src/prerender/writable-store.ts +278 -0
  230. package/src/prerender.ts +29 -0
  231. package/src/reverse.ts +1 -1
  232. package/src/route-content-wrapper.tsx +107 -13
  233. package/src/router/error-handling.ts +12 -12
  234. package/src/router/find-match.ts +1 -0
  235. package/src/router/handler-context.ts +2 -0
  236. package/src/router/intercept-resolution.ts +15 -8
  237. package/src/router/loader-resolution.ts +2 -10
  238. package/src/router/manifest.ts +2 -0
  239. package/src/router/match-middleware/background-revalidation.ts +2 -0
  240. package/src/router/match-middleware/cache-lookup.ts +239 -54
  241. package/src/router/navigation-snapshot.ts +25 -6
  242. package/src/router/pattern-matching.ts +14 -0
  243. package/src/router/prefetch-default.ts +4 -4
  244. package/src/router/prerender-match.ts +460 -352
  245. package/src/router/request-classification.ts +15 -12
  246. package/src/router/route-trie-builder.ts +9 -0
  247. package/src/router/router-context.ts +1 -0
  248. package/src/router/router-interfaces.ts +35 -1
  249. package/src/router/router-options.ts +45 -5
  250. package/src/router/segment-resolution/fresh.ts +20 -19
  251. package/src/router/segment-resolution/loader-cache.ts +110 -77
  252. package/src/router/segment-resolution/revalidation.ts +34 -24
  253. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  254. package/src/router/trie-matching.ts +3 -0
  255. package/src/router-versions.ts +60 -0
  256. package/src/router.ts +107 -1
  257. package/src/rsc/full-payload.ts +6 -2
  258. package/src/rsc/handler-context.ts +5 -0
  259. package/src/rsc/handler.ts +64 -7
  260. package/src/rsc/progressive-enhancement.ts +5 -1
  261. package/src/rsc/response-cache-serve.ts +10 -4
  262. package/src/rsc/rsc-rendering.ts +65 -12
  263. package/src/rsc/shell-build-manifest.ts +17 -12
  264. package/src/rsc/shell-capture-constants.ts +39 -7
  265. package/src/rsc/shell-capture.ts +144 -45
  266. package/src/rsc/shell-serve.ts +12 -6
  267. package/src/rsc/ssr-module-loader.ts +26 -0
  268. package/src/rsc/types.ts +12 -6
  269. package/src/segment-boundary-content.ts +25 -0
  270. package/src/segment-loader-promise.ts +28 -38
  271. package/src/segment-system.tsx +117 -57
  272. package/src/server/build-version-table.ts +130 -0
  273. package/src/server/context.ts +40 -17
  274. package/src/server/cookie-store.ts +5 -0
  275. package/src/server/handle-store.ts +357 -127
  276. package/src/server/request-context.ts +149 -12
  277. package/src/ssr/ssr-root.tsx +32 -13
  278. package/src/suspense-audit-react.tsx +98 -0
  279. package/src/suspense-audit.ts +1030 -0
  280. package/src/testing/action-form.ts +66 -0
  281. package/src/testing/build-versions.ts +64 -0
  282. package/src/testing/dispatch.ts +6 -0
  283. package/src/testing/dom.entry.ts +1 -0
  284. package/src/testing/flight-runtime.d.ts +27 -0
  285. package/src/testing/flight.entry.ts +4 -0
  286. package/src/testing/index.ts +22 -0
  287. package/src/testing/internal/context.ts +2 -1
  288. package/src/testing/internal/seed-vars.ts +14 -2
  289. package/src/testing/internal/server-modules.ts +32 -0
  290. package/src/testing/location-state-key.ts +18 -8
  291. package/src/testing/render-handler.ts +2 -1
  292. package/src/testing/render-route.tsx +293 -46
  293. package/src/testing/serve-shell-request.ts +262 -37
  294. package/src/testing/shell-status.ts +17 -6
  295. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  296. package/src/testing/vitest-stubs/version.ts +4 -1
  297. package/src/thenable-status.ts +10 -0
  298. package/src/types/handler-context.ts +15 -5
  299. package/src/types/route-entry.ts +6 -0
  300. package/src/urls/path-helper.ts +69 -9
  301. package/src/use-loader.tsx +57 -30
  302. package/src/vite/discovery/build-versions.ts +1281 -0
  303. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  304. package/src/vite/discovery/discover-routers.ts +15 -0
  305. package/src/vite/discovery/installed-packages.ts +159 -0
  306. package/src/vite/discovery/prerender-collection.ts +38 -1
  307. package/src/vite/discovery/router-versions-phase.ts +336 -0
  308. package/src/vite/discovery/shell-prerender-phase.ts +33 -43
  309. package/src/vite/discovery/state.ts +36 -1
  310. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  311. package/src/vite/encryption-key.ts +101 -9
  312. package/src/vite/inject-client-debug.ts +42 -20
  313. package/src/vite/plugin-types.ts +28 -0
  314. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  315. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  316. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  317. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  318. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  319. package/src/vite/plugins/version-plugin.ts +19 -24
  320. package/src/vite/plugins/version.d.ts +17 -2
  321. package/src/vite/plugins/virtual-entries.ts +39 -6
  322. package/src/vite/rango.ts +28 -3
  323. package/src/vite/router-discovery.ts +96 -13
  324. package/src/vite/utils/bundle-analysis.ts +39 -7
  325. package/src/vite/utils/client-chunks.ts +5 -1
  326. package/src/vite/utils/compare-strings.ts +9 -0
  327. package/src/vite/utils/package-resolution.ts +9 -0
  328. package/src/vite/utils/prerender-utils.ts +4 -1
  329. package/src/vite/utils/shared-utils.ts +1 -1
  330. package/dist/types/segment-content-promise.d.ts +0 -13
  331. package/src/segment-content-promise.ts +0 -67
@@ -4,7 +4,10 @@ import type {
4
4
  ResolvedSegment,
5
5
  StreamingToken,
6
6
  } from "./types.js";
7
- import { generateHistoryKey } from "./navigation-store.js";
7
+ import {
8
+ generateHistoryKey,
9
+ liveHandleDataForCache,
10
+ } from "./navigation-store.js";
8
11
  import {
9
12
  handleNavigationStart,
10
13
  ensureHistoryKey,
@@ -13,7 +16,6 @@ import type { EventController, NavigationHandle } from "./event-controller.js";
13
16
  import { debugLog } from "./logging.js";
14
17
  import {
15
18
  buildHistoryState,
16
- hasLocationState,
17
19
  mergeLocationState,
18
20
  pushHistoryWithIdx,
19
21
  } from "./history-state.js";
@@ -59,6 +61,11 @@ interface CommitOptions {
59
61
  * partial updater reuses it instead of deciding again.
60
62
  */
61
63
  transitionGatedOff?: boolean;
64
+ /**
65
+ * No payload follows this commit: every segment is kept and the tree on
66
+ * screen stays (EventController.commitLocationState).
67
+ */
68
+ treeless?: boolean;
62
69
  }
63
70
 
64
71
  /**
@@ -81,6 +88,8 @@ interface BoundCommitOverrides {
81
88
  serverState?: Record<string, unknown>;
82
89
  /** The committed route's name (payload metadata), remembered per history entry. */
83
90
  routeName?: string;
91
+ /** No payload follows this commit (CommitOptions.treeless). */
92
+ treeless?: boolean;
84
93
  }
85
94
 
86
95
  /**
@@ -174,7 +183,11 @@ export function createNavigationTransaction(
174
183
 
175
184
  store.setHistoryKey(historyKey);
176
185
 
177
- const currentHandleData = eventController.getHandleState().data;
186
+ // storeOnly re-caches the page on screen (an action refetch), which can
187
+ // commit with no tree and so never release the hydration window.
188
+ const currentHandleData = storeOnly
189
+ ? liveHandleDataForCache(eventController, store, historyKey)
190
+ : eventController.getHandleState().data;
178
191
  store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
179
192
 
180
193
  if (storeOnly) {
@@ -187,10 +200,13 @@ export function createNavigationTransaction(
187
200
  }
188
201
 
189
202
  if (traversal) {
190
- if (serverState && Object.keys(serverState).length > 0) {
191
- mergeLocationState(serverState);
192
- }
203
+ // The entry as history restored it, plus what the server adds.
204
+ const entryState: unknown =
205
+ serverState && Object.keys(serverState).length > 0
206
+ ? mergeLocationState(serverState)
207
+ : window.history.state;
193
208
  store.rememberDisplayedEntry(opts.routeName);
209
+ eventController.commitLocationState(entryState, opts.treeless);
194
210
  handle.complete(parsedUrl);
195
211
  debugLog("[Browser] Traversal committed, historyKey:", historyKey);
196
212
  return { scroll };
@@ -202,15 +218,10 @@ export function createNavigationTransaction(
202
218
  serverState,
203
219
  );
204
220
 
205
- const oldState = window.history.state;
206
-
207
221
  pushHistoryWithIdx(historyState, url, replace ?? false);
208
222
  ensureHistoryKey();
209
223
  store.rememberDisplayedEntry(opts.routeName);
210
-
211
- if (hasLocationState(oldState) || hasLocationState(historyState)) {
212
- window.dispatchEvent(new Event("__rsc_locationstate"));
213
- }
224
+ eventController.commitLocationState(historyState, opts.treeless);
214
225
 
215
226
  handle.complete(parsedUrl);
216
227
 
@@ -269,6 +280,7 @@ export function createNavigationTransaction(
269
280
  cacheOnly,
270
281
  serverState,
271
282
  routeName: overrides?.routeName ?? opts.routeName,
283
+ treeless: overrides?.treeless,
272
284
  });
273
285
  },
274
286
  };
@@ -2,6 +2,7 @@ import { NetworkError, isNetworkError } from "../errors.js";
2
2
  import { RenderErrorThrower } from "../render-error-thrower.js";
3
3
  import type { UpdateSubscriber } from "./types.js";
4
4
  import { createElement, startTransition } from "react";
5
+ import { auditTreeCause } from "../suspense-audit.js";
5
6
 
6
7
  /**
7
8
  * Convert an unknown error to a NetworkError, or return null if not network-related.
@@ -33,6 +34,7 @@ function emitErrorToBoundary(
33
34
  error: unknown,
34
35
  pathname: string,
35
36
  ): void {
37
+ if (process.env.NODE_ENV !== "production") auditTreeCause("error");
36
38
  startTransition(() => {
37
39
  onUpdate({
38
40
  root: createElement(RenderErrorThrower, { error }),
@@ -31,7 +31,7 @@ import {
31
31
  } from "./validate-redirect-origin.js";
32
32
  import type { NavigationUpdate } from "./types.js";
33
33
  import { OPTIMISTIC_COMMIT_TRANSITION_TYPE } from "./optimistic-commit.js";
34
- import { loaderStore } from "../loader-store.js";
34
+ import type { LoaderStore } from "../loader-store.js";
35
35
  import {
36
36
  collectClientRevalidationDecisions,
37
37
  setActiveInterceptTargets,
@@ -42,7 +42,9 @@ import {
42
42
  type TransitionWhenActionInput,
43
43
  } from "./transition-when.js";
44
44
  import { buildHistoryState } from "./history-state.js";
45
+ import { addLocationState } from "./react/location-state-shared.js";
45
46
  import type { TransitionWhenKind } from "../types/segments.js";
47
+ import { auditTreeCause, type TreeUpdateCause } from "../suspense-audit.js";
46
48
 
47
49
  function toScrollPayload(
48
50
  scroll: boolean | undefined,
@@ -69,6 +71,13 @@ export function shouldStartViewTransition(
69
71
  return !hasIntercept && hasTransition;
70
72
  }
71
73
 
74
+ // Dev only (suspense-audit.ts I6): what this updater's commit is.
75
+ function treeCause(mode: UpdateMode, traversal?: boolean): TreeUpdateCause {
76
+ if (mode.type === "action") return "action";
77
+ if (mode.type === "stale-revalidation") return "stale-revalidation";
78
+ return traversal ? "popstate" : "navigation";
79
+ }
80
+
72
81
  /**
73
82
  * Transition commit. Every held commit announces the loader streams the
74
83
  * committed tree is still receiving (loader-store.ts announcePendingStreams)
@@ -78,13 +87,14 @@ export function shouldStartViewTransition(
78
87
  * its fallback. Also used by renderRoute's navigate() (testing/render-route.tsx).
79
88
  */
80
89
  export function commitInTransition(
90
+ loaders: LoaderStore,
81
91
  onUpdate: UpdateSubscriber,
82
92
  segments: readonly ResolvedSegment[],
83
93
  update: NavigationUpdate,
84
94
  transitionTypes: readonly string[],
85
95
  ): void {
86
96
  startTransition(() => {
87
- loaderStore.announcePendingStreams(segments);
97
+ loaders.announcePendingStreams(segments);
88
98
  if (addTransitionType) {
89
99
  for (const type of transitionTypes) addTransitionType(type);
90
100
  }
@@ -225,8 +235,14 @@ export function createPartialUpdater(
225
235
  segments = segmentIds ?? segmentState.currentSegmentIds;
226
236
  }
227
237
 
238
+ // The page the client is on, which the server diffs the target against.
239
+ // tx.currentUrl is window.location when the transaction was created: on
240
+ // a back/forward the popstate event had already moved it to the target,
241
+ // and a server told the target is also the source revalidates nothing on
242
+ // the same route, so the page being left stayed on screen (#1030). The
243
+ // store's URL is the committed entry's until tx.commit().
228
244
  const previousUrl =
229
- mode.type === "leave-intercept"
245
+ mode.type === "leave-intercept" || (tx.traversal && !interceptSourceUrl)
230
246
  ? segmentState.currentUrl || tx.currentUrl
231
247
  : interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
232
248
 
@@ -279,7 +295,7 @@ export function createPartialUpdater(
279
295
  params: metadata.params ?? mergeSegmentParams(segmentsToCommit),
280
296
  routeName: metadata.routeName,
281
297
  state: tx.traversal
282
- ? { ...window.history.state, ...metadata.locationState }
298
+ ? addLocationState(window.history.state, metadata.locationState)
283
299
  : buildHistoryState(tx.state, undefined, metadata.locationState),
284
300
  }),
285
301
  action:
@@ -457,10 +473,17 @@ export function createPartialUpdater(
457
473
  scroll: toScrollPayload(commitScroll),
458
474
  };
459
475
 
476
+ if (process.env.NODE_ENV !== "production") {
477
+ auditTreeCause(treeCause(mode, tx.traversal));
478
+ }
460
479
  if (shouldStartViewTransition(existingSegments, cachedGatedOff)) {
461
- commitInTransition(onUpdate, existingSegments, cachedUpdate, [
462
- "navigation",
463
- ]);
480
+ commitInTransition(
481
+ store.loaders,
482
+ onUpdate,
483
+ existingSegments,
484
+ cachedUpdate,
485
+ ["navigation"],
486
+ );
464
487
  } else {
465
488
  onUpdate(cachedUpdate);
466
489
  }
@@ -484,6 +507,9 @@ export function createPartialUpdater(
484
507
  { routeName: payload.metadata.routeName },
485
508
  );
486
509
 
510
+ if (process.env.NODE_ENV !== "production") {
511
+ auditTreeCause(treeCause(mode, tx.traversal));
512
+ }
487
513
  onUpdate({
488
514
  root: newTree,
489
515
  metadata: payload.metadata,
@@ -499,6 +525,7 @@ export function createPartialUpdater(
499
525
  );
500
526
  tx.commit(matchedIds, existingSegments, {
501
527
  routeName: payload.metadata.routeName,
528
+ treeless: true,
502
529
  });
503
530
  debugLog("[Browser] Navigation complete (no re-render)");
504
531
  return;
@@ -710,11 +737,15 @@ export function createPartialUpdater(
710
737
  metadata: payload.metadata!,
711
738
  scroll: scrollPayload,
712
739
  };
740
+ if (process.env.NODE_ENV !== "production") {
741
+ auditTreeCause(treeCause(mode, tx.traversal));
742
+ }
713
743
  if (
714
744
  !gatedOff &&
715
745
  (mode.type === "action" || mode.type === "stale-revalidation")
716
746
  ) {
717
747
  commitInTransition(
748
+ store.loaders,
718
749
  onUpdate,
719
750
  reconciled.mainSegments,
720
751
  update,
@@ -722,6 +753,7 @@ export function createPartialUpdater(
722
753
  );
723
754
  } else if (hasTransition) {
724
755
  commitInTransition(
756
+ store.loaders,
725
757
  onUpdate,
726
758
  reconciled.mainSegments,
727
759
  update,
@@ -756,6 +788,7 @@ export function createPartialUpdater(
756
788
  // segment reconciles in place (clientGroup key), so a read that still
757
789
  // suspends must hold the presented content, not flash a fallback.
758
790
  commitInTransition(
791
+ store.loaders,
759
792
  onUpdate,
760
793
  reconciled.mainSegments,
761
794
  update,
@@ -764,12 +797,13 @@ export function createPartialUpdater(
764
797
  } else {
765
798
  // Cold/partially-prefetched nav that mounts NEW segments, or any
766
799
  // commit transition({ when }) gated off (navigation, action refetch,
767
- // stale revalidation): normal commit so fallbacks stream
768
- // like a first load and the click has visible feedback. A gated-off
769
- // segment keeps its key, so its loading() fallback comes from its
770
- // boundary re-suspending on this urgent commit, not from a remount
771
- // (#995). Explicit transition() routes keep the content-hold via the
772
- // hasTransition branch above (the opt-in).
800
+ // stale revalidation): normal, unanimated commit. A segment with work
801
+ // still pending shows its loading() fallback like a first load; one
802
+ // whose data is already in hand shows none. A gated-off segment keeps
803
+ // its key, so a fallback comes from its boundary suspending on pending
804
+ // work in this urgent commit, not from a remount (#995). Explicit
805
+ // transition() routes keep the content-hold via the hasTransition
806
+ // branch above (the opt-in).
773
807
  onUpdate(update);
774
808
  }
775
809
 
@@ -814,6 +848,9 @@ export function createPartialUpdater(
814
848
  scroll: fullScrollPayload,
815
849
  };
816
850
 
851
+ if (process.env.NODE_ENV !== "production") {
852
+ auditTreeCause(treeCause(mode, tx.traversal));
853
+ }
817
854
  if (mode.type === "stale-revalidation") {
818
855
  await rawStreamComplete;
819
856
  // Mirror the partial branch's history-key staleness guard (above): the
@@ -828,9 +865,13 @@ export function createPartialUpdater(
828
865
  );
829
866
  return;
830
867
  }
868
+ if (process.env.NODE_ENV !== "production") {
869
+ auditTreeCause("stale-revalidation");
870
+ }
831
871
  if (fullGatedOff) onUpdate(fullUpdate);
832
872
  else {
833
873
  commitInTransition(
874
+ store.loaders,
834
875
  onUpdate,
835
876
  segments,
836
877
  fullUpdate,
@@ -839,13 +880,16 @@ export function createPartialUpdater(
839
880
  }
840
881
  } else if (mode.type === "action" && !fullGatedOff) {
841
882
  commitInTransition(
883
+ store.loaders,
842
884
  onUpdate,
843
885
  segments,
844
886
  fullUpdate,
845
887
  fullHasTransition ? ["action"] : [],
846
888
  );
847
889
  } else if (fullHasTransition) {
848
- commitInTransition(onUpdate, segments, fullUpdate, ["navigation"]);
890
+ commitInTransition(store.loaders, onUpdate, segments, fullUpdate, [
891
+ "navigation",
892
+ ]);
849
893
  } else {
850
894
  onUpdate(fullUpdate);
851
895
  }
@@ -2,68 +2,48 @@
2
2
  * Router-wide default prefetch strategy (client seat).
3
3
  *
4
4
  * The server resolves `createRouter({ defaultPrefetch })` once at router init
5
- * (router/prefetch-default.ts) and ships it in initial payload metadata; the
6
- * browser entry applies it here before hydration — same lifecycle as
7
- * `initPrefetchCache` / `setPrefetchConcurrency`. Every `<Link>` without an
8
- * explicit `prefetch` prop and every eligible intercepted plain anchor that has
9
- * not opted out with `data-prefetch="false"` or `data-prefetch="none"` uses it.
10
- * Containers use the same `false`/`none` vocabulary via
11
- * `data-prefetch-scope`.
12
- *
13
- * The module initial value must equal the server resolver's environment default:
14
- * `"none"` in development and `"viewport"` in production. During SSR this
15
- * module is never initialized from metadata, so keeping both seats aligned also
16
- * gives metadata-less payloads the documented behavior.
5
+ * (router/prefetch-default.ts) and ships it in initial payload metadata. The
6
+ * router store carries it to its readers: `<Link>` reads
7
+ * `NavigationStoreContext.defaultPrefetch`, and delegated plain anchors get it
8
+ * from the navigation bridge. Every `<Link>` without an explicit `prefetch`
9
+ * prop and every eligible intercepted plain anchor that has not opted out with
10
+ * `data-prefetch="false"` or `data-prefetch="none"` uses it. Containers use the
11
+ * same `false`/`none` vocabulary via `data-prefetch-scope`.
17
12
  */
18
13
 
19
14
  import type { PrefetchStrategy } from "../../router/prefetch-default.js";
20
15
 
21
- let hoverNoneQuery: MediaQueryList | null = null;
22
-
23
- // Mirrors DEFAULT_PREFETCH_STRATEGY without pulling router-layer code into the
24
- // client bundle. NODE_ENV is folded by the app build.
25
- let defaultStrategy: PrefetchStrategy =
16
+ /**
17
+ * The default for a payload without `defaultPrefetch`: the server resolver's
18
+ * environment default (`"none"` in development, `"viewport"` in production),
19
+ * mirrored without pulling router-layer code into the client bundle. NODE_ENV
20
+ * is folded by the app build.
21
+ */
22
+ export const ENVIRONMENT_DEFAULT_PREFETCH: PrefetchStrategy =
26
23
  process.env.NODE_ENV === "production" ? "viewport" : "none";
27
24
 
28
- function getHoverNoneQuery(): MediaQueryList | null {
29
- if (typeof window === "undefined") return null;
30
- if (!hoverNoneQuery) {
31
- hoverNoneQuery = window.matchMedia("(hover: none)");
32
- }
33
- return hoverNoneQuery;
34
- }
35
-
36
- /** Reset module state between tests that replace browser media globals. */
37
- export function resetAdaptiveStrategyForTesting(): void {
38
- hoverNoneQuery = null;
25
+ function hoverNoneQuery(): MediaQueryList | null {
26
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function")
27
+ return null;
28
+ return window.matchMedia("(hover: none)");
39
29
  }
40
30
 
41
31
  /**
42
- * Apply the server-resolved default strategy. Called once at browser app init
43
- * from payload metadata; also used by tests to reset state.
32
+ * Resolve adaptive to the strategy for the current input capability. Called
33
+ * when a prefetch is armed or triggered, never during render.
44
34
  */
45
- export function setDefaultPrefetchStrategy(strategy: PrefetchStrategy): void {
46
- defaultStrategy = strategy;
47
- }
48
-
49
- /** Current default strategy for Links and delegated plain anchors. */
50
- export function getDefaultPrefetchStrategy(): PrefetchStrategy {
51
- return defaultStrategy;
52
- }
53
-
54
- /** Resolve adaptive to the strategy for the current input capability. */
55
35
  export function resolveAdaptiveStrategy(
56
36
  strategy: PrefetchStrategy,
57
37
  ): PrefetchStrategy {
58
38
  if (strategy !== "adaptive") return strategy;
59
- return getHoverNoneQuery()?.matches ? "viewport" : "hover";
39
+ return hoverNoneQuery()?.matches ? "viewport" : "hover";
60
40
  }
61
41
 
62
42
  /** Subscribe to changes in the input capability used by adaptive prefetch. */
63
43
  export function subscribeToAdaptiveStrategyChange(
64
44
  listener: () => void,
65
45
  ): () => void {
66
- const query = getHoverNoneQuery();
46
+ const query = hoverNoneQuery();
67
47
  if (!query) return () => {};
68
48
  if (typeof query.addEventListener === "function") {
69
49
  query.addEventListener("change", listener);
@@ -7,7 +7,9 @@
7
7
  * cache keys responses by (URL, X-Rango-State value).
8
8
  *
9
9
  * Value format: `{buildVersion}:{invalidationTimestamp}`
10
- * - Build version changes on deploy, busting all cached prefetches at boot.
10
+ * - Build version: the router's document version. It changes when a deploy
11
+ * changes the router's server code or the client assets, busting all cached
12
+ * prefetches at boot; a deploy that changes neither keeps them.
11
13
  * - Timestamp rotates on invalidation (server action, invalidateClientCache).
12
14
  *
13
15
  * Storage is a session cookie named by the server-resolved name passed to
@@ -7,7 +7,6 @@ import React, {
7
7
  useEffect,
8
8
  useMemo,
9
9
  useRef,
10
- useSyncExternalStore,
11
10
  type ForwardRefExoticComponent,
12
11
  type RefAttributes,
13
12
  } from "react";
@@ -38,7 +37,7 @@ import {
38
37
  } from "../prefetch/loader.js";
39
38
  import { observeForPrefetch } from "../prefetch/observer.js";
40
39
  import {
41
- getDefaultPrefetchStrategy,
40
+ ENVIRONMENT_DEFAULT_PREFETCH,
42
41
  resolveAdaptiveStrategy,
43
42
  subscribeToAdaptiveStrategyChange,
44
43
  } from "../prefetch/default-strategy.js";
@@ -51,8 +50,6 @@ import type { PrefetchStrategy } from "../../router/prefetch-default.js";
51
50
  export type { PrefetchStrategy } from "../../router/prefetch-default.js";
52
51
  export { resolveAdaptiveStrategy } from "../prefetch/default-strategy.js";
53
52
 
54
- const IGNORE_STRATEGY_CHANGES = (_listener: () => void) => () => {};
55
-
56
53
  /**
57
54
  * Link component props
58
55
  */
@@ -241,19 +238,11 @@ export const Link: ForwardRefExoticComponent<
241
238
  ref,
242
239
  ) {
243
240
  const ctx = useContext(NavigationStoreContext);
244
- // Origin from the store location — the same both-sides source
245
- // useSearchParams seeds from (SSR: the live request's URL; browser:
246
- // window.location), so data-external agrees across hydration. Origin is
247
- // immutable per document, so the inline getState() read cannot tear.
248
- // window is the provider-less browser fallback (tests, portals outside
249
- // the app root); provider-less SSR has no origin and keeps links internal.
250
- const storeLocation = ctx?.eventController.getState().location as
251
- | URL
252
- | undefined;
253
- const origin =
254
- storeLocation?.origin ??
255
- (typeof window !== "undefined" ? window.location.origin : undefined);
256
- const isExternal = isExternalUrl(to, origin);
241
+ // The document's origin from the router store, the same on both sides
242
+ // (SSR: the live request's URL; browser: the store location), so
243
+ // data-external agrees across hydration. Without a provider there is no
244
+ // store and no origin: links stay internal.
245
+ const isExternal = isExternalUrl(to, ctx?.origin);
257
246
 
258
247
  // Auto-prefix with basename for app-local paths.
259
248
  // Skip if external, already prefixed, or not a root-relative path.
@@ -265,19 +254,11 @@ export const Link: ForwardRefExoticComponent<
265
254
  return to === "/" ? bn : bn + to;
266
255
  }, [to, isExternal, ctx?.basename]);
267
256
 
268
- // No explicit `prefetch` prop: fall back to the router-wide default
269
- // (server-resolved, applied at browser init — before hydration, so this
270
- // render-time read never races the metadata). Adaptive reads the current
271
- // input capability rather than a module-load snapshot.
257
+ // No explicit `prefetch` prop: fall back to the router-wide default from
258
+ // the router store. Adaptive is resolved against the input capability when a
259
+ // prefetch is armed or triggered, never during render.
272
260
  const configuredStrategy =
273
- prefetch ?? ctx?.defaultPrefetch ?? getDefaultPrefetchStrategy();
274
- const resolvedStrategy = useSyncExternalStore(
275
- configuredStrategy === "adaptive"
276
- ? subscribeToAdaptiveStrategyChange
277
- : IGNORE_STRATEGY_CHANGES,
278
- () => resolveAdaptiveStrategy(configuredStrategy),
279
- () => (configuredStrategy === "adaptive" ? "hover" : configuredStrategy),
280
- );
261
+ prefetch ?? ctx?.defaultPrefetch ?? ENVIRONMENT_DEFAULT_PREFETCH;
281
262
 
282
263
  // Internal ref for viewport observation; merge with forwarded ref
283
264
  const internalRef = useRef<HTMLAnchorElement | null>(null);
@@ -376,8 +357,9 @@ export const Link: ForwardRefExoticComponent<
376
357
 
377
358
  const handleMouseEnter = useCallback(() => {
378
359
  const element = internalRef.current;
360
+ const strategy = resolveAdaptiveStrategy(configuredStrategy);
379
361
  if (
380
- (resolvedStrategy === "hover" || resolvedStrategy === "viewport") &&
362
+ (strategy === "hover" || strategy === "viewport") &&
381
363
  !isExternal &&
382
364
  ctx?.store &&
383
365
  (!element ||
@@ -396,17 +378,26 @@ export const Link: ForwardRefExoticComponent<
396
378
  prefetchKey,
397
379
  );
398
380
  }
399
- }, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
381
+ }, [configuredStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
400
382
 
401
383
  // Viewport/render prefetch: waits for idle before starting,
402
384
  // uses concurrency-limited queue to avoid flooding.
403
385
  useEffect(() => {
404
386
  if (isExternal || !ctx?.store) return;
405
- const isViewport = resolvedStrategy === "viewport";
406
- const isRender = resolvedStrategy === "render";
407
- if (!isViewport && !isRender) return;
387
+ if (
388
+ configuredStrategy !== "viewport" &&
389
+ configuredStrategy !== "render" &&
390
+ configuredStrategy !== "adaptive"
391
+ ) {
392
+ return;
393
+ }
408
394
 
409
395
  const armPrefetch = (): (() => void) => {
396
+ const strategy = resolveAdaptiveStrategy(configuredStrategy);
397
+ const isViewport = strategy === "viewport";
398
+ const isRender = strategy === "render";
399
+ if (!isViewport && !isRender) return () => {};
400
+
410
401
  const element = internalRef.current;
411
402
  if (
412
403
  element &&
@@ -458,27 +449,30 @@ export const Link: ForwardRefExoticComponent<
458
449
  };
459
450
 
460
451
  let disarmPrefetch = armPrefetch();
452
+ const rearm = () => {
453
+ disarmPrefetch();
454
+ disarmPrefetch = armPrefetch();
455
+ };
461
456
  const element = internalRef.current;
462
457
  const unsubscribeScope = element
463
- ? subscribeToPrefetchScopeChange(element, () => {
464
- disarmPrefetch();
465
- disarmPrefetch = armPrefetch();
466
- })
458
+ ? subscribeToPrefetchScopeChange(element, rearm)
467
459
  : undefined;
468
460
  const unsubscribeLocation = subscribeToLocationChange(
469
461
  ctx.eventController,
470
- () => {
471
- disarmPrefetch();
472
- disarmPrefetch = armPrefetch();
473
- },
462
+ rearm,
474
463
  );
464
+ const unsubscribeAdaptive =
465
+ configuredStrategy === "adaptive"
466
+ ? subscribeToAdaptiveStrategyChange(rearm)
467
+ : undefined;
475
468
 
476
469
  return () => {
477
470
  unsubscribeScope?.();
478
471
  unsubscribeLocation();
472
+ unsubscribeAdaptive?.();
479
473
  disarmPrefetch();
480
474
  };
481
- }, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
475
+ }, [configuredStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
482
476
 
483
477
  return (
484
478
  <a