DEV Community

101arrowz
101arrowz

Posted on

Creating a modern JS library: package.json and dependencies

Your package.json is among the most important files in your project. It handles dependencies, exports, versioning, naming, etc. package.json effectively includes all the metadata a user would need to use your library. Therefore, it's critical that you create package.json properly; if you don't, about half of your bug reports will be issues involving imports, mismatched dependencies, etc.

Let's go through the fields that a typical package.json will contain. We'll be creating an example package for re-encoding UTF-8 data or strings into the fictitious "Catlang" format.

name (required)

The name of your library. Even if you have a preferred style, the convention is to use all-lowercase letters and dashes to separate words.

If you're creating a fork of an existing package, don't add a number to the end: either describe what you changed or, if that's too difficult, express the same idea with different words.

Poor choice of name:

{
  "name": "EncodeInCatlang2",
}
Enter fullscreen mode Exit fullscreen mode

Good choice of name:

{
  "name": "encode-in-catlang"
}
Enter fullscreen mode Exit fullscreen mode

If the above was already taken:

{
  "name": "catlang-encoder"
}
Enter fullscreen mode Exit fullscreen mode

version (required)

The current version of the package. You will change this every time you want to publish a new version of your package. Try to follow semantic versioning (more details on what this is later). My suggestions are as follows:

  • When you first create a package, use 0.0.1.
  • Whenever you fix a bug, you want a "patch" revision. Increment the last digit.
    • 1.0.11.0.2
    • 3.4.93.4.10
  • Whenever you add a new feature, soft-deprecate (i.e. discourage) an existing feature, or do anything else that doesn't break code that previously worked fine, you want a "minor" revision. Increment the second-to-last digit and set the last digit to zero.
    • 1.0.11.1.0
    • 3.9.03.10.0
    • 0.3.50.3.6*
  • Whenever you hard-deprecate (i.e. remove) an existing feature, change the behavior of something, or otherwise do anything that will break code that worked fine on a prior version, you must use a "major" revision. Increment the first digit and set the other two to zero.
    • 1.1.32.0.0
    • 10.1.311.0.0
    • 0.3.50.4.0*
    • 0.0.30.0.4*

Note the examples with an asterisk. For versions below 1.0.0, a patch revision is not possible and the other two types shift down; incrementing the second-to-last digit is major and the last digit is minor. For versions below 0.1.0, this is even more severe, since every version bump is a new major version.

This is why updating from 0.0.X to 0.1.0 and from 0.X.X to 1.0.0 are what I like to call "maturity" revisions; they completely change the meaning of each digit. As good practice, try to reduce the number of major versions you make after 1.0.0, but use as many minor and patch versions as you'd like.

As a notation guide, semantic versions are usually prefixed with a "v" outside of package.json. When referring to version 1.2.3 in a GitHub issue, for example, say "v1.2.3".

You also may want to note that alpha, beta, and release candidate versions are supported by semantic versioning. For example, 1.0.0-alpha.1, 2.2.0-beta.4, 0.3.0-rc.0. Typically, the patch version is unset for these prerelease versions, and they aren't downloaded by package managers unless the user requests a prerelease version.

Last thing: NPM considers packages under v1.0.0 to be unstable and ranks them lower in the search. If you want a quick boost to search score, make your library stable!

Let's update our package.json:

{
  "name": "catlang-encoder",
  "version": "0.0.1"
}
Enter fullscreen mode Exit fullscreen mode

description (strongly recommended)

A brief description of what your package does. Even if the name is self-explanatory, it doesn't hurt to repeat yourself. The description is used for search results in NPM, so make sure to highlight the library's most major features.

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter"
}
Enter fullscreen mode Exit fullscreen mode

author (strongly recommended)

The name (and optionally email and website) of the author of the package. Optimally, you will use your full name here, but if you're not comfortable doing so, your online alias will suffice. The field can take one of two formats:

"Your Name <youremail@yourdomain.com> (https://yoursite.com)"
Enter fullscreen mode Exit fullscreen mode

or

{
  "name": "Your Name",
  "email": "youremail@yourdomain.com",
  "url": "https://yoursite.com"
}
Enter fullscreen mode Exit fullscreen mode

The email and website are optional, but you must add your name or alias.

New package.json:

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>"
}
Enter fullscreen mode Exit fullscreen mode

license (strongly recommended)

The license under which your library's code may be used. We'll get into licensing more in another article, but for now you should at least know the syntax.

If you're using a common license (such as MIT, ICS, BSD, GPL, Apache, etc.), you can use its identifier, which you can find in this list. Try to pick a license from that list, but if you can't, mention the file containing your license instead:

"SEE LICENSE IN LICENSE.md"
Enter fullscreen mode Exit fullscreen mode

If you'd like to distribute your library under multiple licenses, you can use OR and AND expressions with parentheses. If you'd like to specify an exception within some license, use the WITH keyword.

"(Apache-2.0 OR MIT)"
"(GPL-3.0-only WITH Bison-exception-2.2)"
Enter fullscreen mode Exit fullscreen mode

If you don't know anything about licensing and just want to freely distribute your code, "MIT" is a safe option.

New package.json:

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

keywords (recommended)

The keywords to associate with your library in the NPM search. These are a way of getting your package in searches that don't include any words in the name or description fields. The point of the keywords field is to prevent keyword spamming in the description or title, but you still shouldn't use unrelated terms within the keywords.

Adding keywords to package.json:

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

homepage (recommended)

The website for your project. This can be a documentation site, example page, etc. If you have a webpage that includes information about your library, even a blog post, use it here. Avoid using the link to your source code (i.e. your GitHub repository) unless you have absolutely no other site to link to.

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

repository (recommended)

Information about the repository. Assuming you're hosting your source code on a version control system (if you're not, you definitely should), use an object with type and url keys:

{
  "type": "git",
  "url": "https://domain.com/your-name/your-library.git"
}
Enter fullscreen mode Exit fullscreen mode

There are some shorthands, such as using just the URL and letting NPM guess what type the repository is, but I advise against doing this for the sake of clarity.

If your library is a part of a monorepo, you can specify the directory subfield to denote the subdirectory within which the package is contained. If you aren't using a monorepo or don't know what a monorepo is, don't use directory.

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "author": "Cat <cat@gmail.com>",
  "description": "Fast Unicode to Catlang converter",
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

bugs (recommended)

Where users should report issues with the library. GitHub has a built-in issue tracker, so often you'll be using the /issues subdomain of your GitHub repository for this. You can specify just a string if you'd like just this URL:

"https://github.com/your-name/your-library/issues"
Enter fullscreen mode Exit fullscreen mode

However, if you'd also like to add an email that users can report bugs to, you can use the object form:

{
  "email": "youremail@yourdomain.com",
  "url": "https://github.com/your-name/your-library/issues"
}
Enter fullscreen mode Exit fullscreen mode

Updated package.json:

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

engines (optional)

