@rangojs/router 0.0.0-experimental.f2337aef → 0.0.0-experimental.fa8a383a
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/bin/rango.js +8 -3
- package/dist/vite/index.js +139 -200
- package/package.json +1 -1
- package/skills/caching/SKILL.md +37 -4
- package/skills/parallel/SKILL.md +59 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +1 -3
- package/src/browser/navigation-client.ts +60 -27
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +39 -9
- package/src/browser/prefetch/cache.ts +57 -5
- package/src/browser/prefetch/fetch.ts +30 -21
- package/src/browser/prefetch/queue.ts +53 -13
- package/src/browser/react/Link.tsx +9 -1
- package/src/browser/react/NavigationProvider.tsx +27 -0
- package/src/browser/rsc-router.tsx +109 -57
- package/src/browser/scroll-restoration.ts +20 -7
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/types.ts +9 -0
- package/src/build/route-types/router-processing.ts +12 -2
- package/src/cache/cache-scope.ts +2 -2
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/debug.ts +2 -2
- package/src/route-definition/dsl-helpers.ts +32 -7
- package/src/route-definition/redirect.ts +2 -2
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/logging.ts +1 -1
- package/src/router/manifest.ts +9 -3
- package/src/router/match-middleware/background-revalidation.ts +18 -1
- package/src/router/match-middleware/cache-lookup.ts +20 -3
- package/src/router/match-middleware/cache-store.ts +32 -6
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +7 -5
- package/src/router/match-result.ts +11 -1
- package/src/router/middleware.ts +2 -1
- package/src/router/segment-resolution/fresh.ts +104 -14
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +307 -272
- package/src/router.ts +5 -1
- package/src/rsc/handler.ts +9 -0
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +90 -13
- package/src/server/request-context.ts +10 -4
- package/src/ssr/index.tsx +1 -0
- package/src/types/route-entry.ts +7 -0
- package/src/types/segments.ts +2 -0
- package/src/urls/path-helper.ts +1 -1
- package/src/vite/discovery/state.ts +0 -2
- package/src/vite/plugin-types.ts +0 -83
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +144 -209
- package/src/vite/router-discovery.ts +0 -8
- package/src/vite/utils/banner.ts +3 -3
|
@@ -5,12 +5,22 @@
|
|
|
5
5
|
* Hover prefetches bypass this queue — they fire directly for immediate response
|
|
6
6
|
* to user intent.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Draining is deferred to the next animation frame so prefetch network activity
|
|
9
|
+
* never blocks paint. This applies to both the initial batch and subsequent
|
|
10
|
+
* batches — every drain cycle yields to the browser first.
|
|
11
|
+
*
|
|
12
|
+
* When a navigation starts, queued prefetches are cancelled but executing ones
|
|
13
|
+
* are left running. Navigation can reuse their in-flight responses via the
|
|
14
|
+
* prefetch cache's inflight promise map, avoiding duplicate requests.
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
const MAX_CONCURRENT = 2;
|
|
13
18
|
|
|
19
|
+
const deferToNextPaint: (fn: () => void) => void =
|
|
20
|
+
typeof requestAnimationFrame === "function"
|
|
21
|
+
? requestAnimationFrame
|
|
22
|
+
: (fn) => setTimeout(fn, 0);
|
|
23
|
+
|
|
14
24
|
let active = 0;
|
|
15
25
|
const queue: Array<{
|
|
16
26
|
key: string;
|
|
@@ -19,6 +29,7 @@ const queue: Array<{
|
|
|
19
29
|
const queued = new Set<string>();
|
|
20
30
|
const executing = new Set<string>();
|
|
21
31
|
let abortController: AbortController | null = null;
|
|
32
|
+
let drainScheduled = false;
|
|
22
33
|
|
|
23
34
|
function startExecution(
|
|
24
35
|
key: string,
|
|
@@ -34,6 +45,21 @@ function startExecution(
|
|
|
34
45
|
if (executing.delete(key)) {
|
|
35
46
|
active--;
|
|
36
47
|
}
|
|
48
|
+
scheduleDrain();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Schedule a drain on the next animation frame.
|
|
54
|
+
* Coalesces multiple drain requests into a single rAF callback so
|
|
55
|
+
* batch completion doesn't schedule redundant frames.
|
|
56
|
+
*/
|
|
57
|
+
function scheduleDrain(): void {
|
|
58
|
+
if (drainScheduled) return;
|
|
59
|
+
if (active >= MAX_CONCURRENT || queue.length === 0) return;
|
|
60
|
+
drainScheduled = true;
|
|
61
|
+
deferToNextPaint(() => {
|
|
62
|
+
drainScheduled = false;
|
|
37
63
|
drain();
|
|
38
64
|
});
|
|
39
65
|
}
|
|
@@ -48,8 +74,8 @@ function drain(): void {
|
|
|
48
74
|
|
|
49
75
|
/**
|
|
50
76
|
* Enqueue a prefetch for concurrency-limited execution.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
77
|
+
* Execution is always deferred to the next animation frame to avoid
|
|
78
|
+
* blocking paint, even when below the concurrency limit.
|
|
53
79
|
* Deduplicates by key — items already queued or executing are skipped.
|
|
54
80
|
*
|
|
55
81
|
* The executor receives an AbortSignal that is aborted when
|
|
@@ -61,20 +87,33 @@ export function enqueuePrefetch(
|
|
|
61
87
|
): void {
|
|
62
88
|
if (queued.has(key) || executing.has(key)) return;
|
|
63
89
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
queued.add(key);
|
|
68
|
-
queue.push({ key, execute });
|
|
69
|
-
}
|
|
90
|
+
queued.add(key);
|
|
91
|
+
queue.push({ key, execute });
|
|
92
|
+
scheduleDrain();
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
/**
|
|
73
|
-
* Cancel
|
|
74
|
-
*
|
|
75
|
-
*
|
|
96
|
+
* Cancel queued prefetches. Executing prefetches are left running so
|
|
97
|
+
* navigation can reuse their in-flight responses (checked via
|
|
98
|
+
* consumeInflightPrefetch in the prefetch cache). With MAX_CONCURRENT=2
|
|
99
|
+
* and priority: "low", in-flight prefetches don't meaningfully compete
|
|
100
|
+
* with navigation fetches under HTTP/2 multiplexing.
|
|
101
|
+
*
|
|
102
|
+
* Called when a navigation starts via the NavigationProvider's
|
|
103
|
+
* event controller subscription.
|
|
76
104
|
*/
|
|
77
105
|
export function cancelAllPrefetches(): void {
|
|
106
|
+
queue.length = 0;
|
|
107
|
+
queued.clear();
|
|
108
|
+
drainScheduled = false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Hard-cancel everything including in-flight prefetches.
|
|
113
|
+
* Used by clearPrefetchCache (server action invalidation) where
|
|
114
|
+
* in-flight responses would be stale.
|
|
115
|
+
*/
|
|
116
|
+
export function abortAllPrefetches(): void {
|
|
78
117
|
abortController?.abort();
|
|
79
118
|
abortController = null;
|
|
80
119
|
|
|
@@ -85,4 +124,5 @@ export function cancelAllPrefetches(): void {
|
|
|
85
124
|
// so active settles at 0 without underflow.
|
|
86
125
|
executing.clear();
|
|
87
126
|
active = 0;
|
|
127
|
+
drainScheduled = false;
|
|
88
128
|
}
|
|
@@ -279,7 +279,15 @@ export const Link: ForwardRefExoticComponent<
|
|
|
279
279
|
);
|
|
280
280
|
|
|
281
281
|
const handleMouseEnter = useCallback(() => {
|
|
282
|
-
if (
|
|
282
|
+
if (
|
|
283
|
+
(resolvedStrategy === "hover" || resolvedStrategy === "viewport") &&
|
|
284
|
+
!isExternal &&
|
|
285
|
+
ctx?.store
|
|
286
|
+
) {
|
|
287
|
+
// For "hover", this is the primary prefetch trigger.
|
|
288
|
+
// For "viewport", this upgrades/prioritizes a potentially queued
|
|
289
|
+
// prefetch — prefetchDirect bypasses the queue, and hasPrefetch
|
|
290
|
+
// deduplicates if the viewport prefetch already completed.
|
|
283
291
|
const segmentState = ctx.store.getSegmentState();
|
|
284
292
|
prefetchDirect(to, segmentState.currentSegmentIds, ctx.version);
|
|
285
293
|
}
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import React, {
|
|
4
4
|
useState,
|
|
5
5
|
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
6
7
|
useCallback,
|
|
7
8
|
useMemo,
|
|
9
|
+
useRef,
|
|
8
10
|
use,
|
|
9
11
|
type ReactNode,
|
|
10
12
|
} from "react";
|
|
@@ -25,6 +27,7 @@ import { ThemeProvider } from "../../theme/ThemeProvider.js";
|
|
|
25
27
|
import { NonceContext } from "./nonce-context.js";
|
|
26
28
|
import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
|
|
27
29
|
import { cancelAllPrefetches } from "../prefetch/queue.js";
|
|
30
|
+
import { handleNavigationEnd } from "../scroll-restoration.js";
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
33
|
* Process handles from an async generator, updating the event controller
|
|
@@ -301,9 +304,33 @@ export function NavigationProvider({
|
|
|
301
304
|
return unsub;
|
|
302
305
|
}, [eventController]);
|
|
303
306
|
|
|
307
|
+
// Pending scroll action to apply after React commits
|
|
308
|
+
const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
|
|
309
|
+
|
|
310
|
+
// Apply scroll after React commits the new content to the DOM
|
|
311
|
+
useLayoutEffect(() => {
|
|
312
|
+
const scrollAction = pendingScrollRef.current;
|
|
313
|
+
if (!scrollAction) return;
|
|
314
|
+
pendingScrollRef.current = undefined;
|
|
315
|
+
|
|
316
|
+
if (scrollAction.enabled === false) return;
|
|
317
|
+
|
|
318
|
+
handleNavigationEnd({
|
|
319
|
+
restore: scrollAction.restore,
|
|
320
|
+
scroll: scrollAction.enabled,
|
|
321
|
+
isStreaming: scrollAction.isStreaming,
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
304
325
|
// Subscribe to UI updates (for re-rendering the tree)
|
|
305
326
|
useEffect(() => {
|
|
306
327
|
const unsubscribe = store.onUpdate((update) => {
|
|
328
|
+
// Capture scroll intent — it will be applied in useLayoutEffect
|
|
329
|
+
// after React commits this state update to the DOM.
|
|
330
|
+
// Always assign (even undefined) to clear stale scroll from prior navigations,
|
|
331
|
+
// so server actions or error updates don't accidentally replay old scroll.
|
|
332
|
+
pendingScrollRef.current = update.scroll;
|
|
333
|
+
|
|
307
334
|
setPayload({
|
|
308
335
|
root: update.root,
|
|
309
336
|
metadata: update.metadata,
|
|
@@ -263,71 +263,123 @@ export async function initBrowserApp(
|
|
|
263
263
|
// Build initial tree with rootLayout
|
|
264
264
|
const initialTree = renderSegments(initialPayload.metadata!.segments);
|
|
265
265
|
|
|
266
|
-
// Setup HMR
|
|
266
|
+
// Setup HMR with debounce — burst saves (format-on-save, rapid edits)
|
|
267
|
+
// fire many rsc:update events in quick succession. Without debouncing,
|
|
268
|
+
// each event triggers a fetchPartial() which on slow routes can pile up
|
|
269
|
+
// and overwhelm the worker (cross-request promise issues, 500s).
|
|
267
270
|
if (import.meta.hot) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const handle = eventController.startNavigation(window.location.href, {
|
|
272
|
-
replace: true,
|
|
273
|
-
});
|
|
274
|
-
const streamingToken = handle.startStreaming();
|
|
275
|
-
|
|
276
|
-
const interceptSourceUrl = store.getInterceptSourceUrl();
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
const { payload, streamComplete } = await client.fetchPartial({
|
|
280
|
-
targetUrl: window.location.href,
|
|
281
|
-
segmentIds: [],
|
|
282
|
-
previousUrl: store.getSegmentState().currentUrl,
|
|
283
|
-
interceptSourceUrl: interceptSourceUrl || undefined,
|
|
284
|
-
hmr: true,
|
|
285
|
-
});
|
|
271
|
+
let hmrTimer: ReturnType<typeof setTimeout> | null = null;
|
|
272
|
+
let hmrAbort: AbortController | null = null;
|
|
286
273
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
274
|
+
import.meta.hot.on("rsc:update", () => {
|
|
275
|
+
// Cancel any pending debounce timer
|
|
276
|
+
if (hmrTimer !== null) {
|
|
277
|
+
clearTimeout(hmrTimer);
|
|
278
|
+
}
|
|
290
279
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
280
|
+
// Abort any in-flight HMR fetch so it doesn't race with the next one
|
|
281
|
+
if (hmrAbort) {
|
|
282
|
+
hmrAbort.abort();
|
|
283
|
+
hmrAbort = null;
|
|
284
|
+
}
|
|
295
285
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
286
|
+
// Debounce: wait 200ms of quiet before fetching
|
|
287
|
+
hmrTimer = setTimeout(async () => {
|
|
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
300
|
|
|
301
|
-
|
|
302
|
-
store.setCurrentUrl(window.location.href);
|
|
301
|
+
console.log("[RSCRouter] HMR: Server update, refetching RSC");
|
|
303
302
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
303
|
+
const abort = new AbortController();
|
|
304
|
+
hmrAbort = abort;
|
|
305
|
+
|
|
306
|
+
const handle = eventController.startNavigation(window.location.href, {
|
|
307
|
+
replace: true,
|
|
308
|
+
});
|
|
309
|
+
const streamingToken = handle.startStreaming();
|
|
310
|
+
|
|
311
|
+
const interceptSourceUrl = store.getInterceptSourceUrl();
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const { payload, streamComplete } = await client.fetchPartial({
|
|
315
|
+
targetUrl: window.location.href,
|
|
316
|
+
segmentIds: [],
|
|
317
|
+
previousUrl: store.getSegmentState().currentUrl,
|
|
318
|
+
interceptSourceUrl: interceptSourceUrl || undefined,
|
|
319
|
+
hmr: true,
|
|
320
|
+
signal: abort.signal,
|
|
321
321
|
});
|
|
322
|
-
}
|
|
323
322
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
323
|
+
if (abort.signal.aborted) return;
|
|
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
|
+
|
|
332
|
+
if (payload.metadata?.isPartial) {
|
|
333
|
+
const segments = payload.metadata.segments || [];
|
|
334
|
+
const matched = payload.metadata.matched || [];
|
|
335
|
+
|
|
336
|
+
// Derive intercept state from the returned payload, not the
|
|
337
|
+
// pre-fetch store snapshot. If the HMR edit removed intercept
|
|
338
|
+
// behavior, the response won't contain intercept segments.
|
|
339
|
+
const responseIsIntercept = segments.some(isInterceptSegment);
|
|
340
|
+
|
|
341
|
+
// Sync store intercept state with what the server returned
|
|
342
|
+
if (!responseIsIntercept && interceptSourceUrl) {
|
|
343
|
+
store.setInterceptSourceUrl(null);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
store.setSegmentIds(matched);
|
|
347
|
+
store.setCurrentUrl(window.location.href);
|
|
348
|
+
|
|
349
|
+
const historyKey = generateHistoryKey(window.location.href, {
|
|
350
|
+
intercept: responseIsIntercept,
|
|
351
|
+
});
|
|
352
|
+
store.setHistoryKey(historyKey);
|
|
353
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
354
|
+
store.cacheSegmentsForHistory(
|
|
355
|
+
historyKey,
|
|
356
|
+
segments,
|
|
357
|
+
currentHandleData,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const { main, intercept } = splitInterceptSegments(segments);
|
|
361
|
+
store.emitUpdate({
|
|
362
|
+
root: renderSegments(main, {
|
|
363
|
+
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
364
|
+
}),
|
|
365
|
+
metadata: payload.metadata,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
await streamComplete;
|
|
370
|
+
handle.complete(new URL(window.location.href));
|
|
371
|
+
console.log("[RSCRouter] HMR: RSC stream complete");
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (abort.signal.aborted) return;
|
|
374
|
+
console.warn("[RSCRouter] HMR: Refetch failed, reloading page", err);
|
|
375
|
+
window.location.reload();
|
|
376
|
+
return;
|
|
377
|
+
} finally {
|
|
378
|
+
if (hmrAbort === abort) hmrAbort = null;
|
|
379
|
+
streamingToken.end();
|
|
380
|
+
handle[Symbol.dispose]();
|
|
381
|
+
}
|
|
382
|
+
}, 200);
|
|
331
383
|
});
|
|
332
384
|
}
|
|
333
385
|
|
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
import { debugLog } from "./logging.js";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Defers a callback to the next animation frame.
|
|
15
|
+
* Falls back to setTimeout(0) in environments without requestAnimationFrame.
|
|
16
|
+
*/
|
|
17
|
+
const deferToNextPaint: (fn: () => void) => void =
|
|
18
|
+
typeof requestAnimationFrame === "function"
|
|
19
|
+
? requestAnimationFrame
|
|
20
|
+
: (fn) => setTimeout(fn, 0);
|
|
21
|
+
|
|
13
22
|
const SCROLL_STORAGE_KEY = "rsc-router-scroll-positions";
|
|
14
23
|
|
|
15
24
|
/**
|
|
@@ -288,7 +297,7 @@ export function restoreScrollPosition(options?: {
|
|
|
288
297
|
// Not streaming — scroll after React commits and browser paints.
|
|
289
298
|
// startTransition defers the DOM commit, so scrolling synchronously
|
|
290
299
|
// would be overwritten when React replaces the content.
|
|
291
|
-
|
|
300
|
+
deferToNextPaint(() => {
|
|
292
301
|
window.scrollTo(0, savedY);
|
|
293
302
|
debugLog("[Scroll] Restored position:", savedY, "for key:", key);
|
|
294
303
|
});
|
|
@@ -366,13 +375,17 @@ export function handleNavigationEnd(options: {
|
|
|
366
375
|
// Fall through to hash or top if no saved position
|
|
367
376
|
}
|
|
368
377
|
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
378
|
+
// Defer hash and scroll-to-top to after React paints the new content,
|
|
379
|
+
// so the user doesn't see the current page jump before the new route appears.
|
|
380
|
+
deferToNextPaint(() => {
|
|
381
|
+
// Try hash scrolling first
|
|
382
|
+
if (scrollToHash()) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
373
385
|
|
|
374
|
-
|
|
375
|
-
|
|
386
|
+
// Default: scroll to top
|
|
387
|
+
scrollToTop();
|
|
388
|
+
});
|
|
376
389
|
}
|
|
377
390
|
|
|
378
391
|
/**
|
|
@@ -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
|
}
|
package/src/browser/types.ts
CHANGED
|
@@ -215,6 +215,15 @@ export interface SegmentState {
|
|
|
215
215
|
export interface NavigationUpdate {
|
|
216
216
|
root: ReactNode | Promise<ReactNode>;
|
|
217
217
|
metadata: RscMetadata;
|
|
218
|
+
/** Scroll behavior to apply after React commits this update */
|
|
219
|
+
scroll?: {
|
|
220
|
+
/** For back/forward: restore saved position */
|
|
221
|
+
restore?: boolean;
|
|
222
|
+
/** Set to false to disable scrolling entirely */
|
|
223
|
+
enabled?: boolean;
|
|
224
|
+
/** Function to check if streaming is in progress */
|
|
225
|
+
isStreaming?: () => boolean;
|
|
226
|
+
};
|
|
218
227
|
}
|
|
219
228
|
|
|
220
229
|
/**
|
|
@@ -45,7 +45,9 @@ function isRoutableSourceFile(name: string): boolean {
|
|
|
45
45
|
name.endsWith(".tsx") ||
|
|
46
46
|
name.endsWith(".js") ||
|
|
47
47
|
name.endsWith(".jsx")) &&
|
|
48
|
-
!name.includes(".gen.")
|
|
48
|
+
!name.includes(".gen.") &&
|
|
49
|
+
!name.includes(".test.") &&
|
|
50
|
+
!name.includes(".spec.")
|
|
49
51
|
);
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -70,7 +72,15 @@ function findRouterFilesRecursive(
|
|
|
70
72
|
for (const entry of entries) {
|
|
71
73
|
const fullPath = join(dir, entry.name);
|
|
72
74
|
if (entry.isDirectory()) {
|
|
73
|
-
if (
|
|
75
|
+
if (
|
|
76
|
+
entry.name === "node_modules" ||
|
|
77
|
+
entry.name === "dist" ||
|
|
78
|
+
entry.name === "coverage" ||
|
|
79
|
+
entry.name === "__tests__" ||
|
|
80
|
+
entry.name === "__mocks__" ||
|
|
81
|
+
entry.name.startsWith(".")
|
|
82
|
+
)
|
|
83
|
+
continue;
|
|
74
84
|
childDirs.push(fullPath);
|
|
75
85
|
continue;
|
|
76
86
|
}
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -73,7 +73,7 @@ function getDefaultRouteCacheKey(
|
|
|
73
73
|
isIntercept?: boolean,
|
|
74
74
|
): string {
|
|
75
75
|
const ctx = getRequestContext();
|
|
76
|
-
const isPartial = ctx?.
|
|
76
|
+
const isPartial = ctx?.originalUrl?.searchParams.has("_rsc_partial") ?? false;
|
|
77
77
|
const searchParams = ctx?.url.searchParams;
|
|
78
78
|
const host = ctx?.url.host ?? "localhost";
|
|
79
79
|
|
|
@@ -326,7 +326,7 @@ export class CacheScope {
|
|
|
326
326
|
const key = await this.resolveKey(pathname, params, isIntercept);
|
|
327
327
|
|
|
328
328
|
// Check if this is a partial request (navigation) vs document request
|
|
329
|
-
const isPartial = requestCtx.
|
|
329
|
+
const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
|
|
330
330
|
|
|
331
331
|
requestCtx.waitUntil(async () => {
|
|
332
332
|
await handleStore.settled;
|