@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125

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 (260) hide show
  1. package/dist/bin/rango.js +10 -6
  2. package/dist/testing/vitest.js +82 -0
  3. package/dist/vite/index.js +55 -48
  4. package/package.json +61 -21
  5. package/skills/caching/SKILL.md +2 -1
  6. package/skills/hooks/SKILL.md +40 -29
  7. package/skills/host-router/SKILL.md +16 -2
  8. package/skills/intercept/SKILL.md +4 -2
  9. package/skills/layout/SKILL.md +11 -6
  10. package/skills/loader/SKILL.md +6 -2
  11. package/skills/middleware/SKILL.md +4 -2
  12. package/skills/migrate-nextjs/SKILL.md +3 -1
  13. package/skills/parallel/SKILL.md +9 -4
  14. package/skills/rango/SKILL.md +12 -0
  15. package/skills/route/SKILL.md +10 -2
  16. package/skills/testing/SKILL.md +129 -0
  17. package/skills/testing/bindings.md +89 -0
  18. package/skills/testing/cache-prerender.md +98 -0
  19. package/skills/testing/client-components.md +122 -0
  20. package/skills/testing/e2e-parity.md +125 -0
  21. package/skills/testing/flight.md +89 -0
  22. package/skills/testing/handles.md +129 -0
  23. package/skills/testing/loader.md +128 -0
  24. package/skills/testing/middleware.md +99 -0
  25. package/skills/testing/render-handler.md +118 -0
  26. package/skills/testing/response-routes.md +95 -0
  27. package/skills/testing/reverse-and-types.md +84 -0
  28. package/skills/testing/server-actions.md +107 -0
  29. package/skills/testing/server-tree.md +128 -0
  30. package/skills/testing/setup.md +120 -0
  31. package/src/__internal.ts +0 -65
  32. package/src/browser/action-coordinator.ts +1 -1
  33. package/src/browser/action-fence.ts +47 -0
  34. package/src/browser/cookie-name.ts +140 -0
  35. package/src/browser/event-controller.ts +1 -83
  36. package/src/browser/invalidate-client-cache.ts +52 -0
  37. package/src/browser/navigation-bridge.ts +14 -1
  38. package/src/browser/navigation-client.ts +14 -1
  39. package/src/browser/navigation-store-handle.ts +38 -0
  40. package/src/browser/navigation-store.ts +26 -51
  41. package/src/browser/navigation-transaction.ts +0 -32
  42. package/src/browser/partial-update.ts +1 -83
  43. package/src/browser/prefetch/cache.ts +6 -45
  44. package/src/browser/prefetch/fetch.ts +7 -0
  45. package/src/browser/prefetch/queue.ts +6 -3
  46. package/src/browser/rango-state.ts +157 -99
  47. package/src/browser/react/Link.tsx +0 -2
  48. package/src/browser/react/NavigationProvider.tsx +2 -1
  49. package/src/browser/react/ScrollRestoration.tsx +10 -6
  50. package/src/browser/react/filter-segment-order.ts +0 -2
  51. package/src/browser/react/index.ts +0 -51
  52. package/src/browser/react/location-state-shared.ts +0 -13
  53. package/src/browser/react/location-state.ts +0 -1
  54. package/src/browser/react/use-action.ts +6 -15
  55. package/src/browser/react/use-handle.ts +0 -5
  56. package/src/browser/react/use-link-status.ts +0 -4
  57. package/src/browser/react/use-navigation.ts +0 -3
  58. package/src/browser/react/use-params.ts +0 -2
  59. package/src/browser/react/use-search-params.ts +0 -5
  60. package/src/browser/react/use-segments.ts +0 -13
  61. package/src/browser/rsc-router.tsx +12 -4
  62. package/src/browser/server-action-bridge.ts +77 -15
  63. package/src/browser/types.ts +7 -2
  64. package/src/browser/validate-redirect-origin.ts +4 -5
  65. package/src/build/route-trie.ts +3 -0
  66. package/src/build/route-types/param-extraction.ts +6 -3
  67. package/src/build/route-types/router-processing.ts +0 -8
  68. package/src/cache/cache-policy.ts +0 -54
  69. package/src/cache/cache-runtime.ts +27 -24
  70. package/src/cache/cache-scope.ts +0 -27
  71. package/src/cache/cache-tag.ts +0 -37
  72. package/src/cache/cf/cf-cache-store.ts +94 -46
  73. package/src/cache/cf/index.ts +0 -24
  74. package/src/cache/document-cache.ts +11 -36
  75. package/src/cache/handle-snapshot.ts +0 -40
  76. package/src/cache/index.ts +0 -27
  77. package/src/cache/memory-segment-store.ts +2 -48
  78. package/src/cache/profile-registry.ts +7 -3
  79. package/src/cache/read-through-swr.ts +41 -11
  80. package/src/cache/segment-codec.ts +0 -16
  81. package/src/cache/types.ts +0 -98
  82. package/src/client.rsc.tsx +1 -22
  83. package/src/client.tsx +14 -38
  84. package/src/component-utils.ts +19 -0
  85. package/src/deps/ssr.ts +0 -1
  86. package/src/handle.ts +28 -18
  87. package/src/handles/MetaTags.tsx +0 -14
  88. package/src/handles/meta.ts +0 -39
  89. package/src/host/cookie-handler.ts +0 -36
  90. package/src/host/errors.ts +0 -24
  91. package/src/host/index.ts +6 -0
  92. package/src/host/pattern-matcher.ts +7 -50
  93. package/src/host/router.ts +1 -65
  94. package/src/host/testing.ts +40 -27
  95. package/src/host/types.ts +6 -2
  96. package/src/href-client.ts +0 -4
  97. package/src/index.rsc.ts +42 -3
  98. package/src/index.ts +31 -1
  99. package/src/internal-debug.ts +2 -4
  100. package/src/loader.rsc.ts +19 -9
  101. package/src/loader.ts +12 -4
  102. package/src/network-error-thrower.tsx +1 -6
  103. package/src/outlet-provider.tsx +1 -5
  104. package/src/prerender/param-hash.ts +10 -11
  105. package/src/prerender/store.ts +23 -30
  106. package/src/prerender.ts +58 -3
  107. package/src/root-error-boundary.tsx +1 -19
  108. package/src/route-content-wrapper.tsx +1 -44
  109. package/src/route-definition/dsl-helpers.ts +7 -19
  110. package/src/route-definition/helpers-types.ts +3 -3
  111. package/src/route-definition/redirect.ts +11 -1
  112. package/src/route-map-builder.ts +0 -16
  113. package/src/router/basename.ts +14 -0
  114. package/src/router/content-negotiation.ts +0 -13
  115. package/src/router/error-handling.ts +12 -16
  116. package/src/router/find-match.ts +4 -30
  117. package/src/router/intercept-resolution.ts +10 -1
  118. package/src/router/lazy-includes.ts +1 -57
  119. package/src/router/loader-resolution.ts +3 -2
  120. package/src/router/logging.ts +0 -6
  121. package/src/router/manifest.ts +1 -25
  122. package/src/router/match-api.ts +0 -20
  123. package/src/router/match-context.ts +0 -22
  124. package/src/router/match-handlers.ts +57 -58
  125. package/src/router/match-middleware/background-revalidation.ts +0 -7
  126. package/src/router/match-middleware/cache-lookup.ts +1 -54
  127. package/src/router/match-middleware/cache-store.ts +0 -31
  128. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  129. package/src/router/match-middleware/segment-resolution.ts +0 -21
  130. package/src/router/match-pipelines.ts +1 -42
  131. package/src/router/match-result.ts +1 -52
  132. package/src/router/metrics.ts +0 -34
  133. package/src/router/middleware-cookies.ts +0 -13
  134. package/src/router/middleware-types.ts +0 -115
  135. package/src/router/middleware.ts +7 -30
  136. package/src/router/navigation-snapshot.ts +0 -51
  137. package/src/router/params-util.ts +23 -0
  138. package/src/router/pattern-matching.ts +1 -33
  139. package/src/router/prerender-match.ts +33 -45
  140. package/src/router/request-classification.ts +1 -38
  141. package/src/router/revalidation.ts +5 -58
  142. package/src/router/router-context.ts +0 -26
  143. package/src/router/router-interfaces.ts +7 -0
  144. package/src/router/router-options.ts +30 -0
  145. package/src/router/segment-resolution/fresh.ts +25 -57
  146. package/src/router/segment-resolution/helpers.ts +34 -0
  147. package/src/router/segment-resolution/loader-cache.ts +10 -13
  148. package/src/router/segment-resolution/revalidation.ts +5 -42
  149. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  150. package/src/router/segment-resolution.ts +4 -1
  151. package/src/router/state-cookie-name.ts +33 -0
  152. package/src/router/telemetry-otel.ts +0 -20
  153. package/src/router/telemetry.ts +96 -19
  154. package/src/router/timeout.ts +0 -20
  155. package/src/router/trie-matching.ts +63 -40
  156. package/src/router/types.ts +1 -63
  157. package/src/router/url-params.ts +0 -5
  158. package/src/router.ts +40 -9
  159. package/src/rsc/handler.ts +14 -2
  160. package/src/rsc/helpers.ts +34 -0
  161. package/src/rsc/origin-guard.ts +0 -12
  162. package/src/rsc/progressive-enhancement.ts +4 -1
  163. package/src/rsc/rsc-rendering.ts +4 -7
  164. package/src/rsc/runtime-warnings.ts +14 -0
  165. package/src/rsc/server-action.ts +30 -28
  166. package/src/rsc/types.ts +2 -1
  167. package/src/runtime-env.ts +18 -0
  168. package/src/search-params.ts +0 -16
  169. package/src/segment-loader-promise.ts +14 -2
  170. package/src/segment-system.tsx +79 -88
  171. package/src/server/cookie-store.ts +52 -1
  172. package/src/server/handle-store.ts +7 -24
  173. package/src/server/loader-registry.ts +5 -24
  174. package/src/server/request-context.ts +74 -77
  175. package/src/ssr/index.tsx +14 -14
  176. package/src/static-handler.ts +10 -13
  177. package/src/testing/cache-status.ts +119 -0
  178. package/src/testing/collect-handle.ts +40 -0
  179. package/src/testing/dispatch.ts +581 -0
  180. package/src/testing/dom.entry.ts +22 -0
  181. package/src/testing/e2e/fixture.ts +188 -0
  182. package/src/testing/e2e/index.ts +127 -0
  183. package/src/testing/e2e/matchers.ts +35 -0
  184. package/src/testing/e2e/page-helpers.ts +272 -0
  185. package/src/testing/e2e/parity.ts +387 -0
  186. package/src/testing/e2e/server.ts +195 -0
  187. package/src/testing/flight-matchers.ts +97 -0
  188. package/src/testing/flight-normalize.ts +11 -0
  189. package/src/testing/flight-runtime.d.ts +57 -0
  190. package/src/testing/flight-tree.ts +682 -0
  191. package/src/testing/flight.entry.ts +52 -0
  192. package/src/testing/flight.ts +186 -0
  193. package/src/testing/generated-routes.ts +183 -0
  194. package/src/testing/index.ts +98 -0
  195. package/src/testing/internal/context.ts +348 -0
  196. package/src/testing/internal/flight-client-globals.ts +30 -0
  197. package/src/testing/internal/seed-vars.ts +54 -0
  198. package/src/testing/render-handler.ts +311 -0
  199. package/src/testing/render-route.tsx +504 -0
  200. package/src/testing/run-loader.ts +378 -0
  201. package/src/testing/run-middleware.ts +205 -0
  202. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  203. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  204. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  205. package/src/testing/vitest-stubs/version.ts +5 -0
  206. package/src/testing/vitest.ts +305 -0
  207. package/src/theme/ThemeProvider.tsx +0 -52
  208. package/src/theme/ThemeScript.tsx +0 -6
  209. package/src/theme/constants.ts +0 -12
  210. package/src/theme/index.ts +0 -7
  211. package/src/theme/theme-context.ts +1 -5
  212. package/src/theme/theme-script.ts +0 -14
  213. package/src/theme/use-theme.ts +0 -3
  214. package/src/types/boundaries.ts +0 -35
  215. package/src/types/error-types.ts +25 -89
  216. package/src/types/global-namespace.ts +15 -15
  217. package/src/types/handler-context.ts +16 -13
  218. package/src/types/index.ts +0 -10
  219. package/src/types/request-scope.ts +0 -19
  220. package/src/types/route-config.ts +6 -50
  221. package/src/types/route-entry.ts +0 -6
  222. package/src/types/segments.ts +0 -13
  223. package/src/urls/include-helper.ts +0 -4
  224. package/src/urls/index.ts +0 -6
  225. package/src/urls/path-helper-types.ts +2 -2
  226. package/src/urls/path-helper.ts +0 -54
  227. package/src/urls/urls-function.ts +0 -13
  228. package/src/use-loader.tsx +0 -186
  229. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  230. package/src/vite/discovery/discover-routers.ts +6 -7
  231. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  232. package/src/vite/plugin-types.ts +3 -1
  233. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  234. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  235. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  236. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  237. package/src/vite/plugins/expose-action-id.ts +2 -73
  238. package/src/vite/plugins/expose-id-utils.ts +0 -55
  239. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  240. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  241. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  242. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  243. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  244. package/src/vite/plugins/performance-tracks.ts +0 -3
  245. package/src/vite/plugins/use-cache-transform.ts +0 -36
  246. package/src/vite/plugins/version-injector.ts +0 -20
  247. package/src/vite/plugins/version-plugin.ts +1 -49
  248. package/src/vite/plugins/virtual-entries.ts +0 -15
  249. package/src/vite/rango.ts +1 -108
  250. package/src/vite/router-discovery.ts +2 -1
  251. package/src/vite/utils/ast-handler-extract.ts +0 -16
  252. package/src/vite/utils/bundle-analysis.ts +6 -13
  253. package/src/vite/utils/client-chunks.ts +0 -6
  254. package/src/vite/utils/forward-user-plugins.ts +0 -22
  255. package/src/vite/utils/manifest-utils.ts +0 -4
  256. package/src/vite/utils/package-resolution.ts +1 -73
  257. package/src/vite/utils/prerender-utils.ts +0 -35
  258. package/src/vite/utils/shared-utils.ts +3 -35
  259. package/src/browser/react/use-client-cache.ts +0 -58
  260. package/src/browser/shallow.ts +0 -40
