diff --git a/packages/bridge/src/auto-imports.ts b/packages/bridge/src/auto-imports.ts index 6160362fac..ee7a78b762 100644 --- a/packages/bridge/src/auto-imports.ts +++ b/packages/bridge/src/auto-imports.ts @@ -30,6 +30,10 @@ export async function setupAutoImports () { // Add auto-imports that are added by ad-hoc modules in nuxt 3 autoImports.push({ name: 'useRouter', as: 'useRouter', from: '#app' }) autoImports.push({ name: 'useRoute', as: 'useRoute', from: '#app' }) + autoImports.push({ name: 'addRouteMiddleware', as: 'addRouteMiddleware', from: '#app' }) + autoImports.push({ name: 'navigateTo', as: 'navigateTo', from: '#app' }) + autoImports.push({ name: 'abortNavigation', as: 'abortNavigation', from: '#app' }) + autoImports.push({ name: 'defineNuxtRouteMiddleware', as: 'defineNuxtRouteMiddleware', from: '#app' }) // Add bridge-only auto-imports autoImports.push({ name: 'useNuxt2Meta', as: 'useNuxt2Meta', from: '#app' }) diff --git a/packages/bridge/src/runtime/app.plugin.mjs b/packages/bridge/src/runtime/app.plugin.mjs index 84e3992ee4..572b53e69e 100644 --- a/packages/bridge/src/runtime/app.plugin.mjs +++ b/packages/bridge/src/runtime/app.plugin.mjs @@ -1,6 +1,6 @@ import Vue from 'vue' import { createHooks } from 'hookable' -import { setNuxtAppInstance } from '#app' +import { callWithNuxt, setNuxtAppInstance } from '#app' // Reshape payload to match key `useLazyAsyncData` expects function proxiedState (state) { @@ -19,7 +19,7 @@ function proxiedState (state) { }) } -export default (ctx, inject) => { +export default async (ctx, inject) => { const nuxtApp = { vueApp: { component: Vue.component.bind(Vue), @@ -48,6 +48,27 @@ export default (ctx, inject) => { nuxtApp.hook = nuxtApp.hooks.hook nuxtApp.callHook = nuxtApp.hooks.callHook + const middleware = await import('#build/middleware').then(r => r.default) + nuxtApp._middleware = nuxtApp._middleware || { + global: [], + named: middleware + } + + ctx.app.router.beforeEach(async (to, from, next) => { + nuxtApp._processingMiddleware = true + + for (const middleware of nuxtApp._middleware.global) { + const result = await callWithNuxt(nuxtApp, middleware, [to, from]) + if (result || result === false) { return next(result) } + } + + next() + }) + + ctx.app.router.afterEach(() => { + delete nuxtApp._processingMiddleware + }) + if (!Array.isArray(ctx.app.created)) { ctx.app.created = [ctx.app.created].filter(Boolean) } diff --git a/packages/bridge/src/runtime/app.ts b/packages/bridge/src/runtime/app.ts index bbd0948328..61d2fa9f65 100644 --- a/packages/bridge/src/runtime/app.ts +++ b/packages/bridge/src/runtime/app.ts @@ -58,12 +58,24 @@ export const setNuxtAppInstance = (nuxt: NuxtAppCompat | null) => { currentNuxtAppInstance = nuxt } -export function defineNuxtPlugin (plugin: (nuxtApp: NuxtAppCompat) => void): (ctx: Context) => void { - return (ctx) => { - setNuxtAppInstance(ctx.$_nuxtApp) - plugin(ctx.$_nuxtApp) +/** + * Ensures that the setup function passed in has access to the Nuxt instance via `useNuxt`. + * + * @param nuxt A Nuxt instance + * @param setup The function to call + */ +export function callWithNuxt any> (nuxt: NuxtAppCompat, setup: T, args?: Parameters) { + setNuxtAppInstance(nuxt) + const p: ReturnType = args ? setup(...args as Parameters) : setup() + if (process.server) { + // Unset nuxt instance to prevent context-sharing in server-side setNuxtAppInstance(null) } + return p +} + +export function defineNuxtPlugin (plugin: (nuxtApp: NuxtAppCompat) => void): (ctx: Context) => void { + return ctx => callWithNuxt(ctx.$_nuxtApp, plugin, [ctx.$_nuxtApp]) } export const useNuxtApp = (): NuxtAppCompat => { diff --git a/packages/bridge/src/runtime/composables.ts b/packages/bridge/src/runtime/composables.ts index 4356b64cb8..1773bfe343 100644 --- a/packages/bridge/src/runtime/composables.ts +++ b/packages/bridge/src/runtime/composables.ts @@ -2,7 +2,7 @@ import { getCurrentInstance, onBeforeUnmount, isRef, watch, reactive, toRef, isR import type { CombinedVueInstance } from 'vue/types/vue' import type { MetaInfo } from 'vue-meta' import type VueRouter from 'vue-router' -import type { Route } from 'vue-router' +import type { Location, Route } from 'vue-router' import type { RuntimeConfig } from '@nuxt/schema' import defu from 'defu' import { useNuxtApp } from './app' @@ -141,3 +141,69 @@ export const useNuxt2Meta = (metaOptions: Reffed | (() => Reffed { + const result = await middleware(ctx.route, ctx.from) + if (result instanceof Error) { + return ctx.error(result) + } + if (result) { + return ctx.redirect(result) + } + return result + } +} + +export const addRouteMiddleware = (name: string, middleware: any, options: AddRouteMiddlewareOptions = {}) => { + const nuxtApp = useNuxtApp() + if (options.global) { + nuxtApp._middleware.global.push(middleware) + } else { + nuxtApp._middleware.named[name] = convertToLegacyMiddleware(middleware) + } +} + +const isProcessingMiddleware = () => { + try { + if (useNuxtApp()._processingMiddleware) { + return true + } + } catch { + // Within an async middleware + return true + } + return false +} + +export const navigateTo = (to: Route) => { + if (isProcessingMiddleware()) { + return to + } + const router: VueRouter = process.server ? useRouter() : (window as any).$nuxt.router + return router.push(to) +} + +/** This will abort navigation within a Nuxt route middleware handler. */ +export const abortNavigation = (err?: Error | string) => { + if (process.dev && !isProcessingMiddleware()) { + throw new Error('abortNavigation() is only usable inside a route middleware handler.') + } + if (err) { + throw err instanceof Error ? err : new Error(err) + } + return false +} + +type RouteMiddlewareReturn = void | Error | string | Location | boolean + +export interface RouteMiddleware { + (to: Route, from: Route): RouteMiddlewareReturn | Promise +} + +export const defineNuxtRouteMiddleware = (middleware: RouteMiddleware) => middleware diff --git a/packages/nuxt3/src/pages/runtime/composables.ts b/packages/nuxt3/src/pages/runtime/composables.ts index 8d9a204a86..c09b9f2079 100644 --- a/packages/nuxt3/src/pages/runtime/composables.ts +++ b/packages/nuxt3/src/pages/runtime/composables.ts @@ -60,7 +60,10 @@ const isProcessingMiddleware = () => { if (useNuxtApp()._processingMiddleware) { return true } - } catch {} + } catch { + // Within an async middleware + return true + } return false }