npm link caveat
Table of Contents
Yesterday, I got bit by an interesting caveat of npm link
due to how the module resolution algorithm works in node
and (with plugins) in your favourite bundler. This post documents what I learnt by debugging the issue.
Set the stage
We have a library libA
at path ~/libA
and a project myApp
that consumes libA
at path ~/myApp
. Now say that you want to test myApp
with a local version of libA
to see if things work fine before cutting a new release of libA
.
Cue the music
How do we achieve this, we think? 🤔
Well, one way to do so is by copying libA
to somewhere in the directory structure of ~/myApp
. Well, hmmm, no, that doesn't sounds right. Hmm 🤔
Wait a second. Why not use the wonderful npm link
!
Action!
We choose to do so by cd
-ing into ~/libA
and running npm link
. Following this we cd
into ~/myApp
and run npm link libA
to (sym)link ~/myApp/node_modules/libA
to your local ~/libA
. You run myApp
, it all works fine and you're a happy cat. 😼
Now say libA
has a peer dependency on graphql
(or any package). This means that the consumer of our library must install graphql
in their application. So we go into ~/myApp
and run npm install graphql
. Now you run the application as always but, oh no! myApp
fails to run! The logs on the screen say that libA
was looking for graphql
but didn't find it! What gives?! 🙀
We sit, then stand, then just lie there, staring at the terminal. I mean, we did everything right, no? We installed the graphql
peer dependency in ~/myApp
as instructed. So, really, what in the actual fruit basket?! 😾
Post production
Well, turns out, we got bit by a small lack of understanding on our part combined with the module resolution algorithm of your favourite bundler/node
/what have you.
When we run npm link libA
in ~/myApp
, ~/myApp/node_modules/libA
is (indirectly) symlinked to our local ~/libA
. Now, when we run ~/myApp
and the libA
import
/require
statement is encountered, the file given by the main
or module
key (depending on your 🤓 bundler) in ~/libA/package.json
is loaded. Let's assume that the key points to ~/libA/dist/libA.cjs.js
. This file, in turn, tries to import
/require
graphql
. To do so, node
or your bundler, does the look up roughly as follows:
- Check if
graphql
is a core module. Nope, it isn't. 🤷🏻♀️ Go to next step. - Look for
graphql
in~/libA/dist/node_modules
. Found it? No? Ok, next step then. 👷🏼♀️ - Look for
graphql
in~/libA/node_modules
. It isn't there either! That's because its a peer dependency and isn't automatically installed when we runnpm install
in~/libA
!
semiiii enlightenment 👼
This process continues, one directory higher at a time in the hierarchy, at every step till it gives up and ends.
But, Mudit, why not just install the peer dependency in ~/libA
using npm install graphql --no-save
and move on with our lives? Well, that defeats the purpose Jimmy, that's why!
In anycase, the biggest reason this all happens is right there in front of us — the module resolution starts off in the wrong directory! Well, not really wrong but wrong for us! Bad computer!
We notice that all the look ups are happening relative to ~/libA
, not ~/myApp
where we already did install graphql
! This is due to the fact that npm link
creates symbolic links! And that's the root cause, Jimmy! Due to the fact that npm link libA
creates a symlink inside ~/myApp/node_modules
(indirectly) to ~/libA
, the module resolution algorithm (correctly) uses the real location of libA
for the look up of graphql
.
totally 100% moksha level enlightenment 👼🏼++
Release and 🎉
Well that was quite a journey. At least for me I'd say heh. It had me scratching my head for a bit and hopefully it helps someone else out there.