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.
  • options – 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.
  • 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.

  • 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.

1 Star2 Stars3 Stars4 Stars5 Stars (No Ratings Yet)
Loading...

Comments

You can leave a response, or trackback from your own site.

13 Responses to “Why I switched to only NodeJS + npm and stopped using Grunt”

  1. Chev, 10 December 2015

    Interesting read. I am also someone who doesn’t like to add things to my workflow unless it really does bring value in the form of time saving, readability, etc. It has to really bring something to the table that is otherwise tedious and inconvenient without it.

    That said, I feel like this article was written by someone who used Grunt, but not gulp. Author may have tried Gulp but I don’t think he’s put into daily practice. Gulp is far less complicated than grunt and is much faster. Running concurrent tasks is a huge deal for our team. We got our Grunt build down from two minutes to less than 30 seconds using gulp.

    Not trying to be a gulp shill or hold it up as the best thing ever, but I do feel it brings value to the table that is otherwise tedious monotony without it. Sure, I could use plain node to iterate over directories, grab all JS files, create transform streams, etc. But gulp provides a very simple API that lets me do all that with much less work.

  2. Cezary Tomczyk, 10 December 2015

    Yes, I was working only with Grunt, but not Gulp. I will try Gulp just to learn how it works.

    As for “[…]I could use plain node to iterate over directories, grab all JS files, create transform streams, etc. But gulp provides a very simple API that lets me do all that with much less work.

    Yes, working with Gulp in some cases there might be less work. It all depends what’s the goal of task.

    However, what I have notice is that some of the Grunt tasks wasn’t doing anything special than just passing options to another npm package. In that case there is no real benefit of having extra layer.

  3. Oskar, 11 December 2015

    Working with make itself was actually quite plrasing. All of the node packages I depended on in a private project had a command line interface.

  4. Lucien, 17 December 2015

    Gulp has the advantage of working in-memory with streams — on large projects this has brought our build times down from 30 seconds to 4 seconds pretty consistently!

    With traditional build systems you don’t get that potential usually, and you read/write to the filesystem for every single step in the build.

  5. Mark Volkmann, 17 December 2015

    OMG, your eslint example convinces me that I do not want to do this! Do you really think that is easy? It is far easier to configure that in webpack, gulp, and Grunt.

  6. Cezary Tomczyk, 17 December 2015

    Yes, sometimes it can be easier to configure it using some tasks runner like Grunt or Gulp. However, it doesn’t have to apply to all type tasks and there are other reasons that I mentioned in the post why I prefer that way.

  7. Ron Wertlen, 17 December 2015

    +1. Powerful approach, and thanks for sharing your ideas.

  8. Rhett Lowe, 17 December 2015

    @Chev, @Cezary,

    I am a Gulp user and I can say that Gulp is far more fluid and comfortable to write than my experiences with Grunt. Gulp allows one to write code in JS instead of just configuration files. This eases much of the common issues and needs for workarounds.

    That said, “[…] when some of the npm packages are updated then you need to wait until author of the Grunt task will update it.” still applies to Gulp in most cases and the need for tricks will still arise, which can be very annoying.

    But I don’t think any system will ever get away from having that. As soon as one adds an abstraction layer to ease something, another thing is made more complicated. Also, I cannot overstate the importance of “[…] it helps to unify the common workflows of web developers.” The more people who know the same thing the more of my code others can debug faster. Even if I custom write a task and it does something “obscure,” that is usually one task out of a dozen; at least others can quickly and easily ready 80%+ of my code without trying.

  9. Francis Kim, 20 December 2015

    Your approach makes a lot of sense, thanks for sharing it!

  10. llihak, 14 January 2016

    Can using PostCSS resolve some of your complaints?

  11. Cezary Tomczyk, 14 January 2016

    I haven’t tried PostCSS yet.

  12. Sergiy Stotskiy, 17 January 2016

    `npm lint` won’t work as it custom script name. Should be `npm run lint`.

    Also I don’t like the idea of using Makefile.js and shelljs. You do the same Grunt/Gulp stuff there.

    Instead Bash scripts are more powerful and available on any linux machine (windows with cygwin installed, gitbash, etc)

  13. Cezary Tomczyk, 17 January 2016

    `npm lint` won’t work as it custom script name. Should be `npm run lint`.

    Thanks for correction.

    Also I don’t like the idea of using Makefile.js and shelljs. You do the same Grunt/Gulp stuff there.

    Sometimes the actions might be the same, but not always. The Grunt/Gulp task may not do exactly what is expected and then one or the other way you want to extend it. I like idea of Makefile.js, but that’s my personal preferences.

Before you add comment see for rules.

Leave a Reply

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

7q8e3u