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 of16.8.x
, but not16.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 of16.x.y
, but not17.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.
Isn't there more?
Yes, a lot more actually. But this is a solid start, so we'll stop here for now.
Top comments (10)
Wow, This was Really Helpful 🙏
Glad to hear it!
Nice article, but I think one of the most important points was not mentioned. The difference between devDependencies and dependencies when running
npm install
.When you run
npm install
all dependencies are installed, both those in devDependencies and in dependencies, but when you runnpm install --production
only those in dependencies are installed. I have seen many devs always runningnpm install
even in production.THANK YOU!!!
Any time!
Thanks, helped me with my package!
Really helpful.
The bin key is cool. Thank you for sharing
Nice and clear
Thanks for that! Very clear and concise!