@@ -453,6 +453,8 @@ export function createRSCHandler<
453
453
  cacheProfiles: router.cacheProfiles,
454
454
  executionContext: executionCtx,
455
455
  themeConfig: router.themeConfig,
456
+ stateCookieName: router.resolvedStateCookieName,
457
+ version,
456
458
  });
457
459
  if (earlyMetricsStore) {
458
460
  requestContext._debugPerformance = true;
@@ -1015,10 +1017,19 @@ export function createRSCHandler<
1015
1017
  } catch (error) {
1016
1018
  // Check if middleware/handler returned Response
1017
1019
  if (error instanceof Response) {
1020
+ // An action revalidation render is delivered to the client over the
1021
+ // same Flight-parsing path as a partial navigation, so a Response
1022
+ // thrown during it must be converted exactly like a partial one
1023
+ // (raw 200 -> hard-nav hint, 3xx -> Flight redirect). Without this,
1024
+ // the no-middleware path returns the raw Response (the with-middleware
1025
+ // path is already covered by the isPartial || actionContinuation
1026
+ // guard below).
1027
+ const treatAsPartial = isPartial || actionContinuation != null;
1028
+
1018
1029
  // During partial (client-side navigation), a 200 Response from a handler
1019
1030
  // means the route serves raw content (JSON, text, etc.), not JSX.
1020
1031
  // Signal the browser to hard-navigate so it renders the raw response.
1021
- if (isPartial && error.status === 200) {
1032
+ if (treatAsPartial && error.status === 200) {
1022
1033
  console.warn(
1023
1034
  `[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
1024
1035
  `Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
@@ -1032,7 +1043,7 @@ export function createRSCHandler<
1032
1043
  });
1033
1044
  }
1034
1045
 
1035
- if (isPartial) {
1046
+ if (treatAsPartial) {
1036
1047
  const intercepted = interceptRedirectForPartial(
1037
1048
  error,
1038
1049
  createRedirectFlightResponse,
@@ -1079,6 +1090,7 @@ export function createRSCHandler<
1079
1090
  rootLayout: router.rootLayout,
1080
1091
  handles: handleStore.stream(),
1081
1092
  version,
1093
+ stateCookieName: router.resolvedStateCookieName,
1082
1094
  themeConfig: router.themeConfig,
1083
1095
  warmupEnabled: router.warmupEnabled,
1084
1096
  initialTheme: requireRequestContext().theme,
@@ -12,6 +12,25 @@ import type { RequestContext } from "../server/request-context.js";
12
12
  import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
13
13
  import { isRedirectResponse } from "../response-utils.js";
14
14
  import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
15
+ import { formatCacheSignalHeader } from "../router/telemetry.js";
16
+ import type { RscPayload } from "./types.js";
17
+
18
+ /**
19
+ * DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
20
+ * match/matchPartial populate ctx._cacheSignal. Emit it as the X-Rango-Cache
21
+ * header. When the gate is off, ctx._cacheSignal is undefined and NOTHING is
22
+ * attached — output is byte-identical to the default. Header mutation failures
23
+ * are swallowed so immutable Response headers (e.g. protocol-switch) are safe.
24
+ */
25
+ function applyCacheSignalHeader(target: Headers, ctx: RequestContext): void {
26
+ const signal = ctx._cacheSignal;
27
+ if (!signal || signal.length === 0) return;
28
+ try {
29
+ target.set("X-Rango-Cache", formatCacheSignalHeader(signal));
30
+ } catch {
31
+ // Headers immutable — skip.
32
+ }
33
+ }
15
34
 
16
35
  /**
17
36
  * Copy stub headers from the request context onto a target Headers instance:
@@ -85,6 +104,7 @@ export function createResponseWithMergedHeaders(
85
104
  const mergedHeaders = new Headers(init.headers);
86
105
  applyStubHeaders(mergedHeaders, ctx.res.headers);
87
106
  ctx.res.headers.delete("set-cookie");
107
+ applyCacheSignalHeader(mergedHeaders, ctx);
88
108
 
89
109
  // ctx.res.status overrides init.status when explicitly set (e.g. 404 for
90
110
  // notFound, 500 for error). Default ctx.res.status is 200.
@@ -166,6 +186,20 @@ export function interceptRedirectForPartial(
166
186
  return intercepted;
167
187
  }
168
188
 
189
+ /**
190
+ * Attach location state set during a request to a payload's metadata.
191
+ * No-op if no location state was set. Callers must ensure payload.metadata
192
+ * is populated (the non-null assertion holds for the partial/action payloads
193
+ * that reach this helper).
194
+ */
195
+ export function attachLocationStateIfPresent(payload: RscPayload): void {
196
+ const locationState = getLocationState();
197
+ if (locationState) {
198
+ payload.metadata!.locationState =
199
+ resolveLocationStateEntries(locationState);
200
+ }
201
+ }
202
+
169
203
  /**
170
204
  * Only cache successful responses. Non-200 statuses (errors, redirects) are
171
205
  * not cached -- notFound() produces 500 in response routes, and explicit
@@ -69,11 +69,8 @@ export type OriginCheckConfig<TEnv = any> =
69
69
  * Returns true to allow, false to reject.
70
70
  */
71
71
  export function defaultOriginCheck(request: Request, url: URL): boolean {
72
- // 1. Read Origin header (present on all cross-origin requests and
73
- // same-origin POST/PUT/PATCH/DELETE in modern browsers)
74
72
  let requestOrigin = request.headers.get("origin");
75
73
 
76
- // 2. Fallback to Referer if Origin is absent (some proxies strip it)
77
74
  if (!requestOrigin) {
78
75
  const referer = request.headers.get("referer");
79
76
  if (referer) {
@@ -85,22 +82,13 @@ export function defaultOriginCheck(request: Request, url: URL): boolean {
85
82
  }
86
83
  }
87
84
 
88
- // 3. No Origin or Referer — allow (can't be browser-initiated CSRF)
89
85
  if (!requestOrigin) return true;
90
86
 
91
- // "null" origin comes from privacy-sensitive contexts (data: URLs,
92
- // sandboxed iframes, cross-origin redirects). Reject it.
93
87
  if (requestOrigin === "null") return false;
94
88
 
95
- // 4. Determine expected host from Host header or URL.
96
- // X-Forwarded-Host/Proto are NOT used — they are client-controllable
97
- // unless a trusted proxy strips them. On standard deployments (Cloudflare
98
- // Workers, Node behind nginx/caddy) the Host header is already correct.
99
- // For non-standard setups, use the custom function escape hatch.
100
89
  const expectedHost = request.headers.get("host") || url.host;
101
90
  const expectedProtocol = url.protocol;
102
91
 
103
- // 5. Build expected origin and compare (case-insensitive)
104
92
  const expectedOrigin = `${expectedProtocol}//${expectedHost}`;
105
93
 
106
94
  return requestOrigin.toLowerCase() === expectedOrigin.toLowerCase();
@@ -254,11 +254,11 @@ export async function handleProgressiveEnhancement<TEnv>(
254
254
  rootLayout: ctx.router.rootLayout,
255
255
  handles: handleStore.stream(),
256
256
  version: ctx.version,
257
+ stateCookieName: ctx.router.resolvedStateCookieName,
257
258
  themeConfig: ctx.router.themeConfig,
258
259
  warmupEnabled: ctx.router.warmupEnabled,
259
260
  initialTheme: requireRequestContext().theme,
260
261
  },
261
- formState: actionResult,
262
262
  };
263
263
 
264
264
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
@@ -276,6 +276,8 @@ export async function handleProgressiveEnhancement<TEnv>(
276
276
  url,
277
277
  undefined,
278
278
  );
279
+ // reactFormState carries the useActionState payload via the SSR-option path
280
+ // (renderToReadableStream({ formState })); it does NOT travel on RscPayload.
279
281
  const htmlStream = await ssrModule.renderHTML(rscStream, {
280
282
  formState: reactFormState,
281
283
  nonce,
@@ -362,6 +364,7 @@ async function renderPeErrorBoundary<TEnv>(
362
364
  rootLayout: ctx.router.rootLayout,
363
365
  handles: handleStore.stream(),
364
366
  version: ctx.version,
367
+ stateCookieName: ctx.router.resolvedStateCookieName,
365
368
  themeConfig: ctx.router.themeConfig,
366
369
  warmupEnabled: ctx.router.warmupEnabled,
367
370
  initialTheme: requireRequestContext().theme,
@@ -9,9 +9,7 @@
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
14
  import { getSSRSetup, isRscRequest } from "./ssr-setup.js";
17
15
  import type { RscPayload } from "./types.js";
@@ -19,6 +17,7 @@ import type { MatchResult } from "../types.js";
19
17
  import {
20
18
  createResponseWithMergedHeaders,
21
19
  createSimpleRedirectResponse,
20
+ attachLocationStateIfPresent,
22
21
  } from "./helpers.js";
23
22
  import type { HandlerContext } from "./handler-context.js";
24
23
 
@@ -53,6 +52,7 @@ export async function handleRscRendering<TEnv>(
53
52
  handles: handleStore.stream(),
54
53
  version: ctx.version,
55
54
  prefetchCacheTTL: ctx.router.prefetchCacheTTL,
55
+ stateCookieName: ctx.router.resolvedStateCookieName,
56
56
  themeConfig: ctx.router.themeConfig,
57
57
  initialTheme: reqCtx.theme,
58
58
  },
@@ -99,6 +99,7 @@ export async function handleRscRendering<TEnv>(
99
99
  handles: handleStore.stream(),
100
100
  version: ctx.version,
101
101
  prefetchCacheTTL: ctx.router.prefetchCacheTTL,
102
+ stateCookieName: ctx.router.resolvedStateCookieName,
102
103
  },
103
104
  };
104
105
  }
@@ -153,11 +154,7 @@ export async function handleRscRendering<TEnv>(
153
154
  // SSR (full page) requests ignore location state since there's no history.state
154
155
  // to write to on a fresh page load.
155
156
  if (isPartial && payload.metadata) {
156
- const locationState = getLocationState();
157
- if (locationState) {
158
- payload.metadata.locationState =
159
- resolveLocationStateEntries(locationState);
160
- }
157
+ attachLocationStateIfPresent(payload);
161
158
  }
162
159
 
163
160
  const metricsStore = reqCtx._metricsStore;
@@ -39,3 +39,17 @@ export function warnNonRedirectPeResponse(): void {
39
39
  `ignored — the page will re-render at the current URL instead.`,
40
40
  );
41
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,9 +18,7 @@
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 {
@@ -28,21 +26,11 @@ import {
28
26
  createResponseWithMergedHeaders,
29
27
  createSimpleRedirectResponse,
30
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
@@ -109,7 +97,7 @@ export async function executeServerAction<TEnv>(
109
97
 
110
98
  try {
111
99
  loadedAction = await ctx.loadServerAction(actionId);
112
- const data = await loadedAction!.apply(null, args);
100
+ let data = await loadedAction!.apply(null, args);
113
101
 
114
102
  // Intercept redirect Responses: serializing one as the action returnValue
115
103
  // would fail, and revalidation would run needlessly.
@@ -119,6 +107,14 @@ export async function executeServerAction<TEnv>(
119
107
  ctx.createRedirectFlightResponse,
120
108
  );
121
109
  if (intercepted) return intercepted;
110
+
111
+ // Non-redirect Response returned (not thrown): a raw Response cannot be
112
+ // serialized into Flight. Discard it and re-render — mirroring the PE
113
+ // path (progressive-enhancement.ts) so JS and no-JS behave identically.
114
+ if (process.env.NODE_ENV !== "production") {
115
+ warnNonRedirectActionResponse(actionId);
116
+ }
117
+ data = undefined;
122
118
  }
123
119
 
124
120
  returnValue = { ok: true, data };
@@ -224,18 +220,21 @@ export async function executeServerAction<TEnv>(
224
220
  }
225
221
 
226
222
  // Build continuation for the revalidation phase
227
- const resolvedActionId =
228
- (loadedAction as { $id?: string; $$id?: string } | undefined)?.$id ??
229
- (loadedAction as { $$id?: string } | undefined)?.$$id ??
230
- actionId;
223
+ const actionMeta = loadedAction as
224
+ | { $id?: string; $$id?: string }
225
+ | undefined;
226
+ const resolvedActionId = actionMeta?.$id ?? actionMeta?.$$id ?? actionId;
231
227
 
232
228
  return {
233
229
  returnValue,
234
230
  actionStatus,
235
231
  temporaryReferences,
236
232
  actionContext: {
233
+ // Defensive copy of the already-parsed url (avoids re-parsing
234
+ // request.url). actionUrl is persisted into the continuation and later
235
+ // flows into matchPartial, so it must not alias the handler's live url.
237
236
  actionId: resolvedActionId,
238
- actionUrl: new URL(request.url),
237
+ actionUrl: new URL(url),
239
238
  actionResult: returnValue.data,
240
239
  formData: actionFormData,
241
240
  },
@@ -274,8 +273,8 @@ export async function revalidateAfterAction<TEnv>(
274
273
  );
275
274
 
276
275
  if (!matchResult) {
277
- // matchPartial returns null when the route is a redirect or the request
278
- // is missing required headers (previousUrl). Check for redirect first.
276
+ // matchPartial returns null when the route is a redirect or no previous-URL
277
+ // context could be resolved. Check for redirect first.
279
278
  const fullMatch = await ctx.router.match(request, { env });
280
279
  setRequestContextParams(fullMatch.params, fullMatch.routeName);
281
280
 
@@ -286,14 +285,17 @@ export async function revalidateAfterAction<TEnv>(
286
285
  return createSimpleRedirectResponse(fullMatch.redirect);
287
286
  }
288
287
 
289
- // Non-redirect: this branch is only reachable when the action request
290
- // is missing the X-RSC-Router-Client-Path header (defensive). The
291
- // client requires isPartial for action responses, so producing a full
292
- // payload here would be rejected. Return 500 instead.
288
+ // Non-redirect: this branch is only reachable when no previous URL could
289
+ // be resolved (neither X-RSC-Router-Client-Path nor a usable Referer), or
290
+ // the previous URL was unparseable (defensive). The client requires
291
+ // isPartial for action responses, so producing a full payload here would
292
+ // be rejected. Return 500 instead.
293
293
  throw new Error(
294
294
  `[RSC] matchPartial returned null for a non-redirect route ` +
295
295
  `during action revalidation (${url.pathname}). This indicates ` +
296
- `a malformed action request (missing X-RSC-Router-Client-Path header).`,
296
+ `a malformed action request: no previous-URL context could be ` +
297
+ `resolved (neither X-RSC-Router-Client-Path nor a usable Referer), ` +
298
+ `or the previous URL was unparseable.`,
297
299
  );
298
300
  }
299
301
 
@@ -319,7 +321,7 @@ export async function revalidateAfterAction<TEnv>(
319
321
  returnValue,
320
322
  };
321
323
 
322
- attachLocationState(payload);
324
+ attachLocationStateIfPresent(payload);
323
325
 
324
326
  const renderStart = performance.now();
325
327
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
package/src/rsc/types.ts CHANGED
@@ -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) */
@@ -57,7 +59,6 @@ export interface RscPayload {
57
59
  locationState?: Record<string, unknown>;
58
60
  };
59
61
  returnValue?: { ok: boolean; data: unknown };
60
- formState?: unknown;
61
62
  }
62
63
 
63
64
  /**
@@ -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
 
@@ -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
  }