Building a plugin with Vite

Why build a Vite plugin

Have you ever needed more capabilities from your Vite configuration like resolving a custom extension e.g index.mycustomextension to index.js? Or converting markdown to JSX?

A Vite plugin allows us to accomplish tasks like these. So you might be wondering how to create a Vite plugin yourself. In this article, we’ll walk through how to create some simple plugins that you can use as the foundation and inspiration for your own custom Vite plugins.


What is a Vite plugin?

A Vite plugin is a Rollup plugin with some options and hooks specific to Vite that lets you add custom functionality on top of Vite to solve a particular problem. For example, Astro uses a Vite plugin to resolve .astro files.

A Vite plugin is an object with one or more:

const vitePluginExample = () => ({
	// properties
  name: 'vite-plugin-example', 
	// build hooks
  config: () => ({
    resolve: {
      alias: {
        foo: 'bar'
      }
    }
  }),
	// output generation hooks
	augmentChunkHash(chunkInfo) {
      if (chunkInfo.name === 'bar') {
        return Date.now().toString();
      }
    }
})

Let’s briefly touch on these three components of a Vite plugin before learning how to build our own .

Properties

The only required property of a Vite plugin is the name property which is the name of the plugin. This is required because it’s useful for debugging and error messages.

Build hooks

This is the primary way a plugin interacts with Vite’s build process. Hooks are functions that are called at various stages of the build. You can use hooks to modify how a build runs, collect information about a build, or modify a build once it is complete.

Output generation hooks

These hooks can be used to provide information about a generated bundle and modify a build once complete.


Creating a Vite plugin

Getting started creating a Vite plugin is as simple as specifying a factory function in your vite.config.js file. This function will return the actual plugin object, which contains all of the plugin definitions and logic. You can then pass this function to the plugins array in the defineConfig object.

Note: The function is a regular function and as such it can accept options, which lets the plugin behavior be customized in userland.

To get you familiar with creating plugins, we will be creating three simple plugins to show you what authoring a Vite plugin looks like.

Let’s get started with the first one.


Example #1: Output Plugins Stats

What better “hello world” example is there than making a plugin that outputs stats about the plugins in your Vite project?

The way this plugin will work is that when you run npm run dev you should see the output of the output-plugin-stats hook, which will be a count of the plugins in your project and a table of all the plugins. So meta.

To create this, let’s first instantiate a Vanilla Vite project by running this command in your terminal:

npm init create-vite@latest

In the prompt, choose the Vanilla preset, and JavaScript for the language.

Next run npm install and npm run dev to see if the setup was successful.

Once this is done, just kill the dev server and create a vite.config.js file in the root of your project and add this code:

📄 vite.config.js

import { defineConfig } from 'vite'

export default defineConfig({
  plugins: []
})

Now we have the skeleton to flesh out the functionality of our plugin, which we’ll pass to the plugins array.

📄 vite.config.js

import { defineConfig } from 'vite'

const outputPluginStats = () => ({
  name: 'output-plugin-stats',
  configResolved(config) {
    const plugins = config.plugins.map((plugin) => plugin.name)
    console.log(`Your project has ${plugins.length} Vite plugins.`)
    console.table(plugins)
  }
})

export default defineConfig({
  plugins: [
    outputPluginStats()
  ]
})

As you can see, we have an outputPluginStats function that returns an object with a name property, which is the name of the plugin, and a configResolved Vite-specific hook.

The configResolved is called after Vite config has been resolved and you can use it to read and store the final resolved config.

Let’s look at what we are doing in the above code:

const plugins = config.plugins.map((plugin) => plugin.name)

We are mapping over the plugins in config.plugins and returning the names of the plugins as an array called plugins

console.log(`Your project has ${plugins.length} Vite plugins.`)

The above line outputs the count of the plugins in the plugins array using Array.prototype.length

Finally, we output the plugins array in a table:

console.table(plugins)

