@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c

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 (189) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +172 -50
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +1160 -508
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +17 -16
  8. package/skills/breadcrumbs/SKILL.md +252 -0
  9. package/skills/cache-guide/SKILL.md +32 -0
  10. package/skills/caching/SKILL.md +49 -8
  11. package/skills/document-cache/SKILL.md +2 -2
  12. package/skills/handler-use/SKILL.md +362 -0
  13. package/skills/hooks/SKILL.md +61 -51
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +20 -0
  16. package/skills/layout/SKILL.md +22 -0
  17. package/skills/links/SKILL.md +91 -17
  18. package/skills/loader/SKILL.md +107 -24
  19. package/skills/middleware/SKILL.md +34 -3
  20. package/skills/migrate-nextjs/SKILL.md +560 -0
  21. package/skills/migrate-react-router/SKILL.md +765 -0
  22. package/skills/parallel/SKILL.md +185 -0
  23. package/skills/prerender/SKILL.md +112 -70
  24. package/skills/rango/SKILL.md +24 -23
  25. package/skills/response-routes/SKILL.md +8 -0
  26. package/skills/route/SKILL.md +58 -4
  27. package/skills/router-setup/SKILL.md +95 -5
  28. package/skills/streams-and-websockets/SKILL.md +283 -0
  29. package/skills/typesafety/SKILL.md +38 -24
  30. package/src/__internal.ts +92 -0
  31. package/src/browser/app-shell.ts +52 -0
  32. package/src/browser/app-version.ts +14 -0
  33. package/src/browser/event-controller.ts +5 -0
  34. package/src/browser/link-interceptor.ts +4 -0
  35. package/src/browser/navigation-bridge.ts +175 -17
  36. package/src/browser/navigation-client.ts +177 -44
  37. package/src/browser/navigation-store.ts +68 -9
  38. package/src/browser/navigation-transaction.ts +11 -9
  39. package/src/browser/partial-update.ts +113 -17
  40. package/src/browser/prefetch/cache.ts +275 -28
  41. package/src/browser/prefetch/fetch.ts +191 -46
  42. package/src/browser/prefetch/policy.ts +6 -0
  43. package/src/browser/prefetch/queue.ts +123 -20
  44. package/src/browser/prefetch/resource-ready.ts +77 -0
  45. package/src/browser/rango-state.ts +53 -13
  46. package/src/browser/react/Link.tsx +98 -14
  47. package/src/browser/react/NavigationProvider.tsx +89 -14
  48. package/src/browser/react/context.ts +7 -2
  49. package/src/browser/react/use-handle.ts +9 -58
  50. package/src/browser/react/use-navigation.ts +22 -2
  51. package/src/browser/react/use-params.ts +11 -1
  52. package/src/browser/react/use-router.ts +29 -9
  53. package/src/browser/rsc-router.tsx +177 -66
  54. package/src/browser/scroll-restoration.ts +41 -42
  55. package/src/browser/segment-reconciler.ts +36 -9
  56. package/src/browser/server-action-bridge.ts +8 -6
  57. package/src/browser/types.ts +73 -5
  58. package/src/build/generate-manifest.ts +6 -6
  59. package/src/build/generate-route-types.ts +3 -0
  60. package/src/build/route-trie.ts +67 -25
  61. package/src/build/route-types/include-resolution.ts +8 -1
  62. package/src/build/route-types/router-processing.ts +223 -74
  63. package/src/build/route-types/scan-filter.ts +8 -1
  64. package/src/cache/cache-runtime.ts +15 -11
  65. package/src/cache/cache-scope.ts +48 -7
  66. package/src/cache/cf/cf-cache-store.ts +455 -15
  67. package/src/cache/cf/index.ts +5 -1
  68. package/src/cache/document-cache.ts +17 -7
  69. package/src/cache/index.ts +1 -0
  70. package/src/cache/taint.ts +55 -0
  71. package/src/client.rsc.tsx +2 -1
  72. package/src/client.tsx +85 -276
  73. package/src/context-var.ts +72 -2
  74. package/src/debug.ts +2 -2
  75. package/src/handle.ts +40 -0
  76. package/src/handles/breadcrumbs.ts +66 -0
  77. package/src/handles/index.ts +1 -0
  78. package/src/host/index.ts +0 -3
  79. package/src/index.rsc.ts +9 -36
  80. package/src/index.ts +79 -70
  81. package/src/outlet-context.ts +1 -1
  82. package/src/prerender/store.ts +57 -15
  83. package/src/prerender.ts +138 -77
  84. package/src/response-utils.ts +28 -0
  85. package/src/reverse.ts +27 -2
  86. package/src/route-definition/dsl-helpers.ts +240 -40
  87. package/src/route-definition/helpers-types.ts +67 -19
  88. package/src/route-definition/index.ts +3 -3
  89. package/src/route-definition/redirect.ts +11 -3
  90. package/src/route-definition/resolve-handler-use.ts +155 -0
  91. package/src/route-map-builder.ts +7 -1
  92. package/src/route-types.ts +18 -0
  93. package/src/router/content-negotiation.ts +100 -1
  94. package/src/router/find-match.ts +4 -2
  95. package/src/router/handler-context.ts +129 -26
  96. package/src/router/intercept-resolution.ts +11 -4
  97. package/src/router/lazy-includes.ts +10 -7
  98. package/src/router/loader-resolution.ts +160 -22
  99. package/src/router/logging.ts +5 -2
  100. package/src/router/manifest.ts +31 -16
  101. package/src/router/match-api.ts +128 -193
  102. package/src/router/match-middleware/background-revalidation.ts +30 -2
  103. package/src/router/match-middleware/cache-lookup.ts +94 -17
  104. package/src/router/match-middleware/cache-store.ts +53 -10
  105. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  106. package/src/router/match-middleware/segment-resolution.ts +61 -5
  107. package/src/router/match-result.ts +103 -18
  108. package/src/router/metrics.ts +238 -13
  109. package/src/router/middleware-types.ts +48 -27
  110. package/src/router/middleware.ts +201 -86
  111. package/src/router/navigation-snapshot.ts +182 -0
  112. package/src/router/pattern-matching.ts +77 -11
  113. package/src/router/prerender-match.ts +114 -10
  114. package/src/router/preview-match.ts +30 -102
  115. package/src/router/request-classification.ts +310 -0
  116. package/src/router/revalidation.ts +27 -7
  117. package/src/router/route-snapshot.ts +245 -0
  118. package/src/router/router-context.ts +6 -1
  119. package/src/router/router-interfaces.ts +50 -5
  120. package/src/router/router-options.ts +50 -19
  121. package/src/router/segment-resolution/fresh.ts +215 -19
  122. package/src/router/segment-resolution/helpers.ts +30 -25
  123. package/src/router/segment-resolution/loader-cache.ts +1 -0
  124. package/src/router/segment-resolution/revalidation.ts +454 -301
  125. package/src/router/segment-wrappers.ts +2 -0
  126. package/src/router/trie-matching.ts +30 -6
  127. package/src/router/types.ts +1 -0
  128. package/src/router/url-params.ts +49 -0
  129. package/src/router.ts +89 -17
  130. package/src/rsc/handler.ts +563 -364
  131. package/src/rsc/helpers.ts +69 -41
  132. package/src/rsc/index.ts +0 -20
  133. package/src/rsc/loader-fetch.ts +23 -3
  134. package/src/rsc/manifest-init.ts +5 -1
  135. package/src/rsc/progressive-enhancement.ts +37 -10
  136. package/src/rsc/response-route-handler.ts +14 -1
  137. package/src/rsc/rsc-rendering.ts +47 -44
  138. package/src/rsc/server-action.ts +24 -10
  139. package/src/rsc/ssr-setup.ts +128 -0
  140. package/src/rsc/types.ts +11 -1
  141. package/src/search-params.ts +16 -13
  142. package/src/segment-content-promise.ts +67 -0
  143. package/src/segment-loader-promise.ts +122 -0
  144. package/src/segment-system.tsx +109 -23
  145. package/src/server/context.ts +174 -19
  146. package/src/server/handle-store.ts +19 -0
  147. package/src/server/loader-registry.ts +9 -8
  148. package/src/server/request-context.ts +218 -65
  149. package/src/server.ts +6 -0
  150. package/src/ssr/index.tsx +4 -0
  151. package/src/static-handler.ts +18 -6
  152. package/src/theme/index.ts +4 -13
  153. package/src/types/cache-types.ts +4 -4
  154. package/src/types/handler-context.ts +140 -72
  155. package/src/types/loader-types.ts +41 -15
  156. package/src/types/request-scope.ts +126 -0
  157. package/src/types/route-config.ts +17 -8
  158. package/src/types/route-entry.ts +19 -1
  159. package/src/types/segments.ts +2 -5
  160. package/src/urls/include-helper.ts +24 -14
  161. package/src/urls/path-helper-types.ts +39 -6
  162. package/src/urls/path-helper.ts +48 -13
  163. package/src/urls/pattern-types.ts +12 -0
  164. package/src/urls/response-types.ts +18 -16
  165. package/src/use-loader.tsx +77 -5
  166. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  167. package/src/vite/discovery/discover-routers.ts +7 -4
  168. package/src/vite/discovery/prerender-collection.ts +162 -88
  169. package/src/vite/discovery/state.ts +17 -13
  170. package/src/vite/index.ts +8 -3
  171. package/src/vite/plugin-types.ts +51 -79
  172. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  173. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  174. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  175. package/src/vite/plugins/expose-action-id.ts +1 -3
  176. package/src/vite/plugins/expose-id-utils.ts +12 -0
  177. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  178. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  179. package/src/vite/plugins/performance-tracks.ts +88 -0
  180. package/src/vite/plugins/refresh-cmd.ts +127 -0
  181. package/src/vite/plugins/version-plugin.ts +13 -1
  182. package/src/vite/rango.ts +190 -217
  183. package/src/vite/router-discovery.ts +241 -45
  184. package/src/vite/utils/banner.ts +4 -4
  185. package/src/vite/utils/package-resolution.ts +34 -1
  186. package/src/vite/utils/prerender-utils.ts +97 -5
  187. package/src/vite/utils/shared-utils.ts +3 -2
  188. package/skills/testing/SKILL.md +0 -226
  189. package/src/route-definition/route-function.ts +0 -119
