@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
@@ -0,0 +1,68 @@
1
+ // Builds the error thrown when a create*() call (createLoader / createHandle)
2
+ // reaches runtime without an injected $$id. The exposeInternalIds Vite transform
3
+ // injects $$id only for an EXPORTED const declaration, so a non-exported const,
4
+ // an `export let/var`, or an inline create*() call gets none. Previously this
5
+ // failed with a terse message and no source location; this helper adds the
6
+ // offending call site (best-effort, from the stack) and actionable guidance.
7
+ //
8
+ // The "<Kind> is missing $$id" prefix is preserved so existing tests and any
9
+ // log scrapers keep matching. Dev-only: the call sites guard on
10
+ // process.env.NODE_ENV === "development", so production builds fold the branch
11
+ // away and tree-shake this module out.
12
+
13
+ // create*() implementation files to skip when locating the user's call site.
14
+ const SELF_FILES = new Set([
15
+ "missing-id-error",
16
+ "loader",
17
+ "loader.rsc",
18
+ "handle",
19
+ ]);
20
+
21
+ /**
22
+ * Best-effort "path:line:column" of the user's create*() call, parsed from the
23
+ * current stack. Skips @rangojs/router internals and node_modules. Returns
24
+ * undefined if nothing usable is found (stack parsing is inherently fragile).
25
+ */
26
+ function findUserCallSite(): string | undefined {
27
+ try {
28
+ const stack = new Error().stack;
29
+ if (!stack) return undefined;
30
+ for (const frame of stack.split("\n").slice(1)) {
31
+ const m = frame.match(
32
+ /(?:\(|@|\s)(?:file:\/\/)?((?:\/|[A-Za-z]:[\\/])[^()\s]+?\.(?:ts|tsx|js|jsx|mts|cts)):(\d+):(\d+)\)?/,
33
+ );
34
+ if (!m) continue;
35
+ const path = m[1];
36
+ if (path.includes("node_modules") || path.includes("@rangojs/router")) {
37
+ continue;
38
+ }
39
+ const base = path
40
+ .split(/[\\/]/)
41
+ .pop()!
42
+ .replace(/\.(?:ts|tsx|js|jsx|mts|cts)$/, "");
43
+ if (SELF_FILES.has(base)) continue;
44
+ return `${path}:${m[2]}:${m[3]}`;
45
+ }
46
+ } catch {
47
+ // best-effort only
48
+ }
49
+ return undefined;
50
+ }
51
+
52
+ export function missingInjectedIdError(
53
+ kind: "Loader" | "Handle",
54
+ fnName: "createLoader" | "createHandle",
55
+ ): Error {
56
+ const site = findUserCallSite();
57
+ const at = site ? ` (created at ${site})` : "";
58
+ return new Error(
59
+ `[rango] ${kind} is missing $$id${at}.\n` +
60
+ `The @rangojs/router:expose-internal-ids Vite transform injects ${fnName}()'s ` +
61
+ `stable $$id from an EXPORTED const declaration only:\n` +
62
+ ` export const X = ${fnName}(...)\n` +
63
+ ` const X = ${fnName}(...); export { X }\n` +
64
+ `A non-exported const, an \`export let/var\`, or an inline ${fnName}(...) ` +
65
+ `call gets no $$id — export it as \`export const\`. (A matching ` +
66
+ `"Unsupported ${fnName} shape" warning names the exact file:line.)`,
67
+ );
68
+ }
@@ -1,4 +1,4 @@
1
- import { Context, createContext, type ReactNode } from "react";
1
+ import { type Context, createContext, type ReactNode } from "react";
2
2
  import type { ResolvedSegment } from "./types";
3
3
 
