Du brauchst diese Abhängigkeit vielleicht gar nicht
Im letzten Jahr oder so hat NodeJS eine Menge neuer Features bekommen, die es früher nur gab, wenn man verschiedene Abhängigkeiten installiert hat. Aber sind diese Features ausreichend, oder macht es immer noch Sinn, weiterhin Drittanbieter-Abhängigkeiten für manche dieser Features zu nutzen? Schauen wir mal.
Lass uns ein paar Abhängigkeiten durchgehen, die ich bis vor Kurzem mit NodeJS benutzt habe. Für jede Abhängigkeit schaue ich mir an, warum wir sie überhaupt brauchen könnten, und wie der NodeJS-Ersatz aussieht. Dann beschreibe ich ein paar Einschränkungen, die es schwer machen könnten, die Abhängigkeit loszuwerden, und versuche schließlich einzuschätzen, wie groß der Vorteil wirklich ist, wenn man sie weglässt.
TS-Node, TSX
Das Tool tsx hat wahrscheinlich den beschissensten Namen aller Zeiten. Warum sollte man sein Kommando zum Ausführen von TypeScript-Dateien nach der Dateiendung benennen, die man für manche dieser Dateien verwendet? Aber ich schweife ab.
Manchmal hat man TypeScript-Quelldateien, und beim Entwickeln will man sie einfach direkt ausführen, ohne sich um den Kompilierungsschritt zu kümmern. Früher haben wir ts-node benutzt, jetzt ist tsx beliebter, aber beide machen im Grunde das Gleiche: TypeScript-Dateien laden, sie spontan zu JavaScript kompilieren und transparent an Node zur Ausführung weitergeben.
Es stellt sich heraus, dass NodeJS das mittlerweile von selbst kann. Du brauchst nicht mal irgendwelche speziellen Flags – node ./index.ts wird index.ts laden und ausführen, als wäre es ganz normales altes JavaScript.
Einschränkungen
- Standardmäßig macht das nur Type-Stripping, was bedeutet, dass du auf eine Teilmenge der TypeScript-Syntax beschränkt bist. Wenn du wirklich Enums oder Parameter Properties brauchst, kannst du
--experimental-transform-typesverwenden. - Wenn du eine TypeScript-Datei importierst, musst du die richtige
.ts-Endung verwenden statt.js. Das erfordert ein bisschen Konfiguration fürtscund die IDE, damit keine roten Kringel auftauchen – du brauchstallowImportingTsExtensionsaktiviert, und möglicherweiserewriteRelativeImportExtensions, je nachdem wie du deinen Code für Production transpilierst. - Du wirst wahrscheinlich immer noch den Kompilierungsschritt machen müssen, wenn du deinen Code veröffentlichst. NodeJS macht überhaupt kein Type-Stripping für Packages, die in
node_modulesinstalliert sind, und empfiehlt generell, deinen Code vor dem Release oder der Ausführung in Production noch zu kompilieren. NodeJS macht auch kein Type-Checking, also brauchst du diesen Schritt immer noch, um sicherzustellen, dass deine TypeScript-Typen auch wirklich Sinn ergeben. (Hinweis: Während NodeJS kein Type-Stripping für Dependencies macht, funktioniert es bei einem Monorepo-Setup, wo lokale Packages als Symlinks innode_moduleslanden, wie erwartet – NodeJS löst erst den Symlink auf und prüft dann, ob die aufgelöste Datei am richtigen Ort ist.)
Lohnt es sich?
Wie einfach diese Änderung ist, hängt stark davon ab, wie viel du verschiedene TypeScript-Features nutzt. Meistens bleibe ich bei der erasableSyntaxOnly-Teilmenge von TypeScript, aber wenn du viele Enums benutzt, möchtest du vielleicht --experimental-transform-types ausprobieren, und wenn du viele Decorators nutzt, willst du diese Änderung vielleicht ganz überspringen.
tsx loszuwerden erlaubt dir, bis zu 25MB Package-Space loszuwerden, hauptsächlich in Form von esbuild und seinen verschiedenen Binary-Dependency-Packages. Allerdings, wenn dein Bundler sowieso esbuild nutzt (wie Vite zum Beispiel), siehst du vielleicht nicht so einen großen Gewinn.
Ich würde sagen, der beste Grund, zu Nodes nativem Type-Stripping zu wechseln, ist die Reduzierung einer Abstraktionsschicht. Statt tsx zu starten, das seine eigenen Prozesse spawnt und sein eigenes CLI-Parsing macht, startest du einfach dein normales node-Binary mit allen Kommandozeilen-Optionen, die du erwartest.
TSX (Nochmal), Nodemon
Okay, das andere, wofür du vielleicht tsx benutzt, ist tsx watch. Du benutzt vielleicht auch nodemon oder eine andere ähnliche Option. Alle machen das Gleiche: Sie beobachten die Quelldateien deiner Anwendung, und wenn sich eine ändert, starten sie den aktuellen Prozess neu.
Das wurde zu NodeJS ungefähr zur gleichen Zeit hinzugefügt wie Type-Stripping und kann über node --watch genutzt werden.
Einschränkungen
- Nodemon kann ein bisschen mehr als nur JS-Dateien beobachten, also wenn du das brauchst, bist du vielleicht besser mit dem dedizierten Tool bedient.
- Wie man angibt, welche Dateien beobachtet werden sollen, ist ein bisschen anders. Nodemon beobachtet das aktuelle Verzeichnis (oder alle Verzeichnisse, die du angibst). Nodes eingebauter Watcher beobachtet stattdessen Änderungen in importierten Dateien, du solltest dir also keine Sorgen machen müssen, welche Dateien du beobachten oder ignorieren willst.
Lohnt es sich?
Außer du nutzt die erweiterten Features von Nodemon (und das tust du wahrscheinlich nicht), ist das ein super einfacher Wechsel. Du kannst vielleicht sogar etwas Konfiguration loswerden.
Nodemon ist ein ziemliches Dependency-Monster, bringt 29 transitive Dependencies mit, die bis zu fünf Ebenen tief gehen. Jede Dependency ist ziemlich klein (obwohl die Summe immer noch fast 1MB Speicher ist), aber es ist trotzdem schön, Supply-Chain-Probleme zu reduzieren, falls eine dieser Dependencies von einem böswilligen Akteur kompromittiert würde. Außerdem haben schnellere Installationszeiten noch niemandem geschadet.
Dotenv & Co.
Environment-Variablen sind praktisch zum Konfigurieren einer Anwendung, aber beim Entwickeln kann es bequem sein, diese Variablen in einer Datei zu speichern und sie automatisch laden zu lassen. Normalerweise würdest du vielleicht ein Modul wie dotenv verwenden, oder ein CLI wie dotenv-cli oder dotenvx, um das zu handhaben, aber NodeJS kann das auch für dich erledigen.
NodeJS bietet das auf mehrere Arten an. Erstens kannst du --env-file oder --env-file-if-exists verwenden, um eine Datei vom CLI zu laden. Alternativ kannst du process.loadEnvPath(...) verwenden, um eine Datei zur Laufzeit programmatisch zu laden.
Einschränkungen
- Manchmal ist es schwierig, ein bestimmtes Flag an NodeJS zu übergeben (zum Beispiel weil es implizit von einem anderen Kommando ausgeführt wird). In dem Fall könnte ein Kommando wie
dotenv-cliimmer noch etwas einfacher zu nutzen sein, weil es sicherstellt, dass jeder erzeugte Child-Prozess automatisch die richtigen Environment-Variablen bekommt. - Manche Env-Parser beinhalten fancy Logik wie Variable-Expansion. Wenn du das nutzt, bist du wahrscheinlich sowieso besser mit einer richtigen Konfigurationsdatei bedient, aber wenn du auf dem
.env-Format bestehst, dann musst du beim fancy Parser bleiben. Ähnlich, wenn du verschlüsselte Secrets handhabst, könnte es sich lohnen, ein dediziertes Tool dafür zu verwenden.
Lohnt es sich?
dotenv ist ehrlich gesagt ein ziemlich einfaches Modul – keine transitiven Dependencies, 20kB Installationsgröße, es ist schwer, sich zu beschweren. Allerdings sind 20kB immer noch größer als 0kB, und du verlierst hier nicht viel.
Wenn du schwerere Dependency-Bedürfnisse hast (wie das Managen von Secrets für ein Team von Entwicklern, die an verschiedenen Projekten arbeiten), brauchst du mehr als das, aber dann hättest du auch mehr als dotenv gebraucht.
Jest, Vitest, Node-TAP, etc.
Es gibt so viele Test-Runner da draußen, und jetzt gibt's noch einen mehr. NodeJS kommt mit seinem eigenen eingebauten Test-Runner, und der ist viel mächtiger, als ich ihm anfangs zugetraut habe. Die Basisfunktionalität ist alles da, und wenn du mit Jests describe/it-Stil vertraut bist, gibt's sogar Aliases dafür.
Was Features angeht, hast du verschiedene Tools zum Mocken, inklusive Mocken von Timers und Modulen; du hast Coverage-Reporting; du hast verschiedene Tools zum Filtern von Tests; und sogar etwas Unterstützung für Snapshot-Testing.
Ich muss zugeben, dass die Dokumentation gerade noch fehlt. Zum Beispiel nutzt viel von der Dokumentation verschachtelte Tests – also einen test('...', () => { ... })-Aufruf innerhalb eines anderen Testblocks, um Suites zu simulieren. Aber der Test-Runner unterstützt auch echte Suites mit den Funktionen suite oder describe, und beim Herumspielen produzieren diese viel bessere Ergebnisse als verschachtelte Tests. Es gibt auch ein paar raue Kanten, wie Snapshot-Testing wird unterstützt, aber Inline-Snapshots werden nicht unterstützt.
Einschränkungen
- Die erwähnten rauen Kanten und fehlende Dokumentation ist der größte Haken hier. Auch Dinge wie Politur und Aussehen – die Fehlermeldung in Vitest zum Beispiel beinhaltet einen Diff der erwarteten und tatsächlichen Werte und zeigt oft genau, was schiefgelaufen ist, während die Fehlermeldung im eingebauten Runner ein JSON-Objekt mit einem Haufen Attribute ist.
- Mehr als bei einigen anderen Optionen in dieser Liste kommt ein Testing-Framework mit einem ganzen Ökosystem. Wenn du Jest nutzt, nutzt du wahrscheinlich auch einen Haufen Jest-Plugins oder hast Tools, die explizite Unterstützung für Jest mitbringen. Den eingebauten Runner zu nutzen könnte daher bedeuten, entweder auf manche dieser Tools und Plugins zu verzichten oder das Gleiche, was das Plugin machen würde, von Hand zu machen. Zum Beispiel, wenn du einen Haufen fancy Custom-Assertions hast, musst du entweder zu Nodes Standard-Assertions wechseln oder deine eigenen Funktionen schreiben, die die Custom-Assertion machen.
- Der NodeJS-Test-Runner läuft in Node, überraschenderweise. Wenn du Tests im Browser ausführst (z.B. mit Karma oder Vitests neuem Browser-Mode), hast du hier Pech.
Lohnt es sich?
Testing-Frameworks sind tendenziell sperrige, komplizierte Tools, weil sie viel Zeug machen. Vitest zum Beispiel kommt auf 37 Packages und über 35MB (wieder größtenteils wegen ESBuild). Aber gleichzeitig machen Testing-Frameworks viel, und es gibt viel Wert in einem Testing-Framework, das alles so reibungslos wie möglich macht.
Der eingebaute Test-Runner kann mehr als man erwarten würde, aber er ist noch nicht ganz reibungslos. Ich empfehle, ihn ein bisschen auszuprobieren, aber in meinen eigenen Projekten bleibe ich generell bei Vitest. Allerdings könnte er für kleine Projekte, die sowieso nicht viele Dependencies brauchen, genau das Richtige sein. Wenn du bisher mit node-tap zufrieden warst, wird der eingebaute Test-Runner wahrscheinlich alles abdecken, was du bisher gemacht hast.
Fazit
In den letzten Monaten gab es eine Handvoll hochkarätiger Angriffe über NPM-Dependencies. Das heißt nicht, dass wir NPM komplett meiden sollten, aber es bedeutet, hart über jede Dependency nachzudenken, die wir nutzen, und zu entscheiden, ob sie das natürliche Risiko wirklich wert ist, das mit der Abhängigkeit von Drittanbieter-Tools einhergeht.
In diesem Artikel habe ich mir ein paar Beispiele angeschaut, wo du vielleicht komplett auf Dependencies verzichten kannst, indem du einfach NodeJS-Features direkt nutzt. Ich habe nur ein paar der Möglichkeiten angerissen – ich habe die Fülle neuer JavaScript-Language-Features und Libraries komplett ignoriert, die existierende Libraries ersetzen können, wie verschiedene neue Array-Methoden oder der Temporal-Namespace zum Handhaben von Daten und Zeiten.
Ich hoffe, einige dieser Ideen sind nützlich für dich, und mehr noch, ich hoffe, dass deine Supply Chain ein bisschen sicherer ist wegen einiger dieser Änderungen.
)
)
)
)
)
)
)
)
)