Modern Frontend Workflow in a .NET World Part 5: Gulp

Originally posted on the DCS Innovation Labs Blog.

All code for this post is available on github.

Over the past few blog posts we've looked at some tools that I believe will make your process as a frontend developer more efficient and more enjoyable. However, in our quest to simplify our lives, we've added new complications in the way of tools. Having Babel, Sass, and an HTTP server open all while keeping a terminal available for version control leads to a screen cluttered with terminals just to keep your website running and updated. And forget about the rest of your team being happy with all of your new changes. Somewhat counter-intuitively, we are going to introduce another tool, gulp, to once again simplify our process. At the end of this, we'll be able to move files, run Sass, run Babble, and serve our app with a single command, gulp. We'll look at:

  1. Installing Gulp
  2. Project setup
  3. Intro to Gulp tasks
  4. Using Gulp to move files
  5. Using Gulp to run Sass
  6. Using Gulp to run Babel
  7. Using Gulp to serve
  8. Watching for changes and composing tasks

Before we jump in, make you that you have NPM installed. If you aren't familiar with it, checkout my post here on installing and using NPM. I'm also going to assume that you already have an understanding of Sass and Babel. If not, check out my posts on installing and using Sass and installing and using Babel.

For this post I'm going to be using PowerShell on Windows but the same steps apply on Bash, ZSH, and pretty much any other Bash-like shell.

1) Installing Gulp

Similar to Sass and Babel, we will be installing a command line tool. Go ahead and run npm install -g gulp:

C:\Projects\gulp_sample_app [master]> npm install -g gulp  
npm WARN deprecated [email protected]: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue  
npm WARN deprecated [email protected]: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue  
npm WARN deprecated [email protected]: [email protected]<3.0.0 is no longer maintained. Upgrade to [email protected]^4.0.0.  
npm WARN deprecated [email protected]: graceful-fs v3.0.0 and before will fail on node releases >= v7.0. Please update to [email protected]^4.0.0 as soon as possible. Use 'npm ls graceful-fs' to find it in the tree.  
C:\Program Files\nodejs\gulp -> C:\Program Files\nodejs\node_modules\gulp\bin\gulp.js  
C:\Program Files\nodejs

...

C:\Projects\gulp_sample_app [master]>  

Gulp should now be available as a command line executable (restart your shell if it isn't).

2) Project setup

The best way to show off Gulp is by implementing it in a project. For this post, I'm going to continue building on the sample app I created for Modern Frontend Workflow in a .NET World Part 3: Sass, with a few modifications and additions. If you want to follow along, you can grab the code I'll be starting with here on github.

Starting off, my project structure looks like this:

    Directory: C:\Projects\gulp_sample_app


Mode                LastWriteTime         Length Name  
----                -------------         ------ ----
d-----        8/25/2016  10:53 AM                bin  
d-----        8/25/2016  11:03 AM                src  
-a----        8/25/2016  10:21 AM           1117 .gitignore
-a----        8/25/2016  10:25 AM            357 README.md


    Directory: C:\Projects\gulp_sample_app\src


Mode                LastWriteTime         Length Name  
----                -------------         ------ ----
-a----        8/25/2016  11:08 AM            267 app.js
-a----        8/25/2016  11:07 AM           3485 index.html
-a----        8/25/2016  11:01 AM           1634 styles.scss

Instead of keeping any actual source code at the root, I put it in the src directory. Another thing to notice is that the bin directory is currently empty. This is because, once I set up Gulp, my source code will live in src and my compiled app will live in bin. Of course you don't have to set up your project this way, but I've been using this pattern for a while and found that it works very well for my purposes.

To get started with Gulp, we'll run npm init and install Gulp as a dev dependency with npm install --save-dev gulp. We've already installed Gulp as a global dependency to make use of the CLI, but we also will need to make use of the node module in our build script.

The last thing we need to do to get setup is to create a gulpfile.js in our project root. The name here is important as the Gulp CLI looks for gulpfile.js when we run the gulp command.

While going through the steps of creating these steps, I'll just be posting snippets of code. It may be helpful to follow along with the completed gulpfile.js here on github.

3) Intro to Gulp tasks

To perform work, Gulp uses tasks. Let's go ahead and test one out so you can see how they work.

