Library authoring
If you publish a Vite library that uses $debug internally, you want the published bundle to ship two parallel variants, so consumers get debug logs in development without paying for them in production. The plugin builds both variants in a single build when you set library: { debugBuild: true }:
// B's vite.config.ts (the library)
import mtDebug from "@mars-tech/vite-debug-plugin";
export default defineConfig({
plugins: [mtDebug({ library: { debugBuild: true } })],
build: {
lib: { entry: "src/index.ts", formats: ["es", "cjs"] },
rollupOptions: { external: ["vue", "pinia"] },
sourcemap: true,
},
});The library option replaces allowDebugBuild and libraryPrefix from 0.6. Both still work, with a warning, until 2.0: see Renamed options and what changed for library authors.
Build output
After vite build the library's dist/ contains:
dist/
├── index.js ← standard variant, every $debug call stripped to `void 0`
├── index.cjs
├── index.d.ts ← single shared type file
├── index.js.map
├── index.cjs.map
└── debug/
├── index.js ← debug variant, $debug calls preserved + fallback shim prepended
└── index.cjsWhy the debug tree has no source maps
The debug/ tree carries no source maps: the only map that could be attached would describe the stripped chunk, and for a development-only artifact that you read directly, no map is more useful than a wrong one. The tree is cloned from the chunk at the plugin's last renderChunk step, so whatever Vite does to the chunk before that is in it: the build.target transpile, and minification by esbuild (the default on Vite 6 and 7) or terser. Vite 8's default minifier runs later and leaves it unminified.
Development code only
The debug/ tree is un-stripped development code: every $debug call is kept as written, with its payload expressions. It exists only to be resolved by your consumers' dev servers through the development export condition (below). If library.debugBuild is set on an application, dist/debug/ sits next to the real assets: keep it out of what you deploy.
Web workers and the scripts Vite copies instead of bundling are stripped in both variants: dist/debug/ keeps the calls of your main code, not theirs.
Wire the conditional exports
Declare conditional exports in the library's package.json so Vite picks the right variant automatically:
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": {
"import": "./dist/debug/index.js",
"require": "./dist/debug/index.cjs"
},
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}If you enable library.debugBuild but your package.json exports has no development condition pointing at the debug/ variant, the plugin prints a build warning, once per build, with the exact snippet to paste. Without that condition, the debug variant is emitted but never resolved, and consumers get no dev logs.
How each side resolves
Three tiers are involved: A is this plugin, B your library, C the app that consumes it.
| Tier | Stage | What happens |
|---|---|---|
| A plugin (this package) | — | Provides the runtime and the dual-build transform. |
| B library (e.g. a Pinia plugin) | vite build | library: { debugBuild: true } → dist/index.* stripped, dist/debug/index.* keeps the calls plus an automatically prepended fallback shim. |
| C end-user app with A | vite (dev) | Vite picks the development condition → debug variant resolved → A's browser runtime defines the real $debug first → the fallback shim leaves it alone → logs appear. |
| C end-user app with A | vite build | Vite picks the default condition → standard variant resolved → zero $debug references in C's bundle, whatever C does in its own source. |
| C end-user app without A | vite (dev) | Debug variant resolved → the fallback shim installs a no-op $debug and emits a one-time console.warn advising to install A. |
| C end-user app without A | vite build | Standard variant resolved → already stripped → silent, no crash. |
With library.debugBuild, consumers do not need to know anything about this plugin. Library authors do not write a fallback shim by hand either: the plugin prepends it to every entry chunk of the debug variant.
Bare calls fail at runtime, not at build time
A library that ships bare $debug(...) calls does not break the consuming app's build: the build succeeds and the ReferenceError is thrown at runtime, in production, wherever that call sits. The fix belongs with the library: build it with library.debugBuild, which needs no configuration in the consuming app. If you do not control the library, ask its author, or stub the global yourself (see Testing for the stub's shape).
Optional calls and Vue templates
$debug?.(…) is stripped like any other call, whatever the build.target. In the debug/ variant it becomes a guarded call, typeof $debug === "function" ? $debug(…) : void 0, which still skips the call when no $debug is installed. globalThis?.$debug(…) is handled the same way.
In a Vue component library, a $debug call in a template (@click="$debug('info', …)") is handled like one in <script>: the standard variant has none of them, whichever minifier or build.target the build uses, and in the debug variant it calls the consuming app's global $debug (or the brandPrefix shadow). You need no app.config.globalProperties.$debug.
Redaction belongs to the consuming app
The debug/ variant carries none of your redact / redactDefaults entries: they mask your own vite dev only. In a consuming app, your library's $debug output goes through that app's plugin instance and is masked by its configuration, or not at all. This is deliberate: a string entry is usually the secret itself, and debug/ is published with your package. Never pass a secret to $debug in library code: log its presence, its length or a fingerprint instead.
Declare the plugin as an optional peer
In B's package.json, keep the plugin as an optional peer dependency, so consumers that do not want logs can still install your library:
{
"peerDependencies": {
"@mars-tech/vite-debug-plugin": ">=1.0.0"
},
"peerDependenciesMeta": {
"@mars-tech/vite-debug-plugin": { "optional": true }
}
}Branding library logs with brandPrefix
By default a library's $debug calls render with the consuming app's prefix: the app's plugin instance installs the global $debug, and the library's bare calls inherit whatever prefix it was configured with. A log from inside your library therefore shows up as [App] …, giving the developer no signal that it came from your library.
Set brandPrefix: true alongside debugBuild to keep your library's logs branded with its own prefix wherever it is used:
// library vite.config.ts
plugins: [
mtDebug({ library: { debugBuild: true, brandPrefix: true }, prefix: "foo" }),
];Every library log now renders with foo, whatever the consuming app's own prefix (here the app is configured with prefix: "bar"):
foo Info [cart/action] - cart.add(Widget ×2) ← from the library
bar Info - user clicked checkout (src/views/Checkout.vue:14:4) ← from the appHow it works: the debug variant gets a small module-local $debug shadow prepended to each chunk. It forwards to the app's real $debug, whose configuration (redaction included) applies as usual, but forces { prefix } as a last-write option, so the library's prefix cannot be overridden by the consumer or by a per-call option. Your library code stays plain $debug(...): nothing to remember at each call site, nothing consumers can turn off. If the consuming app has no $debug at all, the shadow warns once and does nothing (the same safety as the fallback shim).
A umd or iife debug build is meant for a <script> tag, where top-level declarations are shared by the whole page. There the shadow and the chunk are wrapped in a block, so the shadow stays local to your library: the page's own $debug calls keep the app's prefix, and two libraries branded this way load side by side. es and cjs output is module-scoped already.
The branded forms are the base call, $debug.ns(...) and $debug.assert(...). Utility helpers (time, count, group) take no per-call options, so their output still shows the consumer's prefix. brandPrefix has no effect without debugBuild, and the plugin warns when it is set that way.
Library logs carry no source location
The shadow defaults to showSource: false, so a branded line renders without the trailing (file:line:col). A position inside your library is not something the consuming developer can act on (they are not debugging your internals), and because the shadow wraps each call, the runtime's stack walk would have reported the shadow's own forwarding frame rather than your call site anyway. If you really want it, pass { showSource: true } explicitly at the call site; that still wins.
See also the library option and Troubleshooting.