@@ -26,12 +26,12 @@ import {
2626 commitClientNavigationState ,
2727 consumePrefetchResponse ,
2828 createClientNavigationRenderSnapshot ,
29+ getCurrentNextUrl ,
2930 getCurrentInterceptionContext ,
3031 getClientNavigationRenderContext ,
3132 getPrefetchCache ,
3233 getPrefetchedUrls ,
3334 pushHistoryStateWithoutNotify ,
34- readHistoryStateInterceptionContext ,
3535 replaceClientParamsWithoutNotify ,
3636 replaceHistoryStateWithoutNotify ,
3737 restoreRscResponse ,
@@ -40,7 +40,6 @@ import {
4040 setMountedSlotsHeader ,
4141 setNavigationContext ,
4242 toRscUrl ,
43- VINEXT_INTERCEPTION_CONTEXT_HISTORY_STATE_KEY ,
4443 type CachedRscResponse ,
4544 type ClientNavigationRenderSnapshot ,
4645} from "../shims/navigation.js" ;
@@ -60,8 +59,11 @@ import {
6059 type AppWireElements ,
6160} from "./app-elements.js" ;
6261import {
62+ createHistoryStateWithPreviousNextUrl ,
6363 createPendingNavigationCommit ,
64+ readHistoryStatePreviousNextUrl ,
6465 resolveAndClassifyNavigationCommit ,
66+ resolveInterceptionContextFromPreviousNextUrl ,
6567 resolvePendingNavigationCommitDisposition ,
6668 routerReducer ,
6769 type AppRouterAction ,
@@ -98,9 +100,6 @@ type VisitedResponseCacheEntry = {
98100const MAX_VISITED_RESPONSE_CACHE_SIZE = 50 ;
99101const VISITED_RESPONSE_CACHE_TTL = 5 * 60_000 ;
100102const MAX_TRAVERSAL_CACHE_TTL = 30 * 60_000 ;
101- type HistoryStateRecord = {
102- [ key : string ] : unknown ;
103- } ;
104103
105104// These are plain module-level variables, unlike ClientNavigationState in
106105// navigation.ts which uses Symbol.for to survive multiple Vite module instances.
@@ -208,15 +207,15 @@ function createNavigationCommitEffect(
208207 href : string ,
209208 historyUpdateMode : HistoryUpdateMode | undefined ,
210209 params : Record < string , string | string [ ] > ,
211- interceptionContext : string | null ,
210+ previousNextUrl : string | null ,
212211) : ( ) => void {
213212 return ( ) => {
214213 const targetHref = new URL ( href , window . location . origin ) . href ;
215214 stageClientParams ( params ) ;
216215 const preserveExistingState = historyUpdateMode === "replace" ;
217- const historyState = createHistoryStateWithInterceptionContext (
216+ const historyState = createHistoryStateWithPreviousNextUrl (
218217 preserveExistingState ? window . history . state : null ,
219- interceptionContext ,
218+ previousNextUrl ,
220219 ) ;
221220
222221 if ( historyUpdateMode === "replace" && window . location . href !== targetHref ) {
@@ -300,41 +299,49 @@ function storeVisitedResponseSnapshot(
300299 } ) ;
301300}
302301
303- function cloneHistoryState ( state : unknown ) : HistoryStateRecord {
304- if ( ! state || typeof state !== "object" ) {
305- return { } ;
306- }
307-
308- const nextState : HistoryStateRecord = { } ;
309- for ( const [ key , value ] of Object . entries ( state ) ) {
310- nextState [ key ] = value ;
311- }
312- return nextState ;
313- }
314-
315- function createHistoryStateWithInterceptionContext (
316- state : unknown ,
317- interceptionContext : string | null ,
318- ) : HistoryStateRecord | null {
319- const nextState = cloneHistoryState ( state ) ;
302+ type NavigationRequestState = {
303+ interceptionContext : string | null ;
304+ previousNextUrl : string | null ;
305+ } ;
320306
321- if ( interceptionContext === null ) {
322- delete nextState [ VINEXT_INTERCEPTION_CONTEXT_HISTORY_STATE_KEY ] ;
323- } else {
324- nextState [ VINEXT_INTERCEPTION_CONTEXT_HISTORY_STATE_KEY ] = interceptionContext ;
307+ function getRequestState (
308+ navigationKind : NavigationKind ,
309+ previousNextUrlOverride ?: string | null ,
310+ ) : NavigationRequestState {
311+ if ( previousNextUrlOverride !== undefined ) {
312+ return {
313+ interceptionContext : resolveInterceptionContextFromPreviousNextUrl (
314+ previousNextUrlOverride ,
315+ __basePath ,
316+ ) ,
317+ previousNextUrl : previousNextUrlOverride ,
318+ } ;
325319 }
326320
327- return Object . keys ( nextState ) . length > 0 ? nextState : null ;
328- }
329-
330- function getRequestInterceptionContext ( navigationKind : NavigationKind ) : string | null {
331321 switch ( navigationKind ) {
332322 case "navigate" :
333- return getCurrentInterceptionContext ( ) ;
334- case "traverse" :
335- return readHistoryStateInterceptionContext ( window . history . state ) ;
323+ return {
324+ interceptionContext : getCurrentInterceptionContext ( ) ,
325+ previousNextUrl : getCurrentNextUrl ( ) ,
326+ } ;
327+ case "traverse" : {
328+ const previousNextUrl = readHistoryStatePreviousNextUrl ( window . history . state ) ;
329+ return {
330+ interceptionContext : resolveInterceptionContextFromPreviousNextUrl (
331+ previousNextUrl ,
332+ __basePath ,
333+ ) ,
334+ previousNextUrl,
335+ } ;
336+ }
336337 case "refresh" :
337- return null ;
338+ return {
339+ interceptionContext : resolveInterceptionContextFromPreviousNextUrl (
340+ getBrowserRouterState ( ) . previousNextUrl ,
341+ __basePath ,
342+ ) ,
343+ previousNextUrl : getBrowserRouterState ( ) . previousNextUrl ,
344+ } ;
338345 default : {
339346 const _exhaustive : never = navigationKind ;
340347 throw new Error ( "[vinext] Unknown navigation kind: " + String ( _exhaustive ) ) ;
@@ -434,6 +441,7 @@ async function commitSameUrlNavigatePayload(
434441 pending . action . renderId ,
435442 "navigate" ,
436443 pending . interceptionContext ,
444+ pending . previousNextUrl ,
437445 pending . routeId ,
438446 pending . rootLayoutTreePath ,
439447 false ,
@@ -467,6 +475,7 @@ function BrowserRoot({
467475 elements : resolvedElements ,
468476 interceptionContext : initialMetadata . interceptionContext ,
469477 navigationSnapshot : initialNavigationSnapshot ,
478+ previousNextUrl : null ,
470479 renderId : 0 ,
471480 rootLayoutTreePath : initialMetadata . rootLayoutTreePath ,
472481 routeId : initialMetadata . routeId ,
@@ -510,14 +519,11 @@ function BrowserRoot({
510519 }
511520
512521 replaceHistoryStateWithoutNotify (
513- createHistoryStateWithInterceptionContext (
514- window . history . state ,
515- treeState . interceptionContext ,
516- ) ,
522+ createHistoryStateWithPreviousNextUrl ( window . history . state , treeState . previousNextUrl ) ,
517523 "" ,
518524 window . location . href ,
519525 ) ;
520- } , [ treeState . interceptionContext , treeState . renderId ] ) ;
526+ } , [ treeState . previousNextUrl , treeState . renderId ] ) ;
521527
522528 const committedTree = createElement (
523529 NavigationCommitSignal ,
@@ -547,6 +553,7 @@ function dispatchBrowserTree(
547553 renderId : number ,
548554 actionType : "navigate" | "replace" | "traverse" ,
549555 interceptionContext : string | null ,
556+ previousNextUrl : string | null ,
550557 routeId : string ,
551558 rootLayoutTreePath : string | null ,
552559 useTransitionMode : boolean ,
@@ -558,6 +565,7 @@ function dispatchBrowserTree(
558565 elements,
559566 interceptionContext,
560567 navigationSnapshot,
568+ previousNextUrl,
561569 renderId,
562570 rootLayoutTreePath,
563571 routeId,
@@ -578,6 +586,7 @@ async function renderNavigationPayload(
578586 navId : number ,
579587 historyUpdateMode : HistoryUpdateMode | undefined ,
580588 params : Record < string , string | string [ ] > ,
589+ previousNextUrl : string | null ,
581590 useTransition = true ,
582591 actionType : "navigate" | "replace" | "traverse" = "navigate" ,
583592) : Promise < void > {
@@ -593,6 +602,7 @@ async function renderNavigationPayload(
593602 currentState,
594603 nextElements : payload ,
595604 navigationSnapshot,
605+ previousNextUrl,
596606 renderId,
597607 type : actionType ,
598608 } ) ;
@@ -619,12 +629,7 @@ async function renderNavigationPayload(
619629
620630 queuePrePaintNavigationEffect (
621631 renderId ,
622- createNavigationCommitEffect (
623- targetHref ,
624- historyUpdateMode ,
625- params ,
626- pending . interceptionContext ,
627- ) ,
632+ createNavigationCommitEffect ( targetHref , historyUpdateMode , params , pending . previousNextUrl ) ,
628633 ) ;
629634 activateNavigationSnapshot ( ) ;
630635 snapshotActivated = true ;
@@ -634,6 +639,7 @@ async function renderNavigationPayload(
634639 renderId ,
635640 actionType ,
636641 pending . interceptionContext ,
642+ pending . previousNextUrl ,
637643 pending . routeId ,
638644 pending . rootLayoutTreePath ,
639645 useTransition ,
@@ -813,6 +819,11 @@ async function main(): Promise<void> {
813819 window . location . href ,
814820 latestClientParams ,
815821 ) ;
822+ replaceHistoryStateWithoutNotify (
823+ createHistoryStateWithPreviousNextUrl ( window . history . state , null ) ,
824+ "" ,
825+ window . location . href ,
826+ ) ;
816827
817828 window . __VINEXT_RSC_ROOT__ = hydrateRoot (
818829 document ,
@@ -829,6 +840,7 @@ async function main(): Promise<void> {
829840 redirectDepth = 0 ,
830841 navigationKind : NavigationKind = "navigate" ,
831842 historyUpdateMode ?: HistoryUpdateMode ,
843+ previousNextUrlOverride ?: string | null ,
832844 ) : Promise < void > {
833845 if ( redirectDepth > 10 ) {
834846 console . error (
@@ -845,7 +857,9 @@ async function main(): Promise<void> {
845857 try {
846858 const url = new URL ( href , window . location . origin ) ;
847859 const rscUrl = toRscUrl ( url . pathname + url . search ) ;
848- const requestInterceptionContext = getRequestInterceptionContext ( navigationKind ) ;
860+ const requestState = getRequestState ( navigationKind , previousNextUrlOverride ) ;
861+ const requestInterceptionContext = requestState . interceptionContext ;
862+ const requestPreviousNextUrl = requestState . previousNextUrl ;
849863 // Use startTransition for same-route navigations (searchParam changes)
850864 // so React keeps the old UI visible during the transition. For cross-route
851865 // navigations (different pathname), use synchronous updates — React's
@@ -895,6 +909,7 @@ async function main(): Promise<void> {
895909 navId ,
896910 historyUpdateMode ,
897911 cachedParams ,
912+ requestPreviousNextUrl ,
898913 isSameRoute ,
899914 toActionType ( navigationKind ) ,
900915 ) ;
@@ -942,7 +957,7 @@ async function main(): Promise<void> {
942957 if ( finalUrl . pathname !== requestedUrl . pathname ) {
943958 const destinationPath = finalUrl . pathname . replace ( / \. r s c $ / , "" ) + finalUrl . search ;
944959 replaceHistoryStateWithoutNotify (
945- createHistoryStateWithInterceptionContext ( null , requestInterceptionContext ) ,
960+ createHistoryStateWithPreviousNextUrl ( null , requestPreviousNextUrl ) ,
946961 "" ,
947962 destinationPath ,
948963 ) ;
@@ -956,7 +971,13 @@ async function main(): Promise<void> {
956971 // The URL has already been updated via replaceHistoryStateWithoutNotify above,
957972 // so the recursive navigation should NOT push/replace again. Pass undefined
958973 // for historyUpdateMode to make the commit effect a no-op for history updates.
959- return navigate ( destinationPath , redirectDepth + 1 , navigationKind , undefined ) ;
974+ return navigate (
975+ destinationPath ,
976+ redirectDepth + 1 ,
977+ navigationKind ,
978+ undefined ,
979+ requestPreviousNextUrl ,
980+ ) ;
960981 }
961982
962983 let navParams : Record < string , string | string [ ] > = { } ;
@@ -993,6 +1014,7 @@ async function main(): Promise<void> {
9931014 navId ,
9941015 historyUpdateMode ,
9951016 navParams ,
1017+ requestPreviousNextUrl ,
9961018 isSameRoute ,
9971019 toActionType ( navigationKind ) ,
9981020 ) ;
@@ -1088,6 +1110,7 @@ async function main(): Promise<void> {
10881110 pending . action . renderId ,
10891111 "replace" ,
10901112 pending . interceptionContext ,
1113+ pending . previousNextUrl ,
10911114 pending . routeId ,
10921115 pending . rootLayoutTreePath ,
10931116 false ,
0 commit comments