@rangojs/router 0.21.0 → 0.22.0
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 +13 -1
- package/dist/types/browser/event-controller.d.ts +30 -13
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/partial-update.d.ts +2 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +14 -0
- package/dist/types/browser/rsc-router.d.ts +13 -2
- package/dist/types/browser/types.d.ts +10 -0
- package/dist/types/build/generate-manifest.d.ts +2 -0
- package/dist/types/cache/background-task.d.ts +22 -0
- package/dist/types/cache/cache-key-utils.d.ts +50 -5
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +1 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +23 -0
- package/dist/types/cache/index.d.ts +1 -1
- package/dist/types/cache/invalidation-order.d.ts +14 -0
- package/dist/types/cache/memory-segment-store.d.ts +33 -1
- package/dist/types/cache/shell-snapshot.d.ts +10 -1
- package/dist/types/cache/store-scope.d.ts +22 -0
- package/dist/types/cache/tag-invalidation.d.ts +7 -6
- package/dist/types/cache/types.d.ts +63 -14
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +7 -0
- package/dist/types/internal-suspense-audit.d.ts +1 -0
- package/dist/types/loader-store.d.ts +4 -8
- package/dist/types/outlet-provider.d.ts +2 -1
- package/dist/types/prerender/cloudflare.d.ts +49 -0
- package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
- package/dist/types/prerender/index.d.ts +12 -0
- package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
- package/dist/types/prerender/on-demand.d.ts +272 -0
- package/dist/types/prerender/producer-guard.d.ts +33 -0
- package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
- package/dist/types/prerender/warm-request.d.ts +65 -0
- package/dist/types/prerender/warm.d.ts +65 -0
- package/dist/types/prerender/writable-store.d.ts +170 -0
- package/dist/types/prerender.d.ts +27 -0
- package/dist/types/reverse.d.ts +1 -1
- package/dist/types/route-content-wrapper.d.ts +3 -1
- package/dist/types/router/error-handling.d.ts +2 -0
- package/dist/types/router/handler-context.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
- package/dist/types/router/pattern-matching.d.ts +2 -0
- package/dist/types/router/prefetch-default.d.ts +4 -4
- package/dist/types/router/prerender-match.d.ts +23 -1
- package/dist/types/router/route-trie-builder.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +1 -0
- package/dist/types/router/router-interfaces.d.ts +30 -0
- package/dist/types/router/router-options.d.ts +24 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
- package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
- package/dist/types/router/trie-matching.d.ts +2 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
- package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
- package/dist/types/rsc/shell-capture.d.ts +14 -1
- package/dist/types/rsc/shell-serve.d.ts +2 -2
- package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
- package/dist/types/rsc/types.d.ts +8 -0
- package/dist/types/segment-boundary-content.d.ts +17 -0
- package/dist/types/segment-loader-promise.d.ts +2 -6
- package/dist/types/server/build-version-table.d.ts +9 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +63 -2
- package/dist/types/suspense-audit-react.d.ts +26 -0
- package/dist/types/suspense-audit.d.ts +157 -0
- package/dist/types/testing/action-form.d.ts +34 -0
- package/dist/types/testing/flight.entry.d.ts +2 -0
- package/dist/types/testing/index.d.ts +4 -0
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/serve-shell-request.d.ts +34 -4
- package/dist/types/testing/shell-status.d.ts +14 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +7 -0
- package/dist/types/types/route-entry.d.ts +5 -0
- package/dist/types/urls/path-helper.d.ts +18 -0
- package/dist/types/vite/discovery/state.d.ts +11 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/vite/index.js +179 -496
- package/package.json +12 -2
- package/skills/cache-guide/SKILL.md +18 -17
- package/skills/caching/SKILL.md +40 -4
- package/skills/catalog.json +1 -1
- package/skills/cloudflare/SKILL.md +131 -0
- package/skills/comparison/references/framework-comparison.md +1 -1
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +22 -0
- package/skills/hooks/handle-and-actions.md +3 -4
- package/skills/hooks/navigation.md +10 -0
- package/skills/hooks/state.md +5 -4
- package/skills/host-router/SKILL.md +18 -0
- package/skills/loader/SKILL.md +6 -4
- package/skills/middleware/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +31 -8
- package/skills/prerender/SKILL.md +645 -8
- package/skills/rango/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/serve-shell-request.md +104 -8
- package/skills/use-cache/SKILL.md +9 -5
- package/skills/vercel/SKILL.md +2 -0
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/browser/event-controller.ts +69 -25
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +75 -35
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +9 -2
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +48 -12
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/react/Link.tsx +37 -42
- package/src/browser/react/NavigationProvider.tsx +63 -3
- package/src/browser/react/context.ts +17 -0
- package/src/browser/react/location-state.ts +5 -19
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +4 -20
- package/src/browser/react/use-link-status.ts +4 -3
- package/src/browser/react/use-navigation.ts +4 -1
- package/src/browser/react/use-params.ts +4 -1
- package/src/browser/react/use-pathname.ts +4 -1
- package/src/browser/react/use-search-params.ts +5 -3
- package/src/browser/react/use-segments.ts +36 -54
- package/src/browser/rsc-router.tsx +79 -39
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +26 -6
- package/src/browser/types.ts +11 -0
- package/src/build/generate-manifest.ts +17 -0
- package/src/build/merge-full-manifests.ts +5 -0
- package/src/build/route-trie.ts +1 -0
- package/src/cache/background-task.ts +61 -0
- package/src/cache/cache-key-utils.ts +91 -12
- package/src/cache/cache-policy.ts +11 -2
- package/src/cache/cache-runtime.ts +23 -23
- package/src/cache/cache-scope.ts +38 -36
- package/src/cache/cf/cf-cache-store.ts +16 -11
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +50 -0
- package/src/cache/index.ts +1 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +17 -0
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +69 -14
- package/src/cache/vercel/vercel-cache-store.ts +20 -6
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +7 -2
- package/src/internal-suspense-audit.ts +12 -0
- package/src/loader-store.ts +4 -9
- package/src/outlet-provider.tsx +25 -1
- package/src/prerender/cloudflare.ts +130 -0
- package/src/prerender/create-prerender-trigger.ts +780 -0
- package/src/prerender/index.ts +39 -0
- package/src/prerender/memory-prerender-store.ts +115 -0
- package/src/prerender/on-demand.ts +339 -0
- package/src/prerender/producer-guard.ts +66 -0
- package/src/prerender/shell-manifest-key.ts +12 -17
- package/src/prerender/warm-request.ts +154 -0
- package/src/prerender/warm.ts +193 -0
- package/src/prerender/writable-store.ts +278 -0
- package/src/prerender.ts +29 -0
- package/src/reverse.ts +1 -1
- package/src/route-content-wrapper.tsx +107 -13
- package/src/router/error-handling.ts +12 -12
- package/src/router/find-match.ts +1 -0
- package/src/router/handler-context.ts +2 -0
- package/src/router/intercept-resolution.ts +15 -8
- package/src/router/loader-resolution.ts +2 -10
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +230 -38
- package/src/router/pattern-matching.ts +14 -0
- package/src/router/prefetch-default.ts +4 -4
- package/src/router/prerender-match.ts +460 -352
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +33 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +15 -17
- package/src/router/segment-resolution/revalidation.ts +34 -24
- package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
- package/src/router/trie-matching.ts +3 -0
- package/src/router.ts +107 -1
- package/src/rsc/handler.ts +51 -4
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +53 -11
- package/src/rsc/shell-build-manifest.ts +10 -8
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +108 -19
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +8 -0
- package/src/segment-boundary-content.ts +25 -0
- package/src/segment-loader-promise.ts +28 -38
- package/src/segment-system.tsx +117 -57
- package/src/server/build-version-table.ts +15 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +13 -0
- package/src/server/request-context.ts +87 -1
- package/src/ssr/ssr-root.tsx +20 -13
- package/src/suspense-audit-react.tsx +98 -0
- package/src/suspense-audit.ts +1030 -0
- package/src/testing/action-form.ts +66 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +3 -0
- package/src/testing/index.ts +19 -0
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/render-route.tsx +25 -12
- package/src/testing/serve-shell-request.ts +190 -34
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +7 -0
- package/src/types/route-entry.ts +6 -0
- package/src/urls/path-helper.ts +69 -9
- package/src/use-loader.tsx +57 -30
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/shell-prerender-phase.ts +5 -5
- package/src/vite/discovery/state.ts +17 -1
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugins/virtual-entries.ts +8 -1
- package/src/vite/router-discovery.ts +14 -0
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
- package/dist/types/segment-content-promise.d.ts +0 -13
- package/src/browser/react/subscribe-to-nothing.ts +0 -7
- package/src/segment-content-promise.ts +0 -67
|
@@ -35,7 +35,10 @@ import {
|
|
|
35
35
|
shouldStartViewTransition,
|
|
36
36
|
} from "./partial-update.js";
|
|
37
37
|
import { decideCommitGatedOff, mergeSegmentParams } from "./transition-when.js";
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
generateHistoryKey,
|
|
40
|
+
liveHandleDataForCache,
|
|
41
|
+
} from "./navigation-store.js";
|
|
39
42
|
import type { EventController } from "./event-controller.js";
|
|
40
43
|
import { isInterceptOnlyCache } from "./intercept-utils.js";
|
|
41
44
|
import {
|
|
@@ -54,6 +57,7 @@ import {
|
|
|
54
57
|
} from "./prefetch/loader.js";
|
|
55
58
|
import type { PrefetchStrategy } from "../router/prefetch-default.js";
|
|
56
59
|
import { beginClientUrlNavigation } from "../client-urls/navigation.js";
|
|
60
|
+
import { auditTreeCause } from "../suspense-audit.js";
|
|
57
61
|
|
|
58
62
|
// Polyfill Symbol.dispose for Safari and older browsers
|
|
59
63
|
if (typeof Symbol.dispose === "undefined") {
|
|
@@ -62,6 +66,9 @@ if (typeof Symbol.dispose === "undefined") {
|
|
|
62
66
|
|
|
63
67
|
export { createNavigationTransaction };
|
|
64
68
|
|
|
69
|
+
// Mirrors the redirect-chain limit browsers apply to document navigations.
|
|
70
|
+
const MAX_REDIRECT_HOPS = 20;
|
|
71
|
+
|
|
65
72
|
/**
|
|
66
73
|
* Extended configuration for navigation bridge with event controller
|
|
67
74
|
*/
|
|
@@ -149,6 +156,45 @@ export function createNavigationBridge(
|
|
|
149
156
|
getVersion: () => version,
|
|
150
157
|
});
|
|
151
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Re-navigates to a server redirect target, counting hops so a target that
|
|
161
|
+
* redirects back ends in the error boundary instead of looping.
|
|
162
|
+
*/
|
|
163
|
+
async function followServerRedirect(
|
|
164
|
+
self: {
|
|
165
|
+
navigate: (
|
|
166
|
+
url: string,
|
|
167
|
+
options?: NavigateOptionsInternal,
|
|
168
|
+
) => Promise<void>;
|
|
169
|
+
},
|
|
170
|
+
error: ServerRedirect,
|
|
171
|
+
url: string,
|
|
172
|
+
hops: number,
|
|
173
|
+
options: { replace?: boolean; transition?: boolean },
|
|
174
|
+
): Promise<void> {
|
|
175
|
+
const redirectUrl = validateRedirectOrigin(
|
|
176
|
+
error.url,
|
|
177
|
+
window.location.origin,
|
|
178
|
+
);
|
|
179
|
+
if (!redirectUrl) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (hops >= MAX_REDIRECT_HOPS) {
|
|
183
|
+
const loopError = new Error(
|
|
184
|
+
`[rango] Server redirect loop: stopped after following ${hops} redirects, at ${url}`,
|
|
185
|
+
);
|
|
186
|
+
console.error(loopError.message);
|
|
187
|
+
emitNavigationError(onUpdate, loopError, url);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
return self.navigate(redirectUrl, {
|
|
191
|
+
state: error.state,
|
|
192
|
+
...options,
|
|
193
|
+
_skipCache: true,
|
|
194
|
+
_redirectHops: hops + 1,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
152
198
|
return {
|
|
153
199
|
/**
|
|
154
200
|
* Navigate to a URL
|
|
@@ -205,7 +251,11 @@ export function createNavigationBridge(
|
|
|
205
251
|
const currentKey = store.getHistoryKey();
|
|
206
252
|
const currentCache = store.getCachedSegments(currentKey);
|
|
207
253
|
if (currentCache?.segments) {
|
|
208
|
-
const currentHandleData =
|
|
254
|
+
const currentHandleData = liveHandleDataForCache(
|
|
255
|
+
eventController,
|
|
256
|
+
store,
|
|
257
|
+
currentKey,
|
|
258
|
+
);
|
|
209
259
|
store.cacheSegmentsForHistory(
|
|
210
260
|
historyKey,
|
|
211
261
|
currentCache.segments,
|
|
@@ -283,7 +333,11 @@ export function createNavigationBridge(
|
|
|
283
333
|
const sourceHistoryKey = store.getHistoryKey();
|
|
284
334
|
const sourceCached = store.getCachedSegments(sourceHistoryKey);
|
|
285
335
|
if (sourceCached?.segments && sourceCached.segments.length > 0) {
|
|
286
|
-
const currentHandleData =
|
|
336
|
+
const currentHandleData = liveHandleDataForCache(
|
|
337
|
+
eventController,
|
|
338
|
+
store,
|
|
339
|
+
sourceHistoryKey,
|
|
340
|
+
);
|
|
287
341
|
store.updateCacheHandleData(sourceHistoryKey, currentHandleData);
|
|
288
342
|
}
|
|
289
343
|
|
|
@@ -413,29 +467,12 @@ export function createNavigationBridge(
|
|
|
413
467
|
},
|
|
414
468
|
);
|
|
415
469
|
} catch (error) {
|
|
416
|
-
// Server-side redirect with location state: the current transaction's
|
|
417
|
-
// cleanup resets loading state. Re-navigate to the redirect
|
|
418
|
-
// target carrying the server-set state into history.pushState.
|
|
419
|
-
if (error instanceof ServerRedirect) {
|
|
420
|
-
const redirectUrl = validateRedirectOrigin(
|
|
421
|
-
error.url,
|
|
422
|
-
window.location.origin,
|
|
423
|
-
);
|
|
424
|
-
if (!redirectUrl) {
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
return this.navigate(redirectUrl, {
|
|
428
|
-
state: error.state,
|
|
429
|
-
replace: options?.replace,
|
|
430
|
-
transition: options?.transition,
|
|
431
|
-
_skipCache: true,
|
|
432
|
-
} as NavigateOptionsInternal);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
470
|
// Aborted, or superseded by a newer navigation. A superseded nav may
|
|
436
471
|
// reject with a non-AbortError (e.g. a Flight decode that fails after its
|
|
437
472
|
// signal was aborted), so check the signal too -- otherwise we would
|
|
438
|
-
// render a boundary that clobbers the newer navigation's content.
|
|
473
|
+
// render a boundary that clobbers the newer navigation's content. Before
|
|
474
|
+
// the redirect branch: a superseded nav must not follow its redirect
|
|
475
|
+
// over the navigation that replaced it.
|
|
439
476
|
if (
|
|
440
477
|
(error instanceof DOMException && error.name === "AbortError") ||
|
|
441
478
|
tx.handle.signal.aborted
|
|
@@ -444,6 +481,19 @@ export function createNavigationBridge(
|
|
|
444
481
|
return;
|
|
445
482
|
}
|
|
446
483
|
|
|
484
|
+
// Server-side redirect with location state: the current transaction's
|
|
485
|
+
// cleanup resets loading state. Re-navigate to the redirect
|
|
486
|
+
// target carrying the server-set state into history.pushState.
|
|
487
|
+
if (error instanceof ServerRedirect) {
|
|
488
|
+
return followServerRedirect(
|
|
489
|
+
this,
|
|
490
|
+
error,
|
|
491
|
+
url,
|
|
492
|
+
options?._redirectHops ?? 0,
|
|
493
|
+
{ replace: options?.replace, transition: options?.transition },
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
447
497
|
const networkError = toNetworkError(error, {
|
|
448
498
|
url,
|
|
449
499
|
operation: "navigation",
|
|
@@ -670,6 +720,7 @@ export function createNavigationBridge(
|
|
|
670
720
|
!isIntercept &&
|
|
671
721
|
!isLeavingIntercept &&
|
|
672
722
|
shouldStartViewTransition(cachedSegments, gatedOff);
|
|
723
|
+
if (process.env.NODE_ENV !== "production") auditTreeCause("popstate");
|
|
673
724
|
if (hasTransition) {
|
|
674
725
|
startTransition(() => {
|
|
675
726
|
if (addTransitionType) {
|
|
@@ -786,18 +837,7 @@ export function createNavigationBridge(
|
|
|
786
837
|
// and Back would hit the redirect again. Before setLocation below, or
|
|
787
838
|
// the hooks would report the redirecting URL first.
|
|
788
839
|
if (error instanceof ServerRedirect) {
|
|
789
|
-
|
|
790
|
-
error.url,
|
|
791
|
-
window.location.origin,
|
|
792
|
-
);
|
|
793
|
-
if (!redirectUrl) {
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
return this.navigate(redirectUrl, {
|
|
797
|
-
state: error.state,
|
|
798
|
-
replace: true,
|
|
799
|
-
_skipCache: true,
|
|
800
|
-
} as NavigateOptionsInternal);
|
|
840
|
+
return followServerRedirect(this, error, url, 0, { replace: true });
|
|
801
841
|
}
|
|
802
842
|
|
|
803
843
|
// tx.commit() never ran, so the location is still the page being
|
|
@@ -8,7 +8,9 @@ import type {
|
|
|
8
8
|
ResolvedSegment,
|
|
9
9
|
HandleData,
|
|
10
10
|
} from "./types.js";
|
|
11
|
+
import type { EventController } from "./event-controller.js";
|
|
11
12
|
import { clearPrefetchCache } from "./prefetch/cache.js";
|
|
13
|
+
import { LoaderStore } from "../loader-store.js";
|
|
12
14
|
import {
|
|
13
15
|
adoptRangoState,
|
|
14
16
|
getRangoState,
|
|
@@ -73,6 +75,28 @@ export function cloneHandleData(handleData: HandleData): HandleData {
|
|
|
73
75
|
return cloned;
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Handle data to write into the history cache for the page on screen. During
|
|
80
|
+
* the hydration window getHandleState() is the document snapshot while late
|
|
81
|
+
* pushes sit queued in the controller (they are already in the cache entry,
|
|
82
|
+
* written by rsc-router), so writing the snapshot back would drop them; the
|
|
83
|
+
* entry's own data is the live view then.
|
|
84
|
+
*/
|
|
85
|
+
export function liveHandleDataForCache(
|
|
86
|
+
eventController: Pick<
|
|
87
|
+
EventController,
|
|
88
|
+
"getHandleState" | "getHydrationSnapshot"
|
|
89
|
+
>,
|
|
90
|
+
store: Pick<NavigationStore, "getCachedSegments">,
|
|
91
|
+
historyKey: string,
|
|
92
|
+
): HandleData {
|
|
93
|
+
if (eventController.getHydrationSnapshot()) {
|
|
94
|
+
const cached = store.getCachedSegments(historyKey)?.handleData;
|
|
95
|
+
if (cached) return cached;
|
|
96
|
+
}
|
|
97
|
+
return eventController.getHandleState().data;
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
// BroadcastChannel for cross-tab cache invalidation
|
|
77
101
|
const CACHE_INVALIDATION_CHANNEL = "rsc-router-cache-invalidation";
|
|
78
102
|
|
|
@@ -390,6 +414,8 @@ export function createNavigationStore(
|
|
|
390
414
|
}
|
|
391
415
|
|
|
392
416
|
const store: NavigationStore = {
|
|
417
|
+
loaders: new LoaderStore(),
|
|
418
|
+
|
|
393
419
|
// ========================================================================
|
|
394
420
|
// Internal Segment State (for bridges)
|
|
395
421
|
// ========================================================================
|
|
@@ -4,7 +4,10 @@ import type {
|
|
|
4
4
|
ResolvedSegment,
|
|
5
5
|
StreamingToken,
|
|
6
6
|
} from "./types.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
generateHistoryKey,
|
|
9
|
+
liveHandleDataForCache,
|
|
10
|
+
} from "./navigation-store.js";
|
|
8
11
|
import {
|
|
9
12
|
handleNavigationStart,
|
|
10
13
|
ensureHistoryKey,
|
|
@@ -180,7 +183,11 @@ export function createNavigationTransaction(
|
|
|
180
183
|
|
|
181
184
|
store.setHistoryKey(historyKey);
|
|
182
185
|
|
|
183
|
-
|
|
186
|
+
// storeOnly re-caches the page on screen (an action refetch), which can
|
|
187
|
+
// commit with no tree and so never release the hydration window.
|
|
188
|
+
const currentHandleData = storeOnly
|
|
189
|
+
? liveHandleDataForCache(eventController, store, historyKey)
|
|
190
|
+
: eventController.getHandleState().data;
|
|
184
191
|
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
185
192
|
|
|
186
193
|
if (storeOnly) {
|
|
@@ -2,6 +2,7 @@ import { NetworkError, isNetworkError } from "../errors.js";
|
|
|
2
2
|
import { RenderErrorThrower } from "../render-error-thrower.js";
|
|
3
3
|
import type { UpdateSubscriber } from "./types.js";
|
|
4
4
|
import { createElement, startTransition } from "react";
|
|
5
|
+
import { auditTreeCause } from "../suspense-audit.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Convert an unknown error to a NetworkError, or return null if not network-related.
|
|
@@ -33,6 +34,7 @@ function emitErrorToBoundary(
|
|
|
33
34
|
error: unknown,
|
|
34
35
|
pathname: string,
|
|
35
36
|
): void {
|
|
37
|
+
if (process.env.NODE_ENV !== "production") auditTreeCause("error");
|
|
36
38
|
startTransition(() => {
|
|
37
39
|
onUpdate({
|
|
38
40
|
root: createElement(RenderErrorThrower, { error }),
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
} from "./validate-redirect-origin.js";
|
|
32
32
|
import type { NavigationUpdate } from "./types.js";
|
|
33
33
|
import { OPTIMISTIC_COMMIT_TRANSITION_TYPE } from "./optimistic-commit.js";
|
|
34
|
-
import {
|
|
34
|
+
import type { LoaderStore } from "../loader-store.js";
|
|
35
35
|
import {
|
|
36
36
|
collectClientRevalidationDecisions,
|
|
37
37
|
setActiveInterceptTargets,
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
import { buildHistoryState } from "./history-state.js";
|
|
45
45
|
import { addLocationState } from "./react/location-state-shared.js";
|
|
46
46
|
import type { TransitionWhenKind } from "../types/segments.js";
|
|
47
|
+
import { auditTreeCause, type TreeUpdateCause } from "../suspense-audit.js";
|
|
47
48
|
|
|
48
49
|
function toScrollPayload(
|
|
49
50
|
scroll: boolean | undefined,
|
|
@@ -70,6 +71,13 @@ export function shouldStartViewTransition(
|
|
|
70
71
|
return !hasIntercept && hasTransition;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
// Dev only (suspense-audit.ts I6): what this updater's commit is.
|
|
75
|
+
function treeCause(mode: UpdateMode, traversal?: boolean): TreeUpdateCause {
|
|
76
|
+
if (mode.type === "action") return "action";
|
|
77
|
+
if (mode.type === "stale-revalidation") return "stale-revalidation";
|
|
78
|
+
return traversal ? "popstate" : "navigation";
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
/**
|
|
74
82
|
* Transition commit. Every held commit announces the loader streams the
|
|
75
83
|
* committed tree is still receiving (loader-store.ts announcePendingStreams)
|
|
@@ -79,13 +87,14 @@ export function shouldStartViewTransition(
|
|
|
79
87
|
* its fallback. Also used by renderRoute's navigate() (testing/render-route.tsx).
|
|
80
88
|
*/
|
|
81
89
|
export function commitInTransition(
|
|
90
|
+
loaders: LoaderStore,
|
|
82
91
|
onUpdate: UpdateSubscriber,
|
|
83
92
|
segments: readonly ResolvedSegment[],
|
|
84
93
|
update: NavigationUpdate,
|
|
85
94
|
transitionTypes: readonly string[],
|
|
86
95
|
): void {
|
|
87
96
|
startTransition(() => {
|
|
88
|
-
|
|
97
|
+
loaders.announcePendingStreams(segments);
|
|
89
98
|
if (addTransitionType) {
|
|
90
99
|
for (const type of transitionTypes) addTransitionType(type);
|
|
91
100
|
}
|
|
@@ -464,10 +473,17 @@ export function createPartialUpdater(
|
|
|
464
473
|
scroll: toScrollPayload(commitScroll),
|
|
465
474
|
};
|
|
466
475
|
|
|
476
|
+
if (process.env.NODE_ENV !== "production") {
|
|
477
|
+
auditTreeCause(treeCause(mode, tx.traversal));
|
|
478
|
+
}
|
|
467
479
|
if (shouldStartViewTransition(existingSegments, cachedGatedOff)) {
|
|
468
|
-
commitInTransition(
|
|
469
|
-
|
|
470
|
-
|
|
480
|
+
commitInTransition(
|
|
481
|
+
store.loaders,
|
|
482
|
+
onUpdate,
|
|
483
|
+
existingSegments,
|
|
484
|
+
cachedUpdate,
|
|
485
|
+
["navigation"],
|
|
486
|
+
);
|
|
471
487
|
} else {
|
|
472
488
|
onUpdate(cachedUpdate);
|
|
473
489
|
}
|
|
@@ -491,6 +507,9 @@ export function createPartialUpdater(
|
|
|
491
507
|
{ routeName: payload.metadata.routeName },
|
|
492
508
|
);
|
|
493
509
|
|
|
510
|
+
if (process.env.NODE_ENV !== "production") {
|
|
511
|
+
auditTreeCause(treeCause(mode, tx.traversal));
|
|
512
|
+
}
|
|
494
513
|
onUpdate({
|
|
495
514
|
root: newTree,
|
|
496
515
|
metadata: payload.metadata,
|
|
@@ -718,11 +737,15 @@ export function createPartialUpdater(
|
|
|
718
737
|
metadata: payload.metadata!,
|
|
719
738
|
scroll: scrollPayload,
|
|
720
739
|
};
|
|
740
|
+
if (process.env.NODE_ENV !== "production") {
|
|
741
|
+
auditTreeCause(treeCause(mode, tx.traversal));
|
|
742
|
+
}
|
|
721
743
|
if (
|
|
722
744
|
!gatedOff &&
|
|
723
745
|
(mode.type === "action" || mode.type === "stale-revalidation")
|
|
724
746
|
) {
|
|
725
747
|
commitInTransition(
|
|
748
|
+
store.loaders,
|
|
726
749
|
onUpdate,
|
|
727
750
|
reconciled.mainSegments,
|
|
728
751
|
update,
|
|
@@ -730,6 +753,7 @@ export function createPartialUpdater(
|
|
|
730
753
|
);
|
|
731
754
|
} else if (hasTransition) {
|
|
732
755
|
commitInTransition(
|
|
756
|
+
store.loaders,
|
|
733
757
|
onUpdate,
|
|
734
758
|
reconciled.mainSegments,
|
|
735
759
|
update,
|
|
@@ -764,6 +788,7 @@ export function createPartialUpdater(
|
|
|
764
788
|
// segment reconciles in place (clientGroup key), so a read that still
|
|
765
789
|
// suspends must hold the presented content, not flash a fallback.
|
|
766
790
|
commitInTransition(
|
|
791
|
+
store.loaders,
|
|
767
792
|
onUpdate,
|
|
768
793
|
reconciled.mainSegments,
|
|
769
794
|
update,
|
|
@@ -772,12 +797,13 @@ export function createPartialUpdater(
|
|
|
772
797
|
} else {
|
|
773
798
|
// Cold/partially-prefetched nav that mounts NEW segments, or any
|
|
774
799
|
// commit transition({ when }) gated off (navigation, action refetch,
|
|
775
|
-
// stale revalidation): normal commit
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
//
|
|
800
|
+
// stale revalidation): normal, unanimated commit. A segment with work
|
|
801
|
+
// still pending shows its loading() fallback like a first load; one
|
|
802
|
+
// whose data is already in hand shows none. A gated-off segment keeps
|
|
803
|
+
// its key, so a fallback comes from its boundary suspending on pending
|
|
804
|
+
// work in this urgent commit, not from a remount (#995). Explicit
|
|
805
|
+
// transition() routes keep the content-hold via the hasTransition
|
|
806
|
+
// branch above (the opt-in).
|
|
781
807
|
onUpdate(update);
|
|
782
808
|
}
|
|
783
809
|
|
|
@@ -822,6 +848,9 @@ export function createPartialUpdater(
|
|
|
822
848
|
scroll: fullScrollPayload,
|
|
823
849
|
};
|
|
824
850
|
|
|
851
|
+
if (process.env.NODE_ENV !== "production") {
|
|
852
|
+
auditTreeCause(treeCause(mode, tx.traversal));
|
|
853
|
+
}
|
|
825
854
|
if (mode.type === "stale-revalidation") {
|
|
826
855
|
await rawStreamComplete;
|
|
827
856
|
// Mirror the partial branch's history-key staleness guard (above): the
|
|
@@ -836,9 +865,13 @@ export function createPartialUpdater(
|
|
|
836
865
|
);
|
|
837
866
|
return;
|
|
838
867
|
}
|
|
868
|
+
if (process.env.NODE_ENV !== "production") {
|
|
869
|
+
auditTreeCause("stale-revalidation");
|
|
870
|
+
}
|
|
839
871
|
if (fullGatedOff) onUpdate(fullUpdate);
|
|
840
872
|
else {
|
|
841
873
|
commitInTransition(
|
|
874
|
+
store.loaders,
|
|
842
875
|
onUpdate,
|
|
843
876
|
segments,
|
|
844
877
|
fullUpdate,
|
|
@@ -847,13 +880,16 @@ export function createPartialUpdater(
|
|
|
847
880
|
}
|
|
848
881
|
} else if (mode.type === "action" && !fullGatedOff) {
|
|
849
882
|
commitInTransition(
|
|
883
|
+
store.loaders,
|
|
850
884
|
onUpdate,
|
|
851
885
|
segments,
|
|
852
886
|
fullUpdate,
|
|
853
887
|
fullHasTransition ? ["action"] : [],
|
|
854
888
|
);
|
|
855
889
|
} else if (fullHasTransition) {
|
|
856
|
-
commitInTransition(onUpdate, segments, fullUpdate, [
|
|
890
|
+
commitInTransition(store.loaders, onUpdate, segments, fullUpdate, [
|
|
891
|
+
"navigation",
|
|
892
|
+
]);
|
|
857
893
|
} else {
|
|
858
894
|
onUpdate(fullUpdate);
|
|
859
895
|
}
|
|
@@ -2,68 +2,48 @@
|
|
|
2
2
|
* Router-wide default prefetch strategy (client seat).
|
|
3
3
|
*
|
|
4
4
|
* The server resolves `createRouter({ defaultPrefetch })` once at router init
|
|
5
|
-
* (router/prefetch-default.ts) and ships it in initial payload metadata
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `data-prefetch-scope`.
|
|
12
|
-
*
|
|
13
|
-
* The module initial value must equal the server resolver's environment default:
|
|
14
|
-
* `"none"` in development and `"viewport"` in production. During SSR this
|
|
15
|
-
* module is never initialized from metadata, so keeping both seats aligned also
|
|
16
|
-
* gives metadata-less payloads the documented behavior.
|
|
5
|
+
* (router/prefetch-default.ts) and ships it in initial payload metadata. The
|
|
6
|
+
* router store carries it to its readers: `<Link>` reads
|
|
7
|
+
* `NavigationStoreContext.defaultPrefetch`, and delegated plain anchors get it
|
|
8
|
+
* from the navigation bridge. Every `<Link>` without an explicit `prefetch`
|
|
9
|
+
* prop and every eligible intercepted plain anchor that has not opted out with
|
|
10
|
+
* `data-prefetch="false"` or `data-prefetch="none"` uses it. Containers use the
|
|
11
|
+
* same `false`/`none` vocabulary via `data-prefetch-scope`.
|
|
17
12
|
*/
|
|
18
13
|
|
|
19
14
|
import type { PrefetchStrategy } from "../../router/prefetch-default.js";
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The default for a payload without `defaultPrefetch`: the server resolver's
|
|
18
|
+
* environment default (`"none"` in development, `"viewport"` in production),
|
|
19
|
+
* mirrored without pulling router-layer code into the client bundle. NODE_ENV
|
|
20
|
+
* is folded by the app build.
|
|
21
|
+
*/
|
|
22
|
+
export const ENVIRONMENT_DEFAULT_PREFETCH: PrefetchStrategy =
|
|
26
23
|
process.env.NODE_ENV === "production" ? "viewport" : "none";
|
|
27
24
|
|
|
28
|
-
function
|
|
29
|
-
if (typeof window === "undefined"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
return hoverNoneQuery;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Reset module state between tests that replace browser media globals. */
|
|
37
|
-
export function resetAdaptiveStrategyForTesting(): void {
|
|
38
|
-
hoverNoneQuery = null;
|
|
25
|
+
function hoverNoneQuery(): MediaQueryList | null {
|
|
26
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function")
|
|
27
|
+
return null;
|
|
28
|
+
return window.matchMedia("(hover: none)");
|
|
39
29
|
}
|
|
40
30
|
|
|
41
31
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
32
|
+
* Resolve adaptive to the strategy for the current input capability. Called
|
|
33
|
+
* when a prefetch is armed or triggered, never during render.
|
|
44
34
|
*/
|
|
45
|
-
export function setDefaultPrefetchStrategy(strategy: PrefetchStrategy): void {
|
|
46
|
-
defaultStrategy = strategy;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Current default strategy for Links and delegated plain anchors. */
|
|
50
|
-
export function getDefaultPrefetchStrategy(): PrefetchStrategy {
|
|
51
|
-
return defaultStrategy;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Resolve adaptive to the strategy for the current input capability. */
|
|
55
35
|
export function resolveAdaptiveStrategy(
|
|
56
36
|
strategy: PrefetchStrategy,
|
|
57
37
|
): PrefetchStrategy {
|
|
58
38
|
if (strategy !== "adaptive") return strategy;
|
|
59
|
-
return
|
|
39
|
+
return hoverNoneQuery()?.matches ? "viewport" : "hover";
|
|
60
40
|
}
|
|
61
41
|
|
|
62
42
|
/** Subscribe to changes in the input capability used by adaptive prefetch. */
|
|
63
43
|
export function subscribeToAdaptiveStrategyChange(
|
|
64
44
|
listener: () => void,
|
|
65
45
|
): () => void {
|
|
66
|
-
const query =
|
|
46
|
+
const query = hoverNoneQuery();
|
|
67
47
|
if (!query) return () => {};
|
|
68
48
|
if (typeof query.addEventListener === "function") {
|
|
69
49
|
query.addEventListener("change", listener);
|