@rangojs/router 0.0.0-experimental.39 → 0.0.0-experimental.3b1deca8

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 (89) hide show
  1. package/dist/bin/rango.js +8 -3
  2. package/dist/vite/index.js +292 -204
  3. package/package.json +1 -1
  4. package/skills/cache-guide/SKILL.md +32 -0
  5. package/skills/caching/SKILL.md +45 -4
  6. package/skills/loader/SKILL.md +53 -43
  7. package/skills/parallel/SKILL.md +126 -0
  8. package/skills/route/SKILL.md +31 -0
  9. package/skills/router-setup/SKILL.md +52 -2
  10. package/skills/typesafety/SKILL.md +10 -0
  11. package/src/browser/debug-channel.ts +93 -0
  12. package/src/browser/event-controller.ts +5 -0
  13. package/src/browser/navigation-bridge.ts +1 -5
  14. package/src/browser/navigation-client.ts +84 -27
  15. package/src/browser/navigation-transaction.ts +11 -9
  16. package/src/browser/partial-update.ts +50 -9
  17. package/src/browser/prefetch/cache.ts +57 -5
  18. package/src/browser/prefetch/fetch.ts +30 -21
  19. package/src/browser/prefetch/queue.ts +92 -20
  20. package/src/browser/prefetch/resource-ready.ts +77 -0
  21. package/src/browser/react/Link.tsx +9 -1
  22. package/src/browser/react/NavigationProvider.tsx +32 -3
  23. package/src/browser/rsc-router.tsx +109 -57
  24. package/src/browser/scroll-restoration.ts +31 -34
  25. package/src/browser/segment-reconciler.ts +6 -1
  26. package/src/browser/server-action-bridge.ts +12 -0
  27. package/src/browser/types.ts +17 -1
  28. package/src/build/route-types/router-processing.ts +12 -2
  29. package/src/cache/cache-runtime.ts +15 -11
  30. package/src/cache/cache-scope.ts +48 -7
  31. package/src/cache/cf/cf-cache-store.ts +453 -11
  32. package/src/cache/cf/index.ts +5 -1
  33. package/src/cache/document-cache.ts +17 -7
  34. package/src/cache/index.ts +1 -0
  35. package/src/cache/taint.ts +55 -0
  36. package/src/context-var.ts +72 -2
  37. package/src/debug.ts +2 -2
  38. package/src/deps/browser.ts +1 -0
  39. package/src/route-definition/dsl-helpers.ts +32 -7
  40. package/src/route-definition/helpers-types.ts +6 -5
  41. package/src/route-definition/redirect.ts +2 -2
  42. package/src/route-map-builder.ts +7 -1
  43. package/src/router/find-match.ts +4 -2
  44. package/src/router/handler-context.ts +31 -8
  45. package/src/router/intercept-resolution.ts +2 -0
  46. package/src/router/lazy-includes.ts +4 -1
  47. package/src/router/loader-resolution.ts +7 -1
  48. package/src/router/logging.ts +5 -2
  49. package/src/router/manifest.ts +9 -3
  50. package/src/router/match-middleware/background-revalidation.ts +30 -2
  51. package/src/router/match-middleware/cache-lookup.ts +66 -9
  52. package/src/router/match-middleware/cache-store.ts +53 -10
  53. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  54. package/src/router/match-middleware/segment-resolution.ts +8 -5
  55. package/src/router/match-result.ts +22 -6
  56. package/src/router/metrics.ts +6 -1
  57. package/src/router/middleware-types.ts +6 -2
  58. package/src/router/middleware.ts +4 -3
  59. package/src/router/router-context.ts +6 -1
  60. package/src/router/segment-resolution/fresh.ts +130 -17
  61. package/src/router/segment-resolution/helpers.ts +29 -24
  62. package/src/router/segment-resolution/loader-cache.ts +1 -0
  63. package/src/router/segment-resolution/revalidation.ts +352 -290
  64. package/src/router/segment-wrappers.ts +2 -0
  65. package/src/router/types.ts +1 -0
  66. package/src/router.ts +6 -1
  67. package/src/rsc/handler.ts +28 -2
  68. package/src/rsc/loader-fetch.ts +7 -2
  69. package/src/rsc/progressive-enhancement.ts +4 -1
  70. package/src/rsc/rsc-rendering.ts +4 -1
  71. package/src/rsc/server-action.ts +2 -0
  72. package/src/rsc/types.ts +7 -1
  73. package/src/segment-system.tsx +140 -4
  74. package/src/server/context.ts +102 -13
  75. package/src/server/request-context.ts +59 -12
  76. package/src/ssr/index.tsx +1 -0
  77. package/src/types/handler-context.ts +120 -22
  78. package/src/types/loader-types.ts +4 -4
  79. package/src/types/route-entry.ts +7 -0
  80. package/src/types/segments.ts +2 -0
  81. package/src/urls/path-helper.ts +1 -1
  82. package/src/vite/discovery/state.ts +0 -2
  83. package/src/vite/plugin-types.ts +0 -83
  84. package/src/vite/plugins/expose-action-id.ts +1 -3
  85. package/src/vite/plugins/performance-tracks.ts +235 -0
  86. package/src/vite/plugins/version-plugin.ts +13 -1
  87. package/src/vite/rango.ts +148 -209
  88. package/src/vite/router-discovery.ts +0 -8
  89. package/src/vite/utils/banner.ts +3 -3
