| | import { |
| | type AppRouterState, |
| | type ReducerActions, |
| | type ReducerState, |
| | ACTION_REFRESH, |
| | ACTION_SERVER_ACTION, |
| | ACTION_NAVIGATE, |
| | ACTION_RESTORE, |
| | type NavigateAction, |
| | ACTION_HMR_REFRESH, |
| | PrefetchKind, |
| | type AppHistoryState, |
| | } from './router-reducer/router-reducer-types' |
| | import { reducer } from './router-reducer/router-reducer' |
| | import { startTransition } from 'react' |
| | import { isThenable } from '../../shared/lib/is-thenable' |
| | import { |
| | FetchStrategy, |
| | type PrefetchTaskFetchStrategy, |
| | } from './segment-cache/types' |
| | import { prefetch as prefetchWithSegmentCache } from './segment-cache/prefetch' |
| | import { dispatchAppRouterAction } from './use-action-queue' |
| | import { addBasePath } from '../add-base-path' |
| | import { isExternalURL } from './app-router-utils' |
| | import type { |
| | AppRouterInstance, |
| | NavigateOptions, |
| | PrefetchOptions, |
| | } from '../../shared/lib/app-router-context.shared-runtime' |
| | import { setLinkForCurrentNavigation, type LinkInstance } from './links' |
| | import type { ClientInstrumentationHooks } from '../app-index' |
| | import type { GlobalErrorComponent } from './builtin/global-error' |
| | import { isJavaScriptURLString } from '../lib/javascript-url' |
| |
|
| | export type DispatchStatePromise = React.Dispatch<ReducerState> |
| |
|
| | export type AppRouterActionQueue = { |
| | state: AppRouterState |
| | dispatch: (payload: ReducerActions, setState: DispatchStatePromise) => void |
| | action: (state: AppRouterState, action: ReducerActions) => ReducerState |
| |
|
| | onRouterTransitionStart: |
| | | ((url: string, type: 'push' | 'replace' | 'traverse') => void) |
| | | null |
| |
|
| | pending: ActionQueueNode | null |
| | needsRefresh?: boolean |
| | last: ActionQueueNode | null |
| | } |
| |
|
| | export type GlobalErrorState = [ |
| | GlobalError: GlobalErrorComponent, |
| | styles: React.ReactNode, |
| | ] |
| |
|
| | export type ActionQueueNode = { |
| | payload: ReducerActions |
| | next: ActionQueueNode | null |
| | resolve: (value: ReducerState) => void |
| | reject: (err: Error) => void |
| | discarded?: boolean |
| | } |
| |
|
| | function runRemainingActions( |
| | actionQueue: AppRouterActionQueue, |
| | setState: DispatchStatePromise |
| | ) { |
| | if (actionQueue.pending !== null) { |
| | actionQueue.pending = actionQueue.pending.next |
| | if (actionQueue.pending !== null) { |
| | runAction({ |
| | actionQueue, |
| | action: actionQueue.pending, |
| | setState, |
| | }) |
| | } |
| | } else { |
| | |
| | |
| | |
| | if (actionQueue.needsRefresh) { |
| | actionQueue.needsRefresh = false |
| | actionQueue.dispatch({ type: ACTION_REFRESH }, setState) |
| | } |
| | } |
| | } |
| |
|
| | async function runAction({ |
| | actionQueue, |
| | action, |
| | setState, |
| | }: { |
| | actionQueue: AppRouterActionQueue |
| | action: ActionQueueNode |
| | setState: DispatchStatePromise |
| | }) { |
| | const prevState = actionQueue.state |
| |
|
| | actionQueue.pending = action |
| |
|
| | const payload = action.payload |
| | const actionResult = actionQueue.action(prevState, payload) |
| |
|
| | function handleResult(nextState: AppRouterState) { |
| | |
| | if (action.discarded) { |
| | |
| | if ( |
| | action.payload.type === ACTION_SERVER_ACTION && |
| | action.payload.didRevalidate |
| | ) { |
| | |
| | |
| | actionQueue.needsRefresh = true |
| | } |
| | |
| | |
| | runRemainingActions(actionQueue, setState) |
| | return |
| | } |
| |
|
| | actionQueue.state = nextState |
| |
|
| | runRemainingActions(actionQueue, setState) |
| | action.resolve(nextState) |
| | } |
| |
|
| | |
| | if (isThenable(actionResult)) { |
| | actionResult.then(handleResult, (err) => { |
| | runRemainingActions(actionQueue, setState) |
| | action.reject(err) |
| | }) |
| | } else { |
| | handleResult(actionResult) |
| | } |
| | } |
| |
|
| | function dispatchAction( |
| | actionQueue: AppRouterActionQueue, |
| | payload: ReducerActions, |
| | setState: DispatchStatePromise |
| | ) { |
| | let resolvers: { |
| | resolve: (value: ReducerState) => void |
| | reject: (reason: any) => void |
| | } = { resolve: setState, reject: () => {} } |
| |
|
| | |
| | |
| | |
| | |
| | if (payload.type !== ACTION_RESTORE) { |
| | |
| | const deferredPromise = new Promise<AppRouterState>((resolve, reject) => { |
| | resolvers = { resolve, reject } |
| | }) |
| |
|
| | startTransition(() => { |
| | |
| | |
| | setState(deferredPromise) |
| | }) |
| | } |
| |
|
| | const newAction: ActionQueueNode = { |
| | payload, |
| | next: null, |
| | resolve: resolvers.resolve, |
| | reject: resolvers.reject, |
| | } |
| |
|
| | |
| | if (actionQueue.pending === null) { |
| | |
| | |
| | actionQueue.last = newAction |
| |
|
| | runAction({ |
| | actionQueue, |
| | action: newAction, |
| | setState, |
| | }) |
| | } else if ( |
| | payload.type === ACTION_NAVIGATE || |
| | payload.type === ACTION_RESTORE |
| | ) { |
| | |
| | |
| | actionQueue.pending.discarded = true |
| |
|
| | |
| | |
| | newAction.next = actionQueue.pending.next |
| |
|
| | runAction({ |
| | actionQueue, |
| | action: newAction, |
| | setState, |
| | }) |
| | } else { |
| | |
| | |
| | if (actionQueue.last !== null) { |
| | actionQueue.last.next = newAction |
| | } |
| | actionQueue.last = newAction |
| | } |
| | } |
| |
|
| | let globalActionQueue: AppRouterActionQueue | null = null |
| |
|
| | export function createMutableActionQueue( |
| | initialState: AppRouterState, |
| | instrumentationHooks: ClientInstrumentationHooks | null |
| | ): AppRouterActionQueue { |
| | const actionQueue: AppRouterActionQueue = { |
| | state: initialState, |
| | dispatch: (payload: ReducerActions, setState: DispatchStatePromise) => |
| | dispatchAction(actionQueue, payload, setState), |
| | action: async (state: AppRouterState, action: ReducerActions) => { |
| | const result = reducer(state, action) |
| | return result |
| | }, |
| | pending: null, |
| | last: null, |
| | onRouterTransitionStart: |
| | instrumentationHooks !== null && |
| | typeof instrumentationHooks.onRouterTransitionStart === 'function' |
| | ? |
| | instrumentationHooks.onRouterTransitionStart |
| | : null, |
| | } |
| |
|
| | if (typeof window !== 'undefined') { |
| | |
| | |
| | |
| | if (globalActionQueue !== null) { |
| | throw new Error( |
| | 'Internal Next.js Error: createMutableActionQueue was called more ' + |
| | 'than once' |
| | ) |
| | } |
| | globalActionQueue = actionQueue |
| | } |
| |
|
| | return actionQueue |
| | } |
| |
|
| | export function getCurrentAppRouterState(): AppRouterState | null { |
| | return globalActionQueue !== null ? globalActionQueue.state : null |
| | } |
| |
|
| | function getAppRouterActionQueue(): AppRouterActionQueue { |
| | if (globalActionQueue === null) { |
| | throw new Error( |
| | 'Internal Next.js error: Router action dispatched before initialization.' |
| | ) |
| | } |
| | return globalActionQueue |
| | } |
| |
|
| | function getProfilingHookForOnNavigationStart() { |
| | if (globalActionQueue !== null) { |
| | return globalActionQueue.onRouterTransitionStart |
| | } |
| | return null |
| | } |
| |
|
| | export function dispatchNavigateAction( |
| | href: string, |
| | navigateType: NavigateAction['navigateType'], |
| | shouldScroll: boolean, |
| | linkInstanceRef: LinkInstance | null |
| | ): void { |
| | |
| | |
| | const url = new URL(addBasePath(href), location.href) |
| | if (process.env.__NEXT_APP_NAV_FAIL_HANDLING) { |
| | window.next.__pendingUrl = url |
| | } |
| |
|
| | setLinkForCurrentNavigation(linkInstanceRef) |
| |
|
| | const onRouterTransitionStart = getProfilingHookForOnNavigationStart() |
| | if (onRouterTransitionStart !== null) { |
| | onRouterTransitionStart(href, navigateType) |
| | } |
| |
|
| | dispatchAppRouterAction({ |
| | type: ACTION_NAVIGATE, |
| | url, |
| | isExternalUrl: isExternalURL(url), |
| | locationSearch: location.search, |
| | shouldScroll, |
| | navigateType, |
| | }) |
| | } |
| |
|
| | export function dispatchTraverseAction( |
| | href: string, |
| | historyState: AppHistoryState | undefined |
| | ) { |
| | const onRouterTransitionStart = getProfilingHookForOnNavigationStart() |
| | if (onRouterTransitionStart !== null) { |
| | onRouterTransitionStart(href, 'traverse') |
| | } |
| | dispatchAppRouterAction({ |
| | type: ACTION_RESTORE, |
| | url: new URL(href), |
| | historyState, |
| | }) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | export const publicAppRouterInstance: AppRouterInstance = { |
| | back: () => window.history.back(), |
| | forward: () => window.history.forward(), |
| | prefetch: |
| | |
| | |
| | |
| | (href: string, options?: PrefetchOptions) => { |
| | if (isJavaScriptURLString(href)) { |
| | throw new Error( |
| | 'Next.js has blocked a javascript: URL as a security precaution.' |
| | ) |
| | } |
| | const actionQueue = getAppRouterActionQueue() |
| | const prefetchKind = options?.kind ?? PrefetchKind.AUTO |
| |
|
| | |
| | |
| | let fetchStrategy: PrefetchTaskFetchStrategy |
| | switch (prefetchKind) { |
| | case PrefetchKind.AUTO: { |
| | |
| | fetchStrategy = FetchStrategy.PPR |
| | break |
| | } |
| | case PrefetchKind.FULL: { |
| | fetchStrategy = FetchStrategy.Full |
| | break |
| | } |
| | default: { |
| | prefetchKind satisfies never |
| | |
| | |
| | |
| | |
| | fetchStrategy = FetchStrategy.PPR |
| | } |
| | } |
| |
|
| | prefetchWithSegmentCache( |
| | href, |
| | actionQueue.state.nextUrl, |
| | actionQueue.state.tree, |
| | fetchStrategy, |
| | options?.onInvalidate ?? null |
| | ) |
| | }, |
| | replace: (href: string, options?: NavigateOptions) => { |
| | if (isJavaScriptURLString(href)) { |
| | throw new Error( |
| | 'Next.js has blocked a javascript: URL as a security precaution.' |
| | ) |
| | } |
| | startTransition(() => { |
| | dispatchNavigateAction(href, 'replace', options?.scroll ?? true, null) |
| | }) |
| | }, |
| | push: (href: string, options?: NavigateOptions) => { |
| | if (isJavaScriptURLString(href)) { |
| | throw new Error( |
| | 'Next.js has blocked a javascript: URL as a security precaution.' |
| | ) |
| | } |
| | startTransition(() => { |
| | dispatchNavigateAction(href, 'push', options?.scroll ?? true, null) |
| | }) |
| | }, |
| | refresh: () => { |
| | startTransition(() => { |
| | dispatchAppRouterAction({ |
| | type: ACTION_REFRESH, |
| | }) |
| | }) |
| | }, |
| | hmrRefresh: () => { |
| | if (process.env.NODE_ENV !== 'development') { |
| | throw new Error( |
| | 'hmrRefresh can only be used in development mode. Please use refresh instead.' |
| | ) |
| | } else { |
| | startTransition(() => { |
| | dispatchAppRouterAction({ |
| | type: ACTION_HMR_REFRESH, |
| | }) |
| | }) |
| | } |
| | }, |
| | } |
| |
|
| | |
| | if (typeof window !== 'undefined' && window.next) { |
| | window.next.router = publicAppRouterInstance |
| | } |
| |
|