Was man mit Vite-Plugins so alles machen kann

Veröffentlicht 18.06.2025

Timo Stovermann Software Development Engineer d.velop AG

Beitragsbild Vite Plugins

Wer in der Welt von JavaScript unterwegs ist, wird früher oder später mit einem Build-Tool in Berührung kommen. Vite spielt dabei seit mehreren Jahren eine immer größere Rolle. Neben der Performance und dem komfortablen Dev-Server ist das Plugin-System ein wichtiger Faktor. Viele beliebte Frameworks wie Vue, SvelteKit oder Nuxt setzen auf Vite-Plugins, um sich zu integrieren.

Wie diese Plugins funktionieren, was sie alles können und wie du selbst ein Plugin schreiben kannst, das erfährst du in diesem Blog-Post.

Wie funktionieren Vite-Plugins?

Vite-Plugins sind eine Erweiterung von Rollup-Plugins. Im Grunde sind es bloß Objekte mit einer Reihe von Methoden, den sogenannten Hooks. Diese werden automatisch während des Build-Prozesses und im Dev-Server aufgerufen. Die genaue Reihenfolge hängt von der Build-Config und auch von den verwendeten Plugins selbst ab. Aber grob lässt sich der Lebenszyklus folgendermaßen abbilden:

Bild zeigt den Vite Plugin Lebenszyklus

Falls mehrere Plugins vorhanden sind, die denselben Hook implementieren, werden sie in der Reihenfolge aufgerufen, in der sie in der vite.config.js registriert wurden. Ein Plugin kann sich allerdings auch über die Attribute enforce am Plugin und order am Hook “vordrängeln”.

Was können Vite-Plugins?

Insgesamt gibt es 32 Hooks, in diesen Hooks stehen 15 Utility-Functions zur Verfügung. Die Möglichkeiten, damit in den Build-Prozess einzugreifen, sind schier unendlich. Deshalb möchte ich an dieser Stelle nur kurz auf die einzelnen Möglichkeiten eingehen. Für eine genauere Dokumentation, schau in die Rollup- bzw. Vite-Dokumentationen. Die Beschreibungen dort sind allerdings auch etwas oberflächlich. Letztlich habe ich es mir dadurch erschlossen, dass ich einfach mal in einem Test-Projekt ausprobiert habe, was die einzelnen Hooks/Functions machen.

Configuration-Hooks

Es gibt drei Hooks, die man der Konfiguration zuordnen kann: config, configResolved und options. Sie können verwendet werden, um die in der vite.config.js angegebene und die von Vite/Rollup generierte Konfiguration auszulesen und zu manipulieren.

config

  • Verändern von Werten in der Konfiguration
  • Auslesen von mode (“production” oder “development”) und command (“build” oder “serve”)

configResolved

  • Auslesen der finalen Vite-Konfiguration

options

  • Auslesen und Verändern von Werten im Options-Objektes (Config-Objekt von Rollup)
  • Ob die Manipulation in der options- oder der config-Hook geschehen sollte, kommt darauf an, was man verändern will

Build-Hooks

Build-Hooks sind dazu gedacht, in den Build-Prozess einzugreifen. Sie können Informationen abgreifen sowie den Input und den Output verändern.

buildStart

  • Auslesen der finalen Rollup-Options
  • Hintergrundprozesse starten

resolveId

  • Die Inputs auf andere Pfade mappen
    • HTML-Referenzen: < script src=“/my-script.js“> => “./src/scripts/foobar.js“
    • JS-Imports: import “foobar” => “./src/scripts/foobar.js“
  • Externe Abhängigkeiten auflösen
  • Wird für jeden Entry-Point sowie für jede von dort aus referenzierte Datei/URL aufgerufen
  • Per Default wird versucht, den Pfad vom konfigurierten Project-Root aus aufzulösen
  • Falls dieser Pfad nicht gefunden wird, kommt es zu einem Fehler
  • Wenn man eine ID/einen Datei-Pfad returned, welcher nicht auf im Dateisystem existiert, wird dennoch kein Fehler geschmissen

load

  • Dateien von einer anderen Location lesen als im Quellcode angegeben
    • “./src/foobar.js“ => fs.readFileSync(“./tmp/src/foobar.js“, …)
  • Imports ins leere laufen lassen, indem man einfach eine leere Datei zurück gibt
    • “dummy-module” => return ““
  • Während resolveId im default nur schaut, ob die Datei existiert, wird sie hier tatsächlich gelesen
  • Man kann auch selbst den gewünschten Datei-Inhalt zurückgeben
  • Sobald ein Plugin hier nicht null zurückgibt, werden die load-Hooks der anderen Plugins übersprungen
  • Wird im Dev-Mode 1x beim ersten Aufrufen der jeweiligen Ressource getriggert (außer die Id wird in resolveId verändert, dann immer wieder)

