In this article, I want to elaborate on things that belong to the dependencies
section in package.json and the ones that should be in devDependencies
.
What do you mean by "incorrectly"?
When I ask a person why they installed Webpack with --save-dev
, the most common response is "--save
is only for what makes it into runtime while --save-dev
is for development and build". However, they can't prove their point with anything other than the esthetic and logical distribution of packages. So, no practical advantages. The worst part about it is that major vendors also suggest installing their packages with that option. For example, different Webpack loaders:
What's the "correct" way?
Apart from dependencies
and devDependencies
in package.json, there's also an interesting --production
option, made specifically for these sections.
For npm install
:
npm install --production
For npm ci
:
npm ci --production
Both of these commands install dependencies in the project but only from the dependencies
section. Come to think of it, is there any sense in that if we can't do anything with packages from the dependencies
section without the ones from devDependencies
? Indeed, if we think about it in this way, there's no point in that flag.
In reality, there is, but under the condition of right use of --save
and --save-dev
. If a package is necessary for the production build, install it with --save
. Otherwise use --save-dev
. For example, we can't build for production without Webpack and Webpack CSS Loader, therefore they must be installed with --save
. At the same time, Webpack Dev Server, Jest, or ESLint are not mandatory for the build, hence --save-dev
.
The profit is, npm install --production
and npm ci --production
will not download spare dependencies, which aren't going to be used in the pipeline, during the building stage in CI . The benefits are obvious: it simply saves you build time, especially if you've got some bloated libraries in dev-dependencies that download (or even compile) binary files via postinstall
.
Important to realise!
💡 The common misconception here is that you may think dependencies
is for the project to work while in reality it's for the project to start working. No matter what makes it into runtime: package.json is far from being about that.
Some examples
Separating dependencies into regular and development dependencies is a common practice among package management tools for many languages that's significantly neglected in the JavaScript dev community.
For instance, a doc article at npmjs.org explains the meaning of this field in a very straight-forward way:
"devDependencies"
: Packages that are only needed for local development and testing.
If we take a look at a neighboring tool, Composer (package manager for PHP inspired by npm) also recommends to put packages for testing and alike into require-dev
:
Lists packages required for developing this package, or running tests, etc. The dev requirements of the root package are installed by default. Both
install
orupdate
support the--no-dev
option that prevents dev dependencies from being installed.
So does Rust's Cargo with its [dev-dependencies]
:
Sometimes there is a need to have dependencies for tests (examples, benchmarks) only. Such dependencies are added to
Cargo.toml
in the[dev-dependencies]
section.
Are there any reasons to do it differently?
Why yes, you could find reasons to do things in a different way. This article's purpose is exactly for you to think about where you list your dependencies. Based on feedback, I noted two cases when it's possible to ignore these rules:
- You are working on a project that is going to be installed as a dependency, e.g. a library. In this case, you should keep in mind how everything works and refrain from filling
dependencies
with things that are not actually needed for the project to work. For example, you could have developed a React component that uses Lodash under the hood and publish it in the npm registry - in that case, React itself could be inpeerDependencies
with Lodash indevDependencies
. Otherwise the user would download both React and Lodash along with your component and you know how that would end up. On another hand, libraries that were written specifically for Node.js may not have been compiled in advance and are published as-is in most cases, therefore if your library depends on something like fs-extra, it's safe to put that independencies
, nothing bad will happen. - The second special case is projects where frontend, backend, and SSR are all one. There, you have to build the bundle and leave node_modules for Node.js to work. Everything is not so trivial in this case and, as I think, it's up to personal preferences: no silver bullet here.
In case you are developing frontend with no backend, just backend, or CLI that will then be wrapped in pkg, then this article's principles will work perfectly.
The content of this text is food for thought on how to manage dependencies in npm. If you don't trust the author or straight-up don't understand what they're trying to say, try thinking about npm's --production
flag and package.json's devDependencies
on your own to come up with a conclusion.
ℹ️ The author rushed the article with a bold headline and forgot to cover many edgecases which cost them a bunch of comments. The text was extended several times in response to that. Thank you!