The environments in which your library will work. This is only applicable for libraries that support Node.js (e.g. a CSS library shouldn't use this field). If your library does not use modern features of JavaScript (such as async iterators), you also don't need to specify this field. The format is as follows:

{
  "node": ">=0.10.3 <15"
}
Enter fullscreen mode Exit fullscreen mode

For now, node is the only key of the engines field that you'll need to use.

Suppose catlang-encoder needs support for ES6 features such as Promise + async/await, for..of, etc. Since async/await was only added in v7.6.0, we use that as the minimum version.

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "engines": {
    "node": ">=7.6.0"
  },
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

contributors (optional)

People other than the author who have contributed in a major way to the project. The format is an array of objects or strings in the same format as the author field. For example:

[
  "John Doe <me@johndoe.net> (johndoe.net)",
  {
    "name": "Place Holder",
    "email": "placeholder@gmail.com"
  }
]
Enter fullscreen mode Exit fullscreen mode

If you worked on this project mostly alone (perhaps with a few pull requests here and there), you don't need to specify this field. However, if someone has helped you many times, it's a good idea to add them.

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "contributors": [
    "Cat 2"
  ],
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "engines": {
    "node": ">=7.6.0"
  },
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

bin (optional)

The location of the package's binary. If you're developing a CLI tool, set this to the entry point of your codebase. The file you set will be executed whenever someone runs npm run your-package or yarn run your-package. Of course, you don't need this field if you don't want to provide a CLI tool with your package.

For a single executable, the field can just be a string:

"path/to/bin.js"
Enter fullscreen mode Exit fullscreen mode

If you have more than one executable, you can specify a mapping as so:

{
  "command-1": "./bin1.js",
  "command-2": "./bin2.js"
}
Enter fullscreen mode Exit fullscreen mode

If we have a CLI executable for quick-and-dirty Catlang encoding from the command line at lib/cli.js:

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "bin": "lib/cli.js",
  "contributors": [
    "Cat 2"
  ],
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "engines": {
    "node": ">=7.6.0"
  },
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

private

Prevents your package from being published if set to true. Obviously, you shouldn't have this field in your package.json but some starter projects/templates include "private": true in package.json to prevent you from accidentally publishing code that isn't meant to be a package. You'll want to remove the private field if it exists; otherwise, NPM will refuse to publish your package.

There are a few rarer fields that you may occasionally need such as os and man, in which case you should take a look at the original documentation for package.json. In addition, we'll be making use of the scripts field later on, and if you aren't familiar with it, you should read this.

Dependencies and exports

You may have noticed that our package.json for catlang-encoder has no dependencies and has no exports. We'll get into how you should handle exports in the next article, since it's quite a complicated topic, but right now we'll discuss dependencies in package.json.

As a rule of thumb, you should try to minimize the number of dependencies you use. If a dependency has under 20 lines of source code, the logic is probably simple enough that you can rewrite it on your own because that will make it easier to maintain your codebase.

If you do end up needing dependencies, there are four fields you can use to specify them: dependencies, peerDependencies, optionalDependencies, and devDependencies.

dependencies

The mapping of package name to versions supported for your library's runtime dependencies. If you use code from another library at runtime (i.e. when someone uses your package), add that library to this field. The syntax is as follows:

{
  "some-package": "^2.3.1",
  "other-package": ">=7.0.0",
  "last-package": ">=2 <3"
}
Enter fullscreen mode Exit fullscreen mode

The keys of the object are the names of the dependencies, while the values are the versions to accept. The syntax for specifying versions is called semantic versioning, or "semver". The full details are detailed on the semantic versioning website, but generally you need to know only two things:

  • The actual version of a package is always three numbers separated by periods, as in the version field of package.json
  • Dependencies in package.json can use version identifiers, which refer to one or more versions of the package

When your users install your package, their package manager will see all the dependencies in package.json and download the relevant ones
There are many types of version identifiers, but the most relevant ones are these:

  • Exact identifiers, which are just the version numbers. They may exclude the patch and minor versions.
    • 1.0.1 matches only v1.0.1
    • 1.0.0-rc.0 matches only v1.0.0-rc.0 (this is the only way to get a prerelease version of a package)
    • 0.10 matches any version in the v0.10 range (at least v0.10.0, before v0.11.0)
    • 1 matches any version in the v1 range (at least v1.0.0, before v2.0.0)
  • Comparative identifiers, which match versions above or below a specific version
    • >1.1.3 matches versions more recent than v1.1.3 (v1.1.4, v2.0.4, etc. all work)
    • <=2.8.7 matches versions older than or equal to v2.8.7 (v2.8.7, v1.0.1, v0.0.1 all work)
  • Match-all identifiers, which use x or * to mark a part of the semver string that can be any version
    • 1.x matches any version in the v1 range (like 1 does)
    • * matches all versions of the package
    • 2.3.* matches any version in the v2.3 range (like 2.3)
    • Careful: 2.*.0 matches any version in the v2 range, not just patch-0 versions
  • Second-digit identifiers, which use tildes to match the second digit of the version provided that the third digit is greater than or equal to that specified in the identifier
    • ~1.2.3 matches all versions >=1.2.3 and <1.3.0
    • ~0.4.0 matches all versions >=0.4.0 and <0.5.0
  • Major version matchers, which use ^ to match the first nonzero digit
    • Technically, the first digit, zero or nonzero, is always the major version. However, when the first digit is zero, a bump to the second digit is a significant change, and ^ prevents your library from accidentally accepting that significant, possibly breaking change.
    • This is the most popular matcher
    • ^3.2.1 matches any version in the v3 range
    • ^0.4.0 matches any version in the v0.4 range
    • ^0.0.5 matches just v0.0.5

Last thing: you can combine version identifiers using a space between two of them. The new identifier matches if both of the subidentifiers match. In other words:

  • >=1.5 <3 matches versions that are at least v1.5 but below v3 (i.e. at most v2)
  • 1.x <=1.8 matches versions that start with v1 but are at most v1.8

If you're not sure that your semver string is correct, you can always try this tool to test that it matches the versions of your dependency in the way you want it to.

Let's say we need catlang-dictionary to tell us which words have direct translations to shorter glyphs in Catlang, and we have found that version 1.2.3 works well. Assuming that catlang-dictionary follows semantic versioning, it's a good idea to use ^1.2.3 as the version identifier.

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "bin": "lib/cli.js",
  "contributors": [
    "Cat 2"
  ],
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "engines": {
    "node": ">=7.6.0"
  },
  "dependencies": {
    "catlang-dictionary": "^1.2.3"
  },
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

peerDependencies

The dependencies for which your package was installed as an add-on, extension, or integration. The difference between dependencies and peerDependencies is that peerDependencies are not automatically installed and are typically used to denote what your library integrates with or extends.

It's hard to define exactly when you should use a peer dependency over a dependency, but if the user installed your library only because they were directly using a certain package, add that package to the peerDependencies.

For instance, a React component library would have "react" in peerDependencies, and a Babel plugin would have "@babel/core" in peerDependencies. On the other hand, a JavaScript wrapper for a REST API would probably leave node-fetch in dependencies rather than peerDependencies. node-fetch is not being used directly by the user and is not the reason the package was installed; it's simply important for the HTTP requests to go smoothly.

It's very important that you use a loose version identifier for peer dependencies. For example, if you use ~16.3 as the version of React in your React component library, when your user updates to React v16.8, they'll get warnings about incompatible versions even though your library probably still works in v16.8. You'd be better off using ^16.3, or if you think the next major version won't break your package, >=16.3.

Since catlang-encoder works universally, regardless of framework, we don't need any peer dependencies and won't need to modify our package.json.

optionalDependencies

Any dependencies you would like to have but can do without. Effectively, there's no guarantee that these dependencies will be installed: they are usually installed if the package is compatible with the operating system and if the user agrees to installing that dependency. The primary use case for this field is preventing your package from failing to install when one of your dependencies is incompatible with the processor architecture, operating system, etc.

It's important to note that a dependency that can be installed for extra functionality is an optional peer dependency. If you can improve or add functionality to a section of your code if a dependency is installed, that's an optional peer dependency and not an optional dependency because you don't want the dependency installed by default.

For example, the @discordjs/opus extension for discord.js enables support for certain voice calling features in the Discord API. Since many users of the Discord API won't need voice support at all, using @discordjs/opus in optionalDependencies would install it by default, adding bloat. Therefore, it's an optional peer dependency, i.e. @discordjs/opus is in peerDependencies and it is specified as optional using the peerDependenciesMeta field:

{
  "@discordjs/opus": {
    "optional": true
  }
}
Enter fullscreen mode Exit fullscreen mode

(As a side note, the actual discord.js does not do this anymore; they've completely removed the dependency from package.json and just ask users in the README to install the optional dependencies if they want them.)

For catlang-encoder, we can optionally use the native utf-8-validate package to verify that the inputs to the encoder are valid, but it's not necessary because the validation isn't strictly necessary. Since generally, most users don't need it, we make it an optional peer dependency. (Remember to use a loose version matcher with peer dependencies! We'll use * to support any version of utf-8-validate.)

On the other hand, we want to use catlang-concat whenever possible to more efficiently concatenate Catlang buffers, but we can still do normal buffer concatenation without it, so we specify it as an optional dependency to effectively tell the package manager: "I really want catlang-concat if you can install it, but if not I'll still work without it."

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "bin": "lib/cli.js",
  "contributors": [
    "Cat 2"
  ],
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "engines": {
    "node": ">=7.6.0"
  },
  "dependencies": {
    "catlang-dictionary": "^1.2.3"
  },
  "peerDependencies": {
    "utf-8-validate": "*"
  },
  "peerDependenciesMeta": {
    "utf-8-validate": {
      "optional": true
    }
  },
  "optionalDependencies": {
    "catlang-concat": "^4.5.6"
  },
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

devDependencies

The list of dependencies that are not needed at runtime but are needed to develop the library. These packages are never installed when a user downloads your package; however, when you run npm/yarn/pnpm install, those packages are added. This is most often filled with packages needed to build the source code into runtime code, if any is ncessary. For instance, you'll often see babel for projects using JSX, or the typescript package for any library with source code in TypeScript.

Since we love stopping type errors before runtime, we have TypeScript source code. We'll need to add the typescript package to our devDependencies in order to use the tsc compiler, which will ultimately allow us to allow both TypeScript and JavaScript consumers to use our catlang-encoder.

{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "bin": "lib/cli.js",
  "contributors": [
    "Cat 2"
  ],
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "engines": {
    "node": ">=7.6.0"
  },
  "dependencies": {
    "catlang-dictionary": "^1.2.3"
  },
  "peerDependencies": {
    "utf-8-validate": "*"
  },
  "peerDependenciesMeta": {
    "utf-8-validate": {
      "optional": true
    }
  },
  "optionalDependencies": {
    "catlang-concat": "^4.5.6"
  },
  "devDependencies": {
    "typescript": "^4.2.2"
  },
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

With that, we've finished going through the vast majority of the fields of package.json. In the next article, we'll discuss how to add proper exports to package.json, which is critical if you want to develop a package that supports users whether they're using a CDN, a package manager, or a buildless web app with ESM.

Top comments (3)

Collapse
 
hello10000 profile image
a

this is really helpful as I want to start an open source project js library

Collapse
 
alphonthewww profile image
Alphonse Bouy

Very informative thanks! I see this is from April, is the next article about exports still planned?

Collapse
 
101arrowz profile image
101arrowz

I completely forgot that I had written these. I'll start working on the exports article and will publish when I have time.