Aller au contenu

Comment ça marche

vite-debug-plugin est un plugin Vite qui expose une fonction globale $debug en développement. Chaque appel s'affiche simultanément dans la console du navigateur et dans le terminal du serveur de développement Vite, avec un formatage et des couleurs cohérents par niveau de sévérité. Tous les appels à $debug sont supprimés des builds de production, y compris leurs expressions d'arguments.

Il fonctionne avec n'importe quel framework basé sur Vite. Il est vérifié de bout en bout sur Vue 3.5, React, Angular (via Analog) et Nuxt 4 ; SvelteKit, Astro et le SSR Vite nu utilisent les mêmes points d'accroche de Vite et devraient fonctionner, mais ne font pas partie de la matrice de tests. Le code côté client et le code rendu côté serveur (SSR) sont pris en charge.

Le plugin en action

La démonstration ci-dessous suit une application Vue sur trois pages. Chaque ligne commence par MARS-TECH, le préfixe par défaut de ce plugin MARS TECH ; réglez l'option prefix sur le nom de votre application pour le remplacer.

Fonctionnement du plugin en 3 étapes : les mêmes appels $debug() affichés dans la console du navigateur et dans le terminal Vite.

Lancer le serveur de développement. Lancez le serveur de développement, ouvrez l’application et cliquez deux fois sur +1. Chaque appel à $debug, d’abord au démarrage de l’application puis dans l’action du store Pinia, affiche la même ligne dans la console du navigateur et dans le terminal Vite : niveau, étiquette, message et emplacement dans le code source.

Adresse dans le navigateur :localhost:4312/
Application Vue de démonstration, page d’accueil après défilement : la fin d’une carte de test de Vue DevTools avec un bouton par type de charge utile, puis la carte du compteur Pinia, qui affiche 2 après deux clics sur +1.
Console du navigateur — La console du navigateur affiche les mêmes lignes que le terminal.
Terminal — vite dev
$ npm run dev -- --port 4312 --strictPort
MARS-TECH Info [main] - bootstrapping Vue app (src/main.ts:7:0)
MARS-TECH Trace [component/App] - App.vue setup (src/App.vue:4:0)
MARS-TECH Trace [router/beforeEach] - nav: / → / (src/router/index.ts:25:2)
MARS-TECH Trace [shell/mounted] - PageShell mounted (src/components/PageShell.vue:10:2)
MARS-TECH Info [SomeButton/mounted] - SomeButton mounted with variant=primary (src/components/SomeButton.vue:26:4)
MARS-TECH Trace [shell/mounted] - PageShell mounted (src/components/PageShell.vue:10:2)
MARS-TECH Trace [shell/mounted] - PageShell mounted (src/components/PageShell.vue:10:2)
MARS-TECH Success [view/Home] - Home mounted (src/views/Home.vue:16:2)
MARS-TECH Info [store/counter] - counter incremented to 1 (src/stores/counter.ts:8:6)
MARS-TECH Info [store/counter] - counter incremented to 2 (src/stores/counter.ts:8:6)

Naviguer vers une autre page. Ouvrez la page About et cliquez sur Fire delayed (500 ms). Les hooks de cycle de vie, une méthode appelée après un minuteur et un observateur journalisent tous via $debug, et chaque ligne indique le fichier, la ligne et la colonne de l’appel.

Adresse dans le navigateur :localhost:4312/about
Application Vue de démonstration, page À propos : une carte de démonstration de l’API Options qui liste les étapes du cycle de vie, un observateur, une méthode asynchrone et un libellé calculé ; après un clic sur le bouton de déclenchement différé (500 ms), l’état indique un déclenchement.
Console du navigateur — La console du navigateur affiche les mêmes lignes que le terminal.
Terminal — vite dev
MARS-TECH Trace [router/beforeEach] - nav: / → /about (src/router/index.ts:25:2)
MARS-TECH Trace [view/About/lifecycle] - About created (src/views/About.vue:27:4)
MARS-TECH Trace [shell/mounted] - PageShell mounted (src/components/PageShell.vue:10:2)
MARS-TECH Success [view/About/lifecycle] - About mounted (src/views/About.vue:30:4)
MARS-TECH Info [view/About/method] - delayed fire #1 (src/views/About.vue:40:8)
MARS-TECH Debug [view/About/watch] - delayedCount: 0 → 1 (src/views/About.vue:21:6)

