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

Steps

  1. Step 1.

    List the direct dependencies as they are installed.

    npm ls --depth=0
    
    h2check@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.8

    Twenty-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.

  2. Step 2.

    Trace one package instead of reading the whole tree.

    npm ls postcss
    
    h2check@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 deduped

    Three requesters, two versions. deduped on the last line means that branch resolves to the 8.5.28 already printed above, so two copies are installed rather than three.

  3. 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

    --parseable prints 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.

  4. 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.8

    Nine 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.

  5. 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 1

    The same command reports a problem in its text and returns success. Step 1 printed extraneous twice 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

Sign: A pipeline step runs npm ls, the log shows extraneous packages, and the step passes.Cause: On npm 10.9.8 the exit code was 0 with two extraneous packages in the tree, and 1 only when a requested name was not installed. The findings are still reported: npm ls --depth=0 --json put both of them in a problems array. Gate on that array, not on the exit status.
Sign: The version in npm ls --depth=0 is not the version the application loads.Cause: Depth 0 shows one entry per direct dependency. A copy nested under another package never appears there. This tree prints zod@4.1.8 at depth 0 and carries zod@3.25.76 inside chromium-bidi, three levels down.
Sign: deduped is read as a package that was removed.Cause: It marks a branch whose requirement is satisfied by a copy already listed above it in the same output. The package is installed once and every requester resolves to it. To see where that single copy sits on disk, ask npm explain for the path.

What to check next

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.

basic6 minpublished updated Maks Verny