4
4
  export interface OutletContextValue {
@@ -5,11 +5,7 @@ import { OutletContext, type OutletContextValue } from "./outlet-context.js";
5
5
  import type { ResolvedSegment } from "./types.js";
6
6
 
7
7
  /**
8
- * Provider for outlet content - used internally by renderSegments
9
- *
10
- * Stores a reference to parent context so useLoader can walk up the chain
11
- * to find loader data from parent layouts. If this segment defines a loading
12
- * component, Outlet will wrap content with Suspense using that as fallback.
8
+ * Outlet content provider stores parent context for useLoader chain walking.
13
9
  */
14
10
  export function OutletProvider({
15
11
  content,
@@ -1,32 +1,32 @@
1
1
  /**
2
2
  * Deterministic param hashing for prerender storage keys.
3
- *
4
- * Used at build time (child process) to generate filenames and at
5
- * runtime (worker) to look up pre-rendered data. Both environments
6
- * must produce identical hashes for the same params.
7
- *
8
- * Uses a simple DJB2-based hash that works in all JS environments
9
- * (Node.js, Cloudflare Workers, browsers) without crypto imports.
3
+ * Used at build time and runtime; both must produce identical hashes.
4
+ * DJB2-based; works in all JS environments without crypto imports.
10
5
  */
11
6
 
12
- /**
13
- * Compute a deterministic hash string from route params.
14
- * For static routes (no params), returns "_".
15
- */
7
+ import { encodeKV } from "../encode-kv.js";
8
+
9
+ // For static routes (no params), returns "_".
16
10
  export function hashParams(params: Record<string, string>): string {
17
11
  const entries = Object.entries(params);
18
12
  if (entries.length === 0) return "_";
19
13
 
20
- const sorted = entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
21
- const str = sorted
22
- .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
23
- .join("&");
24
- return djb2Hex(str);
14
+ // Byte-order sort + encodeURIComponent join (see encodeKV); output is
15
+ // byte-identical to the prior inline implementation, so build-time and
16
+ // runtime hashes stay stable.
17
+ return djb2Hex(encodeKV(entries, { sort: true }));
25
18
  }
26
19
 
27
20
  /**
28
21
  * DJB2 hash returning an 8-char hex string.
29
22
  * Deterministic across all JS runtimes.
23
+ *
24
+ * 32-bit output: per-route collision probability hits ~50% near ~77k distinct
25
+ * param sets (birthday bound). The production store keys solely on
26
+ * routeName/paramHash and does not verify the canonical param string, so a
27
+ * collision serves the surviving entry for both param sets. Benign for typical
28
+ * catalogs; revisit (wider hash or stored-param verification) before
29
+ * pre-rendering hundreds of thousands of pages per route.
30
30
  */
31
31
  function djb2Hex(str: string): string {
32
32
  let hash = 5381;
@@ -1,21 +1,17 @@
1
1
  /**
2
- * Prerender Store
3
- *
4
- * Reads pre-rendered segment data from the worker bundle at build time.
5
- * The manifest module is lazily loaded via globalThis.__loadPrerenderManifestModule,
6
- * a function injected into the RSC entry that returns the manifest module
7
- * containing a key-to-specifier map and a `loadPrerenderAsset` function
8
- * that anchors import() resolution relative to the manifest file.
2
+ * Prerender Store — reads pre-rendered segment data from the worker bundle.
3
+ * Manifest module (injected via globalThis.__loadPrerenderManifestModule)
4
+ * contains key-to-specifier map and loadPrerenderAsset for import() resolution.
9
5
  */
10
6
 
11
- import type {
12
- SerializedSegmentData,
13
- SegmentHandleData,
14
- } from "../cache/types.js";
7
+ import type { SerializedSegmentData } from "../cache/types.js";
15
8
 
16
9
  export interface PrerenderEntry {
17
10
  segments: SerializedSegmentData[];
18
- handles: Record<string, SegmentHandleData>;
11
+ /** RSC-encoded handle map (see handle-snapshot.ts encodeHandles); "" when the
12
+ * route pushed no handles. Encoded so Promise/ReactNode handle values survive
13
+ * the JSON-serialized build artifact / dev wire, identical to the runtime cache. */
14
+ handles: string;
19
15
  }
20
16
 
21
17
  export interface PrerenderStore {
@@ -28,7 +24,9 @@ export interface PrerenderStore {
28
24
 
29
25
  export interface StaticEntry {
30
26
  encoded: string;
31
- handles: Record<string, unknown[]>;
27
+ /** RSC-encoded single-segment handle data (see encodeHandleValue); "" when the
28
+ * Static handler pushed no handles. */
29
+ handles: string;
32
30
  }
33
31
 
34
32
  export interface StaticStore {
@@ -99,13 +97,20 @@ export function createPrerenderStore(): PrerenderStore | null {
99
97
  if (!globalThis.__loadPrerenderManifestModule) return null;
100
98
 
101
99
  const cache = new Map<string, Promise<PrerenderEntry | null>>();
102
- let manifestModulePromise: Promise<PrerenderManifestModule | null> | null =
103
- null;
100
+ let manifestModulePromise: Promise<PrerenderManifestModule> | null = null;
104
101
 
105
- function loadManifestModule(): Promise<PrerenderManifestModule | null> {
102
+ function loadManifestModule(): Promise<PrerenderManifestModule> {
106
103
  if (!manifestModulePromise) {
104
+ // Do not cache a failed manifest-module load: clear the memoized promise
105
+ // on rejection so the next get() retries, and let the error propagate
106
+ // (consistent with the per-asset load policy below) instead of caching a
107
+ // null for the isolate lifetime, which would silently degrade every
108
+ // prerendered route to a miss after one transient failure.
107
109
  manifestModulePromise = globalThis.__loadPrerenderManifestModule!().catch(
108
- () => null,
110
+ (err) => {
111
+ manifestModulePromise = null;
112
+ throw err;
113
+ },
109
114
  );
110
115
  }
111
116
  return manifestModulePromise;
@@ -118,7 +123,6 @@ export function createPrerenderStore(): PrerenderStore | null {
118
123
  if (cached) return cached;
119
124
 
120
125
  const promise = loadManifestModule().then((mod) => {
121
- if (!mod) return null;
122
126
  const specifier = mod.default[key];
123
127
  if (!specifier) return null;
124
128
  // Let asset load errors propagate — a missing/corrupted artifact
@@ -127,29 +131,20 @@ export function createPrerenderStore(): PrerenderStore | null {
127
131
  // (which the handler stub would misreport as a 404).
128
132
  return mod.loadPrerenderAsset(specifier).then((asset) => asset.default);
129
133
  });
130
- cache.set(key, promise);
134
+ // Only memoize once the manifest module resolved: a manifest-load
135
+ // rejection must not poison the per-key cache, or the retry above is moot.
136
+ cache.set(
137
+ key,
138
+ promise.catch((err) => {
139
+ cache.delete(key);
140
+ throw err;
141
+ }),
142
+ );
131
143
  return promise;
132
144
  },
133
145
  };
134
146
  }
135
147
 
136
- /**
137
- * Load the prerender manifest index for test introspection.
138
- * Returns the key→specifier map or null if unavailable.
139
- */
140
- export async function loadPrerenderManifestIndex(): Promise<Record<
141
- string,
142
- string
143
- > | null> {
144
- if (!globalThis.__loadPrerenderManifestModule) return null;
145
- try {
146
- const mod = await globalThis.__loadPrerenderManifestModule();
147
- return mod.default;
148
- } catch {
149
- return null;
150
- }
151
- }
152
-
153
148
  /**
154
149
  * Create a static segment store.
155
150
  * Production only: backed by globalThis.__STATIC_MANIFEST injected at build time.
@@ -174,7 +169,7 @@ export function createStaticStore(): StaticStore | null {
174
169
  const val = mod.default;
175
170
  // Normalize: string-only (no handles) or { encoded, handles }
176
171
  if (typeof val === "string") {
177
- return { encoded: val, handles: {} } as StaticEntry;
172
+ return { encoded: val, handles: "" } as StaticEntry;
178
173
  }
179
174
  return val as StaticEntry;
180
175
  })
package/src/prerender.ts CHANGED
@@ -33,11 +33,13 @@ import type {
33
33
  ExtractParams,
34
34
  } from "./types.js";
35
35
  import type { Handle } from "./handle.js";
36
+ import type { HandlePush } from "./defer.js";
36
37
  import type { ContextVar } from "./context-var.js";
37
38
  import type { ReverseFunction } from "./reverse.js";
38
39
  import type { DefaultReverseRouteMap } from "./types/global-namespace.js";
39
40
  import type { UseItems, HandlerUseItem } from "./route-types.js";
40
41
  import { isCachedFunction } from "./cache/taint.js";
42
+ import { isUnderTestRunner } from "./runtime-env.js";
41
43
 
42
44
  // -- Named route resolution types -------------------------------------------
43
45
 
@@ -69,9 +71,9 @@ type BuildReverseFunction = [DefaultReverseRouteMap] extends [
69
71
  * Default route map for Prerender named route resolution.
70
72
  * Uses GeneratedRouteMap (from gen file) to avoid circular dependencies.
71
73
  */
72
- type DefaultPrerenderRouteMap = keyof RSCRouter.GeneratedRouteMap extends never
74
+ type DefaultPrerenderRouteMap = keyof Rango.GeneratedRouteMap extends never
73
75
  ? {}
74
- : RSCRouter.GeneratedRouteMap;
76
+ : Rango.GeneratedRouteMap;
75
77
 
76
78
  /** Extract params from a route map entry (string pattern or { path } object). */
77
79
  type ExtractParamsFromEntry<TEntry> = TEntry extends string
@@ -164,8 +166,14 @@ export interface BuildContext<TParams> {
164
166
  (key: string, value: any): void;
165
167
  };
166
168
 
167
- /** Push handle data (frozen into pre-rendered output at build time). */
168
- use: <T>(handle: Handle<T>) => (data: T) => void;
169
+ /**
170
+ * Push handle data (frozen into pre-rendered output at build time). Returns
171
+ * the full push function, including `.defer()` — a deferred slot resolved by
172
+ * a deep async component during the prerender render is awaited before the
173
+ * artifact is baked (resolve-by-default), so the baked output holds the
174
+ * resolved value.
175
+ */
176
+ use: <T>(handle: Handle<T>) => HandlePush<T>;
169
177
 
170
178
  /** Synthetic URL built from pattern + params (no real request). */
171
179
  url: URL;
@@ -221,8 +229,14 @@ export interface StaticBuildContext {
221
229
  (key: string, value: any): void;
222
230
  };
223
231
 
224
- /** Push handle data (frozen into pre-rendered output at build time). */
225
- use: <T>(handle: Handle<T>) => (data: T) => void;
232
+ /**
233
+ * Push handle data (frozen into pre-rendered output at build time). Returns
234
+ * the full push function, including `.defer()` — a deferred slot resolved by
235
+ * a deep async component during the prerender render is awaited before the
236
+ * artifact is baked (resolve-by-default), so the baked output holds the
237
+ * resolved value.
238
+ */
239
+ use: <T>(handle: Handle<T>) => HandlePush<T>;
226
240
 
227
241
  /** URL generation by route name. */
228
242
  reverse: BuildReverseFunction;
@@ -273,6 +287,11 @@ export interface PrerenderHandlerDefinition<
273
287
  use?: () => UseItems<HandlerUseItem>;
274
288
  }
275
289
 
290
+ // Process-stable fallback id counter (mirrors createHandle / createLoader). Only
291
+ // assigned in a bare unit test where the Vite plugin did not inject an id; never
292
+ // fires in a real build (the plugin always injects).
293
+ let runtimePrerenderIdCounter = 0;
294
+
276
295
  // -- Overloads --------------------------------------------------------------
277
296
  //
278
297
  // T accepts: named route string (global or .local) OR explicit param object.
@@ -376,12 +395,27 @@ export function Prerender<TParams extends Record<string, any>>(
376
395
  );
377
396
  }
378
397
 
379
- if (!id) {
398
+ // Throw unless under a test runner. The plugin always injects $$id for a
399
+ // supported `export const` Prerender on every build, so a missing id means
400
+ // either no plugin (a bare test — fall back below) or an UNSUPPORTED shape the
401
+ // plugin silently skipped (dev OR a real build — fail loud; a synthetic id
402
+ // would degrade to a silent prerender miss). The message is already small (no
403
+ // stack-parsing diagnostic), so it ships as-is. isUnderTestRunner() is
404
+ // runtime-safe — never a bare `process.env` access.
405
+ if (!id && !isUnderTestRunner()) {
380
406
  throw new Error(
381
- "[rsc-router] Prerender: missing $$id. " +
382
- "Ensure the exposeInternalIds Vite plugin is configured.",
407
+ "[rango] Prerender: missing $$id. Use `export const X = Prerender(...)` " +
408
+ "and ensure the exposeInternalIds Vite plugin is configured.",
383
409
  );
384
410
  }
411
+ // Under vitest with no plugin id: assign a process-stable runtime id so a
412
+ // whole-app router with Prerender routes constructs in a bare test (for
413
+ // dispatch / assertGeneratedRoutesMatch). Never reached in a real build (the
414
+ // throw above fires there); prerender storage/lookup keys on routeName +
415
+ // paramHash, never $$id (mirrors createHandle / createLoader).
416
+ if (!id) {
417
+ id = `__rango_runtime_prerender_${runtimePrerenderIdCounter++}`;
418
+ }
385
419
 
386
420
  return {
387
421
  __brand: "prerenderHandler" as const,
@@ -421,6 +455,40 @@ export function isPrerenderPassthrough(
421
455
  );
422
456
  }
423
457
 
458
+ /**
459
+ * Detect whether any resolved segment carries the passthrough sentinel.
460
+ *
461
+ * A build handler signals passthrough by returning `ctx.passthrough()` (the
462
+ * PRERENDER_PASSTHROUGH sentinel), which lands on the segment's `component`.
463
+ * But when the route declares `loading()`, the handler result is deferred
464
+ * upstream (segment-resolution/fresh.ts), so `component` is a thenable resolving
465
+ * to the sentinel rather than the sentinel itself — a synchronous
466
+ * `isPrerenderPassthrough(component)` on the Promise returns false and the build
467
+ * bakes a corrupt artifact instead of deferring. Resolve thenables first.
468
+ *
469
+ * Rejections are swallowed here: a throwing build handler resurfaces during
470
+ * segment serialization, preserving the prior error-handling behavior.
471
+ */
472
+ export async function detectPrerenderPassthrough(
473
+ segments: ReadonlyArray<{ component: unknown }>,
474
+ ): Promise<boolean> {
475
+ for (const seg of segments) {
476
+ let component: unknown = seg.component;
477
+ if (
478
+ component &&
479
+ typeof (component as { then?: unknown }).then === "function"
480
+ ) {
481
+ try {
482
+ component = await component;
483
+ } catch {
484
+ continue;
485
+ }
486
+ }
487
+ if (isPrerenderPassthrough(component)) return true;
488
+ }
489
+ return false;
490
+ }
491
+
424
492
  // -- Type guards ------------------------------------------------------------
425
493
 
426
494
  /**
@@ -499,7 +567,7 @@ export function Passthrough<
499
567
  ): PassthroughHandlerDefinition<TParams, TEnv> {
500
568
  if (!isPrerenderHandler(prerenderDef)) {
501
569
  throw new Error(
502
- "[rsc-router] Passthrough: first argument must be a Prerender() definition.",
570
+ "[rango] Passthrough: first argument must be a Prerender() definition.",
503
571
  );
504
572
  }
505
573
  return {
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Runtime-neutral same-origin redirect rule.
3
+ *
4
+ * Shared by the client redirect guard (`browser/validate-redirect-origin.ts`,
5
+ * which validates redirect targets the client JS is about to navigate to) and
6
+ * the server outgoing-redirect guard (`rsc/redirect-guard.ts`, which validates
7
+ * every browser-followed `Location` header before it leaves the handler). Kept
8
+ * at the `src/` root so both layers import the ONE rule and cannot drift -- a
9
+ * cross-origin target blocked on the JS/fetch path is blocked identically on the
10
+ * no-JS (PE) and full-page document paths.
11
+ */
12
+
13
+ /**
14
+ * Resolve a redirect target against the current origin.
15
+ *
16
+ * Returns the canonical (normalized) same-origin href -- which also collapses
17
+ * protocol-relative (`//evil.com`) and other ambiguous forms -- or `null` when
18
+ * the target resolves to a different origin or is unparseable. Pure: no logging,
19
+ * no side effects.
20
+ */
21
+ export function resolveSameOriginRedirect(
22
+ url: string,
23
+ currentOrigin: string,
24
+ ): string | null {
25
+ try {
26
+ const target = new URL(url, currentOrigin);
27
+ if (target.origin !== currentOrigin) {
28
+ return null;
29
+ }
30
+ return target.href;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Validate an explicit off-origin redirect target (`redirect(url, { external:
38
+ * true })`).
39
+ *
40
+ * `external` opts out of the same-origin rule, but NOT out of scheme safety:
41
+ * only `http:`/`https:` targets are allowed. A redirect ultimately reaches the
42
+ * browser via `window.location.assign()` on the SPA/action client paths, so a
43
+ * forged or mistaken `redirect("javascript:...", { external: true })` would be a
44
+ * scriptable navigation if the scheme were not checked here. Returns the
45
+ * normalized href for an http(s) target (same- or cross-origin), or `null`
46
+ * otherwise. Pure: no logging, no side effects.
47
+ */
48
+ export function resolveExternalRedirect(
49
+ url: string,
50
+ currentOrigin: string,
51
+ ): string | null {
52
+ try {
53
+ const target = new URL(url, currentOrigin);
54
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
55
+ return null;
56
+ }
57
+ return target.href;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * The safe same-origin landing for a blocked redirect.
65
+ *
66
+ * Every guard that neutralizes a cross-origin/unsafe redirect target sends the
67
+ * browser here instead: the app's basename root, or `"/"` when unset. Kept
68
+ * beside the resolvers so the "where does a blocked redirect go" answer lives
69
+ * in ONE place -- the server 3xx guard (`rsc/redirect-guard.ts`) and the
70
+ * shell-HIT degradation path (`rsc/rsc-rendering.ts`) must agree, or a blocked
71
+ * redirect lands differently depending on which exit it took.
72
+ */
73
+ export function safeSameOriginLanding(basename: string | undefined): string {
74
+ return basename && basename !== "/" ? basename : "/";
75
+ }
76
+
77
+ /**
78
+ * Out-of-band brand for `redirect(url, { external: true })`.
79
+ *
80
+ * The external opt-in MUST be settable only by app code calling `redirect(...,
81
+ * { external: true })`, never by an attacker. An earlier design carried the
82
+ * opt-in as a wire header (`x-rango-redirect-external`), but a wire header is
83
+ * forgeable: a proxy-style response route that copies an attacker-controlled
84
+ * upstream response's headers would let `302 Location: https://evil` plus that
85
+ * header bypass the same-origin guard without app code ever opting in. So the
86
+ * opt-in is now an out-of-band brand on the Response object itself, tracked in a
87
+ * `WeakSet` that cannot cross the wire. `redirect()` brands the Response; the
88
+ * small set of internal redirect-rebuild paths (middleware `mergeResponse`,
89
+ * `carryOverRedirectHeaders`, the response-route rewrap) transfer the brand onto
90
+ * the rebuilt Response; the guard and the SPA intercept read it. An upstream
91
+ * Response an app proxies is never branded, so its forged header is inert.
92
+ *
93
+ * Fail-closed: if a rebuild path ever drops the brand, the redirect is
94
+ * neutralized to the app root rather than allowed off-host.
95
+ */
96
+ const externalRedirects = new WeakSet<Response>();
97
+
98
+ /** Brand a Response as an explicit `{ external: true }` redirect (out-of-band). */
99
+ export function markExternalRedirect(response: Response): void {
100
+ externalRedirects.add(response);
101
+ }
102
+
103
+ /** Read the out-of-band `{ external: true }` brand off a Response. */
104
+ export function isExternalRedirect(response: Response): boolean {
105
+ return externalRedirects.has(response);
106
+ }
107
+
108
+ /**
109
+ * Reserved internal header name. No longer a trust signal -- the external
110
+ * opt-in is the out-of-band brand above. It is kept only so the redirect-rebuild
111
+ * paths and the guard can defensively strip any value (e.g. one forged by a
112
+ * proxied upstream) and guarantee it never reaches the browser.
113
+ */
114
+ export const EXTERNAL_REDIRECT_MARKER: string = "x-rango-redirect-external";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Escape a string for literal use inside a RegExp. Single source of truth for
3
+ * the router runtime (matching) and the vite build (transform/scan); a pure,
4
+ * dependency-free leaf so both environments can share it.
5
+ */
6
+ export function escapeRegExp(input: string): string {
7
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8
+ }
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ interface RenderErrorThrowerProps {
6
+ error: unknown;
7
+ }
8
+
9
+ /**
10
+ * Client component that throws the given error during render, so the nearest
11
+ * error boundary catches it. Errors thrown during render are caught by error
12
+ * boundaries; async errors (rejected promises) are not -- which is why the
13
+ * navigation bridge funnels processing failures through this component instead
14
+ * of letting them surface as uncaught rejections.
15
+ */
16
+ export function RenderErrorThrower({
17
+ error,
18
+ }: RenderErrorThrowerProps): ReactNode {
19
+ throw error;
20
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Runtime-neutral Response shape utilities.
3
+ *
4
+ * Kept at the src/ root so both `router/` and `rsc/` can depend on it
5
+ * without creating a cross-layer import cycle.
6
+ */
7
+
8
+ /**
9
+ * True when a Response represents a WebSocket upgrade handoff and must not
10
+ * be reconstructed or mutated:
11
+ *
12
+ * - Status 101 (Switching Protocols) is outside the standard Response
13
+ * constructor's 200–599 range, so `new Response(body, { status: 101 })`
14
+ * throws RangeError on Node/undici and any spec-compliant runtime.
15
+ * - Cloudflare's workerd attaches a non-standard `webSocket` property on
16
+ * the upgrade Response (e.g. from `acceptWebSocket`/`handleWebSocketUpgrade`
17
+ * or the `agents` library's `routeAgentRequest`). That property is dropped
18
+ * by a `new Response(...)` copy, breaking the upgrade even on workerd
19
+ * where the status range is relaxed.
20
+ *
21
+ * Callers should short-circuit header/body merges for these responses.
22
+ */
23
+ export function isWebSocketUpgradeResponse(response: Response): boolean {
24
+ return (
25
+ response.status === 101 ||
26
+ (response as unknown as { webSocket?: unknown }).webSocket != null
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Append `Accept` to a response's `Vary` header without duplicating it.
32
+ *
33
+ * Content-negotiated responses already carry `Vary: Accept` from the
34
+ * upstream layer (response-route-handler's callHandlerWithVary, or
35
+ * handleRscRendering baking `accept` into its vary list). The negotiated
36
+ * post-append in the handler would otherwise emit `Vary: Accept, Accept`,
37
+ * a redundant token some proxies/CDNs treat as a distinct cache key.
38
+ * Token match is case-insensitive (HTTP field tokens are case-insensitive)
39
+ * and whitespace-tolerant.
40
+ */
41
+ export function appendVaryAccept(response: Response): void {
42
+ const existing = response.headers.get("Vary");
43
+ if (!existing) {
44
+ response.headers.set("Vary", "Accept");
45
+ return;
46
+ }
47
+ const hasAccept = existing
48
+ .split(",")
49
+ .some((token) => token.trim().toLowerCase() === "accept");
50
+ if (!hasAccept) {
51
+ response.headers.append("Vary", "Accept");
52
+ }
53
+ }
54
+
55
+ // Location truthiness (not presence) so an empty `Location: ""` is not a redirect.
56
+ export function isRedirectResponse(response: Response): boolean {
57
+ return (
58
+ response.status >= 300 &&
59
+ response.status < 400 &&
60
+ Boolean(response.headers.get("Location"))
61
+ );
62
+ }