@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d

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 (278) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +2154 -861
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +57 -11
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +220 -30
  11. package/skills/caching/SKILL.md +116 -8
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +247 -17
  21. package/skills/loader/SKILL.md +219 -9
  22. package/skills/middleware/SKILL.md +47 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +71 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +243 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +122 -47
  32. package/skills/route/SKILL.md +57 -4
  33. package/skills/router-setup/SKILL.md +3 -3
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +128 -0
  37. package/skills/testing/bindings.md +89 -0
  38. package/skills/testing/cache-prerender.md +98 -0
  39. package/skills/testing/client-components.md +121 -0
  40. package/skills/testing/e2e-parity.md +124 -0
  41. package/skills/testing/flight.md +89 -0
  42. package/skills/testing/handles.md +127 -0
  43. package/skills/testing/loader.md +108 -0
  44. package/skills/testing/middleware.md +97 -0
  45. package/skills/testing/render-handler.md +102 -0
  46. package/skills/testing/response-routes.md +94 -0
  47. package/skills/testing/reverse-and-types.md +83 -0
  48. package/skills/testing/server-actions.md +89 -0
  49. package/skills/testing/server-tree.md +128 -0
  50. package/skills/testing/setup.md +120 -0
  51. package/skills/typesafety/SKILL.md +319 -27
  52. package/skills/use-cache/SKILL.md +34 -5
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/browser/action-coordinator.ts +53 -36
  57. package/src/browser/app-shell.ts +52 -0
  58. package/src/browser/event-controller.ts +86 -70
  59. package/src/browser/history-state.ts +21 -0
  60. package/src/browser/index.ts +3 -3
  61. package/src/browser/navigation-bridge.ts +84 -11
  62. package/src/browser/navigation-client.ts +104 -68
  63. package/src/browser/navigation-store.ts +32 -9
  64. package/src/browser/navigation-transaction.ts +10 -28
  65. package/src/browser/partial-update.ts +64 -26
  66. package/src/browser/prefetch/cache.ts +183 -44
  67. package/src/browser/prefetch/fetch.ts +228 -37
  68. package/src/browser/prefetch/queue.ts +36 -5
  69. package/src/browser/rango-state.ts +53 -13
  70. package/src/browser/react/Link.tsx +30 -2
  71. package/src/browser/react/NavigationProvider.tsx +72 -31
  72. package/src/browser/react/filter-segment-order.ts +51 -7
  73. package/src/browser/react/index.ts +3 -0
  74. package/src/browser/react/location-state-shared.ts +175 -4
  75. package/src/browser/react/location-state.ts +39 -13
  76. package/src/browser/react/use-handle.ts +17 -9
  77. package/src/browser/react/use-navigation.ts +22 -2
  78. package/src/browser/react/use-params.ts +20 -8
  79. package/src/browser/react/use-reverse.ts +106 -0
  80. package/src/browser/react/use-router.ts +22 -2
  81. package/src/browser/react/use-segments.ts +11 -8
  82. package/src/browser/response-adapter.ts +32 -1
  83. package/src/browser/rsc-router.tsx +69 -22
  84. package/src/browser/scroll-restoration.ts +22 -14
  85. package/src/browser/segment-reconciler.ts +36 -14
  86. package/src/browser/segment-structure-assert.ts +2 -2
  87. package/src/browser/server-action-bridge.ts +23 -30
  88. package/src/browser/types.ts +21 -0
  89. package/src/build/collect-fallback-refs.ts +107 -0
  90. package/src/build/generate-manifest.ts +60 -35
  91. package/src/build/generate-route-types.ts +2 -0
  92. package/src/build/index.ts +8 -1
  93. package/src/build/prefix-tree-utils.ts +123 -0
  94. package/src/build/route-trie.ts +95 -25
  95. package/src/build/route-types/codegen.ts +4 -4
  96. package/src/build/route-types/include-resolution.ts +1 -1
  97. package/src/build/route-types/per-module-writer.ts +7 -4
  98. package/src/build/route-types/router-processing.ts +55 -14
  99. package/src/build/route-types/scan-filter.ts +1 -1
  100. package/src/build/route-types/source-scan.ts +118 -0
  101. package/src/build/runtime-discovery.ts +9 -20
  102. package/src/cache/cache-scope.ts +28 -42
  103. package/src/cache/cf/cf-cache-store.ts +54 -13
  104. package/src/client.rsc.tsx +3 -0
  105. package/src/client.tsx +96 -205
  106. package/src/context-var.ts +5 -5
  107. package/src/decode-loader-results.ts +36 -0
  108. package/src/errors.ts +30 -4
  109. package/src/handle.ts +32 -14
  110. package/src/host/index.ts +2 -2
  111. package/src/host/router.ts +129 -57
  112. package/src/host/types.ts +31 -2
  113. package/src/host/utils.ts +1 -1
  114. package/src/href-client.ts +140 -21
  115. package/src/index.rsc.ts +10 -6
  116. package/src/index.ts +54 -17
  117. package/src/loader-store.ts +500 -0
  118. package/src/loader.rsc.ts +25 -7
  119. package/src/loader.ts +16 -9
  120. package/src/missing-id-error.ts +68 -0
  121. package/src/outlet-context.ts +1 -1
  122. package/src/prerender.ts +27 -6
  123. package/src/response-utils.ts +37 -0
  124. package/src/reverse.ts +65 -36
  125. package/src/route-content-wrapper.tsx +6 -28
  126. package/src/route-definition/dsl-helpers.ts +384 -257
  127. package/src/route-definition/helper-factories.ts +29 -139
  128. package/src/route-definition/helpers-types.ts +100 -28
  129. package/src/route-definition/resolve-handler-use.ts +6 -0
  130. package/src/route-definition/use-item-types.ts +32 -0
  131. package/src/route-types.ts +26 -41
  132. package/src/router/basename.ts +14 -0
  133. package/src/router/content-negotiation.ts +15 -2
  134. package/src/router/error-handling.ts +1 -1
  135. package/src/router/find-match.ts +54 -6
  136. package/src/router/handler-context.ts +21 -38
  137. package/src/router/intercept-resolution.ts +4 -18
  138. package/src/router/lazy-includes.ts +41 -22
  139. package/src/router/loader-resolution.ts +82 -36
  140. package/src/router/manifest.ts +41 -19
  141. package/src/router/match-api.ts +4 -3
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/cache-lookup.ts +44 -91
  144. package/src/router/match-middleware/cache-store.ts +3 -2
  145. package/src/router/match-result.ts +53 -32
  146. package/src/router/metrics.ts +1 -1
  147. package/src/router/middleware-types.ts +15 -26
  148. package/src/router/middleware.ts +99 -84
  149. package/src/router/pattern-matching.ts +116 -19
  150. package/src/router/prerender-match.ts +1 -1
  151. package/src/router/preview-match.ts +3 -1
  152. package/src/router/request-classification.ts +4 -28
  153. package/src/router/revalidation.ts +58 -2
  154. package/src/router/router-interfaces.ts +45 -28
  155. package/src/router/router-options.ts +40 -1
  156. package/src/router/router-registry.ts +2 -5
  157. package/src/router/segment-resolution/fresh.ts +27 -6
  158. package/src/router/segment-resolution/revalidation.ts +147 -106
  159. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  160. package/src/router/substitute-pattern-params.ts +56 -0
  161. package/src/router/telemetry.ts +99 -0
  162. package/src/router/trie-matching.ts +40 -16
  163. package/src/router/types.ts +8 -0
  164. package/src/router/url-params.ts +49 -0
  165. package/src/router.ts +52 -30
  166. package/src/rsc/handler-context.ts +2 -2
  167. package/src/rsc/handler.ts +28 -69
  168. package/src/rsc/helpers.ts +91 -43
  169. package/src/rsc/index.ts +1 -1
  170. package/src/rsc/manifest-init.ts +28 -41
  171. package/src/rsc/origin-guard.ts +28 -10
  172. package/src/rsc/progressive-enhancement.ts +4 -0
  173. package/src/rsc/response-error.ts +79 -12
  174. package/src/rsc/response-route-handler.ts +57 -61
  175. package/src/rsc/rsc-rendering.ts +35 -51
  176. package/src/rsc/runtime-warnings.ts +9 -10
  177. package/src/rsc/server-action.ts +17 -37
  178. package/src/rsc/ssr-setup.ts +16 -0
  179. package/src/rsc/types.ts +8 -2
  180. package/src/runtime-env.ts +18 -0
  181. package/src/search-params.ts +4 -4
  182. package/src/segment-content-promise.ts +67 -0
  183. package/src/segment-loader-promise.ts +122 -0
  184. package/src/segment-system.tsx +132 -116
  185. package/src/serialize.ts +243 -0
  186. package/src/server/context.ts +175 -53
  187. package/src/server/cookie-store.ts +28 -4
  188. package/src/server/request-context.ts +67 -51
  189. package/src/ssr/index.tsx +5 -1
  190. package/src/static-handler.ts +25 -3
  191. package/src/testing/cache-status.ts +166 -0
  192. package/src/testing/collect-handle.ts +63 -0
  193. package/src/testing/dispatch.ts +581 -0
  194. package/src/testing/dom.entry.ts +22 -0
  195. package/src/testing/e2e/fixture.ts +188 -0
  196. package/src/testing/e2e/index.ts +149 -0
  197. package/src/testing/e2e/matchers.ts +51 -0
  198. package/src/testing/e2e/page-helpers.ts +272 -0
  199. package/src/testing/e2e/parity.ts +326 -0
  200. package/src/testing/e2e/server.ts +195 -0
  201. package/src/testing/flight-matchers.ts +110 -0
  202. package/src/testing/flight-normalize.ts +38 -0
  203. package/src/testing/flight-runtime.d.ts +57 -0
  204. package/src/testing/flight-tree.ts +682 -0
  205. package/src/testing/flight.entry.ts +51 -0
  206. package/src/testing/flight.ts +234 -0
  207. package/src/testing/generated-routes.ts +223 -0
  208. package/src/testing/index.ts +106 -0
  209. package/src/testing/internal/context.ts +304 -0
  210. package/src/testing/internal/flight-client-globals.ts +30 -0
  211. package/src/testing/internal/seed-vars.ts +42 -0
  212. package/src/testing/render-handler.ts +323 -0
  213. package/src/testing/render-route.tsx +590 -0
  214. package/src/testing/run-loader.ts +363 -0
  215. package/src/testing/run-middleware.ts +205 -0
  216. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  217. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  218. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  219. package/src/testing/vitest-stubs/version.ts +5 -0
  220. package/src/testing/vitest.ts +285 -0
  221. package/src/types/global-namespace.ts +39 -26
  222. package/src/types/handler-context.ts +68 -50
  223. package/src/types/index.ts +1 -0
  224. package/src/types/loader-types.ts +11 -9
  225. package/src/types/request-scope.ts +126 -0
  226. package/src/types/route-entry.ts +11 -0
  227. package/src/types/segments.ts +35 -2
  228. package/src/urls/include-helper.ts +34 -67
  229. package/src/urls/index.ts +1 -5
  230. package/src/urls/path-helper-types.ts +41 -7
  231. package/src/urls/path-helper.ts +17 -52
  232. package/src/urls/pattern-types.ts +36 -19
  233. package/src/urls/response-types.ts +22 -29
  234. package/src/urls/type-extraction.ts +58 -139
  235. package/src/urls/urls-function.ts +1 -5
  236. package/src/use-loader.tsx +413 -42
  237. package/src/vite/debug.ts +185 -0
  238. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  239. package/src/vite/discovery/discover-routers.ts +106 -75
  240. package/src/vite/discovery/discovery-errors.ts +194 -0
  241. package/src/vite/discovery/gate-state.ts +171 -0
  242. package/src/vite/discovery/prerender-collection.ts +67 -26
  243. package/src/vite/discovery/route-types-writer.ts +40 -84
  244. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  245. package/src/vite/discovery/state.ts +33 -0
  246. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  247. package/src/vite/index.ts +2 -0
  248. package/src/vite/plugin-types.ts +67 -0
  249. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  250. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  251. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  252. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  253. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  254. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  255. package/src/vite/plugins/expose-action-id.ts +54 -30
  256. package/src/vite/plugins/expose-id-utils.ts +12 -8
  257. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  258. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  259. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  260. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  261. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  262. package/src/vite/plugins/performance-tracks.ts +29 -25
  263. package/src/vite/plugins/use-cache-transform.ts +65 -50
  264. package/src/vite/plugins/version-injector.ts +39 -23
  265. package/src/vite/plugins/version-plugin.ts +59 -2
  266. package/src/vite/plugins/virtual-entries.ts +2 -2
  267. package/src/vite/rango.ts +116 -29
  268. package/src/vite/router-discovery.ts +750 -100
  269. package/src/vite/utils/ast-handler-extract.ts +15 -15
  270. package/src/vite/utils/banner.ts +1 -1
  271. package/src/vite/utils/bundle-analysis.ts +4 -2
  272. package/src/vite/utils/client-chunks.ts +190 -0
  273. package/src/vite/utils/forward-user-plugins.ts +193 -0
  274. package/src/vite/utils/manifest-utils.ts +8 -59
  275. package/src/vite/utils/package-resolution.ts +41 -1
  276. package/src/vite/utils/prerender-utils.ts +21 -6
  277. package/src/vite/utils/shared-utils.ts +107 -26
  278. package/src/browser/action-response-classifier.ts +0 -99
