@rangojs/router 0.27.0 → 0.29.0

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 (243) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/action-fence.d.ts +2 -1
  4. package/dist/types/browser/event-controller.d.ts +8 -2
  5. package/dist/types/browser/history-state.d.ts +3 -9
  6. package/dist/types/browser/navigation-store.d.ts +8 -6
  7. package/dist/types/browser/partial-update.d.ts +11 -0
  8. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  9. package/dist/types/browser/react/use-action.d.ts +7 -2
  10. package/dist/types/browser/react/use-handle.d.ts +19 -16
  11. package/dist/types/browser/react/use-link-status.d.ts +5 -2
  12. package/dist/types/browser/segment-reconciler.d.ts +32 -4
  13. package/dist/types/browser/types.d.ts +24 -5
  14. package/dist/types/browser/wire-protocol.d.ts +26 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  16. package/dist/types/cache/cache-scope.d.ts +34 -22
  17. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  19. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  20. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  21. package/dist/types/cache/segment-codec.d.ts +14 -0
  22. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  23. package/dist/types/cache/taint.d.ts +5 -3
  24. package/dist/types/cache/types.d.ts +7 -3
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  26. package/dist/types/prefetch-markers.d.ts +5 -14
  27. package/dist/types/prerender/store.d.ts +31 -0
  28. package/dist/types/prerender/warm-request.d.ts +3 -3
  29. package/dist/types/prerender.d.ts +2 -2
  30. package/dist/types/route-name.d.ts +5 -0
  31. package/dist/types/router/error-handling.d.ts +11 -5
  32. package/dist/types/router/instrument.d.ts +4 -3
  33. package/dist/types/router/intercept-resolution.d.ts +25 -9
  34. package/dist/types/router/match-context.d.ts +6 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +13 -22
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  38. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  39. package/dist/types/router/navigation-snapshot.d.ts +7 -0
  40. package/dist/types/router/prerender-match.d.ts +21 -6
  41. package/dist/types/router/preview-match.d.ts +2 -7
  42. package/dist/types/router/request-classification.d.ts +5 -0
  43. package/dist/types/router/router-context.d.ts +7 -46
  44. package/dist/types/router/router-interfaces.d.ts +8 -14
  45. package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
  46. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  47. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  48. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  49. package/dist/types/router/segment-resolution/resolution-pass.d.ts +95 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
  52. package/dist/types/router/segment-resolution.d.ts +1 -2
  53. package/dist/types/router/segment-wrappers.d.ts +11 -48
  54. package/dist/types/router/telemetry.d.ts +2 -0
  55. package/dist/types/router/types.d.ts +11 -1
  56. package/dist/types/rsc/full-payload.d.ts +2 -2
  57. package/dist/types/rsc/handler-context.d.ts +6 -6
  58. package/dist/types/rsc/json-route-result.d.ts +5 -9
  59. package/dist/types/rsc/request-stages.d.ts +203 -0
  60. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  61. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  62. package/dist/types/rsc/routine-plan.d.ts +1 -1
  63. package/dist/types/rsc/server-action.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  65. package/dist/types/rsc/shell-capture.d.ts +132 -15
  66. package/dist/types/rsc/shell-serve.d.ts +37 -12
  67. package/dist/types/server/context.d.ts +9 -8
  68. package/dist/types/server/handle-store.d.ts +16 -0
  69. package/dist/types/server/render-pass.d.ts +72 -0
  70. package/dist/types/server/request-context.d.ts +53 -104
  71. package/dist/types/server/rewrite-trail.d.ts +2 -2
  72. package/dist/types/testing/dispatch.d.ts +35 -18
  73. package/dist/types/testing/render-route.d.ts +43 -8
  74. package/dist/types/testing/serve-shell-request.d.ts +8 -1
  75. package/dist/types/types/cache-types.d.ts +6 -3
  76. package/dist/types/types/error-types.d.ts +30 -3
  77. package/dist/types/types/handler-context.d.ts +9 -1
  78. package/dist/types/types/segments.d.ts +1 -1
  79. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  80. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  81. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  82. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  83. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  84. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  85. package/dist/types/vite/discovery/state.d.ts +12 -0
  86. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  87. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  88. package/dist/types/vite/router-discovery.d.ts +8 -47
  89. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  90. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  91. package/dist/types/wire-params.d.ts +39 -0
  92. package/dist/vite/index.js +3822 -3727
  93. package/package.json +1 -1
  94. package/skills/cache-guide/SKILL.md +8 -5
  95. package/skills/caching/SKILL.md +37 -18
  96. package/skills/client-urls/SKILL.md +4 -2
  97. package/skills/document-cache/SKILL.md +13 -1
  98. package/skills/handler-use/SKILL.md +1 -1
  99. package/skills/hooks/SKILL.md +1 -1
  100. package/skills/hooks/data.md +8 -1
  101. package/skills/hooks/handle-and-actions.md +33 -2
  102. package/skills/hooks/navigation.md +121 -15
  103. package/skills/hooks/state.md +6 -4
  104. package/skills/hooks/urls.md +16 -5
  105. package/skills/intercept/SKILL.md +23 -7
  106. package/skills/middleware/SKILL.md +18 -0
  107. package/skills/observability/SKILL.md +30 -0
  108. package/skills/ppr/SKILL.md +18 -3
  109. package/skills/prerender/SKILL.md +39 -15
  110. package/skills/rango/SKILL.md +5 -1
  111. package/skills/server-actions/SKILL.md +15 -1
  112. package/skills/testing/client-components.md +25 -18
  113. package/skills/testing/response-routes.md +6 -4
  114. package/skills/testing/serve-shell-request.md +7 -7
  115. package/skills/use-cache/SKILL.md +46 -8
  116. package/skills/view-transitions/SKILL.md +1 -1
  117. package/src/__internal.ts +7 -1
  118. package/src/browser/action-fence.ts +2 -1
  119. package/src/browser/connection-warmup.ts +3 -1
  120. package/src/browser/event-controller.ts +37 -13
  121. package/src/browser/history-state.ts +6 -12
  122. package/src/browser/navigation-bridge.ts +59 -26
  123. package/src/browser/navigation-client.ts +20 -25
  124. package/src/browser/navigation-store.ts +25 -27
  125. package/src/browser/navigation-transaction.ts +4 -1
  126. package/src/browser/network-error-handler.ts +4 -0
  127. package/src/browser/partial-update.ts +33 -6
  128. package/src/browser/prefetch/cache.ts +2 -1
  129. package/src/browser/prefetch/fetch.ts +8 -16
  130. package/src/browser/prefetch/queue.ts +4 -4
  131. package/src/browser/react/NavigationProvider.tsx +2 -1
  132. package/src/browser/react/filter-segment-order.ts +2 -2
  133. package/src/browser/react/use-action.ts +24 -48
  134. package/src/browser/react/use-handle.ts +22 -18
  135. package/src/browser/react/use-link-status.ts +48 -60
  136. package/src/browser/react/use-navigation.ts +92 -41
  137. package/src/browser/rsc-router.tsx +1 -0
  138. package/src/browser/segment-reconciler.ts +109 -53
  139. package/src/browser/server-action-bridge.ts +81 -61
  140. package/src/browser/types.ts +25 -5
  141. package/src/browser/wire-protocol.ts +43 -0
  142. package/src/cache/cache-key-utils.ts +11 -5
  143. package/src/cache/cache-runtime.ts +132 -75
  144. package/src/cache/cache-scope.ts +110 -64
  145. package/src/cache/cf/cf-cache-constants.ts +2 -2
  146. package/src/cache/cf/cf-cache-store.ts +486 -874
  147. package/src/cache/cf/cf-kv-utils.ts +1 -1
  148. package/src/cache/document-cache.ts +64 -46
  149. package/src/cache/handle-snapshot.ts +19 -7
  150. package/src/cache/read-through-swr.ts +20 -18
  151. package/src/cache/segment-codec.ts +55 -0
  152. package/src/cache/shell-snapshot.ts +2 -2
  153. package/src/cache/tag-invalidation.ts +6 -2
  154. package/src/cache/taint.ts +5 -3
  155. package/src/cache/types.ts +7 -3
  156. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  157. package/src/handles/pushed-by.ts +1 -1
  158. package/src/prefetch-markers.ts +5 -16
  159. package/src/prerender/store.ts +76 -0
  160. package/src/prerender/warm-request.ts +8 -4
  161. package/src/prerender.ts +2 -2
  162. package/src/route-name.ts +8 -0
  163. package/src/router/error-handling.ts +22 -10
  164. package/src/router/instrument.ts +4 -3
  165. package/src/router/intercept-resolution.ts +82 -75
  166. package/src/router/loader-resolution.ts +5 -4
  167. package/src/router/match-api.ts +5 -5
  168. package/src/router/match-context.ts +23 -12
  169. package/src/router/match-handlers.ts +8 -20
  170. package/src/router/match-middleware/background-revalidation.ts +46 -57
  171. package/src/router/match-middleware/cache-lookup.ts +677 -726
  172. package/src/router/match-middleware/cache-store.ts +4 -8
  173. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  174. package/src/router/match-middleware/segment-resolution.ts +34 -61
  175. package/src/router/middleware.ts +69 -11
  176. package/src/router/navigation-snapshot.ts +14 -4
  177. package/src/router/prerender-match.ts +100 -44
  178. package/src/router/preview-match.ts +6 -41
  179. package/src/router/request-classification.ts +13 -7
  180. package/src/router/route-snapshot.ts +2 -5
  181. package/src/router/router-context.ts +10 -95
  182. package/src/router/router-interfaces.ts +8 -14
  183. package/src/router/segment-resolution/helpers.ts +49 -13
  184. package/src/router/segment-resolution/loader-cache.ts +15 -9
  185. package/src/router/segment-resolution/loader-mask.ts +5 -5
  186. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  187. package/src/router/segment-resolution/resolution-pass.ts +139 -0
  188. package/src/router/segment-resolution/revalidation.ts +939 -789
  189. package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
  190. package/src/router/segment-resolution.ts +4 -13
  191. package/src/router/segment-wrappers.ts +51 -269
  192. package/src/router/telemetry.ts +5 -0
  193. package/src/router/types.ts +11 -1
  194. package/src/router.ts +19 -17
  195. package/src/rsc/full-payload.ts +6 -5
  196. package/src/rsc/handler-context.ts +8 -14
  197. package/src/rsc/handler.ts +126 -507
  198. package/src/rsc/json-route-result.ts +5 -9
  199. package/src/rsc/loader-fetch.ts +6 -5
  200. package/src/rsc/progressive-enhancement.ts +5 -0
  201. package/src/rsc/request-stages.ts +759 -0
  202. package/src/rsc/response-cache-serve.ts +10 -14
  203. package/src/rsc/response-route-handler.ts +13 -5
  204. package/src/rsc/rewrite-passes.ts +2 -1
  205. package/src/rsc/routine-plan.ts +1 -1
  206. package/src/rsc/rsc-rendering.ts +164 -349
  207. package/src/rsc/server-action.ts +87 -113
  208. package/src/rsc/shell-capture-constants.ts +1 -1
  209. package/src/rsc/shell-capture.ts +275 -227
  210. package/src/rsc/shell-serve.ts +121 -18
  211. package/src/rsc/ssr-setup.ts +5 -4
  212. package/src/segment-system.tsx +3 -3
  213. package/src/server/context.ts +22 -25
  214. package/src/server/handle-store.ts +55 -0
  215. package/src/server/render-pass.ts +291 -0
  216. package/src/server/request-context.ts +75 -115
  217. package/src/server/rewrite-trail.ts +6 -4
  218. package/src/ssr/index.tsx +1 -1
  219. package/src/testing/dispatch.ts +226 -710
  220. package/src/testing/e2e/page-helpers.ts +2 -2
  221. package/src/testing/render-route.tsx +199 -13
  222. package/src/testing/serve-shell-request.ts +36 -40
  223. package/src/types/cache-types.ts +6 -3
  224. package/src/types/error-types.ts +30 -3
  225. package/src/types/handler-context.ts +12 -1
  226. package/src/types/segments.ts +1 -1
  227. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  228. package/src/vite/discovery/build-env.ts +148 -0
  229. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  230. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  231. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  232. package/src/vite/discovery/discover-routers.ts +1 -1
  233. package/src/vite/discovery/discovery-errors.ts +1 -1
  234. package/src/vite/discovery/prerender-collection.ts +24 -39
  235. package/src/vite/discovery/state.ts +15 -0
  236. package/src/vite/plugins/version-plugin.ts +1 -1
  237. package/src/vite/plugins/virtual-entries.ts +1 -1
  238. package/src/vite/router-discovery.ts +29 -2428
  239. package/src/vite/temp-rsc-server.ts +223 -0
  240. package/src/vite/utils/prerender-utils.ts +47 -0
  241. package/src/wire-params.ts +40 -0
  242. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  243. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -1,945 +0,0 @@
