@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa

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 (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. package/src/router/middleware-cookies.ts +0 -55
@@ -7,56 +7,226 @@
7
7
  */
8
8
 
9
9
  import {
10
- requireRequestContext,
10
+ getRequestContext,
11
11
  setRequestContextParams,
12
- getLocationState,
12
+ runWithRequestContext,
13
13
  } from "../server/request-context.js";
14
- import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
14
+ import {
15
+ SeededShellStore,
16
+ buildShellLoaderSeed,
17
+ } from "../cache/shell-snapshot.js";
15
18
  import { appendMetric } from "../router/metrics.js";
19
+ import { observePhase, PHASES } from "../router/instrument.js";
16
20
  import { getSSRSetup, isRscRequest } from "./ssr-setup.js";
17
21
  import type { RscPayload } from "./types.js";
18
- import type { MatchResult } from "../types.js";
22
+ import type { SSRModule } from "./types.js";
23
+ import type { RequestContext } from "../server/request-context.js";
19
24
  import {
20
25
  createResponseWithMergedHeaders,
21
26
  createSimpleRedirectResponse,
27
+ attachLocationStateIfPresent,
22
28
  } from "./helpers.js";
23
29
  import type { HandlerContext } from "./handler-context.js";
30
+ import { gateTransitions } from "./transition-gate.js";
31
+ import { buildFullPayload } from "./full-payload.js";
32
+ import {
33
+ scheduleShellCapture,
34
+ type ShellCaptureDescriptor,
35
+ } from "./shell-capture.js";
36
+ import {
37
+ SHELL_STATUS_HEADER,
38
+ resolvePprConfig,
39
+ buildShellKey,
40
+ isValidShellHit,
41
+ hasIntactShellPayload,
42
+ base64ToBytes,
43
+ hasShellFamily,
44
+ warnShellStoreMissingOnce,
45
+ warnPprNonceActiveOnce,
46
+ } from "./shell-serve.js";
47
+ import { contextGet } from "../context-var.js";
48
+ import {
49
+ resolveSameOriginRedirect,
50
+ safeSameOriginLanding,
51
+ } from "../redirect-origin.js";
52
+ import { nonce as nonceToken } from "./nonce.js";
53
+ import { reportCacheError } from "../cache/cache-error.js";
54
+ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
55
+ import type { ShellCacheEntry } from "../cache/types.js";
24
56
 
25
- export async function handleRscRendering<TEnv>(
57
+ export function handleRscRendering<TEnv>(
26
58
  ctx: HandlerContext<TEnv>,
27
59
  request: Request,
28
60
  env: TEnv,
29
61
  url: URL,
30
62
  isPartial: boolean,
31
- handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
63
+ handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
32
64
  nonce: string | undefined,
33
65
  ): Promise<Response> {
34
- const reqCtx = requireRequestContext();
66
+ // Instrument the whole render phase once through the unified API: it records
67
+ // the "render:total" perf metric AND opens the "rango.render" span from the
68
+ // same boundary (match -> serialize -> SSR), so the two surfaces agree.
69
+ // Loaders kicked off during matching nest under the span; the SSR HTML pass
70
+ // below opens "rango.ssr" the same way.
71
+ return observePhase(PHASES.render, () =>
72
+ handleRscRenderingInner(
73
+ ctx,
74
+ request,
75
+ env,
76
+ url,
77
+ isPartial,
78
+ handleStore,
79
+ nonce,
80
+ ),
81
+ );
82
+ }
83
+
84
+ async function handleRscRenderingInner<TEnv>(
85
+ ctx: HandlerContext<TEnv>,
86
+ request: Request,
87
+ env: TEnv,
88
+ url: URL,
89
+ isPartial: boolean,
90
+ handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
91
+ nonce: string | undefined,
92
+ ): Promise<Response> {
93
+ const reqCtx = getRequestContext();
35
94
 
36
95
  let payload: RscPayload;
37
96
  let hasInterceptSlots = false;
38
97
 
39
- // Shared by the partial-fallback and full-render paths. The partial-success
40
- // payload below is intentionally different (omits rootLayout/theme, adds slots).
41
- const buildFullPayload = (m: MatchResult): RscPayload => ({
42
- metadata: {
43
- pathname: url.pathname,
44
- routerId: ctx.router.id,
45
- basename: ctx.router.basename,
46
- segments: m.segments,
47
- matched: m.matched,
48
- diff: m.diff,
49
- resolvedIds: m.resolvedIds,
50
- params: m.params,
51
- isPartial: false,
52
- rootLayout: ctx.router.rootLayout,
53
- handles: handleStore.stream(),
54
- version: ctx.version,
55
- prefetchCacheTTL: ctx.router.prefetchCacheTTL,
56
- themeConfig: ctx.router.themeConfig,
57
- initialTheme: reqCtx.theme,
58
- },
59
- });
98
+ // --- Axis 2: integrated PPR shell serve (docs/design/ppr-shell-resume.md) ---
99
+ //
100
+ // COMMIT POINT. This function is the render pass executeRender wraps, so it runs
101
+ // strictly AFTER the whole middleware chain — the global router.use() chain AND
102
+ // route DSL middleware() both wrap it. Any middleware rejection/redirect/401 has
103
+ // already returned before this line, which is what makes a shared shell safe:
104
+ // not a single shell byte can precede a guard decision, on MISS or HIT.
105
+ //
106
+ // PPR is opt-in per PAGE ROUTE via the `ppr` path option (read off the classified
107
+ // route snapshot — the same matched entry match() will resolve). No `ppr` option
108
+ // means pure axis 1: no store read, no capture, no logs, zero cost.
109
+ //
110
+ // On a valid HIT the composed response is committed HERE — the stored prelude
111
+ // bytes flush immediately while match()/segment resolution/Flight render/resume
112
+ // run behind them inside the response stream (ring-3 reads and render setup hide
113
+ // behind wire bytes). On a MISS the request continues as plain axis 1 and a
114
+ // background capture is scheduled after the response is built.
115
+ let pprMiss: {
116
+ descriptor: ShellCaptureDescriptor;
117
+ ssrModule: SSRModule;
118
+ } | null = null;
119
+ if (
120
+ !isPartial &&
121
+ request.method === "GET" &&
122
+ !url.searchParams.has("__prerender_collect") &&
123
+ !isRscRequest(request, url, false)
124
+ ) {
125
+ const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry);
126
+ if (pprConfig) {
127
+ // A per-request CSP nonce pins the route to axis 1: useNonce() (and any app
128
+ // code reading the nonce) renders it into every nonced script/style/meta, so
129
+ // a shell shared per host+URL would freeze one request's nonce for every
130
+ // visitor and the browser's CSP would reject the frozen nonce for all but the
131
+ // capture request. The nonce arrives two ways and BOTH must gate: the
132
+ // createRouter({ nonce }) provider (threaded here as `nonce`), and a direct
133
+ // token write in middleware (ctx.set(nonce, value)). The token is only
134
+ // visible in the post-middleware request variables — and this commit point
135
+ // runs AFTER the whole middleware chain (see the block header), so it is
136
+ // present here. Reading it closes the gap the provider-only check left open
137
+ // (issue #656). The threaded-param check stays first: the provider path is
138
+ // resolved before any variable read and short-circuits cheaply.
139
+ const activeNonce = nonce ?? contextGet(reqCtx._variables, nonceToken);
140
+ const store = reqCtx._cacheStore;
141
+ const key = buildShellKey(url);
142
+ if (activeNonce !== undefined) {
143
+ // Declared intent that cannot be honored deserves a diagnostic (unlike an
144
+ // undeclared route, which is silent): a ppr route gated off by an active
145
+ // per-request nonce warns once per key. Axis 1 after the warning.
146
+ warnPprNonceActiveOnce(key);
147
+ } else if (!hasShellFamily(store)) {
148
+ // Declared intent that cannot be honored deserves a diagnostic (unlike an
149
+ // undeclared route, which is silent). Axis 1 after the warning.
150
+ warnShellStoreMissingOnce(key);
151
+ } else {
152
+ // allReady (ssr.resolveStreaming) bypasses PPR entirely: buffering defeats
153
+ // streaming, so bots/SEO crawlers get one complete axis-1 document.
154
+ const [ssrModule, streamMode] = await getSSRSetup(
155
+ ctx,
156
+ request,
157
+ env,
158
+ url,
159
+ reqCtx._metricsStore,
160
+ );
161
+ if (
162
+ streamMode !== "allReady" &&
163
+ ssrModule.resumeShellHTML &&
164
+ ssrModule.captureShellHTML
165
+ ) {
166
+ const descriptor: ShellCaptureDescriptor = {
167
+ key,
168
+ buildVersion: ctx.version,
169
+ ttl: pprConfig.ttl,
170
+ swr: pprConfig.swr,
171
+ tags: pprConfig.tags,
172
+ store,
173
+ debug: INTERNAL_RANGO_DEBUG,
174
+ };
175
+ let cached: Awaited<ReturnType<typeof store.getShell>> = null;
176
+ try {
177
+ cached = await store.getShell(key);
178
+ } catch (error) {
179
+ // A failing store read degrades to axis 1 (MISS), never a 500.
180
+ reportCacheError(error, "cache-read", "[ShellServe] getShell");
181
+ }
182
+ if (cached && isValidShellHit(cached.entry, ctx.version)) {
183
+ if (!hasIntactShellPayload(cached.entry)) {
184
+ // Corrupt stored payload (undecodable prelude / unparseable
185
+ // postponed): a store-layer fault worth a diagnostic, unlike the
186
+ // silent version-mismatch lifecycle misses above. Degrade to MISS
187
+ // — pprMiss below schedules the recapture that overwrites it.
188
+ reportCacheError(
189
+ new Error(
190
+ `corrupt shell entry for "${key}": prelude/postponed failed ` +
191
+ "the integrity check; serving axis 1 and recapturing",
192
+ ),
193
+ "cache-read",
194
+ "[ShellServe] getShell",
195
+ );
196
+ } else {
197
+ // Stale (SWR) hit: serve the stale shell now, recapture in the
198
+ // background (stampede-guarded + backoff inside scheduleShellCapture).
199
+ if (cached.shouldRevalidate) {
200
+ scheduleShellCapture(
201
+ ctx,
202
+ request,
203
+ env,
204
+ url,
205
+ reqCtx,
206
+ ssrModule,
207
+ descriptor,
208
+ );
209
+ }
210
+ return serveShellHit(
211
+ ctx,
212
+ request,
213
+ env,
214
+ url,
215
+ reqCtx,
216
+ handleStore,
217
+ ssrModule,
218
+ cached.entry,
219
+ descriptor,
220
+ );
221
+ }
222
+ }
223
+ // MISS (no entry, invalid reactVersion, or store read failure): axis 1
224
+ // + a background capture scheduled once the response is known servable.
225
+ pprMiss = { descriptor, ssrModule };
226
+ }
227
+ }
228
+ }
229
+ }
60
230
 
61
231
  if (isPartial) {
62
232
  // Partial render (navigation)
@@ -74,7 +244,7 @@ export async function handleRscRendering<TEnv>(
74
244
  return createSimpleRedirectResponse(match.redirect);
75
245
  }
76
246
 
77
- payload = buildFullPayload(match);
247
+ payload = buildFullPayload(match, ctx, url, reqCtx, handleStore);
78
248
  } else {
79
249
  setRequestContextParams(result.params, result.routeName);
80
250
 
@@ -83,8 +253,17 @@ export async function handleRscRendering<TEnv>(
83
253
  payload = {
84
254
  metadata: {
85
255
  pathname: url.pathname,
256
+ // routerId is serialized on every payload (including within-session
257
+ // ones) so the frontend can read the current app/router identity. It
258
+ // always equals the current app's id: a cross-app navigation is
259
+ // intercepted server-side (X-RSC-Reload) and never delivers a
260
+ // different-router payload to the client.
86
261
  routerId: ctx.router.id,
87
- segments: result.segments,
262
+ segments: gateTransitions(
263
+ result.segments,
264
+ reqCtx,
265
+ ctx.router.onError,
266
+ ),
88
267
  matched: result.matched,
89
268
  diff: result.diff,
90
269
  resolvedIds: result.resolvedIds,
@@ -94,6 +273,9 @@ export async function handleRscRendering<TEnv>(
94
273
  handles: handleStore.stream(),
95
274
  version: ctx.version,
96
275
  prefetchCacheTTL: ctx.router.prefetchCacheTTL,
276
+ prefetchCacheSize: ctx.router.prefetchCacheSize,
277
+ prefetchConcurrency: ctx.router.prefetchConcurrency,
278
+ stateCookieName: ctx.router.resolvedStateCookieName,
97
279
  },
98
280
  };
99
281
  }
@@ -140,7 +322,7 @@ export async function handleRscRendering<TEnv>(
140
322
  { headers: { "Content-Type": "application/json" } },
141
323
  );
142
324
  } else {
143
- payload = buildFullPayload(match);
325
+ payload = buildFullPayload(match, ctx, url, reqCtx, handleStore);
144
326
  }
145
327
  }
146
328
 
@@ -148,15 +330,10 @@ export async function handleRscRendering<TEnv>(
148
330
  // SSR (full page) requests ignore location state since there's no history.state
149
331
  // to write to on a fresh page load.
150
332
  if (isPartial && payload.metadata) {
151
- const locationState = getLocationState();
152
- if (locationState) {
153
- payload.metadata.locationState =
154
- resolveLocationStateEntries(locationState);
155
- }
333
+ attachLocationStateIfPresent(payload);
156
334
  }
157
335
 
158
336
  const metricsStore = reqCtx._metricsStore;
159
- const renderStart = performance.now();
160
337
 
161
338
  // Serialize to RSC stream
162
339
  const rscSerializeStart = performance.now();
@@ -175,11 +352,15 @@ export async function handleRscRendering<TEnv>(
175
352
  );
176
353
 
177
354
  if (isRscRequest(request, url, isPartial)) {
178
- const renderDur = performance.now() - renderStart;
179
- appendMetric(metricsStore, "render:total", renderStart, renderDur);
355
+ // render:total is recorded by the observePhase wrapper around this function.
180
356
  const rscHeaders: Record<string, string> = {
181
357
  "content-type": "text/x-component;charset=utf-8",
182
358
  vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
359
+ // Router identity, so the client can verify pre-decode (before importing
360
+ // chunks) that this content payload belongs to its app and refuse a
361
+ // foreign one (cache/proxy/bug). Control-only reload/redirect responses
362
+ // are deliberately NOT stamped. See browser/response-adapter.ts.
363
+ "X-RSC-Router-Id": ctx.router.id,
183
364
  };
184
365
  // Tell the client's prefetch cache to scope this response to its source
185
366
  // URL (instead of the default source-agnostic wildcard). Intercept
@@ -213,18 +394,280 @@ export async function handleRscRendering<TEnv>(
213
394
  metricsStore,
214
395
  );
215
396
 
216
- const ssrRenderStart = performance.now();
217
- const htmlStream = await ssrModule.renderHTML(rscStream, {
218
- nonce,
219
- streamMode,
397
+ // ssr-render-html metric + rango.ssr span from one boundary. render:total is
398
+ // recorded by the observePhase wrapper around this function.
399
+ const htmlStream = await observePhase(PHASES.ssr, () =>
400
+ ssrModule.renderHTML(rscStream, {
401
+ nonce,
402
+ streamMode,
403
+ }),
404
+ );
405
+ const response = createResponseWithMergedHeaders(htmlStream, {
406
+ headers: { "content-type": "text/html;charset=utf-8" },
220
407
  });
221
- const ssrRenderDur = performance.now() - ssrRenderStart;
222
- appendMetric(metricsStore, "ssr-render-html", ssrRenderStart, ssrRenderDur);
223
408
 
224
- const renderDur = performance.now() - renderStart;
225
- appendMetric(metricsStore, "render:total", renderStart, renderDur);
409
+ // --- Axis 2: PPR shell CAPTURE on MISS (background task; see design doc) ---
410
+ // The ppr route missed its shell above. Schedule the background capture only
411
+ // when the served response is a 200 HTML document (a 404/error render is not a
412
+ // cacheable shell), and tag the response for observability either way. Capture
413
+ // does NOT flow through the HTTP pipeline: scheduleShellCapture re-derives the
414
+ // page via router.match() under a derived context (fresh handle store,
415
+ // _shellCaptureRun: true) — middleware never re-runs; it already ran for this
416
+ // request and guarding is serve-time.
417
+ if (pprMiss) {
418
+ if (
419
+ response.status === 200 &&
420
+ (response.headers.get("content-type") ?? "").includes("text/html")
421
+ ) {
422
+ scheduleShellCapture(
423
+ ctx,
424
+ request,
425
+ env,
426
+ url,
427
+ reqCtx,
428
+ pprMiss.ssrModule,
429
+ pprMiss.descriptor,
430
+ );
431
+ }
432
+ response.headers.set(SHELL_STATUS_HEADER, "MISS");
433
+ }
226
434
 
227
- return createResponseWithMergedHeaders(htmlStream, {
228
- headers: { "content-type": "text/html;charset=utf-8" },
435
+ return response;
436
+ }
437
+
438
+ /**
439
+ * Neutralize the shell-HIT degradation redirect target.
440
+ *
441
+ * The inline `location.replace` emitted by serveShellHit when a shell HIT lands
442
+ * on a URL whose route became redirecting mid-TTL is a document-native redirect
443
+ * exit that BYPASSES the 3xx chokepoint (guardOutgoingRedirect acts only on 3xx
444
+ * + Location responses, never a committed 200 body). So it reuses the ONE
445
+ * same-origin resolver directly: a cross-origin/unparseable/unsafe target
446
+ * neutralizes to the same safe same-origin landing as redirect-guard.ts
447
+ * (basename root, or "/" when unset) rather than navigating the user off-host.
448
+ * A safe same-origin/relative target passes through as its normalized href.
449
+ */
450
+ export function resolveShellHitRedirectTarget(
451
+ rawTarget: string,
452
+ requestOrigin: string,
453
+ basename: string | undefined,
454
+ ): string {
455
+ return (
456
+ resolveSameOriginRedirect(rawTarget, requestOrigin) ??
457
+ safeSameOriginLanding(basename)
458
+ );
459
+ }
460
+
461
+ /**
462
+ * Serve a validated shell HIT: commit the composed response NOW — the stored
463
+ * prelude bytes are the first thing on the wire — and run the live tail
464
+ * (match(), fresh loaders, full Flight render for hydration, fizz resume of just
465
+ * the holes) BEHIND them inside the response stream. React relies on HTML-parser
466
+ * foster-parenting for content streamed after the prelude's closing
467
+ * `</body></html>`, so plain byte concatenation is the correct composition.
468
+ *
469
+ * Status and headers are committed at the flush: middleware already ran (their
470
+ * ctx.res headers merge in via createResponseWithMergedHeaders), and route
471
+ * middleware code after its next() can still adjust headers on the returned
472
+ * Response object. A failing hole cannot become a 500/redirect after this point —
473
+ * error UI renders inline via Suspense/error boundaries, the documented PPR
474
+ * constraint.
475
+ *
476
+ * The tail promise is kicked off SYNCHRONOUSLY so match/Flight/resume run inside
477
+ * the current ALS request-context frame (the stream may be pulled by the server
478
+ * adapter outside it).
479
+ */
480
+ function serveShellHit(
481
+ ctx: HandlerContext<any>,
482
+ request: Request,
483
+ env: any,
484
+ url: URL,
485
+ reqCtx: RequestContext<any>,
486
+ handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
487
+ ssrModule: SSRModule,
488
+ entry: ShellCacheEntry,
489
+ descriptor: ShellCaptureDescriptor,
490
+ ): Response {
491
+ const preludeBytes = base64ToBytes(entry.prelude);
492
+
493
+ const renderTail = async (
494
+ activeCtx: RequestContext<any>,
495
+ ): Promise<ReadableStream<Uint8Array> | { redirect: string }> => {
496
+ const match = await ctx.router.match(request, { env });
497
+ if (match.redirect) return { redirect: match.redirect };
498
+ setRequestContextParams(match.params, match.routeName);
499
+ const payload = buildFullPayload(match, ctx, url, activeCtx, handleStore);
500
+ // Theme fidelity for resume: initialTheme is per-request METADATA (the
501
+ // visitor's cookie), but React resume requires the tree above the holes to
502
+ // match the frozen prelude, which was rendered with the CAPTURE's
503
+ // initialTheme. Replay the captured value into the payload (the SSR resume
504
+ // tree AND client hydration both read it) so the trees agree by
505
+ // construction. The visitor still sees THEIR theme: the FOUC script in the
506
+ // prelude applies it pre-paint from the cookie, and ThemeProvider re-syncs
507
+ // its state from the cookie post-mount.
508
+ if (payload.metadata) {
509
+ payload.metadata.initialTheme = entry.initialTheme as
510
+ | import("../theme/types.js").Theme
511
+ | undefined;
512
+ }
513
+ // Full Flight render per request: hydration needs the whole payload (there
514
+ // is no Flight-side resume — a React limitation, not ours).
515
+ let rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
516
+ onError: (error: unknown) => {
517
+ ctx.callOnError(error, "rendering", { request, url, env });
518
+ },
519
+ });
520
+ // Timing tap: when does the Flight render produce its FIRST byte? Compared
521
+ // with the eager-inject/first-tail logs this proves whether hydration-start
522
+ // latency is genuine server work (loaders) or stream plumbing holding
523
+ // ready bytes back.
524
+ if (INTERNAL_RANGO_DEBUG) {
525
+ const tapStart = performance.now();
526
+ let first = false;
527
+ rscStream = rscStream.pipeThrough(
528
+ new TransformStream({
529
+ transform(chunk, controller) {
530
+ if (!first) {
531
+ first = true;
532
+ console.log(
533
+ `[Server][ppr] flight render: first chunk +${Math.round(performance.now() - tapStart)}ms`,
534
+ );
535
+ }
536
+ controller.enqueue(chunk);
537
+ },
538
+ }),
539
+ );
540
+ }
541
+ return observePhase(PHASES.ssr, () =>
542
+ ssrModule.resumeShellHTML!(rscStream, {
543
+ postponed: entry.postponed,
544
+ nonce: undefined,
545
+ }),
546
+ );
547
+ };
548
+
549
+ const tailPromise: Promise<
550
+ ReadableStream<Uint8Array> | { redirect: string }
551
+ > = (async () => {
552
+ // Capture data snapshot seeding (docs/design/ppr-shell-resume.md): the tail
553
+ // is a FULL FRESH render whose payload must match the frozen prelude. If the
554
+ // capture recorded a snapshot, run the tail through a SeededShellStore
555
+ // overlay so every cache-store read the capture pinned returns its
556
+ // capture-time value AS FRESH — the shell region reproduces byte-identically
557
+ // even after the underlying cache entries drifted (expired/recomputed/
558
+ // tag-invalidated). Everything not pinned (the holes — masked loaders were
559
+ // never recorded) falls through to the real store and stays LIVE. The
560
+ // overlay lives on a DERIVED context (own _cacheStore), so the shared reqCtx
561
+ // is untouched; an entry without a snapshot keeps the pre-snapshot behavior.
562
+ if (entry.snapshot && entry.snapshot.length > 0) {
563
+ const seededCtx: RequestContext<any> = Object.create(reqCtx);
564
+ if (reqCtx._cacheStore) {
565
+ seededCtx._cacheStore = new SeededShellStore(
566
+ reqCtx._cacheStore,
567
+ entry.snapshot,
568
+ );
569
+ }
570
+ // Loader-family records (bake-lane containers, loader-container-bake):
571
+ // decode into a seed Map for the resolveLoaderData overlay, so the
572
+ // payload's baked container bytes match the frozen prelude while the
573
+ // hole-marker paths keep the fresh run's live nested promises.
574
+ const loaderSeed = await buildShellLoaderSeed(entry.snapshot);
575
+ if (loaderSeed) seededCtx._shellLoaderSeed = loaderSeed;
576
+ return runWithRequestContext(seededCtx, () => renderTail(seededCtx));
577
+ }
578
+ return renderTail(reqCtx);
579
+ })();
580
+ // The stream below is the only consumer; pre-attach a no-op catch so a tail
581
+ // failure before the stream is pulled never surfaces as an unhandled rejection.
582
+ tailPromise.catch(() => {});
583
+
584
+ const serveStart = INTERNAL_RANGO_DEBUG ? performance.now() : 0;
585
+ const body = new ReadableStream<Uint8Array>({
586
+ async start(controller) {
587
+ controller.enqueue(preludeBytes);
588
+ if (INTERNAL_RANGO_DEBUG) {
589
+ console.log(
590
+ `[Server][ppr] shell HIT: prelude enqueued (${preludeBytes.length}b) +${Math.round(performance.now() - serveStart)}ms`,
591
+ );
592
+ }
593
+ try {
594
+ const tail = await tailPromise;
595
+ if (tail instanceof ReadableStream) {
596
+ const reader = tail.getReader();
597
+ let firstTailChunk = true;
598
+ let tailBytes = 0;
599
+ try {
600
+ for (;;) {
601
+ const { done, value } = await reader.read();
602
+ if (done) break;
603
+ if (INTERNAL_RANGO_DEBUG && firstTailChunk) {
604
+ firstTailChunk = false;
605
+ console.log(
606
+ `[Server][ppr] shell HIT: first tail chunk on the wire +${Math.round(performance.now() - serveStart)}ms`,
607
+ );
608
+ }
609
+ if (INTERNAL_RANGO_DEBUG) tailBytes += value.length;
610
+ controller.enqueue(value);
611
+ }
612
+ } finally {
613
+ reader.releaseLock();
614
+ }
615
+ // Bounds the post-header work Server-Timing structurally cannot see:
616
+ // the HIT commits headers at the flush, so ALL live-tail time (match,
617
+ // loaders, Flight, resume) happens inside the response body. This
618
+ // line plus the [Server][segments] build logs narrate that window.
619
+ if (INTERNAL_RANGO_DEBUG) {
620
+ console.log(
621
+ `[Server][ppr] shell HIT: tail complete +${Math.round(performance.now() - serveStart)}ms (${tailBytes}b)`,
622
+ );
623
+ }
624
+ } else {
625
+ // Defensive, near-unreachable: a redirecting match cannot have captured
626
+ // a shell (capture bails on redirects), so a HIT on a redirecting URL
627
+ // requires the route to have BECOME redirecting within the shell TTL.
628
+ // The 200 + prelude are already committed; degrade to a client-side
629
+ // replace so the user still lands on the target. The target is
630
+ // neutralized first (see resolveShellHitRedirectTarget).
631
+ const safeTarget = resolveShellHitRedirectTarget(
632
+ tail.redirect,
633
+ url.origin,
634
+ ctx.router.basename,
635
+ );
636
+ controller.enqueue(
637
+ new TextEncoder().encode(
638
+ `<script>location.replace(${JSON.stringify(safeTarget)})</script>`,
639
+ ),
640
+ );
641
+ }
642
+ controller.close();
643
+ } catch (error) {
644
+ // Self-heal on a failed tail: the pre-commit gates (isValidShellHit +
645
+ // hasIntactShellPayload) cannot catch a parseable-but-mismatched
646
+ // postponed blob or a hard render error above the holes — those throw
647
+ // here, AFTER the 200 + prelude flushed, and would otherwise re-fail on
648
+ // every request until the entry ages out (nothing else evicts it).
649
+ // Recapturing overwrites the entry with one the current server
650
+ // produced. A client disconnect mid-stream also lands here and
651
+ // schedules a spurious-but-idempotent recapture — bounded by the
652
+ // stampede guard + backoff inside scheduleShellCapture.
653
+ scheduleShellCapture(
654
+ ctx,
655
+ request,
656
+ env,
657
+ url,
658
+ reqCtx,
659
+ ssrModule,
660
+ descriptor,
661
+ );
662
+ controller.error(error);
663
+ }
664
+ },
665
+ });
666
+
667
+ return createResponseWithMergedHeaders(body, {
668
+ headers: {
669
+ "content-type": "text/html;charset=utf-8",
670
+ [SHELL_STATUS_HEADER]: "HIT",
671
+ },
229
672
  });
230
673
  }
@@ -39,3 +39,17 @@ export function warnNonRedirectPeResponse(): void {
39
39
  `ignored — the page will re-render at the current URL instead.`,
40
40
  );
41
41
  }
42
+
43
+ /**
44
+ * Warn when a non-redirect Response is returned (not thrown) from an action
45
+ * on the JS (fetch) path. A raw Response cannot be serialized into Flight, so
46
+ * it is discarded — mirroring the PE path. Use `throw redirect('/path')` for
47
+ * redirects.
48
+ */
49
+ export function warnNonRedirectActionResponse(actionId: string): void {
50
+ console.warn(
51
+ `[@rangojs/router] Server action "${actionId}" returned a Response ` +
52
+ `that is not a redirect. Non-redirect Responses cannot be serialized ` +
53
+ `and are ignored. Use \`throw redirect('/path')\` for redirects.`,
54
+ );
55
+ }