@@ -39,11 +39,12 @@ import {
39
39
  resolveLayoutComponent,
40
40
  resolveWithErrorBoundary,
41
41
  } from "./helpers.js";
42
+ import { applyViewTransitionDefault } from "./view-transition-default.js";
42
43
  import { getRouterContext } from "../router-context.js";
43
44
  import { resolveSink, safeEmit } from "../telemetry.js";
44
45
  import {
45
46
  track,
46
- RSCRouterContext,
47
+ RangoContext,
47
48
  runInsideLoaderScope,
48
49
  } from "../../server/context.js";
49
50
 
@@ -89,6 +90,27 @@ function observeStreamedHandler(
89
90
  });
90
91
  }
91
92
 
93
+ /**
94
+ * Trace a parallel slot that's being force-rendered on a full refetch (client
95
+ * has no cached state). User revalidate fns are bypassed in this case — see
96
+ * the call sites for the load-bearing rationale.
97
+ */
98
+ function traceFullRefetchedParallelSlot(
99
+ parallelId: string,
100
+ belongsToRoute: boolean,
101
+ ): void {
102
+ if (!isTraceActive()) return;
103
+ pushRevalidationTraceEntry({
104
+ segmentId: parallelId,
105
+ segmentType: "parallel",
106
+ belongsToRoute,
107
+ source: "parallel",
108
+ defaultShouldRevalidate: true,
109
+ finalShouldRevalidate: true,
110
+ reason: "full-refetch",
111
+ });
112
+ }
113
+
92
114
  // ---------------------------------------------------------------------------
