npm and package.json

In the past, there was no package manager available for the JavaScript community. Instead, individuals and organizations would typically release code themselves, enabling developers to manually download the latest releases. With the introduction of Node.js and npm, this all changed. Finally, there was a central repository of packages available to pull into our projects with ease. This wasn't only useful for server-side Node.js projects but also entirely frontend projects as well. The emergence of npm is likely one of the most significant events that precipitated the maturation of the JavaScript ecosystem.

Nowadays, every project that heavily involves JavaScript will set out its manifest in a top-level package.json file, usually specifying, at the very least, a name, a description, a version, and a list of versioned dependencies:

{
"name": "the-fruit-lister",
"description": "An application that lists types of fruits",
"version": "1.0",
"dependencies": {
"express": "4.17.1"
},
"main": "app/init.js"
}

There are a variety of available fields you can use in package.json so it's worth exploring the npm documentation to understand all of them. Here's a rundown of the most common ones:

  • name: The name of the package is perhaps the most important thing. If you plan to publish the package to npm, then this name will need to be unique.
  • description: This is a brief description of your module, to help developers understand its purpose. More detailed information is typically placed in a README or README.md file.
  • version: This is a Semantic Versioning (SemVer) compatible version (of the form, [Major].[Minor].[Patch]for example, 5.11.23).
  • dependencies: This is an object that maps every dependency package name to a version range. The version range is a string that has one or more space-separated descriptors. Dependencies can also be specified as a tarball/Git URL.
  • devDependencies: This is identical in function to dependencies except for the fact that it is intended only for dependencies that are required during development, such as code quality analyzers and testing libraries.
  • main: This can refer to the module ID that is the primary entry point to your program. For example, if your package were named super-utils, and someone installed it and then did require("super-utils"), then your main module's export object would be returned.
npm assumes that your package and any packages you rely on follow the rules of SemVer, which uses a pattern of [Major].[Minor].[Patch] (for example, 1.0.2). SemVer prescribes that any breaking changes must result in the major portion incrementing, whereas backward-compatible feature additions should result in only the minor portion incrementing, and backward-compatible bug fixes should result in the patch portion incrementing. Full details can be found at https://semver.org/.

Running npm install in the directory where package.json resides will cause npm to download the versions of dependencies that you have specified. When declaring dependencies, by default, npm will do so with a caret (^) attached, meaning that npm will pick the latest available version that complies with the major version specified. So, if you specify ^1.2.3, then anything up to 1.99.99 (and so on) may validly be installed. 

There are several fuzzy version ranges that you can use:

  • version: Must match version exactly
  • >version: Must be greater than version
  • >=version: Must be greater than or equal to version
  • <version: Must be less than version
  • <=version: Must be less than or equal to version
  • ~version: Approximately equivalent to version (increment patch portion only)
  • ^version: Compatible with version (increment minor/patch portions only)
  • 1.2.x: 1.2.0, 1.2.1, and so on, but not 1.3.0 (x means anything here)

Arguably, the greatest issue with npm is that the unchecked introduction of new packages and their granularity in terms of functionality has led to projects with incredibly large and unwieldy dependency graphs. It's not unheard of for there to be individual packages that only export a singular narrow utility function. For example, in addition to a generic string utility package, you may also find a specific string function as a package of its own, such as uppercase. These packages are not inherently problematic—many of them serve useful purposes—but having an unwieldy dependency graph can lead to problems of its own. Any popular package that either is compromised or has not followed SemVer religiously can result in a propagation of issues across the JavaScript ecosystem, eventually affecting your project.

To help to prevent bugs and security issues, it is highly recommended to specify your dependencies with fixed versions and update dependencies manually only when you have checked their respective changelogs. Nowadays, some tools can help you to keep dependencies up to date without sacrificing security (for example, dependabot, owned by GitHub).

It's recommended to use a dependency management system that ensures the integrity of downloaded packages with cryptographic hashes (a checksum that would highlight malicious changes), to ensure that the package you end up executing is definitely the one you intended to install and has not been compromised or damaged during transmission. Yarn is an example of such a system (see https://yarnpkg.com). It is effectively a more secure and efficient abstraction atop npm. In addition to being more secure, Yarn has the added benefit of avoiding inconsistent package resolution, which is when two installs of a given code base's dependencies will result in a potentially different set of downloaded dependencies (due to the potentially fuzzy nature of npm's version declarations). Such inconsistencies can result in the same code base behaving differently across two instances (a massive headache and harbinger of bugs!). Yarn stores the current locked dependency graph and corresponding versions and checksums in a yarn.lock file, which would look like this:

# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

[email protected]:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=

...

Here, we see just one dependency but there'd usually be hundreds if not thousands as it would have to include not only your direct dependencies but also dependencies of those dependencies.

Dependency management is a topic that has had much written about it, so if you look online, there is no shortage of opinions and solutions. Fundamentally, as our concern is clean code, we should go back to our principles. Foremost, what we should seek in both our dependency systems and the dependencies themselves is reliability, efficiency, maintainability, and usability. In the context of dependencies, when it comes to maintainability, we are interested in both our ability to maintain code that consumes and depends upon the dependency and the ability for the dependency's maintainers to keep the dependency up to date and bug-free.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.131.72