@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.
- package/dist/vite/index.js +4 -4
- package/package.json +1 -1
- package/skills/parallel/SKILL.md +59 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-client.ts +14 -27
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +13 -14
- package/src/browser/rsc-router.tsx +19 -0
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/debug.ts +2 -2
- package/src/route-definition/dsl-helpers.ts +32 -7
- package/src/router/lazy-includes.ts +2 -1
- package/src/router/manifest.ts +6 -2
- package/src/router/segment-resolution/fresh.ts +74 -9
- package/src/router/segment-resolution/revalidation.ts +298 -272
- package/src/router.ts +1 -1
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +90 -13
- package/src/ssr/index.tsx +1 -0
- package/src/types/segments.ts +2 -0
- package/src/urls/path-helper.ts +1 -1
- package/src/vite/utils/banner.ts +3 -3
package/dist/vite/index.js
CHANGED
|
@@ -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.
|
|
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}
|
|
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 \
|
|
2891
|
-
${bold}\u2550\u2550\u2550\
|
|
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
package/skills/parallel/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 (
|
|
194
|
+
} else if (inflightResponsePromise) {
|
|
199
195
|
if (tx) {
|
|
200
196
|
browserDebugLog(tx, "reusing inflight prefetch", { key: cacheKey });
|
|
201
197
|
}
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
216
|
-
browserDebugLog(tx, "inflight prefetch resolved", {
|
|
217
|
-
key: cacheKey,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
205
|
+
|
|
220
206
|
return teeWithCompletion(
|
|
221
|
-
|
|
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
|
|
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
134
|
-
*
|
|
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,
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* in-flight
|
|
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(
|
|
80
|
+
.then((response) => {
|
|
81
81
|
if (!response.ok) return null;
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
const
|
|
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,
|
|
93
|
-
return
|
|
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
|
-
// -
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
246
|
-
|
|
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 =
|
|
267
|
+
const clientSegments = rawUrl.searchParams.get("_rsc_segments") || "";
|
|
258
268
|
const segmentHash =
|
|
259
269
|
isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
|
|
260
|
-
const typeSuffix =
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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,
|
package/src/router/manifest.ts
CHANGED
|
@@ -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
|
-
(
|
|
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
|