@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c

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 (189) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +172 -50
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +1160 -508
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +17 -16
  8. package/skills/breadcrumbs/SKILL.md +252 -0
  9. package/skills/cache-guide/SKILL.md +32 -0
  10. package/skills/caching/SKILL.md +49 -8
  11. package/skills/document-cache/SKILL.md +2 -2
  12. package/skills/handler-use/SKILL.md +362 -0
  13. package/skills/hooks/SKILL.md +61 -51
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +20 -0
  16. package/skills/layout/SKILL.md +22 -0
  17. package/skills/links/SKILL.md +91 -17
  18. package/skills/loader/SKILL.md +107 -24
  19. package/skills/middleware/SKILL.md +34 -3
  20. package/skills/migrate-nextjs/SKILL.md +560 -0
  21. package/skills/migrate-react-router/SKILL.md +765 -0
  22. package/skills/parallel/SKILL.md +185 -0
  23. package/skills/prerender/SKILL.md +112 -70
  24. package/skills/rango/SKILL.md +24 -23
  25. package/skills/response-routes/SKILL.md +8 -0
  26. package/skills/route/SKILL.md +58 -4
  27. package/skills/router-setup/SKILL.md +95 -5
  28. package/skills/streams-and-websockets/SKILL.md +283 -0
  29. package/skills/typesafety/SKILL.md +38 -24
  30. package/src/__internal.ts +92 -0
  31. package/src/browser/app-shell.ts +52 -0
  32. package/src/browser/app-version.ts +14 -0
  33. package/src/browser/event-controller.ts +5 -0
  34. package/src/browser/link-interceptor.ts +4 -0
  35. package/src/browser/navigation-bridge.ts +175 -17
  36. package/src/browser/navigation-client.ts +177 -44
  37. package/src/browser/navigation-store.ts +68 -9
  38. package/src/browser/navigation-transaction.ts +11 -9
  39. package/src/browser/partial-update.ts +113 -17
  40. package/src/browser/prefetch/cache.ts +275 -28
  41. package/src/browser/prefetch/fetch.ts +191 -46
  42. package/src/browser/prefetch/policy.ts +6 -0
  43. package/src/browser/prefetch/queue.ts +123 -20
  44. package/src/browser/prefetch/resource-ready.ts +77 -0
  45. package/src/browser/rango-state.ts +53 -13
  46. package/src/browser/react/Link.tsx +98 -14
  47. package/src/browser/react/NavigationProvider.tsx +89 -14
  48. package/src/browser/react/context.ts +7 -2
  49. package/src/browser/react/use-handle.ts +9 -58
  50. package/src/browser/react/use-navigation.ts +22 -2
  51. package/src/browser/react/use-params.ts +11 -1
  52. package/src/browser/react/use-router.ts +29 -9
  53. package/src/browser/rsc-router.tsx +177 -66
  54. package/src/browser/scroll-restoration.ts +41 -42
  55. package/src/browser/segment-reconciler.ts +36 -9
  56. package/src/browser/server-action-bridge.ts +8 -6
  57. package/src/browser/types.ts +73 -5
  58. package/src/build/generate-manifest.ts +6 -6
  59. package/src/build/generate-route-types.ts +3 -0
  60. package/src/build/route-trie.ts +67 -25
  61. package/src/build/route-types/include-resolution.ts +8 -1
  62. package/src/build/route-types/router-processing.ts +223 -74
  63. package/src/build/route-types/scan-filter.ts +8 -1
  64. package/src/cache/cache-runtime.ts +15 -11
  65. package/src/cache/cache-scope.ts +48 -7
  66. package/src/cache/cf/cf-cache-store.ts +455 -15
  67. package/src/cache/cf/index.ts +5 -1
  68. package/src/cache/document-cache.ts +17 -7
  69. package/src/cache/index.ts +1 -0
  70. package/src/cache/taint.ts +55 -0
  71. package/src/client.rsc.tsx +2 -1
  72. package/src/client.tsx +85 -276
  73. package/src/context-var.ts +72 -2
  74. package/src/debug.ts +2 -2
  75. package/src/handle.ts +40 -0
  76. package/src/handles/breadcrumbs.ts +66 -0
  77. package/src/handles/index.ts +1 -0
  78. package/src/host/index.ts +0 -3
  79. package/src/index.rsc.ts +9 -36
  80. package/src/index.ts +79 -70
  81. package/src/outlet-context.ts +1 -1
  82. package/src/prerender/store.ts +57 -15
  83. package/src/prerender.ts +138 -77
  84. package/src/response-utils.ts +28 -0
  85. package/src/reverse.ts +27 -2
  86. package/src/route-definition/dsl-helpers.ts +240 -40
  87. package/src/route-definition/helpers-types.ts +67 -19
  88. package/src/route-definition/index.ts +3 -3
  89. package/src/route-definition/redirect.ts +11 -3
  90. package/src/route-definition/resolve-handler-use.ts +155 -0
  91. package/src/route-map-builder.ts +7 -1
  92. package/src/route-types.ts +18 -0
  93. package/src/router/content-negotiation.ts +100 -1
  94. package/src/router/find-match.ts +4 -2
  95. package/src/router/handler-context.ts +129 -26
  96. package/src/router/intercept-resolution.ts +11 -4
  97. package/src/router/lazy-includes.ts +10 -7
  98. package/src/router/loader-resolution.ts +160 -22
  99. package/src/router/logging.ts +5 -2
  100. package/src/router/manifest.ts +31 -16
  101. package/src/router/match-api.ts +128 -193
  102. package/src/router/match-middleware/background-revalidation.ts +30 -2
  103. package/src/router/match-middleware/cache-lookup.ts +94 -17
  104. package/src/router/match-middleware/cache-store.ts +53 -10
  105. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  106. package/src/router/match-middleware/segment-resolution.ts +61 -5
  107. package/src/router/match-result.ts +103 -18
  108. package/src/router/metrics.ts +238 -13
  109. package/src/router/middleware-types.ts +48 -27
  110. package/src/router/middleware.ts +201 -86
  111. package/src/router/navigation-snapshot.ts +182 -0
  112. package/src/router/pattern-matching.ts +77 -11
  113. package/src/router/prerender-match.ts +114 -10
  114. package/src/router/preview-match.ts +30 -102
  115. package/src/router/request-classification.ts +310 -0
  116. package/src/router/revalidation.ts +27 -7
  117. package/src/router/route-snapshot.ts +245 -0
  118. package/src/router/router-context.ts +6 -1
  119. package/src/router/router-interfaces.ts +50 -5
  120. package/src/router/router-options.ts +50 -19
  121. package/src/router/segment-resolution/fresh.ts +215 -19
  122. package/src/router/segment-resolution/helpers.ts +30 -25
  123. package/src/router/segment-resolution/loader-cache.ts +1 -0
  124. package/src/router/segment-resolution/revalidation.ts +454 -301
  125. package/src/router/segment-wrappers.ts +2 -0
  126. package/src/router/trie-matching.ts +30 -6
  127. package/src/router/types.ts +1 -0
  128. package/src/router/url-params.ts +49 -0
  129. package/src/router.ts +89 -17
  130. package/src/rsc/handler.ts +563 -364
  131. package/src/rsc/helpers.ts +69 -41
  132. package/src/rsc/index.ts +0 -20
  133. package/src/rsc/loader-fetch.ts +23 -3
  134. package/src/rsc/manifest-init.ts +5 -1
  135. package/src/rsc/progressive-enhancement.ts +37 -10
  136. package/src/rsc/response-route-handler.ts +14 -1
  137. package/src/rsc/rsc-rendering.ts +47 -44
  138. package/src/rsc/server-action.ts +24 -10
  139. package/src/rsc/ssr-setup.ts +128 -0
  140. package/src/rsc/types.ts +11 -1
  141. package/src/search-params.ts +16 -13
  142. package/src/segment-content-promise.ts +67 -0
  143. package/src/segment-loader-promise.ts +122 -0
  144. package/src/segment-system.tsx +109 -23
  145. package/src/server/context.ts +174 -19
  146. package/src/server/handle-store.ts +19 -0
  147. package/src/server/loader-registry.ts +9 -8
  148. package/src/server/request-context.ts +218 -65
  149. package/src/server.ts +6 -0
  150. package/src/ssr/index.tsx +4 -0
  151. package/src/static-handler.ts +18 -6
  152. package/src/theme/index.ts +4 -13
  153. package/src/types/cache-types.ts +4 -4
  154. package/src/types/handler-context.ts +140 -72
  155. package/src/types/loader-types.ts +41 -15
  156. package/src/types/request-scope.ts +126 -0
  157. package/src/types/route-config.ts +17 -8
  158. package/src/types/route-entry.ts +19 -1
  159. package/src/types/segments.ts +2 -5
  160. package/src/urls/include-helper.ts +24 -14
  161. package/src/urls/path-helper-types.ts +39 -6
  162. package/src/urls/path-helper.ts +48 -13
  163. package/src/urls/pattern-types.ts +12 -0
  164. package/src/urls/response-types.ts +18 -16
  165. package/src/use-loader.tsx +77 -5
  166. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  167. package/src/vite/discovery/discover-routers.ts +7 -4
  168. package/src/vite/discovery/prerender-collection.ts +162 -88
  169. package/src/vite/discovery/state.ts +17 -13
  170. package/src/vite/index.ts +8 -3
  171. package/src/vite/plugin-types.ts +51 -79
  172. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  173. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  174. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  175. package/src/vite/plugins/expose-action-id.ts +1 -3
  176. package/src/vite/plugins/expose-id-utils.ts +12 -0
  177. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  178. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  179. package/src/vite/plugins/performance-tracks.ts +88 -0
  180. package/src/vite/plugins/refresh-cmd.ts +127 -0
  181. package/src/vite/plugins/version-plugin.ts +13 -1
  182. package/src/vite/rango.ts +190 -217
  183. package/src/vite/router-discovery.ts +241 -45
  184. package/src/vite/utils/banner.ts +4 -4
  185. package/src/vite/utils/package-resolution.ts +34 -1
  186. package/src/vite/utils/prerender-utils.ts +97 -5
  187. package/src/vite/utils/shared-utils.ts +3 -2
  188. package/skills/testing/SKILL.md +0 -226
  189. package/src/route-definition/route-function.ts +0 -119