93
115
  // Revalidation telemetry helper
94
116
  // ---------------------------------------------------------------------------
@@ -448,44 +470,30 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
448
470
 
449
471
  const isFullRefetch = clientSegmentIds.size === 0;
450
472
  const isNewParent = !clientSegmentIds.has(entry.shortCode);
451
- if (
452
- isFullRefetch ||
453
- clientSegmentIds.has(parallelId) ||
454
- belongsToRoute ||
455
- isNewParent
456
- ) {
457
- matchedIds.push(parallelId);
458
- }
473
+ // Always announce the slot in matchedIds — it's unconditionally appended
474
+ // to `segments` below, and a segment present in segments but missing from
475
+ // matched lets the client prune it (then it's missing from clientSegmentIds
476
+ // on the next request, perpetuating the staleness).
477
+ matchedIds.push(parallelId);
459
478
 
460
- const shouldResolve = await (async () => {
461
- if (isFullRefetch) {
462
- if (isTraceActive()) {
463
- pushRevalidationTraceEntry({
464
- segmentId: parallelId,
465
- segmentType: "parallel",
466
- belongsToRoute,
467
- source: "parallel",
468
- defaultShouldRevalidate: true,
469
- finalShouldRevalidate: true,
470
- reason: "full-refetch",
471
- });
472
- }
473
- return true;
474
- }
479
+ let shouldResolve: boolean;
480
+ if (isFullRefetch) {
481
+ // Client has nothing cached — slot MUST render. User revalidate fns are
482
+ // bypassed here because returning false would leave the segment blank
483
+ // with no client-side fallback.
484
+ traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
485
+ shouldResolve = true;
486
+ } else {
487
+ // For non-empty client sets, consult user revalidate fns. When the slot
488
+ // is unknown to the client, override the type-derived default so the
489
+ // soft chain seeds with the right "new segment" / "parent-chain" value.
490
+ let defaultOverride: { value: boolean; reason: string } | undefined;
475
491
  if (!clientSegmentIds.has(parallelId)) {
476
- const result = belongsToRoute || isNewParent;
477
- if (isTraceActive()) {
478
- pushRevalidationTraceEntry({
479
- segmentId: parallelId,
480
- segmentType: "parallel",
481
- belongsToRoute,
482
- source: "parallel",
483
- defaultShouldRevalidate: result,
484
- finalShouldRevalidate: result,
485
- reason: result ? "new-segment" : "skip-parent-chain",
486
- });
487
- }
488
- return result;
492
+ const value = belongsToRoute || isNewParent;
493
+ defaultOverride = {
494
+ value,
495
+ reason: value ? "new-segment" : "skip-parent-chain",
496
+ };
489
497
  }
490
498
 
491
499
  const dummySegment: ResolvedSegment = {
@@ -503,7 +511,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
503
511
  : {}),
504
512
  };
505
513
 
506
- return await evaluateRevalidation({
514
+ shouldResolve = await evaluateRevalidation({
507
515
  segment: dummySegment,
508
516
  prevParams,
509
517
  getPrevSegment: null,
@@ -519,8 +527,9 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
519
527
  actionContext,
520
528
  stale,
521
529
  traceSource: "parallel",
530
+ defaultOverride,
522
531
  });
523
- })();
532
+ }
524
533
  emitRevalidationDecision(
525
534
  parallelId,
526
535
  context.pathname,
@@ -529,8 +538,11 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
529
538
  );
530
539
 
531
540
  let component: ReactNode | undefined;
541
+ let handlerRan = false;
532
542
  if (shouldResolve) {
533
543
  component = await tryStaticSlot(parallelEntry, slot, parallelId);
544
+ // tryStaticSlot returning a value means the static cache supplied the
545
+ // component — handler did NOT run. handlerRan stays false.
534
546
  }
535
547
  if (component === undefined) {
536
548
  const hasLoadingFallback =
@@ -541,29 +553,37 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
541
553
  // Handler evicted (production static slot) but static lookup missed.
542
554
  // Nothing to render — use null so the client keeps its cached version.
543
555
  component = null;
544
- } else if (hasLoadingFallback) {
545
- const result =
546
- typeof handler === "function" ? handler(context) : handler;
547
- if (result instanceof Promise) {
548
- const tracked = deps.trackHandler(result, {
549
- segmentId: parallelId,
550
- segmentType: "parallel",
551
- });
552
- observeStreamedHandler(
553
- tracked,
554
- parallelId,
555
- "parallel",
556
- context.pathname,
557
- routeKey,
558
- params,
559
- );
560
- component = tracked as ReactNode;
556
+ } else {
557
+ // Slot-keyed pushes — slot owns its own bucket, parent layout owns
558
+ // its own. On slot-only revalidations the partial merge updates only
559
+ // the slot's bucket; the parent's bucket stays intact.
560
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
561
+ parallelId;
562
+ handlerRan = true;
563
+ if (hasLoadingFallback) {
564
+ const result =
565
+ typeof handler === "function" ? handler(context) : handler;
566
+ if (result instanceof Promise) {
567
+ const tracked = deps.trackHandler(result, {
568
+ segmentId: parallelId,
569
+ segmentType: "parallel",
570
+ });
571
+ observeStreamedHandler(
572
+ tracked,
573
+ parallelId,
574
+ "parallel",
575
+ context.pathname,
576
+ routeKey,
577
+ params,
578
+ );
579
+ component = tracked as ReactNode;
580
+ } else {
581
+ component = result as ReactNode;
582
+ }
561
583
  } else {
562
- component = result as ReactNode;
584
+ component =
585
+ typeof handler === "function" ? await handler(context) : handler;
563
586
  }
564
- } else {
565
- component =
566
- typeof handler === "function" ? await handler(context) : handler;
567
587
  }
568
588
  }
569
589
 
@@ -574,9 +594,13 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
574
594
  index: 0,
575
595
  component,
576
596
  loading: parallelEntry.loading === false ? null : parallelEntry.loading,
577
- transition: parallelEntry.transition,
597
+ transition: applyViewTransitionDefault(
598
+ parallelEntry.transition,
599
+ deps.viewTransitionDefault,
600
+ ),
578
601
  params,
579
602
  slot,
603
+ _handlerRan: handlerRan,
580
604
  belongsToRoute,
581
605
  parallelName: `${parallelEntry.id}.${slot}`,
582
606
  ...(parallelEntry.mountPath
@@ -631,6 +655,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
631
655
  ): Promise<{ segment: ResolvedSegment; matchedId: string }> {
632
656
  const matchedId = entry.shortCode;
633
657
 
658
+ let handlerRan = false;
634
659
  const component = await revalidate(
635
660
  async () => {
636
661
  const hasSegment = clientSegmentIds.has(entry.shortCode);
@@ -707,6 +732,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
707
732
  return shouldRevalidate;
708
733
  },
709
734
  async () => {
735
+ handlerRan = true;
710
736
  const doneHandler = track(`handler:${entry.id}`, 2);
711
737
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
712
738
  entry.shortCode;
@@ -781,13 +807,17 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
781
807
  index: 0,
782
808
  component: resolvedComponent,
783
809
  loading: entry.loading === false ? null : entry.loading,
784
- transition: entry.transition,
810
+ transition: applyViewTransitionDefault(
811
+ entry.transition,
812
+ deps.viewTransitionDefault,
813
+ ),
785
814
  params,
786
815
  belongsToRoute,
787
816
  ...(entry.type === "layout" || entry.type === "cache"
788
817
  ? { layoutName: entry.id }
789
818
  : {}),
790
819
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
820
+ _handlerRan: handlerRan,
791
821
  };
792
822
 
793
823
  return { segment, matchedId };
@@ -868,7 +898,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
868
898
  prevUrl,
869
899
  nextUrl,
870
900
  routeKey,
871
- loaderPromises,
872
901
  true,
873
902
  deps,
874
903
  actionContext,
@@ -953,7 +982,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
953
982
  prevUrl,
954
983
  nextUrl,
955
984
  routeKey,
956
- loaderPromises,
957
985
  false,
958
986
  deps,
959
987
  actionContext,
@@ -980,7 +1008,6 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
980
1008
  prevUrl: URL,
981
1009
  nextUrl: URL,
982
1010
  routeKey: string,
983
- loaderPromises: Map<string, Promise<any>>,
984
1011
  belongsToRoute: boolean,
985
1012
  deps: SegmentResolutionDeps<TEnv>,
986
1013
  actionContext?: ActionContext,
@@ -1117,7 +1144,10 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1117
1144
  belongsToRoute,
1118
1145
  layoutName: orphan.id,
1119
1146
  loading: orphan.loading === false ? null : orphan.loading,
1120
- transition: orphan.transition,
1147
+ transition: applyViewTransitionDefault(
1148
+ orphan.transition,
1149
+ deps.viewTransitionDefault,
1150
+ ),
1121
1151
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
1122
1152
  });
1123
1153
 
@@ -1166,21 +1196,20 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1166
1196
  const parallelId = `${orphan.shortCode}.${slot}`;
1167
1197
  matchedIds.push(parallelId);
1168
1198
 
1169
- const shouldResolve = await (async () => {
1170
- if (!clientSegmentIds.has(parallelId)) {
1171
- if (isTraceActive()) {
1172
- pushRevalidationTraceEntry({
1173
- segmentId: parallelId,
1174
- segmentType: "parallel",
1175
- belongsToRoute,
1176
- source: "parallel",
1177
- defaultShouldRevalidate: true,
1178
- finalShouldRevalidate: true,
1179
- reason: "new-segment",
1180
- });
1181
- }
1182
- return true;
1183
- }
1199
+ const isFullRefetch = clientSegmentIds.size === 0;
1200
+ let shouldResolve: boolean;
1201
+ if (isFullRefetch) {
1202
+ // Same load-bearing rationale as the main parallel path: full refetch
1203
+ // means the client has nothing to fall back to, so the slot must render.
1204
+ traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
1205
+ shouldResolve = true;
1206
+ } else {
1207
+ // When slot is unknown to the client, seed the soft chain with `true`
1208
+ // (orphan parallels always belong to the route — we want them rendered
1209
+ // unless the user explicitly opts out via revalidate()).
1210
+ const defaultOverride = clientSegmentIds.has(parallelId)
1211
+ ? undefined
1212
+ : { value: true, reason: "new-segment" };
1184
1213
 
1185
1214
  const dummySegment: ResolvedSegment = {
1186
1215
  id: parallelId,
@@ -1197,7 +1226,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1197
1226
  : {}),
1198
1227
  };
1199
1228
 
1200
- return await evaluateRevalidation({
1229
+ shouldResolve = await evaluateRevalidation({
1201
1230
  segment: dummySegment,
1202
1231
  prevParams,
1203
1232
  getPrevSegment: null,
@@ -1213,8 +1242,9 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1213
1242
  actionContext,
1214
1243
  stale,
1215
1244
  traceSource: "parallel",
1245
+ defaultOverride,
1216
1246
  });
1217
- })();
1247
+ }
1218
1248
  emitRevalidationDecision(
1219
1249
  parallelId,
1220
1250
  context.pathname,
@@ -1223,6 +1253,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1223
1253
  );
1224
1254
 
1225
1255
  let component: ReactNode | undefined;
1256
+ let handlerRan = false;
1226
1257
  if (shouldResolve) {
1227
1258
  component = await tryStaticSlot(parallelEntry, slot, parallelId);
1228
1259
  }
@@ -1234,29 +1265,35 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1234
1265
  } else if (handler === undefined) {
1235
1266
  // Handler evicted (production static slot) but static lookup missed.
1236
1267
  component = null;
1237
- } else if (hasLoadingFallback) {
1238
- const result =
1239
- typeof handler === "function" ? handler(context) : handler;
1240
- if (result instanceof Promise) {
1241
- const tracked = deps.trackHandler(result, {
1242
- segmentId: parallelId,
1243
- segmentType: "parallel",
1244
- });
1245
- observeStreamedHandler(
1246
- tracked,
1247
- parallelId,
1248
- "parallel",
1249
- context.pathname,
1250
- routeKey,
1251
- params,
1252
- );
1253
- component = tracked as ReactNode;
1268
+ } else {
1269
+ // Slot-keyed pushes — see resolveParallelSegmentsWithRevalidation.
1270
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
1271
+ parallelId;
1272
+ handlerRan = true;
1273
+ if (hasLoadingFallback) {
1274
+ const result =
1275
+ typeof handler === "function" ? handler(context) : handler;
1276
+ if (result instanceof Promise) {
1277
+ const tracked = deps.trackHandler(result, {
1278
+ segmentId: parallelId,
1279
+ segmentType: "parallel",
1280
+ });
1281
+ observeStreamedHandler(
1282
+ tracked,
1283
+ parallelId,
1284
+ "parallel",
1285
+ context.pathname,
1286
+ routeKey,
1287
+ params,
1288
+ );
1289
+ component = tracked as ReactNode;
1290
+ } else {
1291
+ component = result as ReactNode;
1292
+ }
1254
1293
  } else {
1255
- component = result as ReactNode;
1294
+ component =
1295
+ typeof handler === "function" ? await handler(context) : handler;
1256
1296
  }
1257
- } else {
1258
- component =
1259
- typeof handler === "function" ? await handler(context) : handler;
1260
1297
  }
1261
1298
  }
1262
1299
 
@@ -1267,9 +1304,13 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1267
1304
  index: 0,
1268
1305
  component,
1269
1306
  loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1270
- transition: parallelEntry.transition,
1307
+ transition: applyViewTransitionDefault(
1308
+ parallelEntry.transition,
1309
+ deps.viewTransitionDefault,
1310
+ ),
1271
1311
  params,
1272
1312
  slot,
1313
+ _handlerRan: handlerRan,
1273
1314
  belongsToRoute,
1274
1315
  parallelName: `${parallelEntry.id}.${slot}`,
1275
1316
  ...(parallelEntry.mountPath
@@ -1328,7 +1369,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1328
1369
 
1329
1370
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1330
1371
  if (entry.type === "cache") {
1331
- const store = RSCRouterContext.getStore();
1372
+ const store = RangoContext.getStore();
1332
1373
  if (store) store.insideCacheScope = true;
1333
1374
  }
1334
1375
  const doneEntry = track(`segment:${entry.id}`, 1);
@@ -0,0 +1,36 @@
1
+ /**
2
+ * View-transition boundary default resolution.
3
+ *
4
+ * Kept in its own module (rather than helpers.ts) because several resolution
5
+ * tests mock helpers.ts with an explicit export list; a shared util here is
6
+ * never mocked, so the fresh and revalidation paths always get the real
7
+ * implementation.
8
+ */
9
+
10
+ import type { EntryData } from "../../server/context";
11
+
12
+ /**
13
+ * Resolve the effective `viewTransition` for a segment's transition config.
14
+ *
15
+ * The per-segment value (set via the transition() DSL) always wins. When it is
16
+ * unset, the router-level createRouter({ viewTransition }) default is stamped
17
+ * in so the render gate reads the boundary decision off the segment — server
18
+ * and client, via the serialized segment — without the router option being
19
+ * threaded to the client. Only `false` is ever stamped; an unset (or "auto")
20
+ * value is left untouched because it already means "wrap" at the gate, which
21
+ * also avoids needless object allocation and payload growth. Used by both the
22
+ * fresh and revalidation resolution paths.
23
+ */
24
+ export function applyViewTransitionDefault(
25
+ transition: EntryData["transition"],
26
+ viewTransitionDefault: "auto" | false | undefined,
27
+ ): EntryData["transition"] {
28
+ if (!transition) return transition;
29
+ if (
30
+ transition.viewTransition === undefined &&
31
+ viewTransitionDefault === false
32
+ ) {
33
+ return { ...transition, viewTransition: false };
34
+ }
35
+ return transition;
36
+ }
@@ -0,0 +1,56 @@
1
+ import { encodePathSegment } from "./url-params.js";
2
+
3
+ /**
4
+ * Substitute `:param` placeholders in a route pattern with values from
5
+ * `params`. Two-pass: optional params (`:name?`) first so absent values
6
+ * collapse cleanly, then required params (throws on missing). Constraint
7
+ * syntax (`:name(en|gb)`) is stripped from the result. Trailing-slash
8
+ * patterns like `/blog/` are preserved unless an optional segment was
9
+ * actually omitted.
10
+ *
11
+ * Shared by `ctx.reverse()` (server), `createReverse()` (typed runtime
12
+ * helper), and `useReverse()` (client hook). The behavior must stay
13
+ * identical across all three call sites.
14
+ */
15
+ export function substitutePatternParams(
16
+ pattern: string,
17
+ params: Record<string, string | undefined>,
18
+ routeName: string,
19
+ ): string {
20
+ let result = pattern;
21
+ let hadOmittedOptional = false;
22
+
23
+ result = result.replace(
24
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
25
+ (_match, key) => {
26
+ const value = params[key as string];
27
+ // The matcher omits absent optional params (so `value` is `undefined`
28
+ // here), but caller-supplied params or `getParams()` shapes may still
29
+ // pass `""` explicitly. Treat both as the absent form.
30
+ if (value === undefined || value === "") {
31
+ hadOmittedOptional = true;
32
+ return "";
33
+ }
34
+ return encodePathSegment(value);
35
+ },
36
+ );
37
+
38
+ result = result.replace(
39
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
40
+ (_match, key) => {
41
+ const value = params[key as string];
42
+ if (value === undefined) {
43
+ throw new Error(`Missing param "${key}" for route "${routeName}"`);
44
+ }
45
+ return encodePathSegment(value);
46
+ },
47
+ );
48
+
49
+ if (hadOmittedOptional) {
50
+ const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
51
+ result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
52
+ if (hadTrailingSlash && !result.endsWith("/")) result += "/";
53
+ }
54
+
55
+ return result;
56
+ }
@@ -90,6 +90,34 @@ export interface HandlerErrorEvent extends BaseEvent {
90
90
  params?: Record<string, string>;
91
91
  }
92
92
 
93
+ /**
94
+ * Per-segment (or coarse route-level) cache status carried on the
95
+ * cache.decision telemetry event and the X-Rango-Cache debug header.
96
+ *
97
+ * v1 is COARSE: the router's pipeline tracks cache decisions at the
98
+ * route/entry level (cacheHit/cacheSource/shouldRevalidate), not per
99
+ * individual segment. The `segments` array therefore contains a single
100
+ * route-level entry keyed by the route key. The shape is forward-compatible
101
+ * with genuine per-segment status if the pipeline later exposes it.
102
+ */
103
+ export type CacheSegmentStatus =
104
+ | "hit"
105
+ | "miss"
106
+ | "stale"
107
+ | "prerendered"
108
+ | "passthrough";
109
+
110
+ export interface CacheSegmentSignal {
111
+ /** Segment id (v1: the route key, since status is route-level). */
112
+ id: string;
113
+ /** Segment type (v1: "route" for the coarse route-level entry). */
114
+ type: string;
115
+ /** Resolved cache status for this segment. */
116
+ cacheStatus: CacheSegmentStatus;
117
+ /** Whether stale-while-revalidate was triggered for this segment. */
118
+ shouldRevalidate?: boolean;
119
+ }
120
+
93
121
  export interface CacheDecisionEvent extends BaseEvent {
94
122
  type: "cache.decision";
95
123
  pathname: string;
@@ -98,6 +126,12 @@ export interface CacheDecisionEvent extends BaseEvent {
98
126
  /** Whether stale-while-revalidate was triggered */
99
127
  shouldRevalidate: boolean;
100
128
  source?: "runtime" | "prerender";
129
+ /**
130
+ * Optional per-segment (v1: coarse route-level) cache status. Present only
131
+ * when telemetry or the debug cache signal is enabled. Optional so existing
132
+ * sinks are unaffected.
133
+ */
134
+ segments?: CacheSegmentSignal[];
101
135
  }
102
136
 
103
137
  export interface RevalidationDecisionEvent extends BaseEvent {
@@ -140,6 +174,71 @@ export type TelemetryEvent =
140
174
  | RequestTimeoutEvent
141
175
  | OriginCheckRejectedEvent;
142
176
 
177
+ // ---------------------------------------------------------------------------
178
+ // Cache signal derivation (coarse, route-level)
179
+ // ---------------------------------------------------------------------------
180
+
181
+ /**
182
+ * Derive the coarse, route-level cache status from pipeline cache state.
183
+ *
184
+ * v1 mapping (route-level — see CacheSegmentSignal):
185
+ * - prerender hit -> "prerendered"
186
+ * - runtime hit + shouldRevalidate (SWR) -> "stale"
187
+ * - runtime hit -> "hit"
188
+ * - no hit -> "miss"
189
+ *
190
+ * Note: "passthrough" is a build-time prerender concept (a route opts out of
191
+ * being prerendered for some params). At runtime a passthrough route renders
192
+ * fresh and is indistinguishable from a normal miss in the pipeline state, so
193
+ * v1 reports it as "miss". The "passthrough" status remains in the type union
194
+ * for forward compatibility.
195
+ */
196
+ export function deriveCacheStatus(state: {
197
+ cacheHit: boolean;
198
+ cacheSource?: "runtime" | "prerender";
199
+ shouldRevalidate?: boolean;
200
+ }): CacheSegmentStatus {
201
+ if (state.cacheHit) {
202
+ if (state.cacheSource === "prerender") return "prerendered";
203
+ if (state.shouldRevalidate) return "stale";
204
+ return "hit";
205
+ }
206
+ return "miss";
207
+ }
208
+
209
+ /**
210
+ * Build the coarse route-level cache signal array (a single entry keyed by
211
+ * the route key). Used for both the cache.decision telemetry event and the
212
+ * X-Rango-Cache debug header.
213
+ */
214
+ export function buildCacheSignalSegments(
215
+ routeKey: string,
216
+ state: {
217
+ cacheHit: boolean;
218
+ cacheSource?: "runtime" | "prerender";
219
+ shouldRevalidate?: boolean;
220
+ },
221
+ ): CacheSegmentSignal[] {
222
+ return [
223
+ {
224
+ id: routeKey,
225
+ type: "route",
226
+ cacheStatus: deriveCacheStatus(state),
227
+ shouldRevalidate: !!state.shouldRevalidate,
228
+ },
229
+ ];
230
+ }
231
+
232
+ /**
233
+ * Serialize cache signal segments into the X-Rango-Cache header value:
234
+ * `<segId>=<status>, <segId2>=<status2>`.
235
+ */
236
+ export function formatCacheSignalHeader(
237
+ segments: CacheSegmentSignal[],
238
+ ): string {
239
+ return segments.map((s) => `${s.id}=${s.cacheStatus}`).join(", ");
240
+ }
241
+
143
242
  // ---------------------------------------------------------------------------
144
243
  // Sink interface
145
244
  // ---------------------------------------------------------------------------