|
| 1 | +import type { RouteLocationNamedRaw, RouteLocationNormalizedLoaded } from './types'; |
| 2 | + |
| 3 | +/** |
| 4 | + * Flags so we can combine them when checking for multiple errors. This is the internal version of |
| 5 | + * {@link NavigationFailureType}. |
| 6 | + * |
| 7 | + * @internal |
| 8 | + */ |
| 9 | +export enum ErrorTypes { |
| 10 | + // they must be literals to be used as values, so we can't write |
| 11 | + // 1 << 2 |
| 12 | + MATCHER_NOT_FOUND = 1, |
| 13 | + NAVIGATION_GUARD_REDIRECT = 2, |
| 14 | + NAVIGATION_ABORTED = 4, |
| 15 | + NAVIGATION_CANCELLED = 8, |
| 16 | + NAVIGATION_DUPLICATED = 16 |
| 17 | +} |
| 18 | + |
| 19 | +export interface MatcherError extends Error { |
| 20 | + type: ErrorTypes.MATCHER_NOT_FOUND; |
| 21 | + location: RouteLocationNamedRaw; |
| 22 | + currentLocation?: RouteLocationNormalizedLoaded; |
| 23 | +} |
| 24 | + |
| 25 | +/** |
| 26 | + * Internal error used to detect a redirection. |
| 27 | + * |
| 28 | + * @internal |
| 29 | + */ |
| 30 | +export interface NavigationRedirectError extends Omit<NavigationFailure, 'to' | 'type'> { |
| 31 | + type: ErrorTypes.NAVIGATION_GUARD_REDIRECT; |
| 32 | + to: RouteLocationNormalizedLoaded; |
| 33 | +} |
| 34 | + |
| 35 | +// Possible internal errors |
| 36 | +type RouterError = NavigationFailure | NavigationRedirectError | MatcherError; |
| 37 | + |
| 38 | +/** Extended Error that contains extra information regarding a failed navigation. */ |
| 39 | +export interface NavigationFailure extends Error { |
| 40 | + /** Type of the navigation. One of {@link NavigationFailureType} */ |
| 41 | + type: ErrorTypes.NAVIGATION_CANCELLED | ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_DUPLICATED; |
| 42 | + /** Route location we were navigating from */ |
| 43 | + from: RouteLocationNormalizedLoaded; |
| 44 | + /** Route location we were navigating to */ |
| 45 | + to: RouteLocationNormalizedLoaded; |
| 46 | +} |
| 47 | + |
| 48 | +// DEV only debug messages |
| 49 | +const ErrorTypeMessages = { |
| 50 | + [ErrorTypes.MATCHER_NOT_FOUND]({ location, currentLocation }: MatcherError) { |
| 51 | + return `No match for\n ${JSON.stringify(location)}${ |
| 52 | + currentLocation ? `\nwhile being at\n${JSON.stringify(currentLocation)}` : '' |
| 53 | + }`; |
| 54 | + }, |
| 55 | + [ErrorTypes.NAVIGATION_GUARD_REDIRECT]({ from, to }: NavigationRedirectError) { |
| 56 | + return `Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard.`; |
| 57 | + }, |
| 58 | + [ErrorTypes.NAVIGATION_ABORTED]({ from, to }: NavigationFailure) { |
| 59 | + return `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard.`; |
| 60 | + }, |
| 61 | + [ErrorTypes.NAVIGATION_CANCELLED]({ from, to }: NavigationFailure) { |
| 62 | + return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new navigation.`; |
| 63 | + }, |
| 64 | + [ErrorTypes.NAVIGATION_DUPLICATED]({ from }: NavigationFailure) { |
| 65 | + return `Avoided redundant navigation to current location: "${from.fullPath}".`; |
| 66 | + } |
| 67 | +}; |
| 68 | + |
| 69 | +const NavigationFailureSymbol = Symbol(import.meta.env.MODE === 'development' ? 'navigation failure' : ''); |
| 70 | + |
| 71 | +/** |
| 72 | + * Creates a typed NavigationFailure object. |
| 73 | + * |
| 74 | + * @param type - NavigationFailureType |
| 75 | + * @param params - { from, to } |
| 76 | + * @internal |
| 77 | + */ |
| 78 | +export function createRouterError<E extends RouterError>(type: E['type'], params: Omit<E, 'type' | keyof Error>): E { |
| 79 | + // keep full error messages in cjs versions |
| 80 | + if (import.meta.env.MODE === 'development') { |
| 81 | + return Object.assign( |
| 82 | + new Error(ErrorTypeMessages[type](params as any)), |
| 83 | + { |
| 84 | + type, |
| 85 | + [NavigationFailureSymbol]: true |
| 86 | + } as { type: typeof type }, |
| 87 | + params |
| 88 | + ) as E; |
| 89 | + } |
| 90 | + return Object.assign( |
| 91 | + new Error('Route not found'), |
| 92 | + { |
| 93 | + type, |
| 94 | + [NavigationFailureSymbol]: true |
| 95 | + } as { type: typeof type }, |
| 96 | + params |
| 97 | + ) as E; |
| 98 | +} |
| 99 | + |
| 100 | +const propertiesToLog = ['query', 'hash'] as const; |
| 101 | + |
| 102 | +function stringifyRoute(to: RouteLocationNormalizedLoaded): string { |
| 103 | + if (typeof to === 'string') return to; |
| 104 | + if (to.path !== null) return to.path; |
| 105 | + const location = {} as Record<string, unknown>; |
| 106 | + for (const key of propertiesToLog) { |
| 107 | + if (key in to) location[key] = to[key]; |
| 108 | + } |
| 109 | + return JSON.stringify(location, null, 2); |
| 110 | +} |
0 commit comments