VitePress, eine Vue.js-Library, und das ganze npm-Geraffel

Seit dem letzten Artikel ist es zwar gar nicht so lange her, aber zuvor hatte ich zumindest hier eine größere Pause eingelegt. Aber falls man meinen sollte, dass ich in der Zeit untätig war – der Eindruck täuscht. Tatsächlich habe ich sogar mal einen Ausflug in die Welt von Excel und Visual Basic for Applications (VBA) gewagt – aber das ist eine ganz andere Geschichte. Seit einiger Zeit beschäftige ich mich jedoch wieder mit der Entwicklung einer Web-Anwendung, im Gegensatz zu früheren Zeiten nicht mehr auf Basis von PHP und dessen Frameworks, sondern mit Go (Golang) im Backend, während das Frontend eine Vue.js-Anwendung darstellt. Und damit wären wir auch bereits mittendrin in der Geschichte.

Eine Warnung vorweg – dieser Beitrag wird voraussichtlich weniger technisch als die meisten anderen hier, dafür mit mehr Anmerkungen und Meinungen gewürzt sein. Wen also meine persönlichen Ansichten nicht interessieren, möge die Seite einfach verlassen – oder aber für immer schweigen.

Es waren einmal… Back- und Frontend

Vom Backend gibt es nicht viel zu berichten, die Programmiersprache der Wahl lautet Go, hinzu kommt das HTTP Web Framework gin-gonic, mit dem ich bereits gute Erfahrungen gemacht hatte. Die Kommunikation mit dem Frontend geschieht über JSON, als Datenbank wird aktuell MariaDB eingesetzt, also insgesamt nichts Besonderes. Da ich mich bis dato eher der Backend-Entwicklung zugehörig gefühlt habe, war die Frontend-Seite schon spannender – hier fiel die Wahl auf Vue.js als JavaScript-Framework. Auch damit hatte ich schon einiges ausprobiert, z.B. die Admin-UI von einem WordPress-Plugin mittels Vue.js erstellt, der pragmatische Ansatz und die komponentenbasierte Entwicklung finden ebenfalls meinen Zuspruch, insofern erschien mir die Wahl nur konsequent. Denn sind wir ehrlich – Plain JavaScript macht einfach keinen Spaß. Zumindest ich konnte mich nie so recht damit anfreunden, dies änderte sich erst mit dem Aufkommen von jQuery. Doch die Entwicklung bleibt nicht stehen, außerdem wollte ich diesmal ein Frontend auf einer modernen Basis erstellen, die aus mehr als klassischen Web-Seiten besteht. Und da ich nebenbei an weiteren Herausforderungen interessiert war, sollte TypeScript zum Einsatz kommen, schließlich hatte ich mich auch im Backend dank Go an typsichere Entwicklung gewöhnt, da sollte das Frontend in nichts nachstehen.

Da ich mich nicht mit den Untiefen von CSS auseinander setzen wollte, sollte Bootstrap als CSS-Framework zum Einsatz kommen – auch hier wiederum ein Standard-Tool, und genau daher gut benutzbar, in steter Weiterentwicklung, und mit einem immer noch ansprechenden Design, das nicht zuletzt durch Themes wie denjenigen aus dem Open-Source-Projekt Bootswatch angepasst werden kann. Die Wahl der Technologien war somit getroffen. Während der Entwicklung wollte ich, sofern nötig und möglich, auf weitere Standard-Libraries, Komponentensammlungen etc. zurückgreifen. Beispielsweise in der Admin-UI, in der häufig Tabellen genutzt werden, oder für Benachrichtigungen an den Benutzer etc.. Diese UI-Elemente sollten jedoch, genau wie die gesamte UI, im Design von Bootstrap gestaltet sein. Diese simple Tatsache hörte sich einfacher an als sie war.

Von Komponenten und Libraries

Nehmen wir als Beispiel eine Tabellen-Komponente. Diese sollte einfach nur die Daten, die vom Backend geliefert werden, halbwegs ansprechend darstellen, und zwar im Design, das durch die Bootstrap-CSS-Definitionen festgelegt wird. Die Blättern-Funktion ist letztlich obligatorisch, eine Möglichkeit der Auswahl von Zeilen per Checkbox wurde ebenfalls schnell benötigt, verbunden mit dem Feedback per Events, welche Auswahl getroffen wurde usw.. Alles keine Raketenwissenschaft, insofern hatte ich mich auf die Suche nach entsprechenden Komponenten-Bibliotheken für Vue.js begeben. Davon gab es einige, als Standard-Bezeichnung hatte sich wohl irgendwas mit “DataTable” etabliert, die meisten Open Source, einige sogar kommerzieller Natur, teilweise im Rahmen von vollständigen UI-Toolkits. A propos UI-Toolkits, namentlich PrimeVue, Element-Plus, Naive UI, Vuetify, Vuestic, Agnostic UI, Qwik UI, Quasar… Ich denke, mehr muss ich gar nicht aufzählen. Besonders ärgerlich empfand ich erneut die Mischung zwischen Open Source und Kommerz. Da waren einige Basis-Elemente vermeintlich Open-Source, aber wenn man auf anspruchsvollere Elemente zurückgreifen wollte, war plötzlich irgendeine Jahreslizenz fällig. In kommerziellen Projekten ist dies natürlich kein Problem, aber da die Absicht besteht, das Projekt, sollte es sich einmal einem Zustand nähern, der sich zu einer Veröffentlichung eignen sollte, ebenso als Open Source freizugeben, fielen kommerzielle Angebote somit aus.

Gleiches galt für die Admin-UI-Templates – die meisten waren irgendwie kommerziell oder zumindest nur gegen Gebühr erhältlich, letztlich habe ich mich dann für “SB Admin” von Start Bootstrap entschieden, das beim Aufbau der Basis für die UI sehr gut geholfen hat. Und dank MIT Lizenz perfekt zu meinem Ansatz passte. Die phantastischen UI-Toolkits und Component Frameworks habe ich jedoch außen vor gelassen, bei meinen Tests hatte mich bei jedem einzelnen irgendein Aspekt derart gestört, dass ich lieber darauf verzichtet habe.

