@rangojs/router 0.0.0-experimental.10

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 (172) hide show
  1. package/CLAUDE.md +43 -0
  2. package/README.md +19 -0
  3. package/dist/bin/rango.js +227 -0
  4. package/dist/vite/index.js +3039 -0
  5. package/package.json +171 -0
  6. package/skills/caching/SKILL.md +191 -0
  7. package/skills/debug-manifest/SKILL.md +108 -0
  8. package/skills/document-cache/SKILL.md +180 -0
  9. package/skills/fonts/SKILL.md +165 -0
  10. package/skills/hooks/SKILL.md +442 -0
  11. package/skills/intercept/SKILL.md +190 -0
  12. package/skills/layout/SKILL.md +213 -0
  13. package/skills/links/SKILL.md +180 -0
  14. package/skills/loader/SKILL.md +246 -0
  15. package/skills/middleware/SKILL.md +202 -0
  16. package/skills/mime-routes/SKILL.md +124 -0
  17. package/skills/parallel/SKILL.md +228 -0
  18. package/skills/prerender/SKILL.md +283 -0
  19. package/skills/rango/SKILL.md +54 -0
  20. package/skills/response-routes/SKILL.md +358 -0
  21. package/skills/route/SKILL.md +173 -0
  22. package/skills/router-setup/SKILL.md +346 -0
  23. package/skills/tailwind/SKILL.md +129 -0
  24. package/skills/theme/SKILL.md +78 -0
  25. package/skills/typesafety/SKILL.md +394 -0
  26. package/src/__internal.ts +175 -0
  27. package/src/bin/rango.ts +24 -0
  28. package/src/browser/event-controller.ts +876 -0
  29. package/src/browser/index.ts +18 -0
  30. package/src/browser/link-interceptor.ts +121 -0
  31. package/src/browser/lru-cache.ts +69 -0
  32. package/src/browser/merge-segment-loaders.ts +126 -0
  33. package/src/browser/navigation-bridge.ts +913 -0
  34. package/src/browser/navigation-client.ts +165 -0
  35. package/src/browser/navigation-store.ts +823 -0
  36. package/src/browser/partial-update.ts +600 -0
  37. package/src/browser/react/Link.tsx +248 -0
  38. package/src/browser/react/NavigationProvider.tsx +346 -0
  39. package/src/browser/react/ScrollRestoration.tsx +94 -0
  40. package/src/browser/react/context.ts +53 -0
  41. package/src/browser/react/index.ts +52 -0
  42. package/src/browser/react/location-state-shared.ts +120 -0
  43. package/src/browser/react/location-state.ts +62 -0
  44. package/src/browser/react/mount-context.ts +32 -0
  45. package/src/browser/react/use-action.ts +240 -0
  46. package/src/browser/react/use-client-cache.ts +56 -0
  47. package/src/browser/react/use-handle.ts +203 -0
  48. package/src/browser/react/use-href.tsx +40 -0
  49. package/src/browser/react/use-link-status.ts +134 -0
  50. package/src/browser/react/use-mount.ts +31 -0
  51. package/src/browser/react/use-navigation.ts +140 -0
  52. package/src/browser/react/use-segments.ts +188 -0
  53. package/src/browser/request-controller.ts +164 -0
  54. package/src/browser/rsc-router.tsx +352 -0
  55. package/src/browser/scroll-restoration.ts +324 -0
  56. package/src/browser/segment-structure-assert.ts +67 -0
  57. package/src/browser/server-action-bridge.ts +762 -0
  58. package/src/browser/shallow.ts +35 -0
  59. package/src/browser/types.ts +478 -0
  60. package/src/build/generate-manifest.ts +377 -0
  61. package/src/build/generate-route-types.ts +828 -0
  62. package/src/build/index.ts +36 -0
  63. package/src/build/route-trie.ts +239 -0
  64. package/src/cache/cache-scope.ts +563 -0
  65. package/src/cache/cf/cf-cache-store.ts +428 -0
  66. package/src/cache/cf/index.ts +19 -0
  67. package/src/cache/document-cache.ts +340 -0
  68. package/src/cache/index.ts +58 -0
  69. package/src/cache/memory-segment-store.ts +150 -0
  70. package/src/cache/memory-store.ts +253 -0
  71. package/src/cache/types.ts +392 -0
  72. package/src/client.rsc.tsx +83 -0
  73. package/src/client.tsx +643 -0
  74. package/src/component-utils.ts +76 -0
  75. package/src/components/DefaultDocument.tsx +23 -0
  76. package/src/debug.ts +233 -0
  77. package/src/default-error-boundary.tsx +88 -0
  78. package/src/deps/browser.ts +8 -0
  79. package/src/deps/html-stream-client.ts +2 -0
  80. package/src/deps/html-stream-server.ts +2 -0
  81. package/src/deps/rsc.ts +10 -0
  82. package/src/deps/ssr.ts +2 -0
  83. package/src/errors.ts +295 -0
  84. package/src/handle.ts +130 -0
  85. package/src/handles/MetaTags.tsx +193 -0
  86. package/src/handles/index.ts +6 -0
  87. package/src/handles/meta.ts +247 -0
  88. package/src/host/cookie-handler.ts +159 -0
  89. package/src/host/errors.ts +97 -0
  90. package/src/host/index.ts +56 -0
  91. package/src/host/pattern-matcher.ts +214 -0
  92. package/src/host/router.ts +330 -0
  93. package/src/host/testing.ts +79 -0
  94. package/src/host/types.ts +138 -0
  95. package/src/host/utils.ts +25 -0
  96. package/src/href-client.ts +202 -0
  97. package/src/href-context.ts +33 -0
  98. package/src/index.rsc.ts +121 -0
  99. package/src/index.ts +165 -0
  100. package/src/loader.rsc.ts +207 -0
  101. package/src/loader.ts +47 -0
  102. package/src/network-error-thrower.tsx +21 -0
  103. package/src/outlet-context.ts +15 -0
  104. package/src/prerender/param-hash.ts +35 -0
  105. package/src/prerender/store.ts +40 -0
  106. package/src/prerender.ts +156 -0
  107. package/src/reverse.ts +267 -0
  108. package/src/root-error-boundary.tsx +277 -0
  109. package/src/route-content-wrapper.tsx +193 -0
  110. package/src/route-definition.ts +1431 -0
  111. package/src/route-map-builder.ts +242 -0
  112. package/src/route-types.ts +220 -0
  113. package/src/router/error-handling.ts +287 -0
  114. package/src/router/handler-context.ts +158 -0
  115. package/src/router/intercept-resolution.ts +387 -0
  116. package/src/router/loader-resolution.ts +327 -0
  117. package/src/router/manifest.ts +216 -0
  118. package/src/router/match-api.ts +621 -0
  119. package/src/router/match-context.ts +264 -0
  120. package/src/router/match-middleware/background-revalidation.ts +236 -0
  121. package/src/router/match-middleware/cache-lookup.ts +382 -0
  122. package/src/router/match-middleware/cache-store.ts +276 -0
  123. package/src/router/match-middleware/index.ts +81 -0
  124. package/src/router/match-middleware/intercept-resolution.ts +281 -0
  125. package/src/router/match-middleware/segment-resolution.ts +184 -0
  126. package/src/router/match-pipelines.ts +214 -0
  127. package/src/router/match-result.ts +213 -0
  128. package/src/router/metrics.ts +62 -0
  129. package/src/router/middleware.ts +791 -0
  130. package/src/router/pattern-matching.ts +407 -0
  131. package/src/router/revalidation.ts +190 -0
  132. package/src/router/router-context.ts +301 -0
  133. package/src/router/segment-resolution.ts +1315 -0
  134. package/src/router/trie-matching.ts +172 -0
  135. package/src/router/types.ts +163 -0
  136. package/src/router.gen.ts +6 -0
  137. package/src/router.ts +2423 -0
  138. package/src/rsc/handler.ts +1443 -0
  139. package/src/rsc/helpers.ts +64 -0
  140. package/src/rsc/index.ts +56 -0
  141. package/src/rsc/nonce.ts +18 -0
  142. package/src/rsc/types.ts +236 -0
  143. package/src/segment-system.tsx +442 -0
  144. package/src/server/context.ts +466 -0
  145. package/src/server/handle-store.ts +229 -0
  146. package/src/server/loader-registry.ts +174 -0
  147. package/src/server/request-context.ts +554 -0
  148. package/src/server/root-layout.tsx +10 -0
  149. package/src/server/tsconfig.json +14 -0
  150. package/src/server.ts +171 -0
  151. package/src/ssr/index.tsx +296 -0
  152. package/src/theme/ThemeProvider.tsx +291 -0
  153. package/src/theme/ThemeScript.tsx +61 -0
  154. package/src/theme/constants.ts +59 -0
  155. package/src/theme/index.ts +58 -0
  156. package/src/theme/theme-context.ts +70 -0
  157. package/src/theme/theme-script.ts +152 -0
  158. package/src/theme/types.ts +182 -0
  159. package/src/theme/use-theme.ts +44 -0
  160. package/src/types.ts +1757 -0
  161. package/src/urls.gen.ts +8 -0
  162. package/src/urls.ts +1282 -0
  163. package/src/use-loader.tsx +346 -0
  164. package/src/vite/expose-action-id.ts +344 -0
  165. package/src/vite/expose-handle-id.ts +209 -0
  166. package/src/vite/expose-loader-id.ts +426 -0
  167. package/src/vite/expose-location-state-id.ts +177 -0
  168. package/src/vite/expose-prerender-handler-id.ts +429 -0
  169. package/src/vite/index.ts +2068 -0
  170. package/src/vite/package-resolution.ts +125 -0
  171. package/src/vite/version.d.ts +12 -0
  172. package/src/vite/virtual-entries.ts +114 -0
