unplugin vs Rollup Plugin vs Vite Plugin: Universal Build Plugins (2026)
TL;DR
unplugin is the universal plugin system — write ONE plugin that works in Vite, Rollup, webpack, esbuild, and rspack simultaneously. Rollup plugins use Rollup's native hook API — the foundation that Vite extends, widely adopted, well-documented. Vite plugins extend Rollup's API with Vite-specific hooks — dev server middleware, HMR handling, and config resolution. In 2026: unplugin if you're publishing a plugin for the ecosystem, native Rollup/Vite plugins for project-specific needs.
Key Takeaways
- unplugin: ~8M weekly downloads — one plugin API → Vite + Rollup + webpack + esbuild + rspack
- Rollup plugins: native to Rollup — Vite uses the same API in build mode
- Vite plugins: extends Rollup — adds
configureServer,handleHotUpdate,confighooks - Vite plugins ARE Rollup plugins + extra hooks — any Rollup plugin works in Vite
- unplugin is used by Vue ecosystem (unplugin-vue-components, unplugin-auto-import) and many others
- Write Rollup-compatible hooks when possible — they work in both Rollup and Vite
Plugin Architecture
Rollup Plugin API (foundation):
resolveId → load → transform → buildEnd → generateBundle
Vite Plugin API (extends Rollup):
config → configResolved → configureServer → resolveId → load → transform
→ handleHotUpdate → buildEnd → generateBundle
unplugin (universal wrapper):
Same hooks → compiles to Rollup, Vite, webpack, esbuild, rspack plugins
Rollup Plugin
Rollup plugins — the foundation:
Basic plugin structure
import type { Plugin } from "rollup"
function myPlugin(): Plugin {
return {
name: "my-plugin",
// Called for each import — resolve module paths:
resolveId(source, importer) {
if (source === "virtual:my-module") {
return source // Handle this module ourselves
}
return null // Let other plugins/default handle it
},
// Called after resolveId — provide module content:
load(id) {
if (id === "virtual:my-module") {
return `export const version = "1.0.0"`
}
return null
},
// Transform module source code:
transform(code, id) {
if (!id.endsWith(".ts")) return null
// Example: inject build metadata
return code.replace(
"__BUILD_TIME__",
JSON.stringify(new Date().toISOString())
)
},
}
}
Virtual modules
function virtualEnvPlugin(): Plugin {
const PREFIX = "\0virtual:env"
return {
name: "virtual-env",
resolveId(id) {
if (id === "virtual:env") return PREFIX
},
load(id) {
if (id === PREFIX) {
return `
export const NODE_ENV = ${JSON.stringify(process.env.NODE_ENV)}
export const BUILD_TIME = ${JSON.stringify(new Date().toISOString())}
export const GIT_SHA = ${JSON.stringify(process.env.GIT_SHA ?? "dev")}
`
}
},
}
}
// Usage in app:
import { NODE_ENV, BUILD_TIME } from "virtual:env"
Code transformation
import MagicString from "magic-string"
function replacePlugin(replacements: Record<string, string>): Plugin {
return {
name: "replace",
transform(code, id) {
let hasReplacement = false
const s = new MagicString(code)
for (const [search, replace] of Object.entries(replacements)) {
const index = code.indexOf(search)
if (index !== -1) {
s.overwrite(index, index + search.length, replace)
hasReplacement = true
}
}
if (!hasReplacement) return null
return {
code: s.toString(),
map: s.generateMap({ hires: true }),
}
},
}
}
Vite Plugin
Vite plugins — Rollup + Vite-specific hooks:
Vite-specific hooks
import type { Plugin } from "vite"
function myVitePlugin(): Plugin {
return {
name: "my-vite-plugin",
// Modify Vite config:
config(config, { command }) {
if (command === "build") {
return { define: { __PROD__: "true" } }
}
return { define: { __PROD__: "false" } }
},
// Access resolved config:
configResolved(config) {
console.log("Root:", config.root)
console.log("Mode:", config.mode)
},
// Add dev server middleware:
configureServer(server) {
server.middlewares.use("/api/health", (req, res) => {
res.end(JSON.stringify({ status: "ok" }))
})
},
// Handle HMR:
handleHotUpdate({ file, server }) {
if (file.endsWith(".mdx")) {
console.log("MDX changed, triggering full reload")
server.ws.send({ type: "full-reload" })
return [] // Prevent default HMR
}
},
// Standard Rollup hooks still work:
resolveId(source) {
if (source === "virtual:app-info") return "\0virtual:app-info"
},
load(id) {
if (id === "\0virtual:app-info") {
return `export const name = "PkgPulse"`
}
},
}
}
Enforce order
function earlyPlugin(): Plugin {
return {
name: "early-plugin",
enforce: "pre", // Run BEFORE other plugins
transform(code, id) {
// Pre-processing: runs before Vite's own transforms
},
}
}
function latePlugin(): Plugin {
return {
name: "late-plugin",
enforce: "post", // Run AFTER other plugins
transform(code, id) {
// Post-processing: runs after Vite's transforms
},
}
}
Build-only vs serve-only
function devOnlyPlugin(): Plugin {
return {
name: "dev-only",
apply: "serve", // Only active during vite dev
configureServer(server) {
// Mock API endpoints in dev
},
}
}
function buildOnlyPlugin(): Plugin {
return {
name: "build-only",
apply: "build", // Only active during vite build
generateBundle(options, bundle) {
// Add build metadata to output
},
}
}
unplugin
unplugin — universal plugin framework:
Write once, use everywhere
import { createUnplugin } from "unplugin"
// Define the plugin ONCE:
const myUnplugin = createUnplugin((options: { prefix?: string }) => {
return {
name: "my-universal-plugin",
// Standard hooks (work in all bundlers):
resolveId(id) {
if (id === "virtual:config") return id
},
load(id) {
if (id === "virtual:config") {
return `export const prefix = ${JSON.stringify(options?.prefix ?? "app")}`
}
},
transformInclude(id) {
return id.endsWith(".ts") // Only transform .ts files
},
transform(code, id) {
return code.replace("__PREFIX__", options?.prefix ?? "app")
},
}
})
// Export for each bundler:
export const vite = myUnplugin.vite
export const rollup = myUnplugin.rollup
export const webpack = myUnplugin.webpack
export const esbuild = myUnplugin.esbuild
export const rspack = myUnplugin.rspack
Usage in each bundler
// vite.config.ts:
import { vite as myPlugin } from "my-universal-plugin"
export default { plugins: [myPlugin({ prefix: "pkgpulse" })] }
// rollup.config.js:
import { rollup as myPlugin } from "my-universal-plugin"
export default { plugins: [myPlugin({ prefix: "pkgpulse" })] }
// webpack.config.js:
const { webpack: MyPlugin } = require("my-universal-plugin")
module.exports = { plugins: [new MyPlugin({ prefix: "pkgpulse" })] }
// esbuild:
import { esbuild as myPlugin } from "my-universal-plugin"
esbuild.build({ plugins: [myPlugin({ prefix: "pkgpulse" })] })
Real-world example: auto-import
import { createUnplugin } from "unplugin"
import { readFileSync } from "node:fs"
// Auto-import components without manual import statements:
const autoImport = createUnplugin((options: { imports: Record<string, string> }) => {
const importMap = options.imports
return {
name: "unplugin-auto-import",
transformInclude(id) {
return /\.[jt]sx?$/.test(id)
},
transform(code, id) {
const usedImports: string[] = []
for (const [name, source] of Object.entries(importMap)) {
if (code.includes(name) && !code.includes(`import.*${name}`)) {
usedImports.push(`import { ${name} } from "${source}"`)
}
}
if (usedImports.length === 0) return null
return usedImports.join("\n") + "\n" + code
},
}
})
Feature Comparison
| Feature | unplugin | Rollup Plugin | Vite Plugin |
|---|---|---|---|
| Vite support | ✅ | ✅ | ✅ (native) |
| Rollup support | ✅ | ✅ (native) | ❌ |
| webpack support | ✅ | ❌ | ❌ |
| esbuild support | ✅ | ❌ | ❌ |
| rspack support | ✅ | ❌ | ❌ |
| Dev server hooks | ❌ | ❌ | ✅ |
| HMR hooks | ❌ | ❌ | ✅ |
| Virtual modules | ✅ | ✅ | ✅ |
| Code transform | ✅ | ✅ | ✅ |
| transformInclude | ✅ | ❌ (manual) | ❌ (manual) |
| Weekly downloads | ~8M | bundled | bundled |
When to Use Each
Write an unplugin if:
- Publishing a plugin for the ecosystem (users on different bundlers)
- Need the plugin to work in Vite, webpack, esbuild, AND rspack
- Building a universal code transform (auto-import, macros, etc.)
- Vue/UnJS ecosystem plugin
Write a Rollup plugin if:
- Plugin is for build-time only (no dev server hooks needed)
- Want compatibility with both Rollup and Vite automatically
- Standard code transformation, virtual modules, or asset generation
Write a Vite plugin if:
- Need Vite-specific hooks (
configureServer,handleHotUpdate) - Dev server middleware or HMR behavior
- Vite-only project (no need for cross-bundler support)
- Extending Vite's config or environment handling
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on unplugin v1.x, Rollup v4.x, and Vite v6.x.