Back

How to Build and Use Plugins in Vite

How to Build and Use Plugins in Vite

Creating custom Vite plugins lets you extend your build process beyond the default configuration—whether you need to transform custom file types, inject build-time logic, or add development server middleware. If you’ve hit the limits of standard Vite configuration, building your own plugin is the natural next step.

This guide covers the essential concepts: how Vite’s Plugin API works, the key lifecycle hooks you’ll use, and practical examples to get you started. You’ll learn to create, test, and publish plugins that solve real-world problems in your development workflow.

Key Takeaways

  • Vite plugins extend both development and production build processes through lifecycle hooks
  • Most Rollup plugins work in Vite, but dev server features require Vite-specific hooks
  • Start with simple transformations before moving to advanced patterns like virtual modules
  • Follow naming conventions and documentation standards when publishing plugins

Understanding Vite Plugins and Their Relationship to Rollup

Vite plugins are JavaScript objects that hook into different stages of the development and build process. They’re built on Rollup’s plugin system, with additional hooks specific to Vite’s development server.

Vite uses two different build tools under the hood: esbuild powers the lightning-fast development server with native ES modules, while Rollup handles production bundling for optimal output. This dual architecture means your plugins can target specific environments using the apply option:

function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    apply: 'serve', // Only runs during development
    // ... hooks
  }
}

Most Rollup plugins work in Vite without modification. However, if you need dev server functionality—like adding middleware or modifying HTML during development—you’ll need Vite-specific hooks.

Basic Plugin Structure and Configuration

Every Vite plugin must have a name property and at least one hook function:

import type { Plugin } from 'vite'

function myPlugin(options?: MyPluginOptions): Plugin {
  return {
    name: 'vite-plugin-example',
    enforce: 'pre', // Optional: control plugin ordering
    apply: 'build', // Optional: 'serve' | 'build' | undefined
    
    // Hook functions go here
    transform(code, id) {
      if (id.endsWith('.custom')) {
        return transformCustomFile(code)
      }
    }
  }
}

The Plugin API provides TypeScript definitions for all hooks and options. Import the Plugin type from ‘vite’ for full type safety and autocompletion.

Essential Lifecycle Hooks in the Plugin API

Different hooks fire at different stages of the build process. Here are the most commonly used ones:

Configuration Hooks

  • config: Modify Vite config before it’s resolved
  • configResolved: Access the final resolved config

Transform Hooks

  • transform: Modify source code of individual modules
  • load: Custom loading logic for specific file types
  • resolveId: Custom module resolution
{
  name: 'transform-plugin',
  transform(code, id) {
    if (!id.includes('node_modules') && id.endsWith('.js')) {
      return {
        code: addCustomHeader(code),
        map: null // Source map support
      }
    }
  }
}

Server and Build Hooks

  • configureServer: Add custom middleware to the dev server
  • transformIndexHtml: Modify HTML files
  • writeBundle: Run after the bundle is written to disk

Each hook has specific return types and execution timing. The transform hook runs for every module, so keep it fast. Use load for expensive operations on specific files.

Building Your First Vite Plugin: Practical Examples

Let’s build three practical plugins that demonstrate common patterns:

Example 1: HTML Modification

function htmlPlugin(): Plugin {
  return {
    name: 'html-modifier',
    transformIndexHtml(html) {
      return html.replace(
        '</head>',
        '<script>console.log("Injected!")</script></head>'
      )
    }
  }
}

Example 2: Dev Server Middleware

function analyticsPlugin(): Plugin {
  return {
    name: 'request-analytics',
    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        console.log(`${req.method} ${req.url}`)
        next()
      })
    }
  }
}

Example 3: Build-Time File Generation

import path from 'path'
import fs from 'fs'
import type { Plugin, ResolvedConfig } from 'vite'

function generateManifest(): Plugin {
  let config: ResolvedConfig
  
  return {
    name: 'generate-manifest',
    configResolved(resolvedConfig) {
      config = resolvedConfig
    },
    writeBundle() {
      const manifestPath = path.join(config.build.outDir, 'manifest.json')
      fs.writeFileSync(manifestPath, JSON.stringify({
        timestamp: Date.now(),
        version: process.env.npm_package_version
      }))
    }
  }
}

For debugging, use console.log in your hooks to trace execution. The this.warn() and this.error() methods provide better error reporting than throwing exceptions.

Advanced Plugin Patterns and Best Practices

Virtual Modules

Create modules that don’t exist on disk:

const virtualModuleId = 'virtual:my-module'
const resolvedVirtualModuleId = '\0' + virtualModuleId

export function virtualPlugin(): Plugin {
  return {
    name: 'virtual-plugin',
    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    },
    load(id) {
      if (id === resolvedVirtualModuleId) {
        return `export const msg = "from virtual module"`
      }
    }
  }
}

Performance Optimization

Cache expensive transformations:

const cache = new Map<string, string>()

function cachedTransform(): Plugin {
  return {
    name: 'cached-transform',
    transform(code, id) {
      if (cache.has(id)) return cache.get(id)
      
      const result = expensiveOperation(code)
      cache.set(id, result)
      return result
    }
  }
}

Test your Vite plugins using Vitest with the createServer API to verify hook behavior in isolation.

Publishing and Sharing Your Plugin

Follow these conventions when publishing:

  1. Name your package vite-plugin-[name]
  2. Include "vite-plugin" in package.json keywords
  3. Document why it’s Vite-specific (if using Vite-only hooks)
  4. Add framework prefixes if framework-specific: vite-plugin-vue-[name]

Create a clear README with installation instructions, configuration examples, and API documentation. Submit your plugin to awesome-vite to reach the community.

Conclusion

Building custom Vite plugins gives you complete control over your build process. Start with simple transformations using hooks like transform or transformIndexHtml, then expand to more complex patterns as needed. The Plugin API is powerful yet approachable—most plugins require just a few hooks to solve specific problems.

For deeper exploration, check the official Vite plugin documentation and study popular plugins in the ecosystem. Your next build optimization or workflow improvement might be just a plugin away.

FAQs

Yes, most Rollup plugins work with Vite without modification. However, plugins that rely on moduleParsed hook or need dev server functionality require Vite-specific adaptations. Check the plugin documentation for Vite compatibility notes.

Use console.log statements in your hooks to trace execution flow. The this.warn() and this.error() methods provide better error reporting. You can also use the DEBUG environment variable with Vite to see detailed logs.

The enforce option controls plugin execution order. Pre plugins run before Vite core plugins, post plugins run after. Use pre for input transformations and post for output modifications. Without enforce, plugins run in the order they appear.

Consider publishing if your plugin solves a problem others might face, even if specialized. Document its specific use case clearly. For truly project-specific plugins, keep them local or in a private registry instead.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay