@rangojs/router 0.22.1 → 0.24.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 (259) hide show
  1. package/dist/bin/rango.js +10 -4
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  7. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  8. package/dist/types/browser/partial-update.d.ts +39 -8
  9. package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
  10. package/dist/types/browser/react/context.d.ts +3 -2
  11. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  12. package/dist/types/browser/response-adapter.d.ts +7 -0
  13. package/dist/types/browser/types.d.ts +24 -10
  14. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  15. package/dist/types/build/generate-route-types.d.ts +1 -1
  16. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  17. package/dist/types/build/route-types/scan-filter.d.ts +22 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  19. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  20. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  21. package/dist/types/cache/document-cache.d.ts +2 -10
  22. package/dist/types/client-urls/client-urls.d.ts +1 -1
  23. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  24. package/dist/types/client-urls/navigation.d.ts +0 -37
  25. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  26. package/dist/types/client-urls/server-projection.d.ts +29 -1
  27. package/dist/types/client-urls/types.d.ts +34 -69
  28. package/dist/types/client.d.ts +0 -1
  29. package/dist/types/context-var.d.ts +12 -0
  30. package/dist/types/errors.d.ts +15 -0
  31. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  32. package/dist/types/handles/pushed-by.d.ts +31 -0
  33. package/dist/types/index.d.ts +4 -0
  34. package/dist/types/index.rsc.d.ts +2 -0
  35. package/dist/types/loader-store.d.ts +8 -0
  36. package/dist/types/prefetch-markers.d.ts +11 -0
  37. package/dist/types/response-utils.d.ts +7 -0
  38. package/dist/types/reverse.d.ts +8 -8
  39. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  40. package/dist/types/route-definition/rewrite.d.ts +56 -0
  41. package/dist/types/route-map-builder.d.ts +7 -2
  42. package/dist/types/router/basename.d.ts +7 -0
  43. package/dist/types/router/handler-context.d.ts +1 -5
  44. package/dist/types/router/internal-params.d.ts +8 -0
  45. package/dist/types/router/is-action.d.ts +2 -1
  46. package/dist/types/router/middleware-types.d.ts +15 -0
  47. package/dist/types/router/revalidation.d.ts +1 -2
  48. package/dist/types/router/route-trie-builder.d.ts +10 -0
  49. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  50. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  51. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
  52. package/dist/types/rsc/full-payload.d.ts +11 -0
  53. package/dist/types/rsc/handler.d.ts +33 -0
  54. package/dist/types/rsc/helpers.d.ts +8 -0
  55. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  56. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  57. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  58. package/dist/types/server/context.d.ts +21 -1
  59. package/dist/types/server/handle-store.d.ts +9 -1
  60. package/dist/types/server/request-context.d.ts +52 -2
  61. package/dist/types/server/rewrite-trail.d.ts +49 -0
  62. package/dist/types/server/slot-accessors.d.ts +29 -0
  63. package/dist/types/server.d.ts +1 -0
  64. package/dist/types/testing/flight.entry.d.ts +1 -1
  65. package/dist/types/testing/index.d.ts +4 -4
  66. package/dist/types/testing/internal/action-id.d.ts +10 -1
  67. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  68. package/dist/types/testing/internal/test-action.d.ts +8 -0
  69. package/dist/types/testing/render-route.d.ts +8 -0
  70. package/dist/types/testing/run-loader.d.ts +2 -1
  71. package/dist/types/testing/run-middleware.d.ts +20 -0
  72. package/dist/types/testing/run-revalidate.d.ts +74 -0
  73. package/dist/types/testing/run-transition-when.d.ts +2 -3
  74. package/dist/types/testing/serve-shell-request.d.ts +40 -1
  75. package/dist/types/testing/vitest.d.ts +11 -0
  76. package/dist/types/thenable-status.d.ts +6 -0
  77. package/dist/types/types/boundaries.d.ts +9 -0
  78. package/dist/types/types/handler-context.d.ts +14 -0
  79. package/dist/types/types/loader-types.d.ts +30 -0
  80. package/dist/types/types/request-scope.d.ts +7 -0
  81. package/dist/types/types/segments.d.ts +21 -4
  82. package/dist/types/urls/path-helper-types.d.ts +25 -4
  83. package/dist/types/urls/type-extraction.d.ts +6 -2
  84. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  85. package/dist/types/vite/discovery/state.d.ts +14 -4
  86. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  87. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  88. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  89. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  90. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  91. package/dist/types/vite/router-discovery.d.ts +10 -0
  92. package/dist/vite/index.js +974 -379
  93. package/package.json +1 -1
  94. package/skills/caching/SKILL.md +54 -5
  95. package/skills/catalog.json +3 -3
  96. package/skills/client-urls/SKILL.md +197 -24
  97. package/skills/cloudflare/SKILL.md +14 -9
  98. package/skills/hooks/SKILL.md +16 -3
  99. package/skills/hooks/data.md +3 -1
  100. package/skills/links/SKILL.md +24 -0
  101. package/skills/loader/SKILL.md +199 -2
  102. package/skills/middleware/SKILL.md +183 -2
  103. package/skills/migrate-nextjs/SKILL.md +2 -2
  104. package/skills/migrate-react-router/SKILL.md +10 -7
  105. package/skills/migrate-react-router/data-and-actions.md +1 -1
  106. package/skills/ppr/SKILL.md +22 -0
  107. package/skills/prerender/SKILL.md +22 -8
  108. package/skills/rango/SKILL.md +13 -7
  109. package/skills/route/SKILL.md +139 -0
  110. package/skills/server-actions/SKILL.md +27 -0
  111. package/skills/testing/SKILL.md +3 -2
  112. package/skills/testing/client-components.md +2 -1
  113. package/skills/testing/middleware.md +31 -10
  114. package/skills/testing/predicates.md +67 -32
  115. package/skills/testing/response-routes.md +1 -0
  116. package/skills/testing/serve-shell-request.md +107 -19
  117. package/skills/testing/setup.md +2 -2
  118. package/skills/typesafety/route-types.md +2 -2
  119. package/skills/use-cache/SKILL.md +4 -2
  120. package/skills/view-transitions/SKILL.md +26 -0
  121. package/src/__augment-tests__/augmented.check.ts +20 -0
  122. package/src/browser/defer.ts +82 -0
  123. package/src/browser/document-revealed.ts +52 -0
  124. package/src/browser/event-controller.ts +46 -32
  125. package/src/browser/merge-segment-loaders.ts +31 -18
  126. package/src/browser/navigation-bridge.ts +82 -16
  127. package/src/browser/navigation-client.ts +48 -36
  128. package/src/browser/navigation-store.ts +13 -0
  129. package/src/browser/navigation-transaction.ts +25 -8
  130. package/src/browser/partial-update.ts +554 -146
  131. package/src/browser/prefetch/fetch.ts +2 -1
  132. package/src/browser/react/Link.tsx +7 -9
  133. package/src/browser/react/NavigationProvider.tsx +88 -49
  134. package/src/browser/react/context.ts +3 -2
  135. package/src/browser/react/filter-segment-order.ts +13 -2
  136. package/src/browser/react/use-router.ts +2 -4
  137. package/src/browser/response-adapter.ts +20 -0
  138. package/src/browser/rsc-router.tsx +17 -45
  139. package/src/browser/server-action-bridge.ts +25 -63
  140. package/src/browser/types.ts +24 -10
  141. package/src/browser/validate-redirect-origin.ts +16 -0
  142. package/src/build/generate-route-types.ts +1 -0
  143. package/src/build/route-types/router-processing.ts +3 -6
  144. package/src/build/route-types/scan-filter.ts +45 -11
  145. package/src/cache/cf/cf-cache-store.ts +59 -21
  146. package/src/cache/cf/cf-cache-types.ts +24 -2
  147. package/src/cache/document-cache-policy.ts +131 -0
  148. package/src/cache/document-cache.ts +21 -107
  149. package/src/client-urls/client-urls.ts +22 -6
  150. package/src/client-urls/hoisted-revalidate.ts +25 -0
  151. package/src/client-urls/navigation.ts +0 -96
  152. package/src/client-urls/revalidate-registry.ts +30 -0
  153. package/src/client-urls/server-projection.ts +151 -65
  154. package/src/client-urls/types.ts +39 -71
  155. package/src/client.tsx +9 -5
  156. package/src/context-var.ts +32 -2
  157. package/src/errors.ts +34 -0
  158. package/src/handles/deferred-resolution.ts +28 -5
  159. package/src/handles/pushed-by.ts +148 -0
  160. package/src/index.rsc.ts +8 -0
  161. package/src/index.ts +7 -0
  162. package/src/loader-store.ts +18 -7
  163. package/src/prefetch-markers.ts +13 -0
  164. package/src/response-utils.ts +19 -0
  165. package/src/reverse.ts +17 -14
  166. package/src/route-content-wrapper.tsx +18 -4
  167. package/src/route-definition/dsl-helpers.ts +27 -4
  168. package/src/route-definition/helpers-types.ts +27 -1
  169. package/src/route-definition/redirect.ts +2 -15
  170. package/src/route-definition/rewrite.ts +146 -0
  171. package/src/route-map-builder.ts +14 -11
  172. package/src/router/basename.ts +20 -0
  173. package/src/router/handler-context.ts +3 -13
  174. package/src/router/intercept-resolution.ts +35 -16
  175. package/src/router/internal-params.ts +16 -0
  176. package/src/router/is-action.ts +2 -1
  177. package/src/router/loader-resolution.ts +85 -4
  178. package/src/router/match-api.ts +36 -1
  179. package/src/router/match-middleware/background-revalidation.ts +16 -4
  180. package/src/router/match-middleware/cache-lookup.ts +35 -7
  181. package/src/router/match-middleware/cache-store.ts +15 -1
  182. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  183. package/src/router/match-result.ts +14 -2
  184. package/src/router/middleware-types.ts +16 -0
  185. package/src/router/middleware.ts +127 -49
  186. package/src/router/navigation-snapshot.ts +2 -1
  187. package/src/router/revalidation.ts +1 -1
  188. package/src/router/route-trie-builder.ts +28 -1
  189. package/src/router/segment-resolution/fresh.ts +22 -18
  190. package/src/router/segment-resolution/loader-cache.ts +2 -1
  191. package/src/router/segment-resolution/loader-mask.ts +5 -3
  192. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  193. package/src/router/segment-resolution/mask-nested.ts +8 -4
  194. package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
  195. package/src/router/segment-resolution/revalidation.ts +215 -82
  196. package/src/rsc/full-payload.ts +37 -5
  197. package/src/rsc/handler.ts +82 -55
  198. package/src/rsc/helpers.ts +24 -0
  199. package/src/rsc/loader-fetch.ts +79 -24
  200. package/src/rsc/response-cache-serve.ts +18 -2
  201. package/src/rsc/response-route-handler.ts +4 -7
  202. package/src/rsc/rewrite-passes.ts +203 -0
  203. package/src/rsc/rsc-rendering.ts +54 -9
  204. package/src/rsc/shell-capture.ts +2 -1
  205. package/src/rsc/ssr-setup.ts +11 -5
  206. package/src/segment-loader-promise.ts +23 -20
  207. package/src/segment-system.tsx +60 -26
  208. package/src/server/context.ts +39 -5
  209. package/src/server/cookie-parse.ts +36 -12
  210. package/src/server/cookie-store.ts +7 -3
  211. package/src/server/handle-store.ts +152 -25
  212. package/src/server/request-context.ts +175 -64
  213. package/src/server/request-headers.ts +4 -14
  214. package/src/server/rewrite-trail.ts +190 -0
  215. package/src/server/slot-accessors.ts +57 -0
  216. package/src/server.ts +4 -0
  217. package/src/ssr/index.tsx +118 -16
  218. package/src/testing/action-form.ts +2 -11
  219. package/src/testing/dispatch.ts +138 -67
  220. package/src/testing/e2e/page-helpers.ts +3 -1
  221. package/src/testing/flight.entry.ts +1 -0
  222. package/src/testing/index.ts +7 -3
  223. package/src/testing/internal/action-id.ts +16 -1
  224. package/src/testing/internal/client-urls-projection.ts +70 -0
  225. package/src/testing/internal/test-action.ts +25 -0
  226. package/src/testing/render-route.tsx +16 -3
  227. package/src/testing/run-loader.ts +4 -1
  228. package/src/testing/run-middleware.ts +56 -11
  229. package/src/testing/run-revalidate.ts +142 -0
  230. package/src/testing/run-transition-when.ts +7 -8
  231. package/src/testing/serve-shell-request.ts +131 -8
  232. package/src/testing/vitest.ts +67 -1
  233. package/src/thenable-status.ts +11 -0
  234. package/src/types/boundaries.ts +12 -0
  235. package/src/types/handler-context.ts +14 -0
  236. package/src/types/loader-types.ts +30 -0
  237. package/src/types/request-scope.ts +8 -0
  238. package/src/types/segments.ts +26 -4
  239. package/src/urls/path-helper-types.ts +27 -5
  240. package/src/urls/type-extraction.ts +30 -10
  241. package/src/use-loader.tsx +18 -13
  242. package/src/vite/discovery/client-urls-projection.ts +20 -21
  243. package/src/vite/discovery/discover-routers.ts +94 -26
  244. package/src/vite/discovery/state.ts +28 -4
  245. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  246. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  247. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  248. package/src/vite/plugins/hoist/literal.ts +417 -0
  249. package/src/vite/plugins/hoist/scope.ts +370 -0
  250. package/src/vite/plugins/hoist/source.ts +84 -0
  251. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  252. package/src/vite/rango.ts +2 -0
  253. package/src/vite/router-discovery.ts +116 -53
  254. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  255. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  256. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  257. package/src/client-urls/revalidate-chain.ts +0 -83
  258. package/src/client-urls/revalidation-protocol.ts +0 -56
  259. package/src/testing/run-client-revalidate.ts +0 -92