transform

  • Verändern des Quellcodes, bevor der nächste Hook aufgerufen wird
  • Platzhalter ersetzen, Code hinzufügen/entfernen, Links verändern, …
  • Wird im Dev-Mode 1x beim ersten Aufrufen der jeweiligen Ressource und nach Änderungen an der Datei getriggert

moduleParsed

  • Informationen zu den Imports einer Datei einsehen
  • Aufruf, nachdem eine Datei vollständig von Rollup analysiert wurde

resolveDynamicImport

  • Auflösen von dynamic imports (return {id: “…”})
  • Ignorieren von dynamic imports (return false)
    • der import(“…”) Befehl im Source Code wird nicht verändert

buildEnd

  • Fehler auslesen und ggf. aussagekräftigere Log-Meldung geben
  • Wird am Ende des Build-Prozesses aufgerufen
  • Falls ein Fehler aufgetreten ist, wird dieser übergeben

tranformIndexHTML

  • Anpassungen an HTML-Files nachdem der Build abgeschlossen ist
  • Injecten von HTML-Tags in den oder
  • Anpassungen an Routen im Vite-Dev-Server
    • “/scripts/index.ts“=>”/src/scripts/index.ts””
  • Ähnlich zu transform
  • Im Dev-Mode, wenn IDs in resolveID angepasst werden, müssen HTML-Dateien hierüber transformed werden, da der transform-Hook dann nicht aufgerufen wird

onLog

  • Filtern der Log-Meldungen von Vite und Vite-Plugins
  • Log-Level ändern

Als ich das alles ausprobiert habe, fand ich es echt gruselig, wie so ein Plugin meine App verdrehen kann. Hier passt das Adjektiv “hacky” wirklich sehr gut. Angenommen, ich habe eine Javascript-File “./scripts/someStuff.js“ referenziert. Dann könnte so ein Plugin hingehen und die Href auf “http://boese-dinge-tun.com/passwoerter-abgreifen.js“ ändern. Oder es kann unseren Frontend-Quell-Code an einen externen Server senden. Oder es kann böswilligen Code injecten. Diese Probleme hat man zwar bei jeder Third-Party-Abhängigkeit, aber da das direkt am Build-Prozess beteiligt ist, fand ich das etwas erschreckend.

Allerdings ist es auch notwendig, dass Plugins so mächtig sind. Denn um von Vue-Komponenten zu nativem JavaScript zu kommen, muss natürlich einiges verändert werden.

Output-Generation-Hooks

Nach dem Abschluss des Builds kann ich auch noch einiges mit dem Output machen. Diese Hooks werden im Dev-Mode nicht aufgerufen, da dort kein Output generiert wird, sondern die Compilation In-Memory stattfindet.

outputOptions

  • Ersetzen oder Verändern der Output-Konfiguration

renderStart

  • Auslesen der finalen Output-Config

renderDynamicImport

  • Mit dynamischen Imports umgehen
  • Man kann dynamische Imports durch Funktionsaufrufe ersetzen
    • import(“foobar”) => doFoobarStuff(…)

resolveImportMeta

  • Injecten von Variablen
  • Ersetzt “import.meta.“ durch den zurückgegebenen Wert

resolveFileURL

  • Die URL von selbst generierten Dateien ändern

intro/outro/banner/ footer

  • Kommentare oder Javascript-Calls am Anfang oder Ende der Assets-Dateien ergänzen

augmentChunkHash

  • Den Hash festsetzen, um Caching zu verbessern
  • Den Cache anhand von externen Werten verändern
  • Der return-Value verändert den Wert, über welchen des Hash für die Datei gebildet wird
  • Per Default wird der Hash über den Datei-Inhalt gebildet

generateBundle

  • Dateien aus dem Output entfernen

writeBundle

  • Auslesen der Output-Options
    • dist-Directory
    • File-Hash-Schema
    • format
  • Dateien werden ins dist-Verzeichnis geschrieben
  • per Default passiert nichts weiter
  • Man kann zusätzliche Dateien schreiben

renderError

  • Fehler in generateBundle und writeBundle abfangen

closeBundle

  • Hintergrundprozesse beenden
  • Error beim Schreiben des dist-Verzeichnisses besser ausgeben

Dev-Server-Hooks

Diese Hooks sind nur im Dev-Mode verfügbar.

configureServer

  • Middleware-Handler hinzufügen
    • URL auf anderen Pfad mappen
    • Content direkt an den Client senden
  • Events an den Browser schicken

configurePreviewServer

  • configureServer für den Preview-Mode

closeWatcher

  • Wird aufgerufen, wenn der Dev-Server beendet wird
  • Hintergrundprozesse beenden

shouldTransformCachedModule

  • Vite zwingen, ein Module immer neu zu bauen anstatt es zu cachen

watchChange

  • Wird bei Änderungen an Dateien aufgerufen

handleHotUpdate

Utility-Functions

In den Hooks können verschiedene Functions aufgerufen werden, welche den Build anpassen. Manche Context-Funktions können überall aufgerufen werden, manche nur in bestimmten Hooks.

