Debugging Vite Plugin Hook Order with enforce and apply
Your transform hook fires, but it sees the wrong input — already-compiled JSX instead of raw source, or raw TypeScript instead of the minified output you wanted to instrument — and the cause is almost always plugin ordering. This guide explains how enforce and apply decide that order and how to print the real sequence; for the underlying hook model it builds on, read Advanced Vite Plugin Configuration first.
Prerequisites & Reproducible Setup
# Vite 5.x / 6.x, Node 20+
npm create vite@latest hook-order -- --template react-ts
cd hook-order && npm install
npm install -D vite-plugin-inspect
The React template is deliberate: @vitejs/plugin-react installs an enforce-unmarked transform that compiles JSX, which is the most common thing user plugins collide with.
How enforce and apply Decide Order
Two facts explain nearly every ordering bug:
enforcesorts plugins into three lanes that run in series:'pre', then unmarked, then'post'. Vite’s own plugins — alias resolution,@vitejs/plugin-react’s JSX transform, the import-analysis pass — sit in the unmarked lane. So a plugin withenforce: 'pre'sees raw source before JSX/TS is stripped; a plugin withenforce: 'post'sees the output after.applydecides whether a plugin is loaded at all for the current command.apply: 'serve'plugins never run duringvite build;apply: 'build'plugins never run duringvite dev. A predicateapply: (config, { command }) => command === 'build'gives finer control (e.g. only for--mode staging).
Within a single lane, plugins run in the order they appear in the plugins array. Nested arrays are flattened, and false/null/undefined entries are dropped — which is how conditional plugins disappear silently.
Diagnosis Workflow
Work top-down; stop at the first step that explains the symptom.
- Confirm whether the plugin even runs. If a build-time transform never fires in
vite build, check forapply: 'serve'(or a predicate that excludesbuild). Add a one-lineconsole.loginconfigResolved— it runs once per command, so silence means the plugin is gated out. - Check the lane. A
transformthat receives compiled output when you wanted raw source is in the wrong lane. Move it toenforce: 'pre'. The opposite — wanting final code but receiving source — meansenforce: 'post'. - Print the resolved plugin list and order. Run with the debug flag:
# Vite 5.x / 6.x
vite --debug plugin-transform 2>&1 | grep "your-plugin-name"
- Inspect per-module transforms in the browser. Start the dev server, open
http://localhost:5173/__inspect/, pick a module, and read the ordered list of transforms applied to it. This is the ground truth for dev-time order. - Diff against built-ins. If a Vite core plugin (e.g. CSS handling, asset URL rewriting) runs at the wrong time relative to yours, your only levers are
enforceand array position — you cannot reorder core plugins among themselves.
The Logging Plugin
Drop this in to print, per module, exactly when each lettered probe runs. Register three copies — pre, default, and post — so the console shows the lane order directly.
// plugins/order-probe.ts — Vite 5.x / 6.x, Rollup 4.x
import type { Plugin } from 'vite';
export function orderProbe(label: string, enforce?: 'pre' | 'post'): Plugin {
return {
name: `order-probe:${label}`,
enforce,
configResolved(config) {
// Runs once; prints the full ordered plugin list for this command
if (label === 'pre') {
const names = config.plugins.map((p) => p.name).join(' -> ');
console.log(`[order-probe] command=${config.command} plugins: ${names}`);
}
},
transform(code, id) {
// Only log app source, not node_modules, to keep output readable
if (id.includes('/node_modules/')) return null;
const head = code.slice(0, 24).replace(/\n/g, ' ');
console.log(`[order-probe:${label}] transform ${id.split('/').pop()} :: "${head}"`);
return null; // never mutate — this is a probe
},
};
}
// vite.config.ts — Vite 5.x / 6.x
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import Inspect from 'vite-plugin-inspect';
import { orderProbe } from './plugins/order-probe';
export default defineConfig({
plugins: [
orderProbe('pre', 'pre'), // fires before plugin-react's JSX transform
react(), // unmarked lane
orderProbe('normal'), // fires after react, in array order
orderProbe('post', 'post'), // fires last, sees compiled output
Inspect(), // adds the /__inspect/ panel (dev only)
],
});
Run vite dev and open a .tsx file in the browser. The console prints the three probes in lane order. The pre probe shows raw import ... from 'react' JSX; the post probe shows the same module after @vitejs/plugin-react has rewritten JSX to _jsx(...) calls — concrete proof of which lane sees what.
Verification
- The
[order-probe]lines appear in the orderpre→normal→postfor every app module. - The
preprobe’s logged head still contains JSX angle brackets ortsxsyntax; thepostprobe’s does not. - Run
vite buildand confirm theapply-gated plugins behave:Inspect()(dev-only by default) does not appear in theconfigResolvedplugin list, while the probes do. - The
/__inspect/panel lists your probe transforms in the same relative position as the console output.
Gotchas & Edge Cases
apply: 'serve'plugins are invisible in CI. A transform that works invite devbut is missing fromdist/usually hasapply: 'serve'(orcommand !== 'build'). Remove the gate or switch to a predicate.enforce: 'post'does not beat Rollup’s render hooks.transformalways precedesrenderChunk. To touch the final bundled output, userenderChunk/generateBundle, not aposttransform.- Array order only breaks ties within a lane. Putting your plugin before
react()in the array does nothing if both are unmarked and you neededenforce: 'pre'— lanes win over array position. vite-plugin-inspectshifts timing slightly. It registers its own transforms; trust the console probes over/__inspect/if the two disagree by one position, and remove Inspect before benchmarking.
Related
- Advanced Vite Plugin Configuration — the full hook pipeline,
enforcelanes, virtual modules, and SSR branching. - Writing a Custom Vite Plugin for Asset Transformation — where
enforce: 'pre'matters so your loader sees raw files. - Optimizing Vite Dev Server and HMR — how transform order affects cold-start and HMR latency.