@@ -0,0 +1,148 @@
1
+ import { LOADER_SEGMENT_ID } from "../browser/react/filter-segment-order.js";
2
+ import type { HandleData } from "../server/handle-store.js";
3
+
4
+ /**
5
+ * Which run pushed each handle value, so a partial response replaces only the
6
+ * pushes of what it re-ran (#1103).
7
+ *
8
+ * A segment's handle bucket holds its handler's pushes and those of the
9
+ * loaders registered on it: a loader pushes under its owning segment's id
10
+ * (fresh.ts, revalidation.ts pin `_currentSegmentId`). A navigation or an
11
+ * action response decides each of them on its own: the handler re-runs and a
12
+ * loader whose revalidate() answers false keeps its data on the client. The
13
+ * response carries only what re-ran, so replacing the whole bucket with it
14
+ * dropped the pushes of the loader whose data the page still shows (the
15
+ * document title, a breadcrumb), and a loader re-run under a handler that did
16
+ * not re-run took the handler's pushes with it.
17
+ *
18
+ * The record rides in the snapshot itself, beside each handle, under a name
19
+ * no handle has: `data[pushedByKey(h)][segmentId][i]` is the $$id of the
20
+ * loader whose run pushed `data[h][segmentId][i]`, or null for the segment's
21
+ * handler. A bucket without a loader push has no entry. Inside the snapshot it
22
+ * crosses Flight and every clone and history-cache copy with the values;
23
+ * collectHandleData reads a handle by its $$id, so no collect sees it.
24
+ * resolveDeferredHandleValues keeps it aligned when it drops a value.
25
+ */
26
+ const PUSHED_BY_SUFFIX = "\u0000pushedBy";
27
+
28
+ /** The snapshot key holding `handleName`'s pushedBy record. */
29
+ export function pushedByKey(handleName: string): string {
30
+ return handleName + PUSHED_BY_SUFFIX;
31
+ }
32
+
33
+ /** True for a pushedBy record's key, false for a handle's. */
34
+ export function isPushedByKey(name: string): boolean {
35
+ return name.endsWith(PUSHED_BY_SUFFIX);
36
+ }
37
+
38
+ /** `data` without its pushedBy records, as a reader outside the router sees it. */
39
+ export function withoutPushedBy(data: HandleData): HandleData {
40
+ const out: HandleData = {};
41
+ for (const name in data) {
42
+ if (!isPushedByKey(name)) out[name] = data[name];
43
+ }
44
+ return out;
45
+ }
46
+
47
+ type PushedBy = readonly (string | null)[] | undefined;
48
+
49
+ const keysOf = (a: object | undefined, b: object | undefined): Set<string> =>
50
+ new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})]);
51
+
52
+ /**
53
+ * `target` with the buckets of the segments in `ids` taken from `source`.
54
+ * A pushedBy record is a snapshot key like a handle's, so a bucket's values
55
+ * and the record of the runs that pushed them always come from the same
56
+ * snapshot. New containers; the value arrays are shared, as cloneHandleData
57
+ * (navigation-store.ts) shares them.
58
+ */
59
+ export function withSegmentHandles(
60
+ target: HandleData,
61
+ source: HandleData,
62
+ ids: ReadonlySet<string>,
63
+ ): HandleData {
64
+ const out: HandleData = {};
65
+ for (const name of keysOf(target, source)) {
66
+ const buckets: Record<string, unknown[]> = {};
67
+ for (const [id, values] of Object.entries(target[name] ?? {})) {
68
+ if (!ids.has(id)) buckets[id] = values;
69
+ }
70
+ for (const [id, values] of Object.entries(source[name] ?? {})) {
71
+ if (ids.has(id)) buckets[id] = values;
72
+ }
73
+ if (Object.keys(buckets).length > 0) out[name] = buckets;
74
+ }
75
+ return out;
76
+ }
77
+
78
+ /**
79
+ * The handle data after a partial response's snapshot `incoming` (each stream
80
+ * yield holds everything the response pushed so far). A segment outside
81
+ * `segmentOrder` loses its buckets. In the others a value stands for the run
82
+ * that pushed it: the segment's handler, or a loader `matched` registers on
83
+ * the segment (`${shortCode}D${index}.${$$id}`, a loader's pushes being filed
84
+ * under its shortCode; an id it does not register, a dependency or a loader a
85
+ * handler read, counts as the handler's). A run that has values in the
86
+ * response, or ran for it (`resolvedIds`: the handlers and loaders that ran),
87
+ * replaces its values with the ones it delivered, nothing when it delivered
88
+ * none. The other runs keep theirs in place, and the response's values take
89
+ * the position of the first value they replace (the end when they replace
90
+ * none). New containers throughout: an object a reader was handed (a history
91
+ * entry's data) is never written to.
92
+ */
93
+ export function mergePartialHandleData(
94
+ current: HandleData,
95
+ incoming: HandleData,
96
+ segmentOrder: readonly string[],
97
+ matched: readonly string[],
98
+ resolvedIds: readonly string[] | undefined,
99
+ ): HandleData {
100
+ // A run's key: a segment's id for its handler, `${segmentId}\0${$$id}` for
101
+ // a loader (its segment id with the `D${index}.` marker replaced).
102
+ const runKey = (id: string): string =>
103
+ id.replace(LOADER_SEGMENT_ID, "\u0000");
104
+ const inOrder = new Set(segmentOrder);
105
+ const registered = new Set(matched.map(runKey));
106
+ const ran = new Set(resolvedIds?.map(runKey));
107
+ const out: HandleData = {};
108
+ for (const name of keysOf(current, incoming)) {
109
+ if (isPushedByKey(name)) continue;
110
+ const byName = pushedByKey(name);
111
+ const buckets: Record<string, unknown[]> = (out[name] = {});
112
+ for (const segmentId of keysOf(current[name], incoming[name])) {
113
+ if (!inOrder.has(segmentId)) continue;
114
+ const run = (by: PushedBy, i: number): string => {
115
+ const key = `${segmentId}\u0000${by?.[i]}`;
116
+ return registered.has(key) ? key : segmentId;
117
+ };
118
+ const currentBy = current[byName]?.[segmentId] as PushedBy;
119
+ const incomingValues = incoming[name]?.[segmentId] ?? [];
120
+ const incomingBy = incoming[byName]?.[segmentId] as PushedBy;
121
+ const delivered = new Set(
122
+ incomingValues.map((_, i) => run(incomingBy, i)),
123
+ );
124
+ const values: unknown[] = [];
125
+ const by: (string | null)[] = [];
126
+ let at = -1;
127
+ (current[name]?.[segmentId] ?? []).forEach((value, i) => {
128
+ const key = run(currentBy, i);
129
+ if (delivered.has(key) || ran.has(key)) {
130
+ if (at < 0) at = values.length;
131
+ } else {
132
+ values.push(value);
133
+ by.push(currentBy?.[i] ?? null);
134
+ }
135
+ });
136
+ if (at < 0) at = values.length;
137
+ values.splice(at, 0, ...incomingValues);
138
+ by.splice(
139
+ at,
140
+ 0,
141
+ ...incomingValues.map((_, i) => incomingBy?.[i] ?? null),
142
+ );
143
+ if (values.length > 0) buckets[segmentId] = values;
144
+ if (by.some((run) => run !== null)) (out[byName] ??= {})[segmentId] = by;
145
+ }
146
+ }
147
+ return out;
148
+ }
package/src/index.rsc.ts CHANGED
@@ -119,6 +119,14 @@ export {
119
119
  transition,
120
120
  } from "./route-definition.js";
