@rangojs/router 0.0.0-experimental.cb54cbba → 0.0.0-experimental.debug-cache-2383ca26

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 (65) hide show
  1. package/AGENTS.md +4 -0
  2. package/dist/bin/rango.js +8 -3
  3. package/dist/vite/index.js +139 -200
  4. package/package.json +15 -14
  5. package/skills/caching/SKILL.md +37 -4
  6. package/skills/parallel/SKILL.md +126 -0
  7. package/src/browser/event-controller.ts +5 -0
  8. package/src/browser/navigation-bridge.ts +1 -3
  9. package/src/browser/navigation-client.ts +60 -27
  10. package/src/browser/navigation-transaction.ts +11 -9
  11. package/src/browser/partial-update.ts +50 -9
  12. package/src/browser/prefetch/cache.ts +57 -5
  13. package/src/browser/prefetch/fetch.ts +30 -21
  14. package/src/browser/prefetch/queue.ts +53 -13
  15. package/src/browser/react/Link.tsx +9 -1
  16. package/src/browser/react/NavigationProvider.tsx +27 -0
  17. package/src/browser/rsc-router.tsx +109 -57
  18. package/src/browser/scroll-restoration.ts +31 -34
  19. package/src/browser/segment-reconciler.ts +6 -1
  20. package/src/browser/types.ts +9 -0
  21. package/src/build/route-types/router-processing.ts +12 -2
  22. package/src/cache/cache-runtime.ts +15 -11
  23. package/src/cache/cache-scope.ts +43 -3
  24. package/src/cache/cf/cf-cache-store.ts +453 -11
  25. package/src/cache/cf/index.ts +5 -1
  26. package/src/cache/document-cache.ts +17 -7
  27. package/src/cache/index.ts +1 -0
  28. package/src/debug.ts +2 -2
  29. package/src/route-definition/dsl-helpers.ts +32 -7
  30. package/src/route-definition/redirect.ts +2 -2
  31. package/src/route-map-builder.ts +7 -1
  32. package/src/router/find-match.ts +4 -2
  33. package/src/router/intercept-resolution.ts +2 -0
  34. package/src/router/lazy-includes.ts +4 -1
  35. package/src/router/logging.ts +5 -2
  36. package/src/router/manifest.ts +9 -3
  37. package/src/router/match-middleware/background-revalidation.ts +30 -2
  38. package/src/router/match-middleware/cache-lookup.ts +66 -9
  39. package/src/router/match-middleware/cache-store.ts +53 -10
  40. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  41. package/src/router/match-middleware/segment-resolution.ts +8 -5
  42. package/src/router/match-result.ts +22 -6
  43. package/src/router/metrics.ts +6 -1
  44. package/src/router/middleware.ts +2 -1
  45. package/src/router/router-context.ts +6 -1
  46. package/src/router/segment-resolution/fresh.ts +122 -15
  47. package/src/router/segment-resolution/loader-cache.ts +1 -0
  48. package/src/router/segment-resolution/revalidation.ts +347 -290
  49. package/src/router/segment-wrappers.ts +2 -0
  50. package/src/router.ts +5 -1
  51. package/src/segment-system.tsx +140 -4
  52. package/src/server/context.ts +90 -13
  53. package/src/server/request-context.ts +10 -4
  54. package/src/ssr/index.tsx +1 -0
  55. package/src/types/handler-context.ts +103 -17
  56. package/src/types/route-entry.ts +7 -0
  57. package/src/types/segments.ts +2 -0
  58. package/src/urls/path-helper.ts +1 -1
  59. package/src/vite/discovery/state.ts +0 -2
  60. package/src/vite/plugin-types.ts +0 -83
  61. package/src/vite/plugins/expose-action-id.ts +1 -3
  62. package/src/vite/plugins/version-plugin.ts +13 -1
  63. package/src/vite/rango.ts +144 -209
  64. package/src/vite/router-discovery.ts +0 -8
  65. package/src/vite/utils/banner.ts +3 -3
