Skip to content

Migrating from 0.6

Version 1.0.0 is a major release. This page takes a project from 0.6.1 to 1.0.0: first what can need action, each with a before/after snippet, then the changes in behaviour that need none but can surprise you. The plugin's migration guide on GitLab covers every edge case, and the changelog on GitLab lists every fix.

In the snippets, // 0.6.1 marks code written for 0.6.1 and // 1.0.0 its replacement. A text block shows what is printed, word for word. MARS-TECH there is the default prefix: yours shows instead.

In 30 seconds

  • Everyone: run Node.js 20.19+ or 22.12+ (24 recommended). Every renamed option still works until 2.0.0, with one warning per group of keys. Only stripInDeps stopped working.
  • If a tsconfig includes your vite.config.ts (as tsconfig.node.json does under vue-tsc -b), the old option names are type errors: rename them, as listed in Renamed options.
  • If you used stripInDeps, it now strips nothing: replace it, as described in stripInDeps.
  • If you import MTPlugin, MTFormatContext, InstallStubOptions or GlobalAccess, those types are gone: see Removed and renamed types.
  • If a test or a tool reads the log text, Map, Set, RegExp, Date, bigint and class instances render differently, and a last argument that is not an options object is now printed: see Changed output and Options in the last argument.
  • If you use redact, three changes can need action (see Redaction):
    • a string entry no longer masks the browser console: add a RegExp for the secret's shape;
    • a capture group no longer keeps its text (the 0.6.1 $1 convention): use a lookbehind;
    • with string entries, $debug prints nothing, with one warning, where it cannot read them: SSR in a process of its own or in workerd, Vitest 3's forks pools, and Vitest browser mode.

Redaction is a safety net, not a vault: it catches a secret logged by mistake. Never log one on purpose.

Upgrade checklist

  1. Check that node -v prints 20.19+ or 22.12+, locally and in every CI image.
  2. Install the new version: npm install -D @mars-tech/vite-debug-plugin@^1.0.0.
  3. Start vite dev once and read the lines that start with your prefix ([MARS-TECH] by default): each one says what to change. Reload any browser tab left open from before: a tab still running the 0.6.1 runtime forwards nothing to the terminal until it reloads.
  4. Replace stripInDeps, as described in stripInDeps.
  5. Update the type imports listed under Removed and renamed types.
  6. Check that globalName is a plain JavaScript identifier: mtDebug() now throws otherwise.
  7. Look for $debug calls that pass a variable as the last argument (see Options in the last argument).
  8. Update the tests and snapshots that assert on log text (see Changed output).
  9. Review redact: a RegExp beside each string entry the browser console must mask, a lookbehind for each capture group, and every RegExp linear-time (see Redaction).
  10. Vue or Nuxt: install @vue/devtools-api if you want the Vue DevTools panel.
  11. Run vite build and read its warnings: each new one points at a call that would throw in production (see Production builds).

Before you upgrade

Requirement0.6.11.0.0
Node.js (engines)>=20.19.0^20.19.0 || >=22.12.0, tested on 20.19, 22.12 and 24
Vite (peer dependency)6, 7 or 8unchanged
@vue/devtools-apinot usedoptional peer dependency, 6, 7 or 8
ESLint, for /eslint9 (flat config) and .eslintrc9 and 10 (flat config); .eslintrc on ESLint 9 and older
Safari, with redactDefaultsno minimum16.4 or newer: the default patterns use lookbehind
  • Node.js 21 and 22.0 to 22.11 are no longer supported. The old range admitted them, but Vite 7 and 8 refuse them; the new range is the one Vite 7 and 8 declare. Node.js 20.19+ still works, but Node.js 20 has received no security fixes since 2026-04-30: plan a move to 22.12+ or 24. On a Node.js outside the range, npm prints an EBADENGINE warning, refuses to install with engine-strict, and Yarn 1 refuses by default. See Requirements.
  • @vue/devtools-api is only needed for the Vue DevTools panel (Vue and Nuxt apps). Without it the integration is left out and nothing else changes. Pinia or Vue Router may already install it, but under pnpm's strict layout a transitive copy does not count: install it yourself (npm install -D @vue/devtools-api), then restart the dev server.
  • ESLint 10 is supported through the flat config. ESLint 10 removed .eslintrc: see TypeScript and ESLint.
  • One new runtime dependency, @jridgewell/trace-mapping, finds each call's source location.

