@rangojs/router 0.24.4 → 0.26.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 (301) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +0 -2
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
  22. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  23. package/dist/types/cache/handle-snapshot.d.ts +2 -2
  24. package/dist/types/cache/types.d.ts +16 -6
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
  26. package/dist/types/client-urls/client-urls.d.ts +6 -2
  27. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  28. package/dist/types/client-urls/server-projection.d.ts +9 -5
  29. package/dist/types/client-urls/types.d.ts +29 -16
  30. package/dist/types/client.d.ts +2 -2
  31. package/dist/types/client.rsc.d.ts +2 -2
  32. package/dist/types/decode-loader-results.d.ts +28 -7
  33. package/dist/types/handles/deferred-resolution.d.ts +3 -2
  34. package/dist/types/handles/meta.d.ts +5 -0
  35. package/dist/types/href-client.d.ts +3 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/loader-redirect.d.ts +2 -3
  38. package/dist/types/outlet-context.d.ts +7 -6
  39. package/dist/types/prefetch-markers.d.ts +14 -5
  40. package/dist/types/prerender/build-shell-capture.d.ts +1 -2
  41. package/dist/types/prerender/writable-store.d.ts +2 -3
  42. package/dist/types/route-content-wrapper.d.ts +17 -14
  43. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  44. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  45. package/dist/types/route-definition/rewrite.d.ts +48 -5
  46. package/dist/types/route-types.d.ts +2 -2
  47. package/dist/types/router/handler-context.d.ts +14 -2
  48. package/dist/types/router/intercept-resolution.d.ts +19 -46
  49. package/dist/types/router/lazy-includes.d.ts +7 -0
  50. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  51. package/dist/types/router/middleware.d.ts +0 -16
  52. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  53. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  54. package/dist/types/router/revalidation.d.ts +8 -1
  55. package/dist/types/router/router-context.d.ts +4 -16
  56. package/dist/types/router/router-interfaces.d.ts +0 -11
  57. package/dist/types/router/router-options.d.ts +4 -27
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
  59. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  60. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  61. package/dist/types/router/segment-resolution.d.ts +1 -1
  62. package/dist/types/router/segment-wrappers.d.ts +3 -5
  63. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  64. package/dist/types/router/tracing.d.ts +2 -2
  65. package/dist/types/router-versions.d.ts +8 -6
  66. package/dist/types/rsc/full-payload.d.ts +14 -2
  67. package/dist/types/rsc/helpers.d.ts +8 -0
  68. package/dist/types/rsc/manifest-init.d.ts +20 -0
  69. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  70. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  71. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  72. package/dist/types/rsc/server-action.d.ts +6 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +14 -4
  74. package/dist/types/rsc/shell-capture.d.ts +23 -2
  75. package/dist/types/rsc/types.d.ts +5 -8
  76. package/dist/types/segment-system.d.ts +15 -0
  77. package/dist/types/server/build-version-table.d.ts +10 -13
  78. package/dist/types/server/context.d.ts +90 -6
  79. package/dist/types/server/handle-store.d.ts +17 -6
  80. package/dist/types/server/request-context.d.ts +45 -7
  81. package/dist/types/server/request-headers.d.ts +12 -0
  82. package/dist/types/server/rewrite-trail.d.ts +8 -1
  83. package/dist/types/testing/build-versions.d.ts +0 -3
  84. package/dist/types/testing/e2e/index.d.ts +1 -1
  85. package/dist/types/testing/flight.entry.d.ts +1 -1
  86. package/dist/types/testing/index.d.ts +1 -1
  87. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  88. package/dist/types/testing/internal/seed-vars.d.ts +4 -4
  89. package/dist/types/testing/render-route.d.ts +21 -11
  90. package/dist/types/testing/run-loader.d.ts +7 -0
  91. package/dist/types/testing/run-middleware.d.ts +7 -0
  92. package/dist/types/testing/run-revalidate.d.ts +5 -4
  93. package/dist/types/testing/serve-shell-request.d.ts +42 -5
  94. package/dist/types/testing/shell-status.d.ts +17 -0
  95. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  96. package/dist/types/testing/vitest.d.ts +10 -1
  97. package/dist/types/thenable-status.d.ts +0 -6
  98. package/dist/types/types/handler-context.d.ts +12 -1
  99. package/dist/types/types/loader-types.d.ts +10 -4
  100. package/dist/types/types/route-entry.d.ts +2 -1
  101. package/dist/types/urls/path-helper-types.d.ts +4 -2
  102. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  103. package/dist/types/vite/discovery/state.d.ts +20 -0
  104. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  105. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  106. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  107. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  108. package/dist/vite/index.js +6819 -6555
  109. package/package.json +1 -1
  110. package/skills/app-structure/SKILL.md +255 -0
  111. package/skills/app-structure/content-site.md +333 -0
  112. package/skills/app-structure/e-commerce.md +435 -0
  113. package/skills/app-structure/large-app.md +885 -0
  114. package/skills/app-structure/multi-app.md +280 -0
  115. package/skills/breadcrumbs/SKILL.md +6 -1
  116. package/skills/cache-guide/SKILL.md +4 -8
  117. package/skills/caching/SKILL.md +6 -6
  118. package/skills/catalog.json +12 -0
  119. package/skills/client-urls/SKILL.md +185 -32
  120. package/skills/comparison/references/framework-comparison.md +1 -1
  121. package/skills/composability/SKILL.md +10 -8
  122. package/skills/handler-use/SKILL.md +48 -9
  123. package/skills/hooks/handle-and-actions.md +104 -0
  124. package/skills/hooks/navigation.md +24 -10
  125. package/skills/hooks/urls.md +1 -1
  126. package/skills/host-router/SKILL.md +1 -1
  127. package/skills/i18n/SKILL.md +2 -1
  128. package/skills/intercept/SKILL.md +60 -29
  129. package/skills/layout/SKILL.md +6 -3
  130. package/skills/links/SKILL.md +151 -17
  131. package/skills/loader/SKILL.md +42 -19
  132. package/skills/middleware/SKILL.md +111 -15
  133. package/skills/observability/SKILL.md +11 -0
  134. package/skills/parallel/SKILL.md +5 -5
  135. package/skills/ppr/SKILL.md +103 -52
  136. package/skills/prerender/SKILL.md +4 -5
  137. package/skills/rango/SKILL.md +3 -0
  138. package/skills/response-routes/SKILL.md +4 -1
  139. package/skills/router-setup/SKILL.md +0 -2
  140. package/skills/scripts/SKILL.md +12 -1
  141. package/skills/server-actions/SKILL.md +60 -20
  142. package/skills/testing/SKILL.md +1 -0
  143. package/skills/testing/client-components.md +13 -10
  144. package/skills/testing/loader.md +25 -24
  145. package/skills/testing/middleware.md +11 -11
  146. package/skills/testing/render-handler.md +17 -17
  147. package/skills/testing/response-routes.md +4 -1
  148. package/skills/testing/serve-shell-request.md +86 -22
  149. package/skills/testing/server-actions.md +16 -16
  150. package/skills/ux-architecture/SKILL.md +980 -0
  151. package/skills/vercel/SKILL.md +1 -1
  152. package/skills/view-transitions/SKILL.md +43 -10
  153. package/src/__augment-tests__/augmented.check.ts +28 -0
  154. package/src/browser/event-controller.ts +44 -18
  155. package/src/browser/intercept-utils.ts +3 -2
  156. package/src/browser/merge-segment-loaders.ts +0 -85
  157. package/src/browser/navigation-bridge.ts +112 -84
  158. package/src/browser/navigation-client.ts +20 -15
  159. package/src/browser/navigation-store.ts +4 -2
  160. package/src/browser/navigation-transaction.ts +9 -8
  161. package/src/browser/partial-update.ts +90 -32
  162. package/src/browser/prefetch/cache.ts +8 -0
  163. package/src/browser/prefetch/fetch.ts +13 -3
  164. package/src/browser/rango-marker.ts +13 -0
  165. package/src/browser/react/NavigationProvider.tsx +25 -7
  166. package/src/browser/react/filter-segment-order.ts +13 -0
  167. package/src/browser/react/use-action.ts +9 -15
  168. package/src/browser/react/use-handle.ts +63 -71
  169. package/src/browser/react/use-href.tsx +24 -5
  170. package/src/browser/react/use-link-status.ts +1 -1
  171. package/src/browser/react/use-navigation.ts +30 -40
  172. package/src/browser/react/use-router.ts +37 -6
  173. package/src/browser/react/use-segments.ts +8 -12
  174. package/src/browser/response-adapter.ts +11 -5
  175. package/src/browser/rsc-router.tsx +3 -1
  176. package/src/browser/scroll-restoration.ts +34 -20
  177. package/src/browser/segment-reconciler.ts +11 -29
  178. package/src/browser/server-action-bridge.ts +52 -16
  179. package/src/browser/types.ts +40 -8
  180. package/src/build/generate-manifest.ts +12 -1
  181. package/src/build/route-types/codegen.ts +6 -2
  182. package/src/build/route-types/per-module-writer.ts +94 -61
  183. package/src/cache/cache-runtime.ts +9 -0
  184. package/src/cache/cache-scope.ts +2 -1
  185. package/src/cache/cf/cf-cache-store.ts +6 -7
  186. package/src/cache/cf/cf-cache-types.ts +0 -17
  187. package/src/cache/cf/cf-shell-frame.ts +3 -0
  188. package/src/cache/handle-snapshot.ts +2 -2
  189. package/src/cache/types.ts +16 -6
  190. package/src/cache/vercel/vercel-cache-store.ts +8 -19
  191. package/src/client-urls/client-urls.ts +202 -153
  192. package/src/client-urls/revalidate-registry.ts +12 -3
  193. package/src/client-urls/server-projection.ts +137 -66
  194. package/src/client-urls/types.ts +30 -15
  195. package/src/client.rsc.tsx +11 -1
  196. package/src/client.tsx +9 -1
  197. package/src/decode-loader-results.ts +64 -31
  198. package/src/handles/deferred-resolution.ts +3 -2
  199. package/src/handles/meta.ts +5 -0
  200. package/src/href-client.ts +3 -1
  201. package/src/index.rsc.ts +1 -0
  202. package/src/index.ts +12 -4
  203. package/src/loader-redirect.tsx +2 -3
  204. package/src/outlet-context.ts +7 -6
  205. package/src/prefetch-markers.ts +15 -5
  206. package/src/prerender/build-shell-capture.ts +1 -2
  207. package/src/prerender/writable-store.ts +2 -3
  208. package/src/reverse.ts +3 -3
  209. package/src/route-content-wrapper.tsx +19 -17
  210. package/src/route-definition/dsl-helpers.ts +41 -21
  211. package/src/route-definition/helpers-types.ts +7 -2
  212. package/src/route-definition/resolve-handler-use.ts +23 -10
  213. package/src/route-definition/rewrite.ts +86 -19
  214. package/src/route-types.ts +1 -2
  215. package/src/router/find-match.ts +37 -1
  216. package/src/router/handler-context.ts +85 -40
  217. package/src/router/intercept-resolution.ts +161 -307
  218. package/src/router/lazy-includes.ts +43 -27
  219. package/src/router/loader-resolution.ts +21 -1
  220. package/src/router/manifest.ts +15 -10
  221. package/src/router/match-api.ts +11 -1
  222. package/src/router/match-middleware/background-revalidation.ts +2 -4
  223. package/src/router/match-middleware/cache-lookup.ts +16 -20
  224. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  225. package/src/router/match-result.ts +31 -2
  226. package/src/router/middleware.ts +6 -147
  227. package/src/router/navigation-snapshot.ts +38 -2
  228. package/src/router/prefetch-cache-ttl.ts +10 -18
  229. package/src/router/prerender-match.ts +2 -1
  230. package/src/router/revalidation.ts +24 -3
  231. package/src/router/router-context.ts +5 -18
  232. package/src/router/router-interfaces.ts +0 -13
  233. package/src/router/router-options.ts +4 -28
  234. package/src/router/segment-resolution/fresh.ts +6 -2
  235. package/src/router/segment-resolution/helpers.ts +7 -1
  236. package/src/router/segment-resolution/loader-cache.ts +4 -3
  237. package/src/router/segment-resolution/mask-nested.ts +5 -3
  238. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  239. package/src/router/segment-resolution/revalidation.ts +75 -35
  240. package/src/router/segment-resolution.ts +0 -1
  241. package/src/router/segment-wrappers.ts +8 -13
  242. package/src/router/substitute-pattern-params.ts +3 -2
  243. package/src/router/tracing.ts +2 -2
  244. package/src/router-versions.ts +7 -7
  245. package/src/router.ts +5 -17
  246. package/src/rsc/full-payload.ts +26 -4
  247. package/src/rsc/handler.ts +86 -48
  248. package/src/rsc/helpers.ts +13 -0
  249. package/src/rsc/manifest-init.ts +56 -3
  250. package/src/rsc/prefetch-render-error.ts +38 -0
  251. package/src/rsc/progressive-enhancement.ts +161 -211
  252. package/src/rsc/rewrite-passes.ts +56 -9
  253. package/src/rsc/rsc-rendering.ts +79 -23
  254. package/src/rsc/server-action.ts +25 -2
  255. package/src/rsc/shell-capture-constants.ts +26 -3
  256. package/src/rsc/shell-capture.ts +137 -51
  257. package/src/rsc/types.ts +5 -9
  258. package/src/segment-system.tsx +125 -119
  259. package/src/server/build-version-table.ts +12 -24
  260. package/src/server/context.ts +160 -7
  261. package/src/server/handle-store.ts +80 -12
  262. package/src/server/request-context.ts +46 -6
  263. package/src/server/request-headers.ts +32 -0
  264. package/src/server/rewrite-trail.ts +55 -9
  265. package/src/ssr/ssr-root.tsx +6 -10
  266. package/src/testing/build-versions.ts +0 -3
  267. package/src/testing/dispatch.ts +5 -8
  268. package/src/testing/e2e/index.ts +1 -0
  269. package/src/testing/flight.entry.ts +1 -0
  270. package/src/testing/index.ts +1 -0
  271. package/src/testing/internal/client-urls-projection.ts +31 -11
  272. package/src/testing/internal/seed-vars.ts +4 -4
  273. package/src/testing/render-route.tsx +83 -40
  274. package/src/testing/run-loader.ts +26 -2
  275. package/src/testing/run-middleware.ts +10 -0
  276. package/src/testing/run-revalidate.ts +13 -6
  277. package/src/testing/serve-shell-request.ts +79 -9
  278. package/src/testing/shell-status.ts +40 -0
  279. package/src/testing/vitest-stubs/version.ts +1 -0
  280. package/src/testing/vitest.ts +74 -7
  281. package/src/thenable-status.ts +0 -11
  282. package/src/types/handler-context.ts +12 -1
  283. package/src/types/loader-types.ts +10 -4
  284. package/src/types/route-entry.ts +2 -1
  285. package/src/urls/include-helper.ts +38 -11
  286. package/src/urls/path-helper-types.ts +4 -2
  287. package/src/vite/discovery/client-urls-projection.ts +205 -16
  288. package/src/vite/discovery/discover-routers.ts +2 -2
  289. package/src/vite/discovery/prerender-collection.ts +2 -6
  290. package/src/vite/discovery/shell-prerender-phase.ts +1 -5
  291. package/src/vite/discovery/state.ts +26 -0
  292. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  293. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  294. package/src/vite/plugins/hoist/source.ts +8 -7
  295. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  296. package/src/vite/plugins/version-plugin.ts +7 -0
  297. package/src/vite/plugins/version.d.ts +7 -0
  298. package/src/vite/plugins/virtual-entries.ts +20 -0
  299. package/src/vite/rango.ts +4 -1
  300. package/src/vite/router-discovery.ts +39 -7
  301. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -16,9 +16,10 @@ import type { NavigationStoreContextValue } from "../browser/react/context.js";
