Lockfiles lyver ikke, men de er heller ikke på din side

Lockfiles lyver ikke, men de er heller ikke på din side

Du sidder i en PR-review og tænker: “Hvorfor i alverden er der 800 ændrede linjer i package-lock.json for én lille dependency?”

Du scroller. Git diff flimrer. CI fejler tilfældigt. Og alle siger “bare slet lockfilen og kør npm install igen”.

Lad os få styr på, hvad der faktisk sker, så din lockfile stopper med at føles som sort magi.

Hvad en lockfile låser (og hvad den ikke gør)

Først lige begreberne:

  • package-lock.json: npm
  • yarn.lock: Yarn (classic/berry har lidt forskellig struktur, men samme idé)
  • pnpm-lock.yaml: pnpm

Alle tre gør i grove træk det samme: de låser dine konkrete versionsvalg for dependencies og deres dependencies (transitive deps).

Hvad lockfilen faktisk styrer

Lockfilen beskriver et konkret dependency-træ:

  • Hvilke pakker du har installeret
  • Præcise versionsnumre (selv hvis package.json siger ^1.2.0)
  • Hvordan træet er “foldet” (hvem der bruger hvad)

Så når du installerer på en anden maskine, burde du få nøjagtig samme træ. Det kaldes ofte en deterministic install: samme input, samme output.

Hvad lockfilen ikke lover

Lockfilen beskytter dig ikke mod alt:

  • Hvis en dependency udgiver den samme version igen med ændret indhold (skidt praksis, men det sker), kan du stadig blive ramt.
  • Hvis du sletter lockfilen, er du tilbage til at stole på version-ranges i package.json.
  • Hvis I blander flere package managers (npm og yarn på samme repo), taber du overblikket lynhurtigt.

Pointen: lockfilen er din snapshot-fil af dependency-verdenen. Men den er kun så stabil, som din praksis omkring den.

npm install vs npm ci – hvorfor det gør en forskel

Dette er et af de steder, hvor små ord giver store forskelle.

npm install: din dagligdags hammer

npm install gør flere ting på én gang:

  • Læser package.json
  • Bruger lockfilen som udgangspunkt, men kan godt opdatere den
  • Kan tilføje nye pakker og skrive dem ind i både package.json og lockfilen

Den er fin til lokal udvikling. Men den kan ændre lockfilen, selv når du ikke lige havde tænkt dig det. Fx hvis npm beslutter sig for at rydde op i duplikerede pakker.

npm ci: din reproducerbare robot

npm ci er mere striks:

  • Forventer at lockfilen allerede findes
  • Ignorerer version-ranges i package.json og stoler kun på lockfilen
  • Sletter node_modules og installerer kun det, der står i lockfilen

Det betyder: hvis lockfil og package.json ikke matcher, fejler den. Perfekt til CI. Dårligt egnet til at “lige prøve at installere noget”.

Yarn og pnpm har samme mønster

Yarn og pnpm har tilsvarende strikse kommandoer:

  • Yarn classic: yarn install --frozen-lockfile
  • Yarn berry: yarn install --immutable
  • pnpm: pnpm install --frozen-lockfile

Navnene er forskellige, men idéen er den samme: brug lockfilen som sandhed, rør den ikke, og fejle hvis noget ikke stemmer.

Hvorfor lockfile-merges går galt (og hvad “spøgelses-deps” er)

Lockfil-konflikter opstår næsten altid på grund af parallelle ændringer i dependency-træet.

Typisk scenarie

  1. Du laver en feature-branch, kører npm install some-lib. Lockfil ændrer sig.
  2. Din kollega laver en anden branch, installerer noget andet. Lockfil ændrer sig på en anden måde.
  3. I bliver begge merged ind i main. Git kan ikke automatisk flette den enorme JSON-diff fornuftigt.

Resultatet kan blive en lockfil, der teknisk set er valid JSON, men som ikke repræsenterer noget, nogen maskine nogensinde har installeret i ét hug.

“Spøgelses-deps”

