@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100

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 (329) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +1037 -4
  3. package/dist/bin/rango.js +1619 -157
  4. package/dist/vite/index.js +5762 -2301
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +71 -63
  7. package/skills/breadcrumbs/SKILL.md +252 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +6 -4
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +367 -71
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +176 -8
  19. package/skills/layout/SKILL.md +124 -3
  20. package/skills/links/SKILL.md +304 -25
  21. package/skills/loader/SKILL.md +474 -47
  22. package/skills/middleware/SKILL.md +207 -37
  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 +15 -11
  26. package/skills/parallel/SKILL.md +272 -1
  27. package/skills/prerender/SKILL.md +467 -65
  28. package/skills/rango/SKILL.md +89 -21
  29. package/skills/response-routes/SKILL.md +152 -91
  30. package/skills/route/SKILL.md +305 -14
  31. package/skills/router-setup/SKILL.md +210 -32
  32. package/skills/server-actions/SKILL.md +739 -0
  33. package/skills/streams-and-websockets/SKILL.md +283 -0
  34. package/skills/theme/SKILL.md +9 -8
  35. package/skills/typesafety/SKILL.md +333 -86
  36. package/skills/use-cache/SKILL.md +324 -0
  37. package/skills/view-transitions/SKILL.md +212 -0
  38. package/src/__internal.ts +102 -4
  39. package/src/bin/rango.ts +312 -15
  40. package/src/browser/action-coordinator.ts +97 -0
  41. package/src/browser/action-response-classifier.ts +99 -0
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/app-version.ts +14 -0
  44. package/src/browser/event-controller.ts +136 -68
  45. package/src/browser/history-state.ts +80 -0
  46. package/src/browser/intercept-utils.ts +52 -0
  47. package/src/browser/link-interceptor.ts +24 -4
  48. package/src/browser/logging.ts +55 -0
  49. package/src/browser/merge-segment-loaders.ts +20 -12
  50. package/src/browser/navigation-bridge.ts +374 -561
  51. package/src/browser/navigation-client.ts +228 -70
  52. package/src/browser/navigation-store.ts +97 -55
  53. package/src/browser/navigation-transaction.ts +297 -0
  54. package/src/browser/network-error-handler.ts +61 -0
  55. package/src/browser/partial-update.ts +376 -315
  56. package/src/browser/prefetch/cache.ts +314 -0
  57. package/src/browser/prefetch/fetch.ts +282 -0
  58. package/src/browser/prefetch/observer.ts +65 -0
  59. package/src/browser/prefetch/policy.ts +48 -0
  60. package/src/browser/prefetch/queue.ts +191 -0
  61. package/src/browser/prefetch/resource-ready.ts +77 -0
  62. package/src/browser/rango-state.ts +152 -0
  63. package/src/browser/react/Link.tsx +255 -71
  64. package/src/browser/react/NavigationProvider.tsx +152 -24
  65. package/src/browser/react/context.ts +11 -0
  66. package/src/browser/react/filter-segment-order.ts +55 -0
  67. package/src/browser/react/index.ts +15 -12
  68. package/src/browser/react/location-state-shared.ts +95 -53
  69. package/src/browser/react/location-state.ts +60 -15
  70. package/src/browser/react/mount-context.ts +6 -1
  71. package/src/browser/react/nonce-context.ts +23 -0
  72. package/src/browser/react/shallow-equal.ts +27 -0
  73. package/src/browser/react/use-action.ts +29 -51
  74. package/src/browser/react/use-client-cache.ts +5 -3
  75. package/src/browser/react/use-handle.ts +30 -120
  76. package/src/browser/react/use-link-status.ts +6 -5
  77. package/src/browser/react/use-navigation.ts +44 -65
  78. package/src/browser/react/use-params.ts +78 -0
  79. package/src/browser/react/use-pathname.ts +47 -0
  80. package/src/browser/react/use-reverse.ts +99 -0
  81. package/src/browser/react/use-router.ts +83 -0
  82. package/src/browser/react/use-search-params.ts +56 -0
  83. package/src/browser/react/use-segments.ts +85 -99
  84. package/src/browser/response-adapter.ts +73 -0
  85. package/src/browser/rsc-router.tsx +246 -64
  86. package/src/browser/scroll-restoration.ts +127 -52
  87. package/src/browser/segment-reconciler.ts +243 -0
  88. package/src/browser/segment-structure-assert.ts +16 -0
  89. package/src/browser/server-action-bridge.ts +510 -603
  90. package/src/browser/shallow.ts +6 -1
  91. package/src/browser/types.ts +158 -48
  92. package/src/browser/validate-redirect-origin.ts +29 -0
  93. package/src/build/generate-manifest.ts +84 -23
  94. package/src/build/generate-route-types.ts +39 -828
  95. package/src/build/index.ts +4 -5
  96. package/src/build/route-trie.ts +85 -32
  97. package/src/build/route-types/ast-helpers.ts +25 -0
  98. package/src/build/route-types/ast-route-extraction.ts +98 -0
  99. package/src/build/route-types/codegen.ts +102 -0
  100. package/src/build/route-types/include-resolution.ts +418 -0
  101. package/src/build/route-types/param-extraction.ts +48 -0
  102. package/src/build/route-types/per-module-writer.ts +128 -0
  103. package/src/build/route-types/router-processing.ts +618 -0
  104. package/src/build/route-types/scan-filter.ts +85 -0
  105. package/src/build/runtime-discovery.ts +231 -0
  106. package/src/cache/background-task.ts +34 -0
  107. package/src/cache/cache-key-utils.ts +44 -0
  108. package/src/cache/cache-policy.ts +125 -0
  109. package/src/cache/cache-runtime.ts +342 -0
  110. package/src/cache/cache-scope.ts +167 -307
  111. package/src/cache/cf/cf-cache-store.ts +573 -21
  112. package/src/cache/cf/index.ts +13 -3
  113. package/src/cache/document-cache.ts +116 -77
  114. package/src/cache/handle-capture.ts +81 -0
  115. package/src/cache/handle-snapshot.ts +41 -0
  116. package/src/cache/index.ts +1 -15
  117. package/src/cache/memory-segment-store.ts +191 -13
  118. package/src/cache/profile-registry.ts +73 -0
  119. package/src/cache/read-through-swr.ts +134 -0
  120. package/src/cache/segment-codec.ts +256 -0
  121. package/src/cache/taint.ts +153 -0
  122. package/src/cache/types.ts +72 -122
  123. package/src/client.rsc.tsx +6 -1
  124. package/src/client.tsx +118 -302
  125. package/src/component-utils.ts +4 -4
  126. package/src/components/DefaultDocument.tsx +5 -1
  127. package/src/context-var.ts +156 -0
  128. package/src/debug.ts +19 -9
  129. package/src/errors.ts +77 -7
  130. package/src/handle.ts +55 -10
  131. package/src/handles/MetaTags.tsx +73 -20
  132. package/src/handles/breadcrumbs.ts +66 -0
  133. package/src/handles/index.ts +1 -0
  134. package/src/handles/meta.ts +30 -13
  135. package/src/host/cookie-handler.ts +21 -15
  136. package/src/host/errors.ts +8 -8
  137. package/src/host/index.ts +4 -7
  138. package/src/host/pattern-matcher.ts +27 -27
  139. package/src/host/router.ts +61 -39
  140. package/src/host/testing.ts +8 -8
  141. package/src/host/types.ts +15 -7
  142. package/src/host/utils.ts +1 -1
  143. package/src/href-client.ts +65 -45
  144. package/src/index.rsc.ts +138 -21
  145. package/src/index.ts +206 -51
  146. package/src/internal-debug.ts +11 -0
  147. package/src/loader.rsc.ts +25 -143
  148. package/src/loader.ts +27 -10
  149. package/src/network-error-thrower.tsx +3 -1
  150. package/src/outlet-context.ts +1 -1
  151. package/src/outlet-provider.tsx +45 -0
  152. package/src/prerender/param-hash.ts +4 -2
  153. package/src/prerender/store.ts +159 -13
  154. package/src/prerender.ts +397 -29
  155. package/src/response-utils.ts +28 -0
  156. package/src/reverse.ts +231 -121
  157. package/src/root-error-boundary.tsx +41 -29
  158. package/src/route-content-wrapper.tsx +7 -4
  159. package/src/route-definition/dsl-helpers.ts +1134 -0
  160. package/src/route-definition/helper-factories.ts +200 -0
  161. package/src/route-definition/helpers-types.ts +483 -0
  162. package/src/route-definition/index.ts +55 -0
  163. package/src/route-definition/redirect.ts +101 -0
  164. package/src/route-definition/resolve-handler-use.ts +155 -0
  165. package/src/route-definition.ts +1 -1431
  166. package/src/route-map-builder.ts +162 -123
  167. package/src/route-name.ts +53 -0
  168. package/src/route-types.ts +66 -9
  169. package/src/router/content-negotiation.ts +215 -0
  170. package/src/router/debug-manifest.ts +72 -0
  171. package/src/router/error-handling.ts +9 -9
  172. package/src/router/find-match.ts +160 -0
  173. package/src/router/handler-context.ts +418 -86
  174. package/src/router/intercept-resolution.ts +35 -20
  175. package/src/router/lazy-includes.ts +237 -0
  176. package/src/router/loader-resolution.ts +359 -128
  177. package/src/router/logging.ts +251 -0
  178. package/src/router/manifest.ts +98 -32
  179. package/src/router/match-api.ts +196 -261
  180. package/src/router/match-context.ts +4 -2
  181. package/src/router/match-handlers.ts +441 -0
  182. package/src/router/match-middleware/background-revalidation.ts +108 -93
  183. package/src/router/match-middleware/cache-lookup.ts +415 -86
  184. package/src/router/match-middleware/cache-store.ts +91 -29
  185. package/src/router/match-middleware/intercept-resolution.ts +48 -21
  186. package/src/router/match-middleware/segment-resolution.ts +73 -9
  187. package/src/router/match-pipelines.ts +10 -45
  188. package/src/router/match-result.ts +154 -35
  189. package/src/router/metrics.ts +240 -15
  190. package/src/router/middleware-cookies.ts +55 -0
  191. package/src/router/middleware-types.ts +209 -0
  192. package/src/router/middleware.ts +373 -371
  193. package/src/router/navigation-snapshot.ts +182 -0
  194. package/src/router/pattern-matching.ts +292 -52
  195. package/src/router/prerender-match.ts +502 -0
  196. package/src/router/preview-match.ts +98 -0
  197. package/src/router/request-classification.ts +310 -0
  198. package/src/router/revalidation.ts +152 -39
  199. package/src/router/route-snapshot.ts +245 -0
  200. package/src/router/router-context.ts +41 -21
  201. package/src/router/router-interfaces.ts +484 -0
  202. package/src/router/router-options.ts +618 -0
  203. package/src/router/router-registry.ts +24 -0
  204. package/src/router/segment-resolution/fresh.ts +756 -0
  205. package/src/router/segment-resolution/helpers.ts +268 -0
  206. package/src/router/segment-resolution/loader-cache.ts +199 -0
  207. package/src/router/segment-resolution/revalidation.ts +1407 -0
  208. package/src/router/segment-resolution/static-store.ts +67 -0
  209. package/src/router/segment-resolution.ts +21 -1315
  210. package/src/router/segment-wrappers.ts +291 -0
  211. package/src/router/substitute-pattern-params.ts +56 -0
  212. package/src/router/telemetry-otel.ts +299 -0
  213. package/src/router/telemetry.ts +300 -0
  214. package/src/router/timeout.ts +148 -0
  215. package/src/router/trie-matching.ts +111 -39
  216. package/src/router/types.ts +17 -9
  217. package/src/router/url-params.ts +49 -0
  218. package/src/router.ts +642 -2011
  219. package/src/rsc/handler-context.ts +45 -0
  220. package/src/rsc/handler.ts +864 -1114
  221. package/src/rsc/helpers.ts +181 -19
  222. package/src/rsc/index.ts +0 -20
  223. package/src/rsc/loader-fetch.ts +229 -0
  224. package/src/rsc/manifest-init.ts +90 -0
  225. package/src/rsc/nonce.ts +14 -0
  226. package/src/rsc/origin-guard.ts +141 -0
  227. package/src/rsc/progressive-enhancement.ts +395 -0
  228. package/src/rsc/response-error.ts +37 -0
  229. package/src/rsc/response-route-handler.ts +360 -0
  230. package/src/rsc/rsc-rendering.ts +256 -0
  231. package/src/rsc/runtime-warnings.ts +42 -0
  232. package/src/rsc/server-action.ts +360 -0
  233. package/src/rsc/ssr-setup.ts +128 -0
  234. package/src/rsc/types.ts +52 -11
  235. package/src/search-params.ts +230 -0
  236. package/src/segment-content-promise.ts +67 -0
  237. package/src/segment-loader-promise.ts +122 -0
  238. package/src/segment-system.tsx +187 -38
  239. package/src/server/context.ts +333 -59
  240. package/src/server/cookie-store.ts +190 -0
  241. package/src/server/fetchable-loader-store.ts +37 -0
  242. package/src/server/handle-store.ts +113 -15
  243. package/src/server/loader-registry.ts +24 -64
  244. package/src/server/request-context.ts +603 -109
  245. package/src/server.ts +35 -155
  246. package/src/ssr/index.tsx +107 -30
  247. package/src/static-handler.ts +126 -0
  248. package/src/theme/ThemeProvider.tsx +21 -15
  249. package/src/theme/ThemeScript.tsx +5 -5
  250. package/src/theme/constants.ts +5 -2
  251. package/src/theme/index.ts +4 -14
  252. package/src/theme/theme-context.ts +4 -30
  253. package/src/theme/theme-script.ts +21 -18
  254. package/src/types/boundaries.ts +158 -0
  255. package/src/types/cache-types.ts +198 -0
  256. package/src/types/error-types.ts +192 -0
  257. package/src/types/global-namespace.ts +100 -0
  258. package/src/types/handler-context.ts +764 -0
  259. package/src/types/index.ts +88 -0
  260. package/src/types/loader-types.ts +209 -0
  261. package/src/types/request-scope.ts +126 -0
  262. package/src/types/route-config.ts +170 -0
  263. package/src/types/route-entry.ts +120 -0
  264. package/src/types/segments.ts +167 -0
  265. package/src/types.ts +1 -1757
  266. package/src/urls/include-helper.ts +207 -0
  267. package/src/urls/index.ts +53 -0
  268. package/src/urls/path-helper-types.ts +372 -0
  269. package/src/urls/path-helper.ts +364 -0
  270. package/src/urls/pattern-types.ts +107 -0
  271. package/src/urls/response-types.ts +108 -0
  272. package/src/urls/type-extraction.ts +372 -0
  273. package/src/urls/urls-function.ts +98 -0
  274. package/src/urls.ts +1 -1282
  275. package/src/use-loader.tsx +161 -81
  276. package/src/vite/debug.ts +184 -0
  277. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  278. package/src/vite/discovery/discover-routers.ts +376 -0
  279. package/src/vite/discovery/gate-state.ts +171 -0
  280. package/src/vite/discovery/prerender-collection.ts +486 -0
  281. package/src/vite/discovery/route-types-writer.ts +258 -0
  282. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  283. package/src/vite/discovery/state.ts +117 -0
  284. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  285. package/src/vite/index.ts +15 -2063
  286. package/src/vite/plugin-types.ts +103 -0
  287. package/src/vite/plugins/cjs-to-esm.ts +98 -0
  288. package/src/vite/plugins/client-ref-dedup.ts +131 -0
  289. package/src/vite/plugins/client-ref-hashing.ts +117 -0
  290. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  291. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  292. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  293. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
  294. package/src/vite/plugins/expose-id-utils.ts +299 -0
  295. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  296. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  297. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  298. package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
  299. package/src/vite/plugins/expose-ids/types.ts +45 -0
  300. package/src/vite/plugins/expose-internal-ids.ts +816 -0
  301. package/src/vite/plugins/performance-tracks.ts +96 -0
  302. package/src/vite/plugins/refresh-cmd.ts +127 -0
  303. package/src/vite/plugins/use-cache-transform.ts +336 -0
  304. package/src/vite/plugins/version-injector.ts +109 -0
  305. package/src/vite/plugins/version-plugin.ts +266 -0
  306. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  307. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  308. package/src/vite/rango.ts +497 -0
  309. package/src/vite/router-discovery.ts +1423 -0
  310. package/src/vite/utils/ast-handler-extract.ts +517 -0
  311. package/src/vite/utils/banner.ts +36 -0
  312. package/src/vite/utils/bundle-analysis.ts +137 -0
  313. package/src/vite/utils/manifest-utils.ts +70 -0
  314. package/src/vite/utils/package-resolution.ts +161 -0
  315. package/src/vite/utils/prerender-utils.ts +222 -0
  316. package/src/vite/utils/shared-utils.ts +170 -0
  317. package/CLAUDE.md +0 -43
  318. package/src/browser/lru-cache.ts +0 -69
  319. package/src/browser/request-controller.ts +0 -164
  320. package/src/cache/memory-store.ts +0 -253
  321. package/src/href-context.ts +0 -33
  322. package/src/router.gen.ts +0 -6
  323. package/src/urls.gen.ts +0 -8
  324. package/src/vite/expose-handle-id.ts +0 -209
  325. package/src/vite/expose-loader-id.ts +0 -426
  326. package/src/vite/expose-location-state-id.ts +0 -177
  327. package/src/vite/expose-prerender-handler-id.ts +0 -429
  328. package/src/vite/package-resolution.ts +0 -125
  329. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,1407 @@