The first thing we are going to need to do is import the gulp module into our gulpfile.js. At the heart of it, gulpfile.js is just a JavaScript file that we are executing using Node JS with some conventions around it. So if you have ever used Node before, you'll feel right at home. If not, no worries, we won't get into anything too complicated.

To import the gulp module, we'll just require it:

var gulp = require('gulp')  

Now let's create a task that will log something to the console:

var gulp = require('gulp')

gulp.task('test', function () {  
  console.log('Gulp is working!')
})

We can run a specific Gulp task using the Gulp CLI command and providing the name of the task we want to run.

Go ahead and run gulp test:

C:\Projects\gulp_sample_app [master ≡]> gulp test  
[11:41:33] Using gulpfile C:\Projects\gulp_sample_app\gulpfile.js
[11:41:33] Starting 'test'...
Gulp is working!  
[11:41:33] Finished 'test' after 301 μs
C:\Projects\gulp_sample_app [master ≡]>  

And there you have your first Gulp task!

4) Using Gulp to move files

As I said earlier regarding our project structure, src is where the source code will live, and bin is where the compiled, runable app will live. The first glaring thing we need to do is make sure index.html ends up in bin. Since there is no processing to do on our HTML, we can just move the file right over.

To do this, we need to understand a couple of Gulp concepts, at least at a high level. The first of these is the pipe() function. Gulp reads in files, performs processing on them in memory, and then spits the new files out. The way we pass the in memory files along is using pipe(). Don't worry if that's not clear here, it'll make sense once you see it in action. We also need to know gulp.src(). This function is what initially reads in the file you provide it. So gulp.src('./index.html') will read index.html into memory. Finally, we have gulp.dest(). This spits out the files it has in memory into the location you provide it. gulp.dest('./bin') will drop the file in memory into the bin directory.

If we put this all together we get:

gulp.task('buildMarkup', function () {  
  return gulp.src('./src/*.html')
    .pipe(gulp.dest('./bin'))
})

This takes any file in src with the .html extension, and copies it over to the bin directory.

Let's run this with gulp buildMarkup:

C:\Projects\gulp_sample_app [master ≡]> gulp buildMarkup  
[11:53:49] Using gulpfile C:\Projects\gulp_sample_app\gulpfile.js
[11:53:49] Starting 'buildMarkup'...
[11:53:49] Finished 'buildMarkup' after 34 ms
C:\Projects\gulp_sample_app [master ≡]>  

If we look at our project, we see that we now have an exact copy of src/index.html as bin/index.html.

5) Using Gulp to run Sass

Gulp can't do a ton on its own, but the real power comes from its plugin ecosystem.

Gulp plugins are just NPM modules, so we will get them from NPM. Gulp has a curated list of plugins over at gulpjs.com/plugins.

The plugin we will use to compile our Sass is gulp-sass, so let's get it with npm install --save-dev gulp-sass.

We can then use it by requiring it with var sass = require('gulp-sass').

Our task will look like this:

gulp.task('buildSass', function () {  
  return gulp.src('./src/*.scss')
    .pipe(sass().on('error', sass.logError))
    .pipe(gulp.dest('./bin'))
})

This works much the same as our buildMarkup task with one more step in the chain. We use pipe(sass().on('error', sass.logError)) to compile our files in memory, and handle any errors that occur while compiling such as having a syntax error in our .scss files.

6) Using Gulp to run Babel

This is very similar to installing and using gulp-sass with the difference that we need to install our Gulp preset like we did back in my Modern Frontend Workflow in a .NET World Part 4: ECMAScript 2015 and Babel post. So let's run npm install --save-dev gulp-babel babel-preset-es2015 to install gulp-babel and our ECMAScript 2015 preset.

As above, we need to require gulp-babel using var babel = require('gulp-babel'), but we don't need to require babel-preset-es2015 as babel will look for that on its own.

Let's create our buildJs task:

gulp.task('buildJs', function () {  
  return gulp.src('./src/*.js')
    .pipe(babel({
      presets: ['es2015']
    }))
    .pipe(gulp.dest('./bin'))
})

And now we can run gulp buildJs to transpile our ECMAScript 2015 code.

7) Using Gulp to serve

The last piece of this puzzle is serving our app. We could have a second terminal open to run the Node http-server, but, we can handle this more elegantly in Gulp. We're going to install gulp-connect to serve our app, and gulp-open to open it in our default web browser with npm install --save-dev gulp-connect gulp-open.

