Most modern services and applications have a mass of dependencies that live in an ever-growing node-modules
folder. Generally a lot of these libraries are being actively maintained, changed, and updated. If your dependencies are poorly managed, you can quickly find yourself in dependency hell.
If you're unfamiliar with npm, check it out here before reading on
🛒 Grocery Shopping
When starting up a node application, one of the first steps is running npm install
. When you run this, node will check for a file called package.json
in the base of your project. If that file is found, it will use the dependency
section as a kind of "grocery shopping list" to go and gather "ingredients" (code bits) your application requires.
The "grocery store" in this case is something npm calls a registry
. By default, your node app will look in the public npm registry for these packages, where most everything you need will be (private registries can be created for proprietary code and whatnot). If the package is found in the registry, node puts that "ingredient" into a node_modules
directory at the base of your project.
It's important to note that packages that you require may have their own "shopping list" (package.json), and all of those nested dependencies must be resolved before your app moves on to the next dependency in its own package.json.
⬆️ Versions, 🥕 Carets, and 🃏 Wildcards
The versions of your dependencies are generally something like v1.3.5
. This is called semantic versioning, or semver. With semver, the numbers represent changes to the code in varying severity - MAJOR.MINOR.PATCH
.
From their docs -
MAJOR version when you make incompatible API changes, MINOR version when you add functionality in a backwards compatible manner, and PATCH version when you make backwards compatible bug fixes.
With this in mind, a lot of people want to automatically update their app with any fresh new stuff their dependencies might have in newer, non-breaking changes.
Prefixing with tilde
~
will give you any newPATCH
updates, but not major or minor. So~1.3.1
could install1.3.9
, but not1.4.0
Prefixing with caret
^
will give you any newPATCH
andMINOR
versions, but not major. So^1.3.1
could install1.4.9
but not1.5.0
> >
Let's take a look at our example code's dependency tree:
my-breakfast
|
|
milk
|
|
coffee-script
Ok, more like a stick, but hopefully the chain of dependency is clear. Our package.json is requiring version v0.5.0
specifically of milk
, but milk is requiring coffee-script
anywhere from 0.9.6
- 1.0.0
. npm install
is run, we develop our app, everything is hunky-dory.
📼 Now let's fast-forward 2 months. Someone finds your project and wants to contribute. They fork and clone your repo, run npm install
, aaaaand it doesn't work. "But it worked on my machine!" you cry. When your collaborator installed the node modules, they were guaranteed a specific version of milk
, but they got a different version of coffee-script
because milk
's package.json used semver.
🗿 Setting your dependencies in stone
One solution to this is to use a package-lock.json
file. This file gives you very granular control over the versions of every dependency that you install. If your package.json
is like the shopping list, then your package-lock.json
is like a budget. You can have cereal, but it's gonna be store brand instead of Cap'n Crunch. This specificity runs all the way down every branch of your dependency tree. You must have a package.json
if you want to use a lock file (the package.json
does a lot more than just dependency management, that's just the focus of this post).
🎁 Wrapping up
I personally feel that a package-lock.json
file should always be used (in newer versions of npm, it is actually automatically generated). It just makes everything more reliable across environments and deployments. Here's some last little nuggets to hopefully help out when it comes to dependecies:
npm install --save
will automatically update your lockfile and package.json with that package.npm ci
instead of justnpm install
will automatically rebuild your node modules, and build from your lockfile. It's a really helpful command for CI/CD and generally best to use in tandem with a lockfile.- For larger projects, and super robust dependency, check out docker and containers. It can function almost like a virtual machine that perfectly contains your code and it's dependencies, and is cloned to promote to different environments. So hopefully you end up with a lot less "it worked on my machine" kind of issues.
Thanks for reading all! Let me know in the comments if I made any egregious errors or left something important out.
MTFBWY