Vite 12 ⚡ Plugins — Using Rollup-compatible extensions
Vite’s plugin system is built directly on Rollup’s plugin interface. A Vite plugin is a Rollup plugin with a few extra Vite-specific hooks and properties. This means the mature Rollup plugin ecosystem is available to Vite projects without adaptation, and a plugin written for Vite can often run in a pure Rollup build with no changes . The compatibility is not superficial: the resolveId, load, and transform hooks run in the same order and with the same signatures as they do in Rollup.
This chapter covers the plugin structure, the plugins array in vite.config.js, the enforce property that controls execution order, the apply property that limits a plugin to development or build, and the Vite-specific hooks that Rollup ignores. It also covers the practical distinction between using an existing Rollup plugin and writing a plugin that uses Vite’s dev server extensions.
Key point: A Vite plugin is a Rollup plugin. The name property is required, the Rollup hooks are the same, and the enforce and apply properties are Vite additions that control when the plugin runs. A plugin that uses configureServer or transformIndexHtml is Vite-specific and will not work in a pure Rollup build.
Why Rollup compatibility matters
The ecosystem problem. Rollup has been the bundler behind Vite’s production build since Vite’s inception. The plugin authors who wrote @rollup/plugin-image, rollup-plugin-visualizer, and hundreds of others solved the same problems that Vite projects face. Reusing their work is faster and more reliable than reimplementing it. The Vite documentation states that plugins are “based on Rollup’s well-designed plugin interface with a few extra Vite-specific options” .
The dev server problem. Rollup only runs a build. Vite runs a development server with HMR, a module graph, and a middleware stack. The Vite-specific hooks—config, configResolved, configureServer, transformIndexHtml, handleHotUpdate—are what allow a plugin to interact with the parts of Vite that Rollup does not have . A plugin that only transforms code can be a pure Rollup plugin. A plugin that adds middleware or modifies the HTML must use the Vite hooks.
The ordering problem. Rollup plugins run in the order they are listed. Vite inserts its own core plugins into the chain, and a Rollup plugin that expects to run before or after the core plugins needs a way to express that. The enforce property is the mechanism: pre runs before Vite’s core plugins, post runs after Vite’s build plugins, and the default runs in between .
a. The plugin structure and the plugins array
A Vite plugin is an object with a name and one or more hook functions. The name is required and should be unique to avoid collisions with other plugins.
// vite.config.js
import { defineConfig } from 'vite'
function myPlugin() {
return {
name: 'my-plugin',
transform(code, id) {
if (id.endsWith('.special')) {
return { code: transformedCode, map: null }
}
},
}
}
export default defineConfig({
plugins: [myPlugin()],
})
The plugins array accepts plugin objects, presets (arrays of plugins), and falsy values. Vite flattens the array internally and ignores the falsy values, which makes it easy to activate a plugin conditionally .
export default defineConfig({
plugins: [
legacy({ targets: ['defaults'] }),
process.env.ANALYZE && visualizer(),
],
})
A Rollup plugin is added the same way. The @rollup/plugin-image plugin is imported and placed in the array, and the enforce: 'pre' property ensures it runs before Vite’s core plugins .
import image from '@rollup/plugin-image'
export default defineConfig({
plugins: [
{ ...image(), enforce: 'pre' },
],
})
b. The enforce property
The enforce property controls the plugin’s position relative to Vite’s own plugins. The resolved order is: alias plugins, user plugins with enforce: 'pre', Vite core plugins, user plugins without enforce, Vite build plugins, and user plugins with enforce: 'post' .
export default defineConfig({
plugins: [
{ ...image(), enforce: 'pre' },
{ ...visualizer(), enforce: 'post' },
],
})
A Rollup plugin that transforms a file type that Vite’s core plugins might not recognize needs enforce: 'pre'. A plugin that analyzes the final bundle needs enforce: 'post'. The default position is correct for most plugins that transform source code, because they run after Vite’s core transforms and before the final bundle generation.
c. The apply property and conditional plugins
The apply property limits a plugin to a specific command. The value can be 'serve', 'build', or a function that receives the config and returns a boolean.
function buildOnlyPlugin() {
return {
name: 'build-only',
apply: 'build',
// ...
}
}
A plugin that generates a bundle analysis report is only useful during the build. A plugin that injects a dev-only debugging overlay is only useful during serve. The apply property prevents the plugin from running in the wrong context .
The function form provides more control:
apply(config, { command }) {
return command === 'build' && !config.build.ssr
}
d. The Vite-specific hooks
Rollup ignores the Vite-specific hooks, and a plugin that uses them is no longer portable to a pure Rollup build. The hooks that matter for most plugins are config, configResolved, configureServer, and transformIndexHtml .
The config hook receives the user config and the env, and returns a partial config to merge or mutates the config directly. It runs before the config is resolved, which means injecting other plugins inside it has no effect.
The configResolved hook receives the final resolved config. It is the place to read the command and mode and store them for use in other hooks.
The configureServer hook receives the dev server instance. It is the place to add custom middleware to the internal Connect app. Returning a function from the hook installs the middleware after Vite’s internal middleware, which is the correct position for a fallback handler.
The transformIndexHtml hook receives the HTML string and can return a modified string or an array of tags to inject. It is the mechanism for injecting analytics scripts, meta tags, or dev-only overlays.
Complete Example Session
// ============================================
// PART 1: A MINIMAL PLUGIN
// ============================================
export default defineConfig({
plugins: [
{
name: 'my-plugin',
transform(code, id) {
if (id.endsWith('.txt')) {
return { code: `export default ${JSON.stringify(code)}` }
}
},
},
],
})
// ============================================
// PART 2: USING A ROLLUP PLUGIN
// ============================================
import image from '@rollup/plugin-image'
export default defineConfig({
plugins: [
{ ...image(), enforce: 'pre' },
],
})
// ============================================
// PART 3: THE ENFORCE PROPERTY
// ============================================
export default defineConfig({
plugins: [
{ ...image(), enforce: 'pre' },
{ ...visualizer(), enforce: 'post' },
],
})
// ============================================
// PART 4: THE APPLY PROPERTY
// ============================================
export default defineConfig({
plugins: [
{
name: 'build-only',
apply: 'build',
generateBundle() { /* ... */ },
},
],
})
// ============================================
// PART 5: THE CONFIG HOOK
// ============================================
function configPlugin() {
return {
name: 'config-plugin',
config(config, { command }) {
if (command === 'serve') {
config.server = { port: 3000 }
}
},
}
}
// ============================================
// PART 6: THE CONFIGRESOLVED HOOK
// ============================================
function readConfigPlugin() {
let config
return {
name: 'read-config',
configResolved(resolvedConfig) {
config = resolvedConfig
},
transform(code, id) {
if (config.command === 'serve') { /* ... */ }
},
}
}
// ============================================
// PART 7: THE CONFIGURESERVER HOOK
// ============================================
function middlewarePlugin() {
return {
name: 'middleware-plugin',
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === '/health') {
res.end('ok')
return
}
next()
})
},
}
}
// ============================================
// PART 8: POST MIDDLEWARE
// ============================================
function fallbackPlugin() {
return {
name: 'fallback',
configureServer(server) {
return () => {
server.middlewares.use((req, res, next) => {
// runs after Vite's internal middleware
})
}
},
}
}
// ============================================
// PART 9: THE TRANSFORMINDEXHTML HOOK
// ============================================
function htmlPlugin() {
return {
name: 'html-plugin',
transformIndexHtml(html) {
return html.replace(
'</head>',
'<meta name="build-time" content="' + Date.now() + '"></head>'
)
},
}
}
// ============================================
// PART 10: THE COMPLETE CONFIG
// ============================================
import image from '@rollup/plugin-image'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
{ ...image(), enforce: 'pre' },
{ ...visualizer(), enforce: 'post', apply: 'build' },
],
})
The ten parts covered the minimal plugin, the Rollup plugin, the enforce property, the apply property, the config hook, the configResolved hook, the configureServer hook, the post middleware, the transformIndexHtml hook, and the complete config.
Quick Reference
Plugin properties
| Property | Required | Purpose |
|---|---|---|
name | Yes | Unique identifier |
enforce | No | 'pre', 'post', or default |
apply | No | 'serve', 'build', or function |
Plugin ordering
| Position | Plugins |
|---|---|
| First | Alias plugins |
| Then | User plugins with enforce: 'pre' |
| Then | Vite core plugins |
| Then | User plugins without enforce |
| Then | Vite build plugins |
| Last | User plugins with enforce: 'post' |
Rollup hooks (shared)
| Hook | Purpose |
|---|---|
resolveId | Resolve an import to a file path |
load | Load the file contents |
transform | Transform the code |
buildStart | Before the build |
buildEnd | After the build |
closeBundle | After the bundle is written |
Vite-specific hooks
| Hook | Purpose |
|---|---|
config | Modify the config before resolution |
configResolved | Read the final resolved config |
configureServer | Add dev server middleware |
transformIndexHtml | Modify the HTML entry |
handleHotUpdate | Customize HMR handling |
Best Practices
✅ Do This:
// Use enforce: 'pre' for Rollup plugins that transform new file types
{ ...image(), enforce: 'pre' } // ✅
// Use apply: 'build' for bundle-analysis plugins
{ ...visualizer(), apply: 'build' } // ✅
// Return a function from configureServer for post middleware
configureServer(server) { return () => { ... } } // ✅
// Use configResolved to store the final config
configResolved(config) { this.config = config } // ✅
❌ Don’t Do This:
// Omit the name property
{ transform() { ... } } // ❌
// Inject plugins inside the config hook
config(config) { config.plugins.push(otherPlugin) } // ❌ (no effect)
// Assume configureServer runs during build
configureServer(server) { ... } // ❌ (dev only)
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Rollup plugin does not run | Wrong order relative to Vite core | Add enforce: 'pre' or 'post' |
| Dev middleware does not run after Vite | Default position is before internal | Return a function from configureServer |
| Plugin runs during build but not serve | apply: 'serve' used | Change to apply: 'build' or remove |
config hook changes have no effect | Plugins are resolved before the hook | Use the configResolved hook instead |
| HTML transform not applied | Framework handles HTML separately | Check the framework’s plugin chain |
Real-World Examples
1. Rollup image plugin
{ ...image(), enforce: 'pre' }
2. Visualizer plugin
{ ...visualizer(), apply: 'build', enforce: 'post' }
3. Config hook
config(config, { command }) { if (command === 'serve') config.server.port = 3000 }
4. ConfigResolved hook
configResolved(config) { this.command = config.command }
5. ConfigureServer hook
configureServer(server) { server.middlewares.use((req, res, next) => next()) }
6. Post middleware
configureServer(server) { return () => { server.middlewares.use(fallback) } }
7. TransformIndexHtml
transformIndexHtml(html) { return html.replace('</head>', '<meta ...></head>') }
8. Conditional plugin
process.env.ANALYZE && visualizer()
9. Preset array
plugins: [frameworkPreset(), customPlugin()]
10. The falsy plugin
plugins: [devOnly && devPlugin()]
Visual
The plugin pipeline
┌─────────────────────────────────────────────────────────────┐
│ Alias plugins │
│ │ │
│ ▼ │
│ User plugins with enforce: 'pre' │
│ │ │
│ ▼ │
│ Vite core plugins │
│ │ │
│ ▼ │
│ User plugins (default) │
│ │ │
│ ▼ │
│ Vite build plugins │
│ │ │
│ ▼ │
│ User plugins with enforce: 'post' │
└─────────────────────────────────────────────────────────────┘
The apply property
┌─────────────────────────────────────────────────────────────┐
│ apply: 'serve' │
│ → runs during vite dev only │
│ │
│ apply: 'build' │
│ → runs during vite build only │
│ │
│ no apply │
│ → runs in both │
└─────────────────────────────────────────────────────────────┘
Rollup vs Vite hooks
┌─────────────────────────────────────────────────────────────┐
│ SHARED (Rollup) VITE-SPECIFIC │
│ │
│ resolveId config │
│ load configResolved │
│ transform configureServer │
│ buildStart transformIndexHtml │
│ buildEnd handleHotUpdate │
└─────────────────────────────────────────────────────────────┘
The middleware position
┌─────────────────────────────────────────────────────────────┐
│ configureServer(server) { │
│ server.middlewares.use(beforeVite) │
│ return () => { │
│ server.middlewares.use(afterVite) │
│ } │
│ } │
│ │
│ beforeVite runs before Vite's internal middleware. │
│ afterVite runs after Vite's internal middleware. │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Plugin basis | Rollup plugin interface |
| Required property | name |
| Order control | enforce: 'pre', 'post', or default |
| Conditional control | apply: 'serve', 'build', or function |
| Shared hooks | resolveId, load, transform |
| Vite hooks | config, configResolved, configureServer, transformIndexHtml |
| Middleware position | Default before internal; return function for after |
| Plugin array | Flattened, falsy values ignored |
Key takeaways:
- A Vite plugin is a Rollup plugin with Vite-specific extensions. The
resolveId,load, andtransformhooks are identical to Rollup’s. A plugin that only uses these hooks can run in a pure Rollup build. A plugin that usesconfigureServerortransformIndexHtmlis Vite-specific . - The
enforceproperty controls the plugin’s position in the chain.preruns before Vite’s core plugins,postruns after Vite’s build plugins, and the default runs in between. A Rollup plugin that transforms a file type Vite does not recognize needsenforce: 'pre'. - The
applyproperty limits a plugin to a specific command. A bundle analyzer runs only duringbuild. A dev overlay runs only duringserve. The function form provides more granular control . configureServeradds middleware to the dev server. The default position is before Vite’s internal middleware. Returning a function from the hook installs the middleware after Vite’s internal middleware, which is the correct position for a fallback handler .configruns before resolution;configResolvedruns after. Theconfighook can return a partial config or mutate the config, but injecting plugins inside it has no effect. TheconfigResolvedhook is the place to read the finalcommandandmodeand store them for other hooks .- The plugins array is flattened and falsy values are ignored. A plugin can be activated conditionally by placing it behind a
&&expression. A framework integration can be a preset array that is flattened into the main array .
Remember: Vite’s plugin system is Rollup’s plugin system plus a dev server. Use a Rollup plugin when the plugin only transforms code. Use enforce to control the order relative to Vite’s core plugins. Use apply to limit the plugin to development or build. Use configureServer and transformIndexHtml when the plugin needs to interact with the dev server or the HTML entry. The compatibility is what makes the ecosystem usable; the Vite-specific hooks are what make the development experience possible.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!