Хуки Rollup, поддерживаемые Vite

Vite использует собственный dev-сервер и механизм трансформации модулей во время разработки, однако на этапе production-сборки полностью опирается на Rollup. Именно поэтому большинство плагинов Vite строятся вокруг Rollup API и поддерживают значительную часть Rollup-хуков.

Плагин Vite представляет собой расширение Rollup-плагина. Многие хуки вызываются одинаково как в Rollup, так и в Vite, однако часть из них работает только во время сборки, а часть — и в dev-режиме.

Типичная структура плагина:

export default function myPlugin() {
    return {
        name: 'my-plugin',

        buildStart() {
            console.log('Сборка началась')
        }
    }
}

Подключение:

import { defineConfig } from 'vite'
import myPlugin from './plugins/my-plugin.js'

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

Категории Rollup-хуков в Vite

Vite поддерживает несколько групп Rollup-хуков:

Категория Назначение
Input hooks Работа со входными файлами
Resolve hooks Разрешение модулей
Load hooks Загрузка содержимого
Transform hooks Трансформация кода
Build hooks Управление процессом сборки
Output hooks Генерация выходных файлов
Close hooks Завершение работы

Не все Rollup-хуки поддерживаются одинаково. Некоторые работают только в production build, некоторые — только частично.


Хук options

Назначение

Хук options вызывается самым первым. Позволяет модифицировать Rollup-конфигурацию до старта сборки.

export default function plugin() {
    return {
        name: 'options-plugin',

        options(options) {
            console.log(options.input)

            return {
                ...options,
                treeshake: false
            }
        }
    }
}

Особенности в Vite

Во время dev-сервера options практически не играет роли, поскольку Vite не запускает полноценный Rollup bundling.

На production build хук работает полноценно.


Хук buildStart

Назначение

Вызывается перед началом обработки графа модулей.

buildStart() {
    console.log('Начало сборки')
}

Основные применения

Инициализация ресурсов

buildStart() {
    this.cache.set('startTime', Date.now())
}

Добавление виртуальных модулей

buildStart() {
    this.emitFile({
        type: 'asset',
        fileName: 'meta.json',
        source: '{"version":"1.0"}'
    })
}

Проверка окружения

buildStart() {
    if (!process.env.API_URL) {
        this.error('API_URL не определён')
    }
}

Хук resolveId

Назначение

Один из важнейших хуков Vite и Rollup.

Отвечает за определение пути модуля.

resolveId(source, importer) {
    console.log(source)
    console.log(importer)
}

Механизм работы

Когда встречается импорт:

import foo from './foo.js'

Vite вызывает цепочку resolveId, пока какой-либо плагин не вернёт результат.


Возврат собственного пути

resolveId(source) {
    if (source === 'virtual:config') {
        return '\0virtual:config'
    }
}

Префикс \0 указывает Rollup, что модуль виртуальный.


Алиасы

resolveId(source) {
    if (source.startsWith('@images')) {
        return source.replace('@images', '/src/assets/images')
    }
}

Блокировка импорта

resolveId(source) {
    if (source.includes('fs')) {
        this.error('fs запрещён')
    }
}

Особенности resolveId в Vite

Хук активно используется как в dev, так и в build.

Именно через resolveId реализуются:

  • виртуальные модули;
  • alias-системы;
  • SSR-резолвинг;
  • обработка npm-пакетов;
  • оптимизация зависимостей.

Хук load

Назначение

Позволяет самостоятельно загрузить содержимое модуля.

load(id) {
    console.log(id)
}

Виртуальные модули

Один из самых распространённых сценариев.

resolveId(source) {
    if (source === 'virtual:data') {
        return '\0virtual:data'
    }
},

load(id) {
    if (id === '\0virtual:data') {
        return `
            export const message = 'hello'
        `
    }
}

Использование:

import { message } from 'virtual:data'

console.log(message)

Генерация кода

load(id) {
    if (id.endsWith('.generated.js')) {
        return `
            export default {
                timestamp: ${Date.now()}
            }
        `
    }
}

Чтение файлов

import fs from 'node:fs'

load(id) {
    if (id.endsWith('.txt')) {
        return `
            export default ${JSON.stringify(
                fs.readFileSync(id, 'utf-8')
            )}
        `
    }
}

