Using NPM scripts correctly
Nowadays, most frontend projects and JavaScript projects use NPM (Node Package Manager) to manage dependencies. This is often done through the file package.json
. This file, as the name extension suggests, contains a JSON object about the project. The follow snippet shows a very simple example of what a package.json
file might contain, for more information about what each section means browsenpm will offer a much better explanation. If you have not seen a package.json
file before, it might be best if you do some quick reading before continuing reading.
A little bit of background info on package.json
Nowadays, most frontend projects and JavaScript projects use NPM (Node Package Manager) to manage dependencies. This is often done through the file package.json
. This file, as the name extension suggests, contains a JSON object about the project. The follow snippet shows a very simple example of what a package.json
file might contain, for more information about what each section means browsenpm will offer a much better explanation. If you have not seen a package.json
file before, it might be best if you do some quick reading before continuing reading.
{
"name": "package-json-sample",
"version": "0.1.0",
"description": "A sample package.json file",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"express": "^4.13.3"
},
"repository": {
"type": "git",
"url": "https://github.com/some-url"
},
"keywords": [
"package", "json", "example"
],
"author": "Hao Dong",
"license": "MIT"
}
Now we got that out of the way, let's talk about why in my opinion how to set up projects correctly by using NPM scripts.
What are NPM scripts?
NPM scripts are actually super customisable and powerful, it is a shame that they are not always being utilised fully/correctly. You can run simple commands to do something like cleaning up projects or more complex chained commands to combine, compile and build projects.
So how do they work? Within scripts
object in the example above, there are two NPM commands start
and test
. When declared in package.json
like this, it means if you navigate to the directory in the terminal and you can execute them by typing npm run start
and npm run test
(npm start
or npm test
also works because start
and test
are reserved keywords). In our case, npm start
will start a node app using a file called index.js
and npm test
will print out "Error: no test specified" in the terminal.
Why should we use it?
1. Using local rather than global NPM packages
This is probably the most important reason in my opinion. Tools such as Bower, Gulp and etc requires to be installed as a global NPM package, like most cases in software development global is never good. This causes a number of problems, just imagine:
- You are working on two different projects but they require different versions of the same NPM package (e.g. web driver). If you install it globally, how much of a pain will this be to constantly change between them?
- What if you are on a newer version of a package than your co-workers? Or even worse, what if everyone is a different version to the production environment? When things start failing, it will be hard to track down issues and creates confusion.
You might ask why do people keep installing NPM packages as global then? Well, if you want to run something like gulp test
in the terminal Gulp must be installed as a global package, otherwise, gulp
is undefined.
How do NPM scripts fix this? Well, we don't have this issue because NPM script will try to find the relevant packages within node_modules
directory. This means that I can run npm test
(assuming the script is "test": "gulp test"
), and it will use the locally installed version of Gulp. The local package version should always be defined in the dependencies
or devDependencies
objects within package.json
file. Therefore, everyone will be using the same version of Gulp this way.
2. Easy to find
Since package.json is used by a lot of tools and it appears in almost every JavaScript project, therefore it is the first place developers will look to understand a project (still important to have a good README file!). So why not put the most important information in the place people will look first?
3. Centralise commands
Different projects use different JavaScript package managers, bundlers, and task runners. E.g. NPM, Bower, Browserify, Gulp, Grunt, Webpack and many others. Rather than having each of these tools doing something different (e.g. Bower as bundler and Gulp as task runner), why not put the commands in one place and centralise them? NPM scripts can execute commands for any of these tools. If I want to run something in gulp I can just have a script like "test": "gulp test"
, then run npm run test
in the terminal to execute it. So when developing I can just run one command, instead of a whole bunch just to get it to build my project.
4. Complex/chained commands
As mentioned before NPM will allow chaining commands, if you want to something quite complicated this is super useful. The snippet below shows what a chained command looks like.
"scripts": {
"start": "npm run command1 && npm run command2 && npm run command3",
"command1": "echo \"Running command 1\" && exit 1"
"command2": "echo \"Running command 2\" && exit 1"
"command3": "echo \"Running command 3\" && exit 1"
}
After looking at the chained commands you might think all the npm run
is a bit ugly when chaining scripts. Well, it happens someone already thought of this. There's a nice package called npm-run-all which lets you write much nicer chained commands. Using this package, you can write nice chained commands in the next section.
The recommended way of using NPM scripts
As of now, the follow snippest shows my recommended way of using NPM sripts and structuring projects.
"scripts": {
"dev": "npm-run-all build:clean build:html watch:html build:css watch:css watch:js",
"build": "npm-run-all build:clean build:html build:css build:js",
"build:clean": "rm -rf ./docs/*",
"build:html": "cp -r ./src/*.html ./docs/",
"watch:html": "onchange './src/*.html' -v -- npm run build:html &",
"build:css": "cp -r ./src/*.css ./docs/",
"watch:css": "onchange './src/*.css' -v -- npm run build:css &",
"build:js": "browserify ./src/app.js -o ./docs/bundle.js",
"watch:js": "watchify ./src/app.js -o ./docs/bundle.js -v",
"test": "mocha ./test/**/*.js"
}
The above example is taken from one of my recent project Finacial Calc if you are interested in looking at the folder structures and etc.