@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.141

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 (460) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +432 -7
  3. package/dist/bin/rango.js +2073 -213
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +7258 -2714
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +140 -67
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +329 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +487 -0
  12. package/skills/caching/SKILL.md +357 -25
  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 +246 -0
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +16 -10
  19. package/skills/document-cache/SKILL.md +87 -62
  20. package/skills/fonts/SKILL.md +6 -4
  21. package/skills/handler-use/SKILL.md +364 -0
  22. package/skills/hooks/SKILL.md +557 -79
  23. package/skills/host-router/SKILL.md +320 -0
  24. package/skills/i18n/SKILL.md +276 -0
  25. package/skills/intercept/SKILL.md +207 -15
  26. package/skills/layout/SKILL.md +146 -6
  27. package/skills/links/SKILL.md +304 -25
  28. package/skills/loader/SKILL.md +616 -54
  29. package/skills/middleware/SKILL.md +217 -37
  30. package/skills/migrate-nextjs/SKILL.md +611 -0
  31. package/skills/migrate-react-router/SKILL.md +927 -0
  32. package/skills/mime-routes/SKILL.md +42 -11
  33. package/skills/observability/SKILL.md +194 -0
  34. package/skills/parallel/SKILL.md +284 -3
  35. package/skills/ppr/SKILL.md +293 -0
  36. package/skills/prerender/SKILL.md +437 -52
  37. package/skills/rango/SKILL.md +369 -22
  38. package/skills/react-compiler/SKILL.md +168 -0
  39. package/skills/response-routes/SKILL.md +263 -121
  40. package/skills/route/SKILL.md +350 -21
  41. package/skills/router-setup/SKILL.md +246 -33
  42. package/skills/scripts/SKILL.md +179 -0
  43. package/skills/server-actions/SKILL.md +775 -0
  44. package/skills/shell-manifest/SKILL.md +185 -0
  45. package/skills/streams-and-websockets/SKILL.md +283 -0
  46. package/skills/tailwind/SKILL.md +27 -3
  47. package/skills/testing/SKILL.md +126 -222
  48. package/skills/testing/bindings.md +103 -0
  49. package/skills/testing/cache-prerender.md +127 -0
  50. package/skills/testing/client-components.md +124 -0
  51. package/skills/testing/e2e-parity.md +125 -0
  52. package/skills/testing/flight.md +91 -0
  53. package/skills/testing/handles.md +131 -0
  54. package/skills/testing/loader.md +128 -0
  55. package/skills/testing/middleware.md +99 -0
  56. package/skills/testing/render-handler.md +122 -0
  57. package/skills/testing/response-routes.md +95 -0
  58. package/skills/testing/reverse-and-types.md +85 -0
  59. package/skills/testing/server-actions.md +107 -0
  60. package/skills/testing/server-tree.md +128 -0
  61. package/skills/testing/setup.md +123 -0
  62. package/skills/theme/SKILL.md +9 -8
  63. package/skills/typesafety/SKILL.md +532 -103
  64. package/skills/use-cache/SKILL.md +367 -0
  65. package/skills/vercel/SKILL.md +128 -0
  66. package/skills/view-transitions/SKILL.md +337 -0
  67. package/src/__augment-tests__/augment.ts +81 -0
  68. package/src/__augment-tests__/augmented.check.ts +116 -0
  69. package/src/__internal.ts +77 -44
  70. package/src/bin/rango.ts +312 -15
  71. package/src/browser/action-coordinator.ts +114 -0
  72. package/src/browser/action-fence.ts +47 -0
  73. package/src/browser/app-shell.ts +39 -0
  74. package/src/browser/app-version.ts +14 -0
  75. package/src/browser/connection-warmup.ts +134 -0
  76. package/src/browser/cookie-name.ts +140 -0
  77. package/src/browser/event-controller.ts +293 -202
  78. package/src/browser/history-state.ts +101 -0
  79. package/src/browser/index.ts +3 -3
  80. package/src/browser/intercept-utils.ts +52 -0
  81. package/src/browser/invalidate-client-cache.ts +52 -0
  82. package/src/browser/link-interceptor.ts +24 -4
  83. package/src/browser/logging.ts +11 -0
  84. package/src/browser/merge-segment-loaders.ts +20 -12
  85. package/src/browser/navigation-bridge.ts +385 -576
  86. package/src/browser/navigation-client.ts +245 -75
  87. package/src/browser/navigation-store-handle.ts +38 -0
  88. package/src/browser/navigation-store.ts +184 -118
  89. package/src/browser/navigation-transaction.ts +247 -0
  90. package/src/browser/network-error-handler.ts +88 -0
  91. package/src/browser/partial-update.ts +412 -364
  92. package/src/browser/prefetch/cache.ts +359 -0
  93. package/src/browser/prefetch/fetch.ts +452 -0
  94. package/src/browser/prefetch/observer.ts +65 -0
  95. package/src/browser/prefetch/policy.ts +48 -0
  96. package/src/browser/prefetch/queue.ts +209 -0
  97. package/src/browser/prefetch/resource-ready.ts +77 -0
  98. package/src/browser/rango-state.ts +194 -0
  99. package/src/browser/react/Link.tsx +275 -68
  100. package/src/browser/react/NavigationProvider.tsx +265 -109
  101. package/src/browser/react/ScrollRestoration.tsx +10 -6
  102. package/src/browser/react/context.ts +11 -0
  103. package/src/browser/react/filter-segment-order.ts +70 -0
  104. package/src/browser/react/index.ts +0 -48
  105. package/src/browser/react/location-state-shared.ts +272 -60
  106. package/src/browser/react/location-state.ts +90 -20
  107. package/src/browser/react/mount-context.ts +6 -1
  108. package/src/browser/react/nonce-context.ts +23 -0
  109. package/src/browser/react/shallow-equal.ts +27 -0
  110. package/src/browser/react/use-action.ts +35 -66
  111. package/src/browser/react/use-handle.ts +39 -126
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +39 -13
  114. package/src/browser/react/use-navigation.ts +53 -69
  115. package/src/browser/react/use-params.ts +75 -0
  116. package/src/browser/react/use-pathname.ts +47 -0
  117. package/src/browser/react/use-reverse.ts +106 -0
  118. package/src/browser/react/use-router.ts +98 -0
  119. package/src/browser/react/use-search-params.ts +51 -0
  120. package/src/browser/react/use-segments.ts +72 -99
  121. package/src/browser/response-adapter.ts +164 -0
  122. package/src/browser/rsc-router.tsx +300 -72
  123. package/src/browser/scroll-restoration.ts +138 -50
  124. package/src/browser/segment-reconciler.ts +243 -0
  125. package/src/browser/segment-structure-assert.ts +17 -1
  126. package/src/browser/server-action-bridge.ts +668 -613
  127. package/src/browser/types.ts +223 -51
  128. package/src/browser/validate-redirect-origin.ts +56 -0
  129. package/src/build/collect-fallback-refs.ts +107 -0
  130. package/src/build/generate-manifest.ts +252 -161
  131. package/src/build/generate-route-types.ts +41 -1038
  132. package/src/build/index.ts +12 -7
  133. package/src/build/prefix-tree-utils.ts +123 -0
  134. package/src/build/route-trie.ts +225 -42
  135. package/src/build/route-types/ast-helpers.ts +25 -0
  136. package/src/build/route-types/ast-route-extraction.ts +105 -0
  137. package/src/build/route-types/codegen.ts +113 -0
  138. package/src/build/route-types/include-resolution.ts +812 -0
  139. package/src/build/route-types/param-extraction.ts +51 -0
  140. package/src/build/route-types/per-module-writer.ts +144 -0
  141. package/src/build/route-types/router-processing.ts +695 -0
  142. package/src/build/route-types/scan-filter.ts +85 -0
  143. package/src/build/route-types/source-scan.ts +216 -0
  144. package/src/build/runtime-discovery.ts +223 -0
  145. package/src/cache/background-task.ts +34 -0
  146. package/src/cache/cache-error.ts +104 -0
  147. package/src/cache/cache-key-utils.ts +60 -0
  148. package/src/cache/cache-policy.ts +199 -0
  149. package/src/cache/cache-runtime.ts +525 -0
  150. package/src/cache/cache-scope.ts +298 -332
  151. package/src/cache/cache-tag.ts +103 -0
  152. package/src/cache/cf/cf-base64.ts +33 -0
  153. package/src/cache/cf/cf-cache-constants.ts +127 -0
  154. package/src/cache/cf/cf-cache-store.ts +2508 -158
  155. package/src/cache/cf/cf-cache-types.ts +349 -0
  156. package/src/cache/cf/cf-kv-utils.ts +46 -0
  157. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  158. package/src/cache/cf/index.ts +17 -17
  159. package/src/cache/document-cache.ts +199 -92
  160. package/src/cache/handle-capture.ts +81 -0
  161. package/src/cache/handle-snapshot.ts +111 -0
  162. package/src/cache/index.ts +24 -35
  163. package/src/cache/memory-segment-store.ts +363 -30
  164. package/src/cache/profile-registry.ts +88 -0
  165. package/src/cache/read-through-swr.ts +178 -0
  166. package/src/cache/segment-codec.ts +248 -0
  167. package/src/cache/shell-snapshot.ts +368 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +222 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1113 -0
  173. package/src/client.rsc.tsx +43 -21
  174. package/src/client.tsx +131 -347
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +109 -0
  177. package/src/component-utils.ts +23 -4
  178. package/src/components/DefaultDocument.tsx +13 -3
  179. package/src/context-var.ts +168 -0
  180. package/src/debug.ts +19 -9
  181. package/src/decode-loader-results.ts +52 -0
  182. package/src/defer.ts +185 -0
  183. package/src/deps/ssr.ts +0 -1
  184. package/src/encode-kv.ts +49 -0
  185. package/src/errors.ts +106 -10
  186. package/src/escape-script.ts +52 -0
  187. package/src/handle.ts +110 -35
  188. package/src/handles/MetaTags.tsx +83 -59
  189. package/src/handles/Scripts.tsx +183 -0
  190. package/src/handles/breadcrumbs.ts +93 -0
  191. package/src/handles/deferred-resolution.ts +127 -0
  192. package/src/handles/is-thenable.ts +18 -0
  193. package/src/handles/meta.ts +44 -53
  194. package/src/handles/script.ts +244 -0
  195. package/src/host/cookie-handler.ts +20 -65
  196. package/src/host/errors.ts +21 -30
  197. package/src/host/index.ts +13 -9
  198. package/src/host/pattern-matcher.ts +50 -79
  199. package/src/host/router.ts +151 -121
  200. package/src/host/testing.ts +45 -32
  201. package/src/host/types.ts +52 -11
  202. package/src/host/utils.ts +2 -2
  203. package/src/href-client.ts +192 -57
  204. package/src/index.rsc.ts +173 -35
  205. package/src/index.ts +241 -73
  206. package/src/internal-debug.ts +9 -2
  207. package/src/loader-store.ts +500 -0
  208. package/src/loader.rsc.ts +31 -99
  209. package/src/loader.ts +30 -12
  210. package/src/missing-id-error.ts +68 -0
  211. package/src/outlet-context.ts +1 -1
  212. package/src/outlet-provider.tsx +41 -0
  213. package/src/prerender/param-hash.ts +16 -14
  214. package/src/prerender/store.ts +121 -21
  215. package/src/prerender.ts +460 -26
  216. package/src/redirect-origin.ts +100 -0
  217. package/src/regex-escape.ts +8 -0
  218. package/src/render-error-thrower.tsx +20 -0
  219. package/src/response-utils.ts +62 -0
  220. package/src/reverse.ts +198 -128
  221. package/src/root-error-boundary.tsx +42 -48
  222. package/src/route-content-wrapper.tsx +22 -77
  223. package/src/route-definition/dsl-helpers.ts +1116 -0
  224. package/src/route-definition/helper-factories.ts +88 -0
  225. package/src/route-definition/helpers-types.ts +505 -0
  226. package/src/route-definition/index.ts +54 -0
  227. package/src/route-definition/redirect.ts +134 -0
  228. package/src/route-definition/resolve-handler-use.ts +160 -0
  229. package/src/route-definition/use-item-types.ts +29 -0
  230. package/src/route-definition.ts +1 -1481
  231. package/src/route-map-builder.ts +82 -144
  232. package/src/route-name.ts +53 -0
  233. package/src/route-types.ts +71 -45
  234. package/src/router/basename.ts +14 -0
  235. package/src/router/content-negotiation.ts +263 -0
  236. package/src/router/debug-manifest.ts +72 -0
  237. package/src/router/error-handling.ts +54 -27
  238. package/src/router/find-match.ts +245 -0
  239. package/src/router/handler-context.ts +377 -125
  240. package/src/router/instrument.ts +350 -0
  241. package/src/router/intercept-resolution.ts +59 -28
  242. package/src/router/lazy-includes.ts +254 -0
  243. package/src/router/loader-resolution.ts +421 -157
  244. package/src/router/logging.ts +106 -6
  245. package/src/router/manifest.ts +131 -57
  246. package/src/router/match-api.ts +167 -246
  247. package/src/router/match-context.ts +4 -24
  248. package/src/router/match-handlers.ts +440 -0
  249. package/src/router/match-middleware/background-revalidation.ts +117 -93
  250. package/src/router/match-middleware/cache-lookup.ts +297 -150
  251. package/src/router/match-middleware/cache-store.ts +123 -51
  252. package/src/router/match-middleware/intercept-resolution.ts +44 -43
  253. package/src/router/match-middleware/segment-resolution.ts +64 -22
  254. package/src/router/match-pipelines.ts +11 -87
  255. package/src/router/match-result.ts +121 -50
  256. package/src/router/metrics.ts +219 -28
  257. package/src/router/middleware-types.ts +93 -0
  258. package/src/router/middleware.ts +505 -441
  259. package/src/router/navigation-snapshot.ts +133 -0
  260. package/src/router/params-util.ts +23 -0
  261. package/src/router/parse-pattern.ts +115 -0
  262. package/src/router/pattern-matching.ts +311 -142
  263. package/src/router/prefetch-cache-ttl.ts +51 -0
  264. package/src/router/prefetch-limits.ts +37 -0
  265. package/src/router/prerender-match.ts +547 -0
  266. package/src/router/preview-match.ts +102 -0
  267. package/src/router/request-classification.ts +278 -0
  268. package/src/router/revalidation.ts +203 -62
  269. package/src/router/route-snapshot.ts +246 -0
  270. package/src/router/router-context.ts +45 -48
  271. package/src/router/router-interfaces.ts +554 -0
  272. package/src/router/router-options.ts +779 -0
  273. package/src/router/router-registry.ts +21 -0
  274. package/src/router/segment-resolution/fresh.ts +772 -0
  275. package/src/router/segment-resolution/helpers.ts +348 -0
  276. package/src/router/segment-resolution/loader-cache.ts +250 -0
  277. package/src/router/segment-resolution/loader-mask.ts +44 -0
  278. package/src/router/segment-resolution/revalidation.ts +1331 -0
  279. package/src/router/segment-resolution/static-store.ts +81 -0
  280. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  281. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  282. package/src/router/segment-resolution.ts +25 -1354
  283. package/src/router/segment-wrappers.ts +292 -0
  284. package/src/router/state-cookie-name.ts +33 -0
  285. package/src/router/substitute-pattern-params.ts +75 -0
  286. package/src/router/telemetry-otel.ts +261 -0
  287. package/src/router/telemetry.ts +377 -0
  288. package/src/router/timeout.ts +128 -0
  289. package/src/router/tracing.ts +206 -0
  290. package/src/router/trie-matching.ts +240 -61
  291. package/src/router/types.ts +23 -70
  292. package/src/router/url-params.ts +57 -0
  293. package/src/router.ts +781 -2378
  294. package/src/rsc/full-payload.ts +70 -0
  295. package/src/rsc/handler-context.ts +46 -0
  296. package/src/rsc/handler.ts +905 -1142
  297. package/src/rsc/helpers.ts +275 -19
  298. package/src/rsc/index.ts +2 -25
  299. package/src/rsc/json-route-result.ts +38 -0
  300. package/src/rsc/loader-fetch.ts +305 -0
  301. package/src/rsc/manifest-init.ts +77 -0
  302. package/src/rsc/nonce.ts +14 -0
  303. package/src/rsc/origin-guard.ts +155 -0
  304. package/src/rsc/progressive-enhancement.ts +502 -0
  305. package/src/rsc/redirect-guard.ts +99 -0
  306. package/src/rsc/response-cache-serve.ts +238 -0
  307. package/src/rsc/response-error.ts +104 -0
  308. package/src/rsc/response-route-handler.ts +257 -0
  309. package/src/rsc/rsc-rendering.ts +527 -0
  310. package/src/rsc/runtime-warnings.ts +55 -0
  311. package/src/rsc/server-action.ts +522 -0
  312. package/src/rsc/shell-capture.ts +897 -0
  313. package/src/rsc/shell-serve.ts +124 -0
  314. package/src/rsc/ssr-setup.ts +144 -0
  315. package/src/rsc/transition-gate.ts +89 -0
  316. package/src/rsc/types.ts +95 -12
  317. package/src/runtime-env.ts +18 -0
  318. package/src/search-params.ts +99 -82
  319. package/src/segment-content-promise.ts +67 -0
  320. package/src/segment-loader-promise.ts +149 -0
  321. package/src/segment-system.tsx +349 -134
  322. package/src/serialize.ts +243 -0
  323. package/src/server/context.ts +459 -85
  324. package/src/server/cookie-parse.ts +32 -0
  325. package/src/server/cookie-store.ts +310 -0
  326. package/src/server/fetchable-loader-store.ts +11 -6
  327. package/src/server/handle-store.ts +123 -42
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +848 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +443 -135
  332. package/src/ssr/ssr-root.tsx +228 -0
  333. package/src/static-handler.ts +45 -18
  334. package/src/testing/cache-status.ts +162 -0
  335. package/src/testing/collect-handle.ts +46 -0
  336. package/src/testing/dispatch.ts +701 -0
  337. package/src/testing/dom.entry.ts +22 -0
  338. package/src/testing/e2e/fixture.ts +188 -0
  339. package/src/testing/e2e/index.ts +128 -0
  340. package/src/testing/e2e/matchers.ts +35 -0
  341. package/src/testing/e2e/page-helpers.ts +272 -0
  342. package/src/testing/e2e/parity.ts +387 -0
  343. package/src/testing/e2e/server.ts +195 -0
  344. package/src/testing/flight-matchers.ts +97 -0
  345. package/src/testing/flight-normalize.ts +11 -0
  346. package/src/testing/flight-runtime.d.ts +57 -0
  347. package/src/testing/flight-tree.ts +682 -0
  348. package/src/testing/flight.entry.ts +52 -0
  349. package/src/testing/flight.ts +257 -0
  350. package/src/testing/generated-routes.ts +199 -0
  351. package/src/testing/index.ts +105 -0
  352. package/src/testing/internal/context.ts +371 -0
  353. package/src/testing/internal/flight-client-globals.ts +30 -0
  354. package/src/testing/internal/seed-vars.ts +54 -0
  355. package/src/testing/render-handler.ts +357 -0
  356. package/src/testing/render-route.tsx +584 -0
  357. package/src/testing/run-loader.ts +385 -0
  358. package/src/testing/run-middleware.ts +205 -0
  359. package/src/testing/run-transition-when.ts +164 -0
  360. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  361. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  362. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  363. package/src/testing/vitest-stubs/version.ts +5 -0
  364. package/src/testing/vitest.ts +305 -0
  365. package/src/theme/ThemeProvider.tsx +76 -98
  366. package/src/theme/ThemeScript.tsx +12 -14
  367. package/src/theme/constants.ts +57 -15
  368. package/src/theme/index.ts +3 -20
  369. package/src/theme/theme-context.ts +5 -35
  370. package/src/theme/theme-script.ts +43 -39
  371. package/src/theme/use-theme.ts +0 -3
  372. package/src/types/boundaries.ts +123 -0
  373. package/src/types/cache-types.ts +207 -0
  374. package/src/types/error-types.ts +132 -0
  375. package/src/types/global-namespace.ts +113 -0
  376. package/src/types/handler-context.ts +839 -0
  377. package/src/types/index.ts +81 -0
  378. package/src/types/loader-types.ts +212 -0
  379. package/src/types/request-scope.ts +112 -0
  380. package/src/types/route-config.ts +138 -0
  381. package/src/types/route-entry.ts +114 -0
  382. package/src/types/segments.ts +271 -0
  383. package/src/types.ts +1 -1795
  384. package/src/urls/include-helper.ts +162 -0
  385. package/src/urls/include-provider.ts +71 -0
  386. package/src/urls/index.ts +44 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +280 -0
  389. package/src/urls/pattern-types.ts +160 -0
  390. package/src/urls/response-types.ts +109 -0
  391. package/src/urls/type-extraction.ts +316 -0
  392. package/src/urls/urls-function.ts +80 -0
  393. package/src/urls.ts +1 -1341
  394. package/src/use-loader.tsx +406 -141
  395. package/src/vercel/index.ts +11 -0
  396. package/src/vercel/tracing.ts +88 -0
  397. package/src/vite/debug.ts +185 -0
  398. package/src/vite/discovery/bundle-postprocess.ts +182 -0
  399. package/src/vite/discovery/discover-routers.ts +389 -0
  400. package/src/vite/discovery/discovery-errors.ts +255 -0
  401. package/src/vite/discovery/gate-state.ts +171 -0
  402. package/src/vite/discovery/prerender-collection.ts +467 -0
  403. package/src/vite/discovery/route-types-writer.ts +214 -0
  404. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  405. package/src/vite/discovery/state.ts +161 -0
  406. package/src/vite/discovery/virtual-module-codegen.ts +183 -0
  407. package/src/vite/index.ts +23 -2255
  408. package/src/vite/inject-client-debug.ts +36 -0
  409. package/src/vite/plugin-types.ts +303 -0
  410. package/src/vite/plugins/cjs-to-esm.ts +90 -0
  411. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  412. package/src/vite/plugins/client-ref-hashing.ts +118 -0
  413. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  414. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  415. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  416. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
  417. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
  418. package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
  419. package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
  420. package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
  421. package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
  422. package/src/vite/plugins/expose-ids/types.ts +45 -0
  423. package/src/vite/plugins/expose-internal-ids.ts +805 -0
  424. package/src/vite/plugins/performance-tracks.ts +89 -0
  425. package/src/vite/plugins/refresh-cmd.ts +127 -0
  426. package/src/vite/plugins/use-cache-transform.ts +313 -0
  427. package/src/vite/plugins/vercel-output.ts +384 -0
  428. package/src/vite/plugins/version-injector.ts +94 -0
  429. package/src/vite/plugins/version-plugin.ts +263 -0
  430. package/src/vite/plugins/virtual-entries.ts +234 -0
  431. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  432. package/src/vite/rango.ts +560 -0
  433. package/src/vite/router-discovery.ts +1638 -0
  434. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
  435. package/src/vite/utils/banner.ts +36 -0
  436. package/src/vite/utils/bundle-analysis.ts +132 -0
  437. package/src/vite/utils/client-chunks.ts +184 -0
  438. package/src/vite/utils/directive-prologue.ts +40 -0
  439. package/src/vite/utils/forward-user-plugins.ts +171 -0
  440. package/src/vite/utils/manifest-utils.ts +15 -0
  441. package/src/vite/utils/package-resolution.ts +89 -0
  442. package/src/vite/utils/prerender-utils.ts +249 -0
  443. package/src/vite/utils/shared-utils.ts +269 -0
  444. package/CLAUDE.md +0 -43
  445. package/dist/vite/index.named-routes.gen.ts +0 -103
  446. package/src/browser/lru-cache.ts +0 -69
  447. package/src/browser/react/use-client-cache.ts +0 -56
  448. package/src/browser/request-controller.ts +0 -164
  449. package/src/browser/shallow.ts +0 -35
  450. package/src/cache/memory-store.ts +0 -253
  451. package/src/handles/index.ts +0 -6
  452. package/src/href-context.ts +0 -33
  453. package/src/network-error-thrower.tsx +0 -21
  454. package/src/router.gen.ts +0 -6
  455. package/src/static-handler.gen.ts +0 -5
  456. package/src/urls.gen.ts +0 -8
  457. package/src/vite/expose-internal-ids.ts +0 -1167
  458. package/src/vite/package-resolution.ts +0 -125
  459. package/src/vite/virtual-entries.ts +0 -114
  460. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,502 @@
