Система плагинов в Vite построена поверх плагинов Rollup, но при этом расширяет их собственными возможностями. Один и тот же плагин может участвовать:
Плагин представляет собой объект с набором хуков.
Минимальная структура:
export default function myPlugin() {
return {
name: 'my-plugin'
}
}
Поле name обязательно. Оно используется:
Подключение плагина:
import { defineConfig } from 'vite'
import myPlugin from './plugins/my-plugin.js'
export default defineConfig({
plugins: [
myPlugin()
]
})
Типичная структура:
project/
├─ plugins/
│ └─ my-plugin/
│ ├─ index.js
│ ├─ utils.js
│ └─ constants.js
├─ vite.config.js
└─ src/
Для сложных решений структура часто разделяется по хукам:
my-plugin/
├─ hooks/
│ ├─ resolveId.js
│ ├─ load.js
│ ├─ transform.js
│ └─ configureServer.js
├─ utils/
├─ index.js
└─ package.json
Практически все плагины Vite реализуются через фабрику:
export default function myPlugin(options = {}) {
return {
name: 'my-plugin'
}
}
Это позволяет:
Пример:
export default function bannerPlugin(options = {}) {
const banner = options.banner || 'DEV'
return {
name: 'banner-plugin'
}
}
Подключение:
bannerPlugin({
banner: 'PROJECT'
})
transform — основной хук обработки модулей.
Он вызывается для каждого файла.
export default function myPlugin() {
return {
name: 'my-plugin',
transform(code, id) {
console.log(id)
return code
}
}
}
Аргументы:
| Аргумент | Описание |
|---|---|
code |
Исходный код модуля |
id |
Полный путь файла |
Простейшая модификация:
transform(code, id) {
if (!id.endsWith('.js')) {
return
}
return code.replace(
'__VERSION__',
'1.0.0'
)
}
Исходный код:
console.log(__VERSION__)
После трансформации:
console.log('1.0.0')
transform может возвращать объект:
transform(code) {
return {
code: code.replace('foo', 'bar'),
map: null
}
}
Поля:
| Поле | Назначение |
|---|---|
code |
Новый код |
map |
Source map |
Для корректных source map обычно применяется библиотека
magic-string.
Установка:
npm install magic-string
Пример:
import MagicString from 'magic-string'
export default function myPlugin() {
return {
name: 'my-plugin',
transform(code, id) {
if (!id.endsWith('.js')) {
return
}
const s = new MagicString(code)
s.prepend('const __DEV__ = true;\n')
return {
code: s.toString(),
map: s.generateMap({
hires: true
})
}
}
}
}
resolveId управляет механизмом резолвинга импортов.
resolveId(source, importer) {
console.log(source)
console.log(importer)
}
Аргументы:
| Аргумент | Описание |
|---|---|
source |
Импортируемый путь |
importer |
Модуль-источник |
Одна из самых популярных задач плагинов.
const virtualModuleId = 'virtual:config'
const resolvedVirtualModuleId = '\0' + virtualModuleId
export default function virtualPlugin() {
return {
name: 'virtual-plugin',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `
export const API_URL = 'https://api.test.com'
`
}
}
}
}
Использование:
import { API_URL } from 'virtual:config'
\0Префикс \0 помечает модуль как виртуальный внутренний
модуль.
Это предотвращает:
load отвечает за загрузку содержимого модуля.
load(id) {
if (id.endsWith('.txt')) {
return 'export default "TEXT"'
}
}
Этот хук особенно полезен:
Пример поддержки .hello:
load(id) {
if (id.endsWith('.hello')) {
return `
export default "Hello World"
`
}
}
Импорт:
import message from './test.hello'
Позволяет получить доступ к dev-серверу Vite.
configureServer(server) {
console.log(server)
}
Через сервер доступны:
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === '/custom') {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({
ok: true
}))
return
}
next()
})
}
После этого появляется новый endpoint:
/custom
Vite использует websocket для HMR.
Плагин может отправлять собственные события:
configureServer(server) {
server.ws.send({
type: 'custom',
event: 'my:event',
data: {
message: 'upd ated'
}
})
}
Клиент:
if (import.meta.hot) {
import.meta.hot.on('my:event', (data) => {
console.log(data)
})
}
Используется для контроля HMR.
handleHotUpdate(ctx) {
console.log(ctx.file)
}
Контекст содержит:
| Поле | Описание |
|---|---|
file |
Изменённый файл |
server |
Dev server |
modules |
Связанные модули |
timestamp |
Время обновления |
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.config.json')) {
ctx.server.ws.send({
type: 'full-reload'
})
return []
}
}
Позволяет модифицировать HTML.
transformIndexHtml(html) {
return html.replace(
'</head>',
'<script src="/inject.js"></script></head>'
)
}
Vite поддерживает декларативный API:
transformIndexHtml() {
return [
{
tag: 'script',
attrs: {
src: '/inject.js'
},
injectTo: 'head'
}
]
}
Варианты вставки:
| Значение | Место вставки |
|---|---|
head |
В конец <head> |
head-prepend |
В начало <head> |
body |
В конец <body> |
body-prepend |
В начало <body> |
Позволяет изменять конфигурацию Vite.
config(config) {
config.server ||= {}
config.server.port = 5000
}
Предпочтительный способ:
config() {
return {
define: {
__DEV__: true
}
}
}
Вызывается после полной обработки конфигурации.
let resolvedConfig
export default function myPlugin() {
return {
name: 'my-plugin',
configResolved(config) {
resolvedConfig = config
}
}
}
Через него можно получить:
let isBuild = false
configResolved(config) {
isBuild = config.command === 'build'
}
transform(code) {
if (!isBuild) {
return
}
return code.replace(
'__BUILD__',
'true'
)
}
Управляет порядком выполнения.
{
name: 'my-plugin',
enforce: 'pre'
}
Варианты:
| Значение | Назначение |
|---|---|
pre |
До обычных плагинов |
post |
После обычных |
Позволяет ограничивать область работы плагина.
{
name: 'my-plugin',
apply: 'build'
}
Варианты:
| Значение | Описание |
|---|---|
serve |
Только dev |
build |
Только build |
apply(config, env) {
return env.mode === 'production'
}
Критически важный аспект производительности.
Плохой вариант:
transform(code) {
return code.replace(...)
}
Правильный вариант:
transform(code, id) {
if (!id.endsWith('.js')) {
return
}
return code.replace(...)
}
if (id.includes('node_modules')) {
return
}
Обычно применяется @rollup/pluginutils.
npm install @rollup/pluginutils
import { createFilter } from '@rollup/pluginutils'
const filter = createFilter(
['**/*.js'],
['node_modules/**']
)
transform(code, id) {
if (!filter(id)) {
return
}
return code.replace(...)
}
Плагины могут хранить внутреннее состояние:
export default function myPlugin() {
const cache = new Map()
return {
name: 'my-plugin',
transform(code, id) {
cache.se t(id, code)
}
}
}
Для этого существует buildStart.
buildStart() {
cache.clear()
}
Во время build доступны хуки Rollup.
Пример:
generateBundle() {
console.log('bundle generated')
}
generateBundle(options, bundle) {
this.emitFile({
type: 'asset',
fileName: 'meta.json',
source: JSON.stringify({
build: Date.now()
})
})
}
this.emitFile({
type: 'chunk',
id: '/src/extra.js'
})
Vite поддерживает полноценную AST-трансформацию.
Пример через Babel:
npm install @babel/parser @babel/traverse @babel/generator
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import generate from '@babel/generator'
transform(code) {
const ast = parse(code, {
sourceType: 'module'
})
traverse(ast, {
Identifier(path) {
if (path.node.name === '__DEV__') {
path.node.name = 'true'
}
}
})
return generate(ast).code
}
Большинство хуков могут быть async.
async transform(code, id) {
const result = await processCode(code)
return result
}
Внутри хуков доступен контекст Rollup.
this.warn('warning')
this.error('fatal error')
this.error({
message: 'Invalid syntax',
id,
pos: 15
})
Удобно использовать namespace:
import debug from 'debug'
const log = debug('vite:my-plugin')
log('transform', id)
Запуск:
DEBUG=vite:* vite
Некоторые плагины должны учитывать SSR.
transform(code, id, options) {
console.log(options?.ssr)
}
if (options?.ssr) {
return code
}
Большинство Rollup-плагинов работают в Vite без изменений:
import replace from '@rollup/plugin-replace'
plugins: [
replace({
__DEV__: true
})
]
Но Vite-специфичные возможности:
В Rollup отсутствуют.
Полноценный пример:
import MagicString from 'magic-string'
import { createFilter } from '@rollup/pluginutils'
export default function replacePlugin(options = {}) {
const filter = createFilter(
options.include || ['**/*.js'],
options.exclude || ['node_modules/**']
)
const values = options.values || {}
return {
name: 'replace-plugin',
transform(code, id) {
if (!filter(id)) {
return
}
let changed = false
const s = new MagicString(code)
for (const [key, value] of Object.entries(values)) {
if (!code.includes(key)) {
continue
}
changed = true
const regexp = new RegExp(key, 'g')
let match
while ((match = regexp.exec(code))) {
s.overwrite(
match.index,
match.index + key.length,
JSON.stringify(value)
)
}
}
if (!changed) {
return
}
return {
code: s.toString(),
map: s.generateMap({
hires: true
})
}
}
}
}
Использование:
replacePlugin({
values: {
__API_URL__: 'https://api.test.com',
__VERSION__: '2.0.0'
}
})
Плагины обычно тестируются через:
Пример Vitest:
import { describe, it, expect } from 'vitest'
import plugin from '../index.js'
describe('plugin', () => {
it('creates plugin', () => {
const result = plugin()
expect(result.name).toBe('my-plugin')
})
})
Минимальный package.json:
{
"name": "vite-plugin-example",
"version": "1.0.0",
"main": "./dist/index.js",
"type": "module",
"peerDependencies": {
"vite": "^5.0.0"
}
}
transform вызывается очень часто.
Дорогостоящие операции:
могут серьёзно замедлить HMR.
Чем уже фильтр — тем быстрее работает dev-сервер.
Хорошо:
createFilter(['src/**/*.js'])
Плохо:
createFilter(['**/*'])
const cache = new Map()
без очистки способен вызывать утечки памяти в больших проектах.
Плохо:
code.replace('foo', 'bar')
Это может случайно изменить:
Для серьёзных трансформаций предпочтительны:
Плохо:
while (true) {}
или:
fs.readFileSync(...)
внутри transform.
Dev-сервер Vite чрезвычайно чувствителен к блокировкам event loop.
import type { Plugin } from 'vite'
export default function myPlugin(): Plugin {
return {
name: 'my-plugin'
}
}
interface PluginOptions {
enabled?: boolean
apiUrl?: string
}
export default function myPlugin(
options: PluginOptions = {}
): Plugin {
return {
name: 'my-plugin'
}
}
Плагин может возвращать массив:
export default function plugins() {
return [
pluginA(),
pluginB()
]
}
Это часто используется для создания meta-plugin архитектуры.