@@ -123,7 +123,6 @@ export function withInterceptResolution<TEnv>(
123
123
  return async function* (
124
124
  source: AsyncGenerator<ResolvedSegment>,
125
125
  ): AsyncGenerator<ResolvedSegment> {
126
- const pipelineStart = performance.now();
127
126
  const ms = ctx.metricsStore;
128
127
 
129
128
  // First, yield all segments from the source (main segment resolution or cache)
@@ -133,13 +132,16 @@ export function withInterceptResolution<TEnv>(
133
132
  yield segment;
134
133
  }
135
134
 
135
+ // Measure own work only (after source iteration completes)
136
+ const ownStart = performance.now();
137
+
136
138
  // Skip intercept resolution for full match (document requests don't have intercepts)
137
139
  if (ctx.isFullMatch) {
138
140
  if (ms) {
139
141
  ms.metrics.push({
140
142
  label: "pipeline:intercept",
141
- duration: performance.now() - pipelineStart,
142
- startTime: pipelineStart - ms.requestStart,
143
+ duration: performance.now() - ownStart,
144
+ startTime: ownStart - ms.requestStart,
143
145
  });
144
146
  }
145
147
  return;
@@ -163,8 +165,8 @@ export function withInterceptResolution<TEnv>(
163
165
  if (ms) {
164
166
  ms.metrics.push({
165
167
  label: "pipeline:intercept",
166
- duration: performance.now() - pipelineStart,
167
- startTime: pipelineStart - ms.requestStart,
168
+ duration: performance.now() - ownStart,
169
+ startTime: ownStart - ms.requestStart,
168
170
  });
169
171
  }
170
172
  return;
@@ -216,8 +218,8 @@ export function withInterceptResolution<TEnv>(
216
218
  if (ms) {
217
219
  ms.metrics.push({
218
220
  label: "pipeline:intercept",
219
- duration: performance.now() - pipelineStart,
220
- startTime: pipelineStart - ms.requestStart,
221
+ duration: performance.now() - ownStart,
222
+ startTime: ownStart - ms.requestStart,
221
223
  });
222
224
  }
223
225
  };
@@ -104,7 +104,6 @@ export function withSegmentResolution<TEnv>(
104
104
  return async function* (
105
105
  source: AsyncGenerator<ResolvedSegment>,
106
106
  ): AsyncGenerator<ResolvedSegment> {
107
- const pipelineStart = performance.now();
108
107
  const ms = ctx.metricsStore;
109
108
 
110
109
  // IMPORTANT: Always iterate source first to give cache-lookup a chance
@@ -113,13 +112,16 @@ export function withSegmentResolution<TEnv>(
113
112
  yield segment;
114
113
  }
115
114
 
115
+ // Measure own work only (after source iteration completes)
116
+ const ownStart = performance.now();
117
+
116
118
  // If cache hit, segments were already yielded by cache lookup
117
119
  if (state.cacheHit) {
118
120
  if (ms) {
119
121
  ms.metrics.push({
120
122
  label: "pipeline:segment-resolve",
121
- duration: performance.now() - pipelineStart,
122
- startTime: pipelineStart - ms.requestStart,
123
+ duration: performance.now() - ownStart,
124
+ startTime: ownStart - ms.requestStart,
123
125
  });
124
126
  }
125
127
  return;
@@ -168,6 +170,7 @@ export function withSegmentResolution<TEnv>(
168
170
  ctx.interceptResult,
169
171
  ctx.localRouteName,
170
172
  ctx.pathname,
173
+ ctx.stale,
171
174
  ),
172
175
  );
173
176
 
@@ -184,8 +187,8 @@ export function withSegmentResolution<TEnv>(
184
187
  if (ms) {
185
188
  ms.metrics.push({
186
189
  label: "pipeline:segment-resolve",
187
- duration: performance.now() - pipelineStart,
188
- startTime: pipelineStart - ms.requestStart,
190
+ duration: performance.now() - ownStart,
191
+ startTime: ownStart - ms.requestStart,
189
192
  });
190
193
  }
191
194
  };
@@ -67,10 +67,11 @@
67
67
  * Keep if:
68
68
  * - component !== null (needs rendering)
69
69
  * - type === "loader" (carries data even with null component)
70
+ * - client doesn't have the segment (structurally required parent node)
70
71
  *
71
72
  * Skip if:
72
- * - component === null AND type !== "loader"
73
- * - (Client already has this segment's UI)
73
+ * - component === null AND type !== "loader" AND client has it cached
74
+ * - (Revalidation skip — client already has this segment's UI)
74
75
  *
75
76
  *
76
77
  * INTERCEPT HANDLING
@@ -109,6 +110,7 @@
109
110
  import type { MatchResult, ResolvedSegment } from "../types.js";
110
111
  import type { MatchContext, MatchPipelineState } from "./match-context.js";
111
112
  import { debugLog } from "./logging.js";
113
+ import { appendMetric } from "./metrics.js";
112
114
 
113
115
  /**
114
116
  * Collect all segments from an async generator
@@ -167,10 +169,15 @@ export function buildMatchResult<TEnv>(
167
169
  // Deduplicate allIds (defense-in-depth for partial match path)
168
170
  allIds = [...new Set(allIds)];
169
171
 
170
- // Filter out segments with null components (client already has them)
171
- // BUT always include loader segments - they carry data even with null component
172
+ // Filter out null-component segments only when the client already has
173
+ // them cached (revalidation skip). If the client doesn't have the segment,
174
+ // it must be included even with null component — it's structurally required
175
+ // as a parent node for child layouts/parallels to reconcile against.
176
+ // Loader segments are always included as they carry data.
177
+ const clientIdSet = new Set(ctx.clientSegmentIds);
172
178
  segmentsToRender = allSegments.filter(
173
- (s) => s.component !== null || s.type === "loader",
179
+ (s) =>
180
+ s.component !== null || s.type === "loader" || !clientIdSet.has(s.id),
174
181
  );
175
182
  }
176
183
 
@@ -210,10 +217,19 @@ export async function collectMatchResult<TEnv>(
210
217
  ): Promise<MatchResult> {
211
218
  const allSegments = await collectSegments(pipeline);
212
219
 
220
+ const buildStart = performance.now();
221
+
213
222
  // Update state with collected segments if not already set
214
223
  if (state.segments.length === 0) {
215
224
  state.segments = allSegments;
216
225
  }
217
226
 
218
- return buildMatchResult(allSegments, ctx, state);
227
+ const result = buildMatchResult(allSegments, ctx, state);
228
+ appendMetric(
229
+ ctx.metricsStore,
230
+ "collect-result",
231
+ buildStart,
232
+ performance.now() - buildStart,
233
+ );
234
+ return result;
219
235
  }
@@ -15,7 +15,12 @@ function formatMs(value: number): string {
15
15
  }
16
16
 
17
17
  function sortMetrics(metrics: PerformanceMetric[]): PerformanceMetric[] {
18
- return [...metrics].sort((a, b) => a.startTime - b.startTime);
18
+ return [...metrics].sort((a, b) => {
19
+ // handler:total always goes last (it wraps everything)
20
+ if (a.label === "handler:total") return 1;
21
+ if (b.label === "handler:total") return -1;
22
+ return a.startTime - b.startTime;
23
+ });
19
24
  }
20
25
 
21
26
  interface Span {
@@ -21,6 +21,7 @@ import type {
21
21
  import { _getRequestContext } from "../server/request-context.js";
22
22
  import { isAutoGeneratedRouteName } from "../route-name.js";
23
23
  import { appendMetric, createMetricsStore } from "./metrics.js";
24
+ import { stripInternalParams } from "./handler-context.js";
24
25
 
25
26
  // Re-export types and cookie utilities for backward compatibility
26
27
  export type {
@@ -147,7 +148,7 @@ export function createMiddlewareContext<TEnv>(
147
148
  search?: Record<string, unknown>,
148
149
  ) => string,
149
150
  ): MiddlewareContext<TEnv> {
150
- const url = new URL(request.url);
151
+ const url = stripInternalParams(new URL(request.url));
151
152
 
152
153
  // Track the initial response to detect pre/post-next() phase.
153
154
  // Before next(): responseHolder.response === initialResponse (the stub).
@@ -138,6 +138,7 @@ export interface RouterContext<TEnv = any> {
138
138
  interceptResult: InterceptResult | null,
139
139
  localRouteName: string,
140
140
  pathname: string,
141
+ stale?: boolean,
141
142
  ) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
142
143
 
143
144
  // Generator-based segment resolution (for pipeline)
@@ -188,7 +189,10 @@ export interface RouterContext<TEnv = any> {
188
189
  | "cache-hit"
189
190
  | "loader"
190
191
  | "parallel"
191
- | "orphan-layout";
192
+ | "orphan-layout"
193
+ | "route-handler"
194
+ | "layout-handler"
195
+ | "intercept-loader";
192
196
  }) => Promise<boolean>;
193
197
 
194
198
  // Request context
@@ -206,6 +210,7 @@ export interface RouterContext<TEnv = any> {
206
210
  params: Record<string, string>,
207
211
  handlerContext: HandlerContext<any, TEnv>,
208
212
  loaderPromises: Map<string, Promise<any>>,
213
+ options?: { skipLoaders?: boolean },
209
214
  ) => Promise<ResolvedSegment[]>;
210
215
 
211
216
  // Generator-based simple resolution
@@ -7,7 +7,11 @@
7
7
 
8
8
  import type { ReactNode } from "react";
9
9
  import { invariant } from "../../errors";
10
- import type { EntryData } from "../../server/context";
10
+ import {
11
+ getParallelEntries,
12
+ getParallelSlotEntries,
13
+ type EntryData,
14
+ } from "../../server/context";
11
15
  import type {
12
16
  HandlerContext,
13
17
  InternalHandlerContext,
@@ -15,6 +19,8 @@ import type {
15
19
  } from "../../types";
16
20
  import type { SegmentResolutionDeps } from "../types.js";
17
21
  import { resolveLoaderData } from "./loader-cache.js";
22
+ import { _getRequestContext } from "../../server/request-context.js";
23
+ import { appendMetric } from "../metrics.js";
18
24
  import {
19
25
  handleHandlerResult,
20
26
  tryStaticHandler,
@@ -90,8 +96,12 @@ export async function resolveLoaders<TEnv>(
90
96
  const shortCode = shortCodeOverride ?? entry.shortCode;
91
97
  const hasLoading = "loading" in entry && entry.loading !== undefined;
92
98
  const loadingDisabled = hasLoading && entry.loading === false;
99
+ const ms = _getRequestContext()?._metricsStore;
93
100
 
94
101
  if (!loadingDisabled) {
102
+ // Streaming loaders: promises kick off now, settle during RSC serialization.
103
+ // No per-loader timing here — settlement happens asynchronously during
104
+ // RSC/SSR stream consumption, after the perf timeline is logged.
95
105
  return loaderEntries.map((loaderEntry, i) => {
96
106
  const { loader } = loaderEntry;
97
107
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
@@ -116,14 +126,31 @@ export async function resolveLoaders<TEnv>(
116
126
 
117
127
  // Loading disabled: still start all loaders in parallel, but only emit
118
128
  // settled promises so handlers don't stream loading placeholders.
119
- const pendingLoaderData = loaderEntries.map((loaderEntry) =>
120
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
121
- );
122
- await Promise.all(pendingLoaderData);
129
+ // We can measure actual execution time here since we await all loaders.
130
+ const pendingLoaderData = loaderEntries.map((loaderEntry) => {
131
+ const start = performance.now();
132
+ const promise = resolveLoaderData(loaderEntry, ctx, ctx.pathname);
133
+ return { promise, start, loaderId: loaderEntry.loader.$$id };
134
+ });
135
+ await Promise.all(pendingLoaderData.map((p) => p.promise));
123
136
 
124
137
  return loaderEntries.map((loaderEntry, i) => {
125
138
  const { loader } = loaderEntry;
126
139
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
140
+ const pending = pendingLoaderData[i]!;
141
+ if (ms && !ms.metrics.some((m) => m.label === `loader:${loader.$$id}`)) {
142
+ // All loaders ran in parallel via Promise.all — each span covers
143
+ // from its own kickoff to the batch settlement, giving a ceiling
144
+ // on that loader's contribution to the overall wait.
145
+ const batchEnd = performance.now();
146
+ appendMetric(
147
+ ms,
148
+ `loader:${loader.$$id}`,
149
+ pending.start,
150
+ batchEnd - pending.start,
151
+ 2,
152
+ );
153
+ }
127
154
  return {
128
155
  id: segmentId,
129
156
  namespace: entry.id,
@@ -133,7 +160,7 @@ export async function resolveLoaders<TEnv>(
133
160
  params: ctx.params,
134
161
  loaderId: loader.$$id,
135
162
  loaderData: deps.wrapLoaderPromise(
136
- pendingLoaderData[i]!,
163
+ pending.promise,
137
164
  entry,
138
165
  segmentId,
139
166
  ctx.pathname,
@@ -197,7 +224,10 @@ export async function resolveSegment<TEnv>(
197
224
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
198
225
  });
199
226
 
200
- for (const parallelEntry of entry.parallel) {
227
+ const resolvedParallelEntries = new Set<string>();
228
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
229
+ entry.parallel,
230
+ )) {
201
231
  const parallelSegments = await resolveParallelEntry(
202
232
  parallelEntry,
203
233
  params,
@@ -207,8 +237,11 @@ export async function resolveSegment<TEnv>(
207
237
  deps,
208
238
  options,
209
239
  routeKey,
240
+ [slot],
241
+ !resolvedParallelEntries.has(parallelEntry.id),
210
242
  );
211
243
  segments.push(...parallelSegments);
244
+ resolvedParallelEntries.add(parallelEntry.id);
212
245
  }
213
246
 
214
247
  for (const orphan of entry.layout) {
@@ -286,7 +319,10 @@ export async function resolveSegment<TEnv>(
286
319
  segments.push(...orphanSegments);
287
320
  }
288
321
 
289
- for (const parallelEntry of entry.parallel) {
322
+ const resolvedParallelEntries = new Set<string>();
323
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
324
+ entry.parallel,
325
+ )) {
290
326
  const parallelSegments = await resolveParallelEntry(
291
327
  parallelEntry,
292
328
  params,
@@ -296,8 +332,11 @@ export async function resolveSegment<TEnv>(
296
332
  deps,
297
333
  options,
298
334
  routeKey,
335
+ [slot],
336
+ !resolvedParallelEntries.has(parallelEntry.id),
299
337
  );
300
338
  segments.push(...parallelSegments);
339
+ resolvedParallelEntries.add(parallelEntry.id);
301
340
  }
302
341
 
303
342
  segments.push({
@@ -305,7 +344,7 @@ export async function resolveSegment<TEnv>(
305
344
  namespace: entry.id,
306
345
  type: "route",
307
346
  index: 0,
308
- component,
347
+ component: component ?? null,
309
348
  loading: entry.loading === false ? null : entry.loading,
310
349
  transition: entry.transition,
311
350
  params,
@@ -368,7 +407,10 @@ export async function resolveOrphanLayout<TEnv>(
368
407
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
369
408
  });
370
409
 
371
- for (const parallelEntry of orphan.parallel) {
410
+ const resolvedParallelEntries = new Set<string>();
411
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
412
+ orphan.parallel,
413
+ )) {
372
414
  const parallelSegments = await resolveParallelEntry(
373
415
  parallelEntry,
374
416
  params,
@@ -378,8 +420,11 @@ export async function resolveOrphanLayout<TEnv>(
378
420
  deps,
379
421
  options,
380
422
  routeKey,
423
+ [slot],
424
+ !resolvedParallelEntries.has(parallelEntry.id),
381
425
  );
382
426
  segments.push(...parallelSegments);
427
+ resolvedParallelEntries.add(parallelEntry.id);
383
428
  }
384
429
 
385
430
  return segments;
@@ -397,6 +442,8 @@ export async function resolveParallelEntry<TEnv>(
397
442
  deps: SegmentResolutionDeps<TEnv>,
398
443
  options?: ResolveSegmentOptions,
399
444
  routeKey?: string,
445
+ slotNames?: `@${string}`[],
446
+ includeLoaders: boolean = true,
400
447
  ): Promise<ResolvedSegment[]> {
401
448
  invariant(
402
449
  parallelEntry.type === "parallel",
@@ -411,7 +458,12 @@ export async function resolveParallelEntry<TEnv>(
411
458
  | ReactNode
412
459
  >;
413
460
 
414
- for (const [slot, handler] of Object.entries(slots)) {
461
+ const slotsToResolve = slotNames ?? (Object.keys(slots) as `@${string}`[]);
462
+
463
+ for (const slot of slotsToResolve) {
464
+ // Try static lookup first — in production, handler bodies are evicted
465
+ // and replaced with stubs that have no .handler property (undefined).
466
+ // The static store holds the pre-rendered component for these slots.
415
467
  let component: ReactNode | undefined = await tryStaticSlot(
416
468
  parallelEntry,
417
469
  slot,
@@ -419,6 +471,10 @@ export async function resolveParallelEntry<TEnv>(
419
471
  );
420
472
 
421
473
  if (component === undefined) {
474
+ const handler = slots[slot];
475
+ if (handler === undefined) {
476
+ continue;
477
+ }
422
478
  const doneParallelHandler = track(
423
479
  `handler:${parallelEntry.id}.${slot}`,
424
480
  2,
@@ -472,7 +528,7 @@ export async function resolveParallelEntry<TEnv>(
472
528
  });
473
529
  }
474
530
 
475
- if (!parallelEntry.loading && !options?.skipLoaders) {
531
+ if (!options?.skipLoaders && includeLoaders) {
476
532
  const loaderSegments = await resolveLoaders(
477
533
  parallelEntry,
478
534
  context,
@@ -480,6 +536,15 @@ export async function resolveParallelEntry<TEnv>(
480
536
  deps,
481
537
  parentShortCode,
482
538
  );
539
+ // Tag parallel-owned loaders so renderSegments can stream them
540
+ // using the parallel's loading() instead of awaiting on the layout
541
+ const parallelLoading =
542
+ parallelEntry.loading === false ? undefined : parallelEntry.loading;
543
+ if (parallelLoading) {
544
+ for (const seg of loaderSegments) {
545
+ seg.parallelLoading = parallelLoading;
546
+ }
547
+ }
483
548
  segments.push(...loaderSegments);
484
549
  }
485
550
 
@@ -559,11 +624,53 @@ export async function resolveLoadersOnly<TEnv>(
559
624
  deps: SegmentResolutionDeps<TEnv>,
560
625
  ): Promise<ResolvedSegment[]> {
561
626
  const loaderSegments: ResolvedSegment[] = [];
627
+ const seenIds = new Set<string>();
628
+
629
+ async function collectEntryLoaders(
630
+ entry: EntryData,
631
+ belongsToRoute: boolean,
632
+ shortCodeOverride?: string,
633
+ ): Promise<void> {
634
+ // Skip if all loaders from this entry have already been resolved
635
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
636
+ const entryLoaders = entry.loader ?? [];
637
+ const sc = shortCodeOverride ?? entry.shortCode;
638
+ const allAlreadySeen =
639
+ entryLoaders.length > 0 &&
640
+ entryLoaders.every((le, i) =>
641
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
642
+ );
643
+ if (!allAlreadySeen) {
644
+ const segments = await resolveLoaders(
645
+ entry,
646
+ context,
647
+ belongsToRoute,
648
+ deps,
649
+ shortCodeOverride,
650
+ );
651
+ for (const seg of segments) {
652
+ if (!seenIds.has(seg.id)) {
653
+ seenIds.add(seg.id);
654
+ loaderSegments.push(seg);
655
+ }
656
+ }
657
+ }
658
+
659
+ const seenParallelEntryIds = new Set<string>();
660
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
661
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
662
+ seenParallelEntryIds.add(parallelEntry.id);
663
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
664
+ }
665
+
666
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
667
+ for (const layoutEntry of entry.layout) {
668
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
669
+ }
670
+ }
562
671
 
563
672
  for (const entry of entries) {
564
- const belongsToRoute = entry.type === "route";
565
- const segments = await resolveLoaders(entry, context, belongsToRoute, deps);
566
- loaderSegments.push(...segments);
673
+ await collectEntryLoaders(entry, entry.type === "route");
567
674
  }
568
675
 
569
676
  return loaderSegments;
@@ -147,6 +147,7 @@ export function resolveLoaderData<TEnv>(
147
147
  }
148
148
 
149
149
  const loaderId = loaderEntry.loader.$$id;
150
+
150
151
  const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
151
152
  const swrWindow = resolveSwrWindow(options.swr, store.defaults);
152
153
  const swr = swrWindow || undefined;