Renamed options

The plugin reads every old key, maps it onto its new name when mtDebug() runs, and prints one warning per group of keys. The old keys keep working until 2.0.0.

0.6.11.0.0Notes
browseroutput.consoleIt only ever switched the browser console.
terminaloutput.terminalfalse now also silences server-side output.
showSourceLocationshowSourceSame name as the per-call option it sets the default for.
allowDebugBuildlibrary.debugBuildFor library authors.
libraryPrefixlibrary.brandPrefixWithout debugBuild, it now warns instead of doing nothing.
testStubtesttestStub: false becomes test: false.
testLogtesttestLog: true becomes test: 'log'.
ts
// 0.6.1
mtDebug({
  browser: false,
  showSourceLocation: false,
  allowDebugBuild: true,
  libraryPrefix: true,
  testLog: true,
});
text
[MARS-TECH] "allowDebugBuild" and "libraryPrefix" moved to "library" in 1.0.0. Use library: { debugBuild: true, brandPrefix?: true }. The old names still work until 2.0.0.
[MARS-TECH] "browser" moved to "output" in 1.0.0. Use output: { console, terminal, inspect, devtools }. The old name still works until 2.0.0.
[MARS-TECH] "showSourceLocation" moved to "showSource" in 1.0.0. It is the same switch as the per-call showSource option. The old name still works until 2.0.0.
[MARS-TECH] "testLog" moved to "test" in 1.0.0. Use test: 'stub' (default), test: 'log', or test: false. The old name still works until 2.0.0.
ts
// 1.0.0
mtDebug({
  output: { console: false },
  showSource: false,
  library: { debugBuild: true, brandPrefix: true },
  test: "log",
});

Notes on each key

  • output holds one switch per surface: console and terminal, and three new in 1.0: inspect (live values in the browser console), devtools (Vue DevTools) and file (an NDJSON log). The first four default to true; file is off unless you set it. With console, terminal and devtools all false and file off, the page gets a no-op $debug, so your calls still do not throw. See output.

  • terminal: false now silences the server side too: SSR and the Vite process, where 0.6.1 still printed SSR output. $debug also stays defined in the Vite process: in 0.6.1, a call from vite.config.ts or another plugin threw ReferenceError until an SSR module had loaded. If you relied on SSR output with terminal: false, leave output.terminal on: the same switch also covers the lines forwarded from the browser.

  • library is for library authors: allowDebugBuild: true, libraryPrefix: true becomes library: { debugBuild: true, brandPrefix: true }. libraryPrefix: true without allowDebugBuild did nothing in 0.6.1, silently. It still does nothing, and now says so:

    text
    [MARS-TECH] library.brandPrefix has no effect without library.debugBuild — the prefix shadow is only prepended to the debug/ variant that debugBuild emits.

    See Library authoring.

  • showSource, the plugin option, now has the same name as the per-call showSource option it sets the default for.

  • test turns two booleans into one option with three values: 'stub' (the default, a no-op $debug), 'log' (a real console logger) and false (register nothing). One combination changes what gets installed: { testStub: false, testLog: true } installed nothing in 0.6.1, and now maps to 'log', because an explicit testLog: true is read as the intent. If you really wanted nothing registered, write test: false. See Testing.

Development builds of 1.0 had flat inspect and devtools keys: they are mapped onto output with the same warning.

Old and new key together

When a config sets an old key and its replacement, the replacement wins where they overlap, and the conflict gets its own warning:

js
// vite.config.js, half-migrated
mtDebug({ browser: false, output: { console: true } });
text
[MARS-TECH] both "browser" and "output" are set; "output" wins where they overlap. Remove "browser".
[MARS-TECH] "browser" moved to "output" in 1.0.0. Use output: { console, terminal, inspect, devtools }. The old name still works until 2.0.0.

