Founding Stack

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.