@rangojs/router 0.17.0 → 0.19.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 (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. package/src/rsc/transition-gate.ts +0 -86
@@ -1,14 +1,17 @@
1
1
  /**
2
- * Capture-side nested-thenable masking — the mechanism behind "nested-promise
3
- * shape is the liveness declaration" for BOTH bake-lane loader containers
4
- * (loader-cache.ts) and pushed handle containers (the capture handle store's
5
- * push wrap in rsc/shell-capture.ts).
2
+ * Capture-side nested thenables. Masking is the mechanism behind "nested-
3
+ * promise shape is the liveness declaration" for the loader lane: bake-lane
4
+ * loader containers (loader-cache.ts) and DSL loader handle pushes (the
5
+ * capture handle store's push wrap in rsc/shell-capture.ts). Handler handle
6
+ * pushes are handler output and bake: the capture waits for their nested
7
+ * promises instead (settleNestedThenables).
6
8
  *
7
9
  * Deliberately a LEAF module: loader-mask (the other natural home) imports
8
10
  * request-context, so any funnel importing the mask from there inherits that
9
- * graph. This module imports only is-thenable.
11
+ * graph. This module imports only is-thenable and react.
10
12
  */
11
13
 
14
+ import { cloneElement, isValidElement, type ReactElement } from "react";
12
15
  import { isThenable } from "../../handles/is-thenable.js";
13
16
 
14
17
  /**
@@ -23,18 +26,127 @@ export function createMaskedLoaderPromise<T = unknown>(): Promise<T> {
23
26
  return new Promise<T>(() => {});
24
27
  }
25
28
 
26
- function isPlainObject(value: unknown): value is Record<string, unknown> {
29
+ /**
30
+ * A data object the container walks descend into: prototype Object.prototype
31
+ * or null, and no symbol `$$typeof`. React elements, lazy nodes and references
32
+ * are plain objects too, told apart by that symbol (Flight switches on it).
33
+ * They are React's, not data: an element's dev-only fields (`_debugStack`,
34
+ * `_debugTask`, `_debugInfo`, `ref`) are non-enumerable, so a key-by-key copy
35
+ * drops them and dev Flight refuses it ("Attempted to render <x> without
36
+ * development properties", issue #942).
37
+ */
38
+ export function isPlainDataObject(
39
+ value: unknown,
40
+ ): value is Record<string, unknown> {
27
41
  if (typeof value !== "object" || value === null) return false;
28
42
  const proto = Object.getPrototypeOf(value);
29
- return proto === Object.prototype || proto === null;
43
+ if (proto !== Object.prototype && proto !== null) return false;
44
+ return typeof (value as { $$typeof?: unknown }).$$typeof !== "symbol";
45
+ }
46
+
47
+ type KeyValidated = { _store?: { validated?: unknown } };
48
+
49
+ /**
50
+ * cloneElement with only the given props replaced, keeping React's dev key
51
+ * validation. React 19 dev cloneElement resets `_store.validated`, so a
52
+ * keyless child of a static children list (validated at creation) would warn
53
+ * "Each child in a list should have a unique key" once cloned. Pass only the
54
+ * props that change: cloneElement re-parents the element (owner) when its
55
+ * config carries `ref`.
56
+ */
57
+ export function cloneElementWithProps(
58
+ element: ReactElement,
59
+ props: Record<string, unknown>,
60
+ ): ReactElement {
61
+ const clone = cloneElement(element, props);
62
+ const from = (element as KeyValidated)._store;
63
+ const to = (clone as KeyValidated)._store;
64
+ if (from && to) to.validated = from.validated;
65
+ return clone;
66
+ }
67
+
68
+ /**
69
+ * Resolves once `value` and every thenable reachable from it through plain
70
+ * data objects, arrays and React element props has settled, following what
71
+ * each thenable resolves to. A rejection counts as settled: the consumer that
72
+ * encodes the value reports it. Other objects are leaves (the same walk as
73
+ * maskNestedContainerThenables), so a promise inside a Map is not waited for.
74
+ * An element is walked through its props only (`<X data={promise} />` in a
75
+ * handle value is handler output too), never its owner or type, and is never
76
+ * rebuilt: nothing is copied, only awaited.
77
+ *
78
+ * The capture waits on this for HANDLER handle pushes (shell-capture.ts):
79
+ * handler output is baked, so its promises must settle before the doc record
80
+ * encodes the handles every HIT replays.
81
+ */
82
+ export function settleNestedThenables(value: unknown): Promise<void> {
83
+ const pending: Promise<void>[] = [];
84
+ someNestedThenable(value, (thenable) => {
85
+ pending.push(
86
+ Promise.resolve(thenable).then(settleNestedThenables, () => undefined),
87
+ );
88
+ return false;
89
+ });
90
+ return pending.length === 0
91
+ ? Promise.resolve()
92
+ : Promise.all(pending).then(() => undefined);
93
+ }
94
+
95
+ /**
96
+ * The walk settleNestedThenables and holdsThenable share: visit each thenable
97
+ * reachable from `value` through plain data objects, arrays and React element
98
+ * props (never an element's owner or type), the shapes
99
+ * maskNestedContainerThenables masks; every other object is a leaf. Stops at
100
+ * the first visit that returns true and reports whether one did.
101
+ */
102
+ function someNestedThenable(
103
+ value: unknown,
104
+ visit: (thenable: PromiseLike<unknown>) => boolean,
105
+ ): boolean {
106
+ let seen: Set<object> | undefined;
107
+ const walk = (v: unknown): boolean => {
108
+ if (isThenable(v)) return visit(v);
109
+ if (typeof v !== "object" || v === null) return false;
110
+ if (isValidElement<Record<string, unknown>>(v)) return walk(v.props);
111
+ if (!Array.isArray(v) && !isPlainDataObject(v)) return false;
112
+ seen ??= new Set();
113
+ if (seen.has(v)) return false;
114
+ seen.add(v);
115
+ // Own enumerable values, as maskNestedContainerThenables walks
116
+ // Object.keys: an array's non-index properties are walked too.
117
+ return Object.values(v).some(walk);
118
+ };
119
+ return walk(value);
30
120
  }
31
121
 
32
122
  /**
33
- * Deep-copy a container with every NESTED thenable replaced by a masked
123
+ * True when `value` is a thenable or holds one where the capture's loader
124
+ * push funnel masks it (maskNestedContainerThenables): through plain data
125
+ * objects, arrays and React element props. Such a push stays out of a shell's
126
+ * record and reaches a HIT only from a run of its loader there, so a HIT
127
+ * keeps it; a push without one is the record's (HandleStore.pushRestored).
128
+ */
129
+ export function holdsThenable(value: unknown): boolean {
130
+ return someNestedThenable(value, () => true);
131
+ }
132
+
133
+ /**
134
+ * Optional single-walk report for maskNestedContainerThenables: `thenable`
135
+ * flips true when the walk masked at least one thenable. The capture push
136
+ * funnel reads it for a loader push: a container with a masked nested
137
+ * promise keeps its loader tag, so the handle encode never waits on a mask.
138
+ * Same single-pass shape as elideLoaderContainer's `hasHole`.
139
+ */
140
+ export interface MaskReport {
141
+ thenable: boolean;
142
+ }
143
+
144
+ /**
145
+ * Copy a container with every NESTED thenable replaced by a masked
34
146
  * (never-resolving) promise. Applied during shell capture to (a) bake-lane
35
- * loader containers (loader-cache.ts) and (b) pushed handle containers
36
- * (shell-capture.ts capture store push wrap) — the two rango-owned funnels
37
- * where consumers declare per-request data by promise SHAPE.
147
+ * loader containers (loader-cache.ts) and (b) DSL loader handle pushes
148
+ * (shell-capture.ts capture store push wrap) — the rango-owned funnels where
149
+ * a loader declares per-request data by promise SHAPE.
38
150
  *
39
151
  * Why: a nested promise that happened to SETTLE before the capture's quiet
40
152
  * window closed used to bake its value into the SHARED shell (and, for
@@ -46,23 +158,16 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
46
158
  * Masking makes the consuming subtree postpone as a hole no matter when the
47
159
  * promise settles: liveness by declaration, not by racing the window.
48
160
  *
49
- * Only plain objects/arrays are traversed; other values are leaves. The INPUT
50
- * IS NEVER MUTATED — handler-side loader consumption (the consumption-lane
51
- * rule, semantic-matrix PPR3) shares the raw container and must keep real
52
- * values. Cycles are preserved as cycles in the copy.
161
+ * Plain data objects, arrays and React element props are traversed: JSX
162
+ * carrying a promise (`<Reviews data={fetchReviews()} />`) declares
163
+ * per-request data the same way a plain container does. Other values, lazy
164
+ * nodes included, are leaves. Copy-on-write: a subtree without a thenable
165
+ * comes back by identity, so an element is rebuilt only when its props hold
166
+ * one, and then through cloneElementWithProps, which keeps its type, key,
167
+ * owner, dev fields and key validation. The INPUT IS NEVER MUTATED: a handler
168
+ * that reads the loader (`ctx.use`) reads the raw container and must keep
169
+ * real values. Cycles are preserved as cycles in the copy.
53
170
  */
54
- /**
55
- * Optional single-walk report for maskNestedContainerThenables: `thenable`
56
- * flips true when the walk masked at least one thenable. The shell fast path
57
- * reads it at handle-push time — a pushed container with a nested thenable
58
- * declares per-request data, and a shell entry whose HANDLER layer made such
59
- * a declaration cannot be served handler-free (the hole would never fill).
60
- * Same single-pass shape as elideLoaderContainer's `hasHole`.
61
- */
62
- export interface MaskReport {
63
- thenable: boolean;
64
- }
65
-
66
171
  export function maskNestedContainerThenables(
67
172
  value: unknown,
68
173
  seen: Map<object, unknown> = new Map(),
@@ -72,27 +177,41 @@ export function maskNestedContainerThenables(
72
177
  if (report) report.thenable = true;
73
178
  return createMaskedLoaderPromise();
74
179
  }
180
+ if (typeof value !== "object" || value === null) return value;
181
+ const cached = seen.get(value);
182
+ if (cached !== undefined) return cached;
75
183
 
76
- if (Array.isArray(value)) {
77
- const cached = seen.get(value);
78
- if (cached !== undefined) return cached;
79
- const out: unknown[] = new Array(value.length);
80
- seen.set(value, out);
81
- for (let i = 0; i < value.length; i++) {
82
- out[i] = maskNestedContainerThenables(value[i], seen, report);
184
+ if (isValidElement<Record<string, unknown>>(value)) {
185
+ // Placeholder: a cycle back to this element through its props keeps the
186
+ // original.
187
+ seen.set(value, value);
188
+ const props = value.props;
189
+ let maskedProps: Record<string, unknown> | undefined;
190
+ for (const key of Object.keys(props)) {
191
+ const masked = maskNestedContainerThenables(props[key], seen, report);
192
+ if (masked !== props[key]) (maskedProps ??= {})[key] = masked;
83
193
  }
84
- return out;
194
+ if (!maskedProps) return value;
195
+ const clone = cloneElementWithProps(value, maskedProps);
196
+ seen.set(value, clone);
197
+ return clone;
85
198
  }
86
199
 
87
- if (isPlainObject(value)) {
88
- const cached = seen.get(value);
89
- if (cached !== undefined) return cached;
90
- const out: Record<string, unknown> = {};
200
+ if (Array.isArray(value) || isPlainDataObject(value)) {
201
+ const source = value as Record<string, unknown>;
202
+ const out = (Array.isArray(value) ? new Array(value.length) : {}) as Record<
203
+ string,
204
+ unknown
205
+ >;
91
206
  seen.set(value, out);
92
- for (const key of Object.keys(value)) {
93
- out[key] = maskNestedContainerThenables(value[key], seen, report);
207
+ let changed = false;
208
+ for (const key of Object.keys(source)) {
209
+ out[key] = maskNestedContainerThenables(source[key], seen, report);
210
+ if (out[key] !== source[key]) changed = true;
94
211
  }
95
- return out;
212
+ if (changed) return out;
213
+ seen.set(value, value);
214
+ return value;
96
215
  }
97
216
 
98
217
  return value;
@@ -45,7 +45,8 @@ import {
45
45
  } from "./helpers.js";
46
46
  import { applyViewTransitionDefault } from "./view-transition-default.js";
47
47
  import { getRouterContext } from "../router-context.js";
48
- import { observeEvent, observeHandler } from "../instrument.js";
48
+ import { observeEvent, observeSegmentHandler } from "../instrument.js";
49
+ import { runInSegmentTagScope } from "../../cache/cache-tag.js";
49
50
  import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
50
51
  import {
51
52
  track,
@@ -221,7 +222,8 @@ export async function resolveLoadersWithRevalidation<TEnv>(
221
222
  // whole-container-mask every loader on this path (a never-settling promise
222
223
  // for a loader that should have executed).
223
224
  // Outside capture the key only activates the _shellLoaderSeed overlay,
224
- // which document-only serveShellHit seeds — inert for partial requests.
225
+ // which serveShellHit seeds for a document HIT and the PPR partial replay
226
+ // (matchPartialWithPprReplay) seeds once its doc record hits.
225
227
  const bakeLane = !entryLoadingMasksLoaders(entry.loading);
226
228
 
227
229
  const loadersToRun = revalidationChecks.filter((c) => c.shouldRun);
@@ -240,9 +242,10 @@ export async function resolveLoadersWithRevalidation<TEnv>(
240
242
  loaderEntry,
241
243
  ctx,
242
244
  ctx.pathname,
243
- bakeLane || loaderEntry.awaitBeforeFlush === true
244
- ? segmentId
245
- : null,
245
+ // `bake`, not awaitBeforeFlush: a navigation evaluation never
246
+ // carries awaitBeforeFlush, and an ssr: false loader must pin
247
+ // here exactly as on the document HIT.
248
+ bakeLane || loaderEntry.bake === true ? segmentId : null,
246
249
  ),
247
250
  ),
248
251
  entry,
@@ -476,7 +479,9 @@ async function resolveParallelSlotComponent<TEnv>(args: {
476
479
  handlerRan = true;
477
480
  if (hasLoadingFallback) {
478
481
  const result =
479
- typeof handler === "function" ? handler(context) : handler;
482
+ typeof handler === "function"
483
+ ? runInSegmentTagScope(parallelId, handler, context)
484
+ : handler;
480
485
  if (result instanceof Promise) {
481
486
  warnOnStreamedResponse(result, parallelId);
482
487
  const tracked = deps.trackHandler(result, {
@@ -497,7 +502,9 @@ async function resolveParallelSlotComponent<TEnv>(args: {
497
502
  }
498
503
  } else {
499
504
  component =
500
- typeof handler === "function" ? await handler(context) : handler;
505
+ typeof handler === "function"
506
+ ? await runInSegmentTagScope(parallelId, handler, context)
507
+ : handler;
501
508
  }
502
509
  }
503
510
  }
@@ -701,7 +708,6 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
701
708
  transition: applyViewTransitionDefault(
702
709
  parallelEntry.transition,
703
710
  deps.viewTransitionDefault,
704
- parallelId,
705
711
  ),
706
712
  params,
707
713
  slot,
@@ -842,14 +848,19 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
842
848
  : routeEntry.handler;
843
849
  if (!routeEntry.loading) {
844
850
  const result = handleHandlerResult(
845
- await observeHandler(entry.id, handler, context),
851
+ await observeSegmentHandler(
852
+ entry.shortCode,
853
+ entry.id,
854
+ handler,
855
+ context,
856
+ ),
846
857
  );
847
858
  doneHandler();
848
859
  return result;
849
860
  }
850
861
  if (!actionContext) {
851
862
  const result = handleHandlerResult(
852
- observeHandler(entry.id, handler, context),
863
+ observeSegmentHandler(entry.shortCode, entry.id, handler, context),
853
864
  );
854
865
  if (result instanceof Promise) {
855
866
  warnOnStreamedResponse(result, routeEntry.id);
@@ -875,7 +886,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
875
886
  entryId: entry.id,
876
887
  });
877
888
  const actionResult = handleHandlerResult(
878
- await observeHandler(entry.id, handler, context),
889
+ await observeSegmentHandler(
890
+ entry.shortCode,
891
+ entry.id,
892
+ handler,
893
+ context,
894
+ ),
879
895
  );
880
896
  doneHandler();
881
897
  return {
@@ -903,7 +919,6 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
903
919
  transition: applyViewTransitionDefault(
904
920
  entry.transition,
905
921
  deps.viewTransitionDefault,
906
- entry.shortCode,
907
922
  ),
908
923
  params,
909
924
  belongsToRoute,
@@ -1244,7 +1259,6 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1244
1259
  transition: applyViewTransitionDefault(
1245
1260
  orphan.transition,
1246
1261
  deps.viewTransitionDefault,
1247
- orphan.shortCode,
1248
1262
  ),
1249
1263
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
1250
1264
  });
@@ -8,7 +8,6 @@
8
8
  */
9
9
 
10
10
  import type { EntryData } from "../../server/context";
11
- import { getRequestContext } from "../../server/request-context.js";
12
11
 
13
12
  /**
14
13
  * Resolve a segment's transition config: stamp the `viewTransition` default and
@@ -22,34 +21,20 @@ import { getRequestContext } from "../../server/request-context.js";
22
21
  * (or "auto") value is left untouched because it already means "wrap" at the
23
22
  * gate, which also avoids needless object allocation and payload growth.
24
23
  *
25
- * `when`: a server-only predicate. It is STRIPPED from the returned config (a
26
- * function cannot cross Flight or the segment cache). PPR routes evaluate it
27
- * from the manifest before the pipeline; other routes record it here for the
28
- * existing post-handler gate. Used by both fresh and revalidation resolution.
24
+ * `when`: a client reference the server never calls. It is STRIPPED here so no
25
+ * segment record (segment cache, prerender artifact, shell snapshot) carries
26
+ * it: those stores JSON-serialize the config and would silently drop it on
27
+ * some stores and keep it on others. rsc/attach-transition-when.ts attaches it
28
+ * from the route definition right before Flight instead. Used by both fresh
29
+ * and revalidation resolution.
29
30
  */
30
31
  export function applyViewTransitionDefault(
31
32
  transition: EntryData["transition"],
32
33
  viewTransitionDefault: "auto" | false | undefined,
33
- segmentId?: string,
34
34
  ): EntryData["transition"] {
35
35
  if (!transition) return transition;
36
36
  let result = transition;
37
37
  if (result.when) {
38
- if (segmentId !== undefined) {
39
- try {
40
- const ctx = getRequestContext();
41
- if (!ctx._pprTransitionDecisions?.has(segmentId)) {
42
- (ctx._transitionWhen ??= []).push({
43
- id: segmentId,
44
- when: result.when,
45
- });
46
- }
47
- } catch {
48
- // No active request context (e.g. a unit test calling this util
49
- // directly). Skip collection; the strip below still applies so the
50
- // serialized config never carries the function.
51
- }
52
- }
53
38
  const { when: _when, ...rest } = result;
54
39
  result = rest;
55
40
  }
@@ -14,6 +14,8 @@
14
14
  * - revalidation.decision (revalidation evaluation)
15
15
  */
16
16
 
17
+ import { requestHeaders } from "../server/request-headers.js";
18
+
17
19
  interface BaseEvent {
18
20
  /** Monotonic timestamp from performance.now() */
19
21
  timestamp: number;
@@ -294,10 +296,11 @@ export function getRequestId(request: Request): string {
294
296
  const existing = requestIds.get(request);
295
297
  if (existing) return existing;
296
298
 
299
+ const headers = requestHeaders(request);
297
300
  const candidate =
298
- request.headers.get("x-rsc-router-request-id") ??
299
- request.headers.get("x-request-id") ??
300
- request.headers.get("cf-ray");
301
+ headers.get("x-rsc-router-request-id") ??
302
+ headers.get("x-request-id") ??
303
+ headers.get("cf-ray");
301
304
 
302
305
  let id: string;
303
306
  if (candidate) {
@@ -1,76 +1,58 @@
1
1
  import { getParallelSlotEntries, type EntryData } from "../server/context.js";
2
2
  import type { RequestContext } from "../server/request-context.js";
3
- import type { TransitionWhenContext } from "../types/segments.js";
3
+ import type { TransitionWhenSite } from "../transition-when-ref.js";
4
+ import type { TransitionWhenFn } from "../types/segments.js";
4
5
 
5
- export type TransitionWhenErrorReporter = (
6
- error: unknown,
7
- segmentId: string,
8
- ) => void;
9
-
10
- export function createTransitionWhenContext<TEnv>(
11
- ctx: RequestContext<TEnv>,
12
- target?: {
13
- params: Record<string, string>;
14
- routeName?: string;
15
- },
16
- ): TransitionWhenContext<Record<string, string>, TEnv> {
17
- return {
18
- currentUrl: ctx._gateCurrentUrl,
19
- currentParams: ctx._gateCurrentParams,
20
- fromRouteName: ctx._prevRouteKey as TransitionWhenContext["fromRouteName"],
21
- nextUrl: ctx.url,
22
- nextParams: target?.params ?? ctx.params,
23
- toRouteName: (target?.routeName ??
24
- ctx.routeName) as TransitionWhenContext["toRouteName"],
25
- actionId: ctx._gateActionId,
26
- actionUrl: ctx._gateActionUrl,
27
- actionResult: ctx._gateActionResult,
28
- formData: ctx._gateFormData,
29
- method: ctx.request.method,
30
- get: ctx.get,
31
- env: ctx.env,
32
- };
6
+ /** A matched entry's transition({ when }) and where it was declared. */
7
+ export interface TransitionWhenRecord {
8
+ readonly when: TransitionWhenFn;
9
+ readonly site: TransitionWhenSite;
33
10
  }
34
11
 
35
- /** Evaluate server-only transition gates before handlers on a PPR route. */
36
- export function evaluatePprTransitionWhen<TEnv>(
37
- entries: EntryData[],
12
+ /**
13
+ * Record the transition({ when }) references of a matched entry chain, keyed
14
+ * by the segment id each entry resolves to. The server never calls them:
15
+ * rsc/attach-transition-when.ts attaches them to the payload's segments right
16
+ * before Flight, and the browser decides (browser/transition-when.ts).
17
+ *
18
+ * Recorded from the static route definition on EVERY match (fresh, cache hit,
19
+ * prerender, PPR replay) because stored segments never carry them:
20
+ * applyViewTransitionDefault strips `when` at resolution, since segment stores
21
+ * JSON-serialize the transition config (dropping a function on some stores,
22
+ * keeping it by reference on others). Segment ids follow resolution: an
23
+ * entry's shortCode, and `${owner.shortCode}.${slot}` for a rendered parallel
24
+ * slot.
25
+ */
26
+ export function recordTransitionWhenRefs<TEnv>(
27
+ entries: readonly EntryData[] | undefined,
38
28
  ctx: RequestContext<TEnv>,
39
- target: {
40
- params: Record<string, string>;
41
- routeName?: string;
42
- },
43
- reportError: TransitionWhenErrorReporter,
29
+ route: { routeName?: string; pattern?: string } = {},
44
30
  ): void {
45
- const decisions = new Map<string, boolean>();
46
- const whenContext = createTransitionWhenContext(ctx, target);
31
+ const refs = new Map<string, TransitionWhenRecord>();
47
32
  const visited = new Set<EntryData>();
48
33
 
49
- const evaluate = (entry: EntryData, segmentId: string): void => {
34
+ const record = (entry: EntryData, segmentId: string): void => {
50
35
  const when = entry.transition?.when;
51
- if (!when) return;
52
- try {
53
- decisions.set(segmentId, when(whenContext) !== false);
54
- } catch (error) {
55
- decisions.set(segmentId, false);
56
- reportError(error, segmentId);
57
- }
36
+ if (when === undefined) return;
37
+ refs.set(segmentId, {
38
+ when,
39
+ site: { ...route, entryType: entry.type },
40
+ });
58
41
  };
59
42
 
60
43
  const visitEntryAndOrphanLayouts = (entry: EntryData): void => {
61
44
  if (visited.has(entry)) return;
62
45
  visited.add(entry);
63
- evaluate(entry, entry.shortCode);
46
+ record(entry, entry.shortCode);
64
47
 
65
48
  for (const orphan of entry.layout) visitEntryAndOrphanLayouts(orphan);
66
- // Rendered parallel slots are leaves identified by their owning segment.
67
49
  for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
68
50
  entry.parallel,
69
51
  )) {
70
- evaluate(parallelEntry, `${entry.shortCode}.${slot}`);
52
+ record(parallelEntry, `${entry.shortCode}.${slot}`);
71
53
  }
72
54
  };
73
55
 
74
- for (const entry of entries) visitEntryAndOrphanLayouts(entry);
75
- ctx._pprTransitionDecisions = decisions.size > 0 ? decisions : undefined;
56
+ for (const entry of entries ?? []) visitEntryAndOrphanLayouts(entry);
57
+ ctx._transitionWhenRefs = refs.size > 0 ? refs : undefined;
76
58
  }
package/src/router.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { requestHeaders } from "./server/request-headers.js";
1
2
  import { type ReactNode } from "react";
2
3
  import { createCacheScope } from "./cache/cache-scope.js";
3
4
  import { resolveCacheProfiles } from "./cache/profile-registry.js";
@@ -1161,7 +1162,7 @@ export function createRouter<TEnv = any>(
1161
1162
  | null = null;
1162
1163
 
1163
1164
  return async (request: Request, input: RouterRequestInput<TEnv> = {}) => {
1164
- const requestedDiscoveryEpoch = request.headers.get(
1165
+ const requestedDiscoveryEpoch = requestHeaders(request).get(
1165
1166
  DEV_DISCOVERY_PROBE_HEADER,
1166
1167
  );
1167
1168
  if (
@@ -0,0 +1,47 @@
1
+ import type { MatchResult } from "../types.js";
2
+ import type { getRequestContext } from "../server/request-context.js";
3
+ import {
4
+ createTransitionWhenError,
5
+ isTransitionWhenValidationEnabled,
6
+ transitionWhenProblem,
7
+ } from "../transition-when-ref.js";
8
+
9
+ /**
10
+ * Attach transition({ when }) references to a payload's segments, right before
11
+ * Flight. Returns a copy for every segment it changes: the input segments may
12
+ * be the records a cache, prerender, or shell snapshot holds.
13
+ *
14
+ * Only a segment that carries `transition` gets its predicate. The config
15
+ * itself never varies per request, so the segment key and the <ViewTransition>
16
+ * wrapper depend only on the route definition (#995); the browser decides for
17
+ * every committed segment, kept ones included (#989).
18
+ *
19
+ * Render-time backstop for an invalid `when` that route discovery did not see
20
+ * (a route evaluated lazily at runtime): it throws the discovery error. A
21
+ * plain function reaching Flight would otherwise fail with React's generic
22
+ * "Functions cannot be passed directly to Client Components", or be dropped.
23
+ */
24
+ export function attachTransitionWhen(
25
+ segments: MatchResult["segments"],
26
+ ctx: ReturnType<typeof getRequestContext>,
27
+ ): MatchResult["segments"] {
28
+ const refs = ctx?._transitionWhenRefs;
29
+ if (!refs) return segments;
30
+ const strict = isTransitionWhenValidationEnabled();
31
+ let out = segments;
32
+ for (let i = 0; i < segments.length; i++) {
33
+ const segment = segments[i];
34
+ const record = segment.transition ? refs.get(segment.id) : undefined;
35
+ if (!record || segment.transition!.when === record.when) continue;
36
+ const problem = transitionWhenProblem(record.when, strict);
37
+ if (problem) {
38
+ throw createTransitionWhenError(record.when, problem, record.site);
39
+ }
40
+ if (out === segments) out = [...segments];
41
+ out[i] = {
42
+ ...segment,
43
+ transition: { ...segment.transition, when: record.when },
44
+ };
45
+ }
46
+ return out;
47
+ }