vite-plugin-vue-middleware
May 1, 2026 ยท View on GitHub
File-based navigation guards for Vue Router with full type safety and automated middleware injection.
โจ Features
- ๐ Zero-Config: Automatically scans your middleware directory and generates configurations.
- ๐ก๏ธ Type-Safe: Autogenerates
.d.tsfiles to provide full IntelliSense forvue-router'sRouteMeta. - ๐ฆ Virtual Module: Seamlessly integrate with your router using
virtual:vue-middleware. - ๐ HMR Support: Adding, removing, or renaming middleware files triggers hot updates and type regeneration.
- ๐ ๏ธ Flexible Order: Supports global middleware (.global) and custom execution weight via numeric prefixes.
- ๐ Async Context: Automatically preserves Vue injection context (
inject()) acrossawaitboundaries in async middleware via a build-time transform.
๐ฆ Installation
Using npm:
npm install -D vite-plugin-vue-middleware
Using yarn:
yarn add -D vite-plugin-vue-middleware
Using pnpm:
pnpm add -D vite-plugin-vue-middleware
๐ Quick Start
1. Configure the Plugin
Add the plugin to your vite.config.ts:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueMiddleware from 'vite-plugin-vue-middleware';
export default defineConfig({
plugins: [
vue(),
vueMiddleware({
// Optional: Custom middleware directory (default: 'src/middleware')
middlewareDir: 'src/middleware',
// Optional: Custom d.ts generation path (default: 'middleware.d.ts')
dts: 'src/types/middleware.d.ts',
}),
],
});
2. TypeScript Setup (Required)
To ensure TypeScript recognizes the virtual module and the generated types, follow these steps:
A. Add to env.d.ts
Reference the plugin's client types in your global declaration file:
/// <reference types="vite-plugin-vue-middleware/client" />
Alternatively, add it to compilerOptions.types in your tsconfig.json:
{
"compilerOptions": {
"types": ["vite-plugin-vue-middleware/client"]
}
}
B. Include the generated .d.ts
Ensure your tsconfig.json includes the generated type file. If you use the default path (project root), you must add it to the include array:
{
"include": [
"src/**/*",
"src/**/*.vue",
"./middleware.d.ts" // Required if generated at project root (default)
]
}
Note: if you generate the file inside src/ (e.g., src/types/middleware.d.ts), it will likely be covered by your existing "src/**/*" rule.
3. Create Middleware
Create middleware files in your src/middleware directory:
// src/middleware/01.auth.global.ts
import { defineMiddleware } from 'virtual:vue-middleware';
export default defineMiddleware((to, from) => {
const isLogged = false; // simulate auth state
if (!isLogged && to.path !== '/login') {
return '/login';
}
});
4. Inject into Router
Import and use setupMiddleware during your router initialization:
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import { setupMiddleware } from 'virtual:vue-middleware';
const router = createRouter({
history: createWebHistory(),
routes: [
/* your routes */
],
});
// Automatically bind middleware logic to router guards
setupMiddleware(router);
export default router;
๐ How it Works
The plugin provides a wrapper around router.beforeEach to handle the middleware lifecycle:
- Global Middlewares: Executed first, followed by the order of their numeric prefixes (e.g.,
01.log.tsbefore02.auth.ts). - Named Middlewares: Executed next, based on the order defined in the route's
meta.middlewarearray.
Return Values
Handlers support async/await and follow the same logic as vue-router guards:
return: Continue to the next middleware or navigation.return false: Abort the navigation.return '/path': Redirect to a specific path.return { name: 'login' }: Redirect to a named route.
๐ Naming Conventions
The plugin uses file naming to determine middleware properties:
| Rule | Example | Description |
|---|---|---|
| Global Execution | auth.global.ts | Applied to all route navigations automatically. |
| Execution Order | 01.log.ts | Numeric prefix determines weight (lower numbers run first). |
| Named Middleware | guest.ts | Manually referenced in route meta or SFC definePage. |
Using Named Middleware in Pages
const routes = [
{
path: '/dashboard',
component: () => import('./Dashboard.vue'),
meta: {
// Benefit from generated .d.ts with full IntelliSense
middleware: ['auth', '02-analytics'],
},
},
];
๐ Integration with Vue Router v5 (File-based Routing)
This plugin is fully compatible with the modern Vue Router v5 ecosystem and unplugin-vue-router (which provides the official File-based Routing logic for Vue). You can define middleware directly in your .vue files using the definePage macro:
<script setup lang="ts">
/**
* Using definePage (Vue Router v5 / unplugin-vue-router)
*/
import { definePage } from 'unplugin-vue-router/runtime';
definePage({
meta: {
// You'll get the same type-safe IntelliSense here!
middleware: ['auth'],
},
});
</script>
โ๏ธ Configuration
| Option | Type | Default | Description |
|---|---|---|---|
middlewareDir | string | 'src/middleware' | Root directory to scan for middleware. |
exclude | string[] | [] | Glob patterns to ignore files. |
dts | boolean | string | true | Enable/Disable .d.ts generation or specify path. |
asyncContext | boolean | true | Preserve Vue injection context (inject()) across await boundaries via a build-time transform. |
๐ Async Context
By default, asyncContext is enabled. It applies a build-time AST transform to every async middleware function that contains await, allowing inject() and composables that rely on it (e.g. useQueryClient(), useStore()) to work correctly even after await statements.
The Problem
Vue's inject() API only works inside an active component or application context. In standard async middleware, calling inject() after an await boundary fails because the execution context is lost:
// โ inject() called after await โ throws outside of Vue context
export default defineMiddleware(async (to, from) => {
await someAsyncCheck();
const store = useStore(); // Runtime error: inject() must be called inside setup()
});
How It Works
When asyncContext: true (default), the plugin transforms async middleware into a generator-based executor at build time. Each segment between yield points runs inside app.runWithContext(), restoring the Vue injection context on every resume:
// โ
Works โ inject() is available on every segment after await
export default defineMiddleware(async (to, from) => {
await someAsyncCheck();
const store = useStore(); // Works correctly
});
The transform is transparent โ you write standard async/await and the plugin handles the rest. No manual changes are required.
Limitations
for await...of: Not supported with the async-context transform. A warning is logged and the affected middleware runs without context preservation acrossawaitboundaries.- Nested async functions: Only top-level
awaitexpressions in thedefineMiddlewarecallback are transformed. Nested async functions are left untouched.
To opt out of the transform entirely:
vueMiddleware({ asyncContext: false });