@@ -10,7 +10,11 @@ import type { ReactNode } from "react";
10
10
  import { invariant } from "../../errors";
11
11
  import { revalidate } from "../loader-resolution.js";
12
12
  import { evaluateRevalidation } from "../revalidation.js";
13
- import type { EntryData } from "../../server/context";
13
+ import {
14
+ getParallelEntries,
15
+ getParallelSlotEntries,
16
+ type EntryData,
17
+ } from "../../server/context";
14
18
  import type {
15
19
  HandlerContext,
16
20
  InternalHandlerContext,
@@ -37,6 +41,11 @@ import {
37
41
  } from "./helpers.js";
38
42
  import { getRouterContext } from "../router-context.js";
39
43
  import { resolveSink, safeEmit } from "../telemetry.js";
44
+ import {
45
+ track,
46
+ RSCRouterContext,
47
+ runInsideLoaderScope,
48
+ } from "../../server/context.js";
40
49
 
41
50
  // ---------------------------------------------------------------------------
42
51
  // Telemetry helpers
@@ -227,7 +236,9 @@ export async function resolveLoadersWithRevalidation<TEnv>(
227
236
  params: ctx.params,
228
237
  loaderId: loader.$$id,
229
238
  loaderData: deps.wrapLoaderPromise(
230
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
239
+ runInsideLoaderScope(() =>
240
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
241
+ ),
231
242
  entry,
232
243
  segmentId,
233
244
  ctx.pathname,
@@ -257,26 +268,95 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
257
268
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
258
269
  const allLoaderSegments: ResolvedSegment[] = [];
259
270
  const allMatchedIds: string[] = [];
271
+ const seenIds = new Set<string>();
272
+
273
+ async function collectEntryLoaders(
274
+ entry: EntryData,
275
+ belongsToRoute: boolean,
276
+ shortCodeOverride?: string,
277
+ ): Promise<void> {
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
+ }
311
+
312
+ const seenParallelEntryIds = new Set<string>();
313
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
314
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
315
+ seenParallelEntryIds.add(parallelEntry.id);
316
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
317
+ }
318
+
319
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
320
+ for (const layoutEntry of entry.layout) {
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
+ }
355
+ }
356
+ }
260
357
 
261
358
  for (const entry of entries) {
262
- const belongsToRoute = entry.type === "route";
263
- const { segments, matchedIds } = await resolveLoadersWithRevalidation(
264
- entry,
265
- context,
266
- belongsToRoute,
267
- clientSegmentIds,
268
- prevParams,
269
- request,
270
- prevUrl,
271
- nextUrl,
272
- routeKey,
273
- deps,
274
- actionContext,
275
- undefined, // shortCodeOverride
276
- stale,
277
- );
278
- allLoaderSegments.push(...segments);
279
- allMatchedIds.push(...matchedIds);
359
+ await collectEntryLoaders(entry, entry.type === "route");
280
360
  }
281
361
 
282
362
  return { segments: allLoaderSegments, matchedIds: allMatchedIds };
@@ -300,22 +380,20 @@ export function buildEntryRevalidateMap(
300
380
  map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
301
381
 
302
382
  if (entry.type !== "parallel") {
303
- for (const parallelEntry of entry.parallel) {
304
- if (parallelEntry.type === "parallel") {
305
- const slots = Object.keys(parallelEntry.handler) as `@${string}`[];
306
- for (const slot of slots) {
307
- const parallelId = `${parallelEntry.shortCode}.${slot}`;
308
- map.set(parallelId, {
309
- entry: parallelEntry,
310
- revalidate: parallelEntry.revalidate,
311
- });
312
- }
313
- }
383
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
384
+ entry.parallel,
385
+ )) {
386
+ const parallelParentShortCode = parentShortCode ?? entry.shortCode;
387
+ const parallelId = `${parallelParentShortCode}.${slot}`;
388
+ map.set(parallelId, {
389
+ entry: parallelEntry,
390
+ revalidate: parallelEntry.revalidate,
391
+ });
314
392
  }
315
393
  }
316
394
 
317
395
  for (const layoutEntry of entry.layout) {
318
- processEntry(layoutEntry);
396
+ processEntry(layoutEntry, entry.shortCode);
319
397
  }
320
398
  }
321
399
 
@@ -347,7 +425,10 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
347
425
  const segments: ResolvedSegment[] = [];
348
426
  const matchedIds: string[] = [];
349
427
 