Provoquer une erreur. Ouvrez la page Sandbox et cliquez sur Throw + log. Quitter la page About journalise son avertissement beforeUnmount, et l’erreur interceptée est journalisée avec sa pile d’appels.

Adresse dans le navigateur :localhost:4312/sandbox
Application Vue de démonstration, page Bac à sable : une carte sur le routeur et les chemins d’erreur, avec un bouton qui lève et journalise une erreur et un bouton de navigation vers /missing ; dessous, le message de la dernière erreur interceptée.
Console du navigateur — La console du navigateur affiche les mêmes lignes que le terminal.
Terminal — vite dev
MARS-TECH Trace [router/beforeEach] - nav: /about → /sandbox (src/router/index.ts:25:2)
MARS-TECH Warning [view/About/lifecycle] - About beforeUnmount (src/views/About.vue:33:4)
MARS-TECH Trace [shell/mounted] - PageShell mounted (src/components/PageShell.vue:10:2)
MARS-TECH Trace [view/Sandbox] - Sandbox mounted (src/views/Sandbox.vue:10:2)
MARS-TECH Error [view/Sandbox/handler] - inline throw inside Sandbox handler (src/views/Sandbox.vue:26:4)
  Error: inline throw inside Sandbox handler
      at throwInline (http://localhost:4312/src/views/Sandbox.vue:38:11)
      at callWithErrorHandling (http://localhost:4312/node_modules/.vite/deps/vue.runtime.esm-bundler-BbnC8aGa.js?v=7dcb8c1b:1889:17)
      at callWithAsyncErrorHandling (http://localhost:4312/node_modules/.vite/deps/vue.runtime.esm-bundler-BbnC8aGa.js?v=7dcb8c1b:1896:15)
      at HTMLButtonElement.invoker (http://localhost:4312/node_modules/.vite/deps/vue.runtime.esm-bundler-BbnC8aGa.js?v=7dcb8c1b:7677:10)
Version texte de cette animation

La console du navigateur et le terminal Vite affichent les mêmes lignes. Dans le navigateur, le niveau est coloré et l’emplacement source est en italique ; dans le terminal, l’emplacement source est grisé. Une pile d’appels suit sa ligne dans le terminal et se trouve dans un groupe « stack » replié dans la console.

  1. Étape 1 sur 3 — Lancer le serveur de développement. Lancez le serveur de développement, ouvrez l’application et cliquez deux fois sur +1. Chaque appel à $debug, d’abord au démarrage de l’application puis dans l’action du store Pinia, affiche la même ligne dans la console du navigateur et dans le terminal Vite : niveau, étiquette, message et emplacement dans le code source.

    Sortie :

    $ npm run dev -- --port 4312 --strictPort
    MARS-TECH Info [main] - bootstrapping Vue app (src/main.ts:7:0)
    MARS-TECH Trace [component/App] - App.vue setup (src/App.vue:4:0)
    MARS-TECH Trace [router/beforeEach] - nav: / → / (src/router/index.ts:25:2)
    MARS-TECH Trace [shell/mounted] - PageShell mounted (src/components/PageShell.vue:10:2)
    MARS-TECH Info [SomeButton/mounted] - SomeButton mounted with variant=primary (src/components/SomeButton.vue:26:4)
    MARS-TECH Trace [shell/mounted] - PageShell mounted (src/components/PageShell.vue:10:2)
    MARS-TECH Trace [shell/mounted] - PageShell mounted (src/components/PageShell.vue:10:2)
    MARS-TECH Success [view/Home] - Home mounted (src/views/Home.vue:16:2)
    MARS-TECH Info [store/counter] - counter incremented to 1 (src/stores/counter.ts:8:6)
    MARS-TECH Info [store/counter] - counter incremented to 2 (src/stores/counter.ts:8:6)
  2. Étape 2 sur 3 — Naviguer vers une autre page. Ouvrez la page About et cliquez sur Fire delayed (500 ms). Les hooks de cycle de vie, une méthode appelée après un minuteur et un observateur journalisent tous via $debug, et chaque ligne indique le fichier, la ligne et la colonne de l’appel.

    Sortie :

    MARS-TECH Trace [router/beforeEach] - nav: / → /about (src/router/index.ts:25:2)
    MARS-TECH Trace [view/About/lifecycle] - About created (src/views/About.vue:27:4)
    MARS-TECH Trace [shell/mounted] - PageShell mounted (src/components/PageShell.vue:10:2)
    MARS-TECH Success [view/About/lifecycle] - About mounted (src/views/About.vue:30:4)
    MARS-TECH Info [view/About/method] - delayed fire #1 (src/views/About.vue:40:8)
    MARS-TECH Debug [view/About/watch] - delayedCount: 0 → 1 (src/views/About.vue:21:6)
  3. Étape 3 sur 3 — Provoquer une erreur. Ouvrez la page Sandbox et cliquez sur Throw + log. Quitter la page About journalise son avertissement beforeUnmount, et l’erreur interceptée est journalisée avec sa pile d’appels.

    Sortie :

    MARS-TECH Trace [router/beforeEach] - nav: /about → /sandbox (src/router/index.ts:25:2)
    MARS-TECH Warning [view/About/lifecycle] - About beforeUnmount (src/views/About.vue:33:4)
    MARS-TECH Trace [shell/mounted] - PageShell mounted (src/components/PageShell.vue:10:2)
    MARS-TECH Trace [view/Sandbox] - Sandbox mounted (src/views/Sandbox.vue:10:2)
    MARS-TECH Error [view/Sandbox/handler] - inline throw inside Sandbox handler (src/views/Sandbox.vue:26:4)
      Error: inline throw inside Sandbox handler
          at throwInline (http://localhost:4312/src/views/Sandbox.vue:38:11)
          at callWithErrorHandling (http://localhost:4312/node_modules/.vite/deps/vue.runtime.esm-bundler-BbnC8aGa.js?v=7dcb8c1b:1889:17)
          at callWithAsyncErrorHandling (http://localhost:4312/node_modules/.vite/deps/vue.runtime.esm-bundler-BbnC8aGa.js?v=7dcb8c1b:1896:15)
          at HTMLButtonElement.invoker (http://localhost:4312/node_modules/.vite/deps/vue.runtime.esm-bundler-BbnC8aGa.js?v=7dcb8c1b:7677:10)

Builds de production

Le plugin doit être enregistré pour vite build

C'est le plugin qui supprime les appels : enregistrez-le sans condition, dans le tableau plugins de premier niveau, jamais derrière command === "serve" ou apply: "serve". Voir Installer et enregistrer le plugin.

Avec vite build (client et SSR), chaque appel à l'identifiant global configuré (par défaut $debug) est remplacé par void 0. Les expressions d'arguments ne sont pas évaluées.

ts
// avant
$debug("debug", expensiveCompute());

// après build
void 0;

Vous pouvez donc placer un coût d'exécution dans les arguments sans craindre d'impact en production, et aucun environnement d'exécution SSR n'est embarqué.

Ce que couvre la suppression

La suppression couvre chaque module de votre propre code, quel que soit son type de fichier : .js et .ts, .vue, .cjs, un <script type="module"> en ligne dans index.html, un module virtuel, une page compilée par Svelte, Astro, MDX ou VitePress, ainsi que les workers et les scripts que Vite copie au lieu de les regrouper. Un fichier que la suppression ne sait pas analyser produit un avertissement de build ; il n'est jamais ignoré en silence.

Deux emplacements sont laissés tels quels :

  • Les dépendances, c'est-à-dire tout ce qui se trouve sous un répertoire node_modules/. Une bibliothèque qui appelle $debug doit livrer sa propre variante dont les appels sont supprimés, construite avec library.debugBuild : voir Création de bibliothèque.
  • public/. Vite copie ses fichiers tels quels dans le build : un appel à $debug dans l'un d'eux n'est donc jamais supprimé. Gardez $debug hors de public/.

Un <script> en ligne classique (sans type="module") ou un attribut on* en ligne dans votre HTML est copié tel quel dans la page construite : un appel à cet endroit ne peut pas être supprimé, et le build émet un avertissement pour chacun. Appelez plutôt $debug depuis du code de module. Un script en ligne classique s'exécute en outre avant le chargement de l'environnement d'exécution de développement : il lève donc aussi une erreur en développement.

Formes méthode et chaînée

Les formes méthode et chaînée sont supprimées aussi : $debug.time(...) → void 0, et un $debug.ns('x')('info') en ligne est réduit à void 0 (la chaîne entière, donc jamais de void 0(...)). Une fabrique de journaliseur dédié ($debug.ns('x')) est remplacée par une fonction vide appelable (() => {}) plutôt que par void 0 : un journaliseur stocké dans une variable (const log = $debug.ns('x')) reste sûr à appeler après la suppression, et la fonction vide est ensuite supprimée par l'élimination du code mort. Une instruction $debug.ns('x') isolée, dont la valeur n'est pas utilisée, est réduite à void 0 : du code sans points-virgules ne peut donc pas lire la fonction vide comme un appel sur la ligne précédente.

Une clé constante sur un objet global désigne la globale elle-même : globalThis['$debug'](...) et window["$debug"].time(...) sont supprimés comme leurs formes pointées. Une méthode calculée ($debug['time'](), globalThis.$debug['time']()) est laissée telle quelle et signalée par un avertissement de build, car elle lève une erreur en production. Il en va de même d'une référence qui n'est pas un appel (const d = $debug, ou $debug passé comme valeur) : seuls les appels sont supprimés. Une clé dynamique (globalThis[name]()) est laissée telle quelle aussi, sans avertissement : la suppression ne peut pas savoir ce qu'elle désigne. Appelez $debug directement, avec des noms statiques.

Code exécuté dans un Worker

vite build regroupe un worker (new Worker(new URL("./worker.js", import.meta.url)), ?worker, ?sharedworker, ?worker&inline) avec worker.plugins seul, jamais avec les plugins de premier niveau. Le plugin ajoute de lui-même sa suppression à worker.plugins, après vos propres plugins de worker : les workers sont donc traités sans rien à configurer. Un mtDebug() à vous dans worker.plugins ne remplace pas cette suppression : comme tout doublon, il peut se mettre en retrait. Avec library.debugBuild, un worker est traité dans les deux variantes : dist/debug/ garde les appels de votre code principal, pas ceux d'un worker.

Si un autre plugin remplace worker.plugins après que celui-ci y a ajouté sa suppression, le build principal supprime les appels du code du worker à sa place : dans le fichier que Vite émet pour lui, ou dans la chaîne que contient un import ?worker&inline. Un cas imbriqué lui échappe : voir Un plugin qui remplace worker.plugins tardivement.

Un worker n'a pas d'environnement d'exécution de développement à lui : avec vite dev, un $debug(...) nu dans un worker lève une ReferenceError. Appelez-le depuis la page, ou sous la forme globalThis.$debug?.(...) dans le code du worker.

Scripts que Vite copie

Vite ne regroupe un worker que si new URL(…) est écrit à l'intérieur de new Worker(…). Les références suivantes désignent au contraire un fichier que Vite copie tel quel :

  • une URL gardée dans une variable (const url = new URL("./worker.js", import.meta.url); new Worker(url)) ;
  • un service worker (navigator.serviceWorker.register(new URL(…))) ;
  • un worklet audio (audioWorklet.addModule(new URL(…))) ;
  • un import ?url.

Vite copie un tel script dans un fichier à part, ou l'intègre sous forme d'URL data: en base64 dans le chunk qui le désigne (quand le fichier est plus petit que build.assetsInlineLimit ou importé avec ?inline, et toujours en mode bibliothèque). Aucun point d'accroche de transformation ne voit ce texte : le plugin en supprime donc les appels là où il arrive, dans chaque fichier .js, .mjs ou .cjs que le build émet ainsi, et dans chaque URL data: JavaScript que contient un chunk ou une page. Avec library.debugBuild, un tel script est traité dans les deux variantes, comme un worker. Un script copié depuis une dépendance est laissé tel quel, comme les modules de la dépendance.

Trois sortes sont laissées telles quelles, et le build émet un avertissement pour chacune qui nomme $debug :

  • un script importé avec ?raw, qui est du texte que votre code demande à l'identique ;
  • un fichier TypeScript ou JSX (.ts, .tsx, .jsx) copié ainsi, que la suppression ne sait pas analyser : donnez-lui une extension .js, ou laissez Vite le regrouper avec new Worker(new URL("./worker.ts", import.meta.url)) ;
  • une URL data: d'un autre type dont le texte la nomme.

Limites connues

En dehors des dépendances, de public/ et d'une clé dynamique, décrits plus haut, trois configurations rares échappent à la suppression sans avertissement de build. Dans chacune, le build réussit et les appels $debug(...) sont livrés tels quels, littéraux des arguments compris. En production, rien n'installe $debug : ils lèvent une ReferenceError ou une TypeError quand ils s'exécutent.

Pages HTML chargées comme ressources

Vite copie une page HTML octet pour octet quand le code y fait référence comme à une ressource au lieu de la lister dans les entrées du build : new URL('./frame.html', import.meta.url) (par exemple comme src d'une <iframe>) ou import url from './frame.html?url' produit assets/frame-[hash].html sans modification. Les appels $debug(...) de ses blocs <script type="module"> en ligne et <script> classiques restent. Avec vite dev, la même page est servie avec l'environnement d'exécution : ses scripts de module loguent en développement et lèvent une erreur en production. Les pages listées dans les entrées du build sont traitées normalement.

N'appelez $debug que depuis du code de module que Vite regroupe, jamais depuis une page HTML chargée comme ressource.

Deux fichiers de même nom

Ce cas suppose des noms de fichiers de sortie sans empreinte à deux endroits : entryFileNames: 'assets/[name].js' dans la sortie des workers (worker.rollupOptions.output, ou worker.rolldownOptions.output avec Vite 8) et assetFileNames: 'assets/[name][extname]' dans la sortie du build principal (build.rollupOptions.output, ou build.rolldownOptions.output avec Vite 8). Si l'application importe un worker en ligne (./a/worker.js?worker&inline) et que Vite copie aussi un script de même nom de base dans un fichier à part (const url = new URL('./b/worker.js', import.meta.url); new Worker(url), plus grand que build.assetsInlineLimit), ce second fichier, copié dans assets/worker.js, garde ses appels $debug(...). Les noms avec empreinte par défaut ne sont pas concernés.

Gardez les noms avec empreinte par défaut, ou donnez des noms de fichiers distincts à vos workers et à vos scripts copiés.

Un plugin qui remplace worker.plugins tardivement

Le plugin ajoute sa suppression à worker.plugins depuis un point d'accroche config ordonné post. Un autre plugin en enforce: 'post', listé après mtDebug(), dont le point d'accroche config est lui aussi ordonné post peut remplacer cette liste ensuite (par exemple config.worker.plugins = () => []). Le build principal supprime alors lui-même les appels du code de chaque worker, comme le décrit Code exécuté dans un Worker, mais un worker ?worker&inline imbriqué dans un autre worker garde ses appels $debug(...). Sans un tel plugin, les workers imbriqués sont traités.

Ne laissez pas un plugin écraser worker.plugins tardivement. Si ce plugin est le vôtre ou se configure, faites en sorte que la liste qu'il définit contienne une nouvelle instance de mtDebug() : appelez mtDebug() à cet endroit, sans réutiliser celle de vos plugins de premier niveau. L'ajouter à votre propre worker.plugins ne sert à rien, puisque le plugin tardif remplace cette liste.

Et ensuite