0

By Mária Jurčovičová

A new release of a typical, non-trivial JavaScript project needs to run unit tests, concatenate all source files into one file and minify the result. Some of them also use code generators, coding style validators or other build time tools.

Grunt.js is an open source tool that is able to help you perform all of those steps. It is extensible and was written in JavaScript, so anyone working on a JavaScript library or project should be able to extend it as needed.

This post explains how to use Grunt.js to build JavaScript library. Grunt.js requires Node.js and npm to run, so first sections explain what those are, how to install them and how to use them. You can feel free to skip those sections if you have already worked with npm. The fourth and fifth sections cover how to configure Grunt and a number of typical Grunt tasks.

The demo code for the Grunt.js configuration discussed here is avaiable on Github.

Tool Chain Overview

To get started, we’ll need three tools:

Node.js is a popular server side JavaScript environment. It is used to write and run JavaScript servers and JavaScript command line tools. If you want to learn more about Node.js, this Stack Overflow answer links everything you might ever need to get you started.

Npm is a package manager for Node.js. It is able to download dependencies from a central repository and to solve most dependency conflicts. The Npm repository stores only Node.js server and command line projects. It does not contain libraries meant to be used in web or mobile applications. We will use it to download Grunt.js.

Grunt.js is task runner that we will use to build our project. It is runs on Node.js and can be installed via Npm.

Node.js and Npm Installation

You can install node.js either directly from the download page or using any of these package managers. If successfully installed, Node.js will print its version number if you type node -v into the console.

Most installers and package managers install both Node.js and Npm at the same time. Type npm -v into the console to test whether you have Npm installed properly. If it is available, it will print its version number. What you should do if it is not available depends on your operating system.

Linux

Download and use the installation script.

Windows

The Windows installer contains npm and updates the path variable for current user. A separate Npm install is needed only if you downloaded Node.exe only or compiled it from the source.

Download the latest Npm zip from this page. Unpack and copy it into node.exe installation directory. If you want, you can also update the path variable to have it available anywhere.

OSX

Npm is bundled inside the installer.

Npm Basics

An understanding of some Npm basics is useful for those who would like to install and use Grunt.js. This is section contains only those basics. Additional details can be found in the npm documentation.

This section will explain four things:

what is npm;

the difference between local and global npm plugin installations;

the package.json file and its content;

the npm install command.

Overview

Npm is package manager that is able to download and install JavaScript dependencies from central repository. Installed packages can be used either as libraries within Node.js projects or as command line tools.

Projects usually keep a list of all their dependencies inside a package.json file and install them from there. Plus, additional Npm libraries can be installed also from command line.

Global vs Local Installation

Each package can be installed either globally or locally. The practical difference is in where they are stored and where they are accessible from.

Globally installed packages are stored directly inside the Node.js installation directory. They are called global, because they are available from any directory or Node.js project.

Local installation puts downloaded packages into current working directory. Locally installed packages are then available only from that one directory and its sub-directories.

Locally installed packages are stored inside the node_modules sub-directory. Whatever version control system you use, it is reasonable to add that directory into its .ignorefile.

Package.json

The package.json file contains an Npm project description. It is always located in project root and contains the project name, version, license and other similar metadata. Most importantly, it contains two lists of project dependencies.

The first list contains dependencies that are needed at runtime. Anyone who wishes to use the project must install all of them. The second list contains dependencies that are needed only during the development. Those include test tools, build tools and coding style checkers.

The easiest way to create a package.json file is via the npm init command. The command asks a few questions and generates a basic package.json file in the current working directory. Only the name and version properties are mandatory. If you do not plan to publish your library to Npm, you can ignore the rest.

The following links contain good package.json descriptions:

The Install Command

Npm packages are installed using the npm install command. The installation is local by default. A global installation has to be specified using the -g switch.

The Npm install command with no other parameters looks for a package.json file in the current directory or any of its parent directories. If one is found, the command then installs all listed dependencies into the current directory.

Concrete Npm packages are installed using npm install <[email protected]> command. The command will find the required version of the package in the central repository and install it into current directory.

Including a version number via @version is optional. If it is missing, Npm simply downloads latest available release.

