Yarn: Ein neuer Paketmanager für JavaScript – Facebook Engineering

In der JavaScript-Community tauschen Ingenieure Hunderttausende von Codeteilen aus, damit wir grundlegende Komponenten, Bibliotheken oder Frameworks nicht selbst neu schreiben müssen. Jedes Codestück kann wiederum von anderen Codestücken abhängen, und diese Abhängigkeiten werden von Paketmanagern verwaltet. Der beliebteste JavaScript-Paketmanager ist der npm-Client, der Zugriff auf mehr als 300.000 Pakete in der npm-Registry bietet. Mehr als 5 Millionen Entwickler nutzen die npm-Registry, die jeden Monat bis zu 5 Milliarden Mal heruntergeladen wird.

Wir haben den npm-Client bei Facebook jahrelang erfolgreich eingesetzt, aber als die Größe unserer Codebasis und die Anzahl der Entwickler wuchs, stießen wir auf Probleme mit der Konsistenz, Sicherheit und Leistung. Nachdem wir versucht hatten, jedes Problem einzeln zu lösen, machten wir uns daran, eine neue Lösung zu entwickeln, mit der wir unsere Abhängigkeiten zuverlässiger verwalten können. Das Produkt dieser Arbeit heißt Yarn – ein schneller, zuverlässiger und sicherer alternativer npm-Client.

Wir freuen uns, die Open-Source-Veröffentlichung von Yarn ankündigen zu können, die in Zusammenarbeit mit Exponent, Google und Tilde entstanden ist. Mit Yarn haben Ingenieure weiterhin Zugriff auf die npm-Registry, können aber Pakete schneller installieren und Abhängigkeiten konsistent über Rechner hinweg oder in sicheren Offline-Umgebungen verwalten. Yarn ermöglicht es den Ingenieuren, bei der Verwendung von gemeinsam genutztem Code schneller und sicherer voranzukommen, so dass sie sich auf das Wesentliche konzentrieren können – die Entwicklung neuer Produkte und Funktionen.

Die Entwicklung der JavaScript-Paketverwaltung bei Facebook

In den Tagen vor der Einführung von Paketmanagern war es für JavaScript-Ingenieure üblich, sich auf eine kleine Anzahl von Abhängigkeiten zu verlassen, die direkt in ihren Projekten gespeichert oder über ein CDN bereitgestellt wurden. Der erste große JavaScript-Paketmanager, npm, wurde kurz nach der Einführung von Node.js entwickelt und entwickelte sich schnell zu einem der beliebtesten Paketmanager der Welt. Tausende neuer Open-Source-Projekte entstanden, und Ingenieure tauschten mehr Code aus als je zuvor.

Viele unserer Projekte bei Facebook, wie z. B. React, hängen von Code in der npm-Registry ab. Als wir jedoch intern skalierten, hatten wir Probleme mit der Konsistenz bei der Installation von Abhängigkeiten auf verschiedenen Rechnern und Nutzern, mit der Zeit, die es brauchte, um Abhängigkeiten einzupflegen, und hatten einige Sicherheitsbedenken mit der Art und Weise, wie der npm-Client Code aus einigen dieser Abhängigkeiten automatisch ausführt. Wir haben versucht, Lösungen für diese Probleme zu finden, aber sie haben oft neue Probleme aufgeworfen.

Versuche zur Skalierung des npm-Clients

Anfänglich haben wir, den vorgeschriebenen Best Practices folgend, nur package.json eingecheckt und die Entwickler gebeten, npm install manuell auszuführen. Dies funktionierte für die Techniker gut genug, versagte aber in unseren kontinuierlichen Integrationsumgebungen, die aus Sicherheits- und Zuverlässigkeitsgründen in einer Sandbox untergebracht und vom Internet abgeschnitten werden müssen.

Die nächste Lösung, die wir implementierten, bestand darin, das gesamte node_modules in das Repository einzuchecken. Das funktionierte zwar, machte aber einige einfache Vorgänge ziemlich schwierig. So erzeugte beispielsweise die Aktualisierung einer kleineren Version von babel einen 800.000 Zeilen umfassenden Commit, der nur schwer zu landen war und Lint-Regeln für ungültige utf8-Byte-Sequenzen, Windows-Zeilenenden, nicht mit png gecrushte Bilder und mehr auslöste. Für das Zusammenführen von Änderungen an node_modules benötigten die Ingenieure oft einen ganzen Tag. Unser Source-Control-Team wies auch darauf hin, dass unser eingecheckter node_modules-Ordner für eine enorme Menge an Metadaten verantwortlich war. Das React Native package.json listet derzeit nur 68 Abhängigkeiten auf, aber nach der Ausführung von npm install enthält das node_modules-Verzeichnis 121.358 Dateien.

