Порядок применения плагинов и enforce

Система плагинов в Vite построена вокруг последовательной обработки модулей, HTML-файлов, CSS, виртуальных ресурсов и стадий сборки. Каждый плагин способен вмешиваться в жизненный цикл проекта, изменяя код, добавляя виртуальные модули, подменяя импорты, трансформируя ассеты или влияя на процесс сборки.

Поскольку одновременно могут использоваться десятки плагинов, порядок их выполнения становится критически важным. Неверная последовательность способна привести к:

  • ошибкам трансформации;
  • некорректной обработке импортов;
  • конфликтам alias;
  • повторной обработке кода;
  • несовместимости HMR;
  • поломке sourcemap;
  • нарушению SSR.

Для управления последовательностью выполнения используется свойство enforce.


Базовый порядок выполнения плагинов

Без дополнительных настроек плагины выполняются в порядке объявления внутри массива plugins.

Пример:

import pluginA from './plugin-a'
import pluginB from './plugin-b'
import pluginC from './plugin-c'

export default {
    plugins: [
        pluginA(),
        pluginB(),
        pluginC()
    ]
}

Последовательность вызова hook-ов:

  1. pluginA
  2. pluginB
  3. pluginC

Это относится ко многим hook-ам:

  • resolveId
  • load
  • transform
  • handleHotUpdate
  • configureServer

Однако часть hook-ов выполняется в обратном порядке. Особенно это касается post-processing стадий.


Свойство enforce

Свойство enforce позволяет сместить плагин в одну из специальных фаз:

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

Допустимые значения:

'enforce: "pre"'
'enforce: "post"'

Если enforce не указан — плагин относится к обычной фазе (normal).


Три группы плагинов

После обработки enforce все плагины делятся на три блока:

  1. pre
  2. normal
  3. post

Итоговый порядок:

pre → normal → post

Плагины pre

Плагины с enforce: 'pre' выполняются раньше остальных.

Пример:

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

Типичные задачи pre-плагинов:

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

Пример порядка

plugins: [
    pluginA(),
    {
        ...pluginB(),
        enforce: 'pre'
    },
    pluginC()
]

Фактический порядок:

pluginB → pluginA → pluginC

Несмотря на расположение в массиве, pluginB поднимается вверх.


Плагины post

Плагины с enforce: 'post' выполняются после остальных.

Пример:

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

Типичные задачи post-плагинов:

  • финальная оптимизация;
  • минификация;
  • post-processing;
  • анализ итогового кода;
  • генерация отчетов;
  • обработка sourcemap;
  • внедрение runtime-кода.

Пример последовательности

plugins: [
    pluginA(),
    {
        ...pluginB(),
        enforce: 'post'
    },
    pluginC()
]

Итог:

pluginA → pluginC → pluginB

Внутренний порядок внутри группы

Внутри одной группы (pre, normal, post) сохраняется порядок объявления.

Пример:

plugins: [
    {
        name: 'a',
        enforce: 'pre'
    },
    {
        name: 'b',
        enforce: 'pre'
    },
    pluginC()
]

Порядок:

a → b → pluginC

Как Vite сортирует плагины

Упрощённая схема:

1. Все pre
2. Все normal
3. Все post

После этого формируется единая цепочка hook-ов.


Связь с Rollup

Система плагинов Vite построена поверх Rollup, поэтому большинство hook-ов совместимы с Rollup API.

Но Vite добавляет собственную фазовую модель:

pre / normal / post

В самом Rollup такого механизма нет.


Типичный жизненный цикл transform

Наиболее показательный hook — transform.

Пример:

transform(code, id) {
    return code
}

При наличии нескольких плагинов цепочка может выглядеть так:

pre-transform
↓
normal-transform
↓
post-transform

Каждый следующий плагин получает результат предыдущего.


Практический пример

Первый плагин

function pluginA() {
    return {
        name: 'plugin-a',

        transform(code) {
            return code.replace('__VALUE__', 'A')
        }
    }
}

Второй плагин

function pluginB() {
    return {
        name: 'plugin-b',

        enforce: 'post',

        transform(code) {
            return code.replace('A', 'B')
        }
    }
}

Исходный код

console.log('__VALUE__')

Результат

console.log('B')

Последовательность:

pluginA → pluginB

Влияние порядка на resolveId

Hook resolveId особенно чувствителен к очередности.

Пример:

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

Если другой плагин изменит import раньше:

import 'virtual:data'

то текущий плагин может никогда не получить нужный source.

Поэтому плагины виртуальных модулей часто используют:

enforce: 'pre'

Влияние порядка на alias

Alias-резолвинг также зависит от очередности.

Пример:

resolveId(id) {
    if (id.startsWith('@')) {
        return id.replace('@', '/src')
    }
}

Если другой плагин изменит путь раньше:

@/components/Button
↓
virtual:@/components/Button

alias уже не сработает.


Почему official plugins часто используют enforce

Многие официальные плагины Vite используют enforce.

Например:

  • ранний JSX-transform;
  • Vue SFC parsing;
  • React Fast Refresh;
  • CSS preprocessing;
  • import analysis.

Это необходимо для строгого контроля цепочки трансформаций.


Конфликт плагинов

Частая проблема — несколько плагинов изменяют один и тот же код.