350
- for (const parallelEntry of entry.parallel) {
428
+ const resolvedParallelEntries = new Set<string>();
429
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
430
+ entry.parallel,
431
+ )) {
351
432
  invariant(
352
433
  parallelEntry.type === "parallel",
353
434
  `Expected parallel entry, got: ${parallelEntry.type}`,
@@ -358,141 +439,61 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
358
439
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
359
440
  | ReactNode
360
441
  >;
442
+ // In production, static handler bodies are evicted and the slot value
443
+ // may be undefined. The static store holds the pre-rendered component.
444
+ // We defer the handler check until after tryStaticSlot.
445
+ const handler = slots[slot];
446
+
447
+ const parallelId = `${entry.shortCode}.${slot}`;
448
+
449
+ const isFullRefetch = clientSegmentIds.size === 0;
450
+ const isNewParent = !clientSegmentIds.has(entry.shortCode);
451
+ if (
452
+ isFullRefetch ||
453
+ clientSegmentIds.has(parallelId) ||
454
+ belongsToRoute ||
455
+ isNewParent
456
+ ) {
457
+ matchedIds.push(parallelId);
458
+ }
361
459
 
362
- for (const [slot, handler] of Object.entries(slots)) {
363
- const parallelId = `${entry.shortCode}.${slot}`;
364
-
365
- const isFullRefetch = clientSegmentIds.size === 0;
366
- // When the parent layout is new (not in client's segment set),
367
- // all its parallel children must be resolved and tracked.
368
- // Without this, navigating to a new layout with parallels
369
- // (e.g., BlogLayout with @sidebar) from a different route
370
- // would silently drop those parallel segments.
371
- const isNewParent = !clientSegmentIds.has(entry.shortCode);
372
- if (
373
- isFullRefetch ||
374
- clientSegmentIds.has(parallelId) ||
375
- belongsToRoute ||
376
- isNewParent
377
- ) {
378
- matchedIds.push(parallelId);
379
- }
380
-
381
- const shouldResolve = await (async () => {
382
- if (isFullRefetch) {
383
- if (isTraceActive()) {
384
- pushRevalidationTraceEntry({
385
- segmentId: parallelId,
386
- segmentType: "parallel",
387
- belongsToRoute,
388
- source: "parallel",
389
- defaultShouldRevalidate: true,
390
- finalShouldRevalidate: true,
391
- reason: "full-refetch",
392
- });
393
- }
394
- return true;
395
- }
396
- if (!clientSegmentIds.has(parallelId)) {
397
- const result = belongsToRoute || isNewParent;
398
- if (isTraceActive()) {
399
- pushRevalidationTraceEntry({
400
- segmentId: parallelId,
401
- segmentType: "parallel",
402
- belongsToRoute,
403
- source: "parallel",
404
- defaultShouldRevalidate: result,
405
- finalShouldRevalidate: result,
406
- reason: result ? "new-segment" : "skip-parent-chain",
407
- });
408
- }
409
- return result;
460
+ const shouldResolve = await (async () => {
461
+ if (isFullRefetch) {
462
+ if (isTraceActive()) {
463
+ pushRevalidationTraceEntry({
464
+ segmentId: parallelId,
465
+ segmentType: "parallel",
466
+ belongsToRoute,
467
+ source: "parallel",
468
+ defaultShouldRevalidate: true,
469
+ finalShouldRevalidate: true,
470
+ reason: "full-refetch",
471
+ });
410
472
  }
411
-
412
- const dummySegment: ResolvedSegment = {
413
- id: parallelId,
414
- namespace: parallelEntry.id,
415
- type: "parallel",
416
- index: 0,
417
- component: null as any,
418
- params,
419
- slot,
420
- belongsToRoute,
421
- parallelName: `${parallelEntry.id}.${slot}`,
422
- ...(parallelEntry.mountPath
423
- ? { mountPath: parallelEntry.mountPath }
424
- : {}),
425
- };
426
-
427
- return await evaluateRevalidation({
428
- segment: dummySegment,
429
- prevParams,
430
- getPrevSegment: null,
431
- request,
432
- prevUrl,
433
- nextUrl,
434
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
435
- name: `revalidate${i}`,
436
- fn,
437
- })),
438
- routeKey,
439
- context,
440
- actionContext,
441
- stale,
442
- traceSource: "parallel",
443
- });
444
- })();
445
- emitRevalidationDecision(
446
- parallelId,
447
- context.pathname,
448
- routeKey,
449
- shouldResolve,
450
- );
451
-
452
- let component: ReactNode | undefined;
453
- if (shouldResolve) {
454
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
473
+ return true;
455
474
  }
456
- if (component === undefined) {
457
- const hasLoadingFallback =
458
- parallelEntry.loading !== undefined &&
459
- parallelEntry.loading !== false;
460
- if (!shouldResolve) {
461
- component = null;
462
- } else if (hasLoadingFallback) {
463
- const result =
464
- typeof handler === "function" ? handler(context) : handler;
465
- if (result instanceof Promise) {
466
- const tracked = deps.trackHandler(result, {
467
- segmentId: parallelId,
468
- segmentType: "parallel",
469
- });
470
- observeStreamedHandler(
471
- tracked,
472
- parallelId,
473
- "parallel",
474
- context.pathname,
475
- routeKey,
476
- params,
477
- );
478
- component = tracked as ReactNode;
479
- } else {
480
- component = result as ReactNode;
481
- }
482
- } else {
483
- component =
484
- typeof handler === "function" ? await handler(context) : handler;
475
+ if (!clientSegmentIds.has(parallelId)) {
476
+ const result = belongsToRoute || isNewParent;
477
+ if (isTraceActive()) {
478
+ pushRevalidationTraceEntry({
479
+ segmentId: parallelId,
480
+ segmentType: "parallel",
481
+ belongsToRoute,
482
+ source: "parallel",
483
+ defaultShouldRevalidate: result,
484
+ finalShouldRevalidate: result,
485
+ reason: result ? "new-segment" : "skip-parent-chain",
486
+ });
485
487
  }
488
+ return result;
486
489
  }
487
490
 
488
- segments.push({
491
+ const dummySegment: ResolvedSegment = {
489
492
  id: parallelId,
490
493
  namespace: parallelEntry.id,
491
494
  type: "parallel",
492
495
  index: 0,
493
- component,
494
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
495
- transition: parallelEntry.transition,
496
+ component: null as any,
496
497
  params,
497
498
  slot,
498
499
  belongsToRoute,
@@ -500,28 +501,111 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
500
501
  ...(parallelEntry.mountPath
501
502
  ? { mountPath: parallelEntry.mountPath }
502
503
  : {}),
503
- });
504
- }
504
+ };
505
505
 
506
- if (!parallelEntry.loading) {
507
- const loaderResult = await resolveLoadersWithRevalidation(
508
- parallelEntry,
509
- context,
510
- belongsToRoute,
511
- clientSegmentIds,
506
+ return await evaluateRevalidation({
507
+ segment: dummySegment,
512
508
  prevParams,
509
+ getPrevSegment: null,
513
510
  request,
514
511
  prevUrl,
515
512
  nextUrl,
513
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
514
+ name: `revalidate${i}`,
515
+ fn,
516
+ })),
516
517
  routeKey,
517
- deps,
518
+ context,
518
519
  actionContext,
519
- entry.shortCode,
520
520
  stale,
521
- );
522
- segments.push(...loaderResult.segments);
523
- matchedIds.push(...loaderResult.matchedIds);
521
+ traceSource: "parallel",
522
+ });
523
+ })();
524
+ emitRevalidationDecision(
525
+ parallelId,
526
+ context.pathname,
527
+ routeKey,
528
+ shouldResolve,
529
+ );
530
+
531
+ let component: ReactNode | undefined;
532
+ if (shouldResolve) {
533
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
524
534
  }
535
+ if (component === undefined) {
536
+ const hasLoadingFallback =
537
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
538
+ if (!shouldResolve) {
539
+ component = null;
540
+ } else if (handler === undefined) {
541
+ // Handler evicted (production static slot) but static lookup missed.
542
+ // Nothing to render — use null so the client keeps its cached version.
543
+ component = null;
544
+ } else if (hasLoadingFallback) {
545
+ const result =
546
+ typeof handler === "function" ? handler(context) : handler;
547
+ if (result instanceof Promise) {
548
+ const tracked = deps.trackHandler(result, {
549
+ segmentId: parallelId,
550
+ segmentType: "parallel",
551
+ });
552
+ observeStreamedHandler(
553
+ tracked,
554
+ parallelId,
555
+ "parallel",
556
+ context.pathname,
557
+ routeKey,
558
+ params,
559
+ );
560
+ component = tracked as ReactNode;
561
+ } else {
562
+ component = result as ReactNode;
563
+ }
564
+ } else {
565
+ component =
566
+ typeof handler === "function" ? await handler(context) : handler;
567
+ }
568
+ }
569
+
570
+ segments.push({
571
+ id: parallelId,
572
+ namespace: parallelEntry.id,
573
+ type: "parallel",
574
+ index: 0,
575
+ component,
576
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
577
+ transition: parallelEntry.transition,
578
+ params,
579
+ slot,
580
+ belongsToRoute,
581
+ parallelName: `${parallelEntry.id}.${slot}`,
582
+ ...(parallelEntry.mountPath
583
+ ? { mountPath: parallelEntry.mountPath }
584
+ : {}),
585
+ });
586
+
587
+ if (resolvedParallelEntries.has(parallelEntry.id)) {
588
+ continue;
589
+ }
590
+
591
+ const loaderResult = await resolveLoadersWithRevalidation(
592
+ parallelEntry,
593
+ context,
594
+ belongsToRoute,
595
+ clientSegmentIds,
596
+ prevParams,
597
+ request,
598
+ prevUrl,
599
+ nextUrl,
600
+ routeKey,
601
+ deps,
602
+ actionContext,
603
+ entry.shortCode,
604
+ stale,
605
+ );
606
+ segments.push(...loaderResult.segments);
607
+ matchedIds.push(...loaderResult.matchedIds);
608
+ resolvedParallelEntries.add(parallelEntry.id);
525
609
  }
