vite-plugin-vue-middleware

May 1, 2026 ยท View on GitHub

npm version NPM Downloads build status license

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.ts files to provide full IntelliSense for vue-router's RouteMeta.
  • ๐Ÿ“ฆ 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()) across await boundaries 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:

  1. Global Middlewares: Executed first, followed by the order of their numeric prefixes (e.g., 01.log.ts before 02.auth.ts).
  2. Named Middlewares: Executed next, based on the order defined in the route's meta.middleware array.

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:

RuleExampleDescription
Global Executionauth.global.tsApplied to all route navigations automatically.
Execution Order01.log.tsNumeric prefix determines weight (lower numbers run first).
Named Middlewareguest.tsManually 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

OptionTypeDefaultDescription
middlewareDirstring'src/middleware'Root directory to scan for middleware.
excludestring[][]Glob patterns to ignore files.
dtsboolean | stringtrueEnable/Disable .d.ts generation or specify path.
asyncContextbooleantruePreserve 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 across await boundaries.
  • Nested async functions: Only top-level await expressions in the defineMiddleware callback are transformed. Nested async functions are left untouched.

To opt out of the transform entirely:

vueMiddleware({ asyncContext: false });

๐Ÿ“„ License

MIT License ยฉ 2026 Roya