Old keys are type errors

The mapping happens at run time, for configs nothing type-checks: Vite never type-checks vite.config.ts itself. MTPluginOptions lists only the 1.0.0 names, so if a tsconfig includes your vite.config.ts, an old key fails the type check even though it still works at run time:

text
error TS2353: Object literal may only specify known properties, and 'browser' does not exist in type 'MTPluginOptions'.

There is no typed spelling of the old names: rename the keys.

Removed

stripInDeps

stripInDeps let a consuming app strip the $debug calls a dependency shipped in its published bundle. It is removed. Passing it warns and strips nothing:

ts
// 0.6.1
mtDebug({ stripInDeps: ["@acme/cart-store"] });
text
[MARS-TECH] the "stripInDeps" option was removed in 1.0.0. A dependency shipping $debug calls should be built with library: { debugBuild: true }, which needs no configuration in the consuming app — ask its author. See MIGRATION.md for the migration.

MIGRATION.md is the plugin's migration guide, which this page follows. Dependencies are never rewritten now, so the dependency's calls reach your production bundle as written, and throw ReferenceError: $debug is not defined there.

The fix belongs to the library. Ask its author to build with library: { debugBuild: true } (see Library authoring): that build publishes a stripped variant for production and a debug/ variant for development, and the consuming app needs no configuration at all.

Until then, define a no-op $debug before the dependency runs, from a classic script (one without type="module"), which runs before every module of the page, however the bundler splits or orders your chunks:

html
<!-- index.html, in <head>, before any module script -->
<script src="/debug-stub.js"></script>
js
// public/debug-stub.js — a classic script, so it runs before every module.
// In `vite dev` the plugin's runtime loads after it and replaces it.
{
  const noop = () => {};
  globalThis.$debug ??= Object.assign(noop, {
    ns: () => noop,
    enable: noop,
    disable: noop,
    setLevel: noop,
    setPrefixFilter: noop,
    assert: noop,
    time: noop,
    timeEnd: noop,
    count: noop,
    group: noop,
    groupCollapsed: noop,
    groupEnd: noop,
    table: noop,
    dir: noop,
  });
}

Files in public/ are copied to the build as they are, so the plugin never strips the stub, and an external <script src> raises no build warning. Do not import the stub from your entry module instead: when the dependency lands in a shared chunk, the bundler places that chunk's import first, and its top-level calls run before the stub. Unlike stripInDeps, the stub keeps the calls in your bundle and still evaluates their arguments: it only makes them harmless. For server-side rendering, run the same code first in the Node.js server that serves your production build.

Removed and renamed types

0.6.11.0.0
MTPlugin from /helpersPlugin from vite: MTPlugin was an alias of it
MTFormatContext from /helpersnone: it described a format hook that never existed
InstallStubOptions from /testingMTInstallStubOptions, same shape
GlobalAccess from /eslintMTGlobalAccess, same shape
ts
// 1.0.0
import type { Plugin } from "vite";
import type { MTInstallStubOptions } from "@mars-tech/vite-debug-plugin/testing";
import type { MTGlobalAccess } from "@mars-tech/vite-debug-plugin/eslint";

TypeScript points at the new names:

text
error TS2724: '"@mars-tech/vite-debug-plugin/testing"' has no exported member named 'InstallStubOptions'. Did you mean 'MTInstallStubOptions'?

If your code used MTFormatContext as a shape, declare it locally. What the plugin really produces for one call is MTLogRecord, from /helpers (see New in 1.0).

Source maps

The package no longer ships .map files: they were half of its size. The JavaScript in dist/ is not minified, so the plugin's own stack frames stay readable without them. A library.debugBuild build no longer writes .map files into debug/ either: the map it copied there described the stripped production chunk, so it pointed at the wrong lines. If the files field of your package.json names dist/debug/*.map, drop that entry.

Deprecated call forms

mtDebugGlobals from @mars-tech/vite-debug-plugin/eslint now takes the same options object as mtDebugFlatConfig:

js
// 0.6.1
mtDebugGlobals("$log", "readonly");

// 1.0.0
mtDebugGlobals({ globalName: "$log", access: "readonly" });

The positional form still works until 2.0.0. It prints no warning: its type is marked @deprecated, so editors strike it through. mtDebugGlobals() with no argument is unchanged.

The per-call { raw } option, which only development builds of 1.0 had, is a deprecated alias of { inspect }: it still works, with a one-time warning, until 2.0.0.

Per-call API

Options in the last argument

$debug now takes any number of payloads, like console.log, and substitutes printf directives (%s, %d, %o…). The last argument counts as options only when it is a plain object whose every key is an option name (tag, showSource, showStack, prefix, inspect); in 0.6.1, the third argument always was. The terminal lines below leave out the (file:line:col) that ends each line by default:

Call0.6.11.0.0
$debug("info", "saved", { tag: "cart" })MARS-TECH Info [cart] - savedMARS-TECH Info [cart] - saved
$debug("info", "saved", undefined)MARS-TECH Info - savedMARS-TECH Info - saved undefined
$debug("info", "saved", {})MARS-TECH Info - savedMARS-TECH Info - saved {}
$debug("info", "saved", { tag: "cart", retry: 1 })MARS-TECH Info [cart] - savedMARS-TECH Info - saved {"tag":"cart","retry":1}
$debug("info", "user", { id: 1 })MARS-TECH Info - userMARS-TECH Info - user {"id":1}
$debug("info", "%s scored %d", "Ann", 42)MARS-TECH Info - %s scored %dMARS-TECH Info - Ann scored 42

The second row is the one to look for: a wrapper that forwards optional options now prints undefined whenever its caller passed none. Pass them only when you have them:

ts
// 0.6.1
import type { MTCallOptions } from "@mars-tech/vite-debug-plugin/helpers";

export function logSaved(options?: MTCallOptions) {
  $debug("info", "saved", options);
}
ts
// 1.0.0
import type { MTCallOptions } from "@mars-tech/vite-debug-plugin/helpers";

export function logSaved(options?: MTCallOptions) {
  if (options) $debug("info", "saved", options);
  else $debug("info", "saved");
}

The two-argument form is unchanged, and an options object keeps its autocomplete. A misspelt key ({ tagg: "x" }) is no longer ignored: it is logged, which shows you the typo. See Multiple values and printf.

$debug.enable(null)

enable now accepts null: it drops the runtime namespace filter, including the copy saved in localStorage, and returns to the plugin's filter default (every namespace, unless you set one):

ts
// 1.0.0
$debug.enable("cart*"); // only the cart namespaces, until cleared
$debug.enable(null); // back to the configured default

In 0.6.1, enable(null) was a type error, and from JavaScript it saved the text "null" as the filter: after the next reload, only a namespace named null printed.

Redaction

redact and redactDefaults are a safety net, not a vault: they catch a secret that reaches a $debug call by mistake. What 1.0.0 guarantees is that the plugin never introduces a leak: every rule runs over every string a call hands the plugin, exactly as your code passed it, and over the line as printed; every match is masked whole; and nothing the plugin does afterwards brings a character of it back. A secret your code transformed before logging it (cut, encoded, escaped) is another string, outside that guarantee. See Redacting secrets.

String entries mask the server side only

A string entry is usually the secret itself. 0.6.1 wrote every entry into the runtime it served to the page, where any script on the dev origin could read it, and into the SSR runtime. 1.0.0 writes string entries into no module: they stay in the memory of the process that loads your config, and never leave the dev server. They mask the dev terminal, SSR output, the log file and test: 'log' outside a browser, never the browser console or Vue DevTools. To mask a value there too, add a RegExp for its shape: a RegExp is sent to the page as written, so it must never contain the secret.

ts
// 1.0.0
mtDebug({
  redact: [
    process.env.STRIPE_KEY ?? "", // the value: terminal, SSR, log file, test: 'log'
    /sk_live_[A-Za-z0-9]+/, // its shape: every surface, the browser included
  ],
});

