@rangojs/router 0.0.0-experimental.135c6902 → 0.0.0-experimental.136

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 (404) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +440 -133
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3386 -1111
  6. package/dist/vite/index.js.bak +5448 -0
  7. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  8. package/package.json +78 -25
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +64 -2
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +247 -23
  13. package/skills/caching/SKILL.md +281 -11
  14. package/skills/composability/SKILL.md +27 -3
  15. package/skills/css/SKILL.md +76 -0
  16. package/skills/debug-manifest/SKILL.md +4 -2
  17. package/skills/document-cache/SKILL.md +78 -55
  18. package/skills/handler-use/SKILL.md +364 -0
  19. package/skills/hooks/SKILL.md +250 -30
  20. package/skills/host-router/SKILL.md +83 -23
  21. package/skills/i18n/SKILL.md +276 -0
  22. package/skills/intercept/SKILL.md +87 -18
  23. package/skills/layout/SKILL.md +35 -9
  24. package/skills/links/SKILL.md +249 -17
  25. package/skills/loader/SKILL.md +289 -53
  26. package/skills/middleware/SKILL.md +52 -13
  27. package/skills/migrate-nextjs/SKILL.md +584 -0
  28. package/skills/migrate-react-router/SKILL.md +914 -0
  29. package/skills/mime-routes/SKILL.md +28 -1
  30. package/skills/observability/SKILL.md +172 -0
  31. package/skills/parallel/SKILL.md +144 -7
  32. package/skills/prerender/SKILL.md +172 -125
  33. package/skills/rango/SKILL.md +251 -22
  34. package/skills/react-compiler/SKILL.md +168 -0
  35. package/skills/response-routes/SKILL.md +123 -48
  36. package/skills/route/SKILL.md +101 -5
  37. package/skills/router-setup/SKILL.md +117 -10
  38. package/skills/scripts/SKILL.md +179 -0
  39. package/skills/server-actions/SKILL.md +775 -0
  40. package/skills/streams-and-websockets/SKILL.md +283 -0
  41. package/skills/tailwind/SKILL.md +27 -3
  42. package/skills/testing/SKILL.md +130 -0
  43. package/skills/testing/bindings.md +103 -0
  44. package/skills/testing/cache-prerender.md +127 -0
  45. package/skills/testing/client-components.md +124 -0
  46. package/skills/testing/e2e-parity.md +125 -0
  47. package/skills/testing/flight.md +91 -0
  48. package/skills/testing/handles.md +129 -0
  49. package/skills/testing/loader.md +128 -0
  50. package/skills/testing/middleware.md +99 -0
  51. package/skills/testing/render-handler.md +122 -0
  52. package/skills/testing/response-routes.md +95 -0
  53. package/skills/testing/reverse-and-types.md +84 -0
  54. package/skills/testing/server-actions.md +107 -0
  55. package/skills/testing/server-tree.md +128 -0
  56. package/skills/testing/setup.md +123 -0
  57. package/skills/typesafety/SKILL.md +332 -29
  58. package/skills/use-cache/SKILL.md +57 -14
  59. package/skills/view-transitions/SKILL.md +337 -0
  60. package/src/__augment-tests__/augment.ts +81 -0
  61. package/src/__augment-tests__/augmented.check.ts +116 -0
  62. package/src/__internal.ts +1 -66
  63. package/src/browser/action-coordinator.ts +53 -36
  64. package/src/browser/action-fence.ts +47 -0
  65. package/src/browser/app-shell.ts +39 -0
  66. package/src/browser/app-version.ts +14 -0
  67. package/src/browser/connection-warmup.ts +134 -0
  68. package/src/browser/cookie-name.ts +140 -0
  69. package/src/browser/event-controller.ts +192 -150
  70. package/src/browser/history-state.ts +21 -0
  71. package/src/browser/index.ts +3 -3
  72. package/src/browser/invalidate-client-cache.ts +52 -0
  73. package/src/browser/navigation-bridge.ts +131 -30
  74. package/src/browser/navigation-client.ts +199 -86
  75. package/src/browser/navigation-store-handle.ts +38 -0
  76. package/src/browser/navigation-store.ts +157 -74
  77. package/src/browser/navigation-transaction.ts +9 -59
  78. package/src/browser/network-error-handler.ts +34 -7
  79. package/src/browser/partial-update.ts +174 -109
  80. package/src/browser/prefetch/cache.ts +222 -69
  81. package/src/browser/prefetch/fetch.ts +347 -39
  82. package/src/browser/prefetch/queue.ts +113 -32
  83. package/src/browser/prefetch/resource-ready.ts +77 -0
  84. package/src/browser/rango-state.ts +158 -76
  85. package/src/browser/react/Link.tsx +102 -15
  86. package/src/browser/react/NavigationProvider.tsx +300 -122
  87. package/src/browser/react/ScrollRestoration.tsx +10 -6
  88. package/src/browser/react/context.ts +7 -2
  89. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  90. package/src/browser/react/filter-segment-order.ts +66 -7
  91. package/src/browser/react/index.ts +0 -48
  92. package/src/browser/react/location-state-shared.ts +178 -8
  93. package/src/browser/react/location-state.ts +39 -14
  94. package/src/browser/react/use-action.ts +6 -15
  95. package/src/browser/react/use-handle.ts +23 -69
  96. package/src/browser/react/use-href.tsx +8 -1
  97. package/src/browser/react/use-link-status.ts +33 -8
  98. package/src/browser/react/use-navigation.ts +32 -7
  99. package/src/browser/react/use-params.ts +20 -10
  100. package/src/browser/react/use-reverse.ts +106 -0
  101. package/src/browser/react/use-router.ts +46 -11
  102. package/src/browser/react/use-search-params.ts +0 -5
  103. package/src/browser/react/use-segments.ts +11 -21
  104. package/src/browser/response-adapter.ts +99 -8
  105. package/src/browser/rsc-router.tsx +125 -28
  106. package/src/browser/scroll-restoration.ts +37 -22
  107. package/src/browser/segment-reconciler.ts +36 -14
  108. package/src/browser/segment-structure-assert.ts +2 -2
  109. package/src/browser/server-action-bridge.ts +222 -61
  110. package/src/browser/types.ts +115 -12
  111. package/src/browser/validate-redirect-origin.ts +43 -16
  112. package/src/build/collect-fallback-refs.ts +107 -0
  113. package/src/build/generate-manifest.ts +65 -40
  114. package/src/build/generate-route-types.ts +5 -1
  115. package/src/build/index.ts +8 -2
  116. package/src/build/prefix-tree-utils.ts +123 -0
  117. package/src/build/route-trie.ts +165 -36
  118. package/src/build/route-types/ast-route-extraction.ts +15 -8
  119. package/src/build/route-types/codegen.ts +16 -5
  120. package/src/build/route-types/include-resolution.ts +125 -24
  121. package/src/build/route-types/param-extraction.ts +6 -3
  122. package/src/build/route-types/per-module-writer.ts +22 -6
  123. package/src/build/route-types/router-processing.ts +260 -94
  124. package/src/build/route-types/scan-filter.ts +9 -2
  125. package/src/build/route-types/source-scan.ts +216 -0
  126. package/src/build/runtime-discovery.ts +9 -20
  127. package/src/cache/cache-error.ts +104 -0
  128. package/src/cache/cache-key-utils.ts +29 -13
  129. package/src/cache/cache-policy.ts +108 -34
  130. package/src/cache/cache-runtime.ts +239 -52
  131. package/src/cache/cache-scope.ts +234 -87
  132. package/src/cache/cache-tag.ts +103 -0
  133. package/src/cache/cf/cf-base64.ts +33 -0
  134. package/src/cache/cf/cf-cache-constants.ts +127 -0
  135. package/src/cache/cf/cf-cache-store.ts +1989 -378
  136. package/src/cache/cf/cf-cache-types.ts +349 -0
  137. package/src/cache/cf/cf-kv-utils.ts +46 -0
  138. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  139. package/src/cache/cf/index.ts +6 -16
  140. package/src/cache/document-cache.ts +89 -21
  141. package/src/cache/handle-snapshot.ts +70 -0
  142. package/src/cache/index.ts +10 -20
  143. package/src/cache/memory-segment-store.ts +136 -37
  144. package/src/cache/profile-registry.ts +46 -31
  145. package/src/cache/read-through-swr.ts +56 -12
  146. package/src/cache/segment-codec.ts +9 -17
  147. package/src/cache/tag-invalidation.ts +230 -0
  148. package/src/cache/taint.ts +55 -0
  149. package/src/cache/types.ts +37 -100
  150. package/src/client.rsc.tsx +44 -21
  151. package/src/client.tsx +119 -290
  152. package/src/cloudflare/index.ts +11 -0
  153. package/src/cloudflare/tracing.ts +109 -0
  154. package/src/component-utils.ts +19 -0
  155. package/src/components/DefaultDocument.tsx +8 -2
  156. package/src/context-var.ts +84 -2
  157. package/src/decode-loader-results.ts +52 -0
  158. package/src/defer.ts +196 -0
  159. package/src/deps/ssr.ts +0 -1
  160. package/src/encode-kv.ts +49 -0
  161. package/src/errors.ts +30 -4
  162. package/src/escape-script.ts +52 -0
  163. package/src/handle.ts +70 -22
  164. package/src/handles/MetaTags.tsx +62 -19
  165. package/src/handles/Scripts.tsx +183 -0
  166. package/src/handles/breadcrumbs.ts +37 -8
  167. package/src/handles/is-thenable.ts +19 -0
  168. package/src/handles/meta.ts +51 -40
  169. package/src/handles/script.ts +244 -0
  170. package/src/host/cookie-handler.ts +9 -60
  171. package/src/host/errors.ts +0 -24
  172. package/src/host/index.ts +8 -2
  173. package/src/host/pattern-matcher.ts +23 -52
  174. package/src/host/router.ts +107 -99
  175. package/src/host/testing.ts +40 -27
  176. package/src/host/types.ts +37 -4
  177. package/src/host/utils.ts +1 -1
  178. package/src/href-client.ts +137 -22
  179. package/src/index.rsc.ts +99 -13
  180. package/src/index.ts +139 -19
  181. package/src/internal-debug.ts +11 -10
  182. package/src/loader-store.ts +500 -0
  183. package/src/loader.rsc.ts +20 -13
  184. package/src/loader.ts +12 -11
  185. package/src/missing-id-error.ts +68 -0
  186. package/src/outlet-context.ts +1 -1
  187. package/src/outlet-provider.tsx +1 -5
  188. package/src/prerender/param-hash.ts +16 -16
  189. package/src/prerender/store.ts +37 -41
  190. package/src/prerender.ts +198 -82
  191. package/src/redirect-origin.ts +100 -0
  192. package/src/regex-escape.ts +8 -0
  193. package/src/render-error-thrower.tsx +20 -0
  194. package/src/response-utils.ts +62 -0
  195. package/src/reverse.ts +65 -15
  196. package/src/root-error-boundary.tsx +1 -19
  197. package/src/route-content-wrapper.tsx +19 -77
  198. package/src/route-definition/dsl-helpers.ts +461 -304
  199. package/src/route-definition/helper-factories.ts +28 -140
  200. package/src/route-definition/helpers-types.ts +149 -74
  201. package/src/route-definition/index.ts +4 -2
  202. package/src/route-definition/redirect.ts +51 -10
  203. package/src/route-definition/resolve-handler-use.ts +160 -0
  204. package/src/route-definition/use-item-types.ts +29 -0
  205. package/src/route-map-builder.ts +0 -16
  206. package/src/route-types.ts +37 -46
  207. package/src/router/basename.ts +14 -0
  208. package/src/router/content-negotiation.ts +164 -17
  209. package/src/router/error-handling.ts +45 -18
  210. package/src/router/find-match.ts +44 -23
  211. package/src/router/handler-context.ts +83 -39
  212. package/src/router/instrument.ts +350 -0
  213. package/src/router/intercept-resolution.ts +48 -24
  214. package/src/router/lazy-includes.ts +15 -52
  215. package/src/router/loader-resolution.ts +274 -56
  216. package/src/router/logging.ts +0 -6
  217. package/src/router/manifest.ts +40 -42
  218. package/src/router/match-api.ts +124 -204
  219. package/src/router/match-context.ts +0 -22
  220. package/src/router/match-handlers.ts +58 -58
  221. package/src/router/match-middleware/background-revalidation.ts +54 -25
  222. package/src/router/match-middleware/cache-lookup.ts +205 -271
  223. package/src/router/match-middleware/cache-store.ts +81 -50
  224. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  225. package/src/router/match-middleware/segment-resolution.ts +45 -14
  226. package/src/router/match-pipelines.ts +1 -42
  227. package/src/router/match-result.ts +93 -39
  228. package/src/router/metrics.ts +5 -34
  229. package/src/router/middleware-types.ts +13 -142
  230. package/src/router/middleware.ts +268 -171
  231. package/src/router/navigation-snapshot.ts +131 -0
  232. package/src/router/params-util.ts +23 -0
  233. package/src/router/pattern-matching.ts +132 -90
  234. package/src/router/prefetch-cache-ttl.ts +51 -0
  235. package/src/router/prefetch-limits.ts +37 -0
  236. package/src/router/prerender-match.ts +195 -56
  237. package/src/router/preview-match.ts +32 -102
  238. package/src/router/request-classification.ts +276 -0
  239. package/src/router/revalidation.ts +123 -73
  240. package/src/router/route-snapshot.ts +244 -0
  241. package/src/router/router-context.ts +3 -27
  242. package/src/router/router-interfaces.ts +129 -35
  243. package/src/router/router-options.ts +202 -15
  244. package/src/router/router-registry.ts +2 -5
  245. package/src/router/segment-resolution/fresh.ts +197 -92
  246. package/src/router/segment-resolution/helpers.ts +115 -30
  247. package/src/router/segment-resolution/loader-cache.ts +76 -39
  248. package/src/router/segment-resolution/revalidation.ts +392 -338
  249. package/src/router/segment-resolution/static-store.ts +19 -5
  250. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  251. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  252. package/src/router/segment-resolution.ts +5 -1
  253. package/src/router/segment-wrappers.ts +6 -5
  254. package/src/router/state-cookie-name.ts +33 -0
  255. package/src/router/substitute-pattern-params.ts +56 -0
  256. package/src/router/telemetry-otel.ts +161 -199
  257. package/src/router/telemetry.ts +96 -19
  258. package/src/router/timeout.ts +0 -20
  259. package/src/router/tracing.ts +206 -0
  260. package/src/router/trie-matching.ts +163 -59
  261. package/src/router/types.ts +10 -63
  262. package/src/router/url-params.ts +44 -0
  263. package/src/router.ts +174 -54
  264. package/src/rsc/handler-context.ts +3 -2
  265. package/src/rsc/handler.ts +660 -518
  266. package/src/rsc/helpers.ts +168 -46
  267. package/src/rsc/index.ts +2 -5
  268. package/src/rsc/json-route-result.ts +38 -0
  269. package/src/rsc/loader-fetch.ts +127 -31
  270. package/src/rsc/manifest-init.ts +33 -42
  271. package/src/rsc/origin-guard.ts +39 -25
  272. package/src/rsc/progressive-enhancement.ts +133 -13
  273. package/src/rsc/redirect-guard.ts +99 -0
  274. package/src/rsc/response-cache-serve.ts +238 -0
  275. package/src/rsc/response-error.ts +79 -12
  276. package/src/rsc/response-route-handler.ts +99 -189
  277. package/src/rsc/rsc-rendering.ts +115 -73
  278. package/src/rsc/runtime-warnings.ts +23 -10
  279. package/src/rsc/server-action.ts +287 -113
  280. package/src/rsc/ssr-setup.ts +18 -2
  281. package/src/rsc/transition-gate.ts +89 -0
  282. package/src/rsc/types.ts +36 -6
  283. package/src/runtime-env.ts +18 -0
  284. package/src/search-params.ts +35 -30
  285. package/src/segment-content-promise.ts +67 -0
  286. package/src/segment-loader-promise.ts +149 -0
  287. package/src/segment-system.tsx +269 -202
  288. package/src/serialize.ts +243 -0
  289. package/src/server/context.ts +235 -51
  290. package/src/server/cookie-parse.ts +32 -0
  291. package/src/server/cookie-store.ts +80 -5
  292. package/src/server/handle-store.ts +40 -38
  293. package/src/server/loader-registry.ts +38 -46
  294. package/src/server/request-context.ts +442 -173
  295. package/src/ssr/index.tsx +24 -16
  296. package/src/static-handler.ts +27 -18
  297. package/src/testing/cache-status.ts +162 -0
  298. package/src/testing/collect-handle.ts +40 -0
  299. package/src/testing/dispatch.ts +701 -0
  300. package/src/testing/dom.entry.ts +22 -0
  301. package/src/testing/e2e/fixture.ts +188 -0
  302. package/src/testing/e2e/index.ts +128 -0
  303. package/src/testing/e2e/matchers.ts +35 -0
  304. package/src/testing/e2e/page-helpers.ts +272 -0
  305. package/src/testing/e2e/parity.ts +387 -0
  306. package/src/testing/e2e/server.ts +195 -0
  307. package/src/testing/flight-matchers.ts +97 -0
  308. package/src/testing/flight-normalize.ts +11 -0
  309. package/src/testing/flight-runtime.d.ts +57 -0
  310. package/src/testing/flight-tree.ts +682 -0
  311. package/src/testing/flight.entry.ts +52 -0
  312. package/src/testing/flight.ts +257 -0
  313. package/src/testing/generated-routes.ts +183 -0
  314. package/src/testing/index.ts +105 -0
  315. package/src/testing/internal/context.ts +371 -0
  316. package/src/testing/internal/flight-client-globals.ts +30 -0
  317. package/src/testing/internal/seed-vars.ts +54 -0
  318. package/src/testing/render-handler.ts +357 -0
  319. package/src/testing/render-route.tsx +581 -0
  320. package/src/testing/run-loader.ts +385 -0
  321. package/src/testing/run-middleware.ts +205 -0
  322. package/src/testing/run-transition-when.ts +164 -0
  323. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  324. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  325. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  326. package/src/testing/vitest-stubs/version.ts +5 -0
  327. package/src/testing/vitest.ts +305 -0
  328. package/src/theme/ThemeProvider.tsx +20 -58
  329. package/src/theme/ThemeScript.tsx +7 -9
  330. package/src/theme/constants.ts +52 -13
  331. package/src/theme/index.ts +0 -7
  332. package/src/theme/theme-context.ts +1 -5
  333. package/src/theme/theme-script.ts +22 -21
  334. package/src/theme/use-theme.ts +0 -3
  335. package/src/types/boundaries.ts +0 -35
  336. package/src/types/cache-types.ts +17 -8
  337. package/src/types/error-types.ts +30 -90
  338. package/src/types/global-namespace.ts +54 -41
  339. package/src/types/handler-context.ts +234 -82
  340. package/src/types/index.ts +3 -10
  341. package/src/types/loader-types.ts +44 -15
  342. package/src/types/request-scope.ts +112 -0
  343. package/src/types/route-config.ts +6 -50
  344. package/src/types/route-entry.ts +12 -7
  345. package/src/types/segments.ts +136 -15
  346. package/src/urls/include-helper.ts +33 -70
  347. package/src/urls/index.ts +1 -11
  348. package/src/urls/path-helper-types.ts +68 -18
  349. package/src/urls/path-helper.ts +57 -111
  350. package/src/urls/pattern-types.ts +48 -19
  351. package/src/urls/response-types.ts +25 -22
  352. package/src/urls/type-extraction.ts +58 -139
  353. package/src/urls/urls-function.ts +1 -19
  354. package/src/use-loader.tsx +346 -89
  355. package/src/vite/debug.ts +185 -0
  356. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  357. package/src/vite/discovery/discover-routers.ts +130 -85
  358. package/src/vite/discovery/discovery-errors.ts +194 -0
  359. package/src/vite/discovery/gate-state.ts +171 -0
  360. package/src/vite/discovery/prerender-collection.ts +214 -132
  361. package/src/vite/discovery/route-types-writer.ts +40 -84
  362. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  363. package/src/vite/discovery/state.ts +57 -4
  364. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  365. package/src/vite/index.ts +6 -0
  366. package/src/vite/inject-client-debug.ts +36 -0
  367. package/src/vite/plugin-types.ts +178 -5
  368. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  369. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  370. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  371. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  372. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  373. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  374. package/src/vite/plugins/expose-action-id.ts +48 -95
  375. package/src/vite/plugins/expose-id-utils.ts +96 -51
  376. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  377. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  378. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  379. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  380. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  381. package/src/vite/plugins/performance-tracks.ts +89 -0
  382. package/src/vite/plugins/refresh-cmd.ts +89 -27
  383. package/src/vite/plugins/use-cache-transform.ts +73 -83
  384. package/src/vite/plugins/version-injector.ts +40 -29
  385. package/src/vite/plugins/version-plugin.ts +37 -40
  386. package/src/vite/plugins/virtual-entries.ts +39 -25
  387. package/src/vite/rango.ts +119 -111
  388. package/src/vite/router-discovery.ts +941 -142
  389. package/src/vite/utils/ast-handler-extract.ts +26 -35
  390. package/src/vite/utils/banner.ts +1 -1
  391. package/src/vite/utils/bundle-analysis.ts +10 -15
  392. package/src/vite/utils/client-chunks.ts +184 -0
  393. package/src/vite/utils/directive-prologue.ts +40 -0
  394. package/src/vite/utils/forward-user-plugins.ts +171 -0
  395. package/src/vite/utils/manifest-utils.ts +4 -59
  396. package/src/vite/utils/package-resolution.ts +20 -52
  397. package/src/vite/utils/prerender-utils.ts +81 -34
  398. package/src/vite/utils/shared-utils.ts +92 -42
  399. package/src/browser/action-response-classifier.ts +0 -99
  400. package/src/browser/react/use-client-cache.ts +0 -58
  401. package/src/browser/shallow.ts +0 -40
  402. package/src/handles/index.ts +0 -7
  403. package/src/network-error-thrower.tsx +0 -23
  404. package/src/router/middleware-cookies.ts +0 -55
