Today I learned: Utilizing pnpm packageExtensions to fix broken dependencies

About 2 min reading time

I'm working on the migration of a monorepo from Vue.js 2 to Vue.js 3 right now. As we are in a monorepo we can migrate our applications stepwise but got into a situation where suddenly our Vue.js 2 tests failed with the following error:

Vue packages version mismatch:

- vue@3.2.40 (...)
- vue-template-compiler@2.7.8 (...)

This may cause things to work incorrectly. Make sure to use the same version for both.
If you are using vue-loader@>=10.0, simply update vue-template-compiler.
If you are using vue-loader@<10.0 or vueify, re-installing vue-loader/vueify should bump `vue-template-compiler` to the latest.

This didn't make sense to me at first because we don't have any project that uses vue-template-compiler and Vue.js in version 3. So, why do we see this conflict?

Cause 1: Hoisting

Hoisting in node package managers refers to the behavior where dependencies that are required by multiple packages in a project are installed only once, at the highest level possible in the package tree. This reduces duplication and helps conserve disk space, since each dependency only needs to be installed once. In our case, our package manager moved Vue.js and the vue-template-compiler into the root node_modules folder. So, we ended with Vue.js 2 and Vue.js 3 in our root node_modules folder.

Cause 2: vue-template-compiler is designed with only one Vue.js version in the project

The vue-template-compiler defines its dependency on Vue.js with the "version" "file:../.." so it assumes that the Vue.js library is in the same node_modules folder as the vue-template-compiler and can refer it via a local path (valid in a package.json).

    // vue-template-compiler doesn't specify a Vue.js version
    "devDependencies": {
        "vue": "file:../.."
    }

This sounds strange, but it makes somehow sense: When vue-template-compiler is installed as a dependency of a Vue.js 2 project, it needs to use the same version of Vue.js as the project itself, to ensure compatibility between the compiled templates and the runtime Vue.js library.

To achieve this, vue-template-compiler uses a local path to the Vue.js package that is installed in the project's node_modules directory, rather than relying on a global or external version of vue.

So, the problem was, that my package manager moves both dependencies to the root node_modules folder of my project and the vue-template-compiler tries to work with the wrong Vue.js version.

PNPM to the rescue

Now there are two ways to fix this with pnpm (thanks StackOverflow):

  1. you can use pnpm to disable hoisting, which was not possible in our project
  2. you can use pnpm packageExtensions to "fix" the dependencies

In your root package.json this is possible by adding the following code:

{
    // ...
    "pnpm": {
        "packageExtensions": {
            "vue-template-compiler": {
                "peerDependencies": {
                    "vue": "<your Vue.js version>"
                }
            }
        }
    }

Please note that you can do similar stuff with other package managers, too. I use pnpm a lot, so this did the trick for me. So if you are on a different package manager, maybe this helps you to find a good solution.

Basically, this fixed my setup, and it can be helpful if you need to adjust some libraries dependencies to work in your specific environment.


This post is based on my opinion and experience. It is based on what worked for me in my context. I recognize, that your context is different.
The "Just Sharing" Principle