Skip to main content

How to Audit a package-lock.json for Duplicate and Risky Dependencies

A practical guide to auditing your package-lock.json: read the full dependency tree, find duplicate versions and bloat, and spot outdated or risky packages.

Published By Li Lei
#package-lock #dependency audit #npm #lockfile #supply chain

How to Audit a package-lock.json for Duplicate and Risky Dependencies

Most teams treat the lockfile as a file they never open. They run npm install, the lockfile changes, they commit it, and nobody reads the diff. But that file is the only honest record of what actually ends up in node_modules. Your package.json lists what you asked for; your package-lock.json records what you got, down to the last transitive dependency.

This guide walks through what a lockfile audit reveals, why duplicate versions quietly inflate your bundle, and how to read the true dependency tree instead of guessing at it.

Why the lockfile is the source of truth

When you write "lodash": "^4.17.0" in package.json, you are stating a range, not a version. The next person who installs could resolve that to 4.17.21 while you got 4.17.15. Ranges are intentions; they are not reproducible.

The lockfile closes that gap. package-lock.json pins the exact version of every direct and transitive dependency, along with the resolved URL and an integrity hash for each package. Two machines installing from the same lockfile get byte-for-byte identical trees. That is what makes npm ci deterministic and what makes a build today match a build six months from now.

Because the lockfile is the resolved truth, it is also where the real problems live. Your package.json might list twelve dependencies. The lockfile might describe nine hundred packages once every transitive dependency is pulled in. Auditing the lockfile is the only way to see all nine hundred at once.

What an audit actually surfaces

A lockfile audit is not a CVE scan. It is a structural inventory. When I run a lockfile through the Package Lock Dependency Auditor, I am looking for a handful of signals that a normal npm install will never warn me about:

  • Total entries vs. unique names. A big gap between "dependency entries" and "unique package names" is the first sign of duplication. If you have 900 entries but only 740 unique names, roughly 160 packages exist at more than one version.
  • Duplicate locked versions. The same library pinned at two or three versions because different parents asked for incompatible ranges.
  • Non-registry sources. Git URLs, GitHub shorthand, and plain HTTP tarballs. These bypass your registry's caching and integrity guarantees and are easy to forget after a quick fix.
  • Prerelease versions. A stray 2.0.0-beta.3 that slipped in during testing and never got bumped to a stable release.
  • Missing integrity metadata. Entries without a integrity hash cannot be verified on reinstall, which weakens reproducibility.

None of these are vulnerabilities on their own. All of them are debt that compounds.

A worked example: the same library at two versions

Here is the case that pushed me to start auditing lockfiles on every release. A frontend build had crept from 240 KB to 310 KB of gzipped JavaScript over a quarter, and nobody could point to a single feature that explained it.

The bundle analyzer showed date-fns appearing twice. Opening the lockfile made the cause obvious. The dependency tree looked like this:

my-app
├── date-fns@3.6.0          (direct dependency, listed in package.json)
└── @company/ui-kit@2.1.0
    └── date-fns@2.30.0     (transitive, pinned by ui-kit's caret range)

Our app depended on date-fns@3.6.0 directly. But an internal UI package, @company/ui-kit, still declared "date-fns": "^2.29.0". Because version 3 and version 2 are not compatible ranges, npm could not collapse them. It kept both. The bundler followed suit and shipped two copies of the same date library to every visitor — roughly 70 KB of duplicate code that did nothing.

The lockfile told the whole story: two date-fns entries, two versions, two different parents. The fix was to bump @company/ui-kit to a release that used date-fns@3, after which a single npm dedupe collapsed the tree back to one copy. The build dropped back under 250 KB. The audit took two minutes; the savings were permanent.

Spotting bloat and outdated packages

Duplicate versions are the most expensive form of bloat because they are invisible in package.json. But the lockfile reveals two more patterns worth chasing.

The first is forgotten transitive depth. A single convenience package can drag in dozens of its own dependencies. When the auditor reports the entry count, an unexpectedly large number is a prompt to ask whether a 40-dependency library is really earning its place over a 20-line helper you could write yourself.

The second is stale pins. Because the lockfile records exact versions, it is easy to scan for packages that are several major versions behind. A library locked at 1.x when the ecosystem has moved to 4.x is not automatically broken, but it is a maintenance flag — old transitive dependencies often carry the unpatched issues that a fresh install would have resolved. The audit gives you the list; you decide what to upgrade. If you need a refresher on the commands to do that cleanup, the npm command cheatsheet covers dedupe, ls, and outdated side by side.

Building lockfile auditing into your workflow

The point of a lockfile audit is to make it routine, not heroic. A few habits keep the tree healthy:

  • Read the lockfile diff in code review. When a one-line package.json change adds 200 lines to the lockfile, that is worth a comment. The diff is where new transitive dependencies and new sources first appear.
  • Audit before a release freeze. Run the lockfile through an inventory pass and flag git dependencies, HTTP tarballs, prereleases, and missing integrity before you cut the release, not after.
  • Catch local debugging leftovers. file: and link: dependencies are great for testing a local package, and a disaster if they get committed. They pin your build to a path that only exists on one machine.
  • Re-check after every dedupe or migration. Moving from npm to pnpm, or running a dedupe, changes the tree shape. Export the before-and-after so you can prove the cleanup actually reduced duplication.

Everything in an audit stays local. A lockfile can expose private package names and internal repository URLs, so a tool that parses it in the browser without uploading anything is the right default. The Package Lock Dependency Auditor reads package-lock.json, pnpm-lock.yaml, and yarn.lock the same way and exports the inventory as Markdown, JSON, or CSV.

The lockfile is already the source of truth for what your users download. Auditing it is just choosing to read what is already written there.


Made by Toolora · Updated 2026-06-13