What are build tools and how do they automate your work?
Build Tools Overview
In the world of front-end software development, there are a lot of menial tasks, which have to be done and consume a lot of our time. These include minifying and concatenating files, linting, transpiling, documentation building, etc. Now, if we were to do every single one of these tasks manually we would lose an enormous amount of time. Fortunately, we have handy build tools that automate these actions, giving us the chance to focus on more challenging tasks. To illustrate the point, let’s have a look at some of the most popular automation tools out there, namely NPM scripts, Grunt and Gulp.
NPM Scripts
NPM is a package manager that gives the ability to install Node.js packages – pieces of code that do some task. What is more, since most of these packages have a command line interface, NPM scripts offer us the ability to give them aliases and reuse them over and over again. There are many packages available in NPM, therefore you will most probably find one that deals with the problem you are facing. There are great examples of how to use NPM as a build tool.
Here are some useful links on the matter:
Why npm Scripts?
How to Use npm as a Build Tool
Give Grunt the Boot! A Guide to Using npm as a Build Tool
Keep in mind that Windows user may have a hard time with most of the commands, since they cannot use globs or other handy UNIX goodies. What is more, one has to be really comfortable with shell scripting since the npm error messages are not really helpful. Last but not least, NPM is IMO harder to read and write in a bigger project, where the tasks are longer, harder and rather confusing.
Grunt
Grunt is a popular build tool, which has a big ecosystem of plugins that can do pretty much everything. It is built on top of Node.js, therefore, it is both a npm package and uses npm to install its plugins. Grunt’s plugins are multi purpose and each one of them can perform various tasks. The community-made plugins are named grunt-[package-name], while the officially maintained ones are registered as grunt-contrib-[package-name].
To start working with Grunt, you have to install its command line interface. Afterward, install some Grunt plugin and configure a task for it in a file named Gruntfile.js. Finally, just execute the task from the command line.
However, Grunt has a few disadvantages, the first of them being that it uses a configuration over code approach which IMO is harder to understand than the alternative. Secondly, Grunt’s tasks are lengthy to configure, therefore, as the project grows the configuration gets harder to manage. Last, but definitely not least, Grunt reads and writes temporary files when performing tasks which is slower than Gulp.
Gulp
The most popular build tool, according to this 2016 survey is Gulp. It is also built with Node.js and is a npm package. What makes Gulp better than Grunt is the fact that it uses a code over configuration approach which means that configuring your automatizations is basically writing JavaScript functions. What is more, Gulp uses Node.js streams and pipes commands simultaneously, which means that all file changes happen in memory and are, therefore, faster than using temporary files. Furthermore, the plugins of Gulp are almost exclusively single purpose and the creators of the tool rigorously whitelist plugins which display good behavior, while blacklisting others that fail to do so.
Build Tasks Implementation
For the purpose of this tutorial, we are going to implement an example build system using Gulp. There are going to be tasks which lint JavaScript, transpile CoffeeScript, watch for file changes and execute other tasks on such. Finally, there is going to be a default task that will recursively lint all JavaScript files in your current directory.
Prerequisites
Files
To begin with, create a file called script1.js, navigate to it in the CMD and fill it with:
1 |
console.log('Jshint error code') |
Next, create a second file called script_coffee.coffee and fill it with:
1 2 3 |
isCoffeeScript = true alert "I am coffee script!" if isCoffeeScript |
Note: Gulp commands are executed from the directory named gulpfile.js, therefore for the examples to work create the gulpfile.js in this new folder or fix the paths in the tasks.
Gulp
Now, let’s install Gulps command-line interface globally so that we can use it from anywhere in our file system. Make sure that you have Node.js and npm installed, then in the CMD run:
1 |
npm install -g gulp-cli |
This gives us access to the gulp shell command and some useful goodies like the process.env.INIT_CWD, which we will see later on.
Afterwards, let’s add a package.json file – responsible for keeping track of all the dependencies for our project. What is more, when we create and populate this file, every time that you or another developer downloads the project, with just a simple
1 |
npm install |
all of the listed dependencies in the file, will be installed. Luckily, NPM has a built in command that creates the file and fills it easily. Run
1 |
npm init |
and either respond to every prompt or just press enter a couple of times to create a default package.json.
Subsequently, let’s install Gulp itself and list it as a dependency.
1 |
npm install --save-dev gulp |
The –save-dev and -D flags instruct npm to save the package as a devDependency. After this command, a node_modules folder should have been created, which holds all of the code that comes from the NPM packages. To verify that you have gulp, run a quick version check.
1 |
gulp -v |
Tasks configuration
In order to configure and run Gulp tasks we have to create a gulpfile.js, therefore, create one in your project’s folder and fill it with:
1 |
var gulp = require('gulp'); |
This line of code uses Node.js’s requiring to use gulp’s code from the node_modules folder. In our case, the gulp package gives us a couple of built in functions and variables.
To automate our tasks we are always going to:
- Install a package and save it as a dependency.
- Create a variable that requires the plugin.
- Setup a Gulp task.
All of the following methods from this task are built into Gulp:
- gulp.task(name, dependency, action) – creates a task that can be called from the command line.
- gulp.src(input)– loads the input files.
- gulp.dest(output) – writes the transformed files into output.
- .pipe(command) – is used to receive input from the last task, perform an action on it and if there is output, send it to the next task.
Transpiling
Firstly, let’s create a task that transpiles CoffeeScript into JavaScript. To do so install gulp-coffee with the command.
1 |
npm install -D gulp-coffee |
Next, in our gulpfile create a variable named coffee and require the plugin.
1 2 |
// Previous variables var coffee = require('gulp-coffee'); |
After that, in the gulpfile add:
1 2 3 4 5 6 7 8 |
// Previous code gulp.task('coffee', function () { return gulp.src('*.coffee') .pipe(coffee({ bare: true })) .pipe(gulp.dest('./')); }); |
This piece of code creates a task named coffee which, on execution, invokes a callback function that takes all of the .coffee files from the project’s folder and uses the coffee plugin to transpile them. Lastly, all of the transformed files are outputted to the root folder.
Furthermore, what makes Gulp fast is that through Node streams the input is loaded into the memory. All of the file operations are also performed in memory and therefore, the output files are created only once, after a gulp.dest execution.
Note: Many Gulp plugins send objects as arguments to set certain options. In our case, the line
1 |
.pipe(coffee({bare: true})) |
calls the coffee plugin and sets the bare option to true.
Just like that, we are done with the task! Now, let’s save the file and start using it.
1 |
gulp coffee |
When you executed this command from anywhere within our project, you will see a new script_coffee.js file appear.
Note: The names of Gulp tasks can be whatever we want them to be, so adding :target to them is one of the ways to have different tasks for different project targets.
Lastly, the task starts with a return statement, because returning a stream is one of the ways to make a task asynchronous, which informs Gulp about when a task finishes, therefore, we can schedule sequential execution of tasks.
Linting
For the second task in our build system, we are going to install gulp-jshint and its prerequisite jshint.
To install jshint and gulp-jshint run:
1 2 |
npm install -D jshint npm install -D gulp-jshint |
Now, that we have the necessary plugins installed and listed as dependencies, open the gulpfile and add the following:
1 2 3 4 5 6 7 8 9 |
// Previous variables var jshint = require('gulp-jshint'); // Previous tasks gulp.task('lint:js', ['coffee'], function () { return gulp.src('*.js') .pipe(jshint()) .pipe(jshint.reporter('default')); }); |
This task uses jshint and its default reporter to scan JavaScript files, and output any style errors to the console. Since there are some style mistakes, we are going to see some error messages.
The second argument in the lint:js task is interesting.
1 2 3 4 5 |
gulp.task('lint:js', ['coffee'], function () { return gulp.src('*.js') .pipe(jshint()) .pipe(jshint.reporter('default')); }); |
It sets coffee as a dependency task for lint:js, so that any time we lint the transpilation task will be executed first.
Note: If the dependency tasks are not async, Gulp will run all tasks in parallel which might produce strange outcomes. For example, if a cleanup task is used.
Watch
A useful task would be one that performs an action every time a file changes. This can be beneficial for rapid development because we can see code errors immediately and prevent them from piling up. Luckily, Gulp has a built in functionality for this.
1 2 3 4 |
// Previous code gulp.task('watch', function () { gulp.watch(['*.js', '*.coffee'], ['lint:js']); }); |
Note: Since we don’t know when this task is going to end, it will not be async.
The built in gulp.watch(files-to-follow, action) watches for changes to the files-to-follow and when they occur executes action. For the parameters of this command, we can use both a single statement or an array of statements.
In our case, the task follows all JavaScript and CoffeeScript files in the folder and on change executes the linting task. Now, since lint:js first calls coffee if a CoffeeScript file is changed, it will first be transpiled and then linted.
To see the result, execute the task
1 |
gulp watch |
and open any .js or .coffee file in the project, add a white line at its end and save it. You should see the task that is automatically performed.
Default task
Gulp offers us the ability to create a default task, surprisingly named default, which can be run with either
1 2 |
gulp gulp default |
As we mentioned, Gulp tasks are executed from the directory of the gulpfile.js and since every Gulp task can be configured to your liking, we will show you how to use your current location in the file system to determine which files get affected by your commands.
1 2 3 4 5 6 7 |
// Previous tasks gulp.task('default', function () { process.chdir(process.env.INIT_CWD); return gulp.src('**/*.js') .pipe(jshint()) .pipe(jshint.reporter('default')); }); |
Since the gulpfile is a JavaScript file, we can execute some code before performing the actual task and since Gulp is built on Node some functionality can be used from there. Now, Gulp commands are executed from the directory where the gulpfile is. Therefore, the first line of our tasks uses Node’s process.chdir to change the working directory to the initial directory from where the command was called with the process.env.INIT_CWD variable which comes with Gulp. Afterwards, we just take all JavaScript files recursively and lint them with JSHint. The way we use this command is by navigating to the directory where our files are and then executing the default task.
To illustrate the example, move the script1.js file to a new folder in your project and lint. We will see no errors, since the file is no longer in the same folder as the gulpfile. From the CMD navigate to the new folder and run the same command again. Still, no errors, because Gulp always runs the task from the location of the gulpfile. Run the default task and you should see the result.
With this task, we have completed our example build system.
Additional Plugins
The following plugins may be useful and are worth checking out:
Additional resources
Check out the following links to read more on the matter:
An Introduction to Gulp.js
Choose: Grunt, Gulp, or npm?
NPM vs. Bower vs. Browserify vs. Gulp vs. Grunt vs. Webpack
Grunt vs Gulp – Beyond the Numbers
Gulp vs Grunt – Comparing Both Automation Tools
My 50 cents about the Webpack vs Gulp vs Grunt vs npm scripts discussion
Grunt Tutorial
Grunt Getting Started