Garn: En ny pakethanterare för JavaScript – Facebook Engineering

I JavaScript-gemenskapen delar ingenjörer med sig av hundratusentals kodbitar så att vi kan undvika att skriva om grundläggande komponenter, bibliotek eller ramverk själva. Varje kodstycke kan i sin tur vara beroende av andra kodstycken, och dessa beroenden hanteras av pakethanterare. Den mest populära JavaScript-pakethanteraren är npm-klienten, som ger tillgång till mer än 300 000 paket i npm-registret. Mer än 5 miljoner ingenjörer använder npm-registret, som varje månad hämtas upp till 5 miljarder gånger.

Vi har använt npm-klienten framgångsrikt på Facebook i flera år, men i takt med att storleken på vår kodbas och antalet ingenjörer ökade stötte vi på problem med konsistens, säkerhet och prestanda. Efter att ha försökt lösa varje problem när det dök upp, började vi bygga en ny lösning som skulle hjälpa oss att hantera våra beroenden på ett mer tillförlitligt sätt. Produkten av det arbetet kallas Yarn – en snabb, pålitlig och säker alternativ npm-klient.

Vi är glada att kunna meddela att Yarn, ett samarbete med Exponent, Google och Tilde, släpps med öppen källkod. Med Yarn har ingenjörer fortfarande tillgång till npm-registret, men kan installera paket snabbare och hantera beroenden konsekvent på olika maskiner eller i säkra offline-miljöer. Yarn gör det möjligt för ingenjörer att agera snabbare och med förtroende när de använder delad kod så att de kan fokusera på det som är viktigt – att bygga nya produkter och funktioner.

Evolutionen av JavaScript-pakethantering på Facebook

Under tiden före pakethanterare var det vanligt att JavaScript-ingenjörer förlitade sig på ett litet antal beroenden som lagrades direkt i deras projekt eller som serverades av en CDN. Den första stora JavaScript-pakethanteraren, npm, byggdes strax efter att Node.js introducerades och blev snabbt en av de mest populära pakethanterarna i världen. Tusentals nya projekt med öppen källkod skapades och ingenjörer delade mer kod än någonsin tidigare.

Många av våra projekt på Facebook, som React, är beroende av kod i npm-registret. När vi skalade internt fick vi dock problem med konsistensen när vi installerade beroenden på olika maskiner och användare, den tid det tog att hämta in beroenden och vi hade vissa säkerhetsproblem med hur npm-klienten exekverar kod från vissa av dessa beroenden automatiskt. Vi försökte bygga lösningar kring dessa problem, men de gav ofta upphov till nya problem i sig.

Försök att skala npm-klienten

I början, enligt de föreskrivna bästa metoderna, kontrollerade vi bara in package.json och bad ingenjörer att manuellt köra npm install. Detta fungerade tillräckligt bra för ingenjörerna, men gick sönder i våra miljöer för kontinuerlig integration, som måste vara sandlåda och avskurna från internet av säkerhets- och tillförlitlighetsskäl.

Den nästa lösning som vi implementerade var att checka in alla node_modules i repositoriet. Detta fungerade visserligen, men det gjorde vissa enkla operationer ganska svåra. Till exempel genererade en uppdatering av en mindre version av babel en 800 000-radiga commit som var svår att landa och utlöste lint-regler för ogiltiga utf8-byte-sekvenser, windows-radslut, icke png-kryssade bilder med mera. Att slå samman ändringar i node_modules tog ofta ingenjörer en hel dag. Vårt källkontrollteam påpekade också att vår incheckade node_modules-mapp var ansvarig för en enorm mängd metadata. React Native package.json listar för närvarande bara 68 beroenden, men efter att ha kört npm install innehåller node_modules-katalogen 121 358 filer.

Vi gjorde ett sista försök att skala npm-klienten för att den ska fungera med antalet ingenjörer på Facebook och mängden kod som vi behöver installera. Vi bestämde oss för att komprimera hela node_modules-mappen och ladda upp den till en intern CDN så att både ingenjörer och våra system för kontinuerlig integration kunde hämta och extrahera filerna på ett konsekvent sätt. På så sätt kunde vi ta bort hundratusentals filer från källkontrollen, men det gjorde att ingenjörerna behövde tillgång till internet inte bara för att hämta ny kod, utan också för att bygga den.

Vi var också tvungna att arbeta med problem med npm:s shrinkwrap-funktion, som vi använde för att låsa beroendeversioner. Shrinkwrap-filer genereras inte som standard och blir osynkroniserade om ingenjörer glömmer att generera dem, så vi skrev ett verktyg för att verifiera att innehållet i shrinkwrap-filen stämmer överens med det som finns i node_modules. Dessa filer är dock enorma JSON-klumpar med osorterade nycklar, så ändringar i dem skulle generera massiva, svårgranskade commits. För att mildra detta behövde vi lägga till ett extra skript för att sortera alla poster.

För att slutligen uppdatera ett enda beroende med npm uppdateras också många orelaterade beroende baserat på semantiska versioneringsregler. Detta gör varje ändring mycket större än väntat, och att behöva göra saker som att lägga in node_modules eller ladda upp den till en CDN gjorde processen mindre än idealisk för ingenjörer.

Bygga en ny klient

