vite/docs/guide/api-plugin.md

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

613 lines
22 KiB
Markdown
Raw Normal View History

2021-01-01 21:40:16 +00:00
# Plugin API
2021-03-30 21:38:57 +00:00
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.
2021-01-01 21:40:16 +00:00
**It is recommended to go through [Rollup's plugin documentation](https://rollupjs.org/plugin-development/) first before reading the sections below.**
2021-01-01 21:40:16 +00:00
## 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](https://vitejs.dev/guide/features) 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. Once you see that a plugin was useful in your projects, consider sharing it to help others [in the ecosystem](https://chat.vitejs.dev).
::: tip
2022-07-08 09:05:58 +00:00
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. Check out install instructions in the [vite-plugin-inspect docs](https://github.com/antfu/vite-plugin-inspect).
![vite-plugin-inspect](/images/vite-plugin-inspect.png)
:::
## Conventions
If the plugin doesn't use Vite specific hooks and can be implemented as a [Compatible Rollup Plugin](#rollup-plugin-compatibility), then it is recommended to use the [Rollup Plugin naming conventions](https://rollupjs.org/plugin-development/#conventions).
- Rollup Plugins should have a clear name with `rollup-plugin-` prefix.
- Include `rollup-plugin` and `vite-plugin` keywords in package.json.
This exposes the plugin to be also used in pure Rollup or WMR based projects
For Vite only plugins
- Vite 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 (for example, it uses Vite specific plugin hooks).
If your plugin is only going to work for a particular framework, its name should be included as part of the prefix
- `vite-plugin-vue-` prefix for Vue Plugins
- `vite-plugin-react-` prefix for React Plugins
- `vite-plugin-svelte-` prefix for Svelte Plugins
See also [Virtual Modules Convention](#virtual-modules-convention).
2021-03-30 21:38:57 +00:00
## Plugins config
Users will add plugins to the project `devDependencies` and configure them using the `plugins` array option.
```js
// vite.config.js
import vitePlugin from 'vite-plugin-feature'
import rollupPlugin from 'rollup-plugin-feature'
2021-07-27 15:00:51 +00:00
export default defineConfig({
plugins: [vitePlugin(), rollupPlugin()],
2021-07-27 15:00:51 +00:00
})
2021-03-30 21:38:57 +00:00
```
Falsy plugins will be ignored, which can be used to easily activate or deactivate plugins.
2021-03-30 21:38:57 +00:00
2022-08-21 19:05:55 +00:00
`plugins` also accepts presets including several plugins as a single element. This is useful for complex features (like framework integration) that are implemented using several plugins. The array will be flattened internally.
2021-03-30 21:38:57 +00:00
```js
// 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)]
2021-03-30 21:38:57 +00:00
}
```
```js
// vite.config.js
2021-07-27 15:00:51 +00:00
import { defineConfig } from 'vite'
2021-03-30 21:38:57 +00:00
import framework from 'vite-plugin-framework'
2021-07-27 15:00:51 +00:00
export default defineConfig({
plugins: [framework()],
2021-07-27 15:00:51 +00:00
})
2021-03-30 21:38:57 +00:00
```
2021-01-01 21:40:16 +00:00
## 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.
:::
### Transforming Custom File Types
```js
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
}
}
},
}
}
```
### Importing a Virtual File
2021-01-01 21:40:16 +00:00
See the example in the [next section](#virtual-modules-convention).
## 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.
2021-01-01 21:40:16 +00:00
```js
export default function myPlugin() {
const virtualModuleId = 'virtual:my-module'
const resolvedVirtualModuleId = '\0' + virtualModuleId
2021-01-01 21:40:16 +00:00
return {
name: 'my-plugin', // required, will show up in warnings and errors
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
2021-01-01 21:40:16 +00:00
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `export const msg = "from virtual module"`
2021-01-01 21:40:16 +00:00
}
},
}
}
```
Which allows importing the module in JavaScript:
```js
import { msg } from 'virtual:my-module'
console.log(msg)
```
Virtual modules in Vite (and Rollup) are prefixed with `virtual:` for the user-facing path by convention. If possible the plugin name should be used as a namespace to avoid collisions with other plugins in the ecosystem. For example, a `vite-plugin-posts` could ask users to import a `virtual:posts` or `virtual:posts/helpers` virtual modules to get build time information. 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. `\0` is not a permitted char in import URLs so we have to replace them during import analysis. A `\0{id}` virtual id ends up encoded as `/@id/__x00__{id}` during dev in the browser. The id will be decoded back before entering the plugins pipeline, so this is not seen by plugins hooks code.
2021-01-05 16:16:41 +00:00
Note that modules directly derived from a real file, as in the case of a script module in a Single File Component (like a .vue or .svelte SFC) don't need to follow this convention. SFCs generally generate a set of submodules when processed but the code in these can be mapped back to the filesystem. Using `\0` for these submodules would prevent sourcemaps from working correctly.
2021-01-01 21:40:16 +00:00
## Universal Hooks
During dev, the Vite dev server creates a plugin container that invokes [Rollup Build Hooks](https://rollupjs.org/plugin-development/#build-hooks) the same way Rollup does it.
2021-01-01 21:40:16 +00:00
The following hooks are called once on server start:
- [`options`](https://rollupjs.org/plugin-development/#options)
- [`buildStart`](https://rollupjs.org/plugin-development/#buildstart)
2021-01-01 21:40:16 +00:00
The following hooks are called on each incoming module request:
- [`resolveId`](https://rollupjs.org/plugin-development/#resolveid)
- [`load`](https://rollupjs.org/plugin-development/#load)
- [`transform`](https://rollupjs.org/plugin-development/#transform)
2021-01-01 21:40:16 +00:00
They also have an extended `options` parameter with additional Vite-specific properties. You can read more in the [SSR documentation](/guide/ssr#ssr-specific-plugin-logic).
2021-01-01 21:40:16 +00:00
The following hooks are called when the server is closed:
- [`buildEnd`](https://rollupjs.org/plugin-development/#buildend)
- [`closeBundle`](https://rollupjs.org/plugin-development/#closebundle)
2021-01-01 21:40:16 +00:00
Note that the [`moduleParsed`](https://rollupjs.org/plugin-development/#moduleparsed) hook is **not** called during dev, because Vite avoids full AST parses for better performance.
2021-01-01 21:40:16 +00:00
[Output Generation Hooks](https://rollupjs.org/plugin-development/#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()`.
2021-01-01 21:40:16 +00:00
## Vite Specific Hooks
Vite plugins can also provide hooks that serve Vite-specific purposes. These hooks are ignored by Rollup.
### `config`
- **Type:** `(config: UserConfig, env: { mode: string, command: string }) => UserConfig | null | void`
2021-07-02 22:17:41 +00:00
- **Kind:** `async`, `sequential`
2021-01-01 21:40:16 +00:00
Modify Vite config before it's resolved. The hook receives the raw user config (CLI options merged with config file) and the current config env which exposes the `mode` and `command` being used. 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).
2021-01-01 21:40:16 +00:00
**Example:**
2021-01-01 21:40:16 +00:00
```js
// return partial config (recommended)
const partialConfigPlugin = () => ({
name: 'return-partial',
config: () => ({
2022-04-06 05:13:38 +00:00
resolve: {
alias: {
foo: 'bar',
},
2021-01-01 21:40:16 +00:00
},
}),
})
// mutate the config directly (use only when merging doesn't work)
const mutateConfigPlugin = () => ({
name: 'mutate-config',
config(config, { command }) {
if (command === 'build') {
2022-07-08 09:05:58 +00:00
config.root = 'foo'
}
2021-01-01 21:40:16 +00:00
},
})
```
::: warning 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 | Promise<void>`
- **Kind:** `async`, `parallel`
2021-01-01 21:40:16 +00:00
2022-08-21 19:05:55 +00:00
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 on the command being run.
2021-01-01 21:40:16 +00:00
**Example:**
```js
2021-01-10 03:00:05 +00:00
const examplePlugin = () => {
2021-01-01 21:40:16 +00:00
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') {
2021-11-08 14:43:42 +00:00
// dev: plugin invoked by dev server
2021-01-01 21:40:16 +00:00
} else {
// build: plugin invoked by Rollup
}
},
}
}
```
2021-11-08 14:43:42 +00:00
Note that the `command` value is `serve` in dev (in the cli `vite`, `vite dev`, and `vite serve` are aliases).
2021-01-01 21:40:16 +00:00
### `configureServer`
- **Type:** `(server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>`
- **Kind:** `async`, `sequential`
- **See also:** [ViteDevServer](./api-javascript#vitedevserver)
Hook for configuring the dev server. The most common use case is adding custom middlewares to the internal [connect](https://github.com/senchalabs/connect) app:
```js
const myPlugin = () => ({
name: 'configure-server',
configureServer(server) {
server.middlewares.use((req, res, next) => {
2021-01-01 21:40:16 +00:00
// 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`, which will be called after internal middlewares are installed:
```js
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) => {
2021-01-01 21:40:16 +00:00
// custom handle request...
})
}
},
})
```
**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:
```js
const myPlugin = () => {
let server
return {
name: 'configure-server',
configureServer(_server) {
server = _server
},
transform(code, id) {
if (server) {
// use server...
}
},
}
}
```
2021-01-02 09:53:38 +00:00
Note `configureServer` is not called when running the production build so your other hooks need to guard against its absence.
2021-01-01 21:40:16 +00:00
### `configurePreviewServer`
- **Type:** `(server: PreviewServerForHook) => (() => void) | void | Promise<(() => void) | void>`
- **Kind:** `async`, `sequential`
- **See also:** [PreviewServerForHook](./api-javascript#previewserverforhook)
Same as [`configureServer`](/guide/api-plugin.html#configureserver) but for the preview server. Similarly to `configureServer`, the `configurePreviewServer` hook is called before other middlewares are installed. If you want to inject a middleware **after** other middlewares, you can return a function from `configurePreviewServer`, which will be called after internal middlewares are installed:
```js
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...
})
}
},
})
```
2021-01-01 21:40:16 +00:00
### `transformIndexHtml`
- **Type:** `IndexHtmlTransformHook | { order?: 'pre' | 'post', handler: IndexHtmlTransformHook }`
2021-01-01 21:40:16 +00:00
- **Kind:** `async`, `sequential`
Dedicated hook for transforming HTML entry point files such as `index.html`. The hook receives the current HTML string and a transform context. The context exposes the [`ViteDevServer`](./api-javascript#vitedevserver) instance during dev, and exposes the Rollup output bundle during build.
2021-01-01 21:40:16 +00:00
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:**
2021-01-01 21:40:16 +00:00
```js
const htmlPlugin = () => {
return {
name: 'html-transform',
transformIndexHtml(html) {
return html.replace(
/<title>(.*?)<\/title>/,
`<title>Title replaced!</title>`,
)
},
}
}
```
**Full Hook Signature:**
```ts
type IndexHtmlTransformHook = (
html: string,
ctx: {
path: string
filename: string
server?: ViteDevServer
bundle?: import('rollup').OutputBundle
chunk?: import('rollup').OutputChunk
2021-01-01 21:40:16 +00:00
},
) =>
| IndexHtmlTransformResult
| void
| Promise<IndexHtmlTransformResult | void>
type IndexHtmlTransformResult =
| string
| HtmlTagDescriptor[]
| {
html: string
tags: HtmlTagDescriptor[]
}
interface HtmlTagDescriptor {
tag: string
attrs?: Record<string, string | boolean>
2021-01-01 21:40:16 +00:00
children?: string | HtmlTagDescriptor[]
/**
* default: 'head-prepend'
*/
injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'
2021-01-01 21:40:16 +00:00
}
```
### `handleHotUpdate`
- **Type:** `(ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>`
2021-01-01 21:40:16 +00:00
Perform custom HMR update handling. The hook receives a context object with the following signature:
2021-01-01 21:40:16 +00:00
```ts
interface HmrContext {
file: string
timestamp: number
modules: Array<ModuleNode>
read: () => string | Promise<string>
server: ViteDevServer
}
```
2021-01-01 21:40:16 +00:00
- `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).
2021-01-01 21:40:16 +00:00
- `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.
2021-01-01 21:40:16 +00:00
The hook can choose to:
2021-01-02 09:53:38 +00:00
- Filter and narrow down the affected module list so that the HMR is more accurate.
2021-01-01 21:40:16 +00:00
2021-01-02 10:42:30 +00:00
- Return an empty array and perform complete custom HMR handling by sending custom events to the client:
2021-01-01 21:40:16 +00:00
```js
handleHotUpdate({ server }) {
2021-01-01 21:40:16 +00:00
server.ws.send({
type: 'custom',
event: 'special-update',
data: {}
})
return []
}
```
Client code should register corresponding handler using the [HMR API](./api-hmr) (this could be injected by the same plugin's `transform` hook):
```js
if (import.meta.hot) {
import.meta.hot.on('special-update', (data) => {
2021-01-01 21:40:16 +00:00
// 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:
- Alias
- User plugins with `enforce: 'pre'`
- Vite core plugins
2021-01-01 21:40:16 +00:00
- User plugins without enforce value
- Vite build plugins
2021-01-01 21:40:16 +00:00
- User plugins with `enforce: 'post'`
- Vite post build plugins (minify, manifest, reporting)
## Conditional Application
2021-03-30 21:38:57 +00:00
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 to only invoke them during `'build'` or `'serve'`:
```js
function myPlugin() {
return {
name: 'build-only',
2021-03-30 21:38:57 +00:00
apply: 'build', // or 'serve'
}
}
```
2021-01-01 21:40:16 +00:00
A function can also be used for more precise control:
```js
apply(config, { command }) {
// apply only on build but not for SSR
return command === 'build' && !config.build.ssr
}
```
2021-01-20 17:40:39 +00:00
## Rollup Plugin Compatibility
2021-01-01 21:40:16 +00:00
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 criteria then it should just work as a Vite plugin:
2021-01-01 21:40:16 +00:00
- It doesn't use the [`moduleParsed`](https://rollupjs.org/plugin-development/#moduleparsed) hook.
2021-01-01 21:40:16 +00:00
- 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. It will work the same as a Vite plugin with `enforce: 'post'` and `apply: 'build'`.
You can also augment an existing Rollup plugin with Vite-only properties:
```js
// vite.config.js
import example from 'rollup-plugin-example'
2021-07-27 15:00:51 +00:00
import { defineConfig } from 'vite'
2021-07-27 15:00:51 +00:00
export default defineConfig({
plugins: [
{
...example(),
enforce: 'post',
apply: 'build',
},
],
2021-07-27 15:00:51 +00:00
})
```
2021-01-20 17:40:39 +00:00
2021-03-30 21:38:57 +00:00
Check out [Vite Rollup Plugins](https://vite-rollup-plugins.patak.dev) for a list of compatible official Rollup plugins with usage instructions.
## 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, so resolved ids have win32 separators ( \\ ) in Windows. However, Rollup plugins use a [`normalizePath` utility function](https://github.com/rollup/plugins/tree/master/packages/pluginutils#normalizepath) from `@rollup/pluginutils` internally, which converts separators to POSIX before performing comparisons. This means that when these plugins are used in Vite, the `include` and `exclude` config pattern and other similar paths against resolved ids comparisons work correctly.
So, 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
import { normalizePath } from 'vite'
normalizePath('foo\\bar') // 'foo/bar'
normalizePath('foo/bar') // 'foo/bar'
```
2022-06-13 14:52:16 +00:00
## Filtering, include/exclude pattern
Vite exposes [`@rollup/pluginutils`'s `createFilter`](https://github.com/rollup/plugins/tree/master/packages/pluginutils#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.
## Client-server Communication
Since Vite 2.9, we provide some utilities for plugins to help handle the communication with clients.
### Server to Client
2022-03-31 14:17:37 +00:00
On the plugin side, we could use `server.ws.send` to broadcast events to all the clients:
```js
// vite.config.js
export default defineConfig({
plugins: [
{
// ...
configureServer(server) {
// Example: wait for a client to connect before sending a message
server.ws.on('connection', () => {
server.ws.send('my:greetings', { msg: 'hello' })
})
},
},
],
})
```
::: tip NOTE
2022-07-03 10:18:27 +00:00
We recommend **always prefixing** your event names to avoid collisions with other plugins.
:::
On the client side, use [`hot.on`](/guide/api-hmr.html#hot-on-event-cb) to listen to the events:
```ts
// 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`](/guide/api-hmr.html#hot-send-event-payload):
```ts
// 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
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
It is possible to type custom events by extending the `CustomEventMap` interface:
```ts
// events.d.ts
import 'vite/types/customEvent'
declare module 'vite/types/customEvent' {
interface CustomEventMap {
'custom:foo': { msg: string }
// 'event-key': payload
}
}
```