vite-mastery

3.6 · difficulty 2/4 · 10 min read

Debugging Vite Plugins

Plugin not working? Hook not firing? Use @vitejs/devtools, DEBUG mode, and the plugin Inspector to quickly locate the problem.

Vite 8.1Stable

The most common plugin problems

During plugin development, the two most frequent issues are:

  1. Hook is never called — the plugin has transform configured, but it never executes at runtime
  2. Hook executes but produces wrong output — the code runs, but the transformed result doesn't match expectations

There are three layers of tools for debugging these two categories, from simplest to most powerful.

Layer 1: console.log (most direct)

The fastest way to check whether a hook fires is to add logging at key points:

ts
import type { Plugin } from "vite"

export function myPlugin(): Plugin {
  return {
    name: "vite-plugin-my",

    configResolved(config) {
      console.log("[my-plugin] configResolved, mode:", config.mode)
    },

    transform(code, id) {
      if (!id.endsWith(".tsx")) return null

      console.log("[my-plugin] transform:", id.split("/").slice(-2).join("/"))

      const result = doTransform(code)
      return { code: result, map: null }
    },

    generateBundle(_, bundle) {
      console.log("[my-plugin] generateBundle,", Object.keys(bundle).length, "chunks total")
    },
  }
}

function doTransform(code: string) {
  return code
}

Tip: Use a [plugin-name] prefix to distinguish logs from different plugins and avoid confusion.

Layer 2: DEBUG environment variable

Vite has detailed internal debug logs that are hidden by default. Enable them with an environment variable:

bash
# Show all Vite internal logs
DEBUG=vite:* pnpm dev

# Show logs for specific subsystems only
DEBUG=vite:resolve pnpm dev     # module resolution
DEBUG=vite:transform pnpm dev   # transform pipeline
DEBUG=vite:hmr pnpm dev         # HMR-related
DEBUG=vite:deps pnpm dev        # dependency pre-bundling

Sample output from DEBUG=vite:resolve:

text
  vite:resolve resolve: react → /node_modules/.vite/deps/react.js +2ms
  vite:resolve resolve: ./App.tsx → /src/App.tsx +0ms
  vite:resolve resolve: ./Button.tsx → /src/Button.tsx +1ms

If your plugin returns an ID from resolveId but the subsequent load is never called, DEBUG=vite:resolve lets you trace the full resolution chain.

Layer 3: vite-plugin-inspect

vite-plugin-inspect is the killer tool for plugin debugging. It records the state of each module after every plugin processes it:

bash
pnpm add -D vite-plugin-inspect
ts
import { defineConfig } from "vite"
import Inspect from "vite-plugin-inspect"

export default defineConfig({
  plugins: [
    Inspect(), // only active in dev mode
    myPlugin(),
  ],
})

After starting the dev server, visit http://localhost:5173/__inspect:

text
┌─────────────────────────────────────────┐
│  vite-plugin-inspect                    │
│                                         │
│  Modules: 42 processed                  │
│                                         │
│  src/App.tsx                            │
│    ├── vite:resolve       0ms           │
│    ├── my-plugin          2ms  [modified]│
│    ├── @vitejs/plugin-react 5ms [modified]│
│    └── vite:esbuild       1ms  [modified]│
│                                         │
│  Click any row to see the diff →        │
└─────────────────────────────────────────┘

The full transform chain for each module can be expanded, showing the before-and-after diff for each plugin.

Layer 4: @vitejs/devtools

@vitejs/devtools is the Vite team's debugging toolkit, currently in early preview. It is not a built-in devtools field in vite.config.ts; it is used through a standalone command or integration modes documented by the DevTools site.

bash
pnpm dlx @vitejs/devtools@latest

After installing or launching it, choose the Standalone, Embedded, or Rolldown integration mode from the official guide. For plugin debugging, it helps answer questions like:

  • Which Vite plugins did the project actually load?
  • Which transforms touched a given module?
  • Where are dependency pre-bundling, module graph, or build-time hotspots?
  • Do Rolldown bundle and chunk details match your expectations?

Systematic troubleshooting steps

When a hook isn't working, follow these steps in order:

Step 1: confirm the plugin is registered

ts
// Wrong: forgot to call the plugin factory function
plugins: [myPlugin] // ❌ myPlugin is a function, not a plugin object

// Correct:
plugins: [myPlugin()] // ✅

Step 2: confirm the apply condition is met

ts
{
  name: "my-plugin",
  apply: "build",  // ← only runs during build
  transform(code, id) {
    // This never executes in dev mode!
  },
}

If you don't see logs when running pnpm dev, check whether apply is set to "build".

Step 3: confirm the file filter condition is correct

ts
transform(code, id) {
  // Common mistake: missing extension variants
  if (!id.endsWith(".ts")) return null  // ❌ misses .tsx

  // Correct:
  if (!/\.(ts|tsx)$/.test(id)) return null  // ✅

  // Another common mistake: forgetting to filter out node_modules
  if (id.includes("node_modules")) return null  // add this
}

Step 4: confirm the hook invocation convention

resolveId uses the first-wins invocation convention: the first plugin to return a non-null value wins. If another higher-priority plugin returns a result first, your resolveId may never get a chance to run.

ts
// Check: is your virtual module ID being intercepted by another plugin first?
resolveId(id) {
  console.log("resolveId called:", id)  // first confirm it's being called
  if (id === "virtual:my-module") return "\0virtual:my-module"
  return null
},

Step 5: use vite-plugin-inspect to trace the transform chain

If the steps above all check out, use vite-plugin-inspect to view the complete transform chain for the file in question. Confirm that your plugin appears in the chain and that its output matches expectations.

Self-check

  1. What kinds of problems is DEBUG=vite:resolve suited for, versus DEBUG=vite:transform?
  2. For a plugin with apply: "serve", will its hooks be called during pnpm build?
  3. resolveId uses the first-wins invocation convention. How does that affect debugging?
  4. What is the bug in the following code? How would you use the tools above to find it?
ts
export function myPlugin(): Plugin {
  return {
    name: "my-plugin",
    transform(code, id) {
      if (id.endsWith(".vue")) return null // don't handle .vue
      if (!id.endsWith(".ts")) return null // only handle .ts

      return {
        code: code.replace("__BUILD_TIME__", new Date().toISOString()),
        map: null,
      }
    },
  }
}

// Problem: the project has an App.tsx file, but this plugin never processes it
// Why? How do you fix it?