Skip To Main Content

A Facade for Tooling with NPM Package Scripts

Posted by K. Adam White

Jun 03 2015

We build a lot of software at Bocoup. Like other types of builders, we tend to grow attached to the particular sets of tools and scripts we use in our work. We don’t play favorites: my colleagues support Grunt, contribute to Gulp, and maintain stand-alone tools such as JSHint. It’s easy to take familiarity with these tools for granted, but for our clients (or new project contributors) every new tool is another barrier to entry.

By using npm package scripts with locally-installed dependencies, however, we can minimize the number of hoops new contributors have to jump through before they get up and running.

Install -G Considered Harmful

To illustrate my point, let’s take look at the npm-related steps in the instructions for building Angular.js locally:

  1. npm install -g grunt-cli
  2. npm install -g bower
  3. npm install
  4. bower install
  5. grunt package

Angular has excellent, straight-forward set-up instructions, and by now it’s reasonable to expect a web developer to be familiar with npm. This process is still founded very heavily upon global package installation, though, which adds to the number of steps required to get up and running.

Two global npm packages means that three separate shell commands are in play: npm, bower and grunt. We need to run “install” four separate times, and at the end we still need grunt package to explicitly kick off the build itself.

Is this “too much” overhead? Absolutely not – if all goes well, you only have to install these dependencies once, and you’re good to go. Plus, both Grunt and Bower are oft-used tools that will be useful across many projects. But more moving parts means more potential for confusion or mistakes, whether you’re a newcomer to open-source contribution or a maintainer of the project who’s just trying to set up your new laptop. We don’t have to sacrifice functionality or give up our ability to use the tools that make sense to us. But we can simplify our libraries’ workflow steps by providing a common facade for our tools in the form of npm package scripts.

Package.json’s “scripts” block

If you’re not familiar with what npm’s package scripts are capable of, I encourage you to read Keith Cirkel’s excellent article How to Use npm as a Build Tool. In short, npm package.json manifests can provide a scripts section to define package-specific commands, covering everything from how the package should be tested to what code should get run after the application is terminated. Most interestingly, commands used within npm script commands have direct access to locally-installed packages: this means that modules don’t have to be globally installed in order to be exposed to the user via npm scripts. As an example, let’s say we don’t have the Grunt CLI package installed:

$ grunt --version
-bash: /usr/local/bin/grunt: No such file or directory

All that CLI module does is expose local copies of grunt from the command line. We can achieve the same thing by installing both grunt and grunt-cli packages locally:

{
  "name": "no_-g_for_me",
  "scripts": {
    "grunt": "grunt"
  },
  "devDependencies": {
    "grunt": "^0.4.5",
    "grunt-cli": "^0.1.13"
  }
}

Note: -- lets you pass arguments through to package scripts

$ npm run grunt -- --version
> grunt --version

grunt-cli v0.1.13
grunt v0.4.5

Best of all, now your package has no implicit dependencies on the versions of globally-installed packages. Last fall, we spent several hours troubleshooting a unit test that was failing on only one of my teammate’s machines: in the end it was due to an outdated version of the globally-installed mocha package. We moved that dependency into our package.json posthaste!

Shortcuts and Hooks

npm provides shortcuts for a handful of common script names: running npm start is equivalent to running npm run start, and npm stop and npm test are also available as script shortcuts. Any other custom commands you define need to be run by name, as in npm run grunt.

Regardless of whether you’re using a shortcut script or not, you can also specify custom behavior to run before and after your script by adding additional commands prefixed with pre or post: npm run myscript will look for and invoke a "premyscript" task before running the "myscript" task, and will call "postmyscript" afterwards. This can be used to set up multi-stage functionality, with a single point of entry:

"scripts": {
  "prelint": "echo 'covered in lint!'",
  "lint": "echo 'running jshint'",
  "postlint": "echo 'lint-free :)'"
},
$ npm run lint

