npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mars-tech/vite-debug-plugin

v0.6.0

Published

Vite plugin exposing a global $debug function in dev that mirrors output to the browser console and the Vite terminal. Calls are stripped from production builds.

Readme

@mars-tech/vite-debug-plugin

🇫🇷 Version française : README-fr.md

npm version npm downloads pipeline status coverage report bundle size types Vite Node license

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.

demo

Works across any Vite-powered framework: Vue 3.5, React, Angular (via Analog), Nuxt 4, SvelteKit, Astro server, Vite SSR, etc. 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.
  • Zero production cost — all $debug(...) calls (including method and chained forms) are rewritten to void 0 at 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 via enable/disable/setLevel, a ?mt-debug= URL param / localStorage, or the MT_DEBUG env — no restart. See Runtime filtering & namespaces.
  • Secret redaction — mask tokens, emails, and custom patterns in output. See Redacting secrets.
  • Utility helpersassert, time/timeEnd, count, group/groupEnd. See Utility helpers.
  • Library authoringallowDebugBuild ships a dual variant so libraries log in dev and strip in build. See Library authoring.
  • First-class TS + ESLint — shipped /client global types (all module-resolution modes) and a /eslint flat config that declares the $debug global.

Compatibility

| Stack | Versions tested | | ----------- | --------------------------------------------------- | | Vite | 6.x · 7.x · 8.x | | Node.js | ≥ 20.19 (LTS 22 recommended) | | 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.

Install

npm install -D @mars-tech/vite-debug-plugin

Quickstart (30 seconds)

npm create vite@latest my-app -- --template vue-ts
cd my-app
npm install -D @mars-tech/vite-debug-plugin

Add mtDebug() to the plugins array in vite.config.ts, drop a triple-slash reference into src/vite-env.d.ts, then call $debug('info', 'hello') anywhere in src/. Open the browser DevTools console and the Vite terminal side-by-side to see the same line on both.

Usage

1. Add the plugin

// 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).

2. Enable the 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 exports map (moduleResolution: "bundler", "node16", or "nodenext"). On legacy "node10" / "classic", the package also ships a root-level client.d.ts that resolves by path — no change needed.

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+)
import mtDebug from "@mars-tech/vite-debug-plugin/eslint";

export default [
  mtDebug, // declares $debug as a readonly global
  // ...your other configs
];

Custom globalName? Use the factory: import { mtDebugFlatConfig } from "@mars-tech/vite-debug-plugin/eslint"mtDebugFlatConfig({ globalName: "$log" }). Legacy .eslintrc: const { mtDebugGlobals } = require("@mars-tech/vite-debug-plugin/eslint"){ globals: mtDebugGlobals() }. (TypeScript users can alternatively just disable no-undef for .ts files — tsc already catches undefined names.)

3. Call $debug anywhere 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"

Function signature

$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
  }
): void

| Payload type | Rendered | | -------------- | ------------------------------------------------------------ | | string | as-is | | Error | error.message (stack appended when showStack: true) | | object / array | JSON.stringify with circular guard, truncated at 500 chars | | Map / Set | converted to plain shape, then stringified | | Vue ref | .value unwrapped, then standard rules apply | | DOM Node | <tag#id.class> summary | | Promise | [Promise] (not awaited) |

$debug also exposes runtime controls (ns, enable, disable, setLevel, setPrefixFilter — see Runtime filtering & namespaces) and utility helpers (assert, time/timeEnd, count, group/groupEnd).

Plugin options

mtDebug({
  enabled: true,
  levels: ["error", "warning", "info", "debug", "success", "trace"],
  minLevel: "debug",
  terminal: true,
  browser: true,
  showSourceLocation: true,
  prefix: "MARS-TECH",
  colors: {
    warning: { label: "Warning" },
  },
  globalName: "$debug",
  prefixFilter: "-NoisyLib",
  stripInDeps: ["@your-org/some-pinia-plugin"],
  allowDebugBuild: false,
  libraryPrefix: false,
  redact: [/x-api-key: \S+/, "supersecret"],
  redactDefaults: false,
  testStub: true,
  testLog: false,
});