121
121
 
122
+ // Render another route at the same URL from middleware (throw rewrite()).
123
+ export {
124
+ rewrite,
125
+ type Rewrite,
126
+ type RewriteFunction,
127
+ } from "./route-definition/rewrite.js";
128
+ export { RewriteLoopError } from "./errors.js";
129
+
122
130
  // Composition types for reusable callback factories
123
131
  export type {
124
132
  RouteUseItem,
package/src/index.ts CHANGED
@@ -155,6 +155,13 @@ export function redirect(): never {
155
155
  throw serverOnlyStubError("redirect");
156
156
  }
157
157
 
158
+ /**
159
+ * Error-throwing stub for server-only `rewrite` function.
160
+ */
161
+ export function rewrite(): never {
162
+ throw serverOnlyStubError("rewrite");
163
+ }
164
+
158
165
  // Handle API (universal - works on both server and client)
159
166
  export { createHandle, isHandle, type Handle } from "./handle.js";
160
167
  export {
@@ -36,6 +36,7 @@
36
36
  */
37
37
 
38
38
  import { unwrapsSynchronously } from "./thenable-status.js";
39
+ import { asStream } from "./browser/defer.js";
39
40
 
40
41
  /** The loader-segment shape announcePendingStreams reads (ResolvedSegment). */
41
42
  export interface PendingStreamSource {
@@ -538,14 +539,23 @@ export class LoaderStore {
538
539
  this.pendingStreams.set(loaderId, set);
539
540
  }
540
541
  set.add(stream);
541
- const release = () => {
542
- const current = this.pendingStreams.get(loaderId);
543
- if (!current || !current.delete(stream)) return;
544
- if (current.size === 0) this.pendingStreams.delete(loaderId);
545
- };
542
+ const release = () => this.releasePendingStream(loaderId, stream);
546
543
  stream.then(release, release);
547
544
  }
548
545
 
546
+ /**
547
+ * Stop tracking a stream. trackPendingStream does it as the stream settles;
548
+ * an abandoned fill's hole (browser/partial-update.ts releaseHoles) never
549
+ * settles, and
550
+ * left tracked it would pin the family's held readers on every later
551
+ * transition.
552
+ */
553
+ releasePendingStream(loaderId: string, stream: Promise<unknown>): void {
554
+ const current = this.pendingStreams.get(loaderId);
555
+ if (!current || !current.delete(stream)) return;
556
+ if (current.size === 0) this.pendingStreams.delete(loaderId);
557
+ }
558
+
549
559
  /** True while any navigation stream for `loaderId` is still pending. */
550
560
  isStreamPending(loaderId: string): boolean {
551
561
  return this.pendingStreams.has(loaderId);
@@ -566,12 +576,13 @@ export class LoaderStore {
566
576
  */
567
577
  announcePendingStreams(segments: readonly PendingStreamSource[]): void {
568
578
  for (const segment of segments) {
579
+ const data = asStream(segment.loaderData);
569
580
  if (
570
581
  segment.type === "loader" &&
571
582
  segment.loaderId &&
572
- segment.loaderData instanceof Promise
583
+ data instanceof Promise
573
584
  ) {
574
- this.trackPendingStream(segment.loaderId, segment.loaderData);
585
+ this.trackPendingStream(segment.loaderId, data);
575
586
  }
576
587
  }
577
588
  for (const loaderId of this.pendingStreams.keys()) {
@@ -0,0 +1,13 @@
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.
7
+ */
8
+
9
+ /** Request header of a `<Link>` prefetch (browser/prefetch/fetch.ts). */
10
+ export const PREFETCH_HEADER = "X-Rango-Prefetch";
11
+
12
+ /** Query param of a fill request (browser/navigation-client.ts). */
13
+ export const FILL_PARAM = "_rsc_fill";
@@ -60,3 +60,22 @@ export function isRedirectResponse(response: Response): boolean {
60
60
  Boolean(response.headers.get("Location"))
61
61
  );
62
62
  }
63
+
64
+ /**
65
+ * Set a header on a response the router hands to the host, skipping a
66
+ * WebSocket upgrade (its headers are not to be touched) and swallowing the
67
+ * TypeError of immutable headers (a passed-through platform Response):
68
+ * instrumentation and debug headers never fail a request.
69
+ */
70
+ export function trySetHeader(
71
+ response: Response,
72
+ name: string,
73
+ value: string,
74
+ ): void {
75
+ if (isWebSocketUpgradeResponse(response)) return;
76
+ try {
77
+ response.headers.set(name, value);
78
+ } catch {
79
+ // Immutable headers: drop the header, never the response.
80
+ }
81
+ }
package/src/reverse.ts CHANGED
@@ -149,6 +149,7 @@ export type ReverseFunction<TRoutes> = {
149
149
  export type ScopedReverseFunction<
150
150
  TLocalRoutes,
151
151
  TGlobalRoutes = TLocalRoutes,
152
+ TResult = string,
152
153
  > = {
153
154
  /**
154
155
  * Global route without params
@@ -159,7 +160,7 @@ export type ScopedReverseFunction<
159
160
  > extends true
160
161
  ? TName
161
162
  : never,
162
- ): string;
163
+ ): TResult;
163
164
 
164
165
  /**
165
166
  * Global route with params
@@ -167,7 +168,7 @@ export type ScopedReverseFunction<
167
168
  <TName extends keyof TGlobalRoutes & string>(
168
169
  name: TName,
169
170
  params: ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>,
170
- ): string;
171
+ ): TResult;
171
172
 
172
173
  /**
173
174
  * Global route with params and search
@@ -176,7 +177,7 @@ export type ScopedReverseFunction<
176
177
  name: TName,
177
178
  params: ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>,
178
179
  search: ResolveSearchSchema<ExtractSearchSchema<TGlobalRoutes, TName>>,
179
- ): string;
180
+ ): TResult;
180
181
 
181
182
  /**
182
183
  * Dot-prefixed local route without params
@@ -187,7 +188,7 @@ export type ScopedReverseFunction<
187
188
  > extends true
188
189
  ? `.${TName}`
189
190
  : never,
190
- ): string;
191
+ ): TResult;
191
192
 
192
193
  /**
193
194
  * Dot-prefixed local route with params
@@ -195,7 +196,7 @@ export type ScopedReverseFunction<
195
196
  <TName extends keyof TLocalRoutes & string>(
196
197
  name: `.${TName}`,
197
198
  params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>,
198
- ): string;
199
+ ): TResult;
199
200
 
200
201
  /**
201
202
  * Dot-prefixed local route with params and search
@@ -204,7 +205,7 @@ export type ScopedReverseFunction<
204
205
  name: `.${TName}`,
205
206
  params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>,
206
207
  search: ResolveSearchSchema<ExtractSearchSchema<TLocalRoutes, TName>>,
207
- ): string;
208
+ ): TResult;
208
209
  };
209
210
 
210
211
  /**
@@ -235,14 +236,16 @@ export type ScopedReverseFunction<
235
236
  * reverse(".post", { slug: "hello" }) // compile error: no include() scope
236
237
  * ```
237
238
  */
238
- export type GlobalReverseFunction<TGlobalRoutes> =
239
- string extends keyof TGlobalRoutes
240
- ? <TName extends string>(
241
- name: Exclude<TName, `.${string}`>,
242
- params?: Record<string, string>,
243
- search?: Record<string, unknown>,
244
- ) => string
245
- : ScopedReverseFunction<{}, TGlobalRoutes>;
239
+ export type GlobalReverseFunction<
240
+ TGlobalRoutes,
241
+ TResult = string,
242
+ > = string extends keyof TGlobalRoutes
243
+ ? <TName extends string>(
244
+ name: Exclude<TName, `.${string}`>,
245
+ params?: Record<string, string>,
246
+ search?: Record<string, unknown>,
247
+ ) => TResult
248
+ : ScopedReverseFunction<{}, TGlobalRoutes, TResult>;
246
249
 
247
250
  /**
248
251
  * Extract local routes type from UrlPatterns
@@ -10,12 +10,14 @@ import {
10
10
  LOADER_REDIRECT,
11
11
  } from "./decode-loader-results.js";
12
12
  import { LoaderRedirect } from "./loader-redirect.js";
13
+ import { Defer, readDefer } from "./browser/defer.js";
13
14
  import type { BoundaryAudit } from "./suspense-audit.js";
14
15
  import {
15
16
  AuditedFallback,
16
17
  useBoundaryAudit,
17
18
  useRevealAudit,
18
19
  } from "./suspense-audit-react.js";
20
+ import { SETTLED_THENABLE } from "./thenable-status.js";
19
21
 
20
22
  /**
21
23
  * Router-owned error boundary for read-site loader errors. segment-system
@@ -141,7 +143,7 @@ export function AuditedRouteContent({
141
143
  const audit = useBoundaryAudit(
142
144
  `content:${segmentId}`,
143
145
  segmentId ?? "",
144
- content,
146
+ content instanceof Defer ? content.promise : content,
145
147
  );
146
148
  return (
147
149
  <Suspense
@@ -174,8 +176,16 @@ const Suspender = ({
174
176
  // A pending promise suspends here and the wrapping Suspense shows the
175
177
  // loading() fallback; a node renders without suspending. The wrapper tree
176
178
  // (RouteContentWrapper > Suspense > Suspender) is identical either way (see
177
- // docs/tree-structure.md), like LoaderResolver's resolved-data branch.
178
- return content instanceof Promise ? use(content) : content;
179
+ // docs/tree-structure.md), like LoaderResolver's resolved-data branch. A
180
+ // `prefetch: false` hole is read here, by the read rule, so the filled
181
+ // page's node later takes the same place. The content can be a pending
182
+ // promise in one tree and its node in the next: use() at this place on
183
+ // every render, as LoaderResolver does.
184
+ return content instanceof Defer
185
+ ? (readDefer(content) as ReactNode)
186
+ : content instanceof Promise
187
+ ? use(content)
188
+ : (use(SETTLED_THENABLE), content);
179
189
  };
180
190
 
181
191
  /**
@@ -329,10 +339,14 @@ function LoaderResolver({
329
339
  );
330
340
  }
331
341
 
342
+ // A slot's aggregate can be a pending promise on one render and its arrived
343
+ // array on the next (segment-loader-promise.ts): use() at this place on
344
+ // every render, or React reports a component that suspended in use() and
345
+ // then finished without it (the useLoader pattern, use-loader.tsx).
332
346
  const resolvedData =
333
347
  loaderDataPromise instanceof Promise
334
348
  ? use(loaderDataPromise)
335
- : loaderDataPromise;
349
+ : (use(SETTLED_THENABLE), loaderDataPromise);
336
350
 
337
351
  const { loaderData, errorFallback } = decodeLoaderResults(
338
352
  resolvedData,
@@ -22,7 +22,7 @@ import {
22
22
  type InterceptConfig,
23
23
  type LoaderEntry,
24
24
  } from "../server/context";
25
- import { invariant } from "../errors";
25
+ import { assertBooleanOption, invariant } from "../errors";
26
26
  import { validateUserRouteName } from "../route-name.js";
27
27
  import { isCachedFunction } from "../cache/taint.js";
28
28
  import { RangoContext } from "../server/context";
@@ -44,6 +44,7 @@ import type {
44
44
  TransitionItem,
45
45
  UseItems,
46
46
  } from "../route-types.js";
47
+ import { isRenderableLoading } from "../types/boundaries.js";
47
48
  import type { RouteHelpers } from "./helpers-types.js";
48
49
  import {
49
50
  resolveHandlerUse,
@@ -899,9 +900,15 @@ const loader: RouteHelpers<any, any>["loader"] = (
899
900
  (optionsGiven as { stream?: unknown } | undefined)?.stream === undefined,
900
901
  "loader() stream was replaced: use loader(Def, { ssr: false }) — the same knob as loading(fallback, { ssr: false }) — to await the loader before first flush on document requests.",
901
902
  );
902
- invariant(
903
- optionsGiven?.ssr === undefined || typeof optionsGiven.ssr === "boolean",
904
- `loader() ssr must be a boolean (got ${JSON.stringify(optionsGiven?.ssr)}). Omit it (or pass true) to stream on every render.`,
903
+ assertBooleanOption(
904
+ optionsGiven?.ssr,
905
+ "loader() ssr",
906
+ "stream on every render",
907
+ );
908
+ assertBooleanOption(
909
+ optionsGiven?.prefetch,
910
+ "loader() prefetch",
911
+ "run the loader in prefetches",
905
912
  );
906
913
 
907
914
  const name = `${ctx.namespace}.$${store.getNextIndex("loader")}`;
@@ -917,6 +924,7 @@ const loader: RouteHelpers<any, any>["loader"] = (
917
924
  ...(optionsGiven?.ssr === false && { bake: true as const }),
918
925
  ...(optionsGiven?.ssr === false &&
919
926
  ctx.isSSR && { awaitBeforeFlush: true as const }),
927
+ ...(optionsGiven?.prefetch === false && { prefetch: false as const }),
920
928
  };
921
929
 
922
930
  // Merge handler.use defaults (attached to the loader definition) with explicit use
@@ -969,6 +977,12 @@ const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
969
977
  invariant(false, "No parent entry available for loading()");
970
978
  }
971
979
 
980
+ assertBooleanOption(
981
+ options?.prefetch,
982
+ "loading() prefetch",
983
+ "render what the fallback covers in prefetches",
984
+ );
985
+
972
986
  // Unwrap function form: loading(() => <Skeleton />) → loading(<Skeleton />)
973
987
  const resolved =
974
988
  typeof component === "function" ? (component as () => any)() : component;
@@ -980,6 +994,15 @@ const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
980
994
  parent.loading = resolved;
981
995
  }
982
996
 
997
+ // The flag means "what this fallback covers": without a fallback to show
998
+ // there is nothing to defer behind. Recorded from `resolved`, not
999
+ // `parent.loading`, so the document evaluation (ssr: false) agrees with
1000
+ // the navigation one. Last loading() wins, like `loading` itself.
1001
+ parent.loadingPrefetch =
1002
+ options?.prefetch === false && isRenderableLoading(resolved)
1003
+ ? false
1004
+ : undefined;
1005
+
983
1006
  const name = `$${store.getNextIndex("loading")}`;
984
1007
  return { name, type: "loading" } as LoadingItem;
985
1008
  };
@@ -328,6 +328,16 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
328
328
  * loader(RecommendationsLoader), // still streams behind loading()
329
329
  * ```
330
330
  *
331
+ * Pass `{ prefetch: false }` to keep the loader out of prefetches: a
332
+ * prefetch skips it and the click that adopts the prefetch fetches it in
333
+ * one follow-up request (see {@link LoaderOptions}). Only on a segment the
334
+ * client does not have yet: a loader the client already holds runs in the
335
+ * prefetch when it revalidates.
336
+ *
337
+ * ```typescript
338
+ * loader(ReviewsLoader, { prefetch: false }),
339
+ * ```
340
+ *
331
341
  * @param loaderDef - Loader created with createLoader()
332
342
  * @param optionsOrUse - Delivery options, or the use() callback when passing none
333
343
  * @param use - Optional callback for loader-specific revalidation rules
@@ -345,14 +355,30 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
345
355
  *
346
356
  * // Skip loading on SSR, only show on client navigation
347
357
  * loading(<Skeleton />, { ssr: false })
358
+ *
359
+ * // Keep everything behind this fallback out of prefetches
360
+ * loading(<Skeleton />, { prefetch: false })
348
361
  * ```
349
362
  * @param component - The loading UI to show during navigation
350
363
  * @param options - Configuration options
351
364
  * @param options.ssr - If false, skip showing loading on document requests (SSR)
365
+ * @param options.prefetch - If false, the flag applies when this entry's
366
+ * segment is new to the client. A prefetch then skips the whole unit: the
367
+ * handler, its loaders, its orphan layouts and parallel slots and, for a
368
+ * layout, every deeper entry; the click that adopts the prefetch shows
369
+ * this fallback and fetches them in one follow-up request, which reads
370
+ * cache() and "use cache" as any request does (a prefetch reads neither
371
+ * for skipped work). A segment the client already holds is never
372
+ * deferred: a prefetch renders it as usual when it revalidates, and what
373
+ * is below it follows its own flags. To keep a child out of prefetches
374
+ * while moving inside a section, flag the child's own loading(). An entry
375
+ * whose handler output is stored (cache(), ppr, Prerender/Static) is
376
+ * served as usual and only its loaders are skipped. Loaders behind the
377
+ * fallback cannot call ctx.rendered().
352
378
  */
353
379
  loading: (
354
380
  component: ReactNode | (() => ReactNode),
355
- options?: { ssr?: boolean },
381
+ options?: { ssr?: boolean; prefetch?: boolean },
356
382
  ) => LoadingItem;
357
383
  /**
358
384
  * Attach an error boundary to catch errors in this segment and children
@@ -8,6 +8,7 @@ import {
8
8
  attachRedirectState,
9
9
  markExternalRedirect,
10
10
  } from "../redirect-origin.js";
11
+ import { prefixBasename } from "../router/basename.js";
11
12
 
12
13
  /**
13
14
  * Create a soft redirect Response for middleware short-circuit
@@ -84,21 +85,7 @@ export function redirect(
84
85
  }
85
86
 
86
87
  // Auto-prefix root-relative URLs with basename for app-local redirects.
87
- // Treat the URL as already-prefixed when the basename is followed by a path
88
- // separator, a query, a fragment, or end-of-string, so "/admin?tab=x" and
89
- // "/admin#frag" are not double-prefixed into "/admin/admin?tab=x".
90
- const bn = _getRequestContext()?._basename;
91
- let resolvedUrl = url;
92
- if (
93
- bn &&
94
- url.startsWith("/") &&
95
- url !== bn &&
96
- !url.startsWith(bn + "/") &&
97
- !url.startsWith(bn + "?") &&
98
- !url.startsWith(bn + "#")
99
- ) {
100
- resolvedUrl = url === "/" ? bn : bn + url;
101
- }
88
+ const resolvedUrl = prefixBasename(url, _getRequestContext()?._basename);
102
89
 
103
90
  const headers: Record<string, string> = {
104
91
  Location: resolvedUrl,