Wer sucht, der findet… oder nicht

Zurück zum Aspekt der Komponenten-Libraries am Beispiel der Data Table. Auch hier habe ich einige Libraries getestet, doch leider nichts Passendes gefunden. Um nicht missverstanden zu werden – es gibt einige sehr gute DataTable-Libraries, die eine enorme Funktionalität aufweisen. Die Entwickler dieser Vue-Komponenten haben einige Mühe und Zeit darauf verwendet, leistungsfähige Libraries bereitzustellen, und ich wäre der Letzte, der ihnen das nicht anerkennen würde. Und vielleicht war es nur mein Hang zum Perfektionismus (manche würden behaupten Pingeligkeit – oder auch Schlimmeres…), der z.B. in einer mit Bootstrap-CSS gestalteten UI auch das Pagination-Design von Bootstrap haben wollte. Es sollten genau die CSS-Klassen verwendet werden, die Bootstrap dafür vorsieht. Runde Buttons nützen mir da nicht viel, ebenso wenig die Möglichkeit der Nutzung eines Stylesheets, das das Bootstrap-Design nachahmt, aber eigene CSS-Klassen mitbringt, die natürlich bei einer Anpassung des Bootstrap-Themes, etwa einer simplen Farbänderung, dann auch wiederum angepasst werden müssten. Eine andere Komponenten-Library bot hingegen umfassende eigene CSS-Klassen an, alles super dokumentiert und vollständig anpassbar. Prima, aber genau damit wollte ich mich eben auch nicht beschäftigen, schließlich haben Tabellen per Bootstrap bereits ein brauchbares Design erhalten, und lassen sich weiterhin umfassend in allen möglichen Aspekten gestalten, von Farbvarianten über Mouseover, Rahmen, Größe allgemein usw.. Gesucht wurde also eine – gerne einfache – Tabellen-Komponente, die sich nicht nur am Bootstrap-Design orientiert, sondern genau dies implementiert bzw. nutzt.

Nun mag einem “BootstrapVue” in den Kopf kommen. Dessen Ansatz empfand ich zwar irgendwie überflüssig, aber auch das ist eine andere Geschichte, der entscheidende Punkt war, dass die aktuell als stabil gekennzeichnete Version von BootstrapVue nur Bootstrap in Version 4 enthält. Damit konnte ich es direkt ausschließen. Und “BootstrapVueNext” mit Support für Bootstrap 5 wird zum jetzigen Zeitpunkt noch immer als in “late stages of alpha version” bezeichnet – darauf zu bauen erschien mir ebenso keine gute Idee.

Komponenten, mal eben schnell..

Wie dem so war, unter den genannten Voraussetzungen und des üblichen Optimismus, auch genannt Selbstüberschätzung, dachte ich – warum eigentlich nicht eine kleine Tabellenkomponente bauen, die nur genau das beinhaltet, was in der Anwendung benötigt wird, und dabei einfach die Tabellen-CSS-Klassen von Bootstrap nutzt. Eine Sache für einen verregneten Sonntagnachmittag… Am Ende war der Sonntagnachmittag zwar längst vorüber, aber die Tabellen-Komponente funktionierte, und mit der Zeit kamen, wie nicht anders zu erwarten, auch immer weitere Features hinzu, etwa Bereitstellung von Inhalten per Slots, das Aufklappen von Zeilen, wobei die aufgeklappten Inhalte ebenfalls per Slot eingebunden werden, das Rendern von Inhalten per Callback-Funktion, Meldungen beim Laden von Daten oder bei leeren Tabellen, und natürlich die Anpassung des Designs per Bootstrap-CSS-Klassen, die der Tabellen-Komponente einfach übergeben werden können.

Ähnlich verlief die Entwicklung bei einer Komponente für Benachrichtigungs-Einblendungen, im Bootstrap-Jargon “Toasts” gekannt. Hier fanden sich initial jedoch keine fertigen Lösungen, allenfalls Beispiele oder Tutorials, wie die Bootstrap-Toast-Elemente unter Vue.js eingesetzt werden können. Also lag ebenso der Gedanke nahe, eine kleine Komponente dafür zu schreiben, die einfach nur an passender Stelle, etwa falls das Laden von Daten fehlschlägt, aufgerufen wird, so dass dem Benutzer die entsprechende Nachricht angezeigt wird. Um die Details, etwa Anzeigedauer, maximale Anzahl von Toast-Elementen usw., sollte sich die Library bitteschön selbst kümmern.

Und zu guter Letzt hatte ich mit dem Standard-Design von Select-Boxen zu kämpfen, denn trotz Bootstrap sind diese nicht wirklich “schön”, zumindest in der Betrachtung meiner Augen. Bei einer Einzelauswahl kommt hinzu, dass die weiteren Optionen nur nach der Anwahl per Maus oder nach dem Tippen sichtbar werden. Mir schwebte hingegen eine scrollbare Liste wie bei der Mehrfachauswahl (“select multiple”) vor, vielleicht ein wenig “schicker”. Und somit entstand zuletzt eine dritte Komponente, die genau diese Anforderungen realisierte.

Gewurschtel in der NPM-, Libraries- und  Abhängigkeiten-Hölle

