@rangojs/router 0.0.0-experimental.56cb65a7 → 0.0.0-experimental.57005a2b

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 (134) hide show
  1. package/README.md +76 -18
  2. package/dist/bin/rango.js +130 -47
  3. package/dist/vite/index.js +716 -237
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/package.json +3 -3
  6. package/skills/cache-guide/SKILL.md +32 -0
  7. package/skills/caching/SKILL.md +8 -0
  8. package/skills/handler-use/SKILL.md +362 -0
  9. package/skills/intercept/SKILL.md +20 -0
  10. package/skills/layout/SKILL.md +22 -0
  11. package/skills/links/SKILL.md +3 -1
  12. package/skills/loader/SKILL.md +53 -43
  13. package/skills/middleware/SKILL.md +34 -3
  14. package/skills/migrate-nextjs/SKILL.md +560 -0
  15. package/skills/migrate-react-router/SKILL.md +764 -0
  16. package/skills/parallel/SKILL.md +126 -0
  17. package/skills/prerender/SKILL.md +110 -68
  18. package/skills/rango/SKILL.md +24 -22
  19. package/skills/route/SKILL.md +55 -0
  20. package/skills/router-setup/SKILL.md +87 -2
  21. package/skills/typesafety/SKILL.md +10 -0
  22. package/src/__internal.ts +1 -1
  23. package/src/browser/app-version.ts +14 -0
  24. package/src/browser/navigation-bridge.ts +37 -5
  25. package/src/browser/navigation-client.ts +98 -46
  26. package/src/browser/navigation-store.ts +43 -8
  27. package/src/browser/partial-update.ts +51 -6
  28. package/src/browser/prefetch/cache.ts +16 -6
  29. package/src/browser/prefetch/fetch.ts +68 -6
  30. package/src/browser/prefetch/queue.ts +61 -29
  31. package/src/browser/prefetch/resource-ready.ts +77 -0
  32. package/src/browser/react/Link.tsx +67 -8
  33. package/src/browser/react/NavigationProvider.tsx +13 -4
  34. package/src/browser/react/context.ts +7 -2
  35. package/src/browser/react/use-handle.ts +9 -58
  36. package/src/browser/react/use-router.ts +21 -8
  37. package/src/browser/rsc-router.tsx +26 -3
  38. package/src/browser/scroll-restoration.ts +10 -8
  39. package/src/browser/segment-reconciler.ts +36 -14
  40. package/src/browser/server-action-bridge.ts +8 -6
  41. package/src/browser/types.ts +27 -5
  42. package/src/build/generate-manifest.ts +6 -6
  43. package/src/build/generate-route-types.ts +3 -0
  44. package/src/build/route-trie.ts +50 -24
  45. package/src/build/route-types/include-resolution.ts +8 -1
  46. package/src/build/route-types/router-processing.ts +211 -72
  47. package/src/build/route-types/scan-filter.ts +8 -1
  48. package/src/cache/cache-runtime.ts +15 -11
  49. package/src/cache/cache-scope.ts +46 -5
  50. package/src/cache/taint.ts +55 -0
  51. package/src/client.tsx +84 -230
  52. package/src/context-var.ts +72 -2
  53. package/src/handle.ts +40 -0
  54. package/src/index.rsc.ts +3 -1
  55. package/src/index.ts +46 -6
  56. package/src/prerender/store.ts +5 -4
  57. package/src/prerender.ts +138 -77
  58. package/src/reverse.ts +25 -1
  59. package/src/route-definition/dsl-helpers.ts +194 -32
  60. package/src/route-definition/helpers-types.ts +67 -19
  61. package/src/route-definition/index.ts +3 -0
  62. package/src/route-definition/redirect.ts +9 -1
  63. package/src/route-definition/resolve-handler-use.ts +149 -0
  64. package/src/route-types.ts +18 -0
  65. package/src/router/content-negotiation.ts +100 -1
  66. package/src/router/handler-context.ts +82 -23
  67. package/src/router/intercept-resolution.ts +9 -4
  68. package/src/router/lazy-includes.ts +5 -5
  69. package/src/router/loader-resolution.ts +156 -21
  70. package/src/router/manifest.ts +12 -7
  71. package/src/router/match-api.ts +124 -189
  72. package/src/router/match-middleware/background-revalidation.ts +12 -1
  73. package/src/router/match-middleware/cache-lookup.ts +74 -14
  74. package/src/router/match-middleware/cache-store.ts +21 -4
  75. package/src/router/match-middleware/segment-resolution.ts +53 -0
  76. package/src/router/match-result.ts +93 -9
  77. package/src/router/middleware-types.ts +6 -8
  78. package/src/router/middleware.ts +2 -5
  79. package/src/router/navigation-snapshot.ts +182 -0
  80. package/src/router/prerender-match.ts +110 -10
  81. package/src/router/preview-match.ts +30 -102
  82. package/src/router/request-classification.ts +310 -0
  83. package/src/router/route-snapshot.ts +245 -0
  84. package/src/router/router-context.ts +1 -0
  85. package/src/router/router-interfaces.ts +36 -4
  86. package/src/router/router-options.ts +37 -11
  87. package/src/router/segment-resolution/fresh.ts +106 -18
  88. package/src/router/segment-resolution/helpers.ts +29 -24
  89. package/src/router/segment-resolution/revalidation.ts +129 -27
  90. package/src/router/types.ts +1 -0
  91. package/src/router.ts +54 -5
  92. package/src/rsc/handler.ts +472 -372
  93. package/src/rsc/loader-fetch.ts +23 -3
  94. package/src/rsc/manifest-init.ts +5 -1
  95. package/src/rsc/progressive-enhancement.ts +14 -2
  96. package/src/rsc/rsc-rendering.ts +10 -1
  97. package/src/rsc/server-action.ts +8 -0
  98. package/src/rsc/ssr-setup.ts +2 -2
  99. package/src/rsc/types.ts +9 -1
  100. package/src/segment-content-promise.ts +67 -0
  101. package/src/segment-loader-promise.ts +122 -0
  102. package/src/segment-system.tsx +11 -61
  103. package/src/server/context.ts +76 -4
  104. package/src/server/handle-store.ts +19 -0
  105. package/src/server/loader-registry.ts +9 -8
  106. package/src/server/request-context.ts +175 -15
  107. package/src/ssr/index.tsx +3 -0
  108. package/src/static-handler.ts +18 -6
  109. package/src/types/cache-types.ts +4 -4
  110. package/src/types/handler-context.ts +137 -33
  111. package/src/types/loader-types.ts +36 -9
  112. package/src/types/route-entry.ts +12 -1
  113. package/src/types/segments.ts +1 -1
  114. package/src/urls/include-helper.ts +24 -14
  115. package/src/urls/path-helper-types.ts +39 -6
  116. package/src/urls/path-helper.ts +47 -12
  117. package/src/urls/pattern-types.ts +12 -0
  118. package/src/urls/response-types.ts +16 -6
  119. package/src/use-loader.tsx +77 -5
  120. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  121. package/src/vite/discovery/discover-routers.ts +5 -1
  122. package/src/vite/discovery/prerender-collection.ts +128 -74
  123. package/src/vite/discovery/state.ts +13 -4
  124. package/src/vite/index.ts +4 -0
  125. package/src/vite/plugin-types.ts +60 -5
  126. package/src/vite/plugins/expose-id-utils.ts +12 -0
  127. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  128. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  129. package/src/vite/plugins/performance-tracks.ts +88 -0
  130. package/src/vite/plugins/refresh-cmd.ts +88 -26
  131. package/src/vite/rango.ts +19 -2
  132. package/src/vite/router-discovery.ts +178 -37
  133. package/src/vite/utils/prerender-utils.ts +37 -5
  134. package/src/vite/utils/shared-utils.ts +3 -2