Пример:

Plugin A:
import.meta.env.API_URL

↓
process.env.API_URL

Plugin B:
import.meta.env.*

Если порядок неверный:

Plugin B не увидит import.meta.env

В подобных случаях один из плагинов должен быть pre.


Комбинация pre и post

Иногда один плагин выполняет ранний анализ, а другой — финальную обработку.

Пример:

pre-plugin:
анализирует imports

normal-plugin:
трансформирует код

post-plugin:
генерирует отчет

Такая архитектура встречается:

  • в SSR;
  • в CSS extraction;
  • в asset pipelines;
  • в module federation;
  • в markdown processing.

Порядок hook-ов и обратное выполнение

Некоторые hook-ы работают в прямом порядке:

A → B → C

Некоторые — в обратном:

C → B → A

Это зависит от типа hook-а.


Последовательные hook-и

Hook-ы типа transform выполняются последовательно:

plugin1
↓
plugin2
↓
plugin3

Каждый получает изменённый код.


Параллельные hook-и

Часть hook-ов может вызываться независимо:

buildStart
generateBundle
closeBundle

Но даже там порядок регистрации остаётся важным.


enforce и HTML-transform

Vite поддерживает hook:

transformIndexHtml(html) {
    return html
}

Он тоже учитывает enforce.

Пример:

{
    enforce: 'pre',

    transformIndexHtml(html) {
        return html.replace('%TITLE%', 'App')
    }
}

Post-фаза может затем дополнительно модифицировать HTML:

{
    enforce: 'post',

    transformIndexHtml(html) {
        return html + '<script src="/debug.js"></script>'
    }
}

enforce и CSS

CSS-пайплайн в Vite активно зависит от порядка.

Типичная цепочка:

pre:
Sass/Less/Stylus

normal:
CSS Modules

post:
autoprefixer/minification

Неверная последовательность приводит к:

  • поломанным sourcemap;
  • потере class names;
  • некорректному HMR;
  • конфликтам PostCSS.

enforce и HMR

Hook:

handleHotUpdate(ctx)

тоже зависит от порядка.

Ранние плагины могут:

  • отменить обновление;
  • изменить список модулей;
  • внедрить custom invalidation.

Поздние плагины получают уже модифицированное состояние.


enforce и SSR

SSR-плагины часто используют pre.

Причина:

SSR transform должен происходить
до browser-specific transform

Иначе серверный рендеринг может получить браузерный код.


enforce и optimizeDeps

Во время pre-bundling через esbuild некоторые плагины должны выполняться раньше анализа зависимостей.

Пример:

virtual modules
alias plugins
framework resolvers

Именно поэтому многие framework plugins работают как pre.


Debugging порядка плагинов

Для анализа цепочки удобно использовать логирование.

Пример:

function debugPlugin(name) {
    return {
        name,

        transform(code, id) {
            console.log(name, id)
            return code
        }
    }
}

Просмотр итогового списка

Можно вывести итоговую конфигурацию:

configResolved(config) {
    console.log(config.plugins)
}

Это помогает увидеть:

  • реальный порядок;
  • встроенные плагины Vite;
  • автоматически добавленные плагины;
  • позиции pre/post.

Встроенные плагины Vite

Сам Vite регистрирует множество внутренних плагинов:

  • import analysis;
  • CSS handling;
  • asset handling;
  • HTML transform;
  • esbuild transform;
  • define replacement;
  • env replacement.

Они тоже имеют собственные фазы.

Поэтому пользовательские плагины могут неожиданно выполняться:

  • раньше встроенных;
  • между встроенными;
  • после встроенных.

Когда использовать enforce: ‘pre’

pre подходит для:

  • alias;
  • virtual modules;
  • framework preprocessors;
  • markdown loaders;
  • import rewriting;
  • AST-анализа до трансформаций;
  • SSR preparation;
  • custom resolvers.

Когда использовать enforce: ‘post’

post подходит для:

  • минификации;
  • финальной инъекции;
  • post-processing;
  • bundle analysis;
  • diagnostics;
  • runtime patching;
  • sourcemap post-processing;
  • cleanup logic.

Когда enforce не нужен

Если плагин:

  • не зависит от других;
  • не влияет на imports;
  • не меняет цепочку трансформаций;
  • работает автономно;

то достаточно обычной фазы normal.


Антипаттерны

Избыточное использование pre

Проблема:

Все плагины становятся pre

Результат:

  • хаотичная цепочка;
  • трудно предсказать поведение;
  • конфликт framework plugins.

Попытка исправить баги только через enforce

Неверный подход:

"Поставим pre — и всё заработает"

Иногда проблема связана:

  • с неправильным hook;
  • с некорректным return;
  • с конфликтом resolveId;
  • с асинхронной трансформацией.

Нарушение idempotency

Плагин должен корректно работать независимо от количества проходов.

Плохой пример:

code += '__INJECTED__'

При повторной обработке:

__INJECTED____INJECTED__

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


Рекомендуемая стратегия

Для сложных экосистем обычно используется схема:

pre:
резолвинг и анализ

normal:
основные трансформации

post:
финальная обработка

Такой подход делает pipeline предсказуемым и совместимым с экосистемой Vite и Rollup.