@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126

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 (235) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/rango.js +3 -4
  3. package/dist/vite/index.js +315 -68
  4. package/package.json +19 -18
  5. package/skills/breadcrumbs/SKILL.md +60 -0
  6. package/skills/hooks/SKILL.md +2 -2
  7. package/skills/route/SKILL.md +6 -0
  8. package/skills/server-actions/SKILL.md +25 -1
  9. package/skills/testing/SKILL.md +17 -17
  10. package/skills/testing/cache-prerender.md +29 -3
  11. package/skills/testing/flight.md +13 -10
  12. package/skills/testing/render-handler.md +3 -0
  13. package/skills/testing/server-tree.md +1 -1
  14. package/skills/testing/setup.md +1 -1
  15. package/src/__internal.ts +0 -65
  16. package/src/browser/action-coordinator.ts +1 -1
  17. package/src/browser/action-fence.ts +10 -0
  18. package/src/browser/event-controller.ts +1 -83
  19. package/src/browser/navigation-store-handle.ts +3 -4
  20. package/src/browser/navigation-store.ts +0 -39
  21. package/src/browser/navigation-transaction.ts +0 -32
  22. package/src/browser/partial-update.ts +23 -84
  23. package/src/browser/prefetch/cache.ts +6 -45
  24. package/src/browser/prefetch/queue.ts +6 -3
  25. package/src/browser/rango-state.ts +2 -23
  26. package/src/browser/react/Link.tsx +0 -2
  27. package/src/browser/react/NavigationProvider.tsx +2 -1
  28. package/src/browser/react/ScrollRestoration.tsx +10 -6
  29. package/src/browser/react/filter-segment-order.ts +0 -2
  30. package/src/browser/react/index.ts +0 -45
  31. package/src/browser/react/location-state-shared.ts +0 -13
  32. package/src/browser/react/location-state.ts +0 -1
  33. package/src/browser/react/use-action.ts +6 -15
  34. package/src/browser/react/use-handle.ts +0 -5
  35. package/src/browser/react/use-link-status.ts +0 -4
  36. package/src/browser/react/use-navigation.ts +0 -3
  37. package/src/browser/react/use-params.ts +0 -2
  38. package/src/browser/react/use-router.ts +2 -1
  39. package/src/browser/react/use-search-params.ts +0 -5
  40. package/src/browser/react/use-segments.ts +0 -13
  41. package/src/browser/rsc-router.tsx +10 -3
  42. package/src/browser/server-action-bridge.ts +51 -3
  43. package/src/browser/types.ts +23 -5
  44. package/src/browser/validate-redirect-origin.ts +43 -16
  45. package/src/build/index.ts +8 -9
  46. package/src/build/route-trie.ts +46 -11
  47. package/src/build/route-types/param-extraction.ts +6 -3
  48. package/src/build/route-types/router-processing.ts +0 -8
  49. package/src/cache/cache-policy.ts +0 -54
  50. package/src/cache/cache-runtime.ts +48 -24
  51. package/src/cache/cache-scope.ts +0 -27
  52. package/src/cache/cache-tag.ts +0 -37
  53. package/src/cache/cf/cf-cache-store.ts +72 -45
  54. package/src/cache/cf/index.ts +0 -24
  55. package/src/cache/document-cache.ts +10 -36
  56. package/src/cache/handle-snapshot.ts +0 -40
  57. package/src/cache/index.ts +0 -27
  58. package/src/cache/memory-segment-store.ts +0 -52
  59. package/src/cache/profile-registry.ts +6 -30
  60. package/src/cache/read-through-swr.ts +41 -11
  61. package/src/cache/segment-codec.ts +0 -16
  62. package/src/cache/types.ts +0 -98
  63. package/src/client.rsc.tsx +4 -22
  64. package/src/client.tsx +19 -32
  65. package/src/context-var.ts +12 -0
  66. package/src/defer.ts +196 -0
  67. package/src/deps/ssr.ts +0 -1
  68. package/src/handle.ts +2 -12
  69. package/src/handles/MetaTags.tsx +0 -14
  70. package/src/handles/breadcrumbs.ts +16 -5
  71. package/src/handles/meta.ts +0 -39
  72. package/src/host/cookie-handler.ts +0 -36
  73. package/src/host/errors.ts +0 -24
  74. package/src/host/index.ts +6 -0
  75. package/src/host/pattern-matcher.ts +7 -50
  76. package/src/host/router.ts +1 -65
  77. package/src/host/testing.ts +0 -16
  78. package/src/host/types.ts +6 -2
  79. package/src/href-client.ts +0 -4
  80. package/src/index.rsc.ts +27 -2
  81. package/src/index.ts +7 -0
  82. package/src/internal-debug.ts +2 -4
  83. package/src/loader.rsc.ts +4 -15
  84. package/src/loader.ts +3 -9
  85. package/src/network-error-thrower.tsx +1 -6
  86. package/src/outlet-provider.tsx +1 -5
  87. package/src/prerender/param-hash.ts +10 -11
  88. package/src/prerender/store.ts +23 -30
  89. package/src/prerender.ts +34 -0
  90. package/src/redirect-origin.ts +100 -0
  91. package/src/root-error-boundary.tsx +1 -19
  92. package/src/route-content-wrapper.tsx +1 -44
  93. package/src/route-definition/dsl-helpers.ts +7 -19
  94. package/src/route-definition/helpers-types.ts +3 -3
  95. package/src/route-definition/redirect.ts +43 -9
  96. package/src/route-definition/resolve-handler-use.ts +6 -0
  97. package/src/route-map-builder.ts +0 -16
  98. package/src/router/content-negotiation.ts +0 -13
  99. package/src/router/error-handling.ts +12 -16
  100. package/src/router/find-match.ts +4 -31
  101. package/src/router/intercept-resolution.ts +10 -1
  102. package/src/router/lazy-includes.ts +1 -57
  103. package/src/router/loader-resolution.ts +25 -23
  104. package/src/router/logging.ts +0 -6
  105. package/src/router/manifest.ts +1 -25
  106. package/src/router/match-api.ts +0 -20
  107. package/src/router/match-context.ts +0 -22
  108. package/src/router/match-handlers.ts +0 -43
  109. package/src/router/match-middleware/background-revalidation.ts +0 -7
  110. package/src/router/match-middleware/cache-lookup.ts +96 -179
  111. package/src/router/match-middleware/cache-store.ts +0 -31
  112. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  113. package/src/router/match-middleware/segment-resolution.ts +0 -22
  114. package/src/router/match-pipelines.ts +1 -42
  115. package/src/router/match-result.ts +1 -52
  116. package/src/router/metrics.ts +0 -34
  117. package/src/router/middleware-types.ts +0 -116
  118. package/src/router/middleware.ts +77 -60
  119. package/src/router/navigation-snapshot.ts +0 -51
  120. package/src/router/params-util.ts +23 -0
  121. package/src/router/pattern-matching.ts +5 -56
  122. package/src/router/prerender-match.ts +56 -51
  123. package/src/router/request-classification.ts +1 -38
  124. package/src/router/revalidation.ts +14 -62
  125. package/src/router/route-snapshot.ts +0 -1
  126. package/src/router/router-context.ts +0 -27
  127. package/src/router/router-interfaces.ts +10 -0
  128. package/src/router/segment-resolution/fresh.ts +25 -57
  129. package/src/router/segment-resolution/helpers.ts +34 -0
  130. package/src/router/segment-resolution/loader-cache.ts +35 -23
  131. package/src/router/segment-resolution/revalidation.ts +188 -283
  132. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  133. package/src/router/segment-resolution.ts +4 -1
  134. package/src/router/segment-wrappers.ts +0 -3
  135. package/src/router/telemetry-otel.ts +0 -20
  136. package/src/router/telemetry.ts +0 -22
  137. package/src/router/timeout.ts +0 -20
  138. package/src/router/trie-matching.ts +66 -45
  139. package/src/router/types.ts +1 -63
  140. package/src/router/url-params.ts +0 -5
  141. package/src/router.ts +8 -11
  142. package/src/rsc/handler-context.ts +1 -0
  143. package/src/rsc/handler.ts +20 -4
  144. package/src/rsc/helpers.ts +71 -3
  145. package/src/rsc/json-route-result.ts +38 -0
  146. package/src/rsc/origin-guard.ts +9 -15
  147. package/src/rsc/progressive-enhancement.ts +10 -1
  148. package/src/rsc/redirect-guard.ts +99 -0
  149. package/src/rsc/response-route-handler.ts +23 -18
  150. package/src/rsc/rsc-rendering.ts +2 -7
  151. package/src/rsc/runtime-warnings.ts +14 -0
  152. package/src/rsc/server-action.ts +34 -29
  153. package/src/rsc/types.ts +6 -3
  154. package/src/search-params.ts +0 -16
  155. package/src/segment-loader-promise.ts +14 -2
  156. package/src/segment-system.tsx +79 -88
  157. package/src/server/handle-store.ts +7 -24
  158. package/src/server/loader-registry.ts +5 -24
  159. package/src/server/request-context.ts +29 -92
  160. package/src/ssr/index.tsx +14 -14
  161. package/src/static-handler.ts +2 -27
  162. package/src/testing/cache-status.ts +44 -48
  163. package/src/testing/collect-handle.ts +1 -24
  164. package/src/testing/dispatch.ts +43 -6
  165. package/src/testing/e2e/index.ts +1 -22
  166. package/src/testing/e2e/matchers.ts +0 -16
  167. package/src/testing/flight-matchers.ts +0 -13
  168. package/src/testing/flight-normalize.ts +3 -30
  169. package/src/testing/flight.ts +46 -48
  170. package/src/testing/generated-routes.ts +1 -41
  171. package/src/testing/index.ts +1 -21
  172. package/src/testing/internal/context.ts +3 -45
  173. package/src/testing/internal/seed-vars.ts +0 -26
  174. package/src/testing/render-handler.ts +31 -61
  175. package/src/testing/render-route.tsx +75 -103
  176. package/src/testing/run-loader.ts +0 -96
  177. package/src/testing/run-middleware.ts +0 -26
  178. package/src/theme/ThemeProvider.tsx +0 -52
  179. package/src/theme/ThemeScript.tsx +0 -6
  180. package/src/theme/constants.ts +0 -12
  181. package/src/theme/index.ts +0 -7
  182. package/src/theme/theme-context.ts +1 -5
  183. package/src/theme/theme-script.ts +0 -14
  184. package/src/theme/use-theme.ts +0 -3
  185. package/src/types/boundaries.ts +0 -35
  186. package/src/types/error-types.ts +25 -89
  187. package/src/types/global-namespace.ts +4 -14
  188. package/src/types/handler-context.ts +28 -9
  189. package/src/types/index.ts +0 -10
  190. package/src/types/request-scope.ts +0 -19
  191. package/src/types/route-config.ts +6 -50
  192. package/src/types/route-entry.ts +0 -6
  193. package/src/types/segments.ts +0 -13
  194. package/src/urls/include-helper.ts +0 -4
  195. package/src/urls/index.ts +0 -6
  196. package/src/urls/path-helper-types.ts +2 -2
  197. package/src/urls/path-helper.ts +0 -54
  198. package/src/urls/urls-function.ts +0 -13
  199. package/src/use-loader.tsx +0 -186
  200. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  201. package/src/vite/discovery/discover-routers.ts +28 -18
  202. package/src/vite/discovery/prerender-collection.ts +2 -4
  203. package/src/vite/discovery/state.ts +5 -0
  204. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  205. package/src/vite/plugin-types.ts +35 -9
  206. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  207. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  208. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  209. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  210. package/src/vite/plugins/expose-action-id.ts +2 -73
  211. package/src/vite/plugins/expose-id-utils.ts +0 -55
  212. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  213. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  214. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  215. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  216. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  217. package/src/vite/plugins/performance-tracks.ts +0 -3
  218. package/src/vite/plugins/refresh-cmd.ts +1 -1
  219. package/src/vite/plugins/use-cache-transform.ts +21 -46
  220. package/src/vite/plugins/version-injector.ts +0 -20
  221. package/src/vite/plugins/version-plugin.ts +1 -49
  222. package/src/vite/plugins/virtual-entries.ts +0 -15
  223. package/src/vite/rango.ts +2 -108
  224. package/src/vite/router-discovery.ts +9 -1
  225. package/src/vite/utils/ast-handler-extract.ts +0 -16
  226. package/src/vite/utils/bundle-analysis.ts +6 -13
  227. package/src/vite/utils/client-chunks.ts +0 -6
  228. package/src/vite/utils/forward-user-plugins.ts +0 -22
  229. package/src/vite/utils/manifest-utils.ts +0 -4
  230. package/src/vite/utils/package-resolution.ts +1 -73
  231. package/src/vite/utils/prerender-utils.ts +0 -35
  232. package/src/vite/utils/shared-utils.ts +3 -35
  233. package/src/browser/shallow.ts +0 -40
  234. package/src/handles/index.ts +0 -7
  235. package/src/router/middleware-cookies.ts +0 -55