Damit hatte ich drei Komponenten erstellt, die im Rahmen des Vue.js-Projektes prima funktionierten. Aber auch das erschien mir nur als die halbe Miete, etwa würde man in einem neuen Projekt die benötigten Dateien hin und her kopieren? Nicht wirklich sinnvoll, daher lautete das Ziel, die drei Komponenten zu extrahieren und gemeinsam in eine kleine Komponenten-Library zu packen. Und irgendwie begann damit das Unheil. Aber ich will nicht vorweg greifen, denn nach den ersten drölf Tutorials, Videos, Blog-Beiträgen und Plaudereien mit ChatGPT (den ich ganz gerne ChatOnkel nenne) schien die Lösung gar nicht so weit entfernt. Der Rahmen für die Komponenten-Library war auch schnell erstellt – nach einer der diversen Anleitungen. Dann noch flugs die Komponenten-Dateien inkl. Typ-Definitionen, Pinia-Store etc. rein kopiert, und – ja, irgendwas zusammen gefriemelt. Anders kann man es nicht nennen, und vielleicht irre ich mich auch in den folgenden Aussagen, aber ich schätze, dass genau dies der Weg ist, der in > 90% aller Fälle beschritten wird. Soll heißen, ich bewundere jeden, der sich in diesem Wust von Konfigurationsdateien, namentlich vite.config.ts, tsconfig.json, package.json, index.ts, plugin.ts und wie sie auch immer heißen mögen, wirklich auskennt. Also in dem Maße auskennt, dass man davon sprechen kann, genau zu wissen, was welche Option und Konfigurationsänderung bewirkt, und mit welchen Risiken und Nebenwirkungen zu rechnen ist. Natürlich lernt man mit der Zeit dazu, und package.json war mir auch bis dato nicht unbekannt. Ebenso npm und dessen wichtigste Kommandos. Aber neben npm gibt’s pnpm, yarn und ich wette, nächste Woche den nächsten Drei- bis Vier-Buchstaben-Package Manager, der irgendwo noch ein, zwei Sekunden bei der Einrichtung der Myriaden Dateien herausholt, die anscheinend bei Java- oder vielmehr TypeScript-Entwicklung inzwischen notwendig sind. Etwas beschönigend könnte man diese Entwicklungstendenz als “dynamisch” bezeichnen, aber um ehrlich zu sein, aus meiner Perspektive ist es einfach nur gruseliges Gewurschtel.

Auch diese Aussage sollte nicht missverstanden werden, denn insgesamt ist die Entwicklung einer Frontend-Anwendung mit Vue.js schon durchaus komfortabel, Live-Aktualisierung der per npm run dev gestarteten Anwendung sei Dank, und auch die Programmierung mit Vue.js macht mehr Spaß als im DOM mit getElementsByWhatever() herum suchen zu müssen. Wenn da nur diese zerklüfteten, chaotischen und somit letztlich suboptimal produktiven Infrastruktur-Tools nicht wären!

Minderbegabte Super-Tools

Ein anfängliches Problem war beispielsweise die Einbindung von Bootstrap. Zwar ist dies per npm install bootstrap @popperjs/core schnell installiert und in die package.json verfrachtet. Doch dies führte dazu, dass Bootstrap irgendwie doppelt eingebunden wurde, d.h. einmal in der Library, einmal in der Vue.js-App, was wiederum Darstellungsprobleme zur Folge hatte. Einerseits verständlich, andererseits – nun, bei den hyperintelligenten Build-Tools hätte ich irgendwie damit gerechnet, dass Derartiges vermieden wird, schließlich ist es fast immer reichlich sinnlos, denselben JavaScript- bzw. CSS-Code doppelt an den Client auszuliefern. Eine Lösung – ich will nicht von der Lösung sprechen, denn vermutlich gibt es noch ein Dutzend andere – war, Bootstrap & Co. in die “peerDependencies” zu verschieben. Damit muss Boostrap eben im Haupt-Projekt eingebunden und eingebaut werden, bei einem auf Bootstrap basierenden Design sollte nicht unbedingt etwas dagegen sprechen.

Schritt für Schritt ließen sich all diese Probleme mit der Zeit lösen, insgesamt hätte ich mir den Prozess der Erstellung der Komponenten-Library jedoch einfacher vorgestellt. Den ChatOnkel habe ich in der Zeit auch mit Fragen gelöchert, wobei er mir in einigen Fällen wirklich prima geholfen hat, in anderen hatte ich eher den Eindruck, mit einem völlig verpeilten und zugekifften Programmierpraktikanten zu “sprechen”. Aber gut, auch eine KI hat ihre jeweilige Tagesform. Mir fällt gerade auf, dass all dies bislang nur die Vorgeschichte war, denn irgendwie ließen sich diese Probleme alle sukzessive lösen, so dass die Library eigentlich bereit war zur Veröffentlichung. Denn wie erwähnt liegt mir der Open-Source-Gedanke nahe, und auch wenn ich nun kaum damit rechnen würde, den Vue.js-Komponenten-Markt aufzumischen, aber vielleicht gibt’s irgendwo da draußen jemanden, der vor ähnlichen Problemen steht wie ich, und vielleicht würde sich derjenige dann über eine weitere Alternative freuen. Andererseits nutzt niemand eine Library ohne ausreichende Dokumentation, insofern hieß es, ein paar Zeilen zu der inzwischen “goar-components” getauften Komponentensammlung zu schreiben, im besten Fall mit ein paar Beispielen zu ergänzen und schließlich zu veröffentlichen.

Nächster Halt: Dokumentation

Da ich keine literarisch anspruchsvollen Texte benötigte, ließ ich  übrigens ChatOnkel die langweilige Arbeit der Erstellung der Dokumentation erledigen, diese Aufgabe hat er recht gut gelöst. Fehlte nur noch die Präsentation, dafür wollte ich auf Tools zurückgreifen, die aus Markdown-Files und ein wenig Code eine ansprechende Dokumentations-Site bauen. Alles Standard und sicherlich auch an einem inzwischen zu heißen Sonntagnachmittag zu erledigen… Der erste Versuch bestand aus VuePress. Laut Beschreibung ein Vue.js-betriebener Generator für statische Web-Seiten, einfach, schnell, mit der Möglichkeit zur Verwendung von Vue-Komponenten doch eigentlich prädestiniert für die gestellte Aufgabe.