@@ -8,9 +8,49 @@ import {
8
8
  _getRequestContext,
9
9
  getLocationState,
10
10
  } from "../server/request-context.js";
11
+ import type { RequestContext } from "../server/request-context.js";
11
12
  import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
12
13
  import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
13
14
 
15
+ /**
16
+ * Copy stub headers from the request context onto a target Headers instance:
17
+ * append Set-Cookie entries, set everything else only if absent. Header
18
+ * mutation failures are swallowed so the same logic works against Response
19
+ * headers that may be immutable (e.g. Cloudflare protocol-switch responses).
20
+ */
21
+ function applyStubHeaders(target: Headers, stub: Headers): void {
22
+ stub.forEach((value, name) => {
23
+ try {
24
+ if (name.toLowerCase() === "set-cookie") {
25
+ target.append(name, value);
26
+ } else if (!target.has(name)) {
27
+ target.set(name, value);
28
+ }
29
+ } catch {
30
+ // Headers immutable — skip.
31
+ }
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Drain ctx._onResponseCallbacks onto a response. Swapping the array before
37
+ * iteration prevents re-entrant registrations from double-firing and matches
38
+ * the contract that each callback runs at most once per request.
39
+ */
40
+ function drainOnResponseCallbacks(
41
+ ctx: RequestContext,
42
+ response: Response,
43
+ ): Response {
44
+ const callbacks = ctx._onResponseCallbacks;
45
+ if (callbacks.length === 0) return response;
46
+ ctx._onResponseCallbacks = [];
47
+ let result = response;
48
+ for (const callback of callbacks) {
49
+ result = callback(result) ?? result;
50
+ }
51
+ return result;
52
+ }
53
+
14
54
  /**
15
55
  * Check if a request body has content to decode
16
56
  */
@@ -39,40 +79,23 @@ export function createResponseWithMergedHeaders(
39
79
  return new Response(body, init);
40
80
  }
41
81
 
42
- // Merge headers from stub response into the new response.
43
- // Delete Set-Cookie from the stub after consuming so that downstream
44
- // merge points (e.g. executeMiddleware) do not duplicate them.
82
+ // Delete Set-Cookie from the stub after consuming so downstream merge
83
+ // points (e.g. executeMiddleware) don't duplicate them.
45
84
  const mergedHeaders = new Headers(init.headers);
46
- ctx.res.headers.forEach((value, name) => {
47
- if (name.toLowerCase() === "set-cookie") {
48
- mergedHeaders.append(name, value);
49
- } else if (!mergedHeaders.has(name)) {
50
- // Only set if not already present in init.headers
51
- mergedHeaders.set(name, value);
52
- }
53
- });
85
+ applyStubHeaders(mergedHeaders, ctx.res.headers);
54
86
  ctx.res.headers.delete("set-cookie");
55
87
 
56
- // Use ctx.res.status if it was set (e.g., 404 for notFound, 500 for error)
57
- // Otherwise use the status from init
88
+ // ctx.res.status overrides init.status when explicitly set (e.g. 404 for
89
+ // notFound, 500 for error). Default ctx.res.status is 200.
58
90
  const status = ctx.res.status !== 200 ? ctx.res.status : init.status;
59
91
 
60
- let response = new Response(body, {
92
+ const response = new Response(body, {
61
93
  ...init,
62
94
  status,
63
95
  headers: mergedHeaders,
64
96
  });
65
97
 
66
- // Run onResponse callbacks - each can inspect/modify the response.
67
- // Drain the array so that downstream callers (e.g. finalizeResponse)
68
- // do not re-execute the same callbacks on this response.
69
- const callbacks = ctx._onResponseCallbacks;
70
- ctx._onResponseCallbacks = [];
71
- for (const callback of callbacks) {
72
- response = callback(response) ?? response;
73
- }
74
-
75
- return response;
98
+ return drainOnResponseCallbacks(ctx, response);
76
99
  }
77
100
 
78
101
  /**
@@ -175,24 +198,29 @@ export function buildRouteMiddlewareEntries<TEnv>(
175
198
  }
176
199
 
177
200
  /**
178
- * Run onResponse callbacks on an existing Response.
179
- *
180
- * Used for code paths that bypass createResponseWithMergedHeaders(), such as
181
- * middleware short-circuits where the Response is already constructed but
182
- * ctx.onResponse() callbacks still need to fire.
201
+ * Merge stub headers from the request context onto an existing Response in
202
+ * place, then drain onResponse callbacks. Used when a Response cannot flow
203
+ * through `new Response()` status 101 is outside the constructor's
204
+ * 200-599 range, and the Cloudflare-specific `webSocket` property would be
205
+ * lost on reconstruction.
183
206
  */
184
- export function finalizeResponse(response: Response): Response {
207
+ export function mergeStubHeadersAndFinalize(response: Response): Response {
185
208
  const ctx = _getRequestContext();
186
- if (!ctx || ctx._onResponseCallbacks.length === 0) {
187
- return response;
188
- }
209
+ if (!ctx) return response;
189
210
 
190
- // Drain the array so callbacks run at most once per request.
191
- const callbacks = ctx._onResponseCallbacks;
192
- ctx._onResponseCallbacks = [];
193
- let result = response;
194
- for (const callback of callbacks) {
195
- result = callback(result) ?? result;
196
- }
197
- return result;
211
+ applyStubHeaders(response.headers, ctx.res.headers);
212
+ ctx.res.headers.delete("set-cookie");
213
+
214
+ return drainOnResponseCallbacks(ctx, response);
215
+ }
216
+
217
+ /**
218
+ * Run onResponse callbacks on an existing Response. Used by code paths that
219
+ * bypass createResponseWithMergedHeaders (e.g. middleware short-circuits)
220
+ * but still need ctx.onResponse() callbacks to fire.
221
+ */
222
+ export function finalizeResponse(response: Response): Response {
223
+ const ctx = _getRequestContext();
224
+ if (!ctx) return response;
225
+ return drainOnResponseCallbacks(ctx, response);
198
226
  }
package/src/rsc/index.ts CHANGED
@@ -29,28 +29,8 @@ export type {
29
29
  NonceProvider,
30
30
  } from "./types.js";
31
31
 
32
- // Re-export HandleStore types for consumers who need custom handling
33
- export {
34
- createHandleStore,
35
- type HandleStore,
36
- type HandleData,
37
- } from "../server/handle-store.js";
38
-
39
32
  // Re-export request context utilities for server-side access to env/request/params
40
33
  export {
41
34
  getRequestContext,
42
35
  requireRequestContext,
43
- setRequestContextParams,
44
36
  } from "../server/request-context.js";
45
-
46
- // Re-export cache store types and implementations
47
- export type {
48
- SegmentCacheStore,
49
- CachedEntryData,
50
- CachedEntryResult,
51
- SegmentCacheProvider,
52
- SegmentHandleData,
53
- } from "../cache/types.js";
54
-
55
- export { MemorySegmentCacheStore } from "../cache/memory-segment-store.js";
56
- export { CFCacheStore, type CFCacheStoreOptions } from "../cache/cf/index.js";
@@ -168,8 +168,19 @@ export async function handleLoaderFetch<TEnv>(
168
168
  loaderResult: unknown;
169
169
  }
170
170
  const loaderPayload: LoaderPayload = { loaderResult: result };
171
- const rscStream =
172
- ctx.renderToReadableStream<LoaderPayload>(loaderPayload);
171
+ const rscStream = ctx.renderToReadableStream<LoaderPayload>(
172
+ loaderPayload,
173
+ {
174
+ onError: (error: unknown) => {
175
+ ctx.callOnError(error, "rendering", {
176
+ request,
177
+ url,
178
+ env,
179
+ loaderName: loaderId,
180
+ });
181
+ },
182
+ },
183
+ );
173
184
 
174
185
  return createResponseWithMergedHeaders(rscStream, {
175
186
  headers: { "content-type": "text/x-component;charset=utf-8" },
@@ -199,7 +210,16 @@ export async function handleLoaderFetch<TEnv>(
199
210
  name: err.name,
200
211
  },
201
212
  };
202
- const rscStream = ctx.renderToReadableStream(errorPayload);
213
+ const rscStream = ctx.renderToReadableStream(errorPayload, {
214
+ onError: (error: unknown) => {
215
+ ctx.callOnError(error, "rendering", {
216
+ request,
217
+ url,
218
+ env,
219
+ loaderName: loaderId,
220
+ });
221
+ },
222
+ });
203
223
 
204
224
  return createResponseWithMergedHeaders(rscStream, {
205
225
  status: 500,
@@ -31,7 +31,11 @@ export async function buildRouterTrieFromUrlpatterns(
31
31
  ): Promise<void> {
32
32
  const { generateManifestFull } =
33
33
  await import("../build/generate-manifest.js");
34
- const generated = generateManifestFull(router.urlpatterns);
34
+ const generated = generateManifestFull(
35
+ router.urlpatterns,
36
+ undefined,
37
+ router.basename ? { urlPrefix: router.basename } : undefined,
38
+ );
35
39
  if (
36
40
  generated._routeAncestry &&
37
41
  Object.keys(generated._routeAncestry).length > 0
@@ -10,6 +10,7 @@ import {
10
10
  requireRequestContext,
11
11
  setRequestContextParams,
12
12
  } from "../server/request-context.js";
13
+ import { getSSRSetup } from "./ssr-setup.js";
13
14
  import type { MiddlewareFn } from "../router/middleware.js";
14
15
  import { executeMiddleware } from "../router/middleware.js";
15
16
  import type { RscPayload, ReactFormState } from "./types.js";
@@ -242,9 +243,12 @@ export async function handleProgressiveEnhancement<TEnv>(
242
243
  const payload: RscPayload = {
243
244
  metadata: {
244
245
  pathname: url.pathname,
246
+ routerId: ctx.router.id,
247
+ basename: ctx.router.basename,
245
248
  segments: match.segments,
246
249
  matched: match.matched,
247
250
  diff: match.diff,
251
+ params: match.params,
248
252
  isPartial: false,
249
253
  rootLayout: ctx.router.rootLayout,
250
254
  handles: handleStore.stream(),
@@ -256,11 +260,21 @@ export async function handleProgressiveEnhancement<TEnv>(
256
260
  formState: actionResult,
257
261
  };
258
262
 
259
- const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
260
- const [ssrModule, streamMode] = await Promise.all([
261
- ctx.loadSSRModule(),
262
- ctx.resolveStreamMode(request, env, url),
263
- ]);
263
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
264
+ onError: (error: unknown) => {
265
+ ctx.callOnError(error, "rendering", { request, url, env });
266
+ },
267
+ });
268
+ // metricsStore=undefined is safe: the handler already stashed the early
269
+ // SSR setup promise on request variables, so getSSRSetup returns it
270
+ // without falling back to a fresh startSSRSetup.
271
+ const [ssrModule, streamMode] = await getSSRSetup(
272
+ ctx,
273
+ request,
274
+ env,
275
+ url,
276
+ undefined,
277
+ );
264
278
  const htmlStream = await ssrModule.renderHTML(rscStream, {
265
279
  formState: reactFormState,
266
280
  nonce,
@@ -335,9 +349,12 @@ async function renderPeErrorBoundary<TEnv>(
335
349
  const payload: RscPayload = {
336
350
  metadata: {
337
351
  pathname: url.pathname,
352
+ routerId: ctx.router.id,
353
+ basename: ctx.router.basename,
338
354
  segments: errorResult.segments,
339
355
  matched: errorResult.matched,
340
356
  diff: errorResult.diff,
357
+ params: errorResult.params,
341
358
  isPartial: false,
342
359
  isError: true,
343
360
  rootLayout: ctx.router.rootLayout,
@@ -349,11 +366,21 @@ async function renderPeErrorBoundary<TEnv>(
349
366
  },
350
367
  };
351
368
 
352
- const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
353
- const [ssrModule, streamMode] = await Promise.all([
354
- ctx.loadSSRModule(),
355
- ctx.resolveStreamMode(request, env, url),
356
- ]);
369
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
370
+ onError: (error: unknown) => {
371
+ ctx.callOnError(error, "rendering", { request, url, env });
372
+ },
373
+ });
374
+ // metricsStore=undefined is safe: the handler already stashed the early
375
+ // SSR setup promise on request variables, so getSSRSetup returns it
376
+ // without falling back to a fresh startSSRSetup.
377
+ const [ssrModule, streamMode] = await getSSRSetup(
378
+ ctx,
379
+ request,
380
+ env,
381
+ url,
382
+ undefined,
383
+ );
357
384
  const htmlStream = await ssrModule.renderHTML(rscStream, {
358
385
  nonce,
359
386
  streamMode,
@@ -26,7 +26,9 @@ import {
26
26
  finalizeResponse,
27
27
  isCacheableStatus,
28
28
  buildRouteMiddlewareEntries,
29
+ mergeStubHeadersAndFinalize,
29
30
  } from "./helpers.js";
31
+ import { isWebSocketUpgradeResponse } from "../response-utils.js";
30
32
 
31
33
  export interface ResponseRouteMatch {
32
34
  responseType: string;
@@ -78,10 +80,13 @@ export async function handleResponseRoute<TEnv>(
78
80
  env,
79
81
  searchParams: cleanUrl.searchParams,
80
82
  url: cleanUrl,
83
+ originalUrl: reqCtx.originalUrl,
81
84
  pathname: url.pathname,
82
85
  reverse: createReverseFunction(handlerCtx.getRequiredRouteMap()),
83
86
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
84
87
  header: (name: string, value: string) => reqCtx.header(name, value),
88
+ waitUntil: reqCtx.waitUntil.bind(reqCtx),
89
+ executionContext: reqCtx.executionContext,
85
90
  _responseType: preview.responseType,
86
91
  };
87
92
  // Brand with taint symbol so "use cache" detects it as request-scoped
@@ -96,6 +101,12 @@ export async function handleResponseRoute<TEnv>(
96
101
  // so that stub headers (cookies, custom headers set via ctx.header()) are included.
97
102
  // Use Headers (not Record<string, string>) to preserve duplicate entries like Set-Cookie.
98
103
  const rewrapResponse = (result: Response) => {
104
+ // 204/205/304 are NOT short-circuited — they're valid for the Response
105
+ // constructor and must honor ctx.setStatus() overrides. Only upgrade
106
+ // responses (status 101 / `webSocket` property) bypass reconstruction.
107
+ if (isWebSocketUpgradeResponse(result)) {
108
+ return mergeStubHeadersAndFinalize(result);
109
+ }
99
110
  const headers = new Headers();
100
111
  result.headers.forEach((value, key) => {
101
112
  if (key.toLowerCase() === "set-cookie") {
@@ -196,7 +207,9 @@ export async function handleResponseRoute<TEnv>(
196
207
  // Wrap callHandler to append Vary: Accept on content-negotiated responses
197
208
  const callHandlerWithVary = async () => {
198
209
  const response = await callHandler();
199
- if (preview.negotiated) {
210
+ if (preview.negotiated && !isWebSocketUpgradeResponse(response)) {
211
+ // Skip Vary on upgrade responses: headers are semantically immutable
212
+ // on some runtimes, and Vary is meaningless for a 101 response.
200
213
  response.headers.append("Vary", "Accept");
201
214
  }
202
215
  return response;
@@ -12,6 +12,8 @@ import {
12
12
  getLocationState,
13
13
  } from "../server/request-context.js";
14
14
  import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
15
+ import { appendMetric } from "../router/metrics.js";
16
+ import { getSSRSetup } from "./ssr-setup.js";
15
17
  import type { RscPayload } from "./types.js";
16
18
  import {
17
19
  createResponseWithMergedHeaders,
@@ -28,13 +30,9 @@ export async function handleRscRendering<TEnv>(
28
30
  handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
29
31
  nonce: string | undefined,
30
32
  ): Promise<Response> {
31
- // Retrieve handler-level timing from variables
32
33
  const reqCtx = requireRequestContext();
33
- const handlerTimingArr: string[] = reqCtx.var.__handlerTiming || [];
34
- const handlerStart: number = reqCtx.var.__handlerStart || 0;
35
34
 
36
35
  let payload: RscPayload;
37
- let serverTiming: string | undefined;
38
36
  let hasInterceptSlots = false;
39
37
 
40
38
  if (isPartial) {
@@ -53,11 +51,11 @@ export async function handleRscRendering<TEnv>(
53
51
  return createSimpleRedirectResponse(match.redirect);
54
52
  }
55
53
 
56
- serverTiming = match.serverTiming;
57
-
58
54
  payload = {
59
55
  metadata: {
60
56
  pathname: url.pathname,
57
+ routerId: ctx.router.id,
58
+ basename: ctx.router.basename,
61
59
  segments: match.segments,
62
60
  matched: match.matched,
63
61
  diff: match.diff,
@@ -66,18 +64,20 @@ export async function handleRscRendering<TEnv>(
66
64
  rootLayout: ctx.router.rootLayout,
67
65
  handles: handleStore.stream(),
68
66
  version: ctx.version,
67
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
69
68
  themeConfig: ctx.router.themeConfig,
70
69
  initialTheme: reqCtx.theme,
71
70
  },
72
71
  };
73
72
  } else {
74
73
  setRequestContextParams(result.params, result.routeName);
75
- serverTiming = result.serverTiming;
74
+
76
75
  hasInterceptSlots = !!result.slots;
77
76
 
78
77
  payload = {
79
78
  metadata: {
80
79
  pathname: url.pathname,
80
+ routerId: ctx.router.id,
81
81
  segments: result.segments,
82
82
  matched: result.matched,
83
83
  diff: result.diff,
@@ -86,6 +86,7 @@ export async function handleRscRendering<TEnv>(
86
86
  slots: result.slots,
87
87
  handles: handleStore.stream(),
88
88
  version: ctx.version,
89
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
89
90
  },
90
91
  };
91
92
  }
@@ -132,14 +133,14 @@ export async function handleRscRendering<TEnv>(
132
133
  { headers: { "Content-Type": "application/json" } },
133
134
  );
134
135
  } else {
135
- serverTiming = match.serverTiming;
136
-
137
136
  payload = {
138
137
  // Initial SSR can reconstruct the tree from segments + rootLayout,
139
138
  // so we omit root to avoid sending the same structure twice.
140
139
 
141
140
  metadata: {
142
141
  pathname: url.pathname,
142
+ routerId: ctx.router.id,
143
+ basename: ctx.router.basename,
143
144
  segments: match.segments,
144
145
  matched: match.matched,
145
146
  diff: match.diff,
@@ -148,6 +149,7 @@ export async function handleRscRendering<TEnv>(
148
149
  rootLayout: ctx.router.rootLayout,
149
150
  handles: handleStore.stream(),
150
151
  version: ctx.version,
152
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
151
153
  themeConfig: ctx.router.themeConfig,
152
154
  initialTheme: reqCtx.theme,
153
155
  },
@@ -166,10 +168,24 @@ export async function handleRscRendering<TEnv>(
166
168
  }
167
169
  }
168
170
 
171
+ const metricsStore = reqCtx._metricsStore;
172
+ const renderStart = performance.now();
173
+
169
174
  // Serialize to RSC stream
170
175
  const rscSerializeStart = performance.now();
171
- const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
176
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
177
+ onError: (error: unknown) => {
178
+ ctx.callOnError(error, "rendering", { request, url, env });
179
+ },
180
+ });
172
181
  const rscSerializeDur = performance.now() - rscSerializeStart;
182
+ // This measures synchronous stream creation, not end-to-end stream consumption.
183
+ appendMetric(
184
+ metricsStore,
185
+ "rsc-serialize",
186
+ rscSerializeStart,
187
+ rscSerializeDur,
188
+ );
173
189
 
174
190
  // Determine if this is an RSC request or HTML request.
175
191
  // Partial requests (_rsc_partial) are always RSC -- they come from client-side
@@ -181,19 +197,20 @@ export async function handleRscRendering<TEnv>(
181
197
  !url.searchParams.has("__html")) ||
182
198
  url.searchParams.has("__rsc");
183
199
 
184
- // Build complete Server-Timing: handler phases + match/manifest + RSC serialize
185
- const timingParts: string[] = [...handlerTimingArr];
186
- if (serverTiming) {
187
- timingParts.push(serverTiming);
188
- }
189
- timingParts.push(`rsc-serialize;dur=${rscSerializeDur.toFixed(2)}`);
190
-
191
200
  if (isRscRequest) {
192
- const fullTiming = timingParts.join(", ");
201
+ const renderDur = performance.now() - renderStart;
202
+ appendMetric(metricsStore, "render:total", renderStart, renderDur);
193
203
  const rscHeaders: Record<string, string> = {
194
204
  "content-type": "text/x-component;charset=utf-8",
195
205
  vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
196
206
  };
207
+ // Tell the client's prefetch cache to scope this response to its source
208
+ // URL (instead of the default source-agnostic wildcard). Intercept
209
+ // responses depend on the source page matching an intercept rule, so
210
+ // they must not be reused for navigations from other sources.
211
+ if (hasInterceptSlots) {
212
+ rscHeaders["x-rsc-prefetch-scope"] = "source";
213
+ }
197
214
  // Enable browser HTTP caching for prefetch responses only.
198
215
  // Requires X-Rango-Prefetch header (sent by Link prefetch fetch),
199
216
  // non-intercept context (intercept responses depend on source page),
@@ -205,22 +222,19 @@ export async function handleRscRendering<TEnv>(
205
222
  rscHeaders["cache-control"] = cc;
206
223
  }
207
224
  }
208
- if (fullTiming) {
209
- rscHeaders["Server-Timing"] = fullTiming;
210
- }
211
225
  return createResponseWithMergedHeaders(rscStream, {
212
226
  headers: rscHeaders,
213
227
  });
214
228
  }
215
229
 
216
- // Delegate to SSR for HTML response
217
- const ssrSetupStart = performance.now();
218
- const [ssrModule, streamMode] = await Promise.all([
219
- ctx.loadSSRModule(),
220
- ctx.resolveStreamMode(request, env, url),
221
- ]);
222
- const ssrSetupDur = performance.now() - ssrSetupStart;
223
- timingParts.push(`ssr-setup;dur=${ssrSetupDur.toFixed(2)}`);
230
+ // Delegate to SSR for HTML response (reuse early setup if available)
231
+ const [ssrModule, streamMode] = await getSSRSetup(
232
+ ctx,
233
+ request,
234
+ env,
235
+ url,
236
+ metricsStore,
237
+ );
224
238
 
225
239
  const ssrRenderStart = performance.now();
226
240
  const htmlStream = await ssrModule.renderHTML(rscStream, {
@@ -228,23 +242,12 @@ export async function handleRscRendering<TEnv>(
228
242
  streamMode,
229
243
  });
230
244
  const ssrRenderDur = performance.now() - ssrRenderStart;
231
- timingParts.push(`ssr-render-html;dur=${ssrRenderDur.toFixed(2)}`);
245
+ appendMetric(metricsStore, "ssr-render-html", ssrRenderStart, ssrRenderDur);
232
246
 
233
- // Add total handler duration
234
- if (handlerStart) {
235
- const totalHandler = performance.now() - handlerStart;
236
- timingParts.push(`handler-total;dur=${totalHandler.toFixed(2)}`);
237
- }
238
-
239
- const fullTiming = timingParts.join(", ");
240
- const htmlHeaders: Record<string, string> = {
241
- "content-type": "text/html;charset=utf-8",
242
- };
243
- if (fullTiming) {
244
- htmlHeaders["Server-Timing"] = fullTiming;
245
- }
247
+ const renderDur = performance.now() - renderStart;
248
+ appendMetric(metricsStore, "render:total", renderStart, renderDur);
246
249
 
247
250
  return createResponseWithMergedHeaders(htmlStream, {
248
- headers: htmlHeaders,
251
+ headers: { "content-type": "text/html;charset=utf-8" },
249
252
  });
250
253
  }
@@ -21,6 +21,7 @@ import {
21
21
  getLocationState,
22
22
  } from "../server/request-context.js";
23
23
  import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
24
+ import { appendMetric } from "../router/metrics.js";
24
25
  import type { RscPayload } from "./types.js";
25
26
  import {
26
27
  hasBodyContent,
@@ -207,10 +208,12 @@ export async function executeServerAction<TEnv>(
207
208
  const payload: RscPayload = {
208
209
  metadata: {
209
210
  pathname: url.pathname,
211
+ routerId: ctx.router.id,
210
212
  segments: errorResult.segments,
211
213
  isPartial: true,
212
214
  matched: errorResult.matched,
213
215
  diff: errorResult.diff,
216
+ params: errorResult.params,
214
217
  isError: true,
215
218
  handles: handleStore.stream(),
216
219
  version: ctx.version,
@@ -224,6 +227,9 @@ export async function executeServerAction<TEnv>(
224
227
 
225
228
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
226
229
  temporaryReferences,
230
+ onError: (error: unknown) => {
231
+ ctx.callOnError(error, "rendering", { request, url, env });
232
+ },
227
233
  });
228
234
 
229
235
  return createResponseWithMergedHeaders(rscStream, {
@@ -274,6 +280,8 @@ export async function revalidateAfterAction<TEnv>(
274
280
  ): Promise<Response> {
275
281
  const { returnValue, actionStatus, temporaryReferences, actionContext } =
276
282
  continuation;
283
+ const reqCtx = requireRequestContext();
284
+ const metricsStore = reqCtx._metricsStore;
277
285
 
278
286
  const matchResult = await ctx.router.matchPartial(
279
287
  request,
@@ -308,15 +316,15 @@ export async function revalidateAfterAction<TEnv>(
308
316
  // Return updated segments
309
317
  setRequestContextParams(matchResult.params, matchResult.routeName);
310
318
 
311
- const serverTiming = matchResult.serverTiming;
312
-
313
319
  const payload: RscPayload = {
314
320
  metadata: {
315
321
  pathname: url.pathname,
322
+ routerId: ctx.router.id,
316
323
  segments: matchResult.segments,
317
324
  isPartial: true,
318
325
  matched: matchResult.matched,
319
326
  diff: matchResult.diff,
327
+ params: matchResult.params,
320
328
  slots: matchResult.slots,
321
329
  handles: handleStore.stream(),
322
330
  version: ctx.version,
@@ -326,19 +334,25 @@ export async function revalidateAfterAction<TEnv>(
326
334
 
327
335
  attachLocationState(payload);
328
336
 
337
+ const renderStart = performance.now();
329
338
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
330
339
  temporaryReferences,
340
+ onError: (error: unknown) => {
341
+ ctx.callOnError(error, "rendering", { request, url, env });
342
+ },
331
343
  });
332
-
333
- const actionHeaders: Record<string, string> = {
334
- "content-type": "text/x-component;charset=utf-8",
335
- };
336
- if (serverTiming) {
337
- actionHeaders["Server-Timing"] = serverTiming;
338
- }
344
+ const rscSerializeDur = performance.now() - renderStart;
345
+ // This measures synchronous stream creation, not end-to-end stream consumption.
346
+ appendMetric(metricsStore, "rsc-serialize", renderStart, rscSerializeDur);
347
+ appendMetric(
348
+ metricsStore,
349
+ "render:total",
350
+ renderStart,
351
+ performance.now() - renderStart,
352
+ );
339
353
 
340
354
  return createResponseWithMergedHeaders(rscStream, {
341
355
  status: actionStatus,
342
- headers: actionHeaders,
356
+ headers: { "content-type": "text/x-component;charset=utf-8" },
343
357
  });
344
358
  }