7 min read

Why I Replaced fnm and Corepack With mise

One tool for project-specific runtimes and package managers, without giving up existing version files.


In my post about managing Node.js and package-manager versions per project, I described a setup using fnm and Corepack. fnm selected the Node.js version, and Corepack selected the package manager and its version.

That worked. But it is no longer what I use.

I now use mise for both and in this post you’ll learn about why and how. The goal is still the same: I want to switch between projects without thinking about which versions I need.

Why I Moved Away From Corepack

One reason to reconsider Corepack came from pnpm itself. The pnpm account posted on X:

That is pretty direct and made me curious. I wasn’t aware that it is discouraged…

This comment by pnpm maintainer Zoltan Kochan explains the reasoning. Corepack adds a JavaScript wrapper around pnpm. With pnpm v12 being a native executable, starting Node.js just to run that wrapper adds overhead before pnpm itself even starts.

The pnpm CI documentation makes the same point: this is a cost paid on each invocation, and CI jobs can invoke pnpm many times.

That does not mean removing Corepack magically makes dependency downloads or resolution faster. It means avoiding an extra startup step for every command.

In addition to that, Corepack doesn’t come wiht node by default anymore and it seems less well maintained.

pnpm’s suggested alternative is to install pnpm directly and let pnpm handle version management. It can also manage Node.js. That is a reasonable option, but it is not the setup I want. It feels weird to me that a package manager also manages package manager.

Why I Chose mise Instead

I want one tool to manage my development tools, not my package manager to manage my development environment.

I use Node.js and pnpm in some projects, but I also use Bun in others. Those projects don’t necessarily use the same Bun version either. Occasionally, I work with other languages, and I don’t want to learn and configure another version manager each time.

mise covers those cases with the same workflow. Instead of fnm for Node.js, Corepack for package managers, and another solution for Bun or another language, I have one place to manage project-specific tool versions.

There is also a useful distinction in how commands run. With mise’s normal shell activation, the selected tools’ installation directories are placed on PATH. Running pnpm then launches the selected executable directly, without going through Corepack’s JavaScript wrapper.

For me, the biggest benefit is simpler day-to-day use. Avoiding the extra Corepack startup step is another reason to prefer it.

ℹ️

Omarchy Quattro also uses mise. Seeing it adopted there gave me more confidence in choosing it for my own setup.

There Is More to mise

mise isn’t just a version manager. It can also run tasks such as building, testing, or linting a project, and manage environment variables per project, including loading .env files. These are not the focus of this post though.

My Setup

I use macOS and install mise through Homebrew:

Terminal window
brew install mise

For Zsh, shell activation goes in ~/.zshrc:

Terminal window
eval "$(mise activate zsh)"

Other shells have their own activation instructions.

You can set personal defaults globally:

Terminal window
mise use --global node@lts pnpm@latest bun@latest

Those are fallbacks, not the versions every project must use. lts and latest are floating version requests, not automatic background updates. I update those defaults with mise upgrade.

In a project, you can select and install specific versions with mise use. For example:

Terminal window
mise use --pin node@24 pnpm@12

This writes the resolved versions to the project’s mise.toml. Commit that file so the project declares its tooling. In another checkout, mise install installs the configured tools. You may first be prompted to trust the repository’s mise configuration; review it before approving. Shell activation selects installed versions as you move between projects.

But you don’t have to move every existing project’s version declarations into mise.toml. In most projects I don’t do that because I work with other people who may not (yet?) use mise.

Keeping Existing Version Files

This setting is particularly useful when moving from fnm or nvm:

~/.config/mise/config.toml
[settings]
idiomatic_version_file_enable_tools = ["node", "pnpm", "bun"]

mise calls these idiomatic version files: the version declarations already used by a language or its ecosystem. Support is disabled by default, so you explicitly enable it for the tools you want.

For my setup, the relevant ones are:

  • Node.js: .nvmrc, .node-version, and supported declarations in package.json.
  • pnpm: package.json, including the top-level packageManager field or devEngines.packageManager.
  • Bun: .bun-version and supported declarations in package.json.

That means an existing project can keep its .nvmrc and a declaration such as "packageManager": "pnpm@12.4.2". mise can read them rather than requiring duplicate version pins in a new configuration file.

For runtime versions in package.json, mise reads devEngines.runtime, not engines. The latter describes compatibility, which is not necessarily the version you want to develop with.

ℹ️

pnpm@12 also uses devEngines, including devEngines.packageManager to select its own version. Since I want mise to handle that, I disable pnpm’s package-manager version management by setting pmOnFail: ignore in pnpm-workspace.yaml. This also skips pnpm’s package-manager version check; it does not disable runtime management via devEngines.runtime.

With shell activation enabled, mise uses those declarations to select the appropriate installed versions when I enter a project. Enabling the setting is not the same as installing missing versions—use mise install to install the tools the project declares.

I particularly like that this doesn’t force everyone on a team to use mise. A teammate can keep using fnm or nvm with the same .nvmrc.

There are idiomatic version files for many more tools, including Python, Ruby, Go, and Java. I enable the integrations I need rather than turning everything on.

You can check why a specific version of a tool is selected (and which are installed overall) by running mise ls. Example output:

~/Code/shopify/mergeit > mise ls
Tool Version Source Requested
bun 1.4.0
bun 1.4.2 ~/.config/mise/config.toml latest
node 22.23.2
node 24.21.0 ~/Code/shopify/mergeit/.nvmrc 24
node 26.1.0
pnpm 10.5.2
pnpm 11.10.0
pnpm 12.4.2 ~/Code/shopify/mergeit/package.json 12.4.2

Note: In the terminal it is also color-coded and the active versions have a different color.

As you can see, in my MergeIt project, I use node@24 and pnpm@12.4.2 at the moment and their sources are found because of that idiomatic_version_file_enable_tools setting.

mise or Homebrew?

I still use Homebrew. There is plenty of overlap: many tools are available through both Homebrew and mise.

My distinction is simple:

  • If I need multiple versions or project-specific versions, I use mise. Node.js, pnpm, and Bun fall into this category.
  • If one current version for the whole system is enough, I use Homebrew. Git and the GitHub CLI are good examples. So is mise itself.

This is not a limitation of either tool. It is just a division of responsibilities that works well for me. I don’t need a different Git version for each repository, but I do need different runtime and package-manager versions.

Switching Over Without Changing Every Project

The important part of migrating is making sure the old and new setups aren’t both trying to select your tools.

My checklist for moving from the setup in the previous post is:

  • Replace fnm or nvm shell initialization with mise activation.
  • Remove obsolete Corepack configuration, such as FNM_COREPACK_ENABLED.
  • Check for existing Corepack shims and other installations that might take precedence on PATH.
  • Enable idiomatic version files for the tools you use, or declare their versions in mise.toml.
  • Install the required tools with mise install.
  • Open a fresh terminal and run type -a node pnpm bun to reveal competing installations on PATH. Check the selected versions with node --version, pnpm --version, and bun --version.

Same Goal, Fewer Moving Parts

My previous post called managing Node.js and package-manager versions a solved problem. I still think it is. I have just changed which solution I prefer.

I recommend using mise instead of nvm/fnm, Corepack (and probably also other version-managing tools) and enjoy a simpler (and IMO better) dev setup.

Homebrew handles the tools I want once. mise handles the tools I need in different versions. And switching projects still doesn’t require manually switching versions.