After we have defined the plugin, we register the plugin in the plugins array of the Vite config object:

export default defineConfig({
  plugins: [
    outputPluginStats()
  ]
})

Example #2: Request Analytics

For our next example, we will be adding a middleware—a function that will run before the server will handle a request—to the underlying Vite connect server to log requests that the server handles.

To do so, let’s add the following function to our vite.config.js file.

📄 vite.config.js

const requestAnalytics = () => ({
  name: 'request-analytics',
  configureServer(server) {
    return () => {
      server.middlewares.use((req, res, next) => {
        console.log(`${req.method.toUpperCase()} ${req.url}`)
        next()
      })
    }
  }
})

In the above plugin, we are using the configureServer hook to add a middleware to the Vite server.

Let’s dive into what each line of the above code is doing.

First, we set the name of the plugin.

name: 'request-analytics'

Then, the configureServer hook lets us configure the Vite server:

configureServer(server) {
    return () => {
     
    }
  }

We then add a middleware which takes in 3 arguments: req, res, next. Inside of the middleware, we are logging the request method and the URL that was requested.

configureServer(server) {
    return () => {
      server.middlewares.use((req, res, next) => {
        console.log(`${req.method.toUpperCase()} ${req.url}`)
        next()
      })
    }
  }

Finally, we pass the request to the next handler with the next() call.

We’ll call the plugin in the plugins array, like so:

📄 vite.config.js

export default defineConfig({
  plugins: [
    outputPluginStats(),
    requestAnalytics()
  ]
})

Now when we start the Vite server by running npm run dev and visit http://localhost:5173/ we should see the below output:

GET /index.html

We just created a sort of request analytics that tells us which files were served by the Vite server.


Example #3: Hot Update Report

For our final example… Let’s say we want to report the modules that were updated during a hot module replacement, inside the console.

To do this, let’s build a plugin called hotUpdateReport.

We can do so by tapping into the handleHotUpdate hook, like so:

📄 vite.config.js

const hotUpdateReport = () => ({
  name: 'hot-update-report',
  handleHotUpdate({file, timestamp, modules}) {
    console.log(`${timestamp}: ${modules.length} module(s) updated`)
  }
})

Note we are using the handleHotUpdate hook to log the timestamp as well as the number of modules that were updated.

The handleHotUpdate hook provides a HmrContext object which we are destructuring to get the file, timestamp and modules properties off of it.

We just need to register the plugin, like so:

📄 vite.config.js

export default defineConfig({
  plugins: [
    outputPluginStats(),
    requestAnalytics(),
    hotUpdateReport()
  ]
})

Now when we update index.html, main.js or style.css, we will get the report in our console.


Publishing a Vite plugin

If after authoring your plugin, you decide you want to share it as a standalone package to be installed via NPM, you should follow these conventions:

  • Your plugin should have a clear name prefixed with vite-plugin-
  • Include the vite-plugin keyword in package.json
{
  "name": "building-a-plugin-with-vite",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "keywords": ["vite-plugin"],
}
  • When documenting your plugin, include a section detailing why it’s a Vite plugin only (and not also a Rollup plugin). A common reason for a plugin being Vite-only is because it uses some Vite-specific plugin hooks.
  • Lastly, if your plugin only works for a particular framework, the framework name should be part of the plugin name prefix e.g vite-plugin-svelte-, vite-plugin-react-, vite-plugin-react-, vite-plugin-lit-, etc.

Where to go from here?

With these examples, you now have the basic building blocks for authoring your own Vite plugins. There are an infinite amount of use cases for such plugins, and I hope you now feel inspired to create your own.

If diving deeper into Vite is interesting to you, check out Vue Mastery’s Lightning Fast Builds with Vite course, taught by Vite’s creator Evan You.

In this article:

Dive Deeper into Vue today

Access our entire course library with a special discount.

Get Deal

Download the cheatsheets

Save time and energy with our cheat sheets.