@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
@@ -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
 
@@ -183,7 +176,6 @@ export function compilePattern(pattern: string): CompiledPattern {
183
176
  }
184
177
  }
185
178
 
186
- // Handle root path
187
179
  if (regexPattern === "") {
188
180
  regexPattern = "/";
189
181
  }
@@ -285,7 +277,6 @@ function buildParamsFromMatch(
285
277
  export function extractStaticPrefix(pattern: string): string {
286
278
  if (!pattern || pattern === "/") return "";
287
279
 
288
- // Find the first occurrence of : or *
289
280
  const paramIndex = pattern.indexOf(":");
290
281
  const wildcardIndex = pattern.indexOf("*");
291
282
 
@@ -299,16 +290,13 @@ export function extractStaticPrefix(pattern: string): string {
299
290
  }
300
291
 
301
292
  if (cutIndex === -1) {
302
- // No params or wildcards - entire pattern is static
303
293
  return pattern;
304
294
  }
305
295
 
306
296
  if (cutIndex === 0) {
307
- // Pattern starts with : or * - no static prefix
308
297
  return "";
309
298
  }
310
299
 
311
- // Find the last / before the param
312
300
  const lastSlash = pattern.lastIndexOf("/", cutIndex - 1);
313
301
  if (lastSlash === -1 || lastSlash === 0) {
314
302
  return "";
@@ -435,8 +423,6 @@ export function findMatch<TEnv>(
435
423
  : pathname + "/";
436
424
 
437
425
  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
426
  if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
441
427
  if (effectiveDebug) {
442
428
  debugStats.entriesSkipped++;
@@ -448,8 +434,6 @@ export function findMatch<TEnv>(
448
434
  continue;
449
435
  }
450
436
 
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
437
  if (entry.lazy && !entry.lazyEvaluated) {
454
438
  if (effectiveDebug) {
455
439
  debugLog("findMatch", "lazy entry requires evaluation", {
@@ -470,7 +454,6 @@ export function findMatch<TEnv>(
470
454
  debugStats.routesChecked++;
471
455
  }
472
456
 
473
- // Join prefix and pattern, handling edge cases
474
457
  let fullPattern: string;
475
458
  if (entry.prefix === "" || entry.prefix === "/") {
476
459
  fullPattern = pattern;
@@ -488,11 +471,9 @@ export function findMatch<TEnv>(
488
471
  constraints,
489
472
  } = getCompiledPattern(fullPattern);
490
473
 
491
- // Get trailing slash mode for this route (per-route config or pattern-based)
492
474
  const trailingSlashMode: TrailingSlashMode | undefined =
493
475
  entry.trailingSlash?.[routeKey];
494
476
 
495
- // Prerender flag from entry metadata (set by urls() for prerender handlers)
496
477
  const prFlag = entry.prerenderRouteKeys?.has(routeKey)
497
478
  ? { pr: true as const }
498
479
  : {};
@@ -500,13 +481,10 @@ export function findMatch<TEnv>(
500
481
  ? { pt: true as const }
501
482
  : {};
502
483
 
503
- // Try exact match first
504
484
  const match = regex.exec(pathname);
505
485
  if (match) {
506
486
  const params = buildParamsFromMatch(match, paramNames);
507
487
 
508
- // Validate constraints against decoded values; a failure falls
509
- // through to the next route so other patterns can still match.
510
488
  if (!satisfiesConstraints(params, constraints)) {
511
489
  continue;
512
490
  }
@@ -519,13 +497,11 @@ export function findMatch<TEnv>(
519
497
  });
520
498
  }
521
499
 
522
- // Check if trailing slash mode requires redirect even on exact match
523
500
  if (
524
501
  trailingSlashMode === "always" &&
525
502
  !pathnameHasTrailingSlash &&
526
503
  pathname !== "/"
527
504
  ) {
528
- // Mode says always have trailing slash, but pathname doesn't have it
529
505
  return {
530
506
  entry,
531
507
  routeKey,
@@ -536,7 +512,6 @@ export function findMatch<TEnv>(
536
512
  ...ptFlag,
537
513
  };
538
514
  } else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
539
- // Mode says never have trailing slash, but pathname has it
540
515
  return {
541
516
  entry,
542
517
  routeKey,
@@ -558,7 +533,6 @@ export function findMatch<TEnv>(
558
533
  };
559
534
  }
560
535
 
561
- // Try alternate pathname (opposite trailing slash)
562
536
  const altMatch = regex.exec(alternatePathname);
563
537
  if (altMatch) {
564
538
  const params = buildParamsFromMatch(altMatch, paramNames);
@@ -567,9 +541,7 @@ export function findMatch<TEnv>(
567
541
  continue;
568
542
  }
569
543
 
570
- // Determine redirect behavior based on mode
571
544
  if (trailingSlashMode === "ignore") {
572
- // Match without redirect
573
545
  return {
574
546
  entry,
575
547
  routeKey,
@@ -579,7 +551,6 @@ export function findMatch<TEnv>(
579
551
  ...ptFlag,
580
552
  };
581
553
  } else if (trailingSlashMode === "never") {
582
- // Redirect to no trailing slash
583
554
  if (pathnameHasTrailingSlash) {
584
555
  return {
585
556
  entry,
@@ -600,7 +571,6 @@ export function findMatch<TEnv>(
600
571
  ...ptFlag,
601
572
  };
602
573
  } else if (trailingSlashMode === "always") {
603
- // Redirect to with trailing slash
604
574
  if (!pathnameHasTrailingSlash) {
605
575
  return {
606
576
  entry,
@@ -621,8 +591,6 @@ export function findMatch<TEnv>(
621
591
  ...ptFlag,
622
592
  };
623
593
  } else {
624
- // No explicit mode - use pattern-based detection
625
- // Redirect to canonical form (what the pattern defines)
626
594
  const canonicalPath = hasTrailingSlash
627
595
  ? alternatePathname
628
596
  : pathname.slice(0, -1);
@@ -651,7 +619,7 @@ export function* traverseBack(entry: EntryData): Generator<EntryData> {
651
619
  let current: EntryData | null = entry;
652
620
  const items = [] as EntryData[];
653
621
  while (current !== null) {
654
- items.push(current); // Move up to next parent
622
+ items.push(current);
655
623
  current = current.parent;
656
624
  }
657
625
  for (let i = items.length - 1; i >= 0; i--) {
@@ -11,7 +11,7 @@ import {
11
11
  createStaticContext,
12
12
  createReverseFunction,
13
13
  } from "./handler-context.js";
14
- import { isPrerenderPassthrough } from "../prerender.js";
14
+ import { detectPrerenderPassthrough } from "../prerender.js";
15
15
  import { isRouteRootScoped } from "../route-map-builder.js";
16
16
  import { setupBuildUse } from "./loader-resolution.js";
17
17
  import { loadManifest } from "./manifest.js";
@@ -82,6 +82,17 @@ export async function matchForPrerender<TEnv = any>(
82
82
  // Build RouterContext for loadManifest/traverseBack
83
83
  const routerCtx = deps.buildRouterContext();
84
84
 
85
+ // Passthrough sentinel result: an unknown-param Passthrough route falls
86
+ // through to the live handler at runtime, so no artifact is baked. A fresh
87
+ // object is returned per call (no site mutates or identity-compares it).
88
+ const passthroughResult = () => ({
89
+ segments: [],
90
+ handles: "",
91
+ routeName: matched.routeKey,
92
+ params: matchedParams,
93
+ passthrough: true as const,
94
+ });
95
+
85
96
  return runWithRouterContext(routerCtx, async () => {
86
97
  // 2. Load the manifest entry tree
87
98
  const manifestEntry = await loadManifest(
@@ -145,13 +156,7 @@ export async function matchForPrerender<TEnv = any>(
145
156
  );
146
157
  });
147
158
  if (!isKnown) {
148
- return {
149
- segments: [],
150
- handles: "",
151
- routeName: matched.routeKey,
152
- params: matchedParams,
153
- passthrough: true as const,
154
- };
159
+ return passthroughResult();
155
160
  }
156
161
  // Preserve vars set by getParams() for the render context
157
162
  if (
@@ -165,13 +170,7 @@ export async function matchForPrerender<TEnv = any>(
165
170
  // Skip errors are intentional — treat as passthrough.
166
171
  // All other errors propagate so dev surfaces them.
167
172
  if (err?.name === "Skip") {
168
- return {
169
- segments: [],
170
- handles: "",
171
- routeName: matched.routeKey,
172
- params: matchedParams,
173
- passthrough: true as const,
174
- };
173
+ return passthroughResult();
175
174
  }
176
175
  throw err;
177
176
  }
@@ -211,6 +210,8 @@ export async function matchForPrerender<TEnv = any>(
211
210
  header: () => {},
212
211
  setStatus: () => {},
213
212
  _setStatus: () => {},
213
+ _rotateStateCookie: () => {},
214
+ _setKeepCacheDirective: () => {},
214
215
  use: (() => {
215
216
  throw new Error("use() not available during pre-rendering");
216
217
  }) as any,
@@ -262,17 +263,13 @@ export async function matchForPrerender<TEnv = any>(
262
263
  { skipLoaders: true },
263
264
  );
264
265
 
265
- // 9. Detect passthrough sentinel: handler returned ctx.passthrough()
266
- for (const seg of allSegments) {
267
- if (isPrerenderPassthrough(seg.component)) {
268
- return {
269
- segments: [],
270
- handles: "",
271
- routeName: matched.routeKey,
272
- params: matchedParams,
273
- passthrough: true as const,
274
- };
275
- }
266
+ // 9. Detect passthrough sentinel: handler returned ctx.passthrough().
267
+ // When the route declares loading(), the handler result is deferred so the
268
+ // component is a thenable resolving to the sentinel — detectPrerenderPassthrough
269
+ // resolves thenables before testing (a sync check would miss it and bake a
270
+ // corrupt artifact).
271
+ if (await detectPrerenderPassthrough(allSegments)) {
272
+ return passthroughResult();
276
273
  }
277
274
 
278
275
  // 10. Filter out any loader segments (belt-and-suspenders)
@@ -317,24 +314,14 @@ export async function matchForPrerender<TEnv = any>(
317
314
  }[] = [];
318
315
  let current: EntryData | null = manifestEntry;
319
316
  while (current) {
320
- if (current.intercept && current.intercept.length > 0) {
321
- for (const ic of current.intercept) {
317
+ // Flatten the entry and its sibling layouts into one source list, the
318
+ // same traversal findInterceptForRoute uses; the build keeps ALL matches
319
+ // (not just the innermost) and skips when(). intercept/layout are
320
+ // non-optional arrays, so empty ones are a no-op here.
321
+ for (const source of [current, ...current.layout]) {
322
+ for (const ic of source.intercept) {
322
323
  if (ic.routeName === matched.routeKey) {
323
- foundIntercepts.push({ intercept: ic, entry: current });
324
- }
325
- }
326
- }
327
- if (current.layout && current.layout.length > 0) {
328
- for (const siblingLayout of current.layout) {
329
- if (siblingLayout.intercept && siblingLayout.intercept.length > 0) {
330
- for (const ic of siblingLayout.intercept) {
331
- if (ic.routeName === matched.routeKey) {
332
- foundIntercepts.push({
333
- intercept: ic,
334
- entry: siblingLayout,
335
- });
336
- }
337
- }
324
+ foundIntercepts.push({ intercept: ic, entry: source });
338
325
  }
339
326
  }
340
327
  }
@@ -461,6 +448,8 @@ export async function renderStaticSegment<TEnv = any>(
461
448
  header: () => {},
462
449
  setStatus: () => {},
463
450
  _setStatus: () => {},
451
+ _rotateStateCookie: () => {},
452
+ _setKeepCacheDirective: () => {},
464
453
  use: (() => {
465
454
  throw new Error("use() not available during static pre-rendering");
466
455
  }) as any,
@@ -499,14 +488,13 @@ export async function renderStaticSegment<TEnv = any>(
499
488
  setupBuildUse(buildCtx);
500
489
 
501
490
  const raw = await handler(buildCtx);
502
- const component = raw?.type ? raw : raw;
503
491
 
504
492
  const segment: ResolvedSegment = {
505
493
  id: handlerId,
506
494
  namespace: handlerId,
507
495
  type: "layout",
508
496
  index: 0,
509
- component,
497
+ component: raw,
510
498
  params: {},
511
499
  belongsToRoute: false,
512
500
  };
@@ -23,10 +23,6 @@ import { negotiateRoute } from "./content-negotiation.js";
23
23
  import { stripInternalParams } from "./handler-context.js";
24
24
  import { resolveRoute, type RouteSnapshot } from "./route-snapshot.js";
25
25
 
26
- // ---------------------------------------------------------------------------
27
- // RequestPlan — discriminated union
28
- // ---------------------------------------------------------------------------
29
-
30
26
  interface RedirectPlan<TEnv = any> {
31
27
  mode: "redirect";
32
28
  route: RouteSnapshot<TEnv>;
@@ -124,10 +120,6 @@ export type {
124
120
  PartialRenderPlan,
125
121
  };
126
122
 
127
- // ---------------------------------------------------------------------------
128
- // classifyRequest — the single authoritative classification step
129
- // ---------------------------------------------------------------------------
130
-
131
123
  export interface ClassifyRequestDeps<TEnv = any> {
132
124
  findMatch: (pathname: string) => RouteMatchResult<TEnv> | null;
133
125
  routerVersion: string;
@@ -157,15 +149,12 @@ export async function classifyRequest<TEnv = any>(
157
149
  const isAction =
158
150
  request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
159
151
 
160
- // Version mismatch — runs BEFORE route resolution so stale clients
161
- // requesting removed routes get a reload, not a 404.
162
152
  const clientVersion = url.searchParams.get("_rsc_v");
163
153
  if (
164
154
  deps.routerVersion &&
165
155
  clientVersion &&
166
156
  clientVersion !== deps.routerVersion
167
157
  ) {
168
- // Strip internal _rsc_* params so the browser reloads to a clean URL
169
158
  let reloadUrl = stripInternalParams(url).toString();
170
159
  if (isAction) {
171
160
  const referer = request.headers.get("referer");
@@ -175,9 +164,7 @@ export async function classifyRequest<TEnv = any>(
175
164
  if (refererUrl.origin === url.origin) {
176
165
  reloadUrl = referer;
177
166
  }
178
- } catch {
179
- // Malformed referer, fall back to stripped url
180
- }
167
+ } catch {}
181
168
  }
182
169
  }
183
170
 
@@ -187,18 +174,6 @@ export async function classifyRequest<TEnv = any>(
187
174
  };
188
175
  }
189
176
 
190
- // App switch — also runs BEFORE route resolution (like version-mismatch
191
- // above), and for the same reason: a cross-app SPA navigation must reload
192
- // even when the target route does NOT exist in the target app. If we resolved
193
- // first, a missing route would throw RouteNotFoundError and the 404 would
194
- // render in-place under the SOURCE app's document — violating the invariant
195
- // that crossing a host-router app boundary is always a full document load.
196
- // A mismatched routerId (_rsc_rid) means the navigation crossed an app
197
- // boundary; force a real document navigation. A soft swap can't faithfully
198
- // re-establish the target app's document (stylesheets shared across apps are
199
- // dropped by React 19's by-href dedup; theme/warmup/prefetch-TTL are
200
- // document-lifetime — see browser/app-shell.ts). Only SPA (`_rsc_partial`)
201
- // requests need this; a direct full load already IS the document navigation.
202
177
  const clientRouterId = url.searchParams.get("_rsc_rid");
203
178
  if (
204
179
  clientRouterId &&
@@ -211,9 +186,6 @@ export async function classifyRequest<TEnv = any>(
211
186
  };
212
187
  }
213
188
 
214
- // No metricsStore — classification is a lightweight gating step.
215
- // Route-matching and manifest-loading metrics belong in the match path
216
- // (createMatchContextForFull/Partial) which runs the authoritative resolution.
217
189
  const result = await resolveRoute<TEnv>(pathname, {
218
190
  findMatch: deps.findMatch,
219
191
  lite: true,
@@ -225,7 +197,6 @@ export async function classifyRequest<TEnv = any>(
225
197
  });
226
198
  }
227
199
 
228
- // Redirect
229
200
  if (result.type === "redirect") {
230
201
  const snapshot: RouteSnapshot<TEnv> = {
231
202
  matched: result as any,
@@ -247,7 +218,6 @@ export async function classifyRequest<TEnv = any>(
247
218
 
248
219
  const snapshot = result.snapshot;
249
220
 
250
- // Response route — non-RSC short-circuit (JSON, streaming, etc.)
251
221
  const responseResult = await classifyResponseRoute(
252
222
  request,
253
223
  pathname,
@@ -257,7 +227,6 @@ export async function classifyRequest<TEnv = any>(
257
227
  return responseResult;
258
228
  }
259
229
 
260
- // Mode detection from request signals
261
230
  const actionId =
262
231
  request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
263
232
  const isLoaderFetch = url.searchParams.has("_rsc_loader");
@@ -275,7 +244,6 @@ export async function classifyRequest<TEnv = any>(
275
244
  return { mode: "loader", route: snapshot };
276
245
  }
277
246
 
278
- // PE detection: POST with form content-type, but not a server action
279
247
  const contentType = request.headers.get("content-type") || "";
280
248
  const isFormSubmission =
281
249
  contentType.includes("multipart/form-data") ||
@@ -291,10 +259,6 @@ export async function classifyRequest<TEnv = any>(
291
259
  return { mode: "full-render", route: snapshot, negotiated };
292
260
  }
293
261
 
294
- // ---------------------------------------------------------------------------
295
- // Content negotiation for response routes
296
- // ---------------------------------------------------------------------------
297
-
298
262
  /**
299
263
  * Check if the route is a response route and perform content negotiation
300
264
  * if negotiate variants exist. Returns a ResponseRoutePlan if the route
@@ -305,7 +269,6 @@ async function classifyResponseRoute<TEnv>(
305
269
  pathname: string,
306
270
  snapshot: RouteSnapshot<TEnv>,
307
271
  ): Promise<ResponseRoutePlan<TEnv> | null> {
308
- // negotiateRoute returns the response plan (variant or plain) or null for RSC.
309
272
  const negotiation = await negotiateRoute(request, pathname, snapshot);
310
273
  return negotiation
311
274
  ? { mode: "response", route: snapshot, ...negotiation }
@@ -14,6 +14,7 @@ import {
14
14
  import type { RevalidationTraceEntry } from "./logging.js";
15
15
  import { _getRequestContext } from "../server/request-context.js";
16
16
  import { isAutoGeneratedRouteName } from "../route-name.js";
17
+ import { paramsEqual } from "./params-util.js";
17
18
 
18
19
  /**
19
20
  * Resolve a server-action reference's stable id, mirroring how the action
@@ -56,22 +57,6 @@ function makeIsAction(
56
57
  };
57
58
  }
58
59
 
59
- function paramsEqual(
60
- a: Record<string, string>,
61
- b: Record<string, string>,
62
- ): boolean {
63
- if (a === b) return true;
64
-
65
- const keysA = Object.keys(a);
66
- if (keysA.length !== Object.keys(b).length) return false;
67
-
68
- for (const key of keysA) {
69
- if (a[key] !== b[key]) return false;
70
- }
71
-
72
- return true;
73
- }
74
-
75
60
  /**
76
61
  * Options for revalidation evaluation
77
62
  */
@@ -136,8 +121,6 @@ export async function evaluateRevalidation<TEnv>(
136
121
  const paramsChanged = !paramsEqual(nextParams, prevParams);
137
122
  const searchChanged = prevUrl.search !== nextUrl.search;
138
123
 
139
- // Trace helper: push a structured entry to the request-scoped trace buffer.
140
- // Guarded by isTraceActive() so object construction is skipped in production.
141
124
  function pushTrace(
142
125
  defaultVal: boolean,
143
126
  finalVal: boolean,
@@ -156,43 +139,28 @@ export async function evaluateRevalidation<TEnv>(
156
139
  });
157
140
  }
158
141
 
159
- // Calculate default revalidation based on segment type and request method
160
142
  let defaultShouldRevalidate: boolean;
161
143
  let defaultReason: string;
162
144
 
163
145
  if (defaultOverride) {
164
- // Caller injected the seed (e.g. parallel slot not in clientSegmentIds).
165
- // Skip the type-derived heuristic — caller knows better in this context.
166
146
  defaultShouldRevalidate = defaultOverride.value;
167
147
  defaultReason = defaultOverride.reason;
168
148
  } else if (request.method === "POST") {
169
- // Actions: revalidate segments that belong to the route, skip parent chain
170
149
  if (segment.type === "route") {
171
- // Route segment always revalidates on actions
172
150
  defaultShouldRevalidate = true;
173
151
  defaultReason = "action:route-segment";
174
152
  } else if (segment.type === "loader") {
175
- // Loaders always revalidate on actions - they often contain action-sensitive data
176
- // (e.g., cart count after add-to-cart action)
177
153
  defaultShouldRevalidate = true;
178
154
  defaultReason = "action:loader-segment";
179
155
  } else if (segment.belongsToRoute) {
180
- // Segment belongs to route (orphan layouts/parallels) - revalidate
181
156
  defaultShouldRevalidate = true;
182
157
  defaultReason = "action:belongs-to-route";
183
158
  } else {
184
- // Parent chain segment (shared layouts/parallels) - don't revalidate
185
159
  defaultShouldRevalidate = false;
186
160
  defaultReason = "action:parent-chain-skip";
187
161
  }
188
162
  } else {
189
- // Navigation (GET): Conservative defaults to minimize unnecessary revalidations
190
- // Only the route segment revalidates by default - all others require explicit opt-in
191
-
192
163
  if (segment.type === "route") {
193
- // Route segments revalidate when path params OR search params change.
194
- // Search params (e.g., ?page=2&sort=price) are server-parsed via ctx.search,
195
- // so the handler must re-execute to produce updated content.
196
164
  const routeChanged = paramsChanged || searchChanged;
197
165
  defaultShouldRevalidate = routeChanged;
198
166
  defaultReason = paramsChanged
@@ -208,8 +176,6 @@ export async function evaluateRevalidation<TEnv>(
208
176
  });
209
177
  }
210
178
  } else if (segment.belongsToRoute && (paramsChanged || searchChanged)) {
211
- // Children of the route path (loaders, orphan layouts/parallels)
212
- // revalidate when path params or search params change
213
179
  defaultShouldRevalidate = true;
214
180
  defaultReason = paramsChanged
215
181
  ? "nav:route-child-params-changed"
@@ -221,9 +187,6 @@ export async function evaluateRevalidation<TEnv>(
221
187
  searchChanged,
222
188
  });
223
189
  } else {
224
- // Parent layouts and parallels default to no revalidation
225
- // Cannot assume these segments depend on params without explicit declaration
226
- // Use custom revalidation functions to opt-in when needed
227
190
  defaultShouldRevalidate = false;
228
191
  defaultReason = "nav:non-route-skip";
229
192
  debugLog("revalidation", "non-route segment skipped by default", {
@@ -233,7 +196,6 @@ export async function evaluateRevalidation<TEnv>(
233
196
  }
234
197
  }
235
198
 
236
- // No custom revalidations defined - return default behavior without prev segment
237
199
  if (revalidations.length === 0) {
238
200
  if (defaultShouldRevalidate) {
239
201
  debugLog("revalidation", "default revalidate=true", {
@@ -250,14 +212,10 @@ export async function evaluateRevalidation<TEnv>(
250
212
  return defaultShouldRevalidate;
251
213
  }
252
214
 
253
- // Custom revalidations exist - may need full prev segment
254
- // Lazy load prev segment only if getPrevSegment provided
255
215
  const prevSegment = getPrevSegment ? await getPrevSegment() : null;
256
216
 
257
- // Execute revalidation functions with soft/hard decision pattern
258
217
  let currentSuggestion = defaultShouldRevalidate;
259
218
 
260
- // Compute public route names (filtered: undefined for auto-generated routes)
261
219
  const toRouteName =
262
220
  routeKey && !isAutoGeneratedRouteName(routeKey) ? routeKey : undefined;
263
221
  const reqCtx = _getRequestContext();
@@ -285,20 +243,14 @@ export async function evaluateRevalidation<TEnv>(
285
243
  actionUrl: actionContext?.actionUrl,
286
244
  actionResult: actionContext?.actionResult,
287
245
  formData: actionContext?.formData,
288
- method: request.method, // GET for navigation, POST for actions
289
- routeName: toRouteName, // Navigation target route name (filtered)
290
- fromRouteName, // Navigation source route name (filtered)
291
- toRouteName, // Navigation target route name (filtered)
292
- // Stale cache context (only true for background revalidation after stale cache render)
246
+ method: request.method,
247
+ routeName: toRouteName,
248
+ fromRouteName,
249
+ toRouteName,
293
250
  stale,
294
251
  });
295
252
 
296
- // Check return type:
297
- // - boolean: hard decision, short-circuit immediately
298
- // - { defaultShouldRevalidate: boolean }: soft decision, update suggestion and continue
299
- // - null/undefined: use default behavior (equivalent to returning { defaultShouldRevalidate })
300
253
  if (typeof result === "boolean") {
301
- // Hard decision - short-circuit
302
254
  debugLog("revalidation", "hard decision", {
303
255
  segmentId: segment.id,
304
256
  revalidator: name,
@@ -311,7 +263,6 @@ export async function evaluateRevalidation<TEnv>(
311
263
  typeof result === "object" &&
312
264
  "defaultShouldRevalidate" in result
313
265
  ) {
314
- // Soft decision - update suggestion and continue
315
266
  currentSuggestion = result.defaultShouldRevalidate;
316
267
  debugLog("revalidation", "soft decision", {
317
268
  segmentId: segment.id,
@@ -319,18 +270,14 @@ export async function evaluateRevalidation<TEnv>(
319
270
  revalidate: currentSuggestion,
320
271
  });
321
272
  } else if (result === null || result === undefined) {
322
- // Defer to default - equivalent to { defaultShouldRevalidate: currentSuggestion }
323
- // This means "I don't care, use whatever the default is"
324
273
  debugLog("revalidation", "deferred to current default", {
325
274
  segmentId: segment.id,
326
275
  revalidator: name,
327
276
  revalidate: currentSuggestion,
328
277
  });
329
- // currentSuggestion stays the same, continue to next function
330
278
  }
331
279
  }
332
280
 
333
- // All revalidators completed - use final suggestion
334
281
  debugLog("revalidation", "final decision", {
335
282
  segmentId: segment.id,
336
283
  revalidate: currentSuggestion,