> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/vitejs/vite/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugin API

> Complete reference for Vite's Plugin API including hooks, conventions, and examples

Vite plugins extend Rolldown's plugin interface with Vite-specific options. You can write a plugin once and have it work for both dev and build.

<Note>
  It is recommended to go through [Rolldown's plugin documentation](https://rolldown.rs/apis/plugin-api) first before reading the sections below.
</Note>

## Authoring a Plugin

Vite strives to offer established patterns out of the box, so before creating a new plugin make sure that you check the Features guide to see if your need is covered. Also review available community plugins, both in the form of a [compatible Rollup plugin](https://github.com/rollup/awesome) and [Vite Specific plugins](https://github.com/vitejs/awesome-vite#plugins).

When creating a plugin, you can inline it in your `vite.config.js`. There is no need to create a new package for it.

<Tip>
  When learning, debugging, or authoring plugins, we suggest including [vite-plugin-inspect](https://github.com/antfu/vite-plugin-inspect) in your project. It allows you to inspect the intermediate state of Vite plugins. After installing, you can visit `localhost:5173/__inspect/` to inspect the modules and transformation stack of your project.
</Tip>

## Conventions

If the plugin doesn't use Vite specific hooks and can be implemented as a Compatible Rolldown Plugin, then it is recommended to use the [Rolldown Plugin naming conventions](https://rolldown.rs/apis/plugin-api#conventions).

**Rolldown Plugins:**

* Should have a clear name with `rolldown-plugin-` prefix
* Include `rolldown-plugin` and `vite-plugin` keywords in package.json

**Vite Only Plugins:**

* Should have a clear name with `vite-plugin-` prefix
* Include `vite-plugin` keyword in package.json
* Include a section in the plugin docs detailing why it is a Vite only plugin

**Framework-Specific Plugins:**

* `vite-plugin-vue-` prefix for Vue Plugins
* `vite-plugin-react-` prefix for React Plugins
* `vite-plugin-svelte-` prefix for Svelte Plugins

## Plugin Configuration

Users add plugins to the project `devDependencies` and configure them using the `plugins` array option:

```js vite.config.js theme={null}
import vitePlugin from 'vite-plugin-feature'
import rollupPlugin from 'rollup-plugin-feature'

export default defineConfig({
  plugins: [vitePlugin(), rollupPlugin()],
})
```

Falsy plugins will be ignored, which can be used to easily activate or deactivate plugins.

`plugins` also accepts presets including several plugins as a single element:

```js theme={null}
// framework-plugin
import frameworkRefresh from 'vite-plugin-framework-refresh'
import frameworkDevtools from 'vite-plugin-framework-devtools'

export default function framework(config) {
  return [frameworkRefresh(config), frameworkDevTools(config)]
}
```

## Simple Examples

<Tip>
  It is common convention to author a Vite/Rolldown/Rollup plugin as a factory function that returns the actual plugin object. The function can accept options which allows users to customize the behavior of the plugin.
</Tip>

### Transforming Custom File Types

```js theme={null}
const fileRegex = /\.(my-file-ext)$/

export default function myPlugin() {
  return {
    name: 'transform-file',

    transform(src, id) {
      if (fileRegex.test(id)) {
        return {
          code: compileFileToJS(src),
          map: null, // provide source map if available
        }
      }
    },
  }
}
```

### Virtual Modules Convention

Virtual modules are a useful scheme that allows you to pass build time information to the source files using normal ESM import syntax.

```js theme={null}
export default function myPlugin() {
  const virtualModuleId = 'virtual:my-module'
  const resolvedVirtualModuleId = '\0' + virtualModuleId

  return {
    name: 'my-plugin', // required, will show up in warnings and errors
    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    },
    load(id) {
      if (id === resolvedVirtualModuleId) {
        return `export const msg = "from virtual module"`
      }
    },
  }
}
```

Which allows importing the module in JavaScript:

```js theme={null}
import { msg } from 'virtual:my-module'

console.log(msg)
```

Virtual modules in Vite (and Rolldown / Rollup) are prefixed with `virtual:` for the user-facing path by convention. Internally, plugins that use virtual modules should prefix the module ID with `\0` while resolving the id, a convention from the rollup ecosystem. This prevents other plugins from trying to process the id (like node resolution), and core features like sourcemaps can use this info to differentiate between virtual modules and regular files.

## Universal Hooks

During dev, the Vite dev server creates a plugin container that invokes [Rolldown Build Hooks](https://rolldown.rs/apis/plugin-api#build-hooks) the same way Rolldown does it.

### Called on Server Start

The following hooks are called once on server start:

<ParamField path="options" type="(options: RolldownOptions) => RolldownOptions | null">
  See [Rolldown options hook](https://rolldown.rs/reference/interface.plugin#options)
</ParamField>

<ParamField path="buildStart" type="(options: RolldownOptions) => void | Promise<void>">
  See [Rolldown buildStart hook](https://rolldown.rs/reference/Interface.Plugin#buildstart)
</ParamField>

### Called on Each Module Request

The following hooks are called on each incoming module request:

<ParamField path="resolveId" type="ObjectHook">
  See [Rolldown resolveId hook](https://rolldown.rs/reference/Interface.Plugin#resolveid)

  Extended with additional Vite-specific properties:

  ```ts theme={null}
  (this: PluginContext,
   source: string,
   importer: string | undefined,
   options: {
     kind?: ImportKind
     custom?: CustomPluginOptions
     ssr?: boolean | undefined
     scan?: boolean | undefined
     isEntry: boolean
   }) => Promise<ResolveIdResult> | ResolveIdResult
  ```

  <Expandable title="Parameters">
    <ParamField path="source" type="string">
      The module to resolve
    </ParamField>

    <ParamField path="importer" type="string | undefined">
      The importing module path
    </ParamField>

    <ParamField path="options.kind" type="ImportKind">
      The kind of import
    </ParamField>

    <ParamField path="options.ssr" type="boolean">
      Whether this is for SSR
    </ParamField>

    <ParamField path="options.isEntry" type="boolean">
      Whether this is an entry point
    </ParamField>
  </Expandable>
</ParamField>

<ParamField path="load" type="ObjectHook">
  See [Rolldown load hook](https://rolldown.rs/reference/Interface.Plugin#load)

  Extended with additional Vite-specific properties:

  ```ts theme={null}
  (this: PluginContext,
   id: string,
   options?: {
     ssr?: boolean | undefined
   }) => Promise<LoadResult> | LoadResult
  ```
</ParamField>

<ParamField path="transform" type="ObjectHook">
  See [Rolldown transform hook](https://rolldown.rs/reference/Interface.Plugin#transform)

  Extended with additional Vite-specific properties:

  ```ts theme={null}
  (this: TransformPluginContext,
   code: string,
   id: string,
   options?: {
     moduleType: ModuleType
     ssr?: boolean | undefined
   }) => Promise<TransformResult> | TransformResult
  ```
</ParamField>

### Called on Server Close

The following hooks are called when the server is closed:

<ParamField path="buildEnd" type="(error?: Error) => void | Promise<void>">
  See [Rolldown buildEnd hook](https://rolldown.rs/reference/Interface.Plugin#buildend)
</ParamField>

<ParamField path="closeBundle" type="() => void | Promise<void>">
  See [Rolldown closeBundle hook](https://rolldown.rs/reference/Interface.Plugin#closebundle)
</ParamField>

<Note>
  The `moduleParsed` hook is **not** called during dev, because Vite avoids full AST parses for better performance.

  Output Generation Hooks (except `closeBundle`) are **not** called during dev.
</Note>

## Vite Specific Hooks

Vite plugins can also provide hooks that serve Vite-specific purposes. These hooks are ignored by Rollup.

### config

<ParamField path="config" type="ObjectHook">
  Modify Vite config before it's resolved.

  ```ts theme={null}
  (config: UserConfig, env: { mode: string, command: string }) =>
    UserConfig | null | void
  ```

  **Kind:** `async`, `sequential`

  <Expandable title="Parameters">
    <ParamField path="config" type="UserConfig">
      The raw user config (CLI options merged with config file)
    </ParamField>

    <ParamField path="env.mode" type="string">
      The mode being used
    </ParamField>

    <ParamField path="env.command" type="'serve' | 'build'">
      The command being run
    </ParamField>
  </Expandable>

  The hook receives the raw user config and can return a partial config object that will be deeply merged into existing config, or directly mutate the config.
</ParamField>

**Example:**

```js theme={null}
// return partial config (recommended)
const partialConfigPlugin = () => ({
  name: 'return-partial',
  config: () => ({
    resolve: {
      alias: {
        foo: 'bar',
      },
    },
  }),
})

// mutate the config directly (use only when merging doesn't work)
const mutateConfigPlugin = () => ({
  name: 'mutate-config',
  config(config, { command }) {
    if (command === 'build') {
      config.root = 'foo'
    }
  },
})
```

<Warning>
  User plugins are resolved before running this hook so injecting other plugins inside the `config` hook will have no effect.
</Warning>

### configResolved

<ParamField path="configResolved" type="ObjectHook">
  Called after the Vite config is resolved.

  ```ts theme={null}
  (config: ResolvedConfig) => void | Promise<void>
  ```

  **Kind:** `async`, `parallel`

  Use this hook to read and store the final resolved config. It is also useful when the plugin needs to do something different based on the command being run.
</ParamField>

**Example:**

```js theme={null}
const examplePlugin = () => {
  let config

  return {
    name: 'read-config',

    configResolved(resolvedConfig) {
      // store the resolved config
      config = resolvedConfig
    },

    // use stored config in other hooks
    transform(code, id) {
      if (config.command === 'serve') {
        // dev: plugin invoked by dev server
      } else {
        // build: plugin invoked by Rollup
      }
    },
  }
}
```

<Note>
  The `command` value is `serve` in dev (in the cli `vite`, `vite dev`, and `vite serve` are aliases).
</Note>

### configureServer

<ParamField path="configureServer" type="ServerHook">
  Hook for configuring the dev server.

  ```ts theme={null}
  (server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>
  ```

  **Kind:** `async`, `sequential`

  The most common use case is adding custom middlewares to the internal [connect](https://github.com/senchalabs/connect) app.
</ParamField>

**Example:**

```js theme={null}
const myPlugin = () => ({
  name: 'configure-server',
  configureServer(server) {
    server.middlewares.use((req, res, next) => {
      // custom handle request...
    })
  },
})
```

**Injecting Post Middleware:**

The `configureServer` hook is called before internal middlewares are installed, so the custom middlewares will run before internal middlewares by default. If you want to inject a middleware **after** internal middlewares, you can return a function from `configureServer`:

```js theme={null}
const myPlugin = () => ({
  name: 'configure-server',
  configureServer(server) {
    // return a post hook that is called after internal middlewares are installed
    return () => {
      server.middlewares.use((req, res, next) => {
        // custom handle request...
      })
    }
  },
})
```

**Storing Server Access:**

In some cases, other plugin hooks may need access to the dev server instance (e.g. accessing the WebSocket server, the file system watcher, or the module graph):

```js theme={null}
const myPlugin = () => {
  let server
  return {
    name: 'configure-server',
    configureServer(_server) {
      server = _server
    },
    transform(code, id) {
      if (server) {
        // use server...
      }
    },
  }
}
```

<Note>
  `configureServer` is not called when running the production build so your other hooks need to guard against its absence.
</Note>

### configurePreviewServer

<ParamField path="configurePreviewServer" type="PreviewServerHook">
  Same as `configureServer` but for the preview server.

  ```ts theme={null}
  (server: PreviewServer) => (() => void) | void | Promise<(() => void) | void>
  ```

  **Kind:** `async`, `sequential`
</ParamField>

**Example:**

```js theme={null}
const myPlugin = () => ({
  name: 'configure-preview-server',
  configurePreviewServer(server) {
    // return a post hook that is called after other middlewares are installed
    return () => {
      server.middlewares.use((req, res, next) => {
        // custom handle request...
      })
    }
  },
})
```

### transformIndexHtml

<ParamField path="transformIndexHtml" type="IndexHtmlTransformHook">
  Dedicated hook for transforming HTML entry point files such as `index.html`.

  ```ts theme={null}
  type IndexHtmlTransformHook = (
    html: string,
    ctx: {
      path: string
      filename: string
      server?: ViteDevServer
      bundle?: import('rollup').OutputBundle
      chunk?: import('rollup').OutputChunk
    },
  ) => IndexHtmlTransformResult | void | Promise<IndexHtmlTransformResult | void>
  ```

  **Kind:** `async`, `sequential`

  The hook receives the current HTML string and a transform context. The context exposes the `ViteDevServer` instance during dev, and exposes the Rollup output bundle during build.
</ParamField>

The hook can be async and can return one of the following:

* Transformed HTML string
* An array of tag descriptor objects (`{ tag, attrs, children }`) to inject to the existing HTML
* An object containing both as `{ html, tags }`

By default `order` is `undefined`, with this hook applied after the HTML has been transformed. In order to inject a script that should go through the Vite plugins pipeline, `order: 'pre'` will apply the hook before processing the HTML. `order: 'post'` applies the hook after all hooks with `order` undefined are applied.

**Basic Example:**

```js theme={null}
const htmlPlugin = () => {
  return {
    name: 'html-transform',
    transformIndexHtml(html) {
      return html.replace(
        /<title>(.*?)<\/title>/,
        `<title>Title replaced!</title>`,
      )
    },
  }
}
```

**Full Hook Signature:**

```ts theme={null}
type IndexHtmlTransformResult =
  | string
  | HtmlTagDescriptor[]
  | {
      html: string
      tags: HtmlTagDescriptor[]
    }

interface HtmlTagDescriptor {
  tag: string
  attrs?: Record<string, string | boolean>
  children?: string | HtmlTagDescriptor[]
  /**
   * default: 'head-prepend'
   */
  injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'
}
```

<Warning>
  This hook won't be called if you are using a framework that has custom handling of entry files (for example SvelteKit).
</Warning>

### handleHotUpdate

<ParamField path="handleHotUpdate" type="ObjectHook">
  Perform custom HMR update handling.

  ```ts theme={null}
  (ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>
  ```

  **Kind:** `async`, `sequential`

  The hook receives a context object with the following signature:

  ```ts theme={null}
  interface HmrContext {
    file: string
    timestamp: number
    modules: Array<ModuleNode>
    read: () => string | Promise<string>
    server: ViteDevServer
  }
  ```
</ParamField>

<Expandable title="Context Properties">
  <ParamField path="file" type="string">
    The file that changed
  </ParamField>

  <ParamField path="timestamp" type="number">
    The timestamp of the change
  </ParamField>

  <ParamField path="modules" type="Array<ModuleNode>">
    An array of modules that are affected by the changed file. It's an array because a single file may map to multiple served modules (e.g. Vue SFCs).
  </ParamField>

  <ParamField path="read" type="() => string | Promise<string>">
    An async read function that returns the content of the file. This is provided because on some systems, the file change callback may fire too fast before the editor finishes updating the file.
  </ParamField>

  <ParamField path="server" type="ViteDevServer">
    The Vite dev server instance
  </ParamField>
</Expandable>

The hook can choose to:

* Filter and narrow down the affected module list so that the HMR is more accurate.
* Return an empty array and perform a full reload:

```js theme={null}
handleHotUpdate({ server, modules, timestamp }) {
  // Invalidate modules manually
  const invalidatedModules = new Set()
  for (const mod of modules) {
    server.moduleGraph.invalidateModule(
      mod,
      invalidatedModules,
      timestamp,
      true
    )
  }
  server.ws.send({ type: 'full-reload' })
  return []
}
```

* Return an empty array and perform complete custom HMR handling by sending custom events to the client:

```js theme={null}
handleHotUpdate({ server }) {
  server.ws.send({
    type: 'custom',
    event: 'special-update',
    data: {}
  })
  return []
}
```

Client code should register corresponding handler using the HMR API:

```js theme={null}
if (import.meta.hot) {
  import.meta.hot.on('special-update', (data) => {
    // perform custom update
  })
}
```

## Plugin Ordering

A Vite plugin can additionally specify an `enforce` property (similar to webpack loaders) to adjust its application order. The value of `enforce` can be either `"pre"` or `"post"`. The resolved plugins will be in the following order:

1. Alias
2. User plugins with `enforce: 'pre'`
3. Vite core plugins
4. User plugins without enforce value
5. Vite build plugins
6. User plugins with `enforce: 'post'`
7. Vite post build plugins (minify, manifest, reporting)

<Note>
  This is separate from hooks ordering, those are still separately subject to their `order` attribute as usual for Rolldown hooks.
</Note>

## Conditional Application

By default plugins are invoked for both serve and build. In cases where a plugin needs to be conditionally applied only during serve or build, use the `apply` property:

```js theme={null}
function myPlugin() {
  return {
    name: 'build-only',
    apply: 'build', // or 'serve'
  }
}
```

A function can also be used for more precise control:

```js theme={null}
apply(config, { command }) {
  // apply only on build but not for SSR
  return command === 'build' && !config.build.ssr
}
```

## Rolldown Plugin Compatibility

A fair number of Rolldown / Rollup plugins will work directly as a Vite plugin (e.g. `@rollup/plugin-alias` or `@rollup/plugin-json`), but not all of them, since some plugin hooks do not make sense in an unbundled dev server context.

In general, as long as a Rolldown / Rollup plugin fits the following criteria then it should just work as a Vite plugin:

* It doesn't use the `moduleParsed` hook
* It doesn't rely on Rolldown specific options like `transform.inject`
* It doesn't have strong coupling between bundle-phase hooks and output-phase hooks

If a Rolldown / Rollup plugin only makes sense for the build phase, then it can be specified under `build.rolldownOptions.plugins` instead.

You can also augment an existing Rolldown / Rollup plugin with Vite-only properties:

```js vite.config.js theme={null}
import example from 'rolldown-plugin-example'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    {
      ...example(),
      enforce: 'post',
      apply: 'build',
    },
  ],
})
```

## Client-Server Communication

Since Vite 2.9, we provide some utilities for plugins to help handle the communication with clients.

### Server to Client

On the plugin side, we could use `server.ws.send` to broadcast events to the client:

```js vite.config.js theme={null}
export default defineConfig({
  plugins: [
    {
      // ...
      configureServer(server) {
        server.ws.on('connection', () => {
          server.ws.send('my:greetings', { msg: 'hello' })
        })
      },
    },
  ],
})
```

<Tip>
  We recommend **always prefixing** your event names to avoid collisions with other plugins.
</Tip>

On the client side, use `hot.on` to listen to the events:

```ts theme={null}
// client side
if (import.meta.hot) {
  import.meta.hot.on('my:greetings', (data) => {
    console.log(data.msg) // hello
  })
}
```

### Client to Server

To send events from the client to the server, we can use `hot.send`:

```ts theme={null}
// client side
if (import.meta.hot) {
  import.meta.hot.send('my:from-client', { msg: 'Hey!' })
}
```

Then use `server.ws.on` and listen to the events on the server side:

```js vite.config.js theme={null}
export default defineConfig({
  plugins: [
    {
      // ...
      configureServer(server) {
        server.ws.on('my:from-client', (data, client) => {
          console.log('Message from client:', data.msg) // Hey!
          // reply only to the client (if needed)
          client.send('my:ack', { msg: 'Hi! I got your message!' })
        })
      },
    },
  ],
})
```

### TypeScript for Custom Events

Internally, vite infers the type of a payload from the `CustomEventMap` interface, it is possible to type custom events by extending the interface:

<Note>
  Make sure to include the `.d.ts` extension when specifying TypeScript declaration files. Otherwise, Typescript may not know which file the module is trying to extend.
</Note>

```ts events.d.ts theme={null}
import 'vite/types/customEvent.d.ts'

declare module 'vite/types/customEvent.d.ts' {
  interface CustomEventMap {
    'custom:foo': { msg: string }
    // 'event-key': payload
  }
}
```

This interface extension is utilized by `InferCustomEventPayload<T>` to infer the payload type for event `T`:

```ts theme={null}
import type { InferCustomEventPayload } from 'vite/types/customEvent.d.ts'

type CustomFooPayload = InferCustomEventPayload<'custom:foo'>
import.meta.hot?.on('custom:foo', (payload) => {
  // The type of payload will be { msg: string }
})
import.meta.hot?.on('unknown:event', (payload) => {
  // The type of payload will be any
})
```

## Hook Filters

Rolldown introduced a [hook filter feature](https://rolldown.rs/apis/plugin-api/hook-filters) to reduce the communication overhead between the Rust and JavaScript runtimes. This feature allows plugins to specify patterns that determine when hooks should be called, improving performance by avoiding unnecessary hook invocations.

This is also supported by Rollup 4.38.0+ and Vite 6.3.0+. To make your plugin backward compatible with older versions, make sure to also run the filter inside the hook handlers.

```js theme={null}
export default function myPlugin() {
  const jsFileRegex = /\.js$/

  return {
    name: 'my-plugin',
    // Example: only call transform for .js files
    transform: {
      filter: {
        id: jsFileRegex,
      },
      handler(code, id) {
        // Additional check for backward compatibility
        if (!jsFileRegex.test(id)) return null

        return {
          code: transformCode(code),
          map: null,
        }
      },
    },
  }
}
```

<Tip>
  `@rolldown/pluginutils` exports some utilities for hook filters like `exactRegex` and `prefixRegex`. These are also re-exported from `rolldown/filter` for convenience.
</Tip>

## Output Bundle Metadata

During build, Vite augments Rolldown's build output objects with a Vite-specific `viteMetadata` field.

This is available through:

* `RenderedChunk` (for example in `renderChunk` and `augmentChunkHash`)
* `OutputChunk` and `OutputAsset` (for example in `generateBundle` and `writeBundle`)

`viteMetadata` provides:

* `viteMetadata.importedCss: Set<string>`
* `viteMetadata.importedAssets: Set<string>`

This is useful when writing plugins that need to inspect emitted CSS and static assets without relying on `build.manifest`.

**Example:**

```ts vite.config.ts theme={null}
function outputMetadataPlugin(): Plugin {
  return {
    name: 'output-metadata-plugin',
    generateBundle(_, bundle) {
      for (const output of Object.values(bundle)) {
        const css = output.viteMetadata?.importedCss
        const assets = output.viteMetadata?.importedAssets
        if (!css?.size && !assets?.size) continue

        console.log(output.fileName, {
          css: css ? [...css] : [],
          assets: assets ? [...assets] : [],
        })
      }
    },
  }
}
```

## Path Normalization

Vite normalizes paths while resolving ids to use POSIX separators ( / ) while preserving the volume in Windows. On the other hand, Rollup keeps resolved paths untouched by default.

For Vite plugins, when comparing paths against resolved ids it is important to first normalize the paths to use POSIX separators. An equivalent `normalizePath` utility function is exported from the `vite` module.

```js theme={null}
import { normalizePath } from 'vite'

normalizePath('foo\\bar') // 'foo/bar'
normalizePath('foo/bar') // 'foo/bar'
```

## Filtering Patterns

Vite exposes `@rollup/pluginutils`'s `createFilter` function to encourage Vite specific plugins and integrations to use the standard include/exclude filtering pattern, which is also used in Vite core itself.