As above, we'll require these two Node modules:

var connect = require('gulp-connect')  
var open = require('gulp-open')  

And here's our task:

gulp.task('serve', function () {  
  connect.server({
    devBaseUrl: 'http://localhost',
    port: 8080,
    root: './bin'
  })

  return gulp.src('./bin/index.html')
    .pipe(open({ uri: 'http://localhost:8080' }))
})

This is basically telling gulp-connect to serve our app rooted in ./bin to http://localhost:8080, and telling gulp-open to open that same url in our default browser.

8) Watching for changes and composing tasks

This is all fine and dandy, but we really haven't made any progress yet. We basically took our individual command line apps we were using before, and throwing them behind another tool. The benefit comes in to play when we start watching our files for changes, automatically running the Gulp tasks when they change, and composing our multiple tasks into one easy to run task.

To watch a file or files and run tasks when changes occur, we simply have to call gulp.watch('<filename>' ['<task1>', '<task2>']). Let's make a watch task:

gulp.task('watch', function () {  
  gulp.watch('./src/*.html', ['buildMarkup'])
  gulp.watch('./src/*.scss', ['buildSass'])
  gulp.watch('./src/*.js', ['buildJs'])
})

Now, if you run gulp watch, Gulp will stay open, monitor your files for changes, and automatically run their build tasks.

The key to making this all seamless is task composition. We can create a task in Gulp that is made up of several other tasks.

If I want a task to run all of my build steps, I just compose them into one new build task:

gulp.task('build', ['buildMarkup', 'buildSass', 'buildJs'])  

When I run gulp build, it's going to actually run buildMarkup, buildSass, and buildJs.

The last task I'm going to create is the default task. This is special because it lets me simply run gulp in the command line to execute it, taking all of the complexity out of the process.

I'm going to compose my build, watch, and serve tasks to automate my entire build process and serve my app:

gulp.task('default', ['build', 'watch', 'serve'])  

This is the final change we'll make to gulpfile.js, leaving us with:

var gulp = require('gulp')  
var babel = require('gulp-babel')  
var connect = require('gulp-connect')  
var open = require('gulp-open')  
var sass = require('gulp-sass')

gulp.task('buildMarkup', function () {  
  return gulp.src('./src/*.html')
    .pipe(gulp.dest('./bin'))
})

gulp.task('buildSass', function () {  
  return gulp.src('./src/*.scss')
    .pipe(sass().on('error', sass.logError))
    .pipe(gulp.dest('./bin'))
})

gulp.task('buildJs', function () {  
  return gulp.src('./src/*.js')
    .pipe(babel({
      presets: ['es2015']
    }))
    .pipe(gulp.dest('./bin'))
})

gulp.task('serve', function () {  
  connect.server({
    devBaseUrl: 'http://localhost',
    port: 8080,
    root: './bin'
  })

  return gulp.src('./bin/index.html')
    .pipe(open({ uri: 'http://localhost:8080' }))
})

gulp.task('watch', function () {  
  gulp.watch('./src/*.html', ['buildMarkup'])
  gulp.watch('./src/*.scss', ['buildSass'])
  gulp.watch('./src/*.js', ['buildJs'])
})

gulp.task('build', ['buildMarkup', 'buildSass', 'buildJs'])

gulp.task('default', ['build', 'watch', 'serve'])  

Now, I can run my entire development environment just by running gulp in my command line.

Wrapping up

We can introduce all of the cool modern development practices we want to, but if we have to start managing a ton of tools, we aren't really saving any time. Gulp allows us to abstract all of those tools down to a single command, validating the use of these new practices. Yes, it does require more boilerplate to get your project up and running, but that's a pretty small price to pay when you see the savings it can bring you over time.

I've only shown you a small taste of what Gulp is capable of. Looking through their plugin library will expose you to the thousands of other tools that available for you to use such as minification and source maps.

There's one more big issue to cover in this series and that's how to integrate all of this tooling in the .NET ecosystem. I haven't covered this as much as I was was originally planning to in this and previous posts, but my next post will be committed entirely to this. Having all of these cool tools is meaningless if they get in the way of the rest of your team and they don't integrate into your workflow. Luckily, Microsoft is very committed to these new processes and has made integrating them simple and elegant.