@@ -8,125 +8,41 @@ import {
8
8
  revalidate,
9
9
  parallel,
10
10
  intercept,
11
- when,
12
11
  errorBoundary,
13
12
  notFoundBoundary,
14
- loaderFn,
15
- loadingFn,
16
- transitionFn,
17
- routeFn,
13
+ route,
14
+ loader,
15
+ loading,
16
+ transition,
18
17
  } from "./dsl-helpers.js";
19
18
  import RootLayout from "../server/root-layout";
20
19
  import { invariant } from "../errors";
21
20
 
22
- /*
23
- * Create revalidate helper
24
- */
25
- const createRevalidateHelper = <TEnv>(): RouteHelpers<
26
- any,
27
- TEnv
28
- >["revalidate"] => {
29
- return revalidate as RouteHelpers<any, TEnv>["revalidate"];
30
- };
31
-
32
- /**
33
- * Create errorBoundary helper
34
- */
35
- const createErrorBoundaryHelper = <TEnv>(): RouteHelpers<
36
- any,
37
- TEnv
38
- >["errorBoundary"] => {
39
- return errorBoundary as RouteHelpers<any, TEnv>["errorBoundary"];
40
- };
41
-
42
- /**
43
- * Create notFoundBoundary helper
44
- */
45
- const createNotFoundBoundaryHelper = <TEnv>(): RouteHelpers<
46
- any,
47
- TEnv
48
- >["notFoundBoundary"] => {
49
- return notFoundBoundary as RouteHelpers<any, TEnv>["notFoundBoundary"];
50
- };
51
-
52
21
  /**
53
- * Create middleware helper
22
+ * Assemble the RouteHelpers object. The helpers are the DSL functions
23
+ * themselves; the single cast erases the phantom generics (and the extra
24
+ * `route` key) that the per-router RouteHelpers<T, TEnv> type carries but the
25
+ * runtime functions do not.
54
26
  */