Erster Versuch: VuePress

Die ersten Versuche waren durchaus vielversprechend, ein paar Markdown-Dateien erstellt, so dass VuePress diese umwandeln und darstellen konnte. Die Probleme begannen beim Einbau der Beispiele, die natürlich auf die Komponenten-Library zurückgreifen sollten. Die VuePress-Dokumentation war auch nur semi sinnvoll, denn so ganz Standard war die Einbindung von Komponenten wohl doch nicht, plötzlich befand ich mich wieder auf der Suche nach geeigneten Informationen, und ebenfalls im Dialog mit ChatOnkel. Der verwechselte dummerweise ständig Version 1 und 2, was die Verwirrung nicht gerade weniger werden ließ. Nach den ersten Problemen mit irgendwelchen Imports (“Error: Dynamic require of "@vuepress/utils" is not supported", "Error: Dynamic require of "path" is not supported“) und weiteren mit Default Exports, die auch noch gelöst werden konnten, kam es jedoch zu einem ziemlich üblen Fehler beim Bauen der statischen Dateien. In der Entwicklungsumgebung hatte alles noch funktioniert, aber wenn ich npm run docs:build ausführte – Rumms! Man könnte sich jetzt fragen, was der Sinn einer Entwicklungsumgebung ist, wenn diese problemlos läuft, während die Vorbereitung für das richtige Deployment dann schief geht. Aber das nur am Rande…

Der Fehler zeigte sich wie folgt:

file:///home/geschke/vue/goar-components-docs/docs/.vuepress/.temp/.server/app.Dxz46QcL.mjs:7
import { Toast } from "bootstrap";
         ^^^^^
SyntaxError: Named export 'Toast' not found. The requested module 'bootstrap' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'bootstrap';
const { Toast } = pkg;

Nun soll also irgendwas mit CommonJS schief gelaufen sein? Bitte? CommonJS? Hatte ich das irgendwie, irgendwo, irgendwann konfiguriert? Oder gar gewollt? Eigentlich hatte ich mich an die Dokumentation von Bootstrap gehalten, denn von dieser hatte ich die besagte und hier angemeckerte Zeile entnommen. Und Bootstrap, vermutlich eines der am häufigsten verwendeten CSS-Frameworks soll nur als CommonJS-Modul verfügbar sein?

JavaScript-Fehler-Chaos mit VuePress

Natürlich habe ich daraufhin gegoogelt, tatsächlich aber erstaunlich wenig gefunden. Ein Hinweis zeigte zwar dasselbe Problem mit einer anderen Library, aber leider keine Lösung, denn genau die vorgeschlagene Syntax beim Importieren hatte ich nun einmal verwendet. Und zu guter Letzt wurden die Vorschläge von ChatOnkel auch nicht besser, eher im Gegenteil. Wir drehten uns im Kreise – erst sollte ich irgendeinen CommonJS-Import verwenden bzw. den Import ändern. Warum, hat sich mir nicht erschlossen. Außerdem funktionierte der in der Komponenten-Library verwendete Import genau so problemlos im Haupt-Projekt, das sich auch wiederum problemlos builden ließ. Und nochmal – laut Bootstrap-Dokumentation war alles korrekt, weshalb ich letztlich kein bisschen motiviert war, jenen Code in der Komponente zu ändern. Die nächsten Versuche drehten sich um dynamischen Import, Anpassen der bis dahin gar nicht vorhandenen vite.config.ts inklusive der üblichen Wiederholungen und Missverständnisse – ChatGPT 4o ist wirklich sehr, sehr gesprächig. Auch mein übliches Vorgehen, d.h. von Grund auf neu zu beginnen, in dem Fall das VuePress-Verzeichnis einfach zu löschen und streng nach Anleitung wieder aufzubauen, anschließend die Komponenten-Library wieder einzubauen, hat nicht zur Lösung beigetragen.

Es zeigte sich genau wie zuvor: In der Dev-Umgebung war noch alles in Ordnung, der statische Build zeigte hingegen denselben Fehler wie beschrieben. Meinen Dialog mit ChatOnkel stelle ich hier lieber nicht zur Verfügung, letztens hatte ich gelesen, “Männer beleidigen KI-Bots“, wobei ich normalerweise zwar zu denjenigen gehöre, die “Bitte” und “Danke” schreiben, aber ich habe hier schon öfter gefragt, ob der KI-Prakti überhaupt wahrgenommen hat, was ich geschrieben hatte. Die Vorschläge daraufhin führten aber auch nicht zum Erfolg, ich sollte beispielweise mit einem weiteren Vite-Plugin auf Jagd gehen, dann wiederum nur die ESM-Versionen von Bootstrap verwenden (warum das nicht längst passiert war, hat sich mir immer noch nicht erschlossen), dann noch ein CommonJS-Plugin installieren, auch wenn CommonJS ja anscheinend den Kern des Problems darstellte, irgendwie eine vite.config.ts in den Build-Prozess von VuePress einfügen, dabei noch mehr Konfigurationsoptionen verwenden als standardmäßig bereits notwendig, und so weiter. Eine funktionierende Lösung war jedoch nicht in Sicht, weshalb ich tatsächlich an dieser Stelle aufgegeben hatte und mich nach Alternativen zu VuePress umschaute.

Nächster Versuch: VitePress

Auch dazu nutzte ich der besseren Stimmung wegen ChatOnkel, denn bei der Auswahl konnte er mir schon recht gut helfen, etwa bei der Gegenüberstellung von Eckdaten wie Anzahl Sterne bei GitHub, Aktivität der Entwicklung, Vue3-Unterstützung usw. der Vorschläge VitePress, Docsify, Nuxt Content und Gridsome. Da ich nicht mit Nuxt-Kanonen auf Rotkehlchen schießen wollte, Docsify mir vom Konzept nicht gefiel, und die Entwicklung von Gridsome eingeschlafen zu sein schien, entschied ich mich für VitePress. Laut KI-Bot “für moderne, leichtgewichtige Dokumentationsseiten mit Vite und Vue 3” bestens geeignet.