@@ -1,13 +1,4 @@
1
1
  /// <reference types="vite/types/importMeta.d.ts" />
2
- /**
3
- * Middleware Execution
4
- *
5
- * True middleware that wraps the entire RSC handler.
6
- * - `await next()` returns actual Response
7
- * - Can modify response headers
8
- * - Can catch errors from RSC rendering
9
- * - Forgiving API: if middleware doesn't return, original response is used
10
- */
11
2
 
12
3
  import { contextGet, contextSet } from "../context-var.js";
13
4
  import { safeDecodeURIComponent } from "./url-params.js";
@@ -21,28 +12,28 @@ import type {
21
12
  ResponseHolder,
22
13
  } from "./middleware-types.js";
23
14
  import { _getRequestContext } from "../server/request-context.js";
15
+ import {
16
+ EXTERNAL_REDIRECT_MARKER,
17
+ isExternalRedirect,
18
+ markExternalRedirect,
19
+ } from "../redirect-origin.js";
24
20
  import { isAutoGeneratedRouteName } from "../route-name.js";
25
21
  import { appendMetric, createMetricsStore } from "./metrics.js";
26
22
  import { stripInternalParams } from "./handler-context.js";
27
23
  import { isWebSocketUpgradeResponse } from "../response-utils.js";
28
24
 