55
- const createMiddlewareHelper = <TEnv>(): RouteHelpers<
56
- any,
27
+ function buildRouteHelpers<T extends RouteDefinition, TEnv>(): RouteHelpers<
28
+ T,
57
29
  TEnv
58
- >["middleware"] => {
59
- return middleware as RouteHelpers<any, TEnv>["middleware"];
60
- };
61
-
62
- /**
63
- * Create parallel helper
64
- */
65
- const createParallelHelper = <TEnv>(): RouteHelpers<any, TEnv>["parallel"] => {
66
- return parallel as RouteHelpers<any, TEnv>["parallel"];
67
- };
68
-
69
- /**
70
- * Create intercept helper
71
- */
72
- const createInterceptHelper = <
73
- const T extends RouteDefinition,
74
- TEnv,
75
- >(): RouteHelpers<T, TEnv>["intercept"] => {
76
- return intercept as RouteHelpers<T, TEnv>["intercept"];
77
- };
78
-
79
- /**
80
- * Create loader helper
81
- */
82
- const createLoaderHelper = <TEnv>(): RouteHelpers<any, TEnv>["loader"] => {
83
- return loaderFn as RouteHelpers<any, TEnv>["loader"];
84
- };
85
-
86
- /**
87
- * Create loading helper
88
- */
89
- const createLoadingHelper = (): RouteHelpers<any, any>["loading"] => {
90
- return loadingFn;
91
- };
92
-
93
- /**
94
- * Create route helper
95
- */
96
- const createRouteHelper = <
97
- const T extends RouteDefinition,
98
- TEnv,
99
- >(): RouteHelpers<T, TEnv>["route"] => {
100
- return routeFn as unknown as RouteHelpers<T, TEnv>["route"];
101
- };
102
-
103
- /**
104
- * Create layout helper
105
- */
106
- const createLayoutHelper = <TEnv>(): RouteHelpers<any, TEnv>["layout"] => {
107
- return layout as RouteHelpers<any, TEnv>["layout"];
108
- };
109
-
110
- /**
111
- * Create when helper for intercept conditions
112
- */
113
- const createWhenHelper = (): RouteHelpers<any, any>["when"] => {
114
- return when;
115
- };
116
-
117
- /**
118
- * Create cache helper for cache configuration
119
- */
120
- const createCacheHelper = (): RouteHelpers<any, any>["cache"] => {
121
- return cache;
122
- };
123
-
124
- /**
125
- * Create transition helper
126
- */
127
- const createTransitionHelper = (): RouteHelpers<any, any>["transition"] => {
128
- return transitionFn as RouteHelpers<any, any>["transition"];
129
- };
30
+ > {
31
+ return {
32
+ route,
33
+ layout,
34
+ parallel,
35
+ intercept,
36
+ middleware,
37
+ revalidate,
38
+ loader,
39
+ loading,
40
+ errorBoundary,
41
+ notFoundBoundary,
42
+ cache,
43
+ transition,
44
+ } as unknown as RouteHelpers<T, TEnv>;
45
+ }
130
46
 