An empty or unset string is ignored, a number is matched as the literal it prints as, and any other value is ignored with a warning. In 0.6.1, an unset entry (undefined) or a number threw while the config loaded, and an empty string masked between every character. See String entries and RegExp entries.

The dev server now masks each line a page forwards with every rule, where 0.6.1 printed it as the page sent it. Your RegExp entries therefore also run in the Vite process, over text any script on the dev origin can send: keep them linear-time, as explained in Writing a RegExp entry.

Every match is masked whole

0.6.1 re-emitted a pattern's first capture group in front of [redacted]. 1.0.0 masks the whole match, so a group keeps nothing: a pattern with a leading group now masks its key too, which is more than before, never less. To keep the text in front of a secret, match it with a lookbehind:

ts
// 0.6.1
mtDebug({
  redact: [/("password"\s*:\s*)"[^"]*"/], // $1 kept: only the value is masked
});
ts
// 1.0.0
mtDebug({
  redact: [/(?<="password"\s*:\s*)"[^"]*"/], // a lookbehind: only the value is masked
});

For $debug("info", { password: "hunter2" }), the 0.6.1 pattern now prints {[redacted]}; the lookbehind prints {"password":[redacted]}, as 0.6.1 did. A group that held the secret kept it in 0.6.1: /token=([0-9a-f]+)/ printed ?deadbeef01[redacted] for ?token=deadbeef01, where 1.0.0 prints ?[redacted]. The redactDefaults patterns are written as lookbehinds and print what they printed. A RegExp's sticky flag y is dropped.

Where $debug prints nothing

With string entries configured, each printer in Node.js reads them from the process that loads your config. Where a printer cannot, $debug prints nothing rather than print them unmasked, and says so once. 0.6.1 printed there, through the entries it wrote into the runtime.

With string entries, $debug prints nothing inTo see output there
SSR in a process of its own, or in workerd (@cloudflare/vite-plugin)a RegExp for each value's shape
Vitest 3's forks and vmForks pools (forks is its default)Vitest 4, or pool: "threads"
A Vitest project that runs in a browser, and Vitest's browser pagea RegExp for each value's shape
A worker thread started before the pluginrestart the worker

The SSR runtime, for instance, says:

text
[MARS-TECH] $debug prints nothing in this process: the string entries of your redact option do not reach it, and it does not print what it cannot mask. They reach SSR in the Vite process and in a worker thread started after the plugin, and Vitest workers (the forks and vmForks pools from Vitest 4). Elsewhere - SSR in a process of its own, workerd - describe each such value with a RegExp for its shape instead.

Without string entries, every printer prints, masked by your RegExp entries and the defaults. The rarer cases are listed in Where string entries cannot reach.

What masks more than before

  • Every string of a call, and the line. Every rule runs over each argument, the format string, every key and string of a logged object, the namespace and tag, a per-call prefix, the labels of count, time, timeEnd and group, an Error's message and stack, and the source path, then over the line as printed. 0.6.1 matched the printed message alone.
  • A string entry is matched as written, and as JSON escapes it, so a secret holding a ", a \ or a newline is masked inside a logged object.
  • test: 'log' redacts, with your redact and redactDefaults. 0.6.1 printed every secret there.
  • Live values are withheld whenever a RegExp entry or redactDefaults is configured: the browser console and Vue DevTools get the redacted text instead of the live object, and table() and dir() print the redacted summary. { inspect: true } opts one call back in, in the console only. See Live values in the browser.
  • A %d, %i or %f number that reads differently from its argument ("042917" prints 42917) is masked when a rule matches the argument. On a line from the browser, with string entries configured, the terminal and the log file mask such a number whatever its argument.
  • redactDefaults covers more: Authorization: Basic credentials; the value of passphrase, x-api-key, private_key, credentials and cookie; a secret key's value in a [key, value] pair (a Map, Object.entries()), and in the JSON a logged string holds. One case masks less: an email glued to the end of another with no separator (a@x.com%2Cb@y.org) leaves the second one visible.
  • On Vite 8, with a redactor configured and server.forwardConsole left unset (Vite turns it on when it detects a coding agent), your own console.error and console.warn calls no longer reach the terminal through Vite's console forwarding; unhandled errors still do. A server.forwardConsole you set is kept. See Vite 8 console forwarding.

