2023-01-14 00:58:54 +00:00
|
|
|
import { reactive, h, isReadonly } from 'vue'
|
2022-08-22 16:08:43 +00:00
|
|
|
import { parseURL, stringifyParsedURL, parseQuery, stringifyQuery, withoutBase, isEqual, joinURL } from 'ufo'
|
2022-02-21 13:03:42 +00:00
|
|
|
import { createError } from 'h3'
|
2023-01-23 11:18:33 +00:00
|
|
|
import { defineNuxtPlugin, clearError, navigateTo, showError, useRuntimeConfig, useState, useRequestEvent } from '..'
|
2022-02-21 13:03:42 +00:00
|
|
|
import { callWithNuxt } from '../nuxt'
|
2022-06-27 12:10:29 +00:00
|
|
|
// @ts-ignore
|
|
|
|
import { globalMiddleware } from '#build/middleware'
|
2022-02-21 13:03:42 +00:00
|
|
|
|
|
|
|
interface Route {
|
2022-11-09 09:08:50 +00:00
|
|
|
/** Percentage encoded pathname section of the URL. */
|
|
|
|
path: string
|
|
|
|
/** The whole location including the `search` and `hash`. */
|
|
|
|
fullPath: string
|
|
|
|
/** Object representation of the `search` property of the current location. */
|
|
|
|
query: Record<string, any>
|
|
|
|
/** Hash of the current location. If present, starts with a `#`. */
|
|
|
|
hash: string
|
|
|
|
/** Name of the matched record */
|
|
|
|
name: string | null | undefined
|
|
|
|
/** Object of decoded params extracted from the `path`. */
|
|
|
|
params: Record<string, any>
|
|
|
|
/**
|
|
|
|
* The location we were initially trying to access before ending up
|
|
|
|
* on the current location.
|
|
|
|
*/
|
|
|
|
redirectedFrom: Route | undefined
|
|
|
|
/** Merged `meta` properties from all of the matched route records. */
|
|
|
|
meta: Record<string, any>
|
2022-02-21 13:03:42 +00:00
|
|
|
}
|
|
|
|
|
2022-08-22 16:08:43 +00:00
|
|
|
function getRouteFromPath (fullPath: string | Partial<Route>) {
|
2022-03-14 13:36:32 +00:00
|
|
|
if (typeof fullPath === 'object') {
|
2022-08-22 16:08:43 +00:00
|
|
|
fullPath = stringifyParsedURL({
|
|
|
|
pathname: fullPath.path || '',
|
|
|
|
search: stringifyQuery(fullPath.query || {}),
|
|
|
|
hash: fullPath.hash || ''
|
|
|
|
})
|
2022-03-14 13:36:32 +00:00
|
|
|
}
|
|
|
|
|
2022-02-21 13:03:42 +00:00
|
|
|
const url = parseURL(fullPath.toString())
|
|
|
|
return {
|
|
|
|
path: url.pathname,
|
|
|
|
fullPath,
|
|
|
|
query: parseQuery(url.search),
|
|
|
|
hash: url.hash,
|
|
|
|
// stub properties for compat with vue-router
|
|
|
|
params: {},
|
|
|
|
name: undefined,
|
|
|
|
matched: [],
|
|
|
|
redirectedFrom: undefined,
|
2022-03-14 13:36:32 +00:00
|
|
|
meta: {},
|
|
|
|
href: fullPath
|
2022-02-21 13:03:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type RouteGuardReturn = void | Error | string | false
|
|
|
|
|
|
|
|
interface RouteGuard {
|
|
|
|
(to: Route, from: Route): RouteGuardReturn | Promise<RouteGuardReturn>
|
|
|
|
}
|
|
|
|
|
|
|
|
interface RouterHooks {
|
|
|
|
'resolve:before': (to: Route, from: Route) => RouteGuardReturn | Promise<RouteGuardReturn>
|
|
|
|
'navigate:before': (to: Route, from: Route) => RouteGuardReturn | Promise<RouteGuardReturn>
|
|
|
|
'navigate:after': (to: Route, from: Route) => void | Promise<void>
|
|
|
|
'error': (err: any) => void | Promise<void>
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Router {
|
|
|
|
currentRoute: Route
|
|
|
|
isReady: () => Promise<void>
|
|
|
|
options: {}
|
|
|
|
install: () => Promise<void>
|
|
|
|
// Navigation
|
|
|
|
push: (url: string) => Promise<void>
|
|
|
|
replace: (url: string) => Promise<void>
|
|
|
|
back: () => void
|
|
|
|
go: (delta: number) => void
|
|
|
|
forward: () => void
|
|
|
|
// Guards
|
|
|
|
beforeResolve: (guard: RouterHooks['resolve:before']) => () => void
|
|
|
|
beforeEach: (guard: RouterHooks['navigate:before']) => () => void
|
|
|
|
afterEach: (guard: RouterHooks['navigate:after']) => () => void
|
|
|
|
onError: (handler: RouterHooks['error']) => () => void
|
|
|
|
// Routes
|
2022-08-22 16:08:43 +00:00
|
|
|
resolve: (url: string | Partial<Route>) => Route
|
2022-02-21 13:03:42 +00:00
|
|
|
addRoute: (parentName: string, route: Route) => void
|
|
|
|
getRoutes: () => any[]
|
|
|
|
hasRoute: (name: string) => boolean
|
|
|
|
removeRoute: (name: string) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
|
2022-05-06 11:02:35 +00:00
|
|
|
const initialURL = process.client
|
|
|
|
? withoutBase(window.location.pathname, useRuntimeConfig().app.baseURL) + window.location.search + window.location.hash
|
2022-08-12 17:47:58 +00:00
|
|
|
: nuxtApp.ssrContext!.url
|
|
|
|
|
|
|
|
const routes: Route[] = []
|
2022-02-21 13:03:42 +00:00
|
|
|
|
|
|
|
const hooks: { [key in keyof RouterHooks]: RouterHooks[key][] } = {
|
|
|
|
'navigate:before': [],
|
|
|
|
'resolve:before': [],
|
|
|
|
'navigate:after': [],
|
|
|
|
error: []
|
|
|
|
}
|
|
|
|
|
|
|
|
const registerHook = <T extends keyof RouterHooks>(hook: T, guard: RouterHooks[T]) => {
|
|
|
|
hooks[hook].push(guard)
|
|
|
|
return () => hooks[hook].splice(hooks[hook].indexOf(guard), 1)
|
|
|
|
}
|
2022-05-11 17:33:29 +00:00
|
|
|
const baseURL = useRuntimeConfig().app.baseURL
|
2022-02-21 13:03:42 +00:00
|
|
|
|
2022-05-02 10:00:08 +00:00
|
|
|
const route: Route = reactive(getRouteFromPath(initialURL))
|
2022-08-22 16:08:43 +00:00
|
|
|
async function handleNavigation (url: string | Partial<Route>, replace?: boolean): Promise<void> {
|
2022-02-21 13:03:42 +00:00
|
|
|
try {
|
|
|
|
// Resolve route
|
|
|
|
const to = getRouteFromPath(url)
|
2022-03-11 08:22:16 +00:00
|
|
|
|
2022-02-21 13:03:42 +00:00
|
|
|
// Run beforeEach hooks
|
|
|
|
for (const middleware of hooks['navigate:before']) {
|
|
|
|
const result = await middleware(to, route)
|
|
|
|
// Cancel navigation
|
|
|
|
if (result === false || result instanceof Error) { return }
|
|
|
|
// Redirect
|
|
|
|
if (result) { return handleNavigation(result, true) }
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const handler of hooks['resolve:before']) {
|
|
|
|
await handler(to, route)
|
|
|
|
}
|
|
|
|
// Perform navigation
|
|
|
|
Object.assign(route, to)
|
|
|
|
if (process.client) {
|
2022-08-22 16:08:43 +00:00
|
|
|
window.history[replace ? 'replaceState' : 'pushState']({}, '', joinURL(baseURL, to.fullPath))
|
2022-05-06 10:50:54 +00:00
|
|
|
if (!nuxtApp.isHydrating) {
|
|
|
|
// Clear any existing errors
|
|
|
|
await callWithNuxt(nuxtApp, clearError)
|
|
|
|
}
|
2022-02-21 13:03:42 +00:00
|
|
|
}
|
|
|
|
// Run afterEach hooks
|
|
|
|
for (const middleware of hooks['navigate:after']) {
|
|
|
|
await middleware(to, route)
|
|
|
|
}
|
|
|
|
} catch (err) {
|
2022-02-25 16:33:27 +00:00
|
|
|
if (process.dev && !hooks.error.length) {
|
|
|
|
console.warn('No error handlers registered to handle middleware errors. You can register an error handler with `router.onError()`', err)
|
|
|
|
}
|
2022-02-21 13:03:42 +00:00
|
|
|
for (const handler of hooks.error) {
|
|
|
|
await handler(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const router: Router = {
|
|
|
|
currentRoute: route,
|
|
|
|
isReady: () => Promise.resolve(),
|
2022-09-01 09:07:44 +00:00
|
|
|
// These options provide a similar API to vue-router but have no effect
|
2022-02-21 13:03:42 +00:00
|
|
|
options: {},
|
|
|
|
install: () => Promise.resolve(),
|
|
|
|
// Navigation
|
|
|
|
push: (url: string) => handleNavigation(url, false),
|
|
|
|
replace: (url: string) => handleNavigation(url, true),
|
|
|
|
back: () => window.history.go(-1),
|
|
|
|
go: (delta: number) => window.history.go(delta),
|
|
|
|
forward: () => window.history.go(1),
|
|
|
|
// Guards
|
|
|
|
beforeResolve: (guard: RouterHooks['resolve:before']) => registerHook('resolve:before', guard),
|
|
|
|
beforeEach: (guard: RouterHooks['navigate:before']) => registerHook('navigate:before', guard),
|
|
|
|
afterEach: (guard: RouterHooks['navigate:after']) => registerHook('navigate:after', guard),
|
|
|
|
onError: (handler: RouterHooks['error']) => registerHook('error', handler),
|
|
|
|
// Routes
|
|
|
|
resolve: getRouteFromPath,
|
|
|
|
addRoute: (parentName: string, route: Route) => { routes.push(route) },
|
|
|
|
getRoutes: () => routes,
|
|
|
|
hasRoute: (name: string) => routes.some(route => route.name === name),
|
|
|
|
removeRoute: (name: string) => {
|
|
|
|
const index = routes.findIndex(route => route.name === name)
|
|
|
|
if (index !== -1) {
|
|
|
|
routes.splice(index, 1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-14 13:36:32 +00:00
|
|
|
nuxtApp.vueApp.component('RouterLink', {
|
|
|
|
functional: true,
|
2022-07-07 17:28:23 +00:00
|
|
|
props: {
|
|
|
|
to: String,
|
|
|
|
custom: Boolean,
|
|
|
|
replace: Boolean,
|
|
|
|
// Not implemented
|
|
|
|
activeClass: String,
|
|
|
|
exactActiveClass: String,
|
|
|
|
ariaCurrentValue: String
|
|
|
|
},
|
|
|
|
setup: (props, { slots }) => {
|
|
|
|
const navigate = () => handleNavigation(props.to, props.replace)
|
|
|
|
return () => {
|
|
|
|
const route = router.resolve(props.to)
|
|
|
|
return props.custom
|
|
|
|
? slots.default?.({ href: props.to, navigate, route })
|
2022-08-12 17:47:58 +00:00
|
|
|
: h('a', { href: props.to, onClick: (e: MouseEvent) => { e.preventDefault(); return navigate() } }, slots)
|
2022-07-07 17:28:23 +00:00
|
|
|
}
|
|
|
|
}
|
2022-03-14 13:36:32 +00:00
|
|
|
})
|
|
|
|
|
2022-02-21 13:03:42 +00:00
|
|
|
if (process.client) {
|
|
|
|
window.addEventListener('popstate', (event) => {
|
|
|
|
const location = (event.target as Window).location
|
|
|
|
router.replace(location.href.replace(location.origin, ''))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
nuxtApp._route = route
|
|
|
|
|
|
|
|
// Handle middleware
|
|
|
|
nuxtApp._middleware = nuxtApp._middleware || {
|
|
|
|
global: [],
|
|
|
|
named: {}
|
|
|
|
}
|
|
|
|
|
2022-08-31 08:02:48 +00:00
|
|
|
const initialLayout = useState('_layout')
|
2022-05-02 10:00:08 +00:00
|
|
|
nuxtApp.hooks.hookOnce('app:created', async () => {
|
|
|
|
router.beforeEach(async (to, from) => {
|
|
|
|
to.meta = reactive(to.meta || {})
|
2023-01-14 00:58:54 +00:00
|
|
|
if (nuxtApp.isHydrating && initialLayout.value && !isReadonly(to.meta.layout)) {
|
|
|
|
to.meta.layout = initialLayout.value
|
2022-08-31 08:02:48 +00:00
|
|
|
}
|
2022-05-02 10:00:08 +00:00
|
|
|
nuxtApp._processingMiddleware = true
|
|
|
|
|
2022-06-27 12:10:29 +00:00
|
|
|
const middlewareEntries = new Set<RouteGuard>([...globalMiddleware, ...nuxtApp._middleware.global])
|
2022-05-02 10:00:08 +00:00
|
|
|
|
|
|
|
for (const middleware of middlewareEntries) {
|
|
|
|
const result = await callWithNuxt(nuxtApp, middleware, [to, from])
|
|
|
|
if (process.server) {
|
|
|
|
if (result === false || result instanceof Error) {
|
|
|
|
const error = result || createError({
|
2022-10-10 10:18:20 +00:00
|
|
|
statusCode: 404,
|
|
|
|
statusMessage: `Page Not Found: ${initialURL}`
|
2022-05-02 10:00:08 +00:00
|
|
|
})
|
2022-07-21 14:29:03 +00:00
|
|
|
return callWithNuxt(nuxtApp, showError, [error])
|
2022-05-02 10:00:08 +00:00
|
|
|
}
|
2022-02-21 13:03:42 +00:00
|
|
|
}
|
2022-05-02 10:00:08 +00:00
|
|
|
if (result || result === false) { return result }
|
2022-02-21 13:03:42 +00:00
|
|
|
}
|
2022-05-02 10:00:08 +00:00
|
|
|
})
|
2022-02-21 13:03:42 +00:00
|
|
|
|
2022-05-02 10:00:08 +00:00
|
|
|
router.afterEach(() => {
|
|
|
|
delete nuxtApp._processingMiddleware
|
2022-02-21 13:03:42 +00:00
|
|
|
})
|
2022-05-02 10:00:08 +00:00
|
|
|
|
|
|
|
await router.replace(initialURL)
|
2022-05-06 16:02:50 +00:00
|
|
|
if (!isEqual(route.fullPath, initialURL)) {
|
2023-01-23 11:18:33 +00:00
|
|
|
const event = await callWithNuxt(nuxtApp, useRequestEvent)
|
|
|
|
const options = { redirectCode: event.node.res.statusCode !== 200 ? event.node.res.statusCode || 302 : 302 }
|
|
|
|
await callWithNuxt(nuxtApp, navigateTo, [route.fullPath, options])
|
2022-05-02 10:00:08 +00:00
|
|
|
}
|
|
|
|
})
|
2022-02-21 13:03:42 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
provide: {
|
|
|
|
route,
|
|
|
|
router
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|