29
- // Re-export types and cookie utilities for backward compatibility
25
+ // Re-export types consumed through this module's path.
30
26
  export type {
31
27
  CookieOptions,
32
- CollectedMiddleware,
33
- MiddlewareCollectableEntry,
34
28
  MiddlewareContext,
35
29
  MiddlewareEntry,
36
30
  MiddlewareFn,
37
- ResponseHolder,
38
31
  } from "./middleware-types.js";
39
- export { parseCookies, serializeCookie } from "./middleware-cookies.js";
40
32
 
41
33
  const MIDDLEWARE_METRIC_DEPTH = 1;
42
- /** Ignore post-next() durations below this threshold (measurement noise). */
43
34
  const POST_METRIC_MIN_DURATION_MS = 0.01;
44
35
 
45
- function getMiddlewareMetricBase<TEnv>(
36
+ function getMiddlewareMetricLabel<TEnv>(
46
37
  entry: MiddlewareEntry<TEnv>,
47
38
  ordinal: number,
48
39
  ): string {
@@ -50,23 +41,12 @@ function getMiddlewareMetricBase<TEnv>(
50
41
  const scope = entry.pattern ?? "*";
51
42
 
52
43
  if (handlerName) {
53
- return `${handlerName}@${scope}`;
44
+ return `middleware:${handlerName}@${scope}`;
54
45
  }
55
46
 
56
- return `${scope}#${ordinal + 1}`;
47
+ return `middleware:${scope}#${ordinal + 1}`;
57
48
  }
58
49
 
59
- function getMiddlewareMetricLabel<TEnv>(
60
- entry: MiddlewareEntry<TEnv>,
61
- ordinal: number,
62
- ): string {
63
- return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
64
- }
65
-
66
- /**
67
- * Parse a route pattern into regex and param names
68
- * Supports: *, /path, /path/*, /path/:param, /path/:param/*
69
- */
70
50
  export function parsePattern(pattern: string): {
71
51
  regex: RegExp;
72
52
  paramNames: string[];
@@ -261,9 +241,13 @@ export function createMiddlewareContext<TEnv>(
261
241
 
262
242
  reverse:
263
243
  reverse ??
264
- ((name: string) => {
244
+ ((
245
+ name: string,
246
+ _params?: Record<string, string>,
247
+ _search?: Record<string, unknown>,
248
+ ) => {
265
249
  throw new Error(
266
- `ctx.reverse() is not available - route map was not provided to middleware context`,
250
+ `ctx.reverse(${JSON.stringify(name)}) is not available: no route map is bound to this middleware context.`,
267
251
  );
268
252
  }),
269
253
 
@@ -316,6 +300,10 @@ function mergeStubHeaders(
316
300
  stubOverridesNonCookie: boolean,
317
301
  ): void {
318
302
  stub.forEach((value, name) => {
303
+ // The reserved external-redirect marker is internal and never a trust
304
+ // signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
305
+ // browser-facing response. The opt-in is the out-of-band brand.
306
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
319
307
  if (name.toLowerCase() === "set-cookie") {
320
308
  target.append(name, value);
321
309
  } else if (stubOverridesNonCookie || !target.has(name)) {
@@ -341,12 +329,50 @@ function mergeReqCtxStub(
341
329
  }
342
330
  }
343
331
  reqCtx.res.headers.forEach((value, name) => {
332
+ // Never propagate the reserved external-redirect marker (see mergeStubHeaders).
333
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
344
334
  if (name !== "set-cookie" && !target.has(name)) {
345
335
  target.set(name, value);
346
336
  }
347
337
  });
348
338
  }
349
339
 
340
+ // Clone `base` with stub headers merged into a fresh Headers (the clone keeps
341
+ // the body mutable for post-next() modifications). Set-Cookie is always
342
+ // appended; other headers obey stubOverridesNonCookie (see mergeStubHeaders).
343
+ // mergeReqCtx folds in RequestContext stub cookies/headers; the intercept
344
+ // short-circuit path passes false (its reqCtx headers are not merged here),
345
+ // which is the one deliberate divergence between the call sites.
346
+ function mergeResponse(
347
+ base: Response,
348
+ stub: Headers,
349
+ opts: { stubOverridesNonCookie: boolean; mergeReqCtx: boolean },
350
+ ): Response {
351
+ const mergedHeaders = new Headers(base.headers);
352
+ // The reserved external-redirect marker is never a trust signal and must never
353
+ // reach the browser. The guard strips it on 3xx redirects; strip it here too so
354
+ // a forged value cannot ride a non-3xx middleware response (which the 3xx-only
355
+ // guard would not touch) to the client. The opt-in is the out-of-band brand.
356
+ mergedHeaders.delete(EXTERNAL_REDIRECT_MARKER);
357
+ mergeStubHeaders(mergedHeaders, stub, opts.stubOverridesNonCookie);
358
+ if (opts.mergeReqCtx) {
359
+ mergeReqCtxStub(mergedHeaders, _getRequestContext());
360
+ }
361
+ const merged = new Response(base.body, {
362
+ status: base.status,
363
+ statusText: base.statusText,
364
+ headers: mergedHeaders,
365
+ });
366
+ // Transfer the out-of-band external-redirect brand across this rebuild: a
367
+ // middleware short-circuit `return redirect(url, { external: true })` reaches
368
+ // the open-redirect guard only after this merge, and the brand lives on the
369
+ // Response object, not in its headers.
370
+ if (isExternalRedirect(base)) {
371
+ markExternalRedirect(merged);
372
+ }
373
+ return merged;
374
+ }
375
+
350
376
  /**
351
377
  * Execute middleware chain
352
378
  *
@@ -355,7 +381,7 @@ function mergeReqCtxStub(
355
381
  * - `ctx.headers` available before and after `await next()`
356
382
  * - `ctx.header()` shorthand for setting a single header
357
383
  * - Forgiving: if middleware doesn't return, uses the downstream response
358
- * - Short-circuit: return Response to stop chain
384
+ * - Short-circuit: return OR throw a Response to stop chain
359
385
  * - Error catching: try/catch around `next()` works
360
386
  */
361
387
  export async function executeMiddleware<TEnv>(
@@ -385,20 +411,16 @@ export async function executeMiddleware<TEnv>(
385
411
  // End of chain - call actual RSC handler
386
412
  const response = await finalHandler();
387
413
 
388
- const mergedHeaders = new Headers(response.headers);
389
- mergeStubHeaders(mergedHeaders, stubResponse.headers, true);
390
- mergeReqCtxStub(mergedHeaders, _getRequestContext());
391
-
392
414
  if (isWebSocketUpgradeResponse(response)) {
393
415
  responseHolder.response = response;
394
416
  return response;
395
417
  }
396
418
 
397
- // Clone response with merged headers (mutable for post-next() modifications)
398
- responseHolder.response = new Response(response.body, {
399
- status: response.status,
400
- statusText: response.statusText,
401
- headers: mergedHeaders,
419
+ // Chain ran to completion: stub headers overwrite (stubOverridesNonCookie)
420
+ // and reqCtx stub headers are merged in.
421
+ responseHolder.response = mergeResponse(response, stubResponse.headers, {
422
+ stubOverridesNonCookie: true,
423
+ mergeReqCtx: true,
402
424
  });
403
425
 
404
426
  return responseHolder.response;
@@ -490,20 +512,18 @@ export async function executeMiddleware<TEnv>(
490
512
 
491
513
  // Explicit return takes precedence (middleware short-circuit).
492
514
  // Merge stub headers (from ctx.header before this point) and
493
- // RequestContext stub headers (from ctx.setCookie) into the
515
+ // RequestContext stub headers (from cookies().set()) into the
494
516
  // returned Response so they are not lost.
495
517
  if (result instanceof Response) {
496
518
  if (isWebSocketUpgradeResponse(result)) {
497
519
  responseHolder.response = result;
498
520
  return result;
499
521
  }
500
- const mergedHeaders = new Headers(result.headers);
501
- mergeStubHeaders(mergedHeaders, stubResponse.headers, false);
502
- mergeReqCtxStub(mergedHeaders, _getRequestContext());
503
- const merged = new Response(result.body, {
504
- status: result.status,
505
- statusText: result.statusText,
506
- headers: mergedHeaders,
522
+ // Explicit short-circuit: the returned Response's own headers win
523
+ // (stubOverridesNonCookie=false); reqCtx stub headers still merge in.
524
+ const merged = mergeResponse(result, stubResponse.headers, {
525
+ stubOverridesNonCookie: false,
526
+ mergeReqCtx: true,
507
527
  });
508
528
  responseHolder.response = merged;
509
529
  return merged;
@@ -564,7 +584,7 @@ export async function executeMiddleware<TEnv>(
564
584
  *
565
585
  * Intercepts use a shared stubResponse from the request context. This function:
566
586
  * - Runs middleware in sequence with a simple next() chain
567
- * - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
587
+ * - Returns Response if any middleware short-circuits (returns OR throws a Response, or redirects, BEFORE next())
568
588
  * - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
569
589
  *
570
590
  * @param middlewares - Array of middleware functions
@@ -658,15 +678,13 @@ export async function executeInterceptMiddleware<TEnv>(
658
678
  });
659
679
 
660
680
  if (hasStubHeaders) {
661
- // Clone and merge headers from stub into early response.
662
- // Only fill in missing headers — the returned Response's explicit
663
- // headers take precedence, matching executeMiddleware behavior.
664
- const mergedHeaders = new Headers(response.headers);
665
- mergeStubHeaders(mergedHeaders, stubResponse.headers, false);
666
- return new Response(response.body, {
667
- status: response.status,
668
- statusText: response.statusText,
669
- headers: mergedHeaders,
681
+ // Only fill in missing headers the returned Response's explicit headers
682
+ // take precedence (stubOverridesNonCookie=false), matching executeMiddleware.
683
+ // mergeReqCtx=false: the intercept path deliberately does NOT merge reqCtx
684
+ // stub headers here (pinned by intercept-middleware-headers.test.ts).
685
+ return mergeResponse(response, stubResponse.headers, {
686
+ stubOverridesNonCookie: false,
687
+ mergeReqCtx: false,
670
688
  });
671
689
  }
672
690
  return response;
@@ -707,7 +725,6 @@ export async function executeLoaderMiddleware<TEnv>(
707
725
  regex: null,
708
726
  paramNames: [],
709
727
  handler,
710
- mountPrefix: null,
711
728
  } as MiddlewareEntry<TEnv>,
712
729
  params,
713
730
  }));
@@ -1,58 +1,26 @@
1
- /**
2
- * Navigation Snapshot
3
- *
4
- * Pure data type representing the navigation-specific state for partial requests.
5
- * Consolidates the header parsing, previous-route matching, intercept-context
6
- * detection, and segment ID filtering that previously lived inline in
7
- * createMatchContextForPartial (match-api.ts).
8
- *
9
- * resolveNavigation() is the factory: given a request + URL + current route key,
10
- * it returns a NavigationSnapshot (or null if no previous URL).
11
- */
12
-
13
1
  import type { RouteMatchResult } from "./pattern-matching.js";
14
2
 
15
- /**
16
- * Snapshot of navigation state for a partial (navigation/action) request.
17
- *
18
- * Contains the "where are we coming from?" data: previous route, intercept
19
- * source, client segment state, and derived flags.
20
- */
21
3
  export interface NavigationSnapshot {
22
- /** Previous page URL (from X-RSC-Router-Client-Path or Referer) */
23
4
  prevUrl: URL;
24
- /** Params from the previous route match */
25
5
  prevParams: Record<string, string>;
26
- /** Previous route match result (null if prev URL doesn't match any route) */
27
6
  prevMatch: RouteMatchResult | null;
28
7
 
29
- /** URL used as intercept context source */
30
8
  interceptContextUrl: URL;
31
- /** Route match for the intercept context URL */
32
9
  interceptContextMatch: RouteMatchResult | null;
33
10
 
34
- /** Raw segment IDs the client currently has */
35
11
  clientSegmentIds: string[];
36
- /** Set version for O(1) lookup */
37
12
  clientSegmentSet: Set<string>;
38
- /** Segment IDs filtered to remove parallel (.@) and loader (D\d+.) entries */
39
13
  filteredSegmentIds: string[];
40
14
 
41
- /** Whether client considers its cache stale */
42
15
  stale: boolean;
43
16
 
44
- /** Whether the intercept context route is the same as the current route */
45
17
  isSameRouteNavigation: boolean;
46
18
 
47
- /** Effective "from" URL (intercept source URL when present, else prevUrl) */
48
19
  effectiveFromUrl: URL;
49
- /** Effective "from" match (intercept source match when present, else prevMatch) */
50
20
  effectiveFromMatch: RouteMatchResult | null;
51
21
 
52
- /** Whether an intercept source header was present */
53
22
  hasInterceptSource: boolean;
54
23
 
55
- /** Whether an HMR request header was present */
56
24
  isHmr: boolean;
57
25
  }
58
26
 
@@ -60,23 +28,12 @@ export interface ResolveNavigationDeps {
60
28
  findMatch: (pathname: string) => RouteMatchResult | null;
61
29
  }
62
30
 
63
- /**
64
- * Resolve navigation state from a partial request.
65
- *
66
- * Returns null if no previous URL is available (required for partial navigation).
67
- *
68
- * @param request - The incoming HTTP request
69
- * @param url - Parsed URL of the request
70
- * @param currentRouteKey - Route key of the current (target) route match
71
- * @param deps - Dependencies (findMatch)
72
- */
73
31
  export function resolveNavigation(
74
32
  request: Request,
75
33
  url: URL,
76
34
  currentRouteKey: string,
77
35
  deps: ResolveNavigationDeps,
78
36
  ): NavigationSnapshot | null {
79
- // Parse client state from RSC request params/headers
80
37
  const clientSegmentIds =
81
38
  url.searchParams.get("_rsc_segments")?.split(",").filter(Boolean) || [];
82
39
  const stale = url.searchParams.get("_rsc_stale") === "true";
@@ -92,7 +49,6 @@ export function resolveNavigation(
92
49
  return null;
93
50
  }
94
51
 
95
- // Parse previous URL
96
52
  let prevUrl: URL;
97
53
  try {
98
54
  prevUrl = new URL(previousUrl, url.origin);
@@ -100,7 +56,6 @@ export function resolveNavigation(
100
56
  return null;
101
57
  }
102
58
 
103
- // Parse intercept context URL
104
59
  let interceptContextUrl: URL;
105
60
  try {
106
61
  interceptContextUrl = interceptSourceUrl
@@ -110,14 +65,12 @@ export function resolveNavigation(
110
65
  interceptContextUrl = prevUrl;
111
66
  }
112
67
 
113
- // Match previous and intercept context routes
114
68
  const prevMatch = deps.findMatch(prevUrl.pathname);
115
69
  const prevParams = prevMatch?.params || {};
116
70
  const interceptContextMatch = interceptSourceUrl
117
71
  ? deps.findMatch(interceptContextUrl.pathname)
118
72
  : prevMatch;
119
73
 
120
- // Derived state
121
74
  const isSameRouteNavigation = !!(
122
75
  interceptContextMatch && interceptContextMatch.routeKey === currentRouteKey
123
76
  );
@@ -128,7 +81,6 @@ export function resolveNavigation(
128
81
  ? interceptContextMatch
129
82
  : prevMatch;
130
83
 
131
- // Filter segment IDs: remove parallel (.@) and loader (D\d+.) entries
132
84
  const filteredSegmentIds = clientSegmentIds.filter((id) => {
133
85
  if (id.includes(".@")) return false;
134
86
  if (/D\d+\./.test(id)) return false;
@@ -155,9 +107,6 @@ export function resolveNavigation(
155
107
  };
156
108
  }
157
109
 
158
- /**
159
- * Test helper: create a NavigationSnapshot with sensible defaults and overrides.
160
- */
161
110
  export function createNavigationSnapshot(
162
111
  overrides?: Partial<NavigationSnapshot>,
163
112
  ): NavigationSnapshot {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared route-param comparison helpers.
3
+ */
4
+
5
+ /**
6
+ * Shallow equality for two route-param records. Same-reference is a fast path;
7
+ * otherwise compares key count then each value.
8
+ */
9
+ export function paramsEqual(
10
+ a: Record<string, string>,
11
+ b: Record<string, string>,
12
+ ): boolean {
13
+ if (a === b) return true;
14
+
15
+ const keysA = Object.keys(a);
16
+ if (keysA.length !== Object.keys(b).length) return false;
17
+
18
+ for (const key of keysA) {
19
+ if (a[key] !== b[key]) return false;
20
+ }
21
+
22
+ return true;
23
+ }
@@ -33,13 +33,6 @@ export interface ParsedSegment {
33
33
  */
34
34
  export function parsePattern(pattern: string): ParsedSegment[] {
35
35
  const segments: ParsedSegment[] = [];
36
- // Match: /segment where segment can be:
37
- // - static text
38
- // - :param
39
- // - :param?
40
- // - :param(a|b)
41
- // - :param(a|b)?
42
- // - *
43
36
  const segmentRegex =
44
37
  /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
45
38
 
@@ -81,7 +74,6 @@ export function parsePattern(pattern: string): ParsedSegment[] {
81
74
  export interface CompiledPattern {
82
75
  regex: RegExp;
83
76
  paramNames: string[];
84
- optionalParams: Set<string>;
85
77
  hasTrailingSlash: boolean;
86
78
  /**
87
79
  * Param-name → allowed values for constrained params (e.g. `:lang(en|gb)`).
@@ -149,7 +141,6 @@ export function compilePattern(pattern: string): CompiledPattern {
149
141
 
150
142
  const segments = parsePattern(normalizedPattern);
151
143
  const paramNames: string[] = [];
152
- const optionalParams = new Set<string>();
153
144
  let constraints: Record<string, string[]> | undefined;
154
145
 
155
146
  let regexPattern = "";
@@ -171,7 +162,6 @@ export function compilePattern(pattern: string): CompiledPattern {
171
162
  }
172
163
 
173
164
  if (segment.optional) {
174
- optionalParams.add(segment.value);
175
165
  // Optional: make the whole /segment optional
176
166
  regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
177
167
  } else {
@@ -183,7 +173,6 @@ export function compilePattern(pattern: string): CompiledPattern {
183
173
  }
184
174
  }
185
175
 
186
- // Handle root path
187
176
  if (regexPattern === "") {
188
177
  regexPattern = "/";
189
178
  }
@@ -210,7 +199,6 @@ export function compilePattern(pattern: string): CompiledPattern {
210
199
  return {
211
200
  regex: new RegExp(`^${regexPattern}$`),
212
201
  paramNames,
213
- optionalParams,
214
202
  hasTrailingSlash,
215
203
  ...(constraints ? { constraints } : {}),
216
204
  };
@@ -285,7 +273,6 @@ function buildParamsFromMatch(
285
273
  export function extractStaticPrefix(pattern: string): string {
286
274
  if (!pattern || pattern === "/") return "";
287
275
 
288
- // Find the first occurrence of : or *
289
276
  const paramIndex = pattern.indexOf(":");
290
277
  const wildcardIndex = pattern.indexOf("*");
291
278
 
@@ -299,16 +286,13 @@ export function extractStaticPrefix(pattern: string): string {
299
286
  }
300
287
 
301
288
  if (cutIndex === -1) {
302
- // No params or wildcards - entire pattern is static
303
289
  return pattern;
304
290
  }
305
291
 
306
292
  if (cutIndex === 0) {
307
- // Pattern starts with : or * - no static prefix
308
293
  return "";
309
294
  }
310
295
 
311
- // Find the last / before the param
312
296
  const lastSlash = pattern.lastIndexOf("/", cutIndex - 1);
313
297
  if (lastSlash === -1 || lastSlash === 0) {
314
298
  return "";
@@ -337,8 +321,8 @@ export function joinPrefix(base: string | undefined, prefix: string): string {
337
321
  *
338
322
  * Note: Optional params that are absent in the path are omitted from the
339
323
  * returned `params` (read as `undefined`), matching the trie matcher and
340
- * the `ExtractParams<"/:locale?/...">` type. Use the pattern definition or
341
- * `optionalParams` to determine which keys are optional.
324
+ * the `ExtractParams<"/:locale?/...">` type. Use the pattern definition to
325
+ * determine which keys are optional.
342
326
  *
343
327
  * Trailing slash handling (priority order):
344
328
  * 1. Per-route `trailingSlash` config from route()
@@ -356,7 +340,6 @@ export interface RouteMatchResult<TEnv = any> {
356
340
  entry: RouteEntry<TEnv>;
357
341
  routeKey: string;
358
342
  params: Record<string, string>;
359
- optionalParams: Set<string>;
360
343
  redirectTo?: string;
361
344
  /** Route has pre-rendered data available (from trie) */
362
345
  pr?: true;
@@ -435,8 +418,6 @@ export function findMatch<TEnv>(
435
418
  : pathname + "/";
436
419
 
437
420
  for (const entry of routesEntries) {
438
- // Short-circuit: skip entry if pathname doesn't start with static prefix
439
- // staticPrefix is pre-computed at registration time, so this is O(1)
440
421
  if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
441
422
  if (effectiveDebug) {
442
423
  debugStats.entriesSkipped++;
@@ -448,8 +429,6 @@ export function findMatch<TEnv>(
448
429
  continue;
449
430
  }
450
431
 
451
- // Check if this is a lazy entry that needs evaluation
452
- // When staticPrefix matches but routes are not yet populated, signal caller to evaluate
453
432
  if (entry.lazy && !entry.lazyEvaluated) {
454
433
  if (effectiveDebug) {
455
434
  debugLog("findMatch", "lazy entry requires evaluation", {
@@ -470,7 +449,6 @@ export function findMatch<TEnv>(
470
449
  debugStats.routesChecked++;
471
450
  }
472
451
 
473
- // Join prefix and pattern, handling edge cases
474
452
  let fullPattern: string;
475
453
  if (entry.prefix === "" || entry.prefix === "/") {
476
454
  fullPattern = pattern;
@@ -480,19 +458,12 @@ export function findMatch<TEnv>(
480
458
  fullPattern = entry.prefix + pattern;
481
459
  }
482
460
 
483
- const {
484
- regex,
485
- paramNames,
486
- optionalParams,
487
- hasTrailingSlash,
488
- constraints,
489
- } = getCompiledPattern(fullPattern);
461
+ const { regex, paramNames, hasTrailingSlash, constraints } =
462
+ getCompiledPattern(fullPattern);
490
463
 
491
- // Get trailing slash mode for this route (per-route config or pattern-based)
492
464
  const trailingSlashMode: TrailingSlashMode | undefined =
493
465
  entry.trailingSlash?.[routeKey];
494
466
 
495
- // Prerender flag from entry metadata (set by urls() for prerender handlers)
496
467
  const prFlag = entry.prerenderRouteKeys?.has(routeKey)
497
468
  ? { pr: true as const }
498
469
  : {};
@@ -500,13 +471,10 @@ export function findMatch<TEnv>(
500
471
  ? { pt: true as const }
501
472
  : {};
502
473
 
503
- // Try exact match first
504
474
  const match = regex.exec(pathname);
505
475
  if (match) {
506
476
  const params = buildParamsFromMatch(match, paramNames);
507
477
 
508
- // Validate constraints against decoded values; a failure falls
509
- // through to the next route so other patterns can still match.
510
478
  if (!satisfiesConstraints(params, constraints)) {
511
479
  continue;
512
480
  }
@@ -519,29 +487,24 @@ export function findMatch<TEnv>(
519
487
  });
520
488
  }
521
489
 
522
- // Check if trailing slash mode requires redirect even on exact match
523
490
  if (
524
491
  trailingSlashMode === "always" &&
525
492
  !pathnameHasTrailingSlash &&
526
493
  pathname !== "/"
527
494
  ) {
528
- // Mode says always have trailing slash, but pathname doesn't have it
529
495
  return {
530
496
  entry,
531
497
  routeKey,
532
498
  params,
533
- optionalParams,
534
499
  redirectTo: pathname + "/",
535
500
  ...prFlag,
536
501
  ...ptFlag,
537
502
  };
538
503
  } else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
539
- // Mode says never have trailing slash, but pathname has it
540
504
  return {
541
505
  entry,
542
506
  routeKey,
543
507
  params,
544
- optionalParams,
545
508
  redirectTo: pathname.slice(0, -1),
546
509
  ...prFlag,
547
510
  ...ptFlag,
@@ -552,13 +515,11 @@ export function findMatch<TEnv>(
552
515
  entry,
553
516
  routeKey,
554
517
  params,
555
- optionalParams,
556
518
  ...prFlag,
557
519
  ...ptFlag,
558
520
  };
559
521
  }
560
522
 
561
- // Try alternate pathname (opposite trailing slash)
562
523
  const altMatch = regex.exec(alternatePathname);
563
524
  if (altMatch) {
564
525
  const params = buildParamsFromMatch(altMatch, paramNames);
@@ -567,25 +528,20 @@ export function findMatch<TEnv>(
567
528
  continue;
568
529
  }
569
530
 
570
- // Determine redirect behavior based on mode
571
531
  if (trailingSlashMode === "ignore") {
572
- // Match without redirect
573
532
  return {
574
533
  entry,
575
534
  routeKey,
576
535
  params,
577
- optionalParams,
578
536
  ...prFlag,
579
537
  ...ptFlag,
580
538
  };
581
539
  } else if (trailingSlashMode === "never") {
582
- // Redirect to no trailing slash
583
540
  if (pathnameHasTrailingSlash) {
584
541
  return {
585
542
  entry,
586
543
  routeKey,
587
544
  params,
588
- optionalParams,
589
545
  redirectTo: alternatePathname,
590
546
  ...prFlag,
591
547
  ...ptFlag,
@@ -595,18 +551,15 @@ export function findMatch<TEnv>(
595
551
  entry,
596
552
  routeKey,
597
553
  params,
598
- optionalParams,
599
554
  ...prFlag,
600
555
  ...ptFlag,
601
556
  };
602
557
  } else if (trailingSlashMode === "always") {
603
- // Redirect to with trailing slash
604
558
  if (!pathnameHasTrailingSlash) {
605
559
  return {
606
560
  entry,
607
561
  routeKey,
608
562
  params,
609
- optionalParams,
610
563
  redirectTo: alternatePathname,
611
564
  ...prFlag,
612
565
  ...ptFlag,
@@ -616,13 +569,10 @@ export function findMatch<TEnv>(
616
569
  entry,
617
570
  routeKey,
618
571
  params,
619
- optionalParams,
620
572
  ...prFlag,
621
573
  ...ptFlag,
622
574
  };
623
575
  } else {
624
- // No explicit mode - use pattern-based detection
625
- // Redirect to canonical form (what the pattern defines)
626
576
  const canonicalPath = hasTrailingSlash
627
577
  ? alternatePathname
628
578
  : pathname.slice(0, -1);
@@ -630,7 +580,6 @@ export function findMatch<TEnv>(
630
580
  entry,
631
581
  routeKey,
632
582
  params,
633
- optionalParams,
634
583
  redirectTo: canonicalPath,
635
584
  ...prFlag,
636
585
  ...ptFlag,
@@ -651,7 +600,7 @@ export function* traverseBack(entry: EntryData): Generator<EntryData> {
651
600
  let current: EntryData | null = entry;
652
601
  const items = [] as EntryData[];
653
602
  while (current !== null) {
654
- items.push(current); // Move up to next parent
603
+ items.push(current);
655
604
  current = current.parent;
656
605
  }
657
606
  for (let i = items.length - 1; i >= 0; i--) {