@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650

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 (356) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +71 -21
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +29 -5
  19. package/skills/layout/SKILL.md +13 -9
  20. package/skills/links/SKILL.md +173 -17
  21. package/skills/loader/SKILL.md +170 -23
  22. package/skills/middleware/SKILL.md +16 -10
  23. package/skills/migrate-nextjs/SKILL.md +38 -16
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +11 -7
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +250 -25
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +114 -47
  31. package/skills/route/SKILL.md +42 -5
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +78 -42
  34. package/skills/tailwind/SKILL.md +27 -3
  35. package/skills/testing/SKILL.md +129 -0
  36. package/skills/testing/bindings.md +89 -0
  37. package/skills/testing/cache-prerender.md +124 -0
  38. package/skills/testing/client-components.md +122 -0
  39. package/skills/testing/e2e-parity.md +125 -0
  40. package/skills/testing/flight.md +92 -0
  41. package/skills/testing/handles.md +129 -0
  42. package/skills/testing/loader.md +128 -0
  43. package/skills/testing/middleware.md +99 -0
  44. package/skills/testing/render-handler.md +121 -0
  45. package/skills/testing/response-routes.md +95 -0
  46. package/skills/testing/reverse-and-types.md +84 -0
  47. package/skills/testing/server-actions.md +107 -0
  48. package/skills/testing/server-tree.md +128 -0
  49. package/skills/testing/setup.md +120 -0
  50. package/skills/typesafety/SKILL.md +316 -26
  51. package/skills/use-cache/SKILL.md +36 -5
  52. package/skills/vercel/SKILL.md +107 -0
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/__internal.ts +0 -65
  57. package/src/browser/action-coordinator.ts +53 -36
  58. package/src/browser/action-fence.ts +47 -0
  59. package/src/browser/app-shell.ts +14 -27
  60. package/src/browser/cookie-name.ts +140 -0
  61. package/src/browser/event-controller.ts +37 -143
  62. package/src/browser/history-state.ts +21 -0
  63. package/src/browser/index.ts +3 -3
  64. package/src/browser/invalidate-client-cache.ts +52 -0
  65. package/src/browser/navigation-bridge.ts +30 -59
  66. package/src/browser/navigation-client.ts +96 -84
  67. package/src/browser/navigation-store-handle.ts +38 -0
  68. package/src/browser/navigation-store.ts +32 -82
  69. package/src/browser/navigation-transaction.ts +9 -59
  70. package/src/browser/partial-update.ts +60 -127
  71. package/src/browser/prefetch/cache.ts +82 -72
  72. package/src/browser/prefetch/fetch.ts +108 -33
  73. package/src/browser/prefetch/queue.ts +6 -3
  74. package/src/browser/rango-state.ts +157 -115
  75. package/src/browser/react/Link.tsx +0 -2
  76. package/src/browser/react/NavigationProvider.tsx +41 -48
  77. package/src/browser/react/ScrollRestoration.tsx +10 -6
  78. package/src/browser/react/filter-segment-order.ts +0 -2
  79. package/src/browser/react/index.ts +0 -48
  80. package/src/browser/react/location-state-shared.ts +166 -8
  81. package/src/browser/react/location-state.ts +39 -14
  82. package/src/browser/react/use-action.ts +6 -15
  83. package/src/browser/react/use-handle.ts +17 -14
  84. package/src/browser/react/use-link-status.ts +0 -4
  85. package/src/browser/react/use-navigation.ts +0 -3
  86. package/src/browser/react/use-params.ts +11 -11
  87. package/src/browser/react/use-reverse.ts +106 -0
  88. package/src/browser/react/use-router.ts +20 -5
  89. package/src/browser/react/use-search-params.ts +0 -5
  90. package/src/browser/react/use-segments.ts +0 -13
  91. package/src/browser/response-adapter.ts +52 -1
  92. package/src/browser/rsc-router.tsx +70 -34
  93. package/src/browser/scroll-restoration.ts +22 -14
  94. package/src/browser/segment-structure-assert.ts +2 -2
  95. package/src/browser/server-action-bridge.ts +168 -44
  96. package/src/browser/types.ts +36 -21
  97. package/src/browser/validate-redirect-origin.ts +43 -16
  98. package/src/build/collect-fallback-refs.ts +107 -0
  99. package/src/build/generate-manifest.ts +60 -35
  100. package/src/build/generate-route-types.ts +3 -0
  101. package/src/build/index.ts +8 -2
  102. package/src/build/prefix-tree-utils.ts +123 -0
  103. package/src/build/route-trie.ts +89 -10
  104. package/src/build/route-types/codegen.ts +4 -4
  105. package/src/build/route-types/include-resolution.ts +1 -1
  106. package/src/build/route-types/param-extraction.ts +6 -3
  107. package/src/build/route-types/per-module-writer.ts +7 -4
  108. package/src/build/route-types/router-processing.ts +122 -22
  109. package/src/build/route-types/scan-filter.ts +1 -1
  110. package/src/build/route-types/source-scan.ts +118 -0
  111. package/src/build/runtime-discovery.ts +9 -20
  112. package/src/cache/cache-error.ts +104 -0
  113. package/src/cache/cache-policy.ts +68 -28
  114. package/src/cache/cache-runtime.ts +134 -32
  115. package/src/cache/cache-scope.ts +100 -74
  116. package/src/cache/cache-tag.ts +98 -0
  117. package/src/cache/cf/cf-cache-store.ts +2255 -238
  118. package/src/cache/cf/index.ts +6 -16
  119. package/src/cache/document-cache.ts +61 -20
  120. package/src/cache/handle-snapshot.ts +63 -0
  121. package/src/cache/index.ts +22 -20
  122. package/src/cache/memory-segment-store.ts +136 -37
  123. package/src/cache/profile-registry.ts +6 -30
  124. package/src/cache/read-through-swr.ts +41 -11
  125. package/src/cache/segment-codec.ts +0 -16
  126. package/src/cache/tag-invalidation.ts +230 -0
  127. package/src/cache/types.ts +33 -100
  128. package/src/cache/vercel/index.ts +11 -0
  129. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  130. package/src/client.rsc.tsx +6 -21
  131. package/src/client.tsx +25 -61
  132. package/src/component-utils.ts +19 -0
  133. package/src/context-var.ts +17 -5
  134. package/src/decode-loader-results.ts +36 -0
  135. package/src/defer.ts +196 -0
  136. package/src/deps/ssr.ts +0 -1
  137. package/src/errors.ts +30 -4
  138. package/src/handle.ts +31 -23
  139. package/src/handles/MetaTags.tsx +0 -14
  140. package/src/handles/breadcrumbs.ts +16 -5
  141. package/src/handles/meta.ts +0 -39
  142. package/src/host/cookie-handler.ts +0 -36
  143. package/src/host/errors.ts +0 -24
  144. package/src/host/index.ts +8 -2
  145. package/src/host/pattern-matcher.ts +7 -50
  146. package/src/host/router.ts +107 -99
  147. package/src/host/testing.ts +40 -27
  148. package/src/host/types.ts +37 -4
  149. package/src/host/utils.ts +1 -1
  150. package/src/href-client.ts +137 -22
  151. package/src/index.rsc.ts +63 -9
  152. package/src/index.ts +64 -9
  153. package/src/internal-debug.ts +2 -4
  154. package/src/loader-store.ts +500 -0
  155. package/src/loader.rsc.ts +20 -13
  156. package/src/loader.ts +12 -11
  157. package/src/missing-id-error.ts +68 -0
  158. package/src/network-error-thrower.tsx +1 -6
  159. package/src/outlet-provider.tsx +1 -5
  160. package/src/prerender/param-hash.ts +10 -11
  161. package/src/prerender/store.ts +32 -37
  162. package/src/prerender.ts +61 -6
  163. package/src/redirect-origin.ts +100 -0
  164. package/src/response-utils.ts +9 -0
  165. package/src/reverse.ts +65 -40
  166. package/src/root-error-boundary.tsx +1 -19
  167. package/src/route-content-wrapper.tsx +7 -72
  168. package/src/route-definition/dsl-helpers.ts +244 -281
  169. package/src/route-definition/helper-factories.ts +29 -139
  170. package/src/route-definition/helpers-types.ts +40 -17
  171. package/src/route-definition/redirect.ts +43 -9
  172. package/src/route-definition/resolve-handler-use.ts +6 -0
  173. package/src/route-definition/use-item-types.ts +32 -0
  174. package/src/route-map-builder.ts +0 -16
  175. package/src/route-types.ts +19 -41
  176. package/src/router/basename.ts +14 -0
  177. package/src/router/content-negotiation.ts +15 -15
  178. package/src/router/error-handling.ts +13 -17
  179. package/src/router/find-match.ts +44 -23
  180. package/src/router/handler-context.ts +4 -41
  181. package/src/router/intercept-resolution.ts +14 -19
  182. package/src/router/lazy-includes.ts +9 -46
  183. package/src/router/loader-resolution.ts +91 -46
  184. package/src/router/logging.ts +0 -6
  185. package/src/router/manifest.ts +18 -29
  186. package/src/router/match-api.ts +0 -20
  187. package/src/router/match-context.ts +0 -22
  188. package/src/router/match-handlers.ts +57 -58
  189. package/src/router/match-middleware/background-revalidation.ts +0 -7
  190. package/src/router/match-middleware/cache-lookup.ts +150 -271
  191. package/src/router/match-middleware/cache-store.ts +3 -33
  192. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  193. package/src/router/match-middleware/segment-resolution.ts +0 -22
  194. package/src/router/match-pipelines.ts +1 -42
  195. package/src/router/match-result.ts +31 -80
  196. package/src/router/metrics.ts +0 -34
  197. package/src/router/middleware-types.ts +5 -112
  198. package/src/router/middleware.ts +118 -133
  199. package/src/router/navigation-snapshot.ts +0 -51
  200. package/src/router/params-util.ts +23 -0
  201. package/src/router/pattern-matching.ts +62 -67
  202. package/src/router/prerender-match.ts +99 -63
  203. package/src/router/preview-match.ts +3 -1
  204. package/src/router/request-classification.ts +28 -62
  205. package/src/router/revalidation.ts +50 -56
  206. package/src/router/route-snapshot.ts +0 -1
  207. package/src/router/router-context.ts +0 -27
  208. package/src/router/router-interfaces.ts +68 -35
  209. package/src/router/router-options.ts +55 -1
  210. package/src/router/router-registry.ts +2 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -63
  212. package/src/router/segment-resolution/helpers.ts +34 -0
  213. package/src/router/segment-resolution/loader-cache.ts +40 -37
  214. package/src/router/segment-resolution/revalidation.ts +203 -285
  215. package/src/router/segment-resolution/static-store.ts +19 -5
  216. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  217. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  218. package/src/router/segment-resolution.ts +4 -1
  219. package/src/router/segment-wrappers.ts +0 -3
  220. package/src/router/state-cookie-name.ts +33 -0
  221. package/src/router/substitute-pattern-params.ts +56 -0
  222. package/src/router/telemetry-otel.ts +0 -20
  223. package/src/router/telemetry.ts +96 -19
  224. package/src/router/timeout.ts +0 -20
  225. package/src/router/trie-matching.ts +87 -48
  226. package/src/router/types.ts +9 -63
  227. package/src/router/url-params.ts +0 -5
  228. package/src/router.ts +80 -41
  229. package/src/rsc/handler-context.ts +3 -2
  230. package/src/rsc/handler.ts +83 -78
  231. package/src/rsc/helpers.ts +93 -5
  232. package/src/rsc/index.ts +1 -1
  233. package/src/rsc/json-route-result.ts +38 -0
  234. package/src/rsc/manifest-init.ts +28 -41
  235. package/src/rsc/origin-guard.ts +39 -25
  236. package/src/rsc/progressive-enhancement.ts +12 -1
  237. package/src/rsc/redirect-guard.ts +99 -0
  238. package/src/rsc/response-error.ts +79 -12
  239. package/src/rsc/response-route-handler.ts +76 -62
  240. package/src/rsc/rsc-rendering.ts +41 -60
  241. package/src/rsc/runtime-warnings.ts +23 -10
  242. package/src/rsc/server-action.ts +62 -67
  243. package/src/rsc/ssr-setup.ts +16 -0
  244. package/src/rsc/types.ts +10 -5
  245. package/src/runtime-env.ts +18 -0
  246. package/src/search-params.ts +4 -20
  247. package/src/segment-loader-promise.ts +14 -2
  248. package/src/segment-system.tsx +199 -142
  249. package/src/serialize.ts +243 -0
  250. package/src/server/context.ts +150 -51
  251. package/src/server/cookie-store.ts +80 -5
  252. package/src/server/handle-store.ts +7 -24
  253. package/src/server/loader-registry.ts +5 -24
  254. package/src/server/request-context.ts +165 -87
  255. package/src/ssr/index.tsx +14 -14
  256. package/src/static-handler.ts +10 -13
  257. package/src/testing/cache-status.ts +162 -0
  258. package/src/testing/collect-handle.ts +40 -0
  259. package/src/testing/dispatch.ts +618 -0
  260. package/src/testing/dom.entry.ts +22 -0
  261. package/src/testing/e2e/fixture.ts +188 -0
  262. package/src/testing/e2e/index.ts +128 -0
  263. package/src/testing/e2e/matchers.ts +35 -0
  264. package/src/testing/e2e/page-helpers.ts +272 -0
  265. package/src/testing/e2e/parity.ts +387 -0
  266. package/src/testing/e2e/server.ts +195 -0
  267. package/src/testing/flight-matchers.ts +97 -0
  268. package/src/testing/flight-normalize.ts +11 -0
  269. package/src/testing/flight-runtime.d.ts +57 -0
  270. package/src/testing/flight-tree.ts +682 -0
  271. package/src/testing/flight.entry.ts +52 -0
  272. package/src/testing/flight.ts +232 -0
  273. package/src/testing/generated-routes.ts +183 -0
  274. package/src/testing/index.ts +99 -0
  275. package/src/testing/internal/context.ts +348 -0
  276. package/src/testing/internal/flight-client-globals.ts +30 -0
  277. package/src/testing/internal/seed-vars.ts +54 -0
  278. package/src/testing/render-handler.ts +330 -0
  279. package/src/testing/render-route.tsx +566 -0
  280. package/src/testing/run-loader.ts +378 -0
  281. package/src/testing/run-middleware.ts +205 -0
  282. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  283. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  284. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  285. package/src/testing/vitest-stubs/version.ts +5 -0
  286. package/src/testing/vitest.ts +305 -0
  287. package/src/theme/ThemeProvider.tsx +0 -52
  288. package/src/theme/ThemeScript.tsx +0 -6
  289. package/src/theme/constants.ts +0 -12
  290. package/src/theme/index.ts +0 -7
  291. package/src/theme/theme-context.ts +1 -5
  292. package/src/theme/theme-script.ts +0 -14
  293. package/src/theme/use-theme.ts +0 -3
  294. package/src/types/boundaries.ts +0 -35
  295. package/src/types/cache-types.ts +13 -4
  296. package/src/types/error-types.ts +30 -90
  297. package/src/types/global-namespace.ts +54 -41
  298. package/src/types/handler-context.ts +97 -22
  299. package/src/types/index.ts +1 -10
  300. package/src/types/loader-types.ts +6 -3
  301. package/src/types/request-scope.ts +0 -19
  302. package/src/types/route-config.ts +6 -50
  303. package/src/types/route-entry.ts +0 -6
  304. package/src/types/segments.ts +18 -14
  305. package/src/urls/include-helper.ts +9 -56
  306. package/src/urls/index.ts +1 -11
  307. package/src/urls/path-helper-types.ts +19 -5
  308. package/src/urls/path-helper.ts +17 -106
  309. package/src/urls/pattern-types.ts +36 -19
  310. package/src/urls/response-types.ts +20 -19
  311. package/src/urls/type-extraction.ts +58 -139
  312. package/src/urls/urls-function.ts +1 -18
  313. package/src/use-loader.tsx +292 -107
  314. package/src/vite/debug.ts +1 -0
  315. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  316. package/src/vite/discovery/discover-routers.ts +95 -82
  317. package/src/vite/discovery/discovery-errors.ts +194 -0
  318. package/src/vite/discovery/prerender-collection.ts +26 -34
  319. package/src/vite/discovery/route-types-writer.ts +40 -84
  320. package/src/vite/discovery/state.ts +39 -1
  321. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  322. package/src/vite/index.ts +4 -0
  323. package/src/vite/plugin-types.ts +185 -10
  324. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  325. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  326. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  327. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  328. package/src/vite/plugins/expose-action-id.ts +4 -75
  329. package/src/vite/plugins/expose-id-utils.ts +3 -54
  330. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  331. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  332. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  333. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  334. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  335. package/src/vite/plugins/performance-tracks.ts +9 -16
  336. package/src/vite/plugins/refresh-cmd.ts +1 -1
  337. package/src/vite/plugins/use-cache-transform.ts +26 -49
  338. package/src/vite/plugins/vercel-output.ts +258 -0
  339. package/src/vite/plugins/version-injector.ts +2 -32
  340. package/src/vite/plugins/version-plugin.ts +32 -23
  341. package/src/vite/plugins/virtual-entries.ts +35 -17
  342. package/src/vite/rango.ts +148 -115
  343. package/src/vite/router-discovery.ts +220 -68
  344. package/src/vite/utils/ast-handler-extract.ts +15 -31
  345. package/src/vite/utils/bundle-analysis.ts +10 -15
  346. package/src/vite/utils/client-chunks.ts +184 -0
  347. package/src/vite/utils/forward-user-plugins.ts +171 -0
  348. package/src/vite/utils/manifest-utils.ts +4 -59
  349. package/src/vite/utils/package-resolution.ts +1 -73
  350. package/src/vite/utils/prerender-utils.ts +0 -34
  351. package/src/vite/utils/shared-utils.ts +95 -43
  352. package/src/browser/action-response-classifier.ts +0 -99
  353. package/src/browser/react/use-client-cache.ts +0 -58
  354. package/src/browser/shallow.ts +0 -40
  355. package/src/handles/index.ts +0 -7
  356. package/src/router/middleware-cookies.ts +0 -55
