td0m's blog

dom@tdom.dev

Escaping the npm hell

I still remember the very first time I used npm.

Wow! I can just use npm install react instead of having to manually add a url! Thats so cool! I love npm!

Yes. JavaScript was my first proper language, that’s probably why I liked it: I didn’t know any better. But this post is not about the flaws with js as a language, that’s already been done.

This article is about how you can use some useful npm libraries without having to install npm. It also includes why I no longer use npm, and some useful alternatives I found.

How it all began

Let me tell you about how we met. I was a young secondary school student when I first got my hands on React. I thought the idea of building these complex highly interactive websites was so cool. And that is what React allowed for. I didn’t need an expensive server to host it on, and databases were free with tools like Firebase.

And it was fun. We built so many material design websites together. Animated so many hamburger menus with each other. I allowed react to do what it did best, and react allowed the inner designer in me to prosper. But that period did not last forever.

Soon enough, other alternatives came around.

Have you heard of Vue? It’s like react, but faster and easier to use!

So I tried Vue. And then Angular. Then Aurelia. I was on the search for the perfect one! Nothing really fit my needs just right! Until one day, someone said to me…

Have you heard of Svelte? It’s like react, but faster, lighter, and with less boilerplate!

So I tried Svelte. Yes, it was faster, more lightweight, and much less boilerplate!

And it was fun. We built so many… Alright you get the point. The constant switching, the craving for perfection was exactly what was stopping me from building new things and learning.

Once I realized that, I started living a more simple lifestyle. I let go of the bloat.js and its bloated environment as a whole. No more prettier. JSLint. NPM. Webpack. SASS. React. Redux. JSX. Jest. Service workers. All that to build a simple website.

My life felt better again. As if each unneeded devDependency was like eating one too many burgers at McDonalds. Feels great to think about it. Almost as good to eat it. But it only goes downhill from there. The more you eat the worse you’ll feel later.

So, what do I eat now? Well, there is still some bloat here and there, but I try my best to reduce it.

  • react -> hugo / html templating
  • json web tokens -> sessions
  • material-ui -> tailwindcss or none

It’s more basic, but that’s a good thing, it doesn’t get in your way. Now, whenever I want to add a new library or feature, I think about whether I really need it. Often, the answer is no. Sometimes, I go through all the existing features, dependencies, libraries. I reconsider what is really needed, and notice I am just as happy without it.

My websites are simpler now, faster to make, run better on old hardware, and I think despite having less functionality they create a better experience.

Hold up… isn’t tailwind an npm module? 🤡

Yes. Exactly. Once a while, you will encounter some good, useful javascript libraries. At the moment, in my opinion, tailwind is one of them.

How does one use tailwind without npm, you might ask.

  • maybe I’m clickbaiting you, and you’re about to find out I use yarn
  • or I suggest you just… compile it? And store the binary only?
  • or, I could tell you to run it in docker

None of the above are good for me. Only if there was a way to run nodejs applications without installing npm. Only if there was a way to create a temporary shell that contained tailwind and its build dependencies… A more seemless integration than containerized environments like docker, and where we can combine multiple binaries from many languages without the need to build a new image and spin up a new linux container.

There is. It’s called Nix. Nix allows you to spin up a shell that contains a desired development environment in seconds. No building or pulling bloated docker images. No more writing or reading “Installation” sections for multiple linux distributions.

You could the required developer environment to build this blog simply by typing:

nix-shell -p hugo tailwindcss

As soon as you close that session, the environment will be reverted back to your normal one. In practice, you would simply make a shell.nix file such as the one that powers this website and activate it via nix-shell.

Think of Nix as a declarative package manager for everything. Even an operating system - I’m currently writing this article on NixOS. Everything about my setup, from the graphical drivers, window manager, vim config, email client, everything is declared and reproducible. If I copied my configuration.nix to another computer, I could set it up exacly the way this one works.

Thats so cool! I love nix!

But what about…

What about prettier for code formatting? Or a build process? I choose to avoid these things specifically, but if you really need them, you can search for available Nix packages.

What if… a package is not available on Nix?

Then you can package it yourself. Chances are, by the time you want to package something, you will already be familiar with it.

If you don’ want to package it, you can still download binaries and add them to path, just know that this change will not be reflected on other systems.

Simpler alternatives

Removing npm from my workflow made me rethink my previous build process. Here are a few alternatives I now use that are simpler and more universally transferrable to projects using other languages.

Run multiple parallel processes

I generally relied on multiple scripts in package.json that often needed to be executed in parallel. For example run-parallel watch-css watch-html.

In this case, the same can be achieved via a Makefile like so:

// run via `make watch-css watch-html`
MAKEFLAGS += -j2
watch-css:
	tailwindcss -i ./static/style.css -o ./static/style.min.css --watch

watch-html:
	hugo server -D

Watch for file changes

I used nodemon for scripts that needed to be executed on change. It became a useful tool even outside the JavaScript environment: for example recompiling a Go server on change.

Replacement? inotify:

inotifywait . --recursive --monitor --event MODIFY --event CREATE