I can’t imagine building a modern JavaScript application or website without using any kind of build system. Recently I’ve used Grunt in several projects. There is also Gulp, Broccoli and probably many more.

I’ve asked myself if the building process can’t be done in a different way. Why do I need extra layer like Grunt? What would be the benefits of having build system created on your own? Why not just using NodeJS and npm packages? The think is that behind Grunt there are also the same packages as you’d use without Grunt.

The problem

All of these task runners try to abstract some kind of task paradigm using their own approach. Grunt is using Gruntfile.js or Gruntfile.coffee to configure or define tasks and load Grunt plugins. Gulp is using gulpfile.js configuration file that tells Gulp its tasks, what those tasks are, and when to run them. All of these task runners gives you some predefined approach.

Obviously, there are advantages and disadvantages. Based on my researches and experiments I think that I can get more freedom and flexibility without these task runners. They are using same npm packages as you’d use without them. Here are some cases that I am considering as a disadvantages:

updates – when some of the npm packages are updated then you need to wait until author of the Grunt task will update it.

– when some of the npm packages are updated then you need to wait until author of the Grunt task will update it. options – you have to stick the available Grunt task options. If you want to do something more you have to use tricks.

– you have to stick the available Grunt task options. If you want to do something more you have to use tricks. actions – every Grunt task is designed to do a specified work. If you want to extend specified task you have to use the tricks.

– every Grunt task is designed to do a specified work. If you want to extend specified task you have to use the tricks. componentization – to combine few tasks into one task you have an option to use Gruntfile.js and define there a task that use other tasks, e.g. grunt.registerTask('build:dev', 'Prepare application package for development env, non-minified', ['validate', 'clean:all', 'copy:images', 'copy:fonts', 'sass:dev', 'requirejs:dev', 'jsdoc']); . You can’t do anything between running those tasks.

– to combine few tasks into one task you have an option to use and define there a task that use other tasks, e.g. performance – every time Grunt is loading all the tasks even, if you want to run only one, specified task. However, as I’ve noticed that this depends also on OS. On Windows Grunt is the slowest. On Linux and Mac OS it works quite fast. I guess this is related to the file system.

A benefit of using Grunt is that it helps to unify the common workflows of web developers.

Note: you can build your own Grunt tasks as well, but this may not be the case anymore as you can use NodeJS and npm packages directly.

The solution

The better option, in my opinion, is to just use npm packages and NodeJS. No need to have anything extra. Even, when we think from the perspective that our build tasks aren’t standardized it still brings more advantages than disadvantages. When you write using only NodeJS and npm packages you have a freedom. You can decide at any time how the tasks should work. You can use any npm packages you think are valuable for you. And you will learn NodeJS. Which is a side, positive effect.

And what’s important – once the tasks are written no need to look at them every day. They just works.

How to

Let me show you how I did it. I’ve decided to create file Makefile.js and use following npm packages:

ShellJS from npm.

ShellJS is a portable (Windows/Linux/OS X) implementation of Unix shell commands on top of the Node.js API. You can use it to eliminate your shell script’s dependency on Unix while still keeping its familiar and powerful commands. You can also install it globally so you can run it from outside Node projects – say goodbye to those gnarly Bash scripts!

glob from npm.

Match files using the patterns the shell uses, like stars and stuff. This is a glob implementation in JavaScript. It uses the minimatch library to do its matching.

shelljs-nodecli from npm.

An extension for ShellJS that makes it easy to find and execute Node.js CLIs.

In that way I’ve got all necessary commands to manipulate on files. The last step before creating build steps is to look at a Make tool from shelljs package.

All you need later is to create target.nameOfStep = function(){}; and later use it from command line: node Makefile.js nameOfStep .

Example

Let’s say we want to build small step that clears destination folder where we’ll put files for distribution. This may look like:

// Define it at the very top of file var nodeCLI = require('shelljs-nodecli'), glob = require('glob'); require('shelljs/make'); // end target.clean = function () { var targetPath = './web/'; if (test('-e', targetPath)) { rm('-rf', targetPath + '*'); } mkdir('-p', targetPath); };

Simple, isn’t it? How about copying files?

target.copy = function () { var files = [ { from: 'frontend/src/images/*', to: 'frontend/web/images/' }, { from: 'frontend/src/fonts/*', to: 'frontend/web/fonts' } ]; files.forEach(function (resource) { cp('-rf', resource.from, resource.to); }); };

Well, how about using some linting tools? Let’s see how eslint can be used:

var CLIEngine = require('eslint').CLIEngine, DO_NOT_INCLUDE = '!', VALUE_NOT_FOUND = -1; target.eslint = function (options) { var cli, files, allFiles = [], report, formatter, fixAutomatically = false; if (options && options.indexOf('fix') > VALUE_NOT_FOUND) { fixAutomatically = true; } files = [ 'build/**/*.js', 'frontend/src/**/*.js', 'frontend/spec/**/*.js', 'Makefile.js', DO_NOT_INCLUDE + 'frontend/spec/reports/**/*' ]; cli = new CLIEngine({ envs: ['browser', 'mocha'], useEslintrc: false, configFile: 'build/eslint.json', cache: true, fix: fixAutomatically }); files.forEach(function (sources) { allFiles = allFiles.concat(glob.sync(sources)); }); report = cli.executeOnFiles(allFiles); formatter = cli.getFormatter(); if (report.errorCount > 0) { console.log(formatter(report.results)); exit(1); } };

package.json and scripts

You can also use npm’s scripts object lives inside package.json , meaning there is no new files to add to your project. So, if your package.json has this:

"scripts": { "lint": "node Makefile.js lint" }

then you could run npm run lint to execute the lint script. Easy, isn’t it?

Summary

I am aware that there might be a cases to use Grunt or similar tasks runner, but I think that in 99% you can just use NodeJS and npm packages. The best way to find out if it suits for you is to try it.

Comments? Feel free to tweet me or leave the comment here.