@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7c7e4327

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