Migrating From Yarn To Pnpm

6 min read

It’s been about two years now since the Yarn dev team released yarn@2 otherwise known as berry. It was a significant change to the initial project and I remember the rumblings in Dev Twitter over the changes. I myself have never given berry a try mostly because I didn’t want to deal with the cognitive load of changing a piece of tooling that was so crucial to my daily workflow. I was scrolling Twitter one night and came across this tweet from @jaredpalmer and snagged the link to check it out later (and by later I mean when I finally get bothered enough by the 90 opened tabs in Safari):

What is pnpm?

pnpm is yet another package manager for the JavaScript ecosystem similar to npm & yarn. Where pnpm differs from the latter two is that:

npm and yarn both will create a new node_modules directory with fresh installs of the dependencies of your projects. So if you have 10 react projects on your machine there are 10 installs of react regardless of version on your machine as well. pnpm instead installs all dependencies in one location on your hard disk and hard links them to the node_modules/.pnpm in your projects. In this case there would only be one install of react (unless your projects utilize different version of the dependency) for all 10 projects. The node_modules for each project would reference back to the dependencies location on the hard disk.

Installing

I use homebrew when I can for installing tooling and other software on my machine so when I was able to install pnpm via homebrew I was very happy. FYI the official documentation does not show this as an install method, however I did verify that it is not malicious:

brew search pnpm
brew info pnpm
  pnpm: stable 6.7.3 (bottled)
  📦🚀 Fast, disk space efficient package manager
  https://pnpm.js.org
  /usr/local/Cellar/pnpm/6.7.3 (631 files, 8.9MB) *
    Poured from bottle on 2021-06-08 at 13:38:58
  From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/pnpm.rb
  License: MIT
  ==> Dependencies
  Required: node
  ==> Analytics
  install: 1,474 (30 days), 3,179 (90 days), 9,829 (365 days)
  install-on-request: 1,475 (30 days), 3,178 (90 days), 9,827 (365 days)
  build-error: 0 (30 days)
brew install pnpm

Everything after this is almost entirely identical to using yarn. You can look at the official documentation for more information on commands.

You will also get pnpx installed globally which is the equivalent of npx.

pnpm even has support for workspaces, which I have not played with yet but plan on doing so in the future.

Issues with the migration

First off I just removed yarn and all it’s artifacts from my machine so every project broke. So needless-to-say that has been fun migrating over projects.

“yarn” cannot be found

Most of them have been as simple as:

rm -rf node_modules yarn.lock
pnpm -i
 
Packages: +1176
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Packages are hard linked from the content-addressable store to the virtual store.
Content-addressable store is at: /Users/codybrunner/.pnpm-store/v3
Virtual store is at:             node_modules/.pnpm
Progress: resolved 1175, reused 512, downloaded 663, added 1176, done
 
dependencies:
 
...
 
devDependencies:
 
...

However this was not the case for this website. I continuously received and error message about esbuild could not be found or that the versions were in conflict. I finally just branched off of staging and removed the majority of the source code, node_modules, and emptied the package.json. It turns out that the way pnpm installs packages the esbuild dependency of mdx-bundler is no longer at the top level where mdx-bundler expected it to be. Installing the correct version of esbuild as a dev dependency fixed the issue. There went 3 hours of my day .

The next issue was my development pipeline. I use commitlint, husky, and lint-staged. At first I just updated this to pnpm run <script> however I would get an error back saying "<script> is not a script!". Makes sense because commitlint is not a script in my package.json it’s a binary to be ran. So instead I needed to make use of pnpx. In the .husky directory I need to change the commands in the bash scripts to read like so:

.husky/commit-msg
- yarn run commitlint -e $1
+ pnpx commitlint -e $1

I ran into a similar "<script> is not a script!" issues with my CI/CD workflows on GitHub:

.github/workflows/feature.yml
    steps:
      - name: Linting
-       run: yarn run eslint
+       run: pnpx eslint
 
      - name: Type checking
-       run: yarn run tsc
+       run: pnpx tsc

Updating the steps to be ran in the CI/CD was actually not as painful as I imagined it would be. Even handling the caching in the container was pretty straightforward. Since I was already using the setup-node action npm was present and one of the official ways to install pnpm is by running:

npm install -g pnpm

The documentation on using pnpm with GitHub Actions on pnpm’s site says that you must run this command with root level permissions though so I needed to add a sudo to the front of that in my yaml file.

.github/workflows/feature.yml
steps:
  ...
- - name Get Yarn Cache
-   id: yarn-cache
-   run: echo "::set-output name=dir::$(yarn cache dir)"
 
  - name: Install Node
    uses: actions/setup-node@v2
    with:
      node-version: ${{ matrix.node }}
 
- - name: Cache Dependencies
+ - name: Cache .pnpm-store
+   uses: actions/[email protected]
+   id: pnpm-cache
    with:
-     path: ${{ steps.yarn-cache.outputs.dir }}
-     key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+     path: ~/.pnpm-store
+     key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}
      restore-keys: |
-       ${{ runner.os }}-yarn-
+       ${{ runner.os }}-node-
 
+ - name: Install pnpm
+   run: sudo npm install -g pnpm
 
  - name: Install Dependencies
-   if: steps.yarn-cache.outputs.cache-hit != 'true'
-   run: yarn install --frozen-lockfile
+   if: steps.pnpm-cache.outputs.cache-hit != 'true'
+   run: pnpm install --frozen-lockfile

The last problem I encountered was deploying to Vercel. At the time of writing Vercel does not support pnpm as a package manager option so when Vercel goes to install dependencies and build your project for deployment the deployment will fail IF you have the following in your package.json like I did:

package.json
{
	"scripts": {
		"preinstall": "npx only-allow pnpm"
	}
}

I opened a discussion on adding support for pnpm to Vercel here.

Aliasing

The last thing I did was to map the pnpm commands to the aliases I had in my .zshrc for yarn. I really did not want to deal with completely new aliases hence the mapping of the commands.

.zshrc
# Yarn related aliases
# These have been updated to alias the use of pnpm NOT Yarn
# and to not mess with my own mental modal I am keeping the
# aliases the same where they can be
 
alias ya="pnpm add";
alias yb="pnpm run build";
alias yc="pnpm run commit";
alias yd="pnpm run dev";
alias yga="pnpm add -g";
alias ygl="pnpm ls -g";
alias ygr="pnpm rm -g";
alias ygu="pnpm up -g -i";
alias yi="pnpm i"
alias yl="pnpm run lint";
alias yr="pnpm rm";
alias ys="pnpm start";
alias yt="pnpm t";
alias ytc="pnpm run type-check";
alias yu="pnpm up -i";
~ Cody 🚀

Related Articles


    Going to the Gopher Side

    The chaos that is the JavaScript/TypeScript ecosystem has become too much to bear in my opinion. I have become unhappy with my direction in the tech industry and in late 2023 made the decision to begin teaching myself Go and pivoting my career out of the Frontend & away from JavaScript.

    Python Scripting In iTerm2

    How to use the Python Scripting API in iTerm2 to open several tabs and execute commands in those tabs.

    Getting Started With Python3

    Installation and setup of Python3 on macOS & development with Visual Studio Code.

Cody Brunner

Cody is a Christian, USN Veteran, Jayhawk, and an American expat living outside of Bogotá, Colombia where he works as a Senior Frontend Developer for WAO Fintech.