> echo 'covered in lint!'

covered in lint!

> echo 'running jshint'

running jslint

> echo 'lint-free :)'

lint-free :)

Since this functionality applies as well to built-in commands such as install or publish, you can set up a chain of tools specific to your own needs. If you maintain an npm library, for example, you can guard against releasing broken code with "prepublish": "npm test". The “prepublish” hook can also be used to bundle in your module’s platform-agnostic dependencies.

If you’re building an application, on the other hand, you’ll probably be using the “private” attribute in your package so “prepublish” won’t be relevant. In this context you’ll be more interested in hooks like “postinstall”: for example, you can have Bower automatically run on package installation with the script "postinstall": "bower install", in many instances obviating the need to have bower installed globally at all. Since all of your normal shell commands are also available inside package script definitions we can also alias non-npm commands such as Ruby. Using jekyll for your documentation site? Add a “start” script that just runs jekyll serve!

If you’re already using npm scripts for some purposes, it’s advantageous to consolidate all your common commands for consistency with that existing tooling. Just be careful about platform-specific commands: Node & npm are committed to cross-platform support, and we should be, too.

npm test is worth special note because it is useful to both package and application developers, and gives you a consistent way to run your unit tests whether you’re using Grunt, Gulp, Karma, Mocha, or any other testing tool. Travis CI actually assumes npm test is defined and available as its default test script for Node applications: your CI shouldn’t care what tools you use internally, and npm scripts are a clean way to interface with those tools. If your project ever switches task runners or changes to a new testing framework, just update the npm test command, and your existing application workflow will still work!

Not every npm package works properly when installed locally, but most will. You’re limited only by the demands of your project: building an Express app? npm start could fire up a local development server, whereas npm run server:watch could start that server with nodemon to auto-reload on file changes. Even if your commands are not complex, providing a package script entry-point makes ramp-up simpler. Take the local installation instructions from the jQuery README:

Clone a copy of the main jQuery git repo by running:

git clone git://github.com/jquery/jquery.git

Enter the jquery directory and run the build script:

cd jquery && npm run build

While the jQuery README does go on to explain how to set up grunt-cli if you want to customize the build process, npm run build is the only Node-specific command that is needed to start working on a jQuery bug. They’re second-nature for us, but Git and npm are always going to be a new tool for somebody: the more we can avoid adding to the list of project dependencies and set-up steps, the easier it will be for those beginners to grow into experienced contributors.

Discoverability

No tool will replace a good step-by-step walk-through in a README, but there’s one final benefit package scripts provide to your application’s users: discoverability via npm run. Calling npm run with no arguments will list out all of the scripts defined in your package, which can be a quick way for a new user to suss out what’s possible with your application.

Pick Your Poison

For the past few years, we’ve been living in a golden age of web development tooling. However excited we get about those tools, when you make an application, your users and contributors shouldn’t need to know whether you prefer Grunt or Gulp, or whether you cut out the task runner entirely in favor of running your tasks directly. These tools are all a means to an end, and judicious use of locally installed development dependencies and package.json scripts can make that end a lot easier to reach.

Package Scripts Are Worth It

There’s nothing wrong with globally-installed tools, or with complicated build commands, but it’s worth it to alias those commands as package script. Even if you always run gulp test, having npm test defined can make your CI system work out of the box. npm run browserify can save a lot of keystrokes over typing out command-line flags each time you build.

Most importantly, package scripts expose important tasks in a consistent, discoverable way. Lowering the barrier to entry means faster ramp-up for new employees, easier contributor onboarding, and less delay if you revisit a codebase after leaving it for a long period of time. There’s no downside to adding package scripts—at worst they just alias through to other tools—and there’s a lot to be gained!

Posted by
K. Adam White
on June 3rd, 2015

Comments

We moved off of Disqus for data privacy and consent concerns, and are currently searching for a new commenting tool.

Contact Us

We'd love to hear from you. Get in touch!