526
610
 
527
611
  return { segments, matchedIds };
@@ -607,6 +691,8 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
607
691
  context,
608
692
  actionContext,
609
693
  stale,
694
+ traceSource:
695
+ entry.type === "route" ? "route-handler" : "layout-handler",
610
696
  });
611
697
  emitRevalidationDecision(
612
698
  entry.shortCode,
@@ -621,20 +707,36 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
621
707
  return shouldRevalidate;
622
708
  },
623
709
  async () => {
710
+ const doneHandler = track(`handler:${entry.id}`, 2);
624
711
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
625
712
  entry.shortCode;
626
713
  if (entry.type === "layout" || entry.type === "cache") {
627
- return resolveLayoutComponent(entry, context);
714
+ const layoutComponent = await resolveLayoutComponent(entry, context);
715
+ doneHandler();
716
+ return layoutComponent;
628
717
  }
629
718
  const staticComponent = await tryStaticHandler(entry, entry.shortCode);
630
- if (staticComponent !== undefined) return staticComponent;
719
+ if (staticComponent !== undefined) {
720
+ doneHandler();
721
+ return staticComponent;
722
+ }
631
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;
632
731
  if (!routeEntry.loading) {
633
- return handleHandlerResult(await routeEntry.handler(context));
732
+ const result = handleHandlerResult(await handler(context));
733
+ doneHandler();
734
+ return result;
634
735
  }
635
736
  if (!actionContext) {
636
- const result = handleHandlerResult(routeEntry.handler(context));
737
+ const result = handleHandlerResult(handler(context));
637
738
  if (result instanceof Promise) {
739
+ result.finally(doneHandler).catch(() => {});
638
740
  const tracked = deps.trackHandler(result, {
639
741
  segmentId: entry.shortCode,
640
742
  segmentType: entry.type,
@@ -649,24 +751,27 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
649
751
  );
650
752
  return { content: tracked };
651
753
  }
754
+ doneHandler();
652
755
  return { content: result };
653
756
  }
654
757
  debugLog("segment.action", "resolving action route with awaited value", {
655
758
  entryId: entry.id,
656
759
  });
760
+ const actionResult = handleHandlerResult(await handler(context));
761
+ doneHandler();
657
762
  return {
658
- content: Promise.resolve(
659
- handleHandlerResult(await routeEntry.handler(context)),
660
- ),
763
+ content: Promise.resolve(actionResult),
661
764
  };
662
765
  },
663
766
  () => null,
664
767
  );
