@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,146 @@
1
+ /**
2
+ * `throw rewrite(target)`: middleware renders another route at the same URL.
3
+ *
4
+ * The value only: what middleware throws and how the router recognizes it.
5
+ * The pass loop that honors it lives in rsc/rewrite-passes.ts, the hop trail
6
+ * in server/rewrite-trail.ts.
7
+ */
8
+
9
+ import type { GlobalReverseFunction } from "../reverse.js";
10
+ import type { DefaultReverseRouteMap } from "../types/global-namespace.js";
11
+
12
+ /**
13
+ * Cross-realm brand: a Vite dev server can load this module twice (the RSC
14
+ * and the SSR graph), so `instanceof` alone misses a Rewrite from the other
15
+ * copy.
16
+ */
17
+ const REWRITE_BRAND: unique symbol = Symbol.for("rango.rewrite");
18
+
19
+ /** The target as written: a path, or a global route name with its params. */
20
+ export type RewriteTarget =
21
+ | { readonly path: string }
22
+ | {
23
+ readonly name: string;
24
+ readonly params?: Record<string, string>;
25
+ readonly search?: Record<string, unknown>;
26
+ };
27
+
28
+ const REWRITE_RULE =
29
+ "rewrite() is for middleware: throw it from router.use() or route " +
30
+ "middleware (middleware() in urls()), before await next(), while it " +
31
+ "handles a GET or HEAD page request. Loaders and route handlers can't " +
32
+ "rewrite: throw rewrite() from the route's middleware instead. Intercept " +
33
+ "middleware and loader fetches can't either. In a server action use " +
34
+ "redirect(); for a mutation return a Response from middleware.";
35
+
36
+ export function describeRewriteTarget(target: RewriteTarget): string {
37
+ return "path" in target
38
+ ? `rewrite(${JSON.stringify(target.path)})`
39
+ : `rewrite(${JSON.stringify(target.name)})`;
40
+ }
41
+
42
+ /**
43
+ * The value `rewrite()` returns, thrown by middleware. An Error, so a throw
44
+ * where a rewrite is not supported fails loudly with the rule in its message.
45
+ */
46
+ export class Rewrite extends Error {
47
+ name = "Rewrite" as const;
48
+ readonly target: RewriteTarget;
49
+
50
+ constructor(target: RewriteTarget) {
51
+ super(`${describeRewriteTarget(target)}: ${REWRITE_RULE}`);
52
+ Object.setPrototypeOf(this, Rewrite.prototype);
53
+ this.target = target;
54
+ }
55
+ }
56
+ Object.defineProperty(Rewrite.prototype, REWRITE_BRAND, { value: true });
57
+
58
+ /** Whether `value` is a Rewrite thrown by `rewrite()` (ctx.isRewrite). */
59
+ export function isRewrite(value: unknown): value is Rewrite {
60
+ return (
61
+ typeof value === "object" &&
62
+ value !== null &&
63
+ (value as { [REWRITE_BRAND]?: unknown })[REWRITE_BRAND] === true
64
+ );
65
+ }
66
+
67
+ /**
68
+ * `rewrite(path)` or `rewrite(name, params?, search?)`: a path in this app
69
+ * (basename-prefixed like `redirect()`, may carry `?search`), or a global
70
+ * route name with the params and search `ctx.reverse` takes.
71
+ */
72
+ export type RewriteFunction<TRoutes = DefaultReverseRouteMap> = ((
73
+ path: `/${string}`,
74
+ ) => Rewrite) &
75
+ GlobalReverseFunction<TRoutes, Rewrite>;
76
+
77
+ function createRewrite(
78
+ target: string,
79
+ params?: Record<string, string>,
80
+ search?: Record<string, unknown>,
81
+ ): Rewrite {
82
+ if (typeof target !== "string" || target === "") {
83
+ throw new TypeError(
84
+ 'rewrite() takes a path starting with "/" or a global route name.',
85
+ );
86
+ }
87
+ if (target.startsWith("/")) {
88
+ if (params !== undefined || search !== undefined) {
89
+ throw new TypeError(
90
+ `rewrite(${JSON.stringify(target)}) takes params only with a route name, not with a path.`,
91
+ );
92
+ }
93
+ return new Rewrite({ path: target });
94
+ }
95
+ if (target.startsWith(".")) {
96
+ throw new TypeError(
97
+ `rewrite(${JSON.stringify(target)}): a rewrite resolves global route names only (no include() scope).`,
98
+ );
99
+ }
100
+ return new Rewrite({ name: target, params, search });
101
+ }
102
+
103
+ /**
104
+ * Render another route at the same URL. Throw it from middleware, before
105
+ * `await next()`:
106
+ *
107
+ * ```ts
108
+ * router.use(async (ctx, next) => {
109
+ * if (!locked || ctx.pathname === "/maintenance") return next();
110
+ * throw rewrite("maintenance");
111
+ * });
112
+ * ```
113
+ *
114
+ * The request runs again as the target (global middleware, route
115
+ * middleware, render) with the same request context: values set before the
116
+ * throw stay readable. `ctx.url` is the target; `ctx.rewrittenFrom` is the
117
+ * visitor's URL. See the `/middleware` skill.
118
+ */
119
+ export const rewrite: RewriteFunction = createRewrite as RewriteFunction;
120
+
121
+ /** A dispatch site that cannot honor a rewrite. */
122
+ export type RewriteSite = "mutation" | "loader-fetch" | "intercept";
123
+
124
+ const SITE_MESSAGE: Record<RewriteSite, string> = {
125
+ mutation:
126
+ "middleware threw it while handling a non-GET request (a server action " +
127
+ "or a form post). Return a Response for mutations instead, e.g. " +
128
+ 'new Response("Maintenance", { status: 503 }).',
129
+ "loader-fetch":
130
+ "middleware threw it during a loader fetch (useFetchLoader, load(), " +
131
+ "useRefreshLoaders). Return a Response there.",
132
+ intercept:
133
+ "intercept middleware threw it. Rewrite from router.use() or the " +
134
+ "route's middleware(), or return a Response.",
135
+ };
136
+
137
+ /** The error a rewrite becomes at a site that cannot honor it. */
138
+ export function rewriteNotSupported(
139
+ rewrite: Rewrite,
140
+ site: RewriteSite,
141
+ ): Error {
142
+ return new Error(
143
+ `${describeRewriteTarget(rewrite.target)} is not supported here: ${SITE_MESSAGE[site]}`,
144
+ { cause: rewrite },
145
+ );
146
+ }
@@ -8,6 +8,11 @@
8
8
  * See docs/manifests.md for the full data flow.
