NPM

Automate your front-end development environment with npm

Joseph Post JavaScript, Technology Snapshot, Tutorial Leave a Comment

For JavaScript development, there are more build tools than you know what to do with. The non-verbal utterance category contains the two most popular ones, Grunt, and Gulp. Then you have your food-related tools, like Brunch, Mimosa and Broccoli. You’ve got fancy new hot stuff like fly. Then there’s classic make-like tools like Cake, Jake and ShellJS’s make. Some folks just use Make.

That’s a lot of options. Maybe it just makes sense to just roll up your sleeves and let out a snort, or take a sip. Yep, just pick one of the big two and move on, it would be a safe choice.

But maybe you were never really sold on the big two. The plugin dependencies that are just wrappers for other modules, hundreds of lines of configuration to setup your tasks, spread across a dozen files… it all seems like an unnecessary abstraction. There’s got to be a better way.

Sometimes it’s easiest to just drop a few lines in a shell script and get cranking. npm let’s you do that.

npm run-script

In your package.json, you define a scripts object. In the scripts object, your keys are the names of your commands and the values are single-line scripts. To execute, you type ‘npm run <command>’. There are also some lifecycle scripts that dont require ‘run’ (like ‘npm start’ and ‘npm test’). Take a look at the official docs, here and here.

That’s basically all there is to it. My goal here is to go over specifics of how to set up a JavaScript development environment without installing any of the task runners, or even writing JavaScript code. We’ll want to be able to bundle up our JavaScript files, minify them, rebundle on changes all while serving a directory of the built static files. We’ll also want to be able to run a test suite, build documentation, and clean our build artifacts.

There are a lot of choices out there for every type of task we cover. I’m just going to pick some tools I’m more familiar with. A great thing about using npm scripts is there are a ton of node modules that have command-line interfaces you can leverage.

Building / Bundling

What we want to do is bundle up our code, stylings, and html into build directory that we can delete anytime.

For the JavaScript, feel free to use your favorite lisp-like or haskell-inspired transpiled language; for this we’ll just stick to plain-jane ES5 and Browserify for now.

We want something like this:

{
  "scripts": {
    "build:js": "browserify src/entry.js -o public/bundle.js"
  }
}

But that won’t work. Browserify will fail if the directory to the output file does not exist. It also seems tacky to have a separate task just to make sure the directory exists before we bundle up. Someone submitted a pull request well over a year ago to fix this, but it has yet to be merged.

Sometimes it’s hard to find the functionality you want in someone else’s node module. You can always write your own. I wrote one called Shove so I can send stdout to files in directories that don’t exist yet. It’s so simple that it’s almost silly, but hey, now we can write something like this:

{
  "scripts": {
    "build:js": "browserify src/entry.js | shove public/bundle.js"
  }
}

What about all the transforms? Some people can’t handle a huge single line command in their config, but I don’t think it’s really that bad. Otherwise, look into using the browserify.transform object in your package.json file.

We can use cpx to copy our ‘index.html’ file over, and node-sass for generating css.

{
  "scripts": {
    "build:css": "node-sass --output-style compressed src/main.sass public/main.css",
    "build:html": "cpx src/index.html public"
    "build:js": "browserify src/entry.js | shove public/bundle.js"
  }
}

Cleanup

Just use rimraf, and recursively delete your build directory.

{
  "scripts": {
    "build:css": "node-sass --output-style compressed src/main.sass public/main.css",
    "build:html": "cpx src/index.html public"
    "build:js": "browserify src/entry.js | shove public/bundle.js",
    "clean": "rimraf public"
  }
}

Code Quality / Testing

You should be using a style linter. When I work on a project, I don’t want to have to spend time manually determining if something I’m writing follows the style guidelines. I should be able to quickly set up my editor so that my tab key does what it should do. And when I don’t follow the style, I should get squigglies, highlights, tooltips, and all that other great stuff.

I’m less interested in what someone thinks is the way you ‘should’ right JavaScript, and more interested using a consistent style in an application. My suggestion is to use something simple, like Standard, XO or JSLint. Or you can get things exactly how you want with ESLint. It should run when you type ‘npm test’. This matters because tools like TravisCI will automatically run ‘npm test’ on Node projects, let you know when PRs fail the tests, and other niceties.

{
  "scripts": {
    "build:css": "node-sass --output-style compressed src/main.sass public/main.css",
    "build:html": "cpx src/index.html public",
    "build:js": "browserify src/entry.js | shove public/bundle.js",
    "clean": "rimraf public",
    "test": "standard"
  }
}

We also want to use ‘npm test’ for… tests. We can just grab something like jasmine-node, add a ‘spec’ folder with some ‘*-spec.js’ files. So let’s make sure we run both the linter and the tests.

