@rangojs/router 0.0.0-experimental.0f44aca1

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