9
9
  */
10
10
 
11
+ import {
12
+ nullPrototypeStaticChildren,
13
+ type TrieNode,
14
+ } from "./router/route-trie-builder.js";
15
+
11
16
  let globalRouteMap: Record<string, string> = {};
12
17
 
13
18
  let cachedManifest: Record<string, string> | null = null;
@@ -86,8 +91,7 @@ export function clearCachedManifest(): void {
86
91
  // precomputed entries so multi-router setups (e.g. site + admin via
87
92
  // createHostRouter()) don't see each other's routes.
88
93
  const perRouterManifestMap: Map<string, Record<string, string>> = new Map();
89
- const perRouterTrieMap: Map<string, import("./build/route-trie.js").TrieNode> =
90
- new Map();
94
+ const perRouterTrieMap: Map<string, TrieNode> = new Map();
91
95
  const perRouterPrecomputedEntriesMap: Map<
92
96
  string,
93
97
  Array<{ staticPrefix: string; routes: Record<string, string> }>
@@ -132,11 +136,12 @@ export function getRouterManifest(
132
136
  return perRouterManifestMap.get(routerId);
133
137
  }
134
138
 
135
- export function setRouterTrie(
136
- routerId: string,
137
- trie: import("./build/route-trie.js").TrieNode,
138
- ): void {
139
- perRouterTrieMap.set(routerId, trie);
139
+ /**
140
+ * Install a router's trie. A trie that arrives as JSON (the build manifest)
141
+ * or across the dev runner boundary gets null-prototype static maps first.
142
+ */
143
+ export function setRouterTrie(routerId: string, trie: TrieNode): void {
144
+ perRouterTrieMap.set(routerId, nullPrototypeStaticChildren(trie));
140
145
  }
141
146
 
142
147
  // Routers whose trie came from the COMPLETE build manifest (deserialized via
@@ -156,9 +161,7 @@ export function isRouterTrieAuthoritative(routerId: string): boolean {
156
161
  return authoritativeTrieRouters.has(routerId);
157
162
  }
158
163
 
159
- export function getRouterTrie(
160
- routerId: string,
161
- ): import("./build/route-trie.js").TrieNode | undefined {
164
+ export function getRouterTrie(routerId: string): TrieNode | undefined {
162
165
  return perRouterTrieMap.get(routerId);
163
166
  }
164
167
 
@@ -199,7 +202,7 @@ export async function ensureRouterManifest(routerId: string): Promise<void> {
199
202
  if (loader) {
200
203
  const mod = await loader();
201
204
  if (mod.trie) {
202
- perRouterTrieMap.set(routerId, mod.trie);
205
+ setRouterTrie(routerId, mod.trie);
203
206
  // A trie serialized into the build manifest comes from complete
204
207
  // discovery — misses are authoritative 404s (see find-match.ts).
205
208
  markRouterTrieAuthoritative(routerId);
@@ -12,3 +12,23 @@ export function normalizeBasename(basename?: string): string | undefined {
12
12
  const trimmed = basename.replace(/^\/+|\/+$/g, "");
13
13
  return trimmed ? "/" + trimmed : undefined;
14
14
  }
15
+
16
+ /**
17
+ * Prefix a root-relative path with the basename unless it already carries it
18
+ * (the basename followed by "/", "?", "#" or the end), so "/admin?tab=x" and
19
+ * "/admin#frag" under basename "/admin" are not prefixed twice. Shared by
20
+ * redirect(), rewrite() and useRouter().
21
+ */
22
+ export function prefixBasename(path: string, basename?: string): string {
23
+ if (
24
+ !basename ||
25
+ !path.startsWith("/") ||
26
+ path === basename ||
27
+ path.startsWith(basename + "/") ||
28
+ path.startsWith(basename + "?") ||
29
+ path.startsWith(basename + "#")
30
+ ) {
31
+ return path;
32
+ }
33
+ return path === "/" ? basename : basename + path;
34
+ }
@@ -30,6 +30,7 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
30
30
  import { PRERENDER_PASSTHROUGH } from "../prerender.js";
31
31
  import { substitutePatternParams } from "./substitute-pattern-params.js";
32
32
  import { fireAndForgetWaitUntil } from "../types/request-scope.js";
33
+ import { defineRewrittenFrom } from "../server/rewrite-trail.js";
33
34
 
34
35
  // Mutating Headers methods guarded so they throw inside "use cache" / cache()
35
36
  // scope. Module-level constant (read-only, .has() lookups) so it is allocated
@@ -87,19 +88,7 @@ export function guardHeaderWrites(
87
88
  });
88
89
  }
89
90
 
90
- /**
91
- * Strip internal _rsc* query params from a URL.
92
- * Returns a new URL with only user-facing params.
93
- */
94
- export function stripInternalParams(url: URL): URL {
95
- const clean = new URL(url);
96
- for (const key of [...clean.searchParams.keys()]) {
97
- if (key.startsWith("_rsc")) {
98
- clean.searchParams.delete(key);
99
- }
100
- }
101
- return clean;
102
- }
91
+ export { stripInternalParams } from "./internal-params.js";
103
92
 
104
93
  /**
105
94
  * Resolve route name with namespace prefix support.
@@ -362,6 +351,7 @@ export function createHandlerContext<TEnv>(
362
351
  (ctx as { [THEME_REQUEST_CONTEXT]?: unknown })[THEME_REQUEST_CONTEXT] =
363
352
  requestContext;
364
353
  Object.defineProperty(ctx, "theme", THEME_DESCRIPTOR);
354
+ defineRewrittenFrom(ctx, requestContext);
365
355
  // Brand with taint symbol so "use cache" excludes ctx from cache keys
366
356
  (ctx as any)[NOCACHE_SYMBOL] = true;
367
357
  return ctx;
@@ -262,13 +262,23 @@ export async function resolveInterceptEntry<TEnv>(
262
262
 
263
263
  latchPprHeaderScopeForInterceptTarget(interceptEntry);
264
264
 
265
+ // The intercept segment's id: the tag scope its handler and layout record in
266
+ // (#957, cache-tag.ts runInSegmentTagScope) and the owner of its handler's
267
+ // and loaders' handle pushes, pinned before the loader kickoffs as
268
+ // fresh.ts/revalidation.ts pin theirs (createLoaderExecutor captures it at
269
+ // kickoff; a handler's ctx.use(Handle) reads it when called).
270
+ const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
271
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
272
+ slotSegmentId;
273
+ const hasLoaders = interceptEntry.loader.length > 0;
274
+
265
275
  const loaderPromises: Promise<any>[] = [];
266
276
  const loaderIds: string[] = [];
267
277
 
268
278
  for (let i = 0; i < interceptEntry.loader.length; i++) {
269
279
  const loaderEntry = interceptEntry.loader[i];
270
280
  const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
271
- const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
281
+ const segmentId = `${slotSegmentId}D${i}.${loader.$$id}`;
272
282
 
273
283
  if (revalidationContext) {
274
284
  const {
@@ -282,8 +292,7 @@ export async function resolveInterceptEntry<TEnv>(
282
292
  stale,
283
293
  } = revalidationContext;
284
294
 
285
- const interceptSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
286
- if (clientSegmentIds.has(interceptSegmentId)) {
295
+ if (clientSegmentIds.has(slotSegmentId)) {
287
296
  const dummySegment: ResolvedSegment = {
288
297
  id: segmentId,
289
298
  namespace: `intercept:${interceptEntry.routeName}`,
@@ -331,8 +340,9 @@ export async function resolveInterceptEntry<TEnv>(
331
340
  deps.wrapLoaderPromise(
332
341
  // The DSL binding's funnel, as for route loaders: a loader's own
333
342
  // cache() applies here too.
334
- runInsideLoaderScope(() =>
335
- resolveLoaderData(loaderEntry, context, context.pathname),
343
+ runInsideLoaderScope(
344
+ () => resolveLoaderData(loaderEntry, context, context.pathname),
345
+ loader.$$id,
336
346
  ),
337
347
  parentEntry,
338
348
  segmentId,
@@ -344,9 +354,6 @@ export async function resolveInterceptEntry<TEnv>(
344
354
  );
345
355
  }
346
356
 
347
- // The intercept segment's id, and the tag scope its handler and layout
348
- // record in (#957, cache-tag.ts runInSegmentTagScope).
349
- const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
350
357
  let handlerResult: ReactNode;
351
358
  try {
352
359
  const handler = interceptEntry.handler;
@@ -398,7 +405,7 @@ export async function resolveInterceptEntry<TEnv>(
398
405
  const streamedComponent = (promise: Promise<unknown>): ReactNode =>
399
406
  trackStreamedHandler(deps, promise, {
400
407
  boundaryEntry: parentEntry,
401
- segmentId: `${parentEntry.shortCode}.${interceptEntry.slotName}`,
408
+ segmentId: slotSegmentId,
402
409
  segmentType: "parallel",
403
410
  context,
404
411
  routeKey: interceptEntry.routeName,
@@ -441,6 +448,13 @@ export async function resolveInterceptEntry<TEnv>(
441
448
  }
442
449
  }
443
450
 
451
+ // The aggregate lists the loaders that ran; empty, not absent, when the
452
+ // client holds the slot and all skipped. Absent replaces the client's
453
+ // aggregate, empty merges into it (merge-segment-loaders.ts).
454
+ if (hasLoaders && loaderPromises.length === 0) {
455
+ loaderDataPromise = [];
456
+ }
457
+
444
458
  const interceptSegment = {
445
459
  id: slotSegmentId,
446
460
  namespace: `intercept:${interceptEntry.routeName}`,
@@ -454,7 +468,7 @@ export async function resolveInterceptEntry<TEnv>(
454
468
  belongsToRoute,
455
469
  parallelName: `intercept:${interceptEntry.routeName}.${interceptEntry.slotName}`,
456
470
  loaderDataPromise,
457
- loaderIds: loaderIds.length > 0 ? loaderIds : undefined,
471
+ loaderIds: hasLoaders ? loaderIds : undefined,
458
472
  };
459
473
  segments.push(interceptSegment);
460
474
 
@@ -497,6 +511,10 @@ export async function resolveInterceptLoadersOnly<TEnv>(
497
511
 
498
512
  latchPprHeaderScopeForInterceptTarget(interceptEntry);
499
513
 
514
+ // The loaders' handle push owner, as in resolveInterceptEntry.
515
+ const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
516
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
517
+ slotSegmentId;
500
518
  const loaderPromises: Promise<any>[] = [];
501
519
  const loaderIds: string[] = [];
502
520
 
@@ -514,10 +532,9 @@ export async function resolveInterceptLoadersOnly<TEnv>(
514
532
  for (let i = 0; i < interceptEntry.loader.length; i++) {
515
533
  const loaderEntry = interceptEntry.loader[i];
516
534
  const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
517
- const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
535
+ const segmentId = `${slotSegmentId}D${i}.${loader.$$id}`;
518
536
 
519
- const interceptSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
520
- if (clientSegmentIds.has(interceptSegmentId)) {
537
+ if (clientSegmentIds.has(slotSegmentId)) {
521
538
  const dummySegment: ResolvedSegment = {
522
539
  id: segmentId,
523
540
  namespace: `intercept:${interceptEntry.routeName}`,
@@ -564,8 +581,9 @@ export async function resolveInterceptLoadersOnly<TEnv>(
564
581
  deps.wrapLoaderPromise(
565
582
  // The DSL binding's funnel, as for route loaders: a loader's own
566
583
  // cache() applies here too.
567
- runInsideLoaderScope(() =>
568
- resolveLoaderData(loaderEntry, context, context.pathname),
584
+ runInsideLoaderScope(
585
+ () => resolveLoaderData(loaderEntry, context, context.pathname),
586
+ loader.$$id,
569
587
  ),
570
588
  parentEntry,
571
589
  segmentId,
@@ -579,8 +597,9 @@ export async function resolveInterceptLoadersOnly<TEnv>(
579
597
  );
580
598
  }
581
599
 
600
+ // Every loader held: the empty aggregate (see resolveInterceptEntry).
582
601
  if (loaderPromises.length === 0) {
583
- return null;
602
+ return { loaderDataPromise: [], loaderIds };
584
603
  }
585
604
 
586
605
  // Match fresh-path semantics: only defer (no await) when loading is truthy.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Strip internal _rsc* query params from a URL.
3
+ * Returns a new URL with only user-facing params.
4
+ *
5
+ * A leaf, so request-scoped modules (server/rewrite-trail.ts) can strip
6
+ * without importing the handler context's graph.
7
+ */
8
+ export function stripInternalParams(url: URL): URL {
9
+ const clean = new URL(url);
10
+ for (const key of [...clean.searchParams.keys()]) {
11
+ if (key.startsWith("_rsc")) {
12
+ clean.searchParams.delete(key);
13
+ }
14
+ }
15
+ return clean;
16
+ }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Shared `isAction()` matcher for revalidate predicates (server + clientUrls).
2
+ * Shared `isAction()` matcher for revalidate() predicates (server) and
3
+ * transition({ when }) (browser).
3
4
  *
4
5
  * Action identity is `actionId`. The helper resolves an imported reference the
5
6
  * same way the action boundary derives that id (`$id ?? $$id`), so a
@@ -55,6 +55,13 @@ import {
55
55
  } from "../server/handle-store.js";
56
56
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
57
57
  import { _getRequestContext } from "../server/request-context.js";
58
+ import { defineRewrittenFrom } from "../server/rewrite-trail.js";
59
+ import {
60
+ firstUnitCandidate,
61
+ loaderOwner,
62
+ startDeferredLoader,
63
+ type PrefetchDeferral,
64
+ } from "./segment-resolution/prefetch-deferral.js";
58
65
  import {
59
66
  assertNonCacheableReadAllowed,
60
67
  isInsideLoaderScope,
@@ -372,6 +379,29 @@ function detectLoaderCycle(
372
379
  return dfs(to);
373
380
  }
374
381
 
382
+ const warnedRenderedBesideUnit = new Set<string>();
383
+
384
+ /**
385
+ * Dev only, once per loader: a loader that is not deferrable awaits
386
+ * ctx.rendered() in a tree where a prefetch can skip a handler. The prefetch
387
+ * rejects that call (see rendered() below); saying so on a document or a
388
+ * navigation shows it before production, where prefetch is automatic.
389
+ */
390
+ function warnRenderedBesideUnit(
391
+ deferral: PrefetchDeferral,
392
+ loaderId: string,
393
+ ): void {
394
+ if (warnedRenderedBesideUnit.has(loaderId)) return;
395
+ const unit = firstUnitCandidate(deferral);
396
+ if (!unit) return;
397
+ warnedRenderedBesideUnit.add(loaderId);
398
+ console.warn(
399
+ `[rango] loader "${loaderId}" awaits ctx.rendered() on a route where "${unit.shortCode}" (${unit.id}) has loading(fallback, { prefetch: false }). ` +
400
+ `A prefetch of this route skips that handler, so ctx.rendered() throws in the prefetch and this loader is an error on the click that adopts it. ` +
401
+ `Remove { prefetch: false } from that loading(), or stop calling ctx.rendered() in this loader.`,
402
+ );
403
+ }
404
+
375
405
  /**
376
406
  * Creates a memoizing loader executor with cycle detection.
377
407
  * Shared by setupLoaderAccess and setupLoaderAccessSilent; only the handle
@@ -397,7 +427,9 @@ function createLoaderExecutor<TEnv>(
397
427
  /**
398
428
  * A stale refresh's executor (setupLoaderAccess _runLoaderIsolated): its
399
429
  * runs' pushes are diverted from the page's store, so a run's end says
400
- * nothing about the page's replayed values (HandleStore.settleLoaderRun).
430
+ * nothing about the page's replayed values (HandleStore.settleLoaderRun),
431
+ * and it never starts a loader the page's prefetch deferred
432
+ * (startDeferredLoader): a refresh reads each loader fresh.
401
433
  */
402
434
  detached?: boolean;
403
435
  /**
@@ -472,7 +504,14 @@ function createLoaderExecutor<TEnv>(
472
504
  const bound =
473
505
  callerLoaderId !== null &&
474
506
  internal._loaderCacheOverrides?.get(loader.$$id);
475
- const memo = bound || loaderPromises.get(loader.$$id);
507
+ // A loader the prefetch deferred starts through its registration, cache()
508
+ // binding included, as a navigation's run of it does.
509
+ const memo =
510
+ bound ||
511
+ loaderPromises.get(loader.$$id) ||
512
+ (executorOptions?.detached
513
+ ? undefined
514
+ : startDeferredLoader(ctx, loader.$$id));
476
515
  if (memo) {
477
516
  if (recordTags) readValueTags(memo);
478
517
  // A "use cache" body reading it refuses an identity read the run made
@@ -511,8 +550,9 @@ function createLoaderExecutor<TEnv>(
511
550
  // resolution continues — by the time the async body pushes, it would be
512
551
  // stale. Attributing to the owning route/layout segment gives handle
513
552
  // pushes the same granularity handler pushes have (segmentOrder-correct
514
- // for breadcrumb accumulation).
515
- const owningSegmentId = internal._currentSegmentId;
553
+ // for breadcrumb accumulation). A loader a prefetch deferred and a reader
554
+ // starts anyway keeps the segment that registers it (loaderOwner).
555
+ const owningSegmentId = loaderOwner(ctx, loader.$$id);
516
556
 
517
557
  let renderedResolved = false;
518
558
  let renderedPromise: Promise<void> | null = null;
@@ -633,6 +673,32 @@ function createLoaderExecutor<TEnv>(
633
673
  );
634
674
  }
635
675
 
676
+ // prefetch: false (segment-resolution/prefetch-deferral.ts). A
677
+ // deferrable loader runs in a fill request, where its entry is held
678
+ // and its handler does not run: there is no render to wait for.
679
+ // Refused on every request kind, so it is seen on a document load.
680
+ const deferral = internal._prefetchDeferral;
681
+ if (deferral?.scope.loaderIds.has(currentLoaderId)) {
682
+ throw new Error(
683
+ `ctx.rendered() is not available to loader "${currentLoaderId}": it is deferred in prefetches ` +
684
+ `(registered with loader(Def, { prefetch: false }), or behind a loading(fallback, { prefetch: false })), ` +
685
+ `and the fill request that runs it afterwards does not re-run its entry's handler, so there is no render to wait for and no handle data to read. ` +
686
+ `Remove { prefetch: false } from the loader (or from the loading() above it), or stop calling ctx.rendered() in this loader.`,
687
+ );
688
+ }
689
+ if (deferral?.mode === "fill") {
690
+ throw new Error(
691
+ `ctx.rendered() is not available to loader "${currentLoaderId}" in a fill request: the handlers the client already holds do not run, so their handle data is missing.`,
692
+ );
693
+ }
694
+ if (
695
+ process.env.NODE_ENV !== "production" &&
696
+ deferral &&
697
+ !deferral.mode
698
+ ) {
699
+ warnRenderedBesideUnit(deferral, currentLoaderId);
700
+ }
701
+
636
702
  // awaitBeforeFlush cycle: segment resolution awaits this loader
637
703
  // (loader(Def, { ssr: false })), the barrier awaits segment
638
704
  // resolution, and rendered() awaits the barrier — waiting here can
@@ -684,6 +750,17 @@ function createLoaderExecutor<TEnv>(
684
750
  // keeps a handler from depending on this loader.
685
751
  const streaming = reqCtx._treeHasStreaming === true;
686
752
  renderedPromise = reqCtx._renderBarrier.then(async () => {
753
+ // A prefetch that skipped a handler unit has none of that handler's
754
+ // pushes, and the fill never re-runs this loader: an error, never
755
+ // a handle list with a hole in it. Known only once resolution has
756
+ // decided the unit, which is what the barrier waits for.
757
+ if (deferral?.mode === "prefetch" && deferral.deferredUnit) {
758
+ throw new Error(
759
+ `ctx.rendered() in loader "${currentLoaderId}" cannot settle in a prefetch of this route: segment "${deferral.deferredUnit}" has loading(fallback, { prefetch: false }), ` +
760
+ `so the prefetch skipped its handler and its handle data does not exist. ` +
761
+ `Remove { prefetch: false } from that loading(), or stop calling ctx.rendered() in this loader.`,
762
+ );
763
+ }
687
764
  if (streaming) {
688
765
  reqCtx._handleStore.seal();
689
766
  await reqCtx._handleStore.settled;
@@ -701,6 +778,7 @@ function createLoaderExecutor<TEnv>(
701
778
  return renderedPromise;
702
779
  },
703
780
  };
781
+ defineRewrittenFrom(loaderCtx, reqCtxRef ?? _getRequestContext());
704
782
  (loaderCtx as any)[NOCACHE_SYMBOL] = true;
705
783
 
706
784
  // Meter this loader once via observePhase (loader:<id> perf metric +
@@ -807,6 +885,9 @@ export function setupLoaderAccess<TEnv>(
807
885
  // pushes, and a later run here replaces them (HandleStore.pushReplayed).
808
886
  const claimed = new Set<string>();
809
887
  const internal = ctx as InternalHandlerContext<any, TEnv>;
888
+ internal._loaderStarted = (loaderId) =>
889
+ loaderPromises.has(loaderId) ||
890
+ internal._loaderCacheOverrides?.has(loaderId) === true;
810
891
  internal._claimLoaderPushes = (loaderId) => {
811
892
  if (loaderPromises.has(loaderId) || claimed.has(loaderId)) return false;
812
893
  claimed.add(loaderId);
@@ -32,7 +32,12 @@ import {
32
32
  getContext,
33
33
  type InterceptSelectorContext,
34
34
  } from "../server/context";
35
- import type { ErrorBoundaryHandler, ErrorInfo, MatchResult } from "../types";
35
+ import type {
36
+ ErrorBoundaryHandler,
37
+ ErrorInfo,
38
+ InternalHandlerContext,
39
+ MatchResult,
40
+ } from "../types";
36
41
  import type { ReactNode } from "react";
37
42
  import type { MatchContext } from "./match-context.js";
38
43
  import type { MatchApiDeps, ActionContext } from "./types.js";
@@ -49,6 +54,11 @@ import {
49
54
  type RouteSnapshot,
50
55
  } from "./route-snapshot.js";
51
56
  import { resolveNavigation } from "./navigation-snapshot.js";
57
+ import {
58
+ partialDeferralMode,
59
+ planPrefetchDeferral,
60
+ requestKind,
61
+ } from "./segment-resolution/prefetch-deferral.js";
52
62
 
53
63
  /**
54
64
  * Create match context for full requests (document/SSR).
@@ -174,6 +184,9 @@ export async function createMatchContextForFull<TEnv>(
174
184
  if (reqCtx?._shellCaptureRun) {
175
185
  reqCtx._shellCaptureLoaderLanes = routeLoaderLanes(snapshot.entries);
176
186
  }
187
+ // A document never defers; the plan feeds the ctx.rendered() guards.
188
+ (handlerContext as InternalHandlerContext<any, TEnv>)._prefetchDeferral =
189
+ planPrefetchDeferral(snapshot.entries, undefined, matched, cacheScope);
177
190
 
178
191
  return {
179
192
  request,
@@ -453,6 +466,28 @@ export async function createMatchContextForPartial<TEnv>(
453
466
  if (cacheScope) armRecordTagOwners();
454
467
  if (bindsLoaderCache(snapshot.entries)) armLoaderTagSets();
455
468
 
469
+ // `prefetch: false` (segment-resolution/prefetch-deferral.ts). With no
470
+ // request context (a match run on its own) the request is asked directly.
471
+ const kind = reqCtx ? reqCtx._requestKind : requestKind(request, rawUrl);
472
+ const deferral = planPrefetchDeferral(
473
+ snapshot.entries,
474
+ partialDeferralMode({
475
+ fill: kind === "fill",
476
+ prefetch: kind === "prefetch",
477
+ method: request.method,
478
+ isAction,
479
+ isIntercept,
480
+ isShellCapture: reqCtx?._shellCaptureRun === true,
481
+ }),
482
+ matched,
483
+ cacheScope,
484
+ // As the request listed them, not the copy forced above: see
485
+ // prefetch-deferral.ts.
486
+ nav.clientSegmentSet,
487
+ );
488
+ (handlerContext as InternalHandlerContext<any, TEnv>)._prefetchDeferral =
489
+ deferral;
490
+
456
491
  return {
457
492
  request,
458
493
  url: cleanUrl,