Why npm, Yarn, pnpm, cnpm, tnpm, and Deno Differ in Dependency Management – A Deep Dive

This article examines how npm, Yarn, pnpm, cnpm, tnpm, and Deno handle dependency installation, version locking, and node_modules structures, highlighting the evolution from nested to flattened layouts, the emergence of phantom and multiple dependencies, and the trade‑offs of each approach.

Alibaba Terminal Technology
Alibaba Terminal Technology
Alibaba Terminal Technology
Why npm, Yarn, pnpm, cnpm, tnpm, and Deno Differ in Dependency Management – A Deep Dive

Preface

npm is the package manager for Node.js. In addition, the community offers similar tools such as Yarn, pnpm, cnpm, and the internally used tnpm. Developers typically use these package managers to generate a node_modules directory, install dependencies, and manage them.

npm

When we run npm install, npm downloads the required packages, extracts them to a local cache, constructs the node_modules directory structure, and writes dependency files.

1. npm v1/v2 Dependency Nesting

Early npm versions used a simple nesting model. If a project depends on modules A and C, and both depend on different versions of B, the generated node_modules looks like:

Dependency Hell

Each module’s dependencies are placed in its own node_modules folder, leading to duplicated installations when multiple top‑level modules require the same package version. This waste of space is known as dependency hell.

node_modules<br/>├── [email protected]<br/>│   └── node_modules<br/>│       └── [email protected]<br/>├── [email protected]<br/>│   └── node_modules<br/>│       └── [email protected]<br/>└── [email protected]<br/>    └── node_modules<br/>        └── [email protected]<br/>

2. npm v3 Flattening

npm v3 rewrote the installer to flatten dependencies (hoisting) into the top‑level node_modules, reducing deep nesting and duplication. npm also implements an upward‑search algorithm to locate modules, installing a package only once unless a version conflict forces a nested copy.

Phantom Dependency

A phantom dependency occurs when a package is not listed in package.json but can still be required because it was hoisted by another dependency.

var A = require('A');
var B = require('B'); // ???

Incompatible dependency: a library may not declare a version range for B, so a major update of B can break consumers.

Missing dependency: dev‑dependency sub‑dependencies may be required at runtime, causing errors on other machines.

Non‑Determinism

With npm v3 the exact node_modules layout depends on the order of installations. Changing a top‑level version can alter the resolved tree, leading to different structures on different machines.

3. npm v5 Flattening + Lock

npm v5 introduced package-lock.json, which records the exact version, source, and integrity hash of every installed package and its sub‑dependencies, guaranteeing deterministic installs.

Consistency

Updating a top‑level dependency does not change the locked sub‑dependency versions, so the resulting node_modules layout remains unchanged.

Compatibility – Semantic Versioning

npm follows SemVer (major.minor.patch). Version ranges in package.json use symbols:

~ : only patch updates

^ : minor and patch updates

* : any newer version

Because many packages ignore these rules, sub‑dependencies can be upgraded unintentionally, causing incompatibilities. package-lock.json pins exact versions to avoid this.

Yarn

Yarn was released in 2016 to address issues in npm v3 before npm v5 existed. It aims to be fast, secure, and reliable.

1. Yarn v1 lockfile

Yarn generates the same node_modules layout as npm v5 and creates a yarn.lock file. Example lockfile excerpt:

A@^1.0.0:
  version "1.0.0"
  resolved "uri"
  dependencies:
    B "^1.0.0"

B@^1.0.0:
  version "1.0.0"
  resolved "uri"

B@^2.0.0:
  version "2.0.0"
  resolved "uri"

C@^2.0.0:
  version "2.0.0"
  resolved "uri"
  dependencies:
    B "^2.0.0"

D@^2.0.0:
  version "2.0.0"
  resolved "uri"
  dependencies:
    B "^2.0.0"

E@^1.0.0:
  version "1.0.0"
  resolved "uri"
  dependencies:
    B "^1.0.0"

Yarn lock vs. npm lock

File format differs: npm uses JSON, Yarn uses a custom format.

npm’s lockfile records exact versions; Yarn’s lockfile may retain range symbols.

npm lockfile contains richer dependency‑tree information.

2. Yarn v2 Plug'n'Play

Yarn 2.x introduced Plug'n'Play (PnP), eliminating node_modules. Running yarn --pnp creates a .pnp.cjs file that maps packages to their locations and resolves require calls directly, removing I/O overhead.

Pros: no node_modules, faster installs, avoids phantom dependencies.

Cons: requires Yarn’s node wrapper, limited compatibility with existing Node ecosystem.

pnpm

pnpm 1.0 launched in 2017, offering fast installs, disk‑space savings, and better security by using hard links and symbolic links to emulate a true DAG.

Hard links save disk space

pnpm stores package contents in a global store and creates hard links in each project’s node_modules, so identical versions are stored only once.

Symbolic links create nested structure

When a project installs bar that depends on foo, pnpm creates a symbolic link from node_modules/bar to the actual package location in the global store.

This layout ensures only true dependencies are accessible, eliminating phantom dependencies and reducing duplicate installations.

Limitations

pnpm’s lockfile ( pnpm-lock.yaml) is not compatible with npm’s package-lock.json.

Symbolic links may not work in environments like Electron or AWS Lambda.

Some tools (e.g., Webpack plugins) rely on relative paths and need adaptation.

Modifying a linked file can unintentionally affect other projects.

cnpm and tnpm

cnpm, maintained by Alibaba, mirrors the official npm registry for China. tnpm builds on cnpm to provide a private registry for Alibaba’s ecosystem. Both adopt pnpm‑style non‑flattened node_modules using symbolic links, though cnpm does not use hard links.

tnpm’s rapid mode uses a user‑space file system (FUSE) to improve compatibility of symbolic links.

Deno

Deno discards npm, package.json, and node_modules. Dependencies are imported via URLs, which are cached globally. Example:

import * as log from "https://deno.land/[email protected]/log/mod.ts";

Developers create a dep.ts file that re‑exports required remote modules, then import from dep.ts in the project to avoid scattered URL imports.

// dep.ts
export { assert, assertEquals, assertStringIncludes } from "https://deno.land/[email protected]/testing/asserts.ts";

// index.ts
import { assert } from "./dep.ts";

While Deno’s approach eliminates node_modules and reduces disk usage, it introduces verbose URLs, security concerns, and a less mature ecosystem compared to Node.

Conclusion

There is no perfect dependency‑management solution yet. The history of package managers shows continuous learning and optimization, driving forward front‑end engineering. Future tools will likely build on these lessons to provide better, more deterministic dependency handling.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

dependency managementpackage managerpnpmnpmYARNnode_modulesDeno
Alibaba Terminal Technology
Written by

Alibaba Terminal Technology

Official public account of Alibaba Terminal

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.