Bryan

Bryan

twitter
medium

Install multiple versions of pnpm simultaneously & publish a custom homebrew package

If you just want to find a way to install pnpm v7 using Homebrew, you can execute brew install ImSingee/pnpm/pnpm@7.

pnpm v8 was released a month ago, and as one of the major updates, it introduced lock file V6 while discontinuing support for V5. However, when making PRs for projects using older versions, if new dependencies need to be introduced, it will inevitably require updating the lock file — this is unacceptable, and we cannot directly expect all collaborators to upgrade their pnpm.

This introduces the issue of coexisting pnpm V7 and V8. This is not a difficult task, with tools like Corepack or pnvm. However, they are too heavy for me — Homebrew seems sufficient.

One issue with Homebrew is that it does not support installing old versions, and the previously introduced homebrew/versions has long been deprecated. The only officially recommended solution is to self-host — of course, currently, pnpm does not maintain an official old version tap, so we have to do it ourselves.

Create a tap#

According to the official guide, you can create an empty tap library by executing the following command:

brew tap-new ImSingee/homebrew-pnpm

Note: The format of the parameter after tap-new must be <repo>/homebrew-<name>, so that this tag can be enabled via brew tap <repo>/<name>, and subsequently, you can directly install related packages using brew install <repo>/<name>/<formula>【Additionally, if the repo name does not start with homebrew-, this command will automatically add this prefix】.

Then it will print out information similar to the following:

Initialized empty Git repository in /opt/homebrew/Library/Taps/imsingee/homebrew-pnpm/.git/
[main (root-commit) 1b89b92] Create imsingee/pnpm tap
 3 files changed, 90 insertions(+)
 create mode 100644 .github/workflows/publish.yml
 create mode 100644 .github/workflows/tests.yml
 create mode 100644 README.md
==> Created imsingee/pnpm
/opt/homebrew/Library/Taps/imsingee/homebrew-pnpm

When a pull request making changes to a formula (or formulae) becomes green
(all checks passed), then you can publish the built bottles.
To do so, label your PR as `pr-pull` and the workflow will be triggered.

The general idea is that it helps you create a project under /opt/homebrew/Library/Taps/imsingee/homebrew-pnpm and configures GitHub Action to help you test and configure bottles.

Add a formula#

We will add a Formula for pnpm@7, here is the official pnpm.rb:

class Pnpm < Formula
  require "language/node"

  desc "📦🚀 Fast, disk space efficient package manager"
  homepage "https://pnpm.io/"
  url "https://registry.npmjs.org/pnpm/-/pnpm-8.3.1.tgz"
  sha256 "ce038ba2617f7a93d0b1f24b733b9d64258b15c97a14c6f37673c8d49e033d9a"
  license "MIT"

  livecheck do
    url "https://registry.npmjs.org/pnpm/latest"
    regex(/["']version["']:\s*?["']([^"']+)["']/i)
  end

  bottle do
    sha256 cellar: :any_skip_relocation, arm64_ventura:  "78ecd13f60c3baf6913933c8494ca17fc4e5b9f93c46bbc131312ffe41fe7f88"
    sha256 cellar: :any_skip_relocation, arm64_monterey: "78ecd13f60c3baf6913933c8494ca17fc4e5b9f93c46bbc131312ffe41fe7f88"
    sha256 cellar: :any_skip_relocation, arm64_big_sur:  "78ecd13f60c3baf6913933c8494ca17fc4e5b9f93c46bbc131312ffe41fe7f88"
    sha256 cellar: :any_skip_relocation, ventura:        "2f4f18876a3e2823f86f5500b7c47c173695e7f21eba007c2b7689dd12301145"
    sha256 cellar: :any_skip_relocation, monterey:       "2f4f18876a3e2823f86f5500b7c47c173695e7f21eba007c2b7689dd12301145"
    sha256 cellar: :any_skip_relocation, big_sur:        "4be656f6ff04e145810fb6e19f08fb01030798cec610c9d618b1fb01121d9f64"
    sha256 cellar: :any_skip_relocation, x86_64_linux:   "78ecd13f60c3baf6913933c8494ca17fc4e5b9f93c46bbc131312ffe41fe7f88"
  end

  depends_on "node" => :test

  conflicts_with "corepack", because: "both installs `pnpm` and `pnpx` binaries"

  def install
    libexec.install buildpath.glob("*")
    bin.install_symlink "#{libexec}/bin/pnpm.cjs" => "pnpm"
    bin.install_symlink "#{libexec}/bin/pnpx.cjs" => "pnpx"
  end

  def caveats
    <<~EOS
      pnpm requires a Node installation to function. You can install one with:
        brew install node
    EOS
  end

  test do
    system "#{bin}/pnpm", "init"
    assert_predicate testpath/"package.json", :exist?, "package.json must exist"
  end
end

We need to make a few small modifications:

  • The official filename is pnpm.rb, we need to rename it to pnpm@7.rb for distinction.
  • The class name Pnpm needs to be changed to PnpmAT7.
  • The url needs to be modified to the version we require, currently the latest version for 7 is 7.32.2.
  • The sha256 needs to be changed to the corresponding version's hash, currently 7.32.2 corresponds to f4b40caa0c6368da2f50b8ef891f225c24f14e7d60e42a703c84d3a9db8efede.
  • The url under the livecheck section should be changed to https://registry.npmjs.org/pnpm, and the regex to /["']latest-7["']:\s*?["']([^"']+)["']/i.
  • Remove the bottle.

The final modified version is:

class PnpmAT7 < Formula
    require "language/node"
  
    desc "📦🚀 Fast, disk space efficient package manager"
    homepage "https://pnpm.io/"
    url "https://registry.npmjs.org/pnpm/-/pnpm-7.32.2.tgz"
    sha256 "f4b40caa0c6368da2f50b8ef891f225c24f14e7d60e42a703c84d3a9db8efede"
    license "MIT"
  
    livecheck do
      url "https://registry.npmjs.org/pnpm"
      regex(/["']latest-7["']:\s*?["']([^"']+)["']/i)
    end
  
    depends_on "node" => :test
  
    conflicts_with "corepack", because: "both installs `pnpm` and `pnpx` binaries"
  
    def install
      libexec.install buildpath.glob("*")
      bin.install_symlink "#{libexec}/bin/pnpm.cjs" => "pnpm"
      bin.install_symlink "#{libexec}/bin/pnpx.cjs" => "pnpx"
    end
  
    def caveats
      <<~EOS
        pnpm requires a Node installation to function. You can install one with:
          brew install node
      EOS
    end
  
    test do
      system "#{bin}/pnpm", "init"
      assert_predicate testpath/"package.json", :exist?, "package.json must exist"
    end
  end

Some small explanations of the modifications#

Regarding naming (file name and formula name), it is essential to distinguish it from the official pnpm, and using the format pnpm@{VERSION} keeps it consistent with the naming format of other multi-version packages from the official (for example, postgresql, llvm, etc., all use this naming). The class name and file name should correspond, pnpm@7 corresponds to PnpmAT7.

Additionally, our pnpm@7.rb file can be stored in the Formula directory created by the previous tap-new command, or it can be placed directly in the root directory (it can also be placed in HomebrewFormula).

The url and sha256 correspond to the package download address and the hash of the downloaded package. We can use the NPM Registry API to get the latest version and download the corresponding package to obtain the hash.

# Get the latest version of V7
curl -s https://registry.npmjs.org/pnpm | jq '."dist-tags"."latest-7"'

# Get the hash for this version
curl -s https://registry.npmjs.org/pnpm/-/pnpm-7.32.2.tgz | sha256sum

livecheck is used to check if the current version is the latest, the official formula for pnpm takes the latest, and we modify it to take latest-7.

Remove the bottle, as this is content for automated builds, and the new formula should not include it.

Release#

Before releasing, we need to execute brew style --fix pnpm@7.rb to ensure the style meets the standards.

We need to use GitHub Action to trigger testing and bottle building. After submitting the code, do not directly push to the main branch, but submit a PR, and after passing all tests, add the pr-pull label.

Then anyone can directly install our pnpm@7!

# case 1: enable tap
brew tap ImSingee/pnpm
brew install pnpm@7

# case 2: simple install single
brew install ImSingee/pnpm/pnpm@7 

The released repository is at https://github.com/ImSingee/homebrew-pnpm, welcome to use and star :-)

A small tip after installation#

One advantage of naming using <name>@{VERSION} is that it will not pollute our current environment, which can be seen in a prompt after installation:

pnpm@7 is keg-only, which means it was not symlinked into /opt/homebrew,
because this is an alternate version of another formula.

If you need to have pnpm@7 first in your PATH, run:
  echo 'export PATH="/opt/homebrew/opt/pnpm@7/bin:$PATH"' >> ~/.zshrc

Unlike general formulae, this will be detected as a different version of another program, so it will not be installed in the system's PATH but will be placed in an additional path independently.

According to the prompt, if we want to execute version 7 of pnpm, we need to use the full path /opt/homebrew/opt/pnpm@7/bin/pnpm. If we want to make this the primary version, we can add /opt/homebrew/opt/pnpm@7/bin to the PATH.

Of course, my approach is to create a symlink:

ln -s /opt/homebrew/opt/pnpm@7/bin/pnpm /usr/local/bin/pnpm7
ln -s /opt/homebrew/opt/pnpm@7/bin/pnpx /usr/local/bin/pnpx7

References#

https://docs.brew.sh/How-to-Create-and-Maintain-a-Tap

https://stackoverflow.com/questions/3987683/homebrew-install-specific-version-of-formula

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.