Function

  • Zusätzliche Informationen im Build-Kontext loggen

addWatchFile

  • Änderungen an Dateien, die nicht direkt referenziert sind, können so einen Rebuild triggern

emitFile

  • Selbst eine Datei generieren und ins dist-Verzeichnis legen
  • Eine Asset-Datei nicht im assets-Ordner ausliefern, sondern gleichberechtigt zu dem HTML-Dateien
  • Der angegebene fileName ist relativ zum dist-Verzeichnis

getFileName

  • Dateinamen von Assets bestimmen, die über this.emitFile erzeugt wurden

getCombinedSourceMaps

  • Source-Maps von Dateien abfragen
  • Nur in transform nutzbar

getModuleIds

  • Ausloggen aller Module (= Source-Dateien)

getModuleInfo

  • Informationen zum aktuellen Module abfragen
  • In Vite scheinen die meisten Werte null zu sein

getWatchFiles

  • Gibt eine Liste aller Dateien, die einen Rebuild triggern
  • Nützlich, falls man wissen muss, welche Dateien gewatched werden

load

  • Triggern des load-Hooks mit der übergebenen ID

meta

  • Version von Rollup abfragen
  • WatchMode abfragen

resolve

  • Triggern des resolveId-Hooks

parse

  • Code in einen AST umwandeln

Wie kann ich selber so ein Plugin schreiben?

Bei dem Ganzen hast du jetzt vermutlich keinen Durchblick behalten. Mir ging es ähnlich. Die Möglichkeiten sind schier unendlich. Außerdem sind die Dokumentationen von Rollup und Vite relativ oberflächlich und helfen nicht unbedingt dabei, reale Anwendungsfälle zu identifizieren. Deshalb schauen wir uns das mal an einem Beispiel an.

Wir wollen ein Plugin schreiben, welches die Datei-Struktur im dist-Verzeichnis anpassen kann. Die URL im Dev-Server soll auch anpassbar sein:

Um das Plugin möglichst generisch zu halten, übergeben wir die gewünschten Mappings als Parameter in die Factory-Function. Da wir den Mode und den Base-Pfad später benötigen, merken wir sie uns im configResolved-Hook:

Um die Input-Pfade zu Mappen nutzen wir die resolveID- und load-Hooks:

Der Build würde per Default bereits bei resolveId fehlschlagen, deswegen geben wir hier explizit die gewünschte ID zurück. Analog lesen wir für gemappte InputIDs die Datei unter dem im PluginOptions-Objekt angegebenen Pfad aus.

Damit ist’s aber noch nicht getan. Denn in den Input-HTML-Dateien stehen ja Referenzen auf andere Dateien. Diese sind allerdings relativ vom ursprünglichen Pfad der HTML-Datei aus. In src/pages/additional/bar.html wird auf ../scripts/foobar.ts verwiesen. Vom gemappten Pfad /page2.html wird unter ../scripts/foobar.ts natürlich nichts gefunden. Es müsste ./src/scripts/foobar.ts sein. Um die Referenzen im HTML zu fixen, kann der transform-Hook genutzt werden:

Die fixLinksInMappedFile-Funktion ersetzt im Datei-String alle Referenzen durch den korrekten relativen Pfad.

Für den Build haben wir es damit schon. Für den Dev-Mode muss eine Middleware registriert werden. Dabei wird wieder das PluginOptions-Objekt verwendet:

Das Ersetzen des Referenzen muss hier im transformIndexHTML-Hook geschehen:

Und fertig ist unser Plugin! Die Registrierung sieht wie folgt aus:

Fazit

Vite-Plugins sind ein sehr mächtiges Werkzeug und können in den verschiedensten Szenarien zum Einsatz kommen. Durch die vielen Möglichkeiten schaffen sie eine große Flexibilität darin, wie man eine Anwendung schreibt und im Source-Code strukturiert. Denn was am Ende dabei raus kommt, kann man ja vielfältig anpassen. In den Entwicklungs-Teams der d.velop AG nutzen wir Vite-Plugins bereits an einigen Stellen, z.B. zur Integration von EJS oder zur Generierung von HTML-Files auf Basis von Markdown.

Was genau man mit den Hooks machen sollte und wie, das hängt vom Anwendungsfall ab. Wenn du selber ein Plugin schreiben willst, wirst du nicht ums Ausprobieren drumherum kommen. Aber es lohnt sich definitiv, sich damit zu beschäftigen. Denn man kann sehr viel sehr cooles Zeug damit machen, soviel steht fest.

Dein Job bei der d.velop wartet auf Dich!

Du möchtest Teil des Teams werden? Wir freuen uns immer über Talente, die mit uns gemeinsam daran arbeiten, die Geschäftsprozesse in Unternehmen digital zu gestalten.

Autor:in

Timo Stovermann ist Software Development Engineer bei der d.velop.

Timo Stovermann Software Development Engineer d.velop AG