If you work with JavaScript code, you come across a package.json file in every project. Every time you run npm install or yarn those package managers look through that file and grab the dependencies you need. However, these files are chockfull of valuable information and powerful features, let's dive in!

We'll work off of this example as a reference point.

{
"name": "example-package",
"description": "A package that does a thing",
"version": "1.0.0",
"author": "laurieontech",
"repository": {
"type": "git",
"url": "https://github.com/some-project-here"
},
"dependencies": {
"react": "16.8.6"
},
"devDependencies": {
"prettier": "^1.18.2"
},
"keywords": ["react"],
"license": "MIT",
"main": "index.js",
"scripts": {
"test": "jest"
},
"bin": "./bin/executable.js"
}

Metadata

The first few items in a package.json are descriptive. description, repository, and author (or contributors if there are multiple) are there to provide context about the project. If you publish the package on npm, that information is available on the package page. name and version do a bit more.

name is a kebab-case package name. This is the name you'll find it under in npm, this is the name you'll use to install the package, etc. If you're used to using packages you're likely familiar with syntax like this "react": "16.8.6". This is a name and a version number.

Most JavaScript projects follow semver as a way to intuitively increment the package version. Every time the package is published to npm, the version should increase. Whether the first, last, or middle number increments is based on the significance of the changes and their impact on everyone else.

Dependencies

Dependencies are a list of runtime packages that your project depends on. They are installed when you run npm install, or similar.

Let's talk about "react": "16.8.6" again. Each dependency is listed as a key-value pair using the name and version of the package. However, there are some extra characters you can add in front of the version.

  • ~: if you add a tilde, your package manager will install the version you listed or any newer patch version. E.g. ~16.8.6 means you will get the latest version of 16.8.x, but not 16.9.0.
  • ^: If you add a caret your package manager will install the version you listed or any newer patch or minor version, but not a major version. E.g. ^16.8.6 means you will get the latest version of 16.x.y, but not 17.0.0.

There are additional supported characters as well, allowing you to specify ranges. All of these are parsed using the semver package. This gets a bit confusing, so let me clarify. Semver is a set of guidelines for versioning your packages. Since npm follows it and uses those guidelines as a basis for its package manager, it named the semantic versioning package it uses accordingly.

devDependencies

Slightly different are devDependencies. These are dependencies that are required for developers working on the package, e.g. testing libraries. However, end users don't need them, so they're included separately. They're included when you run npm install inside example-package, but not when you npm install example-package inside another project.

peerDependencies

This is yet another type of dependencies. It's mostly there for package authors to prevent conflicts when they're using a package that other dependencies you have are also using. E.g. making sure the package is using the Babel version from your project and not a local one that might not be compatible.

keywords

Keywords are a helper for the npm search function.

license

Mandatory "I am not a lawyer" comment here. Licenses are a topic on which there are experts and I am not one of them. The license(s) listed are the terms under which you are permitted to use the project. You can read more on the various licenses.

main entry point

This is the file that is referenced when someone imports a package. Given "main": "index.js", const example = require("example-package") will grab the example export from the index.js.

scripts

This is where we get into the meat of the file. The scripts section includes more key-value pairs. The key is the name of the command and the value is the command line instructions that run when you call it.

Let's start with a straightforward example.

{
"test": "npm run jest"
}

This is more of an alias than anything. It allows us to run npm test in our command line and it will actually run npm run jest.

What about something a bit more complex?

{
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ."
}

This runs eslint against the entire project directory with some specific flags.

Nothing prevents you from running these scripts yourself. Giving you a shorter command with the correct configuration is just a better experience.

However, there are some scripts that are meant to build the project so that it can be published and installed in other projects as a package. There are special keys that execute scripts at specified times but we're not going to dive into that here.

Instead, we're going to look at a couple types of scripts you might see that bundle up a project and prepare it for installation.

Babel example

{
"build": "babel src --out-dir . --ignore \"**/__tests__\""
}

This first script is using babel. Using a configuration file in the root of the project, this takes all of the files in the src directory and compiles them into the root directory. It also includes a flag to ignore the files in src/__tests__.

Microbundle example

{
"build": "microbundle -i src/example.js"
}

This script uses microbundle to bundle up the project. In this case, we're specifying a src/example.js as the entry point for building.

Running scripts

Scripts are runnable. I mentioned above that npm test runs npm jest and it does. However, that's because test is an alias for npm run test. There are a few of these.

For any other custom scripts you specify, a user needs to to run npm run <script>.

bin

One more fun thing! In addition to the npm command, there is now an npx command. npx allows you to run commands without installing the package first. 🤯

Package authors enable this by using the bin section of the package.json file. It can be written as a key-value pair or using the below syntax.

{
"bin": "./bin/executable.js"
}

In this case, the ./bin and extension get stripped and a user can run npx executable. If you ever decide to write a package that implements this, note that the relative file path is based on the bundled version of the project. This makes sense since it's being executed directly from the published package.

Read my post on symlinks for even more about this field.

Isn't there more?

Yes, a lot more actually. But this is a solid start, so we'll stop here for now.