Wir haben einen letzten Versuch unternommen, den npm-Client so zu skalieren, dass er mit der Anzahl der Techniker bei Facebook und der Menge an Code, die wir installieren müssen, funktioniert. Wir beschlossen, den gesamten node_modules-Ordner zu zippen und in ein internes CDN hochzuladen, damit sowohl die Techniker als auch unsere kontinuierlichen Integrationssysteme die Dateien konsistent herunterladen und extrahieren konnten. Auf diese Weise konnten wir Hunderttausende von Dateien aus der Versionskontrolle entfernen, aber die Techniker brauchten einen Internetzugang nicht nur, um neuen Code zu ziehen, sondern auch, um ihn zu bauen.

Wir mussten auch Probleme mit der Shrinkwrap-Funktion von npm umgehen, die wir zum Sperren von Abhängigkeitsversionen verwendeten. Shrinkwrap-Dateien werden standardmäßig nicht generiert und geraten aus dem Takt, wenn die Entwickler vergessen, sie zu generieren. Deshalb haben wir ein Tool geschrieben, das überprüft, ob der Inhalt der Shrinkwrap-Datei mit dem Inhalt von node_modules übereinstimmt. Diese Dateien sind allerdings riesige JSON-Blobs mit unsortierten Schlüsseln, so dass Änderungen an ihnen zu massiven, schwer zu überprüfenden Commits führen würden. Um dies abzumildern, mussten wir ein zusätzliches Skript hinzufügen, um alle Einträge zu sortieren.

Schließlich aktualisiert die Aktualisierung einer einzelnen Abhängigkeit mit npm auch viele nicht verwandte Abhängigkeiten auf der Grundlage semantischer Versionsregeln. Das macht jede Änderung viel größer als erwartet, und Dinge wie das Commit node_modules oder das Hochladen in ein CDN machten den Prozess für Ingenieure nicht gerade ideal.

Bau eines neuen Clients

Anstatt die Infrastruktur um den npm-Client herum weiter aufzubauen, beschlossen wir, das Problem ganzheitlicher zu betrachten. Wie wäre es, wenn wir stattdessen versuchen würden, einen neuen Client zu entwickeln, der die Kernprobleme, die wir hatten, löst? Sebastian McKenzie in unserem Londoner Büro begann, an dieser Idee zu arbeiten, und wir waren schnell von ihrem Potenzial begeistert.

Während wir daran arbeiteten, begannen wir mit Ingenieuren in der gesamten Branche zu sprechen und stellten fest, dass sie mit ähnlichen Problemen konfrontiert waren und viele der gleichen Lösungen ausprobiert hatten, wobei sie sich oft auf die Lösung eines einzelnen Problems konzentrierten. Es wurde klar, dass wir durch die Zusammenarbeit bei allen Problemen, mit denen die Gemeinschaft konfrontiert war, eine Lösung entwickeln konnten, die für alle funktioniert. Mit der Hilfe von Ingenieuren von Exponent, Google und Tilde haben wir den Yarn-Client entwickelt und seine Leistung auf jedem wichtigen JS-Framework und für zusätzliche Anwendungsfälle außerhalb von Facebook getestet und validiert. Heute freuen wir uns, ihn mit der Community zu teilen.

Einführung in Yarn

Yarn ist ein neuer Paketmanager, der den bestehenden Workflow für den npm-Client oder andere Paketmanager ersetzt und gleichzeitig mit der npm-Registry kompatibel bleibt. Er hat den gleichen Funktionsumfang wie bestehende Workflows, arbeitet aber schneller, sicherer und zuverlässiger.

Die primäre Funktion eines jeden Paketmanagers ist es, ein Paket – ein Stück Code, das einen bestimmten Zweck erfüllt – aus einer globalen Registry in die lokale Umgebung eines Entwicklers zu installieren. Jedes Paket kann von anderen Paketen abhängig sein, muss es aber nicht. Ein typisches Projekt kann Dutzende, Hunderte oder sogar Tausende von Paketen in seinem Abhängigkeitsbaum haben.

Diese Abhängigkeiten werden auf der Grundlage der semantischen Versionierung (semver) versioniert und installiert. Semver definiert ein Versionsschema, das die Art der Änderungen in jeder neuen Version widerspiegelt, ob eine Änderung eine API bricht, eine neue Funktion hinzufügt oder einen Fehler behebt. Semver verlässt sich jedoch darauf, dass Paketentwickler keine Fehler machen – Änderungen oder neue Fehler können ihren Weg in die installierten Abhängigkeiten finden, wenn die Abhängigkeiten nicht gesichert sind.

Architektur

