Gulp is a JavaScript task runner. I prefer it over Grunt because I prefer code over configuration. In this blog post, I will show you how to use Gulp as a build automation tool for large Ionic applications. But we will only cover the development build pipeline. But don’t worry ! In a next post we will cover the production build pipeline.

Initial setup

We will be using the ionic "tabs starter" template so check it out.

Let’s start by setting up the directory structure according to best practices. See part 1 for more information.

  1. Create src folder containing app and assets
  2. Copy the content of the www/img folder in src/assets/images
  3. Copy the remains files in src/app
  4. Open the file src/app/js/services.js and correct the paths to the images i.e replace img/X.jpg by ../assets/images/X.jpg for every X in app/images (search & replace all img/ by ../assets/images/)
  5. Edit the .bowerrc file : set the directory to bower_components instead of www/lib. The libraries must reside outside the src folder because we won’t add them to our VCS. Sure we can exclude them using a .gitignore file but why put them in the src folder in the first place ?

The development build pipeline consists of serving our code and previewing it the browser. That’s exactly what the gulp serve task does.

Development build pipeline

Dependency tree representing our development build pipeline

This figure shows all our tasks and their dependencies in a dependency tree. Our goal is to implement the root task which is gulp serve.

Gulp serve

gulp.task('serve', ['dev'], function () {
    gulp.start('browser-sync');
});

This task serves all the files in the .tmp folder using browser-sync plugin. It depends on the dev task.

Gulp browser-sync

gulp.task('browser-sync', ['watch'], function () {
    browserSync.init(paths.tmp + '**/*', {
        server: {
            baseDir: paths.tmp
        }
    });
});

// just to run browserSync.reload in sequence (see watch-scripts)
gulp.task('browserSync.reload', function () {
    browserSync.reload();
});

This task serves the .tmp folder. browser-sync will reload the browser if a change in source code is detected. This is achieved in the gulp watch task.

Gulp watch

gulp.task('watch', ['watch-html', 'watch-styles', 'watch-scripts']);

gulp.task('watch-styles', function () {
    gulp.watch([
        path.join(paths.src, '/app/**/*.scss')
    ], function (event) {
        if (isOnlyChange(event)) {
            sequence('styles', 'browserSync.reload');
        } else {
            sequence('inject-styles', 'browserSync.reload');
        }
    });
});
gulp.task('watch-scripts', function () {
    gulp.watch([
        path.join(paths.src, '/app/**/*.js')
    ], function (event) {
        console.log(event);
        if (isOnlyChange(event)) {
            sequence('scripts', 'browserSync.reload');
        } else {
            sequence('inject-scripts', 'browserSync.reload');
        }
    });
});

gulp.task('watch-html', function () {
    gulp.watch([path.join(paths.src, '/app/**/*.html')], function () {
        sequence('html', 'browserSync.reload');
    });
    gulp.watch(path.join(paths.src, 'index.html'), function () {
        del(paths.tmp + '/index.html');
        sequence('inject', 'browserSync.reload');
    })
});
function isOnlyChange(event) {
    return event.type === 'changed';
}

Gulp dev:

This task copies all html templates, images, libraries (bower components), styles to the .tmp folder without any optimization. To achieve that, we use gulp html, gulp images, gulp bower and gulp inject tasks as it’s dependencies.

gulp.task('dev', ['html', 'images', 'bower', 'inject']);

A. Gulp html:

This task copies all the html files from the src folder to the .tmp folder after having freed the .tmp folder from any .html file.

gulp.task('html', ['clean-html'], function () {
    config.log('Copying html files');
    return gulp
        .src(paths.src + '/app/**/*.html')
        .pipe(gulp.dest(path.join(paths.tmp, '/app')));
});

gulp.task('clean-html', function () {
    config.log('Cleaning html');
    return del(path.join(paths.tmp, 'app/**/*.html'));
});

B. Gulp images:

This task copies all the images from the src/assets/images folder to the .tmp folder after having deleted the .tmp/assets/images folder.

gulp.task('images', ['clean-images'], function () {
    return images(false);
});

gulp.task('clean-images', function () {
    config.log('Cleaning images');
    var devImages = path.join(paths.tmp, '/assets/images');
    var buildImages = path.join(paths.dist, '/images');
    return del([devImages, buildImages]);
});

function images(isProduction) {
    var targetDir = paths.tmp + '/assets/images/';
    if (isProduction) {
        targetDir = paths.dist + '/images/';
    }
    config.log('Copying images');
    return gulp.src(paths.src + '/assets/images/**/*')
        .pipe(gulpif(isProduction, imagemin({optimizationLevel: 4})))
        .pipe(gulp.dest(targetDir));
}

C. Gulp bower:

This task copies the bower_components directory to the .tmp folder after having deleted the previous .tmp/bower_components/ folder.

gulp.task('bower', ['clean-bower'], function () {
    config.log('Copying vendor libs');
    return gulp
        .src(path.join('bower_components', '/**/*'))
        .pipe(gulp.dest(path.join(paths.tmp, '/bower_components')));
});

gulp.task('clean-bower', function () {
    return $.del(paths.tmp + 'bower_components');
});

