How to check dependency tree npm
Run npm ls --depth=0 for the packages your package.json asked for, and npm ls --all for everything beneath them. In this repository the first lists 21 packages, the second 691 lines covering 252 distinct name and version pairs. Pass a package name to trace one branch.
Why check this
A tester runs this after any install that moved the lockfile, and again before a release build. The tree is the only place where a second copy of a library becomes visible.
The failure it catches is a split runtime. This repository declares zod@4.1.8 and also carries zod@3.25.76 under chromium-bidi. A schema object built by one major version is not recognised by the other, so a value that validates in a unit test fails inside a library that resolved the other copy. Nothing in package.json says so, and both test suites stay green.
Prerequisites
- Node 22 and npm 10. Every output below came from npm 10.9.8 on Node 22.23.2.
- A project with node_modules installed.
npm lsreports what is on disk, which is why packages that no manifest requires still show up in it. - The npm ls documentation for the flags used here.
- The counts on this page are this site's repository on 2026-09-12. Yours will differ; the shapes in the output will not.
Steps
- Step 1.
List the direct dependencies as they are installed.
npm ls --depth=0h2check@0.1.0 D:\how2check +-- @emnapi/runtime@1.11.3 extraneous +-- @img/sharp-wasm32@0.35.4 extraneous +-- @tailwindcss/postcss@4.1.13 +-- @types/node@22.18.1 … +-- typescript@5.9.2 +-- vitest@5.0.0 `-- zod@4.1.8Twenty-one packages, two of them marked
extraneous. That word means the directory exists in node_modules and no manifest in the tree asks for it. - Step 2.
Trace one package instead of reading the whole tree.
npm ls postcssh2check@0.1.0 D:\how2check +-- @tailwindcss/postcss@4.1.13 | `-- postcss@8.5.28 +-- next@16.3.5 | `-- postcss@8.5.23 `-- vitest@5.0.0 `-- vite@8.3.0 `-- postcss@8.5.28 dedupedThree requesters, two versions.
dedupedon the last line means that branch resolves to the8.5.28already printed above, so two copies are installed rather than three. - Step 3.
Measure the tree instead of scrolling it.
npm ls --all --parseable | sort -u | awk '{n++} /node_modules.*node_modules/ {d++} END {print n" dirs, "d" nested"}'255 dirs, 11 nested--parseableprints one filesystem path per installed package, so this counts directories rather than branches. The 11 nested paths are the packages npm could not hoist to the top level. - Step 4.
Name the packages that are installed at more than one version.
npm ls --all --json | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const m=new Map();(function w(n){for(const [k,v] of Object.entries(n.dependencies||{})){if(v.version){if(!m.has(k))m.set(k,new Set());m.get(k).add(v.version);}w(v);}})(JSON.parse(s));for(const [k,v] of [...m].sort())if(v.size>1)console.log(k,[...v].join(' '));})"@esbuild/win32-x64 0.25.12 0.28.2 @types/unist 3.0.3 2.0.11 esbuild 0.25.12 0.28.2 lightningcss 1.30.1 1.33.0 lightningcss-win32-x64-msvc 1.30.1 1.33.0 magic-string 0.30.21 1.3.1 postcss 8.5.28 8.5.23 string-width 7.2.0 8.2.2 zod 3.25.76 4.1.8Nine names, and the last one crosses a major version. That line is the finding named in "Why check this", and no reading of package.json produces it.
- Step 5.
Read the exit code, because a pipeline will.
npm ls --depth=0 >/dev/null 2>&1; echo "extraneous: exit $?"; npm ls definitely-not-installed-xyz >/dev/null 2>&1; echo "missing: exit $?"extraneous: exit 0 missing: exit 1The same command reports a problem in its text and returns success. Step 1 printed
extraneoustwice and exited 0.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| extraneous after a name | The directory is installed and nothing in the tree requires it | Delete node_modules, run npm ci, then look again. |
| deduped on a branch | This requester shares a copy printed earlier | Read it as one install, not zero. |
| One name, two versions | A requester pinned a range the hoisted copy does not satisfy | Trace both with npm explain. |
| (empty) under the root | No installed package matches the name you passed | Check the spelling before concluding it is absent. |
Common mistakes
What to check next
- How to check why a package is installed: turns one line of this tree into the chain of requesters behind it.
- How to check unused dependencies in package json: the opposite direction, packages declared and never imported.
- How to check if package lock is in sync: whether the tree on disk matches the lockfile CI installs from.
- How to check npm packages for vulnerabilities: the same tree read for advisories rather than for shape.
FAQ
How do I check transitive dependencies?
npm ls --all prints every level. For one package, npm ls <name> prints only the branches that lead to it, which was 8 lines here instead of 691.
Does npm ls read package-lock.json?
It reports what is installed in node_modules. The two extraneous lines in step 1 are the proof: those directories exist on disk and no manifest in the tree names them.
How do I see only what ships to production?
npm ls --omit=dev --depth=0. In this repository that cut 21 entries to 6, of which 4 are real dependencies and 2 are the extraneous pair.
Why does the tree differ between my machine and CI?
Optional packages are selected by platform, so a Windows tree and a Linux tree hold different binary packages. Compare the lockfile, which is platform independent, rather than two npm ls runs.
Verified
Verified by Maks Vernynpm 10.9.8Node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic6 minpublished updated Maks Verny