Im Node-Ökosystem werden Abhängigkeiten in einem node_modules Verzeichnis im Projekt abgelegt. Diese Dateistruktur kann jedoch von der tatsächlichen Abhängigkeitsstruktur abweichen, da doppelte Abhängigkeiten zusammengeführt werden. Der npm-Client installiert die Abhängigkeiten in das node_modules-Verzeichnis nicht-deterministisch. Das bedeutet, dass die Struktur eines node_modules-Verzeichnisses je nach der Reihenfolge der Installation der Abhängigkeiten von einer Person zur anderen unterschiedlich sein kann. Diese Unterschiede können zu „funktioniert auf meinem Rechner“-Fehlern führen, die lange Zeit in Anspruch nehmen.

Yarn löst diese Probleme der Versionierung und des Nicht-Determinismus, indem es Lockfiles und einen Installationsalgorithmus verwendet, der deterministisch und zuverlässig ist. Diese Lockfiles binden die installierten Abhängigkeiten an eine bestimmte Version und stellen sicher, dass jede Installation auf allen Rechnern zur exakt gleichen Dateistruktur in node_modules führt. Das geschriebene Lockfile verwendet ein prägnantes Format mit geordneten Schlüsseln, um sicherzustellen, dass Änderungen minimal sind und die Überprüfung einfach ist.

Der Installationsprozess ist in drei Schritte unterteilt:

  1. Auflösung: Yarn beginnt mit der Auflösung von Abhängigkeiten, indem es Anfragen an die Registry stellt und rekursiv jede Abhängigkeit nachschlägt.
  2. Fetching: Als nächstes schaut Yarn in einem globalen Cache-Verzeichnis nach, ob das benötigte Paket bereits heruntergeladen wurde. Wenn dies nicht der Fall ist, holt Yarn den Tarball für das Paket und legt ihn im globalen Cache ab, damit es offline arbeiten kann und die Abhängigkeiten nicht mehrmals herunterladen muss. Abhängigkeiten können auch in der Versionskontrolle als Tarballs für vollständige Offline-Installationen abgelegt werden.
  3. Verknüpfen: Zum Schluss verknüpft Yarn alles miteinander, indem es alle benötigten Dateien aus dem globalen Cache in das lokale node_modules Verzeichnis kopiert.

Durch die saubere Aufteilung dieser Schritte und die deterministischen Ergebnisse ist Yarn in der Lage, Operationen zu parallelisieren, was die Ressourcenauslastung maximiert und den Installationsprozess beschleunigt. Bei einigen Facebook-Projekten hat Yarn den Installationsprozess um eine Größenordnung verkürzt, von mehreren Minuten auf nur wenige Sekunden. Yarn verwendet außerdem eine Mutex, um sicherzustellen, dass mehrere laufende CLI-Instanzen nicht miteinander kollidieren und sich gegenseitig verunreinigen.

Während dieses gesamten Prozesses gibt Yarn strenge Garantien für die Paketinstallation. Sie haben die Kontrolle darüber, welche Lebenszyklus-Skripte für welche Pakete ausgeführt werden. Die Prüfsummen der Pakete werden ebenfalls im Lockfile gespeichert, um sicherzustellen, dass Sie jedes Mal das gleiche Paket erhalten.

Features

Neben der schnelleren und zuverlässigeren Installation bietet Yarn zusätzliche Features, um den Workflow der Abhängigkeitsverwaltung weiter zu vereinfachen.

  • Kompatibilität mit den Workflows von npm und bower und Unterstützung von gemischten Registries.
  • Fähigkeit, Lizenzen von installierten Modulen einzuschränken und ein Mittel zur Ausgabe von Lizenzinformationen.
  • Bietet eine stabile öffentliche JS-API mit abstrahiertem Logging für die Verwendung durch Build-Tools.
  • Lesbare, minimale, hübsche CLI-Ausgabe.

Yarn in der Produktion

Bei Facebook verwenden wir Yarn bereits in der Produktion, und es hat sich als sehr gut für uns erwiesen. Es unterstützt das Abhängigkeits- und Paketmanagement für viele unserer JavaScript-Projekte. Mit jeder Migration haben wir es den Ingenieuren ermöglicht, offline zu entwickeln und ihre Arbeitsabläufe zu beschleunigen. You can see how install times for Yarn and npm compare on React Native under different conditions, which you can find here.

Getting started

The easiest way to get started is to run:

npm install -g yarn

yarn

The yarn CLI replaces npm in your development workflow, either with a matching command or a new, similar command:

  • npm installyarn

    With no arguments, the yarn command will read your package.json, fetch packages from the npm registry, and populate your node_modules folder. It is equivalent to running npm install.

  • npm install --save <name>yarn add <name>

    We removed the „invisible dependency“ behavior of npm install <name> and split the command. Running yarn add <name> is equivalent to running npm install --save <name>.

Future

Many of us came together to build Yarn to solve common problems, and we knew that we wanted Yarn to be a true community project that everyone can use. Yarn is now available on GitHub and we’re ready for the Node community to do what it does best: Use Yarn, share ideas, write documentation, support each other, and help build a great community to care for it. We believe that Yarn is already off to a great start, and it can be even better with your help.