Times have changed a lot since 2015 and while a lot of the gulp patterns in this post are still valid the tooling landscape is quite different. Today we leverage gulp as a "general purpose automation" tool that's well suited to projects with complex and unique build requirements. For web application frameworks in most cases we recommend your respective framework's CLI (every big framework has one) or a tool like webpack.

Over the past 2 years, Gulp has established itself as an important piece of most modern web applications. As early adopters of this life changing technology at Rangle, we've encountered our share of pitfalls which guided us to valuable lessons about sublteties that can make a world of difference. Today, we'd like to share some of the learned insights with you.

1. Automate all Imports

The number of files in a project grows daily, and so do the places these files get imported in. Under an ES5/Angular 1.x environment normally all files are included inside an index.html file, however when working with a more complex environment which includes testing, coverage reports, and automated builds, it pays off to automate the imports, and even try to unify them into a few generic importing tasks that can be reused in multiple workflows.

Gulp has quite a few great plugins for dealing with such automated imports, gulp-inject, wiredep, useref, and angular-file-sort being a few good examples, however initial configuration could be challenging.

2. Understand directory structure requirements

A well thought out directory structure can go a long way in preventing issues that can occur in the era of precompiled languages, transpilers and multiple environment modes. Due to this we understand that:

  • Our source code is not the same code (and most of the time not in the same programming language) that's being served to the browser.

  • Our source code is served minified and concatenated in production, but this can be confusing for development.

Here is an example directory structure in which source code (src) is separated from temporary precompiled assets (.tmp), which are separated from the final distribution folder (dist). The src folder contains higher level languages such as jade, typescript and scss; the .tmp contains compiled js, css and html files; while the dist folder contains only concatenated and minified files optimized to be served in production.

├── .tmp
│   ├── app.css
│   ├── app.js
│   ├── header.html
│   └── index.html
├── bower_components
│   └── angular
├── dist
│   ├── app.min.css
│   ├── app.min.js
│   └── index.html
└── src
    ├── app.scss
    ├── app.ts
    ├── components
    ├── header.jade
    ├── index.html
    └── shared

3. Provide distinct development and production builds

With the right directory structure, injection automation and build automation in place, we can use browser-sync, the tool recommended by the gulp team, to serve either a production or a development version of our app.

function browserSyncInit(baseDir, files) {
  browserSync.instance = browserSync.init(files, {
    startPath: '/', server: { baseDir: baseDir }

// starts a development server
// runs preprocessor tasks before, 
// and serves the src and .tmp folders
	['typescript', 'jade', 'sass', 'inject' ], 
    function () {
  ], [
    paths.tmp + '/**/*.css',
    paths.tmp + '/**/*.js',
    paths.tmp + '/**/*.html'

// starts a production server
// runs the build task before, 
// and serves the dist folder
gulp.task('serve:dist', ['build'], function () {

Now we can run our development server with gulp serve and simulate a production build with gulp serve:dist.

4. Inject files with gulp-inject and wiredep

Between gulp-inject and wiredep there is no excuse to ever have to manually inject a file into index.html. gulp-inject does a great job at selecting all your javascript and css files, while wiredep assists with selecting the right files from any installed bower packages.

gulp.task('inject', function () {

  var injectStyles = gulp.src([
      // selects all css files from the .tmp dir
      paths.tmp + '/**/*.css'
    ], { read: false }

  var injectScripts = gulp.src([
    // selects all js files from .tmp dir
    paths.tmp + '/**/*.js',
    // but ignores test files
    '!' + paths.src + '/**/*.test.js' 
    // then uses the gulp-angular-filesort plugin
    // to order the file injection
  		.on('error', $.util.log));
  // tell wiredep where your bower_components are
  var wiredepOptions = { 
    directory: 'bower_components'

  return gulp.src(paths.src + '/*.html')
    .pipe($.inject(injectStyles, injectOptions))
    .pipe($.inject(injectScripts, injectOptions))
    // write the injections to the .tmp/index.html file
    // so that src/index.html file isn't modified  
    // with every commit by automatic injects


5. Create production builds with gulp-useref

gulp-useref is a hidden gem within the gulp ecosystem. It's a wonderful plugin which reads all the files included in a html file, then concatenates and minifies them, returning and including a single file.

That means that during your gulp build process you can use gulp-useref to turn your index.html imports from this:

    <!-- build:js scripts/minified.js -->
    <script type="text/javascript" src="scripts/one.js"></script> 
    <script type="text/javascript" src="scripts/deux.js"></script> 
    <script type="text/javascript" src="scripts/drie.js"></script> 
    <!-- endbuild -->

to this:

<body><script src="scripts/minified.js"></script></body>

with relative ease:

  return gulp.src('src/index.html')

6. Separate Gulp tasks into multiple files

As the project grows, so does the number of gulp tasks. This can leave you with a very large and messy gulpfile.js. A usefull trick is to split the file into multiple files inside a directory, then use require-dir to require all the task files within gulpfile.js.

A gulp structure we've been using is :

├── build.js
├── server.js
├── tdd.js
└── deploy.js

Now we can separate task concerns across multiple files, by adding the following to the bottom of our gulpfile.js (after we've done: npm install --save-dev require-dir):