@mars-tech/vite-debug-plugin
| Install | |
|---|---|
npm install @mars-tech/vite-debug-plugin |
|
| Latest Version: | 1.0.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 23, 2026 |
| Links: | GitLab · npm |
@mars-tech/vite-debug-plugin
🇫🇷 Version française : README-fr.md
Upgrading from 0.x? See MIGRATION.md.
Vite plugin that exposes a global $debug function during development. Each call renders simultaneously in the browser console and the Vite dev server terminal, with consistent formatting and colors per severity. All $debug calls are stripped from production builds, including their argument expressions.

Works across any Vite-powered framework. Verified end to end on Vue 3.5, React, Angular (via Analog) and Nuxt 4; SvelteKit, Astro and plain Vite SSR use the same Vite hooks and are expected to work, but are not part of the test matrix. Both client and server-rendered (SSR) code paths are supported.
Features
- Dual output — every call renders in the browser console (styled) and the Vite terminal (ANSI), with per-level colors, tags, and source location.
- Live browser inspection — objects, class instances, DOM nodes (click to Reveal in Elements), and
Errorstacks stay interactive in the console, just likeconsole.log. See Live object inspection. - Vue DevTools — in Vue/Nuxt apps, every
$debugalso appears in a dedicated MARS-TECH timeline layer + inspector inside Vue DevTools, each entry carrying its payload and original source location. See Vue DevTools. - Zero production cost — all
$debug(...)calls (including method and chained forms) are rewritten tovoid 0at build and tree-shaken away; argument expressions never run. - SSR-ready — the global is installed on the server too, so router guards, store actions, and page setup can log during SSR.
- Namespaces + runtime filtering —
$debug.ns('auth')scoped loggers; toggle what shows at runtime viaenable/disable/setLevel, a?mt-debug=URL param /localStorage, or theMT_DEBUGenv — no restart. See Runtime filtering & namespaces. - Secret redaction — mask tokens, emails, and custom patterns in output. See Redacting secrets.
- Utility helpers —
assert,time/timeEnd,count,group/groupEnd,table,dir. See Utility helpers. - Library authoring —
library.debugBuildships a dual variant so libraries log in dev and strip in build. See Library authoring. - First-class TS + ESLint — shipped
/clientglobal types (all module-resolution modes) and a/eslintflat config that declares the$debugglobal.
Contents
- Features
- Quickstart
- Framework recipes
- The
$debugAPI - Where output appears
- Configuration reference
- Testing (Vitest / Jest)
- Library authoring with
library.debugBuild - Production builds
- Troubleshooting
- Project
Quickstart
Requirements
| Stack | Versions tested |
|---|---|
| Vite | 6.x · 7.x · 8.x |
| Node.js | 20.19 · 22.12 · 24 (24 recommended; 20 is past end of life) |
| Vue | 3.5+ (Composition + Options API, Pinia, Vue Router) |
| React | 18+ (Redux Toolkit, React Router) |
| Angular | 20+ via Analog (NgRx, Angular Router) |
| Nuxt | 4.x (Pinia, route middleware) |
| SSR engines | Vite SSR, Analog, Nitro/Nuxt, vite-node |
See Framework recipes below for call-site snippets per stack.
1. Install and register the plugin
npm install -D @mars-tech/vite-debug-plugin
Register it in vite.config.ts:
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import mtDebug from "@mars-tech/vite-debug-plugin";
export default defineConfig({
plugins: [vue(), mtDebug()],
});
The default export is the plugin factory — works identically with import (ESM) and require() (CommonJS). It takes no arguments by default; every knob is in Plugin options.
Register
mtDebug()unconditionally, in the top-levelpluginsarray. It is not only dev tooling: duringvite buildit is what strips the$debugcalls. Do not gate it on the command or mode (command === "serve" && mtDebug()) and do not setapply: "serve"— the build would then ship every call live, and each one throws in production. To turn the dev runtime off, useenabled: false, which still strips. Underbuild.rollupOptions.plugins, or in a Rollup or Rolldown config, the plugin only strips.
2. Enable the $debug global type
Add a triple-slash reference where your project's ambient types live (commonly src/vite-env.d.ts):
/// <reference types="@mars-tech/vite-debug-plugin/client" />
Module resolution must read the package
exportsmap (moduleResolution: "bundler","node16", or"nodenext"). On legacy"node10"/"classic", the package also ships a root-levelclient.d.tsthat resolves by path — no change needed.
3. Tell ESLint about the global
TypeScript now knows $debug, but ESLint's no-undef rule doesn't read TS types — it will report '$debug' is not defined. Spread the shipped flat config to declare the global:
// eslint.config.js (flat config, ESLint 9 & 10)
import mtDebug from "@mars-tech/vite-debug-plugin/eslint";
export default [
mtDebug, // declares $debug as a readonly global
// ...your other configs
];
The /eslint export bundles no eslint dependency, so it never pins your version — the same import works on ESLint 9 and 10.
| Your setup | Import |
|---|---|
| Flat config (ESLint 9 & 10) | the default export, spread as above |
Custom globalName |
mtDebugFlatConfig({ globalName: "$log" }) |
.eslintrc (ESLint ≤9 only) |
{ globals: mtDebugGlobals() } — the eslintrc system was removed in v10 |
TypeScript users can instead disable
no-undeffor.tsfiles —tscalready catches undefined names.
4. Call $debug in dev code
$debug("warning", new Error('Invalid variant "danger"'), {
tag: "PropsValidator",
});
Output (both browser console and Vite terminal):
MARS-TECH Warning [PropsValidator] - Invalid variant "danger"
Framework recipes
$debug is a global. The call is identical in every framework — there is no
adapter, no import, no per-framework API. The only thing that differs is where you
register the plugin:
| Stack | Where mtDebug() goes |
|---|---|
| Vue / React / Svelte | plugins: [] in vite.config.ts |
| Nuxt 4 | vite: { plugins: [] } in nuxt.config.ts (see below) |
| Angular via Analog | plugins: [] in vite.config.ts, alongside analog() |
| SvelteKit | plugins: [] in vite.config.ts, alongside sveltekit() |
// nuxt.config.ts — the one config that is not vite.config.ts
import mtDebug from "@mars-tech/vite-debug-plugin";
export default defineNuxtConfig({
vite: { plugins: [mtDebug()] },
});
An idiomatic call site
Anywhere a framework gives you a hook with no good place to console.log, $debug
fits — here a Vue props validator, which runs before the component renders:
<script setup lang="ts">
defineProps({
variant: {
type: String,
required: true,
validator(value: string) {
const ok = ["primary", "secondary", "ghost"].includes(value);
if (!ok) {
$debug("warning", new Error(`Invalid variant "${value}"`), {
tag: "PropsValidator",
showSource: true,
});
}
return ok;
},
},
});
</script>
Store actions, route guards, reducers and middleware all take the same call — see Namespaces and runtime filtering for keeping them separable once you have a lot of them.
The $debug API
Call signature and payload rendering
$debug<T>(
level: 'error' | 'warning' | 'info' | 'debug' | 'success' | 'trace',
payload: string | Error | T,
options?: {
showStack?: boolean
showSource?: boolean
tag?: string
prefix?: string // override the rendered prefix for this call
inspect?: boolean // force/suppress the live browser value for this call
}
): void
// also variadic — log several values, or a printf format string:
$debug('info', label, user, el) // space-joined + each stays live
$debug('info', '%s scored %d', name, score) // printf substitution
The table below is the one-line summary (used for the dev terminal and as the console label). In the browser, the raw value is also handed to the console so it stays live and inspectable — see Live object inspection.
| Payload type | Summary |
|---|---|
string |
as-is |
number / boolean |
String(value) — 42, true |
null / undefined |
the literal words null / undefined |
function |
name [Function] (anonymous [Function] when unnamed) |
symbol |
Symbol(description) |
Error |
error.message (stack appended when showStack: true; both redacted) |
| object / array | JSON.stringify with circular guard, truncated at 500 chars (redacted first — one exception) |
| class instance | ClassName { … } |
Map / Set |
Map(n) / Set(n) with entries |
RegExp |
/pattern/flags |
Date |
ISO-8601 string |
bigint |
123n |
| Vue ref | .value unwrapped, then standard rules apply |
| DOM Node | <tag#id.class> summary |
Promise |
[Promise] (not awaited) |
Multiple values & printf
Pass more than one payload and they render like console.log(a, b, c) — summaries
space-joined on one line, and in the browser each non-primitive stays live and
inspectable:
$debug("info", "user", user, el); // "user { … } <div#app>" + live user + live node
Or lead with a printf-style format string and its substitution args. Unlike native
console substitution, this is done by the plugin, so it works in the browser
and the terminal:
$debug("info", "%s scored %d", name, score); // "Ann scored 42"
Supported specifiers: %s %o %O %j (stringify), %d %i (integer), %f
(float), %c (consumed), %% (literal %). Leftover args are space-joined; a
missing arg leaves its specifier literal. A trailing MTCallOptions-shaped object
is still treated as options, so $debug("info", value, { tag: "x" }) keeps working.
Typos in that trailing object are not type errors — on the variadic form any object is a valid payload, so TypeScript cannot flag
{ tagg: "x" }. At run time an object counts as options only when every key is a recognised option; anything else is logged as a payload, which puts the typo in front of you instead of silently applying nothing. Use the two-argument form when you want autocomplete onoptions.
Utility helpers
$debug carries console-style helpers. Every one is stripped from production builds along with the base calls — including method ($debug.time(...)) and chained ($debug.ns('x')('info')) forms.
$debug.assert(user.id != null, "user must have an id", { tag: "auth" });
// logs at "error" level ONLY when the condition is falsy
$debug.time("render");
renderExpensiveThing();
$debug.timeEnd("render"); // → MARS-TECH Debug [render] - render: 12.4ms
$debug.count("api-call"); // → api-call: 1
$debug.count("api-call"); // → api-call: 2
$debug.group("checkout"); // real collapsible group in the browser
$debug("info", "validating cart"); // nested under the group
$debug("info", "charging card");
$debug.groupEnd();
$debug.table(rows, ["name", "total"]); // console.table in the browser
$debug.dir(document.body); // console.dir deep-object inspection
| Helper | Behavior |
|---|---|
assert(condition, payload, opts?) |
Logs payload at error level only when condition is falsy. |
time(label?) / timeEnd(label?) |
Logs elapsed ms between the two calls at debug level. Label defaults to default. |
count(label?) |
Increments and logs a named counter at debug level. |
group(label?) / groupCollapsed(label?) / groupEnd() |
Opens a real (optionally collapsed) console group in the browser; indents the dev-terminal output. The label is redacted like a message. |
table(data, columns?) |
console.table in the browser; a text summary is sent to the terminal. Under redaction the browser prints the redacted summary instead. |
dir(item, options?) |
console.dir deep-object inspection in the browser; a text summary is sent to the terminal. Under redaction the browser prints the redacted summary instead. |
Every helper follows the runtime switches — disable(), setLevel, the prefix filter. For time/timeEnd and count the label doubles as the namespace, so a targeted filter such as -render mutes a time("render") pair; group/groupCollapsed, table and dir carry no namespace, so they answer to the global switches only (a -cart filter leaves them alone, disable() silences them). A suppressed group keeps its groupEnd paired, so nesting never drifts. Timing uses performance.now() when available.
count and time remember up to 1,000 labels each. Past that the least recently used label is forgotten — its count starts again at 1 and its timeEnd prints nothing — so a label built from data (count(item.id)) cannot grow memory for the life of the page. The group indent stops growing at 32 levels; groupEnd still closes every level.
Every helper above exists on every $debug — browser, SSR, the Vite process, the Vitest no-op stub, test: 'log', and the shims prepended to a brandPrefix build — so a call that works in a component cannot fail during server render or under test.
Namespaces and runtime filtering
Beyond the build-time levels / minLevel allowlist, $debug carries runtime controls so you can change what shows without editing config or restarting — and scope logs by namespace.
const auth = $debug.ns("auth"); // scoped logger, tags every call [auth]
auth("info", "login ok"); // → MARS-TECH Info [auth] - login ok
auth("debug", payload, { tag: "token" }); // namespace becomes "auth:token"
$debug.enable("auth:*,-auth:token"); // show auth:* except auth:token
$debug.disable(); // mute everything
$debug.enable(null); // drop the runtime override → back to the plugin's `filter` default
$debug.setLevel("warning"); // raise the floor (within the build set)
$debug.setLevel(null); // clear the runtime level override
The namespace is the tag — $debug("info", "x", { tag: "auth" }) and $debug.ns("auth")("info", "x") filter identically. With no filter set, every call shows (unchanged behavior).
Filter syntax (à la the debug package): comma/space-separated terms, * wildcard, leading - to exclude (exclusions win).
Where the filter comes from, highest priority first:
| Surface | Namespace filter | Level floor |
|---|---|---|
| Browser | ?mt-debug=auth:* URL param → localStorage["mt-debug"] |
?mt-debug-level= → localStorage["mt-debug-level"] |
| SSR/Node | MT_DEBUG=auth:* env |
MT_DEBUG_LEVEL=warning env |
enable / disable / setLevel persist to localStorage in the browser (so the choice survives reloads); on the server they apply for the process lifetime. Runtime narrowing is bounded by the build-time level set — you can hide compiled levels, not resurrect stripped ones.
Prefix filtering
A second, orthogonal axis filters by the rendered prefix — the head of each line ([MARS-TECH]-style). With brandPrefix a prefix is a per-source identity, so this is how you mute a noisy library or isolate your app's own logs:
$debug.setPrefixFilter("MyApp"); // show only "MyApp"-prefixed logs
$debug.setPrefixFilter("-NoisyLib"); // hide everything from "NoisyLib"
$debug.setPrefixFilter(null); // clear → all prefixes pass
Same debug-style syntax as the namespace filter (*, comma/space-separated, leading - excludes). Sources, highest priority first:
| Surface | Prefix filter |
|---|---|
| Browser | ?mt-debug-prefix=MyApp URL param → localStorage["mt-debug-prefix"] |
| SSR/Node | MT_DEBUG_PREFIX=MyApp env |
| Build-time default | mtDebug({ prefixFilter: "-NoisyLib" }) plugin option |
Matched against each call's effective prefix (options.prefix if set, else the configured prefix). Under test: 'log', the test console logger honors it too. Caveat: utility helpers (time/count/group) always carry the configured prefix, so a library's helper output filters under the app prefix, not the library's.
Where output appears
Logged text is treated as untrusted. Any script running in the page during development
can send log frames to the dev server, and server code may log request data. Before text
reaches the terminal or the log file, its control characters — escape
sequences, a bare \r, C1 controls, bidi overrides, the line and paragraph separators
U+2028 / U+2029 — are shown escaped (\x1b]52;…, \x0d, \u202e, \u2028) instead of
being acted on; tabs and newlines in a message are kept. A string that already carries
ANSI colours (from chalk, say) prints its codes escaped too.
Fields forwarded from the browser are also bounded: the message at 10 000 characters,
tag and prefix at 256, the source file at 1 024. The message, tag and prefix are
redacted before they are cut, because a cut can split a secret so
that half of it escapes redaction. For the same reason a field too long to scan is replaced
whole rather than cut: a stack over 16 384 characters by [stack omitted: N chars], a
message over 65 536 (64 KiB) by [message omitted: N chars], a tag or prefix over
16 384 the same way. So is a field the browser would forward in more than 4 096 pieces: a
stack with thousands of masked matches, a message of thousands of %d numbers.
Over time, too. A page script can send log frames for as long as its tab is open, as
fast as the dev server takes them, over as many HMR connections as it opens. Each
connection may forward a burst of 10 000 lines, then 2 000 a second, and every connection
of the dev server together a burst of 20 000, then 4 000 a second: far more than an app
logs, even from a few tabs. A frame over either is dropped before the plugin reads any of
it; while a page floods, lines from your other tabs are dropped with its own. Where stdout
does not make Node wait — a pipe, on macOS — and is not read as fast as lines come
(vite | tee, a process manager), Node keeps the lines it has not written yet in memory:
past 4 MiB of them, a forwarded line is left out of the terminal, and still written to the
log file, which has a size limit of its own. Only the dev server's copy
is limited: the browser console prints every call, and lines from SSR and the Vite process
are not counted. When anything was dropped, one line per 10 s says how much:
[MARS-TECH] dropped 1240 browser log frames in the last 10 s: the page(s) sent more than 2000 a second on one connection, or 4000 in all.
Nothing is printed while nothing is dropped.
Live object inspection
The dev terminal can only show text, but the browser console can render live
values. So in the browser $debug passes the raw payload to console as a
trailing argument — you get the same interactivity as console.log(value):
- objects / arrays — expandable tree, Copy and Store as global variable
- class instances — constructor name, methods, prototype, getters
- DOM nodes (
document.querySelector(...)) — hover-highlight, click to Reveal in Elements;querySelectorAllNodeLists too Error— real name + collapsible stack with clickable framesMap/Set/Date/RegExp/ typed arrays — native rendering
The one-line styled summary is still printed as the label, and the dev terminal still receives the (redacted) string form.
Control it:
- Plugin option
output.inspect(defaulttrue) — master switch for the whole app. - Per call
{ inspect: true }forces the live value even whenoutput.inspectis off;{ inspect: false }suppresses it for that call. (The pre-1.0 name{ raw }still works, with a one-time console warning, until 2.0;inspectwins if both are set.) - Redaction interplay: when a
RegExpredaction entry orredactDefaultsis configured, no live value is appended — DOM nodes included, since a node'shref,data-*attributes and an input'svaluerender unmasked — so the redacted string form stands alone and secrets can't slip past redaction into the live object. Use{ inspect: true }to override when you know a value is safe. String entries never reach the browser, so they do not change this.
Vue DevTools
In Vue and Nuxt apps, $debug registers a Vue DevTools plugin so every call
also shows up inside Vue DevTools:
- a custom inspector (the always-on view) — a browsable list of recent calls, each expandable to its level, message, payload, source location, and namespace. Look for the MARS-TECH entry (with its logo) in the Inspector list.
- a MARS-TECH timeline layer — one event per call, each with its timestamp, level, source location and payload.
Both live in the plugin's own tab in the DevTools sidebar (the MARS-TECH logo), split into State (the inspector) and Timeline.