@@ -8,6 +8,7 @@ import type {
8
8
  import type { NonceProvider } from "../rsc/types.js";
9
9
  import type { ExecutionContext } from "../server/request-context.js";
10
10
  import type { UrlPatterns } from "../urls.js";
11
+ import type { UrlBuilder } from "../urls/pattern-types.js";
11
12
  import type { NamedRouteEntry } from "./content-negotiation.js";
12
13
  import type { TelemetrySink } from "./telemetry.js";
13
14
  import type { RouterTimeouts, OnTimeoutCallback } from "./timeout.js";
@@ -95,6 +96,28 @@ export interface RSCRouterOptions<TEnv = any> {
95
96
  */
96
97
  $$sourceFile?: string;
97
98
 
99
+ /**
100
+ * URL prefix applied to all routes registered with this router.
101
+ *
102
+ * Useful when the app is served under a sub-path (e.g. `/admin` or `/v2`).
103
+ * All `path()` patterns are automatically prefixed and `reverse()` returns
104
+ * full paths including the basename. Route names are NOT prefixed.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * const router = createRouter({
109
+ * basename: "/admin",
110
+ * }).routes(({ path }) => [
111
+ * path("/", Dashboard, { name: "home" }), // matches /admin
112
+ * path("/users", Users, { name: "users" }), // matches /admin/users
113
+ * ]);
114
+ *
115
+ * router.reverse("home"); // "/admin"
116
+ * router.reverse("users"); // "/admin/users"
117
+ * ```
118
+ */
119
+ basename?: string;
120
+
98
121
  /**
99
122
  * Enable performance metrics collection
100
123
  * When enabled, metrics are output to console and available via Server-Timing header
@@ -337,25 +360,28 @@ export interface RSCRouterOptions<TEnv = any> {
337
360
  /**
338
361
  * URL patterns to register with the router.
339
362
  *
340
- * Alternative to calling `.routes()` method - allows passing patterns
341
- * directly in the config for a more concise setup.
363
+ * Accepts either a `UrlPatterns` object from `urls()` or a builder function
364
+ * directly (urls() is called implicitly).
342
365
  *
343
366
  * @example
344
367
  * ```typescript
345
- * import { urls } from "@rangojs/router/server";
346
- *
347
- * const urlpatterns = urls(({ path, layout }) => [
348
- * path("/", HomePage, { name: "home" }),
349
- * path("/about", AboutPage, { name: "about" }),
350
- * ]);
351
- *
352
- * const router = createRouter<AppEnv>({
368
+ * // With urls()
369
+ * createRouter<AppEnv>({
353
370
  * document: Document,
354
371
  * urls: urlpatterns,
355
372
  * });
373
+ *
374
+ * // With builder function
375
+ * createRouter<AppEnv>({
376
+ * document: Document,
377
+ * urls: ({ path }) => [
378
+ * path("/", HomePage, { name: "home" }),
379
+ * path("/about", AboutPage, { name: "about" }),
380
+ * ],
381
+ * });
356
382
  * ```
357
383
  */
358
- urls?: UrlPatterns<TEnv, any>;
384
+ urls?: UrlPatterns<TEnv, any> | UrlBuilder<TEnv>;
359
385
 
360
386
  /**
361
387
  * Injected by the Vite transform at compile time.
@@ -30,7 +30,11 @@ import {
30
30
  } from "./helpers.js";
31
31
  import { getRouterContext } from "../router-context.js";
32
32
  import { resolveSink, safeEmit } from "../telemetry.js";
33
- import { track } from "../../server/context.js";
33
+ import {
34
+ track,
35
+ RSCRouterContext,
36
+ runInsideLoaderScope,
37
+ } from "../../server/context.js";
34
38
 
35
39
  // ---------------------------------------------------------------------------
36
40
  // Streamed handler telemetry
@@ -100,9 +104,7 @@ export async function resolveLoaders<TEnv>(
100
104
 
101
105
  if (!loadingDisabled) {
102
106
  // 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.
105
- return loaderEntries.map((loaderEntry, i) => {
107
+ const segments = loaderEntries.map((loaderEntry, i) => {
106
108
  const { loader } = loaderEntry;
107
109
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
108
110
  return {
@@ -114,7 +116,9 @@ export async function resolveLoaders<TEnv>(
114
116
  params: ctx.params,
115
117
  loaderId: loader.$$id,
116
118
  loaderData: deps.wrapLoaderPromise(
117
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
119
+ runInsideLoaderScope(() =>
120
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
121
+ ),
118
122
  entry,
119
123
  segmentId,
120
124
  ctx.pathname,
@@ -122,14 +126,17 @@ export async function resolveLoaders<TEnv>(
122
126
  belongsToRoute,
123
127
  };
124
128
  });
129
+
130
+ return segments;
125
131
  }
126
132
 
127
133
  // Loading disabled: still start all loaders in parallel, but only emit
128
134
  // settled promises so handlers don't stream loading placeholders.
129
- // We can measure actual execution time here since we await all loaders.
130
135
  const pendingLoaderData = loaderEntries.map((loaderEntry) => {
131
136
  const start = performance.now();
132
- const promise = resolveLoaderData(loaderEntry, ctx, ctx.pathname);
137
+ const promise = runInsideLoaderScope(() =>
138
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
139
+ );
133
140
  return { promise, start, loaderId: loaderEntry.loader.$$id };
134
141
  });
135
142
  await Promise.all(pendingLoaderData.map((p) => p.promise));
@@ -277,9 +284,14 @@ export async function resolveSegment<TEnv>(
277
284
  entry.shortCode,
278
285
  );
279
286
  if (component === undefined) {
287
+ // For Passthrough routes at runtime, use the live handler instead of
288
+ // the build handler. At build time (context.build === true), always
289
+ // use the build handler from entry.handler.
290
+ const handler =
291
+ !context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
280
292
  const doneRouteHandler = track(`handler:${entry.id}`, 2);
281
293
  if (entry.loading) {
282
- const result = handleHandlerResult(entry.handler(context));
294
+ const result = handleHandlerResult(handler(context));
283
295
  if (result instanceof Promise) {
284
296
  result.finally(doneRouteHandler).catch(() => {});
285
297
  const tracked = deps.trackHandler(result, {
@@ -300,7 +312,7 @@ export async function resolveSegment<TEnv>(
300
312
  component = result;
301
313
  }
302
314
  } else {
303
- component = handleHandlerResult(await entry.handler(context));
315
+ component = handleHandlerResult(await handler(context));
304
316
  doneRouteHandler();
305
317
  }
306
318
  }
@@ -315,6 +327,7 @@ export async function resolveSegment<TEnv>(
315
327
  deps,
316
328
  options,
317
329
  routeKey,
330
+ entry,
318
331
  );
319
332
  segments.push(...orphanSegments);
320
333
  }
@@ -344,7 +357,7 @@ export async function resolveSegment<TEnv>(
344
357
  namespace: entry.id,
345
358
  type: "route",
346
359
  index: 0,
347
- component,
360
+ component: component ?? null,
348
361
  loading: entry.loading === false ? null : entry.loading,
349
362
  transition: entry.transition,
350
363
  params,
@@ -370,6 +383,9 @@ export async function resolveOrphanLayout<TEnv>(
370
383
  deps: SegmentResolutionDeps<TEnv>,
371
384
  options?: ResolveSegmentOptions,
372
385
  routeKey?: string,
386
+ /** Parent route entry — its loaders are inherited by the layout so
387
+ * parallel slots inside this layout can access them via useLoader(). */
388
+ parentRouteEntry?: EntryData,
373
389
  ): Promise<ResolvedSegment[]> {
374
390
  invariant(
375
391
  orphan.type === "layout" || orphan.type === "cache",
@@ -385,6 +401,30 @@ export async function resolveOrphanLayout<TEnv>(
385
401
  deps,
386
402
  );
387
403
  segments.push(...loaderSegments);
404
+
405
+ // Inherit parent route's loaders so parallel slots inside this layout
406
+ // can access them via useLoader(). Without this, the route's loaders
407
+ // are only in the route's OutletProvider (rendered as <Outlet /> content),
408
+ // which is a child — not a parent — of the layout's context.
409
+ if (
410
+ parentRouteEntry &&
411
+ parentRouteEntry.loader &&
412
+ parentRouteEntry.loader.length > 0 &&
413
+ Object.keys(orphan.parallel).length > 0
414
+ ) {
415
+ const inheritedLoaders = await resolveLoaders(
416
+ parentRouteEntry,
417
+ context,
418
+ belongsToRoute,
419
+ deps,
420
+ orphan.shortCode,
421
+ );
422
+ // Tag as inherited so buildMatchResult can deduplicate when safe
423
+ for (const s of inheritedLoaders) {
424
+ s._inherited = true;
425
+ }
426
+ segments.push(...inheritedLoaders);
427
+ }
388
428
  }
389
429
 
390
430
  // Handler-first: orphan layout handler executes before its parallels
@@ -580,6 +620,13 @@ export async function resolveAllSegments<TEnv>(
580
620
  } catch {}
581
621
 
582
622
  for (const entry of entries) {
623
+ // Set ALS flag when entering a cache() boundary so that ctx.get()
624
+ // can guard non-cacheable variable reads. Also guards response-level
625
+ // side effects (headers.set). Persists for all descendant entries.
626
+ if (entry.type === "cache") {
627
+ const store = RSCRouterContext.getStore();
628
+ if (store) store.insideCacheScope = true;
629
+ }
583
630
  const doneEntry = track(`segment:${entry.id}`, 1);
584
631
  const resolvedSegments = await resolveWithErrorBoundary(
585
632
  entry,
@@ -624,20 +671,37 @@ export async function resolveLoadersOnly<TEnv>(
624
671
  deps: SegmentResolutionDeps<TEnv>,
625
672
  ): Promise<ResolvedSegment[]> {
626
673
  const loaderSegments: ResolvedSegment[] = [];
674
+ const seenIds = new Set<string>();
627
675
 
628
676
  async function collectEntryLoaders(
629
677
  entry: EntryData,
630
678
  belongsToRoute: boolean,
631
679
  shortCodeOverride?: string,
632
680
  ): Promise<void> {
633
- const segments = await resolveLoaders(
634
- entry,
635
- context,
636
- belongsToRoute,
637
- deps,
638
- shortCodeOverride,
639
- );
640
- loaderSegments.push(...segments);
681
+ // Skip if all loaders from this entry have already been resolved
682
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
683
+ const entryLoaders = entry.loader ?? [];
684
+ const sc = shortCodeOverride ?? entry.shortCode;
685
+ const allAlreadySeen =
686
+ entryLoaders.length > 0 &&
687
+ entryLoaders.every((le, i) =>
688
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
689
+ );
690
+ if (!allAlreadySeen) {
691
+ const segments = await resolveLoaders(
692
+ entry,
693
+ context,
694
+ belongsToRoute,
695
+ deps,
696
+ shortCodeOverride,
697
+ );
698
+ for (const seg of segments) {
699
+ if (!seenIds.has(seg.id)) {
700
+ seenIds.add(seg.id);
701
+ loaderSegments.push(seg);
702
+ }
703
+ }
704
+ }
641
705
 
642
706
  const seenParallelEntryIds = new Set<string>();
643
707
  for (const parallelEntry of getParallelEntries(entry.parallel)) {
@@ -649,6 +713,30 @@ export async function resolveLoadersOnly<TEnv>(
649
713
  const childBelongsToRoute = belongsToRoute || entry.type === "route";
650
714
  for (const layoutEntry of entry.layout) {
651
715
  await collectEntryLoaders(layoutEntry, childBelongsToRoute);
716
+ // Inherit route loaders for orphan layouts with parallels.
717
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
718
+ // route entry, as that would re-iterate route.layout and loop.
719
+ if (
720
+ entry.type === "route" &&
721
+ entry.loader &&
722
+ entry.loader.length > 0 &&
723
+ Object.keys(layoutEntry.parallel).length > 0
724
+ ) {
725
+ const inherited = await resolveLoaders(
726
+ entry,
727
+ context,
728
+ childBelongsToRoute,
729
+ deps,
730
+ layoutEntry.shortCode,
731
+ );
732
+ for (const seg of inherited) {
733
+ if (!seenIds.has(seg.id)) {
734
+ seenIds.add(seg.id);
735
+ seg._inherited = true;
736
+ loaderSegments.push(seg);
737
+ }
738
+ }
739
+ }
652
740
  }
653
741
  }
654
742
 
@@ -8,7 +8,7 @@
8
8
  * - Error boundary segment creation
9
9
  */
10
10
 
11
- import type { ReactNode } from "react";
11
+ import { createElement, type ReactNode } from "react";
12
12
  import { DataNotFoundError } from "../../errors";
13
13
  import {
14
14
  createErrorInfo,
@@ -180,34 +180,39 @@ export function catchSegmentError<TEnv>(
180
180
 
181
181
  if (error instanceof DataNotFoundError) {
182
182
  const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
183
+ // Fall back to router's notFound component, then a plain default
184
+ const notFoundOption = deps.notFoundComponent;
185
+ const defaultFallback =
186
+ typeof notFoundOption === "function"
187
+ ? notFoundOption({ pathname: pathname ?? "" })
188
+ : (notFoundOption ?? createElement("h1", null, "Not Found"));
189
+ const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
183
190
 
184
- if (notFoundFallback) {
185
- const notFoundInfo = createNotFoundInfo(
186
- error,
187
- entry.shortCode,
188
- entry.type,
189
- pathname,
190
- );
191
+ const notFoundInfo = createNotFoundInfo(
192
+ error,
193
+ entry.shortCode,
194
+ entry.type,
195
+ pathname,
196
+ );
191
197
 
192
- reportError(true, {
193
- notFound: true,
194
- message: notFoundInfo.message,
195
- });
198
+ reportError(true, {
199
+ notFound: true,
200
+ message: notFoundInfo.message,
201
+ });
196
202
 
197
- debugLog("segment", "notFound boundary handled error", {
198
- segmentId: entry.shortCode,
199
- message: notFoundInfo.message,
200
- });
203
+ debugLog("segment", "notFound boundary handled error", {
204
+ segmentId: entry.shortCode,
205
+ message: notFoundInfo.message,
206
+ });
201
207
 
202
- setResponseStatus(404);
208
+ setResponseStatus(404);
203
209
 
204
- return createNotFoundSegment(
205
- notFoundInfo,
206
- notFoundFallback,
207
- entry,
208
- params,
209
- );
210
- }
210
+ return createNotFoundSegment(
211
+ notFoundInfo,
212
+ effectiveNotFoundFallback,
213
+ entry,
214
+ params,
215
+ );
211
216
  }
212
217
 
213
218
  const fallback = deps.findNearestErrorBoundary(entry);
@@ -41,7 +41,11 @@ import {
41
41
  } from "./helpers.js";
42
42
  import { getRouterContext } from "../router-context.js";
43
43
  import { resolveSink, safeEmit } from "../telemetry.js";
44
- import { track } from "../../server/context.js";
44
+ import {
45
+ track,
46
+ RSCRouterContext,
47
+ runInsideLoaderScope,
48
+ } from "../../server/context.js";
45
49
 
46
50
  // ---------------------------------------------------------------------------
47
51
  // Telemetry helpers
@@ -232,7 +236,9 @@ export async function resolveLoadersWithRevalidation<TEnv>(
232
236
  params: ctx.params,
233
237
  loaderId: loader.$$id,
234
238
  loaderData: deps.wrapLoaderPromise(
235
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
239
+ runInsideLoaderScope(() =>
240
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
241
+ ),
236
242
  entry,
237
243
  segmentId,
238
244
  ctx.pathname,
@@ -262,29 +268,46 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
262
268
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
263
269
  const allLoaderSegments: ResolvedSegment[] = [];
264
270
  const allMatchedIds: string[] = [];
271
+ const seenIds = new Set<string>();
265
272
 
266
273
  async function collectEntryLoaders(
267
274
  entry: EntryData,
268
275
  belongsToRoute: boolean,
269
276
  shortCodeOverride?: string,
270
277
  ): Promise<void> {
271
- const { segments, matchedIds } = await resolveLoadersWithRevalidation(
272
- entry,
273
- context,
274
- belongsToRoute,
275
- clientSegmentIds,
276
- prevParams,
277
- request,
278
- prevUrl,
279
- nextUrl,
280
- routeKey,
281
- deps,
282
- actionContext,
283
- shortCodeOverride,
284
- stale,
285
- );
286
- allLoaderSegments.push(...segments);
287
- allMatchedIds.push(...matchedIds);
278
+ // Skip if all loaders from this entry have already been resolved
279
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
280
+ const loaderEntries = entry.loader ?? [];
281
+ const sc = shortCodeOverride ?? entry.shortCode;
282
+ const allAlreadySeen =
283
+ loaderEntries.length > 0 &&
284
+ loaderEntries.every((le, i) =>
285
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
286
+ );
287
+ if (!allAlreadySeen) {
288
+ const { segments, matchedIds } = await resolveLoadersWithRevalidation(
289
+ entry,
290
+ context,
291
+ belongsToRoute,
292
+ clientSegmentIds,
293
+ prevParams,
294
+ request,
295
+ prevUrl,
296
+ nextUrl,
297
+ routeKey,
298
+ deps,
299
+ actionContext,
300
+ shortCodeOverride,
301
+ stale,
302
+ );
303
+ for (const seg of segments) {
304
+ if (!seenIds.has(seg.id)) {
305
+ seenIds.add(seg.id);
306
+ allLoaderSegments.push(seg);
307
+ }
308
+ }
309
+ allMatchedIds.push(...matchedIds);
310
+ }
288
311
 
289
312
  const seenParallelEntryIds = new Set<string>();
290
313
  for (const parallelEntry of getParallelEntries(entry.parallel)) {
@@ -296,6 +319,39 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
296
319
  const childBelongsToRoute = belongsToRoute || entry.type === "route";
297
320
  for (const layoutEntry of entry.layout) {
298
321
  await collectEntryLoaders(layoutEntry, childBelongsToRoute);
322
+ // Inherit route loaders for orphan layouts with parallels.
323
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
324
+ // route entry, as that would re-iterate route.layout and loop.
325
+ if (
326
+ entry.type === "route" &&
327
+ entry.loader &&
328
+ entry.loader.length > 0 &&
329
+ Object.keys(layoutEntry.parallel).length > 0
330
+ ) {
331
+ const inherited = await resolveLoadersWithRevalidation(
332
+ entry,
333
+ context,
334
+ childBelongsToRoute,
335
+ clientSegmentIds,
336
+ prevParams,
337
+ request,
338
+ prevUrl,
339
+ nextUrl,
340
+ routeKey,
341
+ deps,
342
+ actionContext,
343
+ layoutEntry.shortCode,
344
+ stale,
345
+ );
346
+ for (const seg of inherited.segments) {
347
+ if (!seenIds.has(seg.id)) {
348
+ seenIds.add(seg.id);
349
+ seg._inherited = true;
350
+ allLoaderSegments.push(seg);
351
+ }
352
+ }
353
+ allMatchedIds.push(...inherited.matchedIds);
354
+ }
299
355
  }
300
356
  }
301
357
 
@@ -665,13 +721,20 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
665
721
  return staticComponent;
666
722
  }
667
723
  const routeEntry = entry as Extract<EntryData, { type: "route" }>;
724
+ // For Passthrough routes at runtime, use the live handler instead of
725
+ // the build handler. At build time (context.build === true), always
726
+ // use the build handler from routeEntry.handler.
727
+ const handler =
728
+ !context.build && routeEntry.liveHandler
729
+ ? routeEntry.liveHandler
730
+ : routeEntry.handler;
668
731
  if (!routeEntry.loading) {
669
- const result = handleHandlerResult(await routeEntry.handler(context));
732
+ const result = handleHandlerResult(await handler(context));
670
733
  doneHandler();
671
734
  return result;
672
735
  }
673
736
  if (!actionContext) {
674
- const result = handleHandlerResult(routeEntry.handler(context));
737
+ const result = handleHandlerResult(handler(context));
675
738
  if (result instanceof Promise) {
676
739
  result.finally(doneHandler).catch(() => {});
677
740
  const tracked = deps.trackHandler(result, {
@@ -694,9 +757,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
694
757
  debugLog("segment.action", "resolving action route with awaited value", {
695
758
  entryId: entry.id,
696
759
  });
697
- const actionResult = handleHandlerResult(
698
- await routeEntry.handler(context),
699
- );
760
+ const actionResult = handleHandlerResult(await handler(context));
700
761
  doneHandler();
701
762
  return {
702
763
  content: Promise.resolve(actionResult),
@@ -705,10 +766,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
705
766
  () => null,
706
767
  );
707
768
 
769
+ // Normalize void handlers (undefined) to null so the reconciler's
770
+ // component === null checks work consistently for both void and explicit null.
708
771
  const resolvedComponent =
709
772
  component && typeof component === "object" && "content" in component
710
- ? (component as { content: ReactNode }).content
711
- : component;
773
+ ? ((component as { content: ReactNode }).content ?? null)
774
+ : (component ?? null);
712
775
 
713
776
  const segment: ResolvedSegment = {
714
777
  id: entry.shortCode,
@@ -810,6 +873,7 @@ export async function resolveSegmentWithRevalidation<TEnv>(
810
873
  deps,
811
874
  actionContext,
812
875
  stale,
876
+ entry,
813
877
  );
814
878
  segments.push(...orphanResult.segments);
815
879
  matchedIds.push(...orphanResult.matchedIds);
@@ -921,6 +985,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
921
985
  deps: SegmentResolutionDeps<TEnv>,
922
986
  actionContext?: ActionContext,
923
987
  stale?: boolean,
988
+ /** Parent route entry — its loaders are inherited so parallel slots can access them. */
989
+ parentRouteEntry?: EntryData,
924
990
  ): Promise<SegmentRevalidationResult> {
925
991
  invariant(
926
992
  orphan.type === "layout" || orphan.type === "cache",
@@ -948,6 +1014,37 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
948
1014
  segments.push(...loaderResult.segments);
949
1015
  matchedIds.push(...loaderResult.matchedIds);
950
1016
 
1017
+ // Inherit parent route's loaders so parallel slots inside this layout
1018
+ // can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
1019
+ if (
1020
+ parentRouteEntry &&
1021
+ parentRouteEntry.loader &&
1022
+ parentRouteEntry.loader.length > 0 &&
1023
+ Object.keys(orphan.parallel).length > 0
1024
+ ) {
1025
+ const inheritedResult = await resolveLoadersWithRevalidation(
1026
+ parentRouteEntry,
1027
+ context,
1028
+ belongsToRoute,
1029
+ clientSegmentIds,
1030
+ prevParams,
1031
+ request,
1032
+ prevUrl,
1033
+ nextUrl,
1034
+ routeKey,
1035
+ deps,
1036
+ actionContext,
1037
+ orphan.shortCode,
1038
+ stale,
1039
+ );
1040
+ // Tag as inherited so buildMatchResult can deduplicate when safe
1041
+ for (const s of inheritedResult.segments) {
1042
+ s._inherited = true;
1043
+ }
1044
+ segments.push(...inheritedResult.segments);
1045
+ matchedIds.push(...inheritedResult.matchedIds);
1046
+ }
1047
+
951
1048
  // Handler-first: resolve orphan layout handler before its parallels
952
1049
  // so ctx.set() values are visible to parallel children.
953
1050
  matchedIds.push(orphan.shortCode);
@@ -1034,6 +1131,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1034
1131
  );
1035
1132
 
1036
1133
  if (!resolvedParallelEntries.has(parallelEntry.id)) {
1134
+ // shortCodeOverride must match the parent layout, not the parallel entry.
1037
1135
  const loaderResult = await resolveLoadersWithRevalidation(
1038
1136
  parallelEntry,
1039
1137
  context,
@@ -1046,7 +1144,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1046
1144
  routeKey,
1047
1145
  deps,
1048
1146
  actionContext,
1049
- undefined,
1147
+ orphan.shortCode,
1050
1148
  stale,
1051
1149
  );
1052
1150
  segments.push(...loaderResult.segments);
@@ -1229,6 +1327,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1229
1327
  }
1230
1328
 
1231
1329
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1330
+ if (entry.type === "cache") {
1331
+ const store = RSCRouterContext.getStore();
1332
+ if (store) store.insideCacheScope = true;
1333
+ }
1232
1334
  const doneEntry = track(`segment:${entry.id}`, 1);
1233
1335
  const resolved = await resolveWithErrorBoundary(
1234
1336
  nonParallelEntry,
@@ -96,6 +96,7 @@ export interface SegmentResolutionDeps<TEnv = any> {
96
96
  findNearestNotFoundBoundary: (
97
97
  entry: EntryData | null,
98
98
  ) => ReactNode | NotFoundBoundaryHandler | null;
99
+ notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
99
100
  callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
100
101
  }
101
102