I built a zero-dep CLI that finds unused dependencies — and is wrong less often than depcheck

javascript dev.to

Open any package.json that's been alive for a year and I'll bet there's a
package in there you stopped importing three refactors ago. It's still installed,
still in the lockfile, still showing up in npm audit, still adding seconds to
every npm install. Nobody removes it because nobody's sure it's safe to
remove.

The classic answer was depcheck. I reached for it for years — until I noticed it
got archived in 2025 with a long tail of
open false-positive bugs. And those false positives are the whole problem: a tool
that tells you react is unused (it isn't, you're just on the new JSX transform)
teaches you to ignore it. The successor, knip, is genuinely
great — but it's a ~150-plugin tool built for big codebases, and sometimes you
just want a five-second answer to "what can I delete right now?"

So I built depsnap, and it makes the opposite bet from depcheck.

The bet: prove unused, not used

depcheck tries to prove a dependency is used — trace the imports, resolve the
specifiers — and every gap in that analysis becomes a false "unused". TypeScript
type-only imports, subpath imports, dynamic import(), CLIs invoked only in npm
scripts, @types/* stubs, peer deps: each one is an edge case where the analysis
misses the usage and wrongly flags the package.

depsnap flips it. It only flags a dependency when its name appears nowhere
not as an import, not as a require, not as a dynamic import(), not as a string
in a config file, not as a CLI in an npm script. The bias is always toward not
flagging. The payoff: every result is high-confidence removable, and all those
edge cases that broke depcheck just work, because a name match doesn't care how
the package is referenced:

Case depcheck depsnap
import type { X } from 'pkg' often flagged unused kept
import 'pkg/subpath' sometimes missed kept
await import('pkg') sometimes missed kept
@types/* stubs flagged unused skipped
CLI used only in a script (tsc) flagged unused kept (reads node_modules bins)
peer dependencies flagged unused not checked

What it looks like

npx depsnap
Enter fullscreen mode Exit fullscreen mode
depsnap — 1 unused dependency in my-app (of 14 checked)

  ● left-pad  dependencies  no import or script reference

Verify before removing — depsnap only flags names it found nowhere · 2 @types/* skipped.
Enter fullscreen mode Exit fullscreen mode
depsnap --dev           # also check devDependencies
depsnap --format json   # for CI
Enter fullscreen mode Exit fullscreen mode

It exits non-zero when it finds something, so it drops into a pipeline:

- run: npx depsnap --dev
Enter fullscreen mode Exit fullscreen mode

How it actually decides

  1. Read package.json for declared dependencies.
  2. Walk the project — skipping node_modules, dist, .git — and build a corpus of all your source and config files. Crucially, it never reads package.json or the lockfile, because those list every dependency by name and would make everything look "used". (That bug is easy to ship; I'm telling you so you don't.)
  3. For each dependency, check whether its name appears in that corpus, in the npm scripts, or as one of its installed bin names — using import-aware word boundaries so lodash doesn't match lodash-es.
  4. Report the ones that appear nowhere.

The honest limitation

depsnap will not catch a dependency you import but never actually call. That
needs real reachability analysis, and that's exactly what knip is for. depsnap is
the fast, zero-config, zero-dependency first pass — the "low-hanging fruit, with no
false alarms" tool. The two compose well: run depsnap to delete the obvious dead
weight today, reach for knip when you want the deep clean.

Install

npx depsnap            # Node — zero deps
pip install depsnap    # Python — pure stdlib, works on any JS/TS project
Enter fullscreen mode Exit fullscreen mode

Both implementations are byte-for-byte identical in their output.

Try it / break it

It's MIT and tiny. I'd genuinely like to know what it flags wrongly in your repo —
the failure mode I care about is a false positive, so if depsnap ever tells you
to remove something you actually need, that's a bug I want to hear about.

What's your current move on a dependency you think is unused — delete it and run
the tests, or leave it because you're not sure?

Source: dev.to

arrow_back Back to Tutorials