Opinionated npm Style Guide for teams by De Voorhoede.
This guide provides a set of rules to better manage, test and build your npm modules and project scripts. It should make them
- easier for a new developer to pick up
- reduce friction with different environment configurations
- have a predictable api
- easier to add new scripts
- Use nvm to manage node versions
- Configure your npm personal info
- Use
save exact
option - Specify engines on
package.json
- Avoid installing modules globally
- Use standard script names
- Write atomic scripts
- Use npm modules for system tasks
- Avoid shorthand command flags
- Group related scripts by prefix
- Document your script API
With nvm you can have multiple different versions available and switch to the one that suits better your project.
To install or update nvm, you can use the install script using cURL:
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.0/install.sh | bash
For Windows check nvm for windows.
If everything goes well, you can now install a specific node version.
nvm install stable
nvm install vX.Y.Z
nvm alias default stable
It’s also easy when updating a newer version, copying your existing global modules.
nvm copy-packages <previous-version>
When creating a new package npm init
your defaults will be already included on the scaffolding.
npm config set init-author-name "{name}"
npm config set init-author-email "{email}"
Check npm config docs, for more info.
You can use cat ~/.npmrc
to check your current definitions.
By default, installing a package with the --save
or --save-dev
option, npm saves the package version with ^
prefix, meaning that will update minor versions if available. While this is a good idea as is, this makes it possible for different developers having different versions of the same package and making it harder to debug if there is inconsistency. Defining the save-exact
option prevents this. More info npm config docs.
npm config set save-exact
Specifying engine versions for your module, warns the user if he is not using a supported version. This is specially important for ensuring npm@3 flat tree dependency on Windows, or ES2015 features that your scripts require on node.
In package.json
:
"engines" : {
"node" : "5.10.0",
"npm" : "3.8.5"
}
Preventing the user from using your module is also possible with check-pkg-engines.
NPM first tries globally installed modules before looking for local ones. Globally installed modules are shared between projects and might not match the required version for the project.
- Locally installed modules are custom and specific for the project.
- Locally installed modules are directly accessible via npm scripts.
# recommended: install locally
npm install --save-dev grunt-cli grunt
and use in package.json
:
"scripts": {
"icons": "grunt grunticon"
}
# avoid: don't install modules globally
npm install -g grunt-cli grunt
(@javajosh: Okay, I don't really agree with this one. I think it's perfectly okay to think in terms of "strings that you emit into the surrounding operating system", in which case it makes more sense to think about all the platforms you want to support, and provide the equivalent string for all of them. This also keeps things simpler, and is much easier to debug. When the inevitable variations come in, thanks to other elements of users environments being different, it is much easier to confront that family of problems as a tree of related strings, as opposed to...whatever it is when you do it this way. (Partly this is also to preserve my own bias that "module" should be bigger than just "mkdir"! There is more than a little similarity to the ORM vs "SQL String" spectrum of solutions to database interaction. Personally, I really appreciate Strings. It is okay if they are somewhat coupled to the program that consumes them! That is inevitable. In fact, I would even argue that it's a good thing because you can characterize a module as triggered when a string gets too long...)
macos (bsd) (tcp/ip, dns, net, clock, disk, usb, bt, cam, mic, key, touch, mouse, screen, proc)
safari chrome
terminal iterm (oh-my-zsh, autojump, ag, nvalt)
homebrew
erlang
java/kotlin/clojure/groovy
maven/gradle
nvm/node/npm/yarn
virtualbox + alpine,nixos,freebsd,win10
postgresql
bootsrap, react/redux/angular/vue, Elixer
Dropwizard (Jetty), Spring,
c/rust/go/clang/llvm/gcc/make
python3
redis
sshd
nginx
sublime
intellij
vim
Each of these, in turn, has many versions and are highly configurable. Wouldn't it be nice if we could just have all the versions and easily change configuration when we need to? The remarkable fact is that we can, but it requires the adoption of some habits and tools. Users cannot trust applications to do the right thing! In fact, quite the opposite. And its time for those that profit from this quiet exfiltration of data to explain themselves (says the naive idealist). But, the more seasoned practical person sees an opportunity: there is an asymmetry here, and a substantial one. That means that there is a big product.
What if applications were routinely written in such a way that allowed multiple versions to be stored? Maven already does this, by just caching, locally, all versions of all libraries you or your dependencies ever required, using the file-system as a simple but effective database. It's also rather wasteful (how wasteful depends on the product, but this is a good reason to keep your bundles as small as possible). With open source libraries, especially using git, you can do better - try to build everything from scratch. If you can make it work, now you can move the working directory around at will. If the project supplies a Dockerfile for a working development environment, by all means use it! by a) depending on your locally checked out and built version, and b) getting the library to build. Now you can have any version you want (that you have in the git repo) by just changing the contents of the working directory with git commands.
When you use system specific commands like rm -rf
or &&
, you are locking your tasks to your current operating system. If you want to make your scripts work everywhere think about Windows developers also.
Use npm modules with node that mimic the same tasks but are system agnostic. Some examples:
- create directory (
mkdir
/mkdir -p
) ->mkdirp
- remove files and directories (
rm ...
) ->rimraf
- copy files (
cp ...
) ->ncp
- run multiple scripts in sequence (
... && ...
) or in parallel (... & ...
) ->npm-run-all
- set environment variable (
ENV_VAR = ...
) ->cross-env
npm and npm modules with a command-line interface support different options using fully written out and / or shorthand flags. For instance, instead of npm install --save-dev
you can use the shorter npm i -D
. For npm test
you can use simply npm t
. But npm start
is not the same as npm s
, as that's an alias for npm search
. So while you can use these shorthands in your daily routine, you should avoid them in scripts and documentation shared with other developers.
- Shorthand flags can only be understood by developers who know the modules and options well.
- Fully written out command options help in writing self documented scripts.
- Fully written out command options make scripts more accessible to other developers.
Always prefer fully written command flags over shorthand. Example using uglifyjs:
# recommended
uglify index.js --compress --mangle --reserved '$' --output index.min.js
# avoid
uglifyjs index.js -c -m -r '$' -o index.min.js
npm lets you define custom scripts. You can give these scripts any name you like, but you should stick to standard names when you can.
Using standard script names creates a predictable script API, which makes your project easier to use by other developers. When used consistently between projects a user doesn't even need to read the documentation or package.json
to know which scripts are available.
npm start
and npm test
are predefined aliases for custom scripts. In addition use of build
, deploy
and watch
are widely spread within the developer community. You should use these script names as follows:
npm run build
to create a distribution of your project (mostly for production).npm run deploy
to put your project on a host environment.npm start
(alias fornpm run start
) to start a web server (defaults tonode server.js
).npm test
(alias fornpm run test
) to run project's entire test suite.npm run watch
to run other scripts on files changes.
In package.json
:
/* recommended: standard script names */
{
"scripts": {
"build": "...",
"deploy": "...",
"start": "...",
"test": "...",
"watch": "..."
}
}
/* avoid: */
{
"scripts": {
"bundle": "...",
"upload": "...",
"serve": "...",
"check": "...",
"watcher": "..."
}
}
Each script should be only responsible for one action.
- Atomic scripts are easy to read and understand.
- Atomic scripts are easy to reuse.
Separate each step of the script to an individual script. For example a "generate icon" script can be split into atomic script like "clean directory", "optimize SVGs", "generate PNGs" and "generate data-uris for SVGs".
Bundle your scripts with a prefix so you can execute them all at once.
- Bundling helps keeping your scripts organized.
- Tasks grouped by prefix can be easily executed with one command.
- Your high-level script API remains unchanged when scripts are added, removed or renamed.
In package.json
:
/* recommended: group related scripts by prefix */
scripts: {
"test": "npm run test:eslint && npm run test:unit && npm run test:e2e",
"test:eslint": "eslint src/**/*.js",
"test:unit": "tape --require dist/index.js src/**/*.test.js",
"test:e2e": "karma start test/config.js"
}
/* avoid */
scripts: {
"eslint": "eslint src/**/*.js",
"tape": "tape --require dist/index.js src/**/*.test.js",
"karma": "karma start test/config.js",
}
Bundled scripts can be executed (in parallel or in sequence) using npm-run-all:
/* recommended: use `npm-run-all` to run all bundled scripts */
scripts: {
"test": "npm-run-all test:*",
"test:eslint": "eslint src/**/*.js",
"test:unit": "tape --require dist/index.js src/**/*.test.js",
"test:e2e": "karma start test/config.js"
}
- Documentation provides developers with a high level overview to the script, without the need to go through all its code. This makes a module more accessible and easier to use.
- Documentation formalises the API.
Document your script API in the project's README.md or CONTRIBUTING.md as those are the first places contributors will look. Describe what each task does using a simple table:
`npm run ...` | Description
---|---
task | What it does as a plain human readable description.
An example:
npm run ... |
Description |
---|---|
build |
Compile, bundle and minify all CSS and JS files.. |
build:css |
Compile, autoprefix and minify all CSS files to dist/index.css . |
build:js |
Compile, bundle and minify all JS files to dist/index.js . |
start |
Starts a server on http://localhost:3000 . |
test |
Run all unit and end-to-end tests. |
De Voorhoede waives all rights to this work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law.
You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.