131
47
  /**
132
48
  * Branded type for route handlers that carries the route type info.
@@ -152,21 +68,7 @@ export function map<const T extends RouteDefinition, TEnv = DefaultEnv>(
152
68
  "map() expects a builder function as its argument",
153
69
  );
154
70
  // Create helpers
155
- const helpers: RouteHelpers<T, TEnv> = {
156
- route: createRouteHelper<T, TEnv>(),
157
- layout: createLayoutHelper<TEnv>(),
158
- parallel: createParallelHelper<TEnv>(),
159
- intercept: createInterceptHelper<T, TEnv>(),
160
- middleware: createMiddlewareHelper<TEnv>(),
161
- revalidate: createRevalidateHelper<TEnv>(),
162
- loader: createLoaderHelper<TEnv>(),
163
- loading: createLoadingHelper(),
164
- errorBoundary: createErrorBoundaryHelper<TEnv>(),
165
- notFoundBoundary: createNotFoundBoundaryHelper<TEnv>(),
166
- when: createWhenHelper(),
167
- cache: createCacheHelper(),
168
- transition: createTransitionHelper(),
169
- };
71
+ const helpers = buildRouteHelpers<T, TEnv>();
170
72
 
171
73
  return [layout(RootLayout, () => builder(helpers))].flat(3);
172
74
  };
@@ -182,19 +84,5 @@ export function createRouteHelpers<
182
84
  T extends RouteDefinition,
183
85
  TEnv,
184
86
  >(): RouteHelpers<T, TEnv> {
185
- return {
186
- route: createRouteHelper<T, TEnv>(),
187
- layout: createLayoutHelper<TEnv>(),
188
- parallel: createParallelHelper<TEnv>(),
189
- intercept: createInterceptHelper<T, TEnv>(),
190
- middleware: createMiddlewareHelper<TEnv>(),
191
- revalidate: createRevalidateHelper<TEnv>(),
192
- loader: createLoaderHelper<TEnv>(),
193
- loading: createLoadingHelper(),
194
- errorBoundary: createErrorBoundaryHelper<TEnv>(),
195
- notFoundBoundary: createNotFoundBoundaryHelper<TEnv>(),
196
- when: createWhenHelper(),
197
- cache: createCacheHelper(),
198
- transition: createTransitionHelper(),
199
- };
87
+ return buildRouteHelpers<T, TEnv>();
200
88
  }
@@ -29,12 +29,10 @@ import type {
29
29
  ParallelUseItem,
30
30
  InterceptUseItem,
31
31
  LoaderUseItem,
32
- WhenItem,
33
32
  CacheItem,
34
33
  TransitionItem,
35
34
  UseItems,
36
35
  } from "../route-types.js";
37
- import type { InterceptWhenFn } from "../server/context";
38
36
 
39
37
  // Re-export route item types for backward compatibility
40
38
  export type {
@@ -52,12 +50,12 @@ export type {
52
50
  RouteUseItem,
53
51
  ParallelUseItem,
54
52
  InterceptUseItem,
55
- WhenItem,
56
53
  CacheItem,
57
54
  } from "../route-types.js";
58
55
 
59
56
  // Re-export intercept selector types for use in handlers
60
57
  export type {
58
+ InterceptConfig,
61
59
  InterceptSelectorContext,
62
60
  InterceptSegmentsState,
63
61
  InterceptWhenFn,
@@ -123,7 +121,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
123
121
  * "@main": async (ctx) => <MainContent data={ctx.use(DataLoader)} />,
124
122
  * })
125
123
  *
126
- * // With loaders and loading states
124
+ * // With loaders and loading states (broadcast to every slot)
127
125
  * parallel({
128
126
  * "@analytics": AnalyticsPanel,
129
127
  * "@metrics": MetricsPanel,
@@ -131,12 +129,36 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
131
129
  * loader(DashboardLoader),
132
130
  * loading(<DashboardSkeleton />),
133
131
  * ])
132
+ *
133
+ * // Per-slot scoped use via slot descriptor — for single-assignment items
134
+ * // like loading() that should not broadcast to siblings.
135
+ * parallel({
136
+ * "@meta": MetaSlot,
137
+ * "@sidebar": {
138
+ * handler: SidebarSlot,
139
+ * use: () => [loading(<SidebarSkeleton />)],
140
+ * },
141
+ * })
134
142
  * ```
135
143
  * @param slots - Object with slot names (prefixed with @) mapped to handlers
144
+ * or `{ handler, use? }` slot descriptors.
136
145
  * @param use - Optional callback for loaders, loading, revalidate, etc.
146
+ * Items here apply to every slot in the call (broadcast).
147
+ * For per-slot single-assignment items, use the slot descriptor's
148
+ * own `use` callback — slot-local items run after the broadcast,
149
+ * so they take precedence on `loading()` and other last-write-wins
150
+ * fields.
137
151
  */