Хук transform

Назначение

Главный хук трансформации кода.

transform(code, id) {
    return code
}

Простейшая модификация

transform(code, id) {
    if (id.endsWith('.js')) {
        return code.replace('__DEV__', 'true')
    }
}

Возврат объекта

transform(code) {
    return {
        code: code.replace('foo', 'bar'),
        map: null
    }
}

Source Map

transform(code) {
    return {
        code,
        map: {
            mappings: ''
        }
    }
}

Фильтрация файлов

Практически всегда требуется ограничение области работы.

transform(code, id) {
    if (!id.endsWith('.vue')) {
        return
    }

    return code
}

Использование MagicString

Часто применяется библиотека MagicString.

import MagicString from 'magic-string'

transform(code) {
    const s = new MagicString(code)

    s.prepend('const injected = true;\n')

    return {
        code: s.toString(),
        map: s.generateMap()
    }
}

AST-анализ

transform(code) {
    const ast = this.parse(code)

    console.log(ast)

    return code
}

Особенности transform в Vite

Хук используется чрезвычайно активно:

Режим Поддержка
Dev server Да
Production build Да
SSR Да

Через transform работают:

  • Vue SFC;
  • React Fast Refresh;
  • JSX;
  • TypeScript;
  • CSS modules;
  • Markdown-плагины;
  • SVG loader;
  • env-инъекции.

Хук moduleParsed

Назначение

Вызывается после парсинга модуля.

moduleParsed(info) {
    console.log(info.id)
}

Содержимое info

{
    id,
    importedIds,
    dynamicallyImportedIds,
    ast
}

Анализ зависимостей

moduleParsed(info) {
    console.log(info.importedIds)
}

Особенности Vite

В dev-режиме хук может работать ограниченно, поскольку Vite избегает полного AST-анализа ради производительности.

Во время production build поддержка полноценная.


Хук buildEnd

Назначение

Вызывается после завершения сборки.

buildEnd(error) {
    if (error) {
        console.error(error)
    }
}

Очистка ресурсов

buildEnd() {
    clearInterval(this.timer)
}

Сбор статистики

buildEnd() {
    console.log('Сборка завершена')
}

Output-хуки

Общая особенность

Output hooks работают только при production build.

Во время dev-server они не вызываются.


Хук outputOptions

Назначение

Позволяет изменить output-конфигурацию.

outputOptions(options) {
    return {
        ...options,
        sourcemap: true
    }
}

Хук renderStart

Назначение

Вызывается перед генерацией output.

renderStart() {
    console.log('Рендер output')
}

Хук banner / footer / intro / outro

Назначение

Добавляют код в bundle.


banner() {
    return '/* build banner */'
}

Результат:

/* build banner */
console.log('app')

footer() {
    return '/* footer */'
}

intro

Добавляет код внутрь bundle в начало.

intro() {
    return 'const BUILD = true;'
}

outro

Добавляет код в конец bundle.

outro() {
    return 'console.log("finish")'
}

Хук renderChunk

Назначение

Позволяет изменить итоговый chunk.

renderChunk(code, chunk) {
    return code
}

Минификация

renderChunk(code) {
    return code.replace(/\s+/g, ' ')
}

Инъекция данных

renderChunk(code) {
    return `
        const VERSION = '1.0';
        ${code}
    `
}

Работа с chunk

renderChunk(code, chunk) {
    console.log(chunk.fileName)

    return code
}

Хук generateBundle

Назначение

Один из самых мощных output-хуков.

Позволяет управлять всем bundle.

generateBundle(options, bundle) {
    console.log(bundle)
}

Структура bundle

{
    'index.js': ChunkInfo,
    'style.css': AssetInfo
}

Удаление файлов

generateBundle(options, bundle) {
    delete bundle['debug.js']
}

Добавление файлов

generateBundle() {
    this.emitFile({
        type: 'asset',
        fileName: 'stats.json',
        source: '{"ok":true}'
    })
}

Изменение ассетов

generateBundle(options, bundle) {
    for (const file in bundle) {
        const item = bundle[file]

        if (item.type === 'asset') {
            item.source = String(item.source).toUpperCase()
        }
    }
}