665
768
 
769
+ // Normalize void handlers (undefined) to null so the reconciler's
770
+ // component === null checks work consistently for both void and explicit null.
666
771
  const resolvedComponent =
667
772
  component && typeof component === "object" && "content" in component
668
- ? (component as { content: ReactNode }).content
669
- : component;
773
+ ? ((component as { content: ReactNode }).content ?? null)
774
+ : (component ?? null);
670
775
 
671
776
  const segment: ResolvedSegment = {
672
777
  id: entry.shortCode,
@@ -768,6 +873,7 @@ export async function resolveSegmentWithRevalidation<TEnv>(
768
873
  deps,
769
874
  actionContext,
770
875
  stale,
876
+ entry,
771
877
  );
772
878
  segments.push(...orphanResult.segments);
773
879
  matchedIds.push(...orphanResult.matchedIds);
@@ -879,6 +985,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
879
985
  deps: SegmentResolutionDeps<TEnv>,
880
986
  actionContext?: ActionContext,
881
987
  stale?: boolean,
988
+ /** Parent route entry — its loaders are inherited so parallel slots can access them. */
989
+ parentRouteEntry?: EntryData,
882
990
  ): Promise<SegmentRevalidationResult> {
883
991
  invariant(
884
992
  orphan.type === "layout" || orphan.type === "cache",
@@ -906,6 +1014,37 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
906
1014
  segments.push(...loaderResult.segments);
907
1015
  matchedIds.push(...loaderResult.matchedIds);
908
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
+
909
1048
  // Handler-first: resolve orphan layout handler before its parallels
910
1049
  // so ctx.set() values are visible to parallel children.
911
1050
  matchedIds.push(orphan.shortCode);
@@ -982,143 +1121,73 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
982
1121
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
983
1122
  });
984
1123
 