138
152
  parallel: <
139
- TSlots extends Record<`@${string}`, Handler<any, any, TEnv> | ReactNode>,
153
+ TSlots extends Record<
154
+ `@${string}`,
155
+ | Handler<any, any, TEnv>
156
+ | ReactNode
157
+ | {
158
+ handler: Handler<any, any, TEnv> | ReactNode;
159
+ use?: () => UseItems<ParallelUseItem>;
160
+ }
161
+ >,
140
162
  >(
141
163
  slots: TSlots,
142
164
  use?: () => UseItems<ParallelUseItem>,
@@ -159,10 +181,26 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
159
181
  * loader(CardModalLoader),
160
182
  * revalidate(() => false),
161
183
  * ])
184
+ *
185
+ * // Conditional activation via the config object's `when` selector
186
+ * intercept("@modal", "card", <CardModal />, {
187
+ * when: ({ from }) => from.pathname.startsWith("/board"),
188
+ * })
189
+ *
190
+ * // Config + other use-items: config is arg 4, use is arg 5
191
+ * intercept(
192
+ * "@modal",
193
+ * "card",
194
+ * <CardModal />,
195
+ * { when: ({ from }) => from.pathname.startsWith("/board") },
196
+ * () => [loader(CardDetailLoader)],
197
+ * )
162
198
  * ```
163
199
  * @param slotName - Named slot (prefixed with @) where intercept renders
164
200
  * @param routeName - Route name to intercept
165
201
  * @param handler - Component or handler for intercepted render
202
+ * @param config - Optional InterceptConfig (e.g. `{ when }`), or the use
203
+ * callback directly when there is no config
166
204
  * @param use - Optional callback for loaders, middleware, revalidate, etc.
167
205
  */
168
206
  intercept: {
@@ -171,32 +209,58 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
171
209
  slotName: `@${string}`,
172
210
  routeName: `.${K}`,
173
211
  handler: ReactNode | Handler<ExtractRouteParams<T, K>, {}, TEnv>,
212
+ config?:
213
+ | import("../server/context.js").InterceptConfig<TEnv>
214
+ | (() => UseItems<InterceptUseItem>),
174
215
  use?: () => UseItems<InterceptUseItem>,
175
216
  ): InterceptItem;
176
217
  // Global: unprefixed, params inferred from global route map
177
- <K extends keyof RSCRouter.GeneratedRouteMap & string>(
218
+ <K extends keyof Rango.GeneratedRouteMap & string>(
178
219
  slotName: `@${string}`,
179
220
  routeName: K,
180
- handler: ReactNode | Handler<K, RSCRouter.GeneratedRouteMap, TEnv>,
221
+ handler: ReactNode | Handler<K, Rango.GeneratedRouteMap, TEnv>,
222
+ config?:
223
+ | import("../server/context.js").InterceptConfig<TEnv>
224
+ | (() => UseItems<InterceptUseItem>),
181
225
  use?: () => UseItems<InterceptUseItem>,
182
226
  ): InterceptItem;
183
227
  };
184
228
  /**
185
- * Attach middleware to the current route/layout
229
+ * Attach middleware to the current route/layout, or wrap child segments
230
+ *
231
+ * **Sibling mode** — attaches middleware to the parent entry:
186
232
  * ```typescript