@@ -204,6 +204,7 @@ export function createSegmentWrappers<TEnv = any>(
204
204
  interceptResult: { intercept: InterceptEntry; entry: EntryData } | null,
205
205
  localRouteName: string,
206
206
  pathname: string,
207
+ stale?: boolean,
207
208
  ): ReturnType<typeof _resolveAllSegmentsWithRevalidation> {
208
209
  return _resolveAllSegmentsWithRevalidation(
209
210
  entries,
@@ -221,6 +222,7 @@ export function createSegmentWrappers<TEnv = any>(
221
222
  localRouteName,
222
223
  pathname,
223
224
  segmentDeps,
225
+ stale,
224
226
  );
225
227
  }
226
228
 
@@ -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
 
package/src/router.ts CHANGED
@@ -526,6 +526,7 @@ export function createRouter<TEnv = any>(
526
526
  trackHandler,
527
527
  findNearestErrorBoundary,
528
528
  findNearestNotFoundBoundary,
529
+ notFoundComponent: notFound,
529
530
  callOnError,
530
531
  };
531
532
 
@@ -560,6 +561,7 @@ export function createRouter<TEnv = any>(
560
561
  mergedRouteMap,
561
562
  nextMountIndex: () => mountIndex++,
562
563
  getPrecomputedByPrefix,
564
+ routerId,
563
565
  };
564
566
 
565
567
  function evaluateLazyEntry(entry: RouteEntry<TEnv>): void {
@@ -689,7 +691,7 @@ export function createRouter<TEnv = any>(
689
691
  errorBoundary: [],
690
692
  notFoundBoundary: [],
691
693
  layout: [],
692
- parallel: [],
694
+ parallel: {},
693
695
  intercept: [],
694
696
  loader: [],
695
697
  };
@@ -751,6 +753,7 @@ export function createRouter<TEnv = any>(
751
753
  trailingSlash: trailingSlashConfig,
752
754
  handler: urlPatterns.handler,
753
755
  mountIndex: currentMountIndex,
756
+ routerId,
754
757
  cacheProfiles: resolvedCacheProfiles,
755
758
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
756
759
  ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
@@ -770,6 +773,7 @@ export function createRouter<TEnv = any>(
770
773
  trailingSlash: trailingSlashConfig,
771
774
  handler: urlPatterns.handler,
772
775
  mountIndex: currentMountIndex,
776
+ routerId,
773
777
  cacheProfiles: resolvedCacheProfiles,
774
778
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
775
779
  ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
@@ -813,6 +817,7 @@ export function createRouter<TEnv = any>(
813
817
  trailingSlash: trailingSlashConfig,
814
818
  handler: urlPatterns.handler,
815
819
  mountIndex: mountIndex++,
820
+ routerId,
816
821
  // Lazy evaluation fields
817
822
  lazy: true,
818
823
  lazyPatterns: lazyInclude.patterns,
@@ -14,9 +14,14 @@ import {
14
14
  runWithRequestContext,
15
15
  setRequestContextParams,
16
16
  requireRequestContext,
17
+ getRequestContext,
17
18
  createRequestContext,
18
19
  } from "../server/request-context.js";
19
20
  import * as rscDeps from "@vitejs/plugin-rsc/rsc";
21
+ import {
22
+ DEBUG_ID_HEADER,
23
+ createServerDebugChannel,
24
+ } from "../vite/plugins/performance-tracks.js";
20
25
 
21
26
  import type {
22
27
  RscPayload,
@@ -262,7 +267,10 @@ export function createRSCHandler<
262
267
  ...(locationState && { locationState }),
263
268
  },
264
269
  };
265
- const rscStream = renderToReadableStream<RscPayload>(redirectPayload);
270
+ const debugChannel = getRequestContext()?._debugChannel;
271
+ const rscStream = renderToReadableStream<RscPayload>(redirectPayload, {
272
+ ...(debugChannel && { debugChannel }),
273
+ });
266
274
  return createResponseWithMergedHeaders(rscStream, {
267
275
  status: 200,
268
276
  headers: { "content-type": "text/x-component;charset=utf-8" },
@@ -418,6 +426,21 @@ export function createRSCHandler<
418
426
  requestContext._debugPerformance = true;
419
427
  requestContext._metricsStore = earlyMetricsStore;
420
428
  }
429
+ // Dev-only: wire debug channel for React Performance Tracks
430
+ if (process.env.NODE_ENV !== "production") {
431
+ const debugId = request.headers.get(DEBUG_ID_HEADER);
432
+ console.log("[perf-tracks] handler: debugId header =", debugId);
433
+ if (debugId) {
434
+ const channel = createServerDebugChannel(debugId);
435
+ console.log(
436
+ "[perf-tracks] handler: channel =",
437
+ channel ? "created" : "NOT FOUND",
438
+ );
439
+ if (channel) {
440
+ requestContext._debugChannel = channel;
441
+ }
442
+ }
443
+ }
421
444
  // Wire background error reporting so "use cache" and other subsystems
422
445
  // can surface non-fatal errors through the router's onError callback.
423
446
  requestContext._reportBackgroundError = (
@@ -1039,7 +1062,10 @@ export function createRSCHandler<
1039
1062
  },
1040
1063
  };
1041
1064
 
1042
- const rscStream = renderToReadableStream(payload);
1065
+ const debugChannel = requireRequestContext()._debugChannel;
1066
+ const rscStream = renderToReadableStream(payload, {
1067
+ ...(debugChannel && { debugChannel }),
1068
+ });
1043
1069
 
1044
1070
  // Determine if this is an RSC request or HTML request.
1045
1071
  // Partial requests are always RSC (see main isRscRequest comment).
@@ -168,8 +168,13 @@ export async function handleLoaderFetch<TEnv>(
168
168
  loaderResult: unknown;
169
169
  }
170
170
  const loaderPayload: LoaderPayload = { loaderResult: result };
171
- const rscStream =
172
- ctx.renderToReadableStream<LoaderPayload>(loaderPayload);
171
+ const debugChannel = reqCtx._debugChannel;
172
+ const rscStream = ctx.renderToReadableStream<LoaderPayload>(
173
+ loaderPayload,
174
+ {
175
+ ...(debugChannel && { debugChannel }),
176
+ },
177
+ );
173
178
 
174
179
  return createResponseWithMergedHeaders(rscStream, {
175
180
  headers: { "content-type": "text/x-component;charset=utf-8" },
@@ -257,7 +257,10 @@ export async function handleProgressiveEnhancement<TEnv>(
257
257
  formState: actionResult,
258
258
  };
259
259
 
260
- const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
260
+ const debugChannel = requireRequestContext()._debugChannel;
261
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
262
+ ...(debugChannel && { debugChannel }),
263
+ });
261
264
  // metricsStore=undefined is safe: the handler already stashed the early
262
265
  // SSR setup promise on request variables, so getSSRSetup returns it
263
266
  // without falling back to a fresh startSSRSetup.
@@ -168,7 +168,10 @@ export async function handleRscRendering<TEnv>(
168
168
 
169
169
  // Serialize to RSC stream
170
170
  const rscSerializeStart = performance.now();
171
- const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
171
+ const debugChannel = reqCtx._debugChannel;
172
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
173
+ ...(debugChannel && { debugChannel }),
174
+ });
172
175
  const rscSerializeDur = performance.now() - rscSerializeStart;
173
176
  // This measures synchronous stream creation, not end-to-end stream consumption.
174
177
  appendMetric(
@@ -223,8 +223,10 @@ export async function executeServerAction<TEnv>(
223
223
  // location state is a success-only semantic. Error boundary responses
224
224
  // update the error UI but should not mutate browser history state.
225
225
 
226
+ const debugChannel = requireRequestContext()._debugChannel;
226
227
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
227
228
  temporaryReferences,
229
+ ...(debugChannel && { debugChannel }),
228
230
  });
229
231
 
230
232
  return createResponseWithMergedHeaders(rscStream, {
package/src/rsc/types.ts CHANGED
@@ -63,7 +63,13 @@ export interface RSCDependencies {
63
63
  */
64
64
  renderToReadableStream: <T>(
65
65
  payload: T,
66
- options?: { temporaryReferences?: unknown },
66
+ options?: {
67
+ temporaryReferences?: unknown;
68
+ debugChannel?: {
69
+ readable?: ReadableStream;
70
+ writable?: WritableStream;
71
+ };
72
+ },
67
73
  ) => ReadableStream<Uint8Array>;
68
74
 
69
75
  /**
@@ -20,6 +20,61 @@ import { RootErrorBoundary } from "./root-error-boundary.js";
20
20
  const ReactViewTransition: any =
21
21
  "ViewTransition" in React ? (React as any).ViewTransition : null;
22
22
 
23
+ function restoreParallelLoaderMarkers(
24
+ segments: ResolvedSegment[],
25
+ ): ResolvedSegment[] {
26
+ const parallelLoadingByNamespace = new Map<string, ReactNode>();
27
+ let nextSegments: ResolvedSegment[] | null = null;
28
+
29
+ for (let i = 0; i < segments.length; i++) {
30
+ const segment = segments[i];
31
+
32
+ if (segment.type === "parallel") {
33
+ if (
34
+ segment.namespace &&
35
+ segment.loading !== undefined &&
36
+ segment.loading !== null &&
37
+ segment.loading !== false
38
+ ) {
39
+ parallelLoadingByNamespace.set(segment.namespace, segment.loading);
40
+ }
41
+ continue;
42
+ }
43
+
44
+ if (segment.type !== "loader" || segment.parallelLoading !== undefined) {
45
+ continue;
46
+ }
47
+
48
+ const parallelLoading = segment.namespace
49
+ ? parallelLoadingByNamespace.get(segment.namespace)
50
+ : undefined;
51
+ if (parallelLoading === undefined) {
52
+ continue;
53
+ }
54
+
55
+ if (!nextSegments) {
56
+ nextSegments = segments.slice();
57
+ }
58
+ nextSegments[i] = { ...segment, parallelLoading };
59
+ }
60
+
61
+ return nextSegments ?? segments;
62
+ }
63
+
64
+ function hasSameReferences(a: unknown[] | undefined, b: unknown[]): boolean {
65
+ if (!a || a.length !== b.length) {
66
+ return false;
67
+ }
68
+
69
+ for (let i = 0; i < a.length; i++) {
70
+ if (a[i] !== b[i]) {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ return true;
76
+ }
77
+
23
78
  /**
24
79
  * Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
25
80
  */
@@ -143,6 +198,10 @@ export async function renderSegments(
143
198
  } = options || {};
144
199
 
145
200
  const temporalLazyRefs: Promise<any>[] = [];
201
+ const normalizedSegments = restoreParallelLoaderMarkers(segments);
202
+ const normalizedInterceptSegments = interceptSegments
203
+ ? restoreParallelLoaderMarkers(interceptSegments)
204
+ : undefined;
146
205
 
147
206
  /**
148
207
  * Registers promises from lazy/async components for awaiting.
@@ -167,7 +226,7 @@ export async function renderSegments(
167
226
  );
168
227
  }
169
228
  // Separate segments by type, passing intercept segments for explicit injection
170
- const tree = segmentTreeWalk(segments, interceptSegments);
229
+ const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
171
230
  // Render content segments as siblings
172
231
  let content: ReactNode = null;
173
232
  for (const node of tree) {
@@ -284,13 +343,90 @@ export async function renderSegments(
284
343
  children: nodeContent,
285
344
  });
286
345
  } else {
287
- // Has loaders but no loading skeleton - await loaders and render directly
288
- const resolvedData = await loaderDataPromise;
346
+ // Has loaders but no loading skeleton.
347
+ // Split: parallel-owned loaders stream (their parallel has loading()),
348
+ // layout-owned loaders are awaited (they gate the layout content).
349
+ const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
350
+ const parallelOwnedLoaders = loaderEntries.filter(
351
+ (l) => !!l.parallelLoading,
352
+ );
353
+
354
+ // Await only layout-owned loaders
355
+ const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
356
+ const layoutLoaderDataPromise =
357
+ layoutLoaders.length > 0
358
+ ? Promise.all(
359
+ layoutLoaders.map((l) =>
360
+ l.loaderData instanceof Promise
361
+ ? l.loaderData
362
+ : Promise.resolve(l.loaderData),
363
+ ),
364
+ )
365
+ : Promise.resolve([]);
366
+ const resolvedData = await layoutLoaderDataPromise;
289
367
  const { loaderData, errorFallback } = resolveLoaderData(
290
368
  resolvedData,
291
- loaderIds,
369
+ layoutLoaderIds,
292
370
  );
293
371
 
372
+ // Parallel-owned loaders: attach to their owning parallel segment
373
+ // as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
374
+ if (parallelOwnedLoaders.length > 0) {
375
+ const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
376
+
377
+ for (const loader of parallelOwnedLoaders) {
378
+ if (!loader.namespace) {
379
+ continue;
380
+ }
381
+ const existing = loadersByParallelNamespace.get(loader.namespace);
382
+ if (existing) {
383
+ existing.push(loader);
384
+ } else {
385
+ loadersByParallelNamespace.set(loader.namespace, [loader]);
386
+ }
387
+ }
388
+
389
+ for (const p of node.parallel) {
390
+ if (!p.loading || !p.namespace) {
391
+ continue;
392
+ }
393
+
394
+ const ownedLoaders = loadersByParallelNamespace.get(p.namespace);
395
+ if (!ownedLoaders || ownedLoaders.length === 0) {
396
+ continue;
397
+ }
398
+
399
+ const parallelLoaderIds = ownedLoaders.map((l) => l.loaderId!);
400
+ const parallelLoaderSources = ownedLoaders.map((l) => l.loaderData);
401
+ p.loaderIds = parallelLoaderIds;
402
+
403
+ const shouldReuseParallelPromise =
404
+ p.loaderDataPromise !== undefined &&
405
+ hasSameReferences(p.parallelLoaderSources, parallelLoaderSources);
406
+
407
+ const parallelLoaderDataPromise = shouldReuseParallelPromise
408
+ ? p.loaderDataPromise
409
+ : forceAwait || isAction
410
+ ? await Promise.all(
411
+ ownedLoaders.map((l) =>
412
+ l.loaderData instanceof Promise
413
+ ? l.loaderData
414
+ : Promise.resolve(l.loaderData),
415
+ ),
416
+ )
417
+ : Promise.all(
418
+ ownedLoaders.map((l) =>
419
+ l.loaderData instanceof Promise
420
+ ? l.loaderData
421
+ : Promise.resolve(l.loaderData),
422
+ ),
423
+ );
424
+
425
+ p.loaderDataPromise = parallelLoaderDataPromise;
426
+ p.parallelLoaderSources = parallelLoaderSources;
427
+ }
428
+ }
429
+
294
430
  content = createElement(OutletProvider, {
295
431
  key,
296
432
  content: outletContent,
@@ -157,10 +157,24 @@ export type InterceptEntry = {
157
157
  when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
158
158
  };
159
159
 
160
+ export interface ParallelEntryData
161
+ extends EntryPropCommon, EntryPropDatas, EntryPropSegments {
162
+ type: "parallel";
163
+ handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
164
+ loading?: ReactNode | false;
165
+ transition?: TransitionConfig;
166
+ /** Set when any parallel slot is a Static definition */
167
+ isStaticPrerender?: true;
168
+ /** Per-slot static handler $$ids for build-time store lookup */
169
+ staticHandlerIds?: Record<string, string>;
170
+ }
171
+
172
+ export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
173
+
160
174
  export type EntryPropSegments = {
161
175
  loader: LoaderEntry[];
162
176
  layout: EntryData[];
163
- parallel: EntryData[]; // type: "parallel" entries with their own loaders/revalidate/loading
177
+ parallel: ParallelEntries; // slot -> parallel entry (same entry may back multiple slots)
164
178
  intercept: InterceptEntry[]; // intercept definitions for soft navigation
165
179
  };
166
180
 
@@ -200,18 +214,7 @@ export type EntryData =
200
214
  } & EntryPropCommon &
201
215
  EntryPropDatas &
202
216
  EntryPropSegments)
203
- | ({
204
- type: "parallel";
205
- handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
206
- loading?: ReactNode | false;
207
- transition?: TransitionConfig;
208
- /** Set when any parallel slot is a Static definition */
209
- isStaticPrerender?: true;
210
- /** Per-slot static handler $$ids for build-time store lookup */
211
- staticHandlerIds?: Record<string, string>;
212
- } & EntryPropCommon &
213
- EntryPropDatas &
214
- EntryPropSegments)
217
+ | ParallelEntryData
215
218
  | ({
216
219
  type: "cache";
217
220
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
@@ -270,6 +273,9 @@ interface HelperContext {
270
273
  string,
271
274
  import("../cache/profile-registry.js").CacheProfile
272
275
  >;
276
+ /** True when resolving handlers inside a cache() DSL boundary.
277
+ * Read by ctx.get() to guard non-cacheable variable reads. */
278
+ insideCacheScope?: boolean;
273
279
  }
274
280
  // Use a global symbol key so the AsyncLocalStorage instance survives HMR
275
281
  // module re-evaluation. Without this, Vite's RSC module runner may create
@@ -553,6 +559,80 @@ export function getRootScoped(): boolean {
553
559
  // Export HelperContext type for use in other modules
554
560
  export type { HelperContext };
555
561
 
562
+ /**
563
+ * Return an isolated copy of a lazy include's captured parent entry.
564
+ *
565
+ * DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
566
+ * Multiple include() scopes capture the *same* syntheticMapRoot as their
567
+ * parent, so without isolation one include's loaders/middleware leak into
568
+ * every other route that shares that root.
569
+ *
570
+ * The clone is shallow: only the mutable arrays are copied so each
571
+ * include pushes to its own list. The rest of the entry (id, shortCode,
572
+ * parent pointer, handler) stays shared, which is correct and cheap.
573
+ */
574
+ export function getIsolatedLazyParent(
575
+ captured: EntryData | null | undefined,
576
+ ): EntryData | null {
577
+ if (!captured) return null;
578
+ return {
579
+ ...captured,
580
+ loader: [...captured.loader],
581
+ middleware: [...captured.middleware],
582
+ revalidate: [...captured.revalidate],
583
+ errorBoundary: [...captured.errorBoundary],
584
+ notFoundBoundary: [...captured.notFoundBoundary],
585
+ layout: [...captured.layout],
586
+ parallel: { ...captured.parallel },
587
+ intercept: [...captured.intercept],
588
+ };
589
+ }
590
+
591
+ export function getParallelEntries(
592
+ parallels: ParallelEntries | EntryData[] | undefined,
593
+ ): ParallelEntryData[] {
594
+ if (!parallels) return [];
595
+ if (Array.isArray(parallels)) {
596
+ return parallels.filter(
597
+ (entry): entry is ParallelEntryData => entry.type === "parallel",
598
+ );
599
+ }
600
+ return Object.values(parallels).filter(
601
+ (entry): entry is ParallelEntryData => !!entry,
602
+ );
603
+ }
604
+
605
+ export function getParallelSlotEntries(
606
+ parallels: ParallelEntries | EntryData[] | undefined,
607
+ ): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
608
+ if (!parallels) return [];
609
+
610
+ if (Array.isArray(parallels)) {
611
+ return getParallelEntries(parallels).flatMap((entry) =>
612
+ (Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
613
+ slot,
614
+ entry,
615
+ })),
616
+ );
617
+ }
618
+
619
+ return Object.entries(parallels)
620
+ .filter(([, entry]) => !!entry)
621
+ .map(([slot, entry]) => ({
622
+ slot: slot as `@${string}`,
623
+ entry: entry!,
624
+ }));
625
+ }
626
+
627
+ export function getParallelSlotCount(
628
+ parallels: ParallelEntries | EntryData[] | undefined,
629
+ ): number {
630
+ if (!parallels) return 0;
631
+ return Array.isArray(parallels)
632
+ ? parallels.filter((entry) => entry?.type === "parallel").length
633
+ : Object.keys(parallels).length;
634
+ }
635
+
556
636
  // ============================================================================
557
637
  // Performance Metrics Helpers
558
638
  // ============================================================================
@@ -589,3 +669,12 @@ export function track(label: string, depth?: number): () => void {
589
669
  });
590
670
  };
591
671
  }
672
+
673
+ /**
674
+ * Check if the current execution is inside a cache() DSL boundary.
675
+ * Returns false inside loader execution — loaders are always fresh
676
+ * (never cached), so non-cacheable reads are safe.
677
+ */
678
+ export function isInsideCacheScope(): boolean {
679
+ return RSCRouterContext.getStore()?.insideCacheScope === true;
680
+ }