| | |

Vite 12 ⚡ Plugins — Using Rollup-compatible extensions

⏱️ Estimated read: 11 min | |

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

PropertyRequiredPurpose
nameYesUnique identifier
enforceNo'pre', 'post', or default
applyNo'serve', 'build', or function

Plugin ordering

PositionPlugins
FirstAlias plugins
ThenUser plugins with enforce: 'pre'
ThenVite core plugins
ThenUser plugins without enforce
ThenVite build plugins
LastUser plugins with enforce: 'post'

Rollup hooks (shared)

HookPurpose
resolveIdResolve an import to a file path
loadLoad the file contents
transformTransform the code
buildStartBefore the build
buildEndAfter the build
closeBundleAfter the bundle is written

Vite-specific hooks

HookPurpose
configModify the config before resolution
configResolvedRead the final resolved config
configureServerAdd dev server middleware
transformIndexHtmlModify the HTML entry
handleHotUpdateCustomize 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

PitfallWhy It HappensFix
Rollup plugin does not runWrong order relative to Vite coreAdd enforce: 'pre' or 'post'
Dev middleware does not run after ViteDefault position is before internalReturn a function from configureServer
Plugin runs during build but not serveapply: 'serve' usedChange to apply: 'build' or remove
config hook changes have no effectPlugins are resolved before the hookUse the configResolved hook instead
HTML transform not appliedFramework handles HTML separatelyCheck 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

ItemValue
Plugin basisRollup plugin interface
Required propertyname
Order controlenforce: 'pre', 'post', or default
Conditional controlapply: 'serve', 'build', or function
Shared hooksresolveId, load, transform
Vite hooksconfig, configResolved, configureServer, transformIndexHtml
Middleware positionDefault before internal; return function for after
Plugin arrayFlattened, falsy values ignored

Key takeaways:

  • A Vite plugin is a Rollup plugin with Vite-specific extensions. The resolveId, load, and transform hooks are identical to Rollup’s. A plugin that only uses these hooks can run in a pure Rollup build. A plugin that uses configureServer or transformIndexHtml is Vite-specific .
  • The enforce property controls the plugin’s position in the chain. pre runs before Vite’s core plugins, post runs after Vite’s build plugins, and the default runs in between. A Rollup plugin that transforms a file type Vite does not recognize needs enforce: 'pre' .
  • The apply property limits a plugin to a specific command. A bundle analyzer runs only during build. A dev overlay runs only during serve. The function form provides more granular control .
  • configureServer adds 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 .
  • config runs before resolution; configResolved runs after. The config hook can return a partial config or mutate the config, but injecting plugins inside it has no effect. The configResolved hook is the place to read the final command and mode and 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.

Was this helpful?


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!