D. Gulp inject:

This task injects the JavaScript and CSS files into the index.html file automatically. It relies on three tasks: gulp wiredep, gulp inject-scripts and gulp inject-styles.

D.2. Gulp wiredep:

This task uses streams to add CSS and JS files of the Bower dependencies.

gulp.task('wiredep', function () {
    config.log('Wire up the bower cs & js into index.html');
    return gulp
        .src(getIndexHtml())
        .pipe(wiredep(config.wiredepOptions))
        .pipe(gulp.dest(paths.tmp));
});

Basically, what this task does is to look at our bower.json file for our dependencies and add them in our index.html file where our injection tags are.
For the css it looks like:

<!-- bower:css -->
<!-- endbower-->

And for the JavaScript:

<!-- bower:js -->
<!-- endbower-->

The wiredep’s options are right here.

var bower = {
    json: require('../bower.json'),
    directory: './bower_components',
    ignorePath: '../'
};

exports.wiredepOptions = {
    bowerJson: bower.json,
    directory: bower.directory,
    ignorePath: bower.ignorePath,
    exclude: [/angular\.js/, /angular-animate\.js/, /angular-sanitize\.js/, /angular-ui-router\.js/, /ionic\.js/, /ionic-angular\.js/]
};

We exclude angular.js angular-animate.js… since all those files are bundled into one file: ionic.bundle.js. The gulp-wiredep plugin must be installed for this task to work.

D.1. Gulp inject-scripts:

gulp.task('inject-scripts', ['scripts'], function () {
    config.log('Injecting my app js into index.html');
    var injectScripts = gulp.src([
        paths.src + '/app/**/*.js',
        '!' + paths.src + '/app/**/*.spec.js',
        '!' + paths.src + '/app/**/*.mock.js'
    ]).pipe($.angularFilesort());
    var injectOptions = {
        ignorePath: [paths.src, paths.tmp],
        addRootSlash: false
    };
    return gulp
        .src(getIndexHtml())
        .pipe($.inject(injectScripts, injectOptions))
        .pipe(gulp.dest(paths.tmp));
});

This task injects all our JavaScript files into our custom js placeholder our index.html file. Our js placeholder looks like:

<!-- inject:js -->
<!-- endinject-->

To avoid dependency injection problems, we use the gulp-angular-filesort plugin to inject the scripts in the right order.
The gulp inject-scripts task depends on the gulp scripts task which copies all our JavaScript from our src folder to the .tmp folder.

D.1.1. gulp scripts

gulp.task('scripts', ['lint-scripts', 'clean-scripts'], function () {
    config.log('Copying styles');
    return gulp
        .src(paths.src + '/app/**/*.js')
        .pipe(gulp.dest(path.join(paths.tmp, 'app')));
});

The gulp scripts task depends on the lint-scripts task which use the plugins gulp-jscs, which checks the JavaScript code style with jscs, and gulp-jshint which is a JShint plugin for gulp.

D.1.2. gulp lint-scripts

gulp.task('lint-scripts', function () {
    config.log('Lint my js files');
    return gulp
        .src([paths.src + '/app/**/*.js'])
        .pipe(plumber())
        .pipe(jscs())
        .pipe(jshint())
        .pipe(stylish.combineWithHintResults())
        .pipe(jshint.reporter('jshint-stylish'), {
            verbose: true
        })
        .pipe(jshint.reporter('fail'))
});

D.3. Gulp inject-styles:

gulp.task('inject-styles', ['styles'], function () {
    config.log('Injecting my app js into index.html');
    var injectStyles = gulp.src(paths.tmp + '/app/**/*.css', {read: false});
    var injectOptions = {
        ignorePath: [paths.src, paths.tmp],
        addRootSlash: false
    };
    return gulp
        .src(getIndexHtml())
        .pipe($.inject(injectStyles, injectOptions))
        .pipe(gulp.dest(paths.tmp));
});

This task injects all our css files into our custom css placeholder our index.html file. Our css placeholder looks like:

<!-- inject:css -->
<!-- endinject-->

This task depends on the gulp styles task which compiles all our sass files from our src folder, copies the results to the .tmp folder and add vendor prefixes. It uses the gulp-inject plugin.

D.3.1. gulp styles

gulp.task('styles', ['clean-styles'], function () {
    config.log('Compiling sass to css');
    return gulp
        .src(paths.src + '/app/**/*.scss')
        .pipe(plumber({errorHandler: config.errorHandler('Sass')}))
        .pipe(sass())
        .pipe(autoprefixer({browsers: ['> 5%', 'last 2 versions']}))
        .pipe(gulp.dest(path.join(paths.tmp, 'app')));
});

gulp.task('clean-styles', function () {
    config.log('Cleaning styles');
    return del([path.join(paths.tmp, 'app/**/*.css')])
});

The gulp styles task uses gulp-sass , gulp-plumber and gulp-autoprefixer plugins.

That’s it for the development build pipeline. In a next post I will be talking about the production build phase.

Thanks you for reading and see you soon.


If you enjoyed this article, follow @ahasall on Twitter for more content like this.