| Option | Type | Default | Notes | | -------------------- | ---------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | enabled | boolean | mode === 'development' | Master switch. | | levels | MTLevel[] | all levels | Allowlist. Mutually exclusive with minLevel. | | minLevel | MTLevel | unset | Threshold (trace < debug < info < success < warning < error). | | terminal | boolean | true | Print to Vite stdout. | | browser | boolean | true | Print to browser console. | | showSourceLocation | boolean | true | Default for per-call showSource. | | prefix | string | 'MARS-TECH' | Output prefix. | | colors | Partial<MTColorMap> | unset | Per-level color overrides; missing keys fall back to defaults. | | globalName | string | '$debug' | Identifier attached to globalThis. | | 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. | | stripInDeps | (string \| RegExp)[] | [] | Opt selected node_modules packages into the production strip transform. Fallback for libraries that don't ship their own dual variant. See Library authoring with allowDebugBuild. | | allowDebugBuild | boolean | false | Library mode: vite build emits a dual output (dist/ stripped + dist/debug/ with calls preserved + auto-prepended fallback shim). See Library authoring with allowDebugBuild. | | libraryPrefix | boolean | false | Library mode: stamp this library's prefix onto its $debug calls so they stay branded when consumed, instead of inheriting the app's prefix. Requires allowDebugBuild. See Branding library logs. | | redact | (string \| RegExp)[] | [] | Mask matches in the rendered message ([redacted]), across browser + terminal. Strings match literally; a RegExp is applied globally and may keep a $1 group. See Redacting secrets. | | redactDefaults | boolean | false | Also apply a curated secret set (JWTs, Bearer tokens, emails, Stripe-style keys, secret JSON values). See Redacting secrets. | | testStub | boolean | true | Under Vitest, auto-register a first setupFiles entry that installs a no-op $debug (default global only). Set false to manage it yourself. See Testing. | | testLog | boolean | false | Under Vitest, make that auto setup install a real, config-aware console logger instead of the no-op — component and test-file $debug(...) print with your configured prefix / globalName / levels. Requires testStub on. See Seeing $debug output in tests. |

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
}

Subpath exports

| Specifier | What it provides | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | @mars-tech/vite-debug-plugin | Default export only — the plugin factory. CJS-callable (const mtDebug = require('@mars-tech/vite-debug-plugin') returns the function). | | @mars-tech/vite-debug-plugin/helpers | All named exports — DEFAULT_COLORS, DEFAULT_PREFIX, DEFAULT_GLOBAL_NAME, plus every MT* type. | | @mars-tech/vite-debug-plugin/eslint | ESLint flat config declaring the $debug global (default export) + mtDebugFlatConfig(opts) / mtDebugGlobals(name?) helpers. | | @mars-tech/vite-debug-plugin/testing | Test-runner setup: importing installs a no-op $debug; also exports installMtDebugStub(opts). See Testing. | | @mars-tech/vite-debug-plugin/testing/log | The setup module testLog: true auto-registers — installs the real console logger. Registered for you; you don't import it directly. See Seeing $debug output in tests. | | @mars-tech/vite-debug-plugin/client | Triple-slash reference target for the $debug global type augmentation. |

import mtDebug from "@mars-tech/vite-debug-plugin";
import {
  DEFAULT_COLORS,
  type MTLevel,
} from "@mars-tech/vite-debug-plugin/helpers";

Custom globalName

When you change globalName, the shipped client .d.ts no longer matches. Add your own augmentation:

// src/vite-env.d.ts
import type {
  MTLevel,
  MTCallOptions,
} from "@mars-tech/vite-debug-plugin/helpers";

declare global {
  function myDebug<T>(
    level: MTLevel,
    payload: string | Error | T,
    options?: MTCallOptions,
  ): void;
}

export {};