187
- * middleware(async (ctx, next) => {
188
- * const session = await getSession(ctx.request);
189
- * if (!session) return redirect("/login");
190
- * ctx.set("user", session.user);
191
- * next();
192
- * })
233
+ * layout(<DashboardShell />, () => [
234
+ * middleware(authMiddleware),
235
+ * middleware([authMiddleware, loggingMiddleware]),
236
+ * path("/", DashboardPage),
237
+ * ])
238
+ * ```
239
+ *
240
+ * **Wrapping mode** — scopes middleware to the children only:
241
+ * ```typescript
242
+ * middleware(authMiddleware, () => [
243
+ * path("/dashboard", DashboardPage),
244
+ * path("/settings", SettingsPage),
245
+ * ])
193
246
  *
194
- * // Chain multiple middleware
195
- * middleware(authMiddleware, loggingMiddleware, rateLimitMiddleware)
247
+ * middleware([authMiddleware, loggingMiddleware], () => [
248
+ * path("/admin", AdminPage),
249
+ * ])
196
250
  * ```
197
- * @param fns - One or more middleware functions to execute in order
198
251
  */
199
- middleware: (...fns: MiddlewareFn<TEnv>[]) => MiddlewareItem;
252
+ middleware: {
253
+ (fn: MiddlewareFn<TEnv>): MiddlewareItem;
254
+ (
255
+ fn: MiddlewareFn<TEnv>,
256
+ children: () => UseItems<LayoutUseItem>,
257
+ ): MiddlewareItem;
258
+ (fns: MiddlewareFn<TEnv>[]): MiddlewareItem;
259
+ (
260
+ fns: MiddlewareFn<TEnv>[],
261
+ children: () => UseItems<LayoutUseItem>,
262
+ ): MiddlewareItem;
263
+ };
200
264
  /**
201
265
  * Control when a segment should revalidate during navigation
202
266
  * ```typescript
@@ -206,8 +270,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
206
270
  * )
207
271
  *
208
272
  * // Revalidate after specific actions (actionId format: "path/to/file.ts#exportName")
273
+ * // Use `|| undefined` (defer), not `?? false` (hard short-circuit), so the
274
+ * // chain and the segment default still apply when there is no match.
209
275
  * revalidate(({ actionId }) =>
210
- * actionId?.includes("Cart") ?? false
276
+ * actionId?.includes("Cart") || undefined
211
277
  * )
212
278
  *
213
279
  * // Soft decision (suggest but allow override)
@@ -215,7 +281,12 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
215
281
  * ({ defaultShouldRevalidate: true })
216
282
  * )
