@rangojs/router 0.20.0 → 0.22.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 (331) hide show
  1. package/dist/bin/rango.js +39 -15
  2. package/dist/types/browser/event-controller.d.ts +49 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-store.d.ts +9 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  6. package/dist/types/browser/partial-update.d.ts +2 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  8. package/dist/types/browser/rango-state.d.ts +3 -1
  9. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  10. package/dist/types/browser/react/context.d.ts +25 -0
  11. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  12. package/dist/types/browser/react/location-state.d.ts +7 -2
  13. package/dist/types/browser/rsc-router.d.ts +13 -2
  14. package/dist/types/browser/types.d.ts +10 -0
  15. package/dist/types/build/generate-manifest.d.ts +2 -0
  16. package/dist/types/cache/background-task.d.ts +22 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  18. package/dist/types/cache/cache-policy.d.ts +17 -0
  19. package/dist/types/cache/cache-scope.d.ts +5 -6
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  22. package/dist/types/cache/document-cache.d.ts +8 -5
  23. package/dist/types/cache/handle-snapshot.d.ts +59 -50
  24. package/dist/types/cache/index.d.ts +3 -1
  25. package/dist/types/cache/invalidation-order.d.ts +14 -0
  26. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  27. package/dist/types/cache/shell-snapshot.d.ts +29 -4
  28. package/dist/types/cache/store-scope.d.ts +22 -0
  29. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  30. package/dist/types/cache/types.d.ts +84 -24
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
  32. package/dist/types/client-urls/navigation.d.ts +7 -1
  33. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  34. package/dist/types/errors.d.ts +6 -0
  35. package/dist/types/handles/deferred-resolution.d.ts +18 -1
  36. package/dist/types/internal-suspense-audit.d.ts +1 -0
  37. package/dist/types/loader-store.d.ts +4 -8
  38. package/dist/types/outlet-provider.d.ts +2 -1
  39. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  40. package/dist/types/prerender/cloudflare.d.ts +49 -0
  41. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  42. package/dist/types/prerender/index.d.ts +12 -0
  43. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  44. package/dist/types/prerender/on-demand.d.ts +272 -0
  45. package/dist/types/prerender/producer-guard.d.ts +33 -0
  46. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  47. package/dist/types/prerender/warm-request.d.ts +65 -0
  48. package/dist/types/prerender/warm.d.ts +65 -0
  49. package/dist/types/prerender/writable-store.d.ts +170 -0
  50. package/dist/types/prerender.d.ts +27 -0
  51. package/dist/types/reverse.d.ts +1 -1
  52. package/dist/types/route-content-wrapper.d.ts +3 -1
  53. package/dist/types/router/error-handling.d.ts +2 -0
  54. package/dist/types/router/handler-context.d.ts +1 -1
  55. package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
  56. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  57. package/dist/types/router/pattern-matching.d.ts +2 -0
  58. package/dist/types/router/prefetch-default.d.ts +4 -4
  59. package/dist/types/router/prerender-match.d.ts +23 -1
  60. package/dist/types/router/route-trie-builder.d.ts +8 -1
  61. package/dist/types/router/router-context.d.ts +1 -0
  62. package/dist/types/router/router-interfaces.d.ts +32 -1
  63. package/dist/types/router/router-options.d.ts +39 -5
  64. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  65. package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  67. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  68. package/dist/types/router/trie-matching.d.ts +2 -0
  69. package/dist/types/router-versions.d.ts +39 -0
  70. package/dist/types/rsc/handler-context.d.ts +5 -0
  71. package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
  72. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  73. package/dist/types/rsc/shell-capture.d.ts +14 -1
  74. package/dist/types/rsc/shell-serve.d.ts +2 -2
  75. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  76. package/dist/types/rsc/types.d.ts +12 -6
  77. package/dist/types/segment-boundary-content.d.ts +17 -0
  78. package/dist/types/segment-loader-promise.d.ts +2 -6
  79. package/dist/types/server/build-version-table.d.ts +53 -0
  80. package/dist/types/server/context.d.ts +12 -4
  81. package/dist/types/server/handle-store.d.ts +89 -46
  82. package/dist/types/server/request-context.d.ts +84 -9
  83. package/dist/types/suspense-audit-react.d.ts +26 -0
  84. package/dist/types/suspense-audit.d.ts +157 -0
  85. package/dist/types/testing/action-form.d.ts +34 -0
  86. package/dist/types/testing/build-versions.d.ts +32 -0
  87. package/dist/types/testing/dom.entry.d.ts +1 -1
  88. package/dist/types/testing/flight.entry.d.ts +3 -1
  89. package/dist/types/testing/index.d.ts +6 -0
  90. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  91. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  92. package/dist/types/testing/location-state-key.d.ts +5 -1
  93. package/dist/types/testing/render-route.d.ts +77 -6
  94. package/dist/types/testing/serve-shell-request.d.ts +55 -4
  95. package/dist/types/testing/shell-status.d.ts +14 -6
  96. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  97. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  98. package/dist/types/thenable-status.d.ts +6 -0
  99. package/dist/types/types/handler-context.d.ts +15 -5
  100. package/dist/types/types/route-entry.d.ts +5 -0
  101. package/dist/types/urls/path-helper.d.ts +18 -0
  102. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  103. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  104. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  105. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  106. package/dist/types/vite/discovery/state.d.ts +25 -0
  107. package/dist/types/vite/encryption-key.d.ts +18 -0
  108. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  109. package/dist/types/vite/plugin-types.d.ts +27 -0
  110. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  111. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  112. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  113. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  114. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  115. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  116. package/dist/types/vite/router-discovery.d.ts +6 -0
  117. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  118. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  119. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  120. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  121. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  122. package/dist/vite/index.js +1707 -931
  123. package/package.json +12 -3
  124. package/skills/cache-guide/SKILL.md +102 -37
  125. package/skills/caching/SKILL.md +56 -9
  126. package/skills/catalog.json +1 -1
  127. package/skills/client-urls/SKILL.md +16 -0
  128. package/skills/cloudflare/SKILL.md +133 -1
  129. package/skills/comparison/references/framework-comparison.md +17 -13
  130. package/skills/deployment-caching/SKILL.md +33 -12
  131. package/skills/document-cache/SKILL.md +16 -14
  132. package/skills/hooks/SKILL.md +25 -0
  133. package/skills/hooks/handle-and-actions.md +5 -0
  134. package/skills/hooks/navigation.md +38 -0
  135. package/skills/hooks/state.md +213 -11
  136. package/skills/hooks/urls.md +40 -1
  137. package/skills/host-router/SKILL.md +31 -0
  138. package/skills/intercept/SKILL.md +6 -0
  139. package/skills/loader/SKILL.md +25 -5
  140. package/skills/middleware/SKILL.md +2 -0
  141. package/skills/migrate-nextjs/SKILL.md +82 -9
  142. package/skills/ppr/SKILL.md +150 -48
  143. package/skills/prerender/SKILL.md +651 -10
  144. package/skills/rango/SKILL.md +9 -7
  145. package/skills/response-routes/SKILL.md +5 -5
  146. package/skills/route/SKILL.md +29 -0
  147. package/skills/router-setup/SKILL.md +15 -1
  148. package/skills/testing/SKILL.md +32 -27
  149. package/skills/testing/cache-prerender.md +18 -17
  150. package/skills/testing/client-components.md +193 -8
  151. package/skills/testing/handles.md +2 -0
  152. package/skills/testing/loader.md +1 -1
  153. package/skills/testing/middleware.md +1 -1
  154. package/skills/testing/render-handler.md +18 -18
  155. package/skills/testing/serve-shell-request.md +167 -22
  156. package/skills/testing/server-actions.md +17 -17
  157. package/skills/testing/server-tree.md +1 -1
  158. package/skills/typesafety/generated-files-and-cli.md +1 -1
  159. package/skills/use-cache/SKILL.md +25 -10
  160. package/skills/vercel/SKILL.md +5 -3
  161. package/skills/view-transitions/SKILL.md +9 -4
  162. package/src/bin/rango.ts +1 -1
  163. package/src/browser/event-controller.ts +119 -2
  164. package/src/browser/history-state.ts +39 -17
  165. package/src/browser/link-interceptor.ts +2 -2
  166. package/src/browser/navigation-bridge.ts +104 -41
  167. package/src/browser/navigation-store.ts +26 -0
  168. package/src/browser/navigation-transaction.ts +24 -12
  169. package/src/browser/network-error-handler.ts +2 -0
  170. package/src/browser/partial-update.ts +58 -14
  171. package/src/browser/prefetch/default-strategy.ts +22 -42
  172. package/src/browser/rango-state.ts +3 -1
  173. package/src/browser/react/Link.tsx +37 -43
  174. package/src/browser/react/NavigationProvider.tsx +85 -1
  175. package/src/browser/react/context.ts +30 -0
  176. package/src/browser/react/location-state-shared.ts +200 -34
  177. package/src/browser/react/location-state.ts +28 -77
  178. package/src/browser/react/use-action.ts +4 -1
  179. package/src/browser/react/use-handle.ts +3 -0
  180. package/src/browser/react/use-link-status.ts +4 -3
  181. package/src/browser/react/use-navigation.ts +4 -1
  182. package/src/browser/react/use-params.ts +4 -1
  183. package/src/browser/react/use-pathname.ts +4 -1
  184. package/src/browser/react/use-search-params.ts +5 -3
  185. package/src/browser/react/use-segments.ts +36 -54
  186. package/src/browser/rsc-router.tsx +93 -31
  187. package/src/browser/segment-reconciler.ts +3 -3
  188. package/src/browser/server-action-bridge.ts +43 -7
  189. package/src/browser/types.ts +11 -0
  190. package/src/build/generate-manifest.ts +17 -0
  191. package/src/build/merge-full-manifests.ts +5 -0
  192. package/src/build/route-trie.ts +1 -0
  193. package/src/cache/background-task.ts +61 -0
  194. package/src/cache/cache-key-utils.ts +91 -12
  195. package/src/cache/cache-policy.ts +11 -2
  196. package/src/cache/cache-runtime.ts +23 -23
  197. package/src/cache/cache-scope.ts +48 -36
  198. package/src/cache/cf/cf-cache-store.ts +91 -66
  199. package/src/cache/cf/cf-cache-types.ts +12 -4
  200. package/src/cache/document-cache.ts +35 -14
  201. package/src/cache/handle-snapshot.ts +118 -86
  202. package/src/cache/index.ts +5 -0
  203. package/src/cache/invalidation-order.ts +53 -0
  204. package/src/cache/memory-segment-store.ts +52 -1
  205. package/src/cache/shell-snapshot.ts +52 -5
  206. package/src/cache/store-scope.ts +36 -0
  207. package/src/cache/tag-invalidation.ts +7 -5
  208. package/src/cache/types.ts +90 -24
  209. package/src/cache/vercel/vercel-cache-store.ts +68 -26
  210. package/src/client-urls/client-root.tsx +1 -0
  211. package/src/client-urls/navigation.ts +10 -0
  212. package/src/client-urls/optimistic-location.ts +10 -4
  213. package/src/client.tsx +40 -3
  214. package/src/errors.ts +13 -6
  215. package/src/handles/deferred-resolution.ts +36 -0
  216. package/src/internal-suspense-audit.ts +12 -0
  217. package/src/loader-store.ts +4 -9
  218. package/src/outlet-provider.tsx +25 -1
  219. package/src/prerender/build-shell-capture.ts +11 -9
  220. package/src/prerender/cloudflare.ts +130 -0
  221. package/src/prerender/create-prerender-trigger.ts +780 -0
  222. package/src/prerender/index.ts +39 -0
  223. package/src/prerender/memory-prerender-store.ts +115 -0
  224. package/src/prerender/on-demand.ts +339 -0
  225. package/src/prerender/producer-guard.ts +66 -0
  226. package/src/prerender/shell-manifest-key.ts +12 -17
  227. package/src/prerender/warm-request.ts +154 -0
  228. package/src/prerender/warm.ts +193 -0
  229. package/src/prerender/writable-store.ts +278 -0
  230. package/src/prerender.ts +29 -0
  231. package/src/reverse.ts +1 -1
  232. package/src/route-content-wrapper.tsx +107 -13
  233. package/src/router/error-handling.ts +12 -12
  234. package/src/router/find-match.ts +1 -0
  235. package/src/router/handler-context.ts +2 -0
  236. package/src/router/intercept-resolution.ts +15 -8
  237. package/src/router/loader-resolution.ts +2 -10
  238. package/src/router/manifest.ts +2 -0
  239. package/src/router/match-middleware/background-revalidation.ts +2 -0
  240. package/src/router/match-middleware/cache-lookup.ts +239 -54
  241. package/src/router/navigation-snapshot.ts +25 -6
  242. package/src/router/pattern-matching.ts +14 -0
  243. package/src/router/prefetch-default.ts +4 -4
  244. package/src/router/prerender-match.ts +460 -352
  245. package/src/router/request-classification.ts +15 -12
  246. package/src/router/route-trie-builder.ts +9 -0
  247. package/src/router/router-context.ts +1 -0
  248. package/src/router/router-interfaces.ts +35 -1
  249. package/src/router/router-options.ts +45 -5
  250. package/src/router/segment-resolution/fresh.ts +20 -19
  251. package/src/router/segment-resolution/loader-cache.ts +110 -77
  252. package/src/router/segment-resolution/revalidation.ts +34 -24
  253. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  254. package/src/router/trie-matching.ts +3 -0
  255. package/src/router-versions.ts +60 -0
  256. package/src/router.ts +107 -1
  257. package/src/rsc/full-payload.ts +6 -2
  258. package/src/rsc/handler-context.ts +5 -0
  259. package/src/rsc/handler.ts +64 -7
  260. package/src/rsc/progressive-enhancement.ts +5 -1
  261. package/src/rsc/response-cache-serve.ts +10 -4
  262. package/src/rsc/rsc-rendering.ts +65 -12
  263. package/src/rsc/shell-build-manifest.ts +17 -12
  264. package/src/rsc/shell-capture-constants.ts +39 -7
  265. package/src/rsc/shell-capture.ts +144 -45
  266. package/src/rsc/shell-serve.ts +12 -6
  267. package/src/rsc/ssr-module-loader.ts +26 -0
  268. package/src/rsc/types.ts +12 -6
  269. package/src/segment-boundary-content.ts +25 -0
  270. package/src/segment-loader-promise.ts +28 -38
  271. package/src/segment-system.tsx +117 -57
  272. package/src/server/build-version-table.ts +130 -0
  273. package/src/server/context.ts +40 -17
  274. package/src/server/cookie-store.ts +5 -0
  275. package/src/server/handle-store.ts +357 -127
  276. package/src/server/request-context.ts +149 -12
  277. package/src/ssr/ssr-root.tsx +32 -13
  278. package/src/suspense-audit-react.tsx +98 -0
  279. package/src/suspense-audit.ts +1030 -0
  280. package/src/testing/action-form.ts +66 -0
  281. package/src/testing/build-versions.ts +64 -0
  282. package/src/testing/dispatch.ts +6 -0
  283. package/src/testing/dom.entry.ts +1 -0
  284. package/src/testing/flight-runtime.d.ts +27 -0
  285. package/src/testing/flight.entry.ts +4 -0
  286. package/src/testing/index.ts +22 -0
  287. package/src/testing/internal/context.ts +2 -1
  288. package/src/testing/internal/seed-vars.ts +14 -2
  289. package/src/testing/internal/server-modules.ts +32 -0
  290. package/src/testing/location-state-key.ts +18 -8
  291. package/src/testing/render-handler.ts +2 -1
  292. package/src/testing/render-route.tsx +293 -46
  293. package/src/testing/serve-shell-request.ts +262 -37
  294. package/src/testing/shell-status.ts +17 -6
  295. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  296. package/src/testing/vitest-stubs/version.ts +4 -1
  297. package/src/thenable-status.ts +10 -0
  298. package/src/types/handler-context.ts +15 -5
  299. package/src/types/route-entry.ts +6 -0
  300. package/src/urls/path-helper.ts +69 -9
  301. package/src/use-loader.tsx +57 -30
  302. package/src/vite/discovery/build-versions.ts +1281 -0
  303. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  304. package/src/vite/discovery/discover-routers.ts +15 -0
  305. package/src/vite/discovery/installed-packages.ts +159 -0
  306. package/src/vite/discovery/prerender-collection.ts +38 -1
  307. package/src/vite/discovery/router-versions-phase.ts +336 -0
  308. package/src/vite/discovery/shell-prerender-phase.ts +33 -43
  309. package/src/vite/discovery/state.ts +36 -1
  310. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  311. package/src/vite/encryption-key.ts +101 -9
  312. package/src/vite/inject-client-debug.ts +42 -20
  313. package/src/vite/plugin-types.ts +28 -0
  314. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  315. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  316. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  317. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  318. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  319. package/src/vite/plugins/version-plugin.ts +19 -24
  320. package/src/vite/plugins/version.d.ts +17 -2
  321. package/src/vite/plugins/virtual-entries.ts +39 -6
  322. package/src/vite/rango.ts +28 -3
  323. package/src/vite/router-discovery.ts +96 -13
  324. package/src/vite/utils/bundle-analysis.ts +39 -7
  325. package/src/vite/utils/client-chunks.ts +5 -1
  326. package/src/vite/utils/compare-strings.ts +9 -0
  327. package/src/vite/utils/package-resolution.ts +9 -0
  328. package/src/vite/utils/prerender-utils.ts +4 -1
  329. package/src/vite/utils/shared-utils.ts +1 -1
  330. package/dist/types/segment-content-promise.d.ts +0 -13
  331. package/src/segment-content-promise.ts +0 -67