Runtime filtering & namespaces

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.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 libraryPrefix 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 testLog, 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.

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");
$debug("info", "validating cart"); // indented under the group
$debug("info", "charging card");
$debug.groupEnd();

| 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?) / groupEnd() | Indents subsequent output one level (label, if given, is logged as the title). |

The label doubles as the namespace/tag, so helpers respect the active filter too. Timing uses performance.now() when available.

Redacting secrets

redact masks matches in the rendered message — browser console, dev terminal, and SSR alike — replacing them with [redacted]. It runs on the stringified payload, so nested object values are covered too.

mtDebug({
  redact: [
    "supersecret", // literal string, every occurrence
    /x-api-key:\s*\S+/, // RegExp, applied globally
    /("password"\s*:\s*)"[^"]*"/, // $1 kept → only the value is masked
  ],
  redactDefaults: true, // also mask JWTs, Bearer tokens, emails, sk_/pk_ keys, secret JSON values
});
$debug("info", { user: "[email protected]", token: "Bearer abc.def" });
// MARS-TECH Info - {"user":"[redacted]","token":"[redacted]"}   (with redactDefaults)

A RegExp is applied with the global flag forced on; if it carries a $1 capture group, that group is preserved and only the rest of the match is replaced. redactDefaults is opt-in — it errs toward high-confidence patterns and skips bare digit runs to avoid eating order ids and timestamps.

Testing (Vitest / Jest)

$debug is installed by the plugin's runtime (browser injection, SSR virtual import, dev-server global). A unit-test runner runs none of that, so source that calls $debug(...) — especially at module-eval time — throws ReferenceError: $debug is not defined. A no-op stub fixes it (it behaves exactly like a stripped production build).

Vitest — automatic (nothing to add)

If mtDebug() is in the Vite config Vitest reads (the usual case — Vitest merges your vite.config), the plugin detects the test run and registers the stub for you as a first setupFiles entry. Just run your tests — $debug is defined. Opt out with mtDebug({ testStub: false }).

Not a Vite/Vitest plugin hook you add — the plugin you already have contributes a setup file (a global installed before your tests import anything). Auto-registration covers the default $debug; for a custom globalName set testStub: false and register the stub yourself (below).

Explicit — Jest, or a Vitest config without the plugin

Add the shipped setup module to setupFilesfirst, before any other setup that imports your source:

// vitest.config.ts (or jest config)
export default defineConfig({
  test: {
    setupFiles: ["@mars-tech/vite-debug-plugin/testing"],
  },
});

Importing the module installs a no-op $debug (callable + ns / enable / disable / setLevel / assert / time / timeEnd / count / group / groupEnd).

Custom globalName, or asserting calls

Use the programmatic form from your own setup file:

// tests/setup.ts   (setupFiles: ["./tests/setup.ts"])
import { installMtDebugStub } from "@mars-tech/vite-debug-plugin/testing";
import { vi } from "vitest";

installMtDebugStub({ globalName: "$log", impl: vi.fn() }); // spy on every call

Manual stub (no dependency)

If you'd rather not import the package in tests, hand-write it — but it must be the first setupFile and contain no imports. ES modules evaluate a file's imports before its body, so a stub placed after imports (or in a file that transitively imports $debug-using modules) runs too late and still throws:

// tests/debug-stub.js   (setupFiles: ["./tests/debug-stub.js", ...])  — NO imports
const noop = () => {};
globalThis.$debug = Object.assign(noop, {
  ns: () => noop,
  enable: noop,
  disable: noop,
  setLevel: noop,
  assert: noop,
  time: noop,
  timeEnd: noop,
  count: noop,
  group: noop,
  groupEnd: noop,
});

Seeing $debug output in tests

The stub is a no-op — it stops tests throwing, but swallows output. To actually see logs during a test run — from your test files and from components / library code exercised by the test — flip one flag:

// vite.config.ts
mtDebug({ testLog: true });

Under Vitest the plugin swaps the no-op for a real, formatted console $debug. It's config-aware — it uses your resolved prefix, globalName, and levels (handed to the test setup over the environment), so test output matches your dev output. Nothing else to add.