985
- for (const parallelEntry of orphan.parallel) {
1124
+ const resolvedParallelEntries = new Set<string>();
1125
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
1126
+ orphan.parallel,
1127
+ )) {
986
1128
  invariant(
987
1129
  parallelEntry.type === "parallel",
988
1130
  `Expected parallel entry, got: ${parallelEntry.type}`,
989
1131
  );
990
1132
 
991
- const loaderResult = await resolveLoadersWithRevalidation(
992
- parallelEntry,
993
- context,
994
- belongsToRoute,
995
- clientSegmentIds,
996
- prevParams,
997
- request,
998
- prevUrl,
999
- nextUrl,
1000
- routeKey,
1001
- deps,
1002
- actionContext,
1003
- undefined,
1004
- stale,
1005
- );
1006
- segments.push(...loaderResult.segments);
1007
- matchedIds.push(...loaderResult.matchedIds);
1133
+ if (!resolvedParallelEntries.has(parallelEntry.id)) {
1134
+ // shortCodeOverride must match the parent layout, not the parallel entry.
1135
+ const loaderResult = await resolveLoadersWithRevalidation(
1136
+ parallelEntry,
1137
+ context,
1138
+ belongsToRoute,
1139
+ clientSegmentIds,
1140
+ prevParams,
1141
+ request,
1142
+ prevUrl,
1143
+ nextUrl,
1144
+ routeKey,
1145
+ deps,
1146
+ actionContext,
1147
+ orphan.shortCode,
1148
+ stale,
1149
+ );
1150
+ segments.push(...loaderResult.segments);
1151
+ matchedIds.push(...loaderResult.matchedIds);
1152
+ resolvedParallelEntries.add(parallelEntry.id);
1153
+ }
1008
1154
 
1009
1155
  const slots = parallelEntry.handler as Record<
1010
1156
  `@${string}`,
1011
1157
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1012
1158
  | ReactNode
1013
1159
  >;
1160
+ // Handler may be undefined in production after static handler eviction.
1161
+ const handler = slots[slot];
1014
1162
 
1015
- for (const [slot, handler] of Object.entries(slots)) {
1016
- // Use orphan.shortCode (the parent layout) to match the SSR path
1017
- // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1018
- // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1019
- const parallelId = `${orphan.shortCode}.${slot}`;
1020
- matchedIds.push(parallelId);
1163
+ // Use orphan.shortCode (the parent layout) to match the SSR path
1164
+ // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1165
+ // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1166
+ const parallelId = `${orphan.shortCode}.${slot}`;
1167
+ matchedIds.push(parallelId);
1021
1168
 
1022
- const shouldResolve = await (async () => {
1023
- if (!clientSegmentIds.has(parallelId)) {
1024
- if (isTraceActive()) {
1025
- pushRevalidationTraceEntry({
1026
- segmentId: parallelId,
1027
- segmentType: "parallel",
1028
- belongsToRoute,
1029
- source: "parallel",
1030
- defaultShouldRevalidate: true,
1031
- finalShouldRevalidate: true,
1032
- reason: "new-segment",
1033
- });
1034
- }
1035
- return true;
1036
- }
1037
-
1038
- const dummySegment: ResolvedSegment = {
1039
- id: parallelId,
1040
- namespace: parallelEntry.id,
1041
- type: "parallel",
1042
- index: 0,
1043
- component: null as any,
1044
- params,
1045
- slot,
1046
- belongsToRoute,
1047
- parallelName: `${parallelEntry.id}.${slot}`,
1048
- ...(parallelEntry.mountPath
1049
- ? { mountPath: parallelEntry.mountPath }
1050
- : {}),
1051
- };
1052
-
1053
- return await evaluateRevalidation({
1054
- segment: dummySegment,
1055
- prevParams,
1056
- getPrevSegment: null,
1057
- request,
1058
- prevUrl,
1059
- nextUrl,
1060
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
1061
- name: `revalidate${i}`,
1062
- fn,
1063
- })),
1064
- routeKey,
1065
- context,
1066
- actionContext,
1067
- stale,
1068
- traceSource: "parallel",
1069
- });
1070
- })();
1071
- emitRevalidationDecision(
1072
- parallelId,
1073
- context.pathname,
1074
- routeKey,
1075
- shouldResolve,
1076
- );
1077
-
1078
- let component: ReactNode | undefined;
1079
- if (shouldResolve) {
1080
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
1081
- }
1082
- if (component === undefined) {
1083
- const hasLoadingFallback =
1084
- parallelEntry.loading !== undefined &&
1085
- parallelEntry.loading !== false;
1086
- if (!shouldResolve) {
1087
- component = null;
1088
- } else if (hasLoadingFallback) {
1089
- const result =
1090
- typeof handler === "function" ? handler(context) : handler;
1091
- if (result instanceof Promise) {
1092
- const tracked = deps.trackHandler(result, {
1093
- segmentId: parallelId,
1094
- segmentType: "parallel",
1095
- });
1096
- observeStreamedHandler(
1097
- tracked,
1098
- parallelId,
1099
- "parallel",
1100
- context.pathname,
1101
- routeKey,
1102
- params,
1103
- );
1104
- component = tracked as ReactNode;
1105
- } else {
1106
- component = result as ReactNode;
1107
- }
1108
- } else {
1109
- component =
1110
- typeof handler === "function" ? await handler(context) : handler;
1169
+ const shouldResolve = await (async () => {
1170
+ if (!clientSegmentIds.has(parallelId)) {
1171
+ if (isTraceActive()) {
1172
+ pushRevalidationTraceEntry({
1173
+ segmentId: parallelId,
1174
+ segmentType: "parallel",
1175
+ belongsToRoute,
1176
+ source: "parallel",
1177
+ defaultShouldRevalidate: true,
1178
+ finalShouldRevalidate: true,
1179
+ reason: "new-segment",
1180
+ });
1111
1181
  }
1182
+ return true;
1112
1183
  }
1113
1184
 
1114
- segments.push({
1185
+ const dummySegment: ResolvedSegment = {
1115
1186
  id: parallelId,
1116
1187
  namespace: parallelEntry.id,
1117
1188
  type: "parallel",
1118
1189
  index: 0,
1119
- component,
1120
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1121
- transition: parallelEntry.transition,
1190
+ component: null as any,
1122
1191
  params,
1123
1192
  slot,
1124
1193
  belongsToRoute,
@@ -1126,8 +1195,87 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1126
1195
  ...(parallelEntry.mountPath
1127
1196
  ? { mountPath: parallelEntry.mountPath }
1128
1197
  : {}),
1198
+ };
1199
+
1200
+ return await evaluateRevalidation({
1201
+ segment: dummySegment,
1202
+ prevParams,
1203
+ getPrevSegment: null,
1204
+ request,
1205
+ prevUrl,
1206
+ nextUrl,
1207
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
1208
+ name: `revalidate${i}`,
1209
+ fn,
1210
+ })),
1211
+ routeKey,
1212
+ context,
1213
+ actionContext,
1214
+ stale,
1215
+ traceSource: "parallel",
1129
1216
  });
1217
+ })();
1218
+ emitRevalidationDecision(
1219
+ parallelId,
1220
+ context.pathname,
1221
+ routeKey,
1222
+ shouldResolve,
1223
+ );
1224
+
1225
+ let component: ReactNode | undefined;
1226
+ if (shouldResolve) {
1227
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
1228
+ }
1229
+ if (component === undefined) {
1230
+ const hasLoadingFallback =
1231
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
1232
+ if (!shouldResolve) {
1233
+ component = null;
1234
+ } else if (handler === undefined) {
1235
+ // Handler evicted (production static slot) but static lookup missed.
1236
+ component = null;
1237
+ } else if (hasLoadingFallback) {
1238
+ const result =
1239
+ typeof handler === "function" ? handler(context) : handler;
1240
+ if (result instanceof Promise) {
1241
+ const tracked = deps.trackHandler(result, {
1242
+ segmentId: parallelId,
1243
+ segmentType: "parallel",
1244
+ });
1245
+ observeStreamedHandler(
1246
+ tracked,
1247
+ parallelId,
1248
+ "parallel",
1249
+ context.pathname,
1250
+ routeKey,
1251
+ params,
1252
+ );
1253
+ component = tracked as ReactNode;
1254
+ } else {
1255
+ component = result as ReactNode;
1256
+ }
1257
+ } else {
1258
+ component =
1259
+ typeof handler === "function" ? await handler(context) : handler;
1260
+ }
1130
1261
  }
1262
+
1263
+ segments.push({
1264
+ id: parallelId,
1265
+ namespace: parallelEntry.id,
1266
+ type: "parallel",
1267
+ index: 0,
1268
+ component,
1269
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1270
+ transition: parallelEntry.transition,
1271
+ params,
1272
+ slot,
1273
+ belongsToRoute,
1274
+ parallelName: `${parallelEntry.id}.${slot}`,
1275
+ ...(parallelEntry.mountPath
1276
+ ? { mountPath: parallelEntry.mountPath }
1277
+ : {}),
1278
+ });
1131
1279
  }