1
+ /**
2
+ * Progressive Enhancement Handler
3
+ *
4
+ * Handles no-JS form submissions. When JavaScript is disabled, React renders
5
+ * forms with hidden fields ($ACTION_REF_*, $ACTION_KEY) containing the action
6
+ * reference. We detect these and return HTML instead of RSC stream.
7
+ */
8
+
9
+ import {
10
+ getRequestContext,
11
+ setRequestContextParams,
12
+ } from "../server/request-context.js";
13
+ import { getSSRSetup } from "./ssr-setup.js";
14
+ import type { MiddlewareFn } from "../router/middleware.js";
15
+ import { executeMiddleware } from "../router/middleware.js";
16
+ import { observePhase, PHASES } from "../router/instrument.js";
17
+ import { gateTransitions } from "./transition-gate.js";
18
+ import { resolvedHandleStream } from "../handles/deferred-resolution.js";
19
+ import type { RscPayload, ReactFormState } from "./types.js";
20
+ import {
21
+ createResponseWithMergedHeaders,
22
+ finalizeResponse,
23
+ buildRouteMiddlewareEntries,
24
+ } from "./helpers.js";
25
+ import type { HandlerContext } from "./handler-context.js";
26
+ import {
27
+ extractRedirectResponse,
28
+ warnNonRedirectPeResponse,
29
+ } from "./runtime-warnings.js";
30
+
31
+ export interface PeRouteMiddlewareInfo {
32
+ routeMiddleware?: Array<{
33
+ handler: MiddlewareFn;
34
+ params: Record<string, string>;
35
+ }>;
36
+ variables: Record<string, any>;
37
+ routeReverse?: (
38
+ name: string,
39
+ params?: Record<string, string>,
40
+ search?: Record<string, unknown>,
41
+ ) => string;
42
+ }
43
+
44
+ export async function handleProgressiveEnhancement<TEnv>(
45
+ ctx: HandlerContext<TEnv>,
46
+ request: Request,
47
+ env: TEnv,
48
+ url: URL,
49
+ isAction: boolean,
50
+ handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
51
+ nonce: string | undefined,
52
+ routeMwInfo?: PeRouteMiddlewareInfo,
53
+ ): Promise<Response | null> {
54
+ const contentType = request.headers.get("content-type") || "";
55
+ const isFormSubmission =
56
+ contentType.includes("multipart/form-data") ||
57
+ contentType.includes("application/x-www-form-urlencoded");
58
+
59
+ if (request.method !== "POST" || isAction || !isFormSubmission) {
60
+ return null;
61
+ }
62
+
63
+ // Clone the request to read FormData without consuming it.
64
+ // Wrap in try-catch so malformed POST bodies are reported as action
65
+ // errors, not routing errors from the outer catch in handler.ts.
66
+ let formData: FormData;
67
+ try {
68
+ formData = await request.clone().formData();
69
+ } catch (error) {
70
+ // Attempt error boundary rendering so the user sees a meaningful page.
71
+ const errorHtml = await renderPeErrorBoundary(
72
+ ctx,
73
+ request,
74
+ env,
75
+ url,
76
+ error,
77
+ handleStore,
78
+ nonce,
79
+ );
80
+ if (errorHtml) {
81
+ ctx.callOnError(error, "action", {
82
+ request,
83
+ url,
84
+ env,
85
+ handledByBoundary: true,
86
+ });
87
+ return errorHtml;
88
+ }
89
+
90
+ ctx.callOnError(error, "action", {
91
+ request,
92
+ url,
93
+ env,
94
+ handledByBoundary: false,
95
+ });
96
+ console.error("[RSC] Progressive enhancement form parse error:", error);
97
+ return createResponseWithMergedHeaders(null, { status: 400 });
98
+ }
99
+
100
+ // Look for React's progressive enhancement hidden fields
101
+ let isDirectAction = false;
102
+ let isUseActionState = false;
103
+ let directActionId: string | null = null;
104
+
105
+ formData.forEach((_value, key) => {
106
+ if (key.startsWith("$ACTION_ID_")) {
107
+ isDirectAction = true;
108
+ directActionId = key.slice("$ACTION_ID_".length);
109
+ } else if (key.startsWith("$ACTION_REF_")) {
110
+ isUseActionState = true;
111
+ }
112
+ });
113
+
114
+ if (!isDirectAction && !isUseActionState) {
115
+ return null;
116
+ }
117
+
118
+ // Execute action and return HTML
119
+ let actionResult: unknown = undefined;
120
+ let reactFormState: ReactFormState | null = null;
121
+ // Status for the fall-through re-render after a boundaryless action error.
122
+ // When an action throws and NO error boundary matches, the PE path re-renders
123
+ // the page (below) the same way it would after a successful action. Without
124
+ // this the re-render serves HTTP 200, diverging from the JS path which serves
125
+ // 500 (server-action.ts sets actionStatus=500 for the same boundaryless error).
126
+ // 500 is carried only into the final HTML response, never the redirect branch.
127
+ let boundarylessErrorStatus: number | undefined;
128
+
129
+ if (isUseActionState) {
130
+ // Decode and extract action identity before execution so error
131
+ // handlers can report actionId even when the action throws.
132
+ let useActionStateId: string | undefined;
133
+ try {
134
+ const boundAction = await ctx.decodeAction(formData);
135
+ // React's custom .bind() preserves $$id on server references.
136
+ useActionStateId = (boundAction as { $$id?: string }).$$id ?? undefined;
137
+ // Meter the no-JS form action as the action phase, same as the JS path.
138
+ actionResult = await observePhase(
139
+ PHASES.action(useActionStateId ?? "useActionState"),
140
+ () => boundAction(),
141
+ );
142
+ } catch (error) {
143
+ // Handle thrown redirect (e.g., throw redirect('/path'))
144
+ const redirectResponse = extractRedirectResponse(error);
145
+ if (redirectResponse) return redirectResponse;
146
+
147
+ // Attempt error boundary rendering for the PE path
148
+ const errorHtml = await renderPeErrorBoundary(
149
+ ctx,
150
+ request,
151
+ env,
152
+ url,
153
+ error,
154
+ handleStore,
155
+ nonce,
156
+ useActionStateId,
157
+ true, // an action ran and threw
158
+ );
159
+ if (errorHtml) return errorHtml;
160
+
161
+ ctx.callOnError(error, "action", {
162
+ request,
163
+ url,
164
+ env,
165
+ actionId: useActionStateId,
166
+ handledByBoundary: false,
167
+ });
168
+ console.error("[RSC] Progressive enhancement action error:", error);
169
+ // No boundary matched — the fall-through re-render must carry 500 to match
170
+ // the JS path's boundaryless-error status (server-action.ts).
171
+ boundarylessErrorStatus = 500;
172
+ }
173
+ } else if (isDirectAction && directActionId) {
174
+ const temporaryReferences = ctx.createTemporaryReferenceSet();
175
+
176
+ // INTENTIONAL JS/PE divergence (do NOT "fix" to match the JS reject path).
177
+ // On the JS path React Flight-encodes the action args, so decodeReply
178
+ // succeeds or a failure means a malformed body (rejected). On the no-JS PE
179
+ // path the browser submits a raw <form action={fn}> POST with NO encoded
180
+ // args, so decodeReply throws by design and the raw FormData IS the action
181
+ // argument (the React form-action convention: fn(formData)). Removing this
182
+ // fallback breaks every unbound no-JS form action (verified: it fails the
183
+ // progressive-enhancement dev+prod e2e suite). See #572 (decided: keep).
184
+ let args: unknown[] = [];
185
+ try {
186
+ args = await ctx.decodeReply(formData, { temporaryReferences });
187
+ } catch {
188
+ args = [formData];
189
+ }
190
+
191
+ try {
192
+ const loadedAction = await ctx.loadServerAction(directActionId);
193
+ actionResult = await observePhase(PHASES.action(directActionId), () =>
194
+ loadedAction.apply(null, args),
195
+ );
196
+ } catch (error) {
197
+ // Handle thrown redirect (e.g., throw redirect('/path'))
198
+ const redirectResponse = extractRedirectResponse(error);
199
+ if (redirectResponse) return redirectResponse;
200
+
201
+ // Attempt error boundary rendering for the PE path
202
+ const errorHtml = await renderPeErrorBoundary(
203
+ ctx,
204
+ request,
205
+ env,
206
+ url,
207
+ error,
208
+ handleStore,
209
+ nonce,
210
+ directActionId,
211
+ true, // an action ran and threw
212
+ );
213
+ if (errorHtml) return errorHtml;
214
+
215
+ ctx.callOnError(error, "action", {
216
+ request,
217
+ url,
218
+ env,
219
+ actionId: directActionId,
220
+ handledByBoundary: false,
221
+ });
222
+ console.error("[RSC] Progressive enhancement action error:", error);
223
+ // No boundary matched — the fall-through re-render must carry 500 to match
224
+ // the JS path's boundaryless-error status (server-action.ts).
225
+ boundarylessErrorStatus = 500;
226
+ }
227
+ }
228
+
229
+ // Handle Response returned from action during PE.
230
+ // In the JS path, executeServerAction intercepts redirect Responses and
231
+ // short-circuits. The PE path must handle them too.
232
+ if (actionResult instanceof Response) {
233
+ const redirectResponse = extractRedirectResponse(actionResult);
234
+ if (redirectResponse) return redirectResponse;
235
+ // W3: Non-redirect Response — discard it so it doesn't flow into
236
+ // decodeFormState or the re-render payload.
237
+ if (process.env.NODE_ENV !== "production") {
238
+ warnNonRedirectPeResponse();
239
+ }
240
+ actionResult = undefined;
241
+ }
242
+
243
+ // Decode form state for useActionState progressive enhancement
244
+ try {
245
+ reactFormState = await ctx.decodeFormState(actionResult, formData);
246
+ } catch (error) {
247
+ ctx.callOnError(error, "action", {
248
+ request,
249
+ url,
250
+ env,
251
+ handledByBoundary: false,
252
+ });
253
+ console.error("[RSC] Failed to decode form state:", error);
254
+ }
255
+
256
+ // Re-render the page and return HTML.
257
+ // Route middleware wraps the render so context variables, headers, and
258
+ // cookies set by route middleware are available during re-render — matching
259
+ // the behavior of JS-enabled requests.
260
+ const renderPage = async (): Promise<Response> => {
261
+ // Preserve the original POST request's headers (Authorization, Cookie,
262
+ // custom headers) so loaders that read request headers/cookies behave
263
+ // identically under PE and the JS action path. Drop body-framing headers
264
+ // from the bodyless GET and force the HTML accept.
265
+ const headers = new Headers(request.headers);
266
+ headers.delete("content-type");
267
+ headers.delete("content-length");
268
+ headers.delete("content-encoding");
269
+ headers.delete("transfer-encoding");
270
+ headers.set("accept", "text/html");
271
+ const renderRequest = new Request(url.toString(), {
272
+ method: "GET",
273
+ headers,
274
+ });
275
+
276
+ // JS/PE parity: this is an action's revalidation render, so mark it BEFORE
277
+ // matching — a stale `foregroundOnAction` cache entry must re-execute in the
278
+ // foreground during the re-render, exactly as the JS path's
279
+ // revalidateAfterAction does. The transition({ when }) gate fields below are
280
+ // set post-match (the gate reads them after rendering); foregroundOnAction
281
+ // reads _inActionRevalidation during the match, so it must be set here.
282
+ getRequestContext()._inActionRevalidation = true;
283
+
284
+ const match = await ctx.router.match(renderRequest, { env });
285
+
286
+ if (match.redirect) {
287
+ return createResponseWithMergedHeaders(null, {
288
+ status: 308,
289
+ headers: { Location: match.redirect },
290
+ });
291
+ }
292
+
293
+ // Expose the no-JS action to the transition({ when }) gate. currentUrl/Params
294
+ // are absent on this full-render path (no navigation snapshot); useActionState
295
+ // ids are block-scoped, so only a direct action id is available here.
296
+ // actionUrl is the page the action was submitted from (this request's url).
297
+ const peReqCtx = getRequestContext();
298
+ peReqCtx._gateActionId = directActionId ?? undefined;
299
+ peReqCtx._gateActionUrl = new URL(url);
300
+ peReqCtx._gateActionResult = actionResult;
301
+ peReqCtx._gateFormData = formData;
302
+
303
+ const payload: RscPayload = {
304
+ metadata: {
305
+ pathname: url.pathname,
306
+ routerId: ctx.router.id,
307
+ basename: ctx.router.basename,
308
+ segments: gateTransitions(
309
+ match.segments,
310
+ getRequestContext(),
311
+ ctx.router.onError,
312
+ ),
313
+ matched: match.matched,
314
+ diff: match.diff,
315
+ resolvedIds: match.resolvedIds,
316
+ params: match.params,
317
+ isPartial: false,
318
+ rootLayout: ctx.router.rootLayout,
319
+ // PE full render: resolve deferred handle values server-side.
320
+ handles: resolvedHandleStream(handleStore),
321
+ version: ctx.version,
322
+ stateCookieName: ctx.router.resolvedStateCookieName,
323
+ themeConfig: ctx.router.themeConfig,
324
+ warmupEnabled: ctx.router.warmupEnabled,
325
+ strictMode: ctx.router.strictMode,
326
+ initialTheme: getRequestContext().theme,
327
+ },
328
+ };
329
+
330
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
331
+ onError: (error: unknown) => {
332
+ ctx.callOnError(error, "rendering", { request, url, env });
333
+ },
334
+ });
335
+ // metricsStore=undefined is safe: the handler already stashed the early
336
+ // SSR setup promise on request variables, so getSSRSetup returns it
337
+ // without falling back to a fresh startSSRSetup.
338
+ const [ssrModule, streamMode] = await getSSRSetup(
339
+ ctx,
340
+ request,
341
+ env,
342
+ url,
343
+ undefined,
344
+ );
345
+ // reactFormState carries the useActionState payload via the SSR-option path
346
+ // (renderToReadableStream({ formState })); it does NOT travel on RscPayload.
347
+ const htmlStream = await ssrModule.renderHTML(rscStream, {
348
+ formState: reactFormState,
349
+ nonce,
350
+ streamMode,
351
+ });
352
+
353
+ return createResponseWithMergedHeaders(htmlStream, {
354
+ // boundarylessErrorStatus is set only when the action threw and no error
355
+ // boundary matched; it makes the re-render carry 500 like the JS path.
356
+ // The redirect branch above returns before this, so a redirect re-render
357
+ // keeps its 308 and is never overridden.
358
+ ...(boundarylessErrorStatus !== undefined
359
+ ? { status: boundarylessErrorStatus }
360
+ : {}),
361
+ headers: { "content-type": "text/html;charset=utf-8" },
362
+ });
363
+ };
364
+
365
+ // Execute route middleware wrapping the render, if any.
366
+ // finalizeResponse drains onResponse callbacks that middleware short-circuits
367
+ // may leave behind (executeMiddleware does not finalize them itself).
368
+ if (routeMwInfo?.routeMiddleware && routeMwInfo.routeMiddleware.length > 0) {
369
+ return finalizeResponse(
370
+ await executeMiddleware(
371
+ buildRouteMiddlewareEntries(routeMwInfo.routeMiddleware),
372
+ request,
373
+ env,
374
+ routeMwInfo.variables,
375
+ renderPage,
376
+ routeMwInfo.routeReverse,
377
+ ),
378
+ );
379
+ }
380
+
381
+ return renderPage();
382
+ }
383
+
384
+ /**
385
+ * Attempt to render an error boundary as full HTML for the PE path.
386
+ * Returns null if no error boundary is found (caller falls through to
387
+ * normal page re-render).
388
+ */
389
+ async function renderPeErrorBoundary<TEnv>(
390
+ ctx: HandlerContext<TEnv>,
391
+ request: Request,
392
+ env: TEnv,
393
+ url: URL,
394
+ error: unknown,
395
+ handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
396
+ nonce: string | undefined,
397
+ actionId?: string | null,
398
+ // True when an action actually ran and threw (vs a malformed form body, where
399
+ // no action executed). Drives _inActionRevalidation for JS/PE parity — it must
400
+ // NOT be inferred from actionId, since a useActionState bound action can run
401
+ // and throw with no $$id (actionId === undefined) yet still be an action error.
402
+ actionRan = false,
403
+ ): Promise<Response | null> {
404
+ // JS/PE parity for an action-triggered error re-render: a stale
405
+ // `foregroundOnAction` cache entry inside the error boundary must foreground
406
+ // too, exactly as the JS path (revalidateAfterAction sets this unconditionally
407
+ // before rendering the error boundary). Set BEFORE matchError (the cached fn
408
+ // runs during it). Gated on actionRan, NOT actionId — a malformed form body
409
+ // (actionRan=false) ran no action and must keep SWR.
410
+ if (actionRan) {
411
+ getRequestContext()._inActionRevalidation = true;
412
+ }
413
+
414
+ let errorResult;
415
+ try {
416
+ errorResult = await ctx.router.matchError(request, { env }, error, "route");
417
+ } catch (matchErr) {
418
+ ctx.callOnError(error, "action", {
419
+ request,
420
+ url,
421
+ env,
422
+ actionId: actionId ?? undefined,
423
+ handledByBoundary: false,
424
+ });
425
+ throw matchErr;
426
+ }
427
+
428
+ if (!errorResult) return null;
429
+
430
+ ctx.callOnError(error, "action", {
431
+ request,
432
+ url,
433
+ env,
434
+ actionId: actionId ?? undefined,
435
+ handledByBoundary: true,
436
+ });
437
+
438
+ setRequestContextParams(errorResult.params, errorResult.routeName);
439
+
440
+ // Only the failing action id + URL are in scope here (no formData/actionResult
441
+ // thread into this helper). Expose the URL only when the action id is known:
442
+ // this helper also handles malformed form bodies before action detection, and
443
+ // those should not look like action-triggered renders to transition({ when }).
444
+ if (actionId != null) {
445
+ const peErrCtx = getRequestContext();
446
+ peErrCtx._gateActionId = actionId;
447
+ peErrCtx._gateActionUrl = new URL(url);
448
+ }
449
+
450
+ const payload: RscPayload = {
451
+ metadata: {
452
+ pathname: url.pathname,
453
+ routerId: ctx.router.id,
454
+ basename: ctx.router.basename,
455
+ segments: gateTransitions(
456
+ errorResult.segments,
457
+ getRequestContext(),
458
+ ctx.router.onError,
459
+ ),
460
+ matched: errorResult.matched,
461
+ diff: errorResult.diff,
462
+ resolvedIds: errorResult.resolvedIds,
463
+ params: errorResult.params,
464
+ isPartial: false,
465
+ isError: true,
466
+ rootLayout: ctx.router.rootLayout,
467
+ // PE error-boundary full render: resolve deferred handle values server-side.
468
+ handles: resolvedHandleStream(handleStore),
469
+ version: ctx.version,
470
+ stateCookieName: ctx.router.resolvedStateCookieName,
471
+ themeConfig: ctx.router.themeConfig,
472
+ warmupEnabled: ctx.router.warmupEnabled,
473
+ strictMode: ctx.router.strictMode,
474
+ initialTheme: getRequestContext().theme,
475
+ },
476
+ };
477
+
478
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
479
+ onError: (error: unknown) => {
480
+ ctx.callOnError(error, "rendering", { request, url, env });
481
+ },
482
+ });
483
+ // metricsStore=undefined is safe: the handler already stashed the early
484
+ // SSR setup promise on request variables, so getSSRSetup returns it
485
+ // without falling back to a fresh startSSRSetup.
486
+ const [ssrModule, streamMode] = await getSSRSetup(
487
+ ctx,
488
+ request,
489
+ env,
490
+ url,
491
+ undefined,
492
+ );
493
+ const htmlStream = await ssrModule.renderHTML(rscStream, {
494
+ nonce,
495
+ streamMode,
496
+ });
497
+
498
+ return createResponseWithMergedHeaders(htmlStream, {
499
+ status: 500,
500
+ headers: { "content-type": "text/html;charset=utf-8" },
501
+ });
502
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Server-side open-redirect guard.
3
+ *
4
+ * Applied to the FINAL handler response (the single top-level return in
5
+ * `handler.ts`) so every browser-followed redirect honors the same same-origin
6
+ * rule the client enforces (`browser/validate-redirect-origin.ts`), via the one
7
+ * shared resolver in `redirect-origin.ts`. This is the server half of the
8
+ * client's existing guard: the client can only validate redirects its own JS
9
+ * navigates to (the SPA/fetch channel), so document-native redirects -- a no-JS
10
+ * PE form POST, a full-page GET `match.redirect`, a middleware `redirect()`
11
+ * short-circuit, a response-route 3xx -- reach the browser with no client in the
12
+ * loop. They all funnel through one handler return, so guarding there covers
13
+ * every one and any future redirect exit.
14
+ *
15
+ * Soft (SPA/Flight) redirects are 200/204 responses (`X-RSC-Redirect` header or
16
+ * `metadata.redirect` payload) and are NOT redirect Responses, so they never
17
+ * reach this guard -- they stay validated client-side.
18
+ *
19
+ * Behavior on a `Location` header:
20
+ * - same-origin / relative -> passes through unchanged
21
+ * - `redirect(url, { external: true })` (out-of-band brand present) and an
22
+ * http(s) target -> allowed (explicit, auditable, unforgeable opt-in)
23
+ * - branded but a non-http(s) target (e.g. `javascript:`) -> neutralized: the
24
+ * opt-in waives the same-origin rule, NOT scheme safety
25
+ * - cross-origin without the brand -> Location rewritten to the basename root
26
+ * (a safe same-origin landing, the document analog of the client's "stay put");
27
+ * dev logs the blocked target and points to `{ external: true }`.
28
+ *
29
+ * The opt-in is an out-of-band brand on the Response object (isExternalRedirect),
30
+ * never a wire header: a header is forgeable by an attacker-controlled upstream
31
+ * response a proxy-style response route copies through, which would defeat the
32
+ * guard without app code ever opting in. The reserved header name is stripped
33
+ * defensively so a forged value can never reach the browser.
34
+ */
35
+
36
+ import { isRedirectResponse } from "../response-utils.js";
37
+ import {
38
+ resolveSameOriginRedirect,
39
+ resolveExternalRedirect,
40
+ isExternalRedirect,
41
+ EXTERNAL_REDIRECT_MARKER,
42
+ } from "../redirect-origin.js";
43
+ import { carryOverRedirectHeaders } from "./helpers.js";
44
+
45
+ export function guardOutgoingRedirect(
46
+ response: Response,
47
+ requestOrigin: string,
48
+ basename: string | undefined,
49
+ ): Response {
50
+ // Only 3xx + Location responses (document-native redirects) are guarded.
51
+ if (!isRedirectResponse(response)) {
52
+ return response;
53
+ }
54
+
55
+ // The reserved marker is never a trust signal. Strip any value -- forged by a
56
+ // proxied upstream or otherwise -- so it can never reach the browser. Trust
57
+ // comes solely from the out-of-band brand below.
58
+ try {
59
+ response.headers.delete(EXTERNAL_REDIRECT_MARKER);
60
+ } catch {
61
+ // Some platform responses carry immutable headers; the header is inert on
62
+ // the browser, so a failed strip is harmless.
63
+ }
64
+
65
+ // isRedirectResponse guarantees a truthy Location.
66
+ const location = response.headers.get("Location")!;
67
+
68
+ // Explicit opt-in via redirect(url, { external: true }): allow an off-host
69
+ // target, but only an http(s) one. external waives the same-origin rule, not
70
+ // scheme safety -- a branded javascript:/data: target falls through to be
71
+ // neutralized so it can never become a scriptable navigation downstream.
72
+ if (isExternalRedirect(response)) {
73
+ if (resolveExternalRedirect(location, requestOrigin) !== null) {
74
+ return response;
75
+ }
76
+ } else if (resolveSameOriginRedirect(location, requestOrigin) !== null) {
77
+ return response;
78
+ }
79
+
80
+ // Cross-origin (or unsafe-scheme external): neutralize to a safe same-origin
81
+ // landing.
82
+ const safeTarget = basename && basename !== "/" ? basename : "/";
83
+ if (process.env.NODE_ENV !== "production") {
84
+ console.error(
85
+ `[rango] Blocked cross-origin redirect to "${location}"; sent to ` +
86
+ `"${safeTarget}" instead. To redirect off-host on purpose, use ` +
87
+ `redirect(url, { external: true }).`,
88
+ );
89
+ }
90
+
91
+ const blocked = new Response(null, {
92
+ status: response.status,
93
+ headers: { Location: safeTarget },
94
+ });
95
+ // Preserve cookies and any other headers (Set-Cookie, Server-Timing, ...);
96
+ // carryOverRedirectHeaders intentionally skips Location.
97
+ carryOverRedirectHeaders(response, blocked);
98
+ return blocked;
99
+ }