Istället för att fortsätta bygga infrastruktur runt npm-klienten bestämde vi oss för att försöka se på problemet mer holistiskt. Tänk om vi i stället försökte bygga en ny klient som tog itu med de centrala problem vi upplevde? Sebastian McKenzie på vårt kontor i London började hacka på den här idén och vi blev snabbt entusiastiska över dess potential.

När vi arbetade med detta började vi prata med ingenjörer i hela branschen och upptäckte att de stod inför liknande problem och hade försökt med många av samma lösningar, ofta med fokus på att lösa ett enda problem i taget. Det blev uppenbart att vi genom att samarbeta kring alla de problem som samhället stod inför kunde utveckla en lösning som fungerade för alla. Med hjälp av ingenjörer från Exponent, Google och Tilde byggde vi ut Yarn-klienten och testade och validerade dess prestanda på alla större JS-ramverk och för ytterligare användningsfall utanför Facebook. Idag är vi glada att kunna dela den med gemenskapen.

Introduktion av Yarn

Yarn är en ny pakethanterare som ersätter det befintliga arbetsflödet för npm-klienten eller andra pakethanterare samtidigt som den är kompatibel med npm-registret. Den har samma funktionalitet som befintliga arbetsflöden samtidigt som den fungerar snabbare, säkrare och mer tillförlitligt.

Den primära funktionen för alla pakethanterare är att installera något paket – en bit kod som tjänar ett visst syfte – från ett globalt register till en ingenjörs lokala miljö. Varje paket kan vara beroende av andra paket eller inte. Ett typiskt projekt kan ha tiotals, hundratals eller till och med tusentals paket i sitt träd av beroenden.

Dessa beroenden versioneras och installeras baserat på semantisk versionering (semver). Semver definierar ett versioneringsschema som återspeglar typen av ändringar i varje ny version, oavsett om en ändring bryter ett API, lägger till en ny funktion eller rättar ett fel. Semver förlitar sig dock på att paketutvecklare inte gör misstag – brytande ändringar eller nya buggar kan hitta sin väg in i installerade beroenden om beroendena inte är låsta.

Arkitektur

I Node-ekosystemet placeras beroenden i en node_modules-katalog i ditt projekt. Denna filstruktur kan dock skilja sig från det faktiska beroendeträdet eftersom dubbla beroenden slås samman. Klienten npm installerar beroenden i katalogen node_modules på ett icke-deterministiskt sätt. Detta innebär att beroende på i vilken ordning beroenden installeras kan strukturen i en node_modules-katalog vara annorlunda från en person till en annan. Dessa skillnader kan orsaka ”fungerar på min maskin”-fel som tar lång tid att hitta.

Yarn löser dessa problem med versionering och icke-determinism genom att använda lockfiles och en installationsalgoritm som är deterministisk och tillförlitlig. Dessa lockfiles låser de installerade beroendena till en specifik version och säkerställer att varje installation resulterar i exakt samma filstruktur i node_modules på alla maskiner. Den skrivna låsfilen använder ett kortfattat format med ordnade nycklar för att säkerställa att ändringarna är minimala och granskningen enkel.

Installationsprocessen är uppdelad i tre steg:

  1. Upplösning: Yarn börjar lösa beroenden genom att göra förfrågningar till registret och rekursivt söka upp varje beroende.
  2. Hämtning: Därefter tittar Yarn i en global cachekatalog för att se om det paket som behövs redan har hämtats. Om så inte är fallet hämtar Yarn tarball-filen för paketet och placerar den i den globala cacheminnet så att den kan arbeta offline och inte behöver hämta beroenden mer än en gång. Beroenden kan också placeras i källkontrollen som tarballs för fullständiga offlineinstallationer.
  3. Länkning: Slutligen länkar Yarn ihop allt genom att kopiera alla filer som behövs från den globala cachen till den lokala node_modules-katalogen.

Då Yarn bryter ner dessa steg på ett rent sätt och har deterministiska resultat kan Yarn parallellisera operationer, vilket maximerar resursutnyttjandet och gör installationsprocessen snabbare. På vissa Facebook-projekt minskade Yarn installationsprocessen med en storleksordning, från flera minuter till bara några sekunder. Yarn använder också en mutex för att se till att flera pågående CLI-instanser inte kolliderar och förorenar varandra.

Under hela denna process inför Yarn strikta garantier kring installation av paket. Du har kontroll över vilka livscykelskript som exekveras för vilka paket. Paketkontrollsummor lagras också i låsfilen för att säkerställa att du får samma paket varje gång.

Funktioner

Förutom att göra installationerna mycket snabbare och mer tillförlitliga har Yarn ytterligare funktioner för att ytterligare förenkla arbetsflödet för hantering av beroenden.

  • Kompatibilitet med både npm- och bower-arbetsflödena och stöd för blandning av register.
  • Möjlighet att begränsa licenser för installerade moduler och ett sätt att ge ut licensinformation.
  • Exponerar ett stabilt offentligt JS-API med loggning abstraherad för konsumtion via byggverktyg.
  • Läsbart, minimalt, vackert CLI-utdata.

Yarn i produktion

På Facebook använder vi redan Yarn i produktion och det har fungerat riktigt bra för oss. Det driver beroende- och pakethantering för många av våra JavaScript-projekt. Med varje migrering har vi gjort det möjligt för ingenjörer att bygga offline och hjälpt till att snabba upp deras arbetsflöde. 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.