{
  "scripts": {
    "build:css": "node-sass --output-style compressed src/main.sass public/main.css",
    "build:html": "cpx src/index.html public",
    "build:js": "browserify src/entry.js | shove public/bundle.js",
    "clean": "rimraf public",
    "test": "standard &amp;&amp; jasmine-node spec"
  }
}

The double ampersand (&&) lets us runs commands in series, and is cross-platform. Other cross-platform commands include pipe ‘|’, stdout ‘>’, stdin ‘<‘, and appending to a file ‘>>’. There’s a guide for comparing *nix commands to Windows commands here.

One more thing on the testing side. It’s nice to use pre-commit hooks that make sure our tests pass before commiting code. There’s a simple solution. pre-commit by default will run ‘npm test’ on commits, and exit with an error if a test fails. Just install it and you’re done.

Documentation

Use JSDoc and set up whatever you need in a ‘conf.json’ file.

{
  "scripts": {
    "build:css": "node-sass --output-style compressed src/main.sass public/main.css",
    "build:html": "cpx src/index.html public",
    "build:js": "browserify src/entry.js | shove public/bundle.js",
    "clean": "rimraf public",
    "docs": "jsdoc -c conf.json"
    "test": "standard &amp;&amp; jasmine-node spec"
  }
}

Dev Server

Now we need a way to serve our static files. One option is http-server, and it’s super simple. If there is a ‘public’ directory, http-server defaults to serving from there. So all we need is this:

{
  "scripts": {
    "serve": "http-server"
  }
}

Need mock data? There is an excellent module called json-server that will set up REST routes based on a json file. It will also serve your static files, again from ‘public’ by default if that folder exists. We can take snapshots while we’re working too, so let’s set that up.

{
  "scripts": {
    "build:css": "node-sass --output-style compressed src/main.sass public/main.css",
    "build:html": "cpx src/index.html public",
    "build:js": "browserify src/entry.js | shove public/bundle.js",
    "clean": "rimraf public",
    "docs": "jsdoc -c conf.json"
    "serve": "json-server mock/db.json --snapshots mock"
    "test": "standard &amp;&amp; jasmine-node spec"
  }
}

Watching

When certain files change, we want certain scripts to run, waiting for the next change. A lot of the modules we are using already have this kind of functionality built in. There’s a way to do this cleanly with npm-watch. We add a new object to the package.json file called ‘watch’. Each property is the name of a npm-script to run, and the value is a path, glob, or array of path/globs to watch. Only the script that is associated with the changed files will run.

{
  "scripts": {
    "build:css": "node-sass --output-style compressed src/main.sass public/main.css",
    "build:html": "cpx src/index.html public",
    "build:js": "browserify src/entry.js | shove public/bundle.js",
    "clean": "rimraf public",
    "docs": "jsdoc -c conf.json"
    "serve": "json-server mock/db.json --snapshots mock"
    "test": "standard &amp;&amp; jasmine-node spec"
    "watch": "npm-watch"
  },
  "watch": {
    "build:css": "src/*.sass"
    "build:html": "src/index.html",
    "build:js": "src/*.js"
  }
}

All Together Now

We have a lot of great tasks to help automate the workflow, but now we want a single command that set up everything we need to get to work. I’ve investigated most of the parallel task-running node modules out there, and npm-run-all is probably my favorite. We want to run the ‘watch’ command and the ‘serve’ command together. We’ll use the ‘start’ lifecycle script so you can just run ‘npm start’ and you’re ready.

Here’s what it looks like all together. It’s easy to read and see all at once, and is an order of magnitude less lines than a Grunt or Gulp setup:

{
  "scripts": {
    "build:css": "node-sass --output-style compressed src/main.sass public/main.css",
    "build:html": "cpx src/index.html public",
    "build:js": "browserify src/entry.js | shove public/bundle.js",
    "clean": "rimraf public",
    "docs": "jsdoc -c conf.json",
    "serve": "json-server mock/db.json --snapshots mock",
    "start": "npm-run-all --silent --parallel serve watch",
    "test": "standard &amp;&amp; jasmine-node spec",
    "watch": "npm-watch"
  },
  "watch": {
    "build:css": "src/*.sass"
    "build:html": "src/index.html",
    "build:js": "src/*.js"
  }
}


Thanks for reading! I put a lot of links in this post, most of these modules have good documentation, check ’em out.


About the Author
Joseph Post

Joseph Post

Joe Post is a software developer and composer based in Kansas City. His favorite operating system is Arch Linux. He prefers using community-driven, open-source technologies. Currently working on a Microservices IOT platform.


Share this Post

Leave a Reply

Your email address will not be published. Required fields are marked *