Tags: web-dev concept

Lockfiles

Date: 2026-08-17


A record of exactly which versions were resolved, so every install produces the same tree. Committing it is not a preference — without one, two people running the same command on the same day can get different code.


A lockfile records the fully-resolved dependency tree: every package, its exact version, its resolved URL, and an integrity hash.

package.json     what you WANT      ^4.17.0
lockfile         what you GOT       4.21.4
                                    + every
                                      transitive
                                      dependency

Why it’s not optional

Version ranges mean the resolved tree depends on when you install.

Monday    you install    → lodash 4.21.4
Tuesday   4.21.5 is published
Tuesday   colleague installs → lodash 4.21.5

same package.json, different code

Without a lockfile, “works on my machine” is structurally guaranteed — and it applies to every transitive dependency, which is where most of the tree lives and where you have no version control at all.

With a lockfile: the tree is fixed until someone deliberately changes it, and that change appears in a diff someone can review.

What’s inside

"node_modules/lodash": {
  "version": "4.21.4",
  "resolved": "https://registry.npmjs.org/...",
  "integrity": "sha512-v2kDEe57..."
}

The integrity hash is the security-relevant part. It’s a hash of the package contents, verified on install — so a package silently altered at the registry fails rather than installing — Hashing, Supply Chain Risk.

Install strictly in CI

npm ci          # not: npm install
pnpm install --frozen-lockfile
yarn install --immutable
npm installnpm ci
Can update the lockfileYesNo
Fails if it disagrees with package.jsonNoYes
Deletes node_modules firstNoYes
SpeedSlowerFaster

npm install in CI defeats the lockfile, because it will happily resolve something new and carry on. npm ci is the correct command in every automated context.

Reviewing lockfile changes

The diff is enormous and mostly unreadable, which is why it gets rubber-stamped. Two things are worth checking anyway:

  • Did a package appear that nobody added? A new transitive dependency arriving with a minor bump is normal and worth noticing
  • Does the change match the intent? A one-line package.json bump producing 400 lockfile changes means a transitive tree moved substantially
npm ls --all | less        # the resolved tree
npm explain lodash         # WHY is this here?

npm explain is the underused one — it answers “which of my dependencies pulled this in”, which is the question every unexpected package raises.

Common problems

  • Merge conflicts. Two branches adding dependencies conflict in the lockfile. Never hand-edit it — regenerate:
git checkout --theirs package-lock.json
npm install       # re-resolve from package.json
  • Committing package.json without the lockfile. The declared range changes, the resolution doesn’t, and the two disagree until someone runs a plain install
  • Mixed managers. Both package-lock.json and pnpm-lock.yaml in one repo means two different trees depending on who installs — Package Managers
  • .gitignore-ing it. Occasionally done deliberately for libraries, and wrong for applications. See below

Applications versus libraries

The one legitimate nuance:

  • Applications commit the lockfile. You control the deployment; reproducibility is the whole point
  • Libraries commit it too — for their own CI — but consumers ignore it. A library’s lockfile doesn’t constrain the projects that depend on it, which is why libraries should use ranges in package.json rather than exact pins — Versioning

The old advice that libraries shouldn’t commit lockfiles predates the security argument. Committing it gives the library’s own CI reproducibility and integrity checking, and costs consumers nothing.

The one-line summary

Commit it, install from it in CI, and treat changes to it as reviewable. Those three habits eliminate an entire class of environment bug and most of the passive supply-chain exposure.