Changed output

Payload rendering

Some payloads rendered through JSON.stringify, which drops what makes them worth logging. The one-line summary (the dev terminal, the browser console label, the log file, test: 'log') changed for these types:

Payload0.6.11.0.0
new Map([["sku-1", 2], ["sku-2", 1]]){"sku-1":2,"sku-2":1}Map(2) [["sku-1",2],["sku-2",1]]
new Set(["a", "b"])["a","b"]Set(2) ["a","b"]
/^tok_[a-z]+$/gi{}/^tok_[a-z]+$/gi
new Date("2026-10-14T08:00:00.000Z")"2026-10-14T08:00:00.000Z"2026-10-14T08:00:00.000Z
123n123123n
new Cart(), with class Cart { items = 2 }{"items":2}Cart {"items":2}
{ a: shared, b: shared }, with shared = { id: 1 }{"a":{"id":1},"b":"[Circular]"}{"a":{"id":1},"b":{"id":1}}

A call made in the Vite process itself (from vite.config.ts, another plugin or same-process SSR) printed {} for a Map and a Set in 0.6.1; it now renders as above. Strings, numbers, plain objects, arrays and Errors render as before. A test that asserts on the old text needs updating. The payload rendering table lists the form for every type.

In the browser console and the terminal

  • Live values. Each browser line also hands the raw payload to the console, so objects, DOM nodes and Errors stay expandable, as with console.log, unless a redactor withholds them. For the 0.6.1 one-line look, set output: { inspect: false }. See Live object inspection.
  • Groups are real console groups. $debug.group() opens a collapsible console.group in the browser instead of indenting the text. The terminal and test: 'log' still indent, and no longer repeat the label as a tag: $debug.group("checkout") prints MARS-TECH Debug - checkout, where 0.6.1 printed MARS-TECH Debug [checkout] - checkout.
  • Source locations point at your source, in one format. On every surface, the file:line:col is the original position in your .vue or .ts file, relative to the Vite root and counted from 1 (src/Cart.vue:12:3), where 0.6.1 printed a served URL or an absolute path. For a call the dev transform cannot record, see My log's file:line is a few lines off.
  • Vue DevTools. With @vue/devtools-api installed, a MARS-TECH tab appears in Vue DevTools. output: { devtools: false } turns it off. See Vue DevTools.

Other behaviour changes

None of these needs a code change.

