Rolldown hooks called during dev server lifecycle
During dev, the Vite dev server creates a plugin container that invokes Rolldown Build Hooks. Hooks called once on server start: options, buildStart. Hooks called on each incoming module request: resolveId, load, transform. Hooks called when server is closed: buildEnd, closeBundle. 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.
Plugin factory function pattern
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.
Vite plugin naming conventions
Vite-only plugins should have a clear name with `vite-plugin-` prefix and include `vite-plugin` keyword in package.json keywords field. Framework-specific plugins use prefixes like `vite-plugin-vue-`, `vite-plugin-react-`, or `vite-plugin-svelte-`. Plugins that work as both Rolldown and Vite plugins should use `rolldown-plugin-` prefix and include both `rolldown-plugin` and `vite-plugin` keywords.
Plugins configuration in vite.config.js
Plugins are configured using the `plugins` array option in vite.config.js. Falsy plugins will be ignored. The plugins array also accepts presets that include several plugins as a single element, and the array will be flattened internally. Example: export default defineConfig({ plugins: [vitePlugin(), rollupPlugin()] })
config hook type and behavior
The config hook has type (config: UserConfig, env: { mode: 'build' | 'serve', command: string, isSsrBuild?: boolean, isPreview?: boolean }) => UserConfig | null | void. It is async and sequential, runs globally, and modifies Vite config before it is resolved. It receives the raw user config and current config env exposing mode and command. It can return a partial config object that will be deeply merged or directly mutate the config. User plugins are resolved before this hook runs so injecting other plugins inside it will have no effect.
configResolved hook type and behavior
The configResolved hook has type (config: ResolvedConfig) => void | Promise<void>. It is async and parallel, runs globally, and is called after the Vite config is resolved. Use this hook to read and store the final resolved config or to do something different based on the command being run. The command value is 'serve' in dev (in the CLI, vite, vite dev, and vite serve are aliases).
configureServer hook for dev server middleware
The configureServer hook has type (server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>. It is async and sequential, runs globally, and is used for configuring the dev server. The most common use case is adding custom middlewares to the internal connect app via server.middlewares.use(). Returning a function from configureServer will inject middleware after internal middlewares are installed. This hook is not called during production build.
configurePreviewServer hook for preview server
The configurePreviewServer hook has type (server: PreviewServer) => (() => void) | void | Promise<(() => void) | void>. It is async and sequential, runs globally, and is the same as configureServer but for the preview server. Like configureServer, returning a function from configurePreviewServer will inject middleware after other middlewares are installed.
transformIndexHtml hook for HTML transformation
The transformIndexHtml hook has type IndexHtmlTransformHook | { order?: 'pre' | 'post', handler: IndexHtmlTransformHook }. It is async and sequential, per-environment scoped, and is dedicated for transforming HTML entry point files. It receives the current HTML string and a transform context that exposes ViteDevServer during dev or Rollup output bundle during build. The hook can return: a transformed HTML string, an array of tag descriptor objects to inject to HTML, or an object containing both. By default order is undefined (applied after HTML transformation), 'pre' applies before processing HTML, 'post' applies after all undefined order hooks.
HtmlTagDescriptor interface for HTML injection
HtmlTagDescriptor has the following structure: { tag: string, attrs?: Record<string, string | boolean>, children?: string | HtmlTagDescriptor[], injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend' }. The injectTo property defaults to 'head-prepend'. Attribute values will be escaped automatically if needed.
handleHotUpdate hook for custom HMR handling
The handleHotUpdate hook has type (ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>. It is async and sequential, per-environment scoped. The hook receives HmrContext with: file (string), timestamp (number), modules (Array<ModuleNode>), read (() => string | Promise<string>), server (ViteDevServer). The hook can filter/narrow affected module list for more accurate HMR, return empty array and perform full reload by invalidating modules and sending 'full-reload' message, or return empty array and perform complete custom HMR by sending custom events to client via server.ws.send().
Virtual module pattern for Vite plugins
Virtual modules allow passing build-time information to source files using normal ESM import syntax. The convention uses a `virtual:` prefix for the virtual module ID (e.g., 'virtual:my-module'). In the plugin, create a resolvedVirtualModuleId by prefixing with '\0' (e.g., '\0virtual:my-module'). Use resolveId hook to resolve the virtual module ID to the resolved ID, and load hook to provide the module content. In Vite dev, the '\0' is encoded as '/@id/__x00__' in browser URLs and decoded before entering the plugins pipeline.
Basic Vite plugin structure with required name property
A basic Vite plugin is a factory function that returns an object with a required `name` property and one or more hook handlers. The name property is a string that identifies the plugin and will show up in warnings and errors. Example: { name: 'my-plugin', transform: { filter: { id: /\.js$/ }, handler(code, id) { return { code: transformedCode, map: null } } } }
Plugin ordering in Vite
Plugins are resolved 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). The enforce property can be 'pre' or 'post'. This ordering is separate from hook ordering, which is still subject to the hook's order attribute.
Plugin context meta properties available in hooks
For plugin hooks with access to plugin context, Vite exposes additional properties on this.meta: this.meta.viteVersion (string, e.g., '8.0.0'), and this.meta.rolldownVersion (only available for Rolldown powered Vite, i.e., Vite 8+). You can use rolldownVersion to detect whether the current Vite instance is powered by Rolldown.
Output bundle metadata with viteMetadata field
During build, Vite augments Rolldown's build output objects with a Vite-specific viteMetadata field. This is available on RenderedChunk (in renderChunk and augmentChunkHash), OutputChunk and OutputAsset (in generateBundle and writeBundle). The viteMetadata provides: importedCss (Set<string>), importedAssets (Set<string>). This is useful for plugins that need to inspect emitted CSS and static assets without relying on build.manifest.
Rolldown plugin compatibility requirements
A Rolldown/Rollup plugin will work as a Vite plugin if: it doesn't use the moduleParsed hook, it doesn't rely on Rolldown specific options like transform.inject, and it doesn't have strong coupling between bundle-phase hooks and output-phase hooks. If a plugin only makes sense for the build phase, specify it under build.rolldownOptions.plugins instead. Vite-only properties can augment existing Rolldown/Rollup plugins.
Path normalization in Vite plugins
Vite normalizes paths while resolving ids to use POSIX separators (/) while preserving the volume in Windows. When comparing paths against resolved ids in Vite plugins, normalize paths first to use POSIX separators. Vite exports a normalizePath utility function from the vite module: import { normalizePath } from 'vite'. Example: normalizePath('foo\\bar') returns 'foo/bar'.
Hook filter feature for performance optimization
Rolldown introduced a hook filter feature to reduce communication overhead between Rust and JavaScript runtimes. Plugins can specify patterns in hook filters that determine when hooks should be called. This is supported by Rollup 4.38.0+, Vite 6.3.0+, and Rolldown powered Vite. To make plugins backward compatible with older versions, also run the filter inside hook handlers. Hook filters use properties like id with regex patterns. @rolldown/pluginutils exports utilities like exactRegex and prefixRegex for hook filters, also re-exported from rolldown/filter.
Chunk import map information access
When build.chunkImportMap option is enabled, import statements in generated chunks use a unique ID for each chunk instead of file path. Access the import map in generateBundle or writeBundle hook. The import map file name is specified by build.rolldownOptions.experimental.chunkImportMap.fileName (defaults to 'importmap.json'). The import map is an OutputAsset in the bundle containing JSON with an 'imports' object mapping chunk IDs to file paths.
Server to client communication via WebSocket
On the plugin side, use server.ws.send() to broadcast events to clients. Example: server.ws.on('connection', () => { server.ws.send('my:event', { data: 'value' }) }). On the client side, use import.meta.hot.on() to listen to events. It is recommended to always prefix event names to avoid collisions with other plugins.
Client to server communication via HMR API
To send events from client to server, use import.meta.hot.send(). Example: if (import.meta.hot) { import.meta.hot.send('my:event', { msg: 'data' }) }. On the server side, use server.ws.on() to listen to events and optionally reply to a specific client with client.send().
TypeScript typing for custom HMR events
Type custom events by extending the CustomEventMap interface from 'vite/types/customEvent.d.ts' in a .d.ts file. Example: declare module 'vite/types/customEvent.d.ts' { interface CustomEventMap { 'custom:foo': { msg: string } } }. Use InferCustomEventPayload<T> to infer the payload type for event T. Make sure to include the .d.ts extension when specifying TypeScript declaration files.
Virtual module example with resolveId and load hooks
Example of virtual module plugin: export default function myPlugin() { const virtualModuleId = 'virtual:my-module'; const resolvedVirtualModuleId = '\0' + virtualModuleId; return { name: 'my-plugin', resolveId: { filter: { id: exactRegex(virtualModuleId) }, handler() { return resolvedVirtualModuleId } }, load: { filter: { id: exactRegex(resolvedVirtualModuleId) }, handler() { return `export const msg = "from virtual module"` } } } }. This allows importing with: import { msg } from 'virtual:my-module'.
Custom file type transform plugin example
Example of transforming custom file types: export default function myPlugin() { const fileRegex = /\.(my-file-ext)$/; return { name: 'transform-file', transform: { filter: { id: fileRegex }, handler(src, id) { return { code: compileFileToJS(src), map: null } } } } }. The transform hook receives source code and module id, and must return an object with code and optionally a source map.
configResolved hook example storing config
Example of using configResolved: export default () => { let config; return { name: 'read-config', configResolved(resolvedConfig) { config = resolvedConfig }, transform(code, id) { if (config.command === 'serve') { /* dev */ } else { /* build */ } } } }. This pattern stores the resolved config for use in other hooks.
transformIndexHtml basic example
Example of basic HTML transformation: export default () => { return { name: 'html-transform', transformIndexHtml(html) { return html.replace(/<title>(.*?)<\/title>/, `<title>Title replaced!</title>`) } } }. The hook receives the HTML string and returns transformed HTML.
handleHotUpdate with full reload example
Example of handleHotUpdate performing full reload: handleHotUpdate({ server, modules, timestamp }) { const invalidatedModules = new Set(); for (const mod of modules) { server.moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true) } server.ws.send({ type: 'full-reload' }); return [] }. Returning empty array prevents the default HMR handling.
handleHotUpdate with custom event example
Example of handleHotUpdate with custom events: handleHotUpdate({ server }) { server.ws.send({ type: 'custom', event: 'special-update', data: {} }); return [] }. Client code registers handler: if (import.meta.hot) { import.meta.hot.on('special-update', (data) => { /* perform custom update */ }) }.
Version detection using this.meta.rolldownVersion
Example of detecting Rolldown powered Vite: function versionCheckPlugin(): Plugin { return { name: 'version-check', buildStart() { if (this.meta.rolldownVersion) { /* only on Rolldown powered Vite */ } else { /* on Rollup powered Vite */ } } } }. The rolldownVersion is only available for Vite 8+.
Output bundle metadata inspection example
Example of inspecting viteMetadata: function outputMetadataPlugin(): Plugin { return { name: 'output-metadata-plugin', enforce: 'post', 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] : [] }) } } } }.
Hook filter example for backward compatibility
Example of hook filter with backward compatibility: export default function myPlugin() { const jsFileRegex = /\.js$/; return { name: 'my-plugin', transform: { filter: { id: jsFileRegex }, handler(code, id) { if (!jsFileRegex.test(id)) return null; return { code: transformCode(code), map: null } } } } }. The filter property reduces invocations, but the handler should also check for backward compatibility.
Server to client WebSocket example
Example of server sending to client: export default defineConfig({ plugins: [{ configureServer(server) { server.ws.on('connection', () => { server.ws.send('my:greetings', { msg: 'hello' }) }) } }] }). Client receives: if (import.meta.hot) { import.meta.hot.on('my:greetings', (data) => { console.log(data.msg) // hello }) }.
Client to server WebSocket communication example
Example of client sending to server: if (import.meta.hot) { import.meta.hot.send('my:from-client', { msg: 'Hey!' }) }. Server receives: configureServer(server) { server.ws.on('my:from-client', (data, client) => { console.log('Message from client:', data.msg); client.send('my:ack', { msg: 'Hi! I got your message!' }) }) }.
Augmenting Rolldown plugin with Vite properties
Example of augmenting existing Rolldown/Rollup plugin: import example from 'rolldown-plugin-example'; export default defineConfig({ plugins: [{ ...example(), enforce: 'post', apply: 'build' }] }). This adds Vite-only properties to a Rolldown plugin.
Plugin load/transform hook moduleType support
In Vite 8, Rolldown automatically sets a module type based on the resolved id's extension. If a plugin converts content from other module types to JavaScript in load or transform hooks, it should return moduleType: 'js' in the returned value to indicate the output is JavaScript.
Rolldown unsupported plugin hooks
Vite 8 no longer supports these plugin hooks because Rolldown does not: shouldTransformCachedModule, resolveImportMeta, renderDynamicImport, and resolveFileUrl.
Rolldown plugin configuration via configResolved hook
When using Vite 8 plugins, you can access the Rolldown options set by the compatibility layer from the configResolved hook by reading config.optimizeDeps.rolldownOptions or config.oxc.
Dynamically import large plugin dependencies
Large dependencies that are only used in certain cases should be dynamically imported to reduce Node.js startup time when using community plugins.
Plugin hooks that block dev server startup
The buildStart, config, and configResolved hooks should not run long and extensive operations because these hooks are awaited during dev server startup, which delays when you can access the site in the browser.
Transform performance in plugin hooks
The resolveId, load, and transform hooks may cause files to load slower than others. The longer it takes to transform a file, the more significant the request waterfall will be when loading the site in the browser. You can optimize by checking if the code contains a specific keyword or if the id matches a specific extension before doing the full transformation. You can inspect the duration it takes to transform a file using vite --debug plugin-transform or vite-plugin-inspect.
Vite plugin system based on Rollup API
Vite's plugin system is based on a superset of Rollup's plugin API, enabling plugins to work across both Vite and plain Rollup projects.
Plugin structure with enforce modifier
Plugins are added to the `plugins` array in `vite.config.js`. The `enforce` modifier controls plugin ordering with three options: 'pre' invokes plugin before Vite core plugins, the default invokes plugin after Vite core plugins, and 'post' invokes plugin after Vite build plugins.
Conditional plugin application
Plugins can be conditionally applied using the `apply` property set to either 'build' or 'serve'. By default, plugins are invoked for both serve and build operations.
Plugin configuration in vite.config.js
To use a plugin, add it to devDependencies and include it in the `plugins` array in the `vite.config.js` configuration file. The `plugins` array accepts presets that include several plugins as a single element, and the array will be flattened internally. Falsy plugins are ignored, which allows easy activation or deactivation of plugins.
Example plugin configuration with enforce ordering
Example showing how to add a plugin with enforce modifier:
```js
import image from '@rollup/plugin-image'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
{
...image(),
enforce: 'pre',
},
],
})
```
This example demonstrates adding the @rollup/plugin-image plugin with 'pre' enforcement to run before Vite core plugins.
Example conditional plugin application
Example showing how to conditionally apply a plugin only during build:
```js
import typescript2 from 'rollup-plugin-typescript2'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
{
...typescript2(),
apply: 'build',
},
],
})
```
This example demonstrates applying rollup-plugin-typescript2 only during the build phase using the `apply` property.
Vite plugin inheritance from Rollup
Vite plugins are based on Rollup's plugin interface with a few extra Vite-specific options. This allows Vite users to rely on the mature ecosystem of Rollup plugins while also extending dev server and SSR functionality as needed.
Vite's plugin API and ecosystem adoption
Vite's plugin API, based on Rollup's conventions, made integration natural for frameworks and tools without requiring them to work around Vite's internals. Major frameworks and tools including Nuxt, SvelteKit, Astro, React Router, Analog, SolidStart, Vitest, Storybook, Laravel, and Ruby on Rails adopted Vite as their foundation or for frontend asset pipelines.