Skip to content
← Blog

Technical explainer

Reading a package-lock.json: what every field actually means

3 min read

package-lock.json is the file npm uses to record the exact dependency tree your project will install. It is also the most-skipped file in code review. A few minutes of familiarity with its structure pays off the next time a dependency update produces a thousand-line lock diff and someone has to decide whether to merge it.

What the file is for

package-lock.json exists so that two installs of the same project produce byte-identical node_modules — same versions, same nested copies, same integrity hashes. Without a lockfile, every npm install re-resolves against the registry and may pick newer versions of transitive dependencies, even if your package.json is unchanged.

npm ci installs strictly from the lockfile. npm install may update the lockfile if your package.json ranges have changed or if npm decides better resolutions are available.

Top-level keys

Modern lockfiles (lockfile version 2 and 3) have these top-level keys.

  • name, version: identification of the project. Mostly informational.
  • lockfileVersion: an integer (2 or 3 today). Determines the on-disk format.
  • requires: a boolean retained for legacy compatibility.
  • packages: the canonical record. Each entry describes one installed package by its path in the tree.
  • dependencies: a legacy-format mirror of packages, kept for older tools.

packages is what you read. dependencies is duplication.

What a packages entry looks like

Here is one entry from a typical project:

"node_modules/lodash": {
  "version": "4.17.21",
  "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
  "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
  "dev": true
}

Key by key:

  • node_modules/lodash — the path inside the dependency tree where this package lives. Top-level packages are node_modules/<name>; nested packages are node_modules/<parent>/node_modules/<child>.
  • version — the exact version installed.
  • resolved — the URL the tarball came from. Useful when reviewing whether a dependency was pulled from your expected registry.
  • integrity — the subresource-integrity hash of the tarball. npm verifies this on install. Any mismatch fails the install.
  • dev / optional / peer — flags about how the package entered the tree.

For workspaces, the packages key also contains entries for the workspace root and each workspace package; their version field comes from the local package.json.

Reading a diff

Most lockfile diffs fall into a few patterns.

Routine version bump: the version, resolved, and integrity of one or more entries change. This is what you get from a npm update or a manual range bump in package.json. Look for: did the change cross a minor or major boundary? Did transitive dependencies change as a side effect?

New dependency added: a new path appears in packages. Read the path — is it node_modules/<expected-name> or something nested under an unexpected parent? Where did the new transitive come from?

Tree restructure: many entries change paths because a shared dependency moved from one location to another (hoisting), but versions are stable. Usually benign; check that nothing critical moved deeper than expected.

resolved URL changed but version did not: the registry source changed. If you expected your packages to come from a private registry and one suddenly resolves to https://registry.npmjs.org/, that is worth investigating.

integrity changed but version did not: this should never happen for a non-mutable version. Some registries support overwriting versions; that's a red flag. Pin the integrity hash and verify against a known-good source.

Common review pitfalls

  • Long diffs from npm install rather than npm ci. A npm install that updates the lockfile produces churn unrelated to your intended change. Run npm ci in CI; commit lockfile updates only from intentional dependency changes.
  • Ignoring peer entries. A new peer dependency can mean a transitive version got bumped in a way that requires a peer you didn't have before. That can break runtime behavior.
  • Trusting the registry URL field on faith. If you have an internal registry, verify the resolved URLs match. A misconfigured npm install can pull from public when you meant private.

Why this matters

Reading a lockfile diff is the only consistent way to see what your next deploy will actually fetch. SemVer ranges are a promise about what might happen; the lockfile is a record of what will. For supply-chain review, that distinction is the whole game.