Options and warnings

  • enabled: true changes nothing, as in 0.6.1, and prints no warning. Unset already means "on in vite dev, stripped in vite build"; true never forced the runtime into a production build. For a debug build, use library: { debugBuild: true }.
  • enabled: false now covers test: 'log' too: it falls back to the no-op stub, where 0.6.1 still installed the console logger under Vitest.
  • globalName must be a plain JavaScript identifier that is not a reserved word ($debug, myDebug). Anything else, such as a dot, a space or a hyphen, now throws when mtDebug() runs; in 0.6.1 it silently broke the strip and a library's debug/ bundle.
  • Registering the plugin twice keeps only the first instance, with a warning. Vite does not deduplicate plugins, so a framework preset that already added mtDebug() used to print every browser log twice. See Every log appears twice.
  • Registered outside the top-level plugins (under build.rollupOptions.plugins, or in a Rollup or Rolldown config), the plugin only strips, without a warning. 0.6.1 assumed vite dev there and shipped development code to production.
  • levels and minLevel together still warn, but only when levels is not empty: an empty levels: [] defers to minLevel without a warning.
  • Server-side rendering outside the Vite process (Nuxt's Nitro worker) now applies your colors[level].ansi overrides, where 0.6.1 printed the built-in colours.

Terminal and log file

  • Control characters are shown escaped in the terminal and the log file: escape sequences, a bare \r, C1 controls, bidi overrides. A string that already carries ANSI colours (from chalk, say) prints its codes as text. Tabs and newlines are kept.
  • Lines forwarded from the browser are bounded: the message at 10 000 characters, tag and prefix at 256, the source file at 1 024, the stack at 16 384 (past it, [stack omitted: N chars]). A long string logged in the browser is cut in the terminal.
  • A page that floods the dev server with logs is rate-limited, across connections too: each HMR connection may forward a burst of 10 000 lines, then 2 000 a second, and every connection together a burst of 20 000, then 4 000 a second. A line over either limit reaches neither the terminal nor the log file, and one line every 10 seconds says how many were dropped. The browser console still shows every call; SSR and Vite-process lines are not limited. See What reaches the terminal.
  • count() and time() remember up to 1 000 labels each, forgetting the least recently used, and the group indent stops growing at 32 levels.

The NDJSON log file is new in 1.0 and has limits of its own:

  • It stops at 100 MiB a session, that last line included, with a last line saying so and one terminal warning. output.fileMaxBytes sets another limit, in bytes; Infinity lifts it. See Sessions and size.
  • It stays off next to dev tools that read any file for the page:vite-plugin-vue-devtools, Nuxt DevTools, Vite DevTools, devframes and TanStack Devtools. The terminal gets one warning naming the tool, and there is no override. A log an earlier session left is then not truncated either: delete it. If you need the log, run those tools in a separate dev session: in Nuxt, devtools: { enabled: false }; in Vite 8, no devtools: true and no DevTools() plugins.
  • The plugin writes a .gitignore holding * into .mt-debug/, and into any log directory it creates, never over one that is there. That keeps the log out of git, and out of Tailwind CSS's automatic source detection. Next to @tailwindcss/vite, a log in a directory of your own whose .gitignore does not hold * is not opened, with one warning. @tailwindcss/postcss is not detected: add the file yourself.

Production builds

  • More files are stripped. 0.6.1 stripped only .js, .ts, .jsx, .tsx, .mjs, .mts and .vue modules. 1.0.0 strips every module of your own source that names the global (.cjs, an inline <script type="module">, a virtual module, Svelte, Astro, MDX, VitePress pages), plus web workers and the scripts Vite copies instead of bundling (a service worker, a worklet, a ?url import). Calls there used to ship and throw.
  • New build warnings point at code that throws in production: a reference to $debug that is not a call (const d = $debug), a computed method ($debug['time']()), a call in a classic inline <script> or an on* attribute, a copied script left as written (a ?raw import, a .ts file), and a file the strip cannot parse. Fix each one where it points.
  • globalThis['$debug'](...) and window["$debug"] now strip like the dotted forms.
  • A bare $debug.ns('x') statement strips to void 0; a stored logger (const log = $debug.ns('x')) still strips to a callable no-op.
  • Dependencies are never rewritten: see stripInDeps.

See What the strip covers, Method and chained forms, Web workers and Scripts Vite copies.

Tests

  • The no-op stub has all 14 methods, so setPrefixFilter, groupCollapsed, table and dir no longer throw under Vitest.
  • test: 'log' gives each Vitest project its own configuration, prints several payloads and printf, renders payloads like dev, and redacts.
  • A project Vitest runs in a browser (--browser included) gets what the dev page gets: your RegExp entries and redactDefaults, never string entries. With string entries configured, $debug prints nothing there.
  • @mars-tech/vite-debug-plugin/testing/log is removed from setupFiles if you listed it under test: 'log': the plugin registers a module generated from your config instead. For a configured logger without the plugin, call installMtDebugConsole({ prefix, redact, … }) from /testing in your own setup file.

See Seeing $debug output in tests.

Library authors

  • Branded library logs carry no source location. With library.brandPrefix, the consuming app sees no file:line:col for your library's calls, unless a call passes { showSource: true }. See Library logs carry no source location.
  • Your redact is not shipped in debug/. The consuming app's configuration masks your library's output, or nothing does. Never pass a secret to $debug in library code.
  • Chunk hashes change. The production variant is stripped before hashing, so two builds that differ only in a $debug payload get the same file names.
  • umd and iife builds wrap the brandPrefix shadow in a block, so it no longer declares a page-wide $debug.
  • Vue template calls and optional calls ($debug?.(…)) are removed from the production variant whatever the minifier or build.target, and template calls in debug/ reach the app's global $debug.

New in 1.0

Nothing below requires a change, but it is worth adopting:

  • An NDJSON log file, for agents and tests that read logs: mtDebug({ output: { file: true } }) writes every call, already masked, to .mt-debug/dev.ndjson under the Vite root. For security reasons, do not use it when your code logs secrets, even redacted ones: the log is a file on disk that any tool reading files can read. Keeping it from the page is a safety net, not a vault: the plugin blocks every known route from a page to the log, and a log you keep is safer outside the project (outside server.fs.allow, which in a monorepo is the workspace root), where Vite refuses every file. Run Vite 6.4.2, 7.3.2, 8.0.5 or later: earlier releases hand any file to a page script, your .env included. See Keeping the log from the page.
  • A default namespace filter: mtDebug({ filter: "cart*" }), overridden at run time by ?mt-debug=, MT_DEBUG or $debug.enable("auth*"). See filter.
  • Stacks by default: mtDebug({ showStack: true }) attaches the redacted stack to every call that carries an Error. See showStack.
  • Several payloads and printf: $debug("info", "%s scored %d", name, score). See Multiple values and printf.
  • More helpers: $debug.groupCollapsed("checkout"), $debug.table(rows, ["name"]), $debug.dir(user). See Utility helpers.
  • Live inspection in the browser console: $debug("info", document.body) gives a node you can reveal in the Elements panel. See Live object inspection.
  • Vue DevTools: a MARS-TECH inspector and timeline layer. See Vue DevTools.
  • Typed log records: MTLogRecord from /helpers is one call as the dev server prints it and writes it to the log file; MTLogLine, a line of that file, adds t, seq and origin.
  • Types from the root entry:import type { MTPluginOptions } from "@mars-tech/vite-debug-plugin". See Subpath exports.
  • A configured test logger without the plugin:installMtDebugConsole({ prefix: "APP", redactDefaults: true }) from /testing. See A logger in your own setup file.
  • ./package.json is exported, for tools that read a dependency's manifest.

TypeScript and ESLint

  • CommonJS projects get CommonJS declarations. Under module: node16 (or nodenext before TypeScript 5.8), a CommonJS file importing the plugin saw TS1479. The require condition now points at .d.cts files: drop any @ts-ignore you added for it.

  • The /client reference is unchanged. Keep /// <reference types="@mars-tech/vite-debug-plugin/client" />. It now types the variadic call, enable(null) and the new helpers, under every moduleResolution.

  • If you implement MTDebug yourself (a typed stub, a custom global), add groupCollapsed, table and dir, and accept null in enable: a 0.6.1-shaped object no longer satisfies the type. The object the stripInDeps stub builds is a complete one.

  • MTDebugFn is overloaded: the typed (level, payload, options?) form and a variadic (level, ...payloads) form. A function you assign to it must accept any value after the level, so a third parameter typed MTCallOptions now fails with TS2322:

    ts
    // 1.0.0
    import type { MTDebugFn, MTLevel } from "@mars-tech/vite-debug-plugin/helpers";
    
    export const log: MTDebugFn = (level: MTLevel, ...rest: unknown[]) => {};
  • colors is typed MTColorOverrides: any subset of levels, each with any subset of label, css and ansi. mtDebug({ colors: { warning: { label: "Warning" } } }), a type error in 0.6.1, now type-checks.

  • MTCallOptions gains inspect, which forces or suppresses the live value for one call.

  • ESLint 10 removed .eslintrc. The flat config works on ESLint 9 and 10. If you move to 10, switch from mtDebugGlobals() in .eslintrc to the default export:

    js
    // eslint.config.js
    import mtDebug from "@mars-tech/vite-debug-plugin/eslint";
    
    export default [mtDebug];

    See Tell ESLint about the global.