Spøgelses-dependencies er pakker, der står i lockfilen, men som:

  • ikke længere bruges af nogen
  • eller findes i et mærkeligt miks af versioner, fordi to grene har kæmpet om samme underdependency

Du opdager dem tit sådan her:

  • CI bygger fint på main, men en ny branch med rebase giver mystiske fejl
  • Lokalt har du én version af en pakke, men CI har en anden
  • node_modules indeholder pakker, du troede du havde fjernet for længst

Det føles random, men kommer ofte fra en lockfil, der er “hånd-flettet” forkert.

Regler for teams: én lockfile, én manager, én strategi

Hvis du vil slippe for 80 % af lockfile-kaos, kan du lave nogle få, ret kedelige regler. Kedelige er gode her.

1. Vælg én package manager pr. repo

Ikke både npm og yarn. Ikke “jeg bruger pnpm, fordi det er hurtigere”, mens resten af teamet bruger npm.

Regel:

  • Brug enten npm + package-lock.json
  • eller Yarn + yarn.lock
  • eller pnpm + pnpm-lock.yaml

Og commit kun den lockfil, der hører til.

2. Lockfilen skal i git

Ja, du skal committe lockfilen. Også i libraries. Det gør det muligt at reproducere præcist det dependency-træ, du udviklede og testede med.

Uden lockfil får du “jeg installerede bare lige i går, og nu virker det ikke længere”-fejl.

3. Aftal, hvornår man må ændre lockfilen

Jeg plejer at bruge noget a la:

  • Du ændrer lockfilen, når du installer eller adder noget bevidst.
  • Du skal ikke committe lockfile-støj, der kommer fra “jeg prøvede bare noget lokalt”.
  • Hvis din branch kun ændrer kode, og lockfilen er modified, så reset den.

Sådan undgår du tilfældige kosmetiske ændringer i hver anden PR.

4. CI bruger altid den strikse mode

På CI skal der kun være én sandhed: lockfilen. Nogle eksempler:

  • npm: npm ci
  • Yarn: yarn install --frozen-lockfile
  • pnpm: pnpm install --frozen-lockfile

Hvis nogen har glemt at committe lockfilen, skal buildet fejle. Hellere der end i produktion.

Sådan løser du en lockfile-merge konflikt sikkert

Lad os tage den situation, du nok allerede har stået i: Git skriger på konflikt i package-lock.json.

Trin 1: Afgør, om lockfil-ændringen er meningsfuld

Kig på din egen PR:

  • Har du selv kørt npm install something, npm uninstall eller tilsvarende?
  • Eller har du kun ændret kode?

Hvis svaret er “kun kode”, så er din lockfil-ændring støj. I så fald:

git checkout --ours package-lock.json
# eller --theirs, alt efter hvad du vil beholde

Og bagefter kører du en frisk install lokalt for at sikre, at alt stadig spiller.

Trin 2: Hvis begge branches har ændret dependencies

Her er det fristende at åbne lockfilen i en editor og prøve at flette i hånden. Lad være. Den fil er genereret af et program, så lad programmet vinde.

Jeg plejer at gøre noget i den her stil (npm-eksempel):

  1. Vælg én sides lockfil i konflikten (typisk main/theirs):
    git checkout --theirs package-lock.json
    
  2. Gem og markér konflikten som løst:
    git add package-lock.json
    
  3. Kør install igen for at få alle nye deps med i lockfilen:
    npm install
    
  4. Commit den nye lockfil.

Idéen er: du tager udgangspunkt i én konsistent version og lader package manageren regne resten ud.

Variant: hård reset og regen

Hvis lockfilen er blevet helt absurd, kan du også:

  1. Slette lockfil og node_modules:
rm package-lock.json
rm -rf node_modules
  1. Køre npm install igen
  2. Committe den friske lockfil

Det giver et stort diff, men du ved i det mindste, at træet svarer til noget, en maskine faktisk har bygget.

CI/CD: caching, clean installs og tidlig drift-detektion

Hvis du vil fange “drift” mellem udviklingsmiljø og CI tidligt, kan du stramme skruerne en smule.

Brug cache uden at ofre determinisme

