Plugin API

Vite plugins extends Rollup’s well-designed plugin interface with a few extra vite-specific options. As a result, you can write a Vite plugin once and have it work for both dev and build.

It is recommended to go through Rollup’s plugin documentation first before reading the sections below.

Simple Examples

TIP

It is common convention to author a Vite/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.

Serving a Virtual File

  1. export default function myPlugin() {
  2. const virtualFileId = '@my-virtual-file'
  3. return {
  4. name: 'my-plugin', // required, will show up in warnings and errors
  5. resolveId(id) {
  6. if (id === virtualFileId) {
  7. return virtualFileId
  8. }
  9. },
  10. load(id) {
  11. if (id === virtualFileId) {
  12. return `export const msg = "from virtual file"`
  13. }
  14. }
  15. }
  16. }

Transforming Custom File Types

  1. const fileRegex = /\.(my-file-ext)$/
  2. export default function myPlugin() {
  3. return {
  4. name: 'transform-file',
  5. transform(src, id) {
  6. if (fileRegex.test(id)) {
  7. return {
  8. code: compileFileToJS(src),
  9. map: null // provide source map if available
  10. }
  11. }
  12. }
  13. }
  14. }

Universal Hooks

During dev, the Vite dev server creates a plugin container that invokes Rollup Build Hooks the same way Rollup does it.

The following hooks are called once on server start:

The following hooks are called on each incoming module request:

The following hooks are called when the server is closed:

Note that 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. You can think of Vite’s dev server as only calling rollup.rollup() without calling bundle.generate().

Vite Specific Hooks

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

config

  • Type: (config: UserConfig) => UserConfig | null | void

  • Kind: sync, sequential

    Modify Vite config before it’s resolved. The hook receives the raw user config (CLI options merged with config file). It can return a partial config object that will be deeply merged into existing config, or directly mutate the config (if the default merging cannot achieve the desired result).

    Example

    1. // return partial config (recommended)
    2. const partialConfigPlugin = () => ({
    3. name: 'return-partial',
    4. config: () => ({
    5. alias: {
    6. foo: 'bar'
    7. }
    8. })
    9. })
    10. // mutate the config directly (use only when merging doesn't work)
    11. const mutateConfigPlugin = () => ({
    12. name: 'mutate-config',
    13. config(config) {
    14. config.root = __dirname
    15. }
    16. })

    Note

    User plugins are resolved before running this hook so injecting other plugins inside the config hook will have no effect.

configResolved

  • Type: (config: ResolvedConfig) => void

  • Kind: sync, sequential

    Called after the Vite config is resolved. Use this hook to read and store the final resolved config. It is also useful when the plugin needs to do something different based the command is being run.

    Example:

    1. const examplePlugin = () => {
    2. let config
    3. return {
    4. name: 'read-config',
    5. configResolved(resolvedConfig) {
    6. // store the resolved config
    7. config = resolvedConfig
    8. },
    9. // use stored config in other hooks
    10. transform(code, id) {
    11. if (config.command === 'serve') {
    12. // serve: plugin invoked by dev server
    13. } else {
    14. // build: plugin invoked by Rollup
    15. }
    16. }
    17. }
    18. }

