import type { App } from "vue"; import { isNavigationFailure, loadRouteLocation, routeLocationKey, START_LOCATION, type NavigationFailure, type RouteLocationNormalizedLoaded, type RouteLocationRaw, type RouteLocationResolved, type Router, } from "vue-router"; import type { NativeTransaction, NativeViewEntry } from "../types"; import type { NativeHistoryLedger } from "./history-ledger"; export type NavigationResult = NavigationFailure | void | true; export function isFailedNavigation(value: unknown) { return Boolean(value && isNavigationFailure(value)); } const nativeScopedRouteProperty = "__nativeVueRouterScopedRoute"; export class VueRouterBridge { private removeAfterEach?: () => void; private pendingPop?: (failure?: NavigationFailure | void) => void; constructor( readonly router: Router, acceptRoute: (route: RouteLocationNormalizedLoaded) => void, ) { this.removeAfterEach = router.afterEach((to, _from, failure) => { if (!failure) acceptRoute(to); this.pendingPop?.(failure); this.pendingPop = undefined; }); } installRouteScope(app: App) { // Vue Router's global `$route` always reads currentRoute. Preview trees // provide their own routeLocationKey, so bridge that injection to the // Options API too. app.mixin({ inject: { [nativeScopedRouteProperty]: { from: routeLocationKey }, }, computed: { $route() { return ( this as unknown as Record< typeof nativeScopedRouteProperty, RouteLocationNormalizedLoaded > )[nativeScopedRouteProperty]; }, }, }); } async initialRoute() { await this.router.isReady(); return this.router.currentRoute.value === START_LOCATION ? undefined : this.router.currentRoute.value; } resolve(to: RouteLocationRaw) { return this.router.resolve(to); } currentFullPath() { return this.router.currentRoute.value.fullPath; } load(route: RouteLocationResolved) { return loadRouteLocation(route); } async commit( transaction: NativeTransaction, target: NativeViewEntry, history: NativeHistoryLedger, ): Promise { if (transaction.kind === "pop" || transaction.kind === "dismiss") { if (target.synthetic) return await this.router.replace(target.route.fullPath); return await this.navigateHistory(-1); } if (transaction.replace) { const targetIndex = history.indexOf(target.key); if (targetIndex >= 0 && targetIndex < history.length - 1) return await this.navigateHistory(targetIndex - (history.length - 1)); } return transaction.replace ? await this.router.replace(target.route.fullPath) : await this.router.push(target.route.fullPath); } dispose() { this.removeAfterEach?.(); } private async navigateHistory(delta: number) { return await new Promise((resolve) => { this.pendingPop = resolve; this.router.go(delta); globalThis.setTimeout(() => { if (this.pendingPop === resolve) { this.pendingPop = undefined; resolve(); } }, 1200); }); } }