Caching handler om at gemme node_modules eller npm/yarn/pnpm caches mellem builds for fart. Men du skal koble den til lockfilen.

Eksempel i GitHub Actions (npm):

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

Når lockfilen ændrer sig, ændrer nøglen sig også, og du får et frisk install.

Byg altid fra rent miljø

Du kan godt cache, men undgå at lave incremental installs oven på et halvgammelt node_modules.

Med npm er det netop npm ci, der løser det: den sletter node_modules først.

Med Yarn/pnpm kan du også slette node_modules eksplicit før install, hvis du vil være sikker.

Opdag drift mellem lokal og CI

En lille øvelse, når I har sat tingene op:

  1. Kør npm ci (eller frozen/immutable) lokalt.
  2. Commit alt, så working directory er rent.
  3. Kør samme kommando igen.

Hvis lockfilen ændrer sig på trin 3, er der noget, der ikke er deterministisk. Så skal du kigge i npm/yarn/pnpm docs og evt. i deres issue trackers. Du vil have, at samme kommando på samme repo ikke laver nye ændringer.

Når det først gælder, står du stærkere, hvis du også har styr på dine database-migrationer i teams. Det er samme type “reproducerbarhed”-problem, bare for data.

Monorepos og workspaces – ekstra sjov med lockfiles

Hvis du har et monorepo med flere pakker, kan lockfilen dække hele træet på én gang.

Én root-lockfil, mange pakker

Fx:

  • Yarn workspaces med én yarn.lock i roden
  • pnpm workspaces med én pnpm-lock.yaml
  • npm workspaces med én package-lock.json

Det er fint og normal praksis. Men det betyder også, at en install i pakke A kan påvirke dependency-træet i pakke B.

Regler jeg selv bruger i monorepos

  • Ingen lokale node_modules overrides i de enkelte pakker.
  • Kun root-kommandoer til install: npm install fra roden, ikke nede i pakkerne.
  • Lockfil-ændringer må gerne være store, men PR-beskrivelsen skal nævne, hvad der er installeret/ændret.

Og så hjælper det at have en dedikeret “dependency onderhouds-PR” en gang imellem, hvor man bevidst opdaterer ting. Lidt som at vande planter struktureret i stedet for at hælde vand, når de allerede hænger.

Mini-øvelser: forstå din egen lockfile bedre

Hvis du vil have lidt mere styr på din egen opsætning, kan du prøve nogle hurtige øvelser.

Øvelse 1: Hvad bruger I egentlig?

  1. Find din lockfil.
  2. Tjek, hvilken package manager repoet faktisk er bygget til. Kig efter:
  • “packageManager” felt i package.json (npm/pnpm med version)
  • Yarn-specifikke felter (fx "yarnPath" i .yarnrc.yml)

Hvis du finder flere spor (både npm og yarn), er første opgave at rydde op og vælge én.

Øvelse 2: Simuler CI lokalt

Prøv at køre samme kommando som din CI gør, lokalt. Fx:

rm -rf node_modules
npm ci

Hvis det fejler, har du allerede fundet en forskel, før den rammer build-pipelinen.

Øvelse 3: Tjek for gamle “spøgelses-deps”

Nogle værktøjer kan hjælpe dig med at spotte ubrugte dependencies, fx depcheck i Node-land.

Du kan også bare:

  • Fjerne en mistænkt dependency fra package.json
  • Køre install
  • Køre tests og build

Det er lidt brutalt, men effektivt. Det minder ret meget om at pille en hylde ned og se, hvad der faktisk stod på den.

Tjekliste: dependency-hygiejne på et kvarter

Hvis du vil lave en hurtig oprydning i dag, kan du bruge den her lille tjekliste.

1. Vælg én package manager

  • Slet lockfiler, der ikke matcher den, I har valgt.
  • Tilføj evt. en note i README om, hvad der er “den rigtige”.

2. Sørg for at lockfilen er committed

  • Tjek .gitignore for at sikre, at lockfilen ikke bliver ignoreret.
  • Commit den aktuelle lockfil til main.

