Introduction
What is pnpm?
pnpm (performant npm) is a fast, disk-space-efficient package manager for JavaScript and Node.js projects. Unlike traditional package managers like Yarn and npm, pnpm introduces a unique approach to dependency management that offers significant advantages in terms of performance and storage efficiency.
Key characteristics of pnpm include:
- Unique dependency resolution strategy
- Significantly reduced disk space usage
- Faster installation times
- Strict dependency linking
- Built-in support for monorepos and workspaces]
Why Consider Migrating from Yarn?
While Yarn has been a popular package manager, pnpm offers several compelling reasons to switch:
- Storage Efficiency
- pnpm uses a content-addressable filesystem
- Dramatically reduces duplicate package storage
- Can save gigabytes of disk space in large projects
- Performance Improvements
- Faster installation times
- More efficient dependency resolution
- Reduced overhead in package management
- Enhanced Dependency Management
- Stricter dependency linking
- Better handling of peer dependencies
- More predictable and consistent package installations
- Modern Development Workflow
- Native monorepo support
- Advanced workspace capabilities
- More intuitive project structure management
Key Benefits of pnpm
Benefit | Description | Impact |
---|---|---|
Disk Space Optimization | Uses hard links and a content-addressable storage | Reduces project storage by up to 50% |
Performance | Faster installations and updates | Saves development time |
Dependency Integrity | Stricter dependency resolution | Reduces dependency-related issues |
Workspace Support | Native monorepo capabilities | Simplifies complex project structures |
Security | Improved dependency isolation | Minimizes potential security risks |
Compatibility and Considerations
Before migrating, it's essential to understand that while pnpm is highly compatible with existing npm and Yarn projects, some minor adjustments might be necessary:
- Node.js version compatibility
- Existing project configuration
- Specific package requirements
- CI/CD pipeline modifications
Migration Complexity
The migration process is generally straightforward, with most projects requiring minimal changes. However, complexity can vary based on:
- Project size
- Dependency complexity
- Unique configuration requirements
Migration Process
Install pnpm according to its official docs.
brew install pnpm
Delete node_modules using
npx npkill
.Add script to package.json to prevent installing by other package manager.
"scripts": { "preinstall": "npx only-allow pnpm", ... }
Create workspace file if necessary
# pnpm-workspace.yaml packages: # include packages in subfolders (e.g. apps/ and packages/) - "apps/**" - 'packages/**' # if required, exclude some directories - '!**/test/**'
- Run
pnpm import
to create apnpm-lock.yaml
file based onyarn.lock
(orpackages-lock.json
) - Remove
yarn.lock
(orpackages-lock.json
) - Run
pnpm i
to install dependencies. - Replace
npm run
(oryarn
) topnpm
in allpackage.json
and other files (E.g.pnpm test
instead ofnpm run test
)
Here is the comparison before and after the migration. Before: After:
Potential Migration Challenges
Important! You need to keep in mind that pnpm
doesn’t use dependency hoisting:
When installing dependencies with npm or Yarn Classic, all packages are hoisted to the root of the modules directory. As a result, source code has access to dependencies that are not added as dependencies to the project.
By default, pnpm uses symlinks to add only the direct dependencies of the project into the root of the modules directory.
In practice it means that if you have a package A
that imports a package B
(import something from 'B'
) but doesn’t explicitly specify B
in the dependencies
or devDependencies
, then the execution will fail.
For mono repos, you may encounter some peer dependency mismatch issues like the versions of react and react-dom. pnpm also provide the way for you to resolve it.
pnpm lets you hook directly into the installation process via special functions (hooks). Hooks can be declared in a file called .pnpmfile.cjs
.
By default, .pnpmfile.cjs
should be located in the same directory as the lockfile. For instance, in a workspace with a shared lockfile, .pnpmfile.cjs
should be in the root of the monorepo.
// .pnpmfile.cjs function readPackage(pkg, context) { // all our workspaces start with @off if (pkg.name.startsWith('@off')) return pkg; const isReactInUse = 'react' in pkg.dependencies || 'react' in pkg.devDependencies || 'react' in pkg.peerDependencies; const isTypesReactInUse = '@types/react' in pkg.devDependencies || '@types/react' in pkg.peerDependencies || '@types/react' in pkg.dependencies; if (isReactInUse || isTypesReactInUse) { delete pkg.devDependencies['@types/react']; const peerDependencies = pkg.peerDependencies || {}; pkg.peerDependencies = peerDependencies; // our workspaces had 17 and 18 in use peerDependencies['@types/react'] = '17 || 18'; } return pkg; } module.exports = { hooks: { readPackage, }, };
Easter egg
"Catalogs" are a workspace feature for defining dependency version ranges as reusable constants. Constants defined in catalogs can later be referenced in
package.json
files.
In a workspace (i.e. monorepo or multi-package repo) it's common for the same dependency to be used by many packages. Catalogs reduce duplication when authoring package.json
files and provide a few benefits in doing so:
- Maintain unique versions — It's usually desirable to have only one version of a dependency in a workspace. Catalogs make this easier to maintain. Duplicated dependencies can conflict at runtime and cause bugs. Duplicates also increase size when using a bundler.
- Easier upgrades — When upgrading a dependency, only the catalog entry in
pnpm-workspace.yaml
needs to be edited rather than allpackage.json
files using that dependency. This saves time — only one line needs to be changed instead of many. - Fewer merge conflicts — Since
package.json
files do not need to be edited when upgrading a dependency, git merge conflicts no longer happen in these files.
# pnpm-workspace.yaml packages: - packages/* # Define a catalog of version ranges. catalog: "@biomejs/biome": 1.9.4 turbo: 2.3.1
# packages/example-app/package.json { "name": "@example/app", "dependencies": { "@biomejs/biome": "catalog:", "turbo": "catalog:" } }
This is equivalent to writing a version range (e.g. 1.9.4
) directly.
# packages/example-app/package.json { "name": "@example/app", "dependencies": { "@biomejs/biome": "1.9.4", "turbo": "2.3.1" } }
Conclusion
Fast, disk space efficient package manager
The transition offers developers a powerful tool that dramatically reduces installation times and minimizes disk space consumption. pnpm's innovative approach to dependency management provides unprecedented efficiency, making it an attractive choice for modern web development workflows.
For teams looking to enhance their package management strategy, pnpm offers a compelling solution that balances performance, simplicity, and scalability. Embrace the change, test thoroughly, and unlock the full potential of your JavaScript projects.