Grau ist alle Theorie – und so weiter… also erstmal nach Anleitung vorgegangen und ein neues VitePress-Projekt angelegt, was auch nach inklusive der üblichen npm-Installationsorgien schnell erledigt war. Um auszuschließen, dass die Probleme vielleicht doch mit irgendwelchen seltsamen Eigenschaften meines eigenen Werkes zusammenhängen, wollte ich diesmal jedoch nicht die goar-components-Library einbinden, sondern nur ein kleines, einfaches Beispiel von Bootstrap einbauen: Ein Button soll bei Klick ein “Toast” darstellen. Übernommen aus der Dokumentation von Bootstrap, nur aufgebaut als Vue.js-Komponente.

Behutsamer Beginn bei Bootstrap

Der Weg dorthin kam mir zwar etwas umständlich vor, aber es funktionierte: Zunächst definierte ich eine Komponente unter docs/.vitepress/components/. Darin befindet sich der Code, der notwendig ist, um die Bootstrap-Elemente einzubinden, zunächst also beispielsweise den “Toast”. Im Markdown-File wird die erstellte Vue-Komponente schließlich importiert und verwendet. Das funktionierte zunächst, aber wiederum nur in der Entwicklungsumgebung. Sobald ich versuchte, statische Dateien zu erstellen, wurde dies mit einer Fehlermeldung quittiert: “ReferenceError: document is not defined…” usw.. Somit hatte ich das nächste, gruselige Problem gefunden. Im Nachhinein ist natürlich alles eindeutig und verständlich – beim Bau der statischen Seiten nutzt VitePress Server Side Rendering (SSR), und dort ist das document-Objekt, das von Bootstrap benutzt wird, nicht verfügbar. Auch hier startete ich wieder mit der Suche nach Lösungen mit Hilfe von ChatGPT, aber das führte nur zu Wiederholungen, man solle die Zeile “import { Toast } from 'bootstrap'” ändern – genau das ging jedoch nicht, da diese genau so – und damit genau wie in der Boostrap-Dokumentation angegeben – in der Komponentenbibliothek eingebaut war. Und diese wollte ich sicherlich nicht ändern, nur damit das Dokumentations-Tool zufrieden ist. Weitere Vorschläge drehten sich dann um irgendwelche SSR-Einstellungen in der Datei docs/.vitepress/config.ts usw., aber auch das führte zu nichts. Letztlich hätte ich Zeit gewonnen, wenn ich etwas früher in den SSR-Abschnitt von VitePress geschaut hätte, denn dort gab es Hinweise auf einen Helper namens defineClientComponent:

<script setup>
import { defineClientComponent } from 'vitepress'

const ClientComp = defineClientComponent(() => {
  return import('component-that-access-window-on-import')
})
</script>

<template>
  <ClientComp />
</template>

Damit funktionierte immerhin die kleine Test-Komponente. VitePress erstellte die statischen Dateien – endlich. Von dort sollte es doch nur ein kleiner Schritt zum Einbinden der goar-components-Library sein! Ja, nein, vielleicht?

Nochmal von ganz, ganz vorne!

Selbstverständlich – nicht! Denn als ich die Komponenten-Library eingebunden hatte, kam es erneut zu einem der bereits genannten Fehler, ich weiß gar nicht mehr, ob es die CommonJS-Geschichte oder irgendetwas Anderes war, jedenfalls funktionierte es nicht. Und das, obwohl, wie bereits erwähnt, die Generierung der eigentlichen Anwendung keine Probleme verursacht hatte, denn das wäre mir wesentlich früher aufgefallen. Also entschied ich mich, erstmal drei Schritte zurück zu gehen, soll heißen, die Komponenten-Library sukzessive neu aufzubauen. Schließlich konnte es nur noch daran liegen, dass irgendeine der gefühlt Myriaden Konfigurationsoptionen an der Misere Schuld hatte, doch mir war beim besten Willen nicht klar, welche. Somit erstellte ich erst eine neue, minimale Library, zunächst mit einer Test-Komponente, die nur aus dem besagten Bootstrap-“Toast”-Element bestand. Als das wundersamerweise funktionierte, fügte ich die weiteren Vue-Komponenten hinzu. Zuletzt hakte die in einer Komponente verwendete Pinia-Library noch ein wenig, aber auch das ließ sich letztlich lösen.

Um diesen Beitrag nicht weiter in epische Längen zu überführen, sollte ich vielleicht noch erwähnen, dass der letzte Schritt darin bestand, die Komponente-Library als dynamischen Import VitePress hinzuzufügen, d.h. die Einbindung in deren index.ts sieht wie folgt aus:

export default {
  [...]

  async enhanceApp({ app }: { app: App }) {
    if (!import.meta.env.SSR) {
      const module = await import('goar-components');
      const GoarComponents = module.GoarComponents; //was: .default;
    
      if (GoarComponents && typeof GoarComponents.install === 'function') {
        app.use(GoarComponents);
      } // else fail silently 
    }
  }
} satisfies Theme;

Tatsächlich muss hier “module.GoarComponents” anstatt “module.default” aufgerufen werden, da die install-Methode in der Komponenten-Library so definiert ist.

Am Ende wird alles gut…

