Don’t Let Vote and pnpm burn you – externalizing dependencies
We recently moved our component library monorepo to use pnpm
. We haven't started taking full advantage of the benefits of pnpm
but we did find some minor improvements right away.
At the same time we took a major step backwards. All of a sudden when running the build on certain packages we got errors like these:
import { defineConfig } from 'vite';
import externalizeDeps from 'vite-plugin-externalize-deps';
export default defineConfig({
plugins: [externalizeDeps()]
});
Although it seemed like react-select
was the issue, it would end up happening with other dependencies if we removed react-select
.
Understanding the problem
This was the hardest part. From doing some research I knew it had to do something with how rollup
resolved dependencies compared to node
.
This issue seemed to surface because of how pnpm
builds the node_modules
folder compared to npm
. It actually appeared to be a side effect of how npm
treated the hoisted node_modules
folder that rollup
somehow used. With that not available with pnpm
, the build would fail.
The first stab - hoist dependencies
The first stab at trying to solve I began to hoist the dependencies so that rollup
could find them.
That meant adding to the ppm-workspace.yaml
file:
import { defineConfig } from 'vite';
import externalizeDeps from 'vite-plugin-externalize-deps';
export default defineConfig({
plugins: [externalizeDeps()]
});
That didn't work but finally did get it to work with:
import { defineConfig } from 'vite';
import externalizeDeps from 'vite-plugin-externalize-deps';
export default defineConfig({
plugins: [externalizeDeps()]
});
That did it! Something about the public
nature of this hoist solved it that I can't fully appreciate. I can't tell you how many times I read over how pnpm
build the node_modules
folder with symlinking and central repositories, but it still never totally clicked.
hoisting was not the solve
But hoisting really wasn't a great solve. I was able to resolve @babel/runtime but then it broke on @emotion
. Solved that then another, and another and another.
I kept adding them until I got about 10 deep and thought, "this isn't going to work..." It's too brittle. At some point some dependency is going to change their dependencies and then I'm SOL.
Externalizing is the key
At some point I hit on the fact that externalizing dependencies was really what I wanted to do.
Now I already knew I needed to externalize dependencies because I was building a library didn't want to bundle all dependencies along with my code. Whenever the consumer installs it should just install all dependencies into the node_modules
folder.
But it was the way I was externalizing that was faulty. I was just taking the package.json
file and import the dependencies
and peerDependencies
fields and externalizing those.
import { defineConfig } from 'vite';
import externalizeDeps from 'vite-plugin-externalize-deps';
export default defineConfig({
plugins: [externalizeDeps()]
});
What pnpm
had exposed is that this will only externalize my dependencies but not the dependencies of those dependencies. For some reason before with npm
rollup
was able to resolve them, even though they weren't being bundled.
That is the part I still don't totally understand is that even though I was seemingly externalizing my dependencies, rollup
didn't look at it that way, it still was trying to resolve for dependencies of dependencies.
The solve: vite-plugin-externalize-deps
After trying to figure out multiple ways to parse through the dependency tree to add to the externalize list, I stumbled upon this plugin by davidmyersdev. This plugin does all the hard work for you, externalizing all dependencies, even dependencies of dependencies and of those dependencies, etc...
I was able to make it work with the default options, but this plugin gives a number of useful options.
import { defineConfig } from 'vite';
import externalizeDeps from 'vite-plugin-externalize-deps';
export default defineConfig({
plugins: [externalizeDeps()]
});
Final thoughts
Bundling is hard. There is no way around it. They are doing some amazingly complex work that I will never fully comprehend. But every now and again I figure out another little piece.
If you’re using Vite to build libraries, and you want to externalize dependencies the right way—especially with pnpm—use vite-plugin-externalize-deps. It will save you hours of debugging and let you focus on shipping code.