@@ -0,0 +1,442 @@
1
+ import { createElement, type ReactNode, type ComponentType } from "react";
2
+ import { OutletProvider } from "./client.js";
3
+ import { MountContextProvider } from "./browser/react/mount-context.js";
4
+ import type {
5
+ ResolvedSegment,
6
+ LoaderDataResult,
7
+ RootLayoutProps,
8
+ } from "./types.js";
9
+ import { isLoaderDataResult } from "./types.js";
10
+ import { invariant } from "./errors.js";
11
+ import {
12
+ RouteContentWrapper,
13
+ LoaderBoundary,
14
+ } from "./route-content-wrapper.js";
15
+ import { RootErrorBoundary } from "./root-error-boundary.js";
16
+
17
+ /**
18
+ * Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
19
+ */
20
+ function resolveLoaderData(
21
+ resolvedData: any[],
22
+ loaderIds: string[],
23
+ ): { loaderData: Record<string, any>; errorFallback: ReactNode } {
24
+ const loaderData: Record<string, any> = {};
25
+ let errorFallback: ReactNode = null;
26
+
27
+ for (let i = 0; i < loaderIds.length; i++) {
28
+ const id = loaderIds[i];
29
+ const result = resolvedData[i];
30
+
31
+ if (!isLoaderDataResult(result)) {
32
+ // Legacy format - direct data
33
+ loaderData[id] = result;
34
+ continue;
35
+ }
36
+
37
+ if (result.ok) {
38
+ loaderData[id] = result.data;
39
+ continue;
40
+ }
41
+
42
+ // Error case
43
+ if (result.fallback) {
44
+ errorFallback = result.fallback;
45
+ } else {
46
+ throw new Error(result.error.message);
47
+ }
48
+ }
49
+
50
+ return { loaderData, errorFallback };
51
+ }
52
+
53
+ /**
54
+ * Options for renderSegments
55
+ */
56
+ export interface RenderSegmentsOptions {
57
+ /**
58
+ * If true, this render is for a server action response.
59
+ * In browser during actions, we await component promises to prevent
60
+ * UI flickering/suspense during optimistic updates.
61
+ */
62
+ isAction?: boolean;
63
+
64
+ /**
65
+ * If true, force awaiting all loaders instead of streaming with Suspense.
66
+ * Used for popstate (back/forward) navigation where we want instant rendering
67
+ * from cache without showing loading skeletons.
68
+ */
69
+ forceAwait?: boolean;
70
+
71
+ /**
72
+ * Intercept segments to inject into the tree.
73
+ * These are parallel segments from intercept routes that need to be
74
+ * associated with their parent layout's named outlet.
75
+ *
76
+ * Passed separately for explicit handling - makes the flow clearer
77
+ * and easier to debug than relying on ID pattern matching.
78
+ */
79
+ interceptSegments?: ResolvedSegment[];
80
+
81
+ /**
82
+ * Root layout component that wraps the entire application.
83
+ * When provided, wraps both route content and the error boundary,
84
+ * preventing the app shell from unmounting during errors (avoids FOUC).
85
+ */
86
+ rootLayout?: ComponentType<RootLayoutProps>;
87
+ }
88
+
89
+ /**
90
+ * Render segments into a React tree with proper layout nesting
91
+ *
92
+ * Layouts nest using OutletProvider, while route + parallel + error + notFound segments
93
+ * render as siblings in a Fragment.
94
+ *
95
+ * Error segments are treated like route segments - they render their fallback
96
+ * component in place of the failed segment. When an error occurs in a handler,
97
+ * loader, or middleware, the router creates an error segment with the nearest
98
+ * error boundary's fallback component.
99
+ *
100
+ * NotFound segments are similar to error segments but are triggered by
101
+ * DataNotFoundError (thrown via notFound()). They render the nearest
102
+ * notFoundBoundary's fallback component.
103
+ *
104
+ * @param segments - Array of resolved segments to render
105
+ * @returns ReactNode representing the component tree
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * const segments = [
110
+ * { id: 'L0.0', type: 'layout', component: <RootLayout /> },
111
+ * { id: 'L1.0', type: 'layout', component: <BlogLayout /> },
112
+ * { id: 'R2.0', type: 'route', component: <BlogPost /> },
113
+ * { id: 'P3.0', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
114
+ * ];
115
+ *
116
+ * const tree = renderSegments(segments);
117
+ * // Results in:
118
+ * // <OutletProvider><RootLayout>
119
+ * // <OutletProvider><BlogLayout>
120
+ * // <><BlogPost /><Sidebar /></>
121
+ * // </BlogLayout></OutletProvider>
122
+ * // </RootLayout></OutletProvider>
123
+ *
124
+ * // For server actions, pass isAction to await components:
125
+ * const tree = renderSegments(segments, { isAction: true });
126
+ * ```
127
+ */
128
+ export async function renderSegments(
129
+ segments: ResolvedSegment[],
130
+ options?: RenderSegmentsOptions,
131
+ ): Promise<ReactNode> {
132
+ const {
133
+ isAction,
134
+ interceptSegments,
135
+ forceAwait,
136
+ rootLayout: RootLayout,
137
+ } = options || {};
138
+
139
+ const temporalLazyRefs: Promise<any>[] = [];
140
+
141
+ /**
142
+ * Registers promises from lazy/async components for awaiting.
143
+ * Handles both direct promises and React lazy components (which store promise in _payload).
144
+ */
145
+ const registerLazyRef = <T,>(node: T): T => {
146
+ if (node instanceof Promise) {
147
+ temporalLazyRefs.push(node);
148
+ } else if (isLazyComponent(node)) {
149
+ temporalLazyRefs.push(node._payload);
150
+ }
151
+ return node;
152
+ };
153
+
154
+ // Type guard for React lazy component internals
155
+ function isLazyComponent(node: unknown): node is { _payload: Promise<any> } {
156
+ return (
157
+ node != null &&
158
+ typeof node === "object" &&
159
+ "_payload" in node &&
160
+ (node as any)._payload instanceof Promise
161
+ );
162
+ }
163
+ // Separate segments by type, passing intercept segments for explicit injection
164
+ const tree = segmentTreeWalk(segments, interceptSegments);
165
+ // Render content segments as siblings
166
+ let content: ReactNode = null;
167
+ for (const node of tree) {
168
+ invariant(
169
+ node.segment.type === "layout" ||
170
+ node.segment.type === "route" ||
171
+ node.segment.type === "error" ||
172
+ node.segment.type === "notFound",
173
+ `Expected layout, route, error, or notFound segment, got ${node.segment.type}`,
174
+ );
175
+ const { component, id, params, loading } = node.segment;
176
+
177
+ // Only include params in key for segments that belong to the route
178
+ // - Routes: always include params (they render param-specific content)
179
+ // - Error/notFound segments: always include params (they replace failed route content)
180
+ // - Route's layouts (orphans): include params (children of parameterized route)
181
+ // - Parent chain layouts: exclude params (shared across routes, param-agnostic)
182
+ // This prevents unnecessary unmounting when params change
183
+ const includeParams =
184
+ node.segment.type === "route" ||
185
+ node.segment.type === "error" ||
186
+ node.segment.type === "notFound" ||
187
+ (node.segment.type === "layout" && node.segment.belongsToRoute);
188
+
189
+ const paramStr =
190
+ includeParams && params && Object.keys(params).length > 0
191
+ ? Object.entries(params)
192
+ .sort(([a], [b]) => a.localeCompare(b))
193
+ .map(([k, v]) => `${k}=${v}`)
194
+ .join(",")
195
+ : "";
196
+ const key = `${paramStr ? `${id}-${paramStr}` : id}`;
197
+
198
+ // Get loader entries for this node
199
+ const loaderEntries = node.loaders.filter(
200
+ (loader) => loader.loaderId && loader.loaderData !== undefined,
201
+ );
202
+
203
+ // Determine the component content (with or without Suspense wrapper)
204
+ // Wrap when loading skeleton defined OR component is Promise (needs Suspense)
205
+ // During actions, await component Promise to prevent Suspense from triggering
206
+ // This keeps existing content visible instead of showing loading skeleton
207
+ let resolvedComponent = component;
208
+ if (isAction && component instanceof Promise) {
209
+ resolvedComponent = await component;
210
+ }
211
+
212
+ let nodeContent: ReactNode =
213
+ loading !== null && loading
214
+ ? createElement(RouteContentWrapper, {
215
+ key: `suspense-loading-${id}`,
216
+ content:
217
+ resolvedComponent instanceof Promise
218
+ ? resolvedComponent
219
+ : Promise.resolve(resolvedComponent),
220
+ fallback: loading,
221
+ segmentId: id,
222
+ })
223
+ : registerLazyRef(resolvedComponent);
224
+ // Common props for OutletProvider
225
+ const outletContent: ReactNode =
226
+ node.segment.type === "layout" ? content : null;
227
+
228
+ // Prepare loader data if there are loaders
229
+ const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
230
+ const loaderDataPromise =
231
+ loaderEntries.length > 0
232
+ ? Promise.all(
233
+ loaderEntries.map((loader) =>
234
+ loader.loaderData instanceof Promise
235
+ ? loader.loaderData
236
+ : Promise.resolve(loader.loaderData),
237
+ ),
238
+ )
239
+ : Promise.resolve([]);
240
+
241
+ // Use LoaderBoundary when loading is defined to maintain consistent tree structure
242
+ // This ensures cached segments (which may not have loader segments) have the same
243
+ // tree structure as fresh segments, preventing React remounts
244
+ // If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
245
+ if (loading !== undefined && loading !== null) {
246
+ content = createElement(LoaderBoundary, {
247
+ key: `loader-boundary-${key}`,
248
+ loaderDataPromise:
249
+ forceAwait || isAction ? await loaderDataPromise : loaderDataPromise,
250
+ loaderIds,
251
+ fallback: loading,
252
+ outletKey: key,
253
+ outletContent,
254
+ segment: node.segment,
255
+ parallel: node.parallel,
256
+ children: nodeContent,
257
+ });
258
+ } else if (loaderEntries.length === 0) {
259
+ // No loaders, no loading - simple OutletProvider
260
+ content = createElement(OutletProvider, {
261
+ key,
262
+ content: outletContent,
263
+ segment: node.segment,
264
+ parallel: node.parallel,
265
+ children: nodeContent,
266
+ });
267
+ } else {
268
+ // Has loaders but no loading skeleton - await loaders and render directly
269
+ const resolvedData = await loaderDataPromise;
270
+ const { loaderData, errorFallback } = resolveLoaderData(
271
+ resolvedData,
272
+ loaderIds,
273
+ );
274
+
275
+ content = createElement(OutletProvider, {
276
+ key,
277
+ content: outletContent,
278
+ segment: node.segment,
279
+ parallel: node.parallel,
280
+ loaderData: Object.keys(loaderData).length > 0 ? loaderData : undefined,
281
+ children: errorFallback ?? nodeContent,
282
+ });
283
+ }
284
+
285
+ // Wrap with MountContextProvider for include() scoped components.
286
+ // Must use MountContextProvider (a proper "use client" export) instead of
287
+ // MountContext.Provider directly, because .Provider is a property on the
288
+ // context object and resolves to undefined through RSC client reference proxies.
289
+ if (node.segment.mountPath) {
290
+ content = createElement(MountContextProvider, {
291
+ value: node.segment.mountPath,
292
+ children: content,
293
+ });
294
+ }
295
+ }
296
+
297
+ // Always wrap with root error boundary to prevent white screens
298
+ // This catches any unhandled errors that bubble up from the segment tree
299
+ const errorBoundaryWrapped = createElement(RootErrorBoundary, {
300
+ children: content,
301
+ });
302
+ if (typeof window === "object") {
303
+ await Promise.allSettled(temporalLazyRefs);
304
+ }
305
+
306
+ // Build the final result, optionally wrapped with root layout
307
+ let result: ReactNode = errorBoundaryWrapped;
308
+
309
+ // If rootLayout is provided, wrap the error boundary with it
310
+ // This ensures the app shell stays mounted even during errors (prevents FOUC)
311
+ if (RootLayout) {
312
+ result = createElement(RootLayout, {
313
+ children: errorBoundaryWrapped,
314
+ });
315
+ }
316
+
317
+ return result;
318
+ }
319
+
320
+ /**
321
+ * Walk segments in bottom-to-top order for React nesting
322
+ *
323
+ * Segments from match() are in top-to-bottom order (root → leaf):
324
+ * Example: [L0, L0L0, L0R1L0.@sidebar, L0R1L0, L0R1]
325
+ *
326
+ * For proper React rendering, we need bottom-to-top (leaf → root):
327
+ * - Innermost content (route) wraps inside layouts
328
+ * - Each layer provides context via OutletProvider
329
+ * - Outer layouts receive inner content via <Outlet />
330
+ *
331
+ * Parallel segments must be matched to their parent by ID prefix:
332
+ * - "L0R1L0.@sidebar" belongs to "L0R1L0"
333
+ * - Pre-grouping prevents parallels from attaching to wrong parents
334
+ * during the reversed iteration (which would cause "L0R1L0.@sidebar"
335
+ * to incorrectly attach to "L0L0" instead of "L0R1L0")
336
+ *
337
+ * Loader segments are also grouped by parent:
338
+ * - "L0D0.cart" belongs to "L0"
339
+ * - Loaders don't render directly, their data is passed to context
340
+ *
341
+ * Intercept segments are passed separately for explicit handling:
342
+ * - They are injected into the correct parent's parallel array
343
+ * - This makes the flow clearer than relying on ID pattern matching
344
+ *
345
+ * @param segments - Main segments from the route tree
346
+ * @param interceptSegments - Optional intercept segments to inject
347
+ */
348
+ function* segmentTreeWalk(
349
+ segments: ResolvedSegment[],
350
+ interceptSegments?: ResolvedSegment[],
351
+ ): Generator<{
352
+ segment: ResolvedSegment;
353
+ parallel: ResolvedSegment[];
354
+ loaders: ResolvedSegment[];
355
+ }> {
356
+ // Pre-group parallel and loader segments by their parent ID using prefix matching
357
+ // This ensures each parallel/loader is associated with the correct parent segment
358
+ // regardless of the iteration order
359
+ const parallelsByParent = new Map<string, ResolvedSegment[]>();
360
+ const loadersByParent = new Map<string, ResolvedSegment[]>();
361
+ const nonParallels: ResolvedSegment[] = [];
362
+
363
+ for (const segment of segments) {
364
+ if (segment.type === "parallel") {
365
+ // Extract parent ID from parallel ID
366
+ // Example: "L0R1L0.@sidebar" → "L0R1L0"
367
+ const parentId = segment.id.split(".")[0];
368
+ if (!parallelsByParent.has(parentId)) {
369
+ parallelsByParent.set(parentId, []);
370
+ }
371
+ parallelsByParent.get(parentId)!.push(segment);
372
+ } else if (segment.type === "loader") {
373
+ // Extract parent ID from loader ID
374
+ // Example: "L0D0.cart" → "L0"
375
+ // Loader ID format: {parentShortCode}D{index}.{loaderId}
376
+ const parentId = segment.id.split("D")[0];
377
+ if (!loadersByParent.has(parentId)) {
378
+ loadersByParent.set(parentId, []);
379
+ }
380
+ loadersByParent.get(parentId)!.push(segment);
381
+ } else {
382
+ // Layout, route, error, and notFound segments are all rendered in the tree
383
+ // Error/notFound segments replace the failed segment with fallback UI
384
+ nonParallels.push(segment);
385
+ }
386
+ }
387
+
388
+ // INTERCEPT SEGMENTS: Explicitly inject into parent's parallel array
389
+ // Intercept segments are passed separately for explicit handling
390
+ if (interceptSegments && interceptSegments.length > 0) {
391
+ for (const intercept of interceptSegments) {
392
+ if (intercept.type === "parallel" && intercept.slot) {
393
+ // Extract parent ID from intercept ID (e.g., "M4L0L0L2.@modal" → "M4L0L0L2")
394
+ const parentId = intercept.id.split(".")[0];
395
+ if (!parallelsByParent.has(parentId)) {
396
+ parallelsByParent.set(parentId, []);
397
+ }
398
+ parallelsByParent.get(parentId)!.push(intercept);
399
+ } else if (intercept.type === "loader") {
400
+ // Intercept loaders - extract parent from loader ID
401
+ const parentId = intercept.id.split("D")[0];
402
+ if (!loadersByParent.has(parentId)) {
403
+ loadersByParent.set(parentId, []);
404
+ }
405
+ loadersByParent.get(parentId)!.push(intercept);
406
+ }
407
+ }
408
+ }
409
+
410
+ // Sort segments by ID to ensure consistent root-to-leaf ordering
411
+ // regardless of the order they arrive in the input array (which can differ
412
+ // between document requests and actions)
413
+ // Shorter IDs come first (closer to root), same length sorted lexicographically
414
+ nonParallels.sort((a, b) => {
415
+ if (a.id.length !== b.id.length) {
416
+ return a.id.length - b.id.length;
417
+ }
418
+ return a.id.localeCompare(b.id);
419
+ });
420
+
421
+ // Iterate bottom-to-top using reverse() to process leaf segments first
422
+ // This processes route/leaf layouts first, then parent layouts
423
+ // Note: We reverse the array to iterate from end to start (bottom-to-top)
424
+ for (let i = nonParallels.length - 1; i >= 0; i--) {
425
+ const segment = nonParallels[i];
426
+
427
+ // Lookup parallels and loaders that belong to this segment by ID prefix
428
+ const parallel = parallelsByParent.get(segment.id) || [];
429
+ const loaders = loadersByParent.get(segment.id) || [];
430
+
431
+ // Also include loaders from parallel segments (e.g., intercept loaders)
432
+ // These have parent IDs like "M9L0L1.@modal" which match the parallel segment ID
433
+ for (const p of parallel) {
434
+ const parallelLoaders = loadersByParent.get(p.id);
435
+ if (parallelLoaders) {
436
+ loaders.push(...parallelLoaders);
437
+ }
438
+ }
439
+
440
+ yield { segment, parallel, loaders };
441
+ }
442
+ }