1
- /**
2
- * Fresh Path Segment Resolution
3
- *
4
- * Functions for resolving segments during a full (non-revalidation) request.
5
- * Handles loaders, layouts, routes, parallels, orphan layouts, and error boundaries.
6
- */
7
-
8
- import type { ReactNode } from "react";
9
- import { invariant } from "../../errors";
10
- import {
11
- getParallelEntries,
12
- getParallelSlotEntries,
13
- type EntryData,
14
- type LoaderEntry,
15
- } from "../../server/context";
16
- import type {
17
- HandlerContext,
18
- InternalHandlerContext,
19
- ResolvedSegment,
20
- } from "../../types";
21
- import type { SegmentResolutionDeps } from "../types.js";
22
- import { resolveLoaderData } from "./loader-cache.js";
23
- import {
24
- isShellCaptureActive,
25
- entryLoadingMasksLoaders,
26
- } from "./loader-mask.js";
27
- import {
28
- handleHandlerResult,
29
- tryStaticHandler,
30
- tryStaticSlot,
31
- resolveLayoutComponent,
32
- resolveWithErrorBoundary,
33
- warnOnStreamedResponse,
34
- buildLoaderErrorContext,
35
- } from "./helpers.js";
36
- import { applyViewTransitionDefault } from "./view-transition-default.js";
37
- import { _getRequestContext } from "../../server/request-context.js";
38
- import { getRouterContext } from "../router-context.js";
39
- import { trackStreamedHandler } from "./streamed-handler-boundary.js";
40
- import { observeSegmentHandler } from "../instrument.js";
41
- import { withReverseScope } from "../handler-context.js";
42
- import {
43
- track,
44
- RangoContext,
45
- runInsideLoaderScope,
46
- latchCachedHeaderScope,
47
- latchPprHeaderScopeForEntries,
48
- } from "../../server/context.js";
49
-
50
- /**
51
- * Register flagged loader $$ids on the request context and return their
52
- * indices. Must run BEFORE kickoff: rendered() checks the set to fail
53
- * fast on the barrier cycle (resolution awaits the loader, the barrier
54
- * awaits resolution).
55
- */
56
- function registerAwaitBeforeFlushIds(loaderEntries: LoaderEntry[]): number[] {
57
- const awaitedIndices: number[] = [];
58
- for (let i = 0; i < loaderEntries.length; i++) {
59
- if (loaderEntries[i]!.awaitBeforeFlush) awaitedIndices.push(i);
60
- }
61
- if (awaitedIndices.length > 0) {
62
- const reqCtx = _getRequestContext();
63
- if (reqCtx) {
64
- reqCtx._awaitBeforeFlushLoaderIds ??= new Set();
65
- for (const i of awaitedIndices) {
66
- reqCtx._awaitBeforeFlushLoaderIds.add(loaderEntries[i]!.loader.$$id);
67
- }
68
- }
69
- }
70
- return awaitedIndices;
71
- }
72
-
73
- function stampAwaitBeforeFlush(
74
- loaderEntry: LoaderEntry,
75
- ): { awaitBeforeFlush: true } | Record<string, never> {
76
- return loaderEntry.awaitBeforeFlush === true
77
- ? { awaitBeforeFlush: true as const }
78
- : {};
79
- }
80
-
81
- // ---------------------------------------------------------------------------
82
- // Fresh path (full match, no revalidation)
83
- // ---------------------------------------------------------------------------
84
-
85
- /**
86
- * Resolve loaders for an entry and emit segments.
87
- * Loaders are run lazily via ctx.use() and memoized for parallel execution.
88
- */
89
- export async function resolveLoaders<TEnv>(
90
- entry: EntryData,
91
- ctx: HandlerContext<any, TEnv>,
92
- belongsToRoute: boolean,
93
- deps: SegmentResolutionDeps<TEnv>,
94
- shortCodeOverride?: string,
95
- ): Promise<ResolvedSegment[]> {
96
- const loaderEntries = entry.loader ?? [];
97
- if (loaderEntries.length === 0) return [];
98
-
99
- const shortCode = shortCodeOverride ?? entry.shortCode;
100
-
101
- // Pin `_currentSegmentId` to the OWNING entry BEFORE the loader kickoffs:
102
- // createLoaderExecutor captures it synchronously at kickoff for
103
- // ctx.use(Handle) push attribution (loader writes land in the same bucket
104
- // as the entry's handler pushes — shortCode is in matched/segmentOrder, so
105
- // collectHandleData keeps them; an id outside the order is silently
106
- // dropped). resolveLoaders runs BEFORE the handler-resolution sites assign
107
- // this (fresh.ts handler-first ordering), so without the pin the captured
108
- // value is a stale sibling's id (document lane) or undefined (navigation
109
- // lane — pushes silently vanished).
110
- (ctx as InternalHandlerContext<any, TEnv>)._currentSegmentId = shortCode;
111
- const hasLoading = "loading" in entry && entry.loading !== undefined;
112
- const loadingDisabled = hasLoading && entry.loading === false;
113
-
114
- // Emit the streaming (non-awaiting) loader shape when loading is enabled OR
115
- // during a PPR shell capture. In capture, LIVE-lane loaders are masked with
116
- // never-resolving promises (loader-mask.ts); the loading-disabled branch below
117
- // AWAITS the loader promises, which would hang the capture render's match()
118
- // forever on those masked promises. Forcing the streaming shape lets match()
119
- // complete so the prerender can postpone the loader subtrees as holes. The
120
- // `!loadingDisabled` short-circuit keeps the ALS check off the hot path (only
121
- // loading-disabled entries consult it), so normal requests are unchanged.
122
- const emitStreaming = !loadingDisabled || isShellCaptureActive();
123
-
124
- // PPR bake/seed key input only; lane rule: see resolveLoaderData
125
- // (loader-cache.ts). Unflagged loaders mask at capture regardless of the key.
126
- const bakeLane = !entryLoadingMasksLoaders(entry.loading);
127
-
128
- // Error context for wrapLoaderPromise: without it, a throwing DSL loader never
129
- // fires createRouter({ onError }) (phase "loader") nor emits the loader.error
130
- // telemetry event — wrapLoaderPromise only builds the onError/telemetry path
131
- // when errorContext is supplied. Built from ctx so the live render path reports
132
- // loader failures the same way handlers/actions/routing/fetchable-loaders do.
133
- const errorContext = buildLoaderErrorContext(ctx);
134
-
135
- if (emitStreaming) {
136
- // awaitBeforeFlush (loader(Def, { ssr: false })): document
137
- // renders await these loaders before returning, so their data is settled,
138
- // their handle pushes beat the barrier snapshot, and a thrown notFound()'s
139
- // status write deterministically precedes Response construction. The ids
140
- // must register on the request context BEFORE kickoff — rendered() checks
141
- // the set to fail fast on the barrier cycle (segment resolution awaits the
142
- // loader, the barrier awaits segment resolution, rendered() awaits the
143
- // barrier), and the loader body can call rendered() before the await below
144
- // is reached.
145
- //
146
- // Shell-capture renders await too: flagged loaders execute at capture
147
- // (lane rule: see resolveLoaderData, loader-cache.ts), so their handle
148
- // pushes must land in the prelude.
149
- const awaitedIndices = registerAwaitBeforeFlushIds(loaderEntries);
150
-
151
- // Streaming loaders: promises kick off now, settle during RSC serialization.
152
- const segments = loaderEntries.map((loaderEntry, i) => {
153
- const { loader } = loaderEntry;
154
- const segmentId = `${shortCode}D${i}.${loader.$$id}`;
155
- return {
156
- id: segmentId,
157
- namespace: entry.id,
158
- type: "loader" as const,
159
- index: i,
160
- component: null,
161
- params: ctx.params,
162
- loaderId: loader.$$id,
163
- // Stamped on document AND capture renders: segment-system's
164
- // value-delivery await and the dev SSR-suspension diagnostic both
165
- // key off it.
166
- ...stampAwaitBeforeFlush(loaderEntry),
167
- loaderData: deps.wrapLoaderPromise(
168
- runInsideLoaderScope(
169
- () =>
170
- resolveLoaderData(
171
- loaderEntry,
172
- ctx,
173
- ctx.pathname,
174
- // The bake key rides for flagged loaders too: ssr:false
175
- // bakes at capture regardless of the entry's loading() lane
176
- // (loader-cache.ts capture branch) and its HIT-tail seed
177
- // overlay needs the same key.
178
- bakeLane || loaderEntry.bake === true ? segmentId : null,
179
- ),
180
- loader.$$id,
181
- loaderEntry.reverseScope,
182
- ),
183
- entry,
184
- segmentId,
185
- ctx.pathname,
186
- errorContext,
187
- loaderEntry.awaitBeforeFlush,
188
- ),
189
- belongsToRoute,
190
- };
191
- });
192
-
193
- // Await only the flagged loaders; unflagged siblings keep streaming (their
194
- // promises were kicked off above and stay pending in the emitted segments).
195
- // The wrapped promise is contracted to never reject (wrapLoaderPromise), so
196
- // a flagged loader failure resolves with its error envelope and cannot
197
- // collapse resolution — same contract the loading-disabled path relies on.
198
- if (awaitedIndices.length > 0) {
199
- await Promise.all(awaitedIndices.map((i) => segments[i]!.loaderData));
200
- }
201
-
202
- return segments;
203
- }
204
-
205
- // Loading disabled: still start all loaders in parallel, but only emit
206
- // settled promises so handlers don't stream loading placeholders.
207
- //
208
- // Wrap each loader promise with wrapLoaderPromise BEFORE awaiting. The wrapped
209
- // promise resolves to a LoaderDataResult and never rejects, routing a failed
210
- // loader to its own per-loader error boundary. Awaiting the RAW promises here
211
- // instead would (1) propagate a rejection to the segment-level boundary,
212
- // collapsing the whole entry and discarding successful sibling data, and
213
- // (2) leave the other in-flight raw promises without a .catch, producing
214
- // unhandled rejections. Mirrors the loading path and intercept-resolution.
215
- //
216
- // Flagged loaders still get awaitBeforeFlush stamped: auto-raise and
217
- // settled-value delivery key off the field, and this path already paid
218
- // the pre-flush await.
219
- registerAwaitBeforeFlushIds(loaderEntries);
220
- const pendingLoaderData = loaderEntries.map((loaderEntry, i) => {
221
- const { loader } = loaderEntry;
222
- const segmentId = `${shortCode}D${i}.${loader.$$id}`;
223
- const wrapped = deps.wrapLoaderPromise(
224
- runInsideLoaderScope(
225
- () =>
226
- resolveLoaderData(
227
- loaderEntry,
228
- ctx,
229
- ctx.pathname,
230
- bakeLane || loaderEntry.bake === true ? segmentId : null,
231
- ),
232
- loader.$$id,
233
- loaderEntry.reverseScope,
234
- ),
235
- entry,
236
- segmentId,
237
- ctx.pathname,
238
- errorContext,
239
- loaderEntry.awaitBeforeFlush,
240
- );
241
- return { wrapped, segmentId };
242
- });
243
- await Promise.all(pendingLoaderData.map((p) => p.wrapped));
244
-
245
- return loaderEntries.map((loaderEntry, i) => {
246
- const { loader } = loaderEntry;
247
- const pending = pendingLoaderData[i]!;
248
- // The "loader:<id>" perf metric is recorded by observePhase at the single
249
- // loader-metering site (useLoader, reached via ctx.use during
250
- // resolveLoaderData), with the real per-loader duration rather than a
251
- // Promise.all batch ceiling.
252
- return {
253
- id: pending.segmentId,
254
- namespace: entry.id,
255
- type: "loader" as const,
256
- index: i,
257
- component: null,
258
- params: ctx.params,
259
- loaderId: loader.$$id,
260
- ...stampAwaitBeforeFlush(loaderEntry),
261
- loaderData: pending.wrapped,
262
- belongsToRoute,
263
- };
264
- });
265
- }
266
-
267
- /**
268
- * Options for segment resolution.
269
- */
270
- export interface ResolveSegmentOptions {
271
- /** When true, skip resolveLoaders() calls (used for pre-rendering) */
272
- skipLoaders?: boolean;
273
- /**
274
- * When true, a thrown render error is re-thrown instead of being converted
275
- * into an error-boundary segment. Set only by the pre-render path so a
276
- * build-time render failure (and a `throw new Skip()` inside a render fn)
277
- * surfaces to the build instead of being silently baked into a frozen error
278
- * page served as a 200 (issue #587). The live request path leaves this unset,
279
- * so error boundaries keep catching at request time.
280
- */
281
- throwOnError?: boolean;
282
- /**
283
- * The matched route's full entry chain. resolveAllSegments defaults it to
284
- * its `entries`; withCacheLookup passes it when it resolves only the entries
285
- * above a cache() boundary. An orphan list can hold an entry of the chain: a
286
- * bare cache() marker sits in its layout's layout[] (it wraps that layout's
287
- * routes) and is the parent of the routes after it. For those routes it
288
- * resolves once, as a chain entry, below the cache() header latch (issue
289
- * #918: resolved twice on a MISS, once and discarded on a HIT).
290
- */
291
- chain?: readonly EntryData[];
292
- /**
293
- * The matched chain entry being resolved: where the sync path catches a
294
- * handler error, so a streamed handler (or slot owned by a routeless layout)
295
- * recovers into the same boundary. resolveSegment sets it for the entry it
296
- * resolves; nested resolutions keep the outer value.
297
- */
298
- boundaryEntry?: EntryData;
299
- }
300
-
301
- /**
302
- * Resolve segments from EntryData.
303
- * Executes middlewares, loaders, parallels, and handlers in correct order.
304
- * Returns array: [main segment, ...orphan layout segments]
305
- */
306
- export async function resolveSegment<TEnv>(
307
- entry: EntryData,
308
- routeKey: string,
309
- params: Record<string, string>,
310
- context: HandlerContext<any, TEnv>,
311
- loaderPromises: Map<string, Promise<any>>,
312
- deps: SegmentResolutionDeps<TEnv>,
313
- isRouteEntry: boolean = false,
314
- options?: ResolveSegmentOptions,
315
- ): Promise<ResolvedSegment[]> {
316
- const segments: ResolvedSegment[] = [];
317
- const boundaryEntry = options?.boundaryEntry ?? entry;
318
- if (!options?.boundaryEntry) options = { ...options, boundaryEntry };
319
-
320
- if (entry.type === "layout" || entry.type === "cache") {
321
- if (!options?.skipLoaders) {
322
- const loaderSegments = await resolveLoaders(entry, context, false, deps);
323
- segments.push(...loaderSegments);
324
- }
325
-
326
- // Handler-first: layout handler executes before its parallels and orphan
327
- // layouts so that ctx.set() values are visible to all children.
328
- (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
329
- entry.shortCode;
330
-
331
- const doneLayoutHandler = track(`handler:${entry.id}`, 2);
332
- const component = await resolveLayoutComponent(entry, context);
333
- doneLayoutHandler();
334
-
335
- segments.push({
336
- id: entry.shortCode,
337
- namespace: entry.id,
338
- type: "layout",
339
- index: 0,
340
- component,
341
- loading: entry.loading === false ? null : entry.loading,
342
- transition: applyViewTransitionDefault(
343
- entry.transition,
344
- deps.viewTransitionDefault,
345
- ),
346
- params,
347
- belongsToRoute: false,
348
- layoutName: entry.id,
349
- ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
350
- });
351
-
352
- const resolvedParallelEntries = new Set<string>();
353
- for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
354
- entry.parallel,
355
- )) {
356
- const parallelSegments = await resolveParallelEntry(
357
- parallelEntry,
358
- params,
359
- context,
360
- false,
361
- entry.shortCode,
362
- deps,
363
- options,
364
- routeKey,
365
- [slot],
366
- !resolvedParallelEntries.has(parallelEntry.id),
367
- );
368
- segments.push(...parallelSegments);
369
- resolvedParallelEntries.add(parallelEntry.id);
370
- }
371
-
372
- for (const orphan of entry.layout) {
373
- if (options?.chain?.includes(orphan)) continue;
374
- const orphanSegments = await resolveOrphanLayout(
375
- orphan,
376
- params,
377
- context,
378
- loaderPromises,
379
- false,
380
- deps,
381
- options,
382
- routeKey,
383
- );
384
- segments.push(...orphanSegments);
385
- }
386
- } else if (entry.type === "route") {
387
- if (!options?.skipLoaders) {
388
- const loaderSegments = await resolveLoaders(entry, context, true, deps);
389
- segments.push(...loaderSegments);
390
- }
391
-
392
- // Route handler EXECUTES before its children (orphan layouts, parallels).
393
- // This lets the handler set() context variables that children can read
394
- // via get(). Caching wraps all segments together (per-route, not
395
- // per-segment), so either all run or none do -- no partial scenarios.
396
- //
397
- // The handler's segment is PUSHED after orphans/parallels to preserve
398
- // the correct tree composition order (layouts wrap the route content).
399
- (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
400
- entry.shortCode;
401
- let component: ReactNode | undefined = await tryStaticHandler(
402
- entry,
403
- entry.shortCode,
404
- );
405
- if (component === undefined) {
406
- // For Passthrough routes at runtime, use the live handler instead of
407
- // the build handler. At build time (context.build === true), always
408
- // use the build handler from entry.handler.
409
- const handler =
410
- !context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
411
- const doneRouteHandler = track(`handler:${entry.id}`, 2);
412
- if (entry.loading) {
413
- const result = handleHandlerResult(
414
- observeSegmentHandler(entry.shortCode, entry.id, handler, context),
415
- );
416
- if (result instanceof Promise) {
417
- warnOnStreamedResponse(result, entry.id);
418
- result.finally(doneRouteHandler).catch(() => {});
419
- component = trackStreamedHandler(deps, result, {
420
- boundaryEntry,
421
- segmentId: entry.shortCode,
422
- segmentType: entry.type,
423
- context,
424
- routeKey,
425
- params,
426
- recover: !options?.throwOnError,
427
- });
428
- } else {
429
- doneRouteHandler();
430
- component = result;
431
- }
432
- } else {
433
- component = handleHandlerResult(
434
- await observeSegmentHandler(
435
- entry.shortCode,
436
- entry.id,
437
- handler,
438
- context,
439
- ),
440
- );
441
- doneRouteHandler();
442
- }
443
- }
444
-
445
- for (const orphan of entry.layout) {
446
- const orphanSegments = await resolveOrphanLayout(
447
- orphan,
448
- params,
449
- context,
450
- loaderPromises,
451
- true,
452
- deps,
453
- options,
454
- routeKey,
455
- entry,
456
- );
457
- segments.push(...orphanSegments);
458
- }
459
-
460
- const resolvedParallelEntries = new Set<string>();
461
- for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
462
- entry.parallel,
463
- )) {
464
- const parallelSegments = await resolveParallelEntry(
465
- parallelEntry,
466
- params,
467
- context,
468
- true,
469
- entry.shortCode,
470
- deps,
471
- options,
472
- routeKey,
473
- [slot],
474
- !resolvedParallelEntries.has(parallelEntry.id),
475
- );
476
- segments.push(...parallelSegments);
477
- resolvedParallelEntries.add(parallelEntry.id);
478
- }
479
-
480
- segments.push({
481
- id: entry.shortCode,
482
- namespace: entry.id,
483
- type: "route",
484
- index: 0,
485
- component: component ?? null,
486
- loading: entry.loading === false ? null : entry.loading,
487
- transition: applyViewTransitionDefault(
488
- entry.transition,
489
- deps.viewTransitionDefault,
490
- ),
491
- params,
492
- belongsToRoute: true,
493
- ...(entry.clientGroup ? { clientGroup: entry.clientGroup } : {}),
494
- ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
495
- });
496
- } else {
497
- throw new Error(`Unknown entry type: ${(entry as any).type}`);
498
- }
499
-
500
- return segments;
501
- }
502
-
503
- /**
504
- * Resolve orphan layout with its middlewares, loaders, and parallels.
505
- */
506
- export async function resolveOrphanLayout<TEnv>(
507
- orphan: EntryData,
508
- params: Record<string, string>,
509
- context: HandlerContext<any, TEnv>,
510
- loaderPromises: Map<string, Promise<any>>,
511
- belongsToRoute: boolean,
512
- deps: SegmentResolutionDeps<TEnv>,
513
- options?: ResolveSegmentOptions,
514
- routeKey?: string,
515
- /** Parent route entry — its loaders are inherited by the layout so
516
- * parallel slots inside this layout can access them via useLoader(). */
517
- parentRouteEntry?: EntryData,
518
- ): Promise<ResolvedSegment[]> {
519
- invariant(
520
- orphan.type === "layout" || orphan.type === "cache",
521
- `Expected orphan to be a layout or cache, got: ${orphan.type}`,
522
- );
523
-
524
- const segments: ResolvedSegment[] = [];
525
- if (!options?.skipLoaders) {
526
- const loaderSegments = await resolveLoaders(
527
- orphan,
528
- context,
529
- belongsToRoute,
530
- deps,
531
- );
532
- segments.push(...loaderSegments);
533
-
534
- // Inherit parent route's loaders so parallel slots inside this layout
535
- // can access them via useLoader(). Without this, the route's loaders
536
- // are only in the route's OutletProvider (rendered as <Outlet /> content),
537
- // which is a child — not a parent — of the layout's context.
538
- if (
539
- parentRouteEntry &&
540
- parentRouteEntry.loader &&
541
- parentRouteEntry.loader.length > 0 &&
542
- Object.keys(orphan.parallel).length > 0
543
- ) {
544
- const inheritedLoaders = await resolveLoaders(
545
- parentRouteEntry,
546
- context,
547
- belongsToRoute,
548
- deps,
549
- orphan.shortCode,
550
- );
551
- // Tag as inherited so buildMatchResult can deduplicate when safe
552
- for (const s of inheritedLoaders) {
553
- s._inherited = true;
554
- }
555
- segments.push(...inheritedLoaders);
556
- }
557
- }
558
-
559
- // Handler-first: orphan layout handler executes before its parallels
560
- // so that ctx.set() values are visible to parallel children.
561
- const doneOrphanHandler = track(`handler:${orphan.id}`, 2);
562
- const component = await resolveLayoutComponent(orphan, context);
563
- doneOrphanHandler();
564
-
565
- segments.push({
566
- id: orphan.shortCode,
567
- namespace: orphan.id,
568
- type: "layout",
569
- index: 0,
570
- component,
571
- params,
572
- belongsToRoute,
573
- layoutName: orphan.id,
574
- loading: orphan.loading === false ? null : orphan.loading,
575
- transition: applyViewTransitionDefault(
576
- orphan.transition,
577
- deps.viewTransitionDefault,
578
- ),
579
- ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
580
- });
581
-
582
- const resolvedParallelEntries = new Set<string>();
583
- for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
584
- orphan.parallel,
585
- )) {
586
- const parallelSegments = await resolveParallelEntry(
587
- parallelEntry,
588
- params,
589
- context,
590
- belongsToRoute,
591
- orphan.shortCode,
592
- deps,
593
- options,
594
- routeKey,
595
- [slot],
596
- !resolvedParallelEntries.has(parallelEntry.id),
597
- );
598
- segments.push(...parallelSegments);
599
- resolvedParallelEntries.add(parallelEntry.id);
600
- }
601
-
602
- // Routeless entries nested in this one (a layout() after a bare cache()
603
- // marker, a wrapper inside a routeless wrapper) wrap the same content one
604
- // level deeper. Issue #918: they were never rendered. None is in the
605
- // matched chain: that would put this orphan in it, and resolveSegment
606
- // skips those.
607
- for (const nested of orphan.layout) {
608
- segments.push(
609
- ...(await resolveOrphanLayout(
610
- nested,
611
- params,
612
- context,
613
- loaderPromises,
614
- belongsToRoute,
615
- deps,
616
- options,
617
- routeKey,
618
- )),
619
- );
620
- }
621
-
622
- return segments;
623
- }
624
-
625
- /**
626
- * Resolve parallel EntryData with its loaders and slot handlers.
627
- */
628
- export async function resolveParallelEntry<TEnv>(
629
- parallelEntry: EntryData,
630
- params: Record<string, string>,
631
- context: HandlerContext<any, TEnv>,
632
- belongsToRoute: boolean,
633
- parentShortCode: string,
634
- deps: SegmentResolutionDeps<TEnv>,
635
- options?: ResolveSegmentOptions,
636
- routeKey?: string,
637
- slotNames?: `@${string}`[],
638
- includeLoaders: boolean = true,
639
- ): Promise<ResolvedSegment[]> {
640
- invariant(
641
- parallelEntry.type === "parallel",
642
- `Expected parallel entry, got: ${parallelEntry.type}`,
643
- );
644
-
645
- const segments: ResolvedSegment[] = [];
646
-
647
- const slots = parallelEntry.handler as Record<
648
- `@${string}`,
649
- | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
650
- | ReactNode
651
- >;
652
-
653
- const slotsToResolve = slotNames ?? (Object.keys(slots) as `@${string}`[]);
654
- const slotContext = withReverseScope(context, parallelEntry.reverseScope);
655
-
656
- for (const slot of slotsToResolve) {
657
- // Try static lookup first — in production, handler bodies are evicted
658
- // and replaced with stubs that have no .handler property (undefined).
659
- // The static store holds the pre-rendered component for these slots.
660
- let component: ReactNode | undefined = await tryStaticSlot(
661
- parallelEntry,
662
- slot,
663
- `${parentShortCode}.${slot}`,
664
- );
665
-
666
- if (component === undefined) {
667
- const handler = slots[slot];
668
- if (handler === undefined) {
669
- continue;
670
- }
671
- // Pin `_currentSegmentId` to the slot's own id so handle pushes from
672
- // inside the slot handler get their own bucket in the HandleStore.
673
- // Parent-keying would collapse them into the parent layout's bucket;
674
- // the partial-update merge then replaces the parent's bucket on a
675
- // slot-only revalidation and drops layout-pushed Meta/Breadcrumbs.
676
- // filterSegmentOrder() retains slot ids so the client preserves them.
677
- (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
678
- `${parentShortCode}.${slot}`;
679
- const doneParallelHandler = track(
680
- `handler:${parallelEntry.id}.${slot}`,
681
- 2,
682
- );
683
- const hasLoadingFallback =
684
- parallelEntry.loading !== undefined && parallelEntry.loading !== false;
685
- if (hasLoadingFallback) {
686
- const result =
687
- typeof handler === "function"
688
- ? observeSegmentHandler(
689
- `${parentShortCode}.${slot}`,
690
- `${parallelEntry.id}.${slot}`,
691
- handler,
692
- slotContext,
693
- )
694
- : handler;
695
- if (result instanceof Promise) {
696
- result.finally(doneParallelHandler).catch(() => {});
697
- component = trackStreamedHandler(deps, result, {
698
- boundaryEntry: options?.boundaryEntry ?? parallelEntry,
699
- segmentId: `${parentShortCode}.${slot}`,
700
- segmentType: "parallel",
701
- context,
702
- routeKey,
703
- params,
704
- recover: !options?.throwOnError,
705
- });
706
- } else {
707
- doneParallelHandler();
708
- component = result as ReactNode;
709
- }
710
- } else {
711
- component =
712
- typeof handler === "function"
713
- ? await observeSegmentHandler(
714
- `${parentShortCode}.${slot}`,
715
- `${parallelEntry.id}.${slot}`,
716
- handler,
717
- slotContext,
718
- )
719
- : handler;
720
- doneParallelHandler();
721
- }
722
- }
723
-
724
- segments.push({
725
- id: `${parentShortCode}.${slot}`,
726
- namespace: parallelEntry.id,
727
- type: "parallel",
728
- index: 0,
729
- component,
730
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
731
- transition: applyViewTransitionDefault(
732
- parallelEntry.transition,
733
- deps.viewTransitionDefault,
734
- ),
735
- params,
736
- slot,
737
- belongsToRoute,
738
- parallelName: `${parallelEntry.id}.${slot}`,
739
- ...(parallelEntry.mountPath
740
- ? { mountPath: parallelEntry.mountPath }
741
- : {}),
742
- });
743
- }
744
-
745
- if (!options?.skipLoaders && includeLoaders) {
746
- const loaderSegments = await resolveLoaders(
747
- parallelEntry,
748
- context,
749
- belongsToRoute,
750
- deps,
751
- parentShortCode,
752
- );
753
- // Tag parallel-owned loaders so renderSegments can stream them
754
- // using the parallel's loading() instead of awaiting on the layout
755
- const parallelLoading =
756
- parallelEntry.loading === false ? undefined : parallelEntry.loading;
757
- if (parallelLoading) {
758
- for (const seg of loaderSegments) {
759
- seg.parallelLoading = parallelLoading;
760
- }
761
- }
762
- segments.push(...loaderSegments);
763
- }
764
-
765
- return segments;
766
- }
767
-
768
- /**
769
- * Resolve all segments for a route (used for single-cache-per-request pattern).
770
- */
771
- export async function resolveAllSegments<TEnv>(
772
- entries: EntryData[],
773
- routeKey: string,
774
- params: Record<string, string>,
775
- context: HandlerContext<any, TEnv>,
776
- loaderPromises: Map<string, Promise<any>>,
777
- deps: SegmentResolutionDeps<TEnv>,
778
- options?: ResolveSegmentOptions,
779
- ): Promise<ResolvedSegment[]> {
780
- const allSegments: ResolvedSegment[] = [];
781
- const seenIds = new Set<string>();
782
- const segmentOptions: ResolveSegmentOptions = {
783
- ...options,
784
- chain: options?.chain ?? entries,
785
- };
786
-
787
- // ppr routes are document-scoped cached territory: the whole chain (root
788
- // layout down to the page) bakes into the shared shell, so the header-write
789
- // guard latches BEFORE any entry resolves (unlike the positional cache()
790
- // latch below). See assertCachedHeaderWriteAllowed (server/context.ts).
791
- latchPprHeaderScopeForEntries(entries, routeKey);
792
-
793
- // Safe request access: during build-time prerendering, context.request
794
- // is a throwing getter. Use undefined when unavailable.
795
- let safeRequest: Request | undefined;
796
- try {
797
- safeRequest = context.request;
798
- } catch {}
799
-
800
- // Get telemetry sink from RouterContext (may not exist during prerendering)
801
- let telemetry;
802
- try {
803
- telemetry = getRouterContext()?.telemetry;
804
- } catch {}
805
-
806
- for (const entry of entries) {
807
- // Set ALS flag when entering a cache() boundary so that ctx.get()
808
- // can guard non-cacheable variable reads. Also latch the header-write
809
- // scope (response-level side effects — headers/cookies/status).
810
- // Persists for all descendant entries. A route entry carries `cache` when
811
- // cache() is among its own children (dsl-helpers.ts cache()).
812
- if (entry.cache) {
813
- const store = RangoContext.getStore();
814
- if (store) store.insideCacheScope = true;
815
- latchCachedHeaderScope("cache", routeKey);
816
- }
817
- const doneEntry = track(`segment:${entry.id}`, 1);
818
- const resolvedSegments = await resolveWithErrorBoundary(
819
- entry,
820
- params,
821
- () =>
822
- resolveSegment(
823
- entry,
824
- routeKey,
825
- params,
826
- context,
827
- loaderPromises,
828
- deps,
829
- false,
830
- segmentOptions,
831
- ),
832
- (seg) => [seg],
833
- deps,
834
- { request: safeRequest, url: context.url, routeKey, telemetry },
835
- context.pathname,
836
- options?.throwOnError,
837
- );
838
- doneEntry();
839
- // Deduplicate by segment ID. include() scopes can produce entries that
840
- // resolve the same shared layout/loader segment. Duplicates in the segment
841
- // array propagate to the client's matched[] and change the React tree depth.
842
- for (const seg of resolvedSegments) {
843
- if (!seenIds.has(seg.id)) {
844
- seenIds.add(seg.id);
845
- allSegments.push(seg);
846
- }
847
- }
848
- }
849
-
850
- return allSegments;
851
- }
852
-
853
- /**
854
- * Resolve only loader segments for all entries (used when serving cached non-loader segments).
855
- */
856
- export async function resolveLoadersOnly<TEnv>(
857
- entries: EntryData[],
858
- context: HandlerContext<any, TEnv>,
859
- deps: SegmentResolutionDeps<TEnv>,
860
- ): Promise<ResolvedSegment[]> {
861
- const loaderSegments: ResolvedSegment[] = [];
862
- const seenIds = new Set<string>();
863
-
864
- // Loader-only serves (cached non-loader segments) still run loaders live —
865
- // on a ppr route their header writes are dead letters (headers flushed with
866
- // the shell), so the guard latches here too. cache() needs no latch: loader
867
- // writes are exempt under cache() (see assertCachedHeaderWriteAllowed).
868
- latchPprHeaderScopeForEntries(
869
- entries,
870
- (context as InternalHandlerContext<any, TEnv>)._routeName,
871
- );
872
-
873
- async function collectEntryLoaders(
874
- entry: EntryData,
875
- belongsToRoute: boolean,
876
- shortCodeOverride?: string,
877
- ): Promise<void> {
878
- // Skip if all loaders from this entry have already been resolved
879
- // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
880
- const entryLoaders = entry.loader ?? [];
881
- const sc = shortCodeOverride ?? entry.shortCode;
882
- const allAlreadySeen =
883
- entryLoaders.length > 0 &&
884
- entryLoaders.every((le, i) =>
885
- seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
886
- );
887
- if (!allAlreadySeen) {
888
- const segments = await resolveLoaders(
889
- entry,
890
- context,
891
- belongsToRoute,
892
- deps,
893
- shortCodeOverride,
894
- );
895
- for (const seg of segments) {
896
- if (!seenIds.has(seg.id)) {
897
- seenIds.add(seg.id);
898
- loaderSegments.push(seg);
899
- }
900
- }
901
- }
902
-
903
- const seenParallelEntryIds = new Set<string>();
904
- for (const parallelEntry of getParallelEntries(entry.parallel)) {
905
- if (seenParallelEntryIds.has(parallelEntry.id)) continue;
906
- seenParallelEntryIds.add(parallelEntry.id);
907
- await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
908
- }
909
-
910
- const childBelongsToRoute = belongsToRoute || entry.type === "route";
911
- for (const layoutEntry of entry.layout) {
912
- await collectEntryLoaders(layoutEntry, childBelongsToRoute);
913
- // Inherit route loaders for orphan layouts with parallels.
914
- // Resolve directly — do NOT re-enter collectEntryLoaders with the
915
- // route entry, as that would re-iterate route.layout and loop.
916
- if (
917
- entry.type === "route" &&
918
- entry.loader &&
919
- entry.loader.length > 0 &&
920
- Object.keys(layoutEntry.parallel).length > 0
921
- ) {
922
- const inherited = await resolveLoaders(
923
- entry,
924
- context,
925
- childBelongsToRoute,
926
- deps,
927
- layoutEntry.shortCode,
928
- );
929
- for (const seg of inherited) {
930
- if (!seenIds.has(seg.id)) {
931
- seenIds.add(seg.id);
932
- seg._inherited = true;
933
- loaderSegments.push(seg);
934
- }
935
- }
936
- }
937
- }
938
- }
939
-
940
- for (const entry of entries) {
941
- await collectEntryLoaders(entry, entry.type === "route");
942
- }
943
-
944
- return loaderSegments;
945
- }