3. Sæt CI op til deterministisk install

  • npm: npm ci
  • Yarn: yarn install --frozen-lockfile
  • pnpm: pnpm install --frozen-lockfile

Hvis du er i tvivl om hvordan, så kig f.eks. på mønstrene i artiklen om JavaScript-CI der fejler på ting, der virker lokalt.

4. Aftal et lille sæt team-regler

  • Hvornår må man ændre lockfilen?
  • Hvem har ansvaret for dependency-opdateringer (alle, roterende, én person)?
  • Hvordan håndterer I merge-konflikter (regen vs. vælge én side)?

5. Lav én “maintenance”-PR

  • Kør en clean install (ci/frozen/immutable).
  • Commit ændringer i lockfilen.
  • Sørg for at tests og build kører igennem.

Det svarer lidt til at tømme skuffen med gamle opladere: det er irriterende én gang, men føles meget bedre bagefter.

Det vigtigste råd

Behandl lockfilen som en build-artifact du kontrollerer med regler, ikke som støj du ignorerer, så forsvinder 9 ud af 10 “mystiske” dependency-fejl af sig selv.

Start med at tjekke om package.json rent faktisk er ændret. Rebase på main og kør npm ci (eller tilsvarende for yarn/pnpm) lokalt for at reproducere diffen; hvis lockfilen stadig ændrer sig uden package.json-ændringer, nulstil lockfilen til main og undersøg node/npm-versioner eller caches før du committer. Hvis ændringer er legitime, hold dem adskilt i en egen commit med forklaring og kør tests i CI.
Aftal én package manager og fastlæg versionspolitik for node/npm i repoet (fx .nvmrc og engines). Automatiser kontrol i CI med en frozen/immutable-install, brug pre-commit hooks eller et script der afviser uautoriserede lockfile-ændringer, og lad én person eller et bot-værktøj stå for opdateringer af afhængigheder.
Prøv først målrettede løsninger som npm overrides (eller resolutions for yarn) for at tvinge en specifik version, test lokalt og i CI med frozen-install. Alternativt brug npm audit fix eller opdater den direkte afhængighed der trækker den ind, og lav en isoleret PR med lockfile-ændringer og testresultater.
Brug kommandoer som npm ls for at se hvem der afhænger af den givne pakke, og sammenlign lockfilens træ før og efter ændringer med git diff. Tjek også om forskellige udviklere bruger forskellige package-manager-versioner eller om CI-cache har påvirket installationen.

Sara Vestergaard er selvlært kode-nørd, der stille og roligt er gået fra at rode med en enkelt HTML-side til at bygge små værktøjer, scripts og hjemmesider til sig selv og vennerne. Hun startede med at lave en simpel band-hjemmeside som teenager og opdagede, hvor tilfredsstillende det er, når noget, du har skrevet, pludselig lever på skærmen.

For Sara handler kodning ikke om store ord eller imponerende titler, men om meget konkrete problemer: den kedelige opgave, der tager for lang tid, den ven der mangler en lille porteføljeside, eller den liste, der burde sortere sig selv. Hun elsker at pille ting fra hinanden – også kode – for at se, hvad der egentlig foregår, og hun har brugt utallige aftener på at google fejlbeskeder, teste små eksempler og langsomt bygge sin forståelse op.

På Coding Class deler hun den tilgang videre. Hun skriver til dig, der gerne vil lære at kode ved at gøre det i praksis: små projekter, korte kodebidder og forklaringer, der hænger sammen med det, du faktisk sidder med på skærmen. Hun skærer ind til benet, viser typiske fejl og deres løsninger og giver altid et forslag til, hvordan du kan bygge en tand videre, når grundideen først virker.

Når hun ikke skriver til Coding Class eller nørkler med nye små projekter, hænger Sara på klatrevæggen, vander sine altanplanter eller spiller gamle Nintendo-spil. Men hun ender næsten altid tilbage ved tasterne – for der er altid endnu en lille ting, der kunne være smartere, hurtigere eller bare lidt sjovere at bruge.

Send kommentar

You May Have Missed