ミツモア Tech blog

「ミツモア」を運営する株式会社ミツモアの技術ブログです

Migrating to PNPM from Yarn

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]

content-store

Why Consider Migrating from Yarn?

While Yarn has been a popular package manager, pnpm offers several compelling reasons to switch:

  1. Storage Efficiency
    • pnpm uses a content-addressable filesystem
    • Dramatically reduces duplicate package storage
    • Can save gigabytes of disk space in large projects
  2. Performance Improvements
    • Faster installation times
    • More efficient dependency resolution
    • Reduced overhead in package management
  3. Enhanced Dependency Management
    • Stricter dependency linking
    • Better handling of peer dependencies
    • More predictable and consistent package installations
  4. Modern Development Workflow
    • Native monorepo support
    • Advanced workspace capabilities
    • More intuitive project structure management

Boosting installation speed
Traditional three-stage installation process

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

  1. Install pnpm according to its official docs.

     brew install pnpm
    
  2. Delete node_modules using npx npkill .

  3. Add script to package.json to prevent installing by other package manager.

     "scripts": {
       "preinstall": "npx only-allow pnpm",
       ...
     }
    
  4. 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/**'
    
  5. Run pnpm import to create a pnpm-lock.yaml file based on yarn.lock (or packages-lock.json)
  6. Remove yarn.lock (or packages-lock.json)
  7. Run pnpm i to install dependencies.
  8. Replace npm run (or yarn) to pnpm in all package.json and other files (E.g. pnpm test instead of npm 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.

pnpm

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 all package.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.

Reference

  1. https://engineering.meetsmore.com/entry/2021/12/06/112931
  2. https://www.kochan.io/nodejs/why-should-we-use-pnpm.html
  3. https://divriots.com/blog/switching-to-pnpm/
  4. https://github.com/pnpm/pnpm/issues/5736#issuecomment-2056948410