A Vite plugin is more than just a way to add custom transformations; it’s a fundamental mechanism for shaping how Vite processes your code and interacts with its build pipeline.
Let’s see this in action. Imagine you want to automatically inject a version number into your index.html file on every build.
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
{
name: 'version-injector',
transformIndexHtml(html) {
const version = Date.now(); // In a real app, this would come from package.json or a build script
return html.replace('</body>', `<script>window.APP_VERSION = ${version};</script></body>`);
}
}
]
});
When you run vite build, Vite will process your index.html, and this plugin will hook into the transformIndexHtml hook, injecting the version number just before the closing </body> tag. The output dist/index.html will look something like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script>window.APP_VERSION = 1678886400000;</script>
</body>
</html>
This demonstrates how plugins can directly manipulate the HTML served during development and built for production.
The power of Vite plugins lies in their ability to tap into specific stages of the build process. Vite uses Rollup under the hood, so many of its plugin hooks are derived from Rollup’s plugin API. However, Vite also introduces its own unique hooks tailored for its client-side rendering and fast development server.
Here’s a breakdown of the core concepts:
- Hooks: These are functions within a plugin object that Vite calls at specific moments. Examples include
config,configResolved,configureServer,transform,load,buildStart,buildEnd, andgenerateBundle. name: Every plugin must have a uniquename. This is crucial for debugging and for Vite to identify and manage plugins.enforce: This optional property can be set to'pre'or'post'.'pre'plugins run before Vite’s built-in plugins, and'post'plugins run after. This is essential for controlling the order of operations, especially when your plugin needs to modify code that other plugins might also interact with.transformHook: This is perhaps the most common hook. It receives the code of a module and itsid(the file path) and allows you to return modified code. This is where you’d do things like transpiling non-standard JavaScript, injecting environment variables, or processing custom file types.loadHook: If you want to handle files that Vite wouldn’t normally recognize (e.g.,.txtfiles as modules), you can use theloadhook. It takes anidand should return the content of the module.configureServerHook: This hook is specific to Vite’s development server. It allows you to add custom middleware (like Express or Koa middleware) to the dev server, enabling features like custom API endpoints or request interception.transformIndexHtmlHook: As seen in the example, this hook lets you modify theindex.htmlfile before it’s served or bundled. This is perfect for injecting scripts, meta tags, or dynamic content.- Rollup Hooks: Vite also exposes many of Rollup’s hooks, such as
options,resolveId,buildStart,generateBundle, andwriteBundle. These are useful for more advanced build-time manipulations that go beyond simple code transformations.
When creating a plugin, you’re essentially building a JavaScript object with specific properties (hooks) that Vite knows how to call. Vite will collect all these plugins from your vite.config.js and execute them in a defined order.
The magic of enforce: 'pre' and enforce: 'post' is subtle but powerful. If you have a plugin that needs to modify code before Vite’s default handling (e.g., an experimental transpiler), you’d use 'pre'. If your plugin needs to process the output of Vite’s default handling or other plugins, you’d use 'post'. Without enforce, plugins are typically inserted based on their order in the vite.config.js array, but the pre/post flags provide explicit control.
The transform hook receives the code as a string. However, it’s often more efficient and robust to return an object with code and map properties. The map is a source map, which is essential for debugging transformed code. If you don’t provide a source map, Vite will generate one, but it might be less accurate.
To make your plugin more flexible, you can accept options when the plugin is instantiated in vite.config.js.
// vite.config.js
import { defineConfig } from 'vite';
import myPlugin from './my-plugin'; // Assume my-plugin.js exports a factory function
export default defineConfig({
plugins: [
myPlugin({
apiKey: 'YOUR_API_KEY',
featureEnabled: true
})
]
});
And in my-plugin.js:
// my-plugin.js
export default function myPlugin(options) {
return {
name: 'my-custom-plugin',
// ... other hooks
transform(code, id) {
if (options.featureEnabled) {
// Use options.apiKey here
console.log(`Transforming ${id} with API key: ${options.apiKey}`);
}
return code;
}
};
}
This pattern allows users to configure your plugin’s behavior without modifying its source code, making it reusable and adaptable.
The configureServer hook is where you can truly extend Vite’s development experience. You can add custom routes, mock API responses, or even implement hot module replacement (HMR) for entirely new types of assets by subscribing to file changes.
When you define a plugin, you’re not just writing code; you’re defining a set of instructions for Vite’s build engine. This allows for incredibly granular control over how your application is processed, from the very first line of code to the final bundled output.
The next step beyond simple transformations is often dealing with asset handling or creating custom HMR updates.