@@ -42,6 +42,18 @@ export interface CacheGetResult {
42
42
  shouldRevalidate: boolean;
43
43
  }
44
44
 
45
+ /**
46
+ * Where an entry a store writes during one request can be read by a later
47
+ * request. `router.prerender()` warms a route only when the app store answers
48
+ * `"global"` or `"regional"`: a warm fills the store where it runs, which
49
+ * serves other traffic only when the store is shared beyond that place.
50
+ * - `"global"`: every location reads the same entries.
51
+ * - `"regional"`: each region has its own copy; a warm fills the region it
52
+ * runs in.
53
+ * - `"local"`: only the process, isolate or edge location that wrote it.
54
+ */
55
+ export type CacheStoreScope = "global" | "regional" | "local";
56
+
45
57
  /**
46
58
  * Low-level segment cache store interface.
47
59
  *
@@ -49,9 +61,22 @@ export interface CacheGetResult {
49
61
  * The store deals with serialized data - RSC serialization is handled
50
62
  * by the cache provider layer.
51
63
  *
64
+ * A store that outlives the process has to keep one build from reading what
65
+ * other code wrote. The built-in stores prefix their keys with the serving
66
+ * router's cache versions; a custom store reads the same pair with
67
+ * `getCacheVersions()` (`@rangojs/router/cache`): `data` for segment entries
68
+ * and items, `document` for responses and shells, and no version on its
69
+ * tag-invalidation records, so an invalidation reaches every version.
70
+ *
52
71
  * @typeParam TEnv - Platform bindings type (e.g., Cloudflare env)
53
72
  */
