Hvordan holder man egentlig alle avhengighetene i JavaScript-prosjektet sitt ved like? Det er mye å holde styr på, og et typisk frontend-prosjekt inneholder gjerne flere tusen avhengigheter.
Her vil jeg skrive litt om hvordan man vedlikeholder og oppdaterer prosjektets direkte, og indirekte (transitive), avhengigheter.
Litt om versjonering
De aller fleste pakkene i npm forholder seg til semantisk versjonering. Dette innebærer at versjonsnummeret er oppdelt slik: major.minor.patch. Når man patcher bugs og sikkerhetshull, oppdaterer man patch-versjonen. Hvis man legger til ny funksjonalitet, oppdaterer man minor-versjonen.
«Man kan oppdatere patch og minor-versjonen av avhengigheter, uten at applikasjonen din slutter å fungere.»
Introduserer man "breaking changes" i en ny versjon, skal man derimot oppdatere major-versjonen. Dette betyr at man kan oppdatere patch og minor-versjonen av avhengigheter, uten at applikasjonen din slutter å fungere (i teorien…).
Når det gjelder avhengigheter i prosjektet ditt, så er det vanlig å skille mellom direkte og indirekte avhengigheter.
Direkte avhengigheter er det som står spesifisert i package.json, under "dependencies" og "devDependencies", og er typisk de du har installert ved hjelp av npm install [pakkenavn]. Dette er programpakker som applikasjonen din direkte avhenger av. Alle disse har derimot sine egne avhengigheter igjen, dette kalles indirekte, eller transitive, avhengigheter.
Npm oversvømmes av svindel og spam: – Bare toppen av isfjellet
Ta kontroll over dine avhengigheter
Hvor mange avhengigheter har egentlig prosjektet ditt? Dette kan du finne ut av med kommandoen npm ls -a, som lister opp alle avhengigheter og transitive avhengigheter. Hvis du har et vanlig moderne frontend-prosjekt, så vil derimot denne listen bli altfor lang til å være nyttig å lese. Da kan du bruke unix-verktøyet wc for å telle hvor mange linjer npm ls egentlig spytter ut:
npm ls -a | wc -l
Dette er et tall på hvor mange avhengighetsrelasjoner det er i kodebasen din. Men, mange pakker kan ha de samme avhengighetene! Lurer du på hvor mange avhengigheter du faktisk laster ned fra internett, må du fjerne disse duplikatene fra listen. Dette kan du bruke grep til:
npm ls -a | grep -v deduped | wc -l
Ved å telle avhengigheter på denne måten i et av de større prosjektene jeg jobber i, går tallet ned fra 5669 til 2489 avhengigheter.
Hvordan fungerer npm?
Å ha tusenvis av avhengigheter i prosjektet ditt, er en sikkerhetsrisiko. Alle disse avhengighetene er gjenstand for målrettet angrep, hvor en aktør kan legge opp en patch-oppdatering, som inneholder ondsinnet kode. På den måten kan sårbarheten lett trekkes inn i applikasjonen, uten at noen merker det. Både utviklerens maskin og sluttbrukernes maskiner kan være målet for slike angrep.
Pakkene trenger ikke å bli angrepet av utenforstående ondsinnede hackere heller. Det er ingen umulighet at personen som utvikler en mye brukt pakke, selv bruker den til angrep, for gevinst eller propaganda. Et nylig eksempel på dette, er utviklere som har lagt inn logg-meldinger om krigen mot Ukraina i sine avhengigheter, noe flere verktøy som søker etter sikkerhetsfeil flagger som ondsinnet og farlig kode.
«Det er ingen umulighet at personen som utvikler en mye brukt pakke, selv bruker den til angrep, for gevinst eller propaganda.»
Frontend-prosjektene vi vedlikeholder, har gjerne tusenvis av unike avhengigheter som blir lastet ned fra internett hver gang prosjektet installeres. Derfor er det greit å holde alle avhengigheter lukket til en kjent versjon, og oppdatere i intervaller, fremfor å alltid legge inn seneste patch-versjon installert.
I tillegg kan patch-versjoner også introdusere bugs, som ødelegger applikasjonen din. Det er her package-lock.json kommer inn og passer på at alle avhengighetene i applikasjonen blir installert med nøyaktig samme versjon. Dette, vel å merke, hvis du bruker npm ci når du skal installere avhengighetene!
Sikre NPM-pakkene dine med overrides i package.json: - Kos deg med færre advarsler!
Du installerer ikke nøyaktig de samme avhengighetene av å skrive npm install og npm ci. Dette kan være greit å vite om av flere grunner. Når du for eksempel etterforsker en sporadisk bug i produksjon, som du ikke får gjenskapt lokalt: Kanskje den rett og slett ikke finnes lokalt fordi du har installert nyere patch-versjoner enn avhengighetene brukt i versjonen som kjører i produksjon?
Får du som vane å bruke npm ci, også under utvikling, slipper du denne fadesen.
Du slipper også å utsette deg for unødvendig risiko for at noen har lastet opp en ondsinnet patch-versjon i en av de tusenvis av avhengighetene dine.
Oppdater alle direkte avhengigheter
Har du noen gang sittet og oppdatert avhengighetene dine i package.json manuelt? Altså gått gjennom én og én avhengighet, sjekket opp mot nyeste versjon i npmjs.com, og så justert versjonstallet i filen manuelt? I så fall burde du skamme deg.
«Din jobb som utvikler, er å lage disse snedige programmene som utfører slike kjedelige monotone jobber.»
En av datamaskinens arbeidsoppgaver er å gjøre slike kjedelige monotone arbeidsoppgaver for oss. Og din jobb som utvikler, er å lage disse snedige programmene som utfører slike kjedelige monotone jobber.
Det du i stedet burde ha gjort, er å skrive et program som tar seg av denne monotone jobben. Hvis du synes det høres for utfordrende ut, slapp av, noen har gjort jobben for deg. 🙂 Du kan installere npm-check-updates med følgende kommando:
npm i -g npm-check-updates
For å se hvilke avhengigheter som trengs å oppdateres, skriver du bare ncu, og for å installere disse skriver du ncu -u. Skriver du bare ncu får du en flott fargekodet liste med direkte avhengigheter som må oppdateres. Rødt betyr at major version kan oppdateres, blå minor og grønn patch.
En ting jeg pleier å gjøre, er å installere nyeste minor og patch-versjoner av alle direkte avhengigheter, før jeg oppdaterer alle transitive avhengigheter, slik jeg beskriver nedenfor. Dette kan gjøres slik
ncu -t minor -u
Hvis du vil, kan du også kun oppdatere dev-avhengigheter. Disse er muligens litt mindre farlig å også oppdatere "major"-versjoner av. (Så lenge applikasjonen bygger, og testene er grønne, så skulle man slippe å trenge regresjonstesting av en slik oppdatering, eller hva?) Dette gjør du ved hjelp av --dep-opsjonen:
ncu --dep dev -u
Noen ganger har man også en samling avhengigheter man vil oppdatere sammen. Eller kanskje man ønsker å oppdatere alle prod-avhengigheter, bortsett fra React og Redux? I slike tilfeller kan man kjøre denne kommandoen, og velge spesifikt hvilke avhengigheter man ønsker å installere:
ncu -i --dep prod
Oppdater alle transitive avhengigheter
Dette er en rask måte å bli kvitt potensielle sikkerhets-sårbarheter, og prosessen er relativt enkel og smertefritt. Den består rett og slett av å slette package-lock fila, og så foreta en ny installasjon fra bunnen. Jeg kjører som regel disse kommandoene, i denne rekkefølgen:
rm package-lock.json
rm -rf node-modules
npm cache clear --force
npm install
Å kjøre en cache clear eller å fjerne node-modules er neppe nødvendig. Hvis du henter pakker direkte fra npmjs.com er cache clear helt unødvendig, men har organisasjonen du jobber i et mellomlager, og du opplever feilmeldinger som påstår at du prøver å installere en versjon av en pakke som ikke eksisterer, kan denne kommandoen hjelpe deg.
Vanen med å slette node_modules før installasjon, var noe jeg fikk for meg for flere år siden, og jeg er usikker på om det fremdeles har noe for seg. Det jeg bekymrer meg for, er at npm skal bruke pakker den ser allerede ligger i node_modules, fremfor å hente ned en nyere fra npmjs.com.
Topp 3 farer: - Den verste hører vi ikke så mye om
Risiko for sårbarheter?
Men når vi oppdaterer avhengigheter, da risikerer vi jo å installere pakker med bugs eller sårbarheter?
Jo da, sjansen for dette ligger der selvfølgelig hele tiden. Men vi må samtidig holde avhengighetene oppdatert fordi bugs og sikkerhetshull patches kontinuerlig.
Hver gang du installerer en ny pakke, er det en liten risiko for at den har en avhengighet som inneholder nye svakheter som slår negativt ut på applikasjonen du vedlikeholder.
Ved å oppdatere transitive avhengigheter ved regelmessige intervaller, ikke bare minimerer vi sjansen for å ta med en versjon med en sårbarhet. Vi har også samtidig en god historikk på hvilke versjoner som ble brukt når: For eksempel, hvis du har en egen git pull-request (eller i det minste en egen git commit) for å oppdatere transitive avhengigheter, er det lettere å rulle tilbake endringen, og det er lettere å ha oversikt over hvilke versjoner av avhengigheter som kjørte når i produksjon.
Oppsummering
Det er lurt å oppdatere applikasjonene sine i et visst intervall. Bruk av npm-check-updates og sletting av lock-fila, for å gjøre en reinstallasjon, hjelper på med dette.
Hvis du skal kjøre sånn omtrent de samme kommandoene etter hverandre på regelmessig basis, så er det selvfølgelig dumt å ikke automatisere denne prosessen. For å hjelpe deg med det, har jeg lagd et bash-script her.
Hvis du bruker windows, kan du jo be ChatGPT om å lage noe tilsvarende for deg til powershell. 😀