The timeline records only while armed. Open the MARS-TECH tab → Timeline → click the ● record button in the top-right corner. Until then DevTools drops every event and the view reads No events. The inspector needs no toggle.
Two things routinely make this look broken:
- The sidebar's own Timeline tab never shows it. That tab has a hardcoded layer list — Mouse, Keyboard, Component events, Performance. No plugin's layer appears there, ours included. Use the MARS-TECH tab.
- Recording resets every time DevTools opens. Vue DevTools registers plugins lazily and re-registering a layer forces its record flag back off, so the choice does not survive a reload. Re-arm it when you need it.
The About tab's view documentation link points at the documentation site.
It's wired through @vue/devtools-api,
declared as an optional peer dependency. The integration is included only when
that package resolves from the Vite root the way Vite resolves it — from a
node_modules folder at or above the root; a copy only NODE_PATH or a global folder
such as ~/.node_modules can reach does not count. Otherwise it is left out of the
runtime and the console/terminal output is unaffected (with output: { devtools: true }
written explicitly, the dev server says so each time it starts). Pinia or Vue Router
may already bring it in, but under a strict layout such as pnpm's a transitive copy
does not resolve from the root — install it explicitly to guarantee the panel, then
restart the dev server:
npm install -D @vue/devtools-api
Turn it off with the output: { devtools: false } plugin option. Non-Vue frameworks are
unaffected — they keep the full console feature set. (Note: this adds a DevTools
presence; a browser page can't programmatically focus the DevTools tab, so
there is no "click the console line to jump to the panel".)
$debug also exposes runtime controls (ns, enable, disable, setLevel, setPrefixFilter — see Runtime filtering & namespaces) and utility helpers (assert, time/timeEnd, count, group/groupEnd, table, dir).
SSR support
$debug is available inside server-rendered code — route guards, store reducers, page setup, server middleware. The plugin installs the global on both sides:
- Browser — runtime injected via
transformIndexHtml; logs toconsole.{error|warn|info|debug|log|trace}plus forwarded over the Vite WebSocket to the dev terminal. - Server — a Node runtime is auto-injected into every SSR-transformed module that references
$debug(via the virtual modulevirtual:mt-debug/ssr). It writes ANSI-colored output directly toprocess.stdout. As a fallback for same-process SSR setups, the plugin also installs$debugon the Vite dev server'sglobalThisfromconfigureServer.
In production builds (vite build, including SSR builds), every call site is stripped to void 0 regardless of side. No SSR runtime ships in the output.
Manual SSR opt-in
For worker-thread or separate-process SSR runners that don't share globalThis with Vite, import the virtual module from your server entry:
// e.g. src/main.server.ts (Analog) or a Nuxt server plugin
import "virtual:mt-debug/ssr";
The module is idempotent (if (!globalThis[GLOBAL_NAME])) and only loads in dev — strip removes any reference in production. It is served to the SSR pipeline only: imported from client code, it is an empty module. With string redact entries configured, a runner in a process of its own prints nothing: see Where string entries cannot reach.
NDJSON log file
Security: do not use
output.filewhen your code logs secrets, even redacted ones. The log is a file on disk: any tool that reads files can read it, and the plugin's protections are a safety net, not a vault. Keep it for logs that hold no secrets.
For readers that are not a human at a terminal — an AI agent tailing what the app did, a test asserting on output — write every call to a file as well:
mtDebug({ output: { file: true } }); // .mt-debug/dev.ndjson under the Vite root
mtDebug({ output: { file: "logs/debug.ndjson" } }); // or a path, relative to the root or absolute
mtDebug({ output: { file: process.env.MT_LOG === "1" } }); // a toggle: false (or unset) is off
mtDebug({ output: { file: true, fileMaxBytes: 20 * 2 ** 20 } }); // stop at 20 MiB, not 100
One JSON object per line, typed as MTLogLine from @mars-tech/vite-debug-plugin/helpers:
{
"level": "info",
"message": "cart loaded",
"tag": "cart",
"source": { "file": "src/Cart.vue", "line": 12, "column": 3 },
"prefix": "MARS-TECH",
"t": "2026-09-15T10:00:00.000Z",
"seq": 0,
"origin": "browser"
}
- One session per file — truncated when the dev server starts, closed when it stops. A
restart (an edit to
vite.configor.env) starts a new session; the server-side lines keep arriving after it. - At most 100 MiB a session, that last line included. Any script on the page can forge
log lines as fast as the dev server takes them — one tab wrote 3 GB in 20 s. The first
line that does not fit beside the room kept for a last one is replaced by that last line,
at level
warningandorigin: "server", whose message says the limit was reached; that server writes nothing more to the file until it restarts, and the terminal gets one warning from it. The limit is measured on the file, not per writer: an old and a new server across a restart, or two dev servers on one config, share it, and in one process the file never goes past it (two processes on one file can each pass it by the line they were writing). A second writer can still add the lines that fit after the first one's last line, then a last line of its own, with a warning of its own.output.fileMaxBytessets another limit, in bytes, andInfinitylifts it;null, like leaving it unset, keeps the default, and any other value that is not a number over 0 warns and keeps it. - Not opened under Vitest. A test run builds its server from the same
vite.config, and used to truncate the log of avite devrunning beside it; withprocess.env.VITESTset the file is never opened. That includes a dev server a test starts itself —createServer()in a test, or one@nuxt/test-utilsspawns — since it inherits the variable: to assert on its file, deleteprocess.env.VITESTbefore it starts. Another tool that starts a dev server from the same config withoutVITEST— Storybook's Vite builder, say — still starts a new session in that file: switch it off there with a toggle (file: process.env.MT_LOG === "1"). Writes always append, so two writers never corrupt the file — they only interleave, each with its own run ofseq. - Written after redaction — exactly what the terminal would print. The file is not a
way around
redact. - Kept from the page — a safety net, not a vault. Any script on the page can forge log
lines, and reading the file back would tell it which of its guesses your string
redactentries masked — an entry falls out one character at a time. So the plugin keeps the file from the page: it adds it toserver.fs.deny, answers any HTTP request that reaches it with an empty 403 — by spelling and by device and inode, so case, the Unicode folding macOS applies, links,resolve.alias,baseandpublicDirlead nowhere — and refuses it inresolveIdandload, ahead of Vite, in every environment, which covers the routes over the HMR WebSocket (fetchModule,ssrLoadModule, vite-plugin-inspect's RPC). The log is not opened at all, with one warning, where Vite or a tool would hand it out anyway: underserver.fs.strict: false, inpublicDir, next to a dev tool that reads any file for the page (vite-plugin-vue-devtools, Nuxt DevTools, Vite DevTools, devframes, TanStack Devtools), and next to@tailwindcss/viteunless the log sits in a directory of its own whose.gitignoreholds*— the plugin writes one into.mt-debug/(Tailwind's scan turns the files it reads into CSS the page fetches).@tailwindcss/postcssis not detected: give such a directory that.gitignoreyourself. More generally, any tool that reads files from disk — Tailwind with an@sourcethat reaches the log, a dev tool, an editor extension — can read the log too, wherever it is: that is beyond the plugin's reach. A log an earlier session left stays on disk: delete it before you turn such a tool on. Nuxt servespublic/through Nitro, outside Vite: keep the log out of Nuxt'spublic/. Vite reaches a file under the project in many ways; the plugin refuses every one known (the full list, and how each is checked, is in SECURITY.md), but a script running in your dev page may still find another. If your code logs secrets, even redacted, do not turnoutput.fileon (see the warning above). A log you keep anyway is safer outside the project — outsideserver.fs.allow, which in a monorepo is the workspace root: Vite refuses every file there. And never put production keys in a dev setup. Run Vite 6.4.2, 7.3.2, 8.0.5 or later: older releases hand a page script any file, your.envincluded. A dev tool that reads any file for the page reads your.envand yourvite.configthe same way — a third-party problem; the plugin can only decline to add its log to it. - Browser calls arrive over the HMR channel, even with
output.terminal: false. SSR calls are captured when the SSR runtime executes inside the Vite process (Vite SSR, vite-node); a separate SSR worker has its own process and is not seen. origintells them apart:"browser"or"server".seqorders lines within the session;tis the write time.sourceis the location the terminal shows in(file:line:col): the file relative to the Vite root, with/, and a line and column counted from 1 — for browser and server lines alike, whether the transform stamped the call or the runtime read its stack. A script from another origin keeps its URL, and a file on another Windows drive stays absolute. A browsergroup()label,table()ordir()line has nosource.- An unopenable path, or a value other than
true,false,nullor a path (0,{},""), warns and leaves the file off for the session — it never failsvite dev. - Fields are untrusted text from page code or request data. Control characters arrive
already escaped, and the file stores DEL, C1, bidi and
line-separator characters as JSON
\uescapes (the parsed values are unchanged). Still escape a field before printing it to a terminal, and never treat it as instructions. - The plugin writes
.mt-debug/.gitignore(*), so.mt-debug/stays out of git without a word from you; a log in a directory of your own needs a.gitignoreof yours. A file left by an earlier session is protected only whileoutput.filenames it — withenabled: false, and on a duplicate instance that stood down, too; delete it when you turn the option off.
Configuration reference
Plugin options
Every option, with its default — mtDebug() with no arguments is the same thing:
mtDebug({
// levels and minLevel are alternatives; setting both warns and keeps levels
minLevel: "trace", // or: levels: ["error", "warning"]
output: {
console: true,
terminal: true,
inspect: true,
devtools: true, // written out, it warns when @vue/devtools-api is missing
file: false,
fileMaxBytes: 100 * 2 ** 20, // 100 MiB
},
showSource: true,
prefix: "MARS-TECH",
colors: {
warning: { label: "Warning" },
},
globalName: "$debug",
prefixFilter: "-NoisyLib",
library: { debugBuild: false, brandPrefix: false },
redact: [/x-api-key: \S+/, /sk_live_[A-Za-z0-9]+/],
redactDefaults: false,
test: "stub",
});
| Option | Type | Default | Notes |
|---|---|---|---|
enabled |
boolean |
command === 'serve' |
Master switch, keyed off the Vite command (the dev runtime runs in vite dev; in vite build the plugin strips the calls instead) — not mode, so vite build --mode development stays inert. false turns the plugin off: no runtime served or injected, no DevTools pre-bundle, and test: 'log' falls back to the no-op stub; with output.file set, vite dev still refuses the log path to a page (a log an earlier session left). $debug calls are still stripped from your source, so disabled dev code cannot throw. true does not force the dev runtime into a production build; use library.debugBuild for that. Passing true explicitly changes nothing. |
levels |
MTLevel[] |
all levels | Allowlist. When both are set a non-empty levels wins and minLevel is ignored (with a startup warning); an empty levels: [] defers to minLevel. |
minLevel |
MTLevel |
unset | Threshold (trace < debug < info < success < warning < error). |
output |
{ console?, terminal?, inspect?, devtools?, file?, fileMaxBytes? } |
true but file |
Where output appears. console — the browser console; terminal — the Vite terminal (browser calls forwarded, SSR / Vite-process calls printed; false silences it everywhere, $debug stays defined); inspect — browser: also hand the raw payload to the console as a live, inspectable argument (per-call { inspect } overrides); devtools — Vue/Nuxt: surface each call in Vue DevTools (needs the optional peer @vue/devtools-api). With console, terminal and devtools all off and no file, a no-op $debug is served instead of the runtime. (Replaces the flat browser → console, terminal, inspect, devtools — all still work, with a warning, until 2.0.) file — also write every call as one NDJSON line (MTLogLine) to a file (not when your code logs secrets, even redacted: see NDJSON log file): true → .mt-debug/dev.ndjson under the Vite root, or a path; one session per file, written after redaction, browser calls included even with terminal: false (see NDJSON log file). fileMaxBytes — the most that file holds in one session, in bytes: 100 MiB by default, Infinity for no limit. See Where output appears. |
showSource |
boolean |
true |
Show each call's file:line:col — the file relative to the Vite root, line and column counted from 1, the same on every surface; the per-call showSource option overrides it call by call. (Was showSourceLocation before 1.0.0 — the old name still works, with a warning, until 2.0.) For a call the dev transform stamps, the location is the original source — captured at transform time from the combined source map (works through .vue SFC compilation, on both client and SSR — Nuxt included). A call it cannot stamp falls back to the runtime stack: see a few lines off. |
prefix |
string |
'MARS-TECH' |
Output prefix. |
colors |
MTColorOverrides |
unset | Per-level color overrides; missing keys fall back to defaults. |
globalName |
string |
'$debug' |
Identifier attached to globalThis. Must be a valid JavaScript identifier that is not a reserved word ($debug, myDebug, 日志); the plugin throws at startup otherwise. |
prefixFilter |
string |
unset | Build-time default prefix filter — mute/isolate logs by rendered prefix (debug-style syntax). Overridden by ?mt-debug-prefix= / localStorage / MT_DEBUG_PREFIX / $debug.setPrefixFilter(...). See Prefix filtering. |
filter |
string |
unset | Build-time default namespace filter, same debug-style syntax as $debug.enable() (cart*, -noisy, auth,cart:*). Overridden by ?mt-debug= / localStorage in the browser, MT_DEBUG on the server, and $debug.enable(pattern) at runtime; $debug.enable(null) returns to it. See Namespaces and runtime filtering. |
showStack |
boolean |
false |
Default for the per-call showStack: attach the (redacted) Error stack to every call that carries an Error. { showStack: false } still opts a single call out. |
library |
{ debugBuild: boolean; brandPrefix?: boolean } |
— | Library authoring. debugBuild: true makes vite build emit a dual output (dist/ stripped + dist/debug/ with calls kept, self-installing fallback prepended). brandPrefix: true additionally stamps this library's prefix onto its $debug calls so they stay branded inside a consuming app. (Replaces allowDebugBuild / libraryPrefix — both still work, with a warning, until 2.0.) See Library authoring. |
redact |
(string | RegExp)[] |
[] |
Mask matches ([redacted], the whole match) in each string a call hands over — payloads, Error stacks, namespaces / tags, per-call prefixes, helper labels, source paths — and in the printed line. A RegExp applies on every surface — browser console, terminal, log file, Vue DevTools, test: 'log' — and is sent to the page as written: describe a secret's shape, never the secret. A string — or a number — matches literally, as written, on the server side only — terminal, log file, SSR output, test: 'log' outside Vitest browser mode — and is never sent to the browser; an empty or unset one is ignored. A RegExp is applied globally; keep text in front of a secret with a lookbehind. See Redacting secrets. |
redactDefaults |
boolean |
false |
Also apply a curated secret set (JWTs, Bearer tokens, Authorization: Basic credentials, emails, Stripe-style keys, secret JSON values — as properties or [key, value] pairs). See Redacting secrets. |
test |
false | 'stub' | 'log' |
'stub' |
Under Vitest, what the plugin registers as a first setupFiles entry: 'stub' installs a no-op $debug (default global only); 'log' installs a real, config-aware console logger so component and test-file $debug(...) print with your configured prefix / globalName / levels and redaction; false registers nothing. (Replaces testStub / testLog — both still work, with a warning, until 2.0.) See Testing. |
Color override merge
User-supplied colors deep-merges per level:
mtDebug({
colors: {
error: { label: "Error123" },
},
});
Resolves to:
{
error: { ansi: red, css: '#ef4444', label: 'Error123' },
// other levels untouched
}
ansiapplies on every terminal surface — with one requirement. Server-side output (SSR renders,$debugcalled fromvite.config.tsor another plugin) comes from a generated runtime that cannot import your function. So the plugin calls each overriddenansionce, at config time, on a probe string, captures the escape codes it wraps around it, and ships those codes to the runtime. Any plain colour
Related Packages
jump to local IDE source code while click the element of browser automatically.
Vue Inspector with enhanced features - supports end line and clipboard copy
jump to local IDE source code while click the element of browser automatically.
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 1.0.0 | ^3.5.0 | MIT | |
| 0.6.1 | MIT | ||
| 0.6.0 | MIT | ||
| 0.4.0 | MIT | ||
| 0.3.2 | MIT | ||
| 0.3.1 | MIT | ||
| 0.3.0 | MIT | ||
| 0.2.0 | MIT | ||
| 0.1.0 | MIT |
Pre-releases (1)
| Version | Released | Vue | License |
|---|---|---|---|
| 1.0.0-rc.0 | ^3.5.0 | MIT |