Damit funktionierte nun nicht nur die Dev-Umgebung, sondern auch die Generierung der statischen Dateien. Dennoch habe ich mir schon des Öfteren die Frage nach dem Sinn einer Entwicklungsumgebung gestellt, die problemlos funktioniert, aber die Überführung in statische Dateien derartige Probleme verursacht. Zumindest ich verstehe eine Dev-Umgebung so, dass sie nicht nur der Bequemlichkeit während des Entwickelns dient, sondern auch darauf ausgelegt sein sollte, die generelle Funktionsfähigkeit der erstellten Software so zu simulieren, dass der letzte Schritt, also das Deployment, bzw. im konkreten Fall das Erstellen der statischen Dateien eine reine Formalität hätte sein müssen. Hier aber zeigte sich das genaue Gegenteil, was zumindest aus meiner Sicht den Sinn einer Dev-Umgebung ad absurdum führt.

Aber gut, diese Hürden waren nun endlich bewältigt, so dass ich insgesamt nun eine funktionsfähige Fassung von VitePress hatte, die nicht nur die Dokumentation, sondern auch Beispiele der goar-components-Library anzeigte. Und das funktionierte sogar nicht nur in der Dev-Umgebung, sondern ließ sich zudem auch in statische Dateien umwandeln. Was wollte ich mehr? Ganz einfach – es sollte halbwegs anständig aussehen, und nicht so, als hätte man mehrere CSS-Frameworks und somit Designs einmal durch den Mixer gejagt. Schließlich verwendete ich Bootstrap CSS, und damit auch dessen Design bereits in den Komponenten, somit wäre es schön gewesen, wenn das Aussehen der Dokumentations-Site nicht noch den VitePress-eigenen Stil beinhaltet hätte. Also begab ich mich in die Untiefen der Gestaltung von VitePress-Seiten…

…aber so weit sind wir noch nicht!

Wobei – kurz nochmal an de Anfang zurück – was wollte ich eigentlich? Ach ja, nur eine kleine Vue-Komponenten-Library veröffentlichen. Dazu noch ein wenig Dokumentation schreiben, dabei ein Tool verwenden, das einen darin unterstützte. Inzwischen hatte ich jedoch die diversen Konfigurationsdateien dreimal umgestrickt, die Library einmal komplett neu aufgebaut, mich mit viel zu vielen Konfigurationsoptionen, JavaScript-Sprachfassungen, Server-Side-Rendering-Problemen beschäftigt, da sollte doch wenigstens die Nutzung – und ich betone hierbei den Begriff Nutzung – von VitePress in Bezug auf eine Anpassung des Designs keine größere Herausforderung mehr sein, oder?

Mein Ziel bestand nur darin, die CSS-Stile des Default-Themes von VitePress auszutauschen bzw. erst gar nicht zu verwenden, sondern auf Bootstrap zurückzugreifen. Das Bootstrap-CSS war sowieso eingebunden, da es von der Komponenten-Library bzw. deren Beispielen benötigt wurde.

VitePress, Themes, CSS & Co.

Zunächst schaute ich mir die Möglichkeiten der Anpassung des Default-Themes an. Davon gab es eine Menge, ebenfalls ließen sich CSS-Einstellungen ändern, und zwar sehr detailliert in einer recht langen Liste. Aber vielleicht gab es ja bereits eine Lösung, also suchte ich nach VitePress-Themes, von denen es zu meiner Überraschung aber gar nicht allzu viele gab. So finden sich in der “Awesome VitePress v1“-Liste nur rund ein Dutzend Themes, von denen auch nicht alle für Dokumentations-Sites geeignet waren, darüber hinaus war kein Theme vorhanden, das Bootstrap beinhaltet hätte. Dennoch habe ich einen näheren Blick auf ein paar Themes geworfen – und war reichlich erstaunt. Warum bestanden die Themes schon wieder aus dermaßen viel Code? Also nicht etwa aus HTML- oder Vue-Code, sondern irgendwelchen Komponenten, TypeScript und sonstigem. Bereits das Default-Theme von VitePress bestand aus diversen “Composables“, dazu kamen Komponenten, in denen wiederum HTML-Code, “scoped” CSS, und nicht zuletzt JavaScript bzw. TypeScript enthalten war. Echt jetzt? Ich hätte vielleicht noch mit einer Template-Sprache à la Twig gerechnet, um eben Funktionalität von Design zu trennen, aber gerade im Default-Theme war dies nicht gegeben. Eine Anpassung, etwa um eine Navigationsleiste oder die “Sidebar” mit den von Bootstrap bereitgestellten CSS-Klassen zu erstellen, war somit unmöglich.

Nochmal zur Verdeutlichung – das CSS des Default-Themes lässt sich umfangreich anpassen, d.h. Farben, Größen, Schriften ändern etc. ist alles möglich. Aber die HTML-Struktur selbst zu ändern, um z.B. andere CSS-Klassen zu verwenden als “VP…irgendwas” – keine Chance! Nun gut, vielleicht hätte ich das vorher mal kurz checken sollen, und wäre vermutlich bei Hugo oder ähnlichem gelandet, hätte damit aber wiederum auch einigen Aufwand mit der Einbindung von Vue.js-Komponenten gehabt. Genau das wollte ich mit der Verwendung eines speziellen Dokumentations-Tools, das noch dazu vom Vue.js-Projekt selbst verwendet wird, vermeiden. Doch nun blieb mir wohl nichts Anderes übrig, als zu allem Überfluss noch ein eigenes Theme für VitePress bauen zu müssen, nur um die Nutzung des VitePress-eigenen CSS zu verhindern bzw. Bootstrap CSS einzusetzen. Wer an dieser Stelle an das Emoticon “Hand vor Kopf” denkt – genau so! Mehrfach.

Und zwischendurch ein VitePress-Theme