1
+ /**
2
+ * Revalidation Path Segment Resolution
3
+ *
4
+ * Functions for resolving segments during partial (revalidation) requests.
5
+ * Mirrors the fresh path but adds revalidation awareness: only re-resolves
6
+ * segments whose revalidate() predicate returns true.
7
+ */
8
+
9
+ import type { ReactNode } from "react";
10
+ import { invariant } from "../../errors";
11
+ import { revalidate } from "../loader-resolution.js";
12
+ import { evaluateRevalidation } from "../revalidation.js";
13
+ import {
14
+ getParallelEntries,
15
+ getParallelSlotEntries,
16
+ type EntryData,
17
+ } from "../../server/context";
18
+ import type {
19
+ HandlerContext,
20
+ InternalHandlerContext,
21
+ ResolvedSegment,
22
+ ShouldRevalidateFn,
23
+ } from "../../types";
24
+ import type {
25
+ SegmentResolutionDeps,
26
+ SegmentRevalidationResult,
27
+ ActionContext,
28
+ } from "../types.js";
29
+ import {
30
+ debugLog,
31
+ pushRevalidationTraceEntry,
32
+ isTraceActive,
33
+ } from "../logging.js";
34
+ import { resolveLoaderData } from "./loader-cache.js";
35
+ import {
36
+ handleHandlerResult,
37
+ tryStaticHandler,
38
+ tryStaticSlot,
39
+ resolveLayoutComponent,
40
+ resolveWithErrorBoundary,
41
+ } from "./helpers.js";
42
+ import { getRouterContext } from "../router-context.js";
43
+ import { resolveSink, safeEmit } from "../telemetry.js";
44
+ import {
45
+ track,
46
+ RSCRouterContext,
47
+ runInsideLoaderScope,
48
+ } from "../../server/context.js";
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Telemetry helpers
52
+ // ---------------------------------------------------------------------------
53
+
54
+ /**
55
+ * Attach a fire-and-forget rejection observer to a streamed handler promise.
56
+ * Silently no-ops when called outside RouterContext (e.g. in unit tests).
57
+ */
58
+ function observeStreamedHandler(
59
+ promise: Promise<ReactNode>,
60
+ segmentId: string,
61
+ segmentType: string,
62
+ pathname?: string,
63
+ routeKey?: string,
64
+ params?: Record<string, string>,
65
+ ): void {
66
+ let routerCtx;
67
+ try {
68
+ routerCtx = getRouterContext();
69
+ } catch {
70
+ return;
71
+ }
72
+ if (!routerCtx?.telemetry) return;
73
+ const sink = resolveSink(routerCtx.telemetry);
74
+ const reqId = routerCtx.requestId;
75
+ promise.catch((err: unknown) => {
76
+ const errorObj = err instanceof Error ? err : new Error(String(err));
77
+ safeEmit(sink, {
78
+ type: "handler.error",
79
+ timestamp: performance.now(),
80
+ requestId: reqId,
81
+ segmentId,
82
+ segmentType,
83
+ error: errorObj,
84
+ handledByBoundary: true,
85
+ pathname,
86
+ routeKey,
87
+ params,
88
+ });
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Trace a parallel slot that's being force-rendered on a full refetch (client
94
+ * has no cached state). User revalidate fns are bypassed in this case — see
95
+ * the call sites for the load-bearing rationale.
96
+ */
97
+ function traceFullRefetchedParallelSlot(
98
+ parallelId: string,
99
+ belongsToRoute: boolean,
100
+ ): void {
101
+ if (!isTraceActive()) return;
102
+ pushRevalidationTraceEntry({
103
+ segmentId: parallelId,
104
+ segmentType: "parallel",
105
+ belongsToRoute,
106
+ source: "parallel",
107
+ defaultShouldRevalidate: true,
108
+ finalShouldRevalidate: true,
109
+ reason: "full-refetch",
110
+ });
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Revalidation telemetry helper
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Emit revalidation.decision telemetry for a segment if a sink is configured.
119
+ * Called after evaluateRevalidation returns to capture the decision.
120
+ * Silently no-ops when called outside RouterContext (e.g. in unit tests).
121
+ */
122
+ function emitRevalidationDecision(
123
+ segmentId: string,
124
+ pathname: string,
125
+ routeKey: string,
126
+ shouldRevalidate: boolean,
127
+ ): void {
128
+ let routerCtx;
129
+ try {
130
+ routerCtx = getRouterContext();
131
+ } catch {
132
+ return;
133
+ }
134
+ if (routerCtx?.telemetry) {
135
+ safeEmit(resolveSink(routerCtx.telemetry), {
136
+ type: "revalidation.decision",
137
+ timestamp: performance.now(),
138
+ requestId: routerCtx.requestId,
139
+ segmentId,
140
+ pathname,
141
+ routeKey,
142
+ shouldRevalidate,
143
+ });
144
+ }
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Revalidation path (partial match)
149
+ // ---------------------------------------------------------------------------
150
+
151
+ /**
152
+ * Resolve loaders with revalidation awareness (for partial rendering).
153
+ * Returns both segments to render AND all matched segment IDs.
154
+ */
155
+ export async function resolveLoadersWithRevalidation<TEnv>(
156
+ entry: EntryData,
157
+ ctx: HandlerContext<any, TEnv>,
158
+ belongsToRoute: boolean,
159
+ clientSegmentIds: Set<string>,
160
+ prevParams: Record<string, string>,
161
+ request: Request,
162
+ prevUrl: URL,
163
+ nextUrl: URL,
164
+ routeKey: string,
165
+ deps: SegmentResolutionDeps<TEnv>,
166
+ actionContext?: ActionContext,
167
+ shortCodeOverride?: string,
168
+ stale?: boolean,
169
+ ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
170
+ const loaderEntries = entry.loader ?? [];
171
+ if (loaderEntries.length === 0) return { segments: [], matchedIds: [] };
172
+
173
+ const shortCode = shortCodeOverride ?? entry.shortCode;
174
+
175
+ const loaderMeta = loaderEntries.map((loaderEntry, i) => ({
176
+ loaderEntry,
177
+ loader: loaderEntry.loader,
178
+ loaderRevalidateFns: loaderEntry.revalidate,
179
+ segmentId: `${shortCode}D${i}.${loaderEntry.loader.$$id}`,
180
+ index: i,
181
+ }));
182
+
183
+ const matchedIds = loaderMeta.map((m) => m.segmentId);
184
+
185
+ const revalidationChecks = await Promise.all(
186
+ loaderMeta.map(
187
+ async ({
188
+ loaderEntry,
189
+ loader,
190
+ loaderRevalidateFns,
191
+ segmentId,
192
+ index,
193
+ }) => {
194
+ const shouldRun = await revalidate(
195
+ async () => {
196
+ if (!clientSegmentIds.has(segmentId)) {
197
+ if (isTraceActive()) {
198
+ pushRevalidationTraceEntry({
199
+ segmentId,
200
+ segmentType: "loader",
201
+ belongsToRoute,
202
+ source: "loader",
203
+ defaultShouldRevalidate: true,
204
+ finalShouldRevalidate: true,
205
+ reason: "new-segment",
206
+ });
207
+ }
208
+ return true;
209
+ }
210
+
211
+ const dummySegment: ResolvedSegment = {
212
+ id: segmentId,
213
+ namespace: entry.id,
214
+ type: "loader",
215
+ index,
216
+ component: null,
217
+ params: ctx.params,
218
+ loaderId: loader.$$id,
219
+ belongsToRoute,
220
+ };
221
+
222
+ return await evaluateRevalidation({
223
+ segment: dummySegment,
224
+ prevParams,
225
+ getPrevSegment: null,
226
+ request,
227
+ prevUrl,
228
+ nextUrl,
229
+ revalidations: loaderRevalidateFns.map((fn, j) => ({
230
+ name: `loader-revalidate${j}`,
231
+ fn,
232
+ })),
233
+ routeKey,
234
+ context: ctx,
235
+ actionContext,
236
+ stale,
237
+ traceSource: "loader",
238
+ });
239
+ },
240
+ async () => true,
241
+ () => false,
242
+ );
243
+ emitRevalidationDecision(segmentId, ctx.pathname, routeKey, shouldRun);
244
+ return { shouldRun, loaderEntry, loader, segmentId, index };
245
+ },
246
+ ),
247
+ );
248
+
249
+ const loadersToRun = revalidationChecks.filter((c) => c.shouldRun);
250
+ const segments: ResolvedSegment[] = loadersToRun.map(
251
+ ({ loaderEntry, loader, segmentId, index }) => ({
252
+ id: segmentId,
253
+ namespace: entry.id,
254
+ type: "loader" as const,
255
+ index,
256
+ component: null,
257
+ params: ctx.params,
258
+ loaderId: loader.$$id,
259
+ loaderData: deps.wrapLoaderPromise(
260
+ runInsideLoaderScope(() =>
261
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
262
+ ),
263
+ entry,
264
+ segmentId,
265
+ ctx.pathname,
266
+ ),
267
+ belongsToRoute,
268
+ }),
269
+ );
270
+
271
+ return { segments, matchedIds };
272
+ }
273
+
274
+ /**
275
+ * Resolve only loader segments for all entries with revalidation logic.
276
+ */
277
+ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
278
+ entries: EntryData[],
279
+ context: HandlerContext<any, TEnv>,
280
+ clientSegmentIds: Set<string>,
281
+ prevParams: Record<string, string>,
282
+ request: Request,
283
+ prevUrl: URL,
284
+ nextUrl: URL,
285
+ routeKey: string,
286
+ deps: SegmentResolutionDeps<TEnv>,
287
+ actionContext?: ActionContext,
288
+ stale?: boolean,
289
+ ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
290
+ const allLoaderSegments: ResolvedSegment[] = [];
291
+ const allMatchedIds: string[] = [];
292
+ const seenIds = new Set<string>();
293
+
294
+ async function collectEntryLoaders(
295
+ entry: EntryData,
296
+ belongsToRoute: boolean,
297
+ shortCodeOverride?: string,
298
+ ): Promise<void> {
299
+ // Skip if all loaders from this entry have already been resolved
300
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
301
+ const loaderEntries = entry.loader ?? [];
302
+ const sc = shortCodeOverride ?? entry.shortCode;
303
+ const allAlreadySeen =
304
+ loaderEntries.length > 0 &&
305
+ loaderEntries.every((le, i) =>
306
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
307
+ );
308
+ if (!allAlreadySeen) {
309
+ const { segments, matchedIds } = await resolveLoadersWithRevalidation(
310
+ entry,
311
+ context,
312
+ belongsToRoute,
313
+ clientSegmentIds,
314
+ prevParams,
315
+ request,
316
+ prevUrl,
317
+ nextUrl,
318
+ routeKey,
319
+ deps,
320
+ actionContext,
321
+ shortCodeOverride,
322
+ stale,
323
+ );
324
+ for (const seg of segments) {
325
+ if (!seenIds.has(seg.id)) {
326
+ seenIds.add(seg.id);
327
+ allLoaderSegments.push(seg);
328
+ }
329
+ }
330
+ allMatchedIds.push(...matchedIds);
331
+ }
332
+
333
+ const seenParallelEntryIds = new Set<string>();
334
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
335
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
336
+ seenParallelEntryIds.add(parallelEntry.id);
337
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
338
+ }
339
+
340
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
341
+ for (const layoutEntry of entry.layout) {
342
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
343
+ // Inherit route loaders for orphan layouts with parallels.
344
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
345
+ // route entry, as that would re-iterate route.layout and loop.
346
+ if (
347
+ entry.type === "route" &&
348
+ entry.loader &&
349
+ entry.loader.length > 0 &&
350
+ Object.keys(layoutEntry.parallel).length > 0
351
+ ) {
352
+ const inherited = await resolveLoadersWithRevalidation(
353
+ entry,
354
+ context,
355
+ childBelongsToRoute,
356
+ clientSegmentIds,
357
+ prevParams,
358
+ request,
359
+ prevUrl,
360
+ nextUrl,
361
+ routeKey,
362
+ deps,
363
+ actionContext,
364
+ layoutEntry.shortCode,
365
+ stale,
366
+ );
367
+ for (const seg of inherited.segments) {
368
+ if (!seenIds.has(seg.id)) {
369
+ seenIds.add(seg.id);
370
+ seg._inherited = true;
371
+ allLoaderSegments.push(seg);
372
+ }
373
+ }
374
+ allMatchedIds.push(...inherited.matchedIds);
375
+ }
376
+ }
377
+ }
378
+
379
+ for (const entry of entries) {
380
+ await collectEntryLoaders(entry, entry.type === "route");
381
+ }
382
+
383
+ return { segments: allLoaderSegments, matchedIds: allMatchedIds };
384
+ }
385
+
386
+ /**
387
+ * Build a map of segment shortCode -> entry with revalidate functions.
388
+ */
389
+ export function buildEntryRevalidateMap(
390
+ entries: EntryData[],
391
+ ): Map<
392
+ string,
393
+ { entry: EntryData; revalidate: ShouldRevalidateFn<any, any>[] }
394
+ > {
395
+ const map = new Map<
396
+ string,
397
+ { entry: EntryData; revalidate: ShouldRevalidateFn<any, any>[] }
398
+ >();
399
+
400
+ function processEntry(entry: EntryData, parentShortCode?: string) {
401
+ map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
402
+
403
+ if (entry.type !== "parallel") {
404
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
405
+ entry.parallel,
406
+ )) {
407
+ const parallelParentShortCode = parentShortCode ?? entry.shortCode;
408
+ const parallelId = `${parallelParentShortCode}.${slot}`;
409
+ map.set(parallelId, {
410
+ entry: parallelEntry,
411
+ revalidate: parallelEntry.revalidate,
412
+ });
413
+ }
414
+ }
415
+
416
+ for (const layoutEntry of entry.layout) {
417
+ processEntry(layoutEntry, entry.shortCode);
418
+ }
419
+ }
420
+
421
+ for (const entry of entries) {
422
+ processEntry(entry);
423
+ }
424
+
425
+ return map;
426
+ }
427
+
428
+ /**
429
+ * Resolve parallel segments with revalidation.
430
+ */
431
+ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
432
+ entry: EntryData,
433
+ params: Record<string, string>,
434
+ context: HandlerContext<any, TEnv>,
435
+ belongsToRoute: boolean,
436
+ clientSegmentIds: Set<string>,
437
+ prevParams: Record<string, string>,
438
+ request: Request,
439
+ prevUrl: URL,
440
+ nextUrl: URL,
441
+ routeKey: string,
442
+ deps: SegmentResolutionDeps<TEnv>,
443
+ actionContext?: ActionContext,
444
+ stale?: boolean,
445
+ ): Promise<SegmentRevalidationResult> {
446
+ const segments: ResolvedSegment[] = [];
447
+ const matchedIds: string[] = [];
448
+
449
+ const resolvedParallelEntries = new Set<string>();
450
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
451
+ entry.parallel,
452
+ )) {
453
+ invariant(
454
+ parallelEntry.type === "parallel",
455
+ `Expected parallel entry, got: ${parallelEntry.type}`,
456
+ );
457
+
458
+ const slots = parallelEntry.handler as Record<
459
+ `@${string}`,
460
+ | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
461
+ | ReactNode
462
+ >;
463
+ // In production, static handler bodies are evicted and the slot value
464
+ // may be undefined. The static store holds the pre-rendered component.
465
+ // We defer the handler check until after tryStaticSlot.
466
+ const handler = slots[slot];
467
+
468
+ const parallelId = `${entry.shortCode}.${slot}`;
469
+
470
+ const isFullRefetch = clientSegmentIds.size === 0;
471
+ const isNewParent = !clientSegmentIds.has(entry.shortCode);
472
+ // Always announce the slot in matchedIds — it's unconditionally appended
473
+ // to `segments` below, and a segment present in segments but missing from
474
+ // matched lets the client prune it (then it's missing from clientSegmentIds
475
+ // on the next request, perpetuating the staleness).
476
+ matchedIds.push(parallelId);
477
+
478
+ let shouldResolve: boolean;
479
+ if (isFullRefetch) {
480
+ // Client has nothing cached — slot MUST render. User revalidate fns are
481
+ // bypassed here because returning false would leave the segment blank
482
+ // with no client-side fallback.
483
+ traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
484
+ shouldResolve = true;
485
+ } else {
486
+ // For non-empty client sets, consult user revalidate fns. When the slot
487
+ // is unknown to the client, override the type-derived default so the
488
+ // soft chain seeds with the right "new segment" / "parent-chain" value.
489
+ let defaultOverride: { value: boolean; reason: string } | undefined;
490
+ if (!clientSegmentIds.has(parallelId)) {
491
+ const value = belongsToRoute || isNewParent;
492
+ defaultOverride = {
493
+ value,
494
+ reason: value ? "new-segment" : "skip-parent-chain",
495
+ };
496
+ }
497
+
498
+ const dummySegment: ResolvedSegment = {
499
+ id: parallelId,
500
+ namespace: parallelEntry.id,
501
+ type: "parallel",
502
+ index: 0,
503
+ component: null as any,
504
+ params,
505
+ slot,
506
+ belongsToRoute,
507
+ parallelName: `${parallelEntry.id}.${slot}`,
508
+ ...(parallelEntry.mountPath
509
+ ? { mountPath: parallelEntry.mountPath }
510
+ : {}),
511
+ };
512
+
513
+ shouldResolve = await evaluateRevalidation({
514
+ segment: dummySegment,
515
+ prevParams,
516
+ getPrevSegment: null,
517
+ request,
518
+ prevUrl,
519
+ nextUrl,
520
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
521
+ name: `revalidate${i}`,
522
+ fn,
523
+ })),
524
+ routeKey,
525
+ context,
526
+ actionContext,
527
+ stale,
528
+ traceSource: "parallel",
529
+ defaultOverride,
530
+ });
531
+ }
532
+ emitRevalidationDecision(
533
+ parallelId,
534
+ context.pathname,
535
+ routeKey,
536
+ shouldResolve,
537
+ );
538
+
539
+ let component: ReactNode | undefined;
540
+ let handlerRan = false;
541
+ if (shouldResolve) {
542
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
543
+ // tryStaticSlot returning a value means the static cache supplied the
544
+ // component — handler did NOT run. handlerRan stays false.
545
+ }
546
+ if (component === undefined) {
547
+ const hasLoadingFallback =
548
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
549
+ if (!shouldResolve) {
550
+ component = null;
551
+ } else if (handler === undefined) {
552
+ // Handler evicted (production static slot) but static lookup missed.
553
+ // Nothing to render — use null so the client keeps its cached version.
554
+ component = null;
555
+ } else {
556
+ // Slot-keyed pushes — slot owns its own bucket, parent layout owns
557
+ // its own. On slot-only revalidations the partial merge updates only
558
+ // the slot's bucket; the parent's bucket stays intact.
559
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
560
+ parallelId;
561
+ handlerRan = true;
562
+ if (hasLoadingFallback) {
563
+ const result =
564
+ typeof handler === "function" ? handler(context) : handler;
565
+ if (result instanceof Promise) {
566
+ const tracked = deps.trackHandler(result, {
567
+ segmentId: parallelId,
568
+ segmentType: "parallel",
569
+ });
570
+ observeStreamedHandler(
571
+ tracked,
572
+ parallelId,
573
+ "parallel",
574
+ context.pathname,
575
+ routeKey,
576
+ params,
577
+ );
578
+ component = tracked as ReactNode;
579
+ } else {
580
+ component = result as ReactNode;
581
+ }
582
+ } else {
583
+ component =
584
+ typeof handler === "function" ? await handler(context) : handler;
585
+ }
586
+ }
587
+ }
588
+
589
+ segments.push({
590
+ id: parallelId,
591
+ namespace: parallelEntry.id,
592
+ type: "parallel",
593
+ index: 0,
594
+ component,
595
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
596
+ transition: parallelEntry.transition,
597
+ params,
598
+ slot,
599
+ _handlerRan: handlerRan,
600
+ belongsToRoute,
601
+ parallelName: `${parallelEntry.id}.${slot}`,
602
+ ...(parallelEntry.mountPath
603
+ ? { mountPath: parallelEntry.mountPath }
604
+ : {}),
605
+ });
606
+
607
+ if (resolvedParallelEntries.has(parallelEntry.id)) {
608
+ continue;
609
+ }
610
+
611
+ const loaderResult = await resolveLoadersWithRevalidation(
612
+ parallelEntry,
613
+ context,
614
+ belongsToRoute,
615
+ clientSegmentIds,
616
+ prevParams,
617
+ request,
618
+ prevUrl,
619
+ nextUrl,
620
+ routeKey,
621
+ deps,
622
+ actionContext,
623
+ entry.shortCode,
624
+ stale,
625
+ );
626
+ segments.push(...loaderResult.segments);
627
+ matchedIds.push(...loaderResult.matchedIds);
628
+ resolvedParallelEntries.add(parallelEntry.id);
629
+ }
630
+
631
+ return { segments, matchedIds };
632
+ }
633
+
634
+ /**
635
+ * Resolve entry handler (layout, cache, or route) with revalidation.
636
+ */
637
+ export async function resolveEntryHandlerWithRevalidation<TEnv>(
638
+ entry: Exclude<EntryData, { type: "parallel" }>,
639
+ params: Record<string, string>,
640
+ context: HandlerContext<any, TEnv>,
641
+ belongsToRoute: boolean,
642
+ clientSegmentIds: Set<string>,
643
+ prevParams: Record<string, string>,
644
+ request: Request,
645
+ prevUrl: URL,
646
+ nextUrl: URL,
647
+ routeKey: string,
648
+ deps: SegmentResolutionDeps<TEnv>,
649
+ actionContext?: ActionContext,
650
+ stale?: boolean,
651
+ ): Promise<{ segment: ResolvedSegment; matchedId: string }> {
652
+ const matchedId = entry.shortCode;
653
+
654
+ let handlerRan = false;
655
+ const component = await revalidate(
656
+ async () => {
657
+ const hasSegment = clientSegmentIds.has(entry.shortCode);
658
+ debugLog("segment.revalidate", "entry presence check", {
659
+ segmentId: entry.shortCode,
660
+ entryType: entry.type,
661
+ clientHasSegment: hasSegment,
662
+ belongsToRoute,
663
+ });
664
+ if (!hasSegment) {
665
+ if (isTraceActive()) {
666
+ const segType =
667
+ entry.type === "cache"
668
+ ? "layout"
669
+ : (entry.type as "layout" | "route");
670
+ pushRevalidationTraceEntry({
671
+ segmentId: entry.shortCode,
672
+ segmentType: segType,
673
+ belongsToRoute,
674
+ source: "segment-resolution",
675
+ defaultShouldRevalidate: true,
676
+ finalShouldRevalidate: true,
677
+ reason: "new-segment",
678
+ });
679
+ }
680
+ return true;
681
+ }
682
+
683
+ const dummySegment: ResolvedSegment = {
684
+ id: entry.shortCode,
685
+ namespace: entry.id,
686
+ type:
687
+ entry.type === "cache"
688
+ ? "layout"
689
+ : (entry.type as "layout" | "route"),
690
+ index: 0,
691
+ component: null as any,
692
+ params,
693
+ belongsToRoute,
694
+ ...(entry.type === "layout" || entry.type === "cache"
695
+ ? { layoutName: entry.id }
696
+ : {}),
697
+ ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
698
+ };
699
+
700
+ const shouldRevalidate = await evaluateRevalidation({
701
+ segment: dummySegment,
702
+ prevParams,
703
+ getPrevSegment: null,
704
+ request,
705
+ prevUrl,
706
+ nextUrl,
707
+ revalidations: entry.revalidate.map((fn, i) => ({
708
+ name: `revalidate${i}`,
709
+ fn,
710
+ })),
711
+ routeKey,
712
+ context,
713
+ actionContext,
714
+ stale,
715
+ traceSource:
716
+ entry.type === "route" ? "route-handler" : "layout-handler",
717
+ });
718
+ emitRevalidationDecision(
719
+ entry.shortCode,
720
+ context.pathname,
721
+ routeKey,
722
+ shouldRevalidate,
723
+ );
724
+ debugLog("segment.revalidate", "entry revalidation decision", {
725
+ segmentId: entry.shortCode,
726
+ shouldRevalidate,
727
+ });
728
+ return shouldRevalidate;
729
+ },
730
+ async () => {
731
+ handlerRan = true;
732
+ const doneHandler = track(`handler:${entry.id}`, 2);
733
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
734
+ entry.shortCode;
735
+ if (entry.type === "layout" || entry.type === "cache") {
736
+ const layoutComponent = await resolveLayoutComponent(entry, context);
737
+ doneHandler();
738
+ return layoutComponent;
739
+ }
740
+ const staticComponent = await tryStaticHandler(entry, entry.shortCode);
741
+ if (staticComponent !== undefined) {
742
+ doneHandler();
743
+ return staticComponent;
744
+ }
745
+ const routeEntry = entry as Extract<EntryData, { type: "route" }>;
746
+ // For Passthrough routes at runtime, use the live handler instead of
747
+ // the build handler. At build time (context.build === true), always
748
+ // use the build handler from routeEntry.handler.
749
+ const handler =
750
+ !context.build && routeEntry.liveHandler
751
+ ? routeEntry.liveHandler
752
+ : routeEntry.handler;
753
+ if (!routeEntry.loading) {
754
+ const result = handleHandlerResult(await handler(context));
755
+ doneHandler();
756
+ return result;
757
+ }
758
+ if (!actionContext) {
759
+ const result = handleHandlerResult(handler(context));
760
+ if (result instanceof Promise) {
761
+ result.finally(doneHandler).catch(() => {});
762
+ const tracked = deps.trackHandler(result, {
763
+ segmentId: entry.shortCode,
764
+ segmentType: entry.type,
765
+ });
766
+ observeStreamedHandler(
767
+ tracked,
768
+ entry.shortCode,
769
+ entry.type,
770
+ context.pathname,
771
+ routeKey,
772
+ params,
773
+ );
774
+ return { content: tracked };
775
+ }
776
+ doneHandler();
777
+ return { content: result };
778
+ }
779
+ debugLog("segment.action", "resolving action route with awaited value", {
780
+ entryId: entry.id,
781
+ });
782
+ const actionResult = handleHandlerResult(await handler(context));
783
+ doneHandler();
784
+ return {
785
+ content: Promise.resolve(actionResult),
786
+ };
787
+ },
788
+ () => null,
789
+ );
790
+
791
+ // Normalize void handlers (undefined) to null so the reconciler's
792
+ // component === null checks work consistently for both void and explicit null.
793
+ const resolvedComponent =
794
+ component && typeof component === "object" && "content" in component
795
+ ? ((component as { content: ReactNode }).content ?? null)
796
+ : (component ?? null);
797
+
798
+ const segment: ResolvedSegment = {
799
+ id: entry.shortCode,
800
+ namespace: entry.id,
801
+ type:
802
+ entry.type === "cache" ? "layout" : (entry.type as "layout" | "route"),
803
+ index: 0,
804
+ component: resolvedComponent,
805
+ loading: entry.loading === false ? null : entry.loading,
806
+ transition: entry.transition,
807
+ params,
808
+ belongsToRoute,
809
+ ...(entry.type === "layout" || entry.type === "cache"
810
+ ? { layoutName: entry.id }
811
+ : {}),
812
+ ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
813
+ _handlerRan: handlerRan,
814
+ };
815
+
816
+ return { segment, matchedId };
817
+ }
818
+
819
+ /**
820
+ * Resolve segments with revalidation awareness (for partial rendering).
821
+ */
822
+ export async function resolveSegmentWithRevalidation<TEnv>(
823
+ entry: Exclude<EntryData, { type: "parallel" }>,
824
+ routeKey: string,
825
+ params: Record<string, string>,
826
+ context: HandlerContext<any, TEnv>,
827
+ clientSegmentIds: Set<string>,
828
+ prevParams: Record<string, string>,
829
+ request: Request,
830
+ prevUrl: URL,
831
+ nextUrl: URL,
832
+ loaderPromises: Map<string, Promise<any>>,
833
+ deps: SegmentResolutionDeps<TEnv>,
834
+ actionContext?: ActionContext,
835
+ stale?: boolean,
836
+ ): Promise<SegmentRevalidationResult> {
837
+ const segments: ResolvedSegment[] = [];
838
+ const matchedIds: string[] = [];
839
+
840
+ const belongsToRoute = entry.type === "route";
841
+
842
+ const loaderResult = await resolveLoadersWithRevalidation(
843
+ entry,
844
+ context,
845
+ belongsToRoute,
846
+ clientSegmentIds,
847
+ prevParams,
848
+ request,
849
+ prevUrl,
850
+ nextUrl,
851
+ routeKey,
852
+ deps,
853
+ actionContext,
854
+ undefined,
855
+ stale,
856
+ );
857
+ segments.push(...loaderResult.segments);
858
+ matchedIds.push(...loaderResult.matchedIds);
859
+
860
+ // For route entries, execute the handler BEFORE orphan layouts and parallels
861
+ // so ctx.set() data is available to them via ctx.get(). The handler's
862
+ // segment is pushed after children to preserve tree composition order.
863
+ let routeHandlerResult:
864
+ | { segment: ResolvedSegment; matchedId: string }
865
+ | undefined;
866
+ if (entry.type === "route") {
867
+ routeHandlerResult = await resolveEntryHandlerWithRevalidation(
868
+ entry,
869
+ params,
870
+ context,
871
+ belongsToRoute,
872
+ clientSegmentIds,
873
+ prevParams,
874
+ request,
875
+ prevUrl,
876
+ nextUrl,
877
+ routeKey,
878
+ deps,
879
+ actionContext,
880
+ stale,
881
+ );
882
+
883
+ for (const orphan of entry.layout) {
884
+ const orphanResult = await resolveOrphanLayoutWithRevalidation(
885
+ orphan,
886
+ params,
887
+ context,
888
+ clientSegmentIds,
889
+ prevParams,
890
+ request,
891
+ prevUrl,
892
+ nextUrl,
893
+ routeKey,
894
+ true,
895
+ deps,
896
+ actionContext,
897
+ stale,
898
+ entry,
899
+ );
900
+ segments.push(...orphanResult.segments);
901
+ matchedIds.push(...orphanResult.matchedIds);
902
+ }
903
+ }
904
+
905
+ if (routeHandlerResult) {
906
+ // Route entry: handler already executed above; resolve parallels
907
+ // (handler data visible) then push handler segment last for tree order.
908
+ const parallelResult = await resolveParallelSegmentsWithRevalidation(
909
+ entry,
910
+ params,
911
+ context,
912
+ belongsToRoute,
913
+ clientSegmentIds,
914
+ prevParams,
915
+ request,
916
+ prevUrl,
917
+ nextUrl,
918
+ routeKey,
919
+ deps,
920
+ actionContext,
921
+ stale,
922
+ );
923
+ segments.push(...parallelResult.segments);
924
+ matchedIds.push(...parallelResult.matchedIds);
925
+
926
+ segments.push(routeHandlerResult.segment);
927
+ matchedIds.push(routeHandlerResult.matchedId);
928
+ } else {
929
+ // Layout/cache entry: handler-first — resolve handler before parallels
930
+ // so ctx.set() values are visible to parallel children.
931
+ const handlerResult = await resolveEntryHandlerWithRevalidation(
932
+ entry,
933
+ params,
934
+ context,
935
+ belongsToRoute,
936
+ clientSegmentIds,
937
+ prevParams,
938
+ request,
939
+ prevUrl,
940
+ nextUrl,
941
+ routeKey,
942
+ deps,
943
+ actionContext,
944
+ stale,
945
+ );
946
+ segments.push(handlerResult.segment);
947
+ matchedIds.push(handlerResult.matchedId);
948
+
949
+ const parallelResult = await resolveParallelSegmentsWithRevalidation(
950
+ entry,
951
+ params,
952
+ context,
953
+ belongsToRoute,
954
+ clientSegmentIds,
955
+ prevParams,
956
+ request,
957
+ prevUrl,
958
+ nextUrl,
959
+ routeKey,
960
+ deps,
961
+ actionContext,
962
+ stale,
963
+ );
964
+ segments.push(...parallelResult.segments);
965
+ matchedIds.push(...parallelResult.matchedIds);
966
+
967
+ for (const orphan of entry.layout) {
968
+ const orphanResult = await resolveOrphanLayoutWithRevalidation(
969
+ orphan,
970
+ params,
971
+ context,
972
+ clientSegmentIds,
973
+ prevParams,
974
+ request,
975
+ prevUrl,
976
+ nextUrl,
977
+ routeKey,
978
+ false,
979
+ deps,
980
+ actionContext,
981
+ stale,
982
+ );
983
+ segments.push(...orphanResult.segments);
984
+ matchedIds.push(...orphanResult.matchedIds);
985
+ }
986
+ }
987
+
988
+ return { segments, matchedIds };
989
+ }
990
+
991
+ /**
992
+ * Resolve orphan layout with revalidation.
993
+ */
994
+ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
995
+ orphan: EntryData,
996
+ params: Record<string, string>,
997
+ context: HandlerContext<any, TEnv>,
998
+ clientSegmentIds: Set<string>,
999
+ prevParams: Record<string, string>,
1000
+ request: Request,
1001
+ prevUrl: URL,
1002
+ nextUrl: URL,
1003
+ routeKey: string,
1004
+ belongsToRoute: boolean,
1005
+ deps: SegmentResolutionDeps<TEnv>,
1006
+ actionContext?: ActionContext,
1007
+ stale?: boolean,
1008
+ /** Parent route entry — its loaders are inherited so parallel slots can access them. */
1009
+ parentRouteEntry?: EntryData,
1010
+ ): Promise<SegmentRevalidationResult> {
1011
+ invariant(
1012
+ orphan.type === "layout" || orphan.type === "cache",
1013
+ `Expected orphan to be a layout or cache, got: ${orphan.type}`,
1014
+ );
1015
+
1016
+ const segments: ResolvedSegment[] = [];
1017
+ const matchedIds: string[] = [];
1018
+
1019
+ const loaderResult = await resolveLoadersWithRevalidation(
1020
+ orphan,
1021
+ context,
1022
+ belongsToRoute,
1023
+ clientSegmentIds,
1024
+ prevParams,
1025
+ request,
1026
+ prevUrl,
1027
+ nextUrl,
1028
+ routeKey,
1029
+ deps,
1030
+ actionContext,
1031
+ undefined,
1032
+ stale,
1033
+ );
1034
+ segments.push(...loaderResult.segments);
1035
+ matchedIds.push(...loaderResult.matchedIds);
1036
+
1037
+ // Inherit parent route's loaders so parallel slots inside this layout
1038
+ // can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
1039
+ if (
1040
+ parentRouteEntry &&
1041
+ parentRouteEntry.loader &&
1042
+ parentRouteEntry.loader.length > 0 &&
1043
+ Object.keys(orphan.parallel).length > 0
1044
+ ) {
1045
+ const inheritedResult = await resolveLoadersWithRevalidation(
1046
+ parentRouteEntry,
1047
+ context,
1048
+ belongsToRoute,
1049
+ clientSegmentIds,
1050
+ prevParams,
1051
+ request,
1052
+ prevUrl,
1053
+ nextUrl,
1054
+ routeKey,
1055
+ deps,
1056
+ actionContext,
1057
+ orphan.shortCode,
1058
+ stale,
1059
+ );
1060
+ // Tag as inherited so buildMatchResult can deduplicate when safe
1061
+ for (const s of inheritedResult.segments) {
1062
+ s._inherited = true;
1063
+ }
1064
+ segments.push(...inheritedResult.segments);
1065
+ matchedIds.push(...inheritedResult.matchedIds);
1066
+ }
1067
+
1068
+ // Handler-first: resolve orphan layout handler before its parallels
1069
+ // so ctx.set() values are visible to parallel children.
1070
+ matchedIds.push(orphan.shortCode);
1071
+
1072
+ const component = await revalidate(
1073
+ async () => {
1074
+ if (!clientSegmentIds.has(orphan.shortCode)) {
1075
+ if (isTraceActive()) {
1076
+ pushRevalidationTraceEntry({
1077
+ segmentId: orphan.shortCode,
1078
+ segmentType: "layout",
1079
+ belongsToRoute,
1080
+ source: "orphan-layout",
1081
+ defaultShouldRevalidate: true,
1082
+ finalShouldRevalidate: true,
1083
+ reason: "new-segment",
1084
+ });
1085
+ }
1086
+ return true;
1087
+ }
1088
+
1089
+ const dummySegment: ResolvedSegment = {
1090
+ id: orphan.shortCode,
1091
+ namespace: orphan.id,
1092
+ type: "layout",
1093
+ index: 0,
1094
+ component: null as any,
1095
+ params,
1096
+ belongsToRoute,
1097
+ layoutName: orphan.id,
1098
+ ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
1099
+ };
1100
+
1101
+ const shouldRevalidate = await evaluateRevalidation({
1102
+ segment: dummySegment,
1103
+ prevParams,
1104
+ getPrevSegment: null,
1105
+ request,
1106
+ prevUrl,
1107
+ nextUrl,
1108
+ revalidations: orphan.revalidate.map((fn, i) => ({
1109
+ name: `revalidate${i}`,
1110
+ fn,
1111
+ })),
1112
+ routeKey,
1113
+ context,
1114
+ actionContext,
1115
+ stale,
1116
+ traceSource: "orphan-layout",
1117
+ });
1118
+ emitRevalidationDecision(
1119
+ orphan.shortCode,
1120
+ context.pathname,
1121
+ routeKey,
1122
+ shouldRevalidate,
1123
+ );
1124
+ return shouldRevalidate;
1125
+ },
1126
+ async () => resolveLayoutComponent(orphan, context),
1127
+ () => null,
1128
+ );
1129
+
1130
+ segments.push({
1131
+ id: orphan.shortCode,
1132
+ namespace: orphan.id,
1133
+ type: "layout",
1134
+ index: 0,
1135
+ component,
1136
+ params,
1137
+ belongsToRoute,
1138
+ layoutName: orphan.id,
1139
+ loading: orphan.loading === false ? null : orphan.loading,
1140
+ transition: orphan.transition,
1141
+ ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
1142
+ });
1143
+
1144
+ const resolvedParallelEntries = new Set<string>();
1145
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
1146
+ orphan.parallel,
1147
+ )) {
1148
+ invariant(
1149
+ parallelEntry.type === "parallel",
1150
+ `Expected parallel entry, got: ${parallelEntry.type}`,
1151
+ );
1152
+
1153
+ if (!resolvedParallelEntries.has(parallelEntry.id)) {
1154
+ // shortCodeOverride must match the parent layout, not the parallel entry.
1155
+ const loaderResult = await resolveLoadersWithRevalidation(
1156
+ parallelEntry,
1157
+ context,
1158
+ belongsToRoute,
1159
+ clientSegmentIds,
1160
+ prevParams,
1161
+ request,
1162
+ prevUrl,
1163
+ nextUrl,
1164
+ routeKey,
1165
+ deps,
1166
+ actionContext,
1167
+ orphan.shortCode,
1168
+ stale,
1169
+ );
1170
+ segments.push(...loaderResult.segments);
1171
+ matchedIds.push(...loaderResult.matchedIds);
1172
+ resolvedParallelEntries.add(parallelEntry.id);
1173
+ }
1174
+
1175
+ const slots = parallelEntry.handler as Record<
1176
+ `@${string}`,
1177
+ | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1178
+ | ReactNode
1179
+ >;
1180
+ // Handler may be undefined in production after static handler eviction.
1181
+ const handler = slots[slot];
1182
+
1183
+ // Use orphan.shortCode (the parent layout) to match the SSR path
1184
+ // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1185
+ // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1186
+ const parallelId = `${orphan.shortCode}.${slot}`;
1187
+ matchedIds.push(parallelId);
1188
+
1189
+ const isFullRefetch = clientSegmentIds.size === 0;
1190
+ let shouldResolve: boolean;
1191
+ if (isFullRefetch) {
1192
+ // Same load-bearing rationale as the main parallel path: full refetch
1193
+ // means the client has nothing to fall back to, so the slot must render.
1194
+ traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
1195
+ shouldResolve = true;
1196
+ } else {
1197
+ // When slot is unknown to the client, seed the soft chain with `true`
1198
+ // (orphan parallels always belong to the route — we want them rendered
1199
+ // unless the user explicitly opts out via revalidate()).
1200
+ const defaultOverride = clientSegmentIds.has(parallelId)
1201
+ ? undefined
1202
+ : { value: true, reason: "new-segment" };
1203
+
1204
+ const dummySegment: ResolvedSegment = {
1205
+ id: parallelId,
1206
+ namespace: parallelEntry.id,
1207
+ type: "parallel",
1208
+ index: 0,
1209
+ component: null as any,
1210
+ params,
1211
+ slot,
1212
+ belongsToRoute,
1213
+ parallelName: `${parallelEntry.id}.${slot}`,
1214
+ ...(parallelEntry.mountPath
1215
+ ? { mountPath: parallelEntry.mountPath }
1216
+ : {}),
1217
+ };
1218
+
1219
+ shouldResolve = await evaluateRevalidation({
1220
+ segment: dummySegment,
1221
+ prevParams,
1222
+ getPrevSegment: null,
1223
+ request,
1224
+ prevUrl,
1225
+ nextUrl,
1226
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
1227
+ name: `revalidate${i}`,
1228
+ fn,
1229
+ })),
1230
+ routeKey,
1231
+ context,
1232
+ actionContext,
1233
+ stale,
1234
+ traceSource: "parallel",
1235
+ defaultOverride,
1236
+ });
1237
+ }
1238
+ emitRevalidationDecision(
1239
+ parallelId,
1240
+ context.pathname,
1241
+ routeKey,
1242
+ shouldResolve,
1243
+ );
1244
+
1245
+ let component: ReactNode | undefined;
1246
+ let handlerRan = false;
1247
+ if (shouldResolve) {
1248
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
1249
+ }
1250
+ if (component === undefined) {
1251
+ const hasLoadingFallback =
1252
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
1253
+ if (!shouldResolve) {
1254
+ component = null;
1255
+ } else if (handler === undefined) {
1256
+ // Handler evicted (production static slot) but static lookup missed.
1257
+ component = null;
1258
+ } else {
1259
+ // Slot-keyed pushes — see resolveParallelSegmentsWithRevalidation.
1260
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
1261
+ parallelId;
1262
+ handlerRan = true;
1263
+ if (hasLoadingFallback) {
1264
+ const result =
1265
+ typeof handler === "function" ? handler(context) : handler;
1266
+ if (result instanceof Promise) {
1267
+ const tracked = deps.trackHandler(result, {
1268
+ segmentId: parallelId,
1269
+ segmentType: "parallel",
1270
+ });
1271
+ observeStreamedHandler(
1272
+ tracked,
1273
+ parallelId,
1274
+ "parallel",
1275
+ context.pathname,
1276
+ routeKey,
1277
+ params,
1278
+ );
1279
+ component = tracked as ReactNode;
1280
+ } else {
1281
+ component = result as ReactNode;
1282
+ }
1283
+ } else {
1284
+ component =
1285
+ typeof handler === "function" ? await handler(context) : handler;
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ segments.push({
1291
+ id: parallelId,
1292
+ namespace: parallelEntry.id,
1293
+ type: "parallel",
1294
+ index: 0,
1295
+ component,
1296
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1297
+ transition: parallelEntry.transition,
1298
+ params,
1299
+ slot,
1300
+ _handlerRan: handlerRan,
1301
+ belongsToRoute,
1302
+ parallelName: `${parallelEntry.id}.${slot}`,
1303
+ ...(parallelEntry.mountPath
1304
+ ? { mountPath: parallelEntry.mountPath }
1305
+ : {}),
1306
+ });
1307
+ }
1308
+
1309
+ return { segments, matchedIds };
1310
+ }
1311
+
1312
+ /**
1313
+ * Resolve all segments for a route with revalidation logic (for matchPartial).
1314
+ */
1315
+ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1316
+ entries: EntryData[],
1317
+ routeKey: string,
1318
+ params: Record<string, string>,
1319
+ context: HandlerContext<any, TEnv>,
1320
+ clientSegmentSet: Set<string>,
1321
+ prevParams: Record<string, string>,
1322
+ request: Request,
1323
+ prevUrl: URL,
1324
+ nextUrl: URL,
1325
+ loaderPromises: Map<string, Promise<any>>,
1326
+ actionContext: ActionContext | undefined,
1327
+ interceptResult: { intercept: any; entry: EntryData } | null,
1328
+ localRouteName: string,
1329
+ pathname: string,
1330
+ deps: SegmentResolutionDeps<TEnv>,
1331
+ stale?: boolean,
1332
+ ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
1333
+ const allSegments: ResolvedSegment[] = [];
1334
+ const matchedIds: string[] = [];
1335
+ const seenSegIds = new Set<string>();
1336
+ const seenMatchIds = new Set<string>();
1337
+
1338
+ const telemetry = getRouterContext()?.telemetry;
1339
+
1340
+ for (const entry of entries) {
1341
+ if (entry.type === "route" && interceptResult) {
1342
+ debugLog(
1343
+ "matchPartial.intercept",
1344
+ "skipping route handler during intercept",
1345
+ {
1346
+ localRouteName,
1347
+ segmentId: entry.shortCode,
1348
+ },
1349
+ );
1350
+ if (!seenMatchIds.has(entry.shortCode)) {
1351
+ seenMatchIds.add(entry.shortCode);
1352
+ matchedIds.push(entry.shortCode);
1353
+ }
1354
+ continue;
1355
+ }
1356
+
1357
+ const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1358
+ if (entry.type === "cache") {
1359
+ const store = RSCRouterContext.getStore();
1360
+ if (store) store.insideCacheScope = true;
1361
+ }
1362
+ const doneEntry = track(`segment:${entry.id}`, 1);
1363
+ const resolved = await resolveWithErrorBoundary(
1364
+ nonParallelEntry,
1365
+ params,
1366
+ () =>
1367
+ resolveSegmentWithRevalidation(
1368
+ nonParallelEntry,
1369
+ routeKey,
1370
+ params,
1371
+ context,
1372
+ clientSegmentSet,
1373
+ prevParams,
1374
+ request,
1375
+ prevUrl,
1376
+ nextUrl,
1377
+ loaderPromises,
1378
+ deps,
1379
+ actionContext,
1380
+ stale,
1381
+ ),
1382
+ (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1383
+ deps,
1384
+ { request, url: context.url, routeKey, isPartial: true, telemetry },
1385
+ pathname,
1386
+ );
1387
+ doneEntry();
1388
+
1389
+ // Deduplicate segments and matchedIds by ID, matching resolveAllSegments.
1390
+ // include() scopes can produce entries that resolve the same shared
1391
+ // layout/loader segment. Duplicates cause React tree depth changes.
1392
+ for (const seg of resolved.segments) {
1393
+ if (!seenSegIds.has(seg.id)) {
1394
+ seenSegIds.add(seg.id);
1395
+ allSegments.push(seg);
1396
+ }
1397
+ }
1398
+ for (const id of resolved.matchedIds) {
1399
+ if (!seenMatchIds.has(id)) {
1400
+ seenMatchIds.add(id);
1401
+ matchedIds.push(id);
1402
+ }
1403
+ }
1404
+ }
1405
+
1406
+ return { segments: allSegments, matchedIds };
1407
+ }