DEV Community

Maciej Wakuła
Maciej Wakuła

Posted on

node --save-*

Content

This article roughly describes node package management concepts especially focusing on dependencies management.

Introduction

Node

The node is a single-threaded asynchronous server-side runtime environment for JavaScript (you can run JavaScript without a web browser) using "V8" JavaScript runtime written in C++ by Google.

Node packages

JavaScript is very (too much?) flexible resulting in millions of libraries (modules) being created and published (over 1.3 million at the moment this article was created).

Project often include many modules - which then include others. This creates a very complex web of dependencies that needs to be solved in order to run the software.

NPM - Node Package Manager

Node comes with its own solution to manage, publish, download, install and also share modules (packages). There are many solid alternatives but npm became a part of the node (while npm is an installable module itself).

Types of dependencies

Node currently defines following types of dependencies:

  • normal dependencies - meant to run on production environment
    • you install them using npm install --save {dependencyName}
    • keep them as least as possible
    • whenever someone installs your module - they would also install those dependencies
  • development dependencies - to be used during development only (but it is used also for testing)
    • whatever you need for your development and testing
    • assume they are not present on production environment
    • you install them using npm install --save-dev {packageName}
    • when installing on non-production environment those gets installed together with your module
  • peer dependencies - which are not installed for the module but must be provided by your application
    • you expect them to be provided, not installed by your module
    • often developers start with dependencies and devDependencies optimizing them later

Each module (package) has a version. NPM is using SemVer (Semantic Versioning). Version has 3 numbers and optionally extra part:
{major}.{minor}.{patch}, ex. 1.2.3 where

  • major should be increased whenever it can break existing code (increase it to indicate that update is not strightforward and might require code change)
  • minor is to indicate new features (with backward compatibility)
  • patch is to indicate non-functional changes (logic remains the same but for example bugfix is applied) Version can have an optional suffix: {major}.{minor}.{patch}-{suffix}
  • suffix is often either
    • {name}.{number} allowing you to have multiple "branches" of builds
    • {number} to just indicate build number

When installing a package dependency to your application it installs by default "latest". When updating it tries to use same major version.

Traps

Developers often release new breaking change with patch version or publish updated version with incorrect SemVer.

Tag pre-id

When releasing new version of package to npmjs.org you always publish it under a "tag". The main one is "latest". The only hint for alternative names is that "next" is often used for development/unstable releases. Often used tags are used:

  • to indicate a version line "npm" (ex. "latest" for current version, "next" for the unstable one in development, "latest-2" to indicate version 2.x which might still receive updates)
  • to indicate flavor (ex. "stable", "unstable" but also "latest")

By default most recent "latest" version is installed and later updated to newer "latest" with same major number.

Different versions in same project

Imagine using libA@1.0.1 and libB@1.0.0 but then libB depends on libA@1.0.0.
Assuming libA@1.0.1 is backward compatible - npm can install libA@1.0.1 and libB@1.0.0.

But let's change it a bit: use libA@2.0.0 and libB@1.0.0 (we know it requires libA@1.0.1). NPM would install both versions of libA.

Some packages (ex. react) would cause multiple problems when different versions are installed all together. Others might create a cross-dependency hell (libA depends on libB and libB depends on libA) what is an implementation bug (but works so often happens).

Checking your dependencies

  • npm outdated shows outdated dependencies: whether any newer compatible exists, and also any newer (non-compatible)
  • npm audit to see if your dependencies contain any known vulnerabilities
  • npm update is a risky operation updating your dependencies to "newer versions that **shouldBB be compatible"

My personal opinion

Huge risk of package marked as compatible (while it is not) discourage developers from updates increasing likeliness of outdated software and technical debt creation.
Having development dependencies leads to scenario where test build differs from target artifact (not reliable tests).
Together the approach is risky and dirty but you must deal with it when working with node. Node has a huge win of being single-threaded but asynchronous nicely fitting to contenerization approach on cloud. Kotlin (soon Java with loom) attempts to offer same functionality but still generally requires more powerful hardware. Node is still a good option for microservices and frontend.

This is just an introduction to the dependencies management hell in NPM.

Top comments (0)