Glücklicherweise waren die ersten Schritte zu einem eigenen Theme relativ einfach – man baue sich ein Layout, das aus einer Navigationsleiste, der Sidebar und dem Haupt-Content-Teil besteht, würzt das ganze mit ein paar Bootstrap-Einlagen, rühre ein wenig herum, und fertig. Das klappte tatsächlich recht gut, es war kein Problem, an die Daten der Sidebar (definiert in der config.mts) bzw. des Themes zu gelangen, um etwa Links auf weitere Websites (“socialLinks“), Navigation, Titel, Icon usw. zu gelangen. Letztlich besteht ein VitePress-Theme somit aus Vue-Komponenten, mit all deren Möglichkeiten. Dabei wird der Inhaltsbereich, also das, was in den Markdown-Dateien enthalten ist, mittels “<Content/>” eingebunden. Soweit kein Problem – eigentlich. Denn im Inhalt sind beispielsweise auch Tabellen oder Code-Fragmente. Diese sind fertig gerendert. Und schon stand das nächste Problem vor der Tür.

Fertig gerendert bedeutet dabei, dass Tabellen, Code etc. auch bereits CSS-Styles enthalten, die angewendet werden, wenn der Inhalt in der Klasse “vp-doc” eingebettet ist. Eine Anpassung von Farben, Schriften etc. ist somit zwar aufwändig, aber relativ leicht möglich, und zwar in der inklusive Kommentaren über 500 Zeilen langen “vars.css” von VitePress. Klingt vielleicht auf den ersten Blick nicht übel, aber letztlich war eine derartige Anpassung gar nicht das Ziel. Schließlich wollte ich einfach nur Bootstrap-CSS-Stile verwenden – und damit deren Design, Farben, etc.. Vor allem sollte die Ausgabe durch Bootstrap-Themes änderbar sein, etwa um von einem hellen auf ein dunkles Theme umschalten zu können. VitePress besitzt zwar eine derartige Vorgabe in besagter CSS-Datei, aber hierfür hätte man sich wieder eine komplette Farbstruktur aus den Fingern saugen müssen. Ein kleines Beispiel soll dies verdeutlichen, der folgende Ausschnitt zeigt einen Teil der Farbdefinitionen von Code-Fragmenten, zunächst der Ausschnitt aus der “vars.css” von VitePress:

--vp-code-line-highlight-color: var(--vp-c-default-soft);
--vp-code-line-number-color: var(--vp-c-text-3);

--vp-code-line-diff-add-color: var(--vp-c-success-soft);
--vp-code-line-diff-add-symbol-color: var(--vp-c-success-1);

--vp-code-line-diff-remove-color: var(--vp-c-danger-soft);
--vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1);

VitePress arbeitet hier mit CSS-Variablen, die wiederum an anderer Stelle definiert werden, so ist “--vp-c-default-soft” beispielsweise im hellen Theme ein Grauton. Glücklicherweise bietet Bootstrap inzwischen eigene CSS-Variablen an, insofern habe ich diesen Teil wie folgt “übersetzt”:

--vp-code-line-highlight-color: var(--bs-gray-400);
--vp-code-line-number-color: var(--bs-gray-900);

--vp-code-line-diff-add-color: var(--bs-success-bg-subtle);
--vp-code-line-diff-add-symbol-color: var(--bs-success);

--vp-code-line-diff-remove-color: var(--bs-danger-bg-subtle);
--vp-code-line-diff-remove-symbol-color: var(--bs-danger);

Das funktioniert durchaus, aber da Bootstrap natürlich auch nicht alle VitePress-Stile beinhaltet, gibt es viele Stellen, die einfach noch unberücksichtigt bzw. nicht getestet worden sind, da ich zunächst nur ein VitePress-Theme erstellen wollte, das für meine Anwendungszwecke funktioniert. Denn letztlich hält sich der Spaß bei diesem Prozess – Variablen suchen, Farben suchen, Bootstrap-Alternativen suchen, einsetzen, testen, und wieder von vorne – auch in durchaus engen Grenzen.

Zwischen Markdown, Plugins und Quellcode

Richtig suboptimal wurde es jedoch erst bei Tabellen. Auch diese kommen fertig gerendert in den Inhaltsbereich. Zum Umsetzung des Markdown-Codes in HTML nutzt VitePress den Markdown-Parser markdown-it nebst einigen Plugins. Bis hierhin alles super, nur zeigt die Dokumentation an keiner Stelle, wie sich der HTML-Code anpassen lässt. Ja, man kann irgendwie per Slots Code an gewissen Stellen einfügen, aber dieses Feature brauchte ich gar nicht. Bootstrap hält es für eine gute Idee, Tabellen als “opt-in” zu definieren, soll heißen, sie werden nur im Bootstrap-Stil angezeigt, wenn die Klasse “table” enthalten ist (‘<table class="table">‘…). Also mussten den gerenderten Tabellen irgendwie CSS-Klassen hinzugefügt werden können. Ich kürze mal ein wenig ab, zunächst hatte ich es per Regex-Plugin von markdown-it probiert, was jedoch krachend gescheitert ist. Letztlich bin ich im Quellcode der Datei gelandet, in der VitePress markdown-it mit seinen Plugins und diversen Einstellungen zusammenbaut und über folgenden Code gestolpert:

md.renderer.rules.table_open = function (tokens, idx, options, env, self) {
    return '<table tabindex="0">\n'
  }

Das ließ sich dann in der config.mts umstellen in:

export default defineConfig({

  [...]
  
  markdown: {
    theme: 'github-light',
    config(md) {
      md.renderer.rules.table_open = function (tokens, idx, options, env, self) {
        return '<table class="table table-striped" tabindex="0">\n'
      }
    }
  }
})

Dummerweise auch eher nicht konfigurierbar, aber wenigstens weniger unhübsch als die Standard-Tabellen ohne Bootstrap-Design. Das Hinzufügen von Attributen für einzelne Tabellen bzw. grundsätzlich einzelne Elemente sollte mit Plugins möglich sein (z.B. markdown-it-attrs), die sich ebenfalls in jener Datei einbinden lassen, aber mir ging es darum, zunächst ein Standard-Design für alle Tabellen festzulegen.

Theme- oder Kern-Funktionalität? API oder nicht?

