Journey to the center of node_modules

Ahmed Hassanein
Frontend Weekly
Published in
8 min readFeb 8, 2021

--

As frontend developers, we usually treat node_modules as this huge magical black-box where you sacrifice mega or gigabytes of your hard disk space in the hope of getting more productive by adding random people code to yours until it work. Crazy but it actually works… until it doesn’t!

I know, it’s that meme that every node_modules related article uses -.-

This is a story about how I managed to get in and out of node_modules in one piece.

Just another normal working day, working on a bunch of features and issues and trying to be as productive as possible.

Ticket done, what's next? Oh nice, an easy one: security guys are not happy that `yarn audit` is failing. Time for some upgrades, yay!

I don’t know about you, but I learn by doing. If you are like me then I have good news for you: you can follow this journey on your own machine.

I tried to reproduce the issue using as little code as possible to mimic our complex real-world micro-frontend-ish monorepo situation where I had this problem. Unless you already tried all of the techniques I mentioned I strongly recommend to clone this repo and follow step by step: clone this repo, open it on vscode and let’s go. I will be waiting right here…

note: I assume you use vscode, but it’s not a requirement. Who am I to start an editor war!

So, what do we have here? In short, it’s a simple monorepo using lerna.

One package is built using an outdated version of create-react-app, the other is just a demo package to give you a feeling of what lerna is if you have never used it before. It’s actually not important nor related to our problem at all. However, I didn’t know this when I started, so I added it ;)

In your terminal, open the repo and in the top folder run yarn then yarn serve

You should see the output of both packages (thanks to lerna’s --parallel flag) and in a few seconds a new tab opens in your browser with the dev server of cra running, and you should see a spinning React logo.

Every thing looks ok, nice.

Let’s assume this is the working status master where you started to work on this “quick” upgrade task.

“quick” -.-

Since I’m greedy and lazy, I try to upgrade all of them at once (after carefully reading all the changelogs for sure, I’m lazy not insane!)

I try to run yarn serve locally before pushing and I notice the dev server opens but it is broken. Just to be sure it’s not just some crazy node_modules issue I run yarn clean but I get the same result, so it’s definitely related to the upgrades.

