@rangojs/router 0.0.0-experimental.002d056c → 0.0.0-experimental.07cdfab0

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.
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
1745
1745
  // package.json
1746
1746
  var package_default = {
1747
1747
  name: "@rangojs/router",
1748
- version: "0.0.0-experimental.002d056c",
1748
+ version: "0.0.0-experimental.07cdfab0",
1749
1749
  description: "Django-inspired RSC router with composable URL patterns",
1750
1750
  keywords: [
1751
1751
  "react",
@@ -2884,11 +2884,11 @@ ${dim} \u2571${reset} ${bold}\u2554\u2550\u2557${reset}${dim} * \u2
2884
2884
  ${dim} ${reset}${bold}\u2551 \u2551${reset} ${bold}\u2554\u2550\u2557${reset}${dim} * \u2727. \u2571${reset}
2885
2885
  ${dim} ${reset}${bold}\u2554\u2557 \u2551 \u2551 \u2551 \u2551${reset}${dim} * \u2571${reset}
2886
2886
  ${dim} ${reset}${bold}\u2551\u2551 \u2551 \u2551 \u2551 \u2551 \u2566\u2550\u2557\u2554\u2550\u2557\u2554\u2557\u2554\u2554\u2550\u2557\u2554\u2550\u2557${reset}${dim} \u2727 \u2726${reset}
2887
- ${dim} ${reset}${bold}\u2550\u2563\u2551 \u2551 \u2560\u2550\u255D \u2551 \u2560\u2566\u255D\u2560\u2550\u2563\u2551\u2551\u2551\u2551 \u2566\u2551 \u2551${reset}${dim} * \u2727${reset}
2887
+ ${dim} ${reset}${bold}\u2551\u2551 \u2551 \u2560\u2550\u255D \u2551 \u2560\u2566\u255D\u2560\u2550\u2563\u2551\u2551\u2551\u2551 \u2566\u2551 \u2551${reset}${dim} * \u2727${reset}
2888
2888
  ${dim} ${reset}${bold}\u2551\u255A\u2550\u255D \u2554\u2550\u2550\u2550\u255D \u2569\u255A\u2550\u2569 \u2569\u255D\u255A\u255D\u255A\u2550\u255D\u255A\u2550\u255D${reset}${dim} \u2726 . *${reset}
2889
2889
  ${dim} ${reset}${bold}\u255A\u2550\u2550\u2557 \u2551${reset}${dim} * RSC Wrangler \u2727 \u2726${reset}
2890
- ${dim} * ${reset}${bold}\u2551 \u2560\u2550${reset}${dim} * \u2727. \u2571${reset}
2891
- ${bold}\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2569\u2550\u2550\u2550${reset}${dim} \u2726 *${reset}
2890
+ ${dim} * ${reset}${bold}\u2551 \u2551${reset}${dim} * \u2727. \u2571${reset}
2891
+ ${dim} ${reset}${bold}\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550${reset}${dim} \u2726 *${reset}
2892
2892
 
2893
2893
  v${version} \xB7 ${preset} \xB7 ${mode}
2894
2894
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.002d056c",
3
+ "version": "0.0.0-experimental.07cdfab0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -109,6 +109,65 @@ parallel(
109
109
  )
110
110
  ```
111
111
 
112
+ ### Streaming Behavior
113
+
114
+ Parallels with `loading()` are **independent streaming units**. They don't
115
+ block the parent layout or sibling routes during SSR:
116
+
117
+ - **With `loading()`**: The skeleton renders immediately. The loader runs
118
+ in the background and streams data to the client when ready. The rest
119
+ of the page (layout, route content, other parallels) renders without
120
+ waiting.
121
+ - **Without `loading()`**: The parallel's loaders block the parent layout's
122
+ rendering. Use this when the data must be available before the page
123
+ paints (e.g., critical above-the-fold content).
124
+ - **SPA navigation**: Parallel loaders resolve in the background. The
125
+ existing parallel UI stays visible — no skeleton flash on route changes
126
+ within the same layout.
127
+
128
+ ```typescript
129
+ // Sidebar streams independently — page renders immediately
130
+ parallel(
131
+ { "@sidebar": () => <Sidebar /> },
132
+ () => [loader(SlowSidebarLoader), loading(<SidebarSkeleton />)]
133
+ )
134
+
135
+ // Cart data blocks layout — must be ready before paint
136
+ parallel(
137
+ { "@cartBadge": () => <CartBadge /> },
138
+ () => [loader(CartCountLoader)] // No loading() = awaited
139
+ )
140
+ ```
141
+
142
+ ## Slot Override Semantics
143
+
144
+ When multiple `parallel()` calls define the same slot name, **the last
145
+ definition wins**. Earlier definitions of that slot are removed. Other
146
+ slots from the earlier call are preserved.
147
+
148
+ This enables composition patterns where included routes override
149
+ parent-defined slots:
150
+
151
+ ```typescript
152
+ layout(DashboardLayout, () => [
153
+ // Base slots
154
+ parallel({
155
+ "@sidebar": () => <DefaultSidebar />,
156
+ "@footer": () => <Footer />,
157
+ }),
158
+
159
+ // Override just @sidebar — @footer is preserved
160
+ parallel({ "@sidebar": () => <CustomSidebar /> }),
161
+
162
+ path("/", DashboardIndex, { name: "index" }),
163
+ ])
164
+ ```
165
+
166
+ After resolution, the layout has two parallel entries:
167
+
168
+ - `{ "@footer": () => <Footer /> }` (first call, `@sidebar` removed)
169
+ - `{ "@sidebar": () => <CustomSidebar /> }` (second call, wins)
170
+
112
171
  ## Multiple Parallel Slots
113
172
 
114
173
  ```typescript
@@ -79,6 +79,8 @@ export interface DerivedNavigationState {
79
79
  state: "idle" | "loading";
80
80
  /** Whether any operation is streaming */
81
81
  isStreaming: boolean;
82
+ /** Whether a navigation is active (fetching or streaming, before commit) */
83
+ isNavigating: boolean;
82
84
  /** Current committed location */
83
85
  location: NavigationLocation;
84
86
  /** URL being navigated to (null if idle) */
@@ -389,6 +391,9 @@ export function createEventController(
389
391
  return {
390
392
  state,
391
393
  isStreaming,
394
+ // True when a navigation is active (fetching or streaming, before
395
+ // commit). Broader than pendingUrl which clears during streaming.
396
+ isNavigating: currentNavigation !== null,
392
397
  location,
393
398
  // pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
394
399
  // Background revalidations (skipLoadingState) don't expose a pending URL.
@@ -19,8 +19,8 @@ import {
19
19
  } from "./response-adapter.js";
20
20
  import {
21
21
  buildPrefetchKey,
22
- consumePrefetch,
23
22
  consumeInflightPrefetch,
23
+ consumePrefetch,
24
24
  } from "./prefetch/cache.js";
25
25
 
26
26
  /**
@@ -89,22 +89,18 @@ export function createNavigationClient(
89
89
  fetchUrl.searchParams.set("_rsc_v", version);
90
90
  }
91
91
 
92
- // Check in-memory prefetch cache before making a network request.
92
+ // Check completed in-memory prefetch cache before making a network request.
93
93
  // The cache key includes the source URL (previousUrl) because the
94
94
  // server's diff response depends on the source page context.
95
95
  // Skip cache for stale revalidation (needs fresh data), HMR (needs
96
96
  // fresh modules), and intercept contexts (source-dependent responses).
97
+ //
97
98
  const canUsePrefetch = !staleRevalidation && !hmr && !interceptSourceUrl;
98
99
  const cacheKey = buildPrefetchKey(previousUrl, fetchUrl);
99
100
  const cachedResponse = canUsePrefetch ? consumePrefetch(cacheKey) : null;
100
- // If no completed cache entry, check for in-flight prefetch.
101
- // This reuses a prefetch that is still downloading rather than
102
- // starting a duplicate request from scratch.
103
- const inflightPrefetch =
104
- !cachedResponse && canUsePrefetch
105
- ? consumeInflightPrefetch(cacheKey)
106
- : null;
107
-
101
+ const inflightResponsePromise = canUsePrefetch
102
+ ? consumeInflightPrefetch(cacheKey)
103
+ : null;
108
104
  // Track when the stream completes
109
105
  let resolveStreamComplete: () => void;
110
106
  const streamComplete = new Promise<void>((resolve) => {
@@ -195,33 +191,24 @@ export function createNavigationClient(
195
191
  signal,
196
192
  );
197
193
  });
198
- } else if (inflightPrefetch) {
194
+ } else if (inflightResponsePromise) {
199
195
  if (tx) {
200
196
  browserDebugLog(tx, "reusing inflight prefetch", { key: cacheKey });
201
197
  }
202
- // Await the in-flight prefetch. If it resolves with a Response,
203
- // use it like a cache hit. If it fails (null), fall back to
204
- // a fresh navigation fetch.
205
- responsePromise = inflightPrefetch.then((prefetchResponse) => {
206
- if (!prefetchResponse) {
198
+ responsePromise = inflightResponsePromise.then(async (response) => {
199
+ if (!response) {
207
200
  if (tx) {
208
- browserDebugLog(
209
- tx,
210
- "inflight prefetch failed, falling back to fetch",
211
- );
201
+ browserDebugLog(tx, "inflight prefetch unavailable, refetching");
212
202
  }
213
203
  return doFreshFetch();
214
204
  }
215
- if (tx) {
216
- browserDebugLog(tx, "inflight prefetch resolved", {
217
- key: cacheKey,
218
- });
219
- }
205
+
220
206
  return teeWithCompletion(
221
- prefetchResponse,
207
+ response,
222
208
  () => {
223
- if (tx)
209
+ if (tx) {
224
210
  browserDebugLog(tx, "stream complete (from inflight prefetch)");
211
+ }
225
212
  resolveStreamComplete();
226
213
  },
227
214
  signal,
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Prefetch Cache
3
3
  *
4
- * In-memory cache storing prefetch Response objects for instant cache hits
4
+ * In-memory cache storing prefetched Response objects for instant cache hits
5
5
  * on subsequent navigation. Cache key is source-dependent (includes the
6
6
  * current page URL) because the server's diff-based response depends on
7
7
  * where the user navigates from.
8
8
  *
9
- * Also tracks in-flight prefetch promises so navigation can reuse a
10
- * prefetch that is still downloading rather than starting a duplicate
11
- * request. See consumeInflightPrefetch().
9
+ * Also tracks in-flight prefetch promises. Each promise resolves to the
10
+ * navigation branch of a tee'd Response, allowing navigation to adopt a
11
+ * still-downloading prefetch without reparsing or buffering the body.
12
12
  *
13
13
  * Replaces the previous browser HTTP cache approach which was unreliable
14
14
  * due to response draining race conditions and browser inconsistencies.
@@ -130,8 +130,8 @@ export function consumeInflightPrefetch(
130
130
 
131
131
  /**
132
132
  * Store a prefetch response in the in-memory cache.
133
- * The response body must be fully buffered (e.g. via arrayBuffer()) before
134
- * storing, so the cached Response is self-contained and network-independent.
133
+ * The response should be a clone() of the original so the caller can
134
+ * still consume the body. The clone's body streams independently.
135
135
  *
136
136
  * Skips storage if the generation has changed since the fetch started
137
137
  * (a server action invalidated the cache mid-flight).
@@ -55,10 +55,10 @@ function buildPrefetchUrl(
55
55
  }
56
56
 
57
57
  /**
58
- * Core prefetch fetch logic. Fetches the response, fully buffers the body,
59
- * and stores it in the in-memory cache. The returned Promise resolves to
60
- * the buffered Response (or null on failure) so navigation can reuse
61
- * in-flight prefetches via consumeInflightPrefetch().
58
+ * Core prefetch fetch logic. Fetches the response, tees the body, and stores
59
+ * one branch in the in-memory cache. The returned Promise resolves to the
60
+ * sibling navigation branch (or null on failure) so navigation can safely
61
+ * reuse an in-flight prefetch via consumeInflightPrefetch().
62
62
  */
63
63
  function executePrefetchFetch(
64
64
  key: string,
@@ -77,20 +77,19 @@ function executePrefetchFetch(
77
77
  "X-Rango-Prefetch": "1",
78
78
  },
79
79
  })
80
- .then(async (response) => {
80
+ .then((response) => {
81
81
  if (!response.ok) return null;
82
- // Fully buffer the response body so the cached Response is
83
- // self-contained and doesn't depend on the network connection.
84
- // This eliminates the race condition where the user clicks before
85
- // the response body has been fully downloaded.
86
- const buffer = await response.arrayBuffer();
87
- const cachedResponse = new Response(buffer, {
82
+ // Don't buffer with arrayBuffer() that blocks until the entire
83
+ // body downloads, defeating streaming for slow loaders.
84
+ // Tee the body: one branch for navigation, one for cache storage.
85
+ const [navStream, cacheStream] = response.body!.tee();
86
+ const responseInit = {
88
87
  headers: response.headers,
89
88
  status: response.status,
90
89
  statusText: response.statusText,
91
- });
92
- storePrefetch(key, cachedResponse.clone(), gen);
93
- return cachedResponse;
90
+ };
91
+ storePrefetch(key, new Response(cacheStream, responseInit), gen);
92
+ return new Response(navStream, responseInit);
94
93
  })
95
94
  .catch(() => null)
96
95
  .finally(() => {
@@ -286,6 +286,18 @@ export async function initBrowserApp(
286
286
  // Debounce: wait 200ms of quiet before fetching
287
287
  hmrTimer = setTimeout(async () => {
288
288
  hmrTimer = null;
289
+
290
+ // Don't interrupt an active user navigation — startNavigation()
291
+ // would abort it and refetch the old URL (window.location.href
292
+ // hasn't updated yet). The user's navigation will pick up the
293
+ // new server code when it completes. isNavigating covers the
294
+ // full lifecycle (fetching + streaming, before commit) without
295
+ // blocking on server actions.
296
+ if (eventController.getState().isNavigating) {
297
+ console.log("[RSCRouter] HMR: Skipping — navigation in progress");
298
+ return;
299
+ }
300
+
289
301
  console.log("[RSCRouter] HMR: Server update, refetching RSC");
290
302
 
291
303
  const abort = new AbortController();
@@ -310,6 +322,13 @@ export async function initBrowserApp(
310
322
 
311
323
  if (abort.signal.aborted) return;
312
324
 
325
+ // If the server returned a non-RSC response (404, 500 without
326
+ // error boundary), the payload won't have valid metadata.
327
+ // Reload to recover rather than leaving the page stale.
328
+ if (!payload.metadata) {
329
+ throw new Error("HMR refetch returned invalid payload");
330
+ }
331
+
313
332
  if (payload.metadata?.isPartial) {
314
333
  const segments = payload.metadata.segments || [];
315
334
  const matched = payload.metadata.matched || [];
@@ -160,8 +160,13 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
160
160
 
161
161
  // For non-action actors: cached segments the server decided not to re-render.
162
162
  // - Preserve loading=false (suppressed boundary) to maintain tree structure
163
- // - Clear truthy loading (active skeleton) to prevent suspense on cached content
163
+ // - Preserve parallel segment loading so renderSegments can reconstruct
164
+ // parallel-owned loader markers from the cached slot metadata
165
+ // - Clear other truthy loading values to prevent suspense on cached content
164
166
  if (actor !== "action") {
167
+ if (fromCache.type === "parallel" && fromCache.loading !== undefined) {
168
+ return fromCache;
169
+ }
165
170
  if (fromCache.loading !== undefined && fromCache.loading !== false) {
166
171
  return { ...fromCache, loading: undefined };
167
172
  }
@@ -13,6 +13,7 @@
13
13
 
14
14
  import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
15
15
  import { getRequestContext } from "../server/request-context.js";
16
+ import { mayNeedSSR } from "../rsc/ssr-setup.js";
16
17
  import { sortedSearchString } from "./cache-key-utils.js";
17
18
  import { runBackground } from "./background-task.js";
18
19
 
@@ -204,18 +205,24 @@ export function createDocumentCacheMiddleware<TEnv = any>(
204
205
  ): Promise<Response> {
205
206
  const url = ctx.url;
206
207
 
208
+ // Use the original request URL for _rsc* param detection and cache key
209
+ // differentiation. ctx.url is stripped of _rsc* params by the middleware
210
+ // pipeline (stripInternalParams), so _rsc_partial, _rsc_segments, etc.
211
+ // are not visible on ctx.url in production.
212
+ const rawUrl = new URL(ctx.request.url);
213
+
207
214
  // Only cache GET requests — mutations and other methods must not be cached
208
215
  if (ctx.request.method !== "GET") {
209
216
  return next();
210
217
  }
211
218
 
212
219
  // Skip RSC action requests (mutations shouldn't be cached)
213
- if (url.searchParams.has("_rsc_action")) {
220
+ if (rawUrl.searchParams.has("_rsc_action")) {
214
221
  return next();
215
222
  }
216
223
 
217
224
  // Skip loader requests (have their own caching)
218
- if (url.searchParams.has("_rsc_loader")) {
225
+ if (rawUrl.searchParams.has("_rsc_loader")) {
219
226
  return next();
220
227
  }
221
228
 
@@ -241,9 +248,12 @@ export function createDocumentCacheMiddleware<TEnv = any>(
241
248
  return next();
242
249
  }
243
250
 
244
- // Determine request type for cache key differentiation
245
- const isPartial = url.searchParams.has("_rsc_partial");
246
- const typeLabel = isPartial ? "RSC" : "HTML";
251
+ // Determine request type for cache key differentiation.
252
+ // Uses rawUrl for _rsc* param checks and mayNeedSSR for Accept-based
253
+ // detection. Full-document RSC fetches must not share the HTML cache slot.
254
+ const isPartial = rawUrl.searchParams.has("_rsc_partial");
255
+ const isRscRequest = !mayNeedSSR(ctx.request, rawUrl);
256
+ const typeLabel = isRscRequest ? "RSC" : "HTML";
247
257
 
248
258
  // Track whether next() has been called so the catch block knows
249
259
  // whether it is safe to fall through to the handler.
@@ -254,10 +264,10 @@ export function createDocumentCacheMiddleware<TEnv = any>(
254
264
  // gracefully to the origin handler instead of rejecting the request.
255
265
  // This is a deliberate fail-open-to-origin policy: the fallback is
256
266
  // "serve uncached from origin", not "use a different cache key".
257
- const clientSegments = url.searchParams.get("_rsc_segments") || "";
267
+ const clientSegments = rawUrl.searchParams.get("_rsc_segments") || "";
258
268
  const segmentHash =
259
269
  isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
260
- const typeSuffix = isPartial ? ":rsc" : ":html";
270
+ const typeSuffix = isRscRequest ? ":rsc" : ":html";
261
271
 
262
272
  let searchSuffix = "";
263
273
  if (!keyGenerator) {
package/src/debug.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Debug utilities for manifest inspection and comparison
3
3
  */
4
4
 
5
- import type { EntryData } from "./server/context";
5
+ import { getParallelSlotCount, type EntryData } from "./server/context";
6
6
 
7
7
  /**
8
8
  * Serialized entry for debug output
@@ -64,7 +64,7 @@ export function serializeManifest(
64
64
  hasLoader: entry.loader?.length > 0,
65
65
  hasMiddleware: entry.middleware?.length > 0,
66
66
  hasErrorBoundary: entry.errorBoundary?.length > 0,
67
- parallelCount: entry.parallel?.length ?? 0,
67
+ parallelCount: getParallelSlotCount(entry.parallel),
68
68
  interceptCount: entry.intercept?.length ?? 0,
69
69
  };
70
70
 
@@ -282,7 +282,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
282
282
  errorBoundary: [],
283
283
  notFoundBoundary: [],
284
284
  layout: [],
285
- parallel: [],
285
+ parallel: {},
286
286
  intercept: [],
287
287
  loader: [],
288
288
  ...(cacheUrlPrefix ? { mountPath: cacheUrlPrefix } : {}),
@@ -320,7 +320,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
320
320
  errorBoundary: [],
321
321
  notFoundBoundary: [],
322
322
  layout: [],
323
- parallel: [],
323
+ parallel: {},
324
324
  intercept: [],
325
325
  loader: [],
326
326
  ...(cacheUrlPrefix2 ? { mountPath: cacheUrlPrefix2 } : {}),
@@ -393,6 +393,8 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
393
393
  "parallel() cannot be nested inside another parallel()",
394
394
  );
395
395
 
396
+ const slotNames = Object.keys(slots as Record<string, any>) as `@${string}`[];
397
+
396
398
  const namespace = `${ctx.namespace}.$${store.getNextIndex("parallel")}`;
397
399
 
398
400
  // Unwrap any static handler definitions in parallel slots
@@ -431,7 +433,7 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
431
433
  errorBoundary: [],
432
434
  notFoundBoundary: [],
433
435
  layout: [],
434
- parallel: [],
436
+ parallel: {},
435
437
  intercept: [],
436
438
  loader: [],
437
439
  ...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
@@ -454,7 +456,30 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
454
456
  );
455
457
  }
456
458
 
457
- ctx.parent.parallel.push(entry);
459
+ for (const slotName of slotNames) {
460
+ const slotEntry = {
461
+ ...entry,
462
+ handler: { [slotName]: unwrappedSlots[slotName]! },
463
+ middleware: [...entry.middleware],
464
+ revalidate: [...entry.revalidate],
465
+ errorBoundary: [...entry.errorBoundary],
466
+ notFoundBoundary: [...entry.notFoundBoundary],
467
+ layout: [...entry.layout],
468
+ parallel: { ...entry.parallel },
469
+ intercept: [...entry.intercept],
470
+ loader: [...entry.loader],
471
+ ...(entry.staticHandlerIds?.[slotName]
472
+ ? {
473
+ isStaticPrerender: true as const,
474
+ staticHandlerIds: { [slotName]: entry.staticHandlerIds[slotName]! },
475
+ }
476
+ : {
477
+ isStaticPrerender: undefined,
478
+ staticHandlerIds: undefined,
479
+ }),
480
+ } satisfies EntryData;
481
+ ctx.parent.parallel[slotName] = slotEntry;
482
+ }
458
483
  return { name: namespace, type: "parallel" } as ParallelItem;
459
484
  };
460
485
 
@@ -687,7 +712,7 @@ const transitionFn = (
687
712
  errorBoundary: [],
688
713
  notFoundBoundary: [],
689
714
  layout: [],
690
- parallel: [],
715
+ parallel: {},
691
716
  intercept: [],
692
717
  loader: [],
693
718
  } as EntryData;
@@ -734,7 +759,7 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
734
759
  errorBoundary: [],
735
760
  notFoundBoundary: [],
736
761
  layout: [],
737
- parallel: [],
762
+ parallel: {},
738
763
  intercept: [],
739
764
  loader: [],
740
765
  } satisfies EntryData;
@@ -791,7 +816,7 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
791
816
  revalidate: [],
792
817
  errorBoundary: [],
793
818
  notFoundBoundary: [],
794
- parallel: [],
819
+ parallel: {},
795
820
  intercept: [],
796
821
  layout: [],
797
822
  loader: [],
@@ -4,6 +4,7 @@ import {
4
4
  EntryData,
5
5
  RSCRouterContext,
6
6
  runWithPrefixes,
7
+ getIsolatedLazyParent,
7
8
  } from "../server/context";
8
9
  import type { UrlPatterns } from "../urls.js";
9
10
  import type { AllUseItems, IncludeItem } from "../route-types.js";
@@ -138,7 +139,7 @@ export function evaluateLazyEntry<TEnv = any>(
138
139
  patternsByPrefix,
139
140
  trailingSlash: trailingSlashMap,
140
141
  namespace: "lazy",
141
- parent: (lazyContext?.parent as EntryData | null) ?? null,
142
+ parent: getIsolatedLazyParent(lazyContext?.parent as EntryData | null),
142
143
  counters: lazyCounters,
143
144
  cacheProfiles: (lazyContext as any)?.cacheProfiles,
144
145
  rootScoped: (lazyContext as any)?.rootScoped,
@@ -9,6 +9,7 @@ import { createRouteHelpers } from "../route-definition";
9
9
  import {
10
10
  getContext,
11
11
  runWithPrefixes,
12
+ getIsolatedLazyParent,
12
13
  type EntryData,
13
14
  type MetricsStore,
14
15
  } from "../server/context";
@@ -114,8 +115,11 @@ export async function loadManifest(
114
115
  // This ensures routes are registered under the correct layout hierarchy
115
116
  const lazyContext =
116
117
  entry.lazy && entry.lazyPatterns ? entry.lazyContext : null;
117
- const parentForContext =
118
- (lazyContext?.parent as EntryData | null) ?? Store.parent;
118
+ const parentForContext = lazyContext
119
+ ? getIsolatedLazyParent(
120
+ (lazyContext.parent as EntryData | null) ?? Store.parent,
121
+ )
122
+ : Store.parent;
119
123
 
120
124
  // For lazy entries, merge captured counters from include() so the
121
125
  // handler's entries get shortCode indices after sibling entries that