npm link caveat

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:

  1. Check if graphql is a core module. Nope, it isn't. 🤷🏻‍♀️ Go to next step.
  2. Look for graphql in ~/libA/dist/node_modules. Found it? No? Ok, next step then. 👷🏼‍♀️
  3. 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 run npm 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.

🖖🏼