@rangojs/router 0.24.4 → 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 (267) 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-urls.d.ts +6 -2
  23. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  24. package/dist/types/client-urls/server-projection.d.ts +9 -5
  25. package/dist/types/client-urls/types.d.ts +29 -16
  26. package/dist/types/client.d.ts +2 -2
  27. package/dist/types/client.rsc.d.ts +2 -2
  28. package/dist/types/decode-loader-results.d.ts +28 -7
  29. package/dist/types/href-client.d.ts +3 -1
  30. package/dist/types/index.rsc.d.ts +1 -1
  31. package/dist/types/loader-redirect.d.ts +2 -3
  32. package/dist/types/outlet-context.d.ts +7 -6
  33. package/dist/types/prefetch-markers.d.ts +14 -5
  34. package/dist/types/route-content-wrapper.d.ts +17 -14
  35. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  36. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  37. package/dist/types/route-definition/rewrite.d.ts +48 -5
  38. package/dist/types/route-types.d.ts +2 -2
  39. package/dist/types/router/handler-context.d.ts +14 -2
  40. package/dist/types/router/intercept-resolution.d.ts +19 -46
  41. package/dist/types/router/lazy-includes.d.ts +7 -0
  42. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  43. package/dist/types/router/middleware.d.ts +0 -16
  44. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  45. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  46. package/dist/types/router/revalidation.d.ts +8 -1
  47. package/dist/types/router/router-context.d.ts +4 -16
  48. package/dist/types/router/router-interfaces.d.ts +0 -6
  49. package/dist/types/router/router-options.d.ts +4 -7
  50. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  51. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  52. package/dist/types/router/segment-resolution.d.ts +1 -1
  53. package/dist/types/router/segment-wrappers.d.ts +3 -5
  54. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  55. package/dist/types/router/tracing.d.ts +2 -2
  56. package/dist/types/rsc/full-payload.d.ts +14 -2
  57. package/dist/types/rsc/helpers.d.ts +8 -0
  58. package/dist/types/rsc/manifest-init.d.ts +20 -0
  59. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  60. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  61. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  62. package/dist/types/rsc/server-action.d.ts +6 -0
  63. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  64. package/dist/types/rsc/shell-capture.d.ts +7 -0
  65. package/dist/types/rsc/types.d.ts +5 -0
  66. package/dist/types/segment-system.d.ts +15 -0
  67. package/dist/types/server/context.d.ts +64 -4
  68. package/dist/types/server/request-context.d.ts +41 -4
  69. package/dist/types/server/request-headers.d.ts +12 -0
  70. package/dist/types/server/rewrite-trail.d.ts +8 -1
  71. package/dist/types/testing/e2e/index.d.ts +1 -1
  72. package/dist/types/testing/flight.entry.d.ts +1 -1
  73. package/dist/types/testing/index.d.ts +1 -1
  74. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  75. package/dist/types/testing/render-route.d.ts +21 -11
  76. package/dist/types/testing/run-loader.d.ts +7 -0
  77. package/dist/types/testing/run-middleware.d.ts +7 -0
  78. package/dist/types/testing/run-revalidate.d.ts +5 -4
  79. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  80. package/dist/types/testing/shell-status.d.ts +17 -0
  81. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  82. package/dist/types/testing/vitest.d.ts +10 -1
  83. package/dist/types/thenable-status.d.ts +0 -6
  84. package/dist/types/types/handler-context.d.ts +12 -1
  85. package/dist/types/types/loader-types.d.ts +4 -3
  86. package/dist/types/types/route-entry.d.ts +2 -1
  87. package/dist/types/urls/path-helper-types.d.ts +4 -2
  88. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  89. package/dist/types/vite/discovery/state.d.ts +20 -0
  90. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  91. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  92. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  93. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  94. package/dist/vite/index.js +6816 -6547
  95. package/package.json +1 -1
  96. package/skills/app-structure/SKILL.md +255 -0
  97. package/skills/app-structure/content-site.md +333 -0
  98. package/skills/app-structure/e-commerce.md +435 -0
  99. package/skills/app-structure/large-app.md +885 -0
  100. package/skills/app-structure/multi-app.md +280 -0
  101. package/skills/breadcrumbs/SKILL.md +6 -1
  102. package/skills/caching/SKILL.md +3 -1
  103. package/skills/catalog.json +12 -0
  104. package/skills/client-urls/SKILL.md +185 -32
  105. package/skills/composability/SKILL.md +10 -8
  106. package/skills/handler-use/SKILL.md +48 -9
  107. package/skills/hooks/handle-and-actions.md +104 -0
  108. package/skills/hooks/navigation.md +24 -10
  109. package/skills/hooks/urls.md +1 -1
  110. package/skills/i18n/SKILL.md +2 -1
  111. package/skills/intercept/SKILL.md +60 -29
  112. package/skills/layout/SKILL.md +6 -3
  113. package/skills/links/SKILL.md +151 -17
  114. package/skills/loader/SKILL.md +32 -10
  115. package/skills/middleware/SKILL.md +111 -15
  116. package/skills/observability/SKILL.md +11 -0
  117. package/skills/parallel/SKILL.md +5 -5
  118. package/skills/ppr/SKILL.md +43 -8
  119. package/skills/prerender/SKILL.md +1 -2
  120. package/skills/rango/SKILL.md +3 -0
  121. package/skills/response-routes/SKILL.md +4 -1
  122. package/skills/scripts/SKILL.md +12 -1
  123. package/skills/server-actions/SKILL.md +60 -20
  124. package/skills/testing/SKILL.md +1 -0
  125. package/skills/testing/client-components.md +13 -10
  126. package/skills/testing/loader.md +25 -24
  127. package/skills/testing/middleware.md +11 -11
  128. package/skills/testing/response-routes.md +4 -1
  129. package/skills/testing/serve-shell-request.md +85 -21
  130. package/skills/ux-architecture/SKILL.md +980 -0
  131. package/skills/view-transitions/SKILL.md +43 -10
  132. package/src/__augment-tests__/augmented.check.ts +28 -0
  133. package/src/browser/event-controller.ts +44 -18
  134. package/src/browser/intercept-utils.ts +3 -2
  135. package/src/browser/merge-segment-loaders.ts +0 -85
  136. package/src/browser/navigation-bridge.ts +112 -84
  137. package/src/browser/navigation-client.ts +20 -15
  138. package/src/browser/navigation-store.ts +4 -2
  139. package/src/browser/navigation-transaction.ts +9 -8
  140. package/src/browser/partial-update.ts +90 -32
  141. package/src/browser/prefetch/cache.ts +8 -0
  142. package/src/browser/prefetch/fetch.ts +13 -3
  143. package/src/browser/rango-marker.ts +13 -0
  144. package/src/browser/react/NavigationProvider.tsx +25 -7
  145. package/src/browser/react/filter-segment-order.ts +13 -0
  146. package/src/browser/react/use-action.ts +9 -15
  147. package/src/browser/react/use-handle.ts +63 -71
  148. package/src/browser/react/use-href.tsx +24 -5
  149. package/src/browser/react/use-link-status.ts +1 -1
  150. package/src/browser/react/use-navigation.ts +30 -40
  151. package/src/browser/react/use-router.ts +37 -6
  152. package/src/browser/react/use-segments.ts +8 -12
  153. package/src/browser/response-adapter.ts +11 -5
  154. package/src/browser/rsc-router.tsx +3 -1
  155. package/src/browser/scroll-restoration.ts +34 -20
  156. package/src/browser/segment-reconciler.ts +11 -29
  157. package/src/browser/server-action-bridge.ts +52 -16
  158. package/src/browser/types.ts +40 -8
  159. package/src/build/generate-manifest.ts +12 -1
  160. package/src/build/route-types/codegen.ts +6 -2
  161. package/src/build/route-types/per-module-writer.ts +94 -61
  162. package/src/cache/cache-runtime.ts +9 -0
  163. package/src/cache/cache-scope.ts +2 -1
  164. package/src/cache/cf/cf-cache-store.ts +2 -0
  165. package/src/cache/cf/cf-shell-frame.ts +3 -0
  166. package/src/cache/types.ts +8 -0
  167. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  168. package/src/client-urls/client-urls.ts +202 -153
  169. package/src/client-urls/revalidate-registry.ts +12 -3
  170. package/src/client-urls/server-projection.ts +137 -66
  171. package/src/client-urls/types.ts +30 -15
  172. package/src/client.rsc.tsx +11 -1
  173. package/src/client.tsx +9 -1
  174. package/src/decode-loader-results.ts +64 -31
  175. package/src/href-client.ts +3 -1
  176. package/src/index.rsc.ts +1 -0
  177. package/src/index.ts +12 -4
  178. package/src/loader-redirect.tsx +2 -3
  179. package/src/outlet-context.ts +7 -6
  180. package/src/prefetch-markers.ts +15 -5
  181. package/src/reverse.ts +3 -3
  182. package/src/route-content-wrapper.tsx +19 -17
  183. package/src/route-definition/dsl-helpers.ts +27 -7
  184. package/src/route-definition/helpers-types.ts +7 -2
  185. package/src/route-definition/resolve-handler-use.ts +23 -10
  186. package/src/route-definition/rewrite.ts +86 -19
  187. package/src/route-types.ts +1 -2
  188. package/src/router/find-match.ts +37 -1
  189. package/src/router/handler-context.ts +85 -40
  190. package/src/router/intercept-resolution.ts +161 -307
  191. package/src/router/lazy-includes.ts +43 -27
  192. package/src/router/loader-resolution.ts +21 -1
  193. package/src/router/manifest.ts +6 -8
  194. package/src/router/match-api.ts +11 -1
  195. package/src/router/match-middleware/background-revalidation.ts +2 -4
  196. package/src/router/match-middleware/cache-lookup.ts +16 -20
  197. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  198. package/src/router/match-result.ts +31 -2
  199. package/src/router/middleware.ts +6 -147
  200. package/src/router/navigation-snapshot.ts +38 -2
  201. package/src/router/prefetch-cache-ttl.ts +10 -18
  202. package/src/router/prerender-match.ts +2 -1
  203. package/src/router/revalidation.ts +24 -3
  204. package/src/router/router-context.ts +5 -18
  205. package/src/router/router-interfaces.ts +0 -7
  206. package/src/router/router-options.ts +4 -7
  207. package/src/router/segment-resolution/fresh.ts +6 -2
  208. package/src/router/segment-resolution/helpers.ts +7 -1
  209. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  210. package/src/router/segment-resolution/revalidation.ts +75 -35
  211. package/src/router/segment-resolution.ts +0 -1
  212. package/src/router/segment-wrappers.ts +8 -13
  213. package/src/router/substitute-pattern-params.ts +3 -2
  214. package/src/router/tracing.ts +2 -2
  215. package/src/router.ts +2 -9
  216. package/src/rsc/full-payload.ts +26 -4
  217. package/src/rsc/handler.ts +83 -41
  218. package/src/rsc/helpers.ts +13 -0
  219. package/src/rsc/manifest-init.ts +56 -3
  220. package/src/rsc/prefetch-render-error.ts +38 -0
  221. package/src/rsc/progressive-enhancement.ts +161 -211
  222. package/src/rsc/rewrite-passes.ts +56 -9
  223. package/src/rsc/rsc-rendering.ts +79 -23
  224. package/src/rsc/server-action.ts +25 -2
  225. package/src/rsc/shell-capture-constants.ts +22 -0
  226. package/src/rsc/shell-capture.ts +11 -0
  227. package/src/rsc/types.ts +5 -0
  228. package/src/segment-system.tsx +125 -119
  229. package/src/server/context.ts +96 -5
  230. package/src/server/handle-store.ts +3 -2
  231. package/src/server/request-context.ts +42 -3
  232. package/src/server/request-headers.ts +32 -0
  233. package/src/server/rewrite-trail.ts +55 -9
  234. package/src/ssr/ssr-root.tsx +6 -10
  235. package/src/testing/dispatch.ts +4 -5
  236. package/src/testing/e2e/index.ts +1 -0
  237. package/src/testing/flight.entry.ts +1 -0
  238. package/src/testing/index.ts +1 -0
  239. package/src/testing/internal/client-urls-projection.ts +31 -11
  240. package/src/testing/render-route.tsx +83 -40
  241. package/src/testing/run-loader.ts +26 -2
  242. package/src/testing/run-middleware.ts +10 -0
  243. package/src/testing/run-revalidate.ts +13 -6
  244. package/src/testing/serve-shell-request.ts +77 -4
  245. package/src/testing/shell-status.ts +40 -0
  246. package/src/testing/vitest-stubs/version.ts +1 -0
  247. package/src/testing/vitest.ts +74 -7
  248. package/src/thenable-status.ts +0 -11
  249. package/src/types/handler-context.ts +12 -1
  250. package/src/types/loader-types.ts +4 -3
  251. package/src/types/route-entry.ts +2 -1
  252. package/src/urls/include-helper.ts +38 -11
  253. package/src/urls/path-helper-types.ts +4 -2
  254. package/src/vite/discovery/client-urls-projection.ts +205 -16
  255. package/src/vite/discovery/discover-routers.ts +2 -2
  256. package/src/vite/discovery/prerender-collection.ts +2 -6
  257. package/src/vite/discovery/state.ts +26 -0
  258. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  259. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  260. package/src/vite/plugins/hoist/source.ts +8 -7
  261. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  262. package/src/vite/plugins/version-plugin.ts +7 -0
  263. package/src/vite/plugins/version.d.ts +7 -0
  264. package/src/vite/plugins/virtual-entries.ts +20 -0
  265. package/src/vite/rango.ts +4 -1
  266. package/src/vite/router-discovery.ts +39 -7
  267. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -39,6 +39,7 @@ import {
39
39
  type RequestContext,
40
40
  } from "../server/request-context.js";