54
73
  export interface SegmentCacheStore<TEnv = unknown> {
74
+ /**
75
+ * Where an entry this store writes can be read ({@link CacheStoreScope}).
76
+ * Absent: unknown to the router, treated as `"local"`.
77
+ */
78
+ readonly scope?: CacheStoreScope;
79
+
55
80
  /**
56
81
  * The store honors getShell(..., { claimRevalidation: false }) without
57
82
  * claiming an SWR lock. Navigation replay requires this opt-in because it
@@ -161,6 +186,11 @@ export interface SegmentCacheStore<TEnv = unknown> {
161
186
  * @param ttl - Time-to-live in seconds
162
187
  * @param swr - Optional stale-while-revalidate window in seconds
163
188
  * @param tags - Optional cache tags for invalidation
189
+ *
190
+ * The built-in stores read the start of the render from the `Response`
191
+ * object itself (#1068). A custom store, or a wrapper that rebuilds the
192
+ * `Response` before delegating, stamps write time. The write gate
193
+ * (`predatesInvalidation`) runs before this call either way.
164
194
  */
165
195
  putResponse?(
166
196
  key: string,
@@ -415,13 +445,14 @@ export interface ShellCacheEntry {
415
445
  /** React.version captured at prerender time; the read-time invalidation gate. */
416
446
  reactVersion: string;
417
447
  /**
418
- * Build version captured at prerender time (the RSC handler's `version` —
419
- * the `@rangojs/router:version` build stamp by default, bumped per build and
420
- * on dev RSC-module edits). The second read-time gate: a persistent shared
448
+ * Build version captured at prerender time: the RSC handler's `version`,
449
+ * which is the router's DOCUMENT version (router-versions.ts) — a hash of
450
+ * its server code and the client asset names by default, a stamp bumped on
451
+ * dev RSC-module edits. The second read-time gate: a persistent shared
421
452
  * store (KV/runtime-cache) survives deploys, and an app-code change that
422
453
  * keeps the same React version would otherwise leave a stale-build
423
- * prelude+postponed live under the same key. A custom store returns it as
424
- * putShell received it.
454
+ * prelude+postponed live under the same key. A deploy that changes neither
455
+ * keeps the entry. A custom store returns it as putShell received it.
425
456
  */
426
457
  buildVersion: string;
427
458
  /**
@@ -443,9 +474,10 @@ export interface ShellCacheEntry {
443
474
  * bake-lane loader pins (pruned to what a HIT reads, issue #941). Replaying
444
475
  * them on a HIT keeps the freshly rendered hydration payload equal to the
445
476
  * frozen prelude after the underlying cache entries have drifted; every
446
- * other read on a HIT, a hole's included, reads the store. Empty when the capture
447
- * recorded nothing (a prerender-served capture without bake-lane loaders,
448
- * a tombstone). A custom store returns it as putShell received it. See
477
+ * other read on a HIT, a hole's included, reads the store. A
478
+ * prerender-served capture holds a `handles` record instead of a doc record
479
+ * (ShellSnapshotHandlesValue). Empty for a tombstone. A custom store
480
+ * returns it as putShell received it. See
449
481
  * docs/design/ppr-shell-resume.md ("the capture data snapshot").
450
482
  */
451
483
  snapshot: ShellSnapshotRecord[];
@@ -606,12 +638,26 @@ export interface ShellReadStats {
606
638
 
607
639
  /**
608
640
  * The families a shell snapshot pins: the doc segment record (recorded by
609
- * RecordingShellStore) and the settled CONTAINER of each bake-lane loader
610
- * (lane rule: see resolveLoaderData, loader-cache.ts). No cache read is
611
- * pinned: a HIT's holes, and a bake-lane loader body that runs on the HIT,
612
- * read the store.
641
+ * RecordingShellStore), the settled CONTAINER of each bake-lane loader
642
+ * (lane rule: see resolveLoaderData, loader-cache.ts), and a prerender-served
643
+ * entry's `handles` record. No cache read is pinned: a HIT's holes, and a bake-lane
644
+ * loader body that runs on the HIT, read the store.
613
645
  */
614
- export type ShellSnapshotFamily = "segment" | "loader";
646
+ export type ShellSnapshotFamily = "segment" | "loader" | "handles";
647
+
648
+ /**
649
+ * The stored form of the `handles` family: the handle arrays of a
650
+ * prerender-served capture that hold a loader-owned push, in the doc record's
651
+ * format (CachedEntryData `handles` + `handleOwners`). A Prerender + ppr
652
+ * route's handler layer comes from the prerender store, whose build-time
653
+ * entry has no loader push, so the entry keeps these for the HIT to restore
654
+ * as a doc record's (issue #1057, yieldFromStore in cache-lookup.ts). An
655
+ * entry written before it (v0.21) has none and serves as it did.
656
+ */
657
+ export type ShellSnapshotHandlesValue = Pick<
658
+ CachedEntryData,
659
+ "handles" | "handleOwners"
660
+ >;
615
661
 
616
662
  /**
617
663
  * The stored form of a loader-family snapshot value: the bake-lane loader's
@@ -646,17 +692,18 @@ export interface ShellSnapshotLoaderValue {
646
692
  }
647
693
 
648
694
  /**
649
- * One snapshot record: the doc segment record or a bake-lane loader pin.
650
- * `value` carries it in its stored/serialized shape so it round-trips
651
- * through a JSON-serializing store (KV, CF, Vercel) with the rest of the
652
- * ShellCacheEntry:
695
+ * One snapshot record: the doc segment record, a bake-lane loader pin, or a
696
+ * prerender-served entry's handle record. `value` carries it in its
697
+ * stored/serialized shape so it round-trips through a JSON-serializing store
698
+ * (KV, CF, Vercel) with the rest of the ShellCacheEntry:
653
699
  * - `segment` -> {@link CachedEntryData} (already JSON-able)
654
700
  * - `loader` -> {@link ShellSnapshotLoaderValue}
701
+ * - `handles` -> {@link ShellSnapshotHandlesValue}
655
702
  */
656
703
  export interface ShellSnapshotRecord {
657
704
  family: ShellSnapshotFamily;
658
705
  key: string;
659
- value: CachedEntryData | ShellSnapshotLoaderValue;
706
+ value: CachedEntryData | ShellSnapshotLoaderValue | ShellSnapshotHandlesValue;
660
707
  }
661
708
 
662
709
  /**
@@ -671,6 +718,17 @@ export interface CacheItemOptions {
671
718
  swr?: number;
672
719
  /** Cache tags for invalidation */
673
720
  tags?: string[];
721
+ /**
722
+ * Ms since epoch when the execution that produced the value started (#1068).
723
+ * A store stamps a tagged entry with it instead of its write time. Later
724
+ * requests then reject an entry an invalidation landed under in
725
+ * CFCacheStore with KV markers; MemorySegmentCacheStore refuses the write.
726
+ * Not rejected: VercelCacheStore (a read compares the stamp with the
727
+ * invalidating request's mask only) and CFCacheStore L1 hits in purge mode
728
+ * or without KV. Absent or not positive: write time. A custom store may
729
+ * ignore it.
730
+ */
731
+ startedAt?: number;
674
732
  }
675
733
 
676
734
  /**
@@ -718,7 +776,12 @@ export interface CachedEntryData {
718
776
  expiresAt: number;
719
777
  /** Cache tags for invalidation */
720
778
  tags?: string[];
721
- /** Timestamp (ms since epoch) when tags were attached, for distributed invalidation */
779
+ /**
780
+ * Timestamp (ms since epoch) the entry's tags are valid as of, for
781
+ * distributed invalidation. The writer sets it to the start of the render
782
+ * that produced the record; a store stamps write time when it is absent
783
+ * (#1068). Which reads reject on it: see `CacheItemOptions.startedAt`.
784
+ */
722
785
  taggedAt?: number;
723
786
  }
724
787
 
@@ -761,10 +824,13 @@ export type SegmentHandleData = Record<string, unknown[]>;
761
824
  * index (null: not owned). Only a PPR shell capture writes owners: the
762
825
  * settled pushes of the loader bodies it ran (an `ssr: false` loader's own,
763
826
  * the loaders it awaits, and its own cache() replays), which the record keeps
764
- * because the prelude rendered them. A document HIT restores a bake-lane
765
- * owner's value through HandleStore.pushRestored, and it stands against a run
766
- * of its loader there; a live-lane owner's (a hole), and every owner's on
767
- * any other replay of the record (a client navigation), go through
768
- * pushReplayed, so a run of the loader replaces it with its live push.
827
+ * because the prelude rendered them. A restore of the record
828
+ * (handle-snapshot.ts restoreHandles) keeps an owner's value where the
829
+ * request serves that loader from the pin stored with the record
830
+ * (HandleStore.pushRestored: it stands against a run of the loader), on a
831
+ * document HIT and on a navigation replay alike. Every other owner's value
832
+ * is a placeholder (pushPlaceholder): a live-lane loader's, and every
833
+ * owner's where the record has no pin for it. The loader's run, or its own
834
+ * cache() entry, replaces it.
769
835
  */
770
836
  export type HandleOwners = Record<string, Record<string, (string | null)[]>>;
@@ -23,7 +23,8 @@
23
23
  * 3. The platform guardrails: the 2 MB per-item ceiling (oversized writes
24
24
  * silently no-op on Vercel, so we skip + report), the per-item tag cap, and
25
25
  * cross-deploy non-reconciliation (TTL/tag updates are not reconciled across
26
- * deployments - bake a build id into `version` or the getCache namespace).
26
+ * deployments - every key carries the serving router's cache version, see
27
+ * toStoreKey).
27
28
  *
28
29
  * Dependency stance: this module imports NOTHING from `@vercel/functions`. The
29
30
  * runtime cache handle (and `waitUntil`) are injected through the constructor and
@@ -34,6 +35,7 @@
34
35
  */
35
36
 
36
37
  import type {
38
+ CacheStoreScope,
37
39
  SegmentCacheStore,
38
40
  CachedEntryData,
39
41
  CacheDefaults,
@@ -52,6 +54,10 @@ import {
52
54
  _getRequestContext,
53
55
  type RequestContext,
54
56
  } from "../../server/request-context.js";
57
+ import {
58
+ versionKeyPrefix,
59
+ type KeyVersion,
60
+ } from "../../server/build-version-table.js";
55
61
  import { isPerClientSignalHeader } from "../../browser/cookie-name.js";
56
62
  import {
57
63
  resolveTtl,
@@ -101,6 +107,7 @@ import {
101
107
  type MarkerFetch,
102
108
  type MarkerMemoOutcome,
103
109
  } from "../isolate-tag-memo.js";
110
+ import { responseStartedAt, taggedStamp } from "../invalidation-order.js";
104
111
 
105
112
  /**
106
113
  * Minimal structural shape of the Vercel Runtime Cache returned by `getCache()`
@@ -170,6 +177,19 @@ const REVALIDATION_LOCK_MS = 30_000;
170
177
  * the suffix; `rg:` namespaces every Rango entry. `h` is the PPR shell tier. */
171
178
  type CacheFamily = "s" | "i" | "r" | "h" | "tm";
172
179
 
180
+ /**
181
+ * Which of the serving router's versions each family's keys carry
182
+ * (versionKeyPrefix). A tag marker carries none: an invalidation applies to
183
+ * every version, including one a rollback brings back.
184
+ */
185
+ const FAMILY_VERSION: Record<CacheFamily, KeyVersion> = {
186
+ s: "data",
187
+ i: "data",
188
+ r: "document",
189
+ h: "document",
190
+ tm: null,
191
+ };
192
+
173
193
  /**
174
194
  * TTL for tag-invalidation marker entries ("tm" family), written by
175
195
  * invalidateTags for isTagsInvalidatedSince (the build-shell read-through,
@@ -177,7 +197,7 @@ type CacheFamily = "s" | "i" | "r" | "h" | "tm";
177
197
  * The platform's expireTag() DELETES tagged entries (no queryable
178
198
  * history), so the markers are rango's own record of "tag X was invalidated
179
199
  * at T". One year: runtime tagged-shell retention is capped to this lifetime,
180
- * while buildVersion retires build shells on the next deploy. An expired marker
200
+ * while a build shell is replaced by the next build's. An expired marker
181
201
  * therefore cannot resurrect a runtime shell that outlived its invalidation.
182
202
  */
183
203
  const TAG_MARKER_TTL_SECONDS = 365 * 24 * 60 * 60;
@@ -299,14 +319,20 @@ export type VercelCacheDebug =
299
319
  export interface VercelCacheStoreOptions<TEnv = unknown> {
300
320
  /**
301
321
  * The Vercel Runtime Cache handle - `getCache()` from `@vercel/functions`.
302
- * Required. Construct it with a build-hash namespace to bust stale-shaped
303
- * entries across deployments, since Vercel does not reconcile TTL/tags between
304
- * deploys:
322
+ * Required.
305
323
  *
306
324
  * ```ts
307
325
  * import { getCache } from "@vercel/functions";
308
- * new VercelCacheStore({ cache: getCache({ namespace: BUILD_ID }) });
326
+ * new VercelCacheStore({ cache: getCache() });
309
327
  * ```
328
+ *
329
+ * Vercel does not reconcile entries between deployments; the store does it
330
+ * through its keys, which carry the serving router's cache versions (see
331
+ * `version`). So a deploy that changes a router's code stops reading that
332
+ * router's old entries, and a deploy that does not keeps them. A namespace
333
+ * that changes per deployment (`getCache({ namespace: VERCEL_DEPLOYMENT_ID })`)
334
+ * still works and clears the cache on every deploy, which is what you want
335
+ * only if you need that.
310
336
  */
311
337
  cache: VercelRuntimeCache;
312
338
 
@@ -336,9 +362,15 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
336
362
  ) => string | Promise<string>;
337
363
 
338
364
  /**
339
- * Build/version id folded into every stored key as `v/{version}/...`. A second
340
- * cross-deploy busting layer in addition to (or instead of) the getCache
341
- * namespace. Changing it invalidates everything this store wrote previously.
365
+ * Version override, folded into every versioned key as `v/{version}/...`.
366
+ * When set, this exact value is used for cached data and stored HTML alike;
367
+ * changing it makes everything this store wrote before unreachable.
368
+ *
369
+ * Leave it unset for the default: the store keys with the versions of the
370
+ * router serving the request, as CFCacheStore does. Segments and `"use
371
+ * cache"` items use the router's data version; responses and PPR shells use
372
+ * its document version (see `createRouter({ version })`). Tag markers are
373
+ * never versioned, so an invalidation applies to every version.
342
374
  */
343
375
  version?: string;
344
376
 
@@ -424,9 +456,15 @@ function isRecord(value: unknown): value is Record<string, unknown> {
424
456
  return typeof value === "object" && value !== null;
425
457
  }
426
458
 
427
- /** The envelope's `ta` write stamp, on tagged entries only. */
428
- function tagStamp(tags: string[]): { ta?: number } {
429
- return tags.length > 0 ? { ta: Date.now() } : {};
459
+ /**
460
+ * The envelope's `ta` stamp, on tagged entries only: the producing execution's
461
+ * start when the writer passed it, else write time (#1068). Only the request
462
+ * mask reads it (expireTag keeps no queryable history), so it decides whether
463
+ * the invalidating request's own late write is masked.
464
+ */
465
+ function tagStamp(tags: string[], startedAt?: number): { ta?: number } {
466
+ const ta = taggedStamp(tags, startedAt);
467
+ return ta === undefined ? {} : { ta };
430
468
  }
431
469
 
432
470
  /**
@@ -447,8 +485,8 @@ function tagStamp(tags: string[]): { ta?: number } {
447
485
  * that propagates expireTag rejections does get the strict guarantee, which is
448
486
  * what the unit suite exercises.)
449
487
  *
450
- * Key hashing / family isolation. The store namespaces its three value tiers as
451
- * `rg:{s|i|r}:{key}` so segment, `"use cache"`, and response entries occupy
488
+ * Key hashing / family isolation. The store namespaces its value tiers as
489
+ * `rg:{s|i|r|h}:{key}` so segment, `"use cache"`, response and shell entries occupy
452
490
  * disjoint keyspaces. That separation is only as strong as the cache's
453
491
  * `keyHashFunction`: `getCache`'s default is djb2, which folds every key to 32
454
492
  * bits (8 hex chars), so at a large live-key count a collision can let one
@@ -464,11 +502,9 @@ function tagStamp(tags: string[]): { ta?: number } {
464
502
  *
465
503
  * // One handle per process: getCache() resolves the platform cache on every
466
504
  * // call, and the PPR shell and tag-marker memos are kept per handle.
467
- * // `process.env` (not `import.meta.env`, which needs a VITE_ prefix and
468
- * // would be undefined here) so cross-deploy busting via `version` works.
469
- * const runtimeCache = getCache({
470
- * namespace: process.env.VERCEL_DEPLOYMENT_ID,
471
- * });
505
+ * // No deployment id in the namespace: the store's keys carry the serving
506
+ * // router's cache versions, so a deploy keeps what its code did not change.
507
+ * const runtimeCache = getCache();
472
508
  *
473
509
  * export const router = createRouter({
474
510
  * cache: () => ({
@@ -484,6 +520,12 @@ function tagStamp(tags: string[]): { ta?: number } {
484
520
  export class VercelCacheStore<
485
521
  TEnv = unknown,
486
522
  > implements SegmentCacheStore<TEnv> {
523
+ /**
524
+ * SegmentCacheStore.scope: the Runtime Cache is per region. A
525
+ * router.prerender() warm fills the region it runs in, which is all traffic
526
+ * only when the project's functions run in one region.
527
+ */
528
+ readonly scope: CacheStoreScope = "regional";
487
529
  readonly supportsPassiveShellReads: true = true;
488
530
  readonly defaults?: CacheDefaults;
489
531
  readonly keyGenerator?: (
@@ -493,7 +535,8 @@ export class VercelCacheStore<
493
535
 
494
536
  private readonly cache: VercelRuntimeCache;
495
537
  private readonly waitUntil?: (promise: Promise<unknown>) => void;
496
- private readonly version?: string;
538
+ /** The `version` option: used for every versioned family when set. */
539
+ private readonly explicitVersion?: string;
497
540
  private readonly maxItemBytes: number;
498
541
  private readonly memo: ResolvedShellMemoOptions;
499
542
  private readonly handleMemos: VercelHandleMemos;
@@ -518,7 +561,7 @@ export class VercelCacheStore<
518
561
  this.waitUntil = options.waitUntil;
519
562
  this.defaults = options.defaults;
520
563
  this.keyGenerator = options.keyGenerator;
521
- this.version = options.version;
564
+ this.explicitVersion = options.version;
522
565
  this.maxItemBytes = options.maxItemBytes ?? VERCEL_MAX_ITEM_BYTES;
523
566
  this.maxShellEntryBytes = Math.floor((this.maxItemBytes * 3) / 4);
524
567
  this.memo = resolveShellMemoOptions(options.memo, {
@@ -620,7 +663,7 @@ export class VercelCacheStore<
620
663
  d,
621
664
  s: staleAt,
622
665
  e: expiresAt,
623
- ...tagStamp(safeTags),
666
+ ...tagStamp(safeTags, data.taggedAt),
624
667
  };
625
668
  await this.write(
626
669
  this.toStoreKey(key, "s"),
@@ -768,7 +811,7 @@ export class VercelCacheStore<
768
811
  s: staleAt,
769
812
  e: expiresAt,
770
813
  t: safeTags.length > 0 ? safeTags : undefined,
771
- ...tagStamp(safeTags),
814
+ ...tagStamp(safeTags, responseStartedAt(response)),
772
815
  };
773
816
  await this.write(
774
817
  this.toStoreKey(key, "r"),
@@ -874,7 +917,7 @@ export class VercelCacheStore<
874
917
  s: staleAt,
875
918
  e: expiresAt,
876
919
  t: safeTags.length > 0 ? safeTags : undefined,
877
- ...tagStamp(safeTags),
920
+ ...tagStamp(safeTags, options?.startedAt),
878
921
  };
879
922
  await this.write(
880
923
  this.toStoreKey(key, "i"),
@@ -1469,8 +1512,7 @@ export class VercelCacheStore<
1469
1512
  // cross-family one reads as corrupt. See the class doc for passing a wider
1470
1513
  // hash (sha256) when many keys are live.
1471
1514
  private toStoreKey(key: string, family: CacheFamily): string {
1472
- const versionPrefix = this.version ? `v/${this.version}/` : "";
1473
- return `${versionPrefix}rg:${family}:${key}`;
1515
+ return `${versionKeyPrefix(this.explicitVersion, FAMILY_VERSION[family])}rg:${family}:${key}`;
1474
1516
  }
1475
1517
 
1476
1518
  private async write(
@@ -172,6 +172,7 @@ export function ClientUrlsRoot({
172
172
  params: presented.params,
173
173
  pathname: presented.pathname,
174
174
  search: presented.search,
175
+ state: presented.state,
175
176
  },
176
177
  }
177
178
  : null,
@@ -17,6 +17,8 @@ export interface ClientUrlNavigationIntent {
17
17
  /** Absolute destination pathname (mount included) and search ("?..." or ""). */
18
18
  readonly pathname: string;
19
19
  readonly search: string;
20
+ /** `history.state` the navigation will push (OptimisticLocation.state). */
21
+ readonly state: unknown;
20
22
  /**
21
23
  * transition({ when }) gated this navigation off at the swap: the
22
24
  * destination presents urgently instead of in a transition lane.
@@ -37,6 +39,8 @@ export interface ClientUrlDestination {
37
39
  * params minus these are the mount's (include prefix) params.
38
40
  */
39
41
  readonly originLocalParamNames: readonly string[];
42
+ /** `history.state` the navigation will push (ClientUrlNavigationIntent.state). */
43
+ readonly state: unknown;
40
44
  }
41
45
 
42
46
  interface ActiveClientUrlGroup {
@@ -124,6 +128,8 @@ export function registerClientUrlGroup(
124
128
  export function beginClientUrlNavigation(
125
129
  targetUrl: URL,
126
130
  signal: AbortSignal,
131
+ /** Builds the entry state; called only when the group presents the target. */
132
+ buildState?: () => unknown,
127
133
  decideTransition?: (destination: ClientUrlDestination) => boolean,
128
134
  ): ClientUrlNavigationPresentation | null {
129
135
  const group = activeGroup;
@@ -149,6 +155,8 @@ export function beginClientUrlNavigation(
149
155
  : undefined;
150
156
  if (canonicalName && activeInterceptTargets.has(canonicalName)) return null;
151
157
 
158
+ const state = buildState?.();
159
+
152
160
  // A same-route intent never swaps (ClientUrlsRoot), so transition({ when })
153
161
  // decides at the canonical commit; a cross-route one decides here, at the
154
162
  // navigation's first presentation.
@@ -163,6 +171,7 @@ export function beginClientUrlNavigation(
163
171
  routeName: canonicalName,
164
172
  when: typeof when === "function" ? when : undefined,
165
173
  originLocalParamNames: Object.keys(originMatch?.params ?? {}),
174
+ state,
166
175
  });
167
176
  }
168
177
 
@@ -171,6 +180,7 @@ export function beginClientUrlNavigation(
171
180
  params: match.params,
172
181
  pathname: targetUrl.pathname,
173
182
  search: targetUrl.search,
183
+ state,
174
184
  transitionGatedOff,
175
185
  };
176
186
  group.intent = intent;
@@ -6,16 +6,22 @@ import { createContext, type Context } from "react";
6
6
  * Route identity of an optimistically rendered clientUrls() destination:
7
7
  * the values the local trie match produced for the URL the user navigated
8
8
  * to. Provided by ClientUrlsRoot around the optimistic branch ONLY, so
9
- * useParams / usePathname / useSearchParams inside that branch describe the
10
- * route being rendered while the same hooks in chrome outside it keep the
11
- * committed location until the canonical response commits (or redirects, in
12
- * which case the branch — and these values — are discarded).
9
+ * useParams / usePathname / useSearchParams / useLocationState inside that
10
+ * branch describe the route being rendered while the same hooks in chrome
11
+ * outside it keep the committed location until the canonical response commits
12
+ * (or redirects, in which case the branch — and these values — are discarded).
13
13
  */
14
14
  export interface OptimisticLocation {
15
15
  readonly params: Readonly<Record<string, string>>;
16
16
  readonly pathname: string;
17
17
  /** Search string including the leading "?" (or ""). */
18
18
  readonly search: string;
19
+ /**
20
+ * `history.state` the navigation will push: its `state` option, without
21
+ * what the server adds (that arrives with the canonical commit). History
22
+ * still holds the entry being left, so useLocationState reads this instead.
23
+ */
24
+ readonly state: unknown;
19
25
  }
20
26
 
21
27
  export const OptimisticLocationContext: Context<OptimisticLocation | null> =
package/src/client.tsx CHANGED
@@ -3,9 +3,11 @@
3
3
  import {
4
4
  Component,
5
5
  createElement,
6
+ isValidElement,
6
7
  useContext,
7
8
  useMemo,
8
9
  Suspense,
10
+ type ComponentProps,
9
11
  type ReactNode,
10
12
  } from "react";
11
13
  // Not `export * as Html`: plugin-rsc's "use client" proxy transform only
@@ -19,12 +21,15 @@ import {
19
21
  type ResolvedSegment,
20
22
  } from "./types";
21
23
  import {
24
+ AuditedLoaderBoundary,
25
+ AuditedRouteContent,
22
26
  RouteContentWrapper,
23
27
  LoaderBoundary,
24
28
  } from "./route-content-wrapper.js";
25
- import { OutletProvider } from "./outlet-provider.js";
29
+ import { AuditedOutletProvider, OutletProvider } from "./outlet-provider.js";
30
+ import { INTERNAL_RANGO_SUSPENSE_AUDIT } from "./internal-suspense-audit.js";
26
31
  import { MountContextProvider } from "./browser/react/mount-context.js";
27
- import { getMemoizedContentPromise } from "./segment-content-promise.js";
32
+ import { getBoundaryContent } from "./segment-boundary-content.js";
28
33
 
29
34
  /**
30
35
  * Render the content for a named parallel/intercept slot segment.
@@ -39,7 +44,7 @@ function renderSlotContent(segment: ResolvedSegment | null): ReactNode {
39
44
  const content: ReactNode =
40
45
  segment.loading || segment.component instanceof Promise ? (
41
46
  <RouteContentWrapper
42
- content={getMemoizedContentPromise(segment.component)}
47
+ content={getBoundaryContent(segment.component)}
43
48
  fallback={segment.loading}
44
49
  segmentId={segment.id}
45
50
  />
@@ -83,6 +88,10 @@ function renderSlotContent(segment: ResolvedSegment | null): ReactNode {
83
88
  result = content;
84
89
  }
85
90
 
91
+ if (process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT) {
92
+ result = auditedSlot(result);
93
+ }
94
+
86
95
  if (segment.mountPath) {
87
96
  return (
88
97
  <MountContextProvider value={segment.mountPath}>
@@ -94,6 +103,34 @@ function renderSlotContent(segment: ResolvedSegment | null): ReactNode {
94
103
  return result;
95
104
  }
96
105
 
106
+ // Dev only (suspense-audit.ts): the boundaries renderSlotContent built, as
107
+ // their audited components with the same props. After the fact, not a choice
108
+ // per JSX element: a build must fold to the product elements alone (see
109
+ // AuditedRouteContent, route-content-wrapper.tsx).
110
+ function auditedSlot(node: ReactNode): ReactNode {
111
+ if (!isValidElement(node)) return node;
112
+ const props = node.props as Record<string, any>;
113
+ if (node.type === OutletProvider) {
114
+ return createElement(AuditedOutletProvider, {
115
+ ...(props as ComponentProps<typeof OutletProvider>),
116
+ content: auditedSlot(props.content),
117
+ });
118
+ }
119
+ if (node.type === LoaderBoundary) {
120
+ return createElement(AuditedLoaderBoundary, {
121
+ ...(props as ComponentProps<typeof LoaderBoundary>),
122
+ children: auditedSlot(props.children),
123
+ });
124
+ }
125
+ if (node.type === RouteContentWrapper) {
126
+ return createElement(
127
+ AuditedRouteContent,
128
+ props as ComponentProps<typeof RouteContentWrapper>,
129
+ );
130
+ }
131
+ return node;
132
+ }
133
+
97
134
  function useSlotSegment(
98
135
  context: OutletContextValue | null,
99
136
  name: `@${string}` | undefined,
package/src/errors.ts CHANGED
@@ -356,6 +356,18 @@ export function invariant(condition: any, message: string): asserts condition {
356
356
  * @param error - Error to sanitize
357
357
  * @returns Response suitable for sending to client
358
358
  */
359
+ /**
360
+ * Vite replaces import.meta.env.DEV at compile time. The fallback covers
361
+ * non-Vite environments (plain Node, test runners without Vite transforms).
362
+ * SECURITY: fail closed — default to production when the environment is ambiguous.
363
+ */
364
+ export function isDevEnvironment(): boolean {
365
+ return (
366
+ (import.meta as any).env?.DEV ??
367
+ globalThis.process?.env?.NODE_ENV === "development"
368
+ );
369
+ }
370
+
359
371
  export function sanitizeError(error: unknown): Response {
360
372
  // CRITICAL: Consume stack trace to prevent memory leaks
361
373
  if (error && typeof error === "object" && "stack" in error) {
@@ -367,12 +379,7 @@ export function sanitizeError(error: unknown): Response {
367
379
  return error;
368
380
  }
369
381
 
370
- // Vite replaces import.meta.env.DEV at compile time. The fallback covers
371
- // non-Vite environments (plain Node, test runners without Vite transforms).
372
- // SECURITY: fail closed — default to production when the environment is ambiguous.
373
- const isDev =
374
- (import.meta as any).env?.DEV ??
375
- globalThis.process?.env?.NODE_ENV === "development";
382
+ const isDev = isDevEnvironment();
376
383
 
377
384
  if (isDev) {
378
385
  // Development: Send full error details for debugging