A Vite plugin that adds data attributes to your JSX and TSX elements so you can tell which component rendered which piece of the DOM. Useful when you are debugging someone else's code, or code you generated and have not read yet.
v2.3.0 repairs a CommonJS entry that threw on require(), widens Vite support to
2 through 8, and fixes 19 defects found in an audit of v2.2.1. Full list in
changelog.md.
- Path filtering with glob patterns, to include or exclude specific files
- Attribute transformers to rewrite any attribute value, for privacy or formatting
- Presets for common setups: minimal, testing, debugging, production
- Conditional tagging through a
shouldTagcallback - Custom attributes of your own, such as git branch or environment
- Metadata encoding as JSON, Base64, or plain text
- Statistics and callbacks for tracking what was processed, with optional JSON export
- Depth filtering to tag only certain nesting levels
- Attribute grouping to collapse everything into one JSON attribute
Every option is opt-in. An existing config keeps working untouched.
Detailed examples and use cases
| Vite | Status | Notes |
|---|---|---|
| 8.x | ✅ Supported | Builds with Rolldown/oxc |
| 7.x | ✅ Supported | Requires Node ^20.19.0 || >=22.12.0 |
| 6.x | ✅ Supported | |
| 5.x | ✅ Supported | |
| 4.x | ✅ Supported | |
| 3.x | ✅ Supported | |
| 2.x | ✅ Supported |
Node.js: >= 18.12.0 for the plugin itself. Vite 7 and 8 require Node 20.19+/22.12+, so your Vite version sets the real floor.
Every version in that table is verified by a real vite build in CI, not just declared in
peerDependencies. Run it yourself with pnpm run vite-compat.
# Install
pnpm add -D vite-plugin-component-debugger
# or: npm install --save-dev vite-plugin-component-debugger
# or: yarn add -D vite-plugin-component-debugger// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import componentDebugger from "vite-plugin-component-debugger";
// A named import works too:
// import { componentDebugger } from "vite-plugin-component-debugger";
export default defineConfig({
plugins: [
componentDebugger({ // ⚠️ IMPORTANT: Must be BEFORE react()
enabled: process.env.NODE_ENV === "development", // When to run
attributePrefix: "data-dev", // Custom prefix
extensions: [".jsx", ".tsx"], // File types
}),
react(),
],
});
⚠️ CRITICAL: componentDebugger() must be placed BEFORE react() in thepluginsarray, otherwise line numbers will be wrong.Both this plugin and
@vitejs/plugin-react'svite:react-babeldeclareenforce: 'pre'. Vite keeps the array order within thepregroup, so listing componentDebugger first is what actually decides which transform runs first. The React plugin injects roughly 19 lines of imports and HMR setup, so running after it shifts everydata-dev-lineby that amount.
⚠️ enableddefaults totrue, including production builds. This plugin has noapplyrestriction, so a barecomponentDebugger()also tags your production bundle, embedding source paths and line numbers in the shipped DOM. Gate it explicitly:componentDebugger({ enabled: process.env.NODE_ENV === "development" });
Before:
// src/components/Button.tsx (line 10)
<button className="btn-primary" onClick={handleClick}>
Click me
</button>After (Default - All Attributes):
<button
className="btn-primary"
onClick={handleClick}
data-dev-id="src/components/Button.tsx:10:2"
data-dev-name="button"
data-dev-path="src/components/Button.tsx"
data-dev-line="10"
data-dev-file="Button.tsx"
data-dev-component="button"
>
Click me
</button>Attributes are appended after your existing props, immediately before the closing
>.
After (Minimal Preset - Clean):
componentDebugger({ preset: 'minimal' })
// Results in:
<button
className="btn-primary"
onClick={handleClick}
data-dev-id="src/components/Button.tsx:10:2"
>
Click me
</button>After (Custom Filtering):
componentDebugger({
includeAttributes: ["id", "name", "line"]
})
// Results in:
<button
data-dev-id="src/components/Button.tsx:10:2"
data-dev-name="button"
data-dev-line="10"
className="btn-primary"
onClick={handleClick}
>
Click me
</button>- Find which component rendered any DOM element, without guessing
- Jump straight from DevTools to the source line
- Select elements in E2E tests by a stable attribute instead of a brittle CSS path
- Fragments and Three.js elements are skipped automatically, so your scene graph stays clean
The transform runs at build time and adds no runtime code. It does add attributes to your
markup, though, so gate it with enabled if you do not want them in a production bundle.
componentDebugger({
enabled: process.env.NODE_ENV === "development", // When to run
attributePrefix: "data-dev", // Custom prefix
extensions: [".jsx", ".tsx"], // File types
});// Minimal - only ID attribute (cleanest DOM)
componentDebugger({ preset: "minimal" });
// Testing - ID, name, component (perfect for E2E)
componentDebugger({ preset: "testing" });
// Debugging - everything + metadata (full visibility)
componentDebugger({ preset: "debugging" });
// Production - privacy-focused with shortened paths
componentDebugger({ preset: "production" });See all preset details in EXAMPLES.md
Clean DOM, minimal attributes
componentDebugger({
includeAttributes: ["id", "name"], // Only these attributes
});
// Result: Only data-dev-id and data-dev-namePath filtering for specific directories
componentDebugger({
includePaths: ["src/components/**", "src/features/**"],
excludePaths: ["**/*.test.tsx", "**/*.stories.tsx"],
});Privacy: transform paths
componentDebugger({
transformers: {
path: (p) => p.split("/").slice(-2).join("/"), // Shorten paths
id: (id) => id.split(":").slice(-2).join(":"), // Remove path from ID
},
});Conditional: tag specific components
componentDebugger({
shouldTag: ({ elementName }) => {
// Only tag custom components (uppercase)
return elementName[0] === elementName[0].toUpperCase();
},
});Prefer
includeAttributesover the legacyincludePropsandincludeContent. It produces a smaller DOM.
⚠️ Gotcha: When bothincludeAttributesandexcludeAttributesare set,includeAttributestakes priority
Core Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
true |
Enable/disable the plugin |
attributePrefix |
string |
'data-dev' |
Prefix for data attributes |
extensions |
string[] |
['.jsx', '.tsx'] |
File extensions to process |
preset |
Preset |
undefined |
Quick config: 'minimal' | 'testing' | 'debugging' | 'production' |
Attribute control
| Option | Type | Default | Description |
|---|---|---|---|
includeAttributes |
AttributeName[] |
undefined |
Recommended: Only include these attributes |
excludeAttributes |
AttributeName[] |
undefined |
Exclude these attributes |
transformers |
object |
undefined |
Transform attribute values (privacy, formatting) |
groupAttributes |
boolean |
false |
Combine all into single JSON attribute |
Available: 'id', 'name', 'path', 'line', 'file', 'component', 'metadata'
Path and element filtering
| Option | Type | Default | Description |
|---|---|---|---|
includePaths |
string[] |
undefined |
Glob patterns to include |
excludePaths |
string[] |
undefined |
Glob patterns to exclude |
excludeElements |
string[] |
['Fragment', 'React.Fragment'] |
Element names to skip |
customExcludes |
Set<string> |
Three.js elements | Custom elements to skip |
Conditional and custom
| Option | Type | Default | Description |
|---|---|---|---|
shouldTag |
(info) => boolean |
undefined |
Conditionally tag components |
customAttributes |
(info) => Record<string, string> |
undefined |
Add custom attributes dynamically |
metadataEncoding |
MetadataEncoding |
'json' |
Encoding: 'json' | 'base64' | 'none' |
Depth, stats and advanced
| Option | Type | Default | Description |
|---|---|---|---|
maxDepth |
number |
undefined |
Maximum nesting depth |
minDepth |
number |
undefined |
Minimum nesting depth |
tagOnlyRoots |
boolean |
false |
Only tag root elements |
onTransform |
(stats) => void |
undefined |
Per-file callback |
onComplete |
(stats) => void |
undefined |
Completion callback |
exportStats |
string |
undefined |
Export stats to file |
includeSourceMapHints |
boolean |
false |
Add a data-dev-sourcemap attribute (requires path) |
debug |
boolean |
false |
Enable debug logging |
See complete TypeScript types:
import { type TagOptions } from 'vite-plugin-component-debugger'
Examples include: E2E testing setups, debug overlays, monorepo configs, feature flags, performance monitoring, and more!
Find components in the DOM:
// In browser console
document.querySelectorAll('[data-dev-component="Button"]');
console.log("Button locations:", [...$$('[data-dev-path*="Button"]')]);Stable selectors for tests:
// Cypress
cy.get('[data-dev-component="SubmitButton"]').click();
cy.get('[data-dev-path*="LoginForm"]').should("be.visible");
// Playwright
await page.click('[data-dev-component="SubmitButton"]');
await expect(page.locator('[data-dev-path*="LoginForm"]')).toBeVisible();Build custom debugging overlays:
// Show component boundaries on hover
document.addEventListener("mouseover", (e) => {
const target = e.target;
if (target.dataset?.devComponent) {
target.style.outline = "2px solid red";
console.log(`Component: ${target.dataset.devComponent}`);
console.log(`Location: ${target.dataset.devPath}:${target.dataset.devLine}`);
}
});Track component render activity:
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList") {
mutation.addedNodes.forEach((node) => {
if (node.dataset?.devId) {
console.log(`Component rendered: ${node.dataset.devId}`);
}
});
}
});
});
observer.observe(document.body, { childList: true, subtree: true });// Different configs per environment
const isDev = process.env.NODE_ENV === "development";
const isStaging = process.env.NODE_ENV === "staging";
export default defineConfig({
plugins: [
componentDebugger({
enabled: isDev || isStaging,
attributePrefix: isStaging ? "data-staging" : "data-dev",
includeProps: isDev, // Enable metadata in development
includeContent: isDev, // Enable content capture in development
}),
react(),
],
});Automatically excludes Three.js elements:
// Default exclusions
componentDebugger({
customExcludes: new Set([
"mesh",
"group",
"scene",
"camera",
"ambientLight",
"directionalLight",
"pointLight",
"boxGeometry",
"sphereGeometry",
"planeGeometry",
"meshBasicMaterial",
"meshStandardMaterial",
// ... and many more
]),
});
// To include Three.js elements
componentDebugger({
customExcludes: new Set(), // Empty set = tag everything
});Full type definitions included:
import componentDebugger, { type TagOptions } from "vite-plugin-component-debugger";
const config: TagOptions = {
enabled: true,
attributePrefix: "data-track",
};
export default defineConfig({
plugins: [componentDebugger(config), react()],
});Component Debugger Statistics:
Total files scanned: 45
Files processed: 32
Elements tagged: 287
How it keeps out of the way:
- Glob patterns are compiled once at startup rather than on every file
- Metadata is serialized once per element instead of repeatedly
node_modulesis skipped before any parsing happens- Files outside
includePathsare rejected before the parser runs
v2.2.0 introduced these as optimizations over v2.1. The original release notes quoted specific
speedups; those numbers are not reproduced here because there is no benchmark in the repo to
back them up. If build time matters to you, measure it with onTransform on your own project.
⚠️ Line numbers are wrong/offset by ~19? (Most common issue)
Problem: data-dev-line shows numbers ~19 higher than expected
Cause: Plugin order is wrong - React plugin adds ~19 lines of imports/HMR setup
Fix: Move componentDebugger() BEFORE react() in Vite config
// ❌ WRONG - Line numbers will be offset
export default defineConfig({
plugins: [
react(), // Transforms code first, adds ~19 lines
componentDebugger(), // Gets wrong line numbers
],
});
// ✅ CORRECT - Accurate line numbers
export default defineConfig({
plugins: [
componentDebugger(), // Processes original source first
react(), // Transforms after tagging
],
});Elements not being tagged?
- Check file extension: File must match
extensions(default:.jsx,.tsx) - Check exclusions: Element not in
excludeElementsorcustomExcludes - Check paths: File not excluded by
excludePathspattern - Check plugin order:
componentDebugger()beforereact() - Check enabled: Plugin is enabled (
enabled: true) - Check shouldTag: If using
shouldTag, callback must returntrue
Debug with:
componentDebugger({
debug: true, // Shows what's being processed
enabled: true,
});Build performance issues?
Quick fixes:
- Use
includeAttributesto reduce DOM size:includeAttributes: ["id", "name"]; // Only essential attributes
- Filter paths to only process needed directories:
includePaths: ['src/components/**'], excludePaths: ['**/*.test.tsx', '**/*.stories.tsx']
- Use
maxDepthto limit deep nesting:maxDepth: 5; // Only tag up to 5 levels deep
- Skip test files with
excludePaths
Attributes appearing in production?
componentDebugger({
enabled: process.env.NODE_ENV !== "production",
});Or use environment-specific configs:
enabled: isDev || isStaging, // Not in productionincludeAttributes vs excludeAttributes priority?
Gotcha: When both are set, includeAttributes takes priority
componentDebugger({
includeAttributes: ["id", "name", "line"],
excludeAttributes: ["name"], // ⚠️ This is IGNORED
});
// Result: Only id, name, line are includedBest practice: Use one or the other, not both
TypeScript type errors?
Import types for full IntelliSense:
import componentDebugger, {
type TagOptions,
type ComponentInfo,
type AttributeName,
} from "vite-plugin-component-debugger";
const config: TagOptions = {
// Full type checking
};Every commit to main triggers an automatic release:
Commit Message → Version Bump:
BREAKING CHANGE:ormajor:→ Major (1.0.0 → 2.0.0)feat:orfeature:orminor:→ Minor (1.0.0 → 1.1.0)- Everything else → Patch (1.0.0 → 1.0.1)
Example commit messages:
# Major version (breaking changes)
git commit -m "BREAKING CHANGE: removed deprecated API"
git commit -m "major: complete rewrite of plugin interface"
# Minor version (new features)
git commit -m "feat: add TypeScript 5.0 support"
git commit -m "feature: new configuration option for props"
git commit -m "minor: add custom exclude patterns"
# Patch version (bug fixes, docs, chores)
git commit -m "fix: resolve memory leak in transformer"
git commit -m "docs: update README examples"
git commit -m "chore: update dependencies"
# Skip release
git commit -m "docs: fix typo [skip ci]"What happens automatically:
- Tests run, package builds
- Version bump based on commit message
- GitHub release created with changelog
- Package published to npm
Setup auto-publishing:
- Get NPM token:
npm token create --type=automation - Add to GitHub repo: Settings → Secrets →
NPM_TOKEN - Commit to
mainbranch to trigger first release
- Fork and clone
pnpm install- Make changes and add tests
pnpm run check(lint + test + build)- Commit with semantic message (see above)
- Open PR
See .github/COMMIT_CONVENTION.md for examples.
git clone https://github.com/yourusername/vite-plugin-component-debugger.git
cd vite-plugin-component-debugger
pnpm install
pnpm run test # Run tests
pnpm run build # Build package
pnpm run check # Full validationTonye Brown - Builder, Front-end developer, designer, and performance optimization expert crafting immersive web experiences. Also a Music Producer and Artist.
Connect:
Support This Project:
- Star this repository
- Buy me a coffee
- Sponsor on GitHub
- Report issues or suggest features
- Contribute code via pull requests
- Share with other developers
MIT © Tonye Brown
Made with ❤️ by Tonye Brown
Inspired by lovable-tagger, enhanced for the Vite ecosystem.
Star this repo if it helped you!