41
41
  import { createReverseFunction } from "../router/handler-context.js";
42
+ import { scopeReverse, type ReverseScope } from "../server/context.js";
42
43
  import { NOCACHE_SYMBOL } from "../cache/taint.js";
43
44
  import { defineRewrittenFrom } from "../server/rewrite-trail.js";
44
45
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
@@ -134,6 +135,13 @@ export interface RunLoaderOptions<TEnv = any> {
134
135
  routeMap?: Record<string, string>;
135
136
  /** Matched route name for scoped `.name` reverse resolution. */
136
137
  routeName?: string;
138
+ /**
139
+ * Include name prefix of the module whose `layout()` / `parallel()` /
140
+ * `cache()` declares the loader (`"account"`, or `""` for the root module).
141
+ * `ctx.reverse(".name")` then resolves in that module, as such a loader's
142
+ * does in production, instead of from `routeName`.
143
+ */
144
+ declaredIn?: string;
137
145
  /** HTTP method surfaced as `ctx.method`. Defaults to "GET". */
138
146
  method?: string;
139
147
  /** Request body surfaced as `ctx.body`. */
@@ -279,9 +287,21 @@ function runWithLoaderContext<R>(
279
287
  const loaderSeeds = new Map<unknown, unknown>(opts.loaders ?? []);
280
288
  let renderedResolved = false;
281
289
 
290
+ // A layout's / parallel's loader: `.name` resolves in its declaring module.
291
+ const reverseScope: ReverseScope | undefined =
292
+ opts.declaredIn === undefined
293
+ ? undefined
294
+ : { prefix: opts.declaredIn || undefined, rootScoped: !opts.declaredIn };
282
295
  return runWithRequestContext(reqCtx, () => {
283
296
  const reverse = opts.routeMap
284
- ? createReverseFunction(opts.routeMap, opts.routeName, opts.params ?? {})
297
+ ? scopeReverse(
298
+ createReverseFunction(
299
+ opts.routeMap,
300
+ opts.routeName,
301
+ opts.params ?? {},
302
+ ),
303
+ reverseScope,
304
+ )
285
305
  : ((() => {
286
306
  throw new Error(
287
307
  "ctx.reverse() requires the `routeMap` option in runLoader(). " +
@@ -289,7 +309,10 @@ function runWithLoaderContext<R>(
289
309
  );
290
310
  }) as TestLoaderContext["reverse"]);
291
311
 
292
- const loaderCtx: TestLoaderContext & { _routeName?: string } = {
312
+ const loaderCtx: TestLoaderContext & {
313
+ _routeName?: string;
314
+ _reverseScope?: ReverseScope;
315
+ } = {
293
316
  params: opts.params ?? {},
294
317
  routeParams: (opts.params ?? {}) as Record<string, string>,
295
318
  request: reqCtx.request,
@@ -346,6 +369,7 @@ function runWithLoaderContext<R>(
346
369
  formData: opts.formData,
347
370
  reverse: reverse as TestLoaderContext["reverse"],
348
371
  _routeName: reqCtx._routeName,
372
+ _reverseScope: reverseScope,
349
373
  rendered:
350
374
  opts.rendered !== undefined && opts.rendered !== false
351
375
  ? async () => {
@@ -106,6 +106,13 @@ export interface RunMiddlewareRewrite {
106
106
  pathname: string;
107
107
  search: string;
108
108
  routeName?: string;
109
+ /**
110
+ * Present, `true`, only when the rewrite was thrown with
111
+ * `.disallowMutations()`: production refuses it for a non-GET, non-HEAD
112
+ * request in global middleware, before the action runs, instead of moving
113
+ * the mutation to the target.
114
+ */
115
+ mutationsDisallowed?: true;
109
116
  }
110
117
 
111
118
  /**
@@ -240,6 +247,9 @@ export async function runMiddleware<TEnv = any>(
240
247
  pathname: target.pathname,
241
248
  search: target.search,
242
249
  ...("name" in error.target && { routeName: error.target.name }),
250
+ ...(error.mutationsDisallowed && {
251
+ mutationsDisallowed: true as const,
252
+ }),
243
253
  };
244
254
  return new Response(null, {
245
255
  status: ctx.res.status,
@@ -10,7 +10,10 @@
10
10
  * a re-implementation. Pass an array to test a chain in declaration order.
11
11
  */
12
12
 
13
- import { evaluateRevalidation } from "../router/revalidation.js";
13
+ import {
14
+ evaluateRevalidation,
15
+ toShouldRevalidateFn,
16
+ } from "../router/revalidation.js";
14
17
  import { runWithRequestContext } from "../server/request-context.js";
15
18
  import type { ResolvedSegment, ShouldRevalidateFn } from "../types.js";
16
19
  import {
@@ -81,12 +84,16 @@ export interface RunRevalidateOptions<TEnv = any> {
81
84
  const DEFAULT_URL = "http://localhost/";
82
85
 
83
86
  /**
84
- * Run one revalidate() predicate, or a chain in declaration order, the way
85
- * the router does for a request. Resolves to the final decision: true
86
- * re-runs the segment, false keeps the client's copy.
87
+ * Run one revalidate() argument, or a chain in declaration order, the way
88
+ * the router does for a request: a predicate, or `true` / `false` as
89
+ * `revalidate()` takes them. Resolves to the final decision: true re-runs
90
+ * the segment, false keeps the client's copy.
87
91
  */
88
92
  export async function runRevalidate<TEnv = any>(
89
- fn: ShouldRevalidateFn<any, TEnv> | readonly ShouldRevalidateFn<any, TEnv>[],
93
+ fn:
94
+ | ShouldRevalidateFn<any, TEnv>
95
+ | boolean
96
+ | readonly (ShouldRevalidateFn<any, TEnv> | boolean)[],
90
97
  opts: RunRevalidateOptions<TEnv> = {},
91
98
  ): Promise<boolean> {
92
99
  const currentUrl = toURL(opts.currentUrl, new URL(DEFAULT_URL));
@@ -124,7 +131,7 @@ export async function runRevalidate<TEnv = any>(
124
131
  nextUrl,
125
132
  revalidations: fns.map((entry, index) => ({
126
133
  name: `runRevalidate#${index}`,
127
- fn: entry,
134
+ fn: toShouldRevalidateFn(entry),
128
135
  })),
129
136
  routeKey: opts.routeName ?? "",
130
137
  context: (opts.context ?? ctx) as RevalidateArgs["context"],
@@ -62,6 +62,8 @@ import { FILL_PARAM, PREFETCH_HEADER } from "../prefetch-markers.js";
62
62
  import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
63
63
  import { withoutPushedBy } from "../handles/pushed-by.js";
64
64
  import type { HandleData } from "../server/handle-store.js";
65
+ import type { ResolvedSegment } from "../types.js";
66
+ import { revalidationErrorOf } from "../decode-loader-results.js";
65
67
  import { _getRequestContext } from "../server/request-context.js";
66
68
  import {
67
69
  parsePprReplayStatus,
@@ -97,15 +99,22 @@ export interface ServeShellRequestOptions<TEnv = any> {
97
99
  * entries are deferred: `result.readDeferred()` lists them. `fill: true`
98
100
  * sends the request the browser follows an adopted prefetch with
99
101
  * (`_rsc_fill`): pass the ids the prefetch delivered, without the deferred
100
- * ones, as `segments`, and the same URL as `from`.
102
+ * ones, as `segments`, and the same URL as `from`. `interceptSource` sends
103
+ * the intercept source (`X-RSC-Router-Intercept-Source`) the browser sends
104
+ * from an open modal and with the fill of an intercept, whose `from` is
105
+ * that source. `rewriteTarget` sends the `metadata.rewriteTarget` the page at
106
+ * `from` carried when middleware rewrote it (`_rsc_rt`), as the browser
107
+ * does: the server then diffs against the route that rendered that page.
101
108
  */
102
109
  partial?:
103
110
  | true
104
111
  | {
105
112
  from?: string;
113
+ rewriteTarget?: string;
106
114
  segments?: readonly string[];
107
115
  prefetch?: boolean;
108
116
  fill?: boolean;
117
+ interceptSource?: string;
109
118
  };
110
119
  /**
111
120
  * POST this form as a submit before hydration: the no-JS request the
@@ -139,6 +148,8 @@ export interface ServeShellRequestAction {
139
148
  args?: readonly unknown[];
140
149
  /** The segment ids the page holds (`_rsc_segments`). */
141
150
  segments?: readonly string[];
151
+ /** The page's payload `metadata.rewriteTarget`, as for `partial`. */
152
+ rewriteTarget?: string;
142
153
  }
143
154
 
144
155
  /** A response's handle data, as the browser consumes it. */
@@ -157,6 +168,25 @@ export interface ShellRequestHandles {
157
168
  late: HandleData[];
158
169
  }
159
170
 
171
+ /**
172
+ * An `action` request's answer, as browser/server-action-bridge.ts reads it.
173
+ */
174
+ export interface ShellRequestAction {
175
+ /** False when the action threw: the call rejects with `result`. */
176
+ ok: boolean;
177
+ /** What the call resolves (or rejects) with. */
178
+ result: unknown;
179
+ /**
180
+ * The error of the first loader that threw while it re-ran in the action's
181
+ * revalidation, the value `useAction(action).revalidationError` reports.
182
+ * That loader throws at its `useLoader` read, where the nearest boundary
183
+ * catches it, and the call still resolves. `null` when every loader
184
+ * answered (a `notFound()` or `redirect()` is not an error), or `ok` is
185
+ * false, as `useAction` reports it.
186
+ */
187
+ revalidationError: Error | null;
188
+ }
189
+
160
190
  /** Result of {@link serveShellRequest}. */
161
191
  export interface ServeShellRequestResult {
162
192
  /** The handler's Response. Its body is already read: see `body`. */
@@ -182,9 +212,12 @@ export interface ServeShellRequestResult {
182
212
  * The shell key production resolved for this request: for a document the
183
213
  * serve path read and captured (a MISS or a HIT), the key it used, request
184
214
  * partition included (a route partitioned by `cache({ key })` or the
185
- * store's `keyGenerator`). Otherwise (a route without `ppr`, a request
186
- * passed to axis 1 before its key was resolved, a partial request) the
187
- * document URL's key without a partition, as `shellCacheKey` builds it.
215
+ * store's `keyGenerator`), and for a request middleware rewrote with
216
+ * `rewrite(...).shell("visitor")` the visitor's key, as
217
+ * `visitorShellCacheKey` builds it. Otherwise (a route without `ppr`, a
218
+ * request passed to axis 1 before its key was resolved, a rewritten
219
+ * request whose rewrite chose no shell, a partial request) the document
220
+ * URL's key without a partition, as `shellCacheKey` builds it.
188
221
  */
189
222
  key: string;
190
223
  /**
@@ -217,6 +250,13 @@ export interface ServeShellRequestResult {
217
250
  * hydrates without it.
218
251
  */
219
252
  readPayloadFormState(): Promise<unknown>;
253
+ /**
254
+ * For an `action` request, the call's answer as the browser reads it: its
255
+ * result and the `revalidationError` its loaders' reruns report.
256
+ * Undefined when no Flight was rendered or the payload carries no action
257
+ * result.
258
+ */
259
+ readAction(): Promise<ShellRequestAction | undefined>;
220
260
  }
221
261
 
222
262
  type ShellHandler = ReturnType<typeof createRSCHandler>;
@@ -352,6 +392,24 @@ async function decodePayloadHandles(
352
392
  };
353
393
  }
354
394
 
395
+ /** An action payload's answer, read as the browser's action bridge reads it. */
396
+ async function decodeAction(
397
+ flight: string,
398
+ ): Promise<ShellRequestAction | undefined> {
399
+ const { returnValue, metadata } = await decodePayload<{
400
+ returnValue?: { ok: boolean; data: unknown };
401
+ metadata?: { segments?: ResolvedSegment[] };
402
+ }>(flight);
403
+ if (!returnValue) return undefined;
404
+ return {
405
+ ok: returnValue.ok,
406
+ result: returnValue.data,
407
+ revalidationError:
408
+ (returnValue.ok && (await revalidationErrorOf(metadata?.segments))) ||
409
+ null,
410
+ };
411
+ }
412
+
355
413
  /** The ids of the segments a payload marks deferred. */
356
414
  async function decodeDeferredIds(flight: string): Promise<string[]> {
357
415
  const { metadata } = await decodePayload<{
@@ -522,6 +580,9 @@ async function buildActionRequest(
522
580
  const target = new URL(url);
523
581
  target.searchParams.set("_rsc_action", action.id);
524
582
  target.searchParams.set("_rsc_segments", (action.segments ?? []).join(","));
583
+ if (action.rewriteTarget) {
584
+ target.searchParams.set("_rsc_rt", action.rewriteTarget);
585
+ }
525
586
  headers.set("rsc-action", action.id);
526
587
  if (!headers.has("X-RSC-Router-Client-Path")) {
527
588
  headers.set("X-RSC-Router-Client-Path", url.href);
@@ -565,13 +626,16 @@ async function buildRequest(
565
626
  }
566
627
  const {
567
628
  from = "/",
629
+ rewriteTarget,
568
630
  segments = [],
569
631
  prefetch = false,
570
632
  fill = false,
633
+ interceptSource,
571
634
  } = partial === true ? {} : partial;
572
635
  const target = new URL(url);
573
636
  target.searchParams.set("_rsc_partial", "true");
574
637
  target.searchParams.set("_rsc_segments", segments.join(","));
638
+ if (rewriteTarget) target.searchParams.set("_rsc_rt", rewriteTarget);
575
639
  // As the browser sends them: browser/navigation-client.ts (fill) and
576
640
  // browser/prefetch/fetch.ts (prefetch).
577
641
  if (fill) target.searchParams.set(FILL_PARAM, "1");
@@ -581,6 +645,12 @@ async function buildRequest(
581
645
  if (!headers.has("X-RSC-Router-Client-Path")) {
582
646
  headers.set("X-RSC-Router-Client-Path", new URL(from, url).href);
583
647
  }
648
+ if (interceptSource !== undefined) {
649
+ headers.set(
650
+ "X-RSC-Router-Intercept-Source",
651
+ new URL(interceptSource, url).href,
652
+ );
653
+ }
584
654
  if (!headers.has(SEGMENT_FRAGMENT_CAPABILITY_HEADER)) {
585
655
  headers.set(SEGMENT_FRAGMENT_CAPABILITY_HEADER, "1");
586
656
  }
@@ -725,5 +795,8 @@ export async function serveShellRequest<TEnv = any>(
725
795
  }>(flight);
726
796
  return payload.formState;
727
797
  },
798
+ async readAction() {
799
+ return flight ? decodeAction(flight) : undefined;
800
+ },
728
801
  };
729
802
  }
@@ -39,6 +39,7 @@ import {
39
39
  SHELL_STATUS_HEADER,
40
40
  buildShellKey,
41
41
  partitionShellKey,
42
+ visitorShellKey,
42
43
  type PprReplayBypassReason,
43
44
  } from "../rsc/shell-capture-constants.js";
44
45
  import {
@@ -119,6 +120,45 @@ export function shellCacheKey(
119
120
  resolved,
120
121
  compileSearchParamsFilter(searchParams),
121
122
  );
123
+ return withPartition(key, partition);
124
+ }
125
+
126
+ /**
127
+ * Shell store key of a request middleware rewrote with
128
+ * `throw rewrite(...).shell("visitor")`: the visitor's `url`, partitioned by
129
+ * the rewrite's `target` (its path and search), as production builds it
130
+ * (`visitorShellKey`, `rsc/shell-capture-constants.ts`). `searchParams` is
131
+ * the target route's `cache.searchParams` config, applied to the visitor's
132
+ * search; `partition` is the target route's own, as for
133
+ * {@link shellCacheKey}, composed after the target's.
134
+ *
135
+ * ```ts
136
+ * const hit = await serveShellRequest(router, "/summer", { cacheStore });
137
+ * expect(hit.key).toBe(visitorShellCacheKey(router, "/summer", "/item/1"));
138
+ * ```
139
+ */
140
+ export function visitorShellCacheKey(
141
+ router: { readonly id: string },
142
+ url: URL | string,
143
+ target: URL | string,
144
+ searchParams?: CacheSearchParams,
145
+ partition?: string | readonly string[] | ShellCachePartition,
146
+ ): string {
147
+ const visitor =
148
+ typeof url === "string" ? new URL(url, "http://localhost") : url;
149
+ const key = visitorShellKey(
150
+ router.id,
151
+ visitor,
152
+ new URL(target, visitor),
153
+ compileSearchParamsFilter(searchParams),
154
+ );
155
+ return withPartition(key, partition);
156
+ }
157
+
158
+ function withPartition(
159
+ key: string,
160
+ partition: string | readonly string[] | ShellCachePartition | undefined,
161
+ ): string {
122
162
  if (partition === undefined) return key;
123
163
  const { keys = [], generated = [] }: ShellCachePartition =
124
164
  typeof partition === "string"
@@ -6,3 +6,4 @@
6
6
  // for a test that needs per-router versions.
7
7
  export const VERSION = "";
8
8
  export const ROUTER_VERSIONS: undefined = undefined;
9
+ export const PACKAGE_VERSION = "0.0.0-test";
@@ -76,8 +76,10 @@
76
76
  * a focused include, or use e2e.
77
77
  */
78
78
 
79
+ import { readFile } from "node:fs/promises";
79
80
  import { createRequire } from "node:module";
80
81
  import { fileURLToPath, pathToFileURL } from "node:url";
82
+ import { parseLang } from "../vite/plugins/hoist/literal.js";
81
83
  import {
82
84
  isUseCacheCandidate,
83
85
  transformUseCache,
@@ -156,6 +158,41 @@ const CLIENT_URLS_INSTALLER_PATH: string = here(
156
158
  "src/testing/internal/client-urls-projection.ts",
157
159
  ).replaceAll("\\", "/");
158
160
 
161
+ /**
162
+ * Whether a clientUrls() source's import loads as source too: a
163
+ * `"use client"` module, or a barrel (imports and `export ... from` only).
164
+ * Vite loads lazily, as in the transform: this module must load where it is
165
+ * not installed (installed-vitest-config.test.ts).
166
+ */
167
+ async function loadsAsSource(code: string, file: string): Promise<boolean> {
168
+ if (!code.includes("use client") && !code.includes("export")) return false;
169
+ const { parseAstAsync } = await import("vite");
170
+ const { hasDirective } = await import("@vitejs/plugin-rsc/transforms");
171
+ type Body = Parameters<typeof hasDirective>[0];
172
+ let body: Body;
173
+ try {
174
+ // The file as it is on disk, TypeScript included.
175
+ body = (
176
+ (await parseAstAsync(code, { lang: parseLang(file) })) as unknown as {
177
+ body: Body;
178
+ }
179
+ ).body;
180
+ } catch {
181
+ return false;
182
+ }
183
+ if (hasDirective(body, "use client")) return true;
184
+ const nodes = body as unknown as { type: string; source?: unknown }[];
185
+ return (
186
+ nodes.some((node) => node.type !== "ImportDeclaration") &&
187
+ nodes.every(
188
+ (node) =>
189
+ node.type === "ImportDeclaration" ||
190
+ node.type === "ExportAllDeclaration" ||
191
+ (node.type === "ExportNamedDeclaration" && node.source != null),
192
+ )
193
+ );
194
+ }
195
+
159
196
  /**
160
197
  * Appended to a transformed `"use client"` clientUrls() module: install the
161
198
  * projection of its own source under its default export's reference id
@@ -270,11 +307,24 @@ export function rangoTestConfig(
270
307
  }
271
308
 
272
309
  /** A minimal Vite plugin shape (avoids a hard dependency on Vite's types). */
310
+ /** The part of a Vite plugin context {@link rangoUseClientTransform} resolves with. */
311
+ interface SourceResolveContext {
312
+ resolve(
313
+ source: string,
314
+ importer?: string,
315
+ options?: { skipSelf?: boolean },
316
+ ): Promise<{ id: string; external?: boolean | string } | null>;
317
+ }
318
+
273
319
  interface FlightTransformPlugin {
274
320
  name: string;
275
321
  resolveId: {
276
322
  order: "pre";
277
- handler(source: string, importer: string | undefined): string | undefined;
323
+ handler(
324
+ this: SourceResolveContext,
325
+ source: string,
326
+ importer: string | undefined,
327
+ ): Promise<string | undefined>;
278
328
  };
279
329
  transform(
280
330
  code: string,
@@ -347,19 +397,36 @@ export function rangoUseClientTransform(): FlightTransformPlugin {
347
397
  resolveId: {
348
398
  // Before Vite's resolver, which resolves the bare specifier first.
349
399
  order: "pre",
350
- handler(source, importer) {
400
+ async handler(source, importer) {
401
+ if (!importer?.includes(CLIENT_URLS_SOURCE_QUERY)) return undefined;
351
402
  // A clientUrls() module's source resolves the router's client entry
352
403
  // as the RSC environment does (the react-server condition): this
353
404
  // project resolves it to client.tsx, a "use client" module whose
354
405
  // clientUrls is a client reference here. Both entries export the
355
- // same clientUrls.
406
+ // same clientUrls and helpers.
407
+ if (source === "@rangojs/router/client") return CLIENT_ENTRY_RSC_PATH;
408
+ // Its "use client" project imports load as source too, as the app's
409
+ // SSR copy runs them: a component's `.use` and its name are
410
+ // unreadable on a client reference. So do the barrels between them,
411
+ // which hold no state; any other module stays one shared instance.
412
+ const resolved = await this.resolve(source, importer, {
413
+ skipSelf: true,
414
+ });
415
+ if (!resolved || resolved.external) return undefined;
416
+ const { id } = resolved;
417
+ const file = id.split("?", 1)[0]!;
356
418
  if (
357
- source === "@rangojs/router/client" &&
358
- importer?.includes(CLIENT_URLS_SOURCE_QUERY)
419
+ id.startsWith("\0") ||
420
+ id.includes("/node_modules/") ||
421
+ id.includes(CLIENT_URLS_SOURCE_QUERY) ||
422
+ !/\.[cm]?[jt]sx?$/.test(file)
359
423
  ) {
360
- return CLIENT_ENTRY_RSC_PATH;
424
+ return undefined;
361
425
  }
362
- return undefined;
426
+ const code = await readFile(file, "utf-8").catch(() => "");
427
+ return (await loadsAsSource(code, file))
428
+ ? `${id}${id.includes("?") ? "&" : "?"}${CLIENT_URLS_SOURCE_QUERY}`
429
+ : undefined;
363
430
  },
364
431
  },
365
432
  async transform(code, id) {
@@ -24,14 +24,3 @@ export const SETTLED_THENABLE: Promise<undefined> = Object.assign(
24
24
  Promise.resolve(undefined),
25
25
  { status: "fulfilled", value: undefined, _debugInfo: [] },
26
26
  );
27
-
28
- /**
29
- * The value React reads from `value` without suspending: `value` itself when
30
- * it is not a promise, a promise's `value` once it carries React's or Flight's
31
- * `status: "fulfilled"`. Undefined while it is pending or rejected.
32
- */
33
- export function settledValue<T>(value: T | Promise<T>): T | undefined {
34
- if (!(value instanceof Promise)) return value;
35
- const thenable = value as Promise<T> & { status?: string; value?: T };
36
- return thenable.status === "fulfilled" ? thenable.value : undefined;
37
- }
@@ -89,7 +89,13 @@ type AutofillSearchFromEntry<TMap, TKey> = TKey extends keyof TMap
89
89
  : Record<string, unknown>
90
90
  : Record<string, unknown>;
91
91
 
92
- type AutofillAwareReverseFunction<TLocalRoutes, TGlobalRoutes> =
92
+ /**
93
+ * `ScopedReverseFunction` whose global-name params are optional: the runtime
94
+ * fills missing ones from the matched route's params (`createReverseFunction`
95
+ * in `router/handler-context.ts`, explicit params win). Shared by handler,
96
+ * loader and request-context `reverse`.
97
+ */
98
+ export type AutofillAwareReverseFunction<TLocalRoutes, TGlobalRoutes> =
93
99
  ScopedReverseFunction<TLocalRoutes, TGlobalRoutes> & {
94
100
  <TName extends keyof TGlobalRoutes & string>(
95
101
  name: TName,
@@ -449,6 +455,11 @@ export type InternalHandlerContext<
449
455
  _responseType?: string;
450
456
  /** Route name for cache key scoping (prevents cross-route collisions). */
451
457
  _routeName?: string;
458
+ /**
459
+ * The segment's ReverseScope on a layout's / parallel's ctx and its
460
+ * loaders' (withReverseScope): keys a "use cache" call taking the ctx.
461
+ */
462
+ _reverseScope?: import("../server/context.js").ReverseScope;
452
463
  /**
453
464
  * @internal The on-demand overlay holds a "removed" marker for this request
454
465
  * (tryPrerenderLookup, cache-lookup.ts); gateOnDemandProducer reads it. On
@@ -2,7 +2,7 @@ import type { ContextVar } from "../context-var.js";
2
2
  import type { Handle } from "../handle.js";
3
3
  import type { HandlePush } from "../defer.js";
4
4
  import type { MiddlewareFn } from "../router/middleware.js";
5
- import type { ScopedReverseFunction } from "../reverse.js";
5
+ import type { AutofillAwareReverseFunction } from "./handler-context.js";
6
6
  import type { SearchSchema, ResolveSearchSchema } from "../search-params.js";
7
7
  import type { UseItems, LoaderUseItem } from "../route-types.js";
8
8
  import type {
@@ -134,9 +134,10 @@ export type LoaderContext<
134
134
  /**
135
135
  * Generate URLs from route names.
136
136
  * Same scoped reverse as route handlers — `.name` resolves within the
137
- * current include() scope, `name` resolves globally.
137
+ * current include() scope, `name` resolves globally, and params the matched
138
+ * route already carries (e.g. include() mount params) are filled in.
138
139
  */
139
- reverse: ScopedReverseFunction<
140
+ reverse: AutofillAwareReverseFunction<
140
141
  Record<string, string>,
141
142
  DefaultReverseRouteMap
142
143
  >;
@@ -1,7 +1,8 @@
1
1
  import type { AllUseItems } from "../route-types.js";
2
+ import type { IncludeScopePosition } from "../server/context.js";
2
3
  import type { TrailingSlashMode, ResolvedRouteMap } from "./route-config.js";
3
4
 
4
- export interface LazyIncludeContext {
5
+ export interface LazyIncludeContext extends IncludeScopePosition {
5
6
  urlPrefix: string;
6
7
  namePrefix: string | undefined;
7
8
  parent: unknown; // EntryData - avoid circular import
@@ -3,6 +3,7 @@ import {
3
3
  getUrlPrefix,
4
4
  getNamePrefix,
5
5
  requireDslContext,
6
+ type IncludeScopePosition,
6
7
  } from "../server/context";
7
8
  import {
8
9
  INTERNAL_INCLUDE_SCOPE_PREFIX,
@@ -21,13 +22,32 @@ function hasExplicitNameOption(options: IncludeOptions | undefined): boolean {
21
22
  return !!options && Object.prototype.hasOwnProperty.call(options, "name");
22
23
  }
23
24
 
24
- function allocateInternalIncludeScopeId(
25
+ /**
26
+ * This include's index among the includes its handler run has called. The
27
+ * counter is keyed by the run's scopePath, which no ancestor run's counter
28
+ * snapshot holds, so every run counts from 0 in every realm (build discovery,
29
+ * runtime expansion, render-time manifest).
30
+ */
31
+ function nextIncludePosition(
25
32
  counters: Record<string, number>,
26
- ): string {
27
- const key = "__include_scope__";
33
+ scopePath: string | undefined,
34
+ ): number {
35
+ const key = `__include_position__:${scopePath ?? ""}`;
28
36
  const index = counters[key] ?? 0;
29
37
  counters[key] = index + 1;
30
- return `${INTERNAL_INCLUDE_SCOPE_PREFIX}${index}`;
38
+ return index;
39
+ }
40
+
41
+ function joinScopePath(parent: string | undefined, index: number): string {
42
+ return parent ? `${parent}_${index}` : `${index}`;
43
+ }
44
+
45
+ function joinNames(
46
+ prefix: string | undefined,
47
+ segment: string | undefined,
48
+ ): string | undefined {
49
+ if (!segment) return prefix;
50
+ return prefix ? `${prefix}.${segment}` : segment;
31
51
  }
32
52
 
33
53
  /**
@@ -100,15 +120,20 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
100
120
  ? capturedUrlPrefix + prefix.slice(1)
101
121
  : capturedUrlPrefix + prefix
102
122
  : prefix;
123
+ // Route names are a pure function of the route tree: an unnamed include's
124
+ // hidden segment is its position path since the nearest include that added
125
+ // a name segment (a transparent { name: "" } adds none), e.g. `$prefix_1_0`.
126
+ const index = nextIncludePosition(ctx.counters, ctx.scopePath);
127
+ const hiddenSoFar = joinScopePath(ctx.hiddenScopePath, index);
103
128
  const nextSegment = hasExplicitName
104
129
  ? explicitName
105
- : allocateInternalIncludeScopeId(ctx.counters);
106
- const fullNamePrefix =
107
- nextSegment !== undefined && nextSegment !== ""
108
- ? capturedNamePrefix
109
- ? `${capturedNamePrefix}.${nextSegment}`
110
- : nextSegment
111
- : capturedNamePrefix;
130
+ : `${INTERNAL_INCLUDE_SCOPE_PREFIX}${hiddenSoFar}`;
131
+ const fullNamePrefix = joinNames(capturedNamePrefix, nextSegment);
132
+ const position: IncludeScopePosition = {
133
+ scopePath: joinScopePath(ctx.scopePath, index),
134
+ // A name segment restarts the hidden path for this include's children.
135
+ hiddenScopePath: nextSegment ? "" : hiddenSoFar,
136
+ };
112
137
 
113
138
  // Track this include for build-time manifest generation
114
139
  if (ctx.trackedIncludes) {
@@ -118,6 +143,7 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
118
143
  namePrefix: fullNamePrefix,
119
144
  patterns,
120
145
  lazy: true,
146
+ ...position,
121
147
  });
122
148
  }
123
149
 
@@ -171,6 +197,7 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
171
197
  rootScoped: capturedRootScoped,
172
198
  routerId: ctx.routerId,
173
199
  includeScope,
200
+ ...position,
174
201
  },
175
202
  } as IncludeItem;
176
203
  };
@@ -346,9 +346,11 @@ export type PathHelpers<TEnv> = {
346
346
  };
347
347
 
348
348
  /**
349
- * Control when a segment should revalidate during navigation
349
+ * Control when a segment should revalidate during navigation.
350
+ * `revalidate(true)` / `revalidate(false)` are shorthand for
351
+ * `revalidate(() => true)` / `revalidate(() => false)`.
350
352
  */
351
- revalidate: (fn: ShouldRevalidateFn<any, TEnv>) => RevalidateItem;
353
+ revalidate: (fn: ShouldRevalidateFn<any, TEnv> | boolean) => RevalidateItem;
352
354
 
353
355
  /**
354
356
  * Attach a data loader to the current route/layout.