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):
- you can use pnpm to disable hoisting, which was not possible in our project
- 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.