Хук writeBundle

Назначение

Вызывается после записи файлов на диск.

writeBundle() {
    console.log('Файлы записаны')
}

Практические применения

Копирование файлов

import fs from 'node:fs'

writeBundle() {
    fs.copyFileSync(
        './extra.txt',
        './dist/extra.txt'
    )
}

Отправка артефактов

writeBundle() {
    console.log('deploy started')
}

Хук closeBundle

Назначение

Последний lifecycle-хук.

closeBundle() {
    console.log('bundler closed')
}

Отличие от buildEnd

buildEnd closeBundle
После сборки После полного завершения
Может вызываться при ошибке Финальный этап
Нет гарантии записи файлов Файлы уже готовы

Виртуальные модули и цепочка Rollup-хуков

Наиболее типичная схема работы Vite-плагинов:

export default function plugin() {
    return {
        name: 'virtual-module',

        resolveId(source) {
            if (source === 'virtual:env') {
                return '\0virtual:env'
            }
        },

        load(id) {
            if (id === '\0virtual:env') {
                return `
                    export const mode = 'development'
                `
            }
        },

        transform(code) {
            return code
        }
    }
}

Цепочка выглядит так:

import → resolveId → load → transform

Контекст плагина

Во всех Rollup-хуках доступен специальный контекст this.


emitFile

Создание файлов:

this.emitFile({
    type: 'asset',
    fileName: 'meta.json',
    source: '{}'
})

error

Генерация ошибки:

this.error('Build failed')

warn

Предупреждение:

this.warn('Deprecated API')

parse

Парсинг AST:

const ast = this.parse(code)

addWatchFile

Добавление файлов в watcher:

this.addWatchFile('./config.json')

Порядок выполнения хуков

Типичная последовательность production build:

options
buildStart
resolveId
load
transform
moduleParsed
buildEnd
outputOptions
renderStart
renderChunk
generateBundle
writeBundle
closeBundle

Во время dev-server цепочка значительно сокращается:

resolveId
load
transform

Различия между dev и build

Dev server

Vite использует:

  • native ES modules;
  • on-demand трансформации;
  • отсутствие bundle;
  • быстрый HMR.

Поэтому многие Rollup output hooks отсутствуют.


Production build

Во время production build запускается полноценный Rollup pipeline.

Доступны:

  • chunk generation;
  • tree shaking;
  • asset emission;
  • output hooks;
  • code splitting.

Совместимость Rollup-плагинов с Vite

Большинство Rollup-плагинов совместимы с Vite:

import legacy from '@rollup/plugin-legacy'

export default {
    plugins: [legacy()]
}

Однако возможны ограничения:

Возможность Dev
resolveId Да
load Да
transform Да
renderChunk Нет
generateBundle Нет
writeBundle Нет

enforce и порядок Vite-плагинов

Vite расширяет Rollup-плагины собственным механизмом enforce.


enforce: ‘pre’

{
    name: 'pre-plugin',
    enforce: 'pre'
}

Запускается раньше стандартных плагинов.


enforce: ‘post’

{
    name: 'post-plugin',
    enforce: 'post'
}

Запускается после остальных.


Порядок transform-хуков

pre → normal → post

Это критически важно для:

  • JSX;
  • Vue;
  • React Refresh;
  • TypeScript;
  • Markdown;
  • AST-модификаций.

apply и условное выполнение

Vite позволяет ограничивать выполнение плагинов.


Только build

{
    apply: 'build'
}

Только serve

{
    apply: 'serve'
}

Условная логика

{
    apply(config, env) {
        return env.mode === 'production'
    }
}

Наиболее используемые хуки в экосистеме Vite

Хук Популярность
resolveId Очень высокая
load Очень высокая
transform Максимальная
configureServer Очень высокая
generateBundle Высокая
renderChunk Высокая

Хуки, специфичные для Vite

Помимо Rollup API, Vite предоставляет собственные хуки:

Хук Назначение
config Изменение конфигурации
configResolved Доступ к финальному config
configureServer Настройка dev server
handleHotUpdate Управление HMR
transformIndexHtml Изменение HTML

Они дополняют Rollup lifecycle, формируя гибридную архитектуру Vite-плагинов.