217
283
  * ```
218
- * @param fn - Function that returns boolean (hard) or { defaultShouldRevalidate } (soft)
284
+ * @param fn - Function returning either:
285
+ * - `boolean` (hard decision — short-circuits the chain),
286
+ * - `{ defaultShouldRevalidate: boolean }` (soft — updates the suggestion
287
+ * for downstream revalidators),
288
+ * - or nothing / `null` / `undefined` (defer — leaves the suggestion
289
+ * unchanged and continues to the next revalidator).
219
290
  */
220
291
  revalidate: (fn: ShouldRevalidateFn<any, TEnv>) => RevalidateItem;
221
292
  /**
@@ -225,14 +296,15 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
225
296
  *
226
297
  * // With loader-specific revalidation (match by file or export name)
227
298
  * loader(CartLoader, () => [
228
- * revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
299
+ * revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
229
300
  * ])
230
301
  *
231
- * // Access loader data in handlers via ctx.use()
232
- * route("products.detail", async (ctx) => {
233
- * const product = await ctx.use(ProductLoader);
234
- * return <ProductPage product={product} />;
235
- * })
302
+ * // Consume in client components with useLoader()
303
+ * // (preferred — cache-safe, always fresh)
304
+ * function ProductDetails() {
305
+ * const { data } = useLoader(ProductLoader);
306
+ * return <div>{data.name}</div>;
307
+ * }
236
308
  * ```
237
309
  * @param loaderDef - Loader created with createLoader()
238
310
  * @param use - Optional callback for loader-specific revalidation rules
@@ -254,7 +326,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
254
326
  * @param options - Configuration options
255
327
  * @param options.ssr - If false, skip showing loading on document requests (SSR)
256
328
  */
257
- loading: (component: ReactNode, options?: { ssr?: boolean }) => LoadingItem;
329
+ loading: (
330
+ component: ReactNode | (() => ReactNode),
331
+ options?: { ssr?: boolean },
332
+ ) => LoadingItem;
258
333
  /**
259
334
  * Attach an error boundary to catch errors in this segment and children
260
335
  * ```typescript
@@ -292,40 +367,6 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
292
367
  notFoundBoundary: (
293
368
  fallback: ReactNode | NotFoundBoundaryHandler,
294
369
  ) => NotFoundBoundaryItem;
295
- /**
296
- * Define a condition for when an intercept should activate
297
- *
298
- * Only valid inside intercept() use() callback. When multiple when() calls
299
- * are present, ALL must return true for the intercept to activate.
300
- * If no when() is defined, the intercept always activates on soft navigation.
301
- *
302
- * Context properties:
303
- * - `from` - Source URL (where user is navigating from)
304
- * - `to` - Destination URL (where user is navigating to)
305
- * - `params` - Matched route params
306
- * - `segments` - Client's current segments with `path` and `ids`
307
- *
308
- * ```typescript
309
- * // Only intercept when coming from the board page
310
- * intercept("@modal", "card", <CardModal />, () => [
311
- * when(({ from }) => from.pathname.startsWith("/board")),
312
- * loader(CardDetailLoader),
313
- * ])
314
- *
315
- * // Use segments to check current route context
316
- * intercept("@modal", "card", <CardModal />, () => [
317
- * when(({ segments }) => segments.path[0] === "kanban"),
318
- * ])
319
- *
320
- * // Multiple conditions (AND logic)
321
- * intercept("@modal", "card", <CardModal />, () => [
322
- * when(({ from }) => from.pathname.startsWith("/board")),
323
- * when(({ segments }) => segments.ids.includes("kanban-layout")),
324
- * ])
325
- * ```
326
- * @param fn - Selector function receiving navigation context, returns boolean
327
- */
328
- when: (fn: InterceptWhenFn) => WhenItem;
329
370
  /**
330
371
  * Define cache configuration for segments
331
372
  *
@@ -386,18 +427,43 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
386
427
  cache: {
387
428
  (): CacheItem;
388
429
  (children: () => UseItems<AllUseItems>): CacheItem;
389
- (profileName: string): CacheItem;
390
- (profileName: string, use: () => UseItems<AllUseItems>): CacheItem;
391
430
  (
392
- options: PartialCacheOptions | false,
431
+ options: PartialCacheOptions<TEnv> | false,
393
432
  use?: () => UseItems<AllUseItems>,
394
433
  ): CacheItem;
395
434
  };
396
435
  /**
397
- * Attach a ViewTransition boundary to the current segment or a group of routes
398
- *
399
- * Wraps segment content with React's `<ViewTransition>` component.
400
- * Only takes effect when React experimental is used (no-op on stable React).
436
+ * Opt a route (or group of routes) into transition-driven navigation.
437
+ *
438
+ * `transition()` does two independent things, and you choose how far to go:
439
+ * 1. startTransition (ALL React versions): the navigation commit is driven
440
+ * through React's startTransition, so a same-route nav (same route,
441
+ * different params, e.g. /product/1 -> /product/2) holds the previous
442
+ * content while the new loader resolves instead of flashing the route's
443
+ * loading() skeleton (see segment-system.tsx inTransitionScope). This is
444
+ * also the precondition for any view-transition animation.
445
+ * 2. <ViewTransition> (experimental React only): the segment content is also
446
+ * wrapped in React's <ViewTransition>, so the held swap cross-fades/morphs.
447
+ * Layered on by default; pass { viewTransition: false } to keep #1 without
448
+ * the router boundary (and place your own <ViewTransition> instead).
449
+ *
450
+ * A view transition cannot fire without a startTransition, so the meaningful
451
+ * choices are (see skills/view-transitions for the full matrix):
452
+ * - no transition() -> neither (remount + skeleton)
453
+ * - transition({ viewTransition: false }) -> startTransition only (hold)
454
+ * - transition({}) / transition({ enter… }) -> startTransition + ViewTransition
455
+ *
456
+ * Precedence: a bare transition({}) inherits createRouter({ viewTransition })
457
+ * (default "auto"); an explicit per-route `viewTransition` always wins. So
458
+ * transition({}) is startTransition + ViewTransition under the default and
459
+ * startTransition only when the router sets viewTransition: false.
460
+ *
461
+ * Conditional hold: pass `when: (ctx) => boolean` to gate the transition per
462
+ * request. It runs server-side AFTER the route handler (so it can read state
463
+ * the handler set via `ctx.get(...)`); returning false drops this transition
464
+ * for the request, so the navigation streams its loading() skeleton instead of
465
+ * holding. This is a post-handler predicate — distinct from intercept()'s
466
+ * match-time `when` config selector (`intercept(slot, route, Comp, { when })`).
401
467
  *
402
468
  * ```typescript
403
469
  * // Attach to a single route
@@ -411,16 +477,25 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
411
477
  * path("/about", AboutPage),
412
478
  * ])
413
479
  *
414
- * // Direction-aware transitions
415
- * transition({
416
- * enter: { "navigation": "slide-right", "navigation-back": "slide-left" },
417
- * exit: { "navigation": "slide-left", "navigation-back": "slide-right" },
418
- * })
480
+ * // Hold content + drive view transitions, but place no router boundary:
481
+ * path("/product/:id", ProductPage, { name: "product" }, () => [
482
+ * transition({ viewTransition: false }),
483
+ * ])
484
+ *
485
+ * // Hold only when the handler decided to (post-handler predicate):
486
+ * path("/product/:id", ProductPage, { name: "product" }, () => [
487
+ * transition({ when: (ctx) => ctx.get(KeepScroll) === true }),
488
+ * ])
419
489
  * ```
420
- * @param config - ViewTransition configuration (enter, exit, update, share, default, name)
490
+ * @param config - ViewTransition configuration (enter, exit, update, share,
491
+ * default, name), `viewTransition: "auto" | false` to toggle the router
492
+ * boundary (createRouter({ viewTransition }) sets the app-wide default), and
493
+ * `when: (ctx) => boolean` to gate the transition per request post-handler
421
494
  * @param children - Optional callback returning child routes to wrap
422
495
  */