configureServer

  • Type: (server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>

  • Kind: async, sequential

  • See also: ViteDevServer

    Hook for configuring the dev server. The most common use case is adding custom middlewares to the internal connect app:

    1. const myPlugin = () => ({
    2. name: 'configure-server',
    3. configureServer(server) {
    4. server.app.use((req, res, next) => {
    5. // custom handle request...
    6. })
    7. }
    8. })

    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, which will be called after internal middlewares are installed:

    1. const myPlugin = () => ({
    2. name: 'configure-server',
    3. configureServer(server) {
    4. // return a post hook that is called after internal middlewares are
    5. // installed
    6. return () => {
    7. server.app.use((req, res, next) => {
    8. // custom handle request...
    9. })
    10. }
    11. }
    12. })

    Storing Server Access

    In some cases, other plugin hooks may need access to the dev server instance (e.g. accessing the web socket server, the file system watcher, or the module graph). This hook can also be used to store the server instance for access in other hooks:

    1. const myPlugin = () => {
    2. let server
    3. return {
    4. name: 'configure-server',
    5. configureServer(_server) {
    6. server = _server
    7. },
    8. transform(code, id) {
    9. if (server) {
    10. // use server...
    11. }
    12. }
    13. }
    14. }

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

transformIndexHtml

  • Type: IndexHtmlTransformHook | { enforce?: 'pre' | 'post' transform: IndexHtmlTransformHook }

  • Kind: async, sequential

    Dedicated hook for transforming index.html. 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.

    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. Each tag can also specify where it should be injected to (default is prepending to <head>)
    • An object containing both as { html, tags }

    Basic Example

    1. const htmlPlugin = () => {
    2. return {
    3. name: 'html-transform',
    4. transformIndexHtml(html) {
    5. return html.replace(
    6. /<title>(.*?)<\/title>/,
    7. `<title>Title replaced!</title>`
    8. )
    9. }
    10. }
    11. }

    Full Hook Signature:

    1. type IndexHtmlTransformHook = (
    2. html: string,
    3. ctx: {
    4. path: string
    5. filename: string
    6. server?: ViteDevServer
    7. bundle?: import('rollup').OutputBundle
    8. chunk?: import('rollup').OutputChunk
    9. }
    10. ) =>
    11. | IndexHtmlTransformResult
    12. | void
    13. | Promise<IndexHtmlTransformResult | void>
    14. type IndexHtmlTransformResult =
    15. | string
    16. | HtmlTagDescriptor[]
    17. | {
    18. html: string
    19. tags: HtmlTagDescriptor[]
    20. }
    21. interface HtmlTagDescriptor {
    22. tag: string
    23. attrs?: Record<string, string | boolean>
    24. children?: string | HtmlTagDescriptor[]
    25. /**
    26. * default: 'head-prepend'
    27. */
    28. injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'
    29. }

handleHotUpdate

  • Type: (ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>

    Perform custom HMR update handling. The hook receives a context object with the following signature:

    1. interface HmrContext {
    2. file: string
    3. timestamp: number
    4. modules: Array<ModuleNode>
    5. read: () => string | Promise<string>
    6. server: ViteDevServer
    7. }
    • modules is 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).

    • read is 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 and direct fs.readFile will return empty content. The read function passed in normalizes this behavior.

    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 complete custom HMR handling by sending custom events to the client:

      1. handleHotUpdate({ server }) {
      2. server.ws.send({
      3. type: 'custom',
      4. event: 'special-update',
      5. data: {}
      6. })
      7. return []
      8. }

      Client code should register corresponding handler using the HMR API (this could be injected by the same plugin’s transform hook):

      1. if (import.meta.hot) {
      2. import.meta.hot.on('special-update', (data) => {
      3. // perform custom update
      4. })
      5. }

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:

  • Alias
  • User plugins with enforce: 'pre'
  • Vite core plugins
  • User plugins without enforce value
  • Vite build plugins
  • User plugins with enforce: 'post'
  • Vite post build plugins (minify, manifest, reporting)

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:

  1. function myPlugin() {
  2. return {
  3. name: 'build-only',
  4. apply: 'build'
  5. }
  6. }

Rollup Plugin Compatiblity

A fair number of 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 rollup plugin fits the following criterias then it should just work as a Vite plugin:

  • It doesn’t use the moduleParsed hook.
  • It doesn’t have strong coupling between bundle-phase hooks and output-phase hooks.

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

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

  1. // vite.config.js
  2. import example from 'rollup-plugin-example'
  3. export default {
  4. plugins: [
  5. {
  6. ...example(),
  7. enforce: 'post',
  8. apply: 'build'
  9. }
  10. ]
  11. }