diff --git a/package.json b/package.json index 83c776139..0cc081fca 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "postinstall": "simple-git-hooks" }, "devDependencies": { + "@types/dom-navigation": "^1.0.6", "@vitest/coverage-v8": "^2.1.9", "@vitest/ui": "^2.1.9", "brotli": "^1.3.3", @@ -44,9 +45,9 @@ "prettier": "^3.5.3", "semver": "^7.7.1", "simple-git-hooks": "^2.13.0", - "typedoc": "^0.26.11", - "typedoc-plugin-markdown": "^4.2.10", - "typescript": "~5.6.3", + "typedoc": "^0.28.13", + "typedoc-plugin-markdown": "^4.8.1", + "typescript": "~5.8.2", "vitest": "^2.1.9" }, "simple-git-hooks": { diff --git a/packages/router/add-dts-module-augmentation.mjs b/packages/router/add-dts-module-augmentation.mjs new file mode 100644 index 000000000..84d738583 --- /dev/null +++ b/packages/router/add-dts-module-augmentation.mjs @@ -0,0 +1,24 @@ +import * as fs from 'node:fs/promises' + +async function patchVueRouterDts() { + const content = await fs.readFile('./src/globalExtensions.ts', { + encoding: 'utf-8', + }) + const moduleAugmentationIdx = content.indexOf('/**') + if (moduleAugmentationIdx === -1) { + throw new Error( + 'Cannot find module augmentation in globalExtensions.ts, first /** comment is expected to start module augmentation' + ) + } + const targetContent = await fs.readFile('./dist/vue-router.d.ts', { + encoding: 'utf-8', + }) + await fs.writeFile( + './dist/vue-router.d.ts', + `${targetContent} +${content.slice(moduleAugmentationIdx)}`, + { encoding: 'utf8' } + ) +} + +patchVueRouterDts() diff --git a/packages/router/package.json b/packages/router/package.json index ba06a10be..9360ac21d 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -91,7 +91,7 @@ "dev": "vitest --ui", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1", "build": "rimraf dist && rollup -c rollup.config.mjs", - "build:dts": "api-extractor run --local --verbose && tail -n +10 src/globalExtensions.ts >> dist/vue-router.d.ts", + "build:dts": "api-extractor run --local --verbose && node ./add-dts-module-augmentation.mjs", "build:playground": "vue-tsc --noEmit && vite build --config playground/vite.config.ts", "build:e2e": "vue-tsc --noEmit && vite build --config e2e/vite.config.mjs", "build:size": "pnpm run build && rollup -c size-checks/rollup.config.mjs", @@ -117,12 +117,13 @@ "@vue/devtools-api": "^6.6.4" }, "devDependencies": { - "@microsoft/api-extractor": "^7.48.0", + "@microsoft/api-extractor": "^7.52.13", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^25.0.8", "@rollup/plugin-node-resolve": "^15.3.1", "@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-terser": "^0.4.4", + "@types/dom-navigation": "^1.0.6", "@types/jsdom": "^21.1.7", "@types/nightwatch": "^2.3.32", "@vitejs/plugin-vue": "^5.2.3", diff --git a/packages/router/src/RouterView.ts b/packages/router/src/RouterView.ts index a456c2b63..4d6bb682f 100644 --- a/packages/router/src/RouterView.ts +++ b/packages/router/src/RouterView.ts @@ -30,6 +30,7 @@ import { import { assign, isArray, isBrowser } from './utils' import { warn } from './warning' import { isSameRouteRecord } from './location' +import { TransitionMode, transitionModeKey } from './transition' export interface RouterViewProps { name?: string @@ -62,6 +63,7 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({ __DEV__ && warnDeprecatedUsage() const injectedRoute = inject(routerViewLocationKey)! + const transitionMode = inject(transitionModeKey, 'auto')! const routeToDisplay = computed( () => props.route || injectedRoute.value ) @@ -145,7 +147,11 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({ matchedRoute && matchedRoute.components![currentName] if (!ViewComponent) { - return normalizeSlot(slots.default, { Component: ViewComponent, route }) + return normalizeSlot(slots.default, { + Component: ViewComponent, + route, + transitionMode, + }) } // props from route configuration @@ -199,8 +205,11 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({ return ( // pass the vnode to the slot as a prop. // h and both accept vnodes - normalizeSlot(slots.default, { Component: component, route }) || - component + normalizeSlot(slots.default, { + Component: component, + route, + transitionMode, + }) || component ) } }, @@ -228,9 +237,11 @@ export const RouterView = RouterViewImpl as unknown as { default?: ({ Component, route, + transitionMode, }: { Component: VNode route: RouteLocationNormalizedLoaded + transitionMode: TransitionMode }) => VNode[] } } diff --git a/packages/router/src/focus.ts b/packages/router/src/focus.ts new file mode 100644 index 000000000..f298a3d2f --- /dev/null +++ b/packages/router/src/focus.ts @@ -0,0 +1,109 @@ +import type { RouteLocationNormalized } from './typed-routes' +import type { Router, RouterOptions } from './router' +import { nextTick } from 'vue' + +export function enableFocusManagement(router: Router) { + // navigation-api router will handle this for us + if (router.name !== 'legacy') { + return + } + + const { handleFocus, clearFocusTimeout } = createFocusManagementHandler() + + const unregisterBeforeEach = router.beforeEach(() => { + clearFocusTimeout() + }) + + const unregister = router.afterEach(async to => { + const focusManagement = + to.meta.focusManagement ?? router.options.focusManagement + + // user wants manual focus + if (focusManagement === false) return + + let selector = '[autofocus], body' + + if (focusManagement === true) { + selector = '[autofocus],h1,main,body' + } else if ( + typeof focusManagement === 'string' && + focusManagement.length > 0 + ) { + selector = focusManagement + } + + // ensure DOM is updated, enqueuing a microtask before handling focus + await nextTick() + + handleFocus(selector) + }) + + return () => { + clearFocusTimeout() + unregisterBeforeEach() + unregister() + } +} + +export function prepareFocusReset( + to: RouteLocationNormalized, + routerFocusManagement?: RouterOptions['focusManagement'] +) { + let focusReset: 'after-transition' | 'manual' = 'after-transition' + let selector: string | undefined + + const focusManagement = to.meta.focusManagement ?? routerFocusManagement + if (focusManagement === false) { + focusReset = 'manual' + } + if (focusManagement === true) { + focusReset = 'manual' + selector = '[autofocus],h1,main,body' + } else if (typeof focusManagement === 'string') { + focusReset = 'manual' + selector = focusManagement || '[autofocus],h1,main,body' + } + + return [focusReset, selector] as const +} + +export function createFocusManagementHandler() { + let timeoutId: ReturnType | undefined + + return { + handleFocus: (selector: string) => { + clearTimeout(timeoutId) + requestAnimationFrame(() => { + timeoutId = handleFocusManagement(selector) + }) + }, + clearFocusTimeout: () => { + clearTimeout(timeoutId) + }, + } +} + +function handleFocusManagement( + selector: string +): ReturnType { + return setTimeout(() => { + const target = document.querySelector(selector) + if (!target) return + target.focus({ preventScroll: true }) + if (document.activeElement === target) return + // element has tabindex already, likely not focusable + // because of some other reason, bail out + if (target.hasAttribute('tabindex')) return + const restoreTabindex = () => { + target.removeAttribute('tabindex') + target.removeEventListener('blur', restoreTabindex) + } + // temporarily make the target element focusable + target.setAttribute('tabindex', '-1') + target.addEventListener('blur', restoreTabindex) + // try to focus again + target.focus({ preventScroll: true }) + // remove tabindex and event listener if focus still not worked + if (document.activeElement !== target) restoreTabindex() + }, 150) // screen readers may need more time to react +} diff --git a/packages/router/src/history/common.ts b/packages/router/src/history/common.ts index 0abf48150..bf57e063f 100644 --- a/packages/router/src/history/common.ts +++ b/packages/router/src/history/common.ts @@ -44,10 +44,50 @@ export enum NavigationDirection { unknown = '', } +export interface NavigationApiEvent { + readonly navigationType: 'reload' | 'push' | 'replace' | 'traverse' + readonly canIntercept: boolean + readonly userInitiated: boolean + readonly hashChange: boolean + readonly hasUAVisualTransition: boolean + readonly destination: { + readonly url: string + readonly key: string | null + readonly id: string | null + readonly index: number + readonly sameDocument: boolean + getState(): unknown + } + readonly signal: AbortSignal + readonly formData: FormData | null + readonly downloadRequest: string | null + readonly info?: unknown + + scroll(): void +} + export interface NavigationInformation { type: NavigationType direction: NavigationDirection delta: number + /** + * True if the navigation was triggered by the browser back button. + * + * Note: available only with the new Navigation API Router. + */ + isBackBrowserButton?: boolean + /** + * True if the navigation was triggered by the browser forward button. + * + * Note: available only with the new Navigation API Router. + */ + isForwardBrowserButton?: boolean + /** + * The native Navigation API Event. + * + * Note: available only with the new Navigation API Router. + */ + navigationApiEvent?: NavigationApiEvent } export interface NavigationCallback { diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 2b27d8329..6feb91279 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -138,7 +138,13 @@ export type { } from './typed-routes' export { createRouter } from './router' +export { createNavigationApiRouter } from './navigation-api' export type { Router, RouterOptions, RouterScrollBehavior } from './router' +export type { RouterApiOptions } from './navigation-api' +export type { TransitionMode, RouterViewTransition } from './transition' +export type { ModernRouterOptions } from './modern-router-factory' +export { injectTransitionMode, transitionModeKey } from './transition' +export { createModernRouter } from './modern-router-factory' export { NavigationFailureType, isNavigationFailure } from './errors' export type { @@ -160,6 +166,7 @@ export type { UseLinkReturn, } from './RouterLink' export { RouterView } from './RouterView' +export { isChangingPage } from './utils/routes' export type { RouterViewProps } from './RouterView' export type { TypesConfig } from './config' diff --git a/packages/router/src/modern-router-factory.ts b/packages/router/src/modern-router-factory.ts new file mode 100644 index 000000000..7240415d8 --- /dev/null +++ b/packages/router/src/modern-router-factory.ts @@ -0,0 +1,55 @@ +import type { Router } from './router' +import type { RouterApiOptions } from './navigation-api' +import type { TransitionMode } from './transition' +import { createNavigationApiRouter } from './navigation-api' +import { isBrowser } from './utils' + +export interface ModernRouterOptions { + /** + * Factory function that creates a legacy router instance. + * Typically: () => createRouter({@ history: createWebHistory(), routes }) + */ + legacy: { + factory: (transitionMode: TransitionMode) => Router + } + /** + * Options for the new Navigation API based router. + * If provided and the browser supports it, this will be used. + */ + navigationApi?: { + options: RouterApiOptions + } + /** + * Enable Native View Transitions. + * + * @default undefined + */ + viewTransition?: boolean +} + +export function createModernRouter(options: ModernRouterOptions): Router { + let transitionMode: TransitionMode = 'auto' + + if ( + options?.viewTransition && + typeof document !== 'undefined' && + !!document.startViewTransition + ) { + transitionMode = 'view-transition' + } + + const useNavigationApi = + options.navigationApi && + isBrowser && + typeof window !== 'undefined' && + window.navigation + + if (useNavigationApi) { + return createNavigationApiRouter( + options.navigationApi!.options, + transitionMode + ) + } else { + return options.legacy.factory(transitionMode) + } +} diff --git a/packages/router/src/navigation-api/index.ts b/packages/router/src/navigation-api/index.ts new file mode 100644 index 000000000..3aa9d9bcf --- /dev/null +++ b/packages/router/src/navigation-api/index.ts @@ -0,0 +1,1096 @@ +import { App, nextTick } from 'vue' +import { shallowReactive, shallowRef, unref } from 'vue' +import { + parseURL, + stringifyURL, + isSameRouteLocation, + isSameRouteRecord, + START_LOCATION_NORMALIZED, +} from '../location' + +import { + normalizeQuery, + parseQuery as originalParseQuery, + stringifyQuery as originalStringifyQuery, + LocationQuery, +} from '../query' +import type { + RouteLocationRaw, + RouteParams, + RouteLocationNormalized, + RouteLocationNormalizedLoaded, + RouteLocationResolved, + RouteRecordNameGeneric, + RouteLocation, +} from '../typed-routes' +import type { Router, RouterOptions } from '../router' +import { createRouterMatcher } from '../matcher' +import { useCallbacks } from '../utils/callbacks' +import { extractComponentsGuards, guardToPromiseFn } from '../navigationGuards' +import { + createRouterError, + ErrorTypes, + isNavigationFailure, + NavigationFailure, + NavigationRedirectError, +} from '../errors' +import { applyToParams, assign, isArray, isBrowser } from '../utils' +import { warn } from '../warning' +import { decode, encodeHash, encodeParam } from '../encoding' +import { + isRouteLocation, + isRouteName, + Lazy, + MatcherLocationRaw, + RouteLocationOptions, + RouteRecordRaw, +} from '../types' +import { RouterLink } from '../RouterLink' +import { RouterView } from '../RouterView' +import { + routeLocationKey, + routerKey, + routerViewLocationKey, +} from '../injectionSymbols' +import { + NavigationDirection, + NavigationInformation, + NavigationType, + RouterHistory, +} from '../history/common' +import { RouteRecordNormalized } from '../matcher/types' +import { TransitionMode, transitionModeKey } from '../transition' +import { isChangingPage } from '../utils/routes' +import { createFocusManagementHandler, prepareFocusReset } from '../focus' + +/** + * Options for {@link createNavigationApiRouter}. + * + * This function creates an "opinionated" router that provides smart, modern + * defaults for features like scroll restoration, focus management, and View + * Transitions, aiming to deliver a best-in-class, accessible user experience + * out of the box with zero configuration. + * + * It differs from the legacy `createRouter`, which acts more like a library by + * providing the tools (`scrollBehavior`) but leaving the implementation of these + * features to the developer. + * + * While this router provides smart defaults, it also allows for full customization + * by providing your own `scroll behavior` function or fine-tuning focus management, + * giving you the best of both worlds. + */ +export interface RouterApiOptions + extends Omit< + RouterOptions, + 'history' | 'scrollBehavior' | 'enableScrollManagement' + > { + base?: string + location: string + /** + * Controls the scroll management strategy, allowing you to opt-into the + * manual `vue-router` `scrollBehavior` system for fine-grained control + * via [NavigateEvent.scroll](https://developer.mozilla.org/en-US/docs/Web/API/NavigateEvent/scroll) + * in your route guards. + * + * This can be overridden per-route by defining a `scrollManagement` in the + * route's `meta` field. This takes precedence over this option. + * + * The default behavior is to leverage the browser's native scroll handling: + * - `undefined` (default) or `after-transition`: The router leverages the + * browser's built-in, performant scroll handling (`scroll: 'after-transition'`). + * This provides an excellent default experience that respects modern CSS + * properties like `scroll-padding-top` and restores scroll position automatically + * on back/forward navigations. + * - `manual`: Disables the browser's native scroll management (`scroll: 'manual'`) + * and enables using scroll via native [NavigateEvent.scroll](https://developer.mozilla.org/en-US/docs/Web/API/NavigateEvent/scroll) in your route guards. + * + * @default undefined + */ + scrollManagement?: 'after-transition' | 'manual' +} + +export function createNavigationApiRouter( + options: RouterApiOptions, + transitionMode: TransitionMode = 'auto' +): Router { + if (typeof window === 'undefined' || !window.navigation) { + throw new Error('Navigation API is not supported in this environment.') + } + const matcher = createRouterMatcher(options.routes, options) + const parseQuery = options.parseQuery || originalParseQuery + const stringifyQuery = options.stringifyQuery || originalStringifyQuery + + const beforeGuards = useCallbacks() + const beforeResolveGuards = useCallbacks() + const afterGuards = useCallbacks() + const errorListeners = useCallbacks() + + const currentRoute = shallowRef( + START_LOCATION_NORMALIZED + ) + + let isRevertingNavigation = false + let pendingLocation: RouteLocation | undefined + let lastSuccessfulLocation: RouteLocationNormalizedLoaded = + START_LOCATION_NORMALIZED + + let started: boolean | undefined + const installedApps = new Set() + + function checkCanceledNavigation( + to: RouteLocationNormalized, + from: RouteLocationNormalized + ): NavigationFailure | undefined { + if (pendingLocation !== to) { + return createRouterError( + ErrorTypes.NAVIGATION_CANCELLED, + { + from, + to, + } + ) + } + + return undefined + } + + function checkCanceledNavigationAndReject( + to: RouteLocationNormalized, + from: RouteLocationNormalized + ) { + const error = checkCanceledNavigation(to, from) + if (error) throw error + } + + function runWithContext(fn: () => T): T { + const app: App | undefined = installedApps.values().next().value + // support Vue < 3.3 + return app && typeof app.runWithContext === 'function' + ? app.runWithContext(fn) + : fn() + } + + // TODO: type this as NavigationGuardReturn or similar instead of any + function runGuardQueue(guards: Lazy[]): Promise { + return guards.reduce( + (promise, guard) => promise.then(() => runWithContext(guard)), + Promise.resolve() + ) + } + + let ready: boolean = false + const readyHandlers = useCallbacks<[(v: any) => void, (e: any) => void]>() + + async function resolveNavigationGuards( + to: RouteLocationNormalized, + from: RouteLocationNormalizedLoaded, + navigationInfo?: NavigationInformation + ): Promise { + const [leavingRecords, updatingRecords, enteringRecords] = + extractChangingRecords(to, from) + + // run the queue of per route beforeRouteLeave guards + let guards = extractComponentsGuards( + leavingRecords.reverse(), + 'beforeRouteLeave', + to, + from, + undefined, + navigationInfo + ) + + // leavingRecords is already reversed + for (const record of leavingRecords) { + record.leaveGuards.forEach(guard => { + guards.push(guardToPromiseFn(guard, to, from, { info: navigationInfo })) + }) + } + + const canceledNavigationCheck = async () => { + checkCanceledNavigationAndReject(to, from) + } + + guards.push(canceledNavigationCheck) + await runGuardQueue(guards) + + // check global guards beforeEach + guards = [] + for (const guard of beforeGuards.list()) { + guards.push(guardToPromiseFn(guard, to, from, { info: navigationInfo })) + } + guards.push(canceledNavigationCheck) + await runGuardQueue(guards) + + // check in components beforeRouteUpdate + guards = extractComponentsGuards( + updatingRecords, + 'beforeRouteUpdate', + to, + from, + undefined, + navigationInfo + ) + guards.push(canceledNavigationCheck) + await runGuardQueue(guards) + + // check the route beforeEnter + guards = [] + for (const record of enteringRecords) { + if (record.beforeEnter) { + if (isArray(record.beforeEnter)) { + for (const beforeEnter of record.beforeEnter) + guards.push( + guardToPromiseFn(beforeEnter, to, from, { info: navigationInfo }) + ) + } else { + guards.push( + guardToPromiseFn(record.beforeEnter, to, from, { + info: navigationInfo, + }) + ) + } + } + } + guards.push(canceledNavigationCheck) + await runGuardQueue(guards) + + // NOTE: at this point to.matched is normalized and does not contain any () => Promise + // clear existing enterCallbacks, these are added by extractComponentsGuards + to.matched.forEach(record => (record.enterCallbacks = {})) + + // Resolve async components and run beforeRouteEnter + guards = extractComponentsGuards( + enteringRecords, + 'beforeRouteEnter', + to, + from, + runWithContext, + navigationInfo + ) + guards.push(canceledNavigationCheck) + await runGuardQueue(guards) + + // check global guards beforeResolve + guards = [] + for (const guard of beforeResolveGuards.list()) { + guards.push(guardToPromiseFn(guard, to, from, { info: navigationInfo })) + } + guards.push(canceledNavigationCheck) + await runGuardQueue(guards) + } + + interface FinalizeNavigationOptions { + failure?: NavigationFailure + focus?: { + focusReset: 'after-transition' | 'manual' + selector?: string + } + } + + const { handleFocus, clearFocusTimeout } = createFocusManagementHandler() + + function finalizeNavigation( + to: RouteLocationNormalized, + from: RouteLocationNormalizedLoaded, + options: FinalizeNavigationOptions = {} + ) { + pendingLocation = undefined + const { failure, focus } = options + if (!failure) { + lastSuccessfulLocation = to + } + currentRoute.value = to as RouteLocationNormalizedLoaded + markAsReady() + afterGuards.list().forEach(guard => guard(to, from, failure)) + + if (!failure && focus) { + const { focusReset, selector } = focus + // We only need to handle focus here, to prevent scrolling. + // When focusManagement is false, selector is undefined. + // So we can have the following cases: + // - focusReset: after-transition -> default browser behavior: no action required here + // - focusReset: manual, selector undefined -> no action required here + // - focusReset: manual, selector with value -> prevent scrolling when focusing the target selector element + // We don't need to handle scroll here, the browser or user guards or components lifecycle hooks will handle it + if (focusReset === 'manual' && selector) { + // ensure DOM is updated, enqueuing a microtask before handling focus + nextTick(() => { + handleFocus(selector) + }) + } + } + } + + function markAsReady(err?: any): void { + if (!ready) { + ready = !err + readyHandlers + .list() + // @ts-expect-error we need to add some types + .forEach(([resolve, reject]) => (err ? reject(err) : resolve())) + readyHandlers.reset() + } + } + + function isReady(): Promise { + if (ready && currentRoute.value !== START_LOCATION_NORMALIZED) + return Promise.resolve() + return new Promise((resolve, reject) => { + readyHandlers.add([resolve, reject]) + }) + } + + function triggerError( + error: any, + to: RouteLocationNormalized, + from: RouteLocationNormalizedLoaded, + silent = false + ): Promise { + markAsReady(error) + const list = errorListeners.list() + if (list.length) { + list.forEach(handler => handler(error, to, from)) + } else if (!silent) { + console.error('uncaught error during route navigation:') + console.error(error) + } + return Promise.reject(error) + } + + function go(delta: number) { + // Case 1: go(0) should trigger a reload. + if (delta === 0) { + window.navigation.reload() + return + } + + // Get the current state safely, without using non-null assertions ('!'). + const entries = window.navigation.entries() + const currentIndex = window.navigation.currentEntry?.index + + // If we don't have a current index, we can't proceed. + if (currentIndex === undefined) { + return + } + + // Calculate the target index in the history stack. + const targetIndex = currentIndex + delta + + // Validate that the target index is within the bounds of the entries array. + // This is the key check that prevents runtime errors. + if (targetIndex >= 0 && targetIndex < entries.length) { + // Each history entry has a unique 'key'. We get the key for our target entry... + // Safely get the target entry from the array. + const targetEntry = entries[targetIndex] + + // Add a check to ensure the entry is not undefined before accessing its key. + // This satisfies TypeScript's strict checks. + if (targetEntry) { + window.navigation.traverseTo(targetEntry.key) + } else { + // This case is unlikely if the index check passed, but it adds robustness. + console.warn( + `go(${delta}) failed: No entry found at index ${targetIndex}.` + ) + } + } else { + console.warn( + `go(${delta}) failed: target index ${targetIndex} is out of bounds.` + ) + } + } + + function locationAsObject( + to: RouteLocationRaw | RouteLocationNormalized + ): Exclude | RouteLocationNormalized { + return typeof to === 'string' + ? parseURL(parseQuery, to, currentRoute.value.path) + : assign({}, to) + } + + function handleRedirectRecord(to: RouteLocation): RouteLocationRaw | void { + const lastMatched = to.matched[to.matched.length - 1] + if (lastMatched && lastMatched.redirect) { + const { redirect } = lastMatched + let newTargetLocation = + typeof redirect === 'function' ? redirect(to) : redirect + + if (typeof newTargetLocation === 'string') { + newTargetLocation = + newTargetLocation.includes('?') || newTargetLocation.includes('#') + ? (newTargetLocation = locationAsObject(newTargetLocation)) + : // force empty params + { path: newTargetLocation } + // @ts-expect-error: force empty params when a string is passed to let + // the router parse them again + newTargetLocation.params = {} + } + + if ( + __DEV__ && + newTargetLocation.path == null && + !('name' in newTargetLocation) + ) { + warn( + `Invalid redirect found:\n${JSON.stringify( + newTargetLocation, + null, + 2 + )}\n when navigating to "${ + to.fullPath + }". A redirect must contain a name or path. This will break in production.` + ) + throw new Error('Invalid redirect') + } + + return assign( + { + query: to.query, + hash: to.hash, + // avoid transferring params if the redirect has a path + params: newTargetLocation.path != null ? {} : to.params, + }, + newTargetLocation + ) + } + } + + async function navigate( + to: RouteLocationRaw, + options: { replace?: boolean; state?: any } = {} + ): Promise { + const { replace = false, state } = options + const toLocation = resolve(to) + const from = currentRoute.value + + const redirect = handleRedirectRecord(toLocation) + if (redirect) { + return navigate(assign({ replace }, redirect), { replace: true, state }) + } + + pendingLocation = toLocation as RouteLocationNormalized + + if ( + !(to as RouteLocationOptions).force && + isSameRouteLocation(stringifyQuery, from, toLocation) + ) { + const failure = createRouterError( + ErrorTypes.NAVIGATION_DUPLICATED, + { + to: toLocation as RouteLocationNormalized, + from, + } + ) + finalizeNavigation(from, from, { failure }) + return failure + } + + pendingLocation = toLocation as RouteLocationNormalized + + const navOptions: NavigationNavigateOptions = { + state: (to as RouteLocationOptions).state, + } + if (replace) { + navOptions.history = 'replace' + } + window.navigation.navigate(toLocation.href, navOptions) + } + + const normalizeParams = applyToParams.bind( + null, + paramValue => '' + paramValue + ) + const encodeParams = applyToParams.bind(null, encodeParam) + const decodeParams: (params: RouteParams | undefined) => RouteParams = + // @ts-expect-error: intentionally avoid the type check + applyToParams.bind(null, decode) + + function addRoute( + parentOrRoute: NonNullable | RouteRecordRaw, + route?: RouteRecordRaw + ) { + let parent: Parameters<(typeof matcher)['addRoute']>[1] | undefined + let record: RouteRecordRaw + if (isRouteName(parentOrRoute)) { + parent = matcher.getRecordMatcher(parentOrRoute) + if (__DEV__ && !parent) { + warn( + `Parent route "${String( + parentOrRoute + )}" not found when adding child route`, + route + ) + } + record = route! + } else { + record = parentOrRoute + } + + return matcher.addRoute(record, parent) + } + + function removeRoute(name: NonNullable) { + const recordMatcher = matcher.getRecordMatcher(name) + if (recordMatcher) { + matcher.removeRoute(recordMatcher) + } else if (__DEV__) { + warn(`Cannot remove non-existent route "${String(name)}"`) + } + } + + function getRoutes() { + return matcher.getRoutes().map(routeMatcher => routeMatcher.record) + } + + function hasRoute(name: NonNullable): boolean { + return !!matcher.getRecordMatcher(name) + } + + function createHref(base: string, path: string): string { + if (path === '/') return base || '/' + return ( + (base.endsWith('/') ? base.slice(0, -1) : base) + + (path.startsWith('/') ? '' : '/') + + path + ) + } + + function resolve( + rawLocation: RouteLocationRaw, + currentLocation?: RouteLocationNormalizedLoaded + ): RouteLocationResolved { + // const resolve: Router['resolve'] = (rawLocation: RouteLocationRaw, currentLocation) => { + // const objectLocation = routerLocationAsObject(rawLocation) + // we create a copy to modify it later + currentLocation = assign({}, currentLocation || currentRoute.value) + if (typeof rawLocation === 'string') { + const locationNormalized = parseURL( + parseQuery, + rawLocation, + currentLocation.path + ) + const matchedRoute = matcher.resolve( + { path: locationNormalized.path }, + currentLocation + ) + + const href = createHref(locationNormalized.fullPath, options.base || '/') + if (__DEV__) { + if (href.startsWith('//')) + warn( + `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.` + ) + else if (!matchedRoute.matched.length) { + warn(`No match found for location with path "${rawLocation}"`) + } + } + + // locationNormalized is always a new object + return assign(locationNormalized, matchedRoute, { + params: decodeParams(matchedRoute.params), + hash: decode(locationNormalized.hash), + redirectedFrom: undefined, + href, + }) + } + + if (__DEV__ && !isRouteLocation(rawLocation)) { + warn( + `router.resolve() was passed an invalid location. This will fail in production.\n- Location:`, + rawLocation + ) + return resolve({}) + } + + let matcherLocation: MatcherLocationRaw + + // path could be relative in object as well + if (rawLocation.path != null) { + if ( + __DEV__ && + 'params' in rawLocation && + !('name' in rawLocation) && + // @ts-expect-error: the type is never + Object.keys(rawLocation.params).length + ) { + warn( + `Path "${rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.` + ) + } + matcherLocation = assign({}, rawLocation, { + path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path, + }) + } else { + // remove any nullish param + const targetParams = assign({}, rawLocation.params) + for (const key in targetParams) { + if (targetParams[key] == null) { + delete targetParams[key] + } + } + // pass encoded values to the matcher, so it can produce encoded path and fullPath + matcherLocation = assign({}, rawLocation, { + params: encodeParams(targetParams), + }) + // current location params are decoded, we need to encode them in case the + // matcher merges the params + currentLocation.params = encodeParams(currentLocation.params) + } + + const matchedRoute = matcher.resolve(matcherLocation, currentLocation) + const hash = rawLocation.hash || '' + + if (__DEV__ && hash && !hash.startsWith('#')) { + warn( + `A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".` + ) + } + + // the matcher might have merged current location params, so + // we need to run the decoding again + matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params)) + + const fullPath = stringifyURL( + stringifyQuery, + assign({}, rawLocation, { + hash: encodeHash(hash), + path: matchedRoute.path, + }) + ) + + const href = createHref(fullPath, options.base || '/') + if (__DEV__) { + if (href.startsWith('//')) { + warn( + `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.` + ) + } else if (!matchedRoute.matched.length) { + warn( + `No match found for location with path "${ + rawLocation.path != null ? rawLocation.path : rawLocation + }"` + ) + } + } + + return assign( + { + fullPath, + // keep the hash encoded so fullPath is effectively path + encodedQuery + + // hash + hash, + query: + // if the user is using a custom query lib like qs, we might have + // nested objects, so we keep the query as is, meaning it can contain + // numbers at `$route.query`, but at the point, the user will have to + // use their own type anyway. + // https://github.com/vuejs/router/issues/328#issuecomment-649481567 + stringifyQuery === originalStringifyQuery + ? normalizeQuery(rawLocation.query) + : ((rawLocation.query || {}) as LocationQuery), + }, + matchedRoute, + { + redirectedFrom: undefined, + href, + } + ) + } + + function prepareTargetLocation( + event: NavigateEvent + ): RouteLocationNormalized { + if (!pendingLocation) { + const destination = new URL(event.destination.url) + const pathWithSearchAndHash = + destination.pathname + destination.search + destination.hash + return resolve(pathWithSearchAndHash) as RouteLocationNormalized + } + + return pendingLocation as RouteLocationNormalized + } + + function prepareScrollManagement( + to: RouteLocationNormalized + ): 'after-transition' | 'manual' { + let scrollManagement: 'after-transition' | 'manual' = 'after-transition' + const scrollMeta = to.meta.scrollManagement ?? options.scrollManagement + if (scrollMeta === 'manual') { + scrollManagement = 'manual' + } + + return scrollManagement + } + + async function handleNavigate(event: NavigateEvent) { + clearFocusTimeout() + + // won't handle here 'traverse' navigations to avoid race conditions, see handleCurrentEntryChange + if (!event.canIntercept || event.navigationType === 'traverse') { + return + } + + const targetLocation = prepareTargetLocation(event) + const from = currentRoute.value + + // the calculation should be here, if running this logic inside the intercept handler + // the back and forward buttons cannot be detected properly since the currentEntry + // is already updated when the handler is executed. + let navigationInfo: NavigationInformation | undefined + if (event.navigationType === 'push' || event.navigationType === 'replace') { + navigationInfo = { + type: + event.navigationType === 'push' + ? NavigationType.push + : NavigationType.pop, + direction: NavigationDirection.unknown, // No specific direction for push/replace. + delta: event.navigationType === 'push' ? 1 : 0, + navigationApiEvent: event, + } + } + + const [focusReset, focusSelector] = prepareFocusReset( + targetLocation, + options.focusManagement + ) + + event.intercept({ + focusReset, + scroll: prepareScrollManagement(targetLocation), + async handler() { + if (!pendingLocation) { + pendingLocation = targetLocation + } + + const to = pendingLocation as RouteLocationNormalized + + if ( + from !== START_LOCATION_NORMALIZED && + !(to as RouteLocationOptions).force && + isSameRouteLocation(stringifyQuery, from, to) + ) { + return + } + + try { + await resolveNavigationGuards(to, from, navigationInfo) + finalizeNavigation(to, from, { + focus: { + focusReset, + selector: focusSelector, + }, + }) + } catch (error) { + const failure = error as NavigationFailure + + afterGuards.list().forEach(guard => guard(to, from, failure)) + + if ( + isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT) + ) { + navigate((failure as NavigationRedirectError).to, { replace: true }) + } else if ( + !isNavigationFailure(failure, ErrorTypes.NAVIGATION_CANCELLED) + ) { + triggerError(failure, to, from) + } + throw failure + } finally { + // update always, we'll have some race condition it the user clicks 2 links + pendingLocation = undefined + } + }, + }) + } + + async function handleCurrentEntryChange( + event: NavigationCurrentEntryChangeEvent + ) { + clearFocusTimeout() + if (isRevertingNavigation) { + isRevertingNavigation = false + return + } + + if (event.navigationType !== 'traverse') { + return + } + + const destination = new URL(window.navigation.currentEntry!.url!) + const pathWithSearchAndHash = + destination.pathname + destination.search + destination.hash + const to = resolve(pathWithSearchAndHash) as RouteLocationNormalized + const from = lastSuccessfulLocation + + const fromIndex = event.from.index + const toIndex = window.navigation.currentEntry!.index + const delta = toIndex - fromIndex + const navigationInfo: NavigationInformation = { + type: NavigationType.pop, + direction: + delta > 0 ? NavigationDirection.forward : NavigationDirection.back, + delta, + isBackBrowserButton: delta < 0, + isForwardBrowserButton: delta > 0, + } + + const [focusReset, focusSelector] = prepareFocusReset( + to, + options.focusManagement + ) + + pendingLocation = to + + try { + // then browser has been done the navigation, we just run the guards + await resolveNavigationGuards(to, from, navigationInfo) + finalizeNavigation(to, from, { + focus: { focusReset, selector: focusSelector }, + }) + } catch (error) { + const failure = error as NavigationFailure + + isRevertingNavigation = true + go(event.from.index - window.navigation.currentEntry!.index) + + // we end up at from to keep consistency + finalizeNavigation(from, to, { failure }) + + if (isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)) { + navigate((failure as NavigationRedirectError).to, { replace: true }) + } else if ( + !isNavigationFailure(failure, ErrorTypes.NAVIGATION_CANCELLED) + ) { + // just ignore errors caused by the cancellation of the navigation + triggerError(failure, to, from, true).catch(() => {}) + } + } finally { + // update always, we'll have some race condition it the user clicks 2 links + pendingLocation = undefined + } + } + + window.navigation.addEventListener('navigate', handleNavigate) + window.navigation.addEventListener( + 'currententrychange', + handleCurrentEntryChange + ) + + function destroy() { + window.navigation.removeEventListener('navigate', handleNavigate) + window.navigation.removeEventListener( + 'currententrychange', + handleCurrentEntryChange + ) + } + + const history: RouterHistory = { + base: options.base || '/', + location: options.location, + state: undefined!, + createHref: createHref.bind(null, options.base || '/'), + destroy, + go, + listen(): () => void { + throw new Error('unsupported operation') + }, + push: (to: RouteLocationRaw) => navigate(to), + replace: (to: RouteLocationRaw) => navigate(to, { replace: true }), + } + + let beforeResolveTransitionGuard: (() => void) | undefined + let afterEachTransitionGuard: (() => void) | undefined + let onErrorTransitionGuard: (() => void) | undefined + + function cleanupNativeViewTransition() { + beforeResolveTransitionGuard?.() + afterEachTransitionGuard?.() + onErrorTransitionGuard?.() + } + + const router: Router = { + name: 'navigation-api', + currentRoute, + listening: true, + + addRoute, + removeRoute, + clearRoutes: matcher.clearRoutes, + hasRoute, + getRoutes, + resolve, + options: { + ...options, + history, + }, + + push: (to: RouteLocationRaw) => navigate(to), + replace: (to: RouteLocationRaw) => navigate(to, { replace: true }), + go, + back: () => go(-1), + forward: () => go(1), + + beforeEach: beforeGuards.add, + beforeResolve: beforeResolveGuards.add, + afterEach: afterGuards.add, + + onError: errorListeners.add, + isReady, + + enableViewTransition(options) { + cleanupNativeViewTransition() + + if (typeof document === 'undefined' || !document.startViewTransition) { + return + } + + if (transitionMode !== 'view-transition') { + if (__DEV__) { + console.warn('Native View Transition is disabled in auto mode.') + } + return + } + + const defaultTransitionSetting = options.defaultViewTransition ?? true + + let finishTransition: (() => void) | undefined + let abortTransition: (() => void) | undefined + + const resetTransitionState = () => { + finishTransition = undefined + abortTransition = undefined + } + + beforeResolveTransitionGuard = this.beforeResolve( + async (to, from, next) => { + const transitionMode = + to.meta.viewTransition ?? defaultTransitionSetting + if ( + transitionMode === false || + (transitionMode !== 'always' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches) || + !isChangingPage(to, from) + ) { + next(true) + return + } + + const promise = new Promise((resolve, reject) => { + finishTransition = resolve + abortTransition = reject + }) + + const transition = document.startViewTransition(() => promise) + + await options.onStart?.(transition) + transition.finished + .then(() => options.onFinished?.(transition)) + .catch(() => options.onAborted?.(transition)) + .finally(resetTransitionState) + + next(true) + } + ) + + afterEachTransitionGuard = this.afterEach(() => { + finishTransition?.() + }) + + onErrorTransitionGuard = this.onError((error, to, from) => { + abortTransition?.() + resetTransitionState() + }) + }, + + install(app) { + app.component('RouterLink', RouterLink) + app.component('RouterView', RouterView) + + app.config.globalProperties.$router = router + Object.defineProperty(app.config.globalProperties, '$route', { + enumerable: true, + get: () => unref(currentRoute), + }) + + // this initial navigation is only necessary on client, on server it doesn't + // make sense because it will create an extra unnecessary navigation and could + // lead to problems + if ( + isBrowser && + // used for the initial navigation client side to avoid pushing + // multiple times when the router is used in multiple apps + !started && + currentRoute.value === START_LOCATION_NORMALIZED + ) { + // see above + started = true + const initialLocation = + window.location.pathname + + window.location.search + + window.location.hash + navigate(initialLocation).catch(err => { + if (__DEV__) warn('Unexpected error when starting the router:', err) + }) + } + + const reactiveRoute = {} as RouteLocationNormalizedLoaded + for (const key in START_LOCATION_NORMALIZED) { + Object.defineProperty(reactiveRoute, key, { + get: () => currentRoute.value[key as keyof RouteLocationNormalized], + enumerable: true, + }) + } + + app.provide(routerKey, router) + app.provide(routeLocationKey, shallowReactive(reactiveRoute)) + app.provide(routerViewLocationKey, currentRoute) + app.provide(transitionModeKey, transitionMode) + + const unmountApp = app.unmount + installedApps.add(app) + app.unmount = function () { + installedApps.delete(app) + // the router is not attached to an app anymore + if (installedApps.size < 1) { + // invalidate the current navigation + pendingLocation = START_LOCATION_NORMALIZED + currentRoute.value = START_LOCATION_NORMALIZED + cleanupNativeViewTransition() + started = false + ready = false + } + unmountApp() + } + }, + } + + return router +} + +function extractChangingRecords( + to: RouteLocationNormalized, + from: RouteLocationNormalizedLoaded +) { + const leavingRecords: RouteRecordNormalized[] = [] + const updatingRecords: RouteRecordNormalized[] = [] + const enteringRecords: RouteRecordNormalized[] = [] + + const len = Math.max(from.matched.length, to.matched.length) + for (let i = 0; i < len; i++) { + const recordFrom = from.matched[i] + if (recordFrom) { + if (to.matched.find(record => isSameRouteRecord(record, recordFrom))) + updatingRecords.push(recordFrom) + else leavingRecords.push(recordFrom) + } + const recordTo = to.matched[i] + if (recordTo) { + // the type doesn't matter because we are comparing per reference + if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) { + enteringRecords.push(recordTo) + } + } + } + + return [leavingRecords, updatingRecords, enteringRecords] +} diff --git a/packages/router/src/navigationGuards.ts b/packages/router/src/navigationGuards.ts index db53c3dc1..ebe160b46 100644 --- a/packages/router/src/navigationGuards.ts +++ b/packages/router/src/navigationGuards.ts @@ -22,6 +22,7 @@ import { matchedRouteKey } from './injectionSymbols' import { RouteRecordNormalized } from './matcher/types' import { isESModule, isRouteComponent } from './utils' import { warn } from './warning' +import { NavigationInformation } from './history/common' function registerGuard( record: RouteRecordNormalized, @@ -106,27 +107,21 @@ export function onBeforeRouteUpdate(updateGuard: NavigationGuard) { registerGuard(activeRecord, 'updateGuards', updateGuard) } -export function guardToPromiseFn( - guard: NavigationGuard, - to: RouteLocationNormalized, - from: RouteLocationNormalizedLoaded -): () => Promise -export function guardToPromiseFn( - guard: NavigationGuard, - to: RouteLocationNormalized, - from: RouteLocationNormalizedLoaded, - record: RouteRecordNormalized, - name: string, - runWithContext: (fn: () => T) => T -): () => Promise +interface GuardToPromiseFnOptions { + record?: RouteRecordNormalized + name?: string + runWithContext?: (fn: () => T) => T + info?: NavigationInformation +} + export function guardToPromiseFn( guard: NavigationGuard, to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded, - record?: RouteRecordNormalized, - name?: string, - runWithContext: (fn: () => T) => T = fn => fn() + options: GuardToPromiseFnOptions = {} ): () => Promise { + const { record, name, runWithContext = fn => fn(), info } = options + // keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place const enterCallbackArray = record && @@ -174,14 +169,26 @@ export function guardToPromiseFn( } // wrapping with Promise.resolve allows it to work with both async and sync guards - const guardReturn = runWithContext(() => - guard.call( - record && record.instances[name!], - to, - from, - __DEV__ ? canOnlyBeCalledOnce(next, to, from) : next - ) - ) + const guardReturn = + guard.length > 3 + ? runWithContext(() => + guard.call( + record && record.instances[name!], + to, + from, + __DEV__ ? canOnlyBeCalledOnce(next, to, from, info) : next, + info + ) + ) + : runWithContext(() => + guard.call( + record && record.instances[name!], + to, + from, + __DEV__ ? canOnlyBeCalledOnce(next, to, from) : next + ) + ) + let guardCall = Promise.resolve(guardReturn) if (guard.length < 3) guardCall = guardCall.then(next) @@ -214,14 +221,19 @@ export function guardToPromiseFn( function canOnlyBeCalledOnce( next: NavigationGuardNext, to: RouteLocationNormalized, - from: RouteLocationNormalized + from: RouteLocationNormalized, + info?: NavigationInformation ): NavigationGuardNext { let called = 0 return function () { - if (called++ === 1) + if (called++ === 1) { + const showInfo = info + ? ` (type=${info.type},direction=${info.direction},delta=${info.delta},back=${info.isBackBrowserButton === true},forward=${info.isForwardBrowserButton === true})` + : '' warn( - `The "next" callback was called more than once in one navigation guard when going from "${from.fullPath}" to "${to.fullPath}". It should be called exactly one time in each navigation guard. This will fail in production.` + `The "next" callback was called more than once in one navigation guard when going from "${from.fullPath}" to "${to.fullPath}${showInfo}". It should be called exactly one time in each navigation guard. This will fail in production.` ) + } // @ts-expect-error: we put it in the original one because it's easier to check next._called = true if (called === 1) next.apply(null, arguments as any) @@ -235,7 +247,8 @@ export function extractComponentsGuards( guardType: GuardType, to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded, - runWithContext: (fn: () => T) => T = fn => fn() + runWithContext: (fn: () => T) => T = fn => fn(), + info?: NavigationInformation ) { const guards: Array<() => Promise> = [] @@ -298,7 +311,12 @@ export function extractComponentsGuards( const guard = options[guardType] guard && guards.push( - guardToPromiseFn(guard, to, from, record, name, runWithContext) + guardToPromiseFn(guard, to, from, { + record, + name, + runWithContext, + info, + }) ) } else { // start requesting the chunk already @@ -334,7 +352,12 @@ export function extractComponentsGuards( return ( guard && - guardToPromiseFn(guard, to, from, record, name, runWithContext)() + guardToPromiseFn(guard, to, from, { + record, + name, + runWithContext, + info, + })() ) }) ) diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index bc448bbd5..7663f817e 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -20,7 +20,12 @@ import type { RouteLocationAsString, RouteRecordNameGeneric, } from './typed-routes' -import { RouterHistory, HistoryState, NavigationType } from './history/common' +import { + RouterHistory, + HistoryState, + NavigationType, + NavigationInformation, +} from './history/common' import { ScrollPosition, getSavedScrollPosition, @@ -66,9 +71,15 @@ import { routerViewLocationKey, } from './injectionSymbols' import { addDevtools } from './devtools' -import { _LiteralUnion } from './types/utils' import { RouteLocationAsRelativeTyped } from './typed-routes/route-location' import { RouteMap } from './typed-routes/route-map' +import { + RouterViewTransition, + TransitionMode, + transitionModeKey, +} from './transition' +import { isChangingPage } from './utils/routes' +import { enableFocusManagement } from './focus' /** * Internal type to define an ErrorHandler @@ -102,7 +113,8 @@ export interface RouterScrollBehavior { ( to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded, - savedPosition: _ScrollPositionNormalized | null + savedPosition: _ScrollPositionNormalized | null, + info?: NavigationInformation ): Awaitable } @@ -181,12 +193,37 @@ export interface RouterOptions extends PathParserOptions { * `router-link-inactive` will be applied. */ // linkInactiveClass?: string + /** + * Focus management. + * + * This can be overridden per route by passing `focusManagement` in the route meta, will take precedence over this option. + * + * If `undefined`, the router will not manage focus: will use the [default behavior](https://developer.mozilla.org/en-US/docs/Web/API/NavigateEvent/intercept#focusreset). + * + * If `true`, the router will focus the first element in the dom using `document.querySelector('[autofocus], h1, main, body')`. + * + * If `false`, the router and the browser will not manage the focus, the consumer should manage the focus in the router guards or target page components. + * + * If a `string`, the router will use `document.querySelector(focusManagement)` to find the element to be focused, if the element is not found, then it will try to find the element using the selector when the option is `true`. + * + * @default undefined + */ + focusManagement?: boolean | string + /** + * Enable automatic scroll restoration when navigating the history. + * + * Enabling this option, will register a custom `scrollBehavior` if none is provided. + * + * `focusManagement` and this option are just used to enable some sort of "polyfills" for browsers that do not support the Navigation API. + */ + enableScrollManagement?: true } /** * Router instance. */ export interface Router { + readonly name: 'legacy' | 'navigation-api' /** * @internal */ @@ -205,6 +242,15 @@ export interface Router { */ listening: boolean + /** + * Enable native view transition. + * + * NOTE: will be a no-op if the browser does not support it. + * + * @param options The options to use. + */ + enableViewTransition(options: RouterViewTransition): void + /** * Add a new {@link RouteRecordRaw | route record} as the child of an existing route. * @@ -216,6 +262,7 @@ export interface Router { parentName: NonNullable, route: RouteRecordRaw ): () => void + /** * Add a new {@link RouteRecordRaw | route record} to the router. * @@ -381,8 +428,12 @@ export interface Router { * Creates a Router instance that can be used by a Vue app. * * @param options - {@link RouterOptions} + * @param transitionMode The new transition mode option. */ -export function createRouter(options: RouterOptions): Router { +export function createRouter( + options: RouterOptions, + transitionMode: TransitionMode = 'auto' +): Router { const matcher = createRouterMatcher(options.routes, options) const parseQuery = options.parseQuery || originalParseQuery const stringifyQuery = options.stringifyQuery || originalStringifyQuery @@ -923,7 +974,8 @@ export function createRouter(options: RouterOptions): Router { 'beforeRouteEnter', to, from, - runWithContext + runWithContext, + undefined ) guards.push(canceledNavigationCheck) @@ -1028,6 +1080,7 @@ export function createRouter(options: RouterOptions): Router { return } + toLocation.meta.__info = info pendingLocation = toLocation const from = currentRoute.value @@ -1200,6 +1253,23 @@ export function createRouter(options: RouterOptions): Router { return err } + const { enableScrollManagement, scrollBehavior } = options + + const useScrollBehavior: RouterScrollBehavior | undefined = + scrollBehavior ?? + (enableScrollManagement + ? async (to, from, savedPosition, info) => { + await nextTick() + if (info?.type === 'pop' && savedPosition) { + return scrollToPosition(savedPosition) + } + if (to.hash) { + return scrollToPosition({ el: to.hash, behavior: 'smooth' }) + } + return scrollToPosition({ top: 0, left: 0 }) + } + : undefined) + // Scroll behavior function handleScroll( to: RouteLocationNormalizedLoaded, @@ -1208,8 +1278,9 @@ export function createRouter(options: RouterOptions): Router { isFirstNavigation: boolean ): // the return is not meant to be used Promise { - const { scrollBehavior } = options - if (!isBrowser || !scrollBehavior) return Promise.resolve() + const info = to.meta.__info as NavigationInformation | undefined + delete to.meta.__info + if (!isBrowser || !useScrollBehavior) return Promise.resolve() const scrollPosition: _ScrollPositionNormalized | null = (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) || @@ -1219,7 +1290,7 @@ export function createRouter(options: RouterOptions): Router { null return nextTick() - .then(() => scrollBehavior(to, from, scrollPosition)) + .then(() => useScrollBehavior(to, from, scrollPosition, info)) .then(position => position && scrollToPosition(position)) .catch(err => triggerError(err, to, from)) } @@ -1229,7 +1300,22 @@ export function createRouter(options: RouterOptions): Router { let started: boolean | undefined const installedApps = new Set() + let beforeResolveTransitionGuard: (() => void) | undefined + let afterEachTransitionGuard: (() => void) | undefined + let onErrorTransitionGuard: (() => void) | undefined + let popStateListener: ((event: PopStateEvent) => void) | undefined + + function cleanupNativeViewTransition() { + beforeResolveTransitionGuard?.() + afterEachTransitionGuard?.() + onErrorTransitionGuard?.() + if (typeof window !== 'undefined' && popStateListener) { + window.removeEventListener('popstate', popStateListener) + } + } + const router: Router = { + name: 'legacy', currentRoute, listening: true, @@ -1254,6 +1340,28 @@ export function createRouter(options: RouterOptions): Router { onError: errorListeners.add, isReady, + enableViewTransition(options) { + cleanupNativeViewTransition() + + if (typeof document === 'undefined' || !document.startViewTransition) { + return + } + + if (transitionMode !== 'view-transition') { + if (__DEV__) { + console.warn('Native View Transition is disabled in auto mode.') + } + return + } + + ;[ + beforeResolveTransitionGuard, + afterEachTransitionGuard, + onErrorTransitionGuard, + popStateListener, + ] = enableViewTransition(this, options) + }, + install(app: App) { app.component('RouterLink', RouterLink) app.component('RouterView', RouterView) @@ -1264,6 +1372,8 @@ export function createRouter(options: RouterOptions): Router { get: () => unref(currentRoute), }) + let cleanupFocusManagement: (() => void) | undefined + // this initial navigation is only necessary on client, on server it doesn't // make sense because it will create an extra unnecessary navigation and could // lead to problems @@ -1274,6 +1384,9 @@ export function createRouter(options: RouterOptions): Router { !started && currentRoute.value === START_LOCATION_NORMALIZED ) { + if ('focusManagement' in options) { + cleanupFocusManagement = enableFocusManagement(router) + } // see above started = true push(routerHistory.location).catch(err => { @@ -1292,6 +1405,7 @@ export function createRouter(options: RouterOptions): Router { app.provide(routerKey, router) app.provide(routeLocationKey, shallowReactive(reactiveRoute)) app.provide(routerViewLocationKey, currentRoute) + app.provide(transitionModeKey, transitionMode) const unmountApp = app.unmount installedApps.add(app) @@ -1304,6 +1418,8 @@ export function createRouter(options: RouterOptions): Router { removeHistoryListener && removeHistoryListener() removeHistoryListener = null currentRoute.value = START_LOCATION_NORMALIZED + cleanupFocusManagement?.() + cleanupNativeViewTransition() started = false ready = false } @@ -1355,3 +1471,80 @@ function extractChangingRecords( return [leavingRecords, updatingRecords, enteringRecords] } + +function enableViewTransition(router: Router, options: RouterViewTransition) { + let transition: undefined | ViewTransition + let hasUAVisualTransition = false + let finishTransition: (() => void) | undefined + let abortTransition: (() => void) | undefined + + const defaultTransitionSetting = options?.defaultViewTransition ?? true + + const resetTransitionState = () => { + transition = undefined + hasUAVisualTransition = false + abortTransition = undefined + finishTransition = undefined + } + + function popStateListener(event: PopStateEvent) { + hasUAVisualTransition = event.hasUAVisualTransition + if (hasUAVisualTransition) { + transition?.skipTransition() + } + } + + window.addEventListener('popstate', popStateListener) + + const beforeResolveTransitionGuard = router.beforeResolve( + async (to, from) => { + const transitionMode = to.meta.viewTransition ?? defaultTransitionSetting + if ( + hasUAVisualTransition || + transitionMode === false || + (transitionMode !== 'always' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches) || + !isChangingPage(to, from) + ) { + return + } + + const promise = new Promise((resolve, reject) => { + finishTransition = resolve + abortTransition = reject + }) + + let changeRoute: () => void + const ready = new Promise(resolve => (changeRoute = resolve)) + + const transition = document.startViewTransition(() => { + changeRoute() + return promise + }) + + await options.onStart?.(transition) + transition.finished + .then(() => options.onFinished?.(transition)) + .catch(() => options.onAborted?.(transition)) + .finally(resetTransitionState) + + return ready + } + ) + + const afterEachTransitionGuard = router.afterEach(() => { + finishTransition?.() + }) + + const onErrorTransitionGuard = router.onError(() => { + abortTransition?.() + resetTransitionState() + }) + + return [ + beforeResolveTransitionGuard, + afterEachTransitionGuard, + onErrorTransitionGuard, + popStateListener, + ] as const +} diff --git a/packages/router/src/transition.ts b/packages/router/src/transition.ts new file mode 100644 index 000000000..39bbaea5b --- /dev/null +++ b/packages/router/src/transition.ts @@ -0,0 +1,26 @@ +import type { InjectionKey } from 'vue' +import { inject } from 'vue' + +export type TransitionMode = 'auto' | 'view-transition' + +export const transitionModeKey = Symbol( + __DEV__ ? 'transition mode' : '' +) as InjectionKey + +export function injectTransitionMode(): TransitionMode { + return inject(transitionModeKey, 'auto') +} + +export type RouteViewTransitionHook = ( + transition: ViewTransition +) => void | Promise + +export interface RouterViewTransition { + defaultViewTransition?: boolean | 'always' + /** Hook called right after the view transition starts */ + onStart?: RouteViewTransitionHook + /** Hook called when the view transition animation is finished */ + onFinished?: RouteViewTransitionHook + /** Hook called if the transition is aborted */ + onAborted?: RouteViewTransitionHook +} diff --git a/packages/router/src/typed-routes/navigation-guards.ts b/packages/router/src/typed-routes/navigation-guards.ts index ab624e1f7..35e6bfba7 100644 --- a/packages/router/src/typed-routes/navigation-guards.ts +++ b/packages/router/src/typed-routes/navigation-guards.ts @@ -6,6 +6,7 @@ import type { } from './route-location' import type { TypesConfig } from '../config' import type { NavigationFailure } from '../errors' +import type { NavigationInformation } from '../history/common' import { ComponentPublicInstance } from 'vue' /** @@ -25,7 +26,8 @@ export interface NavigationGuardWithThis { to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded, // intentionally not typed to make people use the return - next: NavigationGuardNext + next: NavigationGuardNext, + info?: NavigationInformation ): _Awaitable } @@ -41,7 +43,8 @@ export interface _NavigationGuardResolved { to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded, // intentionally not typed to make people use the return - next: NavigationGuardNext + next: NavigationGuardNext, + info?: NavigationInformation ): _Awaitable } @@ -53,7 +56,8 @@ export interface NavigationGuard { to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded, // intentionally not typed to make people use the return - next: NavigationGuardNext + next: NavigationGuardNext, + info?: NavigationInformation ): _Awaitable } diff --git a/packages/router/src/utils/routes.ts b/packages/router/src/utils/routes.ts new file mode 100644 index 000000000..777a9fea2 --- /dev/null +++ b/packages/router/src/utils/routes.ts @@ -0,0 +1,46 @@ +import { RouteLocationNormalized } from '../typed-routes' +import { START_LOCATION_NORMALIZED } from '../location' + +// from Nuxt +const ROUTE_KEY_PARENTHESES_RE = /(:\w+)\([^)]+\)/g +const ROUTE_KEY_SYMBOLS_RE = /(:\w+)[?+*]/g +const ROUTE_KEY_NORMAL_RE = /:\w+/g +// TODO: consider refactoring into single utility +// See https://github.com/nuxt/nuxt/tree/main/packages/nuxt/src/pages/runtime/utils.ts#L8-L19 +function generateRouteKey(route: RouteLocationNormalized) { + const source = + route?.meta.key ?? + route.path + .replace(ROUTE_KEY_PARENTHESES_RE, '$1') + .replace(ROUTE_KEY_SYMBOLS_RE, '$1') + .replace( + ROUTE_KEY_NORMAL_RE, + r => route.params[r.slice(1)]?.toString() || '' + ) + return typeof source === 'function' ? source(route) : source +} + +export function isChangingPage( + to: RouteLocationNormalized, + from: RouteLocationNormalized +) { + if ( + to === START_LOCATION_NORMALIZED || + to === from || + from === START_LOCATION_NORMALIZED + ) { + return false + } + + // If route keys are different then it will result in a rerender + if (generateRouteKey(to) !== generateRouteKey(from)) { + return true + } + + const areComponentsSame = to.matched.every( + (comp, index) => + comp.components && + comp.components.default === from.matched[index]?.components?.default + ) + return !areComponentsSame +} diff --git a/packages/router/tsconfig.json b/packages/router/tsconfig.json index 41fc6c378..e54beb84f 100644 --- a/packages/router/tsconfig.json +++ b/packages/router/tsconfig.json @@ -34,7 +34,8 @@ ], "types": [ "node", - "vite/client" + "vite/client", + "dom-navigation" ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e238c902..53665e518 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@types/dom-navigation': + specifier: ^1.0.6 + version: 1.0.6 '@vitest/coverage-v8': specifier: ^2.1.9 version: 2.1.9(vitest@2.1.9(@types/node@22.15.2)(@vitest/ui@2.1.9)(happy-dom@15.11.7)(jsdom@19.0.0)(terser@5.32.0)) @@ -48,14 +51,14 @@ importers: specifier: ^2.13.0 version: 2.13.0 typedoc: - specifier: ^0.26.11 - version: 0.26.11(typescript@5.6.3) + specifier: ^0.28.13 + version: 0.28.13(typescript@5.8.3) typedoc-plugin-markdown: - specifier: ^4.2.10 - version: 4.2.10(typedoc@0.26.11(typescript@5.6.3)) + specifier: ^4.8.1 + version: 4.8.1(typedoc@0.28.13(typescript@5.8.3)) typescript: - specifier: ~5.6.3 - version: 5.6.3 + specifier: ~5.8.2 + version: 5.8.3 vitest: specifier: ^2.1.9 version: 2.1.9(@types/node@22.15.2)(@vitest/ui@2.1.9)(happy-dom@15.11.7)(jsdom@19.0.0)(terser@5.32.0) @@ -67,10 +70,10 @@ importers: version: 3.27.0 vitepress: specifier: 1.5.0 - version: 1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.6.3) + version: 1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.8.3) vitepress-translation-helper: specifier: ^0.2.1 - version: 0.2.1(vitepress@1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.6.3))(vue@3.5.13(typescript@5.6.3)) + version: 0.2.1(vitepress@1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3)) vue-router: specifier: workspace:* version: link:../router @@ -79,20 +82,20 @@ importers: dependencies: vue: specifier: ~3.5.13 - version: 3.5.13(typescript@5.6.3) + version: 3.5.13(typescript@5.8.3) devDependencies: '@types/node': specifier: ^20.17.31 version: 20.17.31 '@vitejs/plugin-vue': specifier: ^5.2.3 - version: 5.2.3(vite@5.4.18(@types/node@20.17.31)(terser@5.32.0))(vue@3.5.13(typescript@5.6.3)) + version: 5.2.3(vite@5.4.18(@types/node@20.17.31)(terser@5.32.0))(vue@3.5.13(typescript@5.8.3)) '@vue/compiler-sfc': specifier: ~3.5.13 version: 3.5.13 '@vue/tsconfig': specifier: ^0.6.0 - version: 0.6.0(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3)) + version: 0.6.0(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)) vite: specifier: ^5.4.18 version: 5.4.18(@types/node@20.17.31)(terser@5.32.0) @@ -101,7 +104,7 @@ importers: version: link:../router vue-tsc: specifier: ^2.2.10 - version: 2.2.10(typescript@5.6.3) + version: 2.2.10(typescript@5.8.3) packages/router: dependencies: @@ -110,8 +113,8 @@ importers: version: 6.6.4 devDependencies: '@microsoft/api-extractor': - specifier: ^7.48.0 - version: 7.48.0(@types/node@22.15.2) + specifier: ^7.52.13 + version: 7.52.13(@types/node@22.15.2) '@rollup/plugin-alias': specifier: ^5.1.1 version: 5.1.1(rollup@3.29.5) @@ -127,6 +130,9 @@ importers: '@rollup/plugin-terser': specifier: ^0.4.4 version: 0.4.4(rollup@3.29.5) + '@types/dom-navigation': + specifier: ^1.0.6 + version: 1.0.6 '@types/jsdom': specifier: ^21.1.7 version: 21.1.7 @@ -135,13 +141,13 @@ importers: version: 2.3.32 '@vitejs/plugin-vue': specifier: ^5.2.3 - version: 5.2.3(vite@5.4.18(@types/node@22.15.2)(terser@5.32.0))(vue@3.5.13(typescript@5.6.3)) + version: 5.2.3(vite@5.4.18(@types/node@22.15.2)(terser@5.32.0))(vue@3.5.13(typescript@5.8.3)) '@vue/compiler-sfc': specifier: ~3.5.13 version: 3.5.13 '@vue/server-renderer': specifier: ~3.5.13 - version: 3.5.13(vue@3.5.13(typescript@5.6.3)) + version: 3.5.13(vue@3.5.13(typescript@5.8.3)) '@vue/test-utils': specifier: ^2.4.6 version: 2.4.6 @@ -186,13 +192,13 @@ importers: version: 4.0.0 rollup-plugin-typescript2: specifier: ^0.36.0 - version: 0.36.0(rollup@3.29.5)(typescript@5.6.3) + version: 0.36.0(rollup@3.29.5)(typescript@5.8.3) vite: specifier: ^5.4.18 version: 5.4.18(@types/node@22.15.2)(terser@5.32.0) vue: specifier: ~3.5.13 - version: 3.5.13(typescript@5.6.3) + version: 3.5.13(typescript@5.8.3) packages: @@ -465,6 +471,9 @@ packages: cpu: [x64] os: [win32] + '@gerrit0/mini-shiki@3.12.2': + resolution: {integrity: sha512-HKZPmO8OSSAAo20H2B3xgJdxZaLTwtlMwxg0967scnrDlPwe6j5+ULGHyIqwgTbFCn9yv/ff8CmfWZLE9YKBzA==} + '@hutson/parse-repository-url@3.0.2': resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} engines: {node: '>=6.9.0'} @@ -475,6 +484,14 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -510,11 +527,11 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} - '@microsoft/api-extractor-model@7.30.0': - resolution: {integrity: sha512-26/LJZBrsWDKAkOWRiQbdVgcfd1F3nyJnAiJzsAgpouPk7LtOIj7PK9aJtBaw/pUXrkotEg27RrT+Jm/q0bbug==} + '@microsoft/api-extractor-model@7.30.7': + resolution: {integrity: sha512-TBbmSI2/BHpfR9YhQA7nH0nqVmGgJ0xH0Ex4D99/qBDAUpnhA2oikGmdXanbw9AWWY/ExBYIpkmY8dBHdla3YQ==} - '@microsoft/api-extractor@7.48.0': - resolution: {integrity: sha512-FMFgPjoilMUWeZXqYRlJ3gCVRhB7WU/HN88n8OLqEsmsG4zBdX/KQdtJfhq95LQTQ++zfu0Em1LLb73NqRCLYQ==} + '@microsoft/api-extractor@7.52.13': + resolution: {integrity: sha512-K6/bBt8zZfn9yc06gNvA+/NlBGJC/iJlObpdufXHEJtqcD4Dln4ITCLZpwP3DNZ5NyBFeTkKdv596go3V72qlA==} hasBin: true '@microsoft/tsdoc-config@0.17.1': @@ -700,8 +717,8 @@ packages: cpu: [x64] os: [win32] - '@rushstack/node-core-library@5.10.0': - resolution: {integrity: sha512-2pPLCuS/3x7DCd7liZkqOewGM0OzLyCacdvOe8j6Yrx9LkETGnxul1t7603bIaB8nUAooORcct9fFDOQMbWAgw==} + '@rushstack/node-core-library@5.14.0': + resolution: {integrity: sha512-eRong84/rwQUlATGFW3TMTYVyqL1vfW9Lf10PH+mVGfIb9HzU3h5AASNIw+axnBLjnD0n3rT5uQBwu9fvzATrg==} peerDependencies: '@types/node': '*' peerDependenciesMeta: @@ -711,16 +728,16 @@ packages: '@rushstack/rig-package@0.5.3': resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==} - '@rushstack/terminal@0.14.3': - resolution: {integrity: sha512-csXbZsAdab/v8DbU1sz7WC2aNaKArcdS/FPmXMOXEj/JBBZMvDK0+1b4Qao0kkG0ciB1Qe86/Mb68GjH6/TnMw==} + '@rushstack/terminal@0.16.0': + resolution: {integrity: sha512-WEvNuKkoR1PXorr9SxO0dqFdSp1BA+xzDrIm/Bwlc5YHg2FFg6oS+uCTYjerOhFuqCW+A3vKBm6EmKWSHfgx/A==} peerDependencies: '@types/node': '*' peerDependenciesMeta: '@types/node': optional: true - '@rushstack/ts-command-line@4.23.1': - resolution: {integrity: sha512-40jTmYoiu/xlIpkkRsVfENtBq4CW3R4azbL0Vmda+fMwHWqss6wwf/Cy/UJmMqIzpfYc2OTnjYP1ZLD3CmyeCA==} + '@rushstack/ts-command-line@5.0.3': + resolution: {integrity: sha512-bgPhQEqLVv/2hwKLYv/XvsTWNZ9B/+X1zJ7WgQE9rO5oiLzrOZvkIW4pk13yOQBhHyjcND5qMOa6p83t+Z66iQ==} '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -734,12 +751,27 @@ packages: '@shikijs/engine-oniguruma@1.23.1': resolution: {integrity: sha512-KQ+lgeJJ5m2ISbUZudLR1qHeH3MnSs2mjFg7bnencgs5jDVPeJ2NVDJ3N5ZHbcTsOIh0qIueyAJnwg7lg7kwXQ==} + '@shikijs/engine-oniguruma@3.12.2': + resolution: {integrity: sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w==} + + '@shikijs/langs@3.12.2': + resolution: {integrity: sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww==} + + '@shikijs/themes@3.12.2': + resolution: {integrity: sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A==} + '@shikijs/transformers@1.23.1': resolution: {integrity: sha512-yQ2Cn0M9i46p30KwbyIzLvKDk+dQNU+lj88RGO0XEj54Hn4Cof1bZoDb9xBRWxFE4R8nmK63w7oHnJwvOtt0NQ==} '@shikijs/types@1.23.1': resolution: {integrity: sha512-98A5hGyEhzzAgQh2dAeHKrWW4HfCMeoFER2z16p5eJ+vmPeF6lZ/elEne6/UCU551F/WqkopqRsr1l2Yu6+A0g==} + '@shikijs/types@3.12.2': + resolution: {integrity: sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@shikijs/vscode-textmate@9.3.0': resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} @@ -767,6 +799,9 @@ packages: '@types/chai@4.3.16': resolution: {integrity: sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==} + '@types/dom-navigation@1.0.6': + resolution: {integrity: sha512-4srBpebg8rFDm0LafYuWhZMgLoSr5J4gx4q1uaTqOXwVk00y+CkTdJ4SC57sR1cMhP0ZRjApMRdHVcFYOvPGTw==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -1853,9 +1888,9 @@ packages: resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} engines: {node: '>=14.14'} - fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} + engines: {node: '>=14.14'} fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2267,9 +2302,6 @@ packages: json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -2515,8 +2547,9 @@ packages: resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} engines: {node: 20 || >=22} - minimatch@3.0.8: - resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3397,26 +3430,26 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} - typedoc-plugin-markdown@4.2.10: - resolution: {integrity: sha512-PLX3pc1/7z13UJm4TDE9vo9jWGcClFUErXXtd5LdnoLjV6mynPpqZLU992DwMGFSRqJFZeKbVyqlNNeNHnk2tQ==} + typedoc-plugin-markdown@4.8.1: + resolution: {integrity: sha512-ug7fc4j0SiJxSwBGLncpSo8tLvrT9VONvPUQqQDTKPxCoFQBADLli832RGPtj6sfSVJebNSrHZQRUdEryYH/7g==} engines: {node: '>= 18'} peerDependencies: - typedoc: 0.26.x + typedoc: 0.28.x - typedoc@0.26.11: - resolution: {integrity: sha512-sFEgRRtrcDl2FxVP58Ze++ZK2UQAEvtvvH8rRlig1Ja3o7dDaMHmaBfvJmdGnNEFaLTpQsN8dpvZaTqJSu/Ugw==} - engines: {node: '>= 18'} + typedoc@0.28.13: + resolution: {integrity: sha512-dNWY8msnYB2a+7Audha+aTF1Pu3euiE7ySp53w8kEsXoYw7dMouV5A1UsTUY345aB152RHnmRMDiovuBi7BD+w==} + engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: - typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x - typescript@5.4.2: - resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} engines: {node: '>=14.17'} hasBin: true - typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} hasBin: true @@ -3453,10 +3486,6 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -3743,16 +3772,16 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.6.1: - resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} - engines: {node: '>= 14'} - hasBin: true - yaml@2.7.1: resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} engines: {node: '>= 14'} hasBin: true + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.4: resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} engines: {node: '>=10'} @@ -4017,6 +4046,14 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true + '@gerrit0/mini-shiki@3.12.2': + dependencies: + '@shikijs/engine-oniguruma': 3.12.2 + '@shikijs/langs': 3.12.2 + '@shikijs/themes': 3.12.2 + '@shikijs/types': 3.12.2 + '@shikijs/vscode-textmate': 10.0.2 + '@hutson/parse-repository-url@3.0.2': {} '@iconify-json/simple-icons@1.2.13': @@ -4025,6 +4062,12 @@ snapshots: '@iconify/types@2.0.0': {} + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -4066,29 +4109,29 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} - '@microsoft/api-extractor-model@7.30.0(@types/node@22.15.2)': + '@microsoft/api-extractor-model@7.30.7(@types/node@22.15.2)': dependencies: '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.10.0(@types/node@22.15.2) + '@rushstack/node-core-library': 5.14.0(@types/node@22.15.2) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.48.0(@types/node@22.15.2)': + '@microsoft/api-extractor@7.52.13(@types/node@22.15.2)': dependencies: - '@microsoft/api-extractor-model': 7.30.0(@types/node@22.15.2) + '@microsoft/api-extractor-model': 7.30.7(@types/node@22.15.2) '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.10.0(@types/node@22.15.2) + '@rushstack/node-core-library': 5.14.0(@types/node@22.15.2) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.14.3(@types/node@22.15.2) - '@rushstack/ts-command-line': 4.23.1(@types/node@22.15.2) + '@rushstack/terminal': 0.16.0(@types/node@22.15.2) + '@rushstack/ts-command-line': 5.0.3(@types/node@22.15.2) lodash: 4.17.21 - minimatch: 3.0.8 + minimatch: 10.0.3 resolve: 1.22.8 semver: 7.5.4 source-map: 0.6.1 - typescript: 5.4.2 + typescript: 5.8.2 transitivePeerDependencies: - '@types/node' @@ -4238,12 +4281,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.27.4': optional: true - '@rushstack/node-core-library@5.10.0(@types/node@22.15.2)': + '@rushstack/node-core-library@5.14.0(@types/node@22.15.2)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) ajv-formats: 3.0.1(ajv@8.13.0) - fs-extra: 7.0.1 + fs-extra: 11.3.1 import-lazy: 4.0.0 jju: 1.4.0 resolve: 1.22.8 @@ -4256,16 +4299,16 @@ snapshots: resolve: 1.22.8 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.14.3(@types/node@22.15.2)': + '@rushstack/terminal@0.16.0(@types/node@22.15.2)': dependencies: - '@rushstack/node-core-library': 5.10.0(@types/node@22.15.2) + '@rushstack/node-core-library': 5.14.0(@types/node@22.15.2) supports-color: 8.1.1 optionalDependencies: '@types/node': 22.15.2 - '@rushstack/ts-command-line@4.23.1(@types/node@22.15.2)': + '@rushstack/ts-command-line@5.0.3(@types/node@22.15.2)': dependencies: - '@rushstack/terminal': 0.14.3(@types/node@22.15.2) + '@rushstack/terminal': 0.16.0(@types/node@22.15.2) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -4294,6 +4337,19 @@ snapshots: '@shikijs/types': 1.23.1 '@shikijs/vscode-textmate': 9.3.0 + '@shikijs/engine-oniguruma@3.12.2': + dependencies: + '@shikijs/types': 3.12.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.12.2': + dependencies: + '@shikijs/types': 3.12.2 + + '@shikijs/themes@3.12.2': + dependencies: + '@shikijs/types': 3.12.2 + '@shikijs/transformers@1.23.1': dependencies: shiki: 1.23.1 @@ -4303,6 +4359,13 @@ snapshots: '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 + '@shikijs/types@3.12.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@shikijs/vscode-textmate@9.3.0': {} '@sindresorhus/merge-streams@2.3.0': {} @@ -4319,6 +4382,8 @@ snapshots: '@types/chai@4.3.16': {} + '@types/dom-navigation@1.0.6': {} + '@types/estree@1.0.6': {} '@types/estree@1.0.7': {} @@ -4396,20 +4461,20 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-vue@5.2.0(vite@5.4.11(@types/node@22.15.2)(terser@5.32.0))(vue@3.5.13(typescript@5.6.3))': + '@vitejs/plugin-vue@5.2.0(vite@5.4.11(@types/node@22.15.2)(terser@5.32.0))(vue@3.5.13(typescript@5.8.3))': dependencies: vite: 5.4.11(@types/node@22.15.2)(terser@5.32.0) - vue: 3.5.13(typescript@5.6.3) + vue: 3.5.13(typescript@5.8.3) - '@vitejs/plugin-vue@5.2.3(vite@5.4.18(@types/node@20.17.31)(terser@5.32.0))(vue@3.5.13(typescript@5.6.3))': + '@vitejs/plugin-vue@5.2.3(vite@5.4.18(@types/node@20.17.31)(terser@5.32.0))(vue@3.5.13(typescript@5.8.3))': dependencies: vite: 5.4.18(@types/node@20.17.31)(terser@5.32.0) - vue: 3.5.13(typescript@5.6.3) + vue: 3.5.13(typescript@5.8.3) - '@vitejs/plugin-vue@5.2.3(vite@5.4.18(@types/node@22.15.2)(terser@5.32.0))(vue@3.5.13(typescript@5.6.3))': + '@vitejs/plugin-vue@5.2.3(vite@5.4.18(@types/node@22.15.2)(terser@5.32.0))(vue@3.5.13(typescript@5.8.3))': dependencies: vite: 5.4.18(@types/node@22.15.2)(terser@5.32.0) - vue: 3.5.13(typescript@5.6.3) + vue: 3.5.13(typescript@5.8.3) '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.15.2)(@vitest/ui@2.1.9)(happy-dom@15.11.7)(jsdom@19.0.0)(terser@5.32.0))': dependencies: @@ -4547,7 +4612,7 @@ snapshots: dependencies: rfdc: 1.4.1 - '@vue/language-core@2.2.10(typescript@5.6.3)': + '@vue/language-core@2.2.10(typescript@5.8.3)': dependencies: '@volar/language-core': 2.4.12 '@vue/compiler-dom': 3.5.13 @@ -4558,7 +4623,7 @@ snapshots: muggle-string: 0.4.1 path-browserify: 1.0.1 optionalDependencies: - typescript: 5.6.3 + typescript: 5.8.3 '@vue/reactivity@3.5.13': dependencies: @@ -4576,11 +4641,11 @@ snapshots: '@vue/shared': 3.5.13 csstype: 3.1.3 - '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.6.3))': + '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.8.3))': dependencies: '@vue/compiler-ssr': 3.5.13 '@vue/shared': 3.5.13 - vue: 3.5.13(typescript@5.6.3) + vue: 3.5.13(typescript@5.8.3) '@vue/shared@3.5.13': {} @@ -4589,26 +4654,26 @@ snapshots: js-beautify: 1.15.1 vue-component-type-helpers: 2.0.21 - '@vue/tsconfig@0.6.0(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))': + '@vue/tsconfig@0.6.0(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))': optionalDependencies: - typescript: 5.6.3 - vue: 3.5.13(typescript@5.6.3) + typescript: 5.8.3 + vue: 3.5.13(typescript@5.8.3) - '@vueuse/core@11.3.0(vue@3.5.13(typescript@5.6.3))': + '@vueuse/core@11.3.0(vue@3.5.13(typescript@5.8.3))': dependencies: '@types/web-bluetooth': 0.0.20 '@vueuse/metadata': 11.3.0 - '@vueuse/shared': 11.3.0(vue@3.5.13(typescript@5.6.3)) - vue-demi: 0.14.10(vue@3.5.13(typescript@5.6.3)) + '@vueuse/shared': 11.3.0(vue@3.5.13(typescript@5.8.3)) + vue-demi: 0.14.10(vue@3.5.13(typescript@5.8.3)) transitivePeerDependencies: - '@vue/composition-api' - vue - '@vueuse/integrations@11.3.0(axios@1.7.7)(focus-trap@7.6.2)(vue@3.5.13(typescript@5.6.3))': + '@vueuse/integrations@11.3.0(axios@1.7.7)(focus-trap@7.6.2)(vue@3.5.13(typescript@5.8.3))': dependencies: - '@vueuse/core': 11.3.0(vue@3.5.13(typescript@5.6.3)) - '@vueuse/shared': 11.3.0(vue@3.5.13(typescript@5.6.3)) - vue-demi: 0.14.10(vue@3.5.13(typescript@5.6.3)) + '@vueuse/core': 11.3.0(vue@3.5.13(typescript@5.8.3)) + '@vueuse/shared': 11.3.0(vue@3.5.13(typescript@5.8.3)) + vue-demi: 0.14.10(vue@3.5.13(typescript@5.8.3)) optionalDependencies: axios: 1.7.7 focus-trap: 7.6.2 @@ -4618,9 +4683,9 @@ snapshots: '@vueuse/metadata@11.3.0': {} - '@vueuse/shared@11.3.0(vue@3.5.13(typescript@5.6.3))': + '@vueuse/shared@11.3.0(vue@3.5.13(typescript@5.8.3))': dependencies: - vue-demi: 0.14.10(vue@3.5.13(typescript@5.6.3)) + vue-demi: 0.14.10(vue@3.5.13(typescript@5.8.3)) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -5494,11 +5559,11 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 - fs-extra@7.0.1: + fs-extra@11.3.1: dependencies: graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 + jsonfile: 6.1.0 + universalify: 2.0.1 fs.realpath@1.0.0: {} @@ -5942,10 +6007,6 @@ snapshots: json-stringify-safe@5.0.1: {} - jsonfile@4.0.0: - optionalDependencies: - graceful-fs: 4.2.11 - jsonfile@6.1.0: dependencies: universalify: 2.0.1 @@ -6217,9 +6278,9 @@ snapshots: dependencies: brace-expansion: 2.0.1 - minimatch@3.0.8: + minimatch@10.0.3: dependencies: - brace-expansion: 1.1.11 + '@isaacs/brace-expansion': 5.0.0 minimatch@3.1.2: dependencies: @@ -6728,7 +6789,7 @@ snapshots: rollup-plugin-analyzer@4.0.0: {} - rollup-plugin-typescript2@0.36.0(rollup@3.29.5)(typescript@5.6.3): + rollup-plugin-typescript2@0.36.0(rollup@3.29.5)(typescript@5.8.3): dependencies: '@rollup/pluginutils': 4.2.1 find-cache-dir: 3.3.2 @@ -6736,7 +6797,7 @@ snapshots: rollup: 3.29.5 semver: 7.6.3 tslib: 2.8.1 - typescript: 5.6.3 + typescript: 5.8.3 rollup@3.29.5: optionalDependencies: @@ -7137,22 +7198,22 @@ snapshots: type-fest@0.8.1: {} - typedoc-plugin-markdown@4.2.10(typedoc@0.26.11(typescript@5.6.3)): + typedoc-plugin-markdown@4.8.1(typedoc@0.28.13(typescript@5.8.3)): dependencies: - typedoc: 0.26.11(typescript@5.6.3) + typedoc: 0.28.13(typescript@5.8.3) - typedoc@0.26.11(typescript@5.6.3): + typedoc@0.28.13(typescript@5.8.3): dependencies: + '@gerrit0/mini-shiki': 3.12.2 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 - shiki: 1.23.1 - typescript: 5.6.3 - yaml: 2.6.1 + typescript: 5.8.3 + yaml: 2.8.1 - typescript@5.4.2: {} + typescript@5.8.2: {} - typescript@5.6.3: {} + typescript@5.8.3: {} uc.micro@2.1.0: {} @@ -7189,8 +7250,6 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - universalify@0.1.2: {} - universalify@0.2.0: {} universalify@2.0.1: {} @@ -7275,16 +7334,16 @@ snapshots: fsevents: 2.3.3 terser: 5.32.0 - vitepress-translation-helper@0.2.1(vitepress@1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.6.3))(vue@3.5.13(typescript@5.6.3)): + vitepress-translation-helper@0.2.1(vitepress@1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3)): dependencies: minimist: 1.2.8 simple-git: 3.27.0 - vitepress: 1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.6.3) - vue: 3.5.13(typescript@5.6.3) + vitepress: 1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.8.3) + vue: 3.5.13(typescript@5.8.3) transitivePeerDependencies: - supports-color - vitepress@1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.6.3): + vitepress@1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.8.3): dependencies: '@docsearch/css': 3.8.0 '@docsearch/js': 3.8.0(@algolia/client-search@5.15.0)(search-insights@2.17.1) @@ -7293,17 +7352,17 @@ snapshots: '@shikijs/transformers': 1.23.1 '@shikijs/types': 1.23.1 '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 5.2.0(vite@5.4.11(@types/node@22.15.2)(terser@5.32.0))(vue@3.5.13(typescript@5.6.3)) + '@vitejs/plugin-vue': 5.2.0(vite@5.4.11(@types/node@22.15.2)(terser@5.32.0))(vue@3.5.13(typescript@5.8.3)) '@vue/devtools-api': 7.6.4 '@vue/shared': 3.5.13 - '@vueuse/core': 11.3.0(vue@3.5.13(typescript@5.6.3)) - '@vueuse/integrations': 11.3.0(axios@1.7.7)(focus-trap@7.6.2)(vue@3.5.13(typescript@5.6.3)) + '@vueuse/core': 11.3.0(vue@3.5.13(typescript@5.8.3)) + '@vueuse/integrations': 11.3.0(axios@1.7.7)(focus-trap@7.6.2)(vue@3.5.13(typescript@5.8.3)) focus-trap: 7.6.2 mark.js: 8.11.1 minisearch: 7.1.1 shiki: 1.23.1 vite: 5.4.11(@types/node@22.15.2)(terser@5.32.0) - vue: 3.5.13(typescript@5.6.3) + vue: 3.5.13(typescript@5.8.3) optionalDependencies: postcss: 8.4.49 transitivePeerDependencies: @@ -7376,25 +7435,25 @@ snapshots: vue-component-type-helpers@2.0.21: {} - vue-demi@0.14.10(vue@3.5.13(typescript@5.6.3)): + vue-demi@0.14.10(vue@3.5.13(typescript@5.8.3)): dependencies: - vue: 3.5.13(typescript@5.6.3) + vue: 3.5.13(typescript@5.8.3) - vue-tsc@2.2.10(typescript@5.6.3): + vue-tsc@2.2.10(typescript@5.8.3): dependencies: '@volar/typescript': 2.4.12 - '@vue/language-core': 2.2.10(typescript@5.6.3) - typescript: 5.6.3 + '@vue/language-core': 2.2.10(typescript@5.8.3) + typescript: 5.8.3 - vue@3.5.13(typescript@5.6.3): + vue@3.5.13(typescript@5.8.3): dependencies: '@vue/compiler-dom': 3.5.13 '@vue/compiler-sfc': 3.5.13 '@vue/runtime-dom': 3.5.13 - '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.6.3)) + '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.8.3)) '@vue/shared': 3.5.13 optionalDependencies: - typescript: 5.6.3 + typescript: 5.8.3 w3c-hr-time@1.0.2: dependencies: @@ -7481,10 +7540,10 @@ snapshots: yallist@4.0.0: {} - yaml@2.6.1: {} - yaml@2.7.1: {} + yaml@2.8.1: {} + yargs-parser@20.2.4: {} yargs-parser@20.2.9: {}