Finally, the install command invoked with the --save-dev switch not only installs the package, but also adds it into package.json as a development dependency.

Adding Grunt.js to the Project

We’ll start a Grunt.js configuration by first adding Grunt.js into our JavaScript project. We’ll need to install two Grunt.js modules:

grunt-cli – command line interface (CLI);

grunt – task runner.

Important: Grunt.js had backward incompatible release recently. Some older tutorials and documents do not work with last Grunt.js version.

Overview

All the real work is done by the task runner. The command line interface is only able to parse arguments and to feed them to the task runner. It does nothing useful if the task runner is not installed too.

The command line interface should be installed globally and the task runner locally. A global command line interface ensures that the same Grunt commands are available in all directories. The task runner must be local, because various projects may require different Grunt versions.

Installation

Install the global Grunt command line interface:

npm install -g grunt-cli

Go to the project root and let Npm generate a package.json file for you via npm init . It will ask few questions and then generate valid package.json file. Only the name and version are required; you can ignore the rest.

To add the latest Grunt.js version into the package.json as a development dependency and locally install it at the same time enter:

npm install grunt --save-dev

Package.json

The package.json created by running previous commands should look like this:

{ "name": "gruntdemo", "version": "0.0.0", "description": "Demo project using grunt.js.", "main": "src/gruntdemo.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "repository": "", "author": "Meri", "license": "BSD", "devDependencies": { "grunt": "~0.4.1" } }

Configure Grunt.js

Grunt.js runs tasks and most of its work is done by tasks. However, the out of the box Grunt.js installation has no tasks available to it. Tasks have to be loaded from plugins and plugins are usually loaded from Npm.

We will use five plugins:

grunt-contrib-concat – concatenates files together,

– concatenates files together, grunt-contrib-uglify – concatenates and minifies files,

– concatenates and minifies files, grunt-contrib-copy – copies files,

– copies files, grunt-contrib-qunit – runs unit tests,

– runs unit tests, grunt-contrib-jshint – looks for bugs and dangerous constructions in JavaScript code.

This section explains how to configure these plugins. It starts with simplest possible configuration that does nothing and then explains the general steps needed to configure a task. The remaining sub-sections are more practical, each of them explaining how to configure one plugin.

Basic Do Nothing Configuration

Grunt’s configuration is kept either as JavaScript inside the Gruntfile.js file or as CoffeeScript inside a Gruntfile.coffee file. Since we are building JavaScript project, we will use the JavaScript version.

The simplest possible Gruntfile.js file looks like:

//Wrapper function with one parameter module.exports = function(grunt) { // What to do by default. In this case, nothing. grunt.registerTask('default', []); };

The configuration is kept inside a module.exports function. It takes a Grunt object as its parameter and configures it by calling its functions.

The configuration function must create one of more task aliases and associate each of them with list of Grunt tasks. For example, the previous snippet created a “default” task alias and associated it with an empty tasks list. In other words, the default task alias is present, but does nothing.

Use the grunt <taskAlias> command to run all tasks associated with specifiedtaskAlias. The taskAlias argument is optional, Grunt will use the “default” task if it is missing.

Save the Gruntfile.js file, go to the command line and run Grunt:

grunt

You should see following output:

Done, without errors.

Grunt beeps if any configured task returns a warning or an error. If that beeping bothers you, run grunt with -no-color parameter:

grunt -no-color

Grunt Npm Tasks

The general steps needed to add a task from a plugin are the same for all plugins. This section gives a general overview of what is needed, while concrete, practical examples will be provided in following sections.

Install the Plugin

First, we need to add the plugin into the package.json as a development dependency and use Npm to install it:

npm install <plugin name> --save-dev

Configure Tasks

Task configuration must be stored in an object property named after the task and passed to grunt.initConfig method:

module.exports = function(grunt) { grunt.initConfig({ firstTask : { /* ... first task configuration ... */ }, secondTask : { /* ... second task configuration ... */ }, // ... all remaining tasks ... lastTask : { /* ... last task configuration ... */ } }); // ... the rest ... };

The full task configuration possibilities are explained in the Grunt.js documentation. This section describes only the most common, simple case. It assumes that the task takes a list of files, processes them and then generates one output file.

A simple task configuration looks like:

firstTask: { options: { someOption: value //all this depends on plugin }, target: { src: ['src/file1.js', 'src/file2.js'], //input files dest: 'dist/output.js' // output file } }

The example task configuration has two properties. One contains task options and its name must be “options.” Grunt.js does not impose any structure on the options property, its content depends on the plugin.

The other can have any name and contains a task target. Most common tasks operate on and produce files, so their targets have two properties, “src” and “dest.” Src contains list of input files and dest contains output file name.

If you configure multiple targets, Grunt will run the task multiple times – once for each target. The following task will run two times, once for all files in the src directory and once for all files in the test directory:

multipleTargetsTask: { target1: { src: ['src/**/*.js'] }, target2: { src: ['test/**/*.js']] } }

Load and Register Tasks

Finally, tasks from plugins must be loaded with the grunt.loadNpmTasks function and registered with a task alias.

All of this gives us the following Gruntfile.js structure:

module.exports = function(grunt) { grunt.initConfig({ /* ... tasks configuration ... */ }); grunt.loadNpmTasks('grunt-plugin-name'); grunt.registerTask('default', ['firstTask', 'secondTask', ...]); };

Configure JSHint

JSHint detects errors and potential problems in JavaScript code. It was designed to be very configurable and comes with reasonable defaults.

We will use the grunt-contrib-jshint plugin to integrate it with Grunt.js.

Install the Plugin

Open the console and run npm install grunt-contrib-jshint --save-dev from the project root directory. It will add the plugin into the package.json as a development dependency and install it into the local Npm repository.

JSHint Options

The grunt-contrib-jshint plugin passes all options directly to JSHint. The complete list is available on the JSHint documentation page.

The JSHint option “eqeqeq” toggles warnings for == and != equality operators. It is turned off by default, because both operators are legitimate. We will turn it on, because they can lead to unexpected results and the alternative operators === and !== are safer.

We will also turn on trailing options which warns about trailing whitespaces in the code. Trailing whitespaces in multi-line strings can cause weird bugs.

Each option is placed into boolean property with the same name and will be turned on if its value is true. To turn on both eqeqeq and trailing JSHint options use:

options: { eqeqeq: true, trailing: true }

Configure the JSHint Task

The grunt-contrib-jshint plugin has one task named “jshint.” We will configure it to check all JavaScript files in both the src and test directories using the options configuration from previous section.

The JSHint configuration must be placed into an object property named “jshint” and sent to the grunt.initConfig method. It has two properties, one with options and another with target.

The target can be placed inside any property other than options, so we will call it simply “target.” It must contain list of JavaScript files to be checked by JSHint. Files list can be placed inside the target’s src property and supports both ** and * wildcards.

To validate all JavaScript files in all sub-directores of both the src and test directories and turn on two additional JSHint checks use:

grunt.initConfig({ jshint: { options: { eqeqeq: true, trailing: true }, target: { src : ['src/**/*.js', 'test/**/*.js'] } } });

Load and Register

The final part loads grunt-contrib-jshint from Npm and registers the task to the default alias:

grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.registerTask('default', ['jshint']);

Full JSHint Configuration

The full Gruntfile.js with full JSHint configuration should look like:

module.exports = function(grunt) { grunt.initConfig({ jshint: { options: { trailing: true, eqeqeq: true }, target: { src : ['src/**/*.js', 'test/**/*.js'] } } }); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.registerTask('default', ['jshint']); };

Concatenate Files

Our library has to be distributed inside a single file whose name contains both the version number and project name. That file should start with a comment containing the library name, version, license, build date and other similar information.

For example, the version 1.0.2 should be distributed inside file named testGrunt-1.0.2.js and start with the following content:

/*! gruntdemo v1.0.2 - 2013-06-04 * License: BSD */ var gruntdemo = function() { ...

We will configure the concat task using the grunt-contrib-concat plugin to generate the file with the right name and initial comment.

Install the Plugin

Exactly as before, open the console, add the plugin into the package.json as a development dependency and install it into the local Npm repository.

Use the command: npm install grunt-contrib-concat --save-dev

Load Package.json

Our first step is to load the project information from the package.json file and store it in a property. This can be done using the grunt.file.readJSON function:

pkg: grunt.file.readJSON('package.json'),

Pkg now contains an object corresponding to package.json. The project name is stored inside pkg.name property, version is stored inside pkg.version, license is stored inside pkg.license and so on.

Compose Banner and File Name

Grunt provides a template system we can use to compose a banner and file name. Templates are JavaScript expressions embedded into strings using a <%= expression > syntax. Grunt evaluates the expression and replaces the template with the result.

For example, the <%= pkg.name %> template is replaced by value of pkg.name property. If the property is a string, the template becomes an equivalent of the string’s concatenation: ...' + pkg.name + ' ...

Templates can reference all functions and properties defined in the initConfigparameter object and in the Grunt object. The system also provides a few date formatting helper functions. We will use the grunt.template.today(format) function which returns current date in the specified format.

Let’s compose a short banner from the project name, version, license and current date. Since we will need to reuse the banner in the Uglify task, we’ll store it in a variable:

var bannerContent = '/*! <%= pkg.name %> v<%= pkg.version %> - ' + '<%= grunt.template.today("yyyy-mm-dd") %> n' + ' * License: <%= pkg.license %> */n';

The previous template generates following banner:

/*! gruntdemo v0.0.1 - 2013-06-04 * License: BSD */

The project name and version part of filename also need to be used in multiple places. To compose the file name from the project name and version and store it in a variable use:

var name = '<%= pkg.name %>-v<%= pkg.version%>';

This generates following name:

gruntdemo-v0.0.1

Configure Target and Options

The target must contain a list of the files to be concatenated and a name of the file we want to create. Target supports both wildcards and templates, so we can use the template we prepared in previous section:

target : { // concatenate all files in src directory src : ['src/**/*.js'], // place the result into the dist directory, // name variable contains template prepared in // previous section dest : 'distrib/' + name + '.js' }

The Concat plugin looks for a banner in the configuration property named banner and banner is the only configuration property we need. Since the banner content is already composed in the bannerContent variable, all we need is to place it into configuration property:

options: { banner: bannerContent }

Load and Register

The final part loads grunt-contrib-concat from Npm and registers the task to the default alias:

grunt.loadNpmTasks('grunt-contrib-concat'); grunt.registerTask('default', ['jshint', 'concat']);

Full Concat Configuration

This section shows Gruntfile.js with the full concat configuration.

Note that the pkg property is defined inside the initConfig method parameter. We could not place it elsewhere, because it is accessed from templates and templates have access only to the initConfig method parameter and the grunt object.

module.exports = function(grunt) { var bannerContent = '... banner template ...'; var name = '<%= pkg.name %>-v<%= pkg.version%>'; grunt.initConfig({ // pkg is used from templates and therefore // MUST be defined inside initConfig object pkg : grunt.file.readJSON('package.json'), // concat configuration concat: { options: { banner: bannerContent }, target : { src : ['src/**/*.js'], dest : 'distrib/' + name + '.js' } }, jshint: { /* ... jshint configuration ... */ } }); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.registerTask('default', ['jshint', 'concat']); };

Minify

Page loading is slower if the the browser has to load and parse big files. That may not matter for all projects, but it does especially matter for mobile apps running on small devices and big web applications using many big libraries.

Therefore, we are going to also produce a minified version of our library. Minification converts the input file into a smaller one while keeping its functionality unchanged. It removes unimportant whitespaces, shortens constant expressions, gives local variables new shorter names and so on.

Minification is implemented in the grunt-contrib-uglify plugin which integrates UglifyJs into Grunt. Its uglify task concatenates and minifies a set of files.

Source Maps

Minification makes generated files hard to read and very difficult to debug, so we will also generate a source map to make these tasks easier.

Source maps link a minified file to its source files. If it is available, the browser debug tools show original human readable .js files instead of the minified version. Source maps are currently supported only by Chrome and nightly builds of Firefox. You can read more about them on HTML5 rocks or Tutsplus.

Install the Plugin

Add the plugin into package.json as a development dependency and install it into the local Npm repository.

Use the command: npm install grunt-contrib-uglify --save-dev

Configure Target

The uglify task target is configured exactly the same way as the concat task target. It must contain a list of JavaScript files to be minified and the name of the file we want to create.

It support both wildcards and templates, so we can use the template prepared in the previous sub-section:

target : { // use all files in src directory src : ['src/**/*.js'], // place the result into the dist directory, // name variable contains template prepared in // previous sub-chapter dest : 'distrib/' + name + '.min.js' }

Configure Options

The banner is configured exactly the same way as in concat – it is read from the “banner” property and supports templates. Therefore, we can reuse the bannerContentvariable prepared from the previous sub-section.

A source map is generated only if the “sourceMap” property is defined. It should contain the name of the source map file. In addition, we have to fill the “sourceMapUrl” and the “sourceMapRoot” properties. The first contains relative path from uglified file to the source map file and second contains relative path from source map file to sources.

Use the bannerContent variable to generate the banner and the name variable to generate the source map file name:

options: { banner: bannerContent, sourceMapRoot: '../', sourceMap: 'distrib/'+name+'.min.js.map', sourceMapUrl: name+'.min.js.map' }

Load and Register

The final part loads grunt-contrib-uglify from Npm and registers the task to the default alias:

grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.registerTask('default', ['jshint', 'concat', 'uglify']);

Full Uglify Configuration

The following is the Gruntfile.js with a full uglify configuration:

module.exports = function(grunt) { var bannerContent = '... banner template ...'; var name = '-v'; grunt.initConfig({ // pkg must be defined inside initConfig object pkg : grunt.file.readJSON('package.json'), // uglify configuration uglify: { options: { banner: bannerContent, sourceMapRoot: '../', sourceMap: 'distrib/'+name+'.min.js.map', sourceMapUrl: name+'.min.js.map' }, target : { src : ['src/**/*.js'], dest : 'distrib/' + name + '.min.js' } }, concat: { /* ... concat configuration ... */ }, jshint: { /* ... jshint configuration ... */ } }); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.registerTask('default', ['jshint', 'concat', 'uglify']); };

Latest Release File

The latest release of our library is now stored in two files and both of them have the version number in the name. That makes it unnecessary difficult for those who want to automatically download each new version.

They would be forced to read and parse a json file each time they want to find whether a new version was released and what its name is. They would also have to update their download scripts if we decide that we want to change the naming structure.

Therefore, we will use the grunt-contrib-copy plugin to create version-less copies of all the created files.

Install the Plugin

Add the plugin into package.json as a development dependency and install it into the local Npm repository.

Use the command: npm install grunt-contrib-copy --save-dev

Configure the Plugin

Our copy configuration uses three targets, one for each released file. The configuration is without options and basically the same as the configuration of the previous plugins.

The only difference is that we need multiple targets. Each target contains src/dest pairs with name of file to be copied and name of file to be created.

We also made one change to the previous task’s configuration. We took all file names and placed them into variables, so we can reuse them:

module.exports = function(grunt) { /* define filenames */ latest = '<%= pkg.name %>'; name = '<%= pkg.name %>-v<%= pkg.version%>'; devRelease = 'distrib/'+name+'.js'; minRelease = 'distrib/'+name+'.min.js'; sourceMapMin = 'distrib/source-map-'+name+'.min.js'; lDevRelease = 'distrib/'+latest+'.js'; lMinRelease = 'distrib/'+latest+'.min.js'; lSourceMapMin = 'distrib/source-map-'+latest+'.min.js'; grunt.initConfig({ copy: { development: { // copy non-minified release file src: devRelease, dest: lDevRelease }, minified: { // copy minified release file src: minRelease, dest: lMinRelease }, smMinified: { // source map of minified release file src: sourceMapMin, dest: lSourceMapMin } }, uglify: { /* ... uglify configuration ... */ }, concat: { /* ... concat configuration ... */ }, jshint: { /* ... jshint configuration ... */ } }); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-copy'); grunt.registerTask('default', ['jshint', 'concat', 'uglify', 'copy']);

Unit Tests

Finally, we will configure grunt.js to run unit tests on our newly released files. We will use the grunt-contrib-qunit plugin for that purpose. The plugin runs QUnit unit tests in a headless PhantomJS instance.

This solution does not account for browser differences and bugs, but it is good enough for our purposes. Those who want to have a better configuration can use js-test-driver or other similar tool. However, theJs-test-driver configuration is out of the scope of this article.

Prepare Tests

Qunit unit tests often run over JavaScript files in src directory because that is practical during the development. If you want to test whether our just released, concatenated and minified versions work as well, we need to create new QUnit HTML file and load the latest release from it.

Below is an example Qunit entry point file:

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>QUnit Example</title> <link rel="stylesheet" href="../libs/qunit/qunit.css"> </head> <body> <div id="qunit"></div> <div id="qunit-fixture"></div> <script src="../libs/qunit/qunit.js"></script> <!-- Use latest versionless copy of current release --> <script src="../distrib/gruntdemo.min.js"></script> <script src="tests.js"></script> </body> </html>

Install the Plugin

Add the plugin into package.json as a development dependency and install it into the local Npm repository.

Use the command: npm install grunt-contrib-qunit --save-dev

Configure Plugin

The grunt-contrib-qunit configuration is the same as the configuration of the previous tasks. Since we are fine with the default QUnit configuration, we can omit the options property. All we have to do is to configure the target which must specify all QUnit HTML files.

Below we specify all HTML files in the test directory and its sub-directories should be run as QUnit tests:

grunt.initConfig({ qunit:{ target: { src: ['test/**/*.html'] } }, // ... all previous tasks ... });

Final Grunt.js File

Below is the complete Gruntfile.js configuration:

module.exports = function(grunt) { var name, latest, bannerContent, devRelease, minRelease, sourceMap, sourceMapUrl, lDevRelease, lMinRelease, lSourceMapMin; latest = '<%= pkg.name %>'; name = '<%= pkg.name %>-v<%= pkg.version%>'; bannerContent = '/*! <%= pkg.name %> v<%= pkg.version %> - ' + '<%= grunt.template.today("yyyy-mm-dd") %> n' + ' * License: <%= pkg.license %> */n'; devRelease = 'distrib/'+name+'.js'; minRelease = 'distrib/'+name+'.min.js'; sourceMapMin = 'distrib/'+name+'.min.js.map'; sourceMapUrl = name+'.min.js.map'; lDevRelease = 'distrib/'+latest+'.js'; lMinRelease = 'distrib/'+latest+'.min.js'; lSourceMapMin = 'distrib/'+latest+'.min.js.map'; grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), qunit:{ target: { src: ['test/**/*.html'] } }, // configure copy task copy: { development: { src: devRelease, dest: lDevRelease }, minified: { src: minRelease, dest: lMinRelease }, smMinified: { src: sourceMapMin, dest: lSourceMapMin } }, // configure uglify task uglify:{ options: { banner: bannerContent, sourceMapRoot: '../', sourceMap: sourceMapMin, sourceMappingURL: sourceMapUrl }, target: { src: ['src/**/*.js'], dest: minRelease } }, // configure concat task concat: { options: { banner: bannerContent }, target: { src: ['src/**/*.js'], dest: devRelease } }, // configure jshint task jshint: { options: { trailing: true, eqeqeq: true }, target: { src: ['src/**/*.js', 'test/**/*.js'] } } }); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.registerTask('default', ['jshint', 'concat', 'uglify', 'copy', 'qunit']); };

Conclusion

Grunt.js is now configured and ready to be used. Our targets are configured in the most simple possible way, using src/dest pairs, wildcards and templates. Of course, Grunt.js also provides other, more advanced options.

It would be even better, if it would be possible to automatically download and manage the libraries our project depends on. I found two possible solutions, Bower and Ender. I have not tested them yet, but both manage front-end JavaScript packages and their dependencies for the web.

This article was originally published at http://meri-stuff.blogspot.sk/2013/06/building-javascript-library-with-gruntjs.html