Automation using NPM run command by James Halliday
The following post is written by James Halliday. You can find more of his work on his personal site substack.net
There are some fancy tools for doing build automation on javascript
projects that I've never felt the appeal of because the lesser-knownnpm run
command has been perfectly adequate for everything I've needed to do while
maintaining a very tiny configuration footprint.
Here are some tricks I use to get the most out of npm run
and the
package.json
"scripts" field.
the scripts field
If you haven't seen it before, npm looks at a field called scripts
in
the package.json of a project in order to make things likenpm test
from the
scripts.test
field and npm start
from the scripts.start
field work.
npm test
and npm start
are just shortcuts for npm run test
and
npm run start
and you can use npm run
to run whichever entries in the
scripts
field you want!
Another thing that makes npm run
really great is that npm will automatically
set up$PATH
to look in node_modules/.bin
, so you can just run commands
supplied by dependencies and devDependencies directly without doing a global
install. Packages from npm that you might want to incorporate into your task
workflow only need to expose a simple command-line interface and you can always
write a simple little program yourself!
building javascript
I write my browser code with node-style commonjs module.exports
and
require()
to organize my code and to use packages published to npm.
browserify can resolve all the require()
calls statically as a build
step to create a single concatenated bundle file you can load with a single
script tag. To use browserify I can just have ascripts['build-js']
entry in
package.json that looks like:
"build-js": "browserify browser/main.js > static/bundle.js"
If I want my javascript build step for production to also do minification, I
can just adduglify-js
as a devDependency and insert it straight into the
pipeline:
"build-js": "browserify browser/main.js | uglifyjs -mc > static/bundle.js"
watching javascript
To recompile my browser javascript automatically whenever I change a file, I
can just substitude thebrowserify
command for watchify and add -d
and
-v
for debugging and more verbose output:
"watch-js": "watchify browser/main.js -o static/bundle.js -dv"
building css
I find that cat
is usually adequate so I just have a script that looks
something like:
"build-css": "cat static/pages/*.css tabs/*/*.css > static/bundle.css"
watching css
Similarly to my watchify build, I can recompile css as it changes by
substitutingcat
with catw:
"watch-css": "catw static/pages/*.css tabs/*/*.css -o static/bundle.css -v"
sequential sub-tasks
If you have 2 tasks you want to run in series, you can just npm run
each task
separated by a&&
:
"build": "npm run build-js && npm run build-css"
parallel sub-tasks
If you want to run some tasks in parallel, just use &
as the separator!
"watch": "npm run watch-js & npm run watch-css"
the complete package.json
Altogether, the package.json I've just described might look like:
{
"name": "my-silly-app",
"version": "1.2.3",
"private": true,
"dependencies": {
"browserify": "~2.35.2",
"uglifyjs": "~2.3.6"
},
"devDependencies": {
"watchify": "~0.1.0",
"catw": "~0.0.1",
"tap": "~0.4.4"
},
"scripts": {
"build-js": "browserify browser/main.js | uglifyjs -mc > static/bundle.js",
"build-css": "cat static/pages/*.css tabs/*/*.css",
"build": "npm run build-js && npm run build-css",
"watch-js": "watchify browser/main.js -o static/bundle.js -dv",
"watch-css": "catw static/pages/*.css tabs/*/*.css -o static/bundle.css -v",
"watch": "npm run watch-js & npm run watch-css",
"start": "node server.js",
"test": "tap test/*.js"
}
}
If I want to build for production I can just do npm run build
and for local
development I can just donpm run watch
!
You can extend this basic approach however you like! For instance you might
want to run thebuild
step before running start
, so you could just do:
"start": "npm run build && node server.js"
or perhaps you want an npm run start-dev
command that also starts the
watchers:
"start-dev": "npm run watch & npm start"
You can reorganize the pieces however you want!
when things get really complicated...
If you find yourself stuffing a lot of commands into a single scripts
field
entry, consider factoring some of those commands out into someplace likebin/
You can write those scripts in bash or node or perl or whatever. Just put the
proper#!
line at the top of the file, chmod +x
, and you're good to go:
#!/bin/bash
(cd site/main; browserify browser/main.js | uglifyjs -mc > static/bundle.js)
(cd site/xyz; browserify browser.js > static/bundle.js)
"build-js": "bin/build.sh"
If you absolutely need your project to build on windows, just make sure that your windows devs have a copy of msysgit which ships with bash or cygwin or something similar. Or tell them to get a UNIX.
I have some experiments in the works to help with this windows-can't-run- bash problem, but the job control and subshell sections aren't finished yet.
conclusion
I hope that this npm run
approach I've documented here will appeal to some of
you who may be unimpressed with the current state of frontend task automation
tooling, particularly those of you like me who just don't "get" the appeal of
some of these things. I tend to prefer tools that are more steeped in the unix
heritage like git or here with npm just providing a rather minimal interface on
top of bash. Some things really don't require a lot of ceremony or coordination
and you can often get a lot of mileage out of very simple tools that do very
ordinary things.
If you don't like the npm run
style I've elaborated upon here you might also
consider Makefiles as a solid and simple alternative to some of the more baroque
approaches to task automation making the rounds these days.