423
496
  transition: {
497
+ (): TransitionItem;
498
+ (children: () => UseItems<AllUseItems>): TransitionItem;
424
499
  (config: TransitionConfig): TransitionItem;
425
500
  (
426
501
  config: TransitionConfig,
@@ -15,8 +15,8 @@ export type {
15
15
  RouteUseItem,
16
16
  ParallelUseItem,
17
17
  InterceptUseItem,
18
- WhenItem,
19
18
  CacheItem,
19
+ InterceptConfig,
20
20
  InterceptSelectorContext,
21
21
  InterceptSegmentsState,
22
22
  InterceptWhenFn,
@@ -30,7 +30,6 @@ export {
30
30
  revalidate,
31
31
  parallel,
32
32
  intercept,
33
- when,
34
33
  errorBoundary,
35
34
  notFoundBoundary,
36
35
  loader,
@@ -45,6 +44,9 @@ export {
45
44
  type RouteHandlers,
46
45
  } from "./helper-factories.js";
47
46
 
47
+ // Handler use resolver
48
+ export { resolveHandlerUse } from "./resolve-handler-use.js";
49
+
48
50
  // Redirect
49
51
  export { redirect } from "./redirect.js";
50
52
 
@@ -1,8 +1,9 @@
1
1
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
2
2
  import {
3
- requireRequestContext,
4
3
  getRequestContext,
4
+ _getRequestContext,
5
5
  } from "../server/request-context.js";
6
+ import { markExternalRedirect } from "../redirect-origin.js";
6
7
 
7
8
  /**
8
9
  * Create a soft redirect Response for middleware short-circuit
@@ -38,6 +39,11 @@ import {
38
39
  * status: 303,
39
40
  * state: [Flash({ text: "Session expired" })],
40
41
  * });
42
+ *
43
+ * // Off-host redirect (opt out of the same-origin guard). Without
44
+ * // `external: true`, a cross-origin target is blocked and replaced with the
45
+ * // app root, matching the client's open-redirect protection.
46
+ * return redirect('https://accounts.example.com/oauth', { external: true });
41
47
  * ```
42
48
  */
43
49
  export function redirect(url: string, status?: number): Response;
@@ -46,13 +52,18 @@ export function redirect(
46
52
  options: {
47
53
  status?: number;
48
54
  state?: LocationStateEntry | LocationStateEntry[];
55
+ external?: boolean;
49
56
  },
50
57
  ): Response;
51
58
  export function redirect(
52
59
  url: string,
53
60
  statusOrOptions?:
54
61
  | number
55
- | { status?: number; state?: LocationStateEntry | LocationStateEntry[] },
62
+ | {
63
+ status?: number;
64
+ state?: LocationStateEntry | LocationStateEntry[];
65
+ external?: boolean;
66
+ },
56
67
  ): Response {
57
68
  const status =
58
69
  typeof statusOrOptions === "number"
@@ -60,9 +71,11 @@ export function redirect(
60
71
  : (statusOrOptions?.status ?? 302);
61
72
  const state =
62
73
  typeof statusOrOptions === "object" ? statusOrOptions?.state : undefined;
74
+ const external =
75
+ typeof statusOrOptions === "object" ? statusOrOptions?.external : undefined;
63
76
 
64
77
  if (state) {
65
- const ctx = requireRequestContext();
78
+ const ctx = getRequestContext();
66
79
  ctx.setLocationState(state);
67
80
 
68
81
  if (process.env.NODE_ENV !== "production") {
@@ -83,11 +96,39 @@ export function redirect(
83
96
  }
84
97
  }
85
98
 
86
- return new Response(null, {
87
- status,
88
- headers: {
89
- Location: url,
90
- "X-RSC-Redirect": "soft",
91
- },
92
- });
99
+ // Auto-prefix root-relative URLs with basename for app-local redirects.
100
+ // Treat the URL as already-prefixed when the basename is followed by a path
101
+ // separator, a query, a fragment, or end-of-string, so "/admin?tab=x" and
102
+ // "/admin#frag" are not double-prefixed into "/admin/admin?tab=x".
103
+ const bn = _getRequestContext()?._basename;
104
+ let resolvedUrl = url;
105
+ if (
106
+ bn &&
107
+ url.startsWith("/") &&
108
+ url !== bn &&
109
+ !url.startsWith(bn + "/") &&
110
+ !url.startsWith(bn + "?") &&
111
+ !url.startsWith(bn + "#")
112
+ ) {
113
+ resolvedUrl = url === "/" ? bn : bn + url;
114
+ }
115
+
116
+ const headers: Record<string, string> = {
117
+ Location: resolvedUrl,
118
+ "X-RSC-Redirect": "soft",
119
+ };
120
+
121
+ const response = new Response(null, { status, headers });
122
+
123
+ // Mark an explicit off-host redirect with an out-of-band brand so the
124
+ // same-origin guard (rsc/redirect-guard.ts) lets it through. The brand is a
125
+ // WeakSet membership on this Response object -- NOT a wire header -- so the
126
+ // opt-in cannot be forged by an attacker-controlled upstream response a
127
+ // proxy-style response route might copy. The internal redirect-rebuild paths
128
+ // transfer the brand; the guard reads and clears it (see markExternalRedirect).
129
+ if (external) {
130
+ markExternalRedirect(response);
131
+ }
132
+
133
+ return response;
93
134
  }