Give npm scripts a chance

tl;dr: Read the npm scripts docs. Then abstract away all scripts/tasks with npm run [name].

Maybe you don't need gulp, or grunt, or whatever. Please, give it five minutes.

I'm reading, what is it?

The npm scripts docs entry give a good succinct explanation that's well worth reading.

Basically, npm scripts lets you define named scripts, which are combinations of shell commands. Not only that, it'll make all the binaries in node_modules/.bin/ (as well as a couple other things) available to the environment.

If you've used npm for a while you've probably used or seen npm test and npm start. These are shorthands for npm run test and npm run start.

This post covers some of the benefits of adding your own scripts, as well as provide examples of how to do so.

What do I get out of it?

Consistency

Using npm scripts lets you maintain a consistent interface for your application's tasks, without having to install any additional modules.

Let's say you're using gulp. If you're using gulp with a custom gulpfile.js, you'd run something like:

terminal

$ gulp --gulpfile tasks/gulpfile.js [task]

Using npm scripts:

package.json

{
  "scripts": {
    "gulp": "gulp tasks/gulpfile.js"
  }
}

terminal

$ npm run gulp -- [task]

With npm scripts you don't have to worry about the configuration arguments.

However, I'd propose that you don't really care that gulp or grunt or whatever is being used, you care about the specific task.

Let's take it one step further, and wrap all the tasks:

package.json

{
  "dev": "gulp --gulpfile tasks/gulpfile.js dev",
  "build": "gulp --gulpfile tasks/gulpfile.js build",
  "test": "karma start test/karma.conf.js test/*.test.js"
}

terminal

$ npm run dev
$ npm run build
$ npm run test

Now instead of worrying about the correct arguments for karma or gulp, you can focus on the tasks themselves.

It doesn't matter that some tasks are using gulp while others are using the tool directly, you can run all tasks the same way.

Composability

You can have as many scripts as you want, and use them to create other scripts.

One problem that you quickly notice in the previous section is that you end up having to repeat arguments. Don't worry, we can solve this!

I've recently been using babeljs, an ES6 to ES5 transpiler. While using babeljs I've been composing scripts, so I believe it shows a good real-world example:

package.json

{
  "scripts": {
    "base": "babel-node --blacklist=regenerator --experimental",
    "cover": "npm run cover:base -- test/*.test.js",
    "cover:base": "NODE_ENV=test npm run base -- node_modules/.bin/isparta cover --report html node_modules/.bin/_mocha -- --reporter dot",
    "dev": "npm run base -- server.js",
    "test": "npm run test:base -- test/*.test.js",
    "test:base": "NODE_ENV=test npm run base -- node_modules/.bin/_mocha"
  }
}

terminal

$ npm run dev
$ npm run test
$ npm run cover

Since blacklist and experimental flags are always passed to babel-node, a base script is made. As the name implies, it's used as the base for others.

When generating code coverage, the same options are always passed to isparta (an istanbul fork that works with babeljs), so a cover:base script is made. The cover script just passes the file glob.

The test:base script sets the env, and calls mocha. This is done so babel-node's flags are respected. The test script, like the cover script, simply passes the tests glob.

This is great, now you can reuse some of these commands!

Want to watch all tests during development? Call the test script with the watch flag, or add a test:live script.

$ npm run test -- --watch

Maybe you only want to run a specific test?

$ npm run test:base -- test/something.test.js

If push comes to shove and you don't have a script configured in a reusable way, you can always call the binary directly, that's the beauty of it! You're not being forced to give up anything.

One problem that could arise with this approach is that the scripts section in your package.json can grow pretty large, and it would become difficult to know what each task is for. With that said, maintaining a TASKS file or keeping the list in the README to explain their use and purpose seems to work fine.

Encapsulation

Since npm scripts provides all the binaries in node_modules/.bin/ to the script environment, instead of relying on globals or having to type node_modules/.bin/babel-node, you can use babel-node.

If you avoid polluting the global env, you can work on multiple apps that might use different tool versions without worrying. You don't install modules globally, so why would you do that for your tools? You're also less likely to encounter "but it works on my computer"-style problems.

Another awesome feature that plays well with npm scripts is npm config. Without diving into details, it allows you to set configuration options that are usable in your scripts. This is useful because you can also overwrite them in different environments (e.g. development, production, test).

Task runners, Unix philosophy and tools

Before you decide to go searching for a tool, take a bit of time to think about the tools that are already in your toolbox. Remember that the command-line provides a lot of fast, powerful, and very composable tools! Maybe instead of having a sequence of gulp tasks to clean up your build folder, build your app, and copy it to the public folder, you can make a script:

package.json

{
  "scripts": {
    "build": "rm -rf build/* && gulp build && cp -Rf build/* public"
  }
}

terminal

$ npm run build

Most tools provide a CLI, give them a chance! If their CLI is not good or powerful enough for your needs, consider writing a node script to invoke the programmatic API and perform your task.

If being cross-platform (e.g. Windows) is a requirement, there's lots of modules that export binaries for common actions. Two examples that I love are mkdirp and trash.

You might think that since you're using gulp or grunt or whatever for X, Y, and Z, that you might as well just keep adding plugins. Instead of learning the module's APIs and using it right away, you're opening up yourself to the possibility of having to jump through hoops for the plugin (e.g. the plugin creator doesn't expose all the module's APIs), or the plugin might introduce subtle bugs, or it might be completely and utterly broken, or it might not play well with other plugins.

Having a bunch of npm scripts to glue tools together might actually yield simpler results than you expect.

Even if you decide to continue using one of these task runners, use npm scripts to alias your tasks! It sucks having to install any of these tools to try out a project or contribute to it.

Closing thoughts

I've used both gulp and grunt, extensively. When I saw articles speaking against them, I thought: "Well, for my case I think it actually makes sense to use [whatever]." Eventually, I grew frustrated with em, and I decided to try npm scripts. I now realize I was wrong, and I couldn't be happier.

Like the title says, just give it a chance.