1132
1280
 
1133
1281
  return { segments, matchedIds };
@@ -1152,6 +1300,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1152
1300
  localRouteName: string,
1153
1301
  pathname: string,
1154
1302
  deps: SegmentResolutionDeps<TEnv>,
1303
+ stale?: boolean,
1155
1304
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
1156
1305
  const allSegments: ResolvedSegment[] = [];
1157
1306
  const matchedIds: string[] = [];
@@ -1178,6 +1327,11 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1178
1327
  }
1179
1328
 
1180
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
+ }
1334
+ const doneEntry = track(`segment:${entry.id}`, 1);
1181
1335
  const resolved = await resolveWithErrorBoundary(
1182
1336
  nonParallelEntry,
1183
1337
  params,
@@ -1195,15 +1349,14 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1195
1349
  loaderPromises,
1196
1350
  deps,
1197
1351
  actionContext,
1198
- false,
1352
+ stale,
1199
1353
  ),
1200
1354
  (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1201
1355
  deps,
1202
- telemetry
1203
- ? { request, url: context.url, routeKey, isPartial: true, telemetry }
1204
- : undefined,
1356
+ { request, url: context.url, routeKey, isPartial: true, telemetry },
1205
1357
  pathname,
1206
1358
  );
1359
+ doneEntry();
1207
1360
 
1208
1361
  // Deduplicate segments and matchedIds by ID, matching resolveAllSegments.
1209
1362
  // include() scopes can produce entries that resolve the same shared