@@ -9,15 +9,15 @@
9
9
  import {
10
10
  requireRequestContext,
11
11
  setRequestContextParams,
12
- getLocationState,
13
12
  } from "../server/request-context.js";
14
- import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
15
13
  import { appendMetric } from "../router/metrics.js";
16
- import { getSSRSetup } from "./ssr-setup.js";
14
+ import { getSSRSetup, isRscRequest } from "./ssr-setup.js";
17
15
  import type { RscPayload } from "./types.js";
16
+ import type { MatchResult } from "../types.js";
18
17
  import {
19
18
  createResponseWithMergedHeaders,
20
19
  createSimpleRedirectResponse,
20
+ attachLocationStateIfPresent,
21
21
  } from "./helpers.js";
22
22
  import type { HandlerContext } from "./handler-context.js";
23
23
 
@@ -35,6 +35,29 @@ export async function handleRscRendering<TEnv>(
35
35
  let payload: RscPayload;
36
36
  let hasInterceptSlots = false;
37
37
 
38
+ // Shared by the partial-fallback and full-render paths. The partial-success
39
+ // payload below is intentionally different (omits rootLayout/theme, adds slots).
40
+ const buildFullPayload = (m: MatchResult): RscPayload => ({
41
+ metadata: {
42
+ pathname: url.pathname,
43
+ routerId: ctx.router.id,
44
+ basename: ctx.router.basename,
45
+ segments: m.segments,
46
+ matched: m.matched,
47
+ diff: m.diff,
48
+ resolvedIds: m.resolvedIds,
49
+ params: m.params,
50
+ isPartial: false,
51
+ rootLayout: ctx.router.rootLayout,
52
+ handles: handleStore.stream(),
53
+ version: ctx.version,
54
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
55
+ stateCookieName: ctx.router.resolvedStateCookieName,
56
+ themeConfig: ctx.router.themeConfig,
57
+ initialTheme: reqCtx.theme,
58
+ },
59
+ });
60
+
38
61
  if (isPartial) {
39
62
  // Partial render (navigation)
40
63
  const result = await ctx.router.matchPartial(request, { env });
@@ -51,25 +74,7 @@ export async function handleRscRendering<TEnv>(
51
74
  return createSimpleRedirectResponse(match.redirect);
52
75
  }
53
76
 
54
- payload = {
55
- metadata: {
56
- pathname: url.pathname,
57
- routerId: ctx.router.id,
58
- basename: ctx.router.basename,
59
- segments: match.segments,
60
- matched: match.matched,
61
- diff: match.diff,
62
- resolvedIds: match.resolvedIds,
63
- params: match.params,
64
- isPartial: false,
65
- rootLayout: ctx.router.rootLayout,
66
- handles: handleStore.stream(),
67
- version: ctx.version,
68
- prefetchCacheTTL: ctx.router.prefetchCacheTTL,
69
- themeConfig: ctx.router.themeConfig,
70
- initialTheme: reqCtx.theme,
71
- },
72
- };
77
+ payload = buildFullPayload(match);
73
78
  } else {
74
79
  setRequestContextParams(result.params, result.routeName);
75
80
 
@@ -78,6 +83,11 @@ export async function handleRscRendering<TEnv>(
78
83
  payload = {
79
84
  metadata: {
80
85
  pathname: url.pathname,
86
+ // routerId is serialized on every payload (including within-session
87
+ // ones) so the frontend can read the current app/router identity. It
88
+ // always equals the current app's id: a cross-app navigation is
89
+ // intercepted server-side (X-RSC-Reload) and never delivers a
90
+ // different-router payload to the client.
81
91
  routerId: ctx.router.id,
82
92
  segments: result.segments,
83
93
  matched: result.matched,
@@ -89,6 +99,7 @@ export async function handleRscRendering<TEnv>(
89
99
  handles: handleStore.stream(),
90
100
  version: ctx.version,
91
101
  prefetchCacheTTL: ctx.router.prefetchCacheTTL,
102
+ stateCookieName: ctx.router.resolvedStateCookieName,
92
103
  },
93
104
  };
94
105
  }
@@ -135,28 +146,7 @@ export async function handleRscRendering<TEnv>(
135
146
  { headers: { "Content-Type": "application/json" } },
136
147
  );
137
148
  } else {
138
- payload = {
139
- // Initial SSR can reconstruct the tree from segments + rootLayout,
140
- // so we omit root to avoid sending the same structure twice.
141
-
142
- metadata: {
143
- pathname: url.pathname,
144
- routerId: ctx.router.id,
145
- basename: ctx.router.basename,
146
- segments: match.segments,
147
- matched: match.matched,
148
- diff: match.diff,
149
- resolvedIds: match.resolvedIds,
150
- params: match.params,
151
- isPartial: false,
152
- rootLayout: ctx.router.rootLayout,
153
- handles: handleStore.stream(),
154
- version: ctx.version,
155
- prefetchCacheTTL: ctx.router.prefetchCacheTTL,
156
- themeConfig: ctx.router.themeConfig,
157
- initialTheme: reqCtx.theme,
158
- },
159
- };
149
+ payload = buildFullPayload(match);
160
150
  }
161
151
  }
162
152
 
@@ -164,11 +154,7 @@ export async function handleRscRendering<TEnv>(
164
154
  // SSR (full page) requests ignore location state since there's no history.state
165
155
  // to write to on a fresh page load.
166
156
  if (isPartial && payload.metadata) {
167
- const locationState = getLocationState();
168
- if (locationState) {
169
- payload.metadata.locationState =
170
- resolveLocationStateEntries(locationState);
171
- }
157
+ attachLocationStateIfPresent(payload);
172
158
  }
173
159
 
174
160
  const metricsStore = reqCtx._metricsStore;
@@ -190,22 +176,17 @@ export async function handleRscRendering<TEnv>(
190
176
  rscSerializeDur,
191
177
  );
192
178
 
193
- // Determine if this is an RSC request or HTML request.
194
- // Partial requests (_rsc_partial) are always RSC -- they come from client-side
195
- // navigation or prefetch fetch(). We cannot rely on Accept alone since some
196
- // browsers may send Accept: text/html for non-HTML requests.
197
- const isRscRequest =
198
- isPartial ||
199
- (!request.headers.get("accept")?.includes("text/html") &&
200
- !url.searchParams.has("__html")) ||
201
- url.searchParams.has("__rsc");
202
-
203
- if (isRscRequest) {
179
+ if (isRscRequest(request, url, isPartial)) {
204
180
  const renderDur = performance.now() - renderStart;
205
181
  appendMetric(metricsStore, "render:total", renderStart, renderDur);
206
182
  const rscHeaders: Record<string, string> = {
207
183
  "content-type": "text/x-component;charset=utf-8",
208
184
  vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
185
+ // Router identity, so the client can verify pre-decode (before importing
186
+ // chunks) that this content payload belongs to its app and refuse a
187
+ // foreign one (cache/proxy/bug). Control-only reload/redirect responses
188
+ // are deliberately NOT stamped. See browser/response-adapter.ts.
189
+ "X-RSC-Router-Id": ctx.router.id,
209
190
  };
210
191
  // Tell the client's prefetch cache to scope this response to its source
211
192
  // URL (instead of the default source-agnostic wildcard). Intercept
@@ -8,6 +8,7 @@ import {
8
8
  createResponseWithMergedHeaders,
9
9
  carryOverRedirectHeaders,
10
10
  } from "./helpers.js";
11
+ import { isRedirectResponse } from "../response-utils.js";
11
12
 
12
13
  // W3 -----------------------------------------------------------------------
13
14
 
@@ -18,16 +19,14 @@ import {
18
19
  */
19
20
  export function extractRedirectResponse(value: unknown): Response | null {
20
21
  if (!(value instanceof Response)) return null;
21
- const location = value.headers.get("Location");
22
- if (value.status >= 300 && value.status < 400 && location) {
23
- const redirect = createResponseWithMergedHeaders(null, {
24
- status: value.status,
25
- headers: { Location: location },
26
- });
27
- carryOverRedirectHeaders(value, redirect);
28
- return redirect;
29
- }
30
- return null;
22
+ if (!isRedirectResponse(value)) return null;
23
+ const location = value.headers.get("Location")!;
24
+ const redirect = createResponseWithMergedHeaders(null, {
25
+ status: value.status,
26
+ headers: { Location: location },
27
+ });
28
+ carryOverRedirectHeaders(value, redirect);
29
+ return redirect;
31
30
  }
32
31
 
33
32
  /**
@@ -40,3 +39,17 @@ export function warnNonRedirectPeResponse(): void {
40
39
  `ignored — the page will re-render at the current URL instead.`,
41
40
  );
42
41
  }
42
+
43
+ /**
44
+ * Warn when a non-redirect Response is returned (not thrown) from an action
45
+ * on the JS (fetch) path. A raw Response cannot be serialized into Flight, so
46
+ * it is discarded — mirroring the PE path. Use `throw redirect('/path')` for
47
+ * redirects.
48
+ */
49
+ export function warnNonRedirectActionResponse(actionId: string): void {
50
+ console.warn(
51
+ `[@rangojs/router] Server action "${actionId}" returned a Response ` +
52
+ `that is not a redirect. Non-redirect Responses cannot be serialized ` +
53
+ `and are ignored. Use \`throw redirect('/path')\` for redirects.`,
54
+ );
55
+ }
@@ -18,31 +18,19 @@
18
18
  import {
19
19
  requireRequestContext,
20
20
  setRequestContextParams,
21
- getLocationState,
22
21
  } from "../server/request-context.js";
23
- import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
24
22
  import { appendMetric } from "../router/metrics.js";
25
23
  import type { RscPayload } from "./types.js";
26
24
  import {
27
25
  hasBodyContent,
28
26
  createResponseWithMergedHeaders,
29
27
  createSimpleRedirectResponse,
30
- carryOverRedirectHeaders,
28
+ interceptRedirectForPartial,
29
+ attachLocationStateIfPresent,
31
30
  } from "./helpers.js";
31
+ import { warnNonRedirectActionResponse } from "./runtime-warnings.js";
32
32
  import type { HandlerContext } from "./handler-context.js";
33
33
 
34
- /**
35
- * Attach location state set during the action to a payload's metadata.
36
- * No-op if no location state was set.
37
- */
38
- function attachLocationState(payload: RscPayload): void {
39
- const locationState = getLocationState();
40
- if (locationState) {
41
- payload.metadata!.locationState =
42
- resolveLocationStateEntries(locationState);
43
- }
44
- }
45
-
46
34
  /**
47
35
  * Data flowing from action execution to the revalidation phase.
48
36
  * When the action completes without redirect/error-boundary, the handler
@@ -97,7 +85,10 @@ export async function executeServerAction<TEnv>(
97
85
  args = await ctx.decodeReply(body, { temporaryReferences });
98
86
  }
99
87
  } catch (error) {
100
- throw new Error(`Failed to decode action arguments: ${error}`, {
88
+ // Keep the original error as `cause` for server-side logging, but do not
89
+ // interpolate it into the message: that string can surface to the client
90
+ // and may leak decode internals.
91
+ throw new Error("Failed to decode action arguments", {
101
92
  cause: error,
102
93
  });
103
94
  }
@@ -109,51 +100,35 @@ export async function executeServerAction<TEnv>(
109
100
 
110
101
  try {
111
102
  loadedAction = await ctx.loadServerAction(actionId);
112
- const data = await loadedAction!.apply(null, args);
103
+ let data = await loadedAction!.apply(null, args);
113
104
 
114
- // Intercept redirect responses from actions. Without this, the redirect
115
- // Response would be serialized as the action returnValue (which fails)
116
- // and the revalidation step would run unnecessarily.
105
+ // Intercept redirect Responses: serializing one as the action returnValue
106
+ // would fail, and revalidation would run needlessly.
117
107
  if (data instanceof Response) {
118
- const redirectUrl = data.headers.get("Location");
119
- const isRedirect = data.status >= 300 && data.status < 400 && redirectUrl;
120
- if (isRedirect) {
121
- const locationState = getLocationState();
122
- let redirect: Response;
123
- if (locationState) {
124
- redirect = ctx.createRedirectFlightResponse(
125
- redirectUrl,
126
- resolveLocationStateEntries(locationState),
127
- );
128
- } else {
129
- redirect = createSimpleRedirectResponse(redirectUrl);
130
- }
131
- carryOverRedirectHeaders(data, redirect);
132
- return redirect;
108
+ const intercepted = interceptRedirectForPartial(
109
+ data,
110
+ ctx.createRedirectFlightResponse,
111
+ );
112
+ if (intercepted) return intercepted;
113
+
114
+ // Non-redirect Response returned (not thrown): a raw Response cannot be
115
+ // serialized into Flight. Discard it and re-render — mirroring the PE
116
+ // path (progressive-enhancement.ts) so JS and no-JS behave identically.
117
+ if (process.env.NODE_ENV !== "production") {
118
+ warnNonRedirectActionResponse(actionId);
133
119
  }
120
+ data = undefined;
134
121
  }
135
122
 
136
123
  returnValue = { ok: true, data };
137
124
  } catch (error) {
138
125
  // Handle thrown redirect (e.g., throw redirect('/path'))
139
126
  if (error instanceof Response) {
140
- const redirectUrl = error.headers.get("Location");
141
- const isRedirect =
142
- error.status >= 300 && error.status < 400 && redirectUrl;
143
- if (isRedirect) {
144
- const locationState = getLocationState();
145
- let redirect: Response;
146
- if (locationState) {
147
- redirect = ctx.createRedirectFlightResponse(
148
- redirectUrl,
149
- resolveLocationStateEntries(locationState),
150
- );
151
- } else {
152
- redirect = createSimpleRedirectResponse(redirectUrl);
153
- }
154
- carryOverRedirectHeaders(error, redirect);
155
- return redirect;
156
- }
127
+ const intercepted = interceptRedirectForPartial(
128
+ error,
129
+ ctx.createRedirectFlightResponse,
130
+ );
131
+ if (intercepted) return intercepted;
157
132
 
158
133
  // Non-redirect Response thrown from action — this will be treated
159
134
  // as a regular error and routed to the error boundary. Warn in dev
@@ -208,6 +183,8 @@ export async function executeServerAction<TEnv>(
208
183
  const payload: RscPayload = {
209
184
  metadata: {
210
185
  pathname: url.pathname,
186
+ // routerId exposed for the frontend (current app identity); see
187
+ // rsc-rendering.ts partial branch.
211
188
  routerId: ctx.router.id,
212
189
  segments: errorResult.segments,
213
190
  isPartial: true,
@@ -235,24 +212,32 @@ export async function executeServerAction<TEnv>(
235
212
 
236
213
  return createResponseWithMergedHeaders(rscStream, {
237
214
  status: actionStatus,
238
- headers: { "content-type": "text/x-component;charset=utf-8" },
215
+ headers: {
216
+ "content-type": "text/x-component;charset=utf-8",
217
+ // Router identity for the client's pre-decode integrity check (the
218
+ // action apply path has no post-decode guard). See response-adapter.
219
+ "X-RSC-Router-Id": ctx.router.id,
220
+ },
239
221
  });
240
222
  }
241
223
  }
242
224
 
243
225
  // Build continuation for the revalidation phase
244
- const resolvedActionId =
245
- (loadedAction as { $id?: string; $$id?: string } | undefined)?.$id ??
246
- (loadedAction as { $$id?: string } | undefined)?.$$id ??
247
- actionId;
226
+ const actionMeta = loadedAction as
227
+ | { $id?: string; $$id?: string }
228
+ | undefined;
229
+ const resolvedActionId = actionMeta?.$id ?? actionMeta?.$$id ?? actionId;
248
230
 
249
231
  return {
250
232
  returnValue,
251
233
  actionStatus,
252
234
  temporaryReferences,
253
235
  actionContext: {
236
+ // Defensive copy of the already-parsed url (avoids re-parsing
237
+ // request.url). actionUrl is persisted into the continuation and later
238
+ // flows into matchPartial, so it must not alias the handler's live url.
254
239
  actionId: resolvedActionId,
255
- actionUrl: new URL(request.url),
240
+ actionUrl: new URL(url),
256
241
  actionResult: returnValue.data,
257
242
  formData: actionFormData,
258
243
  },
@@ -291,8 +276,8 @@ export async function revalidateAfterAction<TEnv>(
291
276
  );
292
277
 
293
278
  if (!matchResult) {
294
- // matchPartial returns null when the route is a redirect or the request
295
- // is missing required headers (previousUrl). Check for redirect first.
279
+ // matchPartial returns null when the route is a redirect or no previous-URL
280
+ // context could be resolved. Check for redirect first.
296
281
  const fullMatch = await ctx.router.match(request, { env });
297
282
  setRequestContextParams(fullMatch.params, fullMatch.routeName);
298
283
 
@@ -303,14 +288,17 @@ export async function revalidateAfterAction<TEnv>(
303
288
  return createSimpleRedirectResponse(fullMatch.redirect);
304
289
  }
305
290
 
306
- // Non-redirect: this branch is only reachable when the action request
307
- // is missing the X-RSC-Router-Client-Path header (defensive). The
308
- // client requires isPartial for action responses, so producing a full
309
- // payload here would be rejected. Return 500 instead.
291
+ // Non-redirect: this branch is only reachable when no previous URL could
292
+ // be resolved (neither X-RSC-Router-Client-Path nor a usable Referer), or
293
+ // the previous URL was unparseable (defensive). The client requires
294
+ // isPartial for action responses, so producing a full payload here would
295
+ // be rejected. Return 500 instead.
310
296
  throw new Error(
311
297
  `[RSC] matchPartial returned null for a non-redirect route ` +
312
298
  `during action revalidation (${url.pathname}). This indicates ` +
313
- `a malformed action request (missing X-RSC-Router-Client-Path header).`,
299
+ `a malformed action request: no previous-URL context could be ` +
300
+ `resolved (neither X-RSC-Router-Client-Path nor a usable Referer), ` +
301
+ `or the previous URL was unparseable.`,
314
302
  );
315
303
  }
316
304
 
@@ -320,6 +308,8 @@ export async function revalidateAfterAction<TEnv>(
320
308
  const payload: RscPayload = {
321
309
  metadata: {
322
310
  pathname: url.pathname,
311
+ // routerId exposed for the frontend (current app identity); see
312
+ // rsc-rendering.ts partial branch.
323
313
  routerId: ctx.router.id,
324
314
  segments: matchResult.segments,
325
315
  isPartial: true,
@@ -334,7 +324,7 @@ export async function revalidateAfterAction<TEnv>(
334
324
  returnValue,
335
325
  };
336
326
 
337
- attachLocationState(payload);
327
+ attachLocationStateIfPresent(payload);
338
328
 
339
329
  const renderStart = performance.now();
340
330
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
@@ -355,6 +345,11 @@ export async function revalidateAfterAction<TEnv>(
355
345
 
356
346
  return createResponseWithMergedHeaders(rscStream, {
357
347
  status: actionStatus,
358
- headers: { "content-type": "text/x-component;charset=utf-8" },
348
+ headers: {
349
+ "content-type": "text/x-component;charset=utf-8",
350
+ // Router identity for the client's pre-decode integrity check (the action
351
+ // apply path has no post-decode guard). See response-adapter.
352
+ "X-RSC-Router-Id": ctx.router.id,
353
+ },
359
354
  });
360
355
  }
@@ -126,3 +126,19 @@ export function mayNeedSSR(request: Request, url: URL): boolean {
126
126
 
127
127
  return true;
128
128
  }
129
+
130
+ // Final render-time decision: is the response an RSC stream (vs HTML)? Distinct
131
+ // from mayNeedSSR, which is a conservative pre-classifier (it treats a missing
132
+ // Accept header as needing SSR; this treats it as RSC).
133
+ export function isRscRequest(
134
+ request: Request,
135
+ url: URL,
136
+ isPartial: boolean,
137
+ ): boolean {
138
+ return (
139
+ isPartial ||
140
+ (!request.headers.get("accept")?.includes("text/html") &&
141
+ !url.searchParams.has("__html")) ||
142
+ url.searchParams.has("__rsc")
143
+ );
144
+ }
package/src/rsc/types.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { ResolvedSegment, SlotState } from "../types.js";
9
9
  import type { HandleData } from "../server/handle-store.js";
10
- import type { RSCRouterInternal } from "../router/router-interfaces.js";
10
+ import type { RangoInternal } from "../router/router-interfaces.js";
11
11
  import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
12
12
 
13
13
  /**
@@ -43,6 +43,8 @@ export interface RscPayload {
43
43
  version?: string;
44
44
  /** TTL in milliseconds for the client-side in-memory prefetch cache */
45
45
  prefetchCacheTTL?: number;
46
+ /** Server-resolved rango state cookie name; the client reads it verbatim. */
47
+ stateCookieName?: string;
46
48
  /** Theme configuration for FOUC prevention */
47
49
  themeConfig?: ResolvedThemeConfig | null;
48
50
  /** Initial theme from cookie (for SSR hydration) */
@@ -51,13 +53,16 @@ export interface RscPayload {
51
53
  basename?: string;
52
54
  /** Whether connection warmup is enabled */
53
55
  warmupEnabled?: boolean;
54
- /** Server-side redirect with optional state (for partial requests) */
55
- redirect?: { url: string };
56
+ /**
57
+ * Server-side redirect with optional state (for partial requests).
58
+ * `external: true` (from redirect(url, { external: true })) tells the client
59
+ * to hard-navigate to an off-host target instead of validating same-origin.
60
+ */
61
+ redirect?: { url: string; external?: boolean };
56
62
  /** Server-set location state to include in history.pushState */
57
63
  locationState?: Record<string, unknown>;
58
64
  };
59
65
  returnValue?: { ok: boolean; data: unknown };
60
- formState?: unknown;
61
66
  }
62
67
 
63
68
  /**
@@ -185,7 +190,7 @@ export interface CreateRSCHandlerOptions<
185
190
  /**
186
191
  * The RSC router instance
187
192
  */
188
- router: RSCRouterInternal<TEnv, TRoutes>;
193
+ router: RangoInternal<TEnv, TRoutes>;
189
194
 
190
195
  /**
191
196
  * RSC dependencies from @vitejs/plugin-rsc/rsc.
@@ -0,0 +1,18 @@
1
+ // Runtime-safe detection of a test runner (Vitest), used to decide whether a
2
+ // create*() call with no plugin-injected $$id may fall back to a synthetic id (a
3
+ // bare test) or must fail loud (dev / a real build).
4
+ //
5
+ // `process` is absent in some target runtimes (the browser, certain edge/worker
6
+ // RSC environments), so probe it through `globalThis` with optional chaining —
7
+ // NEVER a bare `process.env.VITEST`, which would ReferenceError before the
8
+ // intended error is thrown. Unlike `process.env.NODE_ENV` (folded by the app's
9
+ // build `define`), `VITEST` is not folded, so this stays a small runtime check;
10
+ // it lives only on the create*() error path (id missing), which never runs in a
11
+ // correct production build.
12
+ //
13
+ // Vitest sets `VITEST` in every test process — the node project and the
14
+ // react-server forks alike (the RSC project forces NODE_ENV=production, so NODE_ENV
15
+ // cannot distinguish it from a real build; `VITEST` can). A real build never sets it.
16
+ export function isUnderTestRunner(): boolean {
17
+ return !!globalThis.process?.env?.VITEST;
18
+ }
@@ -7,10 +7,6 @@
7
7
  * URLSearchParams instance.
8
8
  */
9
9
 
10
- // ============================================================================
11
- // Schema Types
12
- // ============================================================================
13
-
14
10
  /** Supported scalar types for search params (append ? for optional). */
15
11
  export type SearchSchemaValue =
16
12
  | "string"
@@ -23,10 +19,6 @@ export type SearchSchemaValue =
23
19
  /** A search schema maps param names to their type descriptors. */
24
20
  export type SearchSchema = Record<string, SearchSchemaValue>;
25
21
 
26
- // ============================================================================
27
- // Type-Level Schema Resolution
28
- // ============================================================================
29
-
30
22
  /** Strip trailing `?` from a schema value to get the base type. */
31
23
  type BaseType<T extends string> = T extends `${infer B}?` ? B : T;
32
24
 
@@ -81,11 +73,11 @@ export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
81
73
  // ============================================================================
82
74
 
83
75
  /** Resolve the global route map from RegisteredRoutes or GeneratedRouteMap. */
84
- type GlobalRouteMap = keyof RSCRouter.RegisteredRoutes extends never
85
- ? keyof RSCRouter.GeneratedRouteMap extends never
76
+ type GlobalRouteMap = keyof Rango.RegisteredRoutes extends never
77
+ ? keyof Rango.GeneratedRouteMap extends never
86
78
  ? Record<string, string>
87
- : RSCRouter.GeneratedRouteMap
88
- : RSCRouter.RegisteredRoutes;
79
+ : Rango.GeneratedRouteMap
80
+ : Rango.RegisteredRoutes;
89
81
 
90
82
  /**
91
83
  * Extract the resolved search params type for a named route.
@@ -163,10 +155,6 @@ type ExtractParamsFromPattern<T extends string> =
163
155
  : { [K in Param]: string }
164
156
  : {};
165
157
 
166
- // ============================================================================
167
- // Runtime Parser
168
- // ============================================================================
169
-
170
158
  /**
171
159
  * Parse URLSearchParams into a typed object using the given schema.
172
160
  *
@@ -210,10 +198,6 @@ export function parseSearchParams<T extends SearchSchema>(
210
198
  return result as ResolveSearchSchema<T>;
211
199
  }
212
200
 
213
- // ============================================================================
214
- // Runtime Serializer
215
- // ============================================================================
216
-
217
201
  /**
218
202
  * Serialize a typed search params object to a query string (without leading `?`).
219
203
  * Skips `undefined` and `null` values.
@@ -26,7 +26,10 @@ const IS_BROWSER = typeof window !== "undefined";
26
26
 
27
27
  interface LoaderCacheEntry {
28
28
  sources: any[];
29
- promise: Promise<any[]> | any[];
29
+ // buildLoaderPromise always returns a Promise, so the cached value is never a
30
+ // bare array. The public getMemoizedLoaderPromise return type stays broader
31
+ // (Promise<any[]> | any[]) to mirror its siblings.
32
+ promise: Promise<any[]>;
30
33
  }
31
34
 
32
35
  const objectLoaderCache = IS_BROWSER
@@ -56,7 +59,16 @@ function hasSameReferences(a: any[], b: any[]): boolean {
56
59
  return true;
57
60
  }
58
61
 
59
- function buildLoaderPromise(loaders: ResolvedSegment[]): Promise<any[]> {
62
+ /**
63
+ * Build a fresh aggregate Promise.all over the loaders' resolved data refs.
64
+ * Unlike getMemoizedLoaderPromise this never caches, so each call yields a new
65
+ * Promise — correct for sites that await the result immediately (a shared,
66
+ * already-resolved promise would leak React's `.status` across server requests
67
+ * and skip the Suspense fallback).
68
+ *
69
+ * @internal
70
+ */
71
+ export function buildLoaderPromise(loaders: ResolvedSegment[]): Promise<any[]> {
60
72
  if (loaders.length === 0) {
61
73
  return Promise.resolve([]);
62
74
  }