Und nein, die bisherigen Kleinigkeiten waren noch nicht alles. Denn VitePress bietet unter dem jeweiligen Inhalt ganz nette Links zum “Blättern” zwischen den Seiten, also letztlich zum “Vor” und “Zurück”-Blättern. Diese Links basieren auf der durch die Sidebar vorgegebene Navigation, somit ist es eine Baumstruktur, und man gelangt immer auf den nächsten bzw. vorherigen Knoten. Auf der ersten Seite existiert kein Vorgänger, genau wie auf der letzten Seite kein Nachfolger vorhanden ist. Schön und gut, auch nicht weiter schwierig, doch im entsprechenden Layout-Template müssen diese Daten dafür nun einmal vorliegen, um die Links entsprechend zu gestalten. Und woher kommen diese Daten? Tja, mit genau der Frage habe ich mich ein wenig länger auseinander gesetzt. Erwartet hätte ich “irgendwas aus dem Sidebar-Objekt”. Schließlich wird in der “themeConfig” die “sidebar” definiert, an die Daten des Themes gelangt man mit “const { theme } = useData()“, die Sidebar wäre “const sidebar = theme.value.sidebar“. Und nun? Genau, das war’s. Man erhält die Daten – genauso roh wie sie in der config-Datei definiert wurden. Mehr nicht.

Nun enthält man per “useData()” noch mehr Angaben über die jeweilige Seite, wie in der VitePress-Dokumentation unter “Runtime API” beschrieben wird. Aber irgendwie musste das Default-Theme von VitePress doch an die Daten für “Vor” und “Zurück” gelangen? Und tatsächlich, das VitePress Default-Theme bedient sich einer Funktion “usePrevNext()“. Klingt auch reichlich logisch, doch wo ist diese definiert? Yeah, im Theme selbst! Also genaugenommen in dem Code, der im Verzeichnis “composables” enthalten ist. Nun gut, der Code ist noch etwas umfangreicher, prüft und macht noch irgendwas mit “frontmatter“, was sich auf den “Vorspann” einer Markdown-Datei bezieht, in dem sich noch einige Einstellungen für die jeweilige Seite treffen lassen, aber grundsätzlich besteht die Aufgabe der Funktion darin, die URLs auf die nächste bzw. vorherige Seite zu generieren. Aber was um alles in der Welt ist dieses Codefragment ein Bestandteil des Themes und nicht einer API, die diesen Namen auch verdient? Aus meiner Sicht würde es in den Kern von VitePress bzw. zumindest in ein Sidebar-Objekt gehören. Ergo flugs mal nachgeschaut, was andere Themes an der Stelle zu bieten haben. Bei einem Theme habe ich dann genau das gefunden, was ich befürchtet hatte – eine 1:1-Kopie der “composables” aus dem VitePress DefaultTheme… Klar, kann man machen, ist aber… zumindest suboptimal. Noch suboptimaler wäre es jedoch, Code, der sowieso schon existiert und vermutlich als Bestandteil des DefaultThemes erprobt und gut getestet ist, neu zu erstellen, also habe ich mit einigen Bauchschmerzen ebenfalls für diesen Weg entschieden, kleinere Anpassungen inklusive, da ich nicht auch noch sämtliche anderen Verzeichnisse mit diversen Code-Fragmenten aus dem DefaultTheme kopieren wollte.

Dennoch kann ich nicht behaupten, dass mir diese Lösung gefällt. Sicher, wenn man das DefaultTheme von VitePress nutzt, ist alles in Butter. Alle Funktionalitäten stehen zur Verfügung, das CSS lässt sich in den Farben, Schriften usw. anpassen, fertig. Aber genau dann ist man eben auf das CSS-“Framework” von VitePress angewiesen und kann dieses nicht einfach durch ein anderes austauschen. Beispielsweise gibt es ein “Starter-Template” für Tailwind CSS. Aber auch das erweitert nur das Default-Theme. Es sorgt dafür, dass Tailwind CSS eingebunden wird, der Rest wird dem Default-Theme überlassen. Zwar habe ich es nicht getestet, aber mich würde schon interessieren, wie sich wirklich das Layout oder etwa Tabellen damit gestalten lassen.

The End (erstmal)

Da ich nur ein simples Theme für die Bereitstellung der Dokumentation benötige, habe ich die weiteren Features des Default-Themes, etwa den Aufbau einer Homepage, die sich von den restlichen Seiten unterscheidet, nicht weiter berücksichtigt. Somit bietet das Theme auch nur eine minimale Lösung ohne besondere Features, und momentan ist es auch noch fest im Dokumentations-Repository von goar-components verfügbar. Vielleicht veröffentliche ich es noch getrennt davon als eigenständiges VitePress-Theme, aber zuvor muss ich mich noch mit dem Veröffentlichungsprozess an sich per npm auseinandersetzen, und davor die Dokumentation um ein paar Beispiele erweitern. Und die Sidebar vielleicht ein wenig aufhübschen. Es wird also nicht langweilig – immerhin könnte man dies als positiven Effekt ansehen.

Update: Nun ist’s öffentlich!

Wie aufregend, (m)ein erstes NPM-Paket! Obwohl – im Vergleich zu der zuvor beschriebenen Odyssee war die Veröffentlichung auf und per npm schon bemerkenswert unauffällig, fast schon langweilig, denn zu meiner Überraschung klappte dieser Schritt ohne große Verrenkungen. Auch die Bereitstellung der Dokumentation auf GitHub-Pages war eher unspektakulär, hierfür genügte es, sich an die Dokumentation von VitePress und die bereitgestellte Deploy-Action zu halten.

Somit können die URLs zum Paket goar-components und dessen Dokumentation nun auch hier genannt werden. Wobei Letztere auch nach wie vor in Arbeit ist, es fehlen noch das eine oder andere Beispiel, außerdem die Dokumentation einer Komponente.

Tags:
Kategorie: Programmierung