unknown39825

How NPM Resolve Modules

A comprehensive guide that explains npm dependency resolution with clear visual examples of how the node_modules directory structure changes at each step.

NPM Dependency Resolution: A Visual Guide

What is Dependency Resolution?

When you run npm install, npm needs to figure out where to place each package in your node_modules directory. The goal is to create the flattest possible tree (minimal nesting) while ensuring all packages get the correct versions they need.

Key Principles

  1. Flatter is better - Less nesting means smaller package size and better performance
  2. Installation order matters - The first package installed gets priority for top-level placement
  3. Compatibility first - If two packages need different versions, npm will nest them to avoid conflicts

Step-by-Step Example

Let's walk through the exact scenario from the article with directory structures and PlantUML dependency diagrams:

Step 1: Install Module-A (depends on Module-B v1.0)

Command: npm install module-a

Dependency Graph:

Loading Diagram...

Directory Structure:

node_modules/
├── module-a/
└── module-b/  (v1.0)

What happened: Both Module-A and its dependency Module-B v1.0 are installed at the top level (flat structure).

Step 2: Install Module-C (depends on Module-B v2.0)

Command: npm install module-c

Dependency Graph:

Loading Diagram...

Directory Structure:

node_modules/
├── module-a/
├── module-b/  (v1.0) ← Already exists at top level
└── module-c/
    └── node_modules/
        └── module-b/  (v2.0) ← Nested because v1.0 is already at top

What happened: Module-C is installed at top level, but its dependency Module-B v2.0 is nested because Module-B v1.0 already occupies the top level.

Step 3: Install Module-D (depends on Module-B v2.0)

Command: npm install module-d

Dependency Graph:

Loading Diagram...

Directory Structure:

node_modules/
├── module-a/
├── module-b/  (v1.0)
├── module-c/
│   └── node_modules/
│       └── module-b/  (v2.0)
└── module-d/
    └── node_modules/
        └── module-b/  (v2.0) ← Another copy! Duplication occurs

What happened: Module-D also needs Module-B v2.0, but since v1.0 is at the top level, npm creates another nested copy.

Step 4: Install Module-E (depends on Module-B v1.0)

Command: npm install module-e

Dependency Graph:

Loading Diagram...

Directory Structure:

node_modules/
├── module-a/
├── module-b/  (v1.0) ← Shared by Module-A and Module-E
├── module-c/
│   └── node_modules/
│       └── module-b/  (v2.0)
├── module-d/
│   └── node_modules/
│       └── module-b/  (v2.0)
└── module-e/  ← No nested module-b needed!

What happened: Module-E can use the existing top-level Module-B v1.0, so no duplication needed.

The Update Scenario

Step 5: Update Module-A to v2.0 (now depends on Module-B v2.0)

Command: npm install [email protected]

Dependency Graph:

Loading Diagram...

Directory Structure:

node_modules/
├── module-a/  (v2.0)
│   └── node_modules/
│       └── module-b/  (v2.0) ← Nested because v1.0 still needed by Module-E
├── module-b/  (v1.0) ← Still needed by Module-E
├── module-c/
│   └── node_modules/
│       └── module-b/  (v2.0)
├── module-d/
│   └── node_modules/
│       └── module-b/  (v2.0)
└── module-e/

What happened: Module-A v2.0 needs Module-B v2.0, but v1.0 is still at the top level (needed by Module-E), so v2.0 gets nested under Module-A.

Step 6: Update Module-E to v2.0 (now depends on Module-B v2.0)

Command: npm install [email protected]

Dependency Graph:

Loading Diagram...

Directory Structure:

node_modules/
├── module-a/  (v2.0)
│   └── node_modules/
│       └── module-b/  (v2.0)
├── module-b/  (v2.0) ← Now v2.0 at top level!
├── module-c/
│   └── node_modules/
│       └── module-b/  (v2.0) ← Still duplicated
├── module-d/
│   └── node_modules/
│       └── module-b/  (v2.0) ← Still duplicated
└── module-e/  (v2.0)

What happened:

  • Module-E v1.0 removed
  • Module-B v1.0 removed (no longer needed)
  • Module-B v2.0 promoted to top level
  • But we still have duplicates!

The Solution: npm dedupe

Command: npm dedupe

Dependency Graph (After Deduplication):

Loading Diagram...

Directory Structure:

node_modules/
├── module-a/  (v2.0)
├── module-b/  (v2.0) ← Single copy at top level
├── module-c/
├── module-d/
└── module-e/  (v2.0)

What happened: All nested copies of Module-B v2.0 are removed, and all modules now reference the single top-level copy.

Visual Summary: Complete Dependency Evolution

Here's a comprehensive PlantUML diagram showing the entire dependency resolution journey:

Loading Diagram...

Key Takeaways

  1. Installation order determines tree structure - First installed package gets top-level priority
  2. Different machines can have different trees - But your app will work the same way
  3. Use npm install from package.json for consistency - It installs in alphabetical order
  4. Clean installs ensure consistency - Delete node_modules and run npm install to get the same tree every time
  5. Use npm dedupe to optimize - Removes unnecessary duplicate packages

Best Practices

  • Always use npm install instead of manually copying packages
  • When in doubt, delete node_modules and reinstall
  • Run npm dedupe periodically to optimize your dependency tree
  • Be aware that different installation orders can create different (but functionally equivalent) trees