@rangojs/router 0.24.3 → 0.25.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 (274) 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-shell-frame.d.ts +2 -0
  21. package/dist/types/cache/types.d.ts +8 -0
  22. package/dist/types/client-urls/client-root.d.ts +4 -1
  23. package/dist/types/client-urls/client-urls.d.ts +28 -2
  24. package/dist/types/client-urls/navigation.d.ts +6 -8
  25. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  26. package/dist/types/client-urls/server-projection.d.ts +20 -5
  27. package/dist/types/client-urls/types.d.ts +35 -17
  28. package/dist/types/client.d.ts +2 -2
  29. package/dist/types/client.rsc.d.ts +2 -2
  30. package/dist/types/decode-loader-results.d.ts +28 -7
  31. package/dist/types/href-client.d.ts +3 -1
  32. package/dist/types/index.rsc.d.ts +1 -1
  33. package/dist/types/loader-redirect.d.ts +2 -3
  34. package/dist/types/outlet-context.d.ts +7 -6
  35. package/dist/types/prefetch-markers.d.ts +14 -5
  36. package/dist/types/route-content-wrapper.d.ts +17 -14
  37. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  38. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  39. package/dist/types/route-definition/rewrite.d.ts +48 -5
  40. package/dist/types/route-types.d.ts +2 -2
  41. package/dist/types/router/handler-context.d.ts +14 -2
  42. package/dist/types/router/intercept-resolution.d.ts +19 -46
  43. package/dist/types/router/lazy-includes.d.ts +7 -0
  44. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  45. package/dist/types/router/middleware.d.ts +0 -16
  46. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  47. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  48. package/dist/types/router/revalidation.d.ts +8 -1
  49. package/dist/types/router/router-context.d.ts +4 -16
  50. package/dist/types/router/router-interfaces.d.ts +0 -6
  51. package/dist/types/router/router-options.d.ts +4 -7
  52. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  53. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  54. package/dist/types/router/segment-resolution.d.ts +1 -1
  55. package/dist/types/router/segment-wrappers.d.ts +3 -5
  56. package/dist/types/router/substitute-pattern-params.d.ts +19 -2
  57. package/dist/types/router/tracing.d.ts +2 -2
  58. package/dist/types/rsc/full-payload.d.ts +14 -2
  59. package/dist/types/rsc/helpers.d.ts +8 -0
  60. package/dist/types/rsc/manifest-init.d.ts +20 -0
  61. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  62. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  63. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  64. package/dist/types/rsc/server-action.d.ts +6 -0
  65. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  66. package/dist/types/rsc/shell-capture.d.ts +7 -0
  67. package/dist/types/rsc/types.d.ts +5 -0
  68. package/dist/types/segment-system.d.ts +15 -0
  69. package/dist/types/server/context.d.ts +64 -4
  70. package/dist/types/server/request-context.d.ts +41 -4
  71. package/dist/types/server/request-headers.d.ts +12 -0
  72. package/dist/types/server/rewrite-trail.d.ts +8 -1
  73. package/dist/types/testing/e2e/index.d.ts +1 -1
  74. package/dist/types/testing/flight.entry.d.ts +1 -1
  75. package/dist/types/testing/index.d.ts +1 -1
  76. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  77. package/dist/types/testing/render-route.d.ts +21 -11
  78. package/dist/types/testing/run-loader.d.ts +7 -0
  79. package/dist/types/testing/run-middleware.d.ts +7 -0
  80. package/dist/types/testing/run-revalidate.d.ts +5 -4
  81. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  82. package/dist/types/testing/shell-status.d.ts +17 -0
  83. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  84. package/dist/types/testing/vitest.d.ts +10 -1
  85. package/dist/types/thenable-status.d.ts +0 -6
  86. package/dist/types/types/handler-context.d.ts +12 -1
  87. package/dist/types/types/loader-types.d.ts +4 -3
  88. package/dist/types/types/route-entry.d.ts +2 -1
  89. package/dist/types/urls/path-helper-types.d.ts +4 -2
  90. package/dist/types/urls/path-helper.d.ts +5 -0
  91. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  92. package/dist/types/vite/discovery/state.d.ts +20 -0
  93. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  94. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  95. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  96. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  97. package/dist/vite/index.js +6816 -6547
  98. package/package.json +1 -1
  99. package/skills/app-structure/SKILL.md +255 -0
  100. package/skills/app-structure/content-site.md +333 -0
  101. package/skills/app-structure/e-commerce.md +435 -0
  102. package/skills/app-structure/large-app.md +885 -0
  103. package/skills/app-structure/multi-app.md +280 -0
  104. package/skills/breadcrumbs/SKILL.md +6 -1
  105. package/skills/caching/SKILL.md +3 -1
  106. package/skills/catalog.json +12 -0
  107. package/skills/client-urls/SKILL.md +195 -32
  108. package/skills/composability/SKILL.md +10 -8
  109. package/skills/handler-use/SKILL.md +48 -9
  110. package/skills/hooks/handle-and-actions.md +104 -0
  111. package/skills/hooks/navigation.md +24 -10
  112. package/skills/hooks/urls.md +1 -1
  113. package/skills/i18n/SKILL.md +2 -1
  114. package/skills/intercept/SKILL.md +60 -29
  115. package/skills/layout/SKILL.md +6 -3
  116. package/skills/links/SKILL.md +151 -17
  117. package/skills/loader/SKILL.md +32 -10
  118. package/skills/middleware/SKILL.md +111 -15
  119. package/skills/observability/SKILL.md +11 -0
  120. package/skills/parallel/SKILL.md +5 -5
  121. package/skills/ppr/SKILL.md +43 -8
  122. package/skills/prerender/SKILL.md +1 -2
  123. package/skills/rango/SKILL.md +3 -0
  124. package/skills/response-routes/SKILL.md +4 -1
  125. package/skills/scripts/SKILL.md +12 -1
  126. package/skills/server-actions/SKILL.md +60 -20
  127. package/skills/testing/SKILL.md +1 -0
  128. package/skills/testing/client-components.md +13 -10
  129. package/skills/testing/loader.md +25 -24
  130. package/skills/testing/middleware.md +11 -11
  131. package/skills/testing/response-routes.md +4 -1
  132. package/skills/testing/serve-shell-request.md +85 -21
  133. package/skills/ux-architecture/SKILL.md +980 -0
  134. package/skills/view-transitions/SKILL.md +43 -10
  135. package/src/__augment-tests__/augmented.check.ts +28 -0
  136. package/src/browser/event-controller.ts +44 -18
  137. package/src/browser/intercept-utils.ts +3 -2
  138. package/src/browser/merge-segment-loaders.ts +0 -85
  139. package/src/browser/navigation-bridge.ts +119 -98
  140. package/src/browser/navigation-client.ts +20 -15
  141. package/src/browser/navigation-store.ts +4 -2
  142. package/src/browser/navigation-transaction.ts +9 -8
  143. package/src/browser/partial-update.ts +90 -32
  144. package/src/browser/prefetch/cache.ts +8 -0
  145. package/src/browser/prefetch/fetch.ts +13 -3
  146. package/src/browser/rango-marker.ts +13 -0
  147. package/src/browser/react/NavigationProvider.tsx +25 -7
  148. package/src/browser/react/filter-segment-order.ts +13 -0
  149. package/src/browser/react/use-action.ts +9 -15
  150. package/src/browser/react/use-handle.ts +63 -71
  151. package/src/browser/react/use-href.tsx +24 -5
  152. package/src/browser/react/use-link-status.ts +1 -1
  153. package/src/browser/react/use-navigation.ts +30 -40
  154. package/src/browser/react/use-reverse.ts +4 -17
  155. package/src/browser/react/use-router.ts +37 -6
  156. package/src/browser/react/use-segments.ts +8 -12
  157. package/src/browser/response-adapter.ts +11 -5
  158. package/src/browser/rsc-router.tsx +3 -1
  159. package/src/browser/scroll-restoration.ts +34 -20
  160. package/src/browser/segment-reconciler.ts +11 -29
  161. package/src/browser/server-action-bridge.ts +52 -16
  162. package/src/browser/types.ts +40 -8
  163. package/src/build/generate-manifest.ts +12 -1
  164. package/src/build/route-types/codegen.ts +6 -2
  165. package/src/build/route-types/per-module-writer.ts +94 -61
  166. package/src/cache/cache-runtime.ts +9 -0
  167. package/src/cache/cache-scope.ts +2 -1
  168. package/src/cache/cf/cf-cache-store.ts +2 -0
  169. package/src/cache/cf/cf-shell-frame.ts +3 -0
  170. package/src/cache/types.ts +8 -0
  171. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  172. package/src/client-urls/client-root.tsx +10 -5
  173. package/src/client-urls/client-urls.ts +246 -166
  174. package/src/client-urls/navigation.ts +45 -38
  175. package/src/client-urls/revalidate-registry.ts +12 -3
  176. package/src/client-urls/server-projection.ts +286 -75
  177. package/src/client-urls/types.ts +40 -16
  178. package/src/client.rsc.tsx +11 -1
  179. package/src/client.tsx +9 -1
  180. package/src/decode-loader-results.ts +64 -31
  181. package/src/href-client.ts +3 -1
  182. package/src/index.rsc.ts +1 -0
  183. package/src/index.ts +12 -4
  184. package/src/loader-redirect.tsx +2 -3
  185. package/src/outlet-context.ts +7 -6
  186. package/src/prefetch-markers.ts +15 -5
  187. package/src/reverse.ts +3 -3
  188. package/src/route-content-wrapper.tsx +19 -17
  189. package/src/route-definition/dsl-helpers.ts +27 -7
  190. package/src/route-definition/helpers-types.ts +7 -2
  191. package/src/route-definition/resolve-handler-use.ts +23 -10
  192. package/src/route-definition/rewrite.ts +86 -19
  193. package/src/route-types.ts +1 -2
  194. package/src/router/find-match.ts +37 -1
  195. package/src/router/handler-context.ts +85 -40
  196. package/src/router/intercept-resolution.ts +161 -307
  197. package/src/router/lazy-includes.ts +43 -27
  198. package/src/router/loader-resolution.ts +21 -1
  199. package/src/router/manifest.ts +6 -8
  200. package/src/router/match-api.ts +11 -1
  201. package/src/router/match-middleware/background-revalidation.ts +2 -4
  202. package/src/router/match-middleware/cache-lookup.ts +16 -20
  203. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  204. package/src/router/match-result.ts +31 -2
  205. package/src/router/middleware.ts +6 -147
  206. package/src/router/navigation-snapshot.ts +38 -2
  207. package/src/router/prefetch-cache-ttl.ts +10 -18
  208. package/src/router/prerender-match.ts +2 -1
  209. package/src/router/revalidation.ts +24 -3
  210. package/src/router/router-context.ts +5 -18
  211. package/src/router/router-interfaces.ts +0 -7
  212. package/src/router/router-options.ts +4 -7
  213. package/src/router/segment-resolution/fresh.ts +6 -2
  214. package/src/router/segment-resolution/helpers.ts +7 -1
  215. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  216. package/src/router/segment-resolution/revalidation.ts +75 -35
  217. package/src/router/segment-resolution.ts +0 -1
  218. package/src/router/segment-wrappers.ts +8 -13
  219. package/src/router/substitute-pattern-params.ts +37 -2
  220. package/src/router/tracing.ts +2 -2
  221. package/src/router.ts +2 -9
  222. package/src/rsc/full-payload.ts +26 -4
  223. package/src/rsc/handler.ts +83 -41
  224. package/src/rsc/helpers.ts +13 -0
  225. package/src/rsc/manifest-init.ts +56 -3
  226. package/src/rsc/prefetch-render-error.ts +38 -0
  227. package/src/rsc/progressive-enhancement.ts +161 -211
  228. package/src/rsc/rewrite-passes.ts +56 -9
  229. package/src/rsc/rsc-rendering.ts +79 -23
  230. package/src/rsc/server-action.ts +25 -2
  231. package/src/rsc/shell-capture-constants.ts +22 -0
  232. package/src/rsc/shell-capture.ts +11 -0
  233. package/src/rsc/types.ts +5 -0
  234. package/src/segment-system.tsx +125 -119
  235. package/src/server/context.ts +96 -5
  236. package/src/server/handle-store.ts +3 -2
  237. package/src/server/request-context.ts +42 -3
  238. package/src/server/request-headers.ts +32 -0
  239. package/src/server/rewrite-trail.ts +55 -9
  240. package/src/ssr/ssr-root.tsx +6 -10
  241. package/src/testing/dispatch.ts +4 -5
  242. package/src/testing/e2e/index.ts +1 -0
  243. package/src/testing/flight.entry.ts +1 -0
  244. package/src/testing/index.ts +1 -0
  245. package/src/testing/internal/client-urls-projection.ts +31 -11
  246. package/src/testing/render-route.tsx +83 -40
  247. package/src/testing/run-loader.ts +26 -2
  248. package/src/testing/run-middleware.ts +10 -0
  249. package/src/testing/run-revalidate.ts +13 -6
  250. package/src/testing/serve-shell-request.ts +77 -4
  251. package/src/testing/shell-status.ts +40 -0
  252. package/src/testing/vitest-stubs/version.ts +1 -0
  253. package/src/testing/vitest.ts +74 -7
  254. package/src/thenable-status.ts +0 -11
  255. package/src/types/handler-context.ts +12 -1
  256. package/src/types/loader-types.ts +4 -3
  257. package/src/types/route-entry.ts +2 -1
  258. package/src/urls/include-helper.ts +38 -11
  259. package/src/urls/path-helper-types.ts +4 -2
  260. package/src/urls/path-helper.ts +16 -3
  261. package/src/vite/discovery/client-urls-projection.ts +205 -16
  262. package/src/vite/discovery/discover-routers.ts +2 -2
  263. package/src/vite/discovery/prerender-collection.ts +2 -6
  264. package/src/vite/discovery/state.ts +26 -0
  265. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  266. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  267. package/src/vite/plugins/hoist/source.ts +8 -7
  268. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  269. package/src/vite/plugins/version-plugin.ts +7 -0
  270. package/src/vite/plugins/version.d.ts +7 -0
  271. package/src/vite/plugins/virtual-entries.ts +20 -0
  272. package/src/vite/rango.ts +4 -1
  273. package/src/vite/router-discovery.ts +39 -7
  274. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -36,10 +36,20 @@ export {
36
36
 
37
37
  export { createLoader } from "./route-definition.js";
38
38
 
39
- export { clientUrls } from "./client-urls/client-urls.js";
39
+ // A module that is not "use client" and declares a component's `.use` can
40
+ // evaluate in the RSC realm too.
41
+ export {
42
+ clientUrls,
43
+ loader,
44
+ loading,
45
+ revalidate,
46
+ transition,
47
+ } from "./client-urls/client-urls.js";
40
48
  export type {
49
+ ClientUrlComponent,
41
50
  ClientUrlPatterns,
42
51
  ClientUrlRouteRecord,
52
+ ClientUrlUse,
43
53
  } from "./client-urls/client-urls.js";
44
54
 
45
55
  // "use client" hooks the default ./client entry exports. They are client
package/src/client.tsx CHANGED
@@ -309,10 +309,18 @@ export {
309
309
  type UseLoaderOptions,
310
310
  } from "./use-loader.js";
311
311
 
312
- export { clientUrls } from "./client-urls/client-urls.js";
312
+ export {
313
+ clientUrls,
314
+ loader,
315
+ loading,
316
+ revalidate,
317
+ transition,
318
+ } from "./client-urls/client-urls.js";
313
319
  export type {
320
+ ClientUrlComponent,
314
321
  ClientUrlPatterns,
315
322
  ClientUrlRouteRecord,
323
+ ClientUrlUse,
316
324
  } from "./client-urls/client-urls.js";
317
325
 
318
326
  /**
@@ -1,6 +1,11 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { createElement } from "react";
3
- import { isLoaderDataResult } from "./types.js";
3
+ import {
4
+ isLoaderDataResult,
5
+ type ErrorInfo,
6
+ type ResolvedSegment,
7
+ } from "./types.js";
8
+ import { isPlaceholder } from "./types/segments.js";
4
9
  import { LoaderRedirect } from "./loader-redirect.js";
5
10
 
6
11
  /**
@@ -15,13 +20,13 @@ import { LoaderRedirect } from "./loader-redirect.js";
15
20
  * Only decodeLoaderEntry (the read-site path — client components, so the
16
21
  * throw lands in the router-owned boundary; during document SSR, Fizz emits
17
22
  * the Suspense fallback and replays the throw at hydration) throws these.
18
- * decodeLoaderResults runs during the SERVER tree build on forceAwait/action
19
- * lanes, where a throw would collapse the whole payload — it routes signals
20
- * through the errorFallback slot instead (same visual: the slot replaces the
21
- * children under OutletProvider). The document lane's `{ ssr: false }`
22
- * results take the same non-throwing route (segment-system
23
- * buildLoaderStreams): they are settled before the tree is built, and the
24
- * read-site throw would land in the Fizz shell with no boundary to absorb it.
23
+ * decodeLoaderResults runs during the tree build on settled results, where a
24
+ * throw would collapse the whole payload — it routes signals through the
25
+ * errorFallback slot instead (same visual: the slot replaces the children
26
+ * under OutletProvider). The document lane's `{ ssr: false }` results take
27
+ * this route (segment-system buildLoaderStreams): they are settled before the
28
+ * tree is built, and the read-site throw would land in the Fizz shell with no
29
+ * boundary to absorb it.
25
30
  */
26
31
  export const LOADER_NOT_FOUND_FALLBACK: unique symbol = Symbol(
27
32
  "rango.loaderNotFound",
@@ -65,6 +70,52 @@ function signalFallback(result: any): ReactNode | undefined {
65
70
  return undefined;
66
71
  }
67
72
 
73
+ /**
74
+ * A failed loader entry's error, rebuilt with the ErrorInfo identity
75
+ * (name/stack/code/cause) instead of a stripped generic Error.
76
+ */
77
+ export function loaderResultError(result: { error: ErrorInfo }): Error {
78
+ const info = result.error;
79
+ const err = new Error(
80
+ info.message,
81
+ info.cause !== undefined ? { cause: info.cause } : undefined,
82
+ );
83
+ if (info.name) err.name = info.name;
84
+ if (info.stack) err.stack = info.stack;
85
+ if (info.code !== undefined) (err as { code?: string }).code = info.code;
86
+ return err;
87
+ }
88
+
89
+ /** A loader that threw: not ok, and not a notFound()/redirect() signal. */
90
+ export function isLoaderError(
91
+ result: unknown,
92
+ ): result is { error: ErrorInfo; fallback: ReactNode | null } {
93
+ return (
94
+ isLoaderDataResult(result) &&
95
+ !result.ok &&
96
+ !result.notFound &&
97
+ !result.redirect
98
+ );
99
+ }
100
+
101
+ /**
102
+ * The error of the first loader that threw while it re-ran in an action's
103
+ * revalidation, for useAction()'s `revalidationError`. The loader itself
104
+ * throws at its read (segment-system buildLoaderStreams), and the action
105
+ * resolves. A stale revalidation rejects with it instead and commits nothing
106
+ * (partial-update.ts). A notFound()/redirect() is a signal, not an error.
107
+ */
108
+ export async function revalidationErrorOf(
109
+ segments: ResolvedSegment[] = [],
110
+ ): Promise<Error | undefined> {
111
+ for (const s of segments) {
112
+ if (s.type !== "loader" || isPlaceholder(s)) continue;
113
+ const entry = await s.loaderData;
114
+ if (isLoaderError(entry)) return loaderResultError(entry);
115
+ }
116
+ return undefined;
117
+ }
118
+
68
119
  // Shared by segment-system (server) and LoaderResolver (client) so the
69
120
  // legacy/ok/error-fallback/throw decode of resolved loader values lives once.
70
121
  // Last failing loader wins errorFallback; an error without a fallback throws.
@@ -90,10 +141,9 @@ export function decodeLoaderResults(
90
141
  }
91
142
 
92
143
  // Authority signals take the errorFallback slot (this decode runs during
93
- // the server tree build on forceAwait/action lanes — throwing here would
94
- // collapse the payload). A signal outranks a plain error fallback and
95
- // stops the scan: notFound renders the server-rendered 404 UI, redirect
96
- // mounts LoaderRedirect which navigates.
144
+ // the tree build — throwing here would collapse the payload). A signal
145
+ // outranks a plain error fallback and stops the scan: notFound renders the
146
+ // server-rendered 404 UI, redirect mounts LoaderRedirect which navigates.
97
147
  const signal = signalFallback(result);
98
148
  if (signal !== undefined) {
99
149
  return { loaderData, errorFallback: signal };
@@ -108,17 +158,7 @@ export function decodeLoaderResults(
108
158
  if (result.fallback != null) {
109
159
  errorFallback = result.fallback;
110
160
  } else {
111
- // No boundary: rethrow preserving the ErrorInfo identity (name/stack/
112
- // code/cause) instead of a stripped generic Error.
113
- const info = result.error;
114
- const err = new Error(
115
- info.message,
116
- info.cause !== undefined ? { cause: info.cause } : undefined,
117
- );
118
- if (info.name) err.name = info.name;
119
- if (info.stack) err.stack = info.stack;
120
- if (info.code !== undefined) (err as { code?: string }).code = info.code;
121
- throw err;
161
+ throw loaderResultError(result);
122
162
  }
123
163
  }
124
164
 
@@ -153,14 +193,7 @@ export function decodeLoaderEntry(result: any): any {
153
193
  return result.data;
154
194
  }
155
195
  throwSignal(result);
156
- const info = result.error;
157
- const err = new Error(
158
- info.message,
159
- info.cause !== undefined ? { cause: info.cause } : undefined,
160
- );
161
- if (info.name) err.name = info.name;
162
- if (info.stack) err.stack = info.stack;
163
- if (info.code !== undefined) (err as { code?: string }).code = info.code;
196
+ const err = loaderResultError(result);
164
197
  if (result.fallback != null) {
165
198
  (err as any)[LOADER_ERROR_FALLBACK] = result.fallback;
166
199
  }
@@ -274,7 +274,9 @@ declare global {
274
274
  * Type-safe href function for client-side use
275
275
  *
276
276
  * Without mount: identity function, validates absolute paths at compile time.
277
- * With mount: prepends mount path, for use with useMount() inside include() scopes.
277
+ * With mount: prepends the mount as given. `useMount()` under a parameterized
278
+ * include returns its pattern (`/:accountId`), which this does not fill: use
279
+ * `useHref()` there, which substitutes the mount params.
278
280
  *
279
281
  * @param path - A valid path matching one of the registered route patterns
280
282
  * @param mount - Optional mount prefix from useMount() for include-scoped paths
package/src/index.rsc.ts CHANGED
@@ -124,6 +124,7 @@ export {
124
124
  rewrite,
125
125
  type Rewrite,
126
126
  type RewriteFunction,
127
+ type RewriteShell,
127
128
  } from "./route-definition/rewrite.js";
128
129
  export { RewriteLoopError } from "./errors.js";
129
130
 
package/src/index.ts CHANGED
@@ -134,6 +134,14 @@ function serverOnlyStubError(name: string): Error {
134
134
  );
135
135
  }
136
136
 
137
+ /** A stub whose name `@rangojs/router/client` exports for clientUrls(). */
138
+ function clientUrlsHelperStubError(name: string): Error {
139
+ return new Error(
140
+ `${name}() is only available from "@rangojs/router" in a react-server/RSC environment. ` +
141
+ `In a "use client" module (a clientUrls() component's .use), import ${name}() from "@rangojs/router/client".`,
142
+ );
143
+ }
144
+
137
145
  /**
138
146
  * Error-throwing stub for server-only `urls` function.
139
147
  */
@@ -276,7 +284,7 @@ export function middleware(): never {
276
284
  throw serverOnlyStubError("middleware");
277
285
  }
278
286
  export function revalidate(): never {
279
- throw serverOnlyStubError("revalidate");
287
+ throw clientUrlsHelperStubError("revalidate");
280
288
  }
281
289
  // Cache tag APIs are server-only (real implementations in index.rsc.ts). These
282
290
  // stubs keep the named-export shape identical under the default/non-react-server
@@ -291,10 +299,10 @@ export function revalidateTag(): never {
291
299
  throw serverOnlyStubError("revalidateTag");
292
300
  }
293
301
  export function loader(): never {
294
- throw serverOnlyStubError("loader");
302
+ throw clientUrlsHelperStubError("loader");
295
303
  }
296
304
  export function loading(): never {
297
- throw serverOnlyStubError("loading");
305
+ throw clientUrlsHelperStubError("loading");
298
306
  }
299
307
  export function parallel(): never {
300
308
  throw serverOnlyStubError("parallel");
@@ -309,7 +317,7 @@ export function notFoundBoundary(): never {
309
317
  throw serverOnlyStubError("notFoundBoundary");
310
318
  }
311
319
  export function transition(): never {
312
- throw serverOnlyStubError("transition");
320
+ throw clientUrlsHelperStubError("transition");
313
321
  }
314
322
 
315
323
  // Request context type (safe for client)
@@ -19,9 +19,8 @@ import { resolveSameOriginRedirect } from "./redirect-origin.js";
19
19
  *
20
20
  * Own module (not route-content-wrapper.tsx) because BOTH consumers need it
21
21
  * without a cycle: the StreamedLoaderErrorBoundary redirect branch (read-site
22
- * throw path) and decodeLoaderResults (aggregate forceAwait/action path, which
23
- * runs during the server tree build and cannot throw — it plants this element
24
- * in the errorFallback slot instead).
22
+ * throw path) and decodeLoaderResults (the tree build's settled results, which
23
+ * cannot throw — it plants this element in the errorFallback slot instead).
25
24
  */
26
25
  export function LoaderRedirect({
27
26
  to,
@@ -13,18 +13,19 @@ export interface OutletContextValue {
13
13
  * $$id. A value may be a pending Promise (loader still streaming) or an
14
14
  * already-settled result entry. useLoader `use()`es a pending value at the
15
15
  * read site — implicit suspension to the nearest consumer Suspense boundary —
16
- * and decodes via decodeLoaderEntry. Populated instead of `loaderData` on
17
- * streaming lanes (plain navs, document SSR); forceAwait/action lanes keep
18
- * the resolved `loaderData` record.
16
+ * and decodes via decodeLoaderEntry. Populated instead of `loaderData` for
17
+ * a route's or layout's loaders on every lane; forceAwait/action lanes hand
18
+ * settled entries. `loaderData` is a slot's decoded aggregate.
19
19
  */
20
20
  loaderStreams?: Record<string, unknown>;
21
21
  /**
22
22
  * Loader $$ids on this segment that the render awaited before first flush
23
23
  * (loader(Def, { ssr: false })). Dev-diagnostic input only: useLoader uses
24
24
  * it to warn when a read still suspends during SSR on a render that awaited
25
- * flagged loaders (ssr-suspension-warning.ts). Present only on streaming
26
- * lanes with flagged loaders (document and shell-capture renders); absent
27
- * on forceAwait/action lanes.
25
+ * flagged loaders (ssr-suspension-warning.ts). Present only where a
26
+ * segment carries flagged loaders (document and shell-capture renders, and
27
+ * a client lane that re-renders their segments); a settled read never
28
+ * suspends, so it never warns.
28
29
  */
29
30
  awaitedLoaderIds?: readonly string[];
30
31
  parent?: OutletContextValue | null;
@@ -1,9 +1,9 @@
1
1
  /**
2
- * The wire markers of `prefetch: false` (docs/design/prefetch-false.md):
3
- * shared by the browser that sends them and the server that reads them
4
- * (router/segment-resolution/prefetch-deferral.ts requestKind). Dependency
5
- * free, like segment-fragments.ts: the browser must not import the server
6
- * module that reads them.
2
+ * The wire markers of prefetches and of `prefetch: false`
3
+ * (docs/design/prefetch-false.md): shared by the browser and the server
4
+ * (router/segment-resolution/prefetch-deferral.ts requestKind,
5
+ * rsc/prefetch-render-error.ts). Dependency free, like segment-fragments.ts:
6
+ * the browser must not import the server modules that use them.
7
7
  */
8
8
 
9
9
  /** Request header of a `<Link>` prefetch (browser/prefetch/fetch.ts). */
@@ -11,3 +11,13 @@ export const PREFETCH_HEADER = "X-Rango-Prefetch";
11
11
 
12
12
  /** Query param of a fill request (browser/navigation-client.ts). */
13
13
  export const FILL_PARAM = "_rsc_fill";
14
+
15
+ /**
16
+ * The last row of a prefetch whose render reported an error
17
+ * (rsc/prefetch-render-error.ts): a Flight hint with a code React does not
18
+ * dispatch, so the decoder skips it (pinned by
19
+ * src/__tests__/flight-unknown-hint.test.ts), and browser/response-adapter.ts
20
+ * reads the stream as not ended cleanly. Five bytes: teeWithCompletion keeps
21
+ * the last five.
22
+ */
23
+ export const PREFETCH_FAILED_ROW = ":H_0\n";
package/src/reverse.ts CHANGED
@@ -406,9 +406,9 @@ export function createReverse<TRoutes extends Record<string, string>>(
406
406
  throw new Error(`Unknown route: ${name}`);
407
407
  }
408
408
 
409
- let result = params
410
- ? substitutePatternParams(pattern, params, name)
411
- : pattern;
409
+ // Always substitute: a required param with no params object throws
410
+ // `Missing param` rather than returning the raw `:param` pattern.
411
+ let result = substitutePatternParams(pattern, params ?? {}, name);
412
412
 
413
413
  // Append search params as query string
414
414
  if (search) {
@@ -18,6 +18,7 @@ import {
18
18
  useRevealAudit,
19
19
  } from "./suspense-audit-react.js";
20
20
  import { SETTLED_THENABLE } from "./thenable-status.js";
21
+ import { shallowEqual } from "./browser/react/shallow-equal.js";
21
22
 
22
23
  /**
23
24
  * Router-owned error boundary for read-site loader errors. segment-system
@@ -40,12 +41,12 @@ import { SETTLED_THENABLE } from "./thenable-status.js";
40
41
  * Errors without any marker rethrow to the app's own boundaries.
41
42
  */
42
43
  export class StreamedLoaderErrorBoundary extends Component<
43
- { children: ReactNode; resetKey?: string },
44
- { error: unknown; resetKey?: string }
44
+ { children: ReactNode; reset: unknown[] },
45
+ { error: unknown; reset: unknown[] }
45
46
  > {
46
- state: { error: unknown; resetKey?: string } = {
47
+ state: { error: unknown; reset: unknown[] } = {
47
48
  error: null,
48
- resetKey: this.props.resetKey,
49
+ reset: this.props.reset,
49
50
  };
50
51
 
51
52
  static getDerivedStateFromError(error: unknown): { error: unknown } {
@@ -54,20 +55,21 @@ export class StreamedLoaderErrorBoundary extends Component<
54
55
 
55
56
  /**
56
57
  * A caught marker (redirect, notFound, error fallback) belongs to ONE route
57
- * + params. Group-keyed segments (ResolvedSegment.clientGroup) keep this
58
- * instance alive across in-group navigations, so the error must clear when
59
- * the route or params change — otherwise a redirect caught for /legacy
60
- * keeps rendering LoaderRedirect for /state. `resetKey` is the segment's
61
- * id-params identity, the cadence the per-route remount used to provide.
58
+ * + params and ONE delivery of the segment's loader data. `reset` is the
59
+ * segment's id-params identity followed by each of its loaders' data; the
60
+ * error clears when any of them changes. Group-keyed segments
61
+ * (ResolvedSegment.clientGroup) keep this instance alive across in-group
62
+ * navigations, so a redirect caught for /legacy must not keep rendering
63
+ * LoaderRedirect for /state. A rerun hands a loader new data, a held loader
64
+ * keeps its own: a later action or navigation that re-ran the loader clears
65
+ * an error fallback left by an earlier failure, and a tree that re-ran
66
+ * nothing here keeps it.
62
67
  */
63
68
  static getDerivedStateFromProps(
64
- props: { resetKey?: string },
65
- state: { error: unknown; resetKey?: string },
66
- ): { error: unknown; resetKey?: string } | null {
67
- if (props.resetKey !== state.resetKey) {
68
- return { error: null, resetKey: props.resetKey };
69
- }
70
- return null;
69
+ { reset }: { reset: unknown[] },
70
+ state: { reset: unknown[] },
71
+ ): { error: null; reset: unknown[] } | null {
72
+ return shallowEqual(reset, state.reset) ? null : { error: null, reset };
71
73
  }
72
74
 
73
75
  render(): ReactNode {
@@ -311,7 +313,7 @@ function AuditedLoaderResolver({
311
313
  * Without streams, the pre-spike behavior is preserved: a pending aggregate
312
314
  * resolves ABOVE via use() (parallel/intercept slots still take this path —
313
315
  * ResolvedSegment carries only the aggregate for slots), and a resolved
314
- * array decodes synchronously (forceAwait/action lanes commit whole).
316
+ * array decodes synchronously (a slot on the forceAwait/action lanes).
315
317
  */
316
318
  function LoaderResolver({
317
319
  loaderDataPromise,
@@ -11,6 +11,7 @@ import type {
11
11
  import {
12
12
  getContext,
13
13
  getNamePrefix,
14
+ getReverseScope,
14
15
  getUrlPrefix,
15
16
  requireDslContext,
16
17
  stampStaticDefScope,
@@ -52,6 +53,7 @@ import {
52
53
  validateInterceptUseItems,
53
54
  } from "./resolve-handler-use.js";
54
55
  import { ALL_USE_ITEM_TYPES } from "./use-item-types.js";
56
+ import { toShouldRevalidateFn } from "../router/revalidation.js";
55
57
 
56
58
  /**
57
59
  * Check if an item contains routes (directly or inside nested wrappers).
@@ -189,6 +191,10 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
189
191
  const { store, ctx } = requireDslContext(
190
192
  "revalidate() must be called inside urls()",
191
193
  );
194
+ invariant(
195
+ typeof fn === "function" || typeof fn === "boolean",
196
+ `revalidate() expects a predicate or a boolean, got ${fn === null ? "null" : typeof fn}`,
197
+ );
192
198
 
193
199
  // Attach to last entry in stack
194
200
  const parent = ctx.parent;
@@ -196,7 +202,7 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
196
202
  invariant(false, "No parent entry available for revalidate()");
197
203
  }
198
204
  const name = `$${store.getNextIndex("revalidate")}`;
199
- parent.revalidate.push(fn);
205
+ parent.revalidate.push(toShouldRevalidateFn(fn));
200
206
  return { name, type: "revalidate" } as RevalidateItem;
201
207
  };
202
208
 
@@ -394,6 +400,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
394
400
  cache: cacheConfig,
395
401
  handler: RootLayout,
396
402
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
403
+ reverseScope: getReverseScope(),
397
404
  } satisfies EntryData;
398
405
 
399
406
  // Attach to parent's layout array (cache entries are structural like layouts)
@@ -433,6 +440,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
433
440
  // Cache entries render like layouts (with Outlet as default handler)
434
441
  handler: RootLayout, // RootLayout just renders <Outlet />
435
442
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
443
+ reverseScope: getReverseScope(),
436
444
  } satisfies EntryData;
437
445
 
438
446
  // Run children with cache entry as parent
@@ -522,6 +530,7 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
522
530
  handler: RootLayout,
523
531
  middleware: [...fns],
524
532
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
533
+ reverseScope: getReverseScope(),
525
534
  } satisfies EntryData;
526
535
 
527
536
  // Run children callback. If the second arg was actually a middleware fn
@@ -643,6 +652,7 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
643
652
  parent: null, // Parallels don't participate in parent chain traversal
644
653
  handler: unwrappedSlots,
645
654
  ...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
655
+ reverseScope: getReverseScope(),
646
656
  ...(hasStaticSlot
647
657
  ? {
648
658
  isStaticPrerender: true as const,
@@ -777,12 +787,11 @@ const intercept = (
777
787
  const prefixedRouteName =
778
788
  isLocal && namePrefix ? `${namePrefix}.${bareRouteName}` : bareRouteName;
779
789
 
780
- // Create intercept entry with its own loaders/middleware/when
790
+ // Create intercept entry with its own loaders/when
781
791
  const entry: InterceptEntry = {
782
792
  slotName: slotName as `@${string}`,
783
793
  routeName: prefixedRouteName,
784
794
  handler,
785
- middleware: [],
786
795
  loader: [],
787
796
  when: [], // Selector conditions for conditional interception
788
797
  };
@@ -800,12 +809,12 @@ const intercept = (
800
809
  const handlerUseFn = resolveHandlerUse(handler);
801
810
  const mergedUse = mergeHandlerUse(handlerUseFn, useFn, null);
802
811
 
803
- // Run merged use callback to collect loaders, middleware, etc.
812
+ // Run merged use callback to collect loaders, loading, layout, etc.
804
813
  if (mergedUse) {
805
814
  // Capture layout() calls into a temporary array
806
815
  const capturedLayouts: EntryData[] = [];
807
816
 
808
- // Temporary parent so middleware/loader attach to the intercept entry;
817
+ // Temporary parent so loader() attaches to the intercept entry;
809
818
  // the loading get/set accessor mirrors writes onto `entry`. It is a
810
819
  // shallow spread of the enclosing entry, so every field a rejected helper
811
820
  // writes to points at a throwaway here: a helper called but not returned
@@ -814,8 +823,12 @@ const intercept = (
814
823
  // is a route.
815
824
  //
816
825
  // Why an intercept rejects these (validateInterceptUseItems):
826
+ // - middleware(): only the live render resolved the intercept's own
827
+ // middleware; a cache() hit or a Prerender() target skipped it, so a
828
+ // guard in it was bypassed. The target route's middleware runs on every
829
+ // pass, the modal's included.
817
830
  // - revalidate(): an intercept only evaluates its loaders' revalidate()
818
- // (router/intercept-resolution.ts).
831
+ // (loader segments of its slot, router/intercept-resolution.ts).
819
832
  // - errorBoundary()/notFoundBoundary(): error lookup walks EntryData
820
833
  // parent chains (router/error-handling.ts), which never include an
821
834
  // InterceptEntry; intercept loader errors resolve against the declaring
@@ -824,7 +837,7 @@ const intercept = (
824
837
  // cache() scope, with its own "intercept:" key (cache/cache-scope.ts).
825
838
  const tempParent = {
826
839
  ...ctx.parent,
827
- middleware: entry.middleware,
840
+ middleware: [],
828
841
  loader: entry.loader,
829
842
  layout: capturedLayouts,
830
843
  revalidate: [],
@@ -833,6 +846,8 @@ const intercept = (
833
846
  parallel: {},
834
847
  intercept: [],
835
848
  cache: undefined,
849
+ // Its loaders resolve `.name` like the intercept handler: from the route.
850
+ reverseScope: undefined,
836
851
  get loading() {
837
852
  return entry.loading;
838
853
  },
@@ -925,6 +940,10 @@ const loader: RouteHelpers<any, any>["loader"] = (
925
940
  ...(optionsGiven?.ssr === false &&
926
941
  ctx.isSSR && { awaitBeforeFlush: true as const }),
927
942
  ...(optionsGiven?.prefetch === false && { prefetch: false as const }),
943
+ // The declaring layout's / parallel's / cache()'s scope. A route's (and
944
+ // an intercept's) loaders have none: they resolve from the route's name,
945
+ // like the handler.
946
+ ...(ctx.parent.reverseScope && { reverseScope: ctx.parent.reverseScope }),
928
947
  };
929
948
 
930
949
  // Merge handler.use defaults (attached to the loader definition) with explicit use
@@ -1156,6 +1175,7 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
1156
1175
  parent: ctx.parent,
1157
1176
  handler: unwrappedHandler,
1158
1177
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
1178
+ reverseScope: getReverseScope(),
1159
1179
  ...(isStatic
1160
1180
  ? {
1161
1181
  isStaticPrerender: true as const,
@@ -183,7 +183,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
183
183
  * // With loaders and revalidation (revalidate() goes on the loader;
184
184
  * // an intercept-level revalidate() throws)
185
185
  * intercept("@modal", "card", () => <CardModal />, () => [
186
- * loader(CardModalLoader, () => [revalidate(() => false)]),
186
+ * loader(CardModalLoader, () => [revalidate(false)]),
187
187
  * ])
188
188
  *
189
189
  * // Conditional activation via the config object's `when` selector
@@ -288,6 +288,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
288
288
  * revalidate(({ defaultShouldRevalidate }) =>
289
289
  * ({ defaultShouldRevalidate: true })
290
290
  * )
291
+ *
292
+ * // Shorthand for a constant hard decision: revalidate(() => false)
293
+ * revalidate(false)
291
294
  * ```
292
295
  * Not valid directly inside intercept() use() (throws at definition time):
293
296
  * intercepts only revalidate their loaders, so attach it there with
@@ -298,8 +301,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
298
301
  * for downstream revalidators),
299
302
  * - or nothing / `null` / `undefined` (defer — leaves the suggestion
300
303
  * unchanged and continues to the next revalidator).
304
+ * Or `true` / `false`: shorthand for `() => true` / `() => false`. A
305
+ * defer has no shorthand; write the function.
301
306
  */
302
- revalidate: (fn: ShouldRevalidateFn<any, TEnv>) => RevalidateItem;
307
+ revalidate: (fn: ShouldRevalidateFn<any, TEnv> | boolean) => RevalidateItem;
303
308
  /**
304
309
  * Attach a data loader to the current route/layout
305
310
  * ```typescript
@@ -93,14 +93,7 @@ const MOUNT_SITE_ALLOWED_TYPES: Record<string, Set<string>> = {
93
93
  ]),
94
94
  // intercept() checks its whole merged use() (explicit and handler.use)
95
95
  // against this set via validateInterceptUseItems below.
96
- intercept: new Set([
97
- "middleware",
98
- "loader",
99
- "loading",
100
- "layout",
101
- "route",
102
- "transition",
103
- ]),
96
+ intercept: new Set(["loader", "loading", "layout", "route", "transition"]),
104
97
  // LoaderUseItem — only revalidate + cache can attach to a loader entry
105
98
  loader: new Set(["revalidate", "cache"]),
106
99
  };
@@ -113,6 +106,8 @@ const INTERCEPT_BOUNDARY_HINT =
113
106
  * hold them is documented at intercept() in dsl-helpers.ts.
114
107
  */
115
108
  const INTERCEPT_REJECTION_HINTS: Record<string, string> = {
109
+ middleware:
110
+ "move it to the route's middleware() (it runs for the modal too) or into the loaders",
116
111
  revalidate:
117
112
  "attach it to the intercept's loader: loader(YourLoader, () => [revalidate(...)])",
118
113
  errorBoundary: INTERCEPT_BOUNDARY_HINT,
@@ -134,7 +129,7 @@ function rejectInterceptItem(
134
129
 
135
130
  /**
136
131
  * Validate every item an intercept's merged use() returned (explicit use()
137
- * and handler.use) against MOUNT_SITE_ALLOWED_TYPES.intercept, plus two
132
+ * and handler.use) against MOUNT_SITE_ALLOWED_TYPES.intercept, plus three
138
133
  * structural rules:
139
134
  * - a nested layout() is only the modal chrome (intercept() keeps its
140
135
  * handler), so it may not carry use() items of its own;
@@ -142,13 +137,20 @@ function rejectInterceptItem(
142
137
  * it was not returned; its orphan form re-parents the following siblings
143
138
  * onto that entry, so it is rejected the same way. Inside a path, cache()
144
139
  * writes `sinks.cache` instead (dsl-helpers.ts cache()).
140
+ * - middleware on a `capturedLayouts` entry is rejected like `sinks.middleware`:
141
+ * it is an unreturned middleware(fn, () => [...]) or one called inside the
142
+ * chrome layout's use(), and nothing would run it.
145
143
  */
146
144
  export function validateInterceptUseItems(
147
145
  items: AllUseItems[],
148
- capturedLayouts: readonly { type: string }[],
146
+ capturedLayouts: readonly {
147
+ type: string;
148
+ middleware?: readonly unknown[];
149
+ }[],
149
150
  slotName: string,
150
151
  routeName: string,
151
152
  sinks: {
153
+ readonly middleware: readonly unknown[];
152
154
  readonly revalidate: readonly unknown[];
153
155
  readonly errorBoundary: readonly unknown[];
154
156
  readonly notFoundBoundary: readonly unknown[];
@@ -206,6 +208,17 @@ export function validateInterceptUseItems(
206
208
  routeName,
207
209
  );
208
210
  }
211
+ if (
212
+ sinks.middleware.length > 0 ||
213
+ capturedLayouts.some((l) => l.middleware?.length)
214
+ ) {
215
+ rejectInterceptItem(
216
+ "middleware()",
217
+ INTERCEPT_REJECTION_HINTS.middleware!,
218
+ slotName,
219
+ routeName,
220
+ );
221
+ }
209
222
  if (
210
223
  capturedLayouts.some((l) => l.type === "cache") ||
211
224
  sinks.cache !== undefined