Filter with the same env controls as the runtime: MT_DEBUG=cart* MT_DEBUG_LEVEL=info vitest.

A logged line from a library built with libraryPrefix keeps its prefix, so app vs. library logs stay distinguishable under test:

XYZ Info [A/UsesB] - UsesB mounted        ← your app (prefix XYZ)
ABC Info [cart/action] - cart.add(…)      ← library B (prefix ABC)

Stub the library (vi.mock('…')) and its lines simply stop appearing — only your app's logs remain.

Vitest groups console output under each test by default — pass --disableConsoleIntercept (or --reporter=verbose) to see the lines inline.

Calling $debug in test files needs no runtime change — the stub/logger defines it. For editor + CI happiness: TypeScript picks up the global from the /// <reference types="@mars-tech/vite-debug-plugin/client" /> you already added (as long as your test files sit in a tsconfig that includes it), and the shipped /eslint flat config declares $debug so no-undef stays quiet.

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 to console.{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 module virtual:mt-debug/ssr). It writes ANSI-colored output directly to process.stdout. As a fallback for same-process SSR setups, the plugin also installs $debug on the Vite dev server's globalThis from configureServer.

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.

Framework recipes

Snippets below show the relevant call sites only — drop them into any Vite-based project after wiring mtDebug() in vite.config.ts (see Usage).

Vue 3 — Composition API (props validator)

<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>

React — Redux Toolkit slice

import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { count: 0 },
  reducers: {
    increment(state) {
      state.count++;
      $debug("info", `counter incremented to ${state.count}`, {
        tag: "store/counter",
      });
    },
    reset(state) {
      $debug("warning", "counter reset", { tag: "store/counter" });
      state.count = 0;
    },
  },
});

Angular (Analog) — NgRx reducer

import { createReducer, on } from "@ngrx/store";

export const counterReducer = createReducer(
  { count: 0 },
  on(increment, (state) => {
    const next = state.count + 1;
    $debug("info", `counter incremented to ${next}`, { tag: "store/counter" });
    return { ...state, count: next };
  }),
);

Nuxt 4 — Pinia store

// app/stores/counter.ts
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++;
      $debug("info", `counter incremented to ${this.count}`, {
        tag: "store/counter",
      });
    },
  },
});
// nuxt.config.ts
import mtDebug from "@mars-tech/vite-debug-plugin";

export default defineNuxtConfig({
  modules: ["@pinia/nuxt"],
  vite: { plugins: [mtDebug()] },
});

Library authoring with allowDebugBuild

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 dev without paying for them in production. The plugin handles both variants in a single build when you set allowDebugBuild: true:

// B's vite.config.ts (the library)
import mtDebug from "@mars-tech/vite-debug-plugin";

export default defineConfig({
  plugins: [mtDebug({ allowDebugBuild: true })],
  build: {
    lib: { entry: "src/index.ts", formats: ["es", "cjs"] },
    rollupOptions: { external: ["vue", "pinia"] },
    sourcemap: true,
  },
});

After vite build the library's dist/ contains:

dist/
├── index.js          ← standard variant, every $debug call stripped → DCE'd
├── 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.cjs
    ├── index.js.map
    └── index.cjs.map

Wire 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"
    }
  }
}

Three-tier behavior:

| Tier | Stage | What happens | | ----------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | A plugin (this package) | — | Provides the runtime + the dual-build transform | | B library (e.g. a Pinia plugin) | vite build | allowDebugBuild: truedist/index.* stripped, dist/debug/index.* keeps calls + auto-prepended fallback shim | | C end-user app w/ A | vite (dev) | Vite picks development condition → debug variant resolved → A's browser runtime defines real $debug first → fallback shim's ??= leaves it alone → logs ✓ | | C end-user app w/ A | vite build | Vite picks default condition → standard variant resolved → zero $debug references in C's bundle, regardless of what C does in its own source | | C end-user app w/o A | vite (dev) | Debug variant resolved → fallback shim installs no-op $debug + emits one-time console.warn advising A be installed | | C end-user app w/o A | vite build | Standard variant resolved → already stripped → silent, no crash |