16
16
  import type { HandleData } from "../browser/types.js";
17
17
  import type { ResolvedSegment } from "../types.js";
18
18
  import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
19
- import type {
20
- EventController,
21
- DerivedNavigationState,
19
+ import {
20
+ DEFAULT_ACTION_STATE,
21
+ type EventController,
22
+ type DerivedNavigationState,
22
23
  } from "../browser/event-controller.js";
23
24
 
24
25
  /**
@@ -111,16 +112,11 @@ function createSsrEventController(opts: {
111
112
  getState: () => state,
112
113
  getLocation: () => location,
113
114
  subscribe: () => () => {},
114
- getActionState: () => ({
115
- state: "idle",
116
- actionId: null,
117
- payload: null,
118
- error: null,
119
- result: null,
120
- }),
115
+ getActionState: () => DEFAULT_ACTION_STATE,
121
116
  subscribeToAction: () => () => {},
122
117
  subscribeToHandles: () => () => {},
123
118
  flushRouteState: () => {},
119
+ releaseRouteState: () => {},
124
120
  setHandleData: () => {},
125
121
  getHandleState: () => handleState,
126
122
  lockHydration: () => {},
@@ -35,9 +35,6 @@ export interface BuildVersions extends RouterVersions {
35
35
  * Install the cache versions of a simulated build, or remove them with no
36
36
  * argument (routers serve unversioned again, the default in tests).
37
37
  *
38
- * A router that sets `createRouter({ version })` keeps that value for both
39
- * versions, exactly as in production.
40
- *
41
38
  * @example A deploy that changes the client only keeps cached data
42
39
  * ```ts
43
40
  * setBuildVersions({ data: "d1", document: "h1" });
@@ -129,7 +129,7 @@ import type { CacheProfile } from "../cache/profile-registry.js";
129
129
  // onto the whole testing barrel's eager graph and break every consumer suite
130
130
  // that imports `@rangojs/router/testing` without mocking plugin-rsc.
131
131
  import type { EntryData } from "../server/context.js";
132
- import { setRouterManifest } from "../route-map-builder.js";
132
+ import { ensureRouterTrie } from "../rsc/manifest-init.js";
133
133
  import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
134
134
  import { RouterError } from "../errors.js";
135
135
  import { createProblemDetails } from "../rsc/response-error.js";
@@ -167,9 +167,9 @@ import { applyStreamIdleTimeout } from "../rsc/stream-idle.js";
167
167
  interface DispatchableRouter<TEnv> {
168
168
  id?: string;
169
169
  routerId?: string;
170
- /** createRouter({ version }), the consumer-set version. */
171
- version?: string;
172
170
  routeMap: Record<string, unknown>;
171
+ /** The routes() patterns the dev trie is rebuilt from (rsc/manifest-init.ts). */
172
+ urlpatterns?: unknown;
173
173
  middleware: MiddlewareEntry<TEnv>[];
174
174
  onError?: OnErrorCallback<TEnv>;
175
175
  /**
@@ -409,11 +409,8 @@ export async function dispatch<TEnv = any>(
409
409
  const url = new URL(req.url);
410
410
  const env = (opts.env ?? {}) as TEnv;
411
411
 
412
- // Seed the per-router manifest so reverse() resolves during handler execution.
413
412
  const routerId = router.id ?? router.routerId;
414
- if (routerId) {
415
- setRouterManifest(routerId, router.routeMap as Record<string, string>);
416
- }
413
+ await ensureRouterTrie(router);
417
414
 
418
415
  // The visitor's URL is matched before any middleware runs, so an RSC route
419
416
  // fails here, before a middleware side effect. A rewrite target is matched
@@ -449,7 +446,7 @@ export async function dispatch<TEnv = any>(
449
446
  cacheProfiles: router.cacheProfiles,
450
447
  // The store keys with the router's versions, resolved as the production
451
448
  // handler resolves them (rsc/handler.ts).
452
- versions: resolveRouterVersions(routerId, router.version),
449
+ versions: resolveRouterVersions(routerId),
453
450
  });
454
451
  // Wire background error reporting so cache degradation (reportCacheError ->
455
452
  // _reportBackgroundError) reaches the router's onError, mirroring the production
@@ -51,6 +51,7 @@ export {
51
51
  PPR_REPLAY_STATUS_HEADER,
52
52
  shellCacheKey,
53
53
  SHELL_STATUS_HEADER,
54
+ visitorShellCacheKey,
54
55
  type PprReplayBypassReason,
55
56
  type PprReplayStatus,
56
57
  type ShellCachePartition,
@@ -67,5 +67,6 @@ export type {
67
67
  ServeShellRequestAction,
68
68
  ServeShellRequestOptions,
69
69
  ServeShellRequestResult,
70
+ ShellRequestAction,
70
71
  ShellRequestHandles,
71
72
  } from "./serve-shell-request.js";
@@ -112,6 +112,7 @@ export {
112
112
  PPR_REPLAY_STATUS_HEADER,
113
113
  shellCacheKey,
114
114
  SHELL_STATUS_HEADER,
115
+ visitorShellCacheKey,
115
116
  } from "./shell-status.js";
116
117
  export type {
117
118
  PprReplayBypassReason,
@@ -19,9 +19,10 @@ import type { ShouldRevalidateFn } from "../../types.js";
19
19
  *
20
20
  * A Vitest project runs no hoist plugin, so the source's revalidate()
21
21
  * predicates are plain functions. Each is registered under a key named for
22
- * its reference, route and loader, as the plugin's generated module registers
23
- * its own, and the projection carries the keys: materialization attaches them
24
- * and the server runs them, as in an app.
22
+ * its reference, route or intercept, and loader, as the plugin's generated
23
+ * module registers its own, and the projection carries the keys:
24
+ * materialization attaches them and the server runs them, as in an app. A
25
+ * boolean rides in the projection as itself, as the plugin leaves a literal.
25
26
  */
26
27
  export function installClientUrlsProjection(
27
28
  referenceId: string,
@@ -50,21 +51,40 @@ function withRevalidateKeys(
50
51
  projection: ClientUrlProjection,
51
52
  ): ClientUrlProjection {
52
53
  const fns: Record<string, ShouldRevalidateFn<any, any>> = {};
53
- const routes = projection.routes.map((route) => {
54
- const record = definition.routes.find(({ id }) => id === route.id);
55
- const loaderRevalidate = (record?.loaders ?? []).map(
54
+ // A route's or an intercept's loaders, keyed under `owner`.
55
+ const withKeys = <T extends object>(
56
+ projected: T,
57
+ owner: string,
58
+ loaders: ClientUrlPatterns["routes"][number]["loaders"] | undefined,
59
+ ): T => {
60
+ const loaderRevalidate = (loaders ?? []).map(
56
61
  ({ revalidate }, loaderIndex) =>
57
62
  revalidate.map((entry, index) => {
58
63
  if (isHoistedRevalidateRef(entry)) return entry.$$rangoRevalidate;
59
- const key = `${referenceId}:${route.id}:${loaderIndex}:${index}`;
64
+ if (typeof entry === "boolean") return entry;
65
+ const key = `${referenceId}:${owner}:${loaderIndex}:${index}`;
60
66
  fns[key] = entry;
61
67
  return key;
62
68
  }),
63
69
  );
64
70
  return loaderRevalidate.some((keys) => keys.length > 0)
65
- ? { ...route, loaderRevalidate }
66
- : route;
67
- });
71
+ ? { ...projected, loaderRevalidate }
72
+ : projected;
73
+ };
74
+ const routes = projection.routes.map((route) =>
75
+ withKeys(
76
+ route,
77
+ route.id,
78
+ definition.routes.find(({ id }) => id === route.id)?.loaders,
79
+ ),
80
+ );
81
+ const intercepts = projection.intercepts?.map((intercept, index) =>
82
+ withKeys(
83
+ intercept,
84
+ `intercept${index}`,
85
+ definition.intercepts[index]?.loaders,
86
+ ),
87
+ );
68
88
  registerClientUrlRevalidators(fns);
69
- return { ...projection, routes };
89
+ return { ...projection, routes, ...(intercepts && { intercepts }) };
70
90
  }
@@ -23,10 +23,10 @@ export interface StateCookieSeed {
23
23
  */
24
24
  routerId?: string;
25
25
  /**
26
- * The app's version, as `createRouter({ version })` would set it: the
27
- * rotated value's prefix (`{version}:{timestamp}`) and the version a cache
28
- * store keys with. Defaults to `"0"` for the prefix (resolved inside
29
- * createRequestContext) and to the build versions for the store.
26
+ * The app's version: the rotated value's prefix (`{version}:{timestamp}`)
27
+ * and the request's cache versions, which a store keys with. Defaults to
28
+ * `"0"` for the prefix (resolved inside createRequestContext) and to the
29
+ * build versions for the store.
30
30
  */
31
31
  version?: string;
32
32
  }
@@ -23,13 +23,16 @@
23
23
  * SEEDED directly into client context (see the `loaders` / `locationState` /
24
24
  * `handles` / `outletPending` options) — nothing is executed on the server.
25
25
  * This exercises the context read path, not the run path.
26
- * - navigate() does NOT drive the navigation lifecycle: useNavigation().state,
27
- * useLinkStatus().pending, and useAction().state stay "idle". Assert
28
- * pending/loading/submitting transition states with renderServerTree / e2e
29
- * instead (navigate() warns once if used). The one pending state it does
30
- * model is the held-navigation useLoader().isLoading: a `transition` spec
31
- * plus a pending Promise seeded via navigate(url, { loaders }) commits
32
- * through production's commitInTransition (browser/partial-update.ts).
26
+ * - navigate() drives the navigation lifecycle as a click does:
27
+ * useNavigation() reads loading from the call and idle with the commit,
28
+ * and useLinkStatus().pending is true for a link to the destination until
29
+ * the commit is handed to React. A `transition` spec plus a pending
30
+ * Promise seeded via navigate(url, { loaders }) commits through
31
+ * production's commitInTransition (browser/partial-update.ts), which React
32
+ * holds: useNavigation() stays loading and the held reader's
33
+ * useLoader().isLoading reads true until the data lands. Without
34
+ * `transition` the commit is urgent (production's same-structure hold is
35
+ * not modeled). useAction().state stays idle: no action runs.
33
36
  * - A spec's transition({ when }) decides every commit with production's
34
37
  * browser decision: navigate() and useRouter().push model kind "push",
35
38
  * useRouter().replace kind "replace", refresh() kind "revalidate".
@@ -81,7 +84,10 @@ import type {
81
84
  NavigationBridge,
82
85
  UpdateSubscriber,
83
86
  } from "../browser/types.js";
84
- import type { EventController } from "../browser/event-controller.js";
87
+ import type {
88
+ EventController,
89
+ NavigationHandle,
90
+ } from "../browser/event-controller.js";
85
91
  import type { ResolvedSegment, RscMetadata } from "../browser/types.js";
86
92
  import { NavigationProvider } from "../browser/react/NavigationProvider.js";
87
93
  import {
@@ -407,9 +413,10 @@ export interface TestRouterHandle {
407
413
  * pending Promise suspends the read like a streaming Flight chunk. When a
408
414
  * spec in the chain has `transition`, the commit goes through production's
409
415
  * commitInTransition: the reader stays on screen with
410
- * `useLoader().isLoading === true` until the promise settles. Settle it
411
- * inside RTL's `act()` to flush the commit. Without `transition` the commit
412
- * is urgent and the read suspends to the nearest Suspense boundary.
416
+ * `useLoader().isLoading === true`, and `useNavigation().state` reads
417
+ * "loading", until the promise settles. Settle it inside RTL's `act()` to
418
+ * flush the commit. Without `transition` the commit is urgent and the read
419
+ * suspends to the nearest Suspense boundary.
413
420
  *
414
421
  * @example
415
422
  * await router.navigate("/products/2", { loaders: [[ProductLoader, next]] });
@@ -429,10 +436,16 @@ export interface TestRouterHandle {
429
436
  * location state as a production replace does). With neither, history is
430
437
  * left alone and readers keep the state they have: `navigate(url)` and
431
438
  * `navigate(url, { replace: false })` are the same call.
439
+ *
440
+ * `options.handles` are the destination's handle pushes (`[handle,
441
+ * values]`, attached to its leaf route as in `renderRoute`'s own
442
+ * `handles`). They arrive on the response's handle stream, so `useHandle`
443
+ * readers take them with the page's commit: while React holds the page,
444
+ * readers keep the values of the page being left.
432
445
  */
433
446
  navigate(
434
447
  url: string,
435
- options?: Pick<RenderRouteOptions, "loaders"> &
448
+ options?: Pick<RenderRouteOptions, "loaders" | "handles"> &
436
449
  Pick<NavigateOptions, "state" | "replace" | "transition">,
437
450
  ): Promise<void>;
438
451
  /**
@@ -791,17 +804,36 @@ export async function renderRoute(
791
804
  };
792
805
  }
793
806
 
794
- let warnedNavLifecycle = false;
807
+ type NavigateCall = Pick<RenderRouteOptions, "loaders" | "handles"> & {
808
+ history?: Pick<NavigateOptionsInternal, "state" | "replace" | "transition">;
809
+ kind?: TransitionWhenKind;
810
+ transition?: boolean;
811
+ };
812
+ // The navigation's lifecycle, as navigation-transaction.ts drives it:
813
+ // useNavigation() reads loading from the call, delivered before the commit
814
+ // as a click's notification goes out while its response is in flight, and
815
+ // idle with the location at the commit, which React may hold.
795
816
  const navigate = async (
796
817
  target: string,
797
- navOptions?: Pick<RenderRouteOptions, "loaders"> & {
798
- history?: Pick<
799
- NavigateOptionsInternal,
800
- "state" | "replace" | "transition"
801
- >;
802
- kind?: TransitionWhenKind;
803
- transition?: boolean;
804
- },
818
+ navOptions?: NavigateCall,
819
+ ): Promise<void> => {
820
+ const handle = eventController.startNavigation(
821
+ new URL(target, TEST_ORIGIN).href,
822
+ );
823
+ // Synchronous, in the caller's task: a <Link> click's own act() scope
824
+ // takes it, and no act() of the caller's can overlap it.
825
+ act(() => eventController.flushRouteState());
826
+ try {
827
+ await commitNavigation(handle, target, navOptions);
828
+ } finally {
829
+ // Back to idle when the commit never came (a render that threw).
830
+ handle[Symbol.dispose]();
831
+ }
832
+ };
833
+ const commitNavigation = async (
834
+ handle: NavigationHandle,
835
+ target: string,
836
+ navOptions?: NavigateCall,
805
837
  ): Promise<void> => {
806
838
  // A useRouter().push/replace or <Link> navigation (`history`) writes its
807
839
  // entry the way production does (navigation-bridge.ts navigate ->
@@ -819,21 +851,6 @@ export async function renderRoute(
819
851
  : undefined,
820
852
  )
821
853
  : undefined;
822
- // No server fetch, so the navigation lifecycle never starts: the state
823
- // useNavigation()/useLinkStatus()/useAction() read stays "idle" — asserting
824
- // a pending/loading/submitting state here proves nothing. Warn once (per
825
- // render) under the test runner so that false-confidence trap is loud.
826
- if (isUnderTestRunner() && !warnedNavLifecycle) {
827
- warnedNavLifecycle = true;
828
- console.warn(
829
- "renderRoute: navigate()/useRouter().push do NOT drive the navigation " +
830
- "lifecycle. useNavigation().state, useLinkStatus().pending, and " +
831
- 'useAction().state stay "idle" here. Assert params/pathname/content ' +
832
- "after navigate(); use renderServerTree or e2e to assert " +
833
- "pending/loading/submitting transition states. A held " +
834
- "useLoader().isLoading is modeled: see navigate(url, { loaders }).",
835
- );
836
- }
837
854
  const nextUrl = new URL(target, TEST_ORIGIN);
838
855
  const match = resolve(nextUrl.pathname);
839
856
  const segments = buildSegments(
@@ -843,6 +860,23 @@ export async function renderRoute(
843
860
  mount,
844
861
  );
845
862
  const metadata = makeMetadata(nextUrl.pathname, segments, match.params);
863
+ // The destination's handle pushes arrive on the response's handle
864
+ // stream, as production's do, behind the page's commit.
865
+ const navHandles = navOptions?.handles;
866
+ if (navHandles) {
867
+ const leafId =
868
+ [...segments].reverse().find((s) => s.type === "route")?.id ??
869
+ segments[segments.length - 1]?.id;
870
+ const pushed: HandleDataSeed = {};
871
+ for (const [h, values] of navHandles) {
872
+ if (leafId === undefined) continue;
873
+ const id = (h as unknown as { $$id: string }).$$id;
874
+ (pushed[id] ??= {})[leafId] = values;
875
+ }
876
+ metadata.handles = (async function* () {
877
+ yield pushed;
878
+ })();
879
+ }
846
880
  // Production's browser-run transition({ when }) decision
847
881
  // (browser/partial-update.ts): the committed location is the source.
848
882
  const kind: TransitionWhenKind =
@@ -872,7 +906,10 @@ export async function renderRoute(
872
906
  outletPending: options.outletPending,
873
907
  transitionGatedOff: gatedOff,
874
908
  });
875
- eventController.setLocation(nextUrl);
909
+ // A newer navigate() started meanwhile: this one commits nothing, as
910
+ // production's (browser/partial-update.ts) returns on its aborted signal.
911
+ if (handle.signal.aborted) return;
912
+ handle.complete(nextUrl);
876
913
  eventController.setParams(match.params);
877
914
  store.setCurrentUrl(nextUrl.href);
878
915
  store.setSegmentIds(segments.map((s) => s.id));
@@ -892,9 +929,14 @@ export async function renderRoute(
892
929
  // same-structure / fully-prefetched / optimistic hold lanes are not
893
930
  // modeled: without transition() the commit stays urgent.
894
931
  if (shouldStartViewTransition(segments, gatedOff)) {
895
- commitInTransition(store.loaders, emit, segments, { root, metadata }, [
896
- "navigation",
897
- ]);
932
+ commitInTransition(
933
+ store.loaders,
934
+ emit,
935
+ segments,
936
+ { root, metadata },
937
+ kind,
938
+ ["navigation"],
939
+ );
898
940
  } else {
899
941
  emit({ root, metadata });
900
942
  }
@@ -1024,6 +1066,7 @@ export async function renderRoute(
1024
1066
  navigate: (target, navOptions) =>
1025
1067
  navigate(target, {
1026
1068
  loaders: navOptions?.loaders,
1069
+ handles: navOptions?.handles,
1027
1070
  transition: navOptions?.transition,
1028
1071
  history:
1029
1072
  navOptions?.state !== undefined || navOptions?.replace
@@ -39,6 +39,7 @@ import {
39
39
  type RequestContext,
40
40
  } from "../server/request-context.js";
41
41
  import { createReverseFunction } from "../router/handler-context.js";
42
+ import { scopeReverse, type ReverseScope } from "../server/context.js";
42
43
  import { NOCACHE_SYMBOL } from "../cache/taint.js";
43
44
  import { defineRewrittenFrom } from "../server/rewrite-trail.js";
44
45
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
@@ -134,6 +135,13 @@ export interface RunLoaderOptions<TEnv = any> {
134
135
  routeMap?: Record<string, string>;
135
136
  /** Matched route name for scoped `.name` reverse resolution. */
136
137
  routeName?: string;
138
+ /**
139
+ * Include name prefix of the module whose `layout()` / `parallel()` /
140
+ * `cache()` declares the loader (`"account"`, or `""` for the root module).
141
+ * `ctx.reverse(".name")` then resolves in that module, as such a loader's
142
+ * does in production, instead of from `routeName`.
143
+ */
144
+ declaredIn?: string;
137
145
  /** HTTP method surfaced as `ctx.method`. Defaults to "GET". */
138
146
  method?: string;
139
147
  /** Request body surfaced as `ctx.body`. */
@@ -279,9 +287,21 @@ function runWithLoaderContext<R>(
279
287
  const loaderSeeds = new Map<unknown, unknown>(opts.loaders ?? []);
280
288
  let renderedResolved = false;
281
289
 
290
+ // A layout's / parallel's loader: `.name` resolves in its declaring module.
291
+ const reverseScope: ReverseScope | undefined =
292
+ opts.declaredIn === undefined
293
+ ? undefined
294
+ : { prefix: opts.declaredIn || undefined, rootScoped: !opts.declaredIn };
282
295
  return runWithRequestContext(reqCtx, () => {
283
296
  const reverse = opts.routeMap
284
- ? createReverseFunction(opts.routeMap, opts.routeName, opts.params ?? {})
297
+ ? scopeReverse(
298
+ createReverseFunction(
299
+ opts.routeMap,
300
+ opts.routeName,
301
+ opts.params ?? {},
302
+ ),
303
+ reverseScope,
304
+ )
285
305
  : ((() => {
286
306
  throw new Error(
287
307
  "ctx.reverse() requires the `routeMap` option in runLoader(). " +
@@ -289,7 +309,10 @@ function runWithLoaderContext<R>(
289
309
  );
290
310
  }) as TestLoaderContext["reverse"]);
291
311
 
292
- const loaderCtx: TestLoaderContext & { _routeName?: string } = {
312
+ const loaderCtx: TestLoaderContext & {
313
+ _routeName?: string;
314
+ _reverseScope?: ReverseScope;
315
+ } = {
293
316
  params: opts.params ?? {},
294
317
  routeParams: (opts.params ?? {}) as Record<string, string>,
295
318
  request: reqCtx.request,
@@ -346,6 +369,7 @@ function runWithLoaderContext<R>(
346
369
  formData: opts.formData,
347
370
  reverse: reverse as TestLoaderContext["reverse"],
348
371
  _routeName: reqCtx._routeName,
372
+ _reverseScope: reverseScope,
349
373
  rendered:
350
374
  opts.rendered !== undefined && opts.rendered !== false
351
375
  ? async () => {
@@ -106,6 +106,13 @@ export interface RunMiddlewareRewrite {
106
106
  pathname: string;
107
107
  search: string;
108
108
  routeName?: string;
109
+ /**
110
+ * Present, `true`, only when the rewrite was thrown with
111
+ * `.disallowMutations()`: production refuses it for a non-GET, non-HEAD
112
+ * request in global middleware, before the action runs, instead of moving
113
+ * the mutation to the target.
114
+ */
115
+ mutationsDisallowed?: true;
109
116
  }
110
117
 
111
118
  /**
@@ -240,6 +247,9 @@ export async function runMiddleware<TEnv = any>(
240
247
  pathname: target.pathname,
241
248
  search: target.search,
242
249
  ...("name" in error.target && { routeName: error.target.name }),
250
+ ...(error.mutationsDisallowed && {
251
+ mutationsDisallowed: true as const,
252
+ }),
243
253
  };
244
254
  return new Response(null, {
245
255
  status: ctx.res.status,
@@ -10,7 +10,10 @@
10
10
  * a re-implementation. Pass an array to test a chain in declaration order.
11
11
  */
12
12
 
13
- import { evaluateRevalidation } from "../router/revalidation.js";
13
+ import {
14
+ evaluateRevalidation,
15
+ toShouldRevalidateFn,
16
+ } from "../router/revalidation.js";
14
17
  import { runWithRequestContext } from "../server/request-context.js";
15
18
  import type { ResolvedSegment, ShouldRevalidateFn } from "../types.js";
16
19
  import {
@@ -81,12 +84,16 @@ export interface RunRevalidateOptions<TEnv = any> {
81
84
  const DEFAULT_URL = "http://localhost/";
82
85
 
83
86
  /**
84
- * Run one revalidate() predicate, or a chain in declaration order, the way
85
- * the router does for a request. Resolves to the final decision: true
86
- * re-runs the segment, false keeps the client's copy.
87
+ * Run one revalidate() argument, or a chain in declaration order, the way
88
+ * the router does for a request: a predicate, or `true` / `false` as
89
+ * `revalidate()` takes them. Resolves to the final decision: true re-runs
90
+ * the segment, false keeps the client's copy.
87
91
  */
88
92
  export async function runRevalidate<TEnv = any>(
89
- fn: ShouldRevalidateFn<any, TEnv> | readonly ShouldRevalidateFn<any, TEnv>[],
93
+ fn:
94
+ | ShouldRevalidateFn<any, TEnv>
95
+ | boolean
96
+ | readonly (ShouldRevalidateFn<any, TEnv> | boolean)[],
90
97
  opts: RunRevalidateOptions<TEnv> = {},
91
98
  ): Promise<boolean> {
92
99
  const currentUrl = toURL(opts.currentUrl, new URL(DEFAULT_URL));
@@ -124,7 +131,7 @@ export async function runRevalidate<TEnv = any>(
124
131
  nextUrl,
125
132
  revalidations: fns.map((entry, index) => ({
126
133
  name: `runRevalidate#${index}`,
127
- fn: entry,
134
+ fn: toShouldRevalidateFn(entry),
128
135
  })),
129
136
  routeKey: opts.routeName ?? "",
130
137
  context: (opts.context ?? ctx) as RevalidateArgs["context"],