Ok, Not my luckiest day but it’s not a big deal, right? …right?? :(

Let’s see. The terminal output seems ok: no errors or warnings whatsoever.

You scratch your head as you start getting this feeling that it will be a long, long day -.-

Before we get lost in technical details, please keep in mind that the actual specific issue we are trying to solve here is not the main reason for writing this article. My point is sharing some debugging concepts and techniques that I use whenever I face a similar situation.

Divide and conquer (aka minimize the variables)

Think of this situation as a puzzle. Answer those 3 questions and you can solve it!

  • What is broken?
  • Why is it broken?
  • How can we fix it?

Whenever you have a broken situation without an error message or a warning, your best friend will be the last working state.

Go back to this state (hopefully using git) and then look deeper into what changed. If many things changed then try to break them down.

In our case the suspects are a bunch of packages, so, I reverted all of them and started the slower but safer process of upgrading them one by one while making sure the server still runs every time. If it does: I commit this small win (both package.jsonand yarn.lock) to rule out this suspect and keep investigating in a slow but air-tight way.

not really, but it did take a couple of hours since each try takes about 10 min :(

AHA, found it. react-scripts is our killer. I went from v3.4.0 to v4 and although no related breaking-changes were mentioned it broke something.

Ok, I follow the Divide and Conquer approach once more and try to reduce changes again. We have many versions between v3.4.0 to v4, so I try to upgrade to even a tiny patch version (v3.4.1) and strangely it doesn’t work!
Try it yourself.

Strange, but at-least now we know the difference between a working app and a broken one is hidden inside THIS. EXACT. VERSION.

We took some time but definitely answered the first question: What is broken?

smol steps

This approach is crucial.

If I didn’t do this I would have been lost in all those other packages trying to guess which upgrade did what.

Now I know for sure where to look for next clues.

One more example to this approach is the yarn clean command I made.

Normally, deleting node_modules completely is not needed and actually slows things down even more but to avoid another variable I eliminated any possibility of cache-related issues making sure the only change is coming from the version upgrade.

Anyway, back to our problem.

I get a cup of coffee and start by committing the working state (aka the same state you have now) to… you know why! Yep, to rule out any possible confusion by whatever changes are coming next.

I update to broken version, run yarn clean Again and as expected: some lines changed in yarn.lock and who knows what ***t in node_modules also changed.

The problem with node_modules is that it’s ignored in git, and we have no easy way to visualize or keep track of its changes. Frankly, even if we came up with a way this sounds like a nightmare with all the gigantic, unformatted and re-generated files.

A good developer knows how to find a solution that is achievable in a reasonable time and effort. Remember, work smart not hard ;)

Ok, Let’s take a step back, we now know the What, now we need the Why.
Let’s try to get more clues from somewhere else. The terminal looks like a good place to start.

I go back to the working state, run yarn serve again and copy everything into my favorite diff tool.

I shut down the server, clear the terminal, upgrade, run yarn serve again and copy the broken state logs into the other window.

When I compare (ignoring expected differences etc.) I notice the working state last line is :

cra-module: ℹ 「wds」: Project is running at http://192.168.0.16/

while the broken one gets stuck at:

cra-module: Starting the development server…

Hmm… interesting.

We definitely have a problem with whichever line that comes after it in our working state.

We go back to our open diff state and we find that:

Compiled successfully!

Is definitely missing.

What a shame! Let’s see what evil forces are blocking it.

Now we have enough clues to do the impossible: look into the forbidden treasure chest of node_modules. Don’t worry, it’s all text files in the end.

vscode is kind enough to disable searching inside node_modules by default. It’s also kind enough to give insane explorers like us a way to easily include it in our search. Let’s search for this string.

Bonus tip: if you find nothing in another case, try to remove some words until you find something.

For example, If you search for “Error: found 5 errors” it’s most lickly that in javascript world it’s written like this “Error: found ${count} errors” so I search for “Error: found” instead and so on.

We are lucky and we find this in “./node_modules/react-dev-utils/WebpackDevServerUtils.js” :

if (isSuccessful) {console.log(chalk.green('Compiled successfully!'))}

Now we are talking, to avoid having a very long article I will skip some details here. In short, I added many console.logs to this file but didn’t find something interesting.

It’s time for a step back again, let’s see what are we running exactly: we run yarn serve and this runs lerna run start and I know that this mean lerna will try to run yarn start inside each package. Let’s see what this start command does inside react-scripts folder in node_modules.

I find a start.js file, I could add more console.logs but I’m starting to lose my patience, so I bring out the big guns ;)
I shut down the server, start a JavaScript debugging terminal and start debugging line by line.

YES, it’s THAT EASY to debug node.js in vscode. LIFE. CHANGER.

Soon enough I run into this suspicious block

if (isInteractive || process.env.CI !== ‘true’) {

// Gracefully exit when stdin ends

process.stdin.on(‘end’, function() {

devServer.close();

process.exit();

});

process.stdin.resume();

}

It shuts down the webpack server silently, this doesn’t sound good.
I leave a breakpoint there and it does indeed hit it!

One last time, I try to take lerna out of the equation now so I try to run the command directly not trough lerna and guess what, it does work!

Seems like isInteractive is false if lerna runs the command and true if we run it directly(cd into ./packages/cra-package and then run start)

We did it! We found the Why, now we need to unlock the How to solve the puzzle.

Unfortunately, you can’t just save some random fix inside node_modules because, as you hopefully already know, as soon as you run yarn again it will be reverted. Not to mention that I’m still not sure whats really wrong here!

Anyway, I ended up patching it for now by setting `process.env.CI` to true simply by changing the start command in cra-package to “CI=true react-scripts start”.

I know, very Ugly fix, but IT WORKS, for now at least.

PHEWWW! What a crazy ride!

An actual photo of me after fixing the damn issue :)

I hope it was worth your time. Let me know if it was helpful and how would you have solved it since I’m not too proud of the time I spent on it tbh.

Thanks for reading!

--

--