allowDebugBuild removes the need for consumers to know anything about VDP. Library authors don't write a fallback shim by hand — the plugin auto-prepends it to every debug-variant entry chunk.

Peer-dep declaration in B's package.json — keep VDP as an optional peer so consumers that don't want logs still install:

{
  "peerDependencies": {
    "@mars-tech/vite-debug-plugin": ">=0.2.0"
  },
  "peerDependenciesMeta": {
    "@mars-tech/vite-debug-plugin": { "optional": true }
  }
}

If you enable allowDebugBuild but forget to add the development condition to your package.json exports, the plugin emits a build warning with the exact snippet to paste — the debug variant is otherwise emitted but never resolved, leaving consumers without dev logs.

For libraries you can't rebuild with allowDebugBuild, the consuming app can strip a dependency's baked-in $debug calls at its own build via the stripInDeps option.

Branding library logs with libraryPrefix

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. So a log from inside your library shows up as [App] …, giving the developer no signal that it came from your library.

Set libraryPrefix: true (alongside allowDebugBuild) to keep your library's logs branded with its own prefix wherever it's used:

// library vite.config.ts
plugins: [
  mtDebug({ allowDebugBuild: true, libraryPrefix: true, prefix: "CliR" }),
];

Now every library log renders with CliR regardless of the consuming app's own prefix:

CliR info [cart/action] - cart.add(Widget ×2)      ← from the library
MARS-TECH info - user clicked checkout             ← from the app

How it works: the debug variant gets a tiny module-local $debug shadow prepended to each chunk. It forwards to the app's real $debug but forces { prefix } as a last-write option, so the library's prefix cannot be overridden by the consumer or 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 no-ops (same safety as the fallback shim).

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.

Production behavior

In vite build (client and SSR), every call to the configured global identifier (default $debug) is replaced with void 0. The argument expressions are NOT evaluated.

// before
$debug("debug", expensiveCompute());

// after build
void 0;

This means it is safe to put cost in the arguments — no production runtime impact and no SSR runtime ships.

Method and chained forms strip too: $debug.time(...)void 0, and an inline $debug.ns('x')('info') collapses to void 0 (the whole chain, so it never degrades to void 0(...)). A scoped-logger factory ($debug.ns('x')) strips to a callable no-op (() => {}) instead of void 0, so a logger stored in a variable (const log = $debug.ns('x')) stays safe to call after the strip — and the no-op is then tree-shaken away.

Changelog

See CHANGELOG.md.

Contributing

Contributions, bug reports, and feature requests are welcome. Read CONTRIBUTING.md for the development setup, branching model, commit conventions (Conventional Commits), DCO sign-off, and the merge-request checklist.

For security vulnerabilities, do not open a public issue — see the security reporting policy.

Issues

File bugs, request features, or browse open work on the GitLab issue board:

https://gitlab.com/mars-tech/vite-debug-plugin/-/issues

Versioning

This project follows Semantic Versioning 2.0.0.

| Bump | When | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Major (x.0.0) | Backwards-incompatible API changes — e.g. removing a plugin option, changing the shape of an export, dropping support for a Vite major. | | Minor (0.x.0) | New backwards-compatible features — e.g. adding a new log level, a new plugin option, a new subpath export. | | Patch (0.0.x) | Backwards-compatible bug fixes, internal refactors, documentation, dependency bumps within range. |

Pre-1.0 caveat: while the major version is 0, the minor bump may include breaking changes. Every breaking change is called out under Changed (breaking) in CHANGELOG.md with a migration snippet.

The peerDependencies.vite range (^6.0.0 || ^7.0.0 || ^8.0